com.eucalyptus.objectstorage.auth.OsgAuthorizationHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.eucalyptus.objectstorage.auth.OsgAuthorizationHandler.java

Source

/*************************************************************************
 * Copyright 2009-2015 Eucalyptus Systems, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see http://www.gnu.org/licenses/.
 *
 * Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta
 * CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need
 * additional information or have any questions.
 ************************************************************************/

package com.eucalyptus.objectstorage.auth;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

import com.eucalyptus.auth.Accounts;
import com.eucalyptus.auth.AuthContextSupplier;
import com.eucalyptus.auth.AuthException;
import com.eucalyptus.auth.Permissions;
import com.eucalyptus.auth.PolicyResourceContext;
import com.eucalyptus.auth.PolicyResourceContext.PolicyResourceInfo;
import com.eucalyptus.auth.principal.AccountFullName;
import com.eucalyptus.auth.principal.AccountIdentifiers;
import com.eucalyptus.auth.principal.PolicyScope;
import com.eucalyptus.auth.principal.PolicyVersion;
import com.eucalyptus.auth.principal.PolicyVersions;
import com.eucalyptus.auth.principal.Principals;
import com.eucalyptus.auth.principal.UserPrincipal;
import com.eucalyptus.component.annotation.ComponentNamed;
import com.eucalyptus.context.Context;
import com.eucalyptus.context.Contexts;
import com.eucalyptus.context.NoSuchContextException;
import com.eucalyptus.objectstorage.ObjectState;
import com.eucalyptus.objectstorage.entities.Bucket;
import com.eucalyptus.objectstorage.entities.ObjectEntity;
import com.eucalyptus.objectstorage.entities.S3AccessControlledEntity;
import com.eucalyptus.objectstorage.msgs.CreateBucketType;
import com.eucalyptus.objectstorage.msgs.ObjectStorageRequestType;
import com.eucalyptus.objectstorage.policy.AdminOverrideAllowed;
import com.eucalyptus.objectstorage.policy.RequiresACLPermission;
import com.eucalyptus.objectstorage.policy.RequiresPermission;
import com.eucalyptus.objectstorage.policy.ResourceType;
import com.eucalyptus.objectstorage.policy.S3PolicySpec;
import com.eucalyptus.objectstorage.util.ObjectStorageProperties;
import com.eucalyptus.system.Ats;
import javaslang.Tuple;
import javaslang.Tuple3;
import javaslang.Tuple4;
import javaslang.control.Option;

@ComponentNamed
public class OsgAuthorizationHandler implements RequestAuthorizationHandler {
    private static final Logger LOG = Logger.getLogger(OsgAuthorizationHandler.class);

