diff --git a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisLexer.g4 b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisLexer.g4 index 12a73beb2c5559..24423fbab74014 100644 --- a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisLexer.g4 +++ b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisLexer.g4 @@ -86,6 +86,7 @@ AS: 'AS'; ASC: 'ASC'; AT: 'AT'; AUTHORS: 'AUTHORS'; +AUTHENTICATION: 'AUTHENTICATION'; AUTO: 'AUTO'; AUTO_INCREMENT: 'AUTO_INCREMENT'; ALWAYS: 'ALWAYS'; @@ -294,6 +295,7 @@ IGNORE: 'IGNORE'; IMMEDIATE: 'IMMEDIATE'; IN: 'IN'; INCREMENTAL: 'INCREMENTAL'; +INTEGRATION: 'INTEGRATION'; INDEX: 'INDEX'; INDEXES: 'INDEXES'; INFILE: 'INFILE'; diff --git a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4 b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4 index 4276eaf55b46d0..5d0172fcdf5e08 100644 --- a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4 +++ b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4 @@ -208,6 +208,8 @@ supportedCreateStatement LIKE existedTable=multipartIdentifier (WITH ROLLUP (rollupNames=identifierList)?)? #createTableLike | CREATE ROLE (IF NOT EXISTS)? name=identifierOrText (COMMENT STRING_LITERAL)? #createRole + | CREATE AUTHENTICATION INTEGRATION (IF NOT EXISTS)? integrationName=identifier + properties=propertyClause commentSpec? #createAuthenticationIntegration | CREATE WORKLOAD GROUP (IF NOT EXISTS)? name=identifierOrText (FOR computeGroup=identifierOrText)? properties=propertyClause? #createWorkloadGroup | CREATE CATALOG (IF NOT EXISTS)? catalogName=identifier @@ -292,6 +294,12 @@ supportedAlterStatement properties=propertyClause? #alterComputeGroup | ALTER CATALOG name=identifier SET PROPERTIES LEFT_PAREN propertyItemList RIGHT_PAREN #alterCatalogProperties + | ALTER AUTHENTICATION INTEGRATION integrationName=identifier + SET properties=propertyClause #alterAuthenticationIntegrationProperties + | ALTER AUTHENTICATION INTEGRATION integrationName=identifier + UNSET properties=propertyKeyClause #alterAuthenticationIntegrationUnsetProperties + | ALTER AUTHENTICATION INTEGRATION integrationName=identifier + SET COMMENT comment=STRING_LITERAL #alterAuthenticationIntegrationComment | ALTER WORKLOAD POLICY name=identifierOrText properties=propertyClause? #alterWorkloadPolicy | ALTER SQL_BLOCK_RULE name=identifier properties=propertyClause? #alterSqlBlockRule @@ -335,6 +343,7 @@ supportedDropStatement | DROP STORAGE POLICY (IF EXISTS)? name=identifier #dropStoragePolicy | DROP WORKLOAD GROUP (IF EXISTS)? name=identifierOrText (FOR computeGroup=identifierOrText)? #dropWorkloadGroup | DROP CATALOG (IF EXISTS)? name=identifier #dropCatalog + | DROP AUTHENTICATION INTEGRATION (IF EXISTS)? name=identifier #dropAuthenticationIntegration | DROP FILE name=STRING_LITERAL ((FROM | IN) database=identifier)? properties=propertyClause #dropFile | DROP WORKLOAD POLICY (IF EXISTS)? name=identifierOrText #dropWorkloadPolicy @@ -1454,6 +1463,10 @@ propertyClause : PROPERTIES LEFT_PAREN fileProperties=propertyItemList RIGHT_PAREN ; +propertyKeyClause + : PROPERTIES LEFT_PAREN keys+=propertyKey (COMMA keys+=propertyKey)* RIGHT_PAREN + ; + propertyItemList : properties+=propertyItem (COMMA properties+=propertyItem)* ; @@ -1955,6 +1968,7 @@ nonReserved | ARRAY | AT | AUTHORS + | AUTHENTICATION | AUTO_INCREMENT | BACKENDS | BACKUP @@ -2106,6 +2120,7 @@ nonReserved | IGNORE | IMMEDIATE | INCREMENTAL + | INTEGRATION | INDEXES | INSERT | INVERTED diff --git a/fe/fe-core/src/main/java/org/apache/doris/authentication/AuthenticationIntegrationMeta.java b/fe/fe-core/src/main/java/org/apache/doris/authentication/AuthenticationIntegrationMeta.java new file mode 100644 index 00000000000000..6fb64a55a95471 --- /dev/null +++ b/fe/fe-core/src/main/java/org/apache/doris/authentication/AuthenticationIntegrationMeta.java @@ -0,0 +1,162 @@ +// 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.authentication; + +import org.apache.doris.common.DdlException; +import org.apache.doris.common.io.Text; +import org.apache.doris.common.io.Writable; +import org.apache.doris.persist.gson.GsonUtils; + +import com.google.gson.annotations.SerializedName; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Persistent metadata for AUTHENTICATION INTEGRATION. + */ +public class AuthenticationIntegrationMeta implements Writable { + public static final String TYPE_PROPERTY = "type"; + + @SerializedName(value = "n") + private String name; + @SerializedName(value = "t") + private String type; + @SerializedName(value = "p") + private Map properties; + @SerializedName(value = "c") + private String comment; + + private AuthenticationIntegrationMeta() { + this.name = ""; + this.type = ""; + this.properties = Collections.emptyMap(); + this.comment = null; + } + + public AuthenticationIntegrationMeta(String name, String type, Map properties, String comment) { + this.name = Objects.requireNonNull(name, "name can not be null"); + this.type = Objects.requireNonNull(type, "type can not be null"); + this.properties = Collections.unmodifiableMap( + new LinkedHashMap<>(Objects.requireNonNull(properties, "properties can not be null"))); + this.comment = comment; + } + + /** + * Build metadata from CREATE SQL arguments. + */ + public static AuthenticationIntegrationMeta fromCreateSql( + String integrationName, Map properties, String comment) throws DdlException { + if (properties == null || properties.isEmpty()) { + throw new DdlException("Property 'type' is required in CREATE AUTHENTICATION INTEGRATION"); + } + String type = null; + Map copiedProperties = new LinkedHashMap<>(); + for (Map.Entry entry : properties.entrySet()) { + String key = Objects.requireNonNull(entry.getKey(), "property key can not be null"); + if (TYPE_PROPERTY.equalsIgnoreCase(key)) { + if (type != null) { + throw new DdlException("Property 'type' is duplicated in CREATE AUTHENTICATION INTEGRATION"); + } + type = entry.getValue(); + continue; + } + copiedProperties.put(key, entry.getValue()); + } + if (type == null || type.isEmpty()) { + throw new DdlException("Property 'type' is required in CREATE AUTHENTICATION INTEGRATION"); + } + return new AuthenticationIntegrationMeta(integrationName, type, copiedProperties, comment); + } + + /** + * Build a new metadata object after ALTER ... SET PROPERTIES. + */ + public AuthenticationIntegrationMeta withAlterProperties(Map propertiesDelta) throws DdlException { + if (propertiesDelta == null || propertiesDelta.isEmpty()) { + throw new DdlException("ALTER AUTHENTICATION INTEGRATION should contain at least one property"); + } + for (String key : propertiesDelta.keySet()) { + if (TYPE_PROPERTY.equalsIgnoreCase(key)) { + throw new DdlException("ALTER AUTHENTICATION INTEGRATION does not allow modifying property 'type'"); + } + } + Map mergedProperties = new LinkedHashMap<>(properties); + mergedProperties.putAll(propertiesDelta); + return new AuthenticationIntegrationMeta(name, type, mergedProperties, comment); + } + + /** + * Build a new metadata object after ALTER ... UNSET PROPERTIES. + */ + public AuthenticationIntegrationMeta withUnsetProperties(Set propertiesToUnset) throws DdlException { + if (propertiesToUnset == null || propertiesToUnset.isEmpty()) { + throw new DdlException("ALTER AUTHENTICATION INTEGRATION should contain at least one property"); + } + Map reducedProperties = new LinkedHashMap<>(properties); + for (String key : propertiesToUnset) { + if (TYPE_PROPERTY.equalsIgnoreCase(key)) { + throw new DdlException("ALTER AUTHENTICATION INTEGRATION does not allow modifying property 'type'"); + } + reducedProperties.remove(key); + } + return new AuthenticationIntegrationMeta(name, type, reducedProperties, comment); + } + + public AuthenticationIntegrationMeta withComment(String newComment) { + return new AuthenticationIntegrationMeta(name, type, properties, newComment); + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public Map getProperties() { + return properties; + } + + public String getComment() { + return comment; + } + + public Map toSqlPropertiesView() { + Map allProperties = new LinkedHashMap<>(); + allProperties.put(TYPE_PROPERTY, type); + allProperties.putAll(properties); + return allProperties; + } + + @Override + public void write(DataOutput out) throws IOException { + Text.writeString(out, GsonUtils.GSON.toJson(this)); + } + + public static AuthenticationIntegrationMeta read(DataInput in) throws IOException { + return GsonUtils.GSON.fromJson(Text.readString(in), AuthenticationIntegrationMeta.class); + } +} diff --git a/fe/fe-core/src/main/java/org/apache/doris/authentication/AuthenticationIntegrationMgr.java b/fe/fe-core/src/main/java/org/apache/doris/authentication/AuthenticationIntegrationMgr.java new file mode 100644 index 00000000000000..6b8218830545bf --- /dev/null +++ b/fe/fe-core/src/main/java/org/apache/doris/authentication/AuthenticationIntegrationMgr.java @@ -0,0 +1,204 @@ +// 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.authentication; + +import org.apache.doris.common.DdlException; +import org.apache.doris.common.io.Text; +import org.apache.doris.common.io.Writable; +import org.apache.doris.persist.DropAuthenticationIntegrationOperationLog; +import org.apache.doris.persist.gson.GsonUtils; + +import com.google.gson.annotations.SerializedName; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Manager for AUTHENTICATION INTEGRATION metadata. + */ +public class AuthenticationIntegrationMgr implements Writable { + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); + + @SerializedName(value = "nTi") + private Map nameToIntegration = new LinkedHashMap<>(); + + private void readLock() { + lock.readLock().lock(); + } + + private void readUnlock() { + lock.readLock().unlock(); + } + + private void writeLock() { + lock.writeLock().lock(); + } + + private void writeUnlock() { + lock.writeLock().unlock(); + } + + public void createAuthenticationIntegration( + String integrationName, boolean ifNotExists, Map properties, String comment) + throws DdlException { + AuthenticationIntegrationMeta meta = + AuthenticationIntegrationMeta.fromCreateSql(integrationName, properties, comment); + writeLock(); + try { + if (nameToIntegration.containsKey(integrationName)) { + if (ifNotExists) { + return; + } + throw new DdlException("Authentication integration " + integrationName + " already exists"); + } + nameToIntegration.put(integrationName, meta); + // TODO(authentication-integration): Re-enable edit log persistence + // when authentication integration is fully integrated. + // Env.getCurrentEnv().getEditLog().logCreateAuthenticationIntegration(meta); + } finally { + writeUnlock(); + } + } + + public void alterAuthenticationIntegrationProperties( + String integrationName, Map properties) throws DdlException { + writeLock(); + try { + AuthenticationIntegrationMeta current = getOrThrow(integrationName); + AuthenticationIntegrationMeta updated = current.withAlterProperties(properties); + nameToIntegration.put(integrationName, updated); + // TODO(authentication-integration): Re-enable edit log persistence + // when authentication integration is fully integrated. + // Env.getCurrentEnv().getEditLog().logAlterAuthenticationIntegration(updated); + } finally { + writeUnlock(); + } + } + + public void alterAuthenticationIntegrationUnsetProperties( + String integrationName, Set propertiesToUnset) throws DdlException { + writeLock(); + try { + AuthenticationIntegrationMeta current = getOrThrow(integrationName); + AuthenticationIntegrationMeta updated = current.withUnsetProperties(propertiesToUnset); + nameToIntegration.put(integrationName, updated); + // TODO(authentication-integration): Re-enable edit log persistence + // when authentication integration is fully integrated. + // Env.getCurrentEnv().getEditLog().logAlterAuthenticationIntegration(updated); + } finally { + writeUnlock(); + } + } + + public void alterAuthenticationIntegrationComment(String integrationName, String comment) throws DdlException { + writeLock(); + try { + AuthenticationIntegrationMeta current = getOrThrow(integrationName); + AuthenticationIntegrationMeta updated = current.withComment(comment); + nameToIntegration.put(integrationName, updated); + // TODO(authentication-integration): Re-enable edit log persistence + // when authentication integration is fully integrated. + // Env.getCurrentEnv().getEditLog().logAlterAuthenticationIntegration(updated); + } finally { + writeUnlock(); + } + } + + public void dropAuthenticationIntegration(String integrationName, boolean ifExists) throws DdlException { + writeLock(); + try { + if (!nameToIntegration.containsKey(integrationName)) { + if (ifExists) { + return; + } + throw new DdlException("Authentication integration " + integrationName + " does not exist"); + } + nameToIntegration.remove(integrationName); + // TODO(authentication-integration): Re-enable edit log persistence + // when authentication integration is fully integrated. + // Env.getCurrentEnv().getEditLog().logDropAuthenticationIntegration( + // new DropAuthenticationIntegrationOperationLog(integrationName)); + } finally { + writeUnlock(); + } + } + + public void replayCreateAuthenticationIntegration(AuthenticationIntegrationMeta meta) { + writeLock(); + try { + nameToIntegration.put(meta.getName(), meta); + } finally { + writeUnlock(); + } + } + + public void replayAlterAuthenticationIntegration(AuthenticationIntegrationMeta meta) { + writeLock(); + try { + nameToIntegration.put(meta.getName(), meta); + } finally { + writeUnlock(); + } + } + + public void replayDropAuthenticationIntegration(DropAuthenticationIntegrationOperationLog log) { + writeLock(); + try { + nameToIntegration.remove(log.getIntegrationName()); + } finally { + writeUnlock(); + } + } + + public Map getAuthenticationIntegrations() { + readLock(); + try { + return Collections.unmodifiableMap(new LinkedHashMap<>(nameToIntegration)); + } finally { + readUnlock(); + } + } + + @Override + public void write(DataOutput out) throws IOException { + Text.writeString(out, GsonUtils.GSON.toJson(this)); + } + + public static AuthenticationIntegrationMgr read(DataInput in) throws IOException { + String json = Text.readString(in); + AuthenticationIntegrationMgr mgr = GsonUtils.GSON.fromJson(json, AuthenticationIntegrationMgr.class); + if (mgr.nameToIntegration == null) { + mgr.nameToIntegration = new LinkedHashMap<>(); + } + return mgr; + } + + private AuthenticationIntegrationMeta getOrThrow(String integrationName) throws DdlException { + AuthenticationIntegrationMeta meta = nameToIntegration.get(integrationName); + if (meta == null) { + throw new DdlException("Authentication integration " + integrationName + " does not exist"); + } + return meta; + } +} diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java b/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java index e68b71e9f9d101..73f8e705358e7d 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java +++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java @@ -27,6 +27,7 @@ import org.apache.doris.analysis.DistributionDesc; import org.apache.doris.analysis.Expr; import org.apache.doris.analysis.SlotRef; +import org.apache.doris.authentication.AuthenticationIntegrationMgr; import org.apache.doris.backup.BackupHandler; import org.apache.doris.backup.RestoreJob; import org.apache.doris.binlog.BinlogGcer; @@ -375,6 +376,7 @@ public class Env { private RoutineLoadManager routineLoadManager; private GroupCommitManager groupCommitManager; private SqlBlockRuleMgr sqlBlockRuleMgr; + private AuthenticationIntegrationMgr authenticationIntegrationMgr; private ExportMgr exportMgr; private Alter alter; private ConsistencyChecker consistencyChecker; @@ -704,6 +706,7 @@ public Env(boolean isCheckpointCatalog) { this.routineLoadManager = EnvFactory.getInstance().createRoutineLoadManager(); this.groupCommitManager = new GroupCommitManager(); this.sqlBlockRuleMgr = new SqlBlockRuleMgr(); + this.authenticationIntegrationMgr = new AuthenticationIntegrationMgr(); this.exportMgr = new ExportMgr(); this.alter = new Alter(); this.consistencyChecker = new ConsistencyChecker(); @@ -2488,6 +2491,17 @@ public long loadSqlBlockRule(DataInputStream in, long checksum) throws IOExcepti return checksum; } + public long loadAuthenticationIntegrations(DataInputStream in, long checksum) throws IOException { + // TODO(authentication-integration): Re-enable image persistence + // when authentication integration is fully integrated. + // Consume persisted bytes to keep image stream alignment, + // but do not restore into in-memory state for now. + AuthenticationIntegrationMgr.read(in); + authenticationIntegrationMgr = new AuthenticationIntegrationMgr(); + LOG.info("skip replay authentication integrations from image temporarily"); + return checksum; + } + /** * Load policy through file. **/ @@ -2800,6 +2814,14 @@ public long saveSqlBlockRule(CountingDataOutputStream out, long checksum) throws return checksum; } + public long saveAuthenticationIntegrations(CountingDataOutputStream out, long checksum) throws IOException { + // TODO(authentication-integration): Re-enable image persistence + // when authentication integration is fully integrated. + // Persist an empty manager temporarily. + new AuthenticationIntegrationMgr().write(out); + return checksum; + } + public long savePolicy(CountingDataOutputStream out, long checksum) throws IOException { Env.getCurrentEnv().getPolicyMgr().write(out); return checksum; @@ -5152,6 +5174,10 @@ public SqlBlockRuleMgr getSqlBlockRuleMgr() { return sqlBlockRuleMgr; } + public AuthenticationIntegrationMgr getAuthenticationIntegrationMgr() { + return authenticationIntegrationMgr; + } + public RoutineLoadTaskScheduler getRoutineLoadTaskScheduler() { return routineLoadTaskScheduler; } diff --git a/fe/fe-core/src/main/java/org/apache/doris/journal/JournalEntity.java b/fe/fe-core/src/main/java/org/apache/doris/journal/JournalEntity.java index 76b4578f892c2b..b5e2b930de4953 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/journal/JournalEntity.java +++ b/fe/fe-core/src/main/java/org/apache/doris/journal/JournalEntity.java @@ -21,6 +21,7 @@ import org.apache.doris.alter.BatchAlterJobPersistInfo; import org.apache.doris.alter.IndexChangeJob; import org.apache.doris.analysis.UserIdentity; +import org.apache.doris.authentication.AuthenticationIntegrationMeta; import org.apache.doris.backup.BackupJob; import org.apache.doris.backup.Repository; import org.apache.doris.backup.RestoreJob; @@ -88,6 +89,7 @@ import org.apache.doris.persist.DatabaseInfo; import org.apache.doris.persist.DictionaryDecreaseVersionInfo; import org.apache.doris.persist.DictionaryIncreaseVersionInfo; +import org.apache.doris.persist.DropAuthenticationIntegrationOperationLog; import org.apache.doris.persist.DropDbInfo; import org.apache.doris.persist.DropDictionaryPersistInfo; import org.apache.doris.persist.DropInfo; @@ -708,6 +710,17 @@ public void readFields(DataInput in) throws IOException { isRead = true; break; } + case OperationType.OP_CREATE_AUTHENTICATION_INTEGRATION: + case OperationType.OP_ALTER_AUTHENTICATION_INTEGRATION: { + data = AuthenticationIntegrationMeta.read(in); + isRead = true; + break; + } + case OperationType.OP_DROP_AUTHENTICATION_INTEGRATION: { + data = DropAuthenticationIntegrationOperationLog.read(in); + isRead = true; + break; + } case OperationType.OP_MODIFY_TABLE_ENGINE: { data = ModifyTableEngineOperationLog.read(in); isRead = true; diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java index b104f33207607b..b9b3bf32a6ea88 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java @@ -635,6 +635,7 @@ import org.apache.doris.nereids.trees.plans.commands.AdminSetReplicaStatusCommand; import org.apache.doris.nereids.trees.plans.commands.AdminSetReplicaVersionCommand; import org.apache.doris.nereids.trees.plans.commands.AdminSetTableStatusCommand; +import org.apache.doris.nereids.trees.plans.commands.AlterAuthenticationIntegrationCommand; import org.apache.doris.nereids.trees.plans.commands.AlterCatalogCommentCommand; import org.apache.doris.nereids.trees.plans.commands.AlterCatalogPropertiesCommand; import org.apache.doris.nereids.trees.plans.commands.AlterCatalogRenameCommand; @@ -676,6 +677,7 @@ import org.apache.doris.nereids.trees.plans.commands.Command; import org.apache.doris.nereids.trees.plans.commands.Constraint; import org.apache.doris.nereids.trees.plans.commands.CopyIntoCommand; +import org.apache.doris.nereids.trees.plans.commands.CreateAuthenticationIntegrationCommand; import org.apache.doris.nereids.trees.plans.commands.CreateCatalogCommand; import org.apache.doris.nereids.trees.plans.commands.CreateDatabaseCommand; import org.apache.doris.nereids.trees.plans.commands.CreateDictionaryCommand; @@ -707,6 +709,7 @@ import org.apache.doris.nereids.trees.plans.commands.DeleteFromUsingCommand; import org.apache.doris.nereids.trees.plans.commands.DescribeCommand; import org.apache.doris.nereids.trees.plans.commands.DropAnalyzeJobCommand; +import org.apache.doris.nereids.trees.plans.commands.DropAuthenticationIntegrationCommand; import org.apache.doris.nereids.trees.plans.commands.DropCachedStatsCommand; import org.apache.doris.nereids.trees.plans.commands.DropCatalogCommand; import org.apache.doris.nereids.trees.plans.commands.DropCatalogRecycleBinCommand; @@ -2117,6 +2120,18 @@ public Map visitPropertyItemList(PropertyItemListContext ctx) { return propertiesMap.build(); } + @Override + public Set visitPropertyKeyClause(DorisParser.PropertyKeyClauseContext ctx) { + if (ctx == null || ctx.keys == null) { + return ImmutableSet.of(); + } + ImmutableSet.Builder propertyKeys = ImmutableSet.builder(); + for (PropertyKeyContext propertyKey : ctx.keys) { + propertyKeys.add(parsePropertyKey(propertyKey)); + } + return propertyKeys.build(); + } + @Override public BrokerDesc visitWithRemoteStorageSystem(WithRemoteStorageSystemContext ctx) { BrokerDesc brokerDesc = null; @@ -5038,6 +5053,15 @@ private String parsePropertyValue(PropertyValueContext item) { return item.getText(); } + private boolean containsPropertyKeyIgnoreCase(Iterable propertyKeys, String expectedKey) { + for (String key : propertyKeys) { + if (key.equalsIgnoreCase(expectedKey)) { + return true; + } + } + return false; + } + private ExplainLevel parseExplainPlanType(PlanTypeContext planTypeContext) { if (planTypeContext == null || planTypeContext.ALL() != null) { return ExplainLevel.ALL_PLAN; @@ -7033,6 +7057,19 @@ public LogicalPlan visitCreateCatalog(CreateCatalogContext ctx) { return new CreateCatalogCommand(catalogName, ifNotExists, resourceName, comment, properties); } + @Override + public LogicalPlan visitCreateAuthenticationIntegration( + DorisParser.CreateAuthenticationIntegrationContext ctx) { + boolean ifNotExists = ctx.IF() != null; + String integrationName = stripQuotes(ctx.integrationName.getText()); + Map properties = Maps.newHashMap(visitPropertyClause(ctx.properties)); + if (!containsPropertyKeyIgnoreCase(properties.keySet(), "type")) { + throw new ParseException("Property 'type' is required in CREATE AUTHENTICATION INTEGRATION", ctx); + } + String comment = ctx.commentSpec() == null ? null : stripQuotes(ctx.commentSpec().STRING_LITERAL().getText()); + return new CreateAuthenticationIntegrationCommand(integrationName, ifNotExists, properties, comment); + } + @Override public LogicalPlan visitShowStages(ShowStagesContext ctx) { return new ShowStagesCommand(); @@ -7069,6 +7106,38 @@ public LogicalPlan visitAlterCatalogProperties(AlterCatalogPropertiesContext ctx return new AlterCatalogPropertiesCommand(catalogName, properties); } + @Override + public LogicalPlan visitAlterAuthenticationIntegrationProperties( + DorisParser.AlterAuthenticationIntegrationPropertiesContext ctx) { + String integrationName = stripQuotes(ctx.integrationName.getText()); + Map properties = Maps.newHashMap(visitPropertyClause(ctx.properties)); + if (containsPropertyKeyIgnoreCase(properties.keySet(), "type")) { + throw new ParseException( + "ALTER AUTHENTICATION INTEGRATION does not allow modifying property 'type'", ctx); + } + return AlterAuthenticationIntegrationCommand.forSetProperties(integrationName, properties); + } + + @Override + public LogicalPlan visitAlterAuthenticationIntegrationUnsetProperties( + DorisParser.AlterAuthenticationIntegrationUnsetPropertiesContext ctx) { + String integrationName = stripQuotes(ctx.integrationName.getText()); + Set unsetProperties = visitPropertyKeyClause(ctx.properties); + if (containsPropertyKeyIgnoreCase(unsetProperties, "type")) { + throw new ParseException( + "ALTER AUTHENTICATION INTEGRATION does not allow modifying property 'type'", ctx); + } + return AlterAuthenticationIntegrationCommand.forUnsetProperties(integrationName, unsetProperties); + } + + @Override + public LogicalPlan visitAlterAuthenticationIntegrationComment( + DorisParser.AlterAuthenticationIntegrationCommentContext ctx) { + String integrationName = stripQuotes(ctx.integrationName.getText()); + String comment = stripQuotes(ctx.comment.getText()); + return AlterAuthenticationIntegrationCommand.forSetComment(integrationName, comment); + } + @Override public RecoverTableCommand visitRecoverTable(RecoverTableContext ctx) { List dbTblNameParts = visitMultipartIdentifier(ctx.name); @@ -7187,6 +7256,14 @@ public LogicalPlan visitDropCatalog(DropCatalogContext ctx) { return new DropCatalogCommand(catalogName, ifExists); } + @Override + public LogicalPlan visitDropAuthenticationIntegration( + DorisParser.DropAuthenticationIntegrationContext ctx) { + String integrationName = stripQuotes(ctx.name.getText()); + boolean ifExists = ctx.EXISTS() != null; + return new DropAuthenticationIntegrationCommand(ifExists, integrationName); + } + @Override public LogicalPlan visitCreateEncryptkey(CreateEncryptkeyContext ctx) { List nameParts = visitMultipartIdentifier(ctx.multipartIdentifier()); diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilderForEncryption.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilderForEncryption.java index 3c9eea1d386e89..0555ea108dc3ec 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilderForEncryption.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilderForEncryption.java @@ -132,6 +132,18 @@ public LogicalPlan visitCreateStorageVault(DorisParser.CreateStorageVaultContext return super.visitCreateStorageVault(ctx); } + // create authentication integration clause + @Override + public LogicalPlan visitCreateAuthenticationIntegration(DorisParser.CreateAuthenticationIntegrationContext ctx) { + if (ctx.properties != null && ctx.properties.fileProperties != null) { + DorisParser.PropertyClauseContext propertyClauseContext = ctx.properties; + encryptProperty(visitPropertyClause(propertyClauseContext), + propertyClauseContext.fileProperties.start.getStartIndex(), + propertyClauseContext.fileProperties.stop.getStopIndex()); + } + return super.visitCreateAuthenticationIntegration(ctx); + } + // alter storage vault clause @Override public LogicalPlan visitAlterStorageVault(DorisParser.AlterStorageVaultContext ctx) { @@ -144,6 +156,19 @@ public LogicalPlan visitAlterStorageVault(DorisParser.AlterStorageVaultContext c return super.visitAlterStorageVault(ctx); } + // alter authentication integration properties clause + @Override + public LogicalPlan visitAlterAuthenticationIntegrationProperties( + DorisParser.AlterAuthenticationIntegrationPropertiesContext ctx) { + if (ctx.properties != null && ctx.properties.fileProperties != null) { + DorisParser.PropertyClauseContext propertyClauseContext = ctx.properties; + encryptProperty(visitPropertyClause(propertyClauseContext), + propertyClauseContext.fileProperties.start.getStartIndex(), + propertyClauseContext.fileProperties.stop.getStopIndex()); + } + return super.visitAlterAuthenticationIntegrationProperties(ctx); + } + // select from tvf @Override public LogicalPlan visitTableValuedFunction(DorisParser.TableValuedFunctionContext ctx) { diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java index 5b89fa7b89bd59..e413a53ead5a6e 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java @@ -155,6 +155,7 @@ public enum PlanType { COPY_INTO_COMMAND, CREATE_POLICY_COMMAND, CREATE_TABLE_COMMAND, + CREATE_AUTHENTICATION_INTEGRATION_COMMAND, CREATE_DICTIONARY_COMMAND, DROP_DICTIONARY_COMMAND, CREATE_SQL_BLOCK_RULE_COMMAND, @@ -183,10 +184,12 @@ public enum PlanType { CANCEL_JOB_COMMAND, DROP_CATALOG_COMMAND, DROP_DATABASE_COMMAND, + DROP_AUTHENTICATION_INTEGRATION_COMMAND, DROP_JOB_COMMAND, RESUME_JOB_COMMAND, ALTER_MTMV_COMMAND, ALTER_CATALOG_PROPERTIES_COMMAND, + ALTER_AUTHENTICATION_INTEGRATION_COMMAND, ADD_CONSTRAINT_COMMAND, ADMIN_COMPACT_TABLE_COMMAND, DROP_CONSTRAINT_COMMAND, diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AlterAuthenticationIntegrationCommand.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AlterAuthenticationIntegrationCommand.java new file mode 100644 index 00000000000000..2955d43dc54030 --- /dev/null +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AlterAuthenticationIntegrationCommand.java @@ -0,0 +1,135 @@ +// 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.catalog.Env; +import org.apache.doris.common.AnalysisException; +import org.apache.doris.common.ErrorCode; +import org.apache.doris.common.ErrorReport; +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 java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * ALTER AUTHENTICATION INTEGRATION command entry. + */ +public class AlterAuthenticationIntegrationCommand extends AlterCommand implements NeedAuditEncryption { + /** alter action. */ + public enum AlterType { + SET_PROPERTIES, + UNSET_PROPERTIES, + SET_COMMENT + } + + private final String integrationName; + private final AlterType alterType; + private final Map properties; + private final Set unsetProperties; + private final String comment; + + private AlterAuthenticationIntegrationCommand(String integrationName, AlterType alterType, + Map properties, Set unsetProperties, String comment) { + super(PlanType.ALTER_AUTHENTICATION_INTEGRATION_COMMAND); + this.integrationName = Objects.requireNonNull(integrationName, "integrationName can not be null"); + this.alterType = Objects.requireNonNull(alterType, "alterType can not be null"); + this.properties = Collections.unmodifiableMap( + new LinkedHashMap<>(Objects.requireNonNull(properties, "properties can not be null"))); + this.unsetProperties = Collections.unmodifiableSet( + new LinkedHashSet<>(Objects.requireNonNull(unsetProperties, "unsetProperties can not be null"))); + this.comment = comment; + } + + public static AlterAuthenticationIntegrationCommand forSetProperties(String integrationName, + Map properties) { + return new AlterAuthenticationIntegrationCommand( + integrationName, AlterType.SET_PROPERTIES, properties, Collections.emptySet(), null); + } + + public static AlterAuthenticationIntegrationCommand forUnsetProperties(String integrationName, + Set unsetProperties) { + return new AlterAuthenticationIntegrationCommand( + integrationName, AlterType.UNSET_PROPERTIES, Collections.emptyMap(), unsetProperties, null); + } + + public static AlterAuthenticationIntegrationCommand forSetComment(String integrationName, String comment) { + return new AlterAuthenticationIntegrationCommand( + integrationName, AlterType.SET_COMMENT, Collections.emptyMap(), Collections.emptySet(), comment); + } + + @Override + public R accept(PlanVisitor visitor, C context) { + return visitor.visitAlterAuthenticationIntegrationCommand(this, context); + } + + @Override + public void doRun(ConnectContext ctx, StmtExecutor executor) throws Exception { + if (!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ConnectContext.get(), PrivPredicate.ADMIN)) { + ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, "ADMIN"); + } + switch (alterType) { + case SET_PROPERTIES: + Env.getCurrentEnv().getAuthenticationIntegrationMgr() + .alterAuthenticationIntegrationProperties(integrationName, properties); + return; + case UNSET_PROPERTIES: + Env.getCurrentEnv().getAuthenticationIntegrationMgr() + .alterAuthenticationIntegrationUnsetProperties(integrationName, unsetProperties); + return; + case SET_COMMENT: + Env.getCurrentEnv().getAuthenticationIntegrationMgr() + .alterAuthenticationIntegrationComment(integrationName, comment); + return; + default: + throw new AnalysisException("Unsupported alter type for AUTHENTICATION INTEGRATION: " + alterType); + } + } + + @Override + public boolean needAuditEncryption() { + return true; + } + + public String getIntegrationName() { + return integrationName; + } + + public AlterType getAlterType() { + return alterType; + } + + public Map getProperties() { + return properties; + } + + public Set getUnsetProperties() { + return unsetProperties; + } + + public String getComment() { + return comment; + } +} diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateAuthenticationIntegrationCommand.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateAuthenticationIntegrationCommand.java new file mode 100644 index 00000000000000..af0531eb87172c --- /dev/null +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateAuthenticationIntegrationCommand.java @@ -0,0 +1,94 @@ +// 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.catalog.Env; +import org.apache.doris.common.ErrorCode; +import org.apache.doris.common.ErrorReport; +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 java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +/** + * CREATE AUTHENTICATION INTEGRATION command entry. + */ +public class CreateAuthenticationIntegrationCommand extends Command implements ForwardWithSync, NeedAuditEncryption { + private final String integrationName; + private final boolean ifNotExists; + private final Map properties; + private final String comment; + + /** Constructor. */ + public CreateAuthenticationIntegrationCommand(String integrationName, boolean ifNotExists, + Map properties, String comment) { + super(PlanType.CREATE_AUTHENTICATION_INTEGRATION_COMMAND); + this.integrationName = Objects.requireNonNull(integrationName, "integrationName can not be null"); + this.ifNotExists = ifNotExists; + this.properties = Collections.unmodifiableMap( + new LinkedHashMap<>(Objects.requireNonNull(properties, "properties can not be null"))); + this.comment = comment; + } + + @Override + public R accept(PlanVisitor visitor, C context) { + return visitor.visitCreateAuthenticationIntegrationCommand(this, context); + } + + @Override + public void run(ConnectContext ctx, StmtExecutor executor) throws Exception { + if (!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ConnectContext.get(), PrivPredicate.ADMIN)) { + ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, "ADMIN"); + } + Env.getCurrentEnv().getAuthenticationIntegrationMgr() + .createAuthenticationIntegration(integrationName, ifNotExists, properties, comment); + } + + @Override + public StmtType stmtType() { + return StmtType.CREATE; + } + + @Override + public boolean needAuditEncryption() { + return true; + } + + public String getIntegrationName() { + return integrationName; + } + + public boolean isSetIfNotExists() { + return ifNotExists; + } + + public Map getProperties() { + return properties; + } + + public String getComment() { + return comment; + } +} diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropAuthenticationIntegrationCommand.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropAuthenticationIntegrationCommand.java new file mode 100644 index 00000000000000..aa041d44678e3c --- /dev/null +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropAuthenticationIntegrationCommand.java @@ -0,0 +1,65 @@ +// 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.catalog.Env; +import org.apache.doris.common.ErrorCode; +import org.apache.doris.common.ErrorReport; +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 java.util.Objects; + +/** + * DROP AUTHENTICATION INTEGRATION command entry. + */ +public class DropAuthenticationIntegrationCommand extends DropCommand { + private final boolean ifExists; + private final String integrationName; + + public DropAuthenticationIntegrationCommand(boolean ifExists, String integrationName) { + super(PlanType.DROP_AUTHENTICATION_INTEGRATION_COMMAND); + this.ifExists = ifExists; + this.integrationName = Objects.requireNonNull(integrationName, "integrationName can not be null"); + } + + @Override + public R accept(PlanVisitor visitor, C context) { + return visitor.visitDropAuthenticationIntegrationCommand(this, context); + } + + @Override + public void doRun(ConnectContext ctx, StmtExecutor executor) throws Exception { + if (!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ConnectContext.get(), PrivPredicate.ADMIN)) { + ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, "ADMIN"); + } + Env.getCurrentEnv().getAuthenticationIntegrationMgr() + .dropAuthenticationIntegration(integrationName, ifExists); + } + + public boolean isIfExists() { + return ifExists; + } + + public String getIntegrationName() { + return integrationName; + } +} diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java index 743dd9dbbb5568..00b64230deb34a 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java @@ -36,6 +36,7 @@ import org.apache.doris.nereids.trees.plans.commands.AdminSetReplicaStatusCommand; import org.apache.doris.nereids.trees.plans.commands.AdminSetReplicaVersionCommand; import org.apache.doris.nereids.trees.plans.commands.AdminSetTableStatusCommand; +import org.apache.doris.nereids.trees.plans.commands.AlterAuthenticationIntegrationCommand; import org.apache.doris.nereids.trees.plans.commands.AlterCatalogCommentCommand; import org.apache.doris.nereids.trees.plans.commands.AlterCatalogPropertiesCommand; import org.apache.doris.nereids.trees.plans.commands.AlterCatalogRenameCommand; @@ -72,6 +73,7 @@ import org.apache.doris.nereids.trees.plans.commands.CleanQueryStatsCommand; import org.apache.doris.nereids.trees.plans.commands.Command; import org.apache.doris.nereids.trees.plans.commands.CopyIntoCommand; +import org.apache.doris.nereids.trees.plans.commands.CreateAuthenticationIntegrationCommand; import org.apache.doris.nereids.trees.plans.commands.CreateCatalogCommand; import org.apache.doris.nereids.trees.plans.commands.CreateDatabaseCommand; import org.apache.doris.nereids.trees.plans.commands.CreateDictionaryCommand; @@ -103,6 +105,7 @@ import org.apache.doris.nereids.trees.plans.commands.DeleteFromUsingCommand; import org.apache.doris.nereids.trees.plans.commands.DescribeCommand; import org.apache.doris.nereids.trees.plans.commands.DropAnalyzeJobCommand; +import org.apache.doris.nereids.trees.plans.commands.DropAuthenticationIntegrationCommand; import org.apache.doris.nereids.trees.plans.commands.DropCachedStatsCommand; import org.apache.doris.nereids.trees.plans.commands.DropCatalogCommand; import org.apache.doris.nereids.trees.plans.commands.DropCatalogRecycleBinCommand; @@ -515,6 +518,11 @@ default R visitCreateCatalogCommand(CreateCatalogCommand createCatalogCommand, C return visitCommand(createCatalogCommand, context); } + default R visitCreateAuthenticationIntegrationCommand( + CreateAuthenticationIntegrationCommand createAuthenticationIntegrationCommand, C context) { + return visitCommand(createAuthenticationIntegrationCommand, context); + } + default R visitShowWarningErrorsCommand(ShowWarningErrorsCommand showWarningErrorsCommand, C context) { return visitCommand(showWarningErrorsCommand, context); } @@ -543,6 +551,11 @@ default R visitDropCatalogCommand(DropCatalogCommand dropCatalogCommand, C conte return visitCommand(dropCatalogCommand, context); } + default R visitDropAuthenticationIntegrationCommand( + DropAuthenticationIntegrationCommand dropAuthenticationIntegrationCommand, C context) { + return visitCommand(dropAuthenticationIntegrationCommand, context); + } + default R visitAlterCatalogCommentCommand(AlterCatalogCommentCommand alterCatalogCommentCommand, C context) { return visitCommand(alterCatalogCommentCommand, context); } @@ -858,6 +871,11 @@ default R visitAlterCatalogPropertiesCommand(AlterCatalogPropertiesCommand alter return visitCommand(alterCatalogPropsCmd, context); } + default R visitAlterAuthenticationIntegrationCommand( + AlterAuthenticationIntegrationCommand alterAuthenticationIntegrationCommand, C context) { + return visitCommand(alterAuthenticationIntegrationCommand, context); + } + default R visitAlterDatabasePropertiesCommand(AlterDatabasePropertiesCommand alterDatabasePropsCmd, C context) { return visitCommand(alterDatabasePropsCmd, context); } diff --git a/fe/fe-core/src/main/java/org/apache/doris/persist/DropAuthenticationIntegrationOperationLog.java b/fe/fe-core/src/main/java/org/apache/doris/persist/DropAuthenticationIntegrationOperationLog.java new file mode 100644 index 00000000000000..a2f2015cdc2a8f --- /dev/null +++ b/fe/fe-core/src/main/java/org/apache/doris/persist/DropAuthenticationIntegrationOperationLog.java @@ -0,0 +1,53 @@ +// 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.persist; + +import org.apache.doris.common.io.Text; +import org.apache.doris.common.io.Writable; +import org.apache.doris.persist.gson.GsonUtils; + +import com.google.gson.annotations.SerializedName; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +/** + * Drop log for AUTHENTICATION INTEGRATION. + */ +public class DropAuthenticationIntegrationOperationLog implements Writable { + @SerializedName(value = "in") + private String integrationName; + + public DropAuthenticationIntegrationOperationLog(String integrationName) { + this.integrationName = integrationName; + } + + public String getIntegrationName() { + return integrationName; + } + + @Override + public void write(DataOutput out) throws IOException { + Text.writeString(out, GsonUtils.GSON.toJson(this)); + } + + public static DropAuthenticationIntegrationOperationLog read(DataInput in) throws IOException { + return GsonUtils.GSON.fromJson(Text.readString(in), DropAuthenticationIntegrationOperationLog.class); + } +} diff --git a/fe/fe-core/src/main/java/org/apache/doris/persist/EditLog.java b/fe/fe-core/src/main/java/org/apache/doris/persist/EditLog.java index d506b474ed70b3..69d10cfd3a7d85 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/persist/EditLog.java +++ b/fe/fe-core/src/main/java/org/apache/doris/persist/EditLog.java @@ -22,6 +22,7 @@ import org.apache.doris.alter.BatchAlterJobPersistInfo; import org.apache.doris.alter.IndexChangeJob; import org.apache.doris.analysis.UserIdentity; +import org.apache.doris.authentication.AuthenticationIntegrationMeta; import org.apache.doris.backup.BackupJob; import org.apache.doris.backup.Repository; import org.apache.doris.backup.RestoreJob; @@ -1082,6 +1083,22 @@ public static void loadJournal(Env env, Long logId, JournalEntity journal) { env.getSqlBlockRuleMgr().replayDrop(log.getRuleNames()); break; } + case OperationType.OP_CREATE_AUTHENTICATION_INTEGRATION: { + AuthenticationIntegrationMeta log = (AuthenticationIntegrationMeta) journal.getData(); + env.getAuthenticationIntegrationMgr().replayCreateAuthenticationIntegration(log); + break; + } + case OperationType.OP_ALTER_AUTHENTICATION_INTEGRATION: { + AuthenticationIntegrationMeta log = (AuthenticationIntegrationMeta) journal.getData(); + env.getAuthenticationIntegrationMgr().replayAlterAuthenticationIntegration(log); + break; + } + case OperationType.OP_DROP_AUTHENTICATION_INTEGRATION: { + DropAuthenticationIntegrationOperationLog log = + (DropAuthenticationIntegrationOperationLog) journal.getData(); + env.getAuthenticationIntegrationMgr().replayDropAuthenticationIntegration(log); + break; + } case OperationType.OP_MODIFY_TABLE_ENGINE: { ModifyTableEngineOperationLog log = (ModifyTableEngineOperationLog) journal.getData(); env.getAlterInstance().replayProcessModifyEngine(log); @@ -2312,6 +2329,18 @@ public void logDropSqlBlockRule(List ruleNames) { logEdit(OperationType.OP_DROP_SQL_BLOCK_RULE, new DropSqlBlockRuleOperationLog(ruleNames)); } + public void logCreateAuthenticationIntegration(AuthenticationIntegrationMeta meta) { + logEdit(OperationType.OP_CREATE_AUTHENTICATION_INTEGRATION, meta); + } + + public void logAlterAuthenticationIntegration(AuthenticationIntegrationMeta meta) { + logEdit(OperationType.OP_ALTER_AUTHENTICATION_INTEGRATION, meta); + } + + public void logDropAuthenticationIntegration(DropAuthenticationIntegrationOperationLog log) { + logEdit(OperationType.OP_DROP_AUTHENTICATION_INTEGRATION, log); + } + public void logModifyTableEngine(ModifyTableEngineOperationLog log) { logEdit(OperationType.OP_MODIFY_TABLE_ENGINE, log); } diff --git a/fe/fe-core/src/main/java/org/apache/doris/persist/OperationType.java b/fe/fe-core/src/main/java/org/apache/doris/persist/OperationType.java index 1174d9c3874817..82eea42ae70560 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/persist/OperationType.java +++ b/fe/fe-core/src/main/java/org/apache/doris/persist/OperationType.java @@ -418,6 +418,9 @@ public class OperationType { public static final short OP_DROP_INDEX_POLICY = 491; public static final short OP_OPERATE_KEY = 492; + public static final short OP_CREATE_AUTHENTICATION_INTEGRATION = 493; + public static final short OP_ALTER_AUTHENTICATION_INTEGRATION = 494; + public static final short OP_DROP_AUTHENTICATION_INTEGRATION = 495; // For cloud. public static final short OP_UPDATE_CLOUD_REPLICA = 1000; diff --git a/fe/fe-core/src/main/java/org/apache/doris/persist/meta/MetaPersistMethod.java b/fe/fe-core/src/main/java/org/apache/doris/persist/meta/MetaPersistMethod.java index 0114bde9eee3e0..ea3585d493bd8c 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/persist/meta/MetaPersistMethod.java +++ b/fe/fe-core/src/main/java/org/apache/doris/persist/meta/MetaPersistMethod.java @@ -186,6 +186,15 @@ public static MetaPersistMethod create(String name) throws NoSuchMethodException metaPersistMethod.writeMethod = Env.class.getDeclaredMethod("saveSqlBlockRule", CountingDataOutputStream.class, long.class); break; + // TODO: Re-enable this module once AuthenticationIntegrations should be persisted again. + // case "authenticationIntegrations": + // metaPersistMethod.readMethod = + // Env.class.getDeclaredMethod("loadAuthenticationIntegrations", DataInputStream.class, + // long.class); + // metaPersistMethod.writeMethod = + // Env.class.getDeclaredMethod("saveAuthenticationIntegrations", + // CountingDataOutputStream.class, long.class); + // break; case "policy": metaPersistMethod.readMethod = Env.class.getDeclaredMethod("loadPolicy", DataInputStream.class, long.class); diff --git a/fe/fe-core/src/main/java/org/apache/doris/persist/meta/PersistMetaModules.java b/fe/fe-core/src/main/java/org/apache/doris/persist/meta/PersistMetaModules.java index 665f3cb09ed035..44809035406666 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/persist/meta/PersistMetaModules.java +++ b/fe/fe-core/src/main/java/org/apache/doris/persist/meta/PersistMetaModules.java @@ -40,9 +40,13 @@ public class PersistMetaModules { "masterInfo", "frontends", "backends", "datasource", "db", "alterJob", "recycleBin", "globalVariable", "cluster", "broker", "resources", "exportJob", "backupHandler", "paloAuth", "transactionState", "colocateTableIndex", "routineLoadJobs", "loadJobV2", "smallFiles", - "plugins", "deleteHandler", "sqlBlockRule", "policy", "globalFunction", "workloadGroups", + "plugins", "deleteHandler", "sqlBlockRule", "policy", + "globalFunction", "workloadGroups", "binlogs", "resourceGroups", "AnalysisMgrV2", "AsyncJobManager", "workloadSchedPolicy", - "insertOverwrite", "plsql", "dictionaryManager", "indexPolicy", "KeyManagerStore"); + "insertOverwrite", "plsql", "dictionaryManager", "indexPolicy", "KeyManagerStore" + // TODO: Re-enable "authenticationIntegrations" after persistence requirements are confirmed. + // , "authenticationIntegrations" + ); // The modules in `CloudEnv`. public static final ImmutableList CLOUD_MODULE_NAMES = ImmutableList.of("cloudWarmUpJob"); diff --git a/fe/fe-core/src/test/java/org/apache/doris/authentication/AuthenticationIntegrationMetaTest.java b/fe/fe-core/src/test/java/org/apache/doris/authentication/AuthenticationIntegrationMetaTest.java new file mode 100644 index 00000000000000..b11268c7cc3a87 --- /dev/null +++ b/fe/fe-core/src/test/java/org/apache/doris/authentication/AuthenticationIntegrationMetaTest.java @@ -0,0 +1,168 @@ +// 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.authentication; + +import org.apache.doris.common.DdlException; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +public class AuthenticationIntegrationMetaTest { + + private static Map map(String... kvs) { + Map result = new LinkedHashMap<>(); + for (int i = 0; i < kvs.length; i += 2) { + result.put(kvs[i], kvs[i + 1]); + } + return result; + } + + private static Set set(String... keys) { + Set result = new LinkedHashSet<>(); + Collections.addAll(result, keys); + return result; + } + + @Test + public void testFromCreateSqlSuccessAndTypeFiltered() throws Exception { + Map properties = new LinkedHashMap<>(); + properties.put("TYPE", "ldap"); + properties.put("ldap.server", "ldap://127.0.0.1:389"); + properties.put("ldap.admin_dn", "cn=admin,dc=example,dc=com"); + + AuthenticationIntegrationMeta meta = + AuthenticationIntegrationMeta.fromCreateSql("corp_ldap", properties, "ldap integration"); + + Assertions.assertEquals("corp_ldap", meta.getName()); + Assertions.assertEquals("ldap", meta.getType()); + Assertions.assertEquals("ldap integration", meta.getComment()); + Assertions.assertEquals(2, meta.getProperties().size()); + Assertions.assertEquals("ldap://127.0.0.1:389", meta.getProperties().get("ldap.server")); + Assertions.assertFalse(meta.getProperties().containsKey("type")); + Assertions.assertFalse(meta.getProperties().containsKey("TYPE")); + + Assertions.assertThrows(UnsupportedOperationException.class, + () -> meta.getProperties().put("x", "y")); + + Map sqlProperties = meta.toSqlPropertiesView(); + Assertions.assertEquals("ldap", sqlProperties.get("type")); + Assertions.assertEquals("cn=admin,dc=example,dc=com", sqlProperties.get("ldap.admin_dn")); + } + + @Test + public void testFromCreateSqlRequireType() { + Assertions.assertThrows(DdlException.class, + () -> AuthenticationIntegrationMeta.fromCreateSql("i1", null, null)); + Assertions.assertThrows(DdlException.class, + () -> AuthenticationIntegrationMeta.fromCreateSql("i1", Collections.emptyMap(), null)); + Assertions.assertThrows(DdlException.class, + () -> AuthenticationIntegrationMeta.fromCreateSql("i1", map("k", "v"), null)); + Assertions.assertThrows(DdlException.class, + () -> AuthenticationIntegrationMeta.fromCreateSql("i1", map("type", ""), null)); + } + + @Test + public void testFromCreateSqlRejectDuplicatedTypeIgnoreCase() { + Map properties = new LinkedHashMap<>(); + properties.put("type", "ldap"); + properties.put("TYPE", "oidc"); + + Assertions.assertThrows(DdlException.class, + () -> AuthenticationIntegrationMeta.fromCreateSql("i1", properties, null)); + } + + @Test + public void testWithAlterProperties() throws Exception { + AuthenticationIntegrationMeta meta = AuthenticationIntegrationMeta.fromCreateSql( + "corp_ldap", + map("type", "ldap", + "ldap.server", "ldap://old", + "ldap.base_dn", "dc=example,dc=com"), + "old comment"); + + AuthenticationIntegrationMeta altered = meta.withAlterProperties(map( + "ldap.server", "ldap://new", + "ldap.user_filter", "(uid={login})")); + + Assertions.assertEquals("ldap", altered.getType()); + Assertions.assertEquals("old comment", altered.getComment()); + Assertions.assertEquals("ldap://new", altered.getProperties().get("ldap.server")); + Assertions.assertEquals("(uid={login})", altered.getProperties().get("ldap.user_filter")); + + Assertions.assertThrows(DdlException.class, + () -> meta.withAlterProperties(Collections.emptyMap())); + Assertions.assertThrows(DdlException.class, + () -> meta.withAlterProperties(map("TYPE", "oidc"))); + } + + @Test + public void testWithUnsetProperties() throws Exception { + AuthenticationIntegrationMeta meta = AuthenticationIntegrationMeta.fromCreateSql( + "corp_ldap", + map("type", "ldap", + "ldap.server", "ldap://old", + "ldap.base_dn", "dc=example,dc=com"), + "old comment"); + + AuthenticationIntegrationMeta altered = meta.withUnsetProperties(set("ldap.base_dn")); + Assertions.assertEquals("ldap", altered.getType()); + Assertions.assertFalse(altered.getProperties().containsKey("ldap.base_dn")); + Assertions.assertEquals("ldap://old", altered.getProperties().get("ldap.server")); + + Assertions.assertThrows(DdlException.class, + () -> meta.withUnsetProperties(Collections.emptySet())); + Assertions.assertThrows(DdlException.class, + () -> meta.withUnsetProperties(set("TYPE"))); + } + + @Test + public void testWriteReadRoundTrip() throws IOException, DdlException { + AuthenticationIntegrationMeta meta = AuthenticationIntegrationMeta.fromCreateSql( + "corp_ldap", + map("type", "ldap", + "ldap.server", "ldap://127.0.0.1:389", + "ldap.admin_password", "123456"), + "comment"); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (DataOutputStream dos = new DataOutputStream(bos)) { + meta.write(dos); + } + + AuthenticationIntegrationMeta read; + try (DataInputStream dis = new DataInputStream(new ByteArrayInputStream(bos.toByteArray()))) { + read = AuthenticationIntegrationMeta.read(dis); + } + + Assertions.assertEquals(meta.getName(), read.getName()); + Assertions.assertEquals(meta.getType(), read.getType()); + Assertions.assertEquals(meta.getComment(), read.getComment()); + Assertions.assertEquals(meta.getProperties(), read.getProperties()); + } +} diff --git a/fe/fe-core/src/test/java/org/apache/doris/authentication/AuthenticationIntegrationMgrTest.java b/fe/fe-core/src/test/java/org/apache/doris/authentication/AuthenticationIntegrationMgrTest.java new file mode 100644 index 00000000000000..18f1dd530c72ec --- /dev/null +++ b/fe/fe-core/src/test/java/org/apache/doris/authentication/AuthenticationIntegrationMgrTest.java @@ -0,0 +1,206 @@ +// 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.authentication; + +import org.apache.doris.catalog.Env; +import org.apache.doris.common.DdlException; +import org.apache.doris.persist.DropAuthenticationIntegrationOperationLog; +import org.apache.doris.persist.EditLog; + +import mockit.Expectations; +import mockit.Mocked; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +public class AuthenticationIntegrationMgrTest { + + @Mocked + private Env env; + + @Mocked + private EditLog editLog; + + private static Map map(String... kvs) { + Map result = new LinkedHashMap<>(); + for (int i = 0; i < kvs.length; i += 2) { + result.put(kvs[i], kvs[i + 1]); + } + return result; + } + + private static Set set(String... keys) { + Set result = new LinkedHashSet<>(); + Collections.addAll(result, keys); + return result; + } + + @Test + public void testCreateAlterDropFlow() throws Exception { + new Expectations() { + { + Env.getCurrentEnv(); + minTimes = 0; + result = env; + + env.getEditLog(); + minTimes = 0; + result = editLog; + + editLog.logCreateAuthenticationIntegration((AuthenticationIntegrationMeta) any); + minTimes = 0; + + editLog.logAlterAuthenticationIntegration((AuthenticationIntegrationMeta) any); + minTimes = 0; + + editLog.logDropAuthenticationIntegration((DropAuthenticationIntegrationOperationLog) any); + minTimes = 0; + } + }; + + AuthenticationIntegrationMgr mgr = new AuthenticationIntegrationMgr(); + Map createProperties = new LinkedHashMap<>(); + createProperties.put("type", "ldap"); + createProperties.put("ldap.server", "ldap://127.0.0.1:389"); + createProperties.put("ldap.admin_password", "123456"); + + mgr.createAuthenticationIntegration("corp_ldap", false, createProperties, "comment"); + AuthenticationIntegrationMeta created = mgr.getAuthenticationIntegrations().get("corp_ldap"); + Assertions.assertNotNull(created); + Assertions.assertEquals("ldap", created.getType()); + Assertions.assertEquals("ldap://127.0.0.1:389", created.getProperties().get("ldap.server")); + + mgr.alterAuthenticationIntegrationProperties("corp_ldap", map("ldap.server", "ldap://127.0.0.1:1389")); + Assertions.assertEquals("ldap://127.0.0.1:1389", + mgr.getAuthenticationIntegrations().get("corp_ldap").getProperties().get("ldap.server")); + + mgr.alterAuthenticationIntegrationUnsetProperties("corp_ldap", set("ldap.admin_password")); + Assertions.assertFalse(mgr.getAuthenticationIntegrations() + .get("corp_ldap").getProperties().containsKey("ldap.admin_password")); + + mgr.alterAuthenticationIntegrationComment("corp_ldap", "new comment"); + Assertions.assertEquals("new comment", mgr.getAuthenticationIntegrations().get("corp_ldap").getComment()); + + mgr.dropAuthenticationIntegration("corp_ldap", false); + Assertions.assertTrue(mgr.getAuthenticationIntegrations().isEmpty()); + } + + @Test + public void testCreateDuplicateAndDropIfExists() throws Exception { + new Expectations() { + { + Env.getCurrentEnv(); + minTimes = 0; + result = env; + + env.getEditLog(); + minTimes = 0; + result = editLog; + + editLog.logCreateAuthenticationIntegration((AuthenticationIntegrationMeta) any); + minTimes = 0; + + editLog.logDropAuthenticationIntegration((DropAuthenticationIntegrationOperationLog) any); + minTimes = 0; + } + }; + + AuthenticationIntegrationMgr mgr = new AuthenticationIntegrationMgr(); + mgr.createAuthenticationIntegration("corp_ldap", false, map( + "type", "ldap", + "ldap.server", "ldap://127.0.0.1:389"), null); + + Assertions.assertThrows(DdlException.class, + () -> mgr.createAuthenticationIntegration("corp_ldap", false, map("type", "ldap"), null)); + Assertions.assertDoesNotThrow( + () -> mgr.createAuthenticationIntegration("corp_ldap", true, map("type", "ldap"), null)); + + Assertions.assertDoesNotThrow(() -> mgr.dropAuthenticationIntegration("not_exist", true)); + Assertions.assertThrows(DdlException.class, + () -> mgr.dropAuthenticationIntegration("not_exist", false)); + } + + @Test + public void testAlterNotExistThrows() { + AuthenticationIntegrationMgr mgr = new AuthenticationIntegrationMgr(); + Assertions.assertThrows(DdlException.class, + () -> mgr.alterAuthenticationIntegrationProperties("not_exist", map("k", "v"))); + Assertions.assertThrows(DdlException.class, + () -> mgr.alterAuthenticationIntegrationUnsetProperties("not_exist", set("k"))); + Assertions.assertThrows(DdlException.class, + () -> mgr.alterAuthenticationIntegrationComment("not_exist", "comment")); + } + + @Test + public void testReplayAndGetUnmodifiableView() throws Exception { + AuthenticationIntegrationMgr mgr = new AuthenticationIntegrationMgr(); + + AuthenticationIntegrationMeta meta1 = AuthenticationIntegrationMeta.fromCreateSql( + "corp_ldap", map("type", "ldap", "ldap.server", "ldap://old"), null); + AuthenticationIntegrationMeta meta2 = meta1.withAlterProperties(map("ldap.server", "ldap://new")); + + mgr.replayCreateAuthenticationIntegration(meta1); + mgr.replayAlterAuthenticationIntegration(meta2); + + Map copy = mgr.getAuthenticationIntegrations(); + Assertions.assertEquals(1, copy.size()); + Assertions.assertThrows(UnsupportedOperationException.class, + () -> copy.put("x", meta1)); + + mgr.replayDropAuthenticationIntegration(new DropAuthenticationIntegrationOperationLog("corp_ldap")); + Assertions.assertTrue(mgr.getAuthenticationIntegrations().isEmpty()); + } + + @Test + public void testWriteReadRoundTrip() throws IOException, DdlException { + AuthenticationIntegrationMgr mgr = new AuthenticationIntegrationMgr(); + AuthenticationIntegrationMeta meta = AuthenticationIntegrationMeta.fromCreateSql( + "corp_ldap", map( + "type", "ldap", + "ldap.server", "ldap://127.0.0.1:389"), + "comment"); + mgr.replayCreateAuthenticationIntegration(meta); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (DataOutputStream dos = new DataOutputStream(bos)) { + mgr.write(dos); + } + + AuthenticationIntegrationMgr read; + try (DataInputStream dis = new DataInputStream(new ByteArrayInputStream(bos.toByteArray()))) { + read = AuthenticationIntegrationMgr.read(dis); + } + + Assertions.assertEquals(1, read.getAuthenticationIntegrations().size()); + AuthenticationIntegrationMeta readMeta = read.getAuthenticationIntegrations().get("corp_ldap"); + Assertions.assertNotNull(readMeta); + Assertions.assertEquals("ldap", readMeta.getType()); + Assertions.assertEquals("ldap://127.0.0.1:389", readMeta.getProperties().get("ldap.server")); + Assertions.assertEquals("comment", readMeta.getComment()); + } +} diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/AuthenticationIntegrationParserTest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/AuthenticationIntegrationParserTest.java new file mode 100644 index 00000000000000..2a01a2372095f7 --- /dev/null +++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/AuthenticationIntegrationParserTest.java @@ -0,0 +1,111 @@ +// 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.parser; + +import org.apache.doris.nereids.exceptions.ParseException; +import org.apache.doris.nereids.trees.plans.commands.AlterAuthenticationIntegrationCommand; +import org.apache.doris.nereids.trees.plans.commands.CreateAuthenticationIntegrationCommand; +import org.apache.doris.nereids.trees.plans.commands.DropAuthenticationIntegrationCommand; +import org.apache.doris.nereids.trees.plans.logical.LogicalPlan; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class AuthenticationIntegrationParserTest { + + private final NereidsParser parser = new NereidsParser(); + + @Test + public void testCreateAuthenticationIntegrationParse() { + LogicalPlan plan = parser.parseSingle("CREATE AUTHENTICATION INTEGRATION IF NOT EXISTS corp_ldap " + + "PROPERTIES ('type'='ldap', 'ldap.server'='ldap://127.0.0.1:389') " + + "COMMENT 'ldap integration'"); + + Assertions.assertInstanceOf(CreateAuthenticationIntegrationCommand.class, plan); + CreateAuthenticationIntegrationCommand command = (CreateAuthenticationIntegrationCommand) plan; + Assertions.assertEquals("corp_ldap", command.getIntegrationName()); + Assertions.assertTrue(command.isSetIfNotExists()); + Assertions.assertEquals("ldap", command.getProperties().get("type")); + Assertions.assertEquals("ldap://127.0.0.1:389", command.getProperties().get("ldap.server")); + Assertions.assertEquals("ldap integration", command.getComment()); + } + + @Test + public void testCreateAuthenticationIntegrationRequireType() { + Assertions.assertThrows(ParseException.class, () -> parser.parseSingle( + "CREATE AUTHENTICATION INTEGRATION corp_ldap " + + "PROPERTIES ('ldap.server'='ldap://127.0.0.1:389')")); + } + + @Test + public void testAlterAuthenticationIntegrationParse() { + LogicalPlan alterProperties = parser.parseSingle("ALTER AUTHENTICATION INTEGRATION corp_ldap " + + "SET PROPERTIES ('ldap.server'='ldap://127.0.0.1:1389')"); + Assertions.assertInstanceOf(AlterAuthenticationIntegrationCommand.class, alterProperties); + + AlterAuthenticationIntegrationCommand alterPropertiesCommand = + (AlterAuthenticationIntegrationCommand) alterProperties; + Assertions.assertEquals("corp_ldap", alterPropertiesCommand.getIntegrationName()); + Assertions.assertEquals(AlterAuthenticationIntegrationCommand.AlterType.SET_PROPERTIES, + alterPropertiesCommand.getAlterType()); + Assertions.assertEquals("ldap://127.0.0.1:1389", + alterPropertiesCommand.getProperties().get("ldap.server")); + + LogicalPlan unsetProperties = parser.parseSingle("ALTER AUTHENTICATION INTEGRATION corp_ldap " + + "UNSET PROPERTIES ('ldap.server')"); + Assertions.assertInstanceOf(AlterAuthenticationIntegrationCommand.class, unsetProperties); + + AlterAuthenticationIntegrationCommand unsetPropertiesCommand = + (AlterAuthenticationIntegrationCommand) unsetProperties; + Assertions.assertEquals(AlterAuthenticationIntegrationCommand.AlterType.UNSET_PROPERTIES, + unsetPropertiesCommand.getAlterType()); + Assertions.assertTrue(unsetPropertiesCommand.getUnsetProperties().contains("ldap.server")); + + LogicalPlan alterComment = parser.parseSingle( + "ALTER AUTHENTICATION INTEGRATION corp_ldap SET COMMENT 'new comment'"); + Assertions.assertInstanceOf(AlterAuthenticationIntegrationCommand.class, alterComment); + + AlterAuthenticationIntegrationCommand alterCommentCommand = + (AlterAuthenticationIntegrationCommand) alterComment; + Assertions.assertEquals(AlterAuthenticationIntegrationCommand.AlterType.SET_COMMENT, + alterCommentCommand.getAlterType()); + Assertions.assertEquals("new comment", alterCommentCommand.getComment()); + } + + @Test + public void testAlterAuthenticationIntegrationRejectType() { + Assertions.assertThrows(ParseException.class, () -> parser.parseSingle( + "ALTER AUTHENTICATION INTEGRATION corp_ldap SET PROPERTIES ('TYPE'='oidc')")); + Assertions.assertThrows(ParseException.class, () -> parser.parseSingle( + "ALTER AUTHENTICATION INTEGRATION corp_ldap UNSET PROPERTIES ('TYPE')")); + } + + @Test + public void testDropAuthenticationIntegrationParse() { + LogicalPlan plan1 = parser.parseSingle("DROP AUTHENTICATION INTEGRATION corp_ldap"); + Assertions.assertInstanceOf(DropAuthenticationIntegrationCommand.class, plan1); + DropAuthenticationIntegrationCommand drop1 = (DropAuthenticationIntegrationCommand) plan1; + Assertions.assertEquals("corp_ldap", drop1.getIntegrationName()); + Assertions.assertFalse(drop1.isIfExists()); + + LogicalPlan plan2 = parser.parseSingle("DROP AUTHENTICATION INTEGRATION IF EXISTS corp_ldap"); + Assertions.assertInstanceOf(DropAuthenticationIntegrationCommand.class, plan2); + DropAuthenticationIntegrationCommand drop2 = (DropAuthenticationIntegrationCommand) plan2; + Assertions.assertTrue(drop2.isIfExists()); + } +} diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/AuthenticationIntegrationCommandTest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/AuthenticationIntegrationCommandTest.java new file mode 100644 index 00000000000000..ed1a70b03d5815 --- /dev/null +++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/AuthenticationIntegrationCommandTest.java @@ -0,0 +1,238 @@ +// 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.authentication.AuthenticationIntegrationMgr; +import org.apache.doris.catalog.Env; +import org.apache.doris.common.AnalysisException; +import org.apache.doris.mysql.privilege.AccessControllerManager; +import org.apache.doris.mysql.privilege.PrivPredicate; +import org.apache.doris.qe.ConnectContext; + +import mockit.Expectations; +import mockit.Mocked; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +public class AuthenticationIntegrationCommandTest { + + @Mocked + private Env env; + + @Mocked + private AccessControllerManager accessManager; + + @Mocked + private AuthenticationIntegrationMgr authenticationIntegrationMgr; + + @Mocked + private ConnectContext connectContext; + + private static Map map(String... kvs) { + Map result = new LinkedHashMap<>(); + for (int i = 0; i < kvs.length; i += 2) { + result.put(kvs[i], kvs[i + 1]); + } + return result; + } + + private static Set set(String... keys) { + Set result = new LinkedHashSet<>(); + Collections.addAll(result, keys); + return result; + } + + @Test + public void testCreateCommandRunAndDenied() throws Exception { + CreateAuthenticationIntegrationCommand createCommand = + new CreateAuthenticationIntegrationCommand("corp_ldap", false, + map("type", "ldap", "ldap.server", "ldap://127.0.0.1:389"), "comment"); + + new Expectations() { + { + Env.getCurrentEnv(); + minTimes = 0; + result = env; + + env.getAccessManager(); + minTimes = 0; + result = accessManager; + + accessManager.checkGlobalPriv((ConnectContext) any, PrivPredicate.ADMIN); + result = true; + + env.getAuthenticationIntegrationMgr(); + minTimes = 0; + result = authenticationIntegrationMgr; + + authenticationIntegrationMgr.createAuthenticationIntegration( + anyString, anyBoolean, (Map) any, anyString); + times = 1; + } + }; + + Assertions.assertDoesNotThrow(() -> createCommand.run(connectContext, null)); + Assertions.assertEquals(StmtType.CREATE, createCommand.stmtType()); + Assertions.assertTrue(createCommand.needAuditEncryption()); + + new Expectations() { + { + Env.getCurrentEnv(); + minTimes = 0; + result = env; + + env.getAccessManager(); + minTimes = 0; + result = accessManager; + + accessManager.checkGlobalPriv((ConnectContext) any, PrivPredicate.ADMIN); + result = false; + } + }; + + Assertions.assertThrows(AnalysisException.class, () -> createCommand.run(connectContext, null)); + } + + @Test + public void testAlterCommandRun() throws Exception { + AlterAuthenticationIntegrationCommand setPropertiesCommand = + AlterAuthenticationIntegrationCommand.forSetProperties( + "corp_ldap", map("ldap.server", "ldap://127.0.0.1:1389")); + AlterAuthenticationIntegrationCommand unsetPropertiesCommand = + AlterAuthenticationIntegrationCommand.forUnsetProperties( + "corp_ldap", set("ldap.server")); + AlterAuthenticationIntegrationCommand setCommentCommand = + AlterAuthenticationIntegrationCommand.forSetComment("corp_ldap", "new comment"); + + new Expectations() { + { + Env.getCurrentEnv(); + minTimes = 0; + result = env; + + env.getAccessManager(); + minTimes = 0; + result = accessManager; + + accessManager.checkGlobalPriv((ConnectContext) any, PrivPredicate.ADMIN); + minTimes = 0; + result = true; + + env.getAuthenticationIntegrationMgr(); + minTimes = 0; + result = authenticationIntegrationMgr; + + authenticationIntegrationMgr.alterAuthenticationIntegrationProperties( + anyString, (Map) any); + times = 1; + + authenticationIntegrationMgr.alterAuthenticationIntegrationUnsetProperties( + anyString, (Set) any); + times = 1; + + authenticationIntegrationMgr.alterAuthenticationIntegrationComment(anyString, anyString); + times = 1; + } + }; + + Assertions.assertDoesNotThrow(() -> setPropertiesCommand.doRun(connectContext, null)); + Assertions.assertDoesNotThrow(() -> unsetPropertiesCommand.doRun(connectContext, null)); + Assertions.assertDoesNotThrow(() -> setCommentCommand.doRun(connectContext, null)); + Assertions.assertTrue(setPropertiesCommand.needAuditEncryption()); + Assertions.assertTrue(unsetPropertiesCommand.needAuditEncryption()); + Assertions.assertTrue(setCommentCommand.needAuditEncryption()); + } + + @Test + public void testAlterCommandDenied() { + AlterAuthenticationIntegrationCommand setPropertiesCommand = + AlterAuthenticationIntegrationCommand.forSetProperties( + "corp_ldap", map("ldap.server", "ldap://127.0.0.1:1389")); + + new Expectations() { + { + Env.getCurrentEnv(); + minTimes = 0; + result = env; + + env.getAccessManager(); + minTimes = 0; + result = accessManager; + + accessManager.checkGlobalPriv((ConnectContext) any, PrivPredicate.ADMIN); + result = false; + } + }; + + Assertions.assertThrows(AnalysisException.class, () -> setPropertiesCommand.doRun(connectContext, null)); + } + + @Test + public void testDropCommandRunAndDenied() throws Exception { + DropAuthenticationIntegrationCommand dropCommand = + new DropAuthenticationIntegrationCommand(true, "corp_ldap"); + + new Expectations() { + { + Env.getCurrentEnv(); + minTimes = 0; + result = env; + + env.getAccessManager(); + minTimes = 0; + result = accessManager; + + accessManager.checkGlobalPriv((ConnectContext) any, PrivPredicate.ADMIN); + result = true; + + env.getAuthenticationIntegrationMgr(); + minTimes = 0; + result = authenticationIntegrationMgr; + + authenticationIntegrationMgr.dropAuthenticationIntegration(anyString, anyBoolean); + times = 1; + } + }; + + Assertions.assertDoesNotThrow(() -> dropCommand.doRun(connectContext, null)); + + new Expectations() { + { + Env.getCurrentEnv(); + minTimes = 0; + result = env; + + env.getAccessManager(); + minTimes = 0; + result = accessManager; + + accessManager.checkGlobalPriv((ConnectContext) any, PrivPredicate.ADMIN); + result = false; + } + }; + + Assertions.assertThrows(AnalysisException.class, () -> dropCommand.doRun(connectContext, null)); + } +} diff --git a/fe/fe-core/src/test/java/org/apache/doris/persist/DropAuthenticationIntegrationOperationLogTest.java b/fe/fe-core/src/test/java/org/apache/doris/persist/DropAuthenticationIntegrationOperationLogTest.java new file mode 100644 index 00000000000000..042a7fc40d27d3 --- /dev/null +++ b/fe/fe-core/src/test/java/org/apache/doris/persist/DropAuthenticationIntegrationOperationLogTest.java @@ -0,0 +1,47 @@ +// 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.persist; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; + +public class DropAuthenticationIntegrationOperationLogTest { + + @Test + public void testWriteReadRoundTrip() throws Exception { + DropAuthenticationIntegrationOperationLog log = + new DropAuthenticationIntegrationOperationLog("corp_ldap"); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (DataOutputStream dos = new DataOutputStream(bos)) { + log.write(dos); + } + + DropAuthenticationIntegrationOperationLog read; + try (DataInputStream dis = new DataInputStream(new ByteArrayInputStream(bos.toByteArray()))) { + read = DropAuthenticationIntegrationOperationLog.read(dis); + } + + Assertions.assertEquals("corp_ldap", read.getIntegrationName()); + } +} diff --git a/regression-test/suites/auth_p0/test_authentication_integration_auth.groovy b/regression-test/suites/auth_p0/test_authentication_integration_auth.groovy new file mode 100644 index 00000000000000..c65ac577574821 --- /dev/null +++ b/regression-test/suites/auth_p0/test_authentication_integration_auth.groovy @@ -0,0 +1,78 @@ +// 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. + +suite("test_authentication_integration_auth", "p0,auth") { + String suiteName = "test_authentication_integration_auth" + String integrationName = "${suiteName}_ldap" + + try_sql("DROP AUTHENTICATION INTEGRATION IF EXISTS ${integrationName}") + + try { + test { + sql """ + CREATE AUTHENTICATION INTEGRATION ${integrationName} + PROPERTIES ('ldap.server'='ldap://127.0.0.1:389') + """ + exception "Property 'type' is required" + } + + sql """ + CREATE AUTHENTICATION INTEGRATION ${integrationName} + PROPERTIES ( + 'type'='ldap', + 'ldap.server'='ldap://127.0.0.1:389', + 'ldap.admin_password'='123456' + ) + COMMENT 'for regression test' + """ + + test { + sql """ + CREATE AUTHENTICATION INTEGRATION ${integrationName} + PROPERTIES ('type'='ldap', 'ldap.server'='ldap://127.0.0.1:1389') + """ + exception "already exists" + } + + test { + sql """ + ALTER AUTHENTICATION INTEGRATION ${integrationName} + SET PROPERTIES ('type'='oidc') + """ + exception "does not allow modifying property 'type'" + } + + sql """ + ALTER AUTHENTICATION INTEGRATION ${integrationName} + SET PROPERTIES ( + 'ldap.server'='ldap://127.0.0.1:1389', + 'ldap.admin_password'='abcdef' + ) + """ + + sql """ALTER AUTHENTICATION INTEGRATION ${integrationName} SET COMMENT 'updated comment'""" + + test { + sql """DROP AUTHENTICATION INTEGRATION ${integrationName}_not_exist""" + exception "does not exist" + } + + sql """DROP AUTHENTICATION INTEGRATION IF EXISTS ${integrationName}_not_exist""" + } finally { + try_sql("DROP AUTHENTICATION INTEGRATION IF EXISTS ${integrationName}") + } +}