Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,17 @@ if [[ "${BUILD_FE}" -eq 1 ]]; then
fi
done
unset _conn_mod
# Authorization plugin modules (loaded at runtime from plugins/authorization/). Keep this list
# identical to the deploy loop's (search AUTHZ_PLUGIN_DIR), for the same reason as the connectors:
# the deploy step unzips whatever archive is left in the module's target/, so a module built here
# but not deployed there - or the other way round - ships a stale plugin without failing anything.
# ranger-common is a library the two below depend on; -am builds it, nothing deploys it alone.
for _authz_mod in ranger-doris ranger-hive; do
if [[ -d "${DORIS_HOME}/fe/fe-authorization/fe-authorization-plugins/fe-authorization-plugin-${_authz_mod}" ]]; then
modules+=("fe-authorization/fe-authorization-plugins/fe-authorization-plugin-${_authz_mod}")
fi
done
unset _authz_mod
for extra_module_path in "${FE_EXTRA_MODULE_PATHS[@]}"; do
modules+=("${extra_module_path}")
done
Expand Down Expand Up @@ -1216,6 +1227,28 @@ if [[ "${BUILD_FE}" -eq 1 ]]; then
done
unset CONN_PLUGIN_DIR conn_module conn_plugin_target conn_module_dir conn_zip conn_conf_tpl

# Deploy authorization sources as independent plugin directories.
# Each sub-directory is one source AccessControllerManager can install, named in fe.conf by
# access_controller_type or in a catalog's access_controller.class. Created even when no module
# produced a zip, because it is also where an administrator drops a third-party source.
# Keep the module list identical to the build list's (search _authz_mod).
AUTHZ_PLUGIN_DIR="${DORIS_OUTPUT}/fe/plugins/authorization"
mkdir -p "${AUTHZ_PLUGIN_DIR}"
for authz_module in ranger-doris ranger-hive; do
authz_plugin_target="${AUTHZ_PLUGIN_DIR}/${authz_module}"
authz_module_dir="${DORIS_HOME}/fe/fe-authorization/fe-authorization-plugins/fe-authorization-plugin-${authz_module}"
if [ ! -d "${authz_module_dir}" ]; then
continue
fi
authz_zip="${authz_module_dir}/target/doris-fe-authorization-${authz_module}.zip"
if [ ! -f "${authz_zip}" ]; then
continue
fi
mkdir -p "${authz_plugin_target}"
unzip -o "${authz_zip}" -d "${authz_plugin_target}/"
done
unset AUTHZ_PLUGIN_DIR authz_module authz_plugin_target authz_module_dir authz_zip

# RC-4: self-contain the paimon connector plugin for OSS. The connector sets
# fs.oss.impl=com.aliyun.jindodata.oss.JindoOssFileSystem; that impl lives in the jindofs jars,
# which are packaged from thirdparty by post-build.sh into fe/lib/jindofs (NOT a maven artifact).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
* {@code fe/fe-authentication/pom.xml} in the SAME commit.
*
* <p>{@code Plugin} / {@code PluginFactory} / {@code PluginContext} from fe-extension-spi are frozen here
* too, and identically in the other three families' baselines. They are loaded parent-first for every family
* too, and identically in the other four families' baselines. They are loaded parent-first for every family
* (see {@code ChildFirstClassLoader.DEFAULT_PARENT_FIRST_PACKAGES}), so a change to them breaks all four
* plugin kinds at once — and turns all four baselines red at once, each asking for its own bump.
*
Expand Down
2 changes: 1 addition & 1 deletion fe/fe-authentication/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ under the License.
is what the FE kernel expects of a plugin.

Bump the MAJOR (and zero the minor) in the SAME commit as ANY change to the authentication
SPI surface - additions included. A change to fe-extension-spi means bumping all four
SPI surface - additions included. A change to fe-extension-spi means bumping all five
families. See plan-doc/designs/2026-07-29-plugin-api-version-check-design.md.
-->
<authentication.plugin.api.version>1.0</authentication.plugin.api.version>
Expand Down
57 changes: 57 additions & 0 deletions fe/fe-authorization/fe-authorization-api/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.doris</groupId>
<version>${revision}</version>
<artifactId>fe-authorization</artifactId>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>fe-authorization-api</artifactId>
<packaging>jar</packaging>
<name>Doris FE Authorization API</name>

