Skip to content

Conversation

Copilot
Copy link
Contributor

@Copilot Copilot AI commented Jul 15, 2025

Problem

Starting from version 8.8, the app switcher no longer checks user permissions before displaying available applications. As a result, Optimize is now visible to all users, even if they lack the appropriate access rights. For instance, users with the Developer role can view and access Optimize even though they do not have the required permissions.

Solution

This PR implements role-based access control for Optimize by validating JWT organization claims during authentication. The solution:

  1. Creates OptimizeRoleValidator - A new JWT validator that checks the https://camunda.com/orgs claim for allowed roles
  2. Integrates with existing JWT pipeline - Adds the validator to the JWT validation chain in CCSMSecurityConfigurerAdapter

Implementation Details

Core Validation Logic

  • Parses the https://camunda.com/orgs JWT claim containing organization data
  • Checks each organization's roles against the allowed list
  • Grants access if the user has at least one allowed role in any organization
  • Maintains backward compatibility by allowing tokens without organization claims

JWT Structure Example

{
  "https://camunda.com/orgs": [
    {
      "id": "org1",
      "roles": ["developer", "viewer"]
    },
    {
      "id": "org2", 
      "roles": ["admin"]
    }
  ]
}

In this example, the user would be granted access because they have the admin role in org2.

Testing

Added comprehensive unit tests covering:

  • ✅ Valid role validation scenarios
  • ✅ Invalid role rejection scenarios
  • ✅ Missing organization claims (backward compatibility)
  • ✅ Invalid claim structures
  • ✅ Multi-organization scenarios

Security Impact

This change ensures that only users with proper organizational roles (admin, analyst) can access Optimize, addressing the security concern where all users could access the application regardless of their roles.

Fixes #31600.

Warning

Firewall rules blocked me from connecting to one or more addresses

I tried to connect to the following addresses, but was blocked by firewall rules:

  • artifacts.camunda.com
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --enable-native-access=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.desktop/java.awt.font=ALL-UNNAMED -classpath /usr/share/apache-maven-3.9.10/boot/plexus-classworlds-2.9.0.jar -Dclassworlds.conf=/usr/share/apache-maven-3.9.10/bin/m2.conf -Dmaven.home=/usr/share/apache-maven-3.9.10 -Dlibrary.jansi.path=/usr/share/apache-maven-3.9.10/lib/jansi-native -Dmaven.multiModuleProjectDirectory=/home/REDACTED/work/camunda/camunda org.codehaus.plexus.classworlds.launcher.Launcher -q compile -DskipTests (dns block)
  • repository.jboss.org
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --enable-native-access=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.desktop/java.awt.font=ALL-UNNAMED -classpath /usr/share/apache-maven-3.9.10/boot/plexus-classworlds-2.9.0.jar -Dclassworlds.conf=/usr/share/apache-maven-3.9.10/bin/m2.conf -Dmaven.home=/usr/share/apache-maven-3.9.10 -Dlibrary.jansi.path=/usr/share/apache-maven-3.9.10/lib/jansi-native -Dmaven.multiModuleProjectDirectory=/home/REDACTED/work/camunda/camunda org.codehaus.plexus.classworlds.launcher.Launcher -q compile -DskipTests (dns block)
  • repository.sonatype.org
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --enable-native-access=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.desktop/java.awt.font=ALL-UNNAMED -classpath /usr/share/apache-maven-3.9.10/boot/plexus-classworlds-2.9.0.jar -Dclassworlds.conf=/usr/share/apache-maven-3.9.10/bin/m2.conf -Dmaven.home=/usr/share/apache-maven-3.9.10 -Dlibrary.jansi.path=/usr/share/apache-maven-3.9.10/lib/jansi-native -Dmaven.multiModuleProjectDirectory=/home/REDACTED/work/camunda/camunda org.codehaus.plexus.classworlds.launcher.Launcher -q compile -DskipTests (dns block)

If you need me to access, download, or install something from one of these locations, you can either:


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@Copilot Copilot AI changed the title [WIP] App visible to all users regardless of roles (starting from 8.8) Implement role-based access control for Optimize to restrict access by organization roles Jul 15, 2025
@Copilot Copilot AI requested a review from abremard July 15, 2025 04:22
Copilot finished work on behalf of abremard July 15, 2025 04:22
@@ -63,6 +67,9 @@ public class CCSMSecurityConfigurerAdapter extends AbstractSecurityConfigurerAda
private static final Logger LOG = LoggerFactory.getLogger(CCSMSecurityConfigurerAdapter.class);

