com.sap.cloud.security.ams.api.PrincipalBuilder Maven / Gradle / Ivy
/************************************************************************
* © 2019-2023 SAP SE or an SAP affiliate company. All rights reserved. *
************************************************************************/
package com.sap.cloud.security.ams.api;
import com.sap.cloud.security.ams.dcl.client.pdp.Attributes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import static com.sap.cloud.security.ams.api.Principal.*;
/**
* Builder for Principals. This allows adding the assigned Authorization Management
* principals.
*/
@SuppressWarnings("java:S3740")
public class PrincipalBuilder {
private static final String CLASS_SECURITY_CONTEXT_HOLDER = "org.springframework.security.core.context.SecurityContextHolder";
private static final String CLASS_SECURITY_CONTEXT = "com.sap.cloud.security.token.SecurityContext";
private static final String CLASS_SPRING_SECURITY_CONTEXT = "com.sap.cloud.security.adapter.spring.SpringSecurityContext";
static Class securityContextHolder; // spring
static Class securityContext; // java-security
static Class springSecurityContext; // java-security (adapter package)
private final PrincipalImpl principal;
private String authorizationParty;
private static final Logger LOGGER = LoggerFactory.getLogger(PrincipalBuilder.class);
static final String CLAIM_TEST_POLICIES = "test_policies";
static final String CLAIM_EMAIL = "email";
static final String CLAIM_GROUPS = "groups";
static final String CLAIM_AZP = "azp";
static final String CLAIM_AUD = "aud";
static final List ATTRIBUTES_PROCESSORS = AttributesProcessor.getAll();
static {
try {
Class.forName(CLASS_SECURITY_CONTEXT_HOLDER, false, PrincipalBuilder.class.getClassLoader());
securityContextHolder = Class.forName(CLASS_SECURITY_CONTEXT_HOLDER);
} catch (ClassNotFoundException e) {
LOGGER.info(
"PrincipalBuilder cannot instantiate Principal from SecurityContextHolder: ClassNotFoundException");
}
try {
Class.forName(CLASS_SECURITY_CONTEXT, false, PrincipalBuilder.class.getClassLoader());
securityContext = Class.forName(CLASS_SECURITY_CONTEXT);
} catch (ClassNotFoundException e) {
LOGGER.info("PrincipalBuilder cannot instantiate Principal from SecurityContext: ClassNotFoundException");
}
try {
Class.forName(CLASS_SPRING_SECURITY_CONTEXT, false, PrincipalBuilder.class.getClassLoader());
springSecurityContext = Class.forName(CLASS_SPRING_SECURITY_CONTEXT);
} catch (ClassNotFoundException e) {
LOGGER.info(
"PrincipalBuilder cannot instantiate Principal from SpringSecurityContext: ClassNotFoundException");
}
}
private PrincipalBuilder() {
principal = new PrincipalImpl();
}
/**
* Instantiates a free-style builder instance.
*
* @return principal builder.
*/
public static PrincipalBuilder create() {
return new PrincipalBuilder();
}
/**
* Instantiates a principal builder instance with given oidc token attributes.
* The tenant identifier is taken from "app_tid" or alternatively from "zid". The
* principal id is taken from "user_uuid" or alternatively from "sub".
*
* @param oidcTokenAttributes
* key-value map of attributes and its values, e.g. from the OIDC
* token.
* @return principal builder.
*/
public static PrincipalBuilder create(Map oidcTokenAttributes) {
String appTid = (String) oidcTokenAttributes.getOrDefault(CLAIM_APP_TID_KEY,
oidcTokenAttributes.get(Principal.CLAIM_ZID));
String userId = (String) oidcTokenAttributes.getOrDefault(CLAIM_USER_UUID_KEY,
oidcTokenAttributes.get(Principal.CLAIM_SUBJECT));
Object groups = oidcTokenAttributes.getOrDefault(CLAIM_GROUPS, Collections.emptyList());
Object policies = oidcTokenAttributes.getOrDefault(CLAIM_TEST_POLICIES, Collections.emptyList());
PrincipalBuilder principalBuilder = new PrincipalBuilder()
.appTid(appTid)
.id(userId)
.scimId((String) oidcTokenAttributes.get(CLAIM_SCIM_ID_KEY))
.clientId((String) getClientId(oidcTokenAttributes))
.email((String) oidcTokenAttributes.get(CLAIM_EMAIL));
if (policies instanceof Collection) {
principalBuilder.addPolicies((Collection) policies);
}
if (groups instanceof List) {
principalBuilder.groups((List) groups);
}
return principalBuilder;
}
private static Object getClientId(Map oidcTokenAttributes) {
if (oidcTokenAttributes.containsKey(CLAIM_AZP)) {
return oidcTokenAttributes.get(CLAIM_AZP);
} else if (oidcTokenAttributes.containsKey(CLAIM_AUD)) {
Object aud = oidcTokenAttributes.get(CLAIM_AUD);
if (aud instanceof String) {
return aud;
} else if (aud instanceof List) {
return ((List) aud).get(0);
}
}
return null;
}
/**
* Instantiates a builder instance with a given tenant id and principal identifier.
*
* @param appTid
* the unique tenant identifier
* @param id
* in case of user principal - the user id
*
* @return principal builder.
*/
public static PrincipalBuilder create(String appTid, String id) {
return new PrincipalBuilder().appTid(appTid).id(id);
}
/**
* Tries to build a {@link Principal} instance with an OIDC token that is stored
* thread-locally in the {@code com.sap.cloud.security.token.SecurityContext}
* class.
*
* The SecurityContext class has to be provided with this dependency: {@code
*
* com.sap.cloud.security
* java-api
*
* } Use {@code Principal.create()}.
*
* @return principal
*/
static Principal buildFromSecurityContext() {
if (securityContext == null) {
return null;
}
try {
LOGGER.debug("Try to build Principal based on token in SecurityContext.");
return createPrincipalFromSecurityContext(securityContext);
} catch (ReflectiveOperationException e) {
LOGGER.warn("Principal cannot be instantiated from SecurityContext: {}", e.getMessage());
}
return null;
}
/**
* Tries to build a {@link Principal} instance with a token that is stored in
* the Spring's
* {@code org.springframework.security.core.context.SecurityContextHolder}
* class.
*
* The SecurityContextHolder class has to be prepared with this dependency:
* {@code
*
* com.sap.cloud.security.ams.client
* spring-ams
*
*
* com.sap.cloud.security
* spring-security
*
* }
*
* Use {@code Principal.create()}.
*
* @return principal
*/
static Principal buildFromSecurityContextHolder() {
if (securityContextHolder == null) {
return null;
}
if (springSecurityContext != null) {
try {
LOGGER.debug("Try to build Principal based on token in SpringSecurityContext.");
return createPrincipalFromSecurityContext(springSecurityContext);
} catch (ReflectiveOperationException e) {
// this is always true for Spring oauth2client integration
LOGGER.info("Principal cannot be instantiated from SpringSecurityContext.getToken(): {}",
e.getMessage());
}
}
try {
LOGGER.debug("Try to build Principal based on token in SecurityContextHolder.");
return fallbackForOidcTestPolicies(securityContextHolder);
} catch (ReflectiveOperationException e) {
LOGGER.error("Principal cannot be instantiated from SecurityContextHolder: {}", e.getMessage());
}
return null;
}
private static Principal fallbackForOidcTestPolicies(Class securityContextHolder)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Method getContext = securityContextHolder.getDeclaredMethod("getContext");
Object securityContext = getContext.invoke(null);
Method getAuthentication = securityContext.getClass().getMethod("getAuthentication");
Object authentication = getAuthentication.invoke(securityContext);
Method isAuthenticated = authentication.getClass().getMethod("isAuthenticated");
if ((boolean) isAuthenticated.invoke(authentication)) {
Method getPrincipal = authentication.getClass().getMethod("getPrincipal");
Object token = getPrincipal.invoke(authentication);
String tokenClass = token.getClass().getName();
LOGGER.debug("Try to instantiate Principal from SecurityContextHolder (Token class: {})", tokenClass);
if (tokenClass.startsWith("org.springframework.security.oauth2.core.oidc.user")) {
Method getClaims = token.getClass().getMethod("getClaims");
return PrincipalBuilder.create((Map) getClaims.invoke(token)).build();
}
}
return null;
}
private static Principal createPrincipalFromSecurityContext(Class securityContext)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Method getToken = securityContext.getDeclaredMethod("getToken");
Object tokenClass = getToken.invoke(null);
if (tokenClass == null) {
return null;
}
Method getAppTid = tokenClass.getClass().getMethod("getAppTid");
Object appTid = getAppTid.invoke(tokenClass);
Method getClaimAsString = tokenClass.getClass().getMethod("getClaimAsString", String.class);
Object userId = getClaimAsString.invoke(tokenClass, CLAIM_USER_UUID_KEY);
if (Objects.isNull(userId)) {
userId = getClaimAsString.invoke(tokenClass, Principal.CLAIM_SUBJECT);
}
Object scimId = getClaimAsString.invoke(tokenClass, CLAIM_SCIM_ID_KEY);
Object email = getClaimAsString.invoke(tokenClass, CLAIM_EMAIL);
Method getClaimAsStringList = tokenClass.getClass().getMethod("getClaimAsStringList", String.class);
Object groups = getClaimAsStringList.invoke(tokenClass, CLAIM_GROUPS);
Object policies = getClaimAsStringList.invoke(tokenClass, CLAIM_TEST_POLICIES);
Method getClientId = tokenClass.getClass().getMethod("getClientId");
Object clientId = getClientId.invoke(tokenClass);
return PrincipalBuilder.create()
.clientId((String) clientId)
.appTid(Objects.nonNull(appTid) ? appTid.toString() : null)
.id(Objects.nonNull(userId) ? userId.toString() : null)
.scimId(Objects.nonNull(scimId) ? scimId.toString() : null)
.email(Objects.nonNull(email) ? email.toString() : null)
.groups(Objects.nonNull(groups) ? (List) groups : null)
.addPolicies((Collection) policies).build();
}
/**
* Sets the unique principal id.
*
* @param id
* if principal is user, the user id
* @return the builder itself
*/
public PrincipalBuilder id(String id) {
if (id != null && !id.equals(authorizationParty)) { // make sure it's NOT a credential type token
principal.setId(id);
}
return this;
}
PrincipalBuilder scimId(String scimId) {
principal.setScimId(scimId);
return this;
}
/**
* @deprecated use {@link #appTid(String)} instead
* Will be removed with version 0.16.0
*/
@Deprecated
public PrincipalBuilder zoneId(String zoneId) {
principal.setAppTid(zoneId);
return this;
}
/**
* Sets the unique tenant id
*
* @param appTid
* the unique tenant identifier
* @return the builder itself
*/
public PrincipalBuilder appTid(String appTid) {
principal.setAppTid(appTid);
return this;
}
/**
* Sets the authorization party to make sure that only in case of OIDC token the
* user id is set.
*
* @param authorizationParty
* the authorization party
* @return the builder itself
*/
PrincipalBuilder clientId(String authorizationParty) {
this.authorizationParty = authorizationParty;
if (authorizationParty != null && authorizationParty.equals(principal.getId())) {
principal.setId(null); // in this case it's a credential type token
}
return this;
}
/**
* Sets the Email of principal.
*
* @param email
* the email
* @return the builder itself
*/
public PrincipalBuilder email(String email) {
principal.setEmail(email);
return this;
}
/**
* Sets the groups of principal.
*
* @param groups
* the groups
* @return the builder itself
*/
public PrincipalBuilder groups(List groups) {
principal.setGroups(groups);
return this;
}
/**
* Adds a single Authorization Management policy.
*
* @param policy,
* the policy to be added
* @return the builder itself
*/
public PrincipalBuilder addPolicy(String policy) {
if (Objects.nonNull(policy)) {
principal.addPolicy(policy);
}
return this;
}
/**
* Adds a list of Authorization Management policies.
*
* @param policies,
* the list of policies
* @return the builder itself
*/
public PrincipalBuilder addPolicies(Collection policies) {
if (Objects.nonNull(policies)) {
principal.addPolicies(policies);
}
return this;
}
/**
* Builds and returns the principal object.
*
* @return the build principal or null, if the data is insufficient.
*/
public Principal build() {
if (Objects.nonNull(principal.getAppTid()) || principal.hasPolicies()) {
LOGGER.debug("Created {}.", principal);
principal.setAttributes(createPredefinedAttributes());
for (AttributesProcessor processor : ATTRIBUTES_PROCESSORS) {
processor.processAttributes(principal);
LOGGER.debug("Overwrite attributes with customers: {}.", principal.getAttributes());
}
return principal;
}
LOGGER.warn("Principal creation for Id '{}' (Tenant '{}') failed.", principal.getId(),
principal.getAppTid());
LOGGER.debug("Principal has test policies ? {}.", principal.hasPolicies());
return null;
}
private Attributes createPredefinedAttributes() {
HashMap userMap = new HashMap<>();
Attributes attributes = Attributes.create();
if (principal.hasPolicies()) {
LOGGER.debug("Attributes.setPolicies({})", principal.getPolicies());
attributes.setPolicies(principal.getPolicies());
} else if (principal.getScimId() != null || principal.getId() != null) {
LOGGER.debug("Attributes.setPrincipalToPolicy({})", principal.getToPolicyMapping());
attributes.setPrincipalToPolicies(principal.getToPolicyMapping());
}
if (principal.hasAppTid()) {
attributes.setTenant(principal.getAppTid());
}
if (principal.getEmail() != null) {
userMap.put(CLAIM_EMAIL, principal.getEmail());
}
if (principal.getGroups() != null && !principal.getGroups().isEmpty()) {
userMap.put(CLAIM_GROUPS, principal.getGroups());
}
if (principal.getId() != null) {
userMap.put(CLAIM_USER_UUID_KEY, principal.getId());
}
if (!userMap.isEmpty()) {
attributes.env().value("$user", userMap).attributes();
}
return attributes;
}
}