| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF 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.apache.storm.blobstore; |
| |
| import org.apache.storm.Config; |
| import org.apache.storm.generated.AccessControl; |
| import org.apache.storm.generated.AccessControlType; |
| import org.apache.storm.generated.AuthorizationException; |
| import org.apache.storm.generated.SettableBlobMeta; |
| import org.apache.storm.security.auth.AuthUtils; |
| import org.apache.storm.security.auth.IPrincipalToLocal; |
| import org.apache.storm.security.auth.NimbusPrincipal; |
| import org.apache.commons.lang.StringUtils; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import javax.security.auth.Subject; |
| import java.security.Principal; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** |
| * Provides common handling of acls for Blobstores. |
| * Also contains some static utility functions related to Blobstores. |
| */ |
| public class BlobStoreAclHandler { |
| public static final Logger LOG = LoggerFactory.getLogger(BlobStoreAclHandler.class); |
| private final IPrincipalToLocal _ptol; |
| |
| public static final int READ = 0x01; |
| public static final int WRITE = 0x02; |
| public static final int ADMIN = 0x04; |
| public static final List<AccessControl> WORLD_EVERYTHING = |
| Arrays.asList(new AccessControl(AccessControlType.OTHER, READ | WRITE | ADMIN)); |
| public static final List<AccessControl> DEFAULT = new ArrayList<AccessControl>(); |
| private Set<String> _supervisors; |
| private Set<String> _admins; |
| private boolean doAclValidation; |
| |
| public BlobStoreAclHandler(Map conf) { |
| _ptol = AuthUtils.GetPrincipalToLocalPlugin(conf); |
| _supervisors = new HashSet<String>(); |
| _admins = new HashSet<String>(); |
| if (conf.containsKey(Config.NIMBUS_SUPERVISOR_USERS)) { |
| _supervisors.addAll((List<String>)conf.get(Config.NIMBUS_SUPERVISOR_USERS)); |
| } |
| if (conf.containsKey(Config.NIMBUS_ADMINS)) { |
| _admins.addAll((List<String>)conf.get(Config.NIMBUS_ADMINS)); |
| } |
| if (conf.containsKey(Config.STORM_BLOBSTORE_ACL_VALIDATION_ENABLED)) { |
| doAclValidation = (boolean)conf.get(Config.STORM_BLOBSTORE_ACL_VALIDATION_ENABLED); |
| } |
| } |
| |
| private static AccessControlType parseACLType(String type) { |
| if ("other".equalsIgnoreCase(type) || "o".equalsIgnoreCase(type)) { |
| return AccessControlType.OTHER; |
| } else if ("user".equalsIgnoreCase(type) || "u".equalsIgnoreCase(type)) { |
| return AccessControlType.USER; |
| } |
| throw new IllegalArgumentException(type+" is not a valid access control type"); |
| } |
| |
| private static int parseAccess(String access) { |
| int ret = 0; |
| for (char c: access.toCharArray()) { |
| if ('r' == c) { |
| ret = ret | READ; |
| } else if ('w' == c) { |
| ret = ret | WRITE; |
| } else if ('a' == c) { |
| ret = ret | ADMIN; |
| } else if ('-' == c) { |
| //ignored |
| } else { |
| throw new IllegalArgumentException(""); |
| } |
| } |
| return ret; |
| } |
| |
| public static AccessControl parseAccessControl(String str) { |
| String[] parts = str.split(":"); |
| String type = "other"; |
| String name = ""; |
| String access = "-"; |
| if (parts.length > 3) { |
| throw new IllegalArgumentException("Don't know how to parse "+str+" into an ACL value"); |
| } else if (parts.length == 1) { |
| type = "other"; |
| name = ""; |
| access = parts[0]; |
| } else if (parts.length == 2) { |
| type = "user"; |
| name = parts[0]; |
| access = parts[1]; |
| } else if (parts.length == 3) { |
| type = parts[0]; |
| name = parts[1]; |
| access = parts[2]; |
| } |
| AccessControl ret = new AccessControl(); |
| ret.set_type(parseACLType(type)); |
| ret.set_name(name); |
| ret.set_access(parseAccess(access)); |
| return ret; |
| } |
| |
| private static String accessToString(int access) { |
| StringBuilder ret = new StringBuilder(); |
| ret.append(((access & READ) > 0) ? "r" : "-"); |
| ret.append(((access & WRITE) > 0) ? "w" : "-"); |
| ret.append(((access & ADMIN) > 0) ? "a" : "-"); |
| return ret.toString(); |
| } |
| |
| public static String accessControlToString(AccessControl ac) { |
| StringBuilder ret = new StringBuilder(); |
| switch(ac.get_type()) { |
| case OTHER: |
| ret.append("o"); |
| break; |
| case USER: |
| ret.append("u"); |
| break; |
| default: |
| throw new IllegalArgumentException("Don't know what a type of "+ac.get_type()+" means "); |
| } |
| ret.append(":"); |
| if (ac.is_set_name()) { |
| ret.append(ac.get_name()); |
| } |
| ret.append(":"); |
| ret.append(accessToString(ac.get_access())); |
| return ret.toString(); |
| } |
| |
| public static void validateSettableACLs(String key, List<AccessControl> acls) throws AuthorizationException { |
| Set<String> aclUsers = new HashSet<>(); |
| List<String> duplicateUsers = new ArrayList<>(); |
| for (AccessControl acl : acls) { |
| String aclUser = acl.get_name(); |
| if (!StringUtils.isEmpty(aclUser) && !aclUsers.add(aclUser)) { |
| LOG.error("'{}' user can't appear more than once in the ACLs", aclUser); |
| duplicateUsers.add(aclUser); |
| } |
| } |
| if (duplicateUsers.size() > 0) { |
| String errorMessage = "user " + Arrays.toString(duplicateUsers.toArray()) |
| + " can't appear more than once in the ACLs for key [" + key +"]."; |
| throw new AuthorizationException(errorMessage); |
| } |
| } |
| |
| private Set<String> constructUserFromPrincipals(Subject who) { |
| Set<String> user = new HashSet<String>(); |
| if (who != null) { |
| for (Principal p : who.getPrincipals()) { |
| user.add(_ptol.toLocal(p)); |
| } |
| } |
| return user; |
| } |
| |
| private boolean isAdmin(Subject who) { |
| Set<String> user = constructUserFromPrincipals(who); |
| for (String u : user) { |
| if (_admins.contains(u)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private boolean isReadOperation(int operation) { |
| if (operation == 1) { |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean isSupervisor(Subject who, int operation) { |
| Set<String> user = constructUserFromPrincipals(who); |
| if (isReadOperation(operation)) { |
| for (String u : user) { |
| if (_supervisors.contains(u)) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| private boolean isNimbus(Subject who) { |
| Set<Principal> principals; |
| boolean isNimbusInstance = false; |
| if (who != null) { |
| principals = who.getPrincipals(); |
| for (Principal principal : principals) { |
| if (principal instanceof NimbusPrincipal) { |
| isNimbusInstance = true; |
| } |
| } |
| } |
| return isNimbusInstance; |
| } |
| |
| public boolean checkForValidUsers(Subject who, int mask) { |
| return isNimbus(who) || isAdmin(who) || isSupervisor(who,mask); |
| } |
| |
| /** |
| * The user should be able to see the metadata if and only if they have any of READ, WRITE, or ADMIN |
| */ |
| public void validateUserCanReadMeta(List<AccessControl> acl, Subject who, String key) throws AuthorizationException { |
| hasAnyPermissions(acl, (READ|WRITE|ADMIN), who, key); |
| } |
| |
| /** |
| * Validates if the user has any of the permissions |
| * mentioned in the mask. |
| * @param acl ACL for the key. |
| * @param mask mask holds the cumulative value of |
| * READ = 1, WRITE = 2 or ADMIN = 4 permissions. |
| * mask = 1 implies READ privilege. |
| * mask = 5 implies READ and ADMIN privileges. |
| * @param who Is the user against whom the permissions |
| * are validated for a key using the ACL and the mask. |
| * @param key Key used to identify the blob. |
| * @throws AuthorizationException |
| */ |
| public void hasAnyPermissions(List<AccessControl> acl, int mask, Subject who, String key) throws AuthorizationException { |
| if (!doAclValidation) { |
| return; |
| } |
| Set<String> user = constructUserFromPrincipals(who); |
| LOG.debug("user {}", user); |
| if (checkForValidUsers(who, mask)) { |
| return; |
| } |
| for (AccessControl ac : acl) { |
| int allowed = getAllowed(ac, user); |
| LOG.debug(" user: {} allowed: {} key: {}", user, allowed, key); |
| if ((allowed & mask) > 0) { |
| return; |
| } |
| } |
| throw new AuthorizationException( |
| user + " does not have access to " + key); |
| } |
| |
| /** |
| * Validates if the user has at least the set of permissions |
| * mentioned in the mask. |
| * @param acl ACL for the key. |
| * @param mask mask holds the cumulative value of |
| * READ = 1, WRITE = 2 or ADMIN = 4 permissions. |
| * mask = 1 implies READ privilege. |
| * mask = 5 implies READ and ADMIN privileges. |
| * @param who Is the user against whom the permissions |
| * are validated for a key using the ACL and the mask. |
| * @param key Key used to identify the blob. |
| * @throws AuthorizationException |
| */ |
| public void hasPermissions(List<AccessControl> acl, int mask, Subject who, String key) throws AuthorizationException { |
| if (!doAclValidation) { |
| return; |
| } |
| Set<String> user = constructUserFromPrincipals(who); |
| LOG.debug("user {}", user); |
| if (checkForValidUsers(who, mask)) { |
| return; |
| } |
| for (AccessControl ac : acl) { |
| int allowed = getAllowed(ac, user); |
| mask = ~allowed & mask; |
| LOG.debug(" user: {} allowed: {} disallowed: {} key: {}", user, allowed, mask, key); |
| } |
| if (mask == 0) { |
| return; |
| } |
| throw new AuthorizationException( |
| user + " does not have " + namedPerms(mask) + " access to " + key); |
| } |
| |
| public void normalizeSettableBlobMeta(String key, SettableBlobMeta meta, Subject who, int opMask) { |
| meta.set_acl(normalizeSettableACLs(key, meta.get_acl(), who, opMask)); |
| } |
| |
| private String namedPerms(int mask) { |
| StringBuilder b = new StringBuilder(); |
| b.append("["); |
| if ((mask & READ) > 0) { |
| b.append("READ "); |
| } |
| if ((mask & WRITE) > 0) { |
| b.append("WRITE "); |
| } |
| if ((mask & ADMIN) > 0) { |
| b.append("ADMIN "); |
| } |
| b.append("]"); |
| return b.toString(); |
| } |
| |
| private int getAllowed(AccessControl ac, Set<String> users) { |
| switch (ac.get_type()) { |
| case OTHER: |
| return ac.get_access(); |
| case USER: |
| if (users.contains(ac.get_name())) { |
| return ac.get_access(); |
| } |
| return 0; |
| default: |
| return 0; |
| } |
| } |
| |
| private List<AccessControl> removeBadACLs(List<AccessControl> accessControls) { |
| List<AccessControl> resultAcl = new ArrayList<AccessControl>(); |
| for (AccessControl control : accessControls) { |
| if(control.get_type().equals(AccessControlType.OTHER) && (control.get_access() == 0 )) { |
| LOG.debug("Removing invalid blobstore world ACL " + |
| BlobStoreAclHandler.accessControlToString(control)); |
| continue; |
| } |
| resultAcl.add(control); |
| } |
| return resultAcl; |
| } |
| |
| private final List<AccessControl> normalizeSettableACLs(String key, List<AccessControl> acls, Subject who, |
| int opMask) { |
| List<AccessControl> cleanAcls = removeBadACLs(acls); |
| Set<String> userNames = getUserNamesFromSubject(who); |
| for (String user : userNames) { |
| fixACLsForUser(cleanAcls, user, opMask); |
| } |
| fixEmptyNameACLForUsers(cleanAcls, userNames, opMask); |
| if ((who == null || userNames.isEmpty()) && !worldEverything(acls)) { |
| cleanAcls.addAll(BlobStoreAclHandler.WORLD_EVERYTHING); |
| LOG.debug("Access Control for key {} is normalized to world everything {}", key, cleanAcls); |
| if (!acls.isEmpty()) |
| LOG.warn("Access control for blob with key {} is normalized to WORLD_EVERYTHING", key); |
| } |
| return cleanAcls; |
| } |
| |
| private boolean worldEverything(List<AccessControl> acls) { |
| boolean isWorldEverything = false; |
| for (AccessControl acl : acls) { |
| if (acl.get_type() == AccessControlType.OTHER && acl.get_access() == (READ|WRITE|ADMIN)) { |
| isWorldEverything = true; |
| break; |
| } |
| } |
| return isWorldEverything; |
| } |
| |
| private void fixACLsForUser(List<AccessControl> acls, String user, int mask) { |
| boolean foundUserACL = false; |
| List<AccessControl> emptyUserACLs = new ArrayList<>(); |
| |
| for (AccessControl control : acls) { |
| if (control.get_type() == AccessControlType.USER) { |
| if (!control.is_set_name()) { |
| emptyUserACLs.add(control); |
| } else if (control.get_name().equals(user)) { |
| int currentAccess = control.get_access(); |
| if ((currentAccess & mask) != mask) { |
| control.set_access(currentAccess | mask); |
| } |
| foundUserACL = true; |
| } |
| } |
| } |
| |
| // if ACLs have two user ACLs for empty user and principal, discard empty user ACL |
| if (!emptyUserACLs.isEmpty() && foundUserACL) { |
| acls.removeAll(emptyUserACLs); |
| } |
| |
| // add default user ACL when only empty user ACL is not present |
| if (emptyUserACLs.isEmpty() && !foundUserACL) { |
| AccessControl userACL = new AccessControl(); |
| userACL.set_type(AccessControlType.USER); |
| userACL.set_name(user); |
| userACL.set_access(mask); |
| acls.add(userACL); |
| } |
| } |
| |
| private void fixEmptyNameACLForUsers(List<AccessControl> acls, Set<String> users, int mask) { |
| List<AccessControl> aclsToAdd = new ArrayList<>(); |
| List<AccessControl> aclsToRemove = new ArrayList<>(); |
| |
| for (AccessControl control : acls) { |
| if (control.get_type() == AccessControlType.USER && !control.is_set_name()) { |
| aclsToRemove.add(control); |
| |
| int currentAccess = control.get_access(); |
| if ((currentAccess & mask) != mask) { |
| control.set_access(currentAccess | mask); |
| } |
| |
| for (String user : users) { |
| AccessControl copiedControl = new AccessControl(control); |
| copiedControl.set_name(user); |
| aclsToAdd.add(copiedControl); |
| } |
| } |
| } |
| |
| acls.removeAll(aclsToRemove); |
| acls.addAll(aclsToAdd); |
| } |
| |
| private Set<String> getUserNamesFromSubject(Subject who) { |
| Set<String> user = new HashSet<String>(); |
| if (who != null) { |
| for(Principal p: who.getPrincipals()) { |
| user.add(_ptol.toLocal(p)); |
| } |
| } |
| return user; |
| } |
| } |