LegacyAccessControllerPlugin.java
// 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(),
database.getDatabase(), wanted), subject, resource, requirement);
return;
}
case TABLE: {
AuthorizedResource.Table table = (AuthorizedResource.Table) resource;
refuseUnless(controller.checkTblPriv(currentUser, table.getCatalog(), table.getDatabase(),
table.getTable(), wanted), subject, resource, requirement);
return;
}
case COLUMNS: {
AuthorizedResource.Columns columns = (AuthorizedResource.Columns) resource;
try {
controller.checkColsPriv(currentUser, columns.getCatalog(), columns.getDatabase(),
columns.getTable(), columns.getColumns(), wanted);
} catch (AuthorizationException e) {
// The message names the column that failed; that is the answer, so it is carried over
// as written rather than restated in terms of the whole column set. As the bare wording,
// not as rendered - the engine wraps it in an AuthorizationException again on the way
// out, and that class prefixes its own error code when it renders.
throw AccessDeniedException.withMessage(e.getDetailMessage(), resource, name);
}
return;
}
case RESOURCE:
refuseUnless(controller.checkResourcePriv(currentUser,
((AuthorizedResource.Named) resource).getName(), wanted),
subject, resource, requirement);
return;
case WORKLOAD_GROUP:
refuseUnless(controller.checkWorkloadGroupPriv(currentUser,
((AuthorizedResource.Named) resource).getName(), wanted),
subject, resource, requirement);
return;
case STORAGE_VAULT:
refuseUnless(controller.checkStorageVaultPriv(currentUser,
((AuthorizedResource.Named) resource).getName(), wanted),
subject, resource, requirement);
return;
case CLOUD_GENERAL:
case CLOUD_COMPUTE_GROUP:
case CLOUD_STAGE:
case CLOUD_STORAGE_VAULT:
refuseUnless(controller.checkCloudPriv(currentUser,
((AuthorizedResource.Named) resource).getName(), wanted,
AccessTranslation.cloudTypeOf(resource.getKind())), subject, resource, requirement);
return;
default:
throw new IllegalStateException("access controller " + name + " has no method answering for"
+ " resource kind " + resource.getKind());
}
}
private void refuseUnless(boolean allowed, AuthorizedSubject subject, AuthorizedResource resource,
AccessRequirement requirement) throws AccessDeniedException {
if (!allowed) {
throw AccessDeniedException.of(subject, resource, requirement, name);
}
}
@Override
public List<RowFilterSpec> getRowFilters(AuthorizedSubject subject, AuthorizedResource.Table table,
AccessContext context) {
return controller.evalRowFilterPolicies(AccessTranslation.userIdentityOf(subject), table.getCatalog(),
table.getDatabase(), table.getTable());
}
@Override
public Map<String, DataMaskSpec> getDataMasks(AuthorizedSubject subject, AuthorizedResource.Table table,
Set<String> columns, AccessContext context) {
UserIdentity currentUser = AccessTranslation.userIdentityOf(subject);
Map<String, DataMaskSpec> masks = new HashMap<>();
for (String column : columns) {
// One question per column, which is what the older interface offers. A source reached over the
// network pays for that per column of every table in the statement; implementing the batch
// method directly is how a plugin stops paying it.
Optional<DataMaskSpec> mask = controller.evalDataMaskPolicy(currentUser, table.getCatalog(),
table.getDatabase(), table.getTable(), column);
mask.ifPresent(spec -> masks.put(column, spec));
}
return masks;
}
@Override
public void close() {
controller.close();
}
}