cancel
Showing results for 
Search instead for 
Did you mean: 

People group security

idwright
Star Collaborator
Star Collaborator
I'm trying to implement group based security for people as listed at http://wiki.alfresco.com/wiki/Security_and_Authentication#To_Do

Actually in more detail I want to restrict people search etc so that you can only see people who are members of the same site (or members of a named group can see anybody)

The obvious(!) approach seems to be to add to the PersonService_security bean in public-services-security-context.xml

e.g.
org.alfresco.service.cmr.security.PersonService.getPerson=ACL_ALLOW,AFTER_ACL_NODE.sys:base.ReadProperties,AFTER_ACL_SHARED_SITE.GROUP_peopleFinders
org.alfresco.service.cmr.security.PersonService.getPeople=ACL_ALLOW,AFTER_ACL_NODE.sys:base.ReadProperties,AFTER_ACL_SHARED_SITE.GROUP_peopleFinders

however there are some flaws with this

In org.alfresco.repo.jscript.People#List<PersonInfo> getPeopleImpl if getPeopleImplSearch is used then
1) it's not using PersonService.getPeople
2) if it's using FTS and afterwards PersonService.getPerson throws a AccessDeniedException(NoSuchPersonException?) (or returns null) then it will cause an error (which in the case of an exception will fall through thereby giving the desired result but not in a good way)
This, I think, would be a relatively simple change although I'm not sure whether to catch an exception (and which exception would be better to use) or use the getPersonOrNull method and ignore the null
e.g.

                // FTS
                List<NodeRef> personRefs = getPeopleImplSearch(filter, pagingRequest, sortBy, sortAsc);
               
                if (personRefs != null)
                {
                    persons = new ArrayList<PersonInfo>(personRefs.size());
                    for (NodeRef personRef : personRefs)
                    {
                   try
                   {
                       persons.add(personService.getPerson(personRef));
                   } catch (AccessDeniedException ade) {
                       //Ignored
                   }
                    }
                }

Firstly any thoughts on this?

Secondly what should the behaviour of the personService methods be?- getPeople/getAllPeople is relatively straightforward - just remove results with no access from the result set but should getPersonOrNull,personExists throw an exception (which one?) or return null/false?, what should getPerson do (given that it may be expected to create a non-existent person)? etc

I'm sure there are other implications which I haven't considered yet
1) Search - I don't think people are included so should be OK but there may be things I haven't thought of
2) Shared Files - if you have access to see a file but don't have access to the connected people what should happen?
3) More things I haven't thought of/come across yet….
5 REPLIES 5

jpotts
World-Class Innovator
World-Class Innovator
I implemented this for a client and did it by modifying the people finder dialog and the web scripts it calls to fetch the people to restrict the results to members of that site. Of course this can be circumvented by people who make calls to the out-of-the-box web scripts that the original finder dialog leverages. So if that is a problem for you then my approach won't be a good fit. But if it does work for you I now have the permission to share that code. I believe it is probably for 3.4 but I could stick it in GitHub so that others could bring it up to speed for 4.x if it helps.

Jeff

idwright
Star Collaborator
Star Collaborator
I saw your previous post on this and it might be useful to see what you've done.

