Skip to content

Instantly share code, notes, and snippets.

@PedroGarci4
Forked from ghostdogpr/rbac.sc
Created June 7, 2024 06:30
Show Gist options
  • Save PedroGarci4/c724d41dd2bd41892fdc70403c03ba6a to your computer and use it in GitHub Desktop.
Save PedroGarci4/c724d41dd2bd41892fdc70403c03ba6a to your computer and use it in GitHub Desktop.
Role-based access control with Caliban
//> using dep com.github.ghostdogpr::caliban-quick:2.7.1
import caliban.*
import caliban.CalibanError.*
import caliban.Value.StringValue
import caliban.execution.FieldInfo
import caliban.parsing.adt.Directive
import caliban.quick.*
import caliban.schema.Annotations.*
import caliban.schema.Schema
import caliban.wrappers.Wrapper.FieldWrapper
import scala.util.Try
import zio.*
import zio.http.*
import zio.query.ZQuery
enum Role {
case Admin, User
}
val directiveName = "hasRole"
val attributeName = "role"
class HasRoleDirective(role: Role)
extends GQLDirective(Directive(directiveName, Map(attributeName -> StringValue(role.toString))))
case class admin() extends HasRoleDirective(Role.Admin)
case class user() extends HasRoleDirective(Role.User)
case class AuthContext(roles: Set[Role])
def getRequiredRoles(info: FieldInfo): Set[Role] =
info.directives
.filter(_.name == directiveName)
.flatMap(_.arguments.get(attributeName))
.flatMap {
case StringValue(role) => Try(Role.valueOf(role)).toOption.toList
case _ => Nil
}
.toSet
val accessControl: FieldWrapper[AuthContext] =
new FieldWrapper[AuthContext](wrapPureValues = true) {
def wrap[R1 <: AuthContext](
query: ZQuery[R1, ExecutionError, ResponseValue],
info: FieldInfo
): ZQuery[R1, ExecutionError, ResponseValue] =
ZQuery.serviceWithQuery[AuthContext] { ctx =>
val missingRoles = getRequiredRoles(info).diff(ctx.roles)
if (missingRoles.isEmpty) query
else ZQuery.fail(ExecutionError(s"Missing required roles: ${missingRoles.mkString(", ")}."))
}
}
case class Query(
@admin adminData: String,
@user userData: String
) derives Schema.SemiAuto
val api = graphQL(RootResolver(Query("admin", "user"))) @@ accessControl
val middleware =
Middleware.customAuthProviding[AuthContext] { req =>
req.headers
.get("Roles")
.map(_.split(",").flatMap(s => Try(Role.valueOf(s)).toOption).toSet)
.map(roles => AuthContext(roles))
}
Unsafe.unsafely {
Runtime.default.unsafe.run {
api
.routes("api/graphql")
.map(_ @@ middleware)
.flatMap(_.serve.provideLayer(Server.defaultWithPort(8080)))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment