A role-based access control library for Scala
To import this project add the following line to build.sbt
in your scala project:
libraryDependencies += "io.github.johnhungerford.rbac" %% "scala-rbac-[module]" % "1.0.0"
where module
is the name of the module you want to import as defined in this
project's build.sbt
file. The current library modules are:
-
core
: the base permissions library, includingPermission
,Permissible
,Role
,Resource
, andUser
-
http
: providesSecureController
trait for integrating scala-rbac with REST controllers. -
scalatra
: providesSecureScalatraServlet
trait for Scalatra projects -
play
: providesSecureAbstractController
class for Play projects.
The fundamental components of scala-rbac
are Permission
and Permissible
.
Permissible
defines something that may or may not be permitted. For
the most part, a Permissible
can be thought about as an operation, and
for this reason, the trait Operation
which is a simple subtype of
Permissible
can be used in its place. Permission
grants access any
number of Permissibles
.
The simplest way to create a Permission
that permits an Operation
is
as follows:
case object DoAThing extends Operation
val perm : Permission = Permission.to(DoAThing)
You can then validate operations by checking them with the .permits
method:
case object DoSomethingElse extends Operation
perm.permits(DoAThing) // true
perm.permits(DoSomethingElse) // false
Alternatively, you can define your own Permission class by extending
the SimplePermission
class and overriding the .permits
method:
object MyPerm extends SimplePermission {
override def permits( permissible : Permissible ) : Boolean = {
permissible match {
case DoAthing => true
case _ => false
}
}
}
MyPerm.permits(DoAThing) // true
MyPerm.permits(DoSomethingElse) // false
The most useful way to employ these classes is the Permissible.secure
method, which is used as so:
def hello(name : String = "World")( implicit ps : PermissionSource ) : Unit = DoAThing.secure {
println( s"Hello $name!" )
}
The hello
method can only be used in the context of an implicit
instance of one of the three scala-rbac
types that provides permissions:
Permission
, Role
, or User
. If that permission source does not grant
permission to DoAThing
, hello
will throw an UnpermittedOperationException
:
implicit var perm = MyPerm
// outputs: Hello Erik!
hello("Erik")
// NoPermissions is a built-in Permission object that
// permits nothing
perm = NoPermissions
// Throws UnpermittedOperationException:
hello("David")
// Permissions can also be passed to a secured method explicitly.
// The line below uses AllPermissions, another built-in Permission
// that permits everything. So despite the inadequate implicit permission,
// it will output "Hello Larry!"
hello("Larry")( AllPermissions )
You can secure against multiple permissibles by composing them with &
or |
:
def hello(name: String = "World")(implicit ps : PermissionSource) : Unit = {
(DoAThing | DoSomethingSomethingElse).secure {
println(s"Hello $name")
}
}
implicit var perm = Permission.to(DoAThing)
// Works
hello("Matthew")
perm = Permission.to(DoSomethingElse)
// Also works
hello("Mark")
Either permission will permit DoAThing | DoSomethingElse
, but
DoAthing & DoSomethingElse
will only be permitted by a permission
that allows both operations:
def strictHello(name: String = "World")(implicit ps: PermissionSource): Unit = {
(DoAThing & DoSomethingSomethingElse).secure {
println(s"Hello $name")
}
}
val perm1 = Permission.to(DoAThing)
val perm2 = Permission.to(DoSomethingElse)
implicit var implPerm = perm1
// Fails
strictHello("Matthew")
implPerm = perm2
// Also fails
strictHello("Mark")
implPerm = perm1 | perm2
// Works!
strictHello("Luke")
Note that we can combine permissions using the |
operator as well, which
generates a new permission permitting whatever each of the two permissions allowed.
scala-rbac-core
also includes Role
, which is defined by one or more permissions,
and User
, which is defined by one or more Role
. You can mix the
SecureController
trait into any type of REST controller class to make available methods
for authenticating and authorizing your routes:
// `RequestType` is the type of the request belonging to `SomeFrameWorkControllerClass`
class MySecureController extends SomeFrameWorkControllerClass with SecureController[ RequestType, MyUserType ] {
// Define a method to authenticate user and retrieve. Note: `MyUserType` must be a
// subtype of scala-rbac `User`
override def authenticateUser( request : RequestType ) : MyUserType = ...
// This will look different depending on the REST framework...
def handleGet( request : RequestType ) : FrameworkRouteAction = {
// This will authenticate the request and allow you to handle request with
// along with the authenticated `User` instance
Authenticate( request : RequestType ).withUser { implicit user =>
...
}
}
def handlePost( request : RequestType ) : FrameworkRouteAction = {
// This will authenticate the request and secure this route against the operation
// `DoAThing`
Secure( request, DoAThing ).withUser { implicit user =>
...
}
}
}
There are also base controller classes for play and scalatra applications that simplify the usage for their respective frameworks:
class MyServiceClass {
// Service method is secured using Permissible
def doAThing(name: String)(implicit ps: PermissionSource) : String = DoAThing.secure {
s"Hello $name"
}
}
// SCALATRA base controller
class MyScalatraServlet( service: MyServiceClass ) extends SecureScalatraServlet[ MyUserType ] {
// Which header contains your Authorization information?
override val authHeader: String = "Authorization"
// Supply some method to validate header and retrieve user:
override def authenticateUser(authHeader: String): MyUserType = ...
// Define a route:
get( "/hello/:name" ) (AuthenticateRoute.withUser {
implicit user: MyUserType =>
val res = service.doAThing(params("name"))
Ok( res )
})
}
// PLAY base controller
class MySecurePlayController @Inject()(service: MyServiceClass, cc: ControllerComponents) extends SecurePlayController[ MyUserType ](cc) {
// Which header contains your Authorization information?
override val authHeader : String = "Authorization"
// Supply some method to validate header and retrieve user:
override def authenticateUser(authHeader: String): MyUserType = ...
// Define a route (note that your handler should accept two parameters: the request
// and the authenticated user:
def getRoot(): Action[ AnyContent ] = AuthenticateAction.withUser {
(req : Request[ AnyContent ], user: MyUserType) =>
implicit val ps = user
val res = service.doAThing(params("name"))
Ok( res )
}
}
The SecuredController method Authenticate.withUser
reads the request object,
parsing out the header we have specified by authHeader
. It then calls our
authenticateUser
method, which either retrieves a User
if authentication
was successful or throws an exception. If a user is successfully
retrieved, withUser
then passes it as an argument to the function
we provide as a parameter. By adding implicit
to the function parameter
user : User =>
, the user
object becomes the permission source that will
enable us to call secured service methods.
In addition to authenticating against a User
, you can also override authenticateRole
or authenticatePermission
and call Authenticate.withRole {}
or
Authenticate.withPermission {}
to use roles and permissions respectively. Finally,
you can use authenticate
and Authenticate {}
simply to validate the header
without retrieving any further permissions information.
It is also possible to secure against operations directly using the
Secure
object:
class MySecureServlet( service : MyServiceClass ) extends SecureScalatraServlet[ MyUserType ] {
// Which header contains your Authorization information?
override val authHeader : String = "Authorization"
// Supply some method to validate header and retrieve user:
override def authenticateUser(authHeader: String): MyUserType = ...
// Define a route:
get( "/hello/:name" ) ( SecureRoute(DoAThing).withUser { _ =>
s"Hello ${params("name")}"
} )
}
This accomplishes the same as the previous servlet: it authenticates against
user credentials, and then only executes the router logic if the credentials
permit DoAThing
. Note that it also passes the authenticated User
object to
our handler. Since we don't need it in the above case (because the authorization
has already happened) we just use _ =>
to ignore it.