All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.cds.feature.identity.IdentityUserInfoProvider Maven / Gradle / Ivy

The newest version!
/**************************************************************************
 * (C) 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
 **************************************************************************/
package com.sap.cds.feature.identity;

import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.sap.cds.services.ErrorStatuses;
import com.sap.cds.services.ServiceException;
import com.sap.cds.services.authentication.AuthenticationInfo;
import com.sap.cds.services.authentication.JwtTokenAuthenticationInfo;
import com.sap.cds.services.request.UserInfo;
import com.sap.cds.services.runtime.CdsRuntime;
import com.sap.cds.services.runtime.UserInfoProvider;
import com.sap.cds.services.utils.ClassMethods;
import com.sap.cds.services.utils.ErrorStatusException;
import com.sap.cloud.environment.servicebinding.api.ServiceBinding;
import com.sap.cloud.security.json.JsonParsingException;
import com.sap.cloud.security.token.GrantType;
import com.sap.cloud.security.token.SecurityContext;
import com.sap.cloud.security.token.Token;
import com.sap.cloud.security.token.TokenClaims;
import com.sap.cloud.security.token.XsuaaToken;

public class IdentityUserInfoProvider implements UserInfoProvider {

	private final static Logger logger = LoggerFactory.getLogger(IdentityUserInfoProvider.class);

	private final CdsRuntime runtime;
	private final ServiceBinding iasBinding;
	private final ServiceBinding xsuaaBinding;

	public IdentityUserInfoProvider(CdsRuntime runtime, Optional iasBinding, Optional xsuaaBinding) {
		this.runtime = runtime;
		this.xsuaaBinding = xsuaaBinding.orElse(null);
		this.iasBinding = iasBinding.orElse(null);
	}

	private static final String SYSTEM_USER_NAME = "system";

	private static final String SYSTEM_INTERNAL_USER_NAME = "system-internal";

	@Override
	public UserInfo get() {
		AuthenticationInfo authenticationInfo = runtime.getProvidedAuthenticationInfo();
		if(authenticationInfo != null && authenticationInfo.is(JwtTokenAuthenticationInfo.class)) {
			try {
				JwtTokenAuthenticationInfo accessToken = authenticationInfo.as(JwtTokenAuthenticationInfo.class);
				Token token = Token.create(accessToken.getToken());
				logger.debug("Creating UserInfo based on token {}", token);

				switch(token.getService()) {
				case IAS:
					UserInfo iasUser = new IasUserInfoImpl(token);
					logger.debug("Resolved {}", iasUser);
					return iasUser;
				case XSUAA:
					UserInfo xsuaaUser = new XsuaaUserInfoImpl((XsuaaToken)token);
					logger.debug("Resolved {}", xsuaaUser);
					return xsuaaUser;
				default:
					throw new ErrorStatusException(ErrorStatuses.UNAUTHORIZED); // unsupported auth type
				}

			} catch (ServiceException e) {
				throw e;
			} catch (Exception e) { // NOSONAR
				throw new ErrorStatusException(ErrorStatuses.UNAUTHORIZED, e); // if there is a authentication the user must be extracted
			}
		} else {
			return null;
		}
	}

	/**
	 * Similar implementation of {@link UserInfo} as in cds-feature-xsuaa.
	 * Adjustments in this class potentially need to be double maintained as long as cds-features-xsuaa
	 * has not been removed.
	 */
	private class XsuaaUserInfoImpl implements UserInfo {

		private final XsuaaToken token;

		private final String name;

		private final boolean isSystemUser;

		private final boolean isInternalUser;

		private final Set roles;

		private final Map> attributes;

		private final Map additionalAttributes;

		private static final String SPECIAL_ATTRIBUTE_LOGON_NAME = "logonName";

		private static final String SPECIAL_ATTRIBUTE_TENANT = "tenant";

		private static final String EXTENSION_ATTRIBUTES = "ext_attr";

		private static final String BINDING_CLIENT_ID = "clientid";

		private static final String SERVICEINSTANCEID_ATTRIBUTE = "serviceinstanceid";

		private static final String SPECIAL_ATTRIBUTE_SERVICEINSTANCEID = EXTENSION_ATTRIBUTES + "." + SERVICEINSTANCEID_ATTRIBUTE;

		private final Set KNOWN_CLAIMS = new HashSet<>(Arrays.asList(TokenClaims.USER_NAME,
				TokenClaims.XSUAA.ZONE_ID,
				"user_id",
				TokenClaims.XSUAA.SCOPES,
				TokenClaims.XSUAA.XS_USER_ATTRIBUTES,
				"xs.system.attributes",
				TokenClaims.XSUAA.GRANT_TYPE,
				"client_id"
				));

