blob: 61d87da289fe353955215fe473ae3123340aa2c3 [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.impala.catalog;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.impala.thrift.TPrivilege;
import org.apache.impala.thrift.TPrivilegeScope;
import com.google.common.base.Preconditions;
/**
* Tree that allows efficient lookup for all privileges that can be relevant in the
* authorization of an object, e.g. getting privileges for server1/db1 should only iterate
* through privileges in db1, but not in other databases.
*
* Performance characteristics:
* add/remove: O(1), as the depth of the tree is limited
* getFilteredList: O(number_of_privileges_that_match_the_filter)
*
* The primary motivation is to speed up SHOW DATABASES/TABLES calls which do a separate
* listing to check access rights for each database/table. For this reason column and URI
* privileges do not get their own "level" in the tree - column privileges are merged with
* table privileges and URI privileges are stored per-server. This avoids unnecessarily
* storing many small hash maps in memory.
*
* This class is expected to be used in pair with a CatalogObjectCache<PrincipalPrivilege>
* which also handles catalog version logic and has efficient "remove" by full name.
*/
public class PrincipalPrivilegeTree {
// Contains database/table/column privileges grouped by server name + database name
// + table name. Column privileges are stored with table privileges to avoid the
// memory cost of creating many small hashmaps for columns with privileges.
Node<PrincipalPrivilege> tableRoot_ = new Node<>();
// Contains URI privileges grouped by server name. Storing these separately from table
// privileges allows Node to be simpler, as the Server level doesn't need to
// differentiate between two kind of privileges.
Node<PrincipalPrivilege> uriRoot_ = new Node<>();
// Contains server privileges grouped by server name. Stored separately from other
// privileges, as these should be returned both when listing URI and non-URI
// privileges.
Node<PrincipalPrivilege> serverRoot_ = new Node<>();
public void add(PrincipalPrivilege privilege) {
TPrivilege priv = privilege.toThrift();
List<String> path = toPath(priv);
Node<PrincipalPrivilege> root = getRootForScope(priv.getScope());
root.add(privilege.getName(), privilege, path);
}
public void remove(PrincipalPrivilege privilege) {
TPrivilege priv = privilege.toThrift();
List<String> path = toPath(priv);
Node<PrincipalPrivilege> root = getRootForScope(priv.getScope());
root.remove(privilege.getName(), privilege, path);
}
/**
* Collect all privileges that match the filter.
* E.g. for server1.db1, it returns privileges on:
* server1, server1.db1, server1.db1.table1,
* but not privileges on:
* server2, server2.db1, server1.db2
*/
public List<PrincipalPrivilege> getFilteredList(Filter filter) {
List<String> path = filter.toPath();
List<PrincipalPrivilege> results = new ArrayList<>();
List<Node<PrincipalPrivilege>> roots = new ArrayList<>();
// Server level privileges apply to both URIs and other objects.
roots.add(serverRoot_);
if (filter.isUri_ || path.size() == 1) roots.add(uriRoot_);
if (!filter.isUri_) roots.add(tableRoot_);
for (Node<PrincipalPrivilege> root : roots) {
root.getAllMatchingValues(results, path);
}
return results;
}
private Node<PrincipalPrivilege> getRootForScope(TPrivilegeScope scope) {
switch(scope) {
case URI: return uriRoot_;
case SERVER: return serverRoot_;
default: return tableRoot_;
}
}
/**
* Creates a path to the given privilege in the tree like ["server1", "db1", "table1"].
*/
private static List<String> toPath(TPrivilege priv) {
List<String> path = new ArrayList<>();
String server = priv.getServer_name();
String db = priv.getDb_name();
String table = priv.getTable_name();
if (server == null) return path;
path.add(server.toLowerCase());
if (db == null) return path;
path.add(db.toLowerCase());
if (table != null) path.add(table.toLowerCase());
return path;
}
/**
* Lossy representation of an Authorizable (doesn't contain column/URI name).
* Can be used to rule out the bulk of the privileges that can have no effect on an
* access check.
*/
public static class Filter {
String server_, db_, table_; // must be lower case, null matches everything
boolean isUri_ = false;
public void setServer(String server) { server_ = server; }
public void setDb(String db) { db_ = db; }
public void setTable(String table) { table_ = table; }
public void setIsUri(boolean isUri) { isUri_ = isUri; }
/**
* Creates a path till the first null element of the filter, e.g.
* ["server1", "db1"] if table_ is null.
*/
private List<String> toPath() {
List<String> path = new ArrayList<>();
if (server_ == null) return path;
path.add(server_);
if (db_ == null) return path;
Preconditions.checkState(!isUri_);
path.add(db_);
if (table_ != null) path.add(table_);
return path;
}
}
/**
* Tree node that holds the privileges for a given object (server, database, table),
* and its children objects (e.g. databases for a server). Descendants can be addressed
* with paths like ["server1", "db1", "table1"].
*
* Only used to store privileges, but creating a generic class seemed clearer.
*/
private static class Node<T> {
Map<String, T> values_ = null;
Map<String, Node<T>> children_= null;
boolean isEmpty() { return values_ == null && children_ == null; }
/**
* Finds the Node at 'path' (or potentially builds it if it doesn't exist),
* and adds 'key' + 'value' to it.
*/
public void add(String key, T value, List<String> path) {
add(key, value, path, 0);
}
/**
* Finds the Node at 'path' (it is treated as error if it doesn't exist),
* and removes 'key' + 'value' from it. If a Node becomes empty, it is removed from
* it's parent.
*/
public void remove(String key, T value, List<String> path) {
remove(key, value, path, 0);
}
/**
* Collect all values in this node and those descendants that match 'path'.
*/
public void getAllMatchingValues(List<T> results, List<String> path) {
getAllMatchingValues(results, path, 0);
}
/**
* Collect all values in this node and its descendants.
*/
public void getAllValues(List<T> results) {
if (values_ != null) results.addAll(values_.values());
if (children_ != null) {
for (Map.Entry<String, Node<T>> entry : children_.entrySet()) {
entry.getValue().getAllValues(results);
}
}
}
private void add(String key, T value, List<String> path, int depth) {
if (path.size() <= depth) {
if (values_ == null) {
// It is very common to have only a single privilege on an object
// (e.g. ownership), so the initial capacity is 1 to avoid wasting memory.
values_ = new HashMap<>(1);
}
values_.put(key, value); // Can update an existing value.
} else {
if (children_ == null) children_ = new HashMap<>();
String child_name = path.get(depth);
Node<T> next = children_.computeIfAbsent(child_name, (k) -> new Node<T>());
next.add(key, value, path, depth + 1);
}
}
private void remove(String key, T value, List<String> path, int depth) {
if (path.size() <= depth) {
Preconditions.checkNotNull(values_);
T found = values_.remove(key);
Preconditions.checkNotNull(found);
if (values_.isEmpty()) values_ = null;
} else {
Preconditions.checkNotNull(children_);
String child_name = path.get(depth);
Node<T> next = children_.get(child_name);
Preconditions.checkNotNull(next);
next.remove(key, value, path, depth + 1);
// Remove the child if it became empty.
if (next.isEmpty()) children_.remove(child_name);
if (children_.isEmpty()) children_ = null;
}
}
private void getAllMatchingValues(List<T> results, List<String> path, int depth) {
if (path.size() <= depth) {
getAllValues(results);
return;
}
if (values_ != null) results.addAll(values_.values());
if (children_ == null) return;
String child_name = path.get(depth);
Preconditions.checkNotNull(child_name);
Node<T> next = children_.get(child_name);
if (next != null) next.getAllMatchingValues(results, path, depth + 1);
}
}
}