Cies Breijs
08/07/2025, 3:42 AM@RequiredPermissions(Perm.SEE_GOD, Perm.HEAR_GOD, Perm.FEEL_LOVE) // or: @RequiresNoPermissions
fun ultimateExperienceGetHandler(req: Request): Resposne { /* ... */ }
The were serialized to the db's user_permissions
table (with columns: userId, permEnum). Kind of worked well.
I liked that all handlers required such annotation (created some test for that in the test suite).
It feels like in http4k the best place to implement this is simply in the handler body. Functional. No annofuzz...
(But please correct me if I'm wrong about this)
fun ultimateExperienceGetHandler(req: Request): Resposne {
if (!currentUserCtx(req).permissions.contains(Perm.SEE_GOD)) return Response(FORBIDDEN)
// ...
}
But there's one thing I miss: the "closed by default" that I had with the annotations.
So I wondered, would it be possible to have some sort of "closed by default" back by somehow specifying them in the router...
Then:
val portalRouter = routes(
Paths.dashboard bind GET to ::dashboardGetHandler,
Paths.profileEdit bind POST to ::profilePostHandler,
Paths.profileEdit bind GET to ::profileGetHandler,
Paths.retailerList bind GET to ::retailerListGetHandler,
Paths.retailerView bind GET to ::retailerViewGetHandler,
// ...
)
Would become something like:
val portalRouter = authorizedRoutes(
Triple(Paths.dashboard, GET to ::dashboardGetHandler, listOf(Perm.SEE_DASHBOARD)),
Triple(Paths.profileEdit, POST to ::profilePostHandler, listOf(Perm.MODIFY_MY_PROFILE)),
Triple(Paths.profileEdit, GET to ::profileGetHandler, listOf(Perm.MODIFY_MY_PROFILE)),
// ...
)
Or even methods on the UserPermissions object like:
val portalRouter = authorizedRoutes(
Triple(Paths.dashboard, GET to ::dashboardGetHandler, listOf(::seeDashboard)),
Triple(Paths.profileEdit, POST to ::profilePostHandler, listOf(::modifyMyProfile)),
Triple(Paths.profileEdit, GET to ::profileGetHandler, listOf(::userHasOrganizationWithTypeX)), // <--
// ...
)
Maybe my question is: how are y'all implementing authorization? (give that i need a very simple scheme for the foreseeable future)MrNiamh
08/07/2025, 8:39 AMsecurity = auth.isUserInTenantWithPermission(UserRolePermissionFeature.DOMAINS.read())
and that function looks like
fun isUserInTenantWithPermission(vararg permissions: PermissionDTO) =
BearerAuthSecurity({ true }).and(object : Security {
override val filter: Filter = Filter filter@{ next ->
{ request ->
withUser(request) { user ->
val tenantId = extractTenantId(request.uri.path) ?: return@withUser Response(Status.UNAUTHORIZED).body("No tenant id found")
isUserInTenantWithPermission(request, next, user, tenantId, permissions)
}
}
}
})
and then to ensure that everything has some security on it, I have this on my routes (which then any time you run your routes, which are most of my tests, that'll be picked up):
routes.forEach {
if (it.meta.security == null) throw IllegalStateException("${it.meta.summary} does not have security on it.")
}
and if you have any end points which don't need any auth, then I have this specifically defined as a security too, which you can then use
val noAuthenticationRequired = object : Security {
override val filter: Filter = Filter filter@{ next ->
{ request -> next(request) }
}
}
Andrew O'Hara
08/07/2025, 2:29 PMResult
classes, so it's easy to return a Forbidden
failure that can be interpreted by the http layer and converted into a 403.Cies Breijs
08/07/2025, 10:17 PMauth.isUserInTenantWithPermission(UserRolePermissionFeature.DOMAINS.read())
) for each endpoint? i wonder then you you define such routes; using the routes(...)
function?
Also in the routes (a list of RoutingHttpHandler objects) I have I find not .meta
property on the RoutingHttpHandler. I dont know where it comes from, sorry 🙂Cies Breijs
08/07/2025, 10:22 PMCies Breijs
08/07/2025, 10:23 PMAndrew O'Hara
08/08/2025, 3:40 AMhttp4k-api-openapi
plugin, basic security is achieved simply by adding a security instance to each route, and then extracting the authorized principal from a lens. However, fine-grained authorization (i.e. does the given principal have the authority to perform an action on a given resource) wasn't the focus of the series, and was actually embedded in the API controller. However, you can easily see how see I do a lightweight layered architecture by wrapping the API layer around the service layer, which then depends on the data layer.
But a layered architecture isn't the only way to authorize requests. All that I'm really suggesting is to start with a basic security filter (or security with http4k-api-openapi) to check if there is an authorized principal. Once you have the principal, you can apply another filter to select routes to check for permissions; but this doesn't scale well beyond the most basic of RBAC. Beyond RBAC, I suggest performing additional authorization (i.e. does the authorized principal have permission to perform a given action on a given resource) in your business logic, which would often be your service layer.MrNiamh
08/08/2025, 7:02 AMfun getCaseStudy(auth: TenantAuthentication, caseStudyService: CaseStudyService): ContractRoute =
"/tenants" / Path.tenantId() / "case-studies" / Path.caseStudyId() meta {
summary = "Get case study"
tags += CASE_STUDIES_TAG
security = auth.isUserInTenantOrLinkedTenant(UserRolePermissionFeature.CASE_STUDIES.read())
returning(Status.OK, getLens<CaseStudyDTO>() to CaseStudyDTO.example)
returningBadRequest()
} bindContract Method.GET to
{ tenantId, _, caseStudyId ->
{ request ->
auth.withUser(request) { user ->
caseStudyService
.getCaseStudy(tenantId, caseStudyId, userId = user.id)
.toResponseWithBody()
}
}
}
my routes are literally a list of routes e.g.
val routes = listOf(
getDomains(auth, domainService),
getCaseStudy(auth, caseStudyService),
)
and so at that point you can check the meta security.
Later on, I pass that list of routes into the actual routes
function i.e.
routes(
swaggerUi(),
contract {
renderer = OpenApi3(ApiInfo("Backend", "v1.0", "API"), CustomJackson(includeNulls = false))
descriptionPath = API_DESCRIPTION_PATH
routes = Appendable(
mutableListOf(
*routes.toTypedArray()
)
)
}
)
Cies Breijs
08/08/2025, 11:27 PM