		@SuppressWarnings("deprecation")
		private final Set SYSTEM_USER_GRANTS = new HashSet<>(Arrays.asList(
				GrantType.CLIENT_CREDENTIALS, GrantType.CLIENT_X509));

		private XsuaaUserInfoImpl(XsuaaToken token) {
			this.token = token;

			this.isSystemUser = SYSTEM_USER_GRANTS.contains(token.getGrantType());

			// an internal user is a technical user from the same client
			this.isInternalUser = this.isSystemUser && token.getClientId() != null && token.getClientId().equals( xsuaaBinding.getCredentials().get(BINDING_CLIENT_ID) );

			if (this.isInternalUser) {
				// on CF system users have no name
				this.name = SYSTEM_INTERNAL_USER_NAME;

			} else if (this.isSystemUser) {
				this.name = SYSTEM_USER_NAME;

			} else {
				this.name = token.getClaimAsString(TokenClaims.USER_NAME);
			}

			// filter the scopes. "$XSAPPNAME." - prefix is always directly in front of the scope (if prefixed).
			// There might be additional prefixes on top, e.g. "$SERVICEINSTANCEID."
			String scopePrefix = (String) xsuaaBinding.getCredentials().get("xsappname") + ".";
			this.roles = token.getScopes().stream().map(scope -> {
				int pos = scope.indexOf(scopePrefix);
				if (pos >= 0) {
					return scope.substring(pos + scopePrefix.length());
				}
				return scope;
			}).collect(Collectors.toSet());

			// add some specific user attributes ($user.tenant and $user.ext_attr.serviceinstanceid)
			this.attributes = new TreeMap<>();
			@SuppressWarnings("unchecked")
			Map> userAttributes = ((Map>)token.getClaims().get(TokenClaims.XSUAA.XS_USER_ATTRIBUTES));
			if (userAttributes != null) {
				this.attributes.putAll(userAttributes);
			}

			this.attributes.put(SPECIAL_ATTRIBUTE_TENANT, Collections.singletonList(token.getZoneId()));

			String serviceInstanceId = token.getAttributeFromClaimAsString(TokenClaims.XSUAA.EXTERNAL_ATTRIBUTE, SERVICEINSTANCEID_ATTRIBUTE);
			if (serviceInstanceId != null) {
				this.attributes.put(SPECIAL_ATTRIBUTE_SERVICEINSTANCEID, Collections.singletonList(serviceInstanceId));
			}

			this.additionalAttributes = new HashMap<>();
			this.additionalAttributes.put("givenName", token.getClaimAsString(TokenClaims.GIVEN_NAME));
			this.additionalAttributes.put("familyName", token.getClaimAsString(TokenClaims.FAMILY_NAME));
			this.additionalAttributes.put("subDomain", token.getAttributeFromClaimAsString(TokenClaims.XSUAA.EXTERNAL_ATTRIBUTE, TokenClaims.XSUAA.EXTERNAL_ATTRIBUTE_ZDN));

			token.getClaims().entrySet().stream().filter(entry -> !KNOWN_CLAIMS.contains(entry.getKey())).forEach(entry ->
				this.additionalAttributes.put(entry.getKey(), entry.getValue())
			);

			// add logon name to additional attributes
			if (!this.isSystemUser) {
				this.additionalAttributes.put(SPECIAL_ATTRIBUTE_LOGON_NAME, token.getPrincipal().getName());
			}
		}

		@Override
		public String getId() {
			return token.getClaimAsString("user_id");
		}

		@Override
		public String getName() {
			return name;
		}

		@Override
		public String getTenant() {
			return token.getZoneId();
		}

		@Override
		public Set getRoles() {
			return roles; // NOSONAR
		}

		@Override
		public boolean isSystemUser() {
			return isSystemUser;
		}

		@Override
		public boolean isInternalUser() {
			return isInternalUser; // implies isSystemUser
		}

		@Override
		public boolean isAuthenticated() {
			// as there is an accepted token we can be sure that the user is authenticated
			return true;
		}

		@Override
		public boolean isPrivileged() {
			return false; // XSUAA user is never a privileged user!
		}

		@Override
		public Map> getAttributes() {
			return attributes; // NOSONAR
		}

		@Override
		public Map getAdditionalAttributes() {
			return additionalAttributes;
		}

		@Override
		public  T as(Class userInfoClazz) {
			return ClassMethods.as(userInfoClazz, UserInfo.class, this, this::getAdditionalAttributes);
		}

		@Override
		public String toString() {
			return MessageFormat.format("XsuaaUserInfo [id=''{0}'', name=''{1}'', roles=''{2}'', attributes=''{3}''",
					getId(), getName(), getRoles(), getAttributes());
		}

	}

	private class IasUserInfoImpl implements UserInfo {

