blob: c03a374aba7b989a6b14b6c95957066b6492d2f1 [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.impala.authorization.sentry;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.impala.catalog.Principal;
import org.apache.impala.thrift.TPrincipalType;
import org.apache.sentry.api.service.thrift.SentryPolicyServiceClient;
import org.apache.sentry.api.service.thrift.TSentryGrantOption;
import org.apache.sentry.api.service.thrift.TSentryPrivilege;
import org.apache.sentry.api.service.thrift.TSentryRole;
import org.apache.sentry.core.common.exception.SentryUserException;
import org.apache.sentry.service.thrift.SentryServiceClientFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.impala.analysis.PrivilegeSpec;
import org.apache.impala.authorization.User;
import org.apache.impala.authorization.AuthorizationException;
import org.apache.impala.common.ImpalaException;
import org.apache.impala.common.InternalException;
import org.apache.impala.thrift.TPrivilege;
import org.apache.impala.thrift.TPrivilegeLevel;
import org.apache.impala.thrift.TPrivilegeScope;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
/**
* Wrapper around the SentryService APIs that are used by Impala and Impala tests.
*/
public class SentryPolicyService {
private final static Logger LOG = LoggerFactory.getLogger(SentryPolicyService.class);
private final String ACCESS_DENIED_ERROR_MSG =
"User '%s' does not have privileges to execute: %s";
private final SentryConfig config_;
/**
* Wrapper around a SentryPolicyServiceClient.
* TODO: When SENTRY-296 is resolved we can more easily cache connections instead of
* opening a new connection for each request.
*/
class SentryServiceClient implements AutoCloseable {
private final SentryPolicyServiceClient client_;
/**
* Creates and opens a new Sentry Service thrift client.
*/
public SentryServiceClient() throws InternalException {
client_ = createClient();
}
/**
* Get the underlying SentryPolicyServiceClient.
*/
public SentryPolicyServiceClient get() {
return client_;
}
/**
* Returns this client back to the connection pool. Can be called multiple times.
*/
public void close() throws InternalException {
try {
client_.close();
} catch (Exception e) {
throw new InternalException("Error closing client: ", e);
}
}
/**
* Creates a new client to the SentryService.
*/
private SentryPolicyServiceClient createClient() throws InternalException {
SentryPolicyServiceClient client;
try {
client = SentryServiceClientFactory.create(config_.getConfig());
} catch (Exception e) {
throw new InternalException("Error creating Sentry Service client: ", e);
}
return client;
}
}
public SentryPolicyService(SentryConfig config) {
config_ = config;
}
/**
* Drops a role.
*
* @param requestingUser - The requesting user.
* @param roleName - The role to drop.
* @param ifExists - If true, no error is thrown if the role does not exist.
* @throws ImpalaException - On any error dropping the role.
*/
public void dropRole(User requestingUser, String roleName, boolean ifExists)
throws ImpalaException {
if (LOG.isTraceEnabled()) {
LOG.trace(String.format("Dropping role: %s on behalf of: %s", roleName,
requestingUser.getName()));
}
SentryServiceClient client = new SentryServiceClient();
try {
if (ifExists) {
client.get().dropRoleIfExists(requestingUser.getShortName(), roleName);
} else {
client.get().dropRole(requestingUser.getShortName(), roleName);
}
} catch (Exception e) {
if (SentryUtil.isSentryAccessDenied(e)) {
throw new AuthorizationException(String.format(ACCESS_DENIED_ERROR_MSG,
requestingUser.getName(), "DROP_ROLE"));
}
throw new InternalException("Error dropping role: ", e);
} finally {
client.close();
}
}
/**
* Creates a new role.
*
* @param requestingUser - The requesting user.
* @param roleName - The role to create.
* @param ifNotExists - If true, no error is thrown if the role already exists.
* @throws ImpalaException - On any error creating the role.
*/
public void createRole(User requestingUser, String roleName, boolean ifNotExists)
throws ImpalaException {
if (LOG.isTraceEnabled()) {
LOG.trace(String.format("Creating role: %s on behalf of: %s", roleName,
requestingUser.getName()));
}
SentryServiceClient client = new SentryServiceClient();
try {
client.get().createRole(requestingUser.getShortName(), roleName);
} catch (Exception e) {
if (SentryUtil.isSentryAccessDenied(e)) {
throw new AuthorizationException(String.format(ACCESS_DENIED_ERROR_MSG,
requestingUser.getName(), "CREATE_ROLE"));
}
if (SentryUtil.isSentryAlreadyExists(e)) {
if (ifNotExists) return;
throw new InternalException("Error creating role: " + e.getMessage(), e);
}
throw new InternalException("Error creating role: " + e.getMessage(), e);
} finally {
client.close();
}
}
/**
* Grants a role to a group.
*
* @param requestingUser - The requesting user.
* @param roleName - The role to grant to a group. Role must already exist.
* @param groupName - The group to grant the role to.
* @throws ImpalaException - On any error.
*/
public void grantRoleToGroup(User requestingUser, String roleName, String groupName)
throws ImpalaException {
if (LOG.isTraceEnabled()) {
LOG.trace(String.format("Granting role '%s' to group '%s' on behalf of: %s",
roleName, groupName, requestingUser.getName()));
}
SentryServiceClient client = new SentryServiceClient();
try {
client.get().grantRoleToGroup(requestingUser.getShortName(), groupName, roleName);
} catch (Exception e) {
if (SentryUtil.isSentryAccessDenied(e)) {
throw new AuthorizationException(String.format(ACCESS_DENIED_ERROR_MSG,
requestingUser.getName(), "GRANT_ROLE"));
}
throw new InternalException(
"Error making 'grantRoleToGroup' RPC to Sentry Service: ", e);
} finally {
client.close();
}
}
/**
* Grants a role to the groups.
*
* @param requestingUser - The requesting user.
* @param roleName - The role to grant to the groups. Role must already exist.
* @param groupNames - The groups to grant the role to.
* @throws ImpalaException - On any error.
*/
public void grantRoleToGroups(User requestingUser, String roleName,
Set<String> groupNames) throws ImpalaException {
for (String groupName : groupNames) {
grantRoleToGroup(requestingUser, roleName, groupName);
}
}
/**
* Removes a role from a group.
*
* @param requestingUser - The requesting user.
* @param roleName - The role name to remove.
* @param groupName - The group to remove the role from.
* @throws InternalException - On any error.
*/
public void revokeRoleFromGroup(User requestingUser, String roleName, String groupName)
throws ImpalaException {
if (LOG.isTraceEnabled()) {
LOG.trace(String.format("Revoking role '%s' from group '%s' on behalf of: %s",
roleName, groupName, requestingUser.getName()));
}
SentryServiceClient client = new SentryServiceClient();
try {
client.get().revokeRoleFromGroup(requestingUser.getShortName(),
groupName, roleName);
} catch (Exception e) {
if (SentryUtil.isSentryAccessDenied(e)) {
throw new AuthorizationException(String.format(ACCESS_DENIED_ERROR_MSG,
requestingUser.getName(), "REVOKE_ROLE"));
}
throw new InternalException(
"Error making 'revokeRoleFromGroup' RPC to Sentry Service: ", e);
} finally {
client.close();
}
}
/**
* Grants a privilege to an existing role.
*/
public void grantRolePrivilege(User requestingUser, String roleName,
TPrivilege privilege) throws ImpalaException {
grantRolePrivileges(requestingUser, roleName, Lists.newArrayList(privilege));
}
/**
* Grants privileges to an existing role.
*
* @param requestingUser - The requesting user.
* @param roleName - The role to grant privileges to (case insensitive).
* @param privileges - The privileges to grant.
* @throws ImpalaException - On any error
*/
public void grantRolePrivileges(User requestingUser, String roleName,
List<TPrivilege> privileges) throws ImpalaException {
Preconditions.checkState(!privileges.isEmpty());
TPrivilege privilege = privileges.get(0);
TPrivilegeScope scope = privilege.getScope();
if (LOG.isTraceEnabled()) {
LOG.trace(String.format(
"Granting role '%s' '%s' privilege on '%s' on behalf of: %s",
roleName, privilege.getPrivilege_level().toString(), scope.toString(),
requestingUser.getName()));
}
// Verify that all privileges have the same scope.
for (int i = 1; i < privileges.size(); ++i) {
Preconditions.checkState(privileges.get(i).getScope() == scope, "All the " +
"privileges must have the same scope.");
}
Preconditions.checkState(scope == TPrivilegeScope.COLUMN || privileges.size() == 1,
"Cannot grant multiple " + scope + " privileges with a singe RPC to the " +
"Sentry Service.");
SentryServiceClient client = new SentryServiceClient();
try {
switch (scope) {
case SERVER:
client.get().grantServerPrivilege(requestingUser.getShortName(), roleName,
privilege.getServer_name(), privilege.getPrivilege_level().toString(),
privilege.isHas_grant_opt());
break;
case DATABASE:
client.get().grantDatabasePrivilege(requestingUser.getShortName(), roleName,
privilege.getServer_name(), privilege.getDb_name(),
privilege.getPrivilege_level().toString(),
privilege.isHas_grant_opt());
break;
case TABLE:
client.get().grantTablePrivilege(requestingUser.getShortName(), roleName,
privilege.getServer_name(), privilege.getDb_name(),
privilege.getTable_name(), privilege.getPrivilege_level().toString(),
privilege.isHas_grant_opt());
break;
case COLUMN:
client.get().grantColumnsPrivileges(requestingUser.getShortName(), roleName,
privilege.getServer_name(), privilege.getDb_name(),
privilege.getTable_name(), getColumnNames(privileges),
privilege.getPrivilege_level().toString(), privilege.isHas_grant_opt());
break;
case URI:
client.get().grantURIPrivilege(requestingUser.getShortName(),
roleName, privilege.getServer_name(), privilege.getUri(),
privilege.isHas_grant_opt());
break;
}
} catch (Exception e) {
if (SentryUtil.isSentryAccessDenied(e)) {
throw new AuthorizationException(String.format(ACCESS_DENIED_ERROR_MSG,
requestingUser.getName(), "GRANT_PRIVILEGE"));
}
throw new InternalException(
"Error making 'grantPrivilege*' RPC to Sentry Service: ", e);
} finally {
client.close();
}
}
/**
* Revokes privileges from an existing role.
*
* @param requestingUser - The requesting user.
* @param roleName - The role to revoke privileges from (case insensitive).
* @param privileges - The privileges to revoke.
* @throws ImpalaException - On any error
*/
public void revokeRolePrivileges(User requestingUser, String roleName,
List<TPrivilege> privileges) throws ImpalaException {
Preconditions.checkState(!privileges.isEmpty());
TPrivilege privilege = privileges.get(0);
TPrivilegeScope scope = privilege.getScope();
if (LOG.isTraceEnabled()) {
LOG.trace(String.format("Revoking from role '%s' '%s' privilege on '%s' on " +
"behalf of: %s", roleName, privilege.getPrivilege_level().toString(),
scope.toString(), requestingUser.getName()));
}
// Verify that all privileges have the same scope.
for (int i = 1; i < privileges.size(); ++i) {
Preconditions.checkState(privileges.get(i).getScope() == scope, "All the " +
"privileges must have the same scope.");
}
Preconditions.checkState(scope == TPrivilegeScope.COLUMN || privileges.size() == 1,
"Cannot revoke multiple " + scope + " privileges with a singe RPC to the " +
"Sentry Service.");
SentryServiceClient client = new SentryServiceClient();
try {
switch (scope) {
case SERVER:
client.get().revokeServerPrivilege(requestingUser.getShortName(), roleName,
privilege.getServer_name(), privilege.getPrivilege_level().toString(),
/* grant option */ null);
break;
case DATABASE:
client.get().revokeDatabasePrivilege(requestingUser.getShortName(), roleName,
privilege.getServer_name(), privilege.getDb_name(),
privilege.getPrivilege_level().toString(), null);
break;
case TABLE:
client.get().revokeTablePrivilege(requestingUser.getShortName(), roleName,
privilege.getServer_name(), privilege.getDb_name(),
privilege.getTable_name(), privilege.getPrivilege_level().toString(),
null);
break;
case COLUMN:
client.get().revokeColumnsPrivilege(requestingUser.getShortName(), roleName,
privilege.getServer_name(), privilege.getDb_name(),
privilege.getTable_name(), getColumnNames(privileges),
privilege.getPrivilege_level().toString(), null);
break;
case URI:
client.get().revokeURIPrivilege(requestingUser.getShortName(),
roleName, privilege.getServer_name(), privilege.getUri(),
null);
break;
}
} catch (Exception e) {
if (SentryUtil.isSentryAccessDenied(e)) {
throw new AuthorizationException(String.format(ACCESS_DENIED_ERROR_MSG,
requestingUser.getName(), "REVOKE_PRIVILEGE"));
}
throw new InternalException(
"Error making 'revokePrivilege*' RPC to Sentry Service: ", e);
} finally {
client.close();
}
}
/**
* Returns the column names referenced in a list of column-level privileges.
* Verifies that all column-level privileges refer to the same table.
*/
private List<String> getColumnNames(List<TPrivilege> privileges) {
List<String> columnNames = Lists.newArrayList();
String tablePath = PrivilegeSpec.getTablePath(privileges.get(0));
columnNames.add(privileges.get(0).getColumn_name());
// Collect all column names and verify that they belong to the same table.
for (int i = 1; i < privileges.size(); ++i) {
TPrivilege privilege = privileges.get(i);
Preconditions.checkState(tablePath.equals(PrivilegeSpec.getTablePath(privilege))
&& privilege.getScope() == TPrivilegeScope.COLUMN);
columnNames.add(privileges.get(i).getColumn_name());
}
return columnNames;
}
/**
* Lists all roles.
*/
public List<TSentryRole> listAllRoles(User requestingUser) throws ImpalaException {
SentryServiceClient client = new SentryServiceClient();
try {
return Lists.newArrayList(client.get().listAllRoles(requestingUser.getShortName()));
} catch (Exception e) {
if (SentryUtil.isSentryAccessDenied(e)) {
throw new AuthorizationException(String.format(ACCESS_DENIED_ERROR_MSG,
requestingUser.getName(), "LIST_ROLES"));
}
throw new InternalException("Error making 'listRoles' RPC to Sentry Service: ", e);
} finally {
client.close();
}
}
/**
* Lists all privileges granted to a role.
*/
public List<TSentryPrivilege> listRolePrivileges(User requestingUser, String roleName)
throws ImpalaException {
SentryServiceClient client = new SentryServiceClient();
try {
return Lists.newArrayList(client.get().listAllPrivilegesByRoleName(
requestingUser.getShortName(), roleName));
} catch (Exception e) {
if (SentryUtil.isSentryAccessDenied(e)) {
throw new AuthorizationException(String.format(ACCESS_DENIED_ERROR_MSG,
requestingUser.getName(), "LIST_ROLE_PRIVILEGES"));
}
throw new InternalException("Error making 'listAllPrivilegesByRoleName' RPC to " +
"Sentry Service: ", e);
} finally {
client.close();
}
}
/**
* Returns a map of all roles with their associated privileges.
*/
public Map<String, Set<TSentryPrivilege>> listAllRolesPrivileges(User requestingUser)
throws ImpalaException {
return listAllPrincipalsPrivileges(requestingUser, TPrincipalType.ROLE);
}
/**
* Returns a map of all users with their associated privileges.
*/
public Map<String, Set<TSentryPrivilege>> listAllUsersPrivileges(User requestingUser)
throws ImpalaException {
return listAllPrincipalsPrivileges(requestingUser, TPrincipalType.USER);
}
private Map<String, Set<TSentryPrivilege>> listAllPrincipalsPrivileges(
User requestingUser, TPrincipalType type) throws ImpalaException {
SentryServiceClient client = new SentryServiceClient();
try {
return type == TPrincipalType.ROLE ?
client.get().listAllRolesPrivileges(requestingUser.getShortName()) :
client.get().listAllUsersPrivileges(requestingUser.getShortName());
} catch (Exception e) {
if (SentryUtil.isSentryAccessDenied(e)) {
throw new AuthorizationException(String.format(ACCESS_DENIED_ERROR_MSG,
requestingUser.getName(),
type == TPrincipalType.ROLE ?
"LIST_ALL_ROLES_PRIVILEGES" : "LIST_ALL_USERS_PRIVILEGES"));
}
throw new InternalException(String.format("Error making '%s' RPC to " +
"Sentry Service: ",
type == TPrincipalType.ROLE ?
"listAllRolesPrivileges" :
"listAllUsersPrivileges"), e);
} finally {
client.close();
}
}
/**
* Returns the configuration value for the specified key. Will return an empty string
* if no value is set.
*/
public String getConfigValue(String key) throws ImpalaException {
try (SentryServiceClient client = new SentryServiceClient()) {
return client.get().getConfigValue(key, "");
} catch (SentryUserException e) {
throw new InternalException("Error making 'getConfigValue' RPC to Sentry Service: ",
e);
}
}
/**
* Utility function that converts a TSentryPrivilege to an Impala TPrivilege object.
*/
public static TPrivilege sentryPrivilegeToTPrivilege(TSentryPrivilege sentryPriv,
Principal principal) {
TPrivilege privilege = new TPrivilege();
privilege.setServer_name(sentryPriv.getServerName());
if (sentryPriv.isSetDbName()) privilege.setDb_name(sentryPriv.getDbName());
if (sentryPriv.isSetTableName()) privilege.setTable_name(sentryPriv.getTableName());
if (sentryPriv.isSetColumnName()) {
privilege.setColumn_name(sentryPriv.getColumnName());
}
if (sentryPriv.isSetURI()) privilege.setUri(sentryPriv.getURI());
privilege.setScope(Enum.valueOf(TPrivilegeScope.class,
sentryPriv.getPrivilegeScope().toUpperCase()));
if (sentryPriv.getAction().equals("*")) {
privilege.setPrivilege_level(TPrivilegeLevel.ALL);
} else {
privilege.setPrivilege_level(Enum.valueOf(TPrivilegeLevel.class,
sentryPriv.getAction().toUpperCase()));
}
privilege.setCreate_time_ms(sentryPriv.getCreateTime());
if (sentryPriv.isSetGrantOption() &&
sentryPriv.getGrantOption() == TSentryGrantOption.TRUE) {
privilege.setHas_grant_opt(true);
} else {
privilege.setHas_grant_opt(false);
}
privilege.setPrincipal_id(principal.getId());
privilege.setPrincipal_type(principal.getPrincipalType());
return privilege;
}
/**
* Checks if the given user is a Sentry admin.
*/
public boolean isSentryAdmin(User user)
throws InternalException, SentryUserException {
try (SentryServiceClient client = new SentryServiceClient()) {
return client.get().isAdmin(user.getName());
}
}
}