Skip to main content
Version: 2.0.0

How to Configure

User data-level access control

Overview

In the IDM application, a dedicated Global Application is maintained to manage user data-level access control across the complete application hierarchy.

This access control determines:

  • Which hierarchy objects a user can view
  • Which modules the user can access
  • What level of permissions the user has

Hierarchy visibility in the IDM UI is driven by these configured access rules.

Application hierarchy structure

The IDM hierarchy consists of the following levels:

Application
└── Rule Maintenance Set (RMS)
└── Rule Set (RS)
├── Decision Tables (DT)
└── Text Rules (TR)

Access can be controlled at every hierarchy level.

Access control modules

The system contains two primary access management rules:

Rule NamePurpose
Entity Access ModelingControls user access in the Modelling module
Entity Access AuthoringControls user access in the Manage Decision / Authoring module

Entity Access Modeling

This rule manages user access permissions for the Modelling module.

Based on configured permissions, users can:

  • View hierarchy data
  • Create or update rules for hierarchies they have access to
  • Work within the Modelling module based on assigned access

Entity Access Authoring

This rule manages user access permissions for the Manage Decision or Authoring module.

It controls access for:

  • Decision Tables
  • Text Rules
  • Rule authoring operations
  • Rule editing and execution permissions

Access configuration attributes

While configuring access rules, the following attributes are maintained:

FieldDescription
ApplicationTarget application
Rule Maintenance SetRMS mapped to the target application
Rule SetRule Set mapped to the target RMS
Authorization TypeUser or Group
User / Group ValueSelected user or role collection / group
Permission TypeRead / Write / All

Permission levels

The system supports the following permission levels:

PermissionDescription
ReadUser can only view hierarchy and rule data
WriteUser can modify existing rule data
AllFull access including create, update, and delete

IDM 3.0 Configuration for Azure

This page covers the Azure-specific IDM 3.0 configuration for backend authentication, database datatype handling, React Keycloak integration, and NGINX routing.

Backend authentication with Keycloak

Azure does not provide SAP XSUAA, so IDM uses Keycloak as the OAuth2/OIDC provider. The backend validates incoming JWT tokens against the configured Keycloak realm.

Required application properties

Set the platform and Keycloak issuer URI in application.properties:

app.platform=AZURE
keycloak.issuer-uri=https://keycloak.cherrywork.com/realms/cw-topas

If keycloak.issuer-uri is missing or blank while app.platform=AZURE, the application throws an IllegalStateException and does not start.

Security behavior

The Spring Security configuration:

  • Whitelists selected endpoints such as Swagger, actuator, and monitoring URLs
  • Uses stateless session handling
  • Validates JWT tokens from Keycloak when running on Azure
  • Falls back to XSUAA only when app.platform=SAP
  • Fails fast when the platform value is invalid or the required auth configuration is missing

Security configuration reference

