/*************************************************************************
* 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.util;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.NoSuchElementException;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.apache.log4j.Logger;
import com.eucalyptus.auth.Accounts;
import com.eucalyptus.auth.AuthException;
import com.eucalyptus.auth.principal.AccountIdentifiers;
import com.eucalyptus.auth.principal.Principals;
import com.eucalyptus.auth.principal.UserPrincipal;
import com.eucalyptus.objectstorage.exceptions.s3.InvalidArgumentException;
import com.eucalyptus.objectstorage.exceptions.s3.UnresolvableGrantByEmailAddressException;
import com.eucalyptus.storage.msgs.s3.AccessControlList;
import com.eucalyptus.storage.msgs.s3.AccessControlPolicy;
import com.eucalyptus.storage.msgs.s3.CanonicalUser;
import com.eucalyptus.storage.msgs.s3.Grant;
import com.eucalyptus.storage.msgs.s3.Grantee;
import com.eucalyptus.storage.msgs.s3.Group;
import com.eucalyptus.util.EucalyptusCloudException;
import com.google.common.base.Function;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
/**
* Maps CannedAcls to access control lists
*/
public class AclUtils {
private static final Logger LOG = Logger.getLogger(AclUtils.class);
/* Use string as key since enum doesn't allow '-' which is in the ACL types. Allows a direct lookup from the msg */
private static final HashMap<String, Function<OwnerIdPair, List<Grant>>> cannedAclMap = new HashMap<String, Function<OwnerIdPair, List<Grant>>>();
// A lookup map for quick verification of group uris
private static final HashMap<String, ObjectStorageProperties.S3_GROUP> groupUriMap = new HashMap<String, ObjectStorageProperties.S3_GROUP>();
static {
// Populate the map
cannedAclMap.put(ObjectStorageProperties.CannedACL.private_only.toString(), PrivateOnlyGrantBuilder.INSTANCE);
cannedAclMap.put(ObjectStorageProperties.CannedACL.authenticated_read.toString(), AuthenticatedReadGrantBuilder.INSTANCE);
cannedAclMap.put(ObjectStorageProperties.CannedACL.public_read.toString(), PublicReadGrantBuilder.INSTANCE);
cannedAclMap.put(ObjectStorageProperties.CannedACL.public_read_write.toString(), PublicReadWriteGrantBuilder.INSTANCE);
cannedAclMap.put(ObjectStorageProperties.CannedACL.aws_exec_read.toString(), AwsExecReadGrantBuilder.INSTANCE);
cannedAclMap.put(ObjectStorageProperties.CannedACL.ec2_bundle_read.toString(), Ec2BundleReadGrantBuilder.INSTANCE);
cannedAclMap.put(ObjectStorageProperties.CannedACL.bucket_owner_full_control.toString(), BucketOwnerFullControlGrantBuilder.INSTANCE);
cannedAclMap.put(ObjectStorageProperties.CannedACL.bucket_owner_read.toString(), BucketOwnerReadGrantBuilder.INSTANCE);
cannedAclMap.put(ObjectStorageProperties.CannedACL.log_delivery_write.toString(), LogDeliveryWriteGrantBuilder.INSTANCE);
for (ObjectStorageProperties.S3_GROUP g : ObjectStorageProperties.S3_GROUP.values()) {
groupUriMap.put(g.toString(), g);
}
}
public static boolean isValidCannedAcl(String candidateAcl) {
return cannedAclMap.containsKey(candidateAcl);
}
/*
* Simply determines if the userId is a member of the groupId, very simplistic only for ALL_USERS and AUTHENTICATED_USERS, not arbitrary groups.
* Arbitrary groups are not yet supported in ObjectStorage bucket policies/IAM policies. userId should be a canonicalId
*/
public static boolean isUserMemberOfGroup(String userId, String groupId) {
if (Strings.isNullOrEmpty(groupId)) {
return false;
}
try {
ObjectStorageProperties.S3_GROUP group = groupUriMap.get(groupId);
return group != null && isUserMember(userId, group);
} catch (IllegalArgumentException e) {
LOG.warn("Unknown group id requested for membership check: " + groupId);
return false;
}
}
/**
* Just checks the basic S3 groups for membership of the userId. Caller must ensure that the userId is a valid ID in the system. That is outside the
* scope of this method.
*
* @param userId
* @param group
* @return
*/
public static boolean isUserMember(String userId, ObjectStorageProperties.S3_GROUP group) {
if (group == null) {
return false;
}
if (ObjectStorageProperties.S3_GROUP.ALL_USERS_GROUP.equals(group)) {
return true;
}
if (ObjectStorageProperties.S3_GROUP.AUTHENTICATED_USERS_GROUP.equals(group) && !Strings.isNullOrEmpty(userId)
&& !userId.equals(Principals.nobodyUser().getUserId())) {
return true;
}
boolean isSystemAdmin = false;
try {
isSystemAdmin = (Principals.systemUser().getUserId().equals(userId) || Accounts.lookupSystemAdmin().getUserId().equals(userId));
} catch (AuthException e) {
// Fall through
LOG.debug("Got auth exception trying to lookup system admin user for group membership check in ec2-bundle-read", e);
}
boolean isAWSExecReadUser = false;
try {
isAWSExecReadUser = Accounts.lookupSystemAccountByAlias( AccountIdentifiers.AWS_EXEC_READ_SYSTEM_ACCOUNT ).getUserId( ).equals( userId );
} catch (AuthException e) {
// Fall through
LOG.debug("Got auth exception trying to lookup aws-exec-read admin user for group membership check in ec2-bundle-read", e);
}
if (ObjectStorageProperties.S3_GROUP.AWS_EXEC_READ.equals(group) && isAWSExecReadUser) {
return true;
}
// System only (or euca/admin) in the ec2-bundle-read group
if (ObjectStorageProperties.S3_GROUP.EC2_BUNDLE_READ.equals(group) && isSystemAdmin) {
return true;
}
// System or euca/admin only in logging
if (ObjectStorageProperties.S3_GROUP.LOGGING_GROUP.equals(group) && isSystemAdmin) {
return true;
}
return false;
}
/**
* Utility class for passing pairs of canonicalIds around without using something ambiguous like an String-array.
*
* @author zhill
*/
public static class OwnerIdPair {
private String bucketOwner;
private String objectOwner;
public OwnerIdPair(String bucketOwnerCanonicalId, String objectOwnerCanonicalId) {
this.bucketOwner = bucketOwnerCanonicalId;
this.objectOwner = objectOwnerCanonicalId;
}
public String getBucketOwnerCanonicalId() {
return this.bucketOwner;
}
public String getObjectOwnerCanonicalId() {
return this.objectOwner;
}
}
/**
* If the object ownerId is set in the OwnerIdPair then this will assume that the resource is an object and will return a full-control grant for
* that user. If bucket owner only is set then this assumes that the bucket is the resource.
*
* @author zhill
*/
protected enum PrivateOnlyGrantBuilder implements Function<OwnerIdPair, List<Grant>> {
INSTANCE;
@Override
public List<Grant> apply(OwnerIdPair ownerIds) {
ArrayList<Grant> privateGrants = new ArrayList<Grant>();
Grant ownerFullControl = new Grant();
Grantee owner = new Grantee();
String displayName = "";
String ownerCanonicalId = null;
if (!Strings.isNullOrEmpty(ownerIds.getObjectOwnerCanonicalId())) {
ownerCanonicalId = ownerIds.getObjectOwnerCanonicalId();
} else {
ownerCanonicalId = ownerIds.getBucketOwnerCanonicalId();
}
try {
displayName = lookupDisplayNameByCanonicalId(ownerCanonicalId);
} catch (AuthException e) {
displayName = "";
}
owner.setCanonicalUser(new CanonicalUser(ownerCanonicalId, displayName));
owner.setType("CanonicalUser");
ownerFullControl.setGrantee(owner);
ownerFullControl.setPermission(ObjectStorageProperties.Permission.FULL_CONTROL.toString());
privateGrants.add(ownerFullControl);
return privateGrants;
}
}
;
protected enum AuthenticatedReadGrantBuilder implements Function<OwnerIdPair, List<Grant>> {
INSTANCE;
@Override
public List<Grant> apply(OwnerIdPair ownerIds) {
List<Grant> authenticatedRead = PrivateOnlyGrantBuilder.INSTANCE.apply(ownerIds);
Grantee authenticatedUsers = new Grantee();
authenticatedUsers.setGroup(new Group(ObjectStorageProperties.S3_GROUP.AUTHENTICATED_USERS_GROUP.toString()));
Grant authUsersGrant = new Grant();
authUsersGrant.setPermission(ObjectStorageProperties.Permission.READ.toString());
authUsersGrant.setGrantee(authenticatedUsers);
authenticatedRead.add(authUsersGrant);
return authenticatedRead;
}
}
;
protected enum PublicReadGrantBuilder implements Function<OwnerIdPair, List<Grant>> {
INSTANCE;
@Override
public List<Grant> apply(OwnerIdPair ownerIds) {
List<Grant> publicRead = PrivateOnlyGrantBuilder.INSTANCE.apply(ownerIds);
Grantee allUsers = new Grantee();
allUsers.setGroup(new Group(ObjectStorageProperties.S3_GROUP.ALL_USERS_GROUP.toString()));
Grant allUsersGrant = new Grant();
allUsersGrant.setPermission(ObjectStorageProperties.Permission.READ.toString());
allUsersGrant.setGrantee(allUsers);
publicRead.add(allUsersGrant);
return publicRead;
}
}
;
protected enum PublicReadWriteGrantBuilder implements Function<OwnerIdPair, List<Grant>> {
INSTANCE;
@Override
public List<Grant> apply(OwnerIdPair ownerIds) {
List<Grant> publicReadWrite = PublicReadGrantBuilder.INSTANCE.apply(ownerIds);
Grantee allUsers = new Grantee();
allUsers.setGroup(new Group(ObjectStorageProperties.S3_GROUP.ALL_USERS_GROUP.toString()));
Grant allUsersGrant = new Grant();
allUsersGrant.setPermission(ObjectStorageProperties.Permission.WRITE.toString());
allUsersGrant.setGrantee(allUsers);
publicReadWrite.add(allUsersGrant);
return publicReadWrite;
}
}
;
/**
* This is inconsistent with S3, because we use a group rather than account for the grant. Makes more sense for euca and can be changed later if
* needed via upgrade
*/
protected enum AwsExecReadGrantBuilder implements Function<OwnerIdPair, List<Grant>> {
INSTANCE;
@Override
public List<Grant> apply(OwnerIdPair ownerIds) {
List<Grant> awsExecRead = PrivateOnlyGrantBuilder.INSTANCE.apply(ownerIds);
Grantee execReadGroup = new Grantee();
execReadGroup.setGroup(new Group(ObjectStorageProperties.S3_GROUP.AWS_EXEC_READ.toString()));
Grant execReadGrant = new Grant();
execReadGrant.setPermission(ObjectStorageProperties.Permission.READ.toString());
execReadGrant.setGrantee(execReadGroup);
awsExecRead.add(execReadGrant);
return awsExecRead;
}
}
;
/**
* This is inconsistent with S3, because we use a group rather than account for the grant. Makes more sense for euca and can be changed later if
* needed via upgrade
*/
protected enum Ec2BundleReadGrantBuilder implements Function<OwnerIdPair, List<Grant>> {
INSTANCE;
@Override
public List<Grant> apply(OwnerIdPair ownerIds) {
List<Grant> grants = PrivateOnlyGrantBuilder.INSTANCE.apply(ownerIds);
Grantee grantee = new Grantee();
grantee.setGroup(new Group(ObjectStorageProperties.S3_GROUP.EC2_BUNDLE_READ.toString()));
Grant grant = new Grant();
grant.setPermission(ObjectStorageProperties.Permission.READ.toString());
grant.setGrantee(grantee);
grants.add(grant);
return grants;
}
}
;
protected enum BucketOwnerFullControlGrantBuilder implements Function<OwnerIdPair, List<Grant>> {
INSTANCE;
@Override
public List<Grant> apply(OwnerIdPair ownerIds) {
List<Grant> bucketOwnerFullControl = PrivateOnlyGrantBuilder.INSTANCE.apply(ownerIds);
String canonicalId = ownerIds.getBucketOwnerCanonicalId();
String displayName = "";
try {
displayName = lookupDisplayNameByCanonicalId(canonicalId);
} catch (AuthException e) {
displayName = "";
}
Grantee bucketOwner = new Grantee();
bucketOwner.setCanonicalUser(new CanonicalUser(canonicalId, displayName));
Grant bucketOwnerGrant = new Grant();
bucketOwnerGrant.setPermission(ObjectStorageProperties.Permission.FULL_CONTROL.toString());
bucketOwnerGrant.setGrantee(bucketOwner);
bucketOwnerFullControl.add(bucketOwnerGrant);
return bucketOwnerFullControl;
}
}
;
protected enum BucketOwnerReadGrantBuilder implements Function<OwnerIdPair, List<Grant>> {
INSTANCE;
@Override
public List<Grant> apply(OwnerIdPair ownerIds) {
List<Grant> bucketOwnerRead = PrivateOnlyGrantBuilder.INSTANCE.apply(ownerIds);
String canonicalId = ownerIds.getBucketOwnerCanonicalId();
String displayName = "";
try {
displayName = lookupDisplayNameByCanonicalId(canonicalId);
} catch (AuthException e) {
displayName = "";
}
Grantee bucketOwner = new Grantee();
bucketOwner.setCanonicalUser(new CanonicalUser(canonicalId, displayName));
Grant bucketOwnerGrant = new Grant();
bucketOwnerGrant.setPermission(ObjectStorageProperties.Permission.READ.toString());
bucketOwnerGrant.setGrantee(bucketOwner);
bucketOwnerRead.add(bucketOwnerGrant);
return bucketOwnerRead;
}
}
;
protected enum LogDeliveryWriteGrantBuilder implements Function<OwnerIdPair, List<Grant>> {
INSTANCE;
@Override
public List<Grant> apply(OwnerIdPair ownerIds) {
List<Grant> logDeliveryWrite = PrivateOnlyGrantBuilder.INSTANCE.apply(ownerIds);
Grantee logGroup = new Grantee();
logGroup.setGroup(new Group(ObjectStorageProperties.S3_GROUP.LOGGING_GROUP.toString()));
Grant loggingWriteGrant = new Grant();
loggingWriteGrant.setPermission(ObjectStorageProperties.Permission.WRITE.toString());
loggingWriteGrant.setGrantee(logGroup);
Grant loggingReadAcpGrant = new Grant();
loggingReadAcpGrant.setPermission(ObjectStorageProperties.Permission.READ_ACP.toString());
loggingReadAcpGrant.setGrantee(logGroup);
logDeliveryWrite.add(loggingWriteGrant);
logDeliveryWrite.add(loggingReadAcpGrant);
return logDeliveryWrite;
}
}
;
public static ObjectStorageProperties.S3_GROUP getGroupFromUri(String uri) throws NoSuchElementException {
ObjectStorageProperties.S3_GROUP foundGroup = groupUriMap.get(uri);
if (foundGroup == null) {
throw new NoSuchElementException(uri);
}
return foundGroup;
}
/**
* Processes a list by finding all canned-acls and expanding those. The returned list is a new list that includes all non-canned ACL entries of the
* input as well as the expanded grants mapped to canned-acls
* <p/>
* CannedAcls are Grants with Grantee = "", and Permision is the canned-acl string
*
* @param msgAcl
* @return
*/
public static AccessControlList expandCannedAcl(@Nonnull AccessControlList msgAcl, @Nullable final String bucketOwnerCanonicalId,
@Nullable final String objectOwnerCanonicalId) throws EucalyptusCloudException {
if (msgAcl == null) {
throw new IllegalArgumentException("Null list received");
}
AccessControlList outputList = new AccessControlList();
if (outputList.getGrants() == null) {
// Should be handled by constructor of ACL, but just to be sure
outputList.setGrants(new ArrayList<Grant>());
}
final OwnerIdPair owners = new OwnerIdPair(bucketOwnerCanonicalId, objectOwnerCanonicalId);
String entryValue = null;
for (Grant msgGrant : msgAcl.getGrants()) {
entryValue = msgGrant.getPermission(); // The OSG binding populates the canned-acl in the permission field
try {
if (cannedAclMap.containsKey(entryValue)) {
outputList.getGrants().addAll(cannedAclMap.get(entryValue).apply(owners));
} else {
// add to output.
outputList.getGrants().add(msgGrant);
}
} catch (Exception e) {
// Failed. Stop now
throw new EucalyptusCloudException("Failed generating the full ACL from canned ACL", e);
}
}
return outputList;
}
public static CanonicalUser buildCanonicalUser( UserPrincipal user ) {
return new CanonicalUser( user.getCanonicalId( ), user.getAccountAlias( ) );
}
/**
* Ensures the the policy is not empty. If found empty or null, a 'private' policy is generated and returned. If creating for an object, the
* BucketOwnerCanonicalId must not be null. If found null, then a bucket-creation is expected and ACLs will be expanded as such.
*
* @param requestUser
* @param policy
* @return
*/
public static AccessControlPolicy processNewResourcePolicy(@Nonnull UserPrincipal requestUser, @Nullable AccessControlPolicy policy,
@Nullable String bucketOwnerCanonicalId) throws Exception {
AccessControlPolicy acPolicy = null;
if (policy != null) {
acPolicy = policy;
} else {
acPolicy = new AccessControlPolicy();
}
if (acPolicy.getOwner() == null) {
acPolicy.setOwner(buildCanonicalUser(requestUser));
}
if (acPolicy.getAccessControlList() == null) {
acPolicy.setAccessControlList(new AccessControlList());
}
if (acPolicy.getAccessControlList().getGrants() == null || acPolicy.getAccessControlList().getGrants().size() == 0) {
// Add default 'fullcontrol' grant for owner.
acPolicy.getAccessControlList().getGrants()
.add(new Grant(new Grantee(buildCanonicalUser(requestUser)), ObjectStorageProperties.Permission.FULL_CONTROL.toString()));
}
final String canonicalId = requestUser.getCanonicalId( );
if (bucketOwnerCanonicalId != null) {
acPolicy.setAccessControlList(AclUtils.expandCannedAcl(acPolicy.getAccessControlList(), bucketOwnerCanonicalId, canonicalId));
} else {
acPolicy.setAccessControlList(AclUtils.expandCannedAcl(acPolicy.getAccessControlList(), canonicalId, null));
}
return acPolicy;
}
/**
* Checks grants and transforms grantees into canonicalId from eucalyptus account id or email address
*
* @param acl
* @return
*/
public static AccessControlList scrubAcl(AccessControlList acl) {
AccessControlList scrubbed = new AccessControlList();
if (acl == null || acl.getGrants() == null || acl.getGrants().size() == 0) {
return scrubbed;
}
String canonicalId = null;
Grantee grantee;
CanonicalUser canonicalUser;
Group group;
String email;
for (Grant g : acl.getGrants()) {
grantee = g.getGrantee();
if (grantee == null) {
continue; // skip, no grantee
} else {
canonicalUser = grantee.getCanonicalUser();
group = grantee.getGroup();
email = grantee.getEmailAddress();
}
canonicalId = canonicalUser == null ? null : resolveCanonicalId(canonicalUser.getID());
if (canonicalId == null) {
try {
canonicalId = Accounts.lookupCanonicalIdByEmail( email );
} catch (AuthException authEx) {
// no-op, we'll check the group
}
}
if (canonicalId == null && group != null && !Strings.isNullOrEmpty(group.getUri())) {
ObjectStorageProperties.S3_GROUP foundGroup = AclUtils.getGroupFromUri(group.getUri());
if (foundGroup == null) {
throw new NoSuchElementException("URI: " + group.getUri() + " not found in group map");
}
// Group URI, use as canonicalId for now.
canonicalId = group.getUri();
}
// the following exception handling looks strange, but this method gets called inside of an enum that
// implements Function<> (guava) and since the targets are checked exceptions it is not easy to pass them
// all the way up the stack where they become significant.
if (email != null && !"".equals(email) && canonicalId == null) {
// build up and throw InvalidArgumentException for the email
UnresolvableGrantByEmailAddressException ugbeae = new UnresolvableGrantByEmailAddressException();
ugbeae.setEmailAddress(email);
throw new RuntimeException(ugbeae);
} else if (canonicalId == null) {
// throw new NoSuchElementException("No canonicalId found for grant: " + g.toString());
InvalidArgumentException iae = new InvalidArgumentException();
iae.setMessage("Invalid id");
iae.setArgumentName("CanonicalUser/ID");
iae.setArgumentValue(canonicalUser.getID());
throw new RuntimeException(iae);
} else {
if (grantee.getCanonicalUser() == null) {
grantee.setCanonicalUser(new CanonicalUser(canonicalId, ""));
} else {
grantee.getCanonicalUser().setID(canonicalId);
}
}
}
return acl;
}
private static interface CanonicalIdChecker {
String check(String id);
}
private static List<CanonicalIdChecker> canonicalIdCheckers = Lists.newArrayList(
new CanonicalIdChecker() {
// is it a canonicalId?
@Override
public String check(String id) {
try {
UserPrincipal userPrincipal = lookupPrincipalByCanonicalId( id );
return userPrincipal.getCanonicalId();
} catch (AuthException authEx) {
}
return null;
}
},
new CanonicalIdChecker() {
// is it a eucalyptus account id?
@Override
public String check(String id) {
if ( Accounts.isAccountNumber( id ) ) try {
UserPrincipal userPrincipal = Accounts.lookupPrincipalByAccountNumber( id );
return userPrincipal.getCanonicalId();
} catch (AuthException authEx) {
}
return null;
}
},
new CanonicalIdChecker() {
// is it an email address?
@Override
public String check(String id) {
try {
if ( Strings.nullToEmpty( id ).contains( "@" ) ) {
return Accounts.lookupCanonicalIdByEmail( id );
}
return null;
} catch (AuthException authEx) {
return null;
}
}
});
public static String resolveCanonicalId(final String inputCanonicalId) {
String resultCanonicalId = null;
for (CanonicalIdChecker checker : canonicalIdCheckers) {
resultCanonicalId = checker.check(inputCanonicalId);
if (resultCanonicalId != null) {
return resultCanonicalId;
}
}
return null;
}
public static String lookupDisplayNameByCanonicalId( final String canonicalId ) throws AuthException {
if ( AccountIdentifiers.NOBODY_CANONICAL_ID.equals( canonicalId ) ) {
return Principals.nobodyAccount( ).getAccountAlias( );
} else {
return Accounts.lookupAccountIdentifiersByCanonicalId( canonicalId ).getAccountAlias();
}
}
public static UserPrincipal lookupPrincipalByCanonicalId( final String canonicalId ) throws AuthException {
if ( AccountIdentifiers.NOBODY_CANONICAL_ID.equals( canonicalId ) ) {
LOG.info("Looked up nobody by canonical ID " + canonicalId);
return Principals.nobodyUser();
} else {
return Accounts.lookupPrincipalByCanonicalId( canonicalId );
}
}
}