    /**
     * Evaluates the authorization for the operation requested, evaluates IAM, ACL, and bucket policy (bucket policy not yet supported).
     * 
     * @param request
     * @param bucketResourceEntity
     * @param objectResourceEntity
     * @param resourceAllocationSize the size for the quota check(s) if applicable
     * @return true if authorized
     */
    public <T extends ObjectStorageRequestType> boolean operationAllowed(@Nonnull T request,
            @Nullable final Bucket bucketResourceEntity, @Nullable final ObjectEntity objectResourceEntity,
            long resourceAllocationSize) throws IllegalArgumentException {
        /*
         * Process the operation's authz requirements based on the request type annotations
         */
        Ats requestAuthzProperties = Ats.from(request);
        ObjectStorageProperties.Permission[] requiredBucketACLPermissions = null;
        ObjectStorageProperties.Permission[] requiredObjectACLPermissions = null;
        boolean allowOwnerOnly = true;
        ObjectStorageProperties.Resource[] requiredOwnerOf = null;
        RequiresACLPermission requiredACLs = requestAuthzProperties.get(RequiresACLPermission.class);
        if (requiredACLs != null) {
            requiredBucketACLPermissions = requiredACLs.bucket();
            requiredObjectACLPermissions = requiredACLs.object();
            requiredOwnerOf = requiredACLs.ownerOf();
            allowOwnerOnly = requiredOwnerOf.length > 0;
        } else {
            // No ACL annotation is ok, maybe a admin only op
        }

        String[] requiredActions = null;
        RequiresPermission perms = requestAuthzProperties.get(RequiresPermission.class);
        if (perms != null) {
            // check for version specific IAM permissions and version Id in the request
            if (perms.version().length > 0 && StringUtils.isNotBlank(request.getVersionId()) && !StringUtils
                    .equalsIgnoreCase(request.getVersionId(), ObjectStorageProperties.NULL_VERSION_ID)) {
                requiredActions = perms.version(); // Use version specific IAM perms
            } else {
                requiredActions = perms.standard(); // Use default/standard IAM perms
            }
        }

        Boolean allowAdmin = (requestAuthzProperties.get(AdminOverrideAllowed.class) != null);

        // Must have at least one of: admin-only, owner-only, ACL, or IAM.
        if (requiredBucketACLPermissions == null && requiredObjectACLPermissions == null && requiredActions == null
                && !allowAdmin) {
            // Insufficient permission set on the message type.
            LOG.error("Insufficient permission annotations on type: " + request.getClass().getName()
                    + " cannot evaluate authorization");
            return false;
        }

        String resourceType = null;
        if (requestAuthzProperties.get(ResourceType.class) != null) {
            resourceType = requestAuthzProperties.get(ResourceType.class).value();
        }

        // Get user from context
        final UserPrincipal requestUser;
        final String requestAccountNumber;
        final String requestCanonicalId;
        final AuthContextSupplier authContext;
        try {
            final Context ctx = Contexts.lookup(request.getCorrelationId());
            requestUser = ctx.getUser();
            requestAccountNumber = ctx.getAccountNumber();
            requestCanonicalId = requestUser.getCanonicalId();
            authContext = ctx.getAuthContext();
        } catch (final NoSuchContextException e) {
            LOG.error("Context not found, cannot evaluate authorization " + request.getCorrelationId());
            return false;
        }

        if (allowAdmin && requestUser.isSystemAdmin()) {
            // Admin override
            return true;
        }

        final Option<Tuple4<String, String, Integer, String>> bucketPolicy;
        final String resourceOwnerAccountNumber;
        final String bucketOwnerAccountNumber;
        final PolicyResourceInfo<S3AccessControlledEntity> policyResourceInfo;
        if (resourceType == null) {
            LOG.error("No resource type found in request class annotations, cannot process.");
            return false;
        } else {
            try {
                // Ensure we have the proper resource entities present and get owner info
                switch (resourceType) {
                case S3PolicySpec.S3_RESOURCE_BUCKET:
                    // Get the bucket owner. bucket and resource owner are same in this case
                    if (bucketResourceEntity == null) {
                        LOG.error("Could not check access for operation due to no bucket resource entity found");
                        return false;
                    } else {
                        bucketOwnerAccountNumber = resourceOwnerAccountNumber = lookupAccountIdByCanonicalId(
                                bucketResourceEntity.getOwnerCanonicalId());
                        policyResourceInfo = PolicyResourceContext.resourceInfo(resourceOwnerAccountNumber,
                                bucketResourceEntity);
                        bucketPolicy = Option
                                .of(Tuple.of(bucketOwnerAccountNumber, bucketResourceEntity.getBucketName(),
                                        bucketResourceEntity.getVersion(), bucketResourceEntity.getPolicy()));
                    }
                    break;
                case S3PolicySpec.S3_RESOURCE_OBJECT:
                    // get the bucket owner account number as the bucket and object owner may be different
                    if (bucketResourceEntity == null) { // cannot be null as every object is associated with one bucket
                        LOG.error("Could not check access for operation due to no bucket resource entity found");
                        return false;
                    } else {
                        bucketOwnerAccountNumber = lookupAccountIdByCanonicalId(
                                bucketResourceEntity.getOwnerCanonicalId());
                        bucketPolicy = Option
                                .of(Tuple.of(bucketOwnerAccountNumber, bucketResourceEntity.getBucketName(),
                                        bucketResourceEntity.getVersion(), bucketResourceEntity.getPolicy()));
                    }
                    // get the object owner.
                    if (objectResourceEntity == null) {
                        LOG.error("Could not check access for operation due to no object resource entity found");
                        return false;
                    } else {
                        // on create treat the object owner as the bucket owner for authorization purposes
                        resourceOwnerAccountNumber = ObjectState.creating == objectResourceEntity.getState()
                                ? bucketOwnerAccountNumber
                                : lookupAccountIdByCanonicalId(objectResourceEntity.getOwnerCanonicalId());
                        policyResourceInfo = PolicyResourceContext.resourceInfo(resourceOwnerAccountNumber,
                                objectResourceEntity);
                    }
                    break;
                default:
                    LOG.error("Unknown resource type looking up resource owner. Disallowing operation.");
                    return false;
                }
            } catch (AuthException e) {
                LOG.error("Exception caught looking up resource owner. Disallowing operation.", e);
                return false;
            }
        }

        // Get the resourceId based on IAM resource type
        final String resourceId;
        if (S3PolicySpec.S3_RESOURCE_BUCKET.equals(resourceType)) {
            resourceId = request.getBucket();
        } else if (S3PolicySpec.S3_RESOURCE_OBJECT.equals(resourceType)) {
            resourceId = request.getFullResource();
        } else {
            resourceId = null;
        }

        // Override for 'eucalyptus' account and workaround for EUCA-11346
        // Skip ACL checks for 'eucalyptus' account only. ACL checks must be performed for all other accounts including system accounts
        // IAM checks must be performed for all accounts
        if (allowAdmin && AccountIdentifiers.SYSTEM_ACCOUNT.equals(requestUser.getAccountAlias())) {
            return iamPermissionsAllow(true, authContext, requiredActions, policyResourceInfo, Option.none(),
                    resourceType, resourceId, resourceAllocationSize);
        }

        // Don't allow anonymous to create buckets. EUCA-12902
        if (S3PolicySpec.S3_RESOURCE_BUCKET.equals(resourceType)
                && Principals.nobodyAccount().getAccountNumber().equals(resourceOwnerAccountNumber)
                && request instanceof CreateBucketType) {
            return false;
        }

        if (requiredBucketACLPermissions == null) {
            throw new IllegalArgumentException(
                    "No requires-permission actions found in request class annotations, cannot process.");
        }

        /*
         * Bucket or object owner only? It is expected that ownerOnly flag can be used solely or in combination with ACL checks. If owner checks are
         * required, evaluate them first before evaluating the ACLs
         */
        Boolean isRequestByOwner = false;
        if (allowOwnerOnly) { // owner checks are in effect
            if (requiredOwnerOf == null || requiredOwnerOf.length == 0) {
                LOG.error(
                        "Owner only flag does not include resource (bucket, object) that ownership checks should be applied to");
                return false;
            }

            for (ObjectStorageProperties.Resource resource : requiredOwnerOf) {
                if (ObjectStorageProperties.Resource.bucket.equals(resource)) {
                    isRequestByOwner = isRequestByOwner || bucketOwnerAccountNumber.equals(requestAccountNumber);
                } else {
                    isRequestByOwner = isRequestByOwner || resourceOwnerAccountNumber.equals(requestAccountNumber);
                }
            }

            if (!isRequestByOwner) {
                LOG.debug("Request is rejected by ACL checks due to account ownership requirements");
                return false;
            }
        } else {
            // owner check does not apply
        }
        final boolean requestAccountIsResourceAccount = // so request account iam policy is sufficient to grant access
                isRequestByOwner || ((S3PolicySpec.S3_RESOURCE_OBJECT.equals(resourceType)
                        || S3PolicySpec.S3_RESOURCE_BUCKET.equals(resourceType))
                        && resourceOwnerAccountNumber.equals(requestAccountNumber));
        final boolean bucketAccountIsObjectAccount = // so bucket policy is sufficient to grant access
                S3PolicySpec.S3_RESOURCE_OBJECT.equals(resourceType)
                        && resourceOwnerAccountNumber.equals(bucketOwnerAccountNumber);

        /* ACL Checks: Is the user's account allowed? */
        Boolean aclAllow = false;
        if (requiredBucketACLPermissions.length > 0 || requiredObjectACLPermissions.length > 0) { // check ACLs if any

            // Check bucket ACLs, if any
            if (requiredBucketACLPermissions.length > 0) {

                // Evaluate the bucket ACL, any matching grant gives permission
                for (ObjectStorageProperties.Permission permission : requiredBucketACLPermissions) {
                    aclAllow = aclAllow || bucketResourceEntity.can(permission, requestCanonicalId);
                }
            }

            // Check object ACLs, if any
            if (requiredObjectACLPermissions != null && requiredObjectACLPermissions.length > 0) {
                if (objectResourceEntity == null) {
                    // There are object ACL requirements but no object entity to check. fail.
                    // Don't bother with other checks, this is an invalid state
                    LOG.error("Null bucket resource, cannot evaluate bucket ACL");
                    return false;
                }
                for (ObjectStorageProperties.Permission permission : requiredObjectACLPermissions) {
                    aclAllow = aclAllow || objectResourceEntity.can(permission, requestCanonicalId);
                }
            }
        } else { // No ACLs, ownership would have been used to determine privilege
            aclAllow = isRequestByOwner;
        }

        final Boolean iamAllow = iamPermissionsAllow(aclAllow, authContext, requiredActions, policyResourceInfo,
                bucketPolicy, resourceType, resourceId, resourceAllocationSize);

        // Must have both acl and iam allow (account & user)
        return (aclAllow || bucketAccountIsObjectAccount || requestAccountIsResourceAccount) && iamAllow;
    }

