http://git-wip-us.apache.org/repos/asf/sentry/blob/b97f5c7a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/generic/thrift/SentryGenericPolicyProcessor.java ---------------------------------------------------------------------- diff --git a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/generic/thrift/SentryGenericPolicyProcessor.java b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/generic/thrift/SentryGenericPolicyProcessor.java new file mode 100644 index 0000000..1cc4b1b --- /dev/null +++ b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/generic/thrift/SentryGenericPolicyProcessor.java @@ -0,0 +1,829 @@ +/** + * 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.sentry.api.generic.thrift; + +import static org.apache.sentry.core.common.utils.SentryConstants.AUTHORIZABLE_JOINER; +import static org.apache.sentry.core.common.utils.SentryConstants.KV_JOINER; + +import java.lang.reflect.Constructor; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.hadoop.conf.Configuration; +import org.apache.sentry.api.common.ThriftConstants; +import org.apache.sentry.core.common.exception.SentryUserException; +import org.apache.sentry.core.common.Authorizable; +import org.apache.sentry.core.common.utils.SentryConstants; +import org.apache.sentry.core.common.exception.SentrySiteConfigurationException; +import org.apache.sentry.core.model.db.AccessConstants; +import org.apache.sentry.core.common.utils.KeyValue; +import org.apache.sentry.provider.common.AuthorizationComponent; +import org.apache.sentry.core.common.exception.SentryAccessDeniedException; +import org.apache.sentry.core.common.exception.SentryAlreadyExistsException; +import org.apache.sentry.core.common.exception.SentryInvalidInputException; +import org.apache.sentry.core.common.exception.SentryNoSuchObjectException; +import org.apache.sentry.core.common.exception.SentryThriftAPIMismatchException; +import org.apache.sentry.provider.db.generic.service.persistent.DelegateSentryStore; +import org.apache.sentry.provider.db.generic.service.persistent.PrivilegeObject; +import org.apache.sentry.provider.db.generic.service.persistent.PrivilegeObject.Builder; +import org.apache.sentry.provider.db.generic.service.persistent.SentryStoreLayer; +import org.apache.sentry.provider.db.log.entity.JsonLogEntityFactory; +import org.apache.sentry.provider.db.log.util.Constants; +import org.apache.sentry.provider.db.service.model.MSentryGMPrivilege; +import org.apache.sentry.provider.db.service.model.MSentryRole; +import org.apache.sentry.core.common.utils.PolicyStoreConstants; +import org.apache.sentry.api.service.thrift.SentryPolicyStoreProcessor; +import org.apache.sentry.service.common.ServiceConstants.ServerConfig; +import org.apache.sentry.api.common.Status; +import org.apache.sentry.service.thrift.TSentryResponseStatus; +import org.apache.thrift.TException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; + +public class SentryGenericPolicyProcessor implements SentryGenericPolicyService.Iface { + private static final Logger LOGGER = LoggerFactory.getLogger(SentryGenericPolicyProcessor.class); + private static final Logger AUDIT_LOGGER = LoggerFactory + .getLogger(Constants.AUDIT_LOGGER_NAME_GENERIC); + private final Configuration conf; + private final ImmutableSet<String> adminGroups; + private final SentryStoreLayer store; + private final NotificationHandlerInvoker handerInvoker; + + private static final String ACCESS_DENIAL_MESSAGE = "Access denied to "; + + SentryGenericPolicyProcessor(Configuration conf) throws Exception { + this.store = new DelegateSentryStore(conf); + this.handerInvoker = new NotificationHandlerInvoker(createHandlers(conf)); + this.conf = conf; + adminGroups = ImmutableSet.copyOf((Sets.newHashSet(conf.getStrings( + ServerConfig.ADMIN_GROUPS, new String[]{})))); + } + + @VisibleForTesting + SentryGenericPolicyProcessor(Configuration conf, SentryStoreLayer store) throws Exception { + this.store = store; + this.handerInvoker = new NotificationHandlerInvoker(createHandlers(conf)); + this.conf = conf; + adminGroups = ImmutableSet.copyOf(toTrimmed(Sets.newHashSet(conf.getStrings( + ServerConfig.ADMIN_GROUPS, new String[]{})))); + } + + private void authorize(String requestorUser, Set<String> requestorGroups) + throws SentryAccessDeniedException { + if (!inAdminGroups(requestorGroups)) { + String msg = "User: " + requestorUser + " is part of " + requestorGroups + + " which does not, intersect admin groups " + adminGroups; + LOGGER.warn(msg); + throw new SentryAccessDeniedException(ACCESS_DENIAL_MESSAGE + requestorUser); + } + } + + private Set<String> toTrimmedLower(Set<String> s) { + if (s == null) { + return Collections.emptySet(); + } + Set<String> result = new HashSet<>(s.size()); + for (String v : s) { + result.add(v.trim().toLowerCase()); + } + return result; + } + + private Set<String> toTrimmed(Set<String> s) { + if (s == null) { + return Collections.emptySet(); + } + Set<String> result = new HashSet<>(s.size()); + for (String v : s) { + result.add(v.trim()); + } + return result; + } + + private String toTrimmedLower(String s) { + if (Strings.isNullOrEmpty(s)){ + return ""; + } + return s.trim().toLowerCase(); + } + + private static Set<String> getRequestorGroups(Configuration conf, String userName) throws SentryUserException { + return SentryPolicyStoreProcessor.getGroupsFromUserName(conf, userName); + } + + private boolean inAdminGroups(Set<String> requestorGroups) { + return !Sets.intersection(adminGroups, requestorGroups).isEmpty(); + } + + static List<NotificationHandler> createHandlers(Configuration conf) throws SentrySiteConfigurationException { + + List<NotificationHandler> handlers = Lists.newArrayList(); + Iterable<String> notificationHandlers = Splitter.onPattern("[\\s,]").trimResults() + .omitEmptyStrings().split(conf.get(PolicyStoreConstants.SENTRY_GENERIC_POLICY_NOTIFICATION, "")); + try { + for (String notificationHandler : notificationHandlers) { + handlers.add(createInstance(notificationHandler, conf, NotificationHandler.class)); + } + } catch (Exception e) { + throw new SentrySiteConfigurationException("Create notificationHandlers error: " + e.getMessage(), e); + } + return handlers; + } + + @SuppressWarnings("unchecked") + private static <T> T createInstance(String className, Configuration conf, Class<T> iface) throws Exception { + T result; + try { + Class<?> clazz = Class.forName(className); + if (!iface.isAssignableFrom(clazz)) { + throw new IllegalArgumentException("Class " + clazz + " is not a " + + iface.getName()); + } + Constructor<T> meth = (Constructor<T>)clazz.getDeclaredConstructor(Configuration.class); + meth.setAccessible(true); + result = meth.newInstance(new Object[]{conf}); + } catch (Exception e) { + throw new RuntimeException(e); + } + return result; + } + + private <T> Response<T> requestHandle(RequestHandler<T> handler) { + Response<T> response = new Response<T>(); + try { + response = handler.handle(); + } catch (SentryAccessDeniedException e) { + String msg = "Sentry access denied: " + e.getMessage(); + LOGGER.error(msg, e); + response.status = Status.AccessDenied(e.getMessage(), e); + } catch (SentryAlreadyExistsException e) { + String msg = "Sentry object already exists: " + e.getMessage(); + LOGGER.error(msg, e); + response.status = Status.AlreadyExists(e.getMessage(), e); + } catch (SentryNoSuchObjectException e) { + String msg = "Sentry object doesn't exist: " + e.getMessage(); + LOGGER.error(msg, e); + response.status = Status.NoSuchObject(e.getMessage(), e); + } catch (SentryInvalidInputException e) { + String msg = "Invalid input privilege object: " + e.getMessage(); + LOGGER.error(msg, e); + response.status = Status.InvalidInput(msg, e); + } catch (SentryThriftAPIMismatchException e) { + String msg = "Sentry thrift API mismatch error: " + e.getMessage(); + LOGGER.error(msg, e); + response.status = Status.THRIFT_VERSION_MISMATCH(e.getMessage(), e); + } catch (Exception e) { + String msg = "Unknown error:" + e.getMessage(); + LOGGER.error(msg, e); + response.status = Status.RuntimeError(msg, e); + } + return response; + } + + private PrivilegeObject toPrivilegeObject(TSentryPrivilege tSentryPrivilege) { + Boolean grantOption; + if (tSentryPrivilege.getGrantOption().equals(TSentryGrantOption.TRUE)) { + grantOption = true; + } else if (tSentryPrivilege.getGrantOption().equals(TSentryGrantOption.FALSE)) { + grantOption = false; + } else { + grantOption = null; + } + return new Builder().setComponent(tSentryPrivilege.getComponent()) + .setService(tSentryPrivilege.getServiceName()) + .setAuthorizables(toAuthorizables(tSentryPrivilege.getAuthorizables())) + .setAction(tSentryPrivilege.getAction()) + .withGrantOption(grantOption) + .build(); + } + + private TSentryPrivilege fromPrivilegeObject(PrivilegeObject privilege) { + + TSentryPrivilege tPrivilege = new TSentryPrivilege(privilege.getComponent(), privilege.getService(), + fromAuthorizable(privilege.getAuthorizables()), + privilege.getAction()); + if (privilege.getGrantOption() == null) { + tPrivilege.setGrantOption(TSentryGrantOption.UNSET); + } else if (privilege.getGrantOption()) { + tPrivilege.setGrantOption(TSentryGrantOption.TRUE); + } else { + tPrivilege.setGrantOption(TSentryGrantOption.FALSE); + } + return tPrivilege; + } + + private List<TAuthorizable> fromAuthorizable(List<? extends Authorizable> authorizables) { + List<TAuthorizable> tAuthorizables = Lists.newArrayList(); + for (Authorizable authorizable : authorizables) { + tAuthorizables.add(new TAuthorizable(authorizable.getTypeName(), authorizable.getName())); + } + return tAuthorizables; + } + + private String fromAuthorizableToStr(List<? extends Authorizable> authorizables) { + if (authorizables != null && !authorizables.isEmpty()) { + List<String> privileges = Lists.newArrayList(); + + for (Authorizable authorizable : authorizables) { + + privileges.add(SentryConstants.KV_JOINER.join(authorizable.getTypeName(), + authorizable.getName())); + } + + return SentryConstants.AUTHORIZABLE_JOINER.join(privileges); + } else { + return ""; + } + } + + private List<? extends Authorizable> toAuthorizables(List<TAuthorizable> tAuthorizables) { + List<Authorizable> authorizables = Lists.newArrayList(); + if (tAuthorizables == null) { + return authorizables; + } + for (final TAuthorizable tAuthorizable : tAuthorizables) { + authorizables.add(new Authorizable() { + @Override + public String getTypeName() { + return tAuthorizable.getType(); + } + @Override + public String getName() { + return tAuthorizable.getName(); + } + }); + } + return authorizables; + } + + private List<? extends Authorizable> toAuthorizables(String privilegeStr) { + List<Authorizable> authorizables = Lists.newArrayList(); + if (privilegeStr == null) { + return authorizables; + } + + for (String authorizable : SentryConstants.AUTHORIZABLE_SPLITTER.split(privilegeStr)) { + KeyValue tempKV = new KeyValue(authorizable); + final String key = tempKV.getKey(); + final String value = tempKV.getValue(); + + authorizables.add(new Authorizable() { + @Override + public String getTypeName() { + return key; + } + + @Override + public String getName() { + return value; + } + }); + } + + return authorizables; + } + + // Construct the role to set of privileges mapping based on the + // MSentryGMPrivilege information. + private TSentryPrivilegeMap toTSentryPrivilegeMap(Set<MSentryGMPrivilege> mPrivileges) { + + // Mapping of <Role, Set<Privilege>>. + Map<String, Set<TSentryPrivilege>> tPrivilegeMap = Maps.newTreeMap(); + + for (MSentryGMPrivilege mPrivilege : mPrivileges) { + for (MSentryRole role : mPrivilege.getRoles()) { + + TSentryPrivilege tPrivilege = toTSentryPrivilege(mPrivilege); + + if (tPrivilegeMap.containsKey(role.getRoleName())) { + tPrivilegeMap.get(role.getRoleName()).add(tPrivilege); + } else { + Set<TSentryPrivilege> tPrivilegeSet = Sets.newTreeSet(); + tPrivilegeSet.add(tPrivilege); + tPrivilegeMap.put(role.getRoleName(), tPrivilegeSet); + } + } + } + + return new TSentryPrivilegeMap(tPrivilegeMap); + } + + // Construct TSentryPrivilege based on MSentryGMPrivilege information. + private TSentryPrivilege toTSentryPrivilege(MSentryGMPrivilege mPrivilege) { + + TSentryPrivilege tPrivilege = new TSentryPrivilege(mPrivilege.getComponentName(), + mPrivilege.getServiceName(), fromAuthorizable(mPrivilege.getAuthorizables()), mPrivilege.getAction()); + + if (mPrivilege.getGrantOption() == null) { + tPrivilege.setGrantOption(TSentryGrantOption.UNSET); + } else if (mPrivilege.getGrantOption()) { + tPrivilege.setGrantOption(TSentryGrantOption.TRUE); + } else { + tPrivilege.setGrantOption(TSentryGrantOption.FALSE); + } + + return tPrivilege; + } + + private Set<String> buildPermissions(Set<PrivilegeObject> privileges) { + Set<String> permissions = Sets.newHashSet(); + for (PrivilegeObject privilege : privileges) { + List<String> hierarchy = Lists.newArrayList(); + if (hasComponentServerPrivilege(privilege.getComponent())) { + hierarchy.add(KV_JOINER.join("server", privilege.getService())); + } + for (Authorizable authorizable : privilege.getAuthorizables()) { + hierarchy.add(KV_JOINER.join(authorizable.getTypeName(),authorizable.getName())); + } + hierarchy.add(KV_JOINER.join("action", privilege.getAction())); + permissions.add(AUTHORIZABLE_JOINER.join(hierarchy)); + } + return permissions; + } + + private boolean hasComponentServerPrivilege(String component) { + //judge the component whether has the server privilege, for example: sqoop has the privilege on the server + return AuthorizationComponent.SQOOP.equalsIgnoreCase(component); + } + + @Override + public TCreateSentryRoleResponse create_sentry_role( + final TCreateSentryRoleRequest request) throws TException { + Response<Void> respose = requestHandle(new RequestHandler<Void>() { + @Override + public Response<Void> handle() throws Exception { + validateClientVersion(request.getProtocol_version()); + authorize(request.getRequestorUserName(), + getRequestorGroups(conf, request.getRequestorUserName())); + store.createRole(request.getComponent(), request.getRoleName(), + request.getRequestorUserName()); + return new Response<Void>(Status.OK()); + } + }); + + TCreateSentryRoleResponse tResponse = new TCreateSentryRoleResponse(respose.status); + if (Status.OK.getCode() == respose.status.getValue()) { + handerInvoker.create_sentry_role(request, tResponse); + } + + try { + AUDIT_LOGGER.info(JsonLogEntityFactory.getInstance() + .createJsonLogEntity(request, tResponse, conf).toJsonFormatLog()); + } catch (Exception e) { + // if any exception, log the exception. + String msg = "Error in creating audit log for create role: " + e.getMessage(); + LOGGER.error(msg, e); + } + return tResponse; + } + + @Override + public TDropSentryRoleResponse drop_sentry_role(final TDropSentryRoleRequest request) + throws TException { + Response<Void> respose = requestHandle(new RequestHandler<Void>() { + @Override + public Response<Void> handle() throws Exception { + validateClientVersion(request.getProtocol_version()); + authorize(request.getRequestorUserName(), + getRequestorGroups(conf, request.getRequestorUserName())); + store.dropRole(request.getComponent(), request.getRoleName(), + request.getRequestorUserName()); + return new Response<Void>(Status.OK()); + } + }); + + TDropSentryRoleResponse tResponse = new TDropSentryRoleResponse(respose.status); + if (Status.OK.getCode() == respose.status.getValue()) { + handerInvoker.drop_sentry_role(request, tResponse); + } + + try { + AUDIT_LOGGER.info(JsonLogEntityFactory.getInstance() + .createJsonLogEntity(request, tResponse, conf).toJsonFormatLog()); + } catch (Exception e) { + // if any exception, log the exception. + String msg = "Error in creating audit log for drop role: " + e.getMessage(); + LOGGER.error(msg, e); + } + return tResponse; + } + + @Override + public TAlterSentryRoleGrantPrivilegeResponse alter_sentry_role_grant_privilege( + final TAlterSentryRoleGrantPrivilegeRequest request) throws TException { + Response<Void> respose = requestHandle(new RequestHandler<Void>() { + @Override + public Response<Void> handle() throws Exception { + validateClientVersion(request.getProtocol_version()); + store.alterRoleGrantPrivilege(request.getComponent(), + request.getRoleName(), + toPrivilegeObject(request.getPrivilege()), + request.getRequestorUserName()); + return new Response<Void>(Status.OK()); + } + }); + + TAlterSentryRoleGrantPrivilegeResponse tResponse = new TAlterSentryRoleGrantPrivilegeResponse(respose.status); + if (Status.OK.getCode() == respose.status.getValue()) { + handerInvoker.alter_sentry_role_grant_privilege(request, tResponse); + } + + try { + AUDIT_LOGGER.info(JsonLogEntityFactory.getInstance() + .createJsonLogEntity(request, tResponse, conf).toJsonFormatLog()); + } catch (Exception e) { + // if any exception, log the exception. + String msg = "Error in creating audit log for grant privilege to role: " + e.getMessage(); + LOGGER.error(msg, e); + } + return tResponse; + } + + @Override + public TAlterSentryRoleRevokePrivilegeResponse alter_sentry_role_revoke_privilege( + final TAlterSentryRoleRevokePrivilegeRequest request) throws TException { + Response<Void> respose = requestHandle(new RequestHandler<Void>() { + @Override + public Response<Void> handle() throws Exception { + validateClientVersion(request.getProtocol_version()); + store.alterRoleRevokePrivilege(request.getComponent(), + request.getRoleName(), + toPrivilegeObject(request.getPrivilege()), + request.getRequestorUserName()); + return new Response<Void>(Status.OK()); + } + }); + + TAlterSentryRoleRevokePrivilegeResponse tResponse = + new TAlterSentryRoleRevokePrivilegeResponse(respose.status); + if (Status.OK.getCode() == respose.status.getValue()) { + handerInvoker.alter_sentry_role_revoke_privilege(request, tResponse); + } + + try { + AUDIT_LOGGER.info(JsonLogEntityFactory.getInstance() + .createJsonLogEntity(request, tResponse, conf).toJsonFormatLog()); + } catch (Exception e) { + // if any exception, log the exception. + String msg = "Error in creating audit log for revoke privilege from role: " + e.getMessage(); + LOGGER.error(msg, e); + } + return tResponse; + } + + @Override + public TAlterSentryRoleAddGroupsResponse alter_sentry_role_add_groups( + final TAlterSentryRoleAddGroupsRequest request) throws TException { + Response<Void> respose = requestHandle(new RequestHandler<Void>() { + @Override + public Response<Void> handle() throws Exception { + validateClientVersion(request.getProtocol_version()); + authorize(request.getRequestorUserName(), + getRequestorGroups(conf, request.getRequestorUserName())); + store.alterRoleAddGroups(request.getComponent(), + request.getRoleName(), + request.getGroups(), + request.getRequestorUserName()); + return new Response<Void>(Status.OK()); + } + }); + + TAlterSentryRoleAddGroupsResponse tResponse = + new TAlterSentryRoleAddGroupsResponse(respose.status); + if (Status.OK.getCode() == respose.status.getValue()) { + handerInvoker.alter_sentry_role_add_groups(request, tResponse); + } + + try { + AUDIT_LOGGER.info(JsonLogEntityFactory.getInstance() + .createJsonLogEntity(request, tResponse, conf).toJsonFormatLog()); + } catch (Exception e) { + // if any exception, log the exception. + String msg = "Error in creating audit log for add role to group: " + e.getMessage(); + LOGGER.error(msg, e); + } + return tResponse; + } + + @Override + public TAlterSentryRoleDeleteGroupsResponse alter_sentry_role_delete_groups( + final TAlterSentryRoleDeleteGroupsRequest request) throws TException { + Response<Void> respose = requestHandle(new RequestHandler<Void>() { + @Override + public Response<Void> handle() throws Exception { + validateClientVersion(request.getProtocol_version()); + authorize(request.getRequestorUserName(), + getRequestorGroups(conf, request.getRequestorUserName())); + store.alterRoleDeleteGroups(request.getComponent(), + request.getRoleName(), + request.getGroups(), + request.getRequestorUserName()); + return new Response<Void>(Status.OK()); + } + }); + + TAlterSentryRoleDeleteGroupsResponse tResponse = + new TAlterSentryRoleDeleteGroupsResponse(respose.status); + if (Status.OK.getCode() == respose.status.getValue()) { + handerInvoker.alter_sentry_role_delete_groups(request, tResponse); + } + + try { + AUDIT_LOGGER.info(JsonLogEntityFactory.getInstance() + .createJsonLogEntity(request, tResponse, conf).toJsonFormatLog()); + } catch (Exception e) { + // if any exception, log the exception. + String msg = "Error in creating audit log for delete role from group: " + + e.getMessage(); + LOGGER.error(msg, e); + } + return tResponse; + } + + @Override + public TListSentryRolesResponse list_sentry_roles_by_group( + final TListSentryRolesRequest request) throws TException { + Response<Set<TSentryRole>> respose = requestHandle(new RequestHandler<Set<TSentryRole>>() { + @Override + public Response<Set<TSentryRole>> handle() throws Exception { + validateClientVersion(request.getProtocol_version()); + Set<String> groups = getRequestorGroups(conf, request.getRequestorUserName()); + if (!AccessConstants.ALL.equalsIgnoreCase(request.getGroupName())) { + boolean admin = inAdminGroups(groups); + //Only admin users can list all roles in the system ( groupname = null) + //Non admin users are only allowed to list only groups which they belong to + if(!admin && (request.getGroupName() == null || !groups.contains(request.getGroupName()))) { + throw new SentryAccessDeniedException(ACCESS_DENIAL_MESSAGE + request.getRequestorUserName()); + } + groups.clear(); + groups.add(request.getGroupName()); + } + + Set<String> roleNames = store.getRolesByGroups(request.getComponent(), groups); + Set<TSentryRole> tSentryRoles = Sets.newHashSet(); + for (String roleName : roleNames) { + Set<String> groupsForRoleName = store.getGroupsByRoles(request.getComponent(), Sets.newHashSet(roleName)); + tSentryRoles.add(new TSentryRole(roleName, groupsForRoleName)); + } + return new Response<Set<TSentryRole>>(Status.OK(), tSentryRoles); + } + }); + TListSentryRolesResponse tResponse = new TListSentryRolesResponse(); + tResponse.setStatus(respose.status); + tResponse.setRoles(respose.content); + return tResponse; + } + + @Override + public TListSentryPrivilegesResponse list_sentry_privileges_by_role( + final TListSentryPrivilegesRequest request) throws TException { + Response<Set<TSentryPrivilege>> respose = requestHandle(new RequestHandler<Set<TSentryPrivilege>>() { + @Override + public Response<Set<TSentryPrivilege>> handle() throws Exception { + validateClientVersion(request.getProtocol_version()); + Set<String> groups = getRequestorGroups(conf, request.getRequestorUserName()); + if (!inAdminGroups(groups)) { + Set<String> roleNamesForGroups = toTrimmedLower(store.getRolesByGroups(request.getComponent(), groups)); + if (!roleNamesForGroups.contains(toTrimmedLower(request.getRoleName()))) { + throw new SentryAccessDeniedException(ACCESS_DENIAL_MESSAGE + request.getRequestorUserName()); + } + } + Set<PrivilegeObject> privileges = store.getPrivilegesByProvider(request.getComponent(), + request.getServiceName(), + Sets.newHashSet(request.getRoleName()), + null, toAuthorizables(request.getAuthorizables())); + Set<TSentryPrivilege> tSentryPrivileges = Sets.newHashSet(); + for (PrivilegeObject privilege : privileges) { + tSentryPrivileges.add(fromPrivilegeObject(privilege)); + } + return new Response<Set<TSentryPrivilege>>(Status.OK(), tSentryPrivileges); + } + }); + TListSentryPrivilegesResponse tResponse = new TListSentryPrivilegesResponse(); + tResponse.setStatus(respose.status); + tResponse.setPrivileges(respose.content); + return tResponse; + } + + @Override + public TListSentryPrivilegesForProviderResponse list_sentry_privileges_for_provider( + final TListSentryPrivilegesForProviderRequest request) throws TException { + Response<Set<String>> respose = requestHandle(new RequestHandler<Set<String>>() { + @Override + public Response<Set<String>> handle() throws Exception { + validateClientVersion(request.getProtocol_version()); + Set<String> activeRoleNames = toTrimmedLower(request.getRoleSet().getRoles()); + Set<String> roleNamesForGroups = store.getRolesByGroups(request.getComponent(), request.getGroups()); + Set<String> rolesToQuery = request.getRoleSet().isAll() ? roleNamesForGroups : Sets.intersection(activeRoleNames, roleNamesForGroups); + Set<PrivilegeObject> privileges = store.getPrivilegesByProvider(request.getComponent(), + request.getServiceName(), + rolesToQuery, null, + toAuthorizables(request.getAuthorizables())); + return new Response<Set<String>>(Status.OK(), buildPermissions(privileges)); + } + }); + TListSentryPrivilegesForProviderResponse tResponse = new TListSentryPrivilegesForProviderResponse(); + tResponse.setStatus(respose.status); + tResponse.setPrivileges(respose.content); + return tResponse; + } + + @Override + public TListSentryPrivilegesByAuthResponse list_sentry_privileges_by_authorizable(TListSentryPrivilegesByAuthRequest request) throws TException { + + TListSentryPrivilegesByAuthResponse response = new TListSentryPrivilegesByAuthResponse(); + Map<String, TSentryPrivilegeMap> authRoleMap = Maps.newHashMap(); + + // Group names are case sensitive. + Set<String> requestedGroups = request.getGroups(); + String subject = request.getRequestorUserName(); + TSentryActiveRoleSet activeRoleSet = request.getRoleSet(); + Set<String> validActiveRoles = Sets.newHashSet(); + + try { + validateClientVersion(request.getProtocol_version()); + Set<String> memberGroups = getRequestorGroups(conf, subject); + + // Disallow non-admin users to lookup groups that + // they are not part of. + if(!inAdminGroups(memberGroups)) { + + if (requestedGroups != null && !requestedGroups.isEmpty()) { + for (String requestedGroup : requestedGroups) { + + // If user doesn't belong to one of the requested groups, + // then raise security exception. + if (!memberGroups.contains(requestedGroup)) { + throw new SentryAccessDeniedException(ACCESS_DENIAL_MESSAGE + subject); + } + } + } else { + // Non-admin's search is limited to its own groups. + requestedGroups = memberGroups; + } + + Set<String> grantedRoles = toTrimmedLower(store.getRolesByGroups(request.getComponent(), requestedGroups)); + + // If activeRoleSet is not null, disallow non-admin to lookup roles that they are not part of. + if (activeRoleSet != null && !activeRoleSet.isAll()) { + + Set<String> activeRoleNames = toTrimmedLower(activeRoleSet.getRoles()); + for (String activeRole : activeRoleNames) { + if (!grantedRoles.contains(activeRole)) { + throw new SentryAccessDeniedException(ACCESS_DENIAL_MESSAGE + + subject); + } + } + + // For non-admin, valid active roles are intersection of active roles and granted roles. + validActiveRoles.addAll(activeRoleSet.isAll() ? grantedRoles : Sets.intersection(activeRoleNames, grantedRoles)); + } else { + // For non-admin, if activeRoleSet is null, valid active roles would be the granted roles. + validActiveRoles.addAll(grantedRoles); + } + } else { + // For admin, if requestedGroups are empty, requested roles will be all roles. + Set<String> requestedRoles = toTrimmedLower(store.getAllRoleNames()); + if (requestedGroups != null && !requestedGroups.isEmpty()) { + requestedRoles = toTrimmedLower(store.getRolesByGroups(request.getComponent(), requestedGroups)); + } + + // If activeRoleSet (which is optional) is not null, valid active role will be intersection + // of active roles and requested roles. Otherwise, valid active roles are the requested roles. + if (activeRoleSet != null && !activeRoleSet.isAll()) { + validActiveRoles.addAll(Sets.intersection(toTrimmedLower(activeRoleSet.getRoles()), requestedRoles)); + } else { + validActiveRoles.addAll(requestedRoles); + } + } + + // If user is not part of any group.. return empty response + if (request.getAuthorizablesSet() != null) { + for (String authorizablesStr : request.getAuthorizablesSet()) { + + List<? extends Authorizable> authorizables = toAuthorizables(authorizablesStr); + Set<MSentryGMPrivilege> sentryPrivileges = store.getPrivilegesByAuthorizable(request.getComponent(), request.getServiceName(), validActiveRoles, authorizables); + authRoleMap.put(fromAuthorizableToStr(authorizables), toTSentryPrivilegeMap(sentryPrivileges)); + } + } + + response.setPrivilegesMapByAuth(authRoleMap); + response.setStatus(Status.OK()); + } catch (SentryAccessDeniedException e) { + LOGGER.error(e.getMessage(), e); + response.setStatus(Status.AccessDenied(e.getMessage(), e)); + } catch (SentryThriftAPIMismatchException e) { + LOGGER.error(e.getMessage(), e); + response.setStatus(Status.THRIFT_VERSION_MISMATCH(e.getMessage(), e)); + } catch (Exception e) { + String msg = "Unknown error for request: " + request + ", message: " + + e.getMessage(); + LOGGER.error(msg, e); + response.setStatus(Status.RuntimeError(msg, e)); + } + + return response; + } + + @Override + public TDropPrivilegesResponse drop_sentry_privilege( + final TDropPrivilegesRequest request) throws TException { + Response<Void> respose = requestHandle(new RequestHandler<Void>() { + @Override + public Response<Void> handle() throws Exception { + validateClientVersion(request.getProtocol_version()); + authorize(request.getRequestorUserName(), + getRequestorGroups(conf, request.getRequestorUserName())); + store.dropPrivilege(request.getComponent(), + toPrivilegeObject(request.getPrivilege()), + request.getRequestorUserName()); + return new Response<Void>(Status.OK()); + } + }); + + TDropPrivilegesResponse tResponse = new TDropPrivilegesResponse(respose.status); + if (Status.OK.getCode() == respose.status.getValue()) { + handerInvoker.drop_sentry_privilege(request, tResponse); + } + return tResponse; + } + + @Override + public TRenamePrivilegesResponse rename_sentry_privilege( + final TRenamePrivilegesRequest request) throws TException { + Response<Void> respose = requestHandle(new RequestHandler<Void>() { + @Override + public Response<Void> handle() throws Exception { + validateClientVersion(request.getProtocol_version()); + authorize(request.getRequestorUserName(), + getRequestorGroups(conf, request.getRequestorUserName())); + store.renamePrivilege(request.getComponent(), request.getServiceName(), + toAuthorizables(request.getOldAuthorizables()), + toAuthorizables(request.getNewAuthorizables()), + request.getRequestorUserName()); + return new Response<Void>(Status.OK()); + } + }); + + TRenamePrivilegesResponse tResponse = new TRenamePrivilegesResponse(respose.status); + if (Status.OK.getCode() == respose.status.getValue()) { + handerInvoker.rename_sentry_privilege(request, tResponse); + } + return tResponse; + } + + private static class Response<T> { + private TSentryResponseStatus status; + private T content; + + Response() { + } + + Response(TSentryResponseStatus status) { + this(status, null); + } + + Response(TSentryResponseStatus status, T content) { + this.status = status; + this.content = content; + } + } + private interface RequestHandler <T>{ + Response<T> handle() throws Exception ; + } + + private static void validateClientVersion(int protocolVersion) throws SentryThriftAPIMismatchException { + if (ThriftConstants.TSENTRY_SERVICE_VERSION_CURRENT != protocolVersion) { + String msg = "Sentry thrift API protocol version mismatch: Client thrift version " + + "is: " + protocolVersion + " , server thrift version " + + "is " + ThriftConstants.TSENTRY_SERVICE_VERSION_CURRENT; + throw new SentryThriftAPIMismatchException(msg); + } + } +}
http://git-wip-us.apache.org/repos/asf/sentry/blob/b97f5c7a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/generic/thrift/SentryGenericPolicyProcessorFactory.java ---------------------------------------------------------------------- diff --git a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/generic/thrift/SentryGenericPolicyProcessorFactory.java b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/generic/thrift/SentryGenericPolicyProcessorFactory.java new file mode 100644 index 0000000..311b020 --- /dev/null +++ b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/generic/thrift/SentryGenericPolicyProcessorFactory.java @@ -0,0 +1,44 @@ +/** + * 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.sentry.api.generic.thrift; + +import org.apache.hadoop.conf.Configuration; +import org.apache.sentry.api.common.ApiConstants.SentryPolicyServiceConstants; +import org.apache.sentry.provider.db.service.persistent.SentryStore; +import org.apache.sentry.service.thrift.ProcessorFactory; +import org.apache.thrift.TMultiplexedProcessor; +import org.apache.thrift.TProcessor; + +public class SentryGenericPolicyProcessorFactory extends ProcessorFactory { + + public SentryGenericPolicyProcessorFactory(Configuration conf) { + super(conf); + } + + @Override + public boolean register(TMultiplexedProcessor multiplexedProcessor, + SentryStore _) throws Exception { + SentryGenericPolicyProcessor processHandler = new SentryGenericPolicyProcessor(conf); + TProcessor processor = new SentryGenericPolicyProcessorWrapper<SentryGenericPolicyService.Iface>( + processHandler); + multiplexedProcessor.registerProcessor( + SentryPolicyServiceConstants.SENTRY_GENERIC_SERVICE_NAME, processor); + return true; + } + +} http://git-wip-us.apache.org/repos/asf/sentry/blob/b97f5c7a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/ConfServlet.java ---------------------------------------------------------------------- diff --git a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/ConfServlet.java b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/ConfServlet.java new file mode 100644 index 0000000..8625487 --- /dev/null +++ b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/ConfServlet.java @@ -0,0 +1,71 @@ +package org.apache.sentry.api.service.thrift; + +/** + * 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. + */ + +import java.io.IOException; +import java.io.Writer; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.hadoop.conf.Configuration; + +import static org.apache.commons.lang.StringEscapeUtils.escapeHtml; + +/** + * Servlet to print out all sentry configuration. + */ +public class ConfServlet extends HttpServlet { + public static final String CONF_CONTEXT_ATTRIBUTE = "sentry.conf"; + public static final String FORMAT_JSON = "json"; + public static final String FORMAT_XML = "xml"; + public static final String FORMAT_PARAM = "format"; + private static final long serialVersionUID = 1L; + + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String format = request.getParameter(FORMAT_PARAM); + if (format == null) { + format = FORMAT_XML; + } + + if (FORMAT_XML.equals(format)) { + response.setContentType("text/xml; charset=utf-8"); + } else if (FORMAT_JSON.equals(format)) { + response.setContentType("application/json; charset=utf-8"); + } + + Configuration conf = (Configuration)getServletContext().getAttribute( + CONF_CONTEXT_ATTRIBUTE); + assert conf != null; + + Writer out = response.getWriter(); + if (FORMAT_JSON.equals(format)) { + Configuration.dumpConfiguration(conf, out); + } else if (FORMAT_XML.equals(format)) { + conf.writeXml(out); + } else { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Bad format: " + escapeHtml(format)); + } + out.close(); + } +} http://git-wip-us.apache.org/repos/asf/sentry/blob/b97f5c7a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/LogLevelServlet.java ---------------------------------------------------------------------- diff --git a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/LogLevelServlet.java b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/LogLevelServlet.java new file mode 100644 index 0000000..af81d6f --- /dev/null +++ b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/LogLevelServlet.java @@ -0,0 +1,122 @@ +/** + * 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 + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * 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.sentry.api.service.thrift; + +import org.apache.log4j.Level; +import org.apache.log4j.LogManager; +import org.apache.log4j.Logger; + +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; + +import static org.apache.commons.lang.StringEscapeUtils.escapeHtml; + +public class LogLevelServlet extends HttpServlet { + private static final String LF = "\n"; + private static final String BR = "<br />"; + private static final String B_BR = "<b>%s</b><br />"; + private static final String FORMS_HEAD = + "<h1>" + "Log Level" + "</h1>" + + LF + BR + "<hr /><h3>Results</h3>" + + LF + " Submitted Log Name: " + B_BR; + private static final String FORMS_CONTENT_GET = + LF + " Effective level: " + B_BR; + private static final String FORMS_CONTENT_SET = + LF + " Submitted Level: " + B_BR + + LF + " Setting Level to %s" + BR + + LF + " Effective level: " + B_BR; + private static final String FORMS_END = + LF + BR + "<hr /><h3>Get / Set</h3>" + + LF + "<form>Log: <input type='text' size='50' name='log' /> " + + "<input type='submit' value='Get Log Level' />" + "</form>" + + LF + "<form>Log: <input type='text' size='50' name='log' /> " + + "Level: <input type='text' name='level' /> " + + "<input type='submit' value='Set Log Level' />" + "</form>"; + private static final String FORMS_GET = FORMS_HEAD + FORMS_CONTENT_GET; + private static final String FORMS_SET = FORMS_HEAD + FORMS_CONTENT_SET; + + /** + * Return parameter on servlet request for the given name + * + * @param request: Servlet request + * @param name: Name of parameter in servlet request + * @return Parameter in servlet request for the given name, return null if can't find parameter. + */ + private String getParameter(ServletRequest request, String name) { + String s = request.getParameter(name); + if (s == null) { + return null; + } + s = s.trim(); + return s.length() == 0 ? null : s; + } + + /** + * Check the validity of the log level. + * @param level: The log level to be checked + * @return + * true: The log level is valid + * false: The log level is invalid + */ + private boolean isLogLevelValid(String level) { + return level.equals(Level.toLevel(level).toString()); + } + + /** + * Parse the class name and log level in the http servlet request. + * If the request contains only class name, return the log level in the response message. + * If the request contains both class name and level, set the log level to the requested level + * and return the setting result in the response message. + */ + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String logName = getParameter(request, "log"); + String level = getParameter(request, "level"); + response.setContentType("text/html;charset=utf-8"); + response.setStatus(HttpServletResponse.SC_OK); + PrintWriter out = response.getWriter(); + + if (logName != null) { + Logger logInstance = LogManager.getLogger(logName); + if (level == null) { + out.write(String.format(FORMS_GET, + escapeHtml(logName), + logInstance.getEffectiveLevel().toString())); + } else if (isLogLevelValid(level)) { + logInstance.setLevel(Level.toLevel(level)); + out.write(String.format(FORMS_SET, + escapeHtml(logName), + escapeHtml(level), + escapeHtml(level), + logInstance.getEffectiveLevel().toString())); + } else { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid log level: " + escapeHtml(level)); + return; + } + } + out.write(FORMS_END); + out.close(); + response.flushBuffer(); + } +} http://git-wip-us.apache.org/repos/asf/sentry/blob/b97f5c7a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/PubSubServlet.java ---------------------------------------------------------------------- diff --git a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/PubSubServlet.java b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/PubSubServlet.java new file mode 100644 index 0000000..8da35f1 --- /dev/null +++ b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/PubSubServlet.java @@ -0,0 +1,128 @@ +/** + * 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 + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * 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.sentry.api.service.thrift; + +import org.apache.sentry.core.common.utils.PubSub; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; + +import static org.apache.commons.lang.StringEscapeUtils.escapeHtml; + +/** + * This servlet facilitates sending {topic, message } tuples to Servlet components + * subscribed to specific topics. + * <p> + * It uses publish-subscribe mechanism implemented by PubSub class. + * The form generated by this servlet consists of the following elements: + * <p> + * a) Topic: pull-down menu of existing topics, i.e. the topics registered with + * PubSub by calling PubSub.subscribe() API. This prevents entering invalid topic. + * <p> + * b) Message: text field for entering a message + * <p> + * c) Submit: button to submit (topic, message) tuple + * <p> + * d) Status: text area printing status of the request or help information. + */ +public class PubSubServlet extends HttpServlet { + + private static final Logger LOGGER = LoggerFactory.getLogger(PubSubServlet.class); + + private static final String FORM_GET = + "<!DOCTYPE html>" + + "<html>" + + "<body>" + + "<form>" + + "<br><br><b>Topic:</b><br><br>" + + "<select name='topic'/>%s</select>" + + "<br><br><b>Message:</b><br><br>" + + "<input type='text' size='50' name='message'/>" + + "<br><br>" + + "<input type='submit' value='Submit'/>" + + "</form>" + + "<br><br><b>Status:</b><br><br>" + + "<textarea rows='4' cols='50'>%s</textarea>" + + "</body>" + + "</html>"; + + /** + * Return parameter on servlet request for the given name + * + * @param request: Servlet request + * @param name: Name of parameter in servlet request + * @return Parameter in servlet request for the given name, return null if can't find parameter. + */ + private static String getParameter(ServletRequest request, String name) { + String s = request.getParameter(name); + if (s == null) { + return null; + } + s = s.trim(); + return s.isEmpty() ? null : s; + } + + /** + * Parse the topic and message values and submit them via PubSub.submit() API. + * Reject request for unknown topic, i.e. topic no one is subscribed to. + */ + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String topic = getParameter(request, "topic"); + String message = getParameter(request, "message"); + response.setContentType("text/html;charset=utf-8"); + response.setStatus(HttpServletResponse.SC_OK); + PrintWriter out = response.getWriter(); + + String msg = "Topic is required, Message is optional.\nValid topics: " + PubSub.getInstance().getTopics(); + if (topic != null) { + LOGGER.info("Submitting topic " + topic + ", message " + message); + try { + PubSub.getInstance().publish(PubSub.Topic.fromString(topic), message); + msg = "Submitted topic " + topic + ", message " + message; + } catch (Exception e) { + msg = "Failed to submit topic " + topic + ", message " + message + " - " + e.getMessage(); + LOGGER.error(msg); + response.sendError(HttpServletResponse.SC_BAD_REQUEST, msg); + return; + } + } + + StringBuilder topics = new StringBuilder(); + for (PubSub.Topic t : PubSub.getInstance().getTopics()) { + topics.append("<option>").append(t.getName()).append("</option>"); + } + + String output = String.format(FORM_GET, topics.toString(), escapeHtml(msg)); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("HTML Page: " + output); + } + out.write(output); + out.close(); + response.flushBuffer(); + } +} http://git-wip-us.apache.org/repos/asf/sentry/blob/b97f5c7a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/SentryAdminServlet.java ---------------------------------------------------------------------- diff --git a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/SentryAdminServlet.java b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/SentryAdminServlet.java new file mode 100644 index 0000000..5dc6cd6 --- /dev/null +++ b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/SentryAdminServlet.java @@ -0,0 +1,132 @@ +/** + * 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.sentry.api.service.thrift; + +import com.google.gson.Gson; +import org.apache.hadoop.conf.Configuration; +import org.apache.sentry.provider.db.service.persistent.SentryStore; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Writer; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * Admin Servlet is only used when SENTRY_WEB_ADMIN_SERVLET_ENABLED is true. + */ +public class SentryAdminServlet extends HttpServlet { + private static final String SHOW_ALL = "/showAll"; + // Here we use the same way as in com.codahale.metrics.servlets.AdminServlet, and just + // use the TEMPLATE as a static html with some links referenced to other debug pages. + private static final String TEMPLATE = "<!DOCTYPE HTML>\n"+ + "<html lang=\"en\">\n"+ + "<head>\n"+ + " <meta charset=\"utf-8\">\n"+ + " <title>Sentry Service Admin</title>\n"+ + " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n"+ + " <meta name=\"description\" content=\"\">\n"+ + " <link href=\"css/bootstrap.min.css\" rel=\"stylesheet\">\n"+ + " <link href=\"css/bootstrap-theme.min.css\" rel=\"stylesheet\">\n"+ + " <link href=\"css/sentry.css\" rel=\"stylesheet\">\n"+ + "</head>\n"+ + "<body>\n"+ + "<nav class=\"navbar navbar-default navbar-fixed-top\">\n"+ + " <div class=\"container\">\n"+ + " <div class=\"navbar-header\">\n"+ + " <a class=\"navbar-brand\" href=\"#\"><img src=\"sentry.png\" alt=\"Sentry Logo\"/></a>\n"+ + " </div>\n"+ + " <div class=\"collapse navbar-collapse\">\n"+ + " <ul class=\"nav navbar-nav\">\n"+ + " <li class=\"active\"><a href=\"#\">Admin</a></li>\n"+ + " <li><a href=\"/metrics?pretty=true\">Metrics</a></li>\n"+ + " <li><a href=\"/threads\">Threads</a></li>\n"+ + " <li><a href=\"/conf\">Configuration</a></li>\n"+ + " <li><a href=\"/admin/showAll\">ShowAllRoles</a></li>\n"+ + " </ul>\n"+ + " </div>\n"+ + " </div>\n"+ + "</nav>\n"+ + "<div class=\"container\">\n"+ + " <ul>\n"+ + " <li><a href=\"/metrics?pretty=true\">Metrics</a></li>\n"+ + " <li><a href=\"/threads\">Threads</a></li>\n"+ + " <li><a href=\"/conf\">Configuration</a></li>\n"+ + " <li><a href=\"/admin/showAll\">ShowAllRoles</a></li>\n"+ + " </ul>\n"+ + "</div>\n"+ + "</body>\n"+ + "</html>"; + + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String uri = request.getPathInfo(); + if(uri != null && !uri.equals("/")) { + if (uri.equals(SHOW_ALL)) { + showAll(response); + } else { + response.sendError(404); + } + } else { + response.setStatus(200); + response.setHeader("Cache-Control", "must-revalidate,no-cache,no-store"); + response.setHeader("Pragma", "no-cache"); + response.setDateHeader("Expires", 0); + response.setContentType("text/html"); + PrintWriter writer = response.getWriter(); + try { + writer.println(TEMPLATE); + } finally { + writer.close(); + } + } + } + + /** + * Print out all the roles and privileges information as json format. + */ + private void showAll(HttpServletResponse response) + throws ServletException, IOException { + Configuration conf = (Configuration)getServletContext().getAttribute( + ConfServlet.CONF_CONTEXT_ATTRIBUTE); + assert conf != null; + + Writer out = response.getWriter(); + try { + SentryStore sentrystore = new SentryStore(conf); + Map<String, Set<TSentryPrivilege>> roleMap = new HashMap<>(); + Set<String> roleSet = sentrystore.getAllRoleNames(); + for (String roleName: roleSet) { + roleMap.put(roleName, sentrystore.getAllTSentryPrivilegesByRoleName(roleName)); + } + String json = new Gson().toJson(roleMap); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + out.write(json); + } catch (Exception e) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage()); + } + out.close(); + } +} http://git-wip-us.apache.org/repos/asf/sentry/blob/b97f5c7a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/SentryAuthFilter.java ---------------------------------------------------------------------- diff --git a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/SentryAuthFilter.java b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/SentryAuthFilter.java new file mode 100644 index 0000000..23121ec --- /dev/null +++ b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/SentryAuthFilter.java @@ -0,0 +1,89 @@ +/** + * 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.sentry.api.service.thrift; + +import java.io.IOException; +import java.util.Enumeration; +import java.util.Properties; +import java.util.Set; + +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.hadoop.security.authentication.server.AuthenticationFilter; +import org.apache.hadoop.util.StringUtils; +import org.apache.sentry.service.common.ServiceConstants.ServerConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.Sets; + +/** + * SentryAuthFilter is a subclass of AuthenticationFilter, + * add authorization: Only allowed users could connect the web server. + */ +public class SentryAuthFilter extends AuthenticationFilter { + + private static final Logger LOG = LoggerFactory.getLogger(SentryAuthFilter.class); + + public static final String ALLOW_WEB_CONNECT_USERS = ServerConfig.SENTRY_WEB_SECURITY_ALLOW_CONNECT_USERS; + + private Set<String> allowUsers; + + @Override + protected void doFilter(FilterChain filterChain, HttpServletRequest request, + HttpServletResponse response) throws IOException, ServletException { + String userName = request.getRemoteUser(); + LOG.debug("Authenticating user: " + userName + " from request."); + if (!allowUsers.contains(userName)) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, + "Unauthorized user status code: " + HttpServletResponse.SC_FORBIDDEN); + throw new ServletException(userName + " is unauthorized. status code: " + HttpServletResponse.SC_FORBIDDEN); + } + super.doFilter(filterChain, request, response); + } + + /** + * Override <code>getConfiguration<code> to get <code>ALLOW_WEB_CONNECT_USERS<code>. + */ + @Override + protected Properties getConfiguration(String configPrefix, FilterConfig filterConfig) throws ServletException { + Properties props = new Properties(); + Enumeration<?> names = filterConfig.getInitParameterNames(); + while (names.hasMoreElements()) { + String name = (String) names.nextElement(); + if (name.startsWith(configPrefix)) { + String value = filterConfig.getInitParameter(name); + if (ALLOW_WEB_CONNECT_USERS.equals(name)) { + allowUsers = parseConnectUsersFromConf(value); + } else { + props.put(name.substring(configPrefix.length()), value); + } + } + } + return props; + } + + private static Set<String> parseConnectUsersFromConf(String value) { + //Removed the logic to convert the allowed users to lower case, as user names need to be case sensitive + return Sets.newHashSet(StringUtils.getStrings(value)); + } +} http://git-wip-us.apache.org/repos/asf/sentry/blob/b97f5c7a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/SentryHealthCheckServletContextListener.java ---------------------------------------------------------------------- diff --git a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/SentryHealthCheckServletContextListener.java b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/SentryHealthCheckServletContextListener.java new file mode 100644 index 0000000..eb11c19 --- /dev/null +++ b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/SentryHealthCheckServletContextListener.java @@ -0,0 +1,35 @@ +/** + * 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.sentry.api.service.thrift; + +import com.codahale.metrics.health.HealthCheckRegistry; +import com.codahale.metrics.servlets.HealthCheckServlet; + +/** + * Use this class's registry to register health checks: Can be some tests which make sure Sentry service is healthy + */ +public class SentryHealthCheckServletContextListener extends HealthCheckServlet.ContextListener { + + //This is just a place holder for health check registry, with out this AdminServlet throws out an error + public static final HealthCheckRegistry HEALTH_CHECK_REGISTRY = new HealthCheckRegistry(); + + @Override + protected HealthCheckRegistry getHealthCheckRegistry() { + return HEALTH_CHECK_REGISTRY; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/sentry/blob/b97f5c7a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/SentryMetrics.java ---------------------------------------------------------------------- diff --git a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/SentryMetrics.java b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/SentryMetrics.java new file mode 100644 index 0000000..80a6343 --- /dev/null +++ b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/SentryMetrics.java @@ -0,0 +1,413 @@ +/* + * 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.sentry.api.service.thrift; + +import com.codahale.metrics.ConsoleReporter; +import com.codahale.metrics.Counter; +import com.codahale.metrics.Gauge; +import com.codahale.metrics.Histogram; +import com.codahale.metrics.JmxReporter; +import com.codahale.metrics.Metric; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.MetricSet; +import com.codahale.metrics.Slf4jReporter; +import com.codahale.metrics.Timer; +import com.codahale.metrics.json.MetricsModule; +import com.codahale.metrics.jvm.BufferPoolMetricSet; +import com.codahale.metrics.jvm.GarbageCollectorMetricSet; +import com.codahale.metrics.jvm.MemoryUsageGaugeSet; +import com.codahale.metrics.jvm.ThreadStatesGaugeSet; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import org.apache.hadoop.conf.Configuration; +import org.apache.sentry.provider.db.service.persistent.SentryStore; +import org.apache.sentry.service.thrift.SentryService; +import org.apache.sentry.api.common.SentryServiceUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.codahale.metrics.MetricRegistry.name; +import static org.apache.sentry.api.service.thrift.SentryMetricsServletContextListener.METRIC_REGISTRY; +import static org.apache.sentry.service.common.ServiceConstants.ServerConfig; + +/** + * A singleton class which holds metrics related utility functions as well as the list of metrics. + */ +public final class SentryMetrics { + public enum Reporting { + JMX, + CONSOLE, + LOG, + JSON, + } + + private static final Logger LOGGER = LoggerFactory + .getLogger(SentryMetrics.class); + + private static SentryMetrics sentryMetrics = null; + private final AtomicBoolean reportingInitialized = new AtomicBoolean(); + private boolean gaugesAdded = false; + private boolean sentryServiceGaugesAdded = false; + + final Timer createRoleTimer = METRIC_REGISTRY.timer( + name(SentryPolicyStoreProcessor.class, "create-role")); + final Timer dropRoleTimer = METRIC_REGISTRY.timer( + name(SentryPolicyStoreProcessor.class, "drop-role")); + final Timer grantRoleTimer = METRIC_REGISTRY.timer( + name(SentryPolicyStoreProcessor.class, "grant-role")); + final Timer revokeRoleTimer = METRIC_REGISTRY.timer( + name(SentryPolicyStoreProcessor.class, "revoke-role")); + final Timer grantTimer = METRIC_REGISTRY.timer( + name(SentryPolicyStoreProcessor.class, "grant-privilege")); + final Timer revokeTimer = METRIC_REGISTRY.timer( + name(SentryPolicyStoreProcessor.class, "revoke-privilege")); + + final Timer dropPrivilegeTimer = METRIC_REGISTRY.timer( + name(SentryPolicyStoreProcessor.class, "drop-privilege")); + final Timer renamePrivilegeTimer = METRIC_REGISTRY.timer( + name(SentryPolicyStoreProcessor.class, "rename-privilege")); + + final Timer listRolesByGroupTimer = METRIC_REGISTRY.timer( + name(SentryPolicyStoreProcessor.class, "list-roles-by-group")); + final Timer listPrivilegesByRoleTimer = METRIC_REGISTRY.timer( + name(SentryPolicyStoreProcessor.class, "list-privileges-by-role")); + final Timer listPrivilegesForProviderTimer = METRIC_REGISTRY.timer( + name(SentryPolicyStoreProcessor.class, "list-privileges-for-provider")); + final Timer listPrivilegesByAuthorizableTimer = METRIC_REGISTRY.timer( + name(SentryPolicyStoreProcessor.class, "list-privileges-by-authorizable")); + + /** + * Return a Timer with name. + */ + public Timer getTimer(String name) { + return METRIC_REGISTRY.timer(name); + } + + /** + * Return a Histogram with name. + */ + public Histogram getHistogram(String name) { + return METRIC_REGISTRY.histogram(name); + } + + /** + * Return a Counter with name. + */ + public Counter getCounter(String name) { + return METRIC_REGISTRY.counter(name); + } + + private SentryMetrics() { + registerMetricSet("gc", new GarbageCollectorMetricSet(), METRIC_REGISTRY); + registerMetricSet("buffers", + new BufferPoolMetricSet(ManagementFactory.getPlatformMBeanServer()), + METRIC_REGISTRY); + registerMetricSet("memory", new MemoryUsageGaugeSet(), METRIC_REGISTRY); + registerMetricSet("threads", new ThreadStatesGaugeSet(), METRIC_REGISTRY); + } + + /** + * Get singleton instance. + */ + public static synchronized SentryMetrics getInstance() { + if (sentryMetrics == null) { + sentryMetrics = new SentryMetrics(); + } + return sentryMetrics; + } + + void addSentryStoreGauges(SentryStore sentryStore) { + if (!gaugesAdded) { + addGauge(SentryStore.class, "role_count", sentryStore.getRoleCountGauge()); + addGauge(SentryStore.class, "privilege_count", + sentryStore.getPrivilegeCountGauge()); + addGauge(SentryStore.class, "group_count", sentryStore.getGroupCountGauge()); + addGauge(SentryStore.class, "hms.waiters", sentryStore.getHMSWaitersCountGauge()); + addGauge(SentryStore.class, "hms.notification.id", + sentryStore.getLastNotificationIdGauge()); + addGauge(SentryStore.class, "hms.snapshot.paths.id", + sentryStore.getLastPathsSnapshotIdGauge()); + addGauge(SentryStore.class, "hms.perm.change.id", + sentryStore.getPermChangeIdGauge()); + addGauge(SentryStore.class, "hms.psth.change.id", + sentryStore.getPathChangeIdGauge()); + gaugesAdded = true; + } + } + + /** + * Add gauges for the SentryService class. + * @param sentryservice + */ + public void addSentryServiceGauges(SentryService sentryservice) { + if (!sentryServiceGaugesAdded) { + addGauge(SentryService.class, "is_active", sentryservice.getIsActiveGauge()); + addGauge(SentryService.class, "activated", sentryservice.getBecomeActiveCount()); + sentryServiceGaugesAdded = true; + } + } + + /** + * Initialize reporters. Only initializes once.<p> + * + * Available reporters: + * <ul> + * <li>console</li> + * <li>log</li> + * <li>jmx</li> + * </ul> + * + * <p><For console reporter configre it to report every + * <em>SENTRY_REPORTER_INTERVAL_SEC</em> seconds. + * + * <p>Method is thread safe. + */ + @SuppressWarnings("squid:S2095") + void initReporting(Configuration conf) { + final String reporter = conf.get(ServerConfig.SENTRY_REPORTER); + if ((reporter == null) || reporter.isEmpty() || reportingInitialized.getAndSet(true)) { + // Nothing to do, just return + return; + } + + final int reportInterval = + conf.getInt(ServerConfig.SENTRY_REPORTER_INTERVAL_SEC, + ServerConfig.SENTRY_REPORTER_INTERVAL_DEFAULT); + + // Get list of configured reporters + Set<String> reporters = new HashSet<>(); + for (String r: reporter.split(",")) { + reporters.add(r.trim().toUpperCase()); + } + + // In case there are no reporters, configure JSON reporter + if (reporters.isEmpty()) { + reporters.add(Reporting.JSON.toString()); + } + + // Configure all reporters + for (String r: reporters) { + switch (SentryMetrics.Reporting.valueOf(r)) { + case CONSOLE: + LOGGER.info("Enabled console metrics reporter with {} seconds interval", + reportInterval); + final ConsoleReporter consoleReporter = + ConsoleReporter.forRegistry(METRIC_REGISTRY) + .convertRatesTo(TimeUnit.SECONDS) + .convertDurationsTo(TimeUnit.MILLISECONDS) + .build(); + consoleReporter.start(reportInterval, TimeUnit.SECONDS); + break; + case JMX: + LOGGER.info("Enabled JMX metrics reporter"); + final JmxReporter jmxReporter = JmxReporter.forRegistry(METRIC_REGISTRY) + .convertRatesTo(TimeUnit.SECONDS) + .convertDurationsTo(TimeUnit.MILLISECONDS) + .build(); + jmxReporter.start(); + break; + case LOG: + LOGGER.info("Enabled Log4J metrics reporter with {} seconds interval", + reportInterval); + final Slf4jReporter logReporter = Slf4jReporter.forRegistry(METRIC_REGISTRY) + .outputTo(LOGGER) + .convertRatesTo(TimeUnit.SECONDS) + .convertDurationsTo(TimeUnit.MILLISECONDS) + .build(); + logReporter.start(reportInterval, TimeUnit.SECONDS); + break; + case JSON: + LOGGER.info("Enabled JSON metrics reporter with {} seconds interval", reportInterval); + JsonFileReporter jsonReporter = new JsonFileReporter(conf, + reportInterval, TimeUnit.SECONDS); + jsonReporter.start(); + break; + default: + LOGGER.warn("Invalid metrics reporter {}", reporter); + break; + } + } + } + + private <T, V> void addGauge(Class<T> tClass, String gaugeName, Gauge<V> gauge) { + METRIC_REGISTRY.register( + name(tClass, gaugeName), gauge); + } + + private void registerMetricSet(String prefix, MetricSet metricSet, MetricRegistry registry) { + for (Map.Entry<String, Metric> entry : metricSet.getMetrics().entrySet()) { + if (entry.getValue() instanceof MetricSet) { + registerMetricSet(prefix + "." + entry.getKey(), (MetricSet) entry.getValue(), registry); + } else { + registry.register(prefix + "." + entry.getKey(), entry.getValue()); + } + } + } + + /** + * Custom reporter that writes metrics as a JSON file. + * This class originated from Apache Hive JSON reporter. + */ + private static class JsonFileReporter implements AutoCloseable, Runnable { + // + // Implementation notes. + // + // 1. Since only local file systems are supported, there is no need to use Hadoop + // version of Path class. + // 2. java.nio package provides modern implementation of file and directory operations + // which is better then the traditional java.io, so we are using it here. + // In particular, it supports atomic creation of temporary files with specified + // permissions in the specified directory. This also avoids various attacks possible + // when temp file name is generated first, followed by file creation. + // See http://www.oracle.com/technetwork/articles/javase/nio-139333.html for + // the description of NIO API and + // http://docs.oracle.com/javase/tutorial/essential/io/legacy.html for the + // description of interoperability between legacy IO api vs NIO API. + // 3. To avoid race conditions with readers of the metrics file, the implementation + // dumps metrics to a temporary file in the same directory as the actual metrics + // file and then renames it to the destination. Since both are located on the same + // filesystem, this rename is likely to be atomic (as long as the underlying OS + // support atomic renames. + // + + // Permissions for the metrics file + private static final FileAttribute<Set<PosixFilePermission>> FILE_ATTRS = + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-r--r--")); + private static final String JSON_REPORTER_THREAD_NAME = "json-reporter"; + + private ScheduledExecutorService executor = null; + private final ObjectMapper jsonMapper = + new ObjectMapper().registerModule(new MetricsModule(TimeUnit.SECONDS, + TimeUnit.MILLISECONDS, + false)); + private final Configuration conf; + /** Destination file name. */ + // Location of JSON file + private final Path path; + // tmpdir is the dirname(path) + private final Path tmpDir; + private final long interval; + private final TimeUnit unit; + + JsonFileReporter(Configuration conf, long interval, TimeUnit unit) { + this.conf = conf; + String pathString = conf.get(ServerConfig.SENTRY_JSON_REPORTER_FILE, + ServerConfig.SENTRY_JSON_REPORTER_FILE_DEFAULT); + path = Paths.get(pathString).toAbsolutePath(); + LOGGER.info("Reporting metrics to {}", path); + // We want to use tmpDir i the same directory as the destination file to support atomic + // move of temp file to the destination metrics file + tmpDir = path.getParent(); + this.interval = interval; + this.unit = unit; + } + + private void start() { + executor = Executors.newScheduledThreadPool(1, + new ThreadFactoryBuilder().setNameFormat(JSON_REPORTER_THREAD_NAME).build()); + executor.scheduleAtFixedRate(this, 0, interval, unit); + } + + @Override + public void run() { + Path tmpFile = null; + try { + String json = null; + try { + json = jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(METRIC_REGISTRY); + } catch (JsonProcessingException e) { + LOGGER.error("Error converting metrics to JSON", e); + return; + } + // Metrics are first dumped to a temp file which is then renamed to the destination + try { + tmpFile = Files.createTempFile(tmpDir, "smetrics", "json", FILE_ATTRS); + } catch (IOException e) { + LOGGER.error("failed to create temp file for JSON metrics", e); + return; + } catch (SecurityException e) { + // This shouldn't ever happen + LOGGER.error("failed to create temp file for JSON metrics: no permissions", e); + return; + } catch (UnsupportedOperationException e) { + // This shouldn't ever happen + LOGGER.error("failed to create temp file for JSON metrics: operartion not supported", e); + return; + } + + try (BufferedWriter bw = new BufferedWriter(new FileWriter(tmpFile.toFile()))) { + bw.write(json); + } + + // Move temp file to the destination file + try { + Files.move(tmpFile, path, StandardCopyOption.ATOMIC_MOVE); + } catch (Exception e) { + LOGGER.error("Failed to move temp metrics file to {}: {}", path, e.getMessage()); + } + } catch (Throwable t) { + // catch all errors (throwable and execptions to prevent subsequent tasks from being suppressed) + LOGGER.error("Error executing scheduled task ", t); + } finally { + // If something happened and we were not able to rename the temp file, attempt to remove it + if (tmpFile != null && tmpFile.toFile().exists()) { + // Attempt to delete temp file, if this fails, not much can be done about it. + try { + Files.delete(tmpFile); + } catch (Exception e) { + LOGGER.error("failed to delete yemporary metrics file {}", tmpFile, e); + } + } + } + } + + @Override + public void close() { + if (executor != null) { + SentryServiceUtil.shutdownAndAwaitTermination(executor, + JSON_REPORTER_THREAD_NAME, 1, TimeUnit.MINUTES, LOGGER); + executor = null; + } + try { + Files.delete(path); + } catch (IOException e) { + LOGGER.error("Unable to delete {}", path, e); + } + } + } +} http://git-wip-us.apache.org/repos/asf/sentry/blob/b97f5c7a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/SentryMetricsServletContextListener.java ---------------------------------------------------------------------- diff --git a/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/SentryMetricsServletContextListener.java b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/SentryMetricsServletContextListener.java new file mode 100644 index 0000000..253e54f --- /dev/null +++ b/sentry-service/sentry-service-server/src/main/java/org/apache/sentry/api/service/thrift/SentryMetricsServletContextListener.java @@ -0,0 +1,32 @@ +/** + * 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.sentry.api.service.thrift; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.servlets.MetricsServlet; + +public class SentryMetricsServletContextListener extends MetricsServlet.ContextListener { + + public static final MetricRegistry METRIC_REGISTRY = new MetricRegistry(); + + @Override + protected MetricRegistry getMetricRegistry() { + return METRIC_REGISTRY; + } + +}
