org.sakaiproject.nakamura.lite.accesscontrol.AccessControlManagerImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.sakaiproject.nakamura.lite.accesscontrol.AccessControlManagerImpl.java

Source

/*
 * Licensed to the Sakai Foundation (SF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The SF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package org.sakaiproject.nakamura.lite.accesscontrol;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import org.apache.commons.codec.binary.Base64;
import org.sakaiproject.nakamura.api.lite.CacheHolder;
import org.sakaiproject.nakamura.api.lite.Configuration;
import org.sakaiproject.nakamura.api.lite.StorageClientException;
import org.sakaiproject.nakamura.api.lite.StorageClientUtils;
import org.sakaiproject.nakamura.api.lite.StoreListener;
import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessControlManager;
import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessDeniedException;
import org.sakaiproject.nakamura.api.lite.accesscontrol.AclModification;
import org.sakaiproject.nakamura.api.lite.accesscontrol.Permission;
import org.sakaiproject.nakamura.api.lite.accesscontrol.Permissions;
import org.sakaiproject.nakamura.api.lite.accesscontrol.PrincipalTokenResolver;
import org.sakaiproject.nakamura.api.lite.accesscontrol.PrincipalValidatorResolver;
import org.sakaiproject.nakamura.api.lite.accesscontrol.Security;
import org.sakaiproject.nakamura.api.lite.authorizable.Authorizable;
import org.sakaiproject.nakamura.api.lite.authorizable.AuthorizableManager;
import org.sakaiproject.nakamura.api.lite.authorizable.Group;
import org.sakaiproject.nakamura.api.lite.authorizable.User;
import org.sakaiproject.nakamura.api.lite.content.Content;
import org.sakaiproject.nakamura.lite.CachingManager;
import org.sakaiproject.nakamura.lite.storage.StorageClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class AccessControlManagerImpl extends CachingManager implements AccessControlManager {

    private static final String _SECRET_KEY = "_secretKey";
    private static final String _PATH = "_aclPath";
    private static final String _OBJECT_TYPE = "_aclType";
    private static final String _KEY = "_aclKey";
    private static final Logger LOGGER = LoggerFactory.getLogger(AccessControlManagerImpl.class);
    private static final Set<String> PROTECTED_PROPERTIES = ImmutableSet.of(_SECRET_KEY);
    private static final Set<String> READ_ONLY_PROPERTIES = ImmutableSet.of(_SECRET_KEY, _PATH, _OBJECT_TYPE, _KEY);
    private User user;
    private String keySpace;
    private String aclColumnFamily;
    private Map<String, int[]> cache = new ConcurrentHashMap<String, int[]>();
    private boolean closed;
    private StoreListener storeListener;
    private PrincipalTokenValidator principalTokenValidator;
    private PrincipalTokenResolver principalTokenResolver;
    private SecureRandom secureRandom;
    private AuthorizableManager authorizableManager;
    private Map<String, String[]> principalCache = new ConcurrentHashMap<String, String[]>();
    private ThreadLocal<String> principalRecursionLock = new ThreadLocal<String>();

    public AccessControlManagerImpl(StorageClient client, User currentUser, Configuration config,
            Map<String, CacheHolder> sharedCache, StoreListener storeListener,
            PrincipalValidatorResolver principalValidatorResolver) throws StorageClientException {
        super(client, sharedCache);
        this.user = currentUser;
        this.aclColumnFamily = config.getAclColumnFamily();
        this.keySpace = config.getKeySpace();
        closed = false;
        this.storeListener = storeListener;
        principalTokenValidator = new PrincipalTokenValidator(principalValidatorResolver);
        secureRandom = new SecureRandom();
    }

    public Map<String, Object> getAcl(String objectType, String objectPath)
            throws StorageClientException, AccessDeniedException {
        checkOpen();
        check(objectType, objectPath, Permissions.CAN_READ_ACL);

        String key = this.getAclKey(objectType, objectPath);
        return StorageClientUtils.getFilterMap(getCached(keySpace, aclColumnFamily, key), null, null,
                PROTECTED_PROPERTIES, false);
    }

    public Map<String, Object> getEffectiveAcl(String objectType, String objectPath)
            throws StorageClientException, AccessDeniedException {
        throw new UnsupportedOperationException("Nag someone to implement this");
    }

    // to sign a token we need setAcl permissions on the delegate path
    public void signContentToken(Content token, String objectPath)
            throws StorageClientException, AccessDeniedException {
        checkOpen();
        check(Security.ZONE_CONTENT, objectPath, Permissions.CAN_WRITE_ACL);
        check(Security.ZONE_CONTENT, objectPath, Permissions.CAN_READ_ACL);
        String key = this.getAclKey(Security.ZONE_CONTENT, objectPath);
        Map<String, Object> currentAcl = getCached(keySpace, aclColumnFamily, key);
        String secretKey = (String) currentAcl.get(_SECRET_KEY);
        principalTokenValidator.signToken(token, secretKey);
        // the caller must save the target.
    }

    public void setAcl(String objectType, String objectPath, AclModification[] aclModifications)
            throws StorageClientException, AccessDeniedException {
        checkOpen();
        check(objectType, objectPath, Permissions.CAN_WRITE_ACL);
        check(objectType, objectPath, Permissions.CAN_READ_ACL);
        String key = this.getAclKey(objectType, objectPath);
        Map<String, Object> currentAcl = getCached(keySpace, aclColumnFamily, key);
        // every ACL gets a secret key, which avoids doing it later with a special call
        Map<String, Object> modifications = Maps.newLinkedHashMap();
        if (!currentAcl.containsKey(_SECRET_KEY)) {
            byte[] secretKeySeed = new byte[20];
            secureRandom.nextBytes(secretKeySeed);
            MessageDigest md;
            try {
                md = MessageDigest.getInstance("SHA1");
                modifications.put(_SECRET_KEY, Base64.encodeBase64URLSafeString(md.digest(secretKeySeed)));
            } catch (NoSuchAlgorithmException e) {
                LOGGER.error(e.getMessage(), e);
            }
        }
        if (!currentAcl.containsKey(_KEY)) {
            modifications.put(_KEY, key);
            modifications.put(_OBJECT_TYPE, objectType); // this is here to make data migration possible in the future 
            modifications.put(_PATH, objectPath); // same
        }
        for (AclModification m : aclModifications) {
            String name = m.getAceKey();
            if (READ_ONLY_PROPERTIES.contains(name)) {
                continue;
            }
            if (m.isRemove()) {
                modifications.put(name, null);
            } else {

                int originalbitmap = getBitMap(name, modifications, currentAcl);
                int modifiedbitmap = m.modify(originalbitmap);
                LOGGER.debug("Adding Modification {} {} ", name, modifiedbitmap);
                modifications.put(name, modifiedbitmap);

                // KERN-1515
                // We need to modify the opposite key to apply the
                // reverse of the change we just made. Otherwise,
                // you can end up with ACLs with contradictions, like:
                // anonymous@g=1, anonymous@d=1
                if (containsKey(inverseKeyOf(name), modifications, currentAcl)) {
                    // XOR gives us a mask of only the bits that changed
                    int difference = originalbitmap ^ modifiedbitmap;
                    int otherbitmap = toInt(getBitMap(inverseKeyOf(name), modifications, currentAcl));

                    // Zero out the bits that have been modified
                    //
                    // KERN-1887: This was originally toggling the modified bits
                    // using: "otherbitmap ^ difference", but this would
                    // incorrectly grant permissions in some cases (see JIRA
                    // issue).  To avoid inconsistencies between grant and deny
                    // lists, setting a bit in one list should unset the
                    // corresponding bit in the other.
                    int modifiedotherbitmap = otherbitmap & ~difference;

                    if (otherbitmap != modifiedotherbitmap) {
                        // We made a change.  Record our modification.
                        modifications.put(inverseKeyOf(name), modifiedotherbitmap);
                    }
                }
            }
        }
        LOGGER.debug("Updating ACL {} {} ", key, modifications);
        putCached(keySpace, aclColumnFamily, key, modifications, (currentAcl == null || currentAcl.size() == 0));
        storeListener.onUpdate(objectType, objectPath, getCurrentUserId(), false, null, "op:acl");
    }

    private boolean containsKey(String name, Map<String, Object> map1, Map<String, Object> map2) {
        return map1.containsKey(name) || map2.containsKey(name);
    }

    private int getBitMap(String name, Map<String, Object> modifications, Map<String, Object> currentAcl) {
        int bm = 0;
        if (modifications.containsKey(name)) {
            bm = toInt(modifications.get(name));
        } else {
            bm = toInt(currentAcl.get(name));
        }
        return bm;
    }

    private String inverseKeyOf(String key) {
        if (key == null) {
            return null;
        }
        if (AclModification.isGrant(key)) {
            return AclModification.getPrincipal(key) + AclModification.DENIED_MARKER;
        } else if (AclModification.isDeny(key)) {
            return AclModification.getPrincipal(key) + AclModification.GRANTED_MARKER;
        } else {
            return key;
        }
    }

    public void check(String objectType, String objectPath, Permission permission)
            throws AccessDeniedException, StorageClientException {
        if (user.isAdmin()) {
            return;
        }
        // users can always operate on their own user object.
        if (Security.ZONE_AUTHORIZABLES.equals(objectType) && user.getId().equals(objectPath)) {
            return;
        }
        int[] privileges = compilePermission(user, objectType, objectPath, 0);
        if (!((permission.getPermission() & privileges[0]) == permission.getPermission())) {
            throw new AccessDeniedException(objectType, objectPath, permission.getName(), user.getId());
        }
    }

    private String getAclKey(String objectType, String objectPath) {
        return objectType + ";" + objectPath;
    }

    public void setRequestPrincipalResolver(PrincipalTokenResolver principalTokenResolver) {
        this.principalTokenResolver = principalTokenResolver;
    }

    public void clearRequestPrincipalResolver() {
        principalTokenResolver = null;
    }

    private int[] compilePermission(Authorizable authorizable, String objectType, String objectPath, int recursion)
            throws StorageClientException {
        String key = getAclKey(objectType, objectPath);
        if (user.getId().equals(authorizable.getId()) && cache.containsKey(key)) {
            return cache.get(key);
        } else {
            LOGGER.debug("Cache Miss {} [{}] ", cache, key);
        }

        Map<String, Object> acl = getCached(keySpace, aclColumnFamily, key);
        LOGGER.debug("ACL on {} is {} ", key, acl);

        int grants = 0;
        int denies = 0;
        if (acl != null) {

            {
                String principal = authorizable.getId();
                int tg = toInt(acl.get(principal + AclModification.GRANTED_MARKER));
                int td = toInt(acl.get(principal + AclModification.DENIED_MARKER));
                grants = grants | tg;
                denies = denies | td;
                LOGGER.debug("Added Permissions for {} g{} d{} => g{} d{}",
                        new Object[] { principal, tg, td, grants, denies });

            }
            /*
             * Deal with any proxy principals, these override groups 
             */
            if (principalTokenResolver != null) {
                Set<String> inspected = Sets.newHashSet();
                if (acl.containsKey(_SECRET_KEY)) {
                    String secretKey = (String) acl.get(_SECRET_KEY);
                    if (secretKey != null) {
                        for (Entry<String, Object> ace : acl.entrySet()) {
                            String k = ace.getKey();
                            LOGGER.debug("Checking {} ", k);
                            if (k.startsWith(DYNAMIC_PRINCIPAL_STEM)) {
                                String proxyPrincipal = AclModification.getPrincipal(k)
                                        .substring(DYNAMIC_PRINCIPAL_STEM.length());
                                if (!inspected.contains(proxyPrincipal)) {
                                    inspected.add(proxyPrincipal);
                                    LOGGER.debug("Is Dynamic {}, checking ", k);
                                    List<Content> proxyPrincipalTokens = Lists.newArrayList();
                                    principalTokenResolver.resolveTokens(proxyPrincipal, proxyPrincipalTokens);
                                    for (Content proxyPrincipalToken : proxyPrincipalTokens) {
                                        if (principalTokenValidator.validatePrincipal(proxyPrincipalToken,
                                                secretKey)) {
                                            String pname = DYNAMIC_PRINCIPAL_STEM + proxyPrincipal;
                                            LOGGER.debug("Has this principal {} ", proxyPrincipal);
                                            int tg = toInt(acl.get(pname + AclModification.GRANTED_MARKER));
                                            int td = toInt(acl.get(pname + AclModification.DENIED_MARKER));
                                            grants = grants | tg;
                                            denies = denies | td;
                                            LOGGER.debug("Added Permissions for {} g{} d{} => g{} d{}",
                                                    new Object[] { pname, tg, td, grants, denies });
                                            break;
                                        }
                                    }
                                }
                            }
                        }
                    } else {
                        LOGGER.debug("Secret Key is null");
                    }
                } else {
                    LOGGER.debug("No Secret Key Key ");
                }
            } else {
                LOGGER.debug("No principalToken Resolver");
            }
            // then deal with static principals
            for (String principal : getPrincipals(authorizable)) {
                int tg = toInt(acl.get(principal + AclModification.GRANTED_MARKER));
                int td = toInt(acl.get(principal + AclModification.DENIED_MARKER));
                grants = grants | tg;
                denies = denies | td;
                LOGGER.debug("Added Permissions for {} g{} d{} => g{} d{}",
                        new Object[] { principal, tg, td, grants, denies });
            }

            // Everyone must be the last principal to be applied
            if (!User.ANON_USER.equals(authorizable.getId())) {
                // all users except anon are in the group everyone, by default
                // but only if not already denied or granted by a more specific
                // permission.
                int tg = (toInt(acl.get(Group.EVERYONE + AclModification.GRANTED_MARKER)) & ~denies);
                int td = (toInt(acl.get(Group.EVERYONE + AclModification.DENIED_MARKER)) & ~grants);
                grants = grants | tg;
                denies = denies | td;
                LOGGER.debug("Added Permissions for {} g{} d{} => g{} d{}",
                        new Object[] { Group.EVERYONE, tg, td, grants, denies });

            }
            /*
             * grants contains the granted permissions in a bitmap denies
             * contains the denied permissions in a bitmap
             */
            int granted = grants;
            int denied = denies;

            /*
             * Only look to parent objects if this is not the root object and
             * everything is not granted and denied
             */
            if (recursion < 20 && !StorageClientUtils.isRoot(objectPath)
                    && (granted != 0xffff || denied != 0xffff)) {
                recursion++;
                int[] parentPriv = compilePermission(authorizable, objectType,
                        StorageClientUtils.getParentObjectPath(objectPath), recursion);
                if (parentPriv != null) {
                    /*
                     * Grant permission not denied at this level parentPriv[0]
                     * is permissions granted by the parent ~denies is
                     * permissions not denied here parentPriv[0] & ~denies is
                     * permissions granted by the parent that have not been
                     * denied here. we need to add those to things granted here.
                     * ie |
                     */
                    granted = grants | (parentPriv[0] & ~denies);
                    /*
                     * Deny permissions not granted at this level
                     */
                    denied = denies | (parentPriv[1] & ~grants);
                }
            }
            // If not denied all users and groups can read other users and
            // groups and all content can be read
            if (((denied & Permissions.CAN_READ.getPermission()) == 0)
                    && (Security.ZONE_AUTHORIZABLES.equals(objectType)
                            || Security.ZONE_CONTENT.equals(objectType))) {
                granted = granted | Permissions.CAN_READ.getPermission();
                LOGGER.debug("Default Read Permission set {} {} ", key, denied);
            } else {
                LOGGER.debug("Default Read has been denied {} {} ", key, denied);
            }
            LOGGER.debug("Permissions on {} for {} is {} {} ", new Object[] { key, user.getId(), granted, denied });
            /*
             * Keep a cached copy
             */
            if (user.getId().equals(authorizable.getId())) {
                cache.put(key, new int[] { granted, denied });
            }
            return new int[] { granted, denied };

        }
        if (Security.ZONE_AUTHORIZABLES.equals(objectType) || Security.ZONE_CONTENT.equals(objectType)) {
            // unless explicitly denied all users can read other users.
            return new int[] { Permissions.CAN_READ.getPermission(), 0 };
        }
        return new int[] { 0, 0 };
    }

    private String[] getPrincipals(final Authorizable authorizable) {
        String k = authorizable.getId();
        if (principalCache.containsKey(k)) {
            return principalCache.get(k);
        }
        Set<String> memberOfSet = Sets.newHashSet(authorizable.getPrincipals());
        if (authorizableManager != null) {
            // membership resolution is possible, but we had better turn off recursion
            if (principalRecursionLock.get() == null) {
                principalRecursionLock.set("l");
                try {
                    for (Iterator<Group> gi = authorizable.memberOf(authorizableManager); gi.hasNext();) {
                        memberOfSet.add(gi.next().getId());
                    }
                } finally {
                    principalRecursionLock.set(null);
                }
            }
        }
        memberOfSet.remove(Group.EVERYONE);
        String[] m = memberOfSet.toArray(new String[memberOfSet.size()]);
        principalCache.put(k, m);
        return m;
    }

    private int toInt(Object object) {
        if (object instanceof Integer) {
            return ((Integer) object).intValue();
        }
        LOGGER.debug("Bitmap Not Present");
        return 0;
    }

    public String getCurrentUserId() {
        return user.getId();
    }

    public void close() {
        closed = true;
    }

    private void checkOpen() throws StorageClientException {
        if (closed) {
            throw new StorageClientException("Access Control Manager is closed");
        }
    }

    public boolean can(Authorizable authorizable, String objectType, String objectPath, Permission permission) {
        if (authorizable instanceof User && ((User) authorizable).isAdmin()) {
            return true;
        }
        // users can always operate on their own user object.
        if (Security.ZONE_AUTHORIZABLES.equals(objectType) && authorizable.getId().equals(objectPath)) {
            return true;
        }
        try {
            int[] privileges = compilePermission(authorizable, objectType, objectPath, 0);
            if (!((permission.getPermission() & privileges[0]) == permission.getPermission())) {
                return false;
            }
        } catch (StorageClientException e) {
            LOGGER.warn(e.getMessage(), e);
            return false;
        }
        return true;
    }

    public Permission[] getPermissions(String objectType, String path) throws StorageClientException {
        int[] perms = compilePermission(this.user, objectType, path, 0);
        List<Permission> permissions = Lists.newArrayList();
        for (Permission p : Permissions.PRIMARY_PERMISSIONS) {
            if ((perms[0] & p.getPermission()) == p.getPermission()) {
                permissions.add(p);
            }
        }
        return permissions.toArray(new Permission[permissions.size()]);
    }

    public String[] findPrincipals(String objectType, String objectPath, int permission, boolean granted)
            throws StorageClientException {
        Map<String, int[]> principalMap = internalCompilePrincipals(objectType, objectPath, 0);
        LOGGER.debug("Got Principals {} ", principalMap);
        List<String> principals = Lists.newArrayList();
        for (Entry<String, int[]> perm : principalMap.entrySet()) {
            int[] p = perm.getValue();
            if (granted && (p[0] & permission) == permission) {
                principals.add(perm.getKey());
                LOGGER.debug("Included {} {} {} ", new Object[] { perm.getKey(), perm.getValue(), permission });
            } else if (!granted && (p[1] & permission) == permission) {
                principals.add(perm.getKey());
                LOGGER.debug("Included {} {} {} ", new Object[] { perm.getKey(), perm.getValue(), permission });
            } else {
                LOGGER.debug("Filtered {} {} {} ", new Object[] { perm.getKey(), perm.getValue(), permission });
            }
        }
        LOGGER.debug(" Found Principals {} ", principals);
        return principals.toArray(new String[principals.size()]);
    }

    private Map<String, int[]> internalCompilePrincipals(String objectType, String objectPath, int recursion)
            throws StorageClientException {
        Map<String, int[]> compiledPermissions = Maps.newHashMap();
        String key = getAclKey(objectType, objectPath);

        Map<String, Object> acl = getCached(keySpace, aclColumnFamily, key);

        if (acl != null) {
            LOGGER.debug("Checking {} {} ", key, acl);
            for (Entry<String, Object> ace : acl.entrySet()) {
                String aceKey = ace.getKey();
                String principal = aceKey.substring(0, aceKey.length() - 2);

                if (!compiledPermissions.containsKey(principal)) {
                    int tg = toInt(acl.get(principal + AclModification.GRANTED_MARKER));
                    int td = toInt(acl.get(principal + AclModification.DENIED_MARKER));
                    compiledPermissions.put(principal, new int[] { tg, td });
                    LOGGER.debug("added {} ", principal);
                }

            }
        }
        /*
         * grants contains the granted permissions in a bitmap denies contains
         * the denied permissions in a bitmap
         */

        /*
         * Only look to parent objects if this is not the root object and
         * everything is not granted and denied
         */
        if (recursion < 20 && !StorageClientUtils.isRoot(objectPath)) {
            recursion++;
            Map<String, int[]> parentPermissions = internalCompilePrincipals(objectType,
                    StorageClientUtils.getParentObjectPath(objectPath), recursion);
            // add the parernt privileges in
            for (Entry<String, int[]> parentPermission : parentPermissions.entrySet()) {
                int[] thisPriv = new int[2];
                String principal = parentPermission.getKey();
                if (compiledPermissions.containsKey(principal)) {
                    thisPriv = compiledPermissions.get(principal);
                    LOGGER.debug("modified {} ", principal);
                } else {
                    LOGGER.debug("creating {} ", principal);
                }
                int[] parentPriv = parentPermission.getValue();

                /*
                 * Grant permission not denied at this level parentPriv[0] is
                 * permissions granted by the parent ~denies is permissions not
                 * denied here parentPriv[0] & ~denies is permissions granted by
                 * the parent that have not been denied here. we need to add
                 * those to things granted here. ie |
                 */
                int granted = thisPriv[0] | (parentPriv[0] & ~thisPriv[1]);
                /*
                 * Deny permissions not granted at this level
                 */
                int denied = thisPriv[1] | (parentPriv[1] & ~thisPriv[0]);

                compiledPermissions.put(principal, new int[] { granted, denied });

            }
        }

        //
        // If not denied all users and groups can read other users and
        // groups and all content can be read
        for (String principal : new String[] { Group.EVERYONE, User.ANON_USER }) {
            int[] perm = new int[2];
            if (compiledPermissions.containsKey(principal)) {
                perm = compiledPermissions.get(principal);
            }
            if (((perm[1] & Permissions.CAN_READ.getPermission()) == 0)
                    && (Security.ZONE_AUTHORIZABLES.equals(objectType)
                            || Security.ZONE_CONTENT.equals(objectType))) {
                perm[0] = perm[0] | Permissions.CAN_READ.getPermission();
                LOGGER.debug("added Default {} ", principal);
                compiledPermissions.put(principal, perm);
            }
        }
        compiledPermissions.put(User.ADMIN_USER, new int[] { 0xffff, 0x0000 });
        return compiledPermissions;
        // only store those permissions the match the requested set.]

    }

    @Override
    protected Logger getLogger() {
        return LOGGER;
    }

    public void setAuthorizableManager(AuthorizableManager authorizableManager) {
        this.authorizableManager = authorizableManager;
    }

}