		private final Token token;

		private final Map> attributes;
		private final boolean isSystemUser;
		private final boolean isInternalUser;
		private Set roles = Collections.emptySet();

		private static final String BINDING_CLIENT_ID = "clientid";
		private static final String SPECIAL_ATTRIBUTE_TENANT = "tenant";
		private static final String CLAIM_IAS_APIS = "ias_apis";

		// keep in sync with TokenClaims
		private static final Set KNOWN_CLAIMS = new HashSet<>(Arrays.asList(
				TokenClaims.IAS_ISSUER,
				TokenClaims.ISSUER,
				TokenClaims.EXPIRATION,
				TokenClaims.AUDIENCE,
				TokenClaims.NOT_BEFORE,
				TokenClaims.SUBJECT,
				TokenClaims.SAP_GLOBAL_USER_ID,
				"zone_uuid", // TokenClaims.SAP_GLOBAL_ZONE_ID
				"app_tid", // TokenClaims.SAP_GLOBAL_APP_TID
				TokenClaims.AUTHORIZATION_PARTY,
				TokenClaims.CNF,
				TokenClaims.CNF_X5T,
				TokenClaims.XSUAA.ISSUED_AT,
				"jti"));

		IasUserInfoImpl(Token token) {
			this.token = token;
			// map all unknown claims to attribute lists
			this.attributes = token.getClaims().entrySet().stream().filter(e -> !KNOWN_CLAIMS.contains(e.getKey()))
					.collect(Collectors.toMap(e -> e.getKey(), e -> {
					try {
						return token.getClaimAsStringList(e.getKey());
					} catch(JsonParsingException ex) {
						return Collections.singletonList(token.getClaimAsString(e.getKey()));
					}}));
			this.attributes.put(SPECIAL_ATTRIBUTE_TENANT, Collections.singletonList(getTenant()));

			// 'azp' will only be provided as dedicated claim if 'aud' is multi-value.
			// in some scenarios (e.g. App2App), 'azp' and 'aud' might be available as single-value claim. 'azp' needs to take precedence.
			List aud = token.getClaimAsStringList(TokenClaims.AUDIENCE);
			String azp = token.getClaimAsString(TokenClaims.AUTHORIZATION_PARTY);
			if ((azp == null) && (aud.size() == 1)) {
				azp = aud.get(0);
			}
			this.isSystemUser = azp.equals(token.getClaimAsString(TokenClaims.SUBJECT));
			this.isInternalUser = isSystemUser && azp.equals(iasBinding.getCredentials().get(BINDING_CLIENT_ID));

			if (runtime.getEnvironment().getCdsProperties().getSecurity().getIdentity().isExposePlansAsRoles()) {
				// granted access to 'provided-apis' from App2App and 'service plans' from App2Service are mapped to roles
				this.roles = new HashSet<>();
				if (token.hasClaim(CLAIM_IAS_APIS)) {
					this.roles.addAll(token.getClaimAsStringList(CLAIM_IAS_APIS).stream().map(String::trim).collect(Collectors.toSet()));
				}
				List servicePlans = SecurityContext.getServicePlans();
				if (servicePlans != null) {
					this.roles.addAll(servicePlans.stream().map(String::trim).collect(Collectors.toSet()));
				}
			}
		}

		@Override
		public boolean isSystemUser() {
			return isSystemUser;
		}

		@Override
		public boolean isInternalUser() {
			return isInternalUser;
		}

		@Override
		public String getId() {
			return token.getClaimAsString(TokenClaims.SAP_GLOBAL_USER_ID);
		}

		@Override
		@SuppressWarnings("deprecation")
		public String getTenant() {
			return token.getZoneId();
		}

		@Override
		public Set getRoles() { return roles; }

		@Override
		public String getName() {
			if(isInternalUser) {
				return SYSTEM_INTERNAL_USER_NAME;
			}
			else if (isSystemUser) {
				return SYSTEM_USER_NAME;
			} else {
				// "sub" is best match for the logon name.
				// IAS migration tools will make sure it matches the email by default.
				return token.getClaimAsString(TokenClaims.SUBJECT);
			}
		}

		@Override
		public boolean isAuthenticated() {
			// as there is an accepted token we can be sure that the user is authenticated
			return true;
		}

		@Override
		public boolean isPrivileged() {
			return false; // IAS user is never a privileged user!
		}

		@Override
		public Map> getAttributes() {
			return attributes;
		}

		@Override
		public Map getAdditionalAttributes() {
			return token.getClaims();
		}

		@Override
		public String toString() {
			return MessageFormat.format("IasUserInfo [id=''{0}'', name=''{1}'', roles=''{2}'', attributes=''{3}''",
					getId(), getName(), getRoles(), getAttributes());
		}
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy