/*************************************************************************
* 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;
}
}