@Configuration
@Profile("idm-security")
public class SecurityConfiguration {

Logger logger = LoggerFactory.getLogger(this.getClass());

private static final String[] authWhitelist = {
"/v1/platform/instance/credentials/**",
"/v3/api-docs/**",
"/swagger-ui.html/**",
"/swagger-ui/index.html/**",
"/swagger-ui/**",
"/swagger-resources/**",
"/monitoring**",
"/v4/**",
"/idm-swagger/**",
"/actuator/**"
};

@Value("${islocal}")
private String islocal;

@Value("${app.platform}")
private String appPlatform;

@Value("${keycloak.issuer-uri:}")
private String keycloakIssuerUri;

@Autowired(required = false)
private XsuaaServiceConfiguration xsuaaServiceConfiguration;

public SecurityConfiguration() {
logger.info("Inside Security Configuration");
}

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http,
FeatureApiMappingDto featureApiMappingDto,
FeatureFlagDto featureFlagDto) throws Exception {

logger.info("Configuring security — platform: {}, islocal: {}", appPlatform, islocal);

List<String> authorizedFeatureEndpoints = new ArrayList<>();
List<String> activeFeatures = new ArrayList<>();

for (Map.Entry<String, String> entry : featureFlagDto.getFeatures().entrySet()) {
if ("True".equals(entry.getValue()))
activeFeatures.add(entry.getKey());
}

for (Map.Entry<String, List<String>> entry : featureApiMappingDto.getFeatureApiMappings().entrySet()) {
if (activeFeatures.contains(entry.getKey()))
authorizedFeatureEndpoints.addAll(entry.getValue());
if (ApplicationConstants.OTHERS.equals(entry.getKey()))
authorizedFeatureEndpoints.addAll(entry.getValue());
}

String[] authorizedEndpoints = authorizedFeatureEndpoints.toArray(new String[0]);

http.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

if ("true".equalsIgnoreCase(islocal)) {
http.authorizeHttpRequests(authz -> authz
.requestMatchers(authWhitelist).permitAll()
.requestMatchers(authorizedEndpoints).permitAll()
.anyRequest().denyAll());

} else if (SharedConstant.AZURE.equals(appPlatform)) {

if (keycloakIssuerUri == null || keycloakIssuerUri.isBlank()) {
throw new IllegalStateException(
"keycloak.issuer-uri must be set when app.platform=AZURE");
}

var jwtDecoder = NimbusJwtDecoder.withIssuerLocation(keycloakIssuerUri).build();
logger.info("Keycloak JwtDecoder created for issuer: {}", keycloakIssuerUri);

http.authorizeHttpRequests(authz -> authz
.requestMatchers(authWhitelist).permitAll()
.requestMatchers(authorizedEndpoints).authenticated()
.anyRequest().denyAll())
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(jwtDecoder)
.jwtAuthenticationConverter(keycloakJwtAuthenticationConverter())));

} else if (SharedConstant.SAP.equals(appPlatform)) {

if (xsuaaServiceConfiguration == null) {
throw new IllegalStateException(
"XsuaaServiceConfiguration bean is required when app.platform=SAP");
}
http.authorizeHttpRequests(authz -> authz
.requestMatchers(authWhitelist).permitAll()
.requestMatchers(authorizedEndpoints).authenticated()
.anyRequest().denyAll())
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(xsuaaJwtAuthenticationConverter())));

} else {
throw new IllegalArgumentException(
"Unknown app.platform: '" + appPlatform
+ "'. Expected '" + SharedConstant.AZURE
+ "' or '" + SharedConstant.SAP + "'.");
}

return http.build();
}

private Converter<Jwt, AbstractAuthenticationToken> keycloakJwtAuthenticationConverter() {
return jwt -> {
List<GrantedAuthority> authorities = new ArrayList<>();

Map<String, Object> realmAccess = jwt.getClaim("realm_access");
if (realmAccess != null && realmAccess.get("roles") instanceof Collection<?> realmRoles) {
realmRoles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.toString().toUpperCase()))
.forEach(authorities::add);
}

Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
if (resourceAccess != null) {
resourceAccess.forEach((client, value) -> {
if (value instanceof Map<?, ?> clientMap
&& clientMap.get("roles") instanceof Collection<?> clientRoles) {
clientRoles.stream()
.map(role -> new SimpleGrantedAuthority(
"ROLE_" + role.toString().toUpperCase()))
.forEach(authorities::add);
}
});
}

logger.debug("Keycloak authorities extracted: {}", authorities);
return new JwtAuthenticationToken(jwt, authorities);
};
}

private Converter<Jwt, AbstractAuthenticationToken> xsuaaJwtAuthenticationConverter() {
TokenAuthenticationConverter converter =
new TokenAuthenticationConverter(xsuaaServiceConfiguration);
converter.setLocalScopeAsAuthorities(true);
return converter;
}
}

MySQL datatype conversion helpers

The following helper methods are used to account for differences in how MySQL handles date, boolean, integer, and blob values.

public static Date convertToDate(Object value) {
return switch (value) {
case Date date -> date;
case LocalDateTime ldt -> Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());
case LocalDate ld -> Date.from(ld.atStartOfDay(ZoneId.systemDefault()).toInstant());
case null, default -> null;
};
}

public static Boolean convertToBoolean(Object value) {
return switch (value) {
case Boolean bool -> bool;
case Long l -> l == 1L;
case Integer i -> i == 1;
case null, default -> Boolean.FALSE;
};
}

public static Integer convertToInteger(Object value) {
return switch (value) {
case Integer i -> i;
case Long l -> l.intValue();
case null, default -> null;
};
}

