Translations of this page:

Implementing Unix File System Permissions with Spring Security 3.0

This section discusses implementing a Unix File System Permission Model using Spring Security 3.*. Full source code is provided at the end of the article. Although relatively limited this model could form the basis of permissions for a CMS.

Unix File System Permission Model

The Unix file-system permission model is based around three classes of user: Owner, Group and Others. For each user three different types of access control can be applied: Read, Write and Execute. Represented as a bit map these permissions can be squeezed into a 16 bit integer, this was important at the inception of Unix in the early 1970s when memory and register sizes were limited. The meaning of execute permission changes depending on whether the object is a File or Directory. A Directory is just a special kind of file with a listing of child files and directories.

When a new file is created by a user the default permissions are taken from the user's UMASK, the owner corresponds to the user and the group to the user's principal group. A user can be a member of multiple groups.

For example user: joe is be a member of the accounts group and the subsidiary group marketing. His umask defines a default permission of:

rw-rw-r– 110110100

If we create a file: accounts-2012.odt

It will be readable and writeable by Joe and anyone in the accounts group and also read and write the file. Other users, including those in marketing, can only read the file. If joe has execute permission the directory he can delete the file.

On Unix there is a special user called the “super user” or “root” who can do anything. This can be seen as a single point of failure. Become root and you can do anything. However in the Unix model it is also a good thing. Joe can give his rights away to a file and it is convenient to have a super admin who can deal with such issues.

Obviously in the less space constrained world of the 21st century we could envisage a file having many owners and belonging to many groups. File permissions could also be finer grained, maybe with append and shrink data permissions. However the basic Unix model is relatively easy to understand, is efficient and covers a wide variety of typical use cases.

Spring Implementation

For the Spring implementation we will create a File object that stores permissions, owner and group membership. User's will belong to groups, the first group being the principal for file permissions. No database will be used for storing ACLs.

Files will be arranged in a tree structure. The actual implementation of the File System is not particularly important. It is represented by four classes:

  • File - Meta data and contents
  • FilePermission - a Helper enum for building permission bit masks
  • FileHandle - A reference to a File object and a Map of child FileHandles, this enables the tree structure to be built
  • FileRepository - A reference to a root FileHandle and support for CRUD operations

The repository is not concerned with permissions and access controls. Note: references to File objects in the file system are never given directly. This would enable users to modify file meta data without passing through the FileRepository methods. All file objects are copied before being passed to/from the user layer.

The class FileRepositoryTest is used to Unit test the file operations.

Spring Security Groups

The first thing to understand is that Spring Groups are really just a bit of administrative sugar. Spring groups are not the same as Unix groups. Instead they provide an easy way of assigning a number of roles to a single user in one step (or database entry). They are effectively a mapping table between users (principals) and roles. ACL permissions (READ, WRITE, CREATE etc) can be assigned to a “recipient”, which is typically a “role” or a “principal”. In the Spring Group model a user can be a member of 1 or more groups. A group can have one or more roles. However it is the Spring Roles that actually decided what operations a user can perform on a domain object (file in our case). For the purposes of our example Spring groups seem to be pretty much redundant and we will work directly with users and roles.