I think my implementation is almost good enough for my purposes but needs more testing (and definitely isn't perfect)

For reference the other key place I've found to make similar changes is the usernamePropertiesDecorator

Thanks,
Ian

jpotts
World-Class Innovator
World-Class Innovator
Okay, let me find it, clean it up, and post it on GitHub. I hope to do that by the end of next week, if possible.

Jeff

kart
Champ in-the-making
Champ in-the-making
Hi Ian, Jeff,

I'm new to Alfresco. I just installed the community version 4.2.e. I read this post and few others to restrict finding people and I'm not yet able to achieve what I want.

Quick overview of my goal.
I would like to create private sites to share documents with customers. I would like to create a specific private site per customer such that one customer can't see the other customers the company has. 

To achieve my goal I thought about the following scheme:
<ul><li>Only member of a special group can search and find all existing users in the Alfresco database. (Let say we call this group CAN_FIND_ALL_USERS )
<li>users that are member of a site and that don't belong to the group CAN_FIND_ALL_USERS can find only other users that are also member of this site. Note: It will also be acceptable to find users that are not member of this site but are member of other sites that the current user (doing the search) is also member. </ul>

So far, I found these ways to find people:
<ul><li>localhost/share/page/people-finder
<li>Select a document, click on Manage Permissions, click on Add User/Group
<li>Select a document, click on Start Workflow and select for example New Task</ul>

Ian, I tried to implement the changes in your original post, (in public-services-security-context.xml) but it doesn't work. Any user can find still find all user. Would you please provide additional guidance on how I could achieve this as it seems my goal is very similar to yours.

Thank you so much,
Stephane

idwright
Star Collaborator
Star Collaborator
Hi Stephane,

There's certainly not enough detail in the original post to implement this.
I've done a high level blog about this at http://tech.wrighting.org/2013/12/17/alfresco-as-extranet/ but again not enough detail to actually implement.

I'll have a go at describing what you need to do for implementation - you'll see from the blog post that there are a few wrinkles but this might be sufficient for you.

You need to write some java code that will understand the AFTER_ACL_SHARED_SITE(_NULL)? directives and make sure that it's invoked as an afterInvocation provider - this is done in the public-services-security-context.xml something like this:

where the custom java is defined in the siteAcl bean and using the group peopleFinders as CAN_FIND_ALL_USERS

<blockcode>
   <bean id="siteAcl"
      class="org.wrighting.repo.security.permissions.impl.acegi.CustomACLEntryAfterInvocationProvider"
      abstract="false" singleton="true" lazy-init="default" autowire="default"
      dependency-check="default">
      <property name="authorityService"><ref bean="authorityService"/></property>
      <property name="personService"><ref bean="personService"/></property>
   </bean>

   <bean id="afterInvocationManager"
      class="net.sf.acegisecurity.afterinvocation.AfterInvocationProviderManager">
      <property name="providers">
         <list>
            <ref bean="afterAcl" />
            <ref bean="afterAclMarking" />
            <ref local="siteAcl" />
         </list>
      </property>
   </bean>


     <bean id="PersonService_security" class="org.alfresco.repo.security.permissions.impl.acegi.MethodSecurityInterceptor">
        <property name="authenticationManager"><ref bean="authenticationManager"/></property>
        <property name="accessDecisionManager"><ref bean="accessDecisionManager"/></property>
        <property name="afterInvocationManager"><ref local="afterInvocationManager"/></property>
        <property name="objectDefinitionSource">
            <value>
                org.alfresco.service.cmr.security.PersonService.getPerson=ACL_ALLOW,AFTER_ACL_NODE.sys:base.ReadProperties,AFTER_ACL_SHARED_SITE.GROUP_peopleFinders
                org.alfresco.service.cmr.security.PersonService.getPersonOrNull=ACL_ALLOW,AFTER_ACL_NODE.sys:base.ReadProperties,AFTER_ACL_SHARED_SITE_NULL.GROUP_peopleFinders
                org.alfresco.service.cmr.security.PersonService.personExists=ACL_ALLOW,AFTER_ACL_SHARED_SITE.GROUP_peopleFinders
                org.alfresco.service.cmr.security.PersonService.isEnabled=ACL_ALLOW
                org.alfresco.service.cmr.security.PersonService.createMissingPeople=ACL_ALLOW
                org.alfresco.service.cmr.security.PersonService.setCreateMissingPeople=ACL_METHOD.ROLE_ADMINISTRATOR
                org.alfresco.service.cmr.security.PersonService.getMutableProperties=ACL_ALLOW
                org.alfresco.service.cmr.security.PersonService.setPersonProperties=ACL_METHOD.ROLE_ADMINISTRATOR
                org.alfresco.service.cmr.security.PersonService.isMutable=ACL_ALLOW
                org.alfresco.service.cmr.security.PersonService.createPerson=ACL_METHOD.ROLE_ADMINISTRATOR
                org.alfresco.service.cmr.security.PersonService.deletePerson=ACL_METHOD.ROLE_ADMINISTRATOR
                org.alfresco.service.cmr.security.PersonService.notifyPerson=ACL_METHOD.ROLE_ADMINISTRATOR
                org.alfresco.service.cmr.security.PersonService.getAllPeople=ACL_ALLOW,AFTER_ACL_SHARED_SITE.GROUP_peopleFinders
                org.alfresco.service.cmr.security.PersonService.getPeople=ACL_ALLOW,AFTER_ACL_NODE.sys:base.ReadProperties,AFTER_ACL_SHARED_SITE.GROUP_peopleFinders
                org.alfresco.service.cmr.security.PersonService.getPeopleFilteredByProperty=ACL_ALLOW,AFTER_ACL_SHARED_SITE.sys:base.ReadProperties,AFTER_ACL_SHARED_SITE.GROUP_peopleFinders
                org.alfresco.service.cmr.security.PersonService.getPeopleContainer=ACL_ALLOW
                org.alfresco.service.cmr.security.PersonService.getUserNamesAreCaseSensitive=ACL_ALLOW
                org.alfresco.service.cmr.security.PersonService.getUserIdentifier=ACL_ALLOW
                org.alfresco.service.cmr.security.PersonService.countPeople=ACL_ALLOW
                org.alfresco.service.cmr.security.PersonService.*=ACL_DENY
         </value>
        </property>
    </bean>
</blockcode>


The following java comes with plenty of caveats as it's something of a work in progress, as indeed does all this :-), but you're welcome to try it - although please do report back.

I'm not entirely sure about the use of PagingResults and what happens if there's more than one page but other than that I think it's reasonably understandable.

<blockcode>
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;

import net.sf.acegisecurity.AccessDeniedException;
import net.sf.acegisecurity.Authentication;
import net.sf.acegisecurity.ConfigAttribute;
import net.sf.acegisecurity.ConfigAttributeDefinition;
import net.sf.acegisecurity.afterinvocation.AfterInvocationProvider;
import net.sf.acegisecurity.providers.dao.User;

import org.alfresco.query.ListBackedPagingResults;
import org.alfresco.query.PagingResults;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.permissions.impl.acegi.ACLEntryVoterException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.cmr.security.PersonService.PersonInfo;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;

public class CustomACLEntryAfterInvocationProvider implements
      AfterInvocationProvider, InitializingBean {

   private static Log log = LogFactory
         .getLog(CustomACLEntryAfterInvocationProvider.class);

   private static final String AFTER_ACL_SHARED_SITE = "AFTER_ACL_SHARED_SITE";

   private static final String AFTER_ACL_SHARED_SITE_NULL = "AFTER_ACL_SHARED_SITE_NULL";

   private AuthorityService authorityService;

   private PersonService personService;

   @Override
   @SuppressWarnings("rawtypes")
   public Object decide(Authentication authentication, Object object,
         ConfigAttributeDefinition config, Object returnedObject)
         throws AccessDeniedException {
      if (log.isDebugEnabled() && object instanceof MethodInvocation) {
         MethodInvocation mi = (MethodInvocation) object;
         log.debug("Method: " + mi.getMethod().toString());
      }
      try {
         if (AuthenticationUtil.isRunAsUserTheSystemUser()) {
            if (log.isDebugEnabled()) {
               log.debug("Allowing system user access");
            }
            return returnedObject;
         } else if (returnedObject == null) {
            if (log.isDebugEnabled()) {
               log.debug("Allowing null object access");
            }
            return null;
         } else {
            
            if (PagingResults.class.isAssignableFrom(returnedObject
                  .getClass())) {

               if (log.isDebugEnabled()) {
                  log.debug("Controlled object 1 - access being checked for "
                        + returnedObject.getClass().getName());

                  log.debug("Received n = "
                        + ((PagingResults) returnedObject).getPage()
                              .size());
               }
               PagingResults retObject = decide(authentication, object,
                     config, (PagingResults) returnedObject);
               if (log.isDebugEnabled()) {
                  log.debug("Returned n=" + retObject.getPage().size());
               }
               return (retObject);

            } else if (PersonInfo.class.isAssignableFrom(returnedObject
                  .getClass())) {
               if (log.isDebugEnabled()) {
                  log.debug("Controlled object 2 - access being checked for "
                        + returnedObject.getClass().getName());
               }
               return decide(authentication, object, config,
                     (PersonInfo) returnedObject);
            } else if (NodeRef.class.isAssignableFrom(returnedObject
                  .getClass())) {
               if (log.isDebugEnabled()) {
                  log.debug("Controlled object 3 - access being checked for "
                        + returnedObject.getClass().getName());
               }
               return decide(authentication, object, config,
                     (NodeRef) returnedObject);

            } else {
               if (log.isDebugEnabled()) {
                  log.debug("Uncontrolled object - access allowed for "
                        + returnedObject.getClass().getName());
               }
               return returnedObject;
            }
         }
      } catch (AccessDeniedException ade) {
         if (log.isDebugEnabled()) {
            log.debug("Access denied", ade);
            ade.printStackTrace();
         }
         throw ade;
      } catch (RuntimeException re) {
         if (log.isDebugEnabled()) {
            log.debug("Access denied by runtime exception", re);
            re.printStackTrace();
         }
         throw re;
      }

   }

   private String getSiteNameFromGroup(String auth) {
      if (auth.startsWith("GROUP_site_")) {
         String groupName = auth.substring("GROUP_site_".length());

         int pos = groupName.indexOf('_');
         if (pos > 0) {
            return groupName.substring(0, pos);
         } else {
            return groupName;
         }
      }
      return null;

   }

   private NodeRef decide(final Authentication authentication,
         final Object object, final ConfigAttributeDefinition config,
         final NodeRef returnedObject) throws AccessDeniedException

   {
      // Repeating logic but ensures not calling personService.getPerson on a
      // non-person node
      List<ConfigAttributeDefintion> supportedDefinitions = extractSupportedDefinitions(config);
      if (log.isDebugEnabled()) {
         log.debug("Entries are " + supportedDefinitions);
      }

      if (supportedDefinitions.size() == 0) {
         if (log.isDebugEnabled()) {
            log.debug("No supported entries");
         }
         return returnedObject;
      }
      /*
       * Safe but possibly OTT QName personQName =
       * QName.createQName("cmSmiley Tongueerson", nspr); QName nodeType =
       * nodeService.getType(returnedObject);
       *
       * if (!(personQName.isMatch(nodeType))) {
       * log.debug("Returning non-person node"); return (returnedObject); }
       */
      PersonInfo pi = personService.getPerson(returnedObject);

      PersonInfo ret = decide(authentication, object, config, pi);
      // Exception will be rethrown if that's what we want
      if (ret == null) {
         return null;
      } else {
         return returnedObject;
      }
   }

   private PersonInfo decide(Authentication authentication, Object object,
         ConfigAttributeDefinition config, PersonInfo returnedObject)
         throws AccessDeniedException {

      List<ConfigAttributeDefintion> supportedDefinitions = extractSupportedDefinitions(config);
      if (log.isDebugEnabled()) {
         log.debug("Entries are " + supportedDefinitions);
      }

      if (supportedDefinitions.size() == 0) {
         if (log.isDebugEnabled()) {
            log.debug("No supported entries");
         }
         return returnedObject;
      }

      String currentUser = ((User) authentication.getPrincipal())
            .getUsername();
      Set<String> userAuthorities = this.getGroups(currentUser);

      if (this.ignoreFilter(supportedDefinitions, userAuthorities)
            || currentUser.equals(returnedObject.getUserName())) {
         return returnedObject;
      }

      ArrayList<String> userSites = this.getUserSites(userAuthorities);

      String personUserName = returnedObject.getUserName();
      Set<String> personAuthorities = authorityService
            .getAuthoritiesForUser(personUserName);

      if (log.isDebugEnabled()) {
         log.debug("Authorities for:" + personUserName);
      }
      boolean found = false;
      for (String auth : personAuthorities) {
         log.debug(auth);
         String site = getSiteNameFromGroup(auth);
         if (userSites.contains(site)) {
            found = true;
            if (log.isDebugEnabled()) {
               log.debug("Found matching site:" + site + " for "
                     + personUserName);
            }
            break;
         }
      }

      if (found) {
         return returnedObject;
      } else {
         boolean returnNull = false;
         for (ConfigAttributeDefintion def : supportedDefinitions) {
            if (def.typeString.equals(AFTER_ACL_SHARED_SITE_NULL)) {
               returnNull = true;
            }
         }
         if (returnNull) {
            return null;
         } else {
            throw new AccessDeniedException("Access denied for person");
         }
      }

   }

   @SuppressWarnings({ "rawtypes", "unchecked" })
   private PagingResults decide(final Authentication authentication,
         final Object object, final ConfigAttributeDefinition config,
         final PagingResults returnedObject) throws AccessDeniedException

   {
      List<ConfigAttributeDefintion> supportedDefinitions = extractSupportedDefinitions(config);
      if (log.isDebugEnabled()) {
         log.debug("Entries are " + supportedDefinitions);
      }

      if (supportedDefinitions.size() == 0) {
         if (log.isDebugEnabled()) {
            log.debug("No supported entries");
         }
         return returnedObject;
      }

      String currentUser = ((User) authentication.getPrincipal())
            .getUsername();
      Set<String> userAuthorities = this.getGroups(currentUser);

      if (this.ignoreFilter(supportedDefinitions, userAuthorities)) {
         return returnedObject;
      }

      ArrayList<String> userSites = this.getUserSites(userAuthorities);

      List<PersonInfo> page = returnedObject.getPage();
      List<PersonInfo> results = page;
      List<PersonInfo> newList = new ArrayList<PersonInfo>();

      for (PersonInfo person : results) {
         String personUserName = person.getUserName();
         Set<String> personAuthorities = authorityService
               .getAuthoritiesForUser(personUserName);

         if (log.isDebugEnabled()) {
            log.debug("Authorities for:" + personUserName);
         }
         boolean found = false;
         for (String auth : personAuthorities) {
            if (log.isDebugEnabled()) {
               log.debug(auth);
            }
            String site = getSiteNameFromGroup(auth);
            if (userSites.contains(site)
                  || currentUser.equals(personUserName)) {
               found = true;
               if (log.isDebugEnabled()) {
                  log.debug("Found matching site:" + site + " for "
                        + personUserName);
               }
               break;
            }
         }
         if (found) {
            newList.add(person);
         }
      }
      return new ListBackedPagingResults(newList);
   }

   private Set<String> getGroups(final String userName) {

      Set<String> userAuthorities = authorityService
            .getAuthoritiesForUser(userName);

      return (userAuthorities);
   }

   private ArrayList<String> getUserSites(final Set<String> userAuthorities) {

      ArrayList<String> sites = new ArrayList<String>();
      for (String auth : userAuthorities) {
         if (log.isDebugEnabled()) {
            log.debug(auth);
         }
         String site = getSiteNameFromGroup(auth);
         if (site != null) {
            sites.add(site);
         }
      }
      return sites;
   }

   private boolean ignoreFilter(
         final List<ConfigAttributeDefintion> supportedDefinitions,
         Set<String> userAuthorities) {

      for (String auth : userAuthorities) {
         if (auth.equals("GROUP_ALFRESCO_ADMINISTRATORS")) {
            if (log.isDebugEnabled()) {
               log.debug("Administrator allowed");
            }
            return true;
         }
         for (ConfigAttributeDefintion cfg : supportedDefinitions) {
            if (cfg.groupName.equals(auth)) {
               if (log.isDebugEnabled()) {
                  log.debug("Allowed member of:" + cfg.groupName);
               }
               return true;
            }
         }
      }
      return (false);
   }

   @SuppressWarnings("rawtypes")
   private List<ConfigAttributeDefintion> extractSupportedDefinitions(
         final ConfigAttributeDefinition config) {
      List<ConfigAttributeDefintion> definitions = new ArrayList<ConfigAttributeDefintion>();
      Iterator iter = config.getConfigAttributes();
      if (log.isDebugEnabled()) {
         log.debug("Config:" + config);
      }
      while (iter.hasNext()) {
         ConfigAttribute attr = (ConfigAttribute) iter.next();
         if (log.isDebugEnabled()) {
            log.debug("Config attr:" + attr);
         }
         if (this.supports(attr)) {
            definitions.add(new ConfigAttributeDefintion(attr));
         }

      }
      return definitions;
   }

   private class ConfigAttributeDefintion {

      String typeString;

      String groupName;

      ConfigAttributeDefintion(final ConfigAttribute attr) {

         StringTokenizer st = new StringTokenizer(attr.getAttribute(), ".",
               false);
         if (st.countTokens() != 2) {
            throw new ACLEntryVoterException(
                  "There must be two . separated tokens in each config attribute");
         }
         typeString = st.nextToken();
         groupName = st.nextToken();

         if (!(typeString.equals(AFTER_ACL_SHARED_SITE) || typeString
               .equals(AFTER_ACL_SHARED_SITE_NULL))) {
            throw new ACLEntryVoterException("Invalid type: must be "
                  + AFTER_ACL_SHARED_SITE + " or "
                  + AFTER_ACL_SHARED_SITE_NULL);
         }

         if (!(groupName.startsWith("GROUP_"))) {
            throw new ACLEntryVoterException(
                  "group name must start with GROUP_ " + groupName);
         }
      }
   }

   public void setAuthorityService(AuthorityService authorityService) {
      this.authorityService = authorityService;
   }

   public void afterPropertiesSet() throws Exception {
      if (authorityService == null) {
         throw new IllegalArgumentException(
               "There must be a authority service");
      }
   }

   public boolean supports(ConfigAttribute attribute) {
      if ((attribute.getAttribute() != null)
            && (attribute.getAttribute().startsWith(AFTER_ACL_SHARED_SITE))) {
         return true;
      } else {
         return false;
      }
   }

   @SuppressWarnings("rawtypes")
   public boolean supports(Class clazz) {
      return (MethodInvocation.class.isAssignableFrom(clazz));
   }

   public void setPersonService(PersonService personService) {
      this.personService = personService;
   }

}
</blockcode>