    private String lookupAccountIdByCanonicalId(final String canonicalId) throws AuthException {
        if (AccountIdentifiers.NOBODY_CANONICAL_ID.equals(canonicalId)) {
            return Principals.nobodyAccount().getAccountNumber();
        } else {
            return Accounts.lookupAccountIdByCanonicalId(canonicalId);
        }
    }

    private static Boolean iamPermissionsAllow(final boolean aclsAllow, final AuthContextSupplier authContext,
            final String[] requiredActions, final PolicyResourceInfo<S3AccessControlledEntity> policyResourceInfo,
            final Option<Tuple4<String, String, Integer, String>> bucketPolicyOption, final String resourceType,
            final String resourceId, final long resourceAllocationSize) {
        final PolicyVersion bucketPolicy = getBucketPolicy(
                bucketPolicyOption.map(t -> Tuple.of(t._2(), t._3(), t._4())));
        final AccountFullName bucketPolicyAccount = bucketPolicyOption.isDefined()
                ? AccountFullName.getInstance(bucketPolicyOption.get()._1())
                : null;
        final AccountFullName resourceAccount = AccountFullName
                .getInstance(policyResourceInfo.getResourceAccountNumber());

        /* IAM checks: Is the user allowed within the account? */
        // the Permissions.isAuthorized() handles the default deny for each action.
        boolean iamAllow = true; // Evaluate each iam action required, all must be allowed
        for (String action : requiredActions) {
            try (final PolicyResourceContext context = PolicyResourceContext.of(policyResourceInfo, action)) {
                // Any deny overrides an allow
                // Note: explicitly set resourceOwnerAccount to null here, otherwise iam will reject even if the ACL checks
                // were valid, let ACLs handle cross-account access.
                iamAllow &= Permissions.isAuthorized(S3PolicySpec.VENDOR_S3, resourceType, resourceId,
                        resourceAccount, bucketPolicy, bucketPolicyAccount, action, authContext, aclsAllow)
                        && Permissions.canAllocate(S3PolicySpec.VENDOR_S3, resourceType, resourceId, action,
                                authContext, resourceAllocationSize);
            }
        }
        return iamAllow;
    }

    private static PolicyVersion getBucketPolicy(final Option<Tuple3<String, Integer, String>> bucketPolicyOption) {
        PolicyVersion policyVersion = null;
        if (bucketPolicyOption.isDefined()) {
            final String bucketName = bucketPolicyOption.get()._1();
            final String policy = bucketPolicyOption.get()._3();
            if (policy != null) {
                final String policyVersionId = "arn:aws:s3:::" + bucketName + "/policy/bucket,"
                        + bucketPolicyOption.get()._2();
                final String policyName = "Policy for bucket " + bucketName;
                final String policyHash = PolicyVersions.hash(policy);
                policyVersion = new PolicyVersion() {
                    @Override
                    public String getPolicyName() {
                        return policyName;
                    }

                    @Override
                    public PolicyScope getPolicyScope() {
                        return PolicyScope.Resource;
                    }

                    @Override
                    public String getPolicyVersionId() {
                        return policyVersionId;
                    }

                    @Override
                    public String getPolicyHash() {
                        return policyHash;
                    }

                    @Override
                    public String getPolicy() {
                        return policy;
                    }
                };
            }
        }
        return policyVersion;
    }
}