We will create a special role: ROLE_ADMIN, for the root or super user Other roles will be simple names like: accounts, marketing etc, these are the equivalent of Unix groups. Voters Whether or not you can perform a particular operation depends on the voters that you configure. I found the explanation of this pretty confusing at first. However for each attribute a voter is called to decided whether the operation is allowed or not.. Lets say we have the following method:

  @Secured({ "ROLE_ADMIN", "ACL_ARTICLE_READ" })
  File get(File a) {
    ...

We are actually going to call two voters (methods), one configured for ROLE_* and the other for ACL_ARTICLE_READ. Our decision manager, which tallies the votes can be configured in a number of ways, for example it can proceed if all the votes are positive, or if just one of the votes is positive, or if the majority of votes are positive. In the above case we want to proceed if we are ADMIN (root) or if we have READ permissions on the file.

Note that default Spring Security voter which deals with roles takes into account only those configuration attributes that match the pattern “ROLE_*”, which is why we call our admin role ROLE_ADMIN and not just ADMIN.

Spring BasePermission

We will use the Spring BasePermission class for file permissions. Only READ, WRITE and CREATE permissions will be used. If you want to know how to extend the basic Spring permissions see the A simple Spring Security 3.0 ACL Tutorial

Configuration Files

  • application.properties
  • applicationContext-business.xml
  • applicationContext-security.xml
  • jdbc.properties
  • security-schema.sql

applicationContext-security.xml

This is the core of the Spring Security setup. First off we configure an authentication manager. This will store usernames, passwords and roles in a JDBC database. Passwords are stored in plaintext.

<authentication-manager alias="authenticationManager">
  <!-- for normal users -->
  <authentication-provider user-service-ref="userManager">
  <password-encoder hash="plaintext" />
  </authentication-provider>
</authentication-manager>
<beans:bean id="userManager" class="org.springframework.security.provisioning.JdbcUserDetailsManager">
  <beans:property name="dataSource" ref="securityDataSource" />
  <beans:property name="enableGroups" value="false" />
  <beans:property name="enableAuthorities" value="true" />
</beans:bean>

All of the JDBC specific information is stored in the external: jdbc.properties file making it easy to swap one database for another. In this implementation we will use an embedded Apache Derby database.

<context:property-placeholder location="classpath:jdbc.properties" />
<beans:bean id="securityDataSource” class="org.springframework.jdbc.datasource.DriverManagerDataSource">
  <beans:property name="driverClassName" value="${userdb.driver}" />
  <beans:property name="url" value="${userdb.url}" />
  <beans:property name="username" value="${userdb.username}" />
  <beans:property name="password" value="${userdb.password}" />
</beans:bean>

Note the INITIALIZE-DATABASE system property. This can be set to either true or false on the command line to initialze the database with the statemetns from security-schema.sql.

<!-- creates security database scheme and adds an administrator -->
<jdbc:initialize-database data-source="securityDataSource"
  ignore-failures="ALL" enabled="#{systemProperties.INITIALIZE_DATABASE}">
  <jdbc:script location="classpath:security-schema.sql" />
</jdbc:initialize-database>

We enable method security and set up a decision manager bean that will decide whether a principal (user) can access a method.

<global-method-security secured-annotations="enabled"
  access-decision-manager ref="fileSystemAccessDecisionManager" />

The decision manager uses two types of voters. One uses the default Spring Security “ROLE_*”, the second uses Access Control Lists. The decision manager is Affirmative based. This means that just one of the voters has to give a Yes vote for the decision to be positive and access allowed. This corresponds to the notion of a “superuser” role and ACL roles. If the user is the superuser they gain access to the resource, no further questions asked. If the user is a normal user than the Access Control Lists will be applied.

<beans:bean id="fileSystemAccessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
  <beans:property name="decisionVoters">
    <beans:list>
      <beans:bean id="roleVoter"				class="org.springframework.security.access.vote.RoleVoter" />
      <beans:ref local="aclArticleWriteVoter" />
      <beans:ref local="aclArticleReadVoter" />
      <beans:ref local="aclArticleCreateVoter" />
    </beans:list>
  </beans:property>
</beans:bean>

We need to configure each ACL voter. The configuration is similar for each so we will just look at the Write voter. This will be applied to any method with the ACL_FILE_WRITE annotation parameter. It uses the BasePermission.WRITE attribute. The aclService is configured. This service will handle permission retrieval from the file-system and configuration of the ACLs. There's one last thing, we need a way of associating and object identity with its ACLs. In Springs default RDBMS implementation a long identifier is used. This is ok(ish) for RDBMS that autogenerate integer sequences and using integers is quite efficient. In our case we have a filename and path. So we write a FileNameRetrievalStrategy class which implements the ObjectIdentityRetrievalStrategy interface. We also specify the domain object class, in our case a String. This matches the path parameter used by the FileSystemServices class where we will put all our security annotated methods. The actual code to check permissions is added to the methods at load time but the magic of byte code modification.

<beans:bean id="aclArticleWriteVoter" class="org.springframework.security.acls.AclEntryVoter">
  <beans:constructor-arg ref="aclService" />
  <beans:constructor-arg value="ACL_FILE_WRITE" />
  <beans:constructor-arg>
    <beans:list>
      <util:constant id="writePermission" static-field="org.springframework.security.acls.domain.BasePermission.WRITE" />
    </beans:list>
  </beans:constructor-arg>
  <beans:property name="objectIdentityRetrievalStrategy">
    <beans:bean class="com.abcseo.infrastructure.FileNameRetrievalStrategy" />
  </beans:property>
  <beans:property name="processDomainObjectClass" value="java.lang.String" />
</beans:bean>

The aclService uses the custom InMemoryAclServiceImpl.

<beans:bean id="aclService" class="com.abcseo.infrastructure.InMemoryAclServiceImpl" />

Creating ACL lists is expensive work. We have to talk to the file-system to retrieve the file's user, groups and permissions then create the ACLs. It is a good idea to configure a cache to store frequently used ACLs for reuse. The only thing to remember is we need to clear the cache entry whenever the file permissions are updated. We'll use the popular ehCache for caching.

<beans:bean id="aclCache" class="org.springframework.security.acls.domain.EhCacheBasedAclCache">
  <beans:constructor-arg>
    <beans:bean class="org.springframework.cache.ehcache.EhCacheFactoryBean">
    <beans:property name="cacheManager">
      <beans:bean class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" />
    </beans:property>
    <beans:property name="cacheName" value="aclCache" />
    </beans:bean>
  </beans:constructor-arg>
</beans:bean>

FileSystemServices

Access to the file-system is controlled via a services layer. We'll look at the create method as this is the most involved. The method can be accessed with any principal that has an ADMIN role or has a CREATE acl on any of its principals (user or group) or “other”. Other is a unix file permission concept and we'll look how it is implemented later. Before the create method is called Spring Security will add the following method call sequence.

  1. FileNameRetrievalStrategy.getObjectIdentity() gets a unique object identifier. In our case this is a simple path string but it could be far more complex.
  2. InMemoryAclServiceImp.readAclById(). This is passed an object identity returned by getObjectIdenity above. In this case a file name and its type (java.lang.String) and a List of sids. The principal (user name) and Granted Authorities (groups in our case).
  3. InMemoryAclService.Impl.readAclsById. This loops through the list of Object Identities (a single file in our case) and builds an Object/ACL map.
  4. SimpleAclImpl.isGranted() Checks to see if we have the needed permissions.
  5. Only at this point will the create() actually be called.

The create method takes a path and fileName. We separate these out because it is the parent domain object where we need to have create permissions in order to add the new file to its list of children.

@Secured({ "ROLE_ADMIN", "ACL_FILE_CREATE" })
public File create(String path, String fileName) throws FileNotFoundException {
  Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 
  User user = (User) authentication.getPrincipal();
  UserDetails ud =  userManager.loadUserByUsername(user.getUsername());
 
  Collection<GrantedAuthority> authorities = ud.getAuthorities();
  String group = null;
  for (GrantedAuthority authority : authorities) {
    group = authority.getAuthority();
    if (!group.startsWith("ROLE_")) {
      break;
    }
  }
 
  return repository.create(path + fileName, user.getUsername(), group, umask);
}

The method recovers the username and main (first) group from the system. As with Unix every user should have at least one group. These are then passed to the file-system create method along with the default umask (permissions). Note that the repository and userManager are autowired which saves on configuration.

AclService

Lets have a closer look at the readAclsById method which is part of the InMemoryAclServiceImpl. It is this method's job to assign Access Control Lists to a specific object returning them as a Map of ObjectIdenties and Acls. The method loops through all the supplied object identities. It sees if the object identifier is already in the cache, if so it just returns the cached acls.

public Map<ObjectIdentity, Acl> readAclsById(List<ObjectIdentity> objects, List<Sid> sids) throws NotFoundException {
  ...
  for (ObjectIdentity object : objects) {
  MutableAcl acl = cache.getFromCache(object);
  if (acl != null) {
    logger.info("Using acl value in cache " + acl);
    result.put(object, acl);
  } else {

Otherwise it recovers the original object identifier path string and gets the corresponding file from the repository. Our file has user, group and permissions meta attributes which will be used to build the ACL.

    File file;
    file = repository.get((String) object.getIdentifier());
    long perms = file.getPerms();
    ObjectIdentity fileId = new ObjectIdentityImpl(String.class, object.getIdentifier());

We create a new CumulativePermission object. We check the owner perms bitmask and add each of the set permissions into the CumulativePermissions object

    CumulativePermission ownerPermission = new CumulativePermission();
    if (FilePermission.OWNER_WRITE.isSet(perms))  
      ownerPermission.set(BasePermission.WRITE);
    if (FilePermission.OWNER_READ.isSet(perms))
      ownerPermission.set(BasePermission.READ);
    if (FilePermission.OWNER_CREATE.isSet(perms))
      ownerPermission.set(BasePermission.CREATE);

Finally we create the acl object and add the culmulative permissions to this along with the principal sid, in this case the file owner.

    acl = new SimpleAclImpl(fileId, new ArrayList<AccessControlEntry>());
    List<AccessControlEntry> entries = acl.getEntries();
    entries.add(new AccessControlEntryImpl("aceOwner", acl,
    new PrincipalSid(file.getOwner()), ownerPermission, true, true, true));
…

We repeat this for the group and other permission. In both cases alled entries.add to add the new sid and culmulative permissions for the sid. We then cache the acl and return the Object/Acl map to the callee.

  cache.putInCache(acl);
  result.put(object, acl);
  return result;

SimpleAclImpl

Finally our voting class for all the ACL_ protected methods. IsGranted returns true or false depending on whether permission to access the domain object is allowed.

public boolean isGranted(List<Permission> permissions, List<Sid> sids, boolean administrativeMode) throws NotFoundException {

The method loops over a list of permissions, these correspond to the permissions declared in the @Secured annotation on the method all. In out example we only have one ACL_ specified, either READ, WRITE or CREATE.

We then loop over the sids belonging to the current authenticated user (username and groups). Finally we check the Acls from the AclService, this is the set of culminative permissions for the object. For example if the permission mask was rw-r–, we would see the following permissions in the loop.

CumulativePermission[…………………………..=0] CumulativePermission[…………………………W.=2] CumulativePermission[…………………………WR=3]

We are looking for a Acl which matches the permission for a given sid. Note that there is no sid for our unix permision“other” so we hard code this in the test below (aceOther).

  for (Permission permission : permissions) {
    for (Sid sid : sids) {
      for (AccessControlEntry ace : aces) {
        if (((ace.getPermission().getMask() & permission.getMask()) != 0)) {
          if (ace.getSid().equals(sid) || ace.getId().equals("aceOther")) {
            if (ace.isGranting()) {
              return true;
            }
 ...

and bingo, that's it. Run the code through the eclipse debugger to see how it works in detail.

Conclusion

Hopefully this article will have inspired you to roll Spring Security into your own application and demystified how it works. There seem to be quite a lot of method calls for what are some fairly simple permission requirements but that is the price to pay for Springs flexible, enterprise level security.

Source Code

springunixacl.zip

mvn -DargLine=”-DINITIALIZE_DATABASE=false” test

tech/java/unix-filesystems-permissions-with-spring-security.txt · Last modified: 2011/05/03 10:35 by davidof
Recent changes RSS feed