Last active
June 7, 2024 06:47
-
-
Save ghostdogpr/f8dc3c33a3d2d85f6f8dfa1fb23ce1ab to your computer and use it in GitHub Desktop.
Role-based access control with Caliban
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//> 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