2 Usage - Reference Documentation
Authors: Burt Beckwith
Version: 2.0.1
Table of Contents
2 Usage
2.1 Securing Service Methods
There are two primary use cases for ACL security: determining whether a user is allowed to perform an action on an instance before the action is invoked, and restricting access to single or multiple instances after methods are invoked (this is typically implemented by collection filtering). You can callaclUtilService.hasPermission() explicitly, but this tends to clutter your code with security logic that often has little to do with business logic. Instead, Spring Security provides some convenient annotations that are used to wrap your method calls in access checks.There are four annotations:
The annotations use security-specific Spring expression language (SpEL) expressions - see the documentation for the available standard and method expressions.Here's an example service that manages a Report domain class and uses these annotations and expressions:import org.springframework.security.access.prepost.PostFilter import org.springframework.security.access.prepost.PreAuthorize import org.springframework.transaction.annotation.Transactionalimport com.yourapp.Reportclass ReportService { @PreAuthorize("hasPermission(#id, 'com.yourapp.Report', read) or " + "hasPermission(#id, 'com.yourapp.Report', admin)") Report getReport(long id) { Report.get(id) } @Transactional @PreAuthorize("hasRole('ROLE_USER')") Report createReport(params) { Report report = new Report(params) report.save() report } @PreAuthorize("hasRole('ROLE_USER')") @PostFilter("hasPermission(filterObject, read) or " + "hasPermission(filterObject, admin)") List getAllReports(params = [:]) { Report.list(params) } @Secured(['ROLE_USER', 'ROLE_ADMIN']) String getReportName(long id) { Report.get(id).name } @Transactional @PreAuthorize("hasPermission(#report, write) or " + "hasPermission(#report, admin)") Report updateReport(Report report, params) { report.properties = params report.save() report } @Transactional @PreAuthorize("hasPermission(#report, delete) or " + "hasPermission(#report, admin)") void deleteReport(Report report) { report.delete() } }
getReportrequires that the authenticated user haveBasePermission.READorBasePermission.ADMINfor the instancecreateReportrequiresROLE_USERgetAllReportsrequiresROLE_USERand will have elements removed from the returnedListthat the user doesn't have an ACL grant for; the user must haveBasePermission.READorBasePermission.ADMINfor each element in the list; elements that don't have access granted will be removedgetReportNamerequires that the authenticated user have eitherROLE_USERorROLE_ADMIN(but no ACL rules)updateReporthas no role restrictions but must satisfy the requirements of theaclReportWriteVotervoter (which has theACL_REPORT_WRITEconfig attribute), i.e.BasePermission.ADMINISTRATIONorBasePermission.WRITEdeleteReporthas no role restrictions but must satisfy the requirements of theaclReportDeleteVotervoter (which has theACL_REPORT_DELETEconfig attribute), i.e.BasePermission.ADMINISTRATIONorBasePermission.DELETE
2.2 Working with ACLs
Suggested application changes
To properly display access denied exceptions (e.g. when a user tries to perform an action but doesn't have a grant authorizing it), you should create a mapping ingrails-app/conf/UrlMappings.groovy for error code 403. In addition, it's possible to trigger a NotFoundException which will create an error 500, but should be treated like a 403 error, so you should add mappings for these conditions:import org.springframework.security.access.AccessDeniedException import org.springframework.security.acls.model.NotFoundExceptionclass UrlMappings { static mappings = { "/$controller/$action?/$id?"{ constraints {} } "/"(view:"/index") "403"(controller: "errors", action: "error403") "500"(controller: "errors", action: "error500") "500"(controller: "errors", action: "error403", exception: AccessDeniedException) "500"(controller: "errors", action: "error403", exception: NotFoundException) } }
ErrorsController:package com.yourcompany.yourappimport grails.plugin.springsecurity.annotation.Secured@Secured(['permitAll']) class ErrorsController { def error403() {} def error500() { render view: '/error' } }
grails-app/views/errors/error403.gsp similar to this:<html> <head> <title>Access denied!</title> <meta name='layout' content='main' /> </head><body> <h1>Access Denied</h1> <p>We're sorry, but you are not authorized to perform the requested operation.</p> </body> </html>
actionSubmit
Grails has a convenient feature where it supports multiple submit actions per form via the<g:actionSubmit> tag. This is done by posting to the index action but with a special parameter that indicates which action to invoke. This is a problem in general for security since any URL rules for edit, delete, save, etc. will be bypassed. It's an even more significant issue with ACLs because of the way that the access denied exception interacts with the actionSubmit processing. If you don't make any adjustments for this, your users will see a blank page when they attempt to submit a form and the action is disallowed. The solution is to remove actionSubmit buttons and replace them with regular submit buttons. This requires one form per button, and without adjusting the CSS the buttons will look differently than if they were in-line actionSubmit buttons, but that is fixable with the appropriate CSS changes.It's simple to adjust the actionSubmit buttons and you'll need to change them in show.gsp and edit.gsp; list.gsp and show.gsp don't need any changes. In show.gsp, replace the two actionSubmit buttons with these two forms (maintain the g:message tags; the strings are hard-coded here to reduce clutter):<div class="buttons"> <g:form action='edit'> <g:hiddenField name="id" value="${reportInstance?.id}" /> <span class="button"> <g:submitButton class="edit" name="Edit" /> </span> </g:form> <g:form action='delete'> <g:hiddenField name="id" value="${reportInstance?.id}" /> <span class="button"> <g:submitButton class="delete" name="Delete" onclick="return confirm('Are you sure?');" /> </span> </g:form> </div>
edit.gsp, change the <form> tag to<g:form action='update'>
<div class="buttons"> <span class="button"> <g:submitButton class="save" name="Update" /> </span> </div>
<g:form action='delete'> <g:hiddenField name="id" value="${reportInstance?.id}" /> <div class="buttons"> <span class="button"> <g:submitButton class="delete" name="Delete" onclick="return confirm('Are you sure?');" /> </span> </div> </g:form>
2.3 Domain Classes
The plugin uses domain classes to manage database state. Ordinarily the database structure isn't all that important, but to be compatible with the traditional JDBC-based Spring Security code, the domain classes are configured to generate the table and column names that are used there.The plugin classes related to persistence use these classes, so they're included in the plugin but can be overridden by running the s2-create-acl-domains script.As you can see, the database structure is highly normalized.AclClass
TheAclClass domain class contains entries for the names of each application domain class that has associated permissions:class AclClass { String className @Override
String toString() {
"AclClass id $id, className $className"
} static mapping = {
className column: 'class'
version false
} static constraints = {
className unique: true
}
}AclSid
TheAclSid domain class contains entries for the names of grant recipients (a principal or authority - SID is an acronym for "security identity"). These are typically usernames (where principal is true) but can also be a GrantedAuthority (role name, where principal is false). When granting permissions to a role, any user with that role receives that permission:class AclSid { String sid
boolean principal @Override
String toString() {
"AclSid id $id, sid $sid, principal $principal"
} static mapping = {
version false
} static constraints = {
principal unique: 'sid'
}
}AclObjectIdentity
TheAclObjectIdentity domain class contains entries representing individual domain class instances (OIDs). It has a field for the instance id (objectId) and domain class (aclClass) that uniquely identify the instance. In addition there are optional nullable fields for the parent OID (parent) and owner (owner). There's also a flag (entriesInheriting) to indicate whether ACL entries can inherit from a parent ACL.class AclObjectIdentity extends AbstractAclObjectIdentity { Long objectId @Override String toString() { "AclObjectIdentity id $id, aclClass $aclClass.className, " + "objectId $objectId, entriesInheriting $entriesInheriting" } static mapping = { version false aclClass column: 'object_id_class' owner column: 'owner_sid' parent column: 'parent_object' objectId column: 'object_id_identity' } static constraints = { objectId unique: 'aclClass' } }
AclObjectIdentity actually extends a base class, AbstractAclObjectIdentity:abstract class AbstractAclObjectIdentity { AclClass aclClass AclObjectIdentity parent AclSid owner boolean entriesInheriting static constraints = { parent nullable: true owner nullable: true } }
Long objectId field, but if you want to support other types of ids you can change that field and retain the other standard functionality from the base class.AclEntry
Finally, theAclEntry domain class contains entries representing grants (or denials) of a permission on an object instance to a recipient. The aclObjectIdentity field references the domain class instance (since an instance can have many granted permissions). The sid field references the recipient. The granting field determines whether the entry grants the permission (true) or denies it (false). The aceOrder field specifies the position of the entry, which is important because the entries are evaluated in order and the first matching entry determines whether access is allowed. auditSuccess and auditFailure determine whether to log success and/or failure events (these both default to false).The mask field holds the permission. This can be a source of confusion because the name (and the Spring Security documentation) indicates that it's a bit mask. A value of 1 indicates permission A, a value of 2 indicates permission B, a value of 4 indicates permission C, a value of 8 indicates permission D, etc. So you would think that a value of 5 would indicate a grant of both permission A and C. Unfortunately this is not the case. There is a CumulativePermission class that supports this, but the standard classes don't support it (AclImpl.isGranted() checks for == rather than using | (bitwise or) so a combined entry would never match). So rather than grouping all permissions for one recipient on one instances into a bit mask, you must create individual records for each. This will be addressed in Spring Security 3.1 however.class AclEntry { AclObjectIdentity aclObjectIdentity
int aceOrder
AclSid sid
int mask
boolean granting
boolean auditSuccess
boolean auditFailure @Override
String toString() {
"AclEntry id $id, aceOrder $aceOrder, mask $mask, " +
"granting $granting, aclObjectIdentity $aclObjectIdentity"
} static mapping = {
version false
sid column: 'sid'
aclObjectIdentity column: 'acl_object_identity'
} static constraints = {
aceOrder unique: 'aclObjectIdentity'
}
}2.4 Configuration
Creating, editing, or deleting permissions requires an authenticated user. In most cases if the authenticated user is the owner of the ACL then access is allowed, but granted roles also affect whether access is allowed. The default required role isROLE_ADMIN for all actions, but this can be configured in grails-app/conf/Config.groovy. This table summarizes the attribute names and the corresponding actions that are allowed for it:| Attribute | Affected methods |
|---|---|
grails.plugin.springsecurity. acl.authority. modifyAuditingDetails | AuditableAcl.updateAuditing() |
grails.plugin.springsecurity. acl.authority.changeOwnership | OwnershipAcl.setOwner() |
grails.plugin.springsecurity. acl.authority. changeAclDetails | MutableAcl.deleteAce(), MutableAcl.insertAce(), MutableAcl.setEntriesInheriting(), MutableAcl.setParent(), MutableAcl.updateAce() |
ROLE_ADMIN or change them to have separate values, e.g.grails.plugin.springsecurity.acl.authority.
modifyAuditingDetails = 'ROLE_ACL_MODIFY_AUDITING'grails.plugin.springsecurity.acl.authority.
changeOwnership = 'ROLE_ACL_CHANGE_OWNERSHIP'grails.plugin.springsecurity.acl.authority.
changeAclDetails = 'ROLE_ACL_CHANGE_DETAILS'Run-As Authentication Replacement
There are also two options to configure Run-As Authentication Replacement:| Attribute | Meaning |
|---|---|
grails.plugin.springsecurity. useRunAs | change to true to enable; defaults to false |
grails.plugin.springsecurity. runAs.key | a shared key between the two standard implementation classes, used to verify that a third party hasn't created a token for the user; should be changed from its default value |
grails.plugin.springsecurity.useRunAs = true
grails.plugin.springsecurity.runAs.key = 'your run-as key'2.5 Run-As Authentication Replacement
Although not strictly related to ACLs, the plugin implements Run-As Authentication Replacement since it's related to method security in general. This feature is similar to the Switch User feature of the Spring Security Core plugin, but instead of running as another user until you choose to revert to your originalAuthentication, the temporary authentication switch only lasts for one method invocation.For example, in this service someMethod() requires that the authenticated user have ROLE_ADMIN and will also be granted ROLE_RUN_AS_SUPERUSER for the duration of the method only:class SecureService { @Secured(['ROLE_ADMIN', 'RUN_AS_SUPERUSER'])
def someMethod() {
…
}
}2.6 Custom Permissions
By default there are 5 permissions available from theorg.springframework.security.acls.domain. BasePermission class: READ, WRITE, CREATE, DELETE, and ADMINISTRATION. You can also add your own permissions if these aren't sufficient.The easiest approach is to create a subclass of BasePermission and add your new permissions there. This way you retain the default permissions and can use them if you need. For example, here's a subclass that adds a new APPROVE permission:package com.mycompany.myapp;import org.springframework.security.acls.domain.BasePermission; import org.springframework.security.acls.model.Permission;public class MyPermission extends BasePermission { public static final Permission APPROVE = new MyPermission(1 << 5, 'V'); protected MyPermission(int mask) { super(mask); } protected MyPermission(int mask, char code) { super(mask, code); } }
grails.plugin.springsecurity.acl. permissionClass attribute either as a Class or a String, for exampleimport com.mycompany.myapp.MyPermissions
…
grails.plugin.springsecurity.acl. permissionClass = MyPermissionsgrails.plugin.springsecurity.acl.permissionClass = 'com.mycompany.myapp.MyPermissions'
aclPermissionFactory bean in grails-app/conf/spring/resources.groovy, keeping the org.springframework.security.acls.domain. DefaultPermissionFactory class but passing your class as the constructor argument to keep it from defaulting to BasePermission, or do a more complex override to more fully reconfigure the behavior:import org.springframework.security.acls.domain.DefaultPermissionFactory import com.mycompany.myapp.MyPermissionbeans = { aclPermissionFactory(DefaultPermissionFactory, MyPermission) }
@PreAuthorize("hasPermission(#id, 'com.testacl.Report', 'approve')") Report get(long id) { Report.get id }