/************************************************************************* * Copyright 2009-2013 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.entities; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.annotation.Nonnull; import javax.persistence.Column; import javax.persistence.Lob; import javax.persistence.MappedSuperclass; import javax.persistence.PostLoad; import javax.persistence.PrePersist; import javax.persistence.Transient; import net.sf.json.JSONObject; import net.sf.json.JSONSerializer; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.hibernate.annotations.Type; import com.eucalyptus.auth.AuthException; import com.eucalyptus.entities.AbstractStatefulStacklessPersistent; import com.eucalyptus.objectstorage.util.AclUtils; import com.eucalyptus.objectstorage.util.ObjectStorageProperties; 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.google.common.base.Function; /** * Common handler for authorization for S3 resources that have access controls via ACLs * * @author zhill */ @MappedSuperclass public abstract class S3AccessControlledEntity<STATE extends Enum<STATE>> extends AbstractStatefulStacklessPersistent<STATE> { @Transient private static final Logger LOG = Logger.getLogger(S3AccessControlledEntity.class); // Display name for IAM user @Column(name = "owner_iam_user_displayname") protected String ownerIamUserDisplayName; // Hold the real owner ID. This is the canonical user Id. @Column(name = "owner_canonical_id", nullable = false, length = 64) protected String ownerCanonicalId; // Needed for enforcing IAM resource quotas (an extension of IAM for Euca) @Column(name = "owner_iam_user_id", nullable = false, length = 64) protected String ownerIamUserId; // 8k size should cover 100 80-byte entries which allows for json chars etc @Column(name = "acl", nullable = false) @Lob @Type(type = "org.hibernate.type.StringClobType") private String acl; // A JSON encoded string that is the acl list. @Column(name = "owner_displayname") protected String ownerDisplayName; /** * Map for running actual checks against. Saved to optimize multiple accesses. Caching */ @Transient private Map<String, Integer> decodedAcl = null; // Lob types don't like comparisons with null, so ensure that doesn't happen. @PostLoad @PrePersist public void nullChecks() { if (acl == null) { acl = "{}"; // empty json } } /** * Returns the full name of the resource. e.g. bucket/object for objects or bucket for bucket * * @return */ public abstract String getResourceFullName(); public String getOwnerCanonicalId() { return ownerCanonicalId; } public void setOwnerCanonicalId(String ownerCanonicalId) { this.ownerCanonicalId = ownerCanonicalId; } public String getOwnerIamUserId() { return ownerIamUserId; } public void setOwnerIamUserId(String ownerIamUserId) { this.ownerIamUserId = ownerIamUserId; } public String getOwnerDisplayName() { return ownerDisplayName; } public void setOwnerDisplayName(String ownerDisplayName) { this.ownerDisplayName = ownerDisplayName; } /** * For internal and JPA use only. This returns a json string * * @return */ protected String getAcl() { return this.acl; } public void setAcl(String aclString) { this.acl = aclString; setDecodedAcl(null); } /** * Set from the messaging type. The owner must be properly set in the message msgAcl. * * @param msgAcl */ public void setAcl(final AccessControlPolicy msgAcl) { AccessControlPolicy policy = msgAcl; // Check for the owner and add it if not already set if (this.getOwnerCanonicalId() != null) { if (policy.getOwner() != null && !StringUtils.equals(policy.getOwner().getID(), this.getOwnerCanonicalId())) { throw new RuntimeException("Owner cannot be changed"); } else if (policy.getOwner() == null) { // Copy into policy from this object policy.setOwner(new CanonicalUser(this.getOwnerCanonicalId(), this.getOwnerDisplayName())); } } Map<String, Integer> resultMap = AccessControlPolicyToMap.INSTANCE.apply(policy); // Serialize into json setAcl(JSONObject.fromObject(resultMap).toString()); setDecodedAcl(resultMap); } /** * Utility method for getting the string version of an access control list * * @param acl * @return */ public static String marshallAclToString(AccessControlList acl) throws Exception { Map<String, Integer> resultMap = AccessControlListToMap.INSTANCE.apply(acl); // Serialize into json return JSONObject.fromObject(resultMap).toString(); } public static String marshallAcpToString(AccessControlPolicy acl) throws Exception { Map<String, Integer> resultMap = AccessControlPolicyToMap.INSTANCE.apply(acl); // Serialize into json return JSONObject.fromObject(resultMap).toString(); } /** * Returns the message-typed version of the acl policy. Not necessary for evaluation, just presentation to the user * * @return * @throws Exception */ public AccessControlPolicy getAccessControlPolicy() throws Exception { AccessControlPolicy policy = MapToAccessControlPolicy.INSTANCE.apply(getDecodedAcl()); policy.setOwner(new CanonicalUser(this.getOwnerCanonicalId(), this.ownerDisplayName)); return policy; } public String getOwnerIamUserDisplayName() { return ownerIamUserDisplayName; } public void setOwnerIamUserDisplayName(String ownerIamUserDisplayName) { this.ownerIamUserDisplayName = ownerIamUserDisplayName; } /** * Authorization check for requested permission by specified user/account via canonicalId Currently only checks the ACLs. * * @param permission * @return */ public boolean can(ObjectStorageProperties.Permission permission, String canonicalId) { try { Map<String, Integer> myAcl = getDecodedAcl(); if (myAcl == null) { LOG.error("Got null acl, cannot authorize " + permission.toString()); return false; } /* * Grants can only allow access, so return true if *any* gives the user access. */ // Check groups first. String groupName; for (ObjectStorageProperties.S3_GROUP group : ObjectStorageProperties.S3_GROUP.values()) { if (can(permission, group) && AclUtils.isUserMemberOfGroup(canonicalId, group.toString())) { // User is member of group and the group has permission return true; } } if (myAcl.containsKey(canonicalId) && BitmapGrant.allows(permission, myAcl.get(canonicalId))) { // Explicitly granted by canonical Id. return true; } else { // fall through } } catch (Exception e) { LOG.error("Error checking authorization", e); } return false; } public boolean can(ObjectStorageProperties.Permission permission, ObjectStorageProperties.S3_GROUP group) { try { Map<String, Integer> myAcl = getDecodedAcl(); if (myAcl == null) { LOG.error("Got null acl, cannot authorize " + permission.toString()); return false; } String groupName = group.toString(); return (myAcl.containsKey(groupName) && BitmapGrant.allows(permission, myAcl.get(groupName))); } catch (Exception e) { LOG.error("Error checking authorization", e); } return false; } private synchronized void setDecodedAcl(Map<String, Integer> permissionsMap) { this.decodedAcl = permissionsMap; } private synchronized Map<String, Integer> getDecodedAcl() throws Exception { if (this.decodedAcl == null) { try { // Jackson requires this method to handle generics Map<String, Integer> aclMap = new HashMap<String, Integer>(); JSONObject aclJson = (JSONObject) JSONSerializer.toJSON(this.getAcl()); String key = null; Iterator keys = aclJson.keys(); while (keys.hasNext()) { key = (String) keys.next(); aclMap.put((String) key, new Integer(aclJson.getInt((String) key))); } setDecodedAcl(aclMap); } catch (Exception e) { setDecodedAcl(null); LOG.error("Error decoding acl from DB string", e); throw e; } } return this.decodedAcl; } /** * Converts internal representation into the messaging representation. NOTE: does NOT add owner information as the owner is unknown at this level. * The caller must find the owner grant and set it specifically. * * @author zhill */ protected enum MapToAccessControlPolicy implements Function<Map<String, Integer>, AccessControlPolicy> { INSTANCE; @Override public AccessControlPolicy apply(Map<String, Integer> srcMap) { AccessControlPolicy policy = new AccessControlPolicy(); AccessControlList acList = new AccessControlList(); ArrayList<Grant> grants = new ArrayList<Grant>(); String displayName = null; for (Map.Entry<String, Integer> entry : srcMap.entrySet()) { Grantee grantee = new Grantee(); // Check if a group uri ObjectStorageProperties.S3_GROUP groupId = null; try { groupId = AclUtils.getGroupFromUri(entry.getKey()); } catch (Exception e) { } if (groupId != null) { grantee.setGroup(new Group(groupId.toString())); grantee.setType("Group"); } else { try { displayName = AclUtils.lookupDisplayNameByCanonicalId(entry.getKey()); } catch (AuthException e) { // Not found displayName = ""; } grantee.setCanonicalUser(new CanonicalUser(entry.getKey(), displayName)); grantee.setType("CanonicalUser"); } for (ObjectStorageProperties.Permission p : AccountGrantsFromBitmap.INSTANCE.apply(entry.getValue())) { grants.add(new Grant(grantee, p.toString())); } } acList.setGrants(grants); policy.setAccessControlList(acList); return policy; } } /** * Convert the specified AccessControlPolicy type (a messaging type) into the persistence type */ protected enum AccessControlPolicyToMap implements Function<AccessControlPolicy, Map<String, Integer>> { INSTANCE; /** * Returns a valid map of canonicalid -> grant. * * @return The canonicalid -> grant map * @throws RuntimeException if an error occurred. */ @Nonnull @Override public Map<String, Integer> apply( final AccessControlPolicy srcPolicy ) { if (srcPolicy == null) { throw new RuntimeException("Null source policy. Cannot map"); } final Map<String, Integer> aclMap = AccessControlListToMap.INSTANCE.apply( srcPolicy.getAccessControlList( ) ); if ( aclMap == null ) { throw new RuntimeException("Null acl map. Cannot proceed with policy generation"); } // Check for valid owner final String ownerCanonicalId = srcPolicy.getOwner().getID(); if ( ownerCanonicalId == null ) { throw new RuntimeException("Invalid ACP: OwnerCanonicalId required."); } else { try { AclUtils.lookupPrincipalByCanonicalId(ownerCanonicalId); } catch ( Exception e ) { LOG.warn("Got invalid owner in AccessControlPolicy during mapping to DB: " + ownerCanonicalId); throw new RuntimeException("Could not find account by canonicalId " + ownerCanonicalId, e); } } return aclMap; } } /** * Handles just the access control list itself without the additional owner info * * @author zhill */ protected enum AccessControlListToMap implements Function<AccessControlList, Map<String, Integer>> { INSTANCE; /** * Returns a valid map of canonicalid -> grant. Returns null if an error occurred. */ @Override public Map<String, Integer> apply(AccessControlList srcList) { HashMap<String, Integer> aclMap = new HashMap<String, Integer>(); if (srcList == null) { // Nothing to do return aclMap; } AclUtils.scrubAcl(srcList); for (Grant g : srcList.getGrants()) { String canonicalId = g.getGrantee().getCanonicalUser().getID(); int oldGrant = (aclMap.containsKey(canonicalId) ? aclMap.get(canonicalId) : 0); int newGrant = BitmapGrant.add(ObjectStorageProperties.Permission.valueOf(g.getPermission().toUpperCase()), oldGrant); if (newGrant != 0) { aclMap.put(canonicalId, newGrant); } else { // skip no-op grants } } return aclMap; } } /** * Converts from BitmapGrant to 1 or more msg Grants (up to 3). Does not set the grantee on the grants, just the permission(s). * <p/> * Represents the grant(s) for a single canonicalId/group */ protected enum AccountGrantsFromBitmap implements Function<Integer, List<ObjectStorageProperties.Permission>> { INSTANCE; @Override public List<ObjectStorageProperties.Permission> apply(Integer srcBitmap) { ArrayList<ObjectStorageProperties.Permission> permissions = new ArrayList<ObjectStorageProperties.Permission>(); if (srcBitmap == null) { return permissions; } if (BitmapGrant.allows(ObjectStorageProperties.Permission.FULL_CONTROL, srcBitmap)) { permissions.add(ObjectStorageProperties.Permission.FULL_CONTROL); } else { int i = 0; if (BitmapGrant.allows(ObjectStorageProperties.Permission.READ, srcBitmap)) { permissions.add(ObjectStorageProperties.Permission.READ); } if (BitmapGrant.allows(ObjectStorageProperties.Permission.WRITE, srcBitmap)) { permissions.add(ObjectStorageProperties.Permission.WRITE); } if (BitmapGrant.allows(ObjectStorageProperties.Permission.READ_ACP, srcBitmap)) { permissions.add(ObjectStorageProperties.Permission.READ_ACP); } if (BitmapGrant.allows(ObjectStorageProperties.Permission.WRITE_ACP, srcBitmap)) { permissions.add(ObjectStorageProperties.Permission.WRITE_ACP); } } return permissions; } } /** * Implements the mapping of perms to the bitmap Mapping: Integer => read,write,readAcp,writeAcp in least significant bits Example: read = 8, write * = 4, ... * * @author zhill */ public enum BitmapGrant { INSTANCE; private static final int readMask = 8; // 4th bit from right private static final int writeMask = 4; // 3rd bit private static final int readACPMask = 2; // 2nd bit private static final int writeACPMask = 1; // 1st bit /** * Does this grant allow the requested permission? * * @param perm * @return */ public static boolean allows(ObjectStorageProperties.Permission perm, int mapValue) { switch (perm) { case FULL_CONTROL: return (mapValue & (readMask | writeMask | readACPMask | writeACPMask)) == (readMask | writeMask | readACPMask | writeACPMask); case READ: return (mapValue & readMask) == readMask; case WRITE: return (mapValue & writeMask) == writeMask; case READ_ACP: return (mapValue & readACPMask) == readACPMask; case WRITE_ACP: return (mapValue & writeACPMask) == writeACPMask; } return false; } /** * Sets to the requested permission ONLY. Not additive, replaces previous value. * * @param perm */ public static int translateToBitmap(ObjectStorageProperties.Permission perm) { switch (perm) { case FULL_CONTROL: return (readMask | writeMask | readACPMask | writeACPMask); case READ: return readMask; case WRITE: return writeMask; case READ_ACP: return readACPMask; case WRITE_ACP: return writeACPMask; } return 0; } /** * Add the specified permission, non-destructive. Returns a new bitmap that adds the requested permission to the oldAclBitmap * * @param perm */ public static int add(ObjectStorageProperties.Permission perm, int oldAclBitmap) { switch (perm) { case FULL_CONTROL: return (int) (oldAclBitmap | readMask | writeMask | readACPMask | writeACPMask); case READ: return (int) (oldAclBitmap | readMask); case WRITE: return (int) (oldAclBitmap | writeMask); case READ_ACP: return (int) (oldAclBitmap | readACPMask); case WRITE_ACP: return (int) (oldAclBitmap | writeACPMask); } return 0; } /** * Returns a log-friendly string of the map in b * * @param b * @return */ public static String toLogString(Integer b) { StringBuilder sb = new StringBuilder("{"); sb.append("read=").append(allows(ObjectStorageProperties.Permission.READ, b)); sb.append(",write=").append(allows(ObjectStorageProperties.Permission.WRITE, b)); sb.append(",readacp=").append(allows(ObjectStorageProperties.Permission.READ_ACP, b)); sb.append(",writeacp=").append(allows(ObjectStorageProperties.Permission.WRITE_ACP, b)); sb.append("}"); return sb.toString(); } } }