blob: 6fa6fe2e4f1b318d42f2ff8246e1fcbeddbe9237 [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.nifi.registry.ranger;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.nifi.registry.properties.NiFiRegistryProperties;
import org.apache.nifi.registry.security.authorization.AccessPolicy;
import org.apache.nifi.registry.security.authorization.AccessPolicyProvider;
import org.apache.nifi.registry.security.authorization.AccessPolicyProviderInitializationContext;
import org.apache.nifi.registry.security.authorization.AuthorizationAuditor;
import org.apache.nifi.registry.security.authorization.AuthorizationRequest;
import org.apache.nifi.registry.security.authorization.AuthorizationResult;
import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext;
import org.apache.nifi.registry.security.authorization.AuthorizerInitializationContext;
import org.apache.nifi.registry.security.authorization.ConfigurableUserGroupProvider;
import org.apache.nifi.registry.security.authorization.ManagedAuthorizer;
import org.apache.nifi.registry.security.authorization.RequestAction;
import org.apache.nifi.registry.security.authorization.UserContextKeys;
import org.apache.nifi.registry.security.authorization.UserGroupProvider;
import org.apache.nifi.registry.security.authorization.UserGroupProviderLookup;
import org.apache.nifi.registry.security.authorization.annotation.AuthorizerContext;
import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException;
import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException;
import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
import org.apache.nifi.registry.util.PropertyValue;
import org.apache.ranger.audit.model.AuthzAuditEvent;
import org.apache.ranger.authorization.hadoop.config.RangerConfiguration;
import org.apache.ranger.authorization.hadoop.config.RangerPluginConfig;
import org.apache.ranger.plugin.audit.RangerDefaultAuditHandler;
import org.apache.ranger.plugin.policyengine.RangerAccessRequestImpl;
import org.apache.ranger.plugin.policyengine.RangerAccessResourceImpl;
import org.apache.ranger.plugin.policyengine.RangerAccessResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
/**
* Authorizer implementation that uses Apache Ranger to make authorization decisions.
*/
public class RangerAuthorizer implements ManagedAuthorizer, AuthorizationAuditor {
private static final Logger logger = LoggerFactory.getLogger(RangerAuthorizer.class);
private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance();
private static final String USER_GROUP_PROVIDER_ELEMENT = "userGroupProvider";
static final String USER_GROUP_PROVIDER = "User Group Provider";
static final String RANGER_AUDIT_PATH_PROP = "Ranger Audit Config Path";
static final String RANGER_SECURITY_PATH_PROP = "Ranger Security Config Path";
static final String RANGER_KERBEROS_ENABLED_PROP = "Ranger Kerberos Enabled";
static final String RANGER_ADMIN_IDENTITY_PROP = "Ranger Admin Identity";
static final String RANGER_SERVICE_TYPE_PROP = "Ranger Service Type";
static final String RANGER_APP_ID_PROP = "Ranger Application Id";
static final String RANGER_NIFI_REG_RESOURCE_NAME = "nifi-registry-resource";
private static final String DEFAULT_SERVICE_TYPE = "nifi-registry";
private static final String DEFAULT_APP_ID = "nifi-registry";
static final String RESOURCES_RESOURCE = "/policies";
static final String HADOOP_SECURITY_AUTHENTICATION = "hadoop.security.authentication";
private static final String KERBEROS_AUTHENTICATION = "kerberos";
private final Map<AuthorizationRequest, RangerAccessResult> resultLookup = new WeakHashMap<>();
private volatile RangerBasePluginWithPolicies rangerPlugin = null;
private volatile RangerDefaultAuditHandler defaultAuditHandler = null;
private volatile String rangerAdminIdentity = null;
private volatile NiFiRegistryProperties registryProperties;
private UserGroupProviderLookup userGroupProviderLookup;
private UserGroupProvider userGroupProvider;
@Override
public void initialize(AuthorizerInitializationContext initializationContext) throws SecurityProviderCreationException {
userGroupProviderLookup = initializationContext.getUserGroupProviderLookup();
}
@Override
public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException {
final String userGroupProviderKey = configurationContext.getProperty(USER_GROUP_PROVIDER).getValue();
if (StringUtils.isEmpty(userGroupProviderKey)) {
throw new SecurityProviderCreationException(USER_GROUP_PROVIDER + " must be specified.");
}
userGroupProvider = userGroupProviderLookup.getUserGroupProvider(userGroupProviderKey);
// ensure the desired access policy provider has a user group provider
if (userGroupProvider == null) {
throw new SecurityProviderCreationException(String.format("Unable to locate configured User Group Provider: %s", userGroupProviderKey));
}
try {
if (rangerPlugin == null) {
logger.info("initializing base plugin");
final String serviceType = getConfigValue(configurationContext, RANGER_SERVICE_TYPE_PROP, DEFAULT_SERVICE_TYPE);
final String appId = getConfigValue(configurationContext, RANGER_APP_ID_PROP, DEFAULT_APP_ID);
rangerPlugin = createRangerBasePlugin(serviceType, appId);
final RangerPluginConfig pluginConfig = rangerPlugin.getConfig();
final PropertyValue securityConfigValue = configurationContext.getProperty(RANGER_SECURITY_PATH_PROP);
addRequiredResource(RANGER_SECURITY_PATH_PROP, securityConfigValue, pluginConfig);
final PropertyValue auditConfigValue = configurationContext.getProperty(RANGER_AUDIT_PATH_PROP);
addRequiredResource(RANGER_AUDIT_PATH_PROP, auditConfigValue, pluginConfig);
boolean rangerKerberosEnabled = Boolean.valueOf(getConfigValue(configurationContext, RANGER_KERBEROS_ENABLED_PROP, Boolean.FALSE.toString()));
if (rangerKerberosEnabled) {
// configure UGI for when RangerAdminRESTClient calls UserGroupInformation.isSecurityEnabled()
final Configuration securityConf = new Configuration();
securityConf.set(HADOOP_SECURITY_AUTHENTICATION, KERBEROS_AUTHENTICATION);
UserGroupInformation.setConfiguration(securityConf);
// login with the nifi registry principal and keytab, RangerAdminRESTClient will use Ranger's MiscUtil which
// will grab UserGroupInformation.getLoginUser() and call ugi.checkTGTAndReloginFromKeytab();
final String registryPrincipal = registryProperties.getKerberosServicePrincipal();
final String registryKeytab = registryProperties.getKerberosServiceKeytabLocation();
if (StringUtils.isBlank(registryPrincipal) || StringUtils.isBlank(registryKeytab)) {
throw new SecurityProviderCreationException("Principal and Keytab must be provided when Kerberos is enabled");
}
UserGroupInformation.loginUserFromKeytab(registryPrincipal.trim(), registryKeytab.trim());
}
rangerPlugin.init();
defaultAuditHandler = new RangerDefaultAuditHandler();
rangerAdminIdentity = getConfigValue(configurationContext, RANGER_ADMIN_IDENTITY_PROP, null);
} else {
logger.info("base plugin already initialized");
}
} catch (Throwable t) {
throw new SecurityProviderCreationException("Error creating RangerBasePlugin", t);
}
}
protected RangerBasePluginWithPolicies createRangerBasePlugin(final String serviceType, final String appId) {
return new RangerBasePluginWithPolicies(serviceType, appId, userGroupProvider);
}
@Override
public AuthorizationResult authorize(final AuthorizationRequest request) throws SecurityProviderCreationException {
final String identity = request.getIdentity();
final Set<String> userGroups = request.getGroups();
final String resourceIdentifier = request.getResource().getIdentifier();
// if a ranger admin identity was provided, and it equals the identity making the request,
// and the request is to retrieve the resources, then allow it through
if (StringUtils.isNotBlank(rangerAdminIdentity) && rangerAdminIdentity.equals(identity)
&& resourceIdentifier.equals(RESOURCES_RESOURCE)) {
return AuthorizationResult.approved();
}
final String clientIp;
if (request.getUserContext() != null) {
clientIp = request.getUserContext().get(UserContextKeys.CLIENT_ADDRESS.name());
} else {
clientIp = null;
}
final RangerAccessResourceImpl resource = new RangerAccessResourceImpl();
resource.setValue(RANGER_NIFI_REG_RESOURCE_NAME, resourceIdentifier);
final RangerAccessRequestImpl rangerRequest = new RangerAccessRequestImpl();
rangerRequest.setResource(resource);
rangerRequest.setAction(request.getAction().name());
rangerRequest.setAccessType(request.getAction().name());
rangerRequest.setUser(identity);
rangerRequest.setUserGroups(userGroups);
rangerRequest.setAccessTime(new Date());
if (!StringUtils.isBlank(clientIp)) {
rangerRequest.setClientIPAddress(clientIp);
}
final RangerAccessResult result = rangerPlugin.isAccessAllowed(rangerRequest);
// store the result for auditing purposes later if appropriate
if (request.isAccessAttempt()) {
synchronized (resultLookup) {
resultLookup.put(request, result);
}
}
if (result != null && result.getIsAllowed()) {
// return approved
return AuthorizationResult.approved();
} else {
// if result.getIsAllowed() is false, then we need to determine if it was because no policy exists for the
// given resource, or if it was because a policy exists but not for the given user or action
final boolean doesPolicyExist = rangerPlugin.doesPolicyExist(request.getResource().getIdentifier(), request.getAction());
if (doesPolicyExist) {
final String reason = result == null ? null : result.getReason();
if (reason != null) {
logger.debug(String.format("Unable to authorize %s due to %s", identity, reason));
}
// a policy does exist for the resource so we were really denied access here
return AuthorizationResult.denied(request.getExplanationSupplier().get());
} else {
// a policy doesn't exist so return resource not found so NiFi Registry can work back up the resource hierarchy
return AuthorizationResult.resourceNotFound();
}
}
}
@Override
public void auditAccessAttempt(final AuthorizationRequest request, final AuthorizationResult result) {
final RangerAccessResult rangerResult;
synchronized (resultLookup) {
rangerResult = resultLookup.remove(request);
}
if (rangerResult != null && rangerResult.getIsAudited()) {
AuthzAuditEvent event = defaultAuditHandler.getAuthzEvents(rangerResult);
// update the event with the originally requested resource
event.setResourceType(RANGER_NIFI_REG_RESOURCE_NAME);
event.setResourcePath(request.getRequestedResource().getIdentifier());
defaultAuditHandler.logAuthzAudit(event);
}
}
@Override
public void preDestruction() throws SecurityProviderCreationException {
if (rangerPlugin != null) {
try {
rangerPlugin.cleanup();
rangerPlugin = null;
} catch (Throwable t) {
throw new SecurityProviderCreationException("Error cleaning up RangerBasePlugin", t);
}
}
}
@AuthorizerContext
public void setRegistryProperties(final NiFiRegistryProperties properties) {
this.registryProperties = properties;
}
/**
* Adds a resource to the RangerConfiguration singleton so it is already there by the time RangerBasePlugin.init()
* is called.
*
* @param name the name of the given PropertyValue from the AuthorizationConfigurationContext
* @param resourceValue the value for the given name, should be a full path to a file
* @param configuration the RangerConfiguration to add the resource to
*/
private void addRequiredResource(final String name, final PropertyValue resourceValue, final RangerConfiguration configuration) {
if (resourceValue == null || StringUtils.isBlank(resourceValue.getValue())) {
throw new SecurityProviderCreationException(name + " must be specified.");
}
final File resourceFile = new File(resourceValue.getValue());
if (!resourceFile.exists() || !resourceFile.canRead()) {
throw new SecurityProviderCreationException(resourceValue + " does not exist, or can not be read");
}
try {
configuration.addResource(resourceFile.toURI().toURL());
} catch (MalformedURLException e) {
throw new SecurityProviderCreationException("Error creating URI for " + resourceValue, e);
}
}
private String getConfigValue(final AuthorizerConfigurationContext context, final String name, final String defaultValue) {
final PropertyValue configValue = context.getProperty(name);
String retValue = defaultValue;
if (configValue != null && !StringUtils.isBlank(configValue.getValue())) {
retValue = configValue.getValue();
}
return retValue;
}
@Override
public String getFingerprint() throws AuthorizationAccessException {
final StringWriter out = new StringWriter();
try {
// create the document
final DocumentBuilder documentBuilder = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder();
final Document document = documentBuilder.newDocument();
// create the root element
final Element managedRangerAuthorizationsElement = document.createElement("managedRangerAuthorizations");
document.appendChild(managedRangerAuthorizationsElement);
// create the user group provider element
final Element userGroupProviderElement = document.createElement(USER_GROUP_PROVIDER_ELEMENT);
managedRangerAuthorizationsElement.appendChild(userGroupProviderElement);
// append fingerprint if the provider is configurable
if (userGroupProvider instanceof ConfigurableUserGroupProvider) {
userGroupProviderElement.appendChild(document.createTextNode(((ConfigurableUserGroupProvider) userGroupProvider).getFingerprint()));
}
final Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.transform(new DOMSource(document), new StreamResult(out));
} catch (ParserConfigurationException | TransformerException e) {
throw new AuthorizationAccessException("Unable to generate fingerprint", e);
}
return out.toString();
}
private String parseFingerprint(final String fingerprint) throws AuthorizationAccessException {
final byte[] fingerprintBytes = fingerprint.getBytes(StandardCharsets.UTF_8);
try (final ByteArrayInputStream in = new ByteArrayInputStream(fingerprintBytes)) {
final DocumentBuilder docBuilder = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder();
final Document document = docBuilder.parse(in);
final Element rootElement = document.getDocumentElement();
final NodeList userGroupProviderList = rootElement.getElementsByTagName(USER_GROUP_PROVIDER_ELEMENT);
if (userGroupProviderList.getLength() != 1) {
throw new AuthorizationAccessException(String.format("Only one %s element is allowed: %s", USER_GROUP_PROVIDER_ELEMENT, fingerprint));
}
final Node userGroupProvider = userGroupProviderList.item(0);
return userGroupProvider.getTextContent();
} catch (SAXException | ParserConfigurationException | IOException e) {
throw new AuthorizationAccessException("Unable to parse fingerprint", e);
}
}
@Override
public void inheritFingerprint(String fingerprint) throws AuthorizationAccessException {
if (StringUtils.isBlank(fingerprint)) {
return;
}
final String userGroupFingerprint = parseFingerprint(fingerprint);
if (StringUtils.isNotBlank(userGroupFingerprint) && userGroupProvider instanceof ConfigurableUserGroupProvider) {
((ConfigurableUserGroupProvider) userGroupProvider).inheritFingerprint(userGroupFingerprint);
}
}
@Override
public void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException {
final String userGroupFingerprint = parseFingerprint(proposedFingerprint);
if (StringUtils.isNotBlank(userGroupFingerprint)) {
if (userGroupProvider instanceof ConfigurableUserGroupProvider) {
((ConfigurableUserGroupProvider) userGroupProvider).checkInheritability(userGroupFingerprint);
} else {
throw new UninheritableAuthorizationsException("User/Group fingerprint is not blank and the configured UserGroupProvider does not support fingerprinting.");
}
}
}
@Override
public AccessPolicyProvider getAccessPolicyProvider() {
return new AccessPolicyProvider() {
@Override
public Set<AccessPolicy> getAccessPolicies() throws AuthorizationAccessException {
return rangerPlugin.getAccessPolicies();
}
@Override
public AccessPolicy getAccessPolicy(String identifier) throws AuthorizationAccessException {
return rangerPlugin.getAccessPolicy(identifier);
}
@Override
public AccessPolicy getAccessPolicy(String resourceIdentifier, RequestAction action) throws AuthorizationAccessException {
return rangerPlugin.getAccessPolicy(resourceIdentifier, action);
}
@Override
public UserGroupProvider getUserGroupProvider() {
return userGroupProvider;
}
@Override
public void initialize(AccessPolicyProviderInitializationContext initializationContext) throws SecurityProviderCreationException {
}
@Override
public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException {
}
@Override
public void preDestruction() throws SecurityProviderCreationException {
}
};
}
}