private String buildBlobConversion(String tableName, String columnName) {
String col = tableName + "." + columnName;
return switch (databaseName) {
case CommonsConfigConstants.DATABASE_TYPE_MYSQL -> "CONVERT(" + col + " USING utf8mb4)";
case CommonsConfigConstants.DATABASE_TYPE_HANA -> "cast(BINTOSTR(cast(" + col + " as binary)) as varchar)";
case CommonsConfigConstants.DATABASE_TYPE_ORACLE -> "UTL_RAW.CAST_TO_VARCHAR2(" + col + ")";
case CommonsConfigConstants.DATABASE_TYPE_MSSQL -> "CONVERT(varchar, " + col + ")";
default -> "CAST(" + col + " AS CHAR)";
};
}

React TypeScript app with Keycloak

This section explains the UI-side Keycloak integration for the IDM React application deployed on Azure.

Keycloak client configuration

Create azureKeycloak.json in /public/azureKeycloak.json:

{
"realm": "cw-topas",
"auth-server-url": "https://keycloak.cherrywork.com/",
"ssl-required": "external",
"resource": "IDM3.0",
"public-client": true,
"confidential-port": 0
}

This file is environment-specific. Values such as realm, auth-server-url, and resource vary by project, IDM setup, and target environment such as Dev, QA, or Prod.

React entry point

Wrap the application with the Keycloak provider:

createRoot(rootElement).render(
<Provider store={store}>
<BrowserRouter>
<KeycloakWrapper>
<Wrapper />
</KeycloakWrapper>
</BrowserRouter>
</Provider>
);

Keycloak flow

The UI flow is:

  • Load /azureKeycloak.json
  • Initialize keycloak-js
  • Wrap the application using ReactKeycloakProvider
  • Store the access token for API calls
  • Refresh the token and update request headers when authentication refresh succeeds

Initialization details

Recommended options:

  • onLoad: "login-required" for production
  • checkLoginIframe: false
  • pkceMethod: "S256"
  • redirectUri: window.location.origin + "/"

For local work, check-sso may be used instead of login-required.

Keycloak wrapper reference

import React, {
createContext,
useContext,
useEffect,
useState,
ReactNode,
} from "react";
import { ReactKeycloakProvider, useKeycloak } from "@react-keycloak/web";
import Keycloak from "keycloak-js";
import { useDispatch } from "react-redux";
import { updateRequestOptions } from "./store/slices/globalSlice";

interface KeycloakContextType {
keycloak: Keycloak | null;
initialized: boolean;
}

let keycloakInstance: Keycloak | null = null;

export const getKeycloakInstance = (): Keycloak | null => keycloakInstance;

const KeycloakContext = createContext<KeycloakContextType>({
keycloak: null,
initialized: false,
});

export const useKeycloakContext = () => useContext(KeycloakContext);

interface KeycloakWrapperProps {
children: ReactNode;
}

const KeycloakInner = ({ children }: { children: ReactNode }) => {
const { keycloak, initialized } = useKeycloak();
const dispatch = useDispatch();

useEffect(() => {
if (!initialized || !keycloak?.authenticated) return;

const storeToken = () => {
if (keycloak?.token) {
dispatch(
updateRequestOptions({
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
Authorization: `Bearer ${keycloak.token}`,
},
method: "GET",
body: undefined,
} as any)
);
}
};

storeToken();

keycloak.onAuthRefreshSuccess = () => {
console.log("Token refreshed, updating Redux");
storeToken();
};
}, [initialized, keycloak?.authenticated, keycloak?.token]);

return (
<KeycloakContext.Provider value={{ keycloak, initialized }}>
{children}
</KeycloakContext.Provider>
);
};

export const KeycloakWrapper = ({ children }: KeycloakWrapperProps) => {
const [keycloak, setKeycloak] = useState<Keycloak | null>(null);
const [configLoaded, setConfigLoaded] = useState(false);

useEffect(() => {
const loadKeycloakConfig = async () => {
try {
const response = await fetch("/azureKeycloak.json");
const jsonConfig = await response.json();

const kc = new Keycloak({
realm: jsonConfig.realm,
clientId: jsonConfig.resource,
url: jsonConfig["auth-server-url"],
});

keycloakInstance = kc;
setKeycloak(kc);
setConfigLoaded(true);
} catch (error) {
console.error("Failed to load Keycloak config:", error);
}
};

loadKeycloakConfig();
}, []);

if (!configLoaded || !keycloak) {
return <div>Loading configuration...</div>;
}

return (
<ReactKeycloakProvider
authClient={keycloak}
initOptions={{
onLoad: "login-required",
checkLoginIframe: false,
pkceMethod: "S256",
redirectUri: window.location.origin + "/",
}}
LoadingComponent={<div>Authenticating...</div>}
>
<KeycloakInner>{children}</KeycloakInner>
</ReactKeycloakProvider>
);
};

