github-actions[bot] commented on code in PR #66770:
URL: https://github.com/apache/doris/pull/66770#discussion_r3793532514
##########
fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerManager.java:
##########
@@ -450,31 +806,31 @@ public boolean checkPrivByAuthInfo(ConnectContext ctx,
AuthorizationInfo authInf
return true;
}
- public Map<String, Optional<DataMaskPolicy>>
evalDataMaskPolicies(UserIdentity currentUser, String
- ctl, String db, String tbl, Set<String> cols) {
- Map<String, Optional<DataMaskPolicy>> res = Maps.newHashMap();
- for (String col : cols) {
- res.put(col, evalDataMaskPolicy(currentUser, ctl, db, tbl, col));
- }
- return res;
- }
-
- public Optional<DataMaskPolicy> evalDataMaskPolicy(UserIdentity
currentUser, String
+ public Optional<DataMaskSpec> evalDataMaskPolicy(UserIdentity currentUser,
String
ctl, String db, String tbl, String col) {
Objects.requireNonNull(currentUser, "require currentUser object");
Objects.requireNonNull(ctl, "require ctl object");
Objects.requireNonNull(db, "require db object");
Objects.requireNonNull(tbl, "require tbl object");
Objects.requireNonNull(col, "require col object");
- return
getAccessControllerOrDefault(ctl).evalDataMaskPolicy(currentUser, ctl, db, tbl,
col.toLowerCase());
+ // Sources are asked about columns in lower case, which is how the
ones that store policies per
+ // column have them written.
+ String column = col.toLowerCase();
+ AuthorizedResource.Table table = AuthorizedResource.table(ctl, db,
tbl);
+ return Optional.ofNullable(controllerOf(table)
+ .getDataMasks(AccessTranslation.subjectOf(currentUser), table,
Review Comment:
[P2] Batch mask lookups before crossing the plugin boundary
The new SPI explicitly accepts all columns at once so a network-backed
source is not called once per column, but this wrapper always sends a
singleton. LogicalCheckPolicy invokes it inside the output-slot loop, and
SQL-cache validation does the same per cached column, so the production path
still makes O(columns) plugin callbacks (and cache hits repeat them). Please
restore a plural manager path, call it once per table, and group cache
revalidation by table; a recording-plugin test should assert one callback for a
multi-column relation.
##########
docker/thirdparties/run-thirdparties-docker.sh:
##########
@@ -1736,11 +1736,36 @@ start_polaris() {
fi
}
+# The Doris plugin jars and service definition used to be curl'ed from inside
+# ranger-admin, with the bucket patched into the tracked scripts by `sed -i`.
+# That both broke on BSD sed and left the working tree dirty, and one flaky
+# download killed the container's `set -e` entrypoint. Fetch them here instead,
+# into the gitignored cache/ dir that the container bind mounts read-only.
+download_ranger_artifacts() {
+ local dest="${ROOT}/docker-compose/ranger/cache"
+ local
url_prefix="https://${s3BucketName}.${s3Endpoint}/regression/docker/ranger-plugins"
+ local name
+
+ mkdir -p "${dest}"
+ for name in ranger-servicedef-doris.json \
+ mysql-connector-java-8.0.25.jar \
+ ranger-doris-plugin-3.0.0-SNAPSHOT.jar; do
+ if [[ -s "${dest}/${name}" ]]; then
+ echo "ranger artifact cached: ${name}"
+ continue
+ fi
+ echo "downloading ${url_prefix}/${name}"
+ curl -fsSL --retry 10 --retry-all-errors --retry-delay 5 \
+ --connect-timeout 30 --speed-limit 1024 --speed-time 120 \
+ -o "${dest}/${name}.part" "${url_prefix}/${name}"
+ mv "${dest}/${name}.part" "${dest}/${name}"
+ done
+}
+
start_ranger() {
echo "RUN_RANGER"
export CONTAINER_UID=${CONTAINER_UID}
- find "${ROOT}/docker-compose/ranger/script" -type f -exec sed -i
"s/s3Endpoint/${s3Endpoint}/g" {} \;
- find "${ROOT}/docker-compose/ranger/script" -type f -exec sed -i
"s/s3BucketName/${s3BucketName}/g" {} \;
+ download_ranger_artifacts
Review Comment:
[P2] Keep Ranger shutdown independent of artifact downloads
`start_ranger` is also the handler for `-c ranger --stop`, but this new call
runs before both `compose_down_stack` and the existing `STOP != 1` gate. With
an empty cache, stopping Ranger now performs three long-retry network downloads
and exits without bringing the stack down whenever the artifact bucket or
network is unavailable. Please move the download into the start-only branch
(before compose up) so shutdown remains a local, reliable operation.
##########
fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/LegacyAccessControllerPlugin.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.doris.mysql.privilege;
+
+import org.apache.doris.analysis.UserIdentity;
+import org.apache.doris.authorization.AccessContext;
+import org.apache.doris.authorization.AccessDeniedException;
+import org.apache.doris.authorization.AccessRequirement;
+import org.apache.doris.authorization.AuthorizedResource;
+import org.apache.doris.authorization.AuthorizedSubject;
+import org.apache.doris.authorization.DataMaskSpec;
+import org.apache.doris.authorization.RowFilterSpec;
+import org.apache.doris.authorization.spi.AuthorizationPlugin;
+import org.apache.doris.common.AuthorizationException;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Presents an access controller written against the older, per-scope
interface as an authorization source.
+ *
+ * <p>That interface asks a separate question per kind of object and answers
each with a boolean; this one
+ * asks a single question about a typed resource and answers by refusing or
not. The translation is the whole
+ * of this class, and it is not a temporary shim: {@code
CatalogAccessController} is what a catalog's
+ * {@code access_controller.class} names, so implementations of it exist
outside this repository and keep
+ * working unchanged.
+ */
+public class LegacyAccessControllerPlugin implements AuthorizationPlugin {
+
+ private final String name;
+ private final CatalogAccessController controller;
+
+ public LegacyAccessControllerPlugin(String name, CatalogAccessController
controller) {
+ this.name = Objects.requireNonNull(name, "name is required");
+ this.controller = Objects.requireNonNull(controller, "controller is
required");
+ }
+
+ /** The controller this presents, for where the controller itself is the
question rather than its answers. */
+ public CatalogAccessController getController() {
+ return controller;
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public void checkPrivilege(AuthorizedSubject subject, AuthorizedResource
resource,
+ AccessRequirement requirement, AccessContext context) throws
AccessDeniedException {
+ UserIdentity currentUser = AccessTranslation.userIdentityOf(subject);
+ PrivPredicate wanted = AccessTranslation.privPredicateOf(requirement);
+ switch (resource.getKind()) {
+ case GLOBAL:
+ refuseUnless(controller.checkGlobalPriv(currentUser, wanted),
subject, resource, requirement);
+ return;
+ case CATALOG:
+ refuseUnless(controller.checkCtlPriv(currentUser,
+ ((AuthorizedResource.Catalog) resource).getCatalog(),
wanted),
+ subject, resource, requirement);
+ return;
+ case DATABASE: {
+ AuthorizedResource.Database database =
(AuthorizedResource.Database) resource;
+ refuseUnless(controller.checkDbPriv(currentUser,
database.getCatalog(),
Review Comment:
[P1] Preserve global grants in the legacy adapter
Before this change the manager computed the instance source's global verdict
and called boolean legacy overloads that returned immediately for a global
grant. This adapter calls only the scoped methods, so an unchanged legacy
source that returns false here now denies a user with global SELECT/ADMIN;
catalog, table, and column checks lose the same override. Legacy sources cannot
opt back in through AuthorizationContext, so please reproduce the old global
short-circuit and cover the globally-allowed/locally-denied upgrade case.
##########
fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerManager.java:
##########
@@ -295,13 +560,111 @@ public Auth getAuth() {
return this.auth;
}
+ /**
+ * Answers whether {@code subject} may act on {@code resource} as {@code
requirement} demands.
+ *
+ * <p>This is the one place a check is routed. Which source is asked
follows from the resource alone -
+ * system-wide objects and catalog-level grants go to the source {@code
access_controller_type}
+ * installs, everything inside a catalog goes to the source that catalog
is bound to - and whatever it
+ * answers is the answer. Combining two sources, or granting anything
before asking, would have to
+ * happen here, and deliberately does not.
+ *
+ * <p>Columns are not decided here: see {@link #decideColumns}.
+ */
+ public boolean decide(UserIdentity subject, AuthorizedResource resource,
AccessRequirement requirement) {
+ if (resource.getKind() == ResourceKind.COLUMNS) {
+ throw new IllegalArgumentException("column access is decided by
decideColumns(), which"
+ + " reports which column was refused instead of a yes or
no");
+ }
+ try {
+ ask(subject, resource, requirement);
+ return true;
+ } catch (AccessDeniedException e) {
+ // The reason travels no further for now: every caller of the
boolean facades phrases its own
+ // error message. It is carried this far so that the day one of
them stops doing so, there is
+ // something to phrase it from.
+ return false;
+ }
+ }
+
+ /**
+ * Checks access to named columns, reporting the column that was refused
rather than a yes or no.
+ *
+ * <p>Kept apart from {@link #decide} because the answer has a different
shape, not because the routing
+ * differs: it is the same source the table itself would be asked about.
+ */
+ public void decideColumns(UserIdentity subject, AuthorizedResource.Columns
columns,
+ AccessRequirement requirement) throws AuthorizationException {
+ try {
+ ask(subject, columns, requirement);
+ } catch (AccessDeniedException e) {
+ throw new AuthorizationException(e.getMessage());
+ }
+ }
+
+ private void ask(UserIdentity subject, AuthorizedResource resource,
AccessRequirement requirement)
+ throws AccessDeniedException {
+
controllerOf(resource).checkPrivilege(AccessTranslation.subjectOf(subject),
resource, requirement,
Review Comment:
[P1] Pin the plugin classloader for runtime callbacks
Only factory creation runs with the plugin loader as TCCL; this query-time
callback runs under the FE thread's app TCCL, as do row filters, masks, and
close. A supported directory plugin that lazily uses
ServiceLoader/Class.forName (or starts a worker here) therefore cannot see its
bundled provider or can bind a parent copy that is incompatible with the
child-loaded interface. The connector plugin paths already wrap every such
boundary for this reason. Please use one try/finally TCCL helper for all
authorization-plugin callbacks and test lazy provider discovery from the plugin
jar.
##########
fe/fe-core/src/main/java/org/apache/doris/policy/RowPolicy.java:
##########
@@ -221,15 +229,39 @@ public boolean isInvalid() {
return (wherePredicate == null);
}
- @Override
- public Expression getFilterExpression() throws AnalysisException {
+ /**
+ * The predicate as SQL text, which is the form the authorization layer
hands to the planner.
+ *
+ * <p>It is the text the administrator wrote, recovered from the stored
statement - not a rendering of
+ * the parsed predicate. Rendering would not survive the round trip:
{@code toSql()} on a compound
+ * predicate produces the diagnostic form {@code AND[a,b]}, which does not
parse back, so any policy
+ * combining two conditions would break.</p>
+ */
+ public String getFilterSql() throws AnalysisException {
if (wherePredicate == null) {
throw new AnalysisException("Invalid row policy [" +
getPolicyIdent() + "], " + getOriginStmt());
}
- return wherePredicate;
+ if (wherePredicateSql == null) {
+ wherePredicateSql = parseWherePredicateSql();
+ }
+ return wherePredicateSql;
+ }
+
+ private String parseWherePredicateSql() throws AnalysisException {
+ try {
+ CreatePolicyCommand command = (CreatePolicyCommand) new
NereidsParser().parseSingle(getOriginStmt());
Review Comment:
[P2] Honor stmtIdx when recovering row-policy SQL
RowPolicy persists both the full `OriginStatement` text and `stmtIdx`, but
this reparses the entire text as one statement and ignores the index. For a
supported full-text-plus-index execution with CREATE ROW POLICY at index 1,
creation has a valid predicate, yet the first governed query reaches
`getFilterSql()`, `parseSingle` fails, and the query is rejected; before this
patch the fresh policy used the already-parsed expression. Please parse all
statements and select `stmtIdx` here and in `gsonPostProcess` (or seed the
exact SQL for fresh objects), and cover index 1 before and after JSON replay.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalCheckPolicy.java:
##########
@@ -180,10 +179,10 @@ public RelatedPolicy findPolicy(LogicalPlan logicalPlan,
CascadesContext cascade
Optional<SqlCacheContext> sqlCacheContext =
statementContext.getSqlCacheContext();
boolean hasDataMask = false;
for (Slot slot : logicalPlan.getOutput()) {
- Optional<DataMaskPolicy> dataMaskPolicy =
accessManager.evalDataMaskPolicy(
+ Optional<DataMaskSpec> dataMaskPolicy =
accessManager.evalDataMaskPolicy(
Review Comment:
[P2] Let the selected plugin decide admin policy exemptions
The root/admin early return above prevents both data-policy callbacks from
ever seeing those subjects, even though the new SPI makes administrator
exemptions the selected source's choice. A plugin can allow the table read
while still returning a tenant filter or sensitive-column mask for these
subjects, but the planner silently drops both and reads unrestricted data.
Please remove or make this exemption plugin-owned, and add a directory-plugin
case that returns a root/admin row filter and mask.
##########
fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerManager.java:
##########
@@ -295,13 +560,111 @@ public Auth getAuth() {
return this.auth;
}
+ /**
+ * Answers whether {@code subject} may act on {@code resource} as {@code
requirement} demands.
+ *
+ * <p>This is the one place a check is routed. Which source is asked
follows from the resource alone -
+ * system-wide objects and catalog-level grants go to the source {@code
access_controller_type}
+ * installs, everything inside a catalog goes to the source that catalog
is bound to - and whatever it
+ * answers is the answer. Combining two sources, or granting anything
before asking, would have to
+ * happen here, and deliberately does not.
+ *
+ * <p>Columns are not decided here: see {@link #decideColumns}.
+ */
+ public boolean decide(UserIdentity subject, AuthorizedResource resource,
AccessRequirement requirement) {
+ if (resource.getKind() == ResourceKind.COLUMNS) {
+ throw new IllegalArgumentException("column access is decided by
decideColumns(), which"
+ + " reports which column was refused instead of a yes or
no");
+ }
+ try {
+ ask(subject, resource, requirement);
+ return true;
+ } catch (AccessDeniedException e) {
+ // The reason travels no further for now: every caller of the
boolean facades phrases its own
+ // error message. It is carried this far so that the day one of
them stops doing so, there is
+ // something to phrase it from.
+ return false;
+ }
+ }
+
+ /**
+ * Checks access to named columns, reporting the column that was refused
rather than a yes or no.
+ *
+ * <p>Kept apart from {@link #decide} because the answer has a different
shape, not because the routing
+ * differs: it is the same source the table itself would be asked about.
+ */
+ public void decideColumns(UserIdentity subject, AuthorizedResource.Columns
columns,
+ AccessRequirement requirement) throws AuthorizationException {
+ try {
+ ask(subject, columns, requirement);
+ } catch (AccessDeniedException e) {
+ throw new AuthorizationException(e.getMessage());
+ }
+ }
+
+ private void ask(UserIdentity subject, AuthorizedResource resource,
AccessRequirement requirement)
+ throws AccessDeniedException {
+
controllerOf(resource).checkPrivilege(AccessTranslation.subjectOf(subject),
resource, requirement,
+ ConnectionAccessContext.current());
Review Comment:
[P2] Preserve an explicit connection's access context
Every ConnectContext overload drops that object and this method rebuilds
context only from the thread local. That loses a known client IP when
authorization happens before the context is installed (HTTP basic/cookie auth
does this) or on an executor thread (metadata listing does this), so an
IP-dependent plugin receives AccessContext.NONE and can refuse a valid client
request. Please thread an explicit AccessContext through decisions, build it
from supplied contexts/request data, and use the thread-local lookup only as a
fallback; cover a supplied non-current context in tests.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]