Custom Authorization Plugin
Besides the built-in authorization model and the Apache Ranger integration, Apache Doris exposes a service provider interface (SPI) so you can plug in your own authorization system. When a custom access controller is registered, Doris routes its privilege checks to your code instead of the built-in GRANT/REVOKE engine, which lets you back Doris authorization with an external policy store such as OpenFGA, OPA, or an in-house entitlement service.
This document explains the two interfaces you implement, how Doris discovers and loads a controller, the configuration that selects one, and a skeleton you can start from. The Ranger integration is the reference implementation and is a good file to read alongside this page.
This page describes the AccessControllerFactory / CatalogAccessController contract in Doris 4.x. The development version has a new authorization SPI and different packaging; do not use its examples with 4.x. Compile plugins against the exact Doris release you deploy.
Applicable Scenarios
| Scenario | Description |
|---|---|
| Central policy store | Authorization decisions already live in an external system (OpenFGA, OPA, a homegrown service) and you want Doris to consult it instead of maintaining a separate copy |
| Relationship-based access | You need fine-grained or relationship-based rules (Zanzibar / ReBAC style) that the built-in RBAC model does not express directly |
| Shared authorization across engines | Several query engines share one policy source and Doris needs to honor the same decisions |
| Custom masking or row filtering | You want to drive column masking or row-level filters from your own policy definitions |
The service provider interface
A custom authorizer is two Java interfaces in the package org.apache.doris.mysql.privilege:
AccessControllerFactorybuilds a controller instance and gives it a stable identifier.CatalogAccessControllerreceives the individual privilege checks.
You implement both, register the factory, and point the configuration at its identifier.
CatalogAccessController
CatalogAccessController is where each authorization decision lands. Doris calls one method per object level. A boolean return of true means the operation is allowed; false means denied.
public interface CatalogAccessController {
// Global
boolean checkGlobalPriv(UserIdentity currentUser, PrivPredicate wanted);
// Catalog
boolean checkCtlPriv(UserIdentity currentUser, String ctl, PrivPredicate wanted);
// Database
boolean checkDbPriv(UserIdentity currentUser, String ctl, String db, PrivPredicate wanted);
// Table
boolean checkTblPriv(UserIdentity currentUser, String ctl, String db, String tbl, PrivPredicate wanted);
// Column (throws when access is denied instead of returning a boolean)
void checkColsPriv(UserIdentity currentUser, String ctl, String db, String tbl,
Set<String> cols, PrivPredicate wanted) throws AuthorizationException;
// Resource
boolean checkResourcePriv(UserIdentity currentUser, String resourceName, PrivPredicate wanted);
// Workload Group
boolean checkWorkloadGroupPriv(UserIdentity currentUser, String workloadGroupName, PrivPredicate wanted);
// Cloud (compute cluster / stage, addressed by ResourceTypeEnum)
boolean checkCloudPriv(UserIdentity currentUser, String cloudName, PrivPredicate wanted, ResourceTypeEnum type);
// Storage Vault
boolean checkStorageVaultPriv(UserIdentity currentUser, String storageVaultName, PrivPredicate wanted);
// Column data masking (return an empty Optional for no masking)
Optional<DataMaskPolicy> evalDataMaskPolicy(UserIdentity currentUser, String ctl, String db, String tbl,
String col);
// Row-level filtering (return an empty list for no filtering)
List<? extends RowFilterPolicy> evalRowFilterPolicies(UserIdentity currentUser, String ctl, String db, String tbl);
}
A few points worth knowing before you implement it:
- The
wantedargument is aPrivPredicate, which groups the concrete privileges an operation needs (for exampleSELECT,LOAD,ADMIN). Map it onto whatever your policy store understands. checkColsPrivis the odd one out. It does not return a boolean; it throwsAuthorizationExceptionwhen access is denied, so a normal return means the columns are allowed.- The interface provides
defaultoverloads ofcheckCtlPriv,checkDbPriv,checkTblPriv, andcheckColsPrivthat take a leadingboolean hasGlobal. The boolean methods first invoke the scoped check and then combine its result withhasGlobal; they do not skip the scoped call. The column overload invokes the scoped check and suppresses anAuthorizationExceptionwhenhasGlobalis true. You usually only implement the methods shown here and let the defaults handle the global short-circuit. evalDataMaskPolicyandevalRowFilterPoliciesare for masking and row filtering. If your system does not drive those, returnOptional.empty()and an empty list. The built-in controller does exactly that.
AccessControllerFactory
AccessControllerFactory is the entry point Doris discovers. It names the provider and builds a controller from a property map.
public interface AccessControllerFactory {
// Identifier used in configuration, for example "ranger-doris".
// Defaults to the simple class name if you do not override it.
default String factoryIdentifier() {
return this.getClass().getSimpleName();
}
CatalogAccessController createAccessController(Map<String, String> prop);
}
The prop map is the configuration passed to your controller (endpoint, credentials, store id, and so on). Where it comes from depends on whether the controller is used for the internal catalog or an external catalog, described under Configuration below.
Registering the plugin
Doris discovers factories in two ways, both wired up in AccessControllerManager.
Classpath service file. Add the fully qualified class name of your factory to the Java ServiceLoader file:
fe/fe-core/src/main/resources/META-INF/services/org.apache.doris.mysql.privilege.AccessControllerFactory
One class name per line. The Ranger factories are already listed there. This path means the plugin is compiled into the FE, so it fits a contribution that lives in the Doris tree.
External plugin directory. Doris also loads factories from an external plugin directory at startup, so you can ship a controller as a standalone jar and drop it in without rebuilding the FE. Package the jar with its own META-INF/services/org.apache.doris.mysql.privilege.AccessControllerFactory entry and place the JAR directly under authorization_plugins_dir (default ${DORIS_HOME}/plugins/authorization). This loader scans root-level JAR files rather than a per-plugin subdirectory. Bundle required dependencies into that JAR and deploy it to every FE, then restart the FEs. This is the lower-friction option when your controller pulls in an SDK you would rather not add to the FE build.
Configuration
How you select a controller depends on which catalog it governs.
Internal catalog
Set the controller for the internal catalog with the FE configuration access_controller_type (in fe.conf). The default is default, which uses the built-in authorizer. ranger-doris selects the Ranger controller. Set it to your factory identifier to make a custom controller the authorizer for the internal catalog:
access_controller_type = your-controller-id
Properties for the internal-catalog controller are read from the file named by authorization_config_file_path (default conf/authorization.conf, and it must sit under DORIS_HOME). The contents are handed to your factory as the prop map.
External catalog
An external catalog carries its own authorizer as catalog properties, so different catalogs can use different controllers. Set two properties when you create or alter the catalog:
access_controller.classis the factory identifier (or the factory class name).access_controller.properties.*carries the controller configuration. Doris strips theaccess_controller.properties.prefix and passes the rest as thepropmap.
For example:
CREATE CATALOG my_catalog PROPERTIES (
"type" = "hms",
"hive.metastore.uris" = "thrift://127.0.0.1:9083",
"access_controller.class" = "your-controller-id",
"access_controller.properties.api_url" = "http://127.0.0.1:8080",
"access_controller.properties.store_id" = "01H...",
"access_controller.properties.token" = "..."
);
Here the controller receives {"api_url": "...", "store_id": "...", "token": "..."} in createAccessController.
A minimal skeleton
This is an outline, not a standalone Java source file: put each public class in its own file, import the referenced Doris and Java types, implement every omitted method, and provide MyPolicyClient. Use the matching release source as the compilation dependency.
The following outline implements the two interfaces with an external policy call stubbed out. Masking and row filtering return empty, which is the right default when your system does relationship or role checks rather than field masking.
package com.example.doris.authz;
public class MyAccessControllerFactory implements AccessControllerFactory {
@Override
public String factoryIdentifier() {
return "my-authz";
}
@Override
public CatalogAccessController createAccessController(Map<String, String> prop) {
return new MyAccessController(prop);
}
}
public class MyAccessController implements CatalogAccessController {
private final MyPolicyClient client;
public MyAccessController(Map<String, String> prop) {
this.client = new MyPolicyClient(prop.get("api_url"), prop.get("token"));
}
@Override
public boolean checkGlobalPriv(UserIdentity currentUser, PrivPredicate wanted) {
return client.isAllowed(currentUser, "global", null, wanted);
}
@Override
public boolean checkTblPriv(UserIdentity currentUser, String ctl, String db, String tbl, PrivPredicate wanted) {
return client.isAllowed(currentUser, "table", ctl + "." + db + "." + tbl, wanted);
}
// checkCtlPriv, checkDbPriv, checkResourcePriv, checkWorkloadGroupPriv,
// checkCloudPriv, checkStorageVaultPriv: same shape as above.
@Override
public void checkColsPriv(UserIdentity currentUser, String ctl, String db, String tbl,
Set<String> cols, PrivPredicate wanted) throws AuthorizationException {
for (String col : cols) {
if (!client.isAllowed(currentUser, "column", ctl + "." + db + "." + tbl + "." + col, wanted)) {
throw new AuthorizationException("Access denied on column " + col);
}
}
}
@Override
public Optional<DataMaskPolicy> evalDataMaskPolicy(UserIdentity currentUser, String ctl, String db, String tbl,
String col) {
return Optional.empty();
}
@Override
public List<? extends RowFilterPolicy> evalRowFilterPolicies(UserIdentity currentUser, String ctl, String db,
String tbl) {
return Collections.emptyList();
}
}
Reference implementation: Ranger
The Ranger integration is the worked example to read. RangerDorisAccessControllerFactory uses the identifier ranger-doris and RangerDorisAccessController implements every check method by building a resource for the target object, mapping each Doris privilege onto a Ranger access type, and asking the Ranger plugin whether the access is allowed. It also implements masking and row filtering on top of Ranger policies. If you are mapping Doris checks onto an external policy engine, its structure translates directly.
Lifecycle and verification
Reuse background resources only when controller properties and policy scope are compatible; do not share one mutable controller across catalogs with different credentials or policy services. If a factory shares an instance, make concurrent access safe. The Ranger factory does this on purpose: each controller starts its own Ranger policy refresher, so RangerDorisAccessControllerFactory returns a single shared controller for all catalogs (see apache/doris#65570). A factory that returns a fresh controller per catalog would leak one refresher thread per catalog. Make the same choice whenever your controller holds resources that should exist once per FE process.
After installing the JAR, first configure a test external catalog. Confirm that an authorized user can query a table and that a denied user receives an access error. Test column denial, unavailable policy services, masking, and row filters before selecting the plugin for the internal catalog. Missing factories or service descriptors prevent the selected controller from being created; inspect the FE startup log and deployment paths. Keep using the built-in controller until the plugin has been validated.