blob: f507734f00fa08238bbab931f939759df5837242 [file] [log] [blame]
/*
* 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.hadoop.ozone.om;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_MULTITENANCY_RANGER_SYNC_INTERVAL;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_MULTITENANCY_RANGER_SYNC_INTERVAL_DEFAULT;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_MULTITENANCY_RANGER_SYNC_TIMEOUT;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_MULTITENANCY_RANGER_SYNC_TIMEOUT_DEFAULT;
import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INTERNAL_ERROR;
import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_ACCESS_ID;
import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.TENANT_AUTHORIZER_ERROR;
import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.TENANT_NOT_FOUND;
import static org.apache.hadoop.ozone.om.multitenant.AccessPolicy.AccessGrantType.ALLOW;
import static org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType.ALL;
import static org.apache.hadoop.ozone.security.acl.OzoneObj.ResourceType.KEY;
import static org.apache.hadoop.ozone.security.acl.OzoneObj.StoreType.OZONE;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.hdds.utils.db.Table;
import org.apache.hadoop.hdds.utils.db.Table.KeyValue;
import org.apache.hadoop.hdds.utils.db.TableIterator;
import org.apache.hadoop.ipc.ProtobufRpcEngine;
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.helpers.OmDBAccessIdInfo;
import org.apache.hadoop.ozone.om.helpers.OmDBTenantState;
import org.apache.hadoop.ozone.om.helpers.OmDBUserPrincipalInfo;
import org.apache.hadoop.ozone.om.helpers.TenantUserList;
import org.apache.hadoop.ozone.om.multitenant.AccessPolicy;
import org.apache.hadoop.ozone.om.multitenant.AuthorizerLock;
import org.apache.hadoop.ozone.om.multitenant.AuthorizerLockImpl;
import org.apache.hadoop.ozone.om.multitenant.BucketNameSpace;
import org.apache.hadoop.ozone.om.multitenant.CachedTenantState;
import org.apache.hadoop.ozone.om.multitenant.CachedTenantState.CachedAccessIdInfo;
import org.apache.hadoop.ozone.om.multitenant.InMemoryMultiTenantAccessController;
import org.apache.hadoop.ozone.om.multitenant.MultiTenantAccessController;
import org.apache.hadoop.ozone.om.multitenant.MultiTenantAccessController.Policy;
import org.apache.hadoop.ozone.om.multitenant.MultiTenantAccessController.Role;
import org.apache.hadoop.ozone.om.multitenant.OMRangerBGSyncService;
import org.apache.hadoop.ozone.om.multitenant.OzoneOwnerPrincipal;
import org.apache.hadoop.ozone.om.multitenant.OzoneTenant;
import org.apache.hadoop.ozone.om.multitenant.RangerAccessPolicy;
import org.apache.hadoop.ozone.om.multitenant.RangerClientMultiTenantAccessController;
import org.apache.hadoop.ozone.om.multitenant.Tenant;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.UserAccessIdInfo;
import org.apache.hadoop.ozone.security.acl.OzoneObj;
import org.apache.hadoop.ozone.security.acl.OzoneObjInfo;
import org.apache.hadoop.security.UserGroupInformation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Preconditions;
/**
* Implements OMMultiTenantManager.
*/
public class OMMultiTenantManagerImpl implements OMMultiTenantManager {
private static final Logger LOG =
LoggerFactory.getLogger(OMMultiTenantManagerImpl.class);
// Internal flag to skip Ranger communication,
// and to skip Ozone config validation for S3 multi-tenancy
public static final String OZONE_OM_TENANT_DEV_SKIP_RANGER =
"ozone.om.tenant.dev.skip.ranger";
private final OzoneManager ozoneManager;
private final OMMetadataManager omMetadataManager;
private final OzoneConfiguration conf;
// tenantCache: tenantId -> CachedTenantState
private final Map<String, CachedTenantState> tenantCache;
private final ReentrantReadWriteLock tenantCacheLock;
private final OMRangerBGSyncService omRangerBGSyncService;
private final MultiTenantAccessController accessController;
private final AuthorizerLock authorizerLock;
/**
* Authorizer operations. Meant to be called in tenant preExecute.
*/
private final TenantOp authorizerOp;
/**
* Cache operations. Meant to be called in tenant validateAndUpdateCache.
*/
private final TenantOp cacheOp;
public OMMultiTenantManagerImpl(OzoneManager ozoneManager,
OzoneConfiguration conf)
throws IOException {
this.conf = conf;
this.ozoneManager = ozoneManager;
this.omMetadataManager = ozoneManager.getMetadataManager();
this.tenantCache = new ConcurrentHashMap<>();
this.tenantCacheLock = new ReentrantReadWriteLock();
this.authorizerLock = new AuthorizerLockImpl();
loadTenantCacheFromDB();
boolean devSkipRanger = conf.getBoolean(
OZONE_OM_TENANT_DEV_SKIP_RANGER, false);
if (devSkipRanger) {
this.accessController = new InMemoryMultiTenantAccessController();
} else {
this.accessController = new RangerClientMultiTenantAccessController(conf);
}
cacheOp = new CacheOp(tenantCache, tenantCacheLock);
authorizerOp = new AuthorizerOp(accessController,
tenantCache, tenantCacheLock);
// Define the internal time unit for the config
final TimeUnit internalTimeUnit = TimeUnit.SECONDS;
// Get the interval in internal time unit
long rangerSyncInterval = ozoneManager.getConfiguration().getTimeDuration(
OZONE_OM_MULTITENANCY_RANGER_SYNC_INTERVAL,
OZONE_OM_MULTITENANCY_RANGER_SYNC_INTERVAL_DEFAULT.getDuration(),
OZONE_OM_MULTITENANCY_RANGER_SYNC_INTERVAL_DEFAULT.getUnit(),
internalTimeUnit);
// Get the timeout in internal time unit
long rangerSyncTimeout = ozoneManager.getConfiguration().getTimeDuration(
OZONE_OM_MULTITENANCY_RANGER_SYNC_TIMEOUT,
OZONE_OM_MULTITENANCY_RANGER_SYNC_TIMEOUT_DEFAULT.getDuration(),
OZONE_OM_MULTITENANCY_RANGER_SYNC_TIMEOUT_DEFAULT.getUnit(),
internalTimeUnit);
// Initialize the Ranger Sync Thread
omRangerBGSyncService = new OMRangerBGSyncService(ozoneManager, this,
accessController, rangerSyncInterval, internalTimeUnit,
rangerSyncTimeout);
// Start the Ranger Sync Thread
this.start();
}
public OMRangerBGSyncService getOMRangerBGSyncService() {
return omRangerBGSyncService;
}
/**
* Start the Ranger policy and role sync thread.
*/
@Override
public void start() throws IOException {
omRangerBGSyncService.start();
}
/**
* Stop the Ranger policy and role sync thread.
*/
@Override
public void stop() throws IOException {
omRangerBGSyncService.shutdown();
}
@Override
public OMMetadataManager getOmMetadataManager() {
return omMetadataManager;
}
@Override
public TenantOp getAuthorizerOp() {
return authorizerOp;
}
@Override
public TenantOp getCacheOp() {
return cacheOp;
}
/**
* Implements tenant authorizer operations.
*/
public class AuthorizerOp implements TenantOp {
private final MultiTenantAccessController accessController;
private final Map<String, CachedTenantState> tenantCache;
private final ReentrantReadWriteLock tenantCacheLock;
AuthorizerOp(MultiTenantAccessController accessController,
Map<String, CachedTenantState> tenantCache,
ReentrantReadWriteLock tenantCacheLock) {
this.accessController = accessController;
this.tenantCache = tenantCache;
this.tenantCacheLock = tenantCacheLock;
}
/**
* Throws if authorizer write lock hasn't been acquired.
*/
private void checkAcquiredAuthorizerWriteLock() throws OMException {
// Check if lock is acquired by the current thread
if (!authorizerLock.isWriteLockHeldByCurrentThread()) {
throw new OMException("Authorizer write lock must have been held "
+ "before calling this", INTERNAL_ERROR);
}
}
/**
* Algorithm
* OM State :
* - Validation (Part of Ratis Request)
* - create volume {Part of RATIS request}
* - Persistence to OM DB {Part of RATIS request}
* Authorizer-plugin(Ranger) State :
* - For every tenant create two user groups
* # GroupTenantAllUsers
* # GroupTenantAllAdmins
*
* - For every tenant create two default policies
* - Note: plugin states are made idempotent. Onus of returning EEXIST is
* part of validation in Ratis-Request. if the groups/policies exist
* with the same name (Due to an earlier failed/success request), in
* plugin, we just update in-memory-map here and return success.
* - The job of cleanup of any half-done authorizer-plugin state is done
* by a background thread.
* Finally :
* - Update all Maps maintained by Multi-Tenant-Manager
* In case of failure :
* - Undo all Ranger State
* - remove updates to the Map
* Locking :
* - Create/Manage Tenant/User operations are control path operations.
* We can do all of this as part of holding a coarse lock and
* synchronize these control path operations.
*
* @param tenantId tenant name
* @param userRoleName user role name
* @param adminRoleName admin role name
* @return Tenant
* @throws IOException
*/
@Override
public void createTenant(
String tenantId, String userRoleName, String adminRoleName)
throws IOException {
checkAcquiredAuthorizerWriteLock();
Tenant tenant = new OzoneTenant(tenantId);
try {
// Create empty admin role first
Role adminRole = new Role.Builder()
.setName(adminRoleName)
.setDescription(OZONE_TENANT_RANGER_ROLE_DESCRIPTION)
.build();
adminRole = accessController.createRole(adminRole);
// Sanity check. Response role name should be equal to the one we sent.
Preconditions.checkState(adminRole.getName().equals(adminRoleName));
tenant.addTenantAccessRole(adminRoleName);
// Then create user role with the admin role as its delegated admin
Role userRole = new Role.Builder()
.setName(userRoleName)
.addRole(adminRoleName, true)
.setDescription(OZONE_TENANT_RANGER_ROLE_DESCRIPTION)
.build();
userRole = accessController.createRole(userRole);
// Sanity check. Response role name should be equal to the one we sent.
Preconditions.checkState(userRole.getName().equals(userRoleName));
tenant.addTenantAccessRole(userRoleName);
BucketNameSpace bucketNameSpace = tenant.getTenantBucketNameSpace();
// Bucket namespace is volume.
// Note at the moment we only support one volume for each tenant.
for (OzoneObj volume : bucketNameSpace.getBucketNameSpaceObjects()) {
String volumeName = volume.getVolumeName();
// Allow Volume List access
Policy volumePolicy =
OMMultiTenantManager.getDefaultVolumeAccessPolicy(
tenantId, volumeName, userRoleName, adminRoleName);
volumePolicy = accessController.createPolicy(volumePolicy);
if (LOG.isDebugEnabled()) {
LOG.debug("Created volume policy: {}", volumePolicy);
}
// TODO: Review if this tenant object is useful.
tenant.addTenantAccessPolicy(volumePolicy.getName());
// Allow Bucket Create within Volume
Policy bucketPolicy =
OMMultiTenantManager.getDefaultBucketAccessPolicy(
tenantId, volumeName, userRoleName);
bucketPolicy = accessController.createPolicy(bucketPolicy);
if (LOG.isDebugEnabled()) {
LOG.debug("Created bucket policy: {}", bucketPolicy);
}
// TODO: Review if this tenant object is useful.
tenant.addTenantAccessPolicy(bucketPolicy.getName());
}
} catch (IOException e) {
// Expect the sync thread to restore the admin role later if op succeeds
throw new OMException(e, TENANT_AUTHORIZER_ERROR);
}
}
@Override
public void deleteTenant(Tenant tenant) throws IOException {
checkAcquiredAuthorizerWriteLock();
LOG.info("Deleting tenant policies and roles from Ranger: {}", tenant);
try {
for (String policyName : tenant.getTenantAccessPolicies()) {
accessController.deletePolicy(policyName);
}
for (String roleName : tenant.getTenantRoles()) {
accessController.deleteRole(roleName);
}
} catch (IOException e) {
// Expect the sync thread to restore the admin role later if op succeeds
throw new OMException(e, TENANT_AUTHORIZER_ERROR);
}
}
/**
* Helper method to check roleId presence in a Role.
*/
private void checkRoleIdExistence(Role role) throws IOException {
if (!role.getId().isPresent()) {
final String errMsg = String.format("Received no role ID in: %s", role);
LOG.error(errMsg);
throw new IOException(errMsg);
}
}
/**
* Algorithm
* Authorizer-plugin(Ranger) State :
* - create User in Ranger DB
* - For every user created
* Add them to # GroupTenantAllUsers
* In case of failure :
* - Undo all Ranger State
* - remove updates to the Map
* Locking :
* - Create/Manage Tenant/User operations are control path operations.
* We can do all of this as part of holding a coarse lock and
* synchronize these control path operations.
*
* @param userPrincipal
* @param tenantId
* @param accessId
* @throws IOException
*/
@Override
public void assignUserToTenant(String userPrincipal,
String tenantId, String accessId) throws IOException {
checkAcquiredAuthorizerWriteLock();
tenantCacheLock.readLock().lock();
try {
final CachedTenantState cachedTenantState = tenantCache.get(tenantId);
Preconditions.checkNotNull(cachedTenantState,
"Cache entry for tenant '" + tenantId + "' does not exist");
final String tenantUserRoleName =
tenantCache.get(tenantId).getTenantUserRoleName();
// Get tenant user role from Ranger
Role userRole = accessController.getRole(tenantUserRoleName);
checkRoleIdExistence(userRole);
final long roleId = userRole.getId().get();
// Sanity check user existence in tenant, but won't throw
if (userRole.getUsersMap().containsKey(userPrincipal)) {
LOG.warn("User '{}' is already assigned to tenant '{}'",
userPrincipal, tenantId);
}
// Add user (not accessId) to the role
userRole = new Role.Builder(userRole)
.addUser(userPrincipal, false)
.build();
// Push updated role to Ranger
userRole = accessController.updateRole(roleId, userRole);
if (LOG.isDebugEnabled()) {
LOG.debug("Updated user role: {}", userRole);
}
} catch (IOException e) {
// If the user name doesn't exist in Ranger, it throws 400 Bad Request
// with message: user with name: USERNAME does not exist
// Expect the sync thread to restore the user role later if op succeeds
throw new OMException(e, TENANT_AUTHORIZER_ERROR);
} finally {
tenantCacheLock.readLock().unlock();
}
}
@Override
public void revokeUserAccessId(String accessId, String tenantId)
throws IOException {
checkAcquiredAuthorizerWriteLock();
tenantCacheLock.readLock().lock();
try {
final OmDBAccessIdInfo omDBAccessIdInfo =
omMetadataManager.getTenantAccessIdTable().get(accessId);
if (omDBAccessIdInfo == null) {
throw new OMException(INVALID_ACCESS_ID);
}
final String tenantIdGot = omDBAccessIdInfo.getTenantId();
Preconditions.checkArgument(tenantIdGot.equals(tenantId));
final String userPrincipal = omDBAccessIdInfo.getUserPrincipal();
final String tenantUserRoleName =
tenantCache.get(tenantId).getTenantUserRoleName();
// Get tenant user role from Ranger
Role userRole = accessController.getRole(tenantUserRoleName);
checkRoleIdExistence(userRole);
final long roleId = userRole.getId().get();
// Sanity check user existence in tenant, but won't throw
if (!userRole.getUsersMap().containsKey(userPrincipal)) {
LOG.warn("User '{}' is not assigned to tenant '{}'",
userPrincipal, tenantId);
}
// Remove user from role
userRole = new Role.Builder(userRole)
.removeUser(userPrincipal)
.build();
// Push updated role to ranger
userRole = accessController.updateRole(roleId, userRole);
if (LOG.isDebugEnabled()) {
LOG.debug("Updated user role: {}", userRole);
}
// Does NOT update tenant cache here
} catch (IOException e) {
// Expect the sync thread to restore the user role later if op succeeds
throw new OMException(e, TENANT_AUTHORIZER_ERROR);
} finally {
tenantCacheLock.readLock().unlock();
}
}
@Override
public void assignTenantAdmin(String accessId, boolean delegated)
throws IOException {
checkAcquiredAuthorizerWriteLock();
tenantCacheLock.readLock().lock();
try {
// tenant name is needed to retrieve role name
final String tenantId = getTenantForAccessIDThrowIfNotFound(accessId);
final CachedTenantState cachedTenantState = tenantCache.get(tenantId);
final String tenantAdminRoleName =
cachedTenantState.getTenantAdminRoleName();
final String userPrincipal = getUserNameGivenAccessId(accessId);
// Get tenant admin role from Ranger
Role adminRole = accessController.getRole(tenantAdminRoleName);
checkRoleIdExistence(adminRole);
// Sanity check user existence in tenant, but won't throw
// TODO: Or throw if user is already in admin role?
if (adminRole.getUsersMap().containsKey(userPrincipal)) {
LOG.warn("User '{}' is already admin in tenant '{}'",
userPrincipal, tenantId);
}
final long roleId = adminRole.getId().get();
// Add user principal (not accessId!) to admin role
adminRole = new Role.Builder(adminRole)
.addUser(userPrincipal, delegated)
.build();
// Push updated role to Ranger
adminRole = accessController.updateRole(roleId, adminRole);
if (LOG.isDebugEnabled()) {
LOG.debug("Updated admin role: {}", adminRole);
}
} catch (IOException e) {
// Expect the sync thread to restore the admin role later if op succeeds
throw new OMException(e, TENANT_AUTHORIZER_ERROR);
} finally {
tenantCacheLock.readLock().unlock();
}
}
@Override
public void revokeTenantAdmin(String accessId)
throws IOException {
checkAcquiredAuthorizerWriteLock();
tenantCacheLock.readLock().lock();
try {
// tenant name is needed to retrieve role name
final String tenantId = getTenantForAccessIDThrowIfNotFound(accessId);
final CachedTenantState cachedTenantState = tenantCache.get(tenantId);
final String tenantAdminRoleName =
cachedTenantState.getTenantAdminRoleName();
final String userPrincipal = getUserNameGivenAccessId(accessId);
// Get tenant admin role from Ranger
Role adminRole = accessController.getRole(tenantAdminRoleName);
checkRoleIdExistence(adminRole);
final long roleId = adminRole.getId().get();
// Sanity check user existence in tenant, but won't throw
// TODO: Or throw if user is not in admin role?
if (!adminRole.getUsersMap().containsKey(userPrincipal)) {
LOG.warn("User '{}' is not admin in tenant '{}'",
userPrincipal, tenantId);
}
// Add user principal (not accessId!) to admin role
adminRole = new Role.Builder(adminRole)
.removeUser(userPrincipal)
.build();
// Push updated role to Ranger
adminRole = accessController.updateRole(roleId, adminRole);
if (LOG.isDebugEnabled()) {
LOG.debug("Updated admin role: {}", adminRole);
}
} catch (IOException e) {
// Expect the sync thread to restore the admin role later if op succeeds
throw new OMException(e, TENANT_AUTHORIZER_ERROR);
} finally {
tenantCacheLock.readLock().unlock();
}
}
}
/**
* Implements tenant cache operations.
*/
public class CacheOp implements TenantOp {
private final Map<String, CachedTenantState> tenantCache;
private final ReentrantReadWriteLock tenantCacheLock;
CacheOp(Map<String, CachedTenantState> tenantCache,
ReentrantReadWriteLock tenantCacheLock) {
this.tenantCache = tenantCache;
this.tenantCacheLock = tenantCacheLock;
}
@Override
public void createTenant(
String tenantId, String userRoleName, String adminRoleName) {
tenantCacheLock.writeLock().lock();
try {
if (tenantCache.containsKey(tenantId)) {
LOG.warn("Cache entry for tenant '{}' already exists, "
+ "will be overwritten", tenantId);
}
// New entry in tenant cache
tenantCache.put(tenantId, new CachedTenantState(
tenantId, userRoleName, adminRoleName));
} finally {
tenantCacheLock.writeLock().unlock();
}
}
@Override
public void deleteTenant(Tenant tenant) throws IOException {
final String tenantId = tenant.getTenantName();
tenantCacheLock.writeLock().lock();
try {
if (tenantCache.containsKey(tenantId)) {
LOG.info("Removing tenant from in-memory cache: {}", tenantId);
tenantCache.remove(tenantId);
} else {
throw new OMException("Tenant does not exist in cache: " + tenantId,
INTERNAL_ERROR);
}
} finally {
tenantCacheLock.writeLock().unlock();
}
}
@Override
public void assignUserToTenant(String userPrincipal,
String tenantId, String accessId) {
final CachedAccessIdInfo cacheEntry =
new CachedAccessIdInfo(userPrincipal, false);
tenantCacheLock.writeLock().lock();
try {
final CachedTenantState cachedTenantState = tenantCache.get(tenantId);
Preconditions.checkNotNull(cachedTenantState,
"Cache entry for tenant '" + tenantId + "' does not exist");
LOG.info("Adding to cache: user '{}' accessId '{}' in tenant '{}'",
userPrincipal, accessId, tenantId);
cachedTenantState.getAccessIdInfoMap().put(accessId, cacheEntry);
} finally {
tenantCacheLock.writeLock().unlock();
}
}
@Override
public void revokeUserAccessId(String accessId, String tenantId)
throws IOException {
tenantCacheLock.writeLock().lock();
try {
LOG.info("Removing from cache: accessId '{}' in tenant '{}'",
accessId, tenantId);
if (!tenantCache.get(tenantId).getAccessIdInfoMap()
.containsKey(accessId)) {
throw new OMException("accessId '" + accessId + "' doesn't exist "
+ "in tenant cache!", INTERNAL_ERROR);
}
tenantCache.get(tenantId).getAccessIdInfoMap().remove(accessId);
} finally {
tenantCacheLock.writeLock().unlock();
}
}
/**
* This should be called in validateAndUpdateCache after
* the InAuthorizer variant (called in preExecute).
*/
@Override
public void assignTenantAdmin(String accessId, boolean delegated)
throws IOException {
tenantCacheLock.writeLock().lock();
try {
// tenant name is needed to retrieve role name
final String tenantId = getTenantForAccessIDThrowIfNotFound(accessId);
final CachedTenantState cachedTenantState = tenantCache.get(tenantId);
LOG.info("Updating cache: accessId '{}' isAdmin '{}' isDelegated '{}'",
accessId, true, delegated);
// Update cache. Note: tenant cache does not store delegated flag yet.
cachedTenantState.getAccessIdInfoMap().get(accessId).setIsAdmin(true);
} finally {
tenantCacheLock.writeLock().unlock();
}
}
@Override
public void revokeTenantAdmin(String accessId) throws IOException {
tenantCacheLock.writeLock().lock();
try {
// tenant name is needed to retrieve role name
final String tenantId = getTenantForAccessIDThrowIfNotFound(accessId);
final CachedTenantState cachedTenantState = tenantCache.get(tenantId);
LOG.info("Updating cache: accessId '{}' isAdmin '{}' isDelegated '{}'",
accessId, false, false);
// Update cache
cachedTenantState.getAccessIdInfoMap().get(accessId).setIsAdmin(false);
} finally {
tenantCacheLock.writeLock().unlock();
}
}
}
@Override
public String getUserNameGivenAccessId(String accessId) {
Preconditions.checkNotNull(accessId);
tenantCacheLock.readLock().lock();
try {
OmDBAccessIdInfo omDBAccessIdInfo =
omMetadataManager.getTenantAccessIdTable().get(accessId);
if (omDBAccessIdInfo != null) {
String userName = omDBAccessIdInfo.getUserPrincipal();
LOG.debug("Username for accessId {} = {}", accessId, userName);
return userName;
}
} catch (IOException ioEx) {
LOG.error("Unexpected error while obtaining DB Access Info for {}",
accessId, ioEx);
} finally {
tenantCacheLock.readLock().unlock();
}
return null;
}
/**
* {@inheritDoc}
*/
public boolean isTenantAdmin(UserGroupInformation callerUgi,
String tenantId, boolean delegated) {
if (callerUgi == null) {
return false;
} else {
return isTenantAdmin(callerUgi.getShortUserName(), tenantId, delegated)
|| isTenantAdmin(callerUgi.getUserName(), tenantId, delegated)
|| ozoneManager.isAdmin(callerUgi);
}
}
/**
* Internal isTenantAdmin method that takes a username String instead of UGI.
*/
private boolean isTenantAdmin(String username, String tenantId,
boolean delegated) {
if (StringUtils.isEmpty(username) || StringUtils.isEmpty(tenantId)) {
return false;
}
try {
final OmDBUserPrincipalInfo principalInfo =
omMetadataManager.getPrincipalToAccessIdsTable().get(username);
if (principalInfo == null) {
// The user is not assigned to any tenant
return false;
}
// Find accessId assigned to the specified tenant
for (final String accessId : principalInfo.getAccessIds()) {
final OmDBAccessIdInfo accessIdInfo =
omMetadataManager.getTenantAccessIdTable().get(accessId);
// accessIdInfo could be null since we may not have a lock on the tenant
if (accessIdInfo == null) {
return false;
}
if (tenantId.equals(accessIdInfo.getTenantId())) {
if (!delegated) {
return accessIdInfo.getIsAdmin();
} else {
return accessIdInfo.getIsAdmin()
&& accessIdInfo.getIsDelegatedAdmin();
}
}
}
} catch (IOException e) {
LOG.error("Error while retrieving value for key '" + username
+ "' in PrincipalToAccessIdsTable");
}
return false;
}
@Override
public TenantUserList listUsersInTenant(String tenantID, String prefix)
throws IOException {
List<UserAccessIdInfo> userAccessIds = new ArrayList<>();
tenantCacheLock.readLock().lock();
try {
if (!omMetadataManager.getTenantStateTable().isExist(tenantID)) {
throw new IOException("Tenant '" + tenantID + "' not found!");
}
CachedTenantState cachedTenantState = tenantCache.get(tenantID);
if (cachedTenantState == null) {
throw new IOException("Inconsistent in memory Tenant cache '" + tenantID
+ "' not found in cache, but present in OM DB!");
}
cachedTenantState.getAccessIdInfoMap().entrySet().stream()
.filter(
// Include if user principal matches the prefix
k -> StringUtils.isEmpty(prefix) ||
k.getValue().getUserPrincipal().startsWith(prefix))
.forEach(k -> {
final String accessId = k.getKey();
final CachedAccessIdInfo cacheEntry = k.getValue();
userAccessIds.add(
UserAccessIdInfo.newBuilder()
.setUserPrincipal(cacheEntry.getUserPrincipal())
.setAccessId(accessId)
.build());
});
} finally {
tenantCacheLock.readLock().unlock();
}
return new TenantUserList(userAccessIds);
}
@Override
public Optional<String> getTenantForAccessID(String accessID)
throws IOException {
OmDBAccessIdInfo omDBAccessIdInfo =
omMetadataManager.getTenantAccessIdTable().get(accessID);
if (omDBAccessIdInfo == null) {
return Optional.absent();
}
return Optional.of(omDBAccessIdInfo.getTenantId());
}
/**
* Internal helper method that gets tenant name from accessId.
* Throws if not found.
*/
private String getTenantForAccessIDThrowIfNotFound(String accessId)
throws IOException {
final Optional<String> optionalTenant = getTenantForAccessID(accessId);
if (!optionalTenant.isPresent()) {
throw new OMException("No tenant found for access ID: " + accessId,
INVALID_ACCESS_ID);
}
return optionalTenant.get();
}
// TODO: This policy doesn't seem necessary as the bucket-level policy has
// already granted the key-level access.
// Not sure if that is the intended behavior in Ranger though.
// Still, could add this KeyAccess policy as well in Ranger, doesn't hurt.
private AccessPolicy newDefaultKeyAccessPolicy(String volumeName,
String bucketName) throws IOException {
AccessPolicy policy = new RangerAccessPolicy(
// principal already contains volume name
volumeName + "-KeyAccess");
OzoneObjInfo obj = OzoneObjInfo.Builder.newBuilder()
.setResType(KEY).setStoreType(OZONE).setVolumeName(volumeName)
.setBucketName("*").setKeyName("*").build();
// Bucket owners should have ALL permission on their keys
policy.addAccessPolicyElem(obj, new OzoneOwnerPrincipal(), ALL, ALLOW);
return policy;
}
public OzoneConfiguration getConf() {
return conf;
}
private void loadTenantCacheFromDB() {
// First load each tenant as a key into the cache.
final Table<String, OmDBTenantState> tenantStateTable =
omMetadataManager.getTenantStateTable();
try (TableIterator<String, ? extends KeyValue<String, OmDBTenantState>>
tenantStateTableIter = tenantStateTable.iterator()) {
while (tenantStateTableIter.hasNext()) {
final KeyValue<String, OmDBTenantState> next =
tenantStateTableIter.next();
final String tenantId = next.getKey();
final OmDBTenantState tenantState = next.getValue();
tenantCache.put(tenantId, new CachedTenantState(tenantId,
tenantState.getUserRoleName(), tenantState.getAdminRoleName()));
}
} catch (IOException ex) {
// Do not allow an inconsistent OM to start up.
throw new RuntimeException(
"Error while building tenant state cache from DB.", ex);
}
// Next use the access ID table to fill in membership info for each tenant.
int userCount = 0;
final Table<String, OmDBAccessIdInfo> tenantAccessIdTable =
omMetadataManager.getTenantAccessIdTable();
try (TableIterator<String, ? extends KeyValue<String, OmDBAccessIdInfo>>
accessIdTableIter = tenantAccessIdTable.iterator()) {
while (accessIdTableIter.hasNext()) {
final KeyValue<String, OmDBAccessIdInfo> next =
accessIdTableIter.next();
final String accessId = next.getKey();
final OmDBAccessIdInfo value = next.getValue();
final String tenantId = value.getTenantId();
final String userPrincipal = value.getUserPrincipal();
final boolean isAdmin = value.getIsAdmin();
// If the TenantState doesn't exist, it means the accessId entry is
// orphaned or incorrect, likely metadata inconsistency
CachedTenantState cachedTenantState = tenantCache.get(tenantId);
Preconditions.checkNotNull(cachedTenantState,
"OmDBTenantState should have existed for " + tenantId);
cachedTenantState.getAccessIdInfoMap().put(accessId,
new CachedAccessIdInfo(userPrincipal, isAdmin));
userCount++;
}
LOG.info("Loaded {} tenants and {} tenant users from the database",
tenantCache.size(), userCount);
} catch (IOException ex) {
// Do not allow an inconsistent OM to start up.
throw new RuntimeException(
"Error while building tenant user cache from DB.", ex);
}
}
@Override
public void checkAdmin() throws OMException {
final UserGroupInformation ugi = ProtobufRpcEngine.Server.getRemoteUser();
if (!ozoneManager.isAdmin(ugi)) {
throw new OMException("User '" + ugi.getUserName() + "' or '" +
ugi.getShortUserName() + "' is not an Ozone admin",
OMException.ResultCodes.PERMISSION_DENIED);
}
}
@Override
public void checkTenantAdmin(String tenantId, boolean delegated)
throws OMException {
final UserGroupInformation ugi = ProtobufRpcEngine.Server.getRemoteUser();
if (!isTenantAdmin(ugi, tenantId, delegated)) {
throw new OMException("User '" + ugi.getUserName() +
"' is neither an Ozone admin nor a delegated admin of tenant '" +
tenantId + "'.", OMException.ResultCodes.PERMISSION_DENIED);
}
}
@Override
public void checkTenantExistence(String tenantId) throws OMException {
try {
if (!omMetadataManager.getTenantStateTable().isExist(tenantId)) {
throw new OMException("Tenant '" + tenantId + "' doesn't exist.",
OMException.ResultCodes.TENANT_NOT_FOUND);
}
} catch (IOException ex) {
if (ex instanceof OMException) {
final OMException omEx = (OMException) ex;
if (omEx.getResult().equals(OMException.ResultCodes.TENANT_NOT_FOUND)) {
throw omEx;
}
}
throw new OMException("Error while retrieving OmDBTenantInfo for tenant "
+ "'" + tenantId + "': " + ex.getMessage(),
OMException.ResultCodes.METADATA_ERROR);
}
}
@Override
public String getTenantVolumeName(String tenantId) throws IOException {
// TODO: lock here?
// tenantCacheLock.readLock().lock();
final OmDBTenantState tenantState =
omMetadataManager.getTenantStateTable().get(tenantId);
if (tenantState == null) {
throw new OMException("Tenant '" + tenantId + "' does not exist",
OMException.ResultCodes.TENANT_NOT_FOUND);
}
final String volumeName = tenantState.getBucketNamespaceName();
if (volumeName == null) {
throw new OMException("Volume for tenant '" + tenantId +
"' is not set!", OMException.ResultCodes.VOLUME_NOT_FOUND);
}
return volumeName;
}
@Override
public String getTenantUserRoleName(String tenantId) throws IOException {
tenantCacheLock.readLock().lock();
try {
final CachedTenantState cachedTenantState = tenantCache.get(tenantId);
if (cachedTenantState == null) {
throw new OMException("Tenant not found in cache: " + tenantId,
TENANT_NOT_FOUND);
}
return cachedTenantState.getTenantUserRoleName();
} finally {
tenantCacheLock.readLock().unlock();
}
}
@Override
public String getTenantAdminRoleName(String tenantId) throws IOException {
tenantCacheLock.readLock().lock();
try {
final CachedTenantState cachedTenantState = tenantCache.get(tenantId);
if (cachedTenantState == null) {
throw new OMException("Tenant not found in cache: " + tenantId,
TENANT_NOT_FOUND);
}
return cachedTenantState.getTenantAdminRoleName();
} finally {
tenantCacheLock.readLock().unlock();
}
}
@Override
public Tenant getTenantFromDBById(String tenantId) throws IOException {
// Policy names (not cached at the moment) have to retrieved from OM DB.
// TODO: Store policy names in cache as well if needed.
final OmDBTenantState tenantState =
omMetadataManager.getTenantStateTable().get(tenantId);
if (tenantState == null) {
throw new OMException("Tenant '" + tenantId + "' does not exist",
OMException.ResultCodes.TENANT_NOT_FOUND);
}
final Tenant tenantObj = new OzoneTenant(tenantState.getTenantId());
tenantObj.addTenantAccessPolicy(tenantState.getBucketNamespacePolicyName());
tenantObj.addTenantAccessPolicy(tenantState.getBucketPolicyName());
tenantObj.addTenantAccessRole(tenantState.getUserRoleName());
tenantObj.addTenantAccessRole(tenantState.getAdminRoleName());
return tenantObj;
}
@Override
public boolean isUserAccessIdPrincipalOrTenantAdmin(String accessId,
UserGroupInformation ugi) throws IOException {
final OmDBAccessIdInfo accessIdInfo =
omMetadataManager.getTenantAccessIdTable().get(accessId);
if (accessIdInfo == null) {
// Doesn't have the accessId entry in TenantAccessIdTable.
// Probably came from `ozone s3 getsecret` with older OM.
return false;
}
final String tenantId = accessIdInfo.getTenantId();
// Sanity check
if (tenantId == null) {
throw new OMException("Unexpected error: OmDBAccessIdInfo " +
"tenantId field should not have been null",
OMException.ResultCodes.METADATA_ERROR);
}
final String accessIdPrincipal = accessIdInfo.getUserPrincipal();
// Sanity check
if (accessIdPrincipal == null) {
throw new OMException("Unexpected error: OmDBAccessIdInfo " +
"kerberosPrincipal field should not have been null",
OMException.ResultCodes.METADATA_ERROR);
}
// Check if ugi matches the holder of the accessId
if (ugi.getShortUserName().equals(accessIdPrincipal)) {
return true;
}
// Check if ugi is a tenant admin (or an Ozone cluster admin)
if (isTenantAdmin(ugi, tenantId, false)) {
return true;
}
return false;
}
@Override
public boolean isTenantEmpty(String tenantId) throws IOException {
if (!tenantCache.containsKey(tenantId)) {
throw new OMException("Tenant does not exist for tenantId: " + tenantId,
TENANT_NOT_FOUND);
}
return tenantCache.get(tenantId).isTenantEmpty();
}
@VisibleForTesting
public Map<String, CachedTenantState> getTenantCache() {
return tenantCache;
}
/**
* Generate and return a mapping from roles to a set of user principals from
* tenantCache.
*/
public HashMap<String, HashSet<String>> getAllRolesFromCache() {
final HashMap<String, HashSet<String>> mtRoles = new HashMap<>();
tenantCacheLock.readLock().lock();
try {
// tenantCache: tenantId -> CachedTenantState
for (Map.Entry<String, CachedTenantState> e1 : tenantCache.entrySet()) {
final CachedTenantState cachedTenantState = e1.getValue();
final String userRoleName = cachedTenantState.getTenantUserRoleName();
mtRoles.computeIfAbsent(userRoleName, any -> new HashSet<>());
final String adminRoleName = cachedTenantState.getTenantAdminRoleName();
mtRoles.computeIfAbsent(adminRoleName, any -> new HashSet<>());
final Map<String, CachedAccessIdInfo> accessIdInfoMap =
cachedTenantState.getAccessIdInfoMap();
// accessIdInfoMap: accessId -> CachedAccessIdInfo
for (Map.Entry<String, CachedAccessIdInfo> e2 :
accessIdInfoMap.entrySet()) {
final CachedAccessIdInfo cachedAccessIdInfo = e2.getValue();
final String userPrincipal = cachedAccessIdInfo.getUserPrincipal();
final boolean isAdmin = cachedAccessIdInfo.getIsAdmin();
addUserToMtRoles(mtRoles, userRoleName, userPrincipal);
if (isAdmin) {
addUserToMtRoles(mtRoles, adminRoleName, userPrincipal);
}
}
}
} finally {
tenantCacheLock.readLock().unlock();
}
return mtRoles;
}
/**
* Helper function to add user principal to a role in mtRoles.
*/
private void addUserToMtRoles(HashMap<String, HashSet<String>> mtRoles,
String roleName, String userPrincipal) {
if (!mtRoles.containsKey(roleName)) {
mtRoles.put(roleName, new HashSet<>(
Collections.singletonList(userPrincipal)));
} else {
final HashSet<String> usersInTheRole = mtRoles.get(roleName);
usersInTheRole.add(userPrincipal);
}
}
@Override
public AuthorizerLock getAuthorizerLock() {
return authorizerLock;
}
}