org.apache.sentry.provider.file.SimpleFileProviderBackend.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.sentry.provider.file.SimpleFileProviderBackend.java

Source

/*
 * 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.provider.file;

import static org.apache.sentry.provider.common.PolicyFileConstants.DATABASES;
import static org.apache.sentry.provider.common.PolicyFileConstants.GROUPS;
import static org.apache.sentry.provider.common.PolicyFileConstants.ROLES;
import static org.apache.sentry.provider.common.PolicyFileConstants.USERS;
import static org.apache.sentry.provider.common.ProviderConstants.ROLE_SPLITTER;

import java.io.IOException;
import java.net.URI;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.Nullable;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.sentry.core.common.ActiveRoleSet;
import org.apache.sentry.core.common.Authorizable;
import org.apache.sentry.core.common.SentryConfigurationException;
import org.apache.sentry.policy.common.PrivilegeUtils;
import org.apache.sentry.policy.common.PrivilegeValidator;
import org.apache.sentry.policy.common.PrivilegeValidatorContext;
import org.apache.sentry.provider.common.ProviderBackend;
import org.apache.sentry.provider.common.ProviderBackendContext;
import org.apache.shiro.config.Ini;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Interner;
import com.google.common.collect.Interners;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;
import com.google.common.collect.Table.Cell;

public class SimpleFileProviderBackend implements ProviderBackend {

    private static final Logger LOGGER = LoggerFactory.getLogger(SimpleFileProviderBackend.class);

    private final FileSystem fileSystem;
    private final Path resourcePath;
    private final Configuration conf;
    private final List<String> configErrors;
    private final List<String> configWarnings;

    /**
     * Sparse table where group is the row key and role is the cell.
     * The value is the set of privileges located in the cell. For example,
     * the following table would be generated for a policy where Group 1
     * has Role 1 and Role 2 while Group 2 has only Role 2.
     * <table border="1">
     *  <tbody>
     *    <tr>
     *      <td><!-- empty --></td>
     *      <td>Role 1</td>
     *      <td>Role 2</td>
     *    </tr>
     *    <tr>
     *      <td>Group 1</td>
     *      <td>Priv 1</td>
     *      <td>Priv 2, Priv 3</td>
     *    </tr>
     *    <tr>
     *      <td>Group 2</td>
     *      <td><!-- empty --></td>
     *      <td>Priv 2, Priv 3</td>
     *    </tr>
     *  </tbody>
     * </table>
     */
    private final Table<String, String, Set<String>> groupRolePrivilegeTable;
    /**
     * Each group, role, and privilege in groupRolePrivilegeTable is
     * interned using a weak interner so that we only store each string
     * once.
     */
    private final Interner<String> stringInterner;

    private ImmutableList<PrivilegeValidator> validators;
    private boolean allowPerDatabaseSection;
    private volatile boolean initialized;

    public SimpleFileProviderBackend(Configuration conf, String resourcePath) throws IOException {
        this(conf, new Path(resourcePath));
    }

    public SimpleFileProviderBackend(Configuration conf, Path resourcePath) throws IOException {
        this.resourcePath = resourcePath;
        this.fileSystem = resourcePath.getFileSystem(conf);
        this.groupRolePrivilegeTable = HashBasedTable.create();
        this.conf = conf;
        this.configErrors = Lists.newArrayList();
        this.configWarnings = Lists.newArrayList();
        this.validators = ImmutableList.of();
        this.allowPerDatabaseSection = true;
        this.initialized = false;
        this.stringInterner = Interners.newWeakInterner();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void initialize(ProviderBackendContext context) {
        if (initialized) {
            throw new IllegalStateException("Backend has already been initialized, cannot be initialized twice");
        }
        this.validators = context.getValidators();
        this.allowPerDatabaseSection = context.isAllowPerDatabase();
        parse();
        this.initialized = true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ImmutableSet<String> getPrivileges(Set<String> groups, ActiveRoleSet roleSet,
            Authorizable... authorizableHierarchy) {
        if (!initialized) {
            throw new IllegalStateException("Backend has not been properly initialized");
        }
        ImmutableSet.Builder<String> resultBuilder = ImmutableSet.builder();
        for (String groupName : groups) {
            for (Map.Entry<String, Set<String>> row : groupRolePrivilegeTable.row(groupName).entrySet()) {
                if (roleSet.containsRole(row.getKey())) {
                    resultBuilder.addAll(row.getValue());
                }
            }
        }
        return resultBuilder.build();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ImmutableSet<String> getRoles(Set<String> groups, ActiveRoleSet roleSet) {
        if (!initialized) {
            throw new IllegalStateException("Backend has not been properly initialized");
        }
        ImmutableSet.Builder<String> resultBuilder = ImmutableSet.builder();
        if (groups != null) {
            for (String groupName : groups) {
                for (Map.Entry<String, Set<String>> row : groupRolePrivilegeTable.row(groupName).entrySet()) {
                    if (roleSet.containsRole(row.getKey())) {
                        resultBuilder.add(row.getKey());
                    }
                }
            }
        }
        return resultBuilder.build();
    }

    @Override
    public void close() {
        // SENTRY-847 will use HiveAuthBinding again, so groupRolePrivilegeTable shouldn't clear itself
    }

    @Override
    public void validatePolicy(boolean strictValidation) throws SentryConfigurationException {
        if (!initialized) {
            throw new IllegalStateException("Backend has not been properly initialized");
        }
        List<String> localConfigErrors = Lists.newArrayList(configErrors);
        List<String> localConfigWarnings = Lists.newArrayList(configWarnings);
        if ((strictValidation && !localConfigWarnings.isEmpty()) || !localConfigErrors.isEmpty()) {
            localConfigErrors.add("Failed to process global policy file " + resourcePath);
            SentryConfigurationException e = new SentryConfigurationException("");
            e.setConfigErrors(localConfigErrors);
            e.setConfigWarnings(localConfigWarnings);
            throw e;
        }
    }

    private void parse() {
        configErrors.clear();
        configWarnings.clear();
        Table<String, String, Set<String>> groupRolePrivilegeTableTemp = HashBasedTable.create();
        Ini ini;
        LOGGER.info("Parsing " + resourcePath);
        LOGGER.info("Filesystem: " + fileSystem.getUri());
        try {
            try {
                ini = PolicyFiles.loadFromPath(fileSystem, resourcePath);
            } catch (IOException e) {
                configErrors.add("Failed to read policy file " + resourcePath + " Error: " + e.getMessage());
                throw new SentryConfigurationException("Error loading policy file " + resourcePath, e);
            } catch (IllegalArgumentException e) {
                configErrors.add("Failed to read policy file " + resourcePath + " Error: " + e.getMessage());
                throw new SentryConfigurationException("Error loading policy file " + resourcePath, e);
            }

            if (LOGGER.isDebugEnabled()) {
                for (String sectionName : ini.getSectionNames()) {
                    LOGGER.debug("Section: " + sectionName);
                    Ini.Section section = ini.get(sectionName);
                    for (String key : section.keySet()) {
                        String value = section.get(key);
                        LOGGER.debug(key + " = " + value);
                    }
                }
            }
            parseIni(null, ini, validators, resourcePath, groupRolePrivilegeTableTemp);
            mergeResult(groupRolePrivilegeTableTemp);
            groupRolePrivilegeTableTemp.clear();
            Ini.Section filesSection = ini.getSection(DATABASES);
            if (filesSection == null) {
                LOGGER.info("Section " + DATABASES + " needs no further processing");
            } else if (!allowPerDatabaseSection) {
                String msg = "Per-db policy file is not expected in this configuration.";
                throw new SentryConfigurationException(msg);
            } else {
                for (Map.Entry<String, String> entry : filesSection.entrySet()) {
                    String database = Strings.nullToEmpty(entry.getKey()).trim().toLowerCase();
                    Path perDbPolicy = new Path(Strings.nullToEmpty(entry.getValue()).trim());
                    if (isRelative(perDbPolicy)) {
                        perDbPolicy = new Path(resourcePath.getParent(), perDbPolicy);
                    }
                    try {
                        LOGGER.debug("Parsing " + perDbPolicy);
                        Ini perDbIni = PolicyFiles.loadFromPath(perDbPolicy.getFileSystem(conf), perDbPolicy);
                        if (perDbIni.containsKey(USERS)) {
                            configErrors.add(
                                    "Per-db policy file cannot contain " + USERS + " section in " + perDbPolicy);
                            throw new SentryConfigurationException(
                                    "Per-db policy files cannot contain " + USERS + " section");
                        }
                        if (perDbIni.containsKey(DATABASES)) {
                            configErrors.add("Per-db policy files cannot contain " + DATABASES + " section in "
                                    + perDbPolicy);
                            throw new SentryConfigurationException(
                                    "Per-db policy files cannot contain " + DATABASES + " section");
                        }
                        parseIni(database, perDbIni, validators, perDbPolicy, groupRolePrivilegeTableTemp);
                    } catch (Exception e) {
                        configErrors.add(
                                "Failed to read per-DB policy file " + perDbPolicy + " Error: " + e.getMessage());
                        LOGGER.error("Error processing key " + entry.getKey() + ", skipping " + entry.getValue(),
                                e);
                    }
                }
            }
            mergeResult(groupRolePrivilegeTableTemp);
            groupRolePrivilegeTableTemp.clear();
        } catch (Exception e) {
            configErrors.add("Error processing file " + resourcePath + e.getMessage());
            LOGGER.error("Error processing file, ignoring " + resourcePath, e);
        }
    }

    /**
     * Relative for our purposes is no scheme, no authority
     * and a non-absolute path portion.
     */
    private boolean isRelative(Path path) {
        URI uri = path.toUri();
        return uri.getAuthority() == null && uri.getScheme() == null && !path.isUriPathAbsolute();
    }

    private void mergeResult(Table<String, String, Set<String>> groupRolePrivilegeTableTemp) {
        for (Cell<String, String, Set<String>> cell : groupRolePrivilegeTableTemp.cellSet()) {
            String groupName = cell.getRowKey();
            String roleName = cell.getColumnKey();
            Set<String> privileges = groupRolePrivilegeTable.get(groupName, roleName);
            if (privileges == null) {
                privileges = new HashSet<String>();
                groupRolePrivilegeTable.put(groupName, roleName, privileges);
            }
            privileges.addAll(cell.getValue());
        }
    }

    private void parseIni(String database, Ini ini, List<? extends PrivilegeValidator> validators, Path policyPath,
            Table<String, String, Set<String>> groupRolePrivilegeTable) {
        Ini.Section privilegesSection = ini.getSection(ROLES);
        boolean invalidConfiguration = false;
        if (privilegesSection == null) {
            String errMsg = String.format("Section %s empty for %s", ROLES, policyPath);
            LOGGER.warn(errMsg);
            configErrors.add(errMsg);
            invalidConfiguration = true;
        }
        Ini.Section groupsSection = ini.getSection(GROUPS);
        if (groupsSection == null) {
            String warnMsg = String.format("Section %s empty for %s", GROUPS, policyPath);
            LOGGER.warn(warnMsg);
            configErrors.add(warnMsg);
            invalidConfiguration = true;
        }
        if (!invalidConfiguration) {
            parsePrivileges(database, privilegesSection, groupsSection, validators, policyPath,
                    groupRolePrivilegeTable);
        }
    }

    private void parsePrivileges(@Nullable String database, Ini.Section rolesSection, Ini.Section groupsSection,
            List<? extends PrivilegeValidator> validators, Path policyPath,
            Table<String, String, Set<String>> groupRolePrivilegeTable) {
        Multimap<String, String> roleNameToPrivilegeMap = HashMultimap.create();
        for (Map.Entry<String, String> entry : rolesSection.entrySet()) {
            String roleName = stringInterner.intern(Strings.nullToEmpty(entry.getKey()).trim());
            String roleValue = Strings.nullToEmpty(entry.getValue()).trim();
            boolean invalidConfiguration = false;
            if (roleName.isEmpty()) {
                String errMsg = String.format("Empty role name encountered in %s", policyPath);
                LOGGER.warn(errMsg);
                configErrors.add(errMsg);
                invalidConfiguration = true;
            }
            if (roleValue.isEmpty()) {
                String errMsg = String.format("Empty role value encountered in %s", policyPath);
                LOGGER.warn(errMsg);
                configErrors.add(errMsg);
                invalidConfiguration = true;
            }
            if (roleNameToPrivilegeMap.containsKey(roleName)) {
                String warnMsg = String.format("Role %s defined twice in %s", roleName, policyPath);
                LOGGER.warn(warnMsg);
                configWarnings.add(warnMsg);
            }
            Set<String> privileges = PrivilegeUtils.toPrivilegeStrings(roleValue);
            if (!invalidConfiguration && privileges != null) {
                Set<String> internedPrivileges = Sets.newHashSet();
                for (String privilege : privileges) {
                    for (PrivilegeValidator validator : validators) {
                        validator.validate(new PrivilegeValidatorContext(database, privilege.trim()));
                    }
                    internedPrivileges.add(stringInterner.intern(privilege));
                }
                roleNameToPrivilegeMap.putAll(roleName, internedPrivileges);
            }
        }
        Splitter roleSplitter = ROLE_SPLITTER.omitEmptyStrings().trimResults();
        for (Map.Entry<String, String> entry : groupsSection.entrySet()) {
            String groupName = stringInterner.intern(Strings.nullToEmpty(entry.getKey()).trim());
            String groupPrivileges = Strings.nullToEmpty(entry.getValue()).trim();
            for (String roleName : roleSplitter.split(groupPrivileges)) {
                roleName = stringInterner.intern(roleName);
                if (roleNameToPrivilegeMap.containsKey(roleName)) {
                    Set<String> privileges = groupRolePrivilegeTable.get(groupName, roleName);
                    if (privileges == null) {
                        privileges = new HashSet<String>();
                        groupRolePrivilegeTable.put(groupName, roleName, privileges);
                    }
                    privileges.addAll(roleNameToPrivilegeMap.get(roleName));
                } else {
                    String warnMsg = String.format(
                            "Role %s for group %s does not exist in privileges section in %s", roleName, groupName,
                            policyPath);
                    LOGGER.warn(warnMsg);
                    configWarnings.add(warnMsg);
                }
            }
        }
    }

    public Table<String, String, Set<String>> getGroupRolePrivilegeTable() {
        return groupRolePrivilegeTable;
    }
}