org.apache.sentry.provider.db.generic.UpdatableCache.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.sentry.provider.db.generic.UpdatableCache.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.db.generic;

import com.google.common.collect.Table;
import com.google.common.collect.HashBasedTable;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.sentry.provider.common.TableCache;
import org.apache.sentry.api.generic.thrift.*;
import org.apache.sentry.api.common.ApiConstants;
import org.apache.sentry.api.tools.TSentryPrivilegeConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashSet;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;

public final class UpdatableCache implements TableCache, AutoCloseable {
    private static final Logger LOGGER = LoggerFactory.getLogger(UpdatableCache.class);

    // Timer for getting updates periodically
    private final Timer timer = new Timer();
    private boolean initialized = false;
    // saved timer is used by tests to cancel previous timer
    private static Timer savedTimer;

    private final String componentType;
    private final String serviceName;
    private final long cacheTtlNs;
    private final int allowedUpdateFailuresCount;
    private final Configuration conf;
    private final TSentryPrivilegeConverter tSentryPrivilegeConverter;

    private volatile long lastRefreshedNs = 0;
    private int consecutiveUpdateFailuresCount = 0;
    /**
     * 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 volatile Table<String, String, Set<String>> table;

    UpdatableCache(Configuration conf, String componentType, String serviceName,
            TSentryPrivilegeConverter tSentryPrivilegeConverter) {
        this.conf = conf;
        this.componentType = componentType;
        this.serviceName = serviceName;
        this.tSentryPrivilegeConverter = tSentryPrivilegeConverter;

        // check caching configuration
        this.cacheTtlNs = TimeUnit.MILLISECONDS.toNanos(conf.getLong(ApiConstants.ClientConfig.CACHE_TTL_MS,
                ApiConstants.ClientConfig.CACHING_TTL_MS_DEFAULT));
        this.allowedUpdateFailuresCount = conf.getInt(
                ApiConstants.ClientConfig.CACHE_UPDATE_FAILURES_BEFORE_PRIV_REVOKE,
                ApiConstants.ClientConfig.CACHE_UPDATE_FAILURES_BEFORE_PRIV_REVOKE_DEFAULT);
    }

    @Override
    public Table<String, String, Set<String>> getCache() {
        return table;
    }

    /**
     * Build cache replica with latest values
     *
     * @return cache replica with latest values
     */
    private Table<String, String, Set<String>> loadFromRemote() throws Exception {
        Table<String, String, Set<String>> tempCache = HashBasedTable.create();
        String requestor;
        requestor = UserGroupInformation.getLoginUser().getShortUserName();

        try (SentryGenericServiceClient client = getClient()) {
            Set<TSentryRole> tSentryRoles = client.listAllRoles(requestor, componentType);

            for (TSentryRole tSentryRole : tSentryRoles) {
                final String roleName = tSentryRole.getRoleName();
                final Set<TSentryPrivilege> tSentryPrivileges = client.listAllPrivilegesByRoleName(requestor,
                        roleName, componentType, serviceName);
                for (String group : tSentryRole.getGroups()) {
                    Set<String> currentPrivileges = tempCache.get(group, roleName);
                    if (currentPrivileges == null) {
                        currentPrivileges = new HashSet<>();
                        tempCache.put(group, roleName, currentPrivileges);
                    }
                    for (TSentryPrivilege tSentryPrivilege : tSentryPrivileges) {
                        currentPrivileges.add(tSentryPrivilegeConverter.toString(tSentryPrivilege));
                    }
                }
            }
            return tempCache;
        }
    }

    /**
     *  The Sentry-296(generate client for connection pooling) has already finished development and reviewed by now. When it
     *  was committed to master, the getClient method was needed to refactor using the connection pool
     *
     *  TODO: Avoid creating new client each time.
     */
    private SentryGenericServiceClient getClient() throws Exception {
        return SentryGenericServiceClientFactory.create(conf);
    }

    void startUpdateThread(boolean blockUntilFirstReload) throws Exception {
        if (blockUntilFirstReload) {
            try {
                reloadData();
            } catch (Exception e) {
                String logMessage = "Unable to load cache on first reload for component[" + this.componentType
                        + "] serviceName[" + this.serviceName + "]. " + "Cache will load with thread in ["
                        + TimeUnit.NANOSECONDS.toSeconds(this.cacheTtlNs) + "]sec";
                LOGGER.warn(logMessage, e);
            }
        }

        if (initialized) {
            LOGGER.info("Already initialized");
            return;
        }

        initialized = true;
        // Save timer to be able to cancel it.
        if (savedTimer != null) {
            LOGGER.debug("Resetting saved timer");
            savedTimer.cancel();
        }
        savedTimer = timer;

        final long refreshIntervalMs = TimeUnit.NANOSECONDS.toMillis(cacheTtlNs);
        timer.scheduleAtFixedRate(new TimerTask() {
            public void run() {
                if (shouldRefresh()) {
                    try {
                        LOGGER.debug("Loading all data.");
                        reloadData();
                    } catch (Exception e) {
                        LOGGER.warn("Exception while updating data from DB", e);
                        revokeAllPrivilegesIfRequired();
                    }
                }
            }
        }, blockUntilFirstReload ? refreshIntervalMs : 0, refreshIntervalMs);
    }

    private void revokeAllPrivilegesIfRequired() {
        if (++consecutiveUpdateFailuresCount > allowedUpdateFailuresCount) {
            consecutiveUpdateFailuresCount = 0;
            // Clear cache to revoke all privileges.
            // Update table cache to point to an empty table to avoid thread-unsafe characteristics of HashBasedTable.
            this.table = HashBasedTable.create();
            LOGGER.error("Failed to update roles and privileges cache for " + consecutiveUpdateFailuresCount
                    + " times."
                    + " Revoking all privileges from cache, which will cause all authorization requests to fail.");
        }
    }

    private void reloadData() throws Exception {
        this.table = loadFromRemote();
        lastRefreshedNs = System.nanoTime();
    }

    private boolean shouldRefresh() {
        final long currentTimeNs = System.nanoTime();
        return lastRefreshedNs + cacheTtlNs < currentTimeNs;
    }

    /**
     * Only called by tests to disable timer.
     */
    public static void disable() {
        if (savedTimer != null) {
            LOGGER.info("Disabling timer");
            savedTimer.cancel();
        }
    }

    @Override
    public void close() {
        timer.cancel();
        savedTimer = null;
        LOGGER.info("Closed Updatable Cache");
    }
}