From ef54fb4c6bd94c9f4b035f97da26d33c854cfd42 Mon Sep 17 00:00:00 2001 From: lucio Date: Fri, 10 Jun 2022 17:12:04 +0200 Subject: [PATCH] ContextBean added --- README.md | 4 +- pom.xml | 4 - .../common/security/AuthorizedTasks.java | 78 +++++++-- .../gcube/common/security/ContextBean.java | 139 +++++++++++++++ .../gcube/common/security/GCubeJWTObject.java | 111 ------------ .../security/providers/ClientIDManager.java | 48 ----- .../security/providers/RenewalProvider.java | 11 -- .../security/secrets/AccessTokenSecret.java | 81 --------- .../common/security/secrets/GCubeSecret.java | 80 --------- .../common/security/secrets/JWTSecret.java | 164 ------------------ .../gcube/common/security/secrets/Secret.java | 2 +- .../security/secrets/SecretUtility.java | 20 --- 12 files changed, 205 insertions(+), 537 deletions(-) create mode 100644 src/main/java/org/gcube/common/security/ContextBean.java delete mode 100644 src/main/java/org/gcube/common/security/GCubeJWTObject.java delete mode 100644 src/main/java/org/gcube/common/security/providers/ClientIDManager.java delete mode 100644 src/main/java/org/gcube/common/security/providers/RenewalProvider.java delete mode 100644 src/main/java/org/gcube/common/security/secrets/AccessTokenSecret.java delete mode 100644 src/main/java/org/gcube/common/security/secrets/GCubeSecret.java delete mode 100644 src/main/java/org/gcube/common/security/secrets/JWTSecret.java delete mode 100644 src/main/java/org/gcube/common/security/secrets/SecretUtility.java diff --git a/README.md b/README.md index 292b036..b57cf41 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# GCube Security +# GCube secrets A core gCube library which empower authorization @@ -13,7 +13,7 @@ A core gCube library which empower authorization ## Change log -See [Releases](https://code-repo.d4science.org/gCubeSystem/gcube-security/releases). +See [Releases](https://code-repo.d4science.org/gCubeSystem/gcube-secrets/releases). ## Authors diff --git a/pom.xml b/pom.xml index 4e3917e..f92c8d8 100644 --- a/pom.xml +++ b/pom.xml @@ -30,9 +30,5 @@ org.slf4j slf4j-api - - org.gcube.common - keycloak-client - \ No newline at end of file diff --git a/src/main/java/org/gcube/common/security/AuthorizedTasks.java b/src/main/java/org/gcube/common/security/AuthorizedTasks.java index 7293f3e..83d2523 100644 --- a/src/main/java/org/gcube/common/security/AuthorizedTasks.java +++ b/src/main/java/org/gcube/common/security/AuthorizedTasks.java @@ -11,23 +11,23 @@ import org.slf4j.LoggerFactory; public class AuthorizedTasks { private static Logger logger= LoggerFactory.getLogger(AuthorizedTasks.class); - + /** * Binds a {@link Callable} task to the current scope and user. * @param task the task * @return an equivalent {@link Callable} task bound to the current scope and user */ static public Callable bind(final Callable task) { - - + + final Secret secret = SecretManagerProvider.instance.get(); - + return new Callable() { @Override public V call() throws Exception { - + SecretManagerProvider.instance.set(secret); - + try { logger.info("setting on authorized task context {} ", secret.getContext()); return task.call(); @@ -35,27 +35,27 @@ public class AuthorizedTasks { finally { SecretManagerProvider.instance.reset(); } - + } }; } - + /** * Binds a {@link Runnable} task to the current scope and user. * @param task the task * @return an equivalent {@link Runnable} task bound to the current scope and user */ static public Runnable bind(final Runnable task) { - - + + final Secret secret = SecretManagerProvider.instance.get(); - + return new Runnable() { @Override public void run() { - + SecretManagerProvider.instance.set(secret); - + try { logger.info("setting on authorized task context {} ", secret.getContext()); task.run(); @@ -63,9 +63,57 @@ public class AuthorizedTasks { finally { SecretManagerProvider.instance.reset(); } - + } }; } - + + /** + * Binds a {@link Runnable} task to the current scope and user. + * @param task the task + * @return an equivalent {@link Runnable} task bound to the current scope and user + */ + static public void executeSafely(final Runnable task, final Secret secret) throws Throwable { + + SafelyExecution se = new SafelyExecution(new Runnable() { + + @Override + public void run() { + SecretManagerProvider.instance.set(secret); + + try { + logger.info("setting on authorized task context {} ", secret.getContext()); + task.run(); + }finally { + SecretManagerProvider.instance.reset(); + } + + } + }); + + se.run(); + + if (se.e != null) throw se.e; + + } + + static private class SafelyExecution extends Thread{ + + protected Throwable e; + + public SafelyExecution(Runnable target) { + super(target); + } + + @Override + public void run() { + try { + super.run(); + }catch (Throwable t) { + e = t; + } + } + + } + } diff --git a/src/main/java/org/gcube/common/security/ContextBean.java b/src/main/java/org/gcube/common/security/ContextBean.java new file mode 100644 index 0000000..2cfe657 --- /dev/null +++ b/src/main/java/org/gcube/common/security/ContextBean.java @@ -0,0 +1,139 @@ +package org.gcube.common.security; + + + +/** + * A object model of a scope. + * + * @author Fabio Simeoni + * + */ +public class ContextBean { + + /** + * Scope separators used in linear syntax. + */ + protected static String separator = "/"; + + /** + * Scope types * + */ + public static enum Type implements Comparable {VRE,VO,INFRASTRUCTURE} + + /** + * The name of the scope. + */ + private String name; + + /** + * The type of the scope. + */ + private Type type; + + /** + * The enclosing scope, if any. + */ + private ContextBean enclosingScope; + + + /** + * Returns the name of the scope. + * @return the name + */ + public String name() { + return name; + } + + /** + * Returns true if the scope has a given {@link Type}. + * @param type the type + * @return true if the scope has the given type, false otherwise + */ + public boolean is(Type type) { + return this.type.equals(type); + } + + /** + * Returns the {@link Type} of the scope. + * @return the type + */ + public Type type() { + return type; + } + + /** + * Returns the enclosing scope, if any. + * @return the enclosing scope, or null if the scope is top-level + */ + public ContextBean enclosingScope() { + return enclosingScope; + } + + public ContextBean(String scope) throws IllegalArgumentException { + + String[] components=scope.split(separator); + + if (components.length<2 || components.length>4) + throw new IllegalArgumentException("scope "+scope+" is malformed"); + + if(components.length>3) { + this.name=components[3]; + this.enclosingScope = new ContextBean(separator+components[1]+separator+components[2]); + this.type=Type.VRE; + } + else if (components.length>2) { + this.name=components[2]; + this.enclosingScope=new ContextBean(separator+components[1]); + this.type=Type.VO; + } + else { + this.name=components[1]; + this.type=Type.INFRASTRUCTURE; + } + + } + + /** + * Returns the linear expression of the scope. + */ + public String toString() { + return is(Type.INFRASTRUCTURE)?separator+name(): + enclosingScope().toString()+separator+name(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((enclosingScope == null) ? 0 : enclosingScope.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ContextBean other = (ContextBean) obj; + if (enclosingScope == null) { + if (other.enclosingScope != null) + return false; + } else if (!enclosingScope.equals(other.enclosingScope)) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (type != other.type) + return false; + return true; + } + + +} diff --git a/src/main/java/org/gcube/common/security/GCubeJWTObject.java b/src/main/java/org/gcube/common/security/GCubeJWTObject.java deleted file mode 100644 index 8f38386..0000000 --- a/src/main/java/org/gcube/common/security/GCubeJWTObject.java +++ /dev/null @@ -1,111 +0,0 @@ -package org.gcube.common.security; - - -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.gcube.com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import org.gcube.com.fasterxml.jackson.annotation.JsonProperty; - -@JsonIgnoreProperties(ignoreUnknown = true) -public class GCubeJWTObject { - - protected final static List MINIMAL_ROLES = Arrays.asList("Member"); - - @JsonProperty("aud") - private String context; - - @JsonProperty("resource_access") - private Map contextAccess = new HashMap<>(); - - @JsonProperty("preferred_username") - private String username; - - @JsonProperty("given_name") - private String firstName; - - @JsonProperty("family_name") - private String lastName; - - @JsonProperty("clientId") - private String clientId; - - //"name", as the client pretty name, e.g. Catalogue for gcat, Workspace for storage-hub - @JsonProperty("name") - private String clientName; - - //username of the user who requested such client - @JsonProperty("contact_person") - private String contactPerson; - - //the name of the organisation / community using such client. D4Science will be used for internal clients. - @JsonProperty("contact_organisation") - private String contactOrganisation; - - private static final String INTERNAL_CLIENT_ORGANISATION_NAME = "D4Science"; - - @JsonProperty("email") - private String email; - public List getRoles(){ - return contextAccess.get(this.context) == null ? MINIMAL_ROLES : contextAccess.get(this.context).roles; - } - - public String getContext() { - try { - return URLDecoder.decode(context, StandardCharsets.UTF_8.toString()); - }catch (UnsupportedEncodingException e) { - return context; - } - } - - public String getUsername() { - return username; - } - public boolean isExternalService() { - return contactOrganisation != null && contactOrganisation.equals(INTERNAL_CLIENT_ORGANISATION_NAME); - } - - public String getFirstName() { - return firstName; - } - public String getLastName() { - return lastName; - } - public String getEmail() { - return email; - } - @Override - public String toString() { - return "GcubeJwt [context=" + getContext() + ", roles=" + getRoles() + ", username=" + username - + ", firstName=" + firstName + ", lastName=" + lastName + ", email=" + email + "]"; - } - - - public String getClientName() { - return clientName; - } - - public String getContactPerson() { - return contactPerson; - } - - public String getContactOrganisation() { - return contactOrganisation; - } - - - - public static class Roles { - - @JsonProperty("roles") - List roles = new ArrayList<>(); - - } - -} - diff --git a/src/main/java/org/gcube/common/security/providers/ClientIDManager.java b/src/main/java/org/gcube/common/security/providers/ClientIDManager.java deleted file mode 100644 index 1210cbf..0000000 --- a/src/main/java/org/gcube/common/security/providers/ClientIDManager.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.gcube.common.security.providers; - -import org.gcube.common.keycloak.KeycloakClientFactory; -import org.gcube.common.keycloak.model.TokenResponse; -import org.gcube.common.security.secrets.JWTSecret; -import org.gcube.common.security.secrets.Secret; - -/** - * @author Luca Frosini (ISTI - CNR) - */ -public class ClientIDManager implements RenewalProvider { - - protected final String clientID; - protected final String clientSecret; - - public ClientIDManager(String clientID, String clientSecret) { - this.clientID = clientID; - this.clientSecret = clientSecret; - } - - public Secret getSecret() throws Exception { - TokenResponse tokenResponse = KeycloakClientFactory.newInstance().queryUMAToken(clientID, clientSecret, null); - - JWTSecret jwtSecret = new JWTSecret(tokenResponse.getAccessToken()); - jwtSecret.setRenewalProvider(this); - - jwtSecret.setTokenResponse(tokenResponse); - - return jwtSecret; - } - - public Secret getSecret(String context) throws Exception { - TokenResponse tokenResponse = KeycloakClientFactory.newInstance().queryUMAToken(clientID, clientSecret, context, null); - - JWTSecret jwtSecret = new JWTSecret(tokenResponse.getAccessToken()); - jwtSecret.setRenewalProvider(this); - - jwtSecret.setTokenResponse(tokenResponse); - - return jwtSecret; - } - - @Override - public Secret renew() throws Exception { - return getSecret(); - } - -} diff --git a/src/main/java/org/gcube/common/security/providers/RenewalProvider.java b/src/main/java/org/gcube/common/security/providers/RenewalProvider.java deleted file mode 100644 index de5b760..0000000 --- a/src/main/java/org/gcube/common/security/providers/RenewalProvider.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.gcube.common.security.providers; - -import org.gcube.common.security.secrets.Secret; - -/** - * @author Luca Frosini (ISTI - CNR) - */ -public interface RenewalProvider { - - public Secret renew() throws Exception; -} diff --git a/src/main/java/org/gcube/common/security/secrets/AccessTokenSecret.java b/src/main/java/org/gcube/common/security/secrets/AccessTokenSecret.java deleted file mode 100644 index cc5deca..0000000 --- a/src/main/java/org/gcube/common/security/secrets/AccessTokenSecret.java +++ /dev/null @@ -1,81 +0,0 @@ -package org.gcube.common.security.secrets; - -import java.util.Base64; -import java.util.HashMap; -import java.util.Map; - -import org.gcube.com.fasterxml.jackson.databind.ObjectMapper; -import org.gcube.common.security.GCubeJWTObject; -import org.gcube.common.security.Owner; - -public class AccessTokenSecret extends Secret { - - private String encodedAccessToken; - - protected Owner owner; - protected String context; - - - private boolean initialised = false; - - public AccessTokenSecret(String encodedAccessToken) { - this.encodedAccessToken = encodedAccessToken; - } - - @Override - public Owner getOwner() { - init(); - return this.owner; - } - - @Override - public String getContext() { - init(); - return this.context; - } - - @Override - public Map getHTTPAuthorizationHeaders() { - Map authorizationHeaders = new HashMap<>(); - authorizationHeaders.put("Authorization", "Bearer " + this.encodedAccessToken.getBytes()); - return authorizationHeaders; - - } - - protected String getEncodedAccessToken() { - return encodedAccessToken; - } - - @Override - public boolean isExpired() { - return false; - } - - @Override - public boolean isRefreshable() { - return false; - } - - private synchronized void init() { - if (!initialised) - try { - - String realAccessTokenEncoded = encodedAccessToken.split("\\.")[1]; - - String decodedAccessPart = new String(Base64.getDecoder().decode(realAccessTokenEncoded.getBytes())); - - ObjectMapper objectMapper = new ObjectMapper(); - GCubeJWTObject obj = objectMapper.readValue(decodedAccessPart, GCubeJWTObject.class); - owner = new Owner(obj.getUsername(), obj.getRoles(), obj.getEmail(), obj.getFirstName(), obj.getLastName(), obj.isExternalService()); - owner.setClientName(obj.getClientName()); - owner.setContactOrganisation(obj.getContactOrganisation()); - owner.setClientName(obj.getClientName()); - context = obj.getContext(); - initialised = true; - } catch (Exception e) { - throw new RuntimeException(e); - } - - } - -} diff --git a/src/main/java/org/gcube/common/security/secrets/GCubeSecret.java b/src/main/java/org/gcube/common/security/secrets/GCubeSecret.java deleted file mode 100644 index ca5e68c..0000000 --- a/src/main/java/org/gcube/common/security/secrets/GCubeSecret.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.gcube.common.security.secrets; - -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.regex.Pattern; - -import org.gcube.common.authorization.client.Constants; -import org.gcube.common.authorization.library.AuthorizationEntry; -import org.gcube.common.authorization.library.ClientType; -import org.gcube.common.security.Owner; - -/** - * @author Luca Frosini (ISTI - CNR) - */ -public class GCubeSecret extends Secret { - - public static final String GCUBE_TOKEN_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}$"; - - private String gcubeToken; - private Owner owner; - private String context; - - public GCubeSecret(String gcubeToken) { - Objects.requireNonNull(gcubeToken); - if(!Pattern.matches(GCubeSecret.GCUBE_TOKEN_REGEX, gcubeToken)) - throw new RuntimeException("The GUCBE token must comply with the regex " + GCUBE_TOKEN_REGEX); - this.gcubeToken = gcubeToken; - } - - private void init() throws Exception{ - AuthorizationEntry authorizationEntry = Constants.authorizationService().get(gcubeToken); - this.owner = new Owner(authorizationEntry.getClientInfo().getId(), - authorizationEntry.getClientInfo().getRoles(), authorizationEntry.getClientInfo().getType()!=ClientType.USER); - this.context = authorizationEntry.getContext(); - } - - - @Override - public Owner getOwner() { - if (Objects.isNull(owner)) - try { - init(); - } catch (Exception e) { - throw new RuntimeException("error retrieving context",e); - } - - return owner; - } - - @Override - public String getContext() { - if (Objects.isNull(context)) - try { - init(); - } catch (Exception e) { - throw new RuntimeException("error retrieving context",e); - } - - return context; - } - - @Override - public Map getHTTPAuthorizationHeaders() { - Map authorizationHeaders = new HashMap<>(); - authorizationHeaders.put(org.gcube.common.authorization.client.Constants.TOKEN_HEADER_ENTRY, gcubeToken); - return authorizationHeaders; - } - - @Override - public boolean isExpired() { - return false; - } - - @Override - public boolean isRefreshable() { - return false; - } - -} diff --git a/src/main/java/org/gcube/common/security/secrets/JWTSecret.java b/src/main/java/org/gcube/common/security/secrets/JWTSecret.java deleted file mode 100644 index d093af4..0000000 --- a/src/main/java/org/gcube/common/security/secrets/JWTSecret.java +++ /dev/null @@ -1,164 +0,0 @@ -package org.gcube.common.security.secrets; - -import java.util.Base64; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import org.gcube.com.fasterxml.jackson.databind.ObjectMapper; -import org.gcube.common.keycloak.KeycloakClientFactory; -import org.gcube.common.keycloak.model.AccessToken; -import org.gcube.common.keycloak.model.ModelUtils; -import org.gcube.common.keycloak.model.RefreshToken; -import org.gcube.common.keycloak.model.TokenResponse; -import org.gcube.common.keycloak.model.util.Time; -import org.gcube.common.security.GCubeJWTObject; -import org.gcube.common.security.Owner; -import org.gcube.common.security.providers.RenewalProvider; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * @author Luca Frosini (ISTI - CNR) - */ -public class JWTSecret extends Secret { - - private static final Logger logger = LoggerFactory.getLogger(JWTSecret.class); - - /** - * The interval of time expressed in milliseconds used as guard to refresh the token before that it expires . - * TimeUnit has been used to in place of just - * using the number to have a clearer code - */ - public static final long TOLERANCE = TimeUnit.MILLISECONDS.toMillis(200); - - protected String jwtToken; - - protected AccessToken accessToken; - protected TokenResponse tokenResponse; - protected RenewalProvider renewalProvider; - protected Owner owner; - protected String context; - - protected boolean initialised = false; - - public JWTSecret(String jwtToken) { - this.jwtToken = jwtToken; - } - - private String getTokenString() { - try { - boolean expired = false; - getAccessToken(); - - if(Time.currentTimeMillis()>=(accessToken.getExp()-TOLERANCE)) { - expired = true; - if(tokenResponse!=null) { - try { - KeycloakClientFactory.newInstance().refreshToken(this.getOwner().getId(), tokenResponse); - expired = false; - }catch (Exception e) { - logger.warn("Unable to refresh the token with RefreshToken. Going to try to renew it if possible.", e); - } - } - } - - if(expired && renewalProvider!=null) { - try { - JWTSecret renewed = (JWTSecret) renewalProvider.renew(); - this.jwtToken = renewed.jwtToken; - this.accessToken = getAccessToken(); - }catch (Exception e) { - logger.warn("Unable to renew the token with the RenewalProvider. I'll continue using the old token.", e); - } - } - }catch (Exception e) { - logger.error("Unexpected error in the procedure to evaluate/refresh the current token. I'll continue using the old token.", e); - } - return jwtToken; - } - - - protected AccessToken getAccessToken() { - if(accessToken==null) { - String realUmaTokenEncoded = jwtToken.split("\\.")[1]; - String realUmaToken = new String(Base64.getDecoder().decode(realUmaTokenEncoded.getBytes())); - ObjectMapper mapper = new ObjectMapper(); - try { - accessToken = mapper.readValue(realUmaToken, AccessToken.class); - }catch(Exception e){ - logger.error("Error parsing JWT token",e); - throw new RuntimeException("Error parsing JWT token", e); - } - } - return accessToken; - } - - private synchronized void init() { - if (!initialised) - try { - ObjectMapper objectMapper = new ObjectMapper(); - String accessTokenString = objectMapper.writeValueAsString(getAccessToken()); - GCubeJWTObject obj = objectMapper.readValue(accessTokenString, GCubeJWTObject.class); - Owner owner = new Owner(obj.getUsername(), obj.getRoles(), obj.getEmail(), obj.getFirstName(), obj.getLastName(), obj.isExternalService()); - owner.setClientName(obj.getClientName()); - owner.setContactOrganisation(obj.getContactOrganisation()); - owner.setClientName(obj.getClientName()); - context = obj.getContext(); - initialised = true; - } catch (Exception e) { - throw new RuntimeException(); - } - - } - - @Override - public Owner getOwner() { - init(); - return this.owner; - } - - @Override - public String getContext() { - init(); - return this.context; - } - - @Override - public Map getHTTPAuthorizationHeaders() { - Map authorizationHeaders = new HashMap<>(); - authorizationHeaders.put("Authorization", "Bearer " + getTokenString()); - return authorizationHeaders; - } - - public void setRenewalProvider(RenewalProvider renewalProvider) { - this.renewalProvider = renewalProvider; - } - - public void setTokenResponse(TokenResponse tokenResponse) { - this.tokenResponse = tokenResponse; - } - - protected boolean isExpired(AccessToken accessToken) { - return Time.currentTimeMillis()>accessToken.getExp(); - } - - @Override - public boolean isExpired() { - return isExpired(getAccessToken()); - } - - @Override - public boolean isRefreshable() { - if(tokenResponse!=null) { - try { - RefreshToken refreshToken = ModelUtils.getRefreshTokenFrom(tokenResponse); - return isExpired(refreshToken); - } catch (Exception e) { - return false; - } - } - return false; - } - -} \ No newline at end of file diff --git a/src/main/java/org/gcube/common/security/secrets/Secret.java b/src/main/java/org/gcube/common/security/secrets/Secret.java index 48c6668..7db3898 100644 --- a/src/main/java/org/gcube/common/security/secrets/Secret.java +++ b/src/main/java/org/gcube/common/security/secrets/Secret.java @@ -14,7 +14,7 @@ public abstract class Secret { public abstract String getContext(); public abstract Map getHTTPAuthorizationHeaders(); - + public abstract boolean isExpired(); public abstract boolean isRefreshable(); diff --git a/src/main/java/org/gcube/common/security/secrets/SecretUtility.java b/src/main/java/org/gcube/common/security/secrets/SecretUtility.java deleted file mode 100644 index e8cf4cf..0000000 --- a/src/main/java/org/gcube/common/security/secrets/SecretUtility.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.gcube.common.security.secrets; - -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}$"; - - public static Secret getSecretByTokenString(String token) { - if(Pattern.matches(UUID_REGEX, token)) { - return new GCubeSecret(token); - }else { - return new JWTSecret(token); - } - } - -}