KYLIN-2761 Table Level ACL
Project: http://git-wip-us.apache.org/repos/asf/kylin/repo Commit: http://git-wip-us.apache.org/repos/asf/kylin/commit/a5a8def8 Tree: http://git-wip-us.apache.org/repos/asf/kylin/tree/a5a8def8 Diff: http://git-wip-us.apache.org/repos/asf/kylin/diff/a5a8def8 Branch: refs/heads/master Commit: a5a8def893644ca2b0bcdd8e34ef251d3c028455 Parents: 2d939a5 Author: tttMelody <245915...@qq.com> Authored: Tue Sep 5 15:11:14 2017 +0800 Committer: Hongbin Ma <m...@kyligence.io> Committed: Wed Sep 6 10:49:28 2017 +0800 ---------------------------------------------------------------------- .../apache/kylin/common/KylinConfigBase.java | 4 + .../org/apache/kylin/common/QueryContext.java | 9 + .../main/resources/kylin-defaults.properties | 3 + .../org/apache/kylin/metadata/acl/TableACL.java | 146 ++++++++++++++++ .../kylin/metadata/acl/TableACLManager.java | 114 +++++++++++++ .../apache/kylin/metadata/model/ColumnDesc.java | 3 +- .../apache/kylin/metadata/model/TblColRef.java | 17 +- .../metadata/model/tool/CalciteParser.java | 9 +- .../apache/kylin/metadata/acl/TableACLTest.java | 55 ++++++ .../relnode/OLAPToEnumerableConverter.java | 18 ++ .../query/security/AccessDeniedException.java | 25 +++ .../kylin/query/security/QueryIntercept.java | 30 ++++ .../query/security/QueryInterceptUtil.java | 170 +++++++++++++++++++ .../kylin/query/security/TableLevelACL.java | 79 +++++++++ .../kylin/query/util/PushDownUtilTest.java | 7 + .../rest/controller2/TableAclControllerV2.java | 99 +++++++++++ .../kylin/rest/security/TableIntercept.java | 66 +++++++ .../apache/kylin/rest/service/BasicService.java | 5 + .../apache/kylin/rest/service/QueryService.java | 3 + .../kylin/rest/service/TableACLService.java | 70 ++++++++ .../apache/kylin/rest/util/ValidateUtil.java | 107 ++++++++++++ .../kylin/rest/service/TableACLServiceTest.java | 86 ++++++++++ 22 files changed, 1121 insertions(+), 4 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/kylin/blob/a5a8def8/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java ---------------------------------------------------------------------- diff --git a/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java b/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java index 78edd67..7184553 100644 --- a/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java +++ b/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java @@ -1038,6 +1038,10 @@ abstract public class KylinConfigBase implements Serializable { return getOptionalStringArray("kylin.query.transformers", new String[0]); } + public String[] getQueryIntercept() { + return getOptionalStringArray("kylin.query.intercepts", new String[0]); + } + public long getQueryDurationCacheThreshold() { return Long.parseLong(this.getOptional("kylin.query.cache-threshold-duration", String.valueOf(2000))); } http://git-wip-us.apache.org/repos/asf/kylin/blob/a5a8def8/core-common/src/main/java/org/apache/kylin/common/QueryContext.java ---------------------------------------------------------------------- diff --git a/core-common/src/main/java/org/apache/kylin/common/QueryContext.java b/core-common/src/main/java/org/apache/kylin/common/QueryContext.java index 0b8d519..9e0c33b 100644 --- a/core-common/src/main/java/org/apache/kylin/common/QueryContext.java +++ b/core-common/src/main/java/org/apache/kylin/common/QueryContext.java @@ -34,6 +34,7 @@ public class QueryContext { }; private String queryId; + private String username; private AtomicLong scannedRows = new AtomicLong(); private AtomicLong scannedBytes = new AtomicLong(); @@ -59,6 +60,14 @@ public class QueryContext { this.queryId = queryId; } + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + public long getScannedRows() { return scannedRows.get(); } http://git-wip-us.apache.org/repos/asf/kylin/blob/a5a8def8/core-common/src/main/resources/kylin-defaults.properties ---------------------------------------------------------------------- diff --git a/core-common/src/main/resources/kylin-defaults.properties b/core-common/src/main/resources/kylin-defaults.properties index f0328fa..83ba320 100644 --- a/core-common/src/main/resources/kylin-defaults.properties +++ b/core-common/src/main/resources/kylin-defaults.properties @@ -253,3 +253,6 @@ kylin.engine.spark-conf.spark.hadoop.yarn.timeline-service.enabled=false #kylin.query.pushdown.jdbc.pool-max-total=8 #kylin.query.pushdown.jdbc.pool-max-idle=8 #kylin.query.pushdown.jdbc.pool-min-idle=0 + +### TABLE ACL +kylin.query.intercepts=org.apache.kylin.rest.security.TableIntercept \ No newline at end of file http://git-wip-us.apache.org/repos/asf/kylin/blob/a5a8def8/core-metadata/src/main/java/org/apache/kylin/metadata/acl/TableACL.java ---------------------------------------------------------------------- diff --git a/core-metadata/src/main/java/org/apache/kylin/metadata/acl/TableACL.java b/core-metadata/src/main/java/org/apache/kylin/metadata/acl/TableACL.java new file mode 100644 index 0000000..6d6f158 --- /dev/null +++ b/core-metadata/src/main/java/org/apache/kylin/metadata/acl/TableACL.java @@ -0,0 +1,146 @@ +/* + * 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.kylin.metadata.acl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.kylin.common.persistence.RootPersistentEntity; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonProperty; + +@SuppressWarnings("serial") +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE, + getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE, + setterVisibility = JsonAutoDetect.Visibility.NONE) +public class TableACL extends RootPersistentEntity { + + //user1 : [DB.TABLE1, DB.TABLE2], means that user1 can not query DB.TABLE1, DB.TABLE2 + @JsonProperty() + private Map<String, TableBlackList> userTableBlackList; + + public TableACL() { + userTableBlackList = new HashMap<>(); + } + + public Map<String, TableBlackList> getUserTableBlackList() { + return userTableBlackList; + } + + public List<String> getTableBlackList(String username) { + TableBlackList tableBlackList = userTableBlackList.get(username); + if (tableBlackList == null) { + tableBlackList = new TableBlackList(); + } + return tableBlackList.getTables(); + } + + //get users that can not query the table + public List<String> getBlockedUserByTable(String table) { + List<String> results = new ArrayList<>(); + for (String user : userTableBlackList.keySet()) { + TableBlackList tables = userTableBlackList.get(user); + if (tables.contains(table)) { + results.add(user); + } + } + return results; + } + + public TableACL add(String username, String table) { + if (userTableBlackList == null) { + userTableBlackList = new HashMap<>(); + } + TableBlackList tableBlackList = userTableBlackList.get(username); + + if (tableBlackList == null) { + tableBlackList = new TableBlackList(); + userTableBlackList.put(username, tableBlackList); + } + + //before add, check exists + checkACLExists(username, table, tableBlackList); + tableBlackList.add(table); + return this; + } + + private void checkACLExists(String username, String table, TableBlackList tableBlackList) { + if (tableBlackList.contains(table)) { + throw new RuntimeException("Operation fail, can not revoke user's table query permission.Table ACL " + table + + ":" + username + " already exists!"); + } + } + + public TableACL delete(String username, String table) { + if (isTableInBlackList(username, table)) { + throw new RuntimeException("Operation fail, can not grant user table query permission.Table ACL " + table + + ":" + username + " is not found!"); + } + TableBlackList tableBlackList = userTableBlackList.get(username); + tableBlackList.remove(table); + return this; + } + + private boolean isTableInBlackList(String username, String table) { + return userTableBlackList == null + || userTableBlackList.get(username) == null + || (!userTableBlackList.get(username).contains(table)); + } + + @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE, + getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE, + setterVisibility = JsonAutoDetect.Visibility.NONE) + static class TableBlackList { + @JsonProperty() + List<String> tables; + + TableBlackList() { + tables = new ArrayList<>(); + } + + public int size() { + return tables.size(); + } + + public boolean isEmpty() { + return tables.isEmpty(); + } + + public boolean contains(String s) { + return tables.contains(s); + } + + public boolean add(String s) { + return tables.add(s); + } + + public boolean remove(String s) { + return tables.remove(s); + } + + public List<String> getTables() { + return tables; + } + } +} http://git-wip-us.apache.org/repos/asf/kylin/blob/a5a8def8/core-metadata/src/main/java/org/apache/kylin/metadata/acl/TableACLManager.java ---------------------------------------------------------------------- diff --git a/core-metadata/src/main/java/org/apache/kylin/metadata/acl/TableACLManager.java b/core-metadata/src/main/java/org/apache/kylin/metadata/acl/TableACLManager.java new file mode 100644 index 0000000..53c0843 --- /dev/null +++ b/core-metadata/src/main/java/org/apache/kylin/metadata/acl/TableACLManager.java @@ -0,0 +1,114 @@ +/* + * 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.kylin.metadata.acl; + +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.kylin.common.KylinConfig; +import org.apache.kylin.common.persistence.JsonSerializer; +import org.apache.kylin.common.persistence.ResourceStore; +import org.apache.kylin.common.persistence.Serializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + */ +public class TableACLManager { + + private static final Logger logger = LoggerFactory.getLogger(TableACLManager.class); + + public static final Serializer<TableACL> TABLE_ACL_SERIALIZER = new JsonSerializer<>(TableACL.class); + private static final String DIR_PREFIX = "/table_acl/"; + + // static cached instances + private static final ConcurrentMap<KylinConfig, TableACLManager> CACHE = new ConcurrentHashMap<>(); + + public static TableACLManager getInstance(KylinConfig config) { + TableACLManager r = CACHE.get(config); + if (r != null) { + return r; + } + + synchronized (TableACLManager.class) { + r = CACHE.get(config); + if (r != null) { + return r; + } + try { + r = new TableACLManager(config); + CACHE.put(config, r); + if (CACHE.size() > 1) { + logger.warn("More than one singleton exist"); + } + return r; + } catch (IOException e) { + throw new IllegalStateException("Failed to init CubeDescManager from " + config, e); + } + } + } + + public static void clearCache() { + CACHE.clear(); + } + + public static void clearCache(KylinConfig kylinConfig) { + if (kylinConfig != null) + CACHE.remove(kylinConfig); + } + + // ============================================================================ + + private KylinConfig config; + + private TableACLManager(KylinConfig config) throws IOException { + this.config = config; + } + + public KylinConfig getConfig() { + return config; + } + + public ResourceStore getStore() { + return ResourceStore.getStore(this.config); + } + + public TableACL getTableACL(String project) throws IOException { + String path = DIR_PREFIX + project; + TableACL tableACLRecord = getStore().getResource(path, TableACL.class, TABLE_ACL_SERIALIZER); + if (tableACLRecord == null || tableACLRecord.getUserTableBlackList() == null) { + return new TableACL(); + } + return tableACLRecord; + } + + public void addTableACL(String project, String username, String table) throws IOException { + String path = DIR_PREFIX + project; + TableACL tableACL = getTableACL(project); + getStore().putResource(path, tableACL.add(username, table), System.currentTimeMillis(), TABLE_ACL_SERIALIZER); + } + + public void deleteTableACL(String project, String username, String table) throws IOException { + String path = DIR_PREFIX + project; + TableACL tableACL = getTableACL(project); + getStore().putResource(path, tableACL.delete(username, table), System.currentTimeMillis(), TABLE_ACL_SERIALIZER); + } + +} http://git-wip-us.apache.org/repos/asf/kylin/blob/a5a8def8/core-metadata/src/main/java/org/apache/kylin/metadata/model/ColumnDesc.java ---------------------------------------------------------------------- diff --git a/core-metadata/src/main/java/org/apache/kylin/metadata/model/ColumnDesc.java b/core-metadata/src/main/java/org/apache/kylin/metadata/model/ColumnDesc.java index 394300e..5848838 100644 --- a/core-metadata/src/main/java/org/apache/kylin/metadata/model/ColumnDesc.java +++ b/core-metadata/src/main/java/org/apache/kylin/metadata/model/ColumnDesc.java @@ -193,9 +193,8 @@ public class ColumnDesc implements Serializable { return index; } - public String getComputedColumnExpr(String tableAlias) { + public String getComputedColumnExpr() { Preconditions.checkState(computedColumnExpr != null); - Preconditions.checkState(tableAlias != null); return computedColumnExpr; } http://git-wip-us.apache.org/repos/asf/kylin/blob/a5a8def8/core-metadata/src/main/java/org/apache/kylin/metadata/model/TblColRef.java ---------------------------------------------------------------------- diff --git a/core-metadata/src/main/java/org/apache/kylin/metadata/model/TblColRef.java b/core-metadata/src/main/java/org/apache/kylin/metadata/model/TblColRef.java index e7250a3..cc2dd03 100644 --- a/core-metadata/src/main/java/org/apache/kylin/metadata/model/TblColRef.java +++ b/core-metadata/src/main/java/org/apache/kylin/metadata/model/TblColRef.java @@ -158,7 +158,7 @@ public class TblColRef implements Serializable { if (!column.isComputedColumnn()) { return getIdentity(); } else { - return column.getComputedColumnExpr(getTableAlias()); + return column.getComputedColumnExpr(); } } @@ -243,4 +243,19 @@ public class TblColRef implements Serializable { } } + // return DB.TABLE + public String getTableWithSchema() { + if (isInnerColumn() && parserDescription != null) + return parserDescription; + if (column.getTable() == null) { + return "NULL"; + } else { + return column.getTable().getIdentity().toUpperCase(); + } + } + + // return DB.TABLE.COLUMN + public String getColumWithTableAndSchema() { + return (getTableWithSchema() + "." + column.getName()).toUpperCase(); + } } http://git-wip-us.apache.org/repos/asf/kylin/blob/a5a8def8/core-metadata/src/main/java/org/apache/kylin/metadata/model/tool/CalciteParser.java ---------------------------------------------------------------------- diff --git a/core-metadata/src/main/java/org/apache/kylin/metadata/model/tool/CalciteParser.java b/core-metadata/src/main/java/org/apache/kylin/metadata/model/tool/CalciteParser.java index fdad33a..c49b037 100644 --- a/core-metadata/src/main/java/org/apache/kylin/metadata/model/tool/CalciteParser.java +++ b/core-metadata/src/main/java/org/apache/kylin/metadata/model/tool/CalciteParser.java @@ -32,11 +32,11 @@ import org.apache.calcite.sql.parser.SqlParser; import org.apache.calcite.sql.parser.SqlParserPos; import org.apache.calcite.sql.util.SqlBasicVisitor; import org.apache.calcite.sql.util.SqlVisitor; +import org.apache.kylin.common.util.Pair; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.common.collect.Sets; -import org.apache.kylin.common.util.Pair; public class CalciteParser { public static SqlNode parse(String sql) throws SqlParseException { @@ -64,6 +64,13 @@ public class CalciteParser { return getOnlySelectNode("select " + expr + " from t"); } + public static String getLastNthName(SqlIdentifier id, int n) { + //n = 1 is getting column + //n = 2 is getting table's alias, if has. + //n = 3 is getting database name, if has. + return id.names.get(id.names.size() - n).replace("\"", "").toUpperCase(); + } + public static void ensureNoAliasInExpr(String expr) { SqlNode sqlNode = getExpNode(expr); http://git-wip-us.apache.org/repos/asf/kylin/blob/a5a8def8/core-metadata/src/test/java/org/apache/kylin/metadata/acl/TableACLTest.java ---------------------------------------------------------------------- diff --git a/core-metadata/src/test/java/org/apache/kylin/metadata/acl/TableACLTest.java b/core-metadata/src/test/java/org/apache/kylin/metadata/acl/TableACLTest.java new file mode 100644 index 0000000..cfcfbf4 --- /dev/null +++ b/core-metadata/src/test/java/org/apache/kylin/metadata/acl/TableACLTest.java @@ -0,0 +1,55 @@ +/* + * 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.kylin.metadata.acl; + +import org.junit.Assert; +import org.junit.Test; + +public class TableACLTest { + + @Test + public void testTableACL() { + TableACL empty = new TableACL(); + try { + empty.delete("a", "b"); + } catch (Exception e) { + Assert.assertEquals( + "Operation fail, can not grant user table query permission.Table ACL b:a is not found!", + e.getMessage()); + } + + //add + TableACL tableACL = new TableACL(); + tableACL.add("user1", "table1"); + Assert.assertEquals(1, tableACL.getUserTableBlackList().size()); + + //add duplicated + try { + tableACL.add("user1", "table1"); + } catch (Exception e) { + Assert.assertEquals( + "Operation fail, can not revoke user's table query permission.Table ACL table1:user1 already exists!", + e.getMessage()); + } + + //add + tableACL.add("user2", "table1"); + Assert.assertEquals(2, tableACL.getUserTableBlackList().size()); + } +} http://git-wip-us.apache.org/repos/asf/kylin/blob/a5a8def8/query/src/main/java/org/apache/kylin/query/relnode/OLAPToEnumerableConverter.java ---------------------------------------------------------------------- diff --git a/query/src/main/java/org/apache/kylin/query/relnode/OLAPToEnumerableConverter.java b/query/src/main/java/org/apache/kylin/query/relnode/OLAPToEnumerableConverter.java index c7b0fe2..db934cf 100644 --- a/query/src/main/java/org/apache/kylin/query/relnode/OLAPToEnumerableConverter.java +++ b/query/src/main/java/org/apache/kylin/query/relnode/OLAPToEnumerableConverter.java @@ -35,6 +35,8 @@ import org.apache.calcite.sql.SqlExplainLevel; import org.apache.kylin.common.KylinConfig; import org.apache.kylin.common.util.ClassUtil; import org.apache.kylin.query.routing.RealizationChooser; +import org.apache.kylin.query.security.QueryIntercept; +import org.apache.kylin.query.security.QueryInterceptUtil; import com.google.common.collect.Lists; @@ -71,6 +73,15 @@ public class OLAPToEnumerableConverter extends ConverterImpl implements Enumerab // identify model & realization List<OLAPContext> contexts = listContextsHavingScan(); + + String project = getProject(contexts); + String user = getUser(contexts); + + List<QueryIntercept> intercepts = QueryInterceptUtil.getQueryIntercepts(); + for (QueryIntercept intercept : intercepts) { + intercept.intercept(project, user, contexts); + } + RealizationChooser.selectRealization(contexts); doAccessControl(contexts); @@ -116,4 +127,11 @@ public class OLAPToEnumerableConverter extends ConverterImpl implements Enumerab } } + public String getProject(List<OLAPContext> contexts) { + return contexts.get(0).olapSchema.getProjectName(); + } + + public String getUser(List<OLAPContext> contexts) { + return contexts.get(0).olapAuthen.getUsername(); + } } http://git-wip-us.apache.org/repos/asf/kylin/blob/a5a8def8/query/src/main/java/org/apache/kylin/query/security/AccessDeniedException.java ---------------------------------------------------------------------- diff --git a/query/src/main/java/org/apache/kylin/query/security/AccessDeniedException.java b/query/src/main/java/org/apache/kylin/query/security/AccessDeniedException.java new file mode 100644 index 0000000..de8cd45 --- /dev/null +++ b/query/src/main/java/org/apache/kylin/query/security/AccessDeniedException.java @@ -0,0 +1,25 @@ +/* + * 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.kylin.query.security; + +public class AccessDeniedException extends RuntimeException { + public AccessDeniedException(String s) { + super("Query failed.Access " + s + " denied"); + } +} http://git-wip-us.apache.org/repos/asf/kylin/blob/a5a8def8/query/src/main/java/org/apache/kylin/query/security/QueryIntercept.java ---------------------------------------------------------------------- diff --git a/query/src/main/java/org/apache/kylin/query/security/QueryIntercept.java b/query/src/main/java/org/apache/kylin/query/security/QueryIntercept.java new file mode 100644 index 0000000..493cc25 --- /dev/null +++ b/query/src/main/java/org/apache/kylin/query/security/QueryIntercept.java @@ -0,0 +1,30 @@ +/* + * 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.kylin.query.security; + +import java.util.Collection; +import java.util.List; + +import org.apache.kylin.query.relnode.OLAPContext; + +public interface QueryIntercept { + void intercept(String project, String username, List<OLAPContext> contexts); + + Collection<String> getQueryIdentifiers(List<OLAPContext> contexts); +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/kylin/blob/a5a8def8/query/src/main/java/org/apache/kylin/query/security/QueryInterceptUtil.java ---------------------------------------------------------------------- diff --git a/query/src/main/java/org/apache/kylin/query/security/QueryInterceptUtil.java b/query/src/main/java/org/apache/kylin/query/security/QueryInterceptUtil.java new file mode 100644 index 0000000..ecfa8da --- /dev/null +++ b/query/src/main/java/org/apache/kylin/query/security/QueryInterceptUtil.java @@ -0,0 +1,170 @@ +/* + * 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.kylin.query.security; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.calcite.sql.SqlCall; +import org.apache.calcite.sql.SqlIdentifier; +import org.apache.calcite.sql.SqlNode; +import org.apache.calcite.sql.util.SqlBasicVisitor; +import org.apache.kylin.common.KylinConfig; +import org.apache.kylin.common.util.ClassUtil; +import org.apache.kylin.common.util.Pair; +import org.apache.kylin.metadata.MetadataManager; +import org.apache.kylin.metadata.model.ColumnDesc; +import org.apache.kylin.metadata.model.DataModelDesc; +import org.apache.kylin.metadata.model.TblColRef; +import org.apache.kylin.metadata.model.tool.CalciteParser; +import org.apache.kylin.query.relnode.OLAPContext; + +import com.google.common.base.Preconditions; + +public class QueryInterceptUtil { + private static List<QueryIntercept> queryIntercepts = new ArrayList<>(); + + private static void setQueryIntercept() { + if (queryIntercepts.size() > 0) { + return; + } + String[] classes = KylinConfig.getInstanceFromEnv().getQueryIntercept(); + for (String clz : classes) { + try { + QueryIntercept i = (QueryIntercept) ClassUtil.newInstance(clz); + queryIntercepts.add(i); + } catch (Exception e) { + throw new RuntimeException("Failed to load query intercept", e); + } + } + } + + public static List<QueryIntercept> getQueryIntercepts() { + setQueryIntercept(); + return queryIntercepts; + } + + public static Set<String> getAllColsWithTblAndSchema(String project, List<OLAPContext> contexts) { + // all columns with table and DB. Like DB.TABLE.COLUMN + Set<String> allColWithTblAndSchema = new HashSet<>(); + + for (OLAPContext context : contexts) { + for (TblColRef tblColRef : context.allColumns) { + ColumnDesc columnDesc = tblColRef.getColumnDesc(); + //computed column + if (columnDesc.isComputedColumnn()) { + allColWithTblAndSchema.addAll(getCCUsedCols(project, columnDesc)); + continue; + } + //normal column + allColWithTblAndSchema.add(tblColRef.getColumWithTableAndSchema()); + } + } + return allColWithTblAndSchema; + } + + private static Set<String> getCCUsedCols(String project, ColumnDesc columnDesc) { + Set<String> usedCols = new HashSet<>(); + Map<String, String> aliasTableMap = getAliasTableMap(project, columnDesc.getName()); + Preconditions.checkState(aliasTableMap.size() > 0, "can not find cc:" + columnDesc.getName() + "'s table alias"); + + List<Pair<String, String>> colsWithAlias = ExprIdentifierFinder.getExprIdentifiers(columnDesc.getComputedColumnExpr()); + for (Pair<String, String> cols : colsWithAlias) { + String tableIdentifier = aliasTableMap.get(cols.getFirst()); + usedCols.add(tableIdentifier + "." + cols.getSecond()); + } + //Preconditions.checkState(usedCols.size() > 0, "can not find cc:" + columnDesc.getName() + "'s used cols"); + return usedCols; + } + + private static Map<String, String> getAliasTableMap(String project, String ccName) { + DataModelDesc model = getModel(project, ccName); + Map<String, String> tableWithAlias = new HashMap<>(); + for (String alias : model.getAliasMap().keySet()) { + String tableName = model.getAliasMap().get(alias).getTableDesc().getIdentity(); + tableWithAlias.put(alias, tableName); + } + return tableWithAlias; + } + + private static DataModelDesc getModel(String project, String ccName) { + List<DataModelDesc> models = MetadataManager.getInstance(KylinConfig.getInstanceFromEnv()).getModels(project); + for (DataModelDesc model : models) { + Set<String> computedColumnNames = model.getComputedColumnNames(); + if (computedColumnNames.contains(ccName)) { + return model; + } + } + return null; + } + + static class ExprIdentifierFinder extends SqlBasicVisitor<SqlNode> { + List<Pair<String, String>> columnWithTableAlias; + + ExprIdentifierFinder() { + this.columnWithTableAlias = new ArrayList<>(); + } + + List<Pair<String, String>> getIdentifiers() { + return columnWithTableAlias; + } + + static List<Pair<String, String>> getExprIdentifiers(String expr) { + SqlNode exprNode = CalciteParser.getExpNode(expr); + ExprIdentifierFinder id = new ExprIdentifierFinder(); + exprNode.accept(id); + return id.getIdentifiers(); + } + + @Override + public SqlNode visit(SqlCall call) { + for (SqlNode operand : call.getOperandList()) { + if (operand != null) { + operand.accept(this); + } + } + return null; + } + + @Override + public SqlNode visit(SqlIdentifier id) { + //Preconditions.checkState(id.names.size() == 2, "error when get identifier in cc's expr"); + if (id.names.size() == 2) { + columnWithTableAlias.add(Pair.newPair(id.names.get(0), id.names.get(1))); + } + return null; + } + } + + public static Set<String> getAllTblsWithSchema(List<OLAPContext> contexts) { + // all tables with DB, Like DB.TABLE, may have same table, so use set. + Set<String> tableWithSchema = new HashSet<>(); + for (OLAPContext context : contexts) { + Set<TblColRef> allColumns = context.allColumns; + for (TblColRef tblColRef : allColumns) { + tableWithSchema.add(tblColRef.getTableWithSchema()); + } + } + return tableWithSchema; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/kylin/blob/a5a8def8/query/src/main/java/org/apache/kylin/query/security/TableLevelACL.java ---------------------------------------------------------------------- diff --git a/query/src/main/java/org/apache/kylin/query/security/TableLevelACL.java b/query/src/main/java/org/apache/kylin/query/security/TableLevelACL.java new file mode 100644 index 0000000..c1b67b0 --- /dev/null +++ b/query/src/main/java/org/apache/kylin/query/security/TableLevelACL.java @@ -0,0 +1,79 @@ +/* + * 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.kylin.query.security; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.kylin.metadata.model.TblColRef; +import org.apache.kylin.query.relnode.OLAPContext; + +public class TableLevelACL { + public static void tableFilter(List<OLAPContext> contexts, List<String> tableBlackList) { + Set<String> tableWithSchema = getTableWithSchema(contexts); + for (String tbl : tableBlackList) { + if (tableWithSchema.contains(tbl.toUpperCase())) { +// throw new kylin.AccessDeniedException("table:" + tbl); + System.out.println("Access table:" + tbl + " denied"); + } + } + } + + public static void columnFilter(List<OLAPContext> contexts, List<String> columnBlackList) { + List<String> allColWithTblAndSchema = getAllColWithTblAndSchema(contexts); + for (String tbl : columnBlackList) { + if (allColWithTblAndSchema.contains(tbl.toUpperCase())) { +// throw new kylin.AccessDeniedException("table:" + tbl); + System.out.println("Access table:" + tbl + " denied"); + } + } + } + + public static List<String> getAllColWithTblAndSchema(List<OLAPContext> contexts) { + // all columns with table and DB. Like DB.TABLE.COLUMN + List<String> allColWithTblAndSchema = new ArrayList<>(); + for (OLAPContext context : contexts) { + Set<TblColRef> allColumns = context.allColumns; + for (TblColRef tblColRef : allColumns) { + allColWithTblAndSchema.add(tblColRef.getColumWithTableAndSchema()); + } + } + return allColWithTblAndSchema; + } + + public static Set<String> getTableWithSchema(List<OLAPContext> contexts) { + // all tables with DB, Like DB.TABLE + Set<String> tableWithSchema = new HashSet<>(); + for (OLAPContext context : contexts) { + Set<TblColRef> allColumns = context.allColumns; + for (TblColRef tblColRef : allColumns) { + tableWithSchema.add(tblColRef.getTableWithSchema()); + } + } + return tableWithSchema; + } + + public static List<String> mockTableBlackList() { + List<String> blackList = new ArrayList<>(); + blackList.add("DEFAULT.KYLIN_SALES"); + return blackList; + } +} http://git-wip-us.apache.org/repos/asf/kylin/blob/a5a8def8/query/src/test/java/org/apache/kylin/query/util/PushDownUtilTest.java ---------------------------------------------------------------------- diff --git a/query/src/test/java/org/apache/kylin/query/util/PushDownUtilTest.java b/query/src/test/java/org/apache/kylin/query/util/PushDownUtilTest.java index 63594bf..7802076 100644 --- a/query/src/test/java/org/apache/kylin/query/util/PushDownUtilTest.java +++ b/query/src/test/java/org/apache/kylin/query/util/PushDownUtilTest.java @@ -86,4 +86,11 @@ public class PushDownUtilTest { + "ON at1.c = t3.c " + "WHERE t3.d > 0 " + "ORDER BY t3.e"; Assert.assertEquals(exceptSQL, PushDownUtil.schemaCompletion(sql, "EDW")); } + + @Test + public void testSchemaCompletionWithJoin() throws SqlParseException { + String sql = "select * from t1 join (select * from t2 join (select * from t3))"; + String exceptSQL = "select * from EDW.t1 join (select * from EDW.t2 join (select * from EDW.t3))"; + Assert.assertEquals(exceptSQL, PushDownUtil.schemaCompletion(sql, "EDW")); + } } http://git-wip-us.apache.org/repos/asf/kylin/blob/a5a8def8/server-base/src/main/java/org/apache/kylin/rest/controller2/TableAclControllerV2.java ---------------------------------------------------------------------- diff --git a/server-base/src/main/java/org/apache/kylin/rest/controller2/TableAclControllerV2.java b/server-base/src/main/java/org/apache/kylin/rest/controller2/TableAclControllerV2.java new file mode 100644 index 0000000..a72efa2 --- /dev/null +++ b/server-base/src/main/java/org/apache/kylin/rest/controller2/TableAclControllerV2.java @@ -0,0 +1,99 @@ +/* + * 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.kylin.rest.controller2; + +import java.io.IOException; +import java.util.List; + +import org.apache.kylin.rest.controller.BasicController; +import org.apache.kylin.rest.response.EnvelopeResponse; +import org.apache.kylin.rest.response.ResponseCode; +import org.apache.kylin.rest.service.TableACLService; +import org.apache.kylin.rest.util.ValidateUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +@RequestMapping(value = "/acl") +public class TableAclControllerV2 extends BasicController { + + @Autowired + @Qualifier("TableAclService") + private TableACLService tableACLService; + + @Autowired + @Qualifier("validateUtil") + private ValidateUtil validateUtil; + + @RequestMapping(value = "/table/{project}/{table:.+}", method = {RequestMethod.GET}, produces = {"application/vnd.apache.kylin-v2+json"}) + @ResponseBody + public EnvelopeResponse getTableWhiteListByTable(@PathVariable String project, @PathVariable String table) throws IOException { + validateUtil.vaildateArgs(project, table); + project = project.toUpperCase(); + validateUtil.validateTable(project, table); + List<String> allUsers = validateUtil.getAllUsers(); + List<String> whiteList = tableACLService.getTableWhiteListByTable(project, table, allUsers); + return new EnvelopeResponse(ResponseCode.CODE_SUCCESS, whiteList, "get table acl"); + } + + @RequestMapping(value = "/table/{project}/black/{table:.+}", method = {RequestMethod.GET}, produces = {"application/vnd.apache.kylin-v2+json"}) + @ResponseBody + public EnvelopeResponse getTableBlackListByTable(@PathVariable String project, @PathVariable String table) throws IOException { + validateUtil.vaildateArgs(project, table); + project = project.toUpperCase(); + validateUtil.validateTable(project, table); + List<String> blackList = tableACLService.getBlockedUserByTable(project, table); + return new EnvelopeResponse(ResponseCode.CODE_SUCCESS, blackList, "get table acl"); + } + + // because the frontend passes user can not visit, so that means put it to the table black list + @RequestMapping(value = "/table/{project}/{table}/{username}", method = {RequestMethod.DELETE}, produces = {"application/vnd.apache.kylin-v2+json"}) + @ResponseBody + public EnvelopeResponse putUserToTableBlackList( + @PathVariable String project, + @PathVariable String table, + @PathVariable String username) throws IOException { + validateUtil.vaildateArgs(project, table, username); + project = project.toUpperCase(); + validateUtil.validateUser(username); + validateUtil.validateTable(project, table); + tableACLService.addToTableBlackList(project, username, table); + return new EnvelopeResponse(ResponseCode.CODE_SUCCESS, "", "revoke user table query permission and add user to table black list."); + } + + // because the frontend passes user can visit, so that means remove the user from the table black list + @RequestMapping(value = "/table/{project}/{table}/{username}", method = {RequestMethod.POST}, produces = {"application/vnd.apache.kylin-v2+json"}) + @ResponseBody + public EnvelopeResponse deleteUserFromTableBlackList( + @PathVariable String project, + @PathVariable String table, + @PathVariable String username) throws IOException { + validateUtil.vaildateArgs(project, table, username); + project = project.toUpperCase(); + validateUtil.validateUser(username); + validateUtil.validateTable(project, table); + tableACLService.deleteFromTableBlackList(project, username, table); + return new EnvelopeResponse(ResponseCode.CODE_SUCCESS, "", "grant user table query permission and remove user from table black list."); + } +} http://git-wip-us.apache.org/repos/asf/kylin/blob/a5a8def8/server-base/src/main/java/org/apache/kylin/rest/security/TableIntercept.java ---------------------------------------------------------------------- diff --git a/server-base/src/main/java/org/apache/kylin/rest/security/TableIntercept.java b/server-base/src/main/java/org/apache/kylin/rest/security/TableIntercept.java new file mode 100644 index 0000000..a980658 --- /dev/null +++ b/server-base/src/main/java/org/apache/kylin/rest/security/TableIntercept.java @@ -0,0 +1,66 @@ +/* + * 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.kylin.rest.security; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.apache.kylin.common.KylinConfig; +import org.apache.kylin.metadata.acl.TableACLManager; +import org.apache.kylin.query.relnode.OLAPContext; +import org.apache.kylin.query.security.AccessDeniedException; +import org.apache.kylin.query.security.QueryIntercept; +import org.apache.kylin.query.security.QueryInterceptUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TableIntercept implements QueryIntercept{ + private static final Logger logger = LoggerFactory.getLogger(QueryIntercept.class); + + @Override + public void intercept(String project, String username, List<OLAPContext> contexts) { + List<String> userTableBlackList = getTableBlackList(project, username); + if (userTableBlackList.isEmpty()) { + return; + } + Set<String> queryTbls = getQueryIdentifiers(contexts); + for (String tbl : userTableBlackList) { + if (queryTbls.contains(tbl.toUpperCase())) { + throw new AccessDeniedException("table:" + tbl); + } + } + } + + @Override + public Set<String> getQueryIdentifiers(List<OLAPContext> contexts) { + return QueryInterceptUtil.getAllTblsWithSchema(contexts); + } + + private List<String> getTableBlackList(String project, String username) { + List<String> tableBlackList = new ArrayList<>(); + try { + tableBlackList = TableACLManager.getInstance(KylinConfig.getInstanceFromEnv()).getTableACL(project).getTableBlackList(username); + } catch (IOException e) { + logger.error("get table black list fail. " + e.getMessage()); + } + return tableBlackList; + } +} http://git-wip-us.apache.org/repos/asf/kylin/blob/a5a8def8/server-base/src/main/java/org/apache/kylin/rest/service/BasicService.java ---------------------------------------------------------------------- diff --git a/server-base/src/main/java/org/apache/kylin/rest/service/BasicService.java b/server-base/src/main/java/org/apache/kylin/rest/service/BasicService.java index 66e7cfb..f662042 100644 --- a/server-base/src/main/java/org/apache/kylin/rest/service/BasicService.java +++ b/server-base/src/main/java/org/apache/kylin/rest/service/BasicService.java @@ -25,6 +25,7 @@ import org.apache.kylin.cube.CubeDescManager; import org.apache.kylin.cube.CubeManager; import org.apache.kylin.job.execution.ExecutableManager; import org.apache.kylin.metadata.MetadataManager; +import org.apache.kylin.metadata.acl.TableACLManager; import org.apache.kylin.metadata.badquery.BadQueryHistoryManager; import org.apache.kylin.metadata.draft.DraftManager; import org.apache.kylin.metadata.project.ProjectManager; @@ -84,4 +85,8 @@ public abstract class BasicService { return DraftManager.getInstance(getConfig()); } + public TableACLManager getTableACLManager() { + return TableACLManager.getInstance(getConfig()); + } + } http://git-wip-us.apache.org/repos/asf/kylin/blob/a5a8def8/server-base/src/main/java/org/apache/kylin/rest/service/QueryService.java ---------------------------------------------------------------------- diff --git a/server-base/src/main/java/org/apache/kylin/rest/service/QueryService.java b/server-base/src/main/java/org/apache/kylin/rest/service/QueryService.java index 51ec902..f57aeb1 100644 --- a/server-base/src/main/java/org/apache/kylin/rest/service/QueryService.java +++ b/server-base/src/main/java/org/apache/kylin/rest/service/QueryService.java @@ -509,6 +509,8 @@ public class QueryService extends BasicService { conn = QueryConnection.getConnection(sqlRequest.getProject()); String userInfo = SecurityContextHolder.getContext().getAuthentication().getName(); + QueryContext context = QueryContext.current(); + context.setUsername(userInfo); final Collection<? extends GrantedAuthority> grantedAuthorities = SecurityContextHolder.getContext() .getAuthentication().getAuthorities(); for (GrantedAuthority grantedAuthority : grantedAuthorities) { @@ -842,6 +844,7 @@ public class QueryService extends BasicService { close(resultSet, stat, null); //conn is passed in, not my duty to close } + return buildSqlResponse(isPushDown, results, columnMetas); } http://git-wip-us.apache.org/repos/asf/kylin/blob/a5a8def8/server-base/src/main/java/org/apache/kylin/rest/service/TableACLService.java ---------------------------------------------------------------------- diff --git a/server-base/src/main/java/org/apache/kylin/rest/service/TableACLService.java b/server-base/src/main/java/org/apache/kylin/rest/service/TableACLService.java new file mode 100644 index 0000000..3f1b515 --- /dev/null +++ b/server-base/src/main/java/org/apache/kylin/rest/service/TableACLService.java @@ -0,0 +1,70 @@ +/* + * 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.kylin.rest.service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.kylin.metadata.acl.TableACL; +import org.apache.kylin.rest.util.AclEvaluate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component("TableAclService") +public class TableACLService extends BasicService { + private static final Logger logger = LoggerFactory.getLogger(TableACLService.class); + + @Autowired + private AclEvaluate aclEvaluate; + + // cuz in the frontend shows user can visit the table, but in the backend stored user that can not visit the table + public List<String> getTableWhiteListByTable(String project, String table, List<String> allUsers) + throws IOException { + List<String> blockedUsers = getBlockedUserByTable(project, table); + List<String> whiteUsers = new ArrayList<>(); + for (String u : allUsers) { + if (!blockedUsers.contains(u)) { + whiteUsers.add(u); + } + } + return whiteUsers; + } + + public TableACL getTableACLByProject(String project) throws IOException { + return getTableACLManager().getTableACL(project); + } + + public List<String> getBlockedUserByTable(String project, String table) throws IOException { + aclEvaluate.checkProjectWritePermission(project); + return getTableACLByProject(project).getBlockedUserByTable(table); + } + + public void addToTableBlackList(String project, String username, String table) throws IOException { + aclEvaluate.checkProjectAdminPermission(project); + getTableACLManager().addTableACL(project, username, table); + } + + public void deleteFromTableBlackList(String project, String username, String table) throws IOException { + aclEvaluate.checkProjectAdminPermission(project); + getTableACLManager().deleteTableACL(project, username, table); + } +} http://git-wip-us.apache.org/repos/asf/kylin/blob/a5a8def8/server-base/src/main/java/org/apache/kylin/rest/util/ValidateUtil.java ---------------------------------------------------------------------- diff --git a/server-base/src/main/java/org/apache/kylin/rest/util/ValidateUtil.java b/server-base/src/main/java/org/apache/kylin/rest/util/ValidateUtil.java new file mode 100644 index 0000000..a95bdd4 --- /dev/null +++ b/server-base/src/main/java/org/apache/kylin/rest/util/ValidateUtil.java @@ -0,0 +1,107 @@ +/* + * 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.kylin.rest.util; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.apache.kylin.metadata.model.ColumnDesc; +import org.apache.kylin.metadata.model.TableDesc; +import org.apache.kylin.rest.security.ManagedUser; +import org.apache.kylin.rest.service.TableService; +import org.apache.kylin.rest.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; + +import com.google.common.base.Preconditions; + +@Component("validateUtil") +public class ValidateUtil { + + @Autowired + @Qualifier("userService") + private UserService userService; + + @Autowired + @Qualifier("tableService") + private TableService tableService; + + public void validateUser(String username) { + if (!userService.userExists(username)) { + throw new RuntimeException("Operation failed, user:" + username + " not exists"); + } + } + + public void validateTable(String project, String table) throws IOException { + List<TableDesc> tableDescs = tableService.getTableDescByProject(project, false); + List<String> tables = new ArrayList<>(); + for (TableDesc tableDesc : tableDescs) { + tables.add(tableDesc.getDatabase() + "." + tableDesc.getName()); + } + + if (!tables.contains(table)) { + throw new RuntimeException("Operation failed, table:" + table + " not exists"); + } + } + + public void validateColumn(String project, String table, Collection<String> columns) throws IOException { + Preconditions.checkState(columns != null && columns.size() > 0); + List<String> cols = getAllColumns(project, table); + for (String c : columns) { + if (!cols.contains(c)) { + throw new RuntimeException("Operation failed, column:" + c + " not exists"); + } + } + } + + private List<String> getAllColumns(String project, String table) throws IOException { + List<TableDesc> tableDescByProject = tableService.getTableDescByProject(project, true); + List<String> cols = new ArrayList<>(); + + for (TableDesc tableDesc : tableDescByProject) { + String tbl = tableDesc.getDatabase() + "." + tableDesc.getName(); + if (tbl.equals(table)) { + for (ColumnDesc column : tableDesc.getColumns()) { + cols.add(column.getName()); + } + break; + } + } + return cols; + } + + public List<String > getAllUsers() throws IOException { + List<ManagedUser> managedUsers = userService.listUsers(); + List<String> allUsers = new ArrayList<>(); + for (ManagedUser managedUser : managedUsers) { + allUsers.add(managedUser.getUsername()); + } + return allUsers; + } + + public void vaildateArgs(String... args) { + for (String arg : args) { + Preconditions.checkState(!StringUtils.isEmpty(arg)); + } + } +} http://git-wip-us.apache.org/repos/asf/kylin/blob/a5a8def8/server/src/test/java/org/apache/kylin/rest/service/TableACLServiceTest.java ---------------------------------------------------------------------- diff --git a/server/src/test/java/org/apache/kylin/rest/service/TableACLServiceTest.java b/server/src/test/java/org/apache/kylin/rest/service/TableACLServiceTest.java new file mode 100644 index 0000000..248d967 --- /dev/null +++ b/server/src/test/java/org/apache/kylin/rest/service/TableACLServiceTest.java @@ -0,0 +1,86 @@ +/* + * 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.kylin.rest.service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.kylin.metadata.acl.TableACL; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; + +public class TableACLServiceTest extends ServiceTestBase { + private final static String PROJECT = "learn_kylin"; + + @Autowired + @Qualifier("TableAclService") + private TableACLService tableACLService; + + @Test + public void testTableACL() throws IOException { + TableACL emptyBlackList = tableACLService.getTableACLByProject(PROJECT); + Assert.assertEquals(0, emptyBlackList.getUserTableBlackList().size()); + + tableACLService.addToTableBlackList(PROJECT, "ADMIN", "DB.TABLE"); + tableACLService.addToTableBlackList(PROJECT, "ADMIN", "DB.TABLE1"); + tableACLService.addToTableBlackList(PROJECT, "ADMIN", "DB.TABLE2"); + tableACLService.addToTableBlackList(PROJECT, "MODELER", "DB.TABLE4"); + tableACLService.addToTableBlackList(PROJECT, "MODELER", "DB.TABLE1"); + tableACLService.addToTableBlackList(PROJECT, "MODELER", "DB.TABLE"); + tableACLService.addToTableBlackList(PROJECT, "ANALYST", "DB.TABLE"); + tableACLService.addToTableBlackList(PROJECT, "ANALYST", "DB.TABLE1"); + tableACLService.addToTableBlackList(PROJECT, "ANALYST", "DB.TABLE2"); + tableACLService.addToTableBlackList(PROJECT, "ANALYST", "DB.TABLE4"); + List<String> tableBlackList = tableACLService.getBlockedUserByTable(PROJECT, "DB.TABLE1"); + Assert.assertEquals(3, tableBlackList.size()); + + //test get black/white list + List<String> allUsers = new ArrayList<>(); + allUsers.add("ADMIN"); + allUsers.add("MODELER"); + allUsers.add("ANALYST"); + allUsers.add("user4"); + allUsers.add("user5"); + allUsers.add("user6"); + allUsers.add("user7"); + List<String> tableWhiteList = tableACLService.getTableWhiteListByTable(PROJECT, "DB.TABLE1", allUsers); + Assert.assertEquals(4, tableWhiteList.size()); + + List<String> emptyTableBlackList = tableACLService.getBlockedUserByTable(PROJECT, "DB.T"); + Assert.assertEquals(0, emptyTableBlackList.size()); + + List<String> tableWhiteList1 = tableACLService.getTableWhiteListByTable(PROJECT, "DB.T", allUsers); + Assert.assertEquals(7, tableWhiteList1.size()); + + //test add + tableACLService.addToTableBlackList(PROJECT, "user7", "DB.T7"); + List<String> tableBlackList2 = tableACLService.getBlockedUserByTable(PROJECT, "DB.T7"); + Assert.assertTrue(tableBlackList2.contains("user7")); + + //test delete + tableACLService.deleteFromTableBlackList(PROJECT, "user7", "DB.T7"); + List<String> tableBlackList3 = tableACLService.getBlockedUserByTable(PROJECT, "DB.T7"); + Assert.assertFalse(tableBlackList3.contains("user7")); + + } + +}