The previous framework that I used had some author...
# http4k
c
The previous framework that I used had some authorization by annotation feature. The permissions were represented with a list of an enum, and passed with a vararg to the annotation:
Copy code
@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)
Copy code
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:
Copy code
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:
Copy code
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:
Copy code
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)
m
Not sure if this is the best way, but works for me. I use the security on the end point, and have my own security filter defined e.g. on a GET
Copy code
security = auth.isUserInTenantWithPermission(UserRolePermissionFeature.DOMAINS.read())
and that function looks like
Copy code
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):
Copy code
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
Copy code
val noAuthenticationRequired = object : Security {
        override val filter: Filter = Filter filter@{ next ->
            { request -> next(request) }
        }
    }
a
There's authorization filters that apply to groups of routes. Normally, I apply a global filter to extract the principal from the request (returning 401 if not found), and then the route handler passes the principal to the service layer: to perform the businss-level authorization. All my service-layer methods return
Result
classes, so it's easy to return a
Forbidden
failure that can be interpreted by the http layer and converted into a 403.
c
@MrNiamh That looks really nice, and is close to what I'm used to right now, but then implemented without the pesky annotations. I just dont get what you mean with "my own security filter defined e.g. on a GET". do you mean that you put one GET endpoint to have its own filter? and thus you need one filter (greated by
auth.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 🙂
@Andrew O'Hara Is there an example app somewhere that uses the layers you describe? It seems that by http layer you mean handlers, "controller action methods" is OO frameworks. So basically you propose to handle it in the body of the handler (request -> response) functions. Which is what I expected for http4k, but I do think it's a bit of a step back to having to use open by default, after having used close by default (security best practice).
Thank you both for your answers gentlemen. Two interesting approaches!
a
A layered architecture isn't exclusive to heavyweight web frameworks; it's just a way to encourage seperation of concerns. I do have a

youtube seriesâ–¾

with a companion repo using the layered architecture I proposed. When using http4k contracts from the
http4k-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.
m
@Cies Breijs A more wide example of one of my end points
Copy code
fun 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.
Copy code
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.
Copy code
routes(
    swaggerUi(),
    contract {
        renderer = OpenApi3(ApiInfo("Backend", "v1.0", "API"), CustomJackson(includeNulls = false))
        descriptionPath = API_DESCRIPTION_PATH
        routes = Appendable(
            mutableListOf(
                *routes.toTypedArray()
            )
        )
    }
)
c
@MrNiamh wow.