From 7ac768281814a660aa08b09aea1e62ffb7f4e497 Mon Sep 17 00:00:00 2001 From: Luca Frosini Date: Thu, 24 Mar 2022 17:58:33 +0100 Subject: [PATCH] Backporting fixes --- CHANGELOG.md | 7 +- pom.xml | 2 +- .../utils/clientid/RenewalProvider.java | 3 + .../utils/secret/GCubeSecret.java | 1 + .../authorization/utils/secret/JWTSecret.java | 70 ++++++++++----- .../utils/secret/SecretUtility.java | 3 + .../authorization/utils/user/GCubeUser.java | 89 ++++++++++++++++--- .../utils/user/KeycloakUser.java | 69 +++++++++++++- .../common/authorization/utils/user/User.java | 18 ++++ 9 files changed, 224 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2551052..5227b7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,12 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm # Changelog for Authorization utils + +## [v1.0.1-SNAPSHOT] + +- Added support for clientID [#21903] + + ## [v1.0.0] - First Release - diff --git a/pom.xml b/pom.xml index 0997d15..ad3229d 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ org.gcube.common authorization-utils - 1.0.0 + 1.0.1-SNAPSHOT UTF-8 diff --git a/src/main/java/org/gcube/common/authorization/utils/clientid/RenewalProvider.java b/src/main/java/org/gcube/common/authorization/utils/clientid/RenewalProvider.java index 8805ef8..003b29f 100644 --- a/src/main/java/org/gcube/common/authorization/utils/clientid/RenewalProvider.java +++ b/src/main/java/org/gcube/common/authorization/utils/clientid/RenewalProvider.java @@ -2,6 +2,9 @@ package org.gcube.common.authorization.utils.clientid; import org.gcube.common.authorization.utils.secret.Secret; +/** + * @author Luca Frosini (ISTI - CNR) + */ public interface RenewalProvider { public Secret renew() throws Exception; diff --git a/src/main/java/org/gcube/common/authorization/utils/secret/GCubeSecret.java b/src/main/java/org/gcube/common/authorization/utils/secret/GCubeSecret.java index 5853a89..4b145c9 100644 --- a/src/main/java/org/gcube/common/authorization/utils/secret/GCubeSecret.java +++ b/src/main/java/org/gcube/common/authorization/utils/secret/GCubeSecret.java @@ -105,6 +105,7 @@ public class GCubeSecret extends Secret { GCubeUser gCubeUser = new GCubeUser(); gCubeUser.setRoles(new HashSet<>(clientInfo.getRoles())); gCubeUser.setUsername(clientInfo.getId()); + gCubeUser.setApplication(true); user = gCubeUser; break; } diff --git a/src/main/java/org/gcube/common/authorization/utils/secret/JWTSecret.java b/src/main/java/org/gcube/common/authorization/utils/secret/JWTSecret.java index 2dfe86d..d86ed61 100644 --- a/src/main/java/org/gcube/common/authorization/utils/secret/JWTSecret.java +++ b/src/main/java/org/gcube/common/authorization/utils/secret/JWTSecret.java @@ -1,12 +1,15 @@ package org.gcube.common.authorization.utils.secret; import java.net.URLDecoder; +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Base64; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; import org.gcube.com.fasterxml.jackson.databind.ObjectMapper; @@ -19,6 +22,7 @@ import org.gcube.common.authorization.utils.user.KeycloakUser; import org.gcube.common.authorization.utils.user.User; import org.gcube.common.keycloak.KeycloakClientFactory; import org.gcube.common.keycloak.model.AccessToken; +import org.gcube.common.keycloak.model.AccessToken.Access; import org.gcube.common.keycloak.model.ModelUtils; import org.gcube.common.keycloak.model.RefreshToken; import org.gcube.common.keycloak.model.TokenResponse; @@ -44,6 +48,10 @@ public class JWTSecret extends Secret { protected AccessToken accessToken; protected TokenResponse tokenResponse; protected RenewalProvider renewalProvider; + protected Set roles; + protected ClientInfo clientInfo; + protected Caller caller; + protected String context; public JWTSecret(String token) { super(10, token); @@ -91,7 +99,7 @@ public class JWTSecret extends Secret { AccessTokenProvider.instance.reset(); } - protected AccessToken getAccessToken() throws Exception { + protected AccessToken getAccessToken() { if(accessToken==null) { String realUmaTokenEncoded = token.split("\\.")[1]; String realUmaToken = new String(Base64.getDecoder().decode(realUmaTokenEncoded.getBytes())); @@ -100,47 +108,68 @@ public class JWTSecret extends Secret { accessToken = mapper.readValue(realUmaToken, AccessToken.class); }catch(Exception e){ logger.error("Error parsing JWT token",e); - throw new Exception("Error parsing JWT token", e); + throw new RuntimeException("Error parsing JWT token", e); } } return accessToken; } + + protected Set getRoles() throws Exception{ + if(roles == null) { + Map accesses = getAccessToken().getResourceAccess(); + String context = getContext(); + Access access = accesses.get(URLEncoder.encode(context, StandardCharsets.UTF_8.toString())); + if(access != null) { + roles = access.getRoles(); + }else { + roles = new HashSet<>(); + } + } + return roles; + } @Override public ClientInfo getClientInfo() throws Exception { - getAccessToken(); - List roles = new ArrayList<>(accessToken.getRealmAccess().getRoles()); - ClientInfo clientInfo = new UserInfo(accessToken.getPreferredUsername(), roles, accessToken.getEmail(), accessToken.getGivenName(), accessToken.getFamilyName()); + if(clientInfo==null) { + getAccessToken(); + List roles = new ArrayList<>(getRoles()); + clientInfo = new UserInfo(accessToken.getPreferredUsername(), roles, accessToken.getEmail(), accessToken.getGivenName(), accessToken.getFamilyName()); + } return clientInfo; } @Override public Caller getCaller() throws Exception { - Caller caller = new Caller(getClientInfo(), "token"); + if(caller==null) { + caller = new Caller(getClientInfo(), "token"); + } return caller; } @Override public String getContext() throws Exception { - String context = null; - String[] audience = getAccessToken().getAudience(); - for (String aud : audience) { - if (aud != null && aud.compareTo("") != 0) { - try { - context = URLDecoder.decode(context, StandardCharsets.UTF_8.toString()); - ScopeBean scopeBean = new ScopeBean(context); - return scopeBean.toString(); - } catch (Exception e) { - logger.error("Invalid context name for audience {} in access token. Trying next one if any.", aud, e); + if(context==null) { + String[] audience = getAccessToken().getAudience(); + for (String aud : audience) { + if (aud != null && aud.compareTo("") != 0) { + try { + String contextToBeValidated = URLDecoder.decode(aud, StandardCharsets.UTF_8.toString()); + ScopeBean scopeBean = new ScopeBean(contextToBeValidated); + context = scopeBean.toString(); + return context; + } catch (Exception e) { + logger.error("Invalid context name for audience {} in access token. Trying next one if any.", aud, e); + } } } + throw new Exception("Invalid context in access token"); } - throw new Exception("Invalid context in access token"); + return context; } @Override public String getUsername() throws Exception { - return accessToken.getPreferredUsername(); + return getAccessToken().getPreferredUsername(); } @Override @@ -164,7 +193,7 @@ public class JWTSecret extends Secret { @Override public boolean isExpired() { - return isExpired(accessToken); + return isExpired(getAccessToken()); } @Override @@ -185,8 +214,9 @@ public class JWTSecret extends Secret { if(user==null) { try { ObjectMapper objectMapper = new ObjectMapper(); - String accessTokenString = objectMapper.writeValueAsString(accessToken); + String accessTokenString = objectMapper.writeValueAsString(getAccessToken()); user = objectMapper.readValue(accessTokenString, KeycloakUser.class); + user.setRoles(getRoles()); } catch (Exception e) { throw new RuntimeException(); } diff --git a/src/main/java/org/gcube/common/authorization/utils/secret/SecretUtility.java b/src/main/java/org/gcube/common/authorization/utils/secret/SecretUtility.java index 4b9f73e..3f9672d 100644 --- a/src/main/java/org/gcube/common/authorization/utils/secret/SecretUtility.java +++ b/src/main/java/org/gcube/common/authorization/utils/secret/SecretUtility.java @@ -2,6 +2,9 @@ package org.gcube.common.authorization.utils.secret; import java.util.regex.Pattern; +/** + * @author Luca Frosini (ISTI - CNR) + */ public class SecretUtility { public static final String UUID_REGEX = "^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}-[a-fA-F0-9]{8,9}){1}$"; diff --git a/src/main/java/org/gcube/common/authorization/utils/user/GCubeUser.java b/src/main/java/org/gcube/common/authorization/utils/user/GCubeUser.java index e5f9fa8..1339fe8 100644 --- a/src/main/java/org/gcube/common/authorization/utils/user/GCubeUser.java +++ b/src/main/java/org/gcube/common/authorization/utils/user/GCubeUser.java @@ -2,6 +2,7 @@ package org.gcube.common.authorization.utils.user; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -21,18 +22,6 @@ public class GCubeUser implements User { @JsonIgnore protected Map additionalProperties; - public GCubeUser() { - this.additionalProperties = new HashMap<>(); - - // This info are not always present. Setting an empty string to avoid null - this.givenName = ""; - this.familyName = ""; - this.eMail = ""; - this.jobTitle = ""; - this.picture = ""; - this.middleName = ""; - } - @JsonProperty("id") protected String username; @JsonProperty("roles") @@ -52,6 +41,23 @@ public class GCubeUser implements User { @JsonProperty("middle_name") protected String middleName; + @JsonIgnore + protected boolean application; + + public GCubeUser() { + this.additionalProperties = new HashMap<>(); + + // This info are not always present. Setting an empty string to avoid null + this.givenName = ""; + this.familyName = ""; + this.eMail = ""; + this.jobTitle = ""; + this.picture = ""; + this.middleName = ""; + this.application = false; + } + + @Override public String getUsername() { return username; @@ -60,14 +66,24 @@ public class GCubeUser implements User { public void setUsername(String username) { this.username = username; } + + @Override + public boolean isApplication() { + return application; + } + public void setApplication(boolean application) { + this.application = application; + } + @Override public Collection getRoles() { return roles; } - public void setRoles(Set roles) { - this.roles = roles; + @Override + public void setRoles(Collection roles) { + this.roles = new HashSet<>(roles); } @Override public String getGivenName() { @@ -115,4 +131,49 @@ public class GCubeUser implements User { this.additionalProperties.put(key, value); } + @Override + public String getFullName() { + return getFullName(false); + } + + @Override + public String getFullName(boolean nameSurname) { + if(isApplication()) { + return getUsername(); + } + + StringBuffer stringBuffer = new StringBuffer(); + boolean found = false; + String surname = getFamilyName(); + String name = getGivenName(); + + if(nameSurname) { + if(name!=null && name.trim().length()>0) { + stringBuffer.append(name.trim()); + found = true; + } + if(surname!=null && surname.trim().length()>0) { + if(found) { + stringBuffer.append(" "); + } + stringBuffer.append(surname.trim()); + found = true; + } + }else { + if(surname!=null && surname.trim().length()>0) { + stringBuffer.append(surname.trim()); + found = true; + } + if(name!=null && name.trim().length()>0) { + if(found) { + stringBuffer.append(" "); + } + stringBuffer.append(name.trim()); + found = true; + } + } + + return stringBuffer.toString(); + } + } diff --git a/src/main/java/org/gcube/common/authorization/utils/user/KeycloakUser.java b/src/main/java/org/gcube/common/authorization/utils/user/KeycloakUser.java index ba8e6fc..e604cc5 100644 --- a/src/main/java/org/gcube/common/authorization/utils/user/KeycloakUser.java +++ b/src/main/java/org/gcube/common/authorization/utils/user/KeycloakUser.java @@ -15,21 +15,86 @@ public class KeycloakUser extends AccessToken implements User { */ private static final long serialVersionUID = -7083648026885406300L; + public static final String CLIENT_ID_PROPERTY = "clientId"; + + protected Collection roles; + protected Boolean application; + @Override @JsonIgnore public String getUsername() { - return getId(); + return getPreferredUsername(); } + @Override + public boolean isApplication() { + if(application==null) { + application = getOtherClaims().get(CLIENT_ID_PROPERTY)!=null; + } + return application; + } + @Override @JsonIgnore public Collection getRoles() { - return getRealmAccess().getRoles(); + return roles; + } + + @Override + @JsonIgnore + public void setRoles(Collection roles) { + this.roles = roles; } @Override public String getAbout() { return ""; } + + @Override + public String getFullName() { + return getFullName(false); + } + + @Override + public String getFullName(boolean nameSurname) { + if(isApplication()) { + String clientID = (String) getOtherClaims().getOrDefault("clientId", getUsername()); + return clientID; + } + + StringBuffer stringBuffer = new StringBuffer(); + boolean found = false; + String surname = getFamilyName(); + String name = getGivenName(); + + if(nameSurname) { + if(name!=null && name.trim().length()>0) { + stringBuffer.append(name.trim()); + found = true; + } + if(surname!=null && surname.trim().length()>0) { + if(found) { + stringBuffer.append(" "); + } + stringBuffer.append(surname.trim()); + found = true; + } + }else { + if(surname!=null && surname.trim().length()>0) { + stringBuffer.append(surname.trim()); + found = true; + } + if(name!=null && name.trim().length()>0) { + if(found) { + stringBuffer.append(" "); + } + stringBuffer.append(name.trim()); + found = true; + } + } + + return stringBuffer.toString(); + } } diff --git a/src/main/java/org/gcube/common/authorization/utils/user/User.java b/src/main/java/org/gcube/common/authorization/utils/user/User.java index 1eb5af4..80b738f 100644 --- a/src/main/java/org/gcube/common/authorization/utils/user/User.java +++ b/src/main/java/org/gcube/common/authorization/utils/user/User.java @@ -9,12 +9,30 @@ public interface User { public String getUsername(); + public boolean isApplication(); + public Collection getRoles(); + public void setRoles(Collection roles); + public String getGivenName(); public String getFamilyName(); + /** + * @return the full name in the form 'Surname Name' for a person + * or the application identifier for an application; + */ + public String getFullName(); + + /** + * @param nameSurname when true the fullname will be formatted as 'Name Surname', + * when false the fullname will be formatted as 'Surname Name', + * @return the full name according to nameSurname boolean for a person + * or the application identifier for an application; + */ + public String getFullName(boolean nameSurname); + public String getEmail(); public String getAbout();