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 Name | Purpose |
|---|---|
| Entity Access Modeling | Controls user access in the Modelling module |
| Entity Access Authoring | Controls 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:
| Field | Description |
|---|---|
| Application | Target application |
| Rule Maintenance Set | RMS mapped to the target application |
| Rule Set | Rule Set mapped to the target RMS |
| Authorization Type | User or Group |
| User / Group Value | Selected user or role collection / group |
| Permission Type | Read / Write / All |
Permission levels
The system supports the following permission levels:
| Permission | Description |
|---|---|
| Read | User can only view hierarchy and rule data |
| Write | User can modify existing rule data |
| All | Full 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-uriis missing or blank whileapp.platform=AZURE, the application throws anIllegalStateExceptionand 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, andresourcevary 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 productioncheckLoginIframe: falsepkceMethod: "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.jsonexists in thepublicfolder - Authentication issue: verify Keycloak realm access and client configuration
401error: verify the bearer token and request headers- CORS issue: route API calls through the NGINX proxy
Best practices
- Use
check-ssofor local development when required - Use
login-requiredfor 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-DEVenvironment - The same instance serves multiple environments such as
DEV,QA, andDEMO - 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:
DEVQADEMO
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:
DEVQADEMO
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:
DEVQADEMO
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-DEVdeployed Java backend - Pass
envfrom UI application state when using the UI artifact - Provide an environment dropdown in the shared IDM UI
- Include the
Envrequest header for all direct backend API calls that do not go through the IDM UI artifact