diff --git a/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/OpenCgaCompleter.java b/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/OpenCgaCompleter.java index d6bd902fff1..d4ff662a7c3 100644 --- a/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/OpenCgaCompleter.java +++ b/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/OpenCgaCompleter.java @@ -89,7 +89,7 @@ public abstract class OpenCgaCompleter implements Completer { .map(Candidate::new) .collect(toList()); - private List organizationsList = asList( "create","notes-create","notes-search","notes-delete","notes-update","info","update") + private List organizationsList = asList( "create","notes-create","notes-search","notes-delete","notes-update","configuration-update","info","update") .stream() .map(Candidate::new) .collect(toList()); diff --git a/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/OpencgaCliOptionsParser.java b/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/OpencgaCliOptionsParser.java index 33d53de3bf8..e0b9f455343 100644 --- a/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/OpencgaCliOptionsParser.java +++ b/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/OpencgaCliOptionsParser.java @@ -272,6 +272,7 @@ public OpencgaCliOptionsParser() { organizationsSubCommands.addCommand("notes-search", organizationsCommandOptions.searchNotesCommandOptions); organizationsSubCommands.addCommand("notes-delete", organizationsCommandOptions.deleteNotesCommandOptions); organizationsSubCommands.addCommand("notes-update", organizationsCommandOptions.updateNotesCommandOptions); + organizationsSubCommands.addCommand("configuration-update", organizationsCommandOptions.updateConfigurationCommandOptions); organizationsSubCommands.addCommand("info", organizationsCommandOptions.infoCommandOptions); organizationsSubCommands.addCommand("update", organizationsCommandOptions.updateCommandOptions); diff --git a/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/executors/OrganizationsCommandExecutor.java b/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/executors/OrganizationsCommandExecutor.java index 991f44fd9d8..a74a27358a5 100644 --- a/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/executors/OrganizationsCommandExecutor.java +++ b/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/executors/OrganizationsCommandExecutor.java @@ -12,6 +12,7 @@ import org.opencb.opencga.app.cli.main.options.OrganizationsCommandOptions; import org.opencb.opencga.catalog.exceptions.CatalogAuthenticationException; import org.opencb.opencga.catalog.utils.ParamUtils.AddRemoveAction; +import org.opencb.opencga.catalog.utils.ParamUtils.UpdateAction; import org.opencb.opencga.client.exceptions.ClientException; import org.opencb.opencga.core.common.JacksonUtils; import org.opencb.opencga.core.config.Optimizations; @@ -75,6 +76,9 @@ public void execute() throws Exception { case "notes-update": queryResponse = updateNotes(); break; + case "configuration-update": + queryResponse = updateConfiguration(); + break; case "info": queryResponse = info(); break; @@ -223,6 +227,41 @@ private RestResponse updateNotes() throws Exception { return openCGAClient.getOrganizationClient().updateNotes(commandOptions.id, noteUpdateParams, queryParams); } + private RestResponse updateConfiguration() throws Exception { + logger.debug("Executing updateConfiguration in Organizations command line"); + + OrganizationsCommandOptions.UpdateConfigurationCommandOptions commandOptions = organizationsCommandOptions.updateConfigurationCommandOptions; + + ObjectMap queryParams = new ObjectMap(); + queryParams.putIfNotEmpty("include", commandOptions.include); + queryParams.putIfNotEmpty("exclude", commandOptions.exclude); + queryParams.putIfNotNull("includeResult", commandOptions.includeResult); + queryParams.putIfNotNull("authenticationOriginsAction", commandOptions.authenticationOriginsAction); + + + OrganizationConfiguration organizationConfiguration = null; + if (commandOptions.jsonDataModel) { + RestResponse res = new RestResponse<>(); + res.setType(QueryType.VOID); + PrintUtils.println(getObjectAsJSON(categoryName,"/{apiVersion}/organizations/{organization}/configuration/update")); + return res; + } else if (commandOptions.jsonFile != null) { + organizationConfiguration = JacksonUtils.getDefaultObjectMapper() + .readValue(new java.io.File(commandOptions.jsonFile), OrganizationConfiguration.class); + } else { + ObjectMap beanParams = new ObjectMap(); + putNestedIfNotNull(beanParams, "optimizations.simplifyPermissions",commandOptions.optimizationsSimplifyPermissions, true); + putNestedIfNotEmpty(beanParams, "token.algorithm",commandOptions.tokenAlgorithm, true); + putNestedIfNotEmpty(beanParams, "token.secretKey",commandOptions.tokenSecretKey, true); + putNestedIfNotNull(beanParams, "token.expiration",commandOptions.tokenExpiration, true); + + organizationConfiguration = JacksonUtils.getDefaultObjectMapper().copy() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true) + .readValue(beanParams.toJson(), OrganizationConfiguration.class); + } + return openCGAClient.getOrganizationClient().updateConfiguration(commandOptions.organization, organizationConfiguration, queryParams); + } + private RestResponse info() throws Exception { logger.debug("Executing info in Organizations command line"); diff --git a/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/options/OrganizationsCommandOptions.java b/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/options/OrganizationsCommandOptions.java index c2ab1e89a90..08120624c87 100644 --- a/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/options/OrganizationsCommandOptions.java +++ b/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/options/OrganizationsCommandOptions.java @@ -38,6 +38,7 @@ public class OrganizationsCommandOptions { public SearchNotesCommandOptions searchNotesCommandOptions; public DeleteNotesCommandOptions deleteNotesCommandOptions; public UpdateNotesCommandOptions updateNotesCommandOptions; + public UpdateConfigurationCommandOptions updateConfigurationCommandOptions; public InfoCommandOptions infoCommandOptions; public UpdateCommandOptions updateCommandOptions; @@ -51,6 +52,7 @@ public OrganizationsCommandOptions(CommonCommandOptions commonCommandOptions, JC this.searchNotesCommandOptions = new SearchNotesCommandOptions(); this.deleteNotesCommandOptions = new DeleteNotesCommandOptions(); this.updateNotesCommandOptions = new UpdateNotesCommandOptions(); + this.updateConfigurationCommandOptions = new UpdateConfigurationCommandOptions(); this.infoCommandOptions = new InfoCommandOptions(); this.updateCommandOptions = new UpdateCommandOptions(); @@ -216,6 +218,47 @@ public class UpdateNotesCommandOptions { } + @Parameters(commandNames = {"configuration-update"}, commandDescription ="Update the Organization configuration attributes") + public class UpdateConfigurationCommandOptions { + + @ParametersDelegate + public CommonCommandOptions commonOptions = commonCommandOptions; + + @Parameter(names = {"--json-file"}, description = "File with the body data in JSON format. Note, that using this parameter will ignore all the other parameters.", required = false, arity = 1) + public String jsonFile; + + @Parameter(names = {"--json-data-model"}, description = "Show example of file structure for body data.", help = true, arity = 0) + public Boolean jsonDataModel = false; + + @Parameter(names = {"--include", "-I"}, description = "Fields included in the response, whole JSON path must be provided", required = false, arity = 1) + public String include; + + @Parameter(names = {"--exclude", "-E"}, description = "Fields excluded in the response, whole JSON path must be provided", required = false, arity = 1) + public String exclude; + + @Parameter(names = {"--organization"}, description = "Organization id", required = true, arity = 1) + public String organization; + + @Parameter(names = {"--include-result"}, description = "Flag indicating to include the created or updated document result in the response", required = false, help = true, arity = 0) + public boolean includeResult = false; + + @Parameter(names = {"--authentication-origins-action"}, description = "Action to be performed if the array of authenticationOrigins is being updated.", required = false, arity = 1) + public String authenticationOriginsAction = "ADD"; + + @Parameter(names = {"--optimizations-simplify-permissions"}, description = "The body web service simplifyPermissions parameter", required = false, help = true, arity = 0) + public boolean optimizationsSimplifyPermissions = false; + + @Parameter(names = {"--token-algorithm"}, description = "The body web service algorithm parameter", required = false, arity = 1) + public String tokenAlgorithm; + + @Parameter(names = {"--token-secret-key"}, description = "The body web service secretKey parameter", required = false, arity = 1) + public String tokenSecretKey; + + @Parameter(names = {"--token-expiration"}, description = "The body web service expiration parameter", required = false, arity = 1) + public Long tokenExpiration; + + } + @Parameters(commandNames = {"info"}, commandDescription ="Return the organization information") public class InfoCommandOptions { diff --git a/opencga-app/src/main/java/org/opencb/opencga/app/migrations/v3/v3_1_0/NoteMigration.java b/opencga-app/src/main/java/org/opencb/opencga/app/migrations/v3/v3_1_0/NoteMigration.java index c5fb031f1da..c3e49a2d85e 100644 --- a/opencga-app/src/main/java/org/opencb/opencga/app/migrations/v3/v3_1_0/NoteMigration.java +++ b/opencga-app/src/main/java/org/opencb/opencga/app/migrations/v3/v3_1_0/NoteMigration.java @@ -6,34 +6,23 @@ import com.mongodb.client.model.RenameCollectionOptions; import com.mongodb.client.model.Updates; import com.mongodb.client.result.UpdateResult; -import org.apache.commons.lang3.StringUtils; import org.bson.Document; import org.bson.conversions.Bson; import org.opencb.commons.datastore.mongodb.MongoDataStore; import org.opencb.opencga.catalog.db.api.NoteDBAdaptor; -import org.opencb.opencga.catalog.db.mongodb.MongoDBAdaptorFactory; import org.opencb.opencga.catalog.db.mongodb.OrganizationMongoDBAdaptorFactory; -import org.opencb.opencga.catalog.exceptions.CatalogDBException; -import org.opencb.opencga.catalog.io.IOManagerFactory; -import org.opencb.opencga.catalog.managers.CatalogManager; import org.opencb.opencga.catalog.migration.Migration; import org.opencb.opencga.catalog.migration.MigrationTool; -import org.opencb.opencga.core.api.ParamConstants; import org.opencb.opencga.core.models.notes.Note; -import java.util.Collections; -import java.util.List; - @Migration(id = "migrate_notes", description = "Migrate notes #TASK-5836", version = "3.1.0", language = Migration.MigrationLanguage.JAVA, domain = Migration.MigrationDomain.CATALOG, date = 20240315) public class NoteMigration extends MigrationTool { @Override protected void run() throws Exception { - IOManagerFactory ioManagerFactory = new IOManagerFactory(); - dbAdaptorFactory = new MongoDBAdaptorFactory(configuration, ioManagerFactory); // First migrate to add the new values - MongoCollection collection = getMongoCollection(ParamConstants.ADMIN_ORGANIZATION, "notes"); + MongoCollection collection = getMongoCollection(organizationId, "notes"); Bson query = Filters.exists(NoteDBAdaptor.QueryParams.STUDY_UID.key(), false); Bson update = Updates.combine( Updates.set(NoteDBAdaptor.QueryParams.STUDY_UID.key(), -1L), @@ -43,41 +32,20 @@ protected void run() throws Exception { ); UpdateResult updateResult = collection.updateMany(query, update); if (updateResult.getModifiedCount() == 0) { - // Check there are at least 2 organizations present - logger.info("Note data model could not be updated. Detected organizations are: {}", StringUtils.join(dbAdaptorFactory.getOrganizationIds(), ",")); - if (dbAdaptorFactory.getOrganizationIds().size() == 2) { - logger.info("Nothing to migrate"); - return; - } else { - throw new CatalogDBException("Notes could not be found to migrate."); - } + logger.info("Note data model could not be updated. Notes found in organization '{}': {}", organizationId, updateResult.getMatchedCount()); } - renameNoteCollection(Collections.singletonList(ParamConstants.ADMIN_ORGANIZATION)); - - dbAdaptorFactory.close(); - dbAdaptorFactory = new MongoDBAdaptorFactory(configuration, ioManagerFactory); - // We run it a second time because the first time it will only rename the "opencga" org as OpenCGA will not be able to know - // which other organizations are present in the installation (trying to fetch the information from "note" instead of old "notes") - List organizationIds = dbAdaptorFactory.getOrganizationIds(); - organizationIds.remove(ParamConstants.ADMIN_ORGANIZATION); - renameNoteCollection(organizationIds); - - // Reload catalog manager to install missing indexes - catalogManager = new CatalogManager(configuration); - catalogManager.installIndexes(token); - } - private void renameNoteCollection(List organizationIds) throws CatalogDBException { + // Rename Note collection RenameCollectionOptions options = new RenameCollectionOptions().dropTarget(true); // Rename collection - for (String organizationId : organizationIds) { - String databaseName = dbAdaptorFactory.getMongoDataStore(organizationId).getDatabaseName(); - logger.info("Renaming notes collection for organization '{}' -> Database: '{}'", organizationId, databaseName); - MongoDataStore mongoDataStore = dbAdaptorFactory.getMongoDataStore(organizationId); - mongoDataStore.getDb().getCollection("notes").renameCollection(new MongoNamespace(databaseName, OrganizationMongoDBAdaptorFactory.NOTE_COLLECTION), options); - mongoDataStore.getDb().getCollection("notes_archive").renameCollection(new MongoNamespace(databaseName, OrganizationMongoDBAdaptorFactory.NOTE_ARCHIVE_COLLECTION), options); - mongoDataStore.getDb().getCollection("notes_deleted").renameCollection(new MongoNamespace(databaseName, OrganizationMongoDBAdaptorFactory.DELETED_NOTE_COLLECTION), options); - } + String databaseName = dbAdaptorFactory.getMongoDataStore(organizationId).getDatabaseName(); + logger.info("Renaming notes collection for organization '{}' -> Database: '{}'", organizationId, databaseName); + MongoDataStore mongoDataStore = dbAdaptorFactory.getMongoDataStore(organizationId); + mongoDataStore.getDb().getCollection("notes").renameCollection(new MongoNamespace(databaseName, OrganizationMongoDBAdaptorFactory.NOTE_COLLECTION), options); + mongoDataStore.getDb().getCollection("notes_archive").renameCollection(new MongoNamespace(databaseName, OrganizationMongoDBAdaptorFactory.NOTE_ARCHIVE_COLLECTION), options); + mongoDataStore.getDb().getCollection("notes_deleted").renameCollection(new MongoNamespace(databaseName, OrganizationMongoDBAdaptorFactory.DELETED_NOTE_COLLECTION), options); + + catalogManager.installIndexes(organizationId, token); } } diff --git a/opencga-catalog/src/main/java/org/opencb/opencga/catalog/auth/authentication/AuthenticationManager.java b/opencga-catalog/src/main/java/org/opencb/opencga/catalog/auth/authentication/AuthenticationManager.java index 345ac2977b7..d93cf48c4a7 100644 --- a/opencga-catalog/src/main/java/org/opencb/opencga/catalog/auth/authentication/AuthenticationManager.java +++ b/opencga-catalog/src/main/java/org/opencb/opencga/catalog/auth/authentication/AuthenticationManager.java @@ -28,6 +28,7 @@ import org.slf4j.LoggerFactory; import javax.crypto.spec.SecretKeySpec; +import java.io.Closeable; import java.security.Key; import java.util.Collections; import java.util.Date; @@ -37,7 +38,7 @@ /** * @author Jacobo Coll <jacobo167@gmail.com> */ -public abstract class AuthenticationManager { +public abstract class AuthenticationManager implements Closeable { protected JwtManager jwtManager; diff --git a/opencga-catalog/src/main/java/org/opencb/opencga/catalog/auth/authentication/AzureADAuthenticationManager.java b/opencga-catalog/src/main/java/org/opencb/opencga/catalog/auth/authentication/AzureADAuthenticationManager.java index b0f525af072..f104f8fdf93 100644 --- a/opencga-catalog/src/main/java/org/opencb/opencga/catalog/auth/authentication/AzureADAuthenticationManager.java +++ b/opencga-catalog/src/main/java/org/opencb/opencga/catalog/auth/authentication/AzureADAuthenticationManager.java @@ -146,6 +146,15 @@ public AzureADAuthenticationManager(AuthenticationOrigin authenticationOrigin) t Configurator.setLevel("com.microsoft.aad.adal4j.AuthenticationAuthority", Level.WARN); } + public static void validateAuthenticationOriginConfiguration(AuthenticationOrigin authenticationOrigin) throws CatalogException { + if (authenticationOrigin.getType() != AuthenticationOrigin.AuthenticationType.AzureAD) { + throw new CatalogException("Unknown authentication type. Expected type '" + AuthenticationOrigin.AuthenticationType.AzureAD + + "' but received '" + authenticationOrigin.getType() + "'."); + } + AzureADAuthenticationManager azureADAuthenticationManager = new AzureADAuthenticationManager(authenticationOrigin); + azureADAuthenticationManager.close(); + } + private OIDCProviderMetadata getProviderMetadata(String host) throws IOException, ParseException { URL providerConfigurationURL = new URL(host); InputStream stream = providerConfigurationURL.openStream(); @@ -420,4 +429,8 @@ public String createNonExpiringToken(String organizationId, String userId, Map claims, long expiration) { - return jwtManager.createJWTToken(organizationId, userId, claims, expiration); + return jwtManager.createJWTToken(organizationId, AuthenticationOrigin.AuthenticationType.OPENCGA, userId, claims, expiration); } @Override public String createNonExpiringToken(String organizationId, String userId, Map claims) { - return jwtManager.createJWTToken(organizationId, userId, claims, 0L); + return jwtManager.createJWTToken(organizationId, AuthenticationOrigin.AuthenticationType.OPENCGA, userId, claims, 0L); } @Override @@ -145,4 +156,8 @@ public static AuthenticationOrigin createOpencgaAuthenticationOrigin() { .setId(CatalogAuthenticationManager.OPENCGA) .setType(AuthenticationOrigin.AuthenticationType.OPENCGA); } + + @Override + public void close() { + } } diff --git a/opencga-catalog/src/main/java/org/opencb/opencga/catalog/auth/authentication/JwtManager.java b/opencga-catalog/src/main/java/org/opencb/opencga/catalog/auth/authentication/JwtManager.java index 8cbb28decca..fd0462e3a98 100644 --- a/opencga-catalog/src/main/java/org/opencb/opencga/catalog/auth/authentication/JwtManager.java +++ b/opencga-catalog/src/main/java/org/opencb/opencga/catalog/auth/authentication/JwtManager.java @@ -18,6 +18,7 @@ import io.jsonwebtoken.*; import org.opencb.opencga.catalog.exceptions.CatalogAuthenticationException; +import org.opencb.opencga.core.config.AuthenticationOrigin; import org.opencb.opencga.core.models.JwtPayload; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,6 +30,8 @@ import java.util.List; import java.util.Map; +import static org.opencb.opencga.core.models.JwtPayload.AUTH_ORIGIN; + public class JwtManager { private SignatureAlgorithm algorithm; @@ -84,13 +87,18 @@ public JwtManager setPublicKey(Key publicKey) { return this; } - public String createJWTToken(String organizationId, String userId, Map claims, long expiration) { + public String createJWTToken(String organizationId, AuthenticationOrigin.AuthenticationType type, String userId, + Map claims, long expiration) { long currentTime = System.currentTimeMillis(); JwtBuilder jwtBuilder = Jwts.builder(); if (claims != null && !claims.isEmpty()) { jwtBuilder.setClaims(claims); } + if (type != null) { + jwtBuilder.addClaims(Collections.singletonMap(AUTH_ORIGIN, type)); + } + jwtBuilder.setSubject(userId) .setAudience(organizationId) .setIssuer("OpenCGA") @@ -115,14 +123,24 @@ public void validateToken(String token, Key publicKey) throws CatalogAuthenticat public JwtPayload getPayload(String token) throws CatalogAuthenticationException { Claims body = parseClaims(token, publicKey).getBody(); - return new JwtPayload(body.getSubject(), body.getAudience(), body.getIssuer(), body.getIssuedAt(), body.getExpiration(), token); + return new JwtPayload(body.getSubject(), body.getAudience(), getAuthOrigin(body), body.getIssuer(), body.getIssuedAt(), + body.getExpiration(), token); } public JwtPayload getPayload(String token, Key publicKey) throws CatalogAuthenticationException { Claims body = parseClaims(token, publicKey).getBody(); - return new JwtPayload(body.getSubject(), body.getAudience(), body.getIssuer(), body.getIssuedAt(), body.getExpiration(), token); + return new JwtPayload(body.getSubject(), body.getAudience(), getAuthOrigin(body), body.getIssuer(), body.getIssuedAt(), + body.getExpiration(), token); } + private AuthenticationOrigin.AuthenticationType getAuthOrigin(Claims claims) { + String o = claims.get(AUTH_ORIGIN, String.class); + if (o != null) { + return AuthenticationOrigin.AuthenticationType.valueOf(o); + } else { + return null; + } + } public String getAudience(String token) throws CatalogAuthenticationException { return getAudience(token, this.publicKey); diff --git a/opencga-catalog/src/main/java/org/opencb/opencga/catalog/auth/authentication/LDAPAuthenticationManager.java b/opencga-catalog/src/main/java/org/opencb/opencga/catalog/auth/authentication/LDAPAuthenticationManager.java index 516a54702c2..35fe615d161 100644 --- a/opencga-catalog/src/main/java/org/opencb/opencga/catalog/auth/authentication/LDAPAuthenticationManager.java +++ b/opencga-catalog/src/main/java/org/opencb/opencga/catalog/auth/authentication/LDAPAuthenticationManager.java @@ -24,8 +24,10 @@ import org.opencb.commons.datastore.core.ObjectMap; import org.opencb.opencga.catalog.exceptions.CatalogAuthenticationException; import org.opencb.opencga.catalog.exceptions.CatalogException; +import org.opencb.opencga.catalog.utils.ParamUtils; import org.opencb.opencga.core.common.TimeUtils; import org.opencb.opencga.core.config.AuthenticationOrigin; +import org.opencb.opencga.core.models.organizations.TokenConfiguration; import org.opencb.opencga.core.models.user.*; import org.opencb.opencga.core.response.OpenCGAResult; import org.slf4j.LoggerFactory; @@ -124,6 +126,24 @@ protected static String envToStringRedacted(Hashtable env) { return string; } + public static void validateAuthenticationOriginConfiguration(AuthenticationOrigin authenticationOrigin) throws CatalogException { + if (authenticationOrigin.getType() != AuthenticationType.LDAP) { + throw new CatalogException("Unknown authentication type. Expected type '" + AuthenticationType.LDAP + "' but received '" + + authenticationOrigin.getType() + "'."); + } + ParamUtils.checkParameter(authenticationOrigin.getHost(), AuthenticationType.LDAP + " host."); + + TokenConfiguration defaultTokenConfig = TokenConfiguration.init(); + LDAPAuthenticationManager ldapAuthenticationManager = new LDAPAuthenticationManager(authenticationOrigin, + defaultTokenConfig.getAlgorithm(), defaultTokenConfig.getSecretKey(), defaultTokenConfig.getExpiration()); + DirContext dirContext = ldapAuthenticationManager.getDirContext(ldapAuthenticationManager.getDefaultEnv(), 1); + if (dirContext == null) { + throw new CatalogException("LDAP: Could not connect to the LDAP server using the provided configuration."); + } + ldapAuthenticationManager.closeDirContextAndSuppress(dirContext, new Exception()); + ldapAuthenticationManager.close(); + } + @Override public AuthenticationResponse authenticate(String organizationId, String userId, String password) throws CatalogAuthenticationException { @@ -213,12 +233,12 @@ public void newPassword(String organizationId, String userId, String newPassword @Override public String createToken(String organizationId, String userId, Map claims, long expiration) { - return jwtManager.createJWTToken(organizationId, userId, claims, expiration); + return jwtManager.createJWTToken(organizationId, AuthenticationType.LDAP, userId, claims, expiration); } @Override public String createNonExpiringToken(String organizationId, String userId, Map claims) { - return jwtManager.createJWTToken(organizationId, userId, claims, 0L); + return jwtManager.createJWTToken(organizationId, AuthenticationType.LDAP, userId, claims, 0L); } /* Private methods */ @@ -503,4 +523,9 @@ private String takeString(ObjectMap objectMap, String key, String defaultValue) objectMap.remove(key); return value; } + + @Override + public void close() { + executorService.shutdown(); + } } diff --git a/opencga-catalog/src/main/java/org/opencb/opencga/catalog/auth/authentication/SSOAuthenticationManager.java b/opencga-catalog/src/main/java/org/opencb/opencga/catalog/auth/authentication/SSOAuthenticationManager.java new file mode 100644 index 00000000000..e676029e413 --- /dev/null +++ b/opencga-catalog/src/main/java/org/opencb/opencga/catalog/auth/authentication/SSOAuthenticationManager.java @@ -0,0 +1,87 @@ +package org.opencb.opencga.catalog.auth.authentication; + +import io.jsonwebtoken.SignatureAlgorithm; +import org.apache.commons.lang3.NotImplementedException; +import org.opencb.opencga.catalog.exceptions.CatalogAuthenticationException; +import org.opencb.opencga.catalog.exceptions.CatalogException; +import org.opencb.opencga.core.config.AuthenticationOrigin; +import org.opencb.opencga.core.models.user.AuthenticationResponse; +import org.opencb.opencga.core.models.user.User; +import org.opencb.opencga.core.response.OpenCGAResult; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.security.Key; +import java.util.List; +import java.util.Map; + +public class SSOAuthenticationManager extends AuthenticationManager { + + public SSOAuthenticationManager(String algorithm, String secretKeyString, long expiration) { + super(expiration); + + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.valueOf(algorithm); + Key secretKey = this.converStringToKeyObject(secretKeyString, signatureAlgorithm.getJcaName()); + this.jwtManager = new JwtManager(signatureAlgorithm.getValue(), secretKey); + + this.logger = LoggerFactory.getLogger(SSOAuthenticationManager.class); + } + + public static void validateAuthenticationOriginConfiguration(AuthenticationOrigin authenticationOrigin) throws CatalogException { + if (authenticationOrigin.getType() != AuthenticationOrigin.AuthenticationType.SSO) { + throw new CatalogException("Unknown authentication type. Expected type '" + AuthenticationOrigin.AuthenticationType.SSO + + "' but received '" + authenticationOrigin.getType() + "'."); + } + } + + @Override + public AuthenticationResponse authenticate(String organizationId, String userId, String password) + throws CatalogAuthenticationException { + throw new NotImplementedException("Authentication should be done through SSO"); + } + + @Override + public List getUsersFromRemoteGroup(String group) throws CatalogException { + throw new NotImplementedException("Operation not implemented"); + } + + @Override + public List getRemoteUserInformation(List userStringList) throws CatalogException { + throw new NotImplementedException("Operation not implemented"); + } + + @Override + public List getRemoteGroups(String token) throws CatalogException { + throw new NotImplementedException("Operation not implemented"); + } + + @Override + public void changePassword(String organizationId, String userId, String oldPassword, String newPassword) throws CatalogException { + throw new NotImplementedException("Change password should be done through SSO"); + } + + @Override + public OpenCGAResult resetPassword(String organizationId, String userId) throws CatalogException { + throw new NotImplementedException("Reset password should be done through SSO"); + } + + @Override + public void newPassword(String organizationId, String userId, String newPassword) throws CatalogException { + throw new NotImplementedException("Setting a new password should be done through SSO"); + } + + @Override + public String createToken(String organizationId, String userId, Map claims, long expiration) { + return jwtManager.createJWTToken(organizationId, AuthenticationOrigin.AuthenticationType.SSO, userId, claims, expiration); + } + + @Override + public String createNonExpiringToken(String organizationId, String userId, Map claims) { + return jwtManager.createJWTToken(organizationId, AuthenticationOrigin.AuthenticationType.SSO, userId, claims, 0L); + } + + @Override + public void close() throws IOException { + + } +} diff --git a/opencga-catalog/src/main/java/org/opencb/opencga/catalog/auth/authentication/azure/AuthenticationFactory.java b/opencga-catalog/src/main/java/org/opencb/opencga/catalog/auth/authentication/azure/AuthenticationFactory.java index d1b1246e468..3acc8124f97 100644 --- a/opencga-catalog/src/main/java/org/opencb/opencga/catalog/auth/authentication/azure/AuthenticationFactory.java +++ b/opencga-catalog/src/main/java/org/opencb/opencga/catalog/auth/authentication/azure/AuthenticationFactory.java @@ -2,14 +2,12 @@ import org.apache.commons.collections4.CollectionUtils; import org.opencb.commons.datastore.core.QueryOptions; -import org.opencb.opencga.catalog.auth.authentication.AuthenticationManager; -import org.opencb.opencga.catalog.auth.authentication.AzureADAuthenticationManager; -import org.opencb.opencga.catalog.auth.authentication.CatalogAuthenticationManager; -import org.opencb.opencga.catalog.auth.authentication.LDAPAuthenticationManager; +import org.opencb.opencga.catalog.auth.authentication.*; import org.opencb.opencga.catalog.db.DBAdaptorFactory; import org.opencb.opencga.catalog.db.api.OrganizationDBAdaptor; import org.opencb.opencga.catalog.exceptions.CatalogException; import org.opencb.opencga.catalog.managers.OrganizationManager; +import org.opencb.opencga.catalog.utils.ParamUtils; import org.opencb.opencga.core.config.AuthenticationOrigin; import org.opencb.opencga.core.config.Email; import org.opencb.opencga.core.models.organizations.Organization; @@ -19,6 +17,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -64,6 +63,10 @@ public void configureOrganizationAuthenticationManager(Organization organization tmpAuthenticationManagerMap.put(CatalogAuthenticationManager.INTERNAL, catalogAuthenticationManager); tmpAuthenticationManagerMap.put(CatalogAuthenticationManager.OPENCGA, catalogAuthenticationManager); break; + case SSO: + tmpAuthenticationManagerMap.put(authOrigin.getId(), new SSOAuthenticationManager(algorithm, secretKey, + expiration)); + break; default: logger.warn("Unexpected authentication origin type '{}' for id '{}' found in organization '{}'. " + "Authentication origin will be ignored.", authOrigin.getType(), organization.getId(), @@ -76,6 +79,18 @@ public void configureOrganizationAuthenticationManager(Organization organization if (tmpAuthenticationManagerMap.isEmpty()) { throw new CatalogException("No authentication origin found for organization '" + organization.getId() + "'"); } + if (authenticationManagerMap.containsKey(organization.getId())) { + for (AuthenticationManager authenticationManager : authenticationManagerMap.get(organization.getId()).values()) { + try { + logger.info("Closing previous authentication manager for organization '{}'", organization.getId()); + authenticationManager.close(); + } catch (IOException e) { + throw new CatalogException("Unable to close previous authentication manager for organization '" + organization.getId() + + "'.", e); + } + } + logger.info("Reloading new set of AuthenticationManagers for organization '{}'", organization.getId()); + } authenticationManagerMap.put(organization.getId(), tmpAuthenticationManagerMap); } @@ -147,4 +162,25 @@ public AuthenticationManager getOrganizationAuthenticationManager(String organiz return organizationAuthenticationManagers.get(authOriginId); } + public void validateAuthenticationOrigin(AuthenticationOrigin authenticationOrigin) throws CatalogException { + ParamUtils.checkParameter(authenticationOrigin.getId(), "authentication origin id"); + ParamUtils.checkObj(authenticationOrigin.getType(), "authentication origin type"); + switch (authenticationOrigin.getType()) { + case OPENCGA: + CatalogAuthenticationManager.validateAuthenticationOriginConfiguration(authenticationOrigin); + break; + case LDAP: + LDAPAuthenticationManager.validateAuthenticationOriginConfiguration(authenticationOrigin); + break; + case AzureAD: + AzureADAuthenticationManager.validateAuthenticationOriginConfiguration(authenticationOrigin); + break; + case SSO: + SSOAuthenticationManager.validateAuthenticationOriginConfiguration(authenticationOrigin); + break; + default: + throw new CatalogException("Unknown authentication origin type '" + authenticationOrigin.getType() + "'"); + } + } + } diff --git a/opencga-catalog/src/main/java/org/opencb/opencga/catalog/db/api/OrganizationDBAdaptor.java b/opencga-catalog/src/main/java/org/opencb/opencga/catalog/db/api/OrganizationDBAdaptor.java index f9175b0a908..d8d8e8a0a51 100644 --- a/opencga-catalog/src/main/java/org/opencb/opencga/catalog/db/api/OrganizationDBAdaptor.java +++ b/opencga-catalog/src/main/java/org/opencb/opencga/catalog/db/api/OrganizationDBAdaptor.java @@ -17,6 +17,7 @@ public interface OrganizationDBAdaptor extends Iterable { String IS_ORGANIZATION_ADMIN_OPTION = "isOrgAdmin"; + String AUTH_ORIGINS_FIELD = "authenticationOrigins"; enum QueryParams implements QueryParam { UID("uid", LONG, ""), @@ -28,8 +29,10 @@ enum QueryParams implements QueryParam { INTERNAL("internal", OBJECT, ""), INTERNAL_MIGRATION_EXECUTIONS("internal.migrationExecutions", OBJECT, ""), CONFIGURATION("configuration", OBJECT, ""), - CONFIGURATION_AUTHENTICATION_ORIGINS("configuration.authenticationOrigins", OBJECT, ""), - CONFIGURATION_AUTHENTICATION_ORIGINS_OPTIONS("configuration.authenticationOrigins.options", OBJECT, ""), + CONFIGURATION_OPTIMIZATIONS("configuration.optimizations", OBJECT, ""), + CONFIGURATION_AUTHENTICATION_ORIGINS("configuration." + AUTH_ORIGINS_FIELD, OBJECT, ""), + CONFIGURATION_AUTHENTICATION_ORIGINS_ID("configuration." + AUTH_ORIGINS_FIELD + ".id", STRING, ""), + CONFIGURATION_AUTHENTICATION_ORIGINS_OPTIONS("configuration." + AUTH_ORIGINS_FIELD + ".options", OBJECT, ""), CONFIGURATION_TOKEN("configuration.token", OBJECT, ""), CREATION_DATE("creationDate", DATE, ""), MODIFICATION_DATE("modificationDate", DATE, ""), diff --git a/opencga-catalog/src/main/java/org/opencb/opencga/catalog/db/mongodb/OrganizationMongoDBAdaptor.java b/opencga-catalog/src/main/java/org/opencb/opencga/catalog/db/mongodb/OrganizationMongoDBAdaptor.java index 4261a247c79..4bb4d663e44 100644 --- a/opencga-catalog/src/main/java/org/opencb/opencga/catalog/db/mongodb/OrganizationMongoDBAdaptor.java +++ b/opencga-catalog/src/main/java/org/opencb/opencga/catalog/db/mongodb/OrganizationMongoDBAdaptor.java @@ -24,6 +24,7 @@ import org.opencb.opencga.catalog.utils.UuidUtils; import org.opencb.opencga.core.api.ParamConstants; import org.opencb.opencga.core.common.TimeUtils; +import org.opencb.opencga.core.config.AuthenticationOrigin; import org.opencb.opencga.core.config.Configuration; import org.opencb.opencga.core.models.organizations.Organization; import org.opencb.opencga.core.response.OpenCGAResult; @@ -137,11 +138,7 @@ private OpenCGAResult privateUpdate(ClientSession clientSession, S UpdateDocument updateDocument = getValidatedUpdateParams(clientSession, parameters, queryOptions); Document organizationUpdate = updateDocument.toFinalUpdateDocument(); -// Query tmpQuery = new Query(QueryParams.ID.key(), organization.getId()); -// Bson queryBson = parseQuery(tmpQuery); - Bson queryBson = Filters.eq(QueryParams.ID.key(), organizationId); - - if (organizationUpdate.isEmpty()) { + if (organizationUpdate.isEmpty() && CollectionUtils.isEmpty(updateDocument.getNestedUpdateList())) { if (!parameters.isEmpty()) { logger.error("Non-processed update parameters: {}", parameters.keySet()); throw new CatalogDBException("Update could not be performed. Some fields could not be processed."); @@ -149,20 +146,52 @@ private OpenCGAResult privateUpdate(ClientSession clientSession, S throw new CatalogDBException("Nothing to be updated"); } - List events = new ArrayList<>(); - logger.debug("Update organization. Query: {}, Update: {}", queryBson.toBsonDocument(), organizationUpdate.toBsonDocument()); - // Update study admins need to be executed before the actual update because we need to fetch the previous owner/admins in case // of an update on these fields. updateStudyAdmins(clientSession, parameters, queryOptions); - DataResult updateResult = organizationCollection.update(clientSession, queryBson, organizationUpdate, null); - if (updateResult.getNumMatches() == 0) { - throw new CatalogDBException("Organization not found"); + List events = new ArrayList<>(); + DataResult updateResult; + if (!organizationUpdate.isEmpty()) { + Bson queryBson = Filters.eq(QueryParams.ID.key(), organizationId); + logger.debug("Update organization. Query: {}, Update: {}", queryBson.toBsonDocument(), organizationUpdate.toBsonDocument()); + + updateResult = organizationCollection.update(clientSession, queryBson, organizationUpdate, null); + + if (updateResult.getNumMatches() == 0) { + throw new CatalogDBException("Organization not found"); + } + if (updateResult.getNumUpdated() == 0) { + events.add(new Event(Event.Type.WARNING, organizationId, "Organization was already updated")); + } } - if (updateResult.getNumUpdated() == 0) { - events.add(new Event(Event.Type.WARNING, organizationId, "Organization was already updated")); + if (CollectionUtils.isNotEmpty(updateDocument.getNestedUpdateList())) { + for (NestedArrayUpdateDocument nestedDocument : updateDocument.getNestedUpdateList()) { + Bson bsonQuery = new Document(nestedDocument.getQuery()); + logger.debug("Update nested element from Organization. Query: {}, Update: {}", bsonQuery.toBsonDocument(), + nestedDocument.getSet()); + DataResult result = organizationCollection.update(clientSession, bsonQuery, nestedDocument.getSet(), null); + if (result.getNumMatches() == 0) { + throw new CatalogDBException("Couldn't update organization. Nothing could be found for query " + + bsonQuery.toBsonDocument()); + } + } + /* + for (NestedArrayUpdateDocument nestedDocument : updateDocument.getNestedUpdateList()) { + Bson nestedBsonQuery = parseQuery(nestedDocument.getQuery() + .append(QueryParams.UID.key(), interpretation.getUid())); + logger.debug("Update nested element from interpretation. Query: {}, Update: {}", + nestedBsonQuery.toBsonDocument(), nestedDocument.getSet()); + + update = interpretationCollection.update(clientSession, nestedBsonQuery, nestedDocument.getSet(), null); + + if (update.getNumMatches() == 0) { + throw CatalogDBException.uidNotFound("Interpretation", interpretationUid); + } + } + * */ } + logger.debug("Organization {} successfully updated", organizationId); @@ -235,9 +264,34 @@ private UpdateDocument getValidatedUpdateParams(ClientSession clientSession, Obj String[] acceptedParams = { QueryParams.NAME.key() }; filterStringParams(parameters, document.getSet(), acceptedParams); - String[] acceptedObjectParams = { QueryParams.CONFIGURATION.key() }; + String[] acceptedObjectParams = { QueryParams.CONFIGURATION_OPTIMIZATIONS.key(), QueryParams.CONFIGURATION_TOKEN.key() }; filterObjectParams(parameters, document.getSet(), acceptedObjectParams); + // Authentication Origins action + if (parameters.containsKey(QueryParams.CONFIGURATION_AUTHENTICATION_ORIGINS.key())) { + Map actionMap = queryOptions.getMap(Constants.ACTIONS, new HashMap<>()); + ParamUtils.UpdateAction operation = ParamUtils.UpdateAction.from(actionMap, OrganizationDBAdaptor.AUTH_ORIGINS_FIELD); + String[] authOriginsParams = {QueryParams.CONFIGURATION_AUTHENTICATION_ORIGINS.key()}; + switch (operation) { + case SET: + filterObjectParams(parameters, document.getSet(), authOriginsParams); + break; + case REMOVE: + fixAuthOriginsForRemoval(parameters); + filterObjectParams(parameters, document.getPull(), authOriginsParams); + break; + case ADD: + filterObjectParams(parameters, document.getAddToSet(), authOriginsParams); + break; + case REPLACE: + filterReplaceParams(parameters.getAsList(QueryParams.CONFIGURATION_AUTHENTICATION_ORIGINS.key(), Map.class), document, + m -> String.valueOf(m.get("id")), QueryParams.CONFIGURATION_AUTHENTICATION_ORIGINS_ID.key()); + break; + default: + throw new IllegalStateException("Unknown operation " + operation); + } + } + String owner = parameters.getString(QueryParams.OWNER.key(), null); if (StringUtils.isNotEmpty(owner)) { // Check user exists @@ -312,6 +366,21 @@ private UpdateDocument getValidatedUpdateParams(ClientSession clientSession, Obj return document; } + private void fixAuthOriginsForRemoval(ObjectMap parameters) { + if (parameters.get(QueryParams.CONFIGURATION_AUTHENTICATION_ORIGINS.key()) == null) { + return; + } + List authOriginParamList = new LinkedList<>(); + for (Object authOrigin : parameters.getAsList(QueryParams.CONFIGURATION_AUTHENTICATION_ORIGINS.key())) { + if (authOrigin instanceof AuthenticationOrigin) { + authOriginParamList.add(new Document("id", ((AuthenticationOrigin) authOrigin).getId())); + } else { + authOriginParamList.add(new Document("id", ((Map) authOrigin).get("id"))); + } + } + parameters.putNested(QueryParams.CONFIGURATION_AUTHENTICATION_ORIGINS.key(), authOriginParamList, false); + } + @Override public OpenCGAResult delete(Organization organization) throws CatalogDBException { return null; diff --git a/opencga-catalog/src/main/java/org/opencb/opencga/catalog/managers/CatalogManager.java b/opencga-catalog/src/main/java/org/opencb/opencga/catalog/managers/CatalogManager.java index 2308b7ad123..ec8c3dcf2ad 100644 --- a/opencga-catalog/src/main/java/org/opencb/opencga/catalog/managers/CatalogManager.java +++ b/opencga-catalog/src/main/java/org/opencb/opencga/catalog/managers/CatalogManager.java @@ -301,7 +301,7 @@ public void installIndexes(String token) throws CatalogException { public void installIndexes(String organizationId, String token) throws CatalogException { JwtPayload payload = userManager.validateToken(token); - String userId = payload.getUserId(); + String userId = payload.getUserId(organizationId); if (!authorizationManager.isAtLeastOrganizationOwnerOrAdmin(organizationId, userId)) { throw CatalogAuthorizationException.notOrganizationOwnerOrAdmin(); } diff --git a/opencga-catalog/src/main/java/org/opencb/opencga/catalog/managers/OrganizationManager.java b/opencga-catalog/src/main/java/org/opencb/opencga/catalog/managers/OrganizationManager.java index f89d0595c30..a1a49273086 100644 --- a/opencga-catalog/src/main/java/org/opencb/opencga/catalog/managers/OrganizationManager.java +++ b/opencga-catalog/src/main/java/org/opencb/opencga/catalog/managers/OrganizationManager.java @@ -1,6 +1,7 @@ package org.opencb.opencga.catalog.managers; import com.fasterxml.jackson.core.JsonProcessingException; +import io.jsonwebtoken.security.InvalidKeyException; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.opencb.commons.datastore.core.Event; @@ -17,6 +18,8 @@ import org.opencb.opencga.catalog.exceptions.CatalogIOException; import org.opencb.opencga.catalog.exceptions.CatalogParameterException; import org.opencb.opencga.catalog.io.CatalogIOManager; +import org.opencb.opencga.catalog.utils.Constants; +import org.opencb.opencga.catalog.utils.JwtUtils; import org.opencb.opencga.catalog.utils.ParamUtils; import org.opencb.opencga.catalog.utils.UuidUtils; import org.opencb.opencga.core.api.ParamConstants; @@ -34,6 +37,9 @@ import org.slf4j.LoggerFactory; import java.util.*; +import java.util.stream.Collectors; + +import static org.opencb.opencga.core.common.JacksonUtils.getUpdateObjectMapper; public class OrganizationManager extends AbstractManager { @@ -257,33 +263,6 @@ public OpenCGAResult update(String organizationId, OrganizationUpd // We set the proper values for the audit organizationId = organization.getId(); - // Avoid duplicated authentication origin ids. - // Validate all mandatory authentication origin fields are filled in. - if (updateParams.getConfiguration() != null && updateParams.getConfiguration().getAuthenticationOrigins() != null) { - String authOriginsPrefixKey = OrganizationDBAdaptor.QueryParams.CONFIGURATION_AUTHENTICATION_ORIGINS.key(); - boolean internal = false; - Set authenticationOriginIds = new HashSet<>(); - for (AuthenticationOrigin authenticationOrigin : updateParams.getConfiguration().getAuthenticationOrigins()) { - if (authenticationOrigin.getType().equals(AuthenticationOrigin.AuthenticationType.OPENCGA)) { - if (internal) { - throw new CatalogException("Found duplicated authentication origin of type OPENCGA."); - } - internal = true; - // Set id to INTERNAL - authenticationOrigin.setId(CatalogAuthenticationManager.OPENCGA); - } - ParamUtils.checkIdentifier(authenticationOrigin.getId(), authOriginsPrefixKey + ".id"); - ParamUtils.checkObj(authenticationOrigin.getType(), authOriginsPrefixKey + ".type"); - if (authenticationOriginIds.contains(authenticationOrigin.getId())) { - throw new CatalogException("Found duplicated authentication origin id '" + authenticationOrigin.getId() + "'."); - } - authenticationOriginIds.add(authenticationOrigin.getId()); - } - if (!internal) { - throw new CatalogException("Missing mandatory AuthenticationOrigin of type OPENCGA."); - } - } - result = getOrganizationDBAdaptor(organizationId).update(organizationId, updateMap, options); auditManager.auditUpdate(organizationId, userId, Enums.Resource.ORGANIZATION, organization.getId(), organization.getUuid(), "", @@ -309,6 +288,180 @@ public OpenCGAResult update(String organizationId, OrganizationUpd return result; } + public OpenCGAResult updateConfiguration(String organizationId, OrganizationConfiguration updateParams, + QueryOptions options, String token) throws CatalogException { + JwtPayload tokenPayload = catalogManager.getUserManager().validateToken(token); + String userId = tokenPayload.getUserId(organizationId); + + ObjectMap auditParams = new ObjectMap() + .append("organizationId", organizationId) + .append("updateParams", updateParams) + .append("options", options) + .append("token", token); + + QueryOptions queryOptions = options != null ? new QueryOptions(options) : new QueryOptions(); + OpenCGAResult result = OpenCGAResult.empty(OrganizationConfiguration.class); + try { + authorizationManager.checkIsAtLeastOrganizationOwnerOrAdmin(organizationId, userId); + + ParamUtils.checkObj(updateParams, "OrganizationConfiguration"); + ObjectMap updateConfigurationMap; + try { + updateConfigurationMap = new ObjectMap(getUpdateObjectMapper().writeValueAsString(updateParams)); + } catch (JsonProcessingException e) { + throw new CatalogException("Could not parse OrganizationConfiguration object: " + e.getMessage(), e); + } + + OpenCGAResult internalResult = get(organizationId, INCLUDE_ORGANIZATION_CONFIGURATION, token); + if (internalResult.getNumResults() == 0) { + throw new CatalogException("Organization '" + organizationId + "' not found"); + } + Organization organization = internalResult.first(); + + // We set the proper values for the audit + organizationId = organization.getId(); + + if (CollectionUtils.isNotEmpty(updateParams.getAuthenticationOrigins())) { + // Check action + ParamUtils.UpdateAction authOriginsAction = null; + Map map = queryOptions.getMap(Constants.ACTIONS); + if (map == null || map.get(OrganizationDBAdaptor.AUTH_ORIGINS_FIELD) == null) { + // Write default option + authOriginsAction = ParamUtils.UpdateAction.ADD; + Map actionMap = new HashMap<>(); + actionMap.put(OrganizationDBAdaptor.AUTH_ORIGINS_FIELD, authOriginsAction); + queryOptions.put(Constants.ACTIONS, actionMap); + } else { + authOriginsAction = ParamUtils.UpdateAction.valueOf(map.get(OrganizationDBAdaptor.AUTH_ORIGINS_FIELD).toString()); + } + + Set currentAuthOriginIds = organization.getConfiguration().getAuthenticationOrigins() + .stream() + .map(AuthenticationOrigin::getId) + .collect(Collectors.toSet()); + + Set updateAuthOriginIds = new HashSet<>(); + StringBuilder authOriginUpdateBuilder = new StringBuilder(); + List authenticationOrigins = updateParams.getAuthenticationOrigins(); + for (int i = 0; i < authenticationOrigins.size(); i++) { + AuthenticationOrigin authenticationOrigin = authenticationOrigins.get(i); + ParamUtils.checkParameter(authenticationOrigin.getId(), "AuthenticationOrigin id"); + ParamUtils.checkObj(authenticationOrigin.getType(), "AuthenticationOrigin type"); + if (updateAuthOriginIds.contains(authenticationOrigin.getId())) { + throw new CatalogParameterException("Found duplicated authentication origin id '" + authenticationOrigin.getId() + + "'."); + } + // Check authOrigin OPENCGA-OPENCGA + if ((authenticationOrigin.getType().equals(AuthenticationOrigin.AuthenticationType.OPENCGA) + && !CatalogAuthenticationManager.OPENCGA.equals(authenticationOrigin.getId())) + || (!authenticationOrigin.getType().equals(AuthenticationOrigin.AuthenticationType.OPENCGA) + && CatalogAuthenticationManager.OPENCGA.equals(authenticationOrigin.getId()))) { + throw new CatalogParameterException("AuthenticationOrigin type '" + AuthenticationOrigin.AuthenticationType.OPENCGA + + "' must go together with id '" + CatalogAuthenticationManager.OPENCGA + "'."); + } + updateAuthOriginIds.add(authenticationOrigin.getId()); + if (i > 0) { + authOriginUpdateBuilder.append(", "); + } + authOriginUpdateBuilder.append(authenticationOrigin.getType()).append(": ").append(authenticationOrigin.getId()); + + if (authOriginsAction != ParamUtils.UpdateAction.REMOVE) { + // Validate configuration is correct and can be used + authenticationFactory.validateAuthenticationOrigin(authenticationOrigin); + } + } + + switch (authOriginsAction) { + case ADD: + for (AuthenticationOrigin authenticationOrigin : updateParams.getAuthenticationOrigins()) { + if (currentAuthOriginIds.contains(authenticationOrigin.getId())) { + throw new CatalogException("Authentication origin '" + authenticationOrigin.getId() + "' already exists. " + + "Please, set the authOriginsAction to 'REPLACE' to replace the current configuration."); + } + } + logger.debug("Adding new list of Authentication Origins: {}.", authOriginUpdateBuilder); + break; + case SET: + boolean userAuthOriginFound = false; + for (AuthenticationOrigin authenticationOrigin : updateParams.getAuthenticationOrigins()) { + if (tokenPayload.getAuthOrigin().equals(authenticationOrigin.getType())) { + userAuthOriginFound = true; + } + } + if (!userAuthOriginFound) { + throw new CatalogException("User authentication origin not found in the list of authentication origins. " + + "Please, add an AuthenticationOrigin of type '" + tokenPayload.getAuthOrigin() + "' to the list."); + } + logger.debug("Set new list of Authentication Origins: {}.", authOriginUpdateBuilder); + break; + case REMOVE: + for (AuthenticationOrigin authenticationOrigin : updateParams.getAuthenticationOrigins()) { + if (!currentAuthOriginIds.contains(authenticationOrigin.getId())) { + throw new CatalogException("Authentication origin '" + authenticationOrigin.getId() + "' does not exist. " + + "The current available authentication origin ids are: '" + + StringUtils.join(currentAuthOriginIds, ", ") + "'."); + } + if (tokenPayload.getAuthOrigin().equals(authenticationOrigin.getType())) { + throw new CatalogException("Removing the authentication origin '" + tokenPayload.getAuthOrigin() + "' " + + "not allowed. Your user account uses that AuthenticationOrigin."); + } + } + logger.debug("Removing list of Authentication Origins: {}.", authOriginUpdateBuilder); + break; + case REPLACE: + for (AuthenticationOrigin authenticationOrigin : updateParams.getAuthenticationOrigins()) { + if (!currentAuthOriginIds.contains(authenticationOrigin.getId())) { + throw new CatalogException("Authentication origin '" + authenticationOrigin.getId() + "' not found." + + "Please, set the authOriginsAction to 'ADD' to add the new AuthenticationOrigin."); + } + } + logger.debug("Replace list of Authentication Origins: {}.", authOriginUpdateBuilder); + break; + default: + throw new CatalogParameterException("Unknown authentication origins action " + authOriginsAction); + } + } + + if (updateParams.getToken() != null) { + try { + JwtUtils.validateJWTKey(updateParams.getToken().getAlgorithm(), updateParams.getToken().getSecretKey()); + } catch (InvalidKeyException e) { + throw new CatalogParameterException("Invalid secret key - algorithm for JWT token: " + e.getMessage(), e); + } + if (updateParams.getToken().getExpiration() <= 0) { + throw new CatalogParameterException("Invalid expiration for JWT token. It must be a positive number."); + } + } + + ObjectMap updateMap = new ObjectMap(OrganizationDBAdaptor.QueryParams.CONFIGURATION.key(), updateConfigurationMap); + OpenCGAResult update = getOrganizationDBAdaptor(organizationId).update(organizationId, updateMap, queryOptions); + result.append(update); + auditManager.auditUpdate(organizationId, userId, Enums.Resource.ORGANIZATION, organization.getId(), organization.getUuid(), "", + "", auditParams, new AuditRecord.Status(AuditRecord.Status.Result.SUCCESS)); + + organization = null; + if (queryOptions.getBoolean(ParamConstants.INCLUDE_RESULT_PARAM)) { + // Fetch updated organization + organization = getOrganizationDBAdaptor(organizationId).get(INCLUDE_ORGANIZATION_CONFIGURATION).first(); + result.setResults(Collections.singletonList(organization.getConfiguration())); + } + + if (CollectionUtils.isNotEmpty(updateParams.getAuthenticationOrigins()) || updateParams.getToken() != null) { + if (organization == null) { + organization = getOrganizationDBAdaptor(organizationId).get(INCLUDE_ORGANIZATION_CONFIGURATION).first(); + } + authenticationFactory.configureOrganizationAuthenticationManager(organization); + } + + } catch (Exception e) { + auditManager.auditUpdate(organizationId, userId, Enums.Resource.ORGANIZATION, organizationId, organizationId, "", "", + auditParams, new AuditRecord.Status(AuditRecord.Status.Result.ERROR, new Error(0, "Update organization", + e.getMessage()))); + throw e; + } + return result; + } + private void validateOrganizationForCreation(Organization organization, String userId) throws CatalogParameterException { ParamUtils.checkParameter(organization.getId(), OrganizationDBAdaptor.QueryParams.ID.key()); diff --git a/opencga-catalog/src/main/java/org/opencb/opencga/catalog/migration/MigrationManager.java b/opencga-catalog/src/main/java/org/opencb/opencga/catalog/migration/MigrationManager.java index 10b247a6d0d..fa8fd7b9ee0 100644 --- a/opencga-catalog/src/main/java/org/opencb/opencga/catalog/migration/MigrationManager.java +++ b/opencga-catalog/src/main/java/org/opencb/opencga/catalog/migration/MigrationManager.java @@ -125,14 +125,20 @@ public void runMigration(String version, Collection d public void runMigration(String version, Collection domains, Collection languages, boolean offline, String appHome, ObjectMap params, String token) throws CatalogException, IOException { - // Migrate all organizations - for (String organizationId : dbAdaptorFactory.getOrganizationIds()) { - if (!ParamConstants.ADMIN_ORGANIZATION.equals(organizationId)) { - runMigration(organizationId, version, domains, languages, offline, appHome, params, token); + runMigration(ParamConstants.ADMIN_ORGANIZATION, version, domains, languages, offline, appHome, params, token); + + // ***** Starts code to remove in future versions. Reload MongoDBAdaptorFactory to avoid Notes migration issue. *****/ + try (MongoDBAdaptorFactory mongoDBAdaptorFactory = new MongoDBAdaptorFactory(configuration, catalogManager.getIoManagerFactory())) { + for (String organizationId : mongoDBAdaptorFactory.getOrganizationIds()) { + // ***** Finish code to remove in future versions. Reload MongoDBAdaptorFactory to avoid Notes migration issue. *****/ + + // Migrate all organizations + // for (String organizationId : dbAdaptorFactory.getOrganizationIds()) { + if (!ParamConstants.ADMIN_ORGANIZATION.equals(organizationId)) { + runMigration(organizationId, version, domains, languages, offline, appHome, params, token); + } } } - // Lastly, migrate the admin organization - runMigration(ParamConstants.ADMIN_ORGANIZATION, version, domains, languages, offline, appHome, params, token); } public void runMigration(String organizationId, String version, Collection domains, diff --git a/opencga-catalog/src/test/java/org/opencb/opencga/catalog/auth/authentication/JwtSessionManagerTest.java b/opencga-catalog/src/test/java/org/opencb/opencga/catalog/auth/authentication/JwtSessionManagerTest.java index 2e9887f85fb..c9d2ca8aec4 100644 --- a/opencga-catalog/src/test/java/org/opencb/opencga/catalog/auth/authentication/JwtSessionManagerTest.java +++ b/opencga-catalog/src/test/java/org/opencb/opencga/catalog/auth/authentication/JwtSessionManagerTest.java @@ -53,7 +53,7 @@ public void setUp() throws Exception { @Test public void testCreateJWTToken() throws Exception { - jwtToken = jwtSessionManager.createJWTToken(organizationId, "testUser", Collections.emptyMap(), 60L); + jwtToken = jwtSessionManager.createJWTToken(organizationId, null, "testUser", Collections.emptyMap(), 60L); } @Test @@ -81,7 +81,7 @@ public void testInvalidSecretKey() throws CatalogAuthenticationException { @Test public void testNonExpiringToken() throws CatalogException { - String nonExpiringToken = jwtSessionManager.createJWTToken(organizationId, "System", null, -1L); + String nonExpiringToken = jwtSessionManager.createJWTToken(organizationId, null, "System", null, -1L); assertEquals(jwtSessionManager.getUser(nonExpiringToken), "System"); assertNull(jwtSessionManager.getExpiration(nonExpiringToken)); } diff --git a/opencga-catalog/src/test/java/org/opencb/opencga/catalog/managers/OrganizationManagerTest.java b/opencga-catalog/src/test/java/org/opencb/opencga/catalog/managers/OrganizationManagerTest.java index b408fec74c1..a6d3bd3540f 100644 --- a/opencga-catalog/src/test/java/org/opencb/opencga/catalog/managers/OrganizationManagerTest.java +++ b/opencga-catalog/src/test/java/org/opencb/opencga/catalog/managers/OrganizationManagerTest.java @@ -3,17 +3,20 @@ import org.apache.commons.collections4.CollectionUtils; import org.junit.Test; import org.junit.experimental.categories.Category; +import org.opencb.commons.datastore.core.ObjectMap; import org.opencb.commons.datastore.core.Query; import org.opencb.commons.datastore.core.QueryOptions; import org.opencb.opencga.catalog.auth.authentication.CatalogAuthenticationManager; import org.opencb.opencga.catalog.db.api.OrganizationDBAdaptor; import org.opencb.opencga.catalog.db.api.StudyDBAdaptor; +import org.opencb.opencga.catalog.exceptions.CatalogAuthenticationException; import org.opencb.opencga.catalog.exceptions.CatalogAuthorizationException; import org.opencb.opencga.catalog.exceptions.CatalogException; import org.opencb.opencga.catalog.utils.Constants; import org.opencb.opencga.catalog.utils.ParamUtils; import org.opencb.opencga.core.api.ParamConstants; import org.opencb.opencga.core.config.AuthenticationOrigin; +import org.opencb.opencga.core.config.Optimizations; import org.opencb.opencga.core.models.organizations.*; import org.opencb.opencga.core.models.project.Project; import org.opencb.opencga.core.models.study.Group; @@ -21,10 +24,7 @@ import org.opencb.opencga.core.response.OpenCGAResult; import org.opencb.opencga.core.testclassification.duration.MediumTests; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; +import java.util.*; import static org.junit.Assert.*; @@ -39,48 +39,125 @@ public void ensureAuthOriginExistsTest() throws CatalogException { } @Test - public void ensureAuthOriginCannotBeRemovedTest() throws CatalogException { - OrganizationUpdateParams updateParams = new OrganizationUpdateParams().setConfiguration(new OrganizationConfiguration( - Collections.emptyList(), null, new TokenConfiguration())); - thrown.expect(CatalogException.class); - thrown.expectMessage("OPENCGA"); - catalogManager.getOrganizationManager().update(organizationId, updateParams, INCLUDE_RESULT, ownerToken); + public void updateConfigurationAuthorizationTest() throws CatalogException { + OrganizationConfiguration configuration = new OrganizationConfiguration() + .setOptimizations(new Optimizations(true)); + + // Owner can update configuration + catalogManager.getOrganizationManager().updateConfiguration(organizationId, configuration, QueryOptions.empty(), ownerToken); + + // Admin can update configuration + catalogManager.getOrganizationManager().updateConfiguration(organizationId, configuration, QueryOptions.empty(), orgAdminToken1); + + // Admin can update configuration + catalogManager.getOrganizationManager().updateConfiguration(organizationId, configuration, QueryOptions.empty(), orgAdminToken2); + + // Study admin cannot update configuration + assertThrows(CatalogAuthorizationException.class, () -> catalogManager.getOrganizationManager().updateConfiguration(organizationId, + configuration, QueryOptions.empty(), studyAdminToken1)); + + // Normal user cannot update configuration + assertThrows(CatalogAuthorizationException.class, () -> catalogManager.getOrganizationManager().updateConfiguration(organizationId, + configuration, QueryOptions.empty(), normalToken1)); } @Test - public void avoidDuplicatedOPENCGAAuthOriginTest() throws CatalogException { - AuthenticationOrigin authOrigin = CatalogAuthenticationManager.createOpencgaAuthenticationOrigin(); - AuthenticationOrigin authOrigin2 = CatalogAuthenticationManager.createOpencgaAuthenticationOrigin(); - OrganizationUpdateParams updateParams = new OrganizationUpdateParams().setConfiguration(new OrganizationConfiguration( - Arrays.asList(authOrigin, authOrigin2), null, new TokenConfiguration())); - - thrown.expect(CatalogException.class); - thrown.expectMessage("OPENCGA"); - catalogManager.getOrganizationManager().update(organizationId, updateParams, null, ownerToken); + public void ensureAuthOriginCannotBeRemovedTest() throws CatalogException { + Organization organization = catalogManager.getOrganizationManager().get(organizationId, null, ownerToken).first(); + + OrganizationConfiguration configuration = new OrganizationConfiguration() + .setAuthenticationOrigins(organization.getConfiguration().getAuthenticationOrigins()); + Map actionMap = new HashMap<>(); + actionMap.put(OrganizationDBAdaptor.AUTH_ORIGINS_FIELD, ParamUtils.UpdateAction.REMOVE); + QueryOptions options = new QueryOptions(Constants.ACTIONS, actionMap); + + CatalogException catalogException = assertThrows(CatalogException.class, () -> catalogManager.getOrganizationManager() + .updateConfiguration(organizationId, configuration, options, ownerToken)); + assertTrue(catalogException.getMessage().contains("user account uses")); } @Test - public void avoidDuplicatedAuthOriginIdTest() throws CatalogException { - AuthenticationOrigin authOrigin = CatalogAuthenticationManager.createOpencgaAuthenticationOrigin(); - AuthenticationOrigin authOrigin2 = CatalogAuthenticationManager.createOpencgaAuthenticationOrigin(); - authOrigin2.setType(AuthenticationOrigin.AuthenticationType.LDAP); - OrganizationUpdateParams updateParams = new OrganizationUpdateParams().setConfiguration(new OrganizationConfiguration( - Arrays.asList(authOrigin, authOrigin2), null, new TokenConfiguration())); - - thrown.expect(CatalogException.class); - thrown.expectMessage("origin id"); - catalogManager.getOrganizationManager().update(organizationId, updateParams, null, ownerToken); + public void authOriginActionTest() throws CatalogException { + OrganizationConfiguration configuration = new OrganizationConfiguration() + .setAuthenticationOrigins(Collections.singletonList(new AuthenticationOrigin("myId", + AuthenticationOrigin.AuthenticationType.SSO, null, null))); + Map actionMap = new HashMap<>(); + actionMap.put(OrganizationDBAdaptor.AUTH_ORIGINS_FIELD, ParamUtils.UpdateAction.ADD); + QueryOptions options = new QueryOptions() + .append(ParamConstants.INCLUDE_RESULT_PARAM, true) + .append(Constants.ACTIONS, actionMap); + + OrganizationConfiguration configurationResult = catalogManager.getOrganizationManager().updateConfiguration(organizationId, + configuration, options, ownerToken).first(); + assertEquals(2, configurationResult.getAuthenticationOrigins().size()); + for (AuthenticationOrigin authenticationOrigin : configurationResult.getAuthenticationOrigins()) { + if (authenticationOrigin.getId().equals("myId")) { + assertEquals(AuthenticationOrigin.AuthenticationType.SSO, authenticationOrigin.getType()); + } else { + assertEquals(AuthenticationOrigin.AuthenticationType.OPENCGA, authenticationOrigin.getType()); + } + } + + // Remove authOrigin + actionMap.put(OrganizationDBAdaptor.AUTH_ORIGINS_FIELD, ParamUtils.UpdateAction.REMOVE); + options.put(Constants.ACTIONS, actionMap); + configurationResult = catalogManager.getOrganizationManager().updateConfiguration(organizationId, configuration, options, + ownerToken).first(); + assertEquals(1, configurationResult.getAuthenticationOrigins().size()); + assertEquals(AuthenticationOrigin.AuthenticationType.OPENCGA, configurationResult.getAuthenticationOrigins().get(0).getType()); + + // Set authOrigin + List authenticationOriginList = new ArrayList<>(); + authenticationOriginList.add(new AuthenticationOrigin("myId", AuthenticationOrigin.AuthenticationType.SSO, null, null)); + authenticationOriginList.add(configurationResult.getAuthenticationOrigins().get(0)); + actionMap.put(OrganizationDBAdaptor.AUTH_ORIGINS_FIELD, ParamUtils.UpdateAction.SET); + options.put(Constants.ACTIONS, actionMap); + configuration.setAuthenticationOrigins(authenticationOriginList); + configurationResult = catalogManager.getOrganizationManager().updateConfiguration(organizationId, configuration, options, + ownerToken).first(); + assertEquals(2, configurationResult.getAuthenticationOrigins().size()); + for (AuthenticationOrigin authenticationOrigin : configurationResult.getAuthenticationOrigins()) { + if (authenticationOrigin.getId().equals("myId")) { + assertEquals(AuthenticationOrigin.AuthenticationType.SSO, authenticationOrigin.getType()); + } else { + assertEquals(AuthenticationOrigin.AuthenticationType.OPENCGA, authenticationOrigin.getType()); + } + } + + // Add existing authOrigin + actionMap.put(OrganizationDBAdaptor.AUTH_ORIGINS_FIELD, ParamUtils.UpdateAction.ADD); + options.put(Constants.ACTIONS, actionMap); + CatalogException catalogException = assertThrows(CatalogException.class, () -> catalogManager.getOrganizationManager() + .updateConfiguration(organizationId, configuration, options, ownerToken)); + assertTrue(catalogException.getMessage().contains("REPLACE")); + + // Replace existing authOrigin + actionMap.put(OrganizationDBAdaptor.AUTH_ORIGINS_FIELD, ParamUtils.UpdateAction.REPLACE); + options.put(Constants.ACTIONS, actionMap); + configuration.setAuthenticationOrigins(Collections.singletonList( + new AuthenticationOrigin(CatalogAuthenticationManager.OPENCGA, AuthenticationOrigin.AuthenticationType.OPENCGA, null, new ObjectMap("key", "value")))); + configurationResult = catalogManager.getOrganizationManager().updateConfiguration(organizationId, configuration, options, ownerToken).first(); + assertEquals(2, configurationResult.getAuthenticationOrigins().size()); + for (AuthenticationOrigin authenticationOrigin : configurationResult.getAuthenticationOrigins()) { + if (authenticationOrigin.getId().equals("myId")) { + assertEquals(AuthenticationOrigin.AuthenticationType.SSO, authenticationOrigin.getType()); + } else { + assertEquals(AuthenticationOrigin.AuthenticationType.OPENCGA, authenticationOrigin.getType()); + assertTrue(authenticationOrigin.getOptions().containsKey("key")); + assertEquals("value", authenticationOrigin.getOptions().get("key")); + } + } } @Test - public void updateAuthOriginTest() throws CatalogException { - AuthenticationOrigin authOrigin = CatalogAuthenticationManager.createOpencgaAuthenticationOrigin(); - OrganizationUpdateParams updateParams = new OrganizationUpdateParams().setConfiguration(new OrganizationConfiguration( - Collections.singletonList(authOrigin), null, new TokenConfiguration())); - - Organization organization = catalogManager.getOrganizationManager().update(organizationId, updateParams, INCLUDE_RESULT, ownerToken).first(); - assertEquals(authOrigin.getId(), organization.getConfiguration().getAuthenticationOrigins().get(0).getId()); - assertEquals(authOrigin.getType(), organization.getConfiguration().getAuthenticationOrigins().get(0).getType()); + public void tokenUpdateTest() throws CatalogException { + TokenConfiguration tokenConfiguration = TokenConfiguration.init(); + OrganizationConfiguration configuration = new OrganizationConfiguration().setToken(tokenConfiguration); + OrganizationConfiguration configurationResult = catalogManager.getOrganizationManager().updateConfiguration(organizationId, + configuration, INCLUDE_RESULT, ownerToken).first(); + assertEquals(tokenConfiguration.getSecretKey(), configurationResult.getToken().getSecretKey()); + + assertThrows(CatalogAuthenticationException.class, () -> catalogManager.getOrganizationManager().get(organizationId, null, ownerToken)); } @Test diff --git a/opencga-client/src/main/R/R/Organization-methods.R b/opencga-client/src/main/R/R/Organization-methods.R index 3efaf5eec2f..aeee2457ec2 100644 --- a/opencga-client/src/main/R/R/Organization-methods.R +++ b/opencga-client/src/main/R/R/Organization-methods.R @@ -24,6 +24,7 @@ #' | searchNotes | /{apiVersion}/organizations/notes/search | include, exclude, creationDate, modificationDate, id, scope, visibility, uuid, userId, tags, version | #' | deleteNotes | /{apiVersion}/organizations/notes/{id}/delete | id[*], includeResult | #' | updateNotes | /{apiVersion}/organizations/notes/{id}/update | include, exclude, id[*], includeResult, body[*] | +#' | updateConfiguration | /{apiVersion}/organizations/{organization}/configuration/update | include, exclude, organization[*], includeResult, authenticationOriginsAction, body[*] | #' | info | /{apiVersion}/organizations/{organization}/info | include, exclude, organization[*] | #' | update | /{apiVersion}/organizations/{organization}/update | include, exclude, organization[*], includeResult, adminsAction, body[*] | #' @@ -87,6 +88,18 @@ setMethod("organizationClient", "OpencgaR", function(OpencgaR, id, organization, updateNotes=fetchOpenCGA(object=OpencgaR, category="organizations", categoryId=NULL, subcategory="notes", subcategoryId=id, action="update", params=params, httpMethod="POST", as.queryParam=NULL, ...), + #' @section Endpoint /{apiVersion}/organizations/{organization}/configuration/update: + #' Update the Organization configuration attributes. + #' @param include Fields included in the response, whole JSON path must be provided. + #' @param exclude Fields excluded in the response, whole JSON path must be provided. + #' @param organization Organization id. + #' @param includeResult Flag indicating to include the created or updated document result in the response. + #' @param authenticationOriginsAction Action to be performed if the array of authenticationOrigins is being updated. Allowed values: ['ADD SET REMOVE REPLACE'] + #' @param data JSON containing the params to be updated. + updateConfiguration=fetchOpenCGA(object=OpencgaR, category="organizations", categoryId=organization, + subcategory="configuration", subcategoryId=NULL, action="update", params=params, httpMethod="POST", + as.queryParam=NULL, ...), + #' @section Endpoint /{apiVersion}/organizations/{organization}/info: #' Return the organization information. #' @param include Fields included in the response, whole JSON path must be provided. diff --git a/opencga-client/src/main/java/org/opencb/opencga/client/rest/clients/OrganizationClient.java b/opencga-client/src/main/java/org/opencb/opencga/client/rest/clients/OrganizationClient.java index aebe0ba61f5..e925df3b441 100644 --- a/opencga-client/src/main/java/org/opencb/opencga/client/rest/clients/OrganizationClient.java +++ b/opencga-client/src/main/java/org/opencb/opencga/client/rest/clients/OrganizationClient.java @@ -24,6 +24,7 @@ import org.opencb.opencga.core.models.notes.NoteCreateParams; import org.opencb.opencga.core.models.notes.NoteUpdateParams; import org.opencb.opencga.core.models.organizations.Organization; +import org.opencb.opencga.core.models.organizations.OrganizationConfiguration; import org.opencb.opencga.core.models.organizations.OrganizationCreateParams; import org.opencb.opencga.core.models.organizations.OrganizationUpdateParams; import org.opencb.opencga.core.response.RestResponse; @@ -134,6 +135,25 @@ public RestResponse updateNotes(String id, NoteUpdateParams data, ObjectMa return execute("organizations", null, "notes", id, "update", params, POST, Note.class); } + /** + * Update the Organization configuration attributes. + * @param organization Organization id. + * @param data JSON containing the params to be updated. + * @param params Map containing any of the following optional parameters. + * include: Fields included in the response, whole JSON path must be provided. + * exclude: Fields excluded in the response, whole JSON path must be provided. + * includeResult: Flag indicating to include the created or updated document result in the response. + * authenticationOriginsAction: Action to be performed if the array of authenticationOrigins is being updated. + * @return a RestResponse object. + * @throws ClientException ClientException if there is any server error. + */ + public RestResponse updateConfiguration(String organization, OrganizationConfiguration data, ObjectMap + params) throws ClientException { + params = params != null ? params : new ObjectMap(); + params.put("body", data); + return execute("organizations", organization, "configuration", null, "update", params, POST, OrganizationConfiguration.class); + } + /** * Return the organization information. * @param organization Organization id. diff --git a/opencga-client/src/main/javascript/Organization.js b/opencga-client/src/main/javascript/Organization.js index 4385a545eb0..a556f706193 100644 --- a/opencga-client/src/main/javascript/Organization.js +++ b/opencga-client/src/main/javascript/Organization.js @@ -105,6 +105,22 @@ export default class Organization extends OpenCGAParentClass { return this._post("organizations", null, "notes", id, "update", data, params); } + /** Update the Organization configuration attributes + * @param {String} organization - Organization id. + * @param {Object} data - JSON containing the params to be updated. + * @param {Object} [params] - The Object containing the following optional parameters: + * @param {String} [params.include] - Fields included in the response, whole JSON path must be provided. + * @param {String} [params.exclude] - Fields excluded in the response, whole JSON path must be provided. + * @param {Boolean} [params.includeResult = "false"] - Flag indicating to include the created or updated document result in the response. + * The default value is false. + * @param {"ADD SET REMOVE REPLACE"} [params.authenticationOriginsAction = "ADD"] - Action to be performed if the array of + * authenticationOrigins is being updated. The default value is ADD. + * @returns {Promise} Promise object in the form of RestResponse instance. + */ + updateConfiguration(organization, data, params) { + return this._post("organizations", organization, "configuration", null, "update", data, params); + } + /** Return the organization information * @param {String} organization - Organization id. * @param {Object} [params] - The Object containing the following optional parameters: diff --git a/opencga-client/src/main/python/pyopencga/rest_clients/organization_client.py b/opencga-client/src/main/python/pyopencga/rest_clients/organization_client.py index a3124bfcf45..bdeac02be52 100644 --- a/opencga-client/src/main/python/pyopencga/rest_clients/organization_client.py +++ b/opencga-client/src/main/python/pyopencga/rest_clients/organization_client.py @@ -110,6 +110,26 @@ def update_notes(self, id, data=None, **options): return self._post(category='organizations', resource='update', subcategory='notes', second_query_id=id, data=data, **options) + def update_configuration(self, organization, data=None, **options): + """ + Update the Organization configuration attributes. + PATH: /{apiVersion}/organizations/{organization}/configuration/update + + :param dict data: JSON containing the params to be updated. (REQUIRED) + :param str organization: Organization id. (REQUIRED) + :param str include: Fields included in the response, whole JSON path + must be provided. + :param str exclude: Fields excluded in the response, whole JSON path + must be provided. + :param bool include_result: Flag indicating to include the created or + updated document result in the response. + :param str authentication_origins_action: Action to be performed if + the array of authenticationOrigins is being updated. Allowed + values: ['ADD SET REMOVE REPLACE'] + """ + + return self._post(category='organizations', resource='update', query_id=organization, subcategory='configuration', data=data, **options) + def info(self, organization, **options): """ Return the organization information. diff --git a/opencga-core/src/main/java/org/opencb/opencga/core/config/AuthenticationOrigin.java b/opencga-core/src/main/java/org/opencb/opencga/core/config/AuthenticationOrigin.java index 2b8bfab8624..2b4962b024a 100644 --- a/opencga-core/src/main/java/org/opencb/opencga/core/config/AuthenticationOrigin.java +++ b/opencga-core/src/main/java/org/opencb/opencga/core/config/AuthenticationOrigin.java @@ -31,7 +31,8 @@ public class AuthenticationOrigin { public enum AuthenticationType { OPENCGA, LDAP, - AzureAD + AzureAD, + SSO } // Possible keys of the options map diff --git a/opencga-core/src/main/java/org/opencb/opencga/core/models/JwtPayload.java b/opencga-core/src/main/java/org/opencb/opencga/core/models/JwtPayload.java index 06949efb937..4d8df0f6f0a 100644 --- a/opencga-core/src/main/java/org/opencb/opencga/core/models/JwtPayload.java +++ b/opencga-core/src/main/java/org/opencb/opencga/core/models/JwtPayload.java @@ -4,6 +4,7 @@ import org.apache.commons.lang3.StringUtils; import org.opencb.commons.datastore.core.ObjectMap; import org.opencb.opencga.core.common.JacksonUtils; +import org.opencb.opencga.core.config.AuthenticationOrigin; import java.util.Base64; import java.util.Date; @@ -12,15 +13,20 @@ public class JwtPayload { private final String userId; private final String organization; + private final AuthenticationOrigin.AuthenticationType authOrigin; private final String issuer; // Issuer of the JWT token. private final Date issuedAt; // Time when the JWT was issued. private final Date expirationTime; // Expiration time of the JWT. private final String token; - public JwtPayload(String userId, String organization, String issuer, Date issuedAt, Date expirationTime, String token) { + public static final String AUTH_ORIGIN = "authOrigin"; + + public JwtPayload(String userId, String organization, AuthenticationOrigin.AuthenticationType authOrigin, String issuer, Date issuedAt, + Date expirationTime, String token) { this.token = token; this.userId = userId; this.organization = organization; + this.authOrigin = authOrigin; this.issuer = issuer; this.issuedAt = issuedAt; this.expirationTime = expirationTime; @@ -56,6 +62,12 @@ public JwtPayload(String token) { this.organization = claimsMap.getString("aud"); this.issuer = claimsMap.getString("iss"); + if (claimsMap.containsKey(AUTH_ORIGIN)) { + this.authOrigin = AuthenticationOrigin.AuthenticationType.valueOf(claimsMap.getString(AUTH_ORIGIN)); + } else { + this.authOrigin = null; + } + if (claimsMap.containsKey("iat")) { long iat = 1000L * claimsMap.getLong("iat"); this.issuedAt = new Date(iat); @@ -77,6 +89,7 @@ public String toString() { final StringBuilder sb = new StringBuilder("JwtPayload{"); sb.append("userId='").append(userId).append('\''); sb.append(", organization='").append(organization).append('\''); + sb.append(", authOrigin=").append(authOrigin); sb.append(", issuer='").append(issuer).append('\''); sb.append(", issuedAt=").append(issuedAt); sb.append(", expirationTime=").append(expirationTime); @@ -101,6 +114,10 @@ public String getOrganization() { return organization; } + public AuthenticationOrigin.AuthenticationType getAuthOrigin() { + return authOrigin; + } + public String getIssuer() { return issuer; } diff --git a/opencga-core/src/main/java/org/opencb/opencga/core/models/organizations/OrganizationUpdateParams.java b/opencga-core/src/main/java/org/opencb/opencga/core/models/organizations/OrganizationUpdateParams.java index 679175e1ac8..8c4d42ea570 100644 --- a/opencga-core/src/main/java/org/opencb/opencga/core/models/organizations/OrganizationUpdateParams.java +++ b/opencga-core/src/main/java/org/opencb/opencga/core/models/organizations/OrganizationUpdateParams.java @@ -11,7 +11,7 @@ import static org.opencb.opencga.core.common.JacksonUtils.getUpdateObjectMapper; -public class OrganizationUpdateParams { +public final class OrganizationUpdateParams { @DataField(id = "name", description = FieldConstants.ORGANIZATION_NAME_DESCRIPTION) private String name; @@ -28,9 +28,6 @@ public class OrganizationUpdateParams { @DataField(id = "modificationDate", description = FieldConstants.GENERIC_MODIFICATION_DATE_DESCRIPTION) private String modificationDate; - @DataField(id = "configuration", description = FieldConstants.ORGANIZATION_CONFIGURATION_DESCRIPTION) - private OrganizationConfiguration configuration; - @DataField(id = "attributes", description = FieldConstants.GENERIC_ATTRIBUTES_DESCRIPTION) private Map attributes; @@ -38,13 +35,12 @@ public OrganizationUpdateParams() { } public OrganizationUpdateParams(String name, String owner, List admins, String creationDate, String modificationDate, - OrganizationConfiguration configuration, Map attributes) { + Map attributes) { this.name = name; this.owner = owner; this.admins = admins; this.creationDate = creationDate; this.modificationDate = modificationDate; - this.configuration = configuration; this.attributes = attributes; } @@ -61,7 +57,6 @@ public String toString() { sb.append(", admins=").append(admins); sb.append(", creationDate='").append(creationDate).append('\''); sb.append(", modificationDate='").append(modificationDate).append('\''); - sb.append(", configuration=").append(configuration); sb.append(", attributes=").append(attributes); sb.append('}'); return sb.toString(); @@ -112,15 +107,6 @@ public OrganizationUpdateParams setModificationDate(String modificationDate) { return this; } - public OrganizationConfiguration getConfiguration() { - return configuration; - } - - public OrganizationUpdateParams setConfiguration(OrganizationConfiguration configuration) { - this.configuration = configuration; - return this; - } - public Map getAttributes() { return attributes; } diff --git a/opencga-server/src/main/java/org/opencb/opencga/server/rest/OrganizationWSServer.java b/opencga-server/src/main/java/org/opencb/opencga/server/rest/OrganizationWSServer.java index 9deeb489110..cc36683e454 100644 --- a/opencga-server/src/main/java/org/opencb/opencga/server/rest/OrganizationWSServer.java +++ b/opencga-server/src/main/java/org/opencb/opencga/server/rest/OrganizationWSServer.java @@ -27,6 +27,7 @@ import org.opencb.opencga.core.models.notes.NoteCreateParams; import org.opencb.opencga.core.models.notes.NoteUpdateParams; import org.opencb.opencga.core.models.organizations.Organization; +import org.opencb.opencga.core.models.organizations.OrganizationConfiguration; import org.opencb.opencga.core.models.organizations.OrganizationCreateParams; import org.opencb.opencga.core.models.organizations.OrganizationUpdateParams; import org.opencb.opencga.core.response.OpenCGAResult; @@ -96,6 +97,34 @@ public Response update( } } + @POST + @Path("/{organization}/configuration/update") + @Consumes(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Update the Organization configuration attributes", response = OrganizationConfiguration.class) + @ApiImplicitParams({ + @ApiImplicitParam(name = QueryOptions.INCLUDE, value = ParamConstants.INCLUDE_DESCRIPTION, dataType = "string", paramType = "query"), + @ApiImplicitParam(name = QueryOptions.EXCLUDE, value = ParamConstants.EXCLUDE_DESCRIPTION, dataType = "string", paramType = "query") + }) + public Response updateConfiguration( + @ApiParam(value = ParamConstants.ORGANIZATION_DESCRIPTION, required = true) @PathParam(ParamConstants.ORGANIZATION) String organizationId, + @ApiParam(value = ParamConstants.INCLUDE_RESULT_DESCRIPTION, defaultValue = "false") @QueryParam(ParamConstants.INCLUDE_RESULT_PARAM) boolean includeResult, + @ApiParam(value = "Action to be performed if the array of authenticationOrigins is being updated.", + allowableValues = "ADD,REMOVE,SET,REPLACE", defaultValue = "ADD") @QueryParam("authenticationOriginsAction") ParamUtils.UpdateAction authOriginsAction, + @ApiParam(value = "JSON containing the params to be updated.", required = true) OrganizationConfiguration parameters) { + try { + if (authOriginsAction == null) { + authOriginsAction = ParamUtils.UpdateAction.ADD; + } + Map actionMap = new HashMap<>(); + actionMap.put(OrganizationDBAdaptor.AUTH_ORIGINS_FIELD, authOriginsAction); + queryOptions.put(Constants.ACTIONS, actionMap); + OpenCGAResult result = catalogManager.getOrganizationManager().updateConfiguration(organizationId, parameters, queryOptions, token); + return createOkResponse(result); + } catch (Exception e) { + return createErrorResponse(e); + } + } + @POST @Path("/create") @Consumes(MediaType.APPLICATION_JSON)