<!-- No runtime dependencies - pure API module -->
<!-- Test dependencies inherited from parent POM -->

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<includes>
<include>**/*Test.java</include>
</includes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// 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.authorization;

/**
* One privilege an authorization source can grant on a resource.
*
* <p>The constants stand one-to-one with the privileges Doris itself grants, and that is a requirement
* rather than a coincidence: the built-in privilege model is one of the authorization sources, so any
* folding here - "cluster usage and stage usage are both just usage", say - would leave it unable to tell
* which privilege it was actually asked about, and it would answer a different question than the caller
* asked.</p>
*
* <p>There is deliberately no {@code SHOW} action. "May this subject see the object at all" is not a
* privilege anyone grants; it is the question "does the subject hold <em>any</em> of the privileges that
* imply visibility", which {@link AccessRequirement#anyOf} expresses.</p>
*/
public enum AccessAction {
/** Cluster node operations. Only ever granted globally. */
NODE,
/** Administration of the cluster. */
ADMIN,
/** Granting privileges to others. */
GRANT,
/** Reading data. */
SELECT,
/** Writing data. */
LOAD,
/** Altering an object's definition. */
ALTER,
/** Creating an object. */
CREATE,
/** Dropping an object. */
DROP,
/** Using a resource or a workload group. */
USAGE,
/** Using a compute group. Distinct from {@link #USAGE}: it is a privilege of its own. */
CLUSTER_USAGE,
/** Using a stage. Distinct from {@link #USAGE}: it is a privilege of its own. */
STAGE_USAGE,
/** Reading a view's definition. */
SHOW_VIEW
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// 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.authorization;

import java.util.Optional;

/**
* What surrounds one access decision: the circumstances of the statement asking for it, as opposed to who is
* asking ({@link AuthorizedSubject}) or about what ({@link AuthorizedResource}).
*
* <p>This is where a decision that depends on more than the subject and the resource - the address the client
* connected from, the time of day, which statement is running - gets its input. A plugin that decides purely
* from grants ignores it; the built-in model does.</p>
*
* <p>Every value is optional because the checks Doris makes do not all originate from a client statement:
* background jobs and internal maintenance ask the same questions with no connection behind them. Absent
* means "not applicable here", never "denied" - a plugin that requires a signal it did not get must say so
* rather than treat the gap as a decision.</p>
*
* <p>An implementation is expected to be lazy: the engine hands one to every check, so producing a query id
* that nobody reads would be paid for on each of them.</p>
*/
public interface AccessContext {

/** No circumstances known - the check does not come from a client statement. */
AccessContext NONE = new AccessContext() {
@Override
public String toString() {
return "no access context";
}
};

/** The address the client connected from. */
default Optional<String> getClientIp() {
return Optional.empty();
}

/** The id of the query being planned, in the form the engine prints it in logs and profiles. */
default Optional<String> getQueryId() {
return Optional.empty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// 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.authorization;

import java.util.Objects;
import java.util.Optional;

/**
* Refusal of an access request, carrying why it was refused and who refused it.
*
* <p>A refusal is an answer, not a failure: deciding that a user may not see a table is the normal outcome of
* asking about a table the user has no grant on, and listing what a user may see asks that question about
* every object there is. So this exception is built for the common case rather than the exceptional one - it
* <b>records no stack trace</b>, and its message is composed only if somebody reads it. What makes it an
* exception at all is that a refusal must not be silently discardable: a plugin that returns without throwing
* has allowed the access, and there is no third answer to forget to handle.</p>
*
* <p>Who refused, and why, is the part that has nowhere else to live. The engine's own callers each phrase
* their own error message today and so lose it; carrying it here is what later lets an error message name the
* plugin that governs the object and lets an audit record say the same.</p>
*/
public class AccessDeniedException extends Exception {

private final AuthorizedSubject subject;
private final AuthorizedResource resource;
private final AccessRequirement requirement;
private final String deniedBy;
private final String explicitMessage;

private AccessDeniedException(AuthorizedSubject subject, AuthorizedResource resource,
AccessRequirement requirement, String deniedBy, String explicitMessage) {
// No cause, no suppression, and above all no stack trace: filling one in costs more than the whole
// decision that produced it, and this is thrown once per object a user may not see.
super(null, null, false, false);
this.subject = subject;
this.resource = resource;
this.requirement = requirement;
this.deniedBy = deniedBy;
this.explicitMessage = explicitMessage;
}

/**
* The refusal of {@code requirement} on {@code resource}.
*
* @param deniedBy name of the authorization source that refused, or null when it does not identify itself
*/
public static AccessDeniedException of(AuthorizedSubject subject, AuthorizedResource resource,
AccessRequirement requirement, String deniedBy) {
Objects.requireNonNull(subject, "subject is required");
Objects.requireNonNull(resource, "resource is required");
Objects.requireNonNull(requirement, "requirement is required");
return new AccessDeniedException(subject, resource, requirement, deniedBy, null);
}

/**
* A refusal that already has its wording, used where the message is itself the answer - a column check
* refuses by naming the column that failed, and rephrasing it here would lose which one it was.
*/
public static AccessDeniedException withMessage(String message, AuthorizedResource resource,
String deniedBy) {
return new AccessDeniedException(null, resource, null,
deniedBy, Objects.requireNonNull(message, "message is required"));
}

/** What was being accessed. */
public AuthorizedResource getResource() {
return resource;
}

/** What was required on it, absent when the refusal came with its own wording. */
public Optional<AccessRequirement> getRequirement() {
return Optional.ofNullable(requirement);
}

/** The authorization source that refused, when it identifies itself. */
public Optional<String> getDeniedBy() {
return Optional.ofNullable(deniedBy);
}

@Override
public String getMessage() {
if (explicitMessage != null) {
return explicitMessage;
}
StringBuilder message = new StringBuilder("Permission denied: user [").append(subject)
.append("] does not have privilege for [").append(requirement)
.append("] on [").append(resource).append("]");
if (deniedBy != null) {
message.append(", denied by [").append(deniedBy).append("]");
}
return message.toString();
}
}
Loading
Loading