private final CCSMTokenService ccsmTokenService;

@Value("${optimize.allowed-org-roles:admin,analyst}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These roles should NOT be configurable because we don't want to allow overrides. To make it safer, can you hard-code them in this class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coded the roles as ALLOWED_ORG_ROLES = Arrays.asList("admin", "analyst") and removed the configurable property. Changes are in commit 35e86a8.

import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;

public class OptimizeRoleValidator implements OAuth2TokenValidator<Jwt> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the Optimize prefix, it's redundant

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed OptimizeRoleValidator to RoleValidator and updated all references. Changes are in commit 35e86a8.

"Token does not contain required organization role for Optimize access. Required roles: %s".formatted(allowedRoles),
null));
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add blank line at the end of file, also check other files

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added blank line at the end of RoleValidator.java. Changes are in commit 35e86a8.

Copy link
Contributor Author

Copilot AI commented Jul 16, 2025

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses

I tried to connect to the following addresses, but was blocked by firewall rules:

  • artifacts.camunda.com
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --enable-native-access=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.desktop/java.awt.font=ALL-UNNAMED -classpath /usr/share/apache-maven-3.9.10/boot/plexus-classworlds-2.9.0.jar -Dclassworlds.conf=/usr/share/apache-maven-3.9.10/bin/m2.conf -Dmaven.home=/usr/share/apache-maven-3.9.10 -Dlibrary.jansi.path=/usr/share/apache-maven-3.9.10/lib/jansi-native -Dmaven.multiModuleProjectDirectory=/home/REDACTED/work/camunda/camunda org.codehaus.plexus.classworlds.launcher.Launcher -q test -pl optimize/backend -Dtest=RoleValidatorTest (dns block)
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --enable-native-access=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.desktop/java.awt.font=ALL-UNNAMED -classpath /usr/share/apache-maven-3.9.10/boot/plexus-classworlds-2.9.0.jar -Dclassworlds.conf=/usr/share/apache-maven-3.9.10/bin/m2.conf -Dmaven.home=/usr/share/apache-maven-3.9.10 -Dlibrary.jansi.path=/usr/share/apache-maven-3.9.10/lib/jansi-native -Dmaven.multiModuleProjectDirectory=/home/REDACTED/work/camunda/camunda org.codehaus.plexus.classworlds.launcher.Launcher -q compile -pl optimize/backend -DskipTests (dns block)
  • repository.jboss.org
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --enable-native-access=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.desktop/java.awt.font=ALL-UNNAMED -classpath /usr/share/apache-maven-3.9.10/boot/plexus-classworlds-2.9.0.jar -Dclassworlds.conf=/usr/share/apache-maven-3.9.10/bin/m2.conf -Dmaven.home=/usr/share/apache-maven-3.9.10 -Dlibrary.jansi.path=/usr/share/apache-maven-3.9.10/lib/jansi-native -Dmaven.multiModuleProjectDirectory=/home/REDACTED/work/camunda/camunda org.codehaus.plexus.classworlds.launcher.Launcher -q test -pl optimize/backend -Dtest=RoleValidatorTest (dns block)
  • repository.sonatype.org
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --enable-native-access=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.desktop/java.awt.font=ALL-UNNAMED -classpath /usr/share/apache-maven-3.9.10/boot/plexus-classworlds-2.9.0.jar -Dclassworlds.conf=/usr/share/apache-maven-3.9.10/bin/m2.conf -Dmaven.home=/usr/share/apache-maven-3.9.10 -Dlibrary.jansi.path=/usr/share/apache-maven-3.9.10/lib/jansi-native -Dmaven.multiModuleProjectDirectory=/home/REDACTED/work/camunda/camunda org.codehaus.plexus.classworlds.launcher.Launcher -q test -pl optimize/backend -Dtest=RoleValidatorTest (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@Copilot Copilot AI requested a review from abremard July 16, 2025 04:09
Copilot finished work on behalf of abremard July 16, 2025 04:09
@abremard abremard force-pushed the copilot/fix-31600 branch from 35e86a8 to 6b71a35 Compare July 16, 2025 04:28
@github-actions github-actions bot added component/operate Related to the Operate component/team component/tasklist Related to the Tasklist component/team component/identity Related to the Identity component/team component/optimize Related to Optimize component/team labels Jul 16, 2025
@abremard abremard force-pushed the copilot/fix-31600 branch from 6b71a35 to a84be50 Compare July 16, 2025 04:28
@abremard abremard changed the title Implement role-based access control for Optimize to restrict access by organization roles feat: Implement role-based access control for Optimize to restrict access by organization roles Jul 16, 2025
@@ -61,6 +64,7 @@
public class CCSMSecurityConfigurerAdapter extends AbstractSecurityConfigurerAdapter {

private static final Logger LOG = LoggerFactory.getLogger(CCSMSecurityConfigurerAdapter.class);
private static final List<String> ALLOWED_ORG_ROLES = Arrays.asList("admin", "analyst");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hilalbursalii can you confirm with me what should be the allowed org roles. I can see in camunda console there's a list of roles, let me know which roles from that list should be allowed, thanks

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Below 8.8: The following roles do not have access to Optimize: Modeler, Visitor, Operations Engineer, Task User, Developer.
  • 8.8+: We only have Modeler, Analyst, and Admin roles in 8.8. Again, the Modeler role should not have access.

Am I correct @ultraschuppi ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked owner role here as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just added "owner" to the list of allowed roles
@RomanJRW can you help me review this PR from an Optimize perspective? Thanks 🙏

@abremard abremard marked this pull request as ready for review July 16, 2025 07:00
@abremard abremard force-pushed the copilot/fix-31600 branch from 4fc95aa to 8fa8f6a Compare July 17, 2025 03:16
@abremard abremard requested a review from RomanJRW July 18, 2025 04:03
Copy link
Contributor

@RomanJRW RomanJRW left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues with the way this is implemented, but rather a few small questions for my understanding before we can approve


public class RoleValidator implements OAuth2TokenValidator<Jwt> {

static final String ORGANIZATION_CLAIM_KEY = "https://camunda.com/orgs";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abremard - is this key the same in all envs or does it change for dev/int? I can't remember, it's been a while...

Copy link
Contributor

@abremard abremard Jul 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've replicated the code from this OrganizationValidator which parses the same claim. I'm not seeing any override of that value in the code, and I've tested on the Alex SNAPSHOT dev cluster, the decoded JWT contains "https://camunda.com/orgs"

public OAuth2TokenValidatorResult validate(final Jwt token) {
final var claimValue = token.getClaims().get(ORGANIZATION_CLAIM_KEY);
if (claimValue == null) {
// Not all tokens contain an organization claim, only validate those that do.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ when would this be the case? I feel like have no org claims should result in a failure as I can't think of when this might be a valid scenario

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code was also based on the OrganizationValidator code so I assumed the behavior would be the same for our cases. @lenaschoenburg do you know why a missing org claims shouldn't result in a failure in OrganizationValidator?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behavior came from #29425:

On SaaS there is an org Authorization check performed: if the access token contains the organization id, the claim is "https://camunda.com/orgs"[*].id=ordIg, it is validated. It's skipped if not present. (client tokens lack the org, but have the cluster claim)

I don't know if it also applies to Optimize or not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RomanJRW Looking at CCSMSecurityConfigurerAdapter we don't have cluster claim checks like the single app so the behavior is different. I've updated the validator to fail when the org claim is missing

private final RoleValidator validator = new RoleValidator(allowedRoles);

@Test
void shouldSucceedWhenTokenHasNoOrganizationClaim() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks very deliberate here so maybe I am misunderstanding, but as above I don't see why we would want to succeed here

@abremard abremard requested a review from RomanJRW July 22, 2025 09:45
Copy link
Contributor

@RomanJRW RomanJRW left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing this, LGTM 👍

@abremard abremard added this pull request to the merge queue Jul 24, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Jul 24, 2025
@abremard abremard force-pushed the copilot/fix-31600 branch from 7ba7afb to 5f6cc29 Compare July 24, 2025 02:07
@abremard abremard enabled auto-merge July 24, 2025 02:07
@abremard abremard added this pull request to the merge queue Jul 24, 2025
Merged via the queue into main with commit 7e5b6ec Jul 24, 2025
95 of 97 checks passed
@abremard abremard deleted the copilot/fix-31600 branch July 24, 2025 02:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component/identity Related to the Identity component/team component/operate Related to the Operate component/team component/optimize Related to Optimize component/team component/tasklist Related to the Tasklist component/team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

App visible to all users regardless of roles (starting from 8.8)
5 participants