NGINX proxy configuration

Use NGINX to serve the React app and proxy backend API calls:

server {
listen 3000;

location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}

location /IDMServices/ {
proxy_pass https://idm-services-dev-v3.cherrywork.com/idm/;
proxy_set_header Authorization $http_authorization;
proxy_set_header Content-Type application/json;
proxy_ssl_server_name on;
}

location /WorkUtilsServices/ {
proxy_pass https://idm-services-dev-v3.cherrywork.com/idm/;
proxy_set_header Authorization $http_authorization;
proxy_ssl_server_name on;
}
}

Using the NGINX proxy helps avoid browser-side CORS issues while keeping the backend reachable through stable application routes.

Azure UI deployment notes

Example deployment flow:

az login
az acr login -n topascaf
docker push topascaf.azurecr.io/idm-ui:0008
az aks get-credentials --resource-group TOPAS-CAF --name topascaf
kubectl apply -f app.yaml

Common issues

  • Loading issue: ensure azureKeycloak.json exists in the public folder
  • Authentication issue: verify Keycloak realm access and client configuration
  • 401 error: verify the bearer token and request headers
  • CORS issue: route API calls through the NGINX proxy

Best practices

  • Use check-sso for local development when required
  • Use login-required for production
  • Keep tokens in managed application state only as needed
  • Do not hardcode secrets in code or config files committed to source control

CAF single instance integration

This section explains how to integrate CW CAF IDM 3.0 as a single instance across multiple environments using a multi-tenancy approach.

In this model:

  • A single IDM 3.0 application instance is deployed in the CW-CAF-DEV environment
  • The same instance serves multiple environments such as DEV, QA, and DEMO
  • Environment-specific behavior is controlled through UI state, destination configuration, and request headers

Architecture overview

The single-instance integration model uses one deployed application instance to connect to multiple environments.

Current backend instance:

  • https://cw-caf-idm-services-v3.cfapps.eu10-004.hana.ondemand.com/

Current UI instance:

  • https://incture-cherrywork-dev-cw-caf-dev-cw-caf-idm-ui-v3.cfapps.eu10-004.hana.ondemand.com/

Using the IDM 3.0 UI artifact

When consuming the IDM 3.0 UI artifact, the UI must accept a new prop named env.

Supported values:

  • DEV
  • QA
  • DEMO

The selected environment should be stored and managed in application state.

Example:

<ManageDecision
token={token}
env={environmentFromApplicationState}
destinations={destinations as Destination[]}
userDetails={userDetails}
applicationDetails={applicationDetails}
/>

UI integration behavior

Since there is only one shared IDM 3.0 UI application, the UI should provide an environment dropdown with:

  • DEV
  • QA
  • DEMO

Based on the selected option, users should be able to:

  • View rules for that environment
  • Edit rules for that environment
  • Manage rules for that environment

The selected environment should drive all downstream backend interactions through the configured integration pattern.

Using the IDM 3.0 backend

All backend destination configurations should point to the IDM 3.0 Java application deployed in CW-CAF-DEV.

Backend URL:

https://cw-caf-idm-services-v3.cfapps.eu10-004.hana.ondemand.com/

This means integrations should not create separate IDM backend targets for each environment when using the single-instance model. Instead, the runtime environment context should be passed explicitly.

Required request header for direct backend APIs

If any backend API call is made without going through the IDM 3.0 UI or UI artifact, the request must include an Env header.

Supported header values:

  • DEV
  • QA
  • DEMO

Example:

Env: DEV

Use the correct value based on the target environment for the request.

Integration summary

To support single-instance IDM 3.0 integration in CAF:

  • Point all IDM backend destinations to the CW-CAF-DEV deployed Java backend
  • Pass env from UI application state when using the UI artifact
  • Provide an environment dropdown in the shared IDM UI
  • Include the Env request header for all direct backend API calls that do not go through the IDM UI artifact