/*
* 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.ranger.authorization;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.nifi.authorization.AuthorizationRequest;
import org.apache.nifi.authorization.AuthorizationResult;
import org.apache.nifi.authorization.Authorizer;
import org.apache.nifi.authorization.AuthorizerConfigurationContext;
import org.apache.nifi.authorization.AuthorizerInitializationContext;
import org.apache.nifi.authorization.UserContextKeys;
import org.apache.nifi.authorization.annotation.AuthorizerContext;
import org.apache.nifi.authorization.exception.AuthorizationAccessException;
import org.apache.nifi.authorization.exception.AuthorizerCreationException;
import org.apache.nifi.authorization.exception.AuthorizerDestructionException;
import org.apache.nifi.components.PropertyValue;
import org.apache.nifi.util.NiFiProperties;
import org.apache.ranger.authorization.hadoop.config.RangerConfiguration;
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.apache.ranger.plugin.policyengine.RangerAccessResultProcessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.net.MalformedURLException;
import java.util.Date;
/**
* Authorizer implementation that uses Apache Ranger to make authorization decisions.
*/
public class RangerNiFiAuthorizer implements Authorizer {
private static final Logger logger = LoggerFactory.getLogger(RangerNiFiAuthorizer.class);
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_RESOURCE_NAME = "nifi-resource";
static final String DEFAULT_SERVICE_TYPE = "nifi";
static final String DEFAULT_APP_ID = "nifi";
static final String RESOURCES_RESOURCE = "/resources";
static final String HADOOP_SECURITY_AUTHENTICATION = "hadoop.security.authentication";
static final String KERBEROS_AUTHENTICATION = "kerberos";
private volatile RangerBasePluginWithPolicies nifiPlugin = null;
private volatile RangerDefaultAuditHandler defaultAuditHandler = null;
private volatile String rangerAdminIdentity = null;
private volatile boolean rangerKerberosEnabled = false;
private volatile NiFiProperties nifiProperties;
@Override
public void initialize(AuthorizerInitializationContext initializationContext) throws AuthorizerCreationException {
}
@Override
public void onConfigured(AuthorizerConfigurationContext configurationContext) throws AuthorizerCreationException {
try {
if (nifiPlugin == null) {
logger.info("RangerNiFiAuthorizer(): initializing base plugin");
final PropertyValue securityConfigValue = configurationContext.getProperty(RANGER_SECURITY_PATH_PROP);
addRequiredResource(RANGER_SECURITY_PATH_PROP, securityConfigValue);
final PropertyValue auditConfigValue = configurationContext.getProperty(RANGER_AUDIT_PATH_PROP);
addRequiredResource(RANGER_AUDIT_PATH_PROP, auditConfigValue);
final String rangerKerberosEnabledValue = getConfigValue(configurationContext, RANGER_KERBEROS_ENABLED_PROP, Boolean.FALSE.toString());
rangerKerberosEnabled = rangerKerberosEnabledValue.equals(Boolean.TRUE.toString()) ? true : false;
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 principal and keytab, RangerAdminRESTClient will use Ranger's MiscUtil which
// will grab UserGroupInformation.getLoginUser() and call ugi.checkTGTAndReloginFromKeytab();
final String nifiPrincipal = nifiProperties.getKerberosServicePrincipal();
final String nifiKeytab = nifiProperties.getKerberosServiceKeytabLocation();
if (StringUtils.isBlank(nifiPrincipal) || StringUtils.isBlank(nifiKeytab)) {
throw new AuthorizerCreationException("Principal and Keytab must be provided when Kerberos is enabled");
}
UserGroupInformation.loginUserFromKeytab(nifiPrincipal.trim(), nifiKeytab.trim());
}
final String serviceType = getConfigValue(configurationContext, RANGER_SERVICE_TYPE_PROP, DEFAULT_SERVICE_TYPE);
final String appId = getConfigValue(configurationContext, RANGER_APP_ID_PROP, DEFAULT_APP_ID);
nifiPlugin = createRangerBasePlugin(serviceType, appId);
nifiPlugin.init();
defaultAuditHandler = new RangerDefaultAuditHandler();
rangerAdminIdentity = getConfigValue(configurationContext, RANGER_ADMIN_IDENTITY_PROP, null);
} else {
logger.info("RangerNiFiAuthorizer(): base plugin already initialized");
}
} catch (Throwable t) {
throw new AuthorizerCreationException("Error creating RangerBasePlugin", t);
}
}
protected RangerBasePluginWithPolicies createRangerBasePlugin(final String serviceType, final String appId) {
return new RangerBasePluginWithPolicies(serviceType, appId);
}
@Override
public AuthorizationResult authorize(final AuthorizationRequest request) throws AuthorizationAccessException {
final String identity = request.getIdentity();
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_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.setAccessTime(new Date());
if (!StringUtils.isBlank(clientIp)) {
rangerRequest.setClientIPAddress(clientIp);
}
// for a direct access request use the default audit handler so we generate audit logs
// for non-direct access provide a null result processor so no audit logs get generated
final RangerAccessResultProcessor resultProcessor = request.isAccessAttempt() ? defaultAuditHandler : null;
final RangerAccessResult result = nifiPlugin.isAccessAllowed(rangerRequest, resultProcessor);
if (result != null && result.getIsAllowed()) {
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 = nifiPlugin.doesPolicyExist(request.getResource().getIdentifier());
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 can work back up the resource hierarchy
return AuthorizationResult.resourceNotFound();
}
}
}
@Override
public void preDestruction() throws AuthorizerDestructionException {
if (nifiPlugin != null) {
try {
nifiPlugin.cleanup();
nifiPlugin = null;
} catch (Throwable t) {
throw new AuthorizerDestructionException("Error cleaning up RangerBasePlugin", t);
}
}
}
@AuthorizerContext
public void setNiFiProperties(final NiFiProperties properties) {
this.nifiProperties = 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
*/
private void addRequiredResource(final String name, final PropertyValue resourceValue) {
if (resourceValue == null || StringUtils.isBlank(resourceValue.getValue())) {
throw new AuthorizerCreationException(name + " must be specified.");
}
final File resourceFile = new File(resourceValue.getValue());
if (!resourceFile.exists() || !resourceFile.canRead()) {
throw new AuthorizerCreationException(resourceValue + " does not exist, or can not be read");
}
try {
RangerConfiguration.getInstance().addResource(resourceFile.toURI().toURL());
} catch (MalformedURLException e) {
throw new AuthorizerCreationException("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;
}
}