ContextBean added

This commit is contained in:
Lucio Lelii 2022-06-10 17:12:04 +02:00
parent 19a50ab9f9
commit ef54fb4c6b
12 changed files with 205 additions and 537 deletions

View File

@ -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

View File

@ -30,9 +30,5 @@
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.gcube.common</groupId>
<artifactId>keycloak-client</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -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 <V> Callable<V> bind(final Callable<V> task) {
final Secret secret = SecretManagerProvider.instance.get();
return new Callable<V>() {
@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 <V> 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;
}
}
}
}

View File

@ -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<Type> {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 <code>true</code> if the scope has a given {@link Type}.
* @param type the type
* @return <code>true</code> if the scope has the given type, <code>false</code> 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 <code>null</code> 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;
}
}

View File

@ -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<String> MINIMAL_ROLES = Arrays.asList("Member");
@JsonProperty("aud")
private String context;
@JsonProperty("resource_access")
private Map<String, Roles> 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<String> 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<String> roles = new ArrayList<>();
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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<String, String> getHTTPAuthorizationHeaders() {
Map<String, String> 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);
}
}
}

View File

@ -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<String, String> getHTTPAuthorizationHeaders() {
Map<String, String> 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;
}
}

View File

@ -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<String, String> getHTTPAuthorizationHeaders() {
Map<String, String> 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;
}
}

View File

@ -14,7 +14,7 @@ public abstract class Secret {
public abstract String getContext();
public abstract Map<String,String> getHTTPAuthorizationHeaders();
public abstract boolean isExpired();
public abstract boolean isRefreshable();

View File

@ -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);
}
}
}