From e489de0ecb5940dca45c37b6715e2708c1d1fe88 Mon Sep 17 00:00:00 2001 From: Artur Selitskiy <201226774+ArturSelitskiy-sm@users.noreply.github.com> Date: Tue, 28 Oct 2025 18:16:27 +0300 Subject: [PATCH] Add additional roles and role validations --- .../client/http/OrkesAuthorizationClient.java | 47 ++++++++++++++++++- .../client/model/UpsertGroupRequest.java | 10 +++- .../client/model/UpsertUserRequest.java | 8 +++- .../conductor/client/util/RoleValidation.java | 40 ++++++++++++++++ 4 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 orkes-client/src/main/java/io/orkes/conductor/client/util/RoleValidation.java diff --git a/orkes-client/src/main/java/io/orkes/conductor/client/http/OrkesAuthorizationClient.java b/orkes-client/src/main/java/io/orkes/conductor/client/http/OrkesAuthorizationClient.java index 556e74bd..0e0c6bd6 100644 --- a/orkes-client/src/main/java/io/orkes/conductor/client/http/OrkesAuthorizationClient.java +++ b/orkes-client/src/main/java/io/orkes/conductor/client/http/OrkesAuthorizationClient.java @@ -30,6 +30,7 @@ import io.orkes.conductor.client.model.TagObject; import io.orkes.conductor.client.model.UpsertGroupRequest; import io.orkes.conductor.client.model.UpsertUserRequest; +import io.orkes.conductor.client.util.RoleValidation; public class OrkesAuthorizationClient implements AuthorizationClient { @@ -97,6 +98,23 @@ public void removeUserFromGroup(String groupId, String userId) { @Override public Group upsertGroup(UpsertGroupRequest upsertGroupRequest, String id) { + // Early validation: USER_READ_ONLY cannot be combined with other roles or with default permissions + if (upsertGroupRequest != null && upsertGroupRequest.getRoles() != null) { + boolean hasReadOnly = upsertGroupRequest.getRoles().stream() + .anyMatch(r -> r == UpsertGroupRequest.RolesEnum.USER_READ_ONLY); + if (hasReadOnly) { + if (upsertGroupRequest.getRoles().size() > 1) { + throw new IllegalArgumentException("Group with role 'USER_READ_ONLY' cannot be assigned any other roles"); + } + if (upsertGroupRequest.getDefaultAccess() != null && !upsertGroupRequest.getDefaultAccess().isEmpty()) { + throw new IllegalArgumentException("Group with role 'USER_READ_ONLY' cannot have default permissions configured"); + } + } + } + // Validate defaultAccess types match server expectations + if (upsertGroupRequest != null) { + RoleValidation.validateGroupDefaultAccessKeys(upsertGroupRequest.getDefaultAccess()); + } return groupResource.upsertGroup(upsertGroupRequest, id); } @@ -127,12 +145,32 @@ public void sendInviteEmail(String email) { @Override public ConductorUser upsertUser(UpsertUserRequest upsertUserRequest, String id) { + // Early validation: userId cannot start with app: + if (id != null && id.startsWith("app:")) { + throw new IllegalArgumentException("Prefix 'app:' is reserved for applications. Cannot create or edit user with id starting with 'app:'"); + } + // USER_READ_ONLY cannot be combined with other roles nor with groups + if (upsertUserRequest != null && upsertUserRequest.getRoles() != null) { + boolean hasReadOnly = upsertUserRequest.getRoles().stream() + .anyMatch(r -> r == UpsertUserRequest.RolesEnum.USER_READ_ONLY); + if (hasReadOnly && upsertUserRequest.getRoles().size() > 1) { + throw new IllegalArgumentException("User with role 'USER_READ_ONLY' cannot be assigned any other roles"); + } + if (hasReadOnly && upsertUserRequest.getGroups() != null && !upsertUserRequest.getGroups().isEmpty()) { + throw new IllegalArgumentException("User with role 'USER_READ_ONLY' cannot be added to groups"); + } + } return userResource.upsertUser(upsertUserRequest, id); } @Override public void addRoleToApplicationUser(String applicationId, String role) { - applicationResource.addRoleToApplicationUser(applicationId, role); + if (role == null || role.isBlank()) { + throw new IllegalArgumentException("role cannot be null or blank"); + } + String normalized = role.toUpperCase(); + // Delegate full authorization to server; only normalize + applicationResource.addRoleToApplicationUser(applicationId, normalized); } @Override @@ -172,7 +210,12 @@ public List listApplications() { @Override public void removeRoleFromApplicationUser(String applicationId, String role) { - applicationResource.removeRoleFromApplicationUser(applicationId, role); + if (role == null || role.isBlank()) { + throw new IllegalArgumentException("role cannot be null or blank"); + } + String normalized = role.toUpperCase(); + // Delegate full authorization to server; only normalize + applicationResource.removeRoleFromApplicationUser(applicationId, normalized); } @Override diff --git a/orkes-client/src/main/java/io/orkes/conductor/client/model/UpsertGroupRequest.java b/orkes-client/src/main/java/io/orkes/conductor/client/model/UpsertGroupRequest.java index 8d17ac89..ed406852 100644 --- a/orkes-client/src/main/java/io/orkes/conductor/client/model/UpsertGroupRequest.java +++ b/orkes-client/src/main/java/io/orkes/conductor/client/model/UpsertGroupRequest.java @@ -28,7 +28,7 @@ public class UpsertGroupRequest { /** * a default Map<TargetType, Set<Access> to share permissions, allowed target types: - * WORKFLOW_DEF, TASK_DEF + * WORKFLOW_DEF, TASK_DEF, WORKFLOW_SCHEDULE */ public enum InnerEnum { CREATE("CREATE"), @@ -71,9 +71,15 @@ public static InnerEnum fromValue(String input) { public enum RolesEnum { ADMIN("ADMIN"), USER("USER"), + USER_READ_ONLY("USER_READ_ONLY"), WORKER("WORKER"), + UNRESTRICTED_WORKER("UNRESTRICTED_WORKER"), METADATA_MANAGER("METADATA_MANAGER"), - WORKFLOW_MANAGER("WORKFLOW_MANAGER"); + WORKFLOW_MANAGER("WORKFLOW_MANAGER"), + APPLICATION_MANAGER("APPLICATION_MANAGER"), + APPLICATION_CREATOR("APPLICATION_CREATOR"), + METADATA_API("METADATA_API"), + PROMPT_MANAGER("PROMPT_MANAGER"); private final String value; diff --git a/orkes-client/src/main/java/io/orkes/conductor/client/model/UpsertUserRequest.java b/orkes-client/src/main/java/io/orkes/conductor/client/model/UpsertUserRequest.java index 30a1a1dd..90a2f5ec 100644 --- a/orkes-client/src/main/java/io/orkes/conductor/client/model/UpsertUserRequest.java +++ b/orkes-client/src/main/java/io/orkes/conductor/client/model/UpsertUserRequest.java @@ -32,9 +32,15 @@ public class UpsertUserRequest { public enum RolesEnum { ADMIN("ADMIN"), USER("USER"), + USER_READ_ONLY("USER_READ_ONLY"), WORKER("WORKER"), + UNRESTRICTED_WORKER("UNRESTRICTED_WORKER"), METADATA_MANAGER("METADATA_MANAGER"), - WORKFLOW_MANAGER("WORKFLOW_MANAGER"); + WORKFLOW_MANAGER("WORKFLOW_MANAGER"), + APPLICATION_MANAGER("APPLICATION_MANAGER"), + APPLICATION_CREATOR("APPLICATION_CREATOR"), + METADATA_API("METADATA_API"), + PROMPT_MANAGER("PROMPT_MANAGER"); private final String value; diff --git a/orkes-client/src/main/java/io/orkes/conductor/client/util/RoleValidation.java b/orkes-client/src/main/java/io/orkes/conductor/client/util/RoleValidation.java new file mode 100644 index 00000000..54f27864 --- /dev/null +++ b/orkes-client/src/main/java/io/orkes/conductor/client/util/RoleValidation.java @@ -0,0 +1,40 @@ +/* + * Copyright 2025 Conductor Authors. + *

+ * Licensed 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 io.orkes.conductor.client.util; + +import java.util.Map; +import java.util.Set; + +public final class RoleValidation { + + private RoleValidation() {} + + private static final Set APP_ASSIGNABLE_ROLES = Set.of("APPLICATION_CREATOR", "WORKER", "METADATA_API"); + + private static final Set GROUP_DEFAULT_TARGET_TYPES = Set.of("WORKFLOW_DEF", "TASK_DEF", "WORKFLOW_SCHEDULE"); + + public static boolean isAssignableToApplication(String role) { + if (role == null) return false; + return APP_ASSIGNABLE_ROLES.contains(role.toUpperCase()); + } + + public static void validateGroupDefaultAccessKeys(Map defaultAccess) { + if (defaultAccess == null || defaultAccess.isEmpty()) return; + for (String key : defaultAccess.keySet()) { + if (!GROUP_DEFAULT_TARGET_TYPES.contains(key)) { + throw new IllegalArgumentException( + "Unsupported target type " + key + ", supported: WORKFLOW_DEF, TASK_DEF, WORKFLOW_SCHEDULE"); + } + } + } +}