SuUserCommand.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.nereids.trees.plans.commands;
import org.apache.doris.analysis.StmtType;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Env;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.FeNameFormat;
import org.apache.doris.mysql.privilege.Auth;
import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.trees.plans.PlanType;
import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.qe.StmtExecutor;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* SU 'user'[@'host'] WITH ROLES ('role1'[, ...]) [WORKLOAD GROUP 'wg']
*
* <p>Session-narrowed identity switch, MySQL's proxy-user model with a mandatory role list: the
* session becomes the TARGET identity (audit, current_user(), user-bound row policies, user
* properties) with a role set that REPLACES every role source, enforced at the single choke point
* {@code Auth.getRolesByUserWithLdap}. Session-only state; nothing is persisted.
*
* <p>Contract:
* <ul>
* <li>Allowed only for accounts holding {@code PROXY_PRIV} (or {@code ADMIN_PRIV}, which implies
* it); a session that does not switch keeps the switching account's own grants.</li>
* <li>Narrowing only: every requested role must already be GRANTED to the target user, so the
* session can never exceed the person's real authority.</li>
* <li>One-shot: a switched session cannot SU again, and nothing widens it. A connection reset
* reverts to the authenticated identity ({@link ConnectContext#revertSessionNarrowing}).</li>
* </ul>
*/
public class SuUserCommand extends Command implements NoForward {
private static final Logger LOG = LogManager.getLogger(SuUserCommand.class);
private final UserIdentity userIdentity;
private final List<String> roles;
private final String workloadGroup; // nullable
public SuUserCommand(UserIdentity userIdentity, List<String> roles, String workloadGroup) {
super(PlanType.SU_USER_COMMAND);
Preconditions.checkArgument(!roles.isEmpty(), "SU requires at least one role");
this.userIdentity = Objects.requireNonNull(userIdentity, "userIdentity is null");
this.roles = Objects.requireNonNull(roles, "roles is null");
this.workloadGroup = workloadGroup;
}
@Override
public void run(ConnectContext ctx, StmtExecutor executor) throws Exception {
// a switched session must never widen or re-target itself
if (ctx.getAuthenticatedIdentity() != null) {
throw new AnalysisException("SU is not allowed in an already-switched session");
}
if (!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ctx, PrivPredicate.PROXY)) {
throw new AnalysisException("Access denied: SU requires PROXY_PRIV (or ADMIN_PRIV)");
}
userIdentity.analyze();
for (String role : roles) {
FeNameFormat.checkRoleName(role, true /* can be admin */, "Invalid role in SU");
}
Auth auth = Env.getCurrentEnv().getAuth();
Set<String> targetRoles = auth.getGrantedRoleNames(userIdentity);
if (targetRoles.isEmpty()) {
// every existing user holds at least its default role
throw new AnalysisException("SU target user does not exist: " + userIdentity);
}
// narrowing only: the requested roles must be a subset of the target's granted roles
for (String role : roles) {
if (!targetRoles.contains(role)) {
throw new AnalysisException("SU role '" + role + "' is not granted to the target user"
+ " -- SU can only narrow, never mint authority");
}
}
UserIdentity authenticated = ctx.getCurrentUserIdentity();
ctx.setAuthenticatedIdentity(authenticated);
ctx.setCurrentUserIdentity(userIdentity);
ctx.setSessionRoleOverride(ImmutableSet.copyOf(roles));
if (!Strings.isNullOrEmpty(workloadGroup)) {
// Placement only. USAGE on the session's workload group (this clause, or the
// target's default_workload_group when omitted) is checked per query against the
// narrowed set widened with the target's own granted roles
// (Auth.getRolesForWorkloadGroupCheck): the person's lane follows the person.
ctx.getSessionVariable().setWorkloadGroup(workloadGroup);
}
// the switch itself: authenticated + effective identity, roles, connection -- the
// statement also lands in the audit log under the effective user
LOG.info("SU: connection {} narrowed. authenticated={} effective={} roles={} workloadGroup={}",
ctx.getConnectionId(), authenticated, userIdentity, roles,
workloadGroup == null ? "<unchanged>" : workloadGroup);
}
@Override
public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
return visitor.visitCommand(this, context);
}
@Override
public StmtType stmtType() {
return StmtType.OTHER;
}
public UserIdentity getUserIdentity() {
return userIdentity;
}
public List<String> getRoles() {
return roles;
}
public String getWorkloadGroup() {
return workloadGroup;
}
}