A lightweight and flexible ACL component
This is a module to implement a simple ACL system to any of your applications. By using the play plugin you can protect controller actions, or anything else.
You can protect an admin area, or hide information / actions for specific users. It's also possible to connect a right to specific conditions for objects.
Let's say you have a shop system and want to display orders. The orders are separated in 2 lists (open orders and closed orders). For this case you can use the acl system to filter entries between these 2 lists.
You can easily use the sbt tool to download the resources to your project.
libraryDependencies ++= Seq(
"de.ceow" %% "scala-acl" % "1.2.0",
"de.ceow" %% "play-acl" % "1.2.0" // for play 2.5
"de.ceow" %% "play-acl" % "1.3.0" // for play 2.6
)
The Acl system is based on classic roles. A user can have one or more roles. Each role have resources and privileges. A resource is a entity or an are / module to separate rights. A privilege is the action the user want to do, like read, update, delete, expand, manage, send, ... .
A User have roles, and will be initialized with the ACL system. An Anonymous user is also a representation of a user, but with none or different roles.
Roles are objects, with a unique identifier which. The identifier is a bit value like 1, 2, 4, 8, 16 ...
A resource have to extend de.ceow.security.acl.Resource
and contain a string identifier.
object AdminResource extends de.ceow.security.acl.Resource("admin")
A privilege have to extend de.ceow.security.acl.Privilege
and contain a string identifier.
object ReadPrivilege extends de.ceow.security.acl.Privilege("read")
A Role have to implement de.ceow.security.acl.Role
interface, which contains 4 abstract methods:
getIdentifier: Long
: the role identifier bit valuegetRoleId: String
: returns a string identifier of this rolegetInheritedRole: List[Role]
: a list of parent rolesgetPrivileges: Map[Resource, Map[Privilege, Seq[Assert]]]
: the complete definition of rights
Assertions are classes with an apply method. The parameters areOption[AclObject]
and Acl
.
You can check specific conditions on a resource. When you have a list of users, and want to show an edit button
to directly edit this user u
can use this. An admin will have no restriction, an anonymous user
is denied for this operation, but a user can edit his own entry.
object myRole extends Role {
override def getPrivileges: Map[Resource, Map[Privilege, Seq[Assert]]] = Map(
Resource -> Map(
Privilege -> Seq(Asserts.isMe)
)
}
}
val myId = 4
case class User(id: Int) extends AclObject
object Asserts {
object IsMe extends Assert {
// the assert definition
def apply(obj: Option[AclObject], acl: Acl) => obj match {
case Some(u: User) => u.exists(_.id == acl.observerEntity.id)
case _ => false
}
}
}
acl.isAllowed(UserResource, EditPrivilege, Some(currentUser)) // returns true or false
For Initialization of the Acl you need to configure the Security Trait. This Trait will be used in a controller action to initialize and retrieve the ACL instance for the current user. When there is no user logged in, there is a guest user defined. To implement the Security trait you need to define these methods:
userByUsername(username: String)(implicit acl: Acl): Option[I]
to retrieve a user from some storage by given usernameroles: List[Role]
the list of available rolesguestRole: Role
the guest roleguestUser: UserEntity
to return a guest / anonymous user
To configure your acl system you have to define "Resources", "Privileges", "Roles" and the "Security" trait. I'll show a dummy implementation:
import de.ceow.security.acl.{Resource, Privilege, Role, Identity}
import de.ceow.security.acl.play.Security
/** Resources */
object MainResource extends Resource("main")
object UserResource extends Resource("user")
object AdminResource extends Resource("admin")
/** Privileges */
object ReadPrivilege extends Privilege("read")
object CreatePrivilege extends Privilege("create")
object LoggedInPrivilege extends Privilege("loggedIn")
object ManagePrivilege extends Privilege("manage")
/** User Roles */
object Guest extends Role {
override def getIdentifier: Long = 1L
override def getPrivileges: Map[Resource, Map[Privilege, Seq[Assert]]] = {
Map(
MainResource -> Map(
ReadPrivilege -> Seq()
),
UserResource -> Map(
ReadPrivilege -> Seq(),
CreatePrivilege -> Seq()
)
)
}
override def getInheritedRoles: List[Role] = List()
override def getRoleId: String = "guest"
}
object Registered extends Role {
override def getIdentifier: Long = 2L
override def getPrivileges: Map[Resource, Map[Privilege, Seq[Assert]]] = {
Map(
UserResource -> Map(
LoggedInPrivilege -> Seq()
)
)
}
override def getInheritedRoles: List[Role] = List(Guest)
override def getRoleId: String = "registered"
}
object Admin extends Role {
override def getIdentifier: Long = 4L
override def getPrivileges: Map[Resource, Map[Privilege, Seq[Assert]]] = {
Map(
AdminResource -> Map(
ReadPrivilege -> Seq(),
ManagePrivilege -> Seq()
)
)
}
override def getInheritedRoles: List[Role] = List(Registered)
override def getRoleId: String = "admin"
}
case class UserEntity(id: Int, roles: Long) extends Identity
trait Security extends de.ceow.security.acl.play.AclSecurity[UserEntity] {
def userByUsername(username: String)(implicit request: RequestHeader): Option[UserEntity] = {
UserRepository.findByUserName(username) match {
case Success(user) => Some(user)
case Failure(ex) => None
}
}
override def roles: List[Role] = Guest :: Registered :: Admin:: Nil
override def guestRole: Role = Guest
override def guestUser: UserEntity = new UserEntity(0, 1L)
override def onUnauthenticated(request: RequestHeader) = Results.Redirect("/login")
override def onUnauthorized(request: RequestHeader) = Results.Redirect("/login")
}
trait AsyncSecurity extends de.ceow.security.acl.play.AsyncSecurity[UserEntity] with Security {}
Because of being stateless, the controller is the entry point for the ACL system and it will be initialized there. Also when you want to retrieve the logged in user.
The Standard implementation contains 4 Methods, which are checking if a user is logged in and return the Acl or the user instance or even check a resource and privilege directly.
Normal Security Trait
withAuth
: check if user is logged in, otherwiseonUnauthenticated
will be calledwithUser
: will provide the logged in or the guest user instancewithAcl
: will provide the acl instancewithProtected(r: Resource, p: Privilege)
:- if acl check against resource privilege fails ->
onUnauthorized
will be called
- if acl check against resource privilege fails ->
withProtected(r: Resource, p: Privilege, objectToCheck: () => Option[AclObject])
:- if acl check against resource privilege on objectToCheck fails ->
onUnauthorized
will be called
- if acl check against resource privilege on objectToCheck fails ->
withProtected(r: Resource, p: Privilege, objectToCheck: Acl => Option[AclObject])
:- same as above except the Acl as an input parameter
withProtectedAcl(r: Resource, p: Privilege)
:- if acl check against resource privilege fails ->
onUnauthorized
will be called - if logged in and acl check is true -> return Acl Instance
- if acl check against resource privilege fails ->
withProtectedAcl(r: Resource, p: Privilege, objectToCheck: () => Option[AclObject])
:- if acl check against resource privilege on objectToCheck fails ->
onUnauthorized
will be called - if logged in and acl check is true -> return Acl Instance
- if acl check against resource privilege on objectToCheck fails ->
withProtectedAcl(r: Resource, p: Privilege, objectToCheck: Acl => Option[AclObject])
:- same as above except the Acl as an input parameter
Async Security Trait
withAuthAsync
: check if user is logged in, otherwiseonUnauthenticated
will be calledwithUserAsync
: will provide the logged in or the guest user instancewithAclAsync
: will provide the acl instancewithProtectedAsync(r: Resource, p: Privilege)
:- if acl check against resource privilege fails ->
onUnauthorized
will be called
- if acl check against resource privilege fails ->
withProtectedAsync(r: Resource, p: Privilege, objectToCheck: () => Option[AclObject])
:- if acl check against resource privilege on objectToCheck fails ->
onUnauthorized
will be called
- if acl check against resource privilege on objectToCheck fails ->
withProtectedAsync(r: Resource, p: Privilege, objectToCheck: Acl => Option[AclObject])
:- same as above except the Acl as an input parameter
withProtectedAclAsync(r: Resource, p: Privilege)
:- if acl check against resource privilege fails ->
onUnauthorized
will be called - if logged in and acl check is true -> return Acl Instance
- if acl check against resource privilege fails ->
withProtectedAclAsync(r: Resource, p: Privilege, objectToCheck: () => Option[AclObject])
:- if acl check against resource privilege on objectToCheck fails ->
onUnauthorized
will be called - if logged in and acl check is true -> return Acl Instance
- if acl check against resource privilege on objectToCheck fails ->
withProtectedAclAsync(r: Resource, p: Privilege, objectToCheck: Acl => Option[AclObject])
:- same as above except the Acl as an input parameter
To protect a controller action or retrieve Acl or current user, see the following example:
package controllers
import <your security trait>
import <your resources and privileges>
import <your user entity>
import play.api.mvc.InjectedController
import de.ceow.security.acl.Acl
/** Admin controller - we want to protected this */
class Admin @Inject()(val messagesApi: MessagesApi, override val configuration: Configuration) extends InjectedController with Security {
def dashboard = withAuth { implicit request =>
Ok("")
}
def dashboard = withUser { implicit user: UserEntity => implicit request =>
Ok("")
}
def dashboard = withAcl { implicit acl: Acl => implicit request =>
Ok("")
}
def dashboard = withProtected(AdminResource, ReadPrivilege) { implicit request =>
Ok("")
}
def dashboard = withProtected(AdminResource, ReadPrivilege, DataSource.getData) { user => implicit request =>
Ok("")
}
def dashboard = withProtectedAcl(AdminResource, ReadPrivilege) { implicit acl: Acl => implicit request =>
Ok("")
}
def dashboard = withProtectedAcl(AdminResource, ReadPrivilege, DataSource.getData) { user => implicit acl: Acl => implicit request =>
Ok("")
}
}
A Requirement for doing this is an instance of acl: Acl
in the current context. The acl object
will be passed as implicit
parameter.
For checking the rights you have a lot of options to ask:
acl.isAllowed(Resource, Privilege, Some(AclObject))
use AllowLike
trait
To use this AllowLike
trait, you need an AclObject.
case class User extends AclObject With AllowLike
acl allows Resource to Privilege
acl allows Privilege at Resource
acl allows AclObject ad Resource to Privilege
AclObject allows Resource to Privilege
AclObject allows Privilege at Resource
Resource allows Privilege
Resource allows AclObject to Privilege
Privilege allows Resource
Privilege allows AclObject at Resource
In a view it's necessary to provide implicit acl instance.
without request
@import de.ceow.security.acl.Acl
@()(implicit acl: Acl)
@if(acl allows Resource to Privilege) {
<h1>Hello World</h1>
}
with request
@import de.ceow.security.acl.Acl
@()(implicit request: RequestHeader, acl: Acl)
@if(acl allows Resource to Privilege) {
<h1>Hello World</h1>
}
scala-acl
- add Scala version (2.11.11, 2.12.1, 2.12.2)
- simplify pattern matching
play-acl
- removed
new
for case class creation - allow to provide acl object as an input for
objectToCheck
besidewithProtectedAcl(r: Resource, p: Privilege, objectToCheck: () => Option[AclObject])
alsowithProtectedAcl(r: Resource, p: Privilege, objectToCheck: Acl => Option[AclObject])
- Change Assert from function to abstract class
- separate acl and play-acl to different modules
- implement Privilege.deny rule
- change inheritance to use role inheritance only
- use Acl.Assert as type definition for assertions
- update play framework to 2.5.x
- implement async trait to use acl also in async actions
- implementing unit tests for Security Trait
- reuse code blocks in security trait
- pass checked object to controller action
- implement travis ci
- update scala version to 2.11.8
- update play framework to 2.5.0
- passing the acl instance to the assertion
- implement 2 new methods to check also in play controller against an AclObject