Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -172,7 +210,12 @@ public List<ConductorApplication> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
public class UpsertGroupRequest {
/**
* a default Map&lt;TargetType, Set&lt;Access&gt; to share permissions, allowed target types:
* WORKFLOW_DEF, TASK_DEF
* WORKFLOW_DEF, TASK_DEF, WORKFLOW_SCHEDULE
*/
public enum InnerEnum {
CREATE("CREATE"),
Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2025 Conductor Authors.
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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<String> APP_ASSIGNABLE_ROLES = Set.of("APPLICATION_CREATOR", "WORKER", "METADATA_API");

private static final Set<String> 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<String, ?> 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");
}
}
}
}
Loading