diff --git a/authenticator/src/main/java/org/openmbee/mms/authenticator/security/JwtTokenGenerator.java b/authenticator/src/main/java/org/openmbee/mms/authenticator/security/JwtTokenGenerator.java index 6c4f2549b..2c7fbce00 100644 --- a/authenticator/src/main/java/org/openmbee/mms/authenticator/security/JwtTokenGenerator.java +++ b/authenticator/src/main/java/org/openmbee/mms/authenticator/security/JwtTokenGenerator.java @@ -13,6 +13,7 @@ import java.util.Map; import javax.crypto.SecretKey; +import org.openmbee.mms.core.services.TokenService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -22,7 +23,7 @@ import org.springframework.stereotype.Component; @Component -public class JwtTokenGenerator implements Serializable { +public class JwtTokenGenerator implements Serializable, TokenService { private static final long serialVersionUID = 6463567580980594813L; private static final String CLAIM_KEY_USERNAME = "sub"; @@ -103,15 +104,26 @@ private boolean isTokenExpired(String token) { return expirationDate.before(new Date()); } + @Override public String generateToken(UserDetails userDetails) { - Map claims = new HashMap<>(); + List authorities = new ArrayList<>(); for (GrantedAuthority ga : userDetails.getAuthorities()) { authorities.add(ga.getAuthority()); } - claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); - claims.put(CLAIM_KEY_USERID, userDetails.getUsername()); - claims.put(CLAIM_KEY_ENABLED, userDetails.isEnabled()); + return generateToken(userDetails.getUsername(), userDetails.isEnabled(), authorities); + } + + @Override + public String generateToken(String principal, Collection authorities) { + return generateToken(principal, true, authorities); + } + + private String generateToken(String principal, boolean enabled, Collection authorities) { + Map claims = new HashMap<>(); + claims.put(CLAIM_KEY_USERNAME, principal); + claims.put(CLAIM_KEY_USERID, principal); + claims.put(CLAIM_KEY_ENABLED, enabled); claims.put(CLAIM_KEY_CREATED, new Date()); claims.put(CLAIM_KEY_AUTHORITIES, authorities); return generateToken(claims); diff --git a/core/src/main/java/org/openmbee/mms/core/config/Constants.java b/core/src/main/java/org/openmbee/mms/core/config/Constants.java index 72cee3c71..ee6b30275 100644 --- a/core/src/main/java/org/openmbee/mms/core/config/Constants.java +++ b/core/src/main/java/org/openmbee/mms/core/config/Constants.java @@ -1,6 +1,7 @@ package org.openmbee.mms.core.config; import java.util.*; +import java.util.regex.Pattern; public class Constants { @@ -17,6 +18,8 @@ public class Constants { public static final String MASTER_BRANCH = "master"; + public static final Pattern BRANCH_ID_VALID_PATTERN = Pattern.compile("^[\\w-]+$"); + public static final Map RPmap = new LinkedHashMap<>(); public static final List aPriv; public static final List rPriv; diff --git a/core/src/main/java/org/openmbee/mms/core/objects/ElementsCommitResponse.java b/core/src/main/java/org/openmbee/mms/core/objects/ElementsCommitResponse.java new file mode 100644 index 000000000..4a0a42732 --- /dev/null +++ b/core/src/main/java/org/openmbee/mms/core/objects/ElementsCommitResponse.java @@ -0,0 +1,17 @@ +package org.openmbee.mms.core.objects; + +import io.swagger.v3.oas.annotations.media.Schema; + +public class ElementsCommitResponse extends ElementsResponse { + + @Schema(nullable = true) + private String commitId; + + public String getCommitId() { + return commitId; + } + + public void setCommitId(String commitId) { + this.commitId = commitId; + } +} diff --git a/core/src/main/java/org/openmbee/mms/core/services/NodeService.java b/core/src/main/java/org/openmbee/mms/core/services/NodeService.java index 7db8b19d4..adaae9665 100644 --- a/core/src/main/java/org/openmbee/mms/core/services/NodeService.java +++ b/core/src/main/java/org/openmbee/mms/core/services/NodeService.java @@ -4,6 +4,7 @@ import java.io.OutputStream; import java.util.Map; +import org.openmbee.mms.core.objects.ElementsCommitResponse; import org.openmbee.mms.core.objects.ElementsRequest; import org.openmbee.mms.core.objects.ElementsResponse; import org.openmbee.mms.data.domains.scoped.Node; @@ -17,8 +18,8 @@ public interface NodeService { ElementsResponse read(String projectId, String refId, ElementsRequest req, Map params); - ElementsResponse createOrUpdate(String projectId, String refId, ElementsRequest req, - Map params, String user); + ElementsCommitResponse createOrUpdate(String projectId, String refId, ElementsRequest req, + Map params, String user); void extraProcessPostedElement(ElementJson element, Node node, NodeChangeInfo info); @@ -26,7 +27,7 @@ ElementsResponse createOrUpdate(String projectId, String refId, ElementsRequest void extraProcessGotElement(ElementJson element, Node node, NodeGetInfo info); - ElementsResponse delete(String projectId, String refId, String id, String user); + ElementsCommitResponse delete(String projectId, String refId, String id, String user); - ElementsResponse delete(String projectId, String refId, ElementsRequest req, String user); + ElementsCommitResponse delete(String projectId, String refId, ElementsRequest req, String user); } diff --git a/core/src/main/java/org/openmbee/mms/core/services/TokenService.java b/core/src/main/java/org/openmbee/mms/core/services/TokenService.java new file mode 100644 index 000000000..27f11f2c5 --- /dev/null +++ b/core/src/main/java/org/openmbee/mms/core/services/TokenService.java @@ -0,0 +1,11 @@ +package org.openmbee.mms.core.services; + +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; + +public interface TokenService { + String generateToken(UserDetails userDetails); + String generateToken(String principal, Collection authorities); +} diff --git a/crud/src/main/java/org/openmbee/mms/crud/controllers/branches/BranchesController.java b/crud/src/main/java/org/openmbee/mms/crud/controllers/branches/BranchesController.java index 60e61318b..be7cf5708 100644 --- a/crud/src/main/java/org/openmbee/mms/crud/controllers/branches/BranchesController.java +++ b/crud/src/main/java/org/openmbee/mms/crud/controllers/branches/BranchesController.java @@ -21,13 +21,13 @@ import java.util.ArrayList; import java.util.List; +import static org.openmbee.mms.core.config.Constants.BRANCH_ID_VALID_PATTERN; + @RestController @RequestMapping("/projects/{projectId}/refs") @Tag(name = "Refs") public class BranchesController extends BaseController { - private static final String BRANCH_ID_VALID_PATTERN = "^[\\w-]+$"; - private BranchService branchService; @Autowired @@ -118,6 +118,6 @@ public RefsResponse deleteRef( } static boolean isBranchIdValid(String branchId) { - return branchId != null && branchId.matches(BRANCH_ID_VALID_PATTERN); + return branchId != null && BRANCH_ID_VALID_PATTERN.matcher(branchId).matches(); } } diff --git a/crud/src/main/java/org/openmbee/mms/crud/controllers/elements/ElementsController.java b/crud/src/main/java/org/openmbee/mms/crud/controllers/elements/ElementsController.java index 23bb377a2..fef7654c1 100644 --- a/crud/src/main/java/org/openmbee/mms/crud/controllers/elements/ElementsController.java +++ b/crud/src/main/java/org/openmbee/mms/crud/controllers/elements/ElementsController.java @@ -15,6 +15,7 @@ import java.io.InputStream; import java.util.*; +import org.openmbee.mms.core.objects.ElementsCommitResponse; import org.openmbee.mms.core.objects.ElementsRequest; import org.openmbee.mms.core.objects.ElementsResponse; import org.openmbee.mms.core.services.CommitService; @@ -107,7 +108,7 @@ public ElementsResponse getElement( @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("@mss.hasBranchPrivilege(authentication, #projectId, #refId, 'BRANCH_EDIT_CONTENT', false)") - public ElementsResponse createOrUpdateElements( + public ElementsCommitResponse createOrUpdateElements( @PathVariable String projectId, @PathVariable String refId, @RequestBody ElementsRequest req, @@ -194,13 +195,13 @@ public ElementsResponse getElements( @DeleteMapping(value = "/{elementId}") @PreAuthorize("@mss.hasBranchPrivilege(authentication, #projectId, #refId, 'BRANCH_EDIT_CONTENT', false)") - public ElementsResponse deleteElement( + public ElementsCommitResponse deleteElement( @PathVariable String projectId, @PathVariable String refId, @PathVariable String elementId, Authentication auth) { - ElementsResponse res = getNodeService(projectId).delete(projectId, refId, elementId, auth.getName()); + ElementsCommitResponse res = getNodeService(projectId).delete(projectId, refId, elementId, auth.getName()); handleSingleResponse(res); return res; } diff --git a/crud/src/main/java/org/openmbee/mms/crud/services/DefaultNodeService.java b/crud/src/main/java/org/openmbee/mms/crud/services/DefaultNodeService.java index 19ca028a6..772825d54 100644 --- a/crud/src/main/java/org/openmbee/mms/crud/services/DefaultNodeService.java +++ b/crud/src/main/java/org/openmbee/mms/crud/services/DefaultNodeService.java @@ -13,6 +13,7 @@ import java.util.stream.IntStream; import java.util.stream.Stream; import org.openmbee.mms.core.exceptions.BadRequestException; +import org.openmbee.mms.core.objects.ElementsCommitResponse; import org.openmbee.mms.core.objects.EventObject; import org.openmbee.mms.core.services.EventService; import org.openmbee.mms.core.services.NodeChangeInfo; @@ -183,8 +184,8 @@ public ElementsResponse read(String projectId, String refId, ElementsRequest req } @Override - public ElementsResponse createOrUpdate(String projectId, String refId, ElementsRequest req, - Map params, String user) { + public ElementsCommitResponse createOrUpdate(String projectId, String refId, ElementsRequest req, + Map params, String user) { ContextHolder.setContext(projectId, refId); boolean overwriteJson = Boolean.parseBoolean(params.get("overwrite")); @@ -197,9 +198,12 @@ public ElementsResponse createOrUpdate(String projectId, String refId, ElementsR commitChanges(info); - ElementsResponse response = new ElementsResponse(); + ElementsCommitResponse response = new ElementsCommitResponse(); response.getElements().addAll(info.getUpdatedMap().values()); response.setRejected(new ArrayList<>(info.getRejected().values())); + if(!info.getUpdatedMap().isEmpty()) { + response.setCommitId(info.getCommitJson().getId()); + } return response; } @@ -262,7 +266,7 @@ public void extraProcessGotElement(ElementJson element, Node node, NodeGetInfo i } @Override - public ElementsResponse delete(String projectId, String refId, String id, String user) { + public ElementsCommitResponse delete(String projectId, String refId, String id, String user) { ElementsRequest req = buildRequest(id); return delete(projectId, refId, req, user); } @@ -286,18 +290,21 @@ protected ElementsRequest buildRequest(Collection ids) { } @Override - public ElementsResponse delete(String projectId, String refId, ElementsRequest req, String user) { + public ElementsCommitResponse delete(String projectId, String refId, ElementsRequest req, String user) { ContextHolder.setContext(projectId, refId); NodeChangeInfo info = nodeDeleteHelper .processDeleteJson(req.getElements(), createCommit(user, refId, projectId, req, null), this); - ElementsResponse response = new ElementsResponse(); + ElementsCommitResponse response = new ElementsCommitResponse(); commitChanges(info); response.getElements().addAll(info.getDeletedMap().values()); response.setRejected(new ArrayList<>(info.getRejected().values())); + if(!info.getDeletedMap().isEmpty()) { + response.setCommitId(info.getCommitJson().getId()); + } return response; } diff --git a/localuser/src/main/java/org/openmbee/mms/localuser/controllers/LocalUserController.java b/localuser/src/main/java/org/openmbee/mms/localuser/controllers/LocalUserController.java index eda199b8e..4d908cccc 100644 --- a/localuser/src/main/java/org/openmbee/mms/localuser/controllers/LocalUserController.java +++ b/localuser/src/main/java/org/openmbee/mms/localuser/controllers/LocalUserController.java @@ -60,7 +60,7 @@ public Object updatePassword(@RequestBody UserCreateRequest req, try { if (requesterAdmin || requester.equals(req.getUsername())) { - userDetailsService.changeUserPassword(req.getUsername(), req.getPassword()); + userDetailsService.changeUserPassword(req.getUsername(), req.getPassword(), requesterAdmin); } else { throw new UnauthorizedException("Not authorized"); } diff --git a/localuser/src/main/java/org/openmbee/mms/localuser/security/UserDetailsServiceImpl.java b/localuser/src/main/java/org/openmbee/mms/localuser/security/UserDetailsServiceImpl.java index aaa4c13e9..824786805 100644 --- a/localuser/src/main/java/org/openmbee/mms/localuser/security/UserDetailsServiceImpl.java +++ b/localuser/src/main/java/org/openmbee/mms/localuser/security/UserDetailsServiceImpl.java @@ -51,14 +51,14 @@ public User register(UserCreateRequest req) { user.setFirstName(req.getFirstname()); user.setLastName(req.getLastname()); user.setUsername(req.getUsername()); - user.setPassword(passwordEncoder.encode(req.getPassword())); + user.setPassword(encodePassword(req.getPassword())); user.setEnabled(true); user.setAdmin(req.isAdmin()); return userRepository.save(user); } @Transactional - public void changeUserPassword(String username, String password) { + public void changeUserPassword(String username, String password, boolean asAdmin) { Optional userOptional = userRepository.findByUsername(username); if(! userOptional.isPresent()) { throw new UsernameNotFoundException( @@ -66,11 +66,16 @@ public void changeUserPassword(String username, String password) { } User user = userOptional.get(); - if(user.getPassword() == null || user.getPassword().isEmpty()) { + if(!asAdmin && (user.getPassword() == null || user.getPassword().isEmpty())) { throw new ForbiddenException("Cannot change or set passwords for external users."); } - user.setPassword(passwordEncoder.encode(password)); + //TODO password strength test? + user.setPassword(encodePassword(password)); userRepository.save(user); } + + private String encodePassword(String password) { + return (password != null && !password.isEmpty()) ? passwordEncoder.encode(password) : null; + } } diff --git a/rdb/src/main/java/org/openmbee/mms/rdb/config/DatabaseDefinitionService.java b/rdb/src/main/java/org/openmbee/mms/rdb/config/DatabaseDefinitionService.java index 5d1470af1..84de0561e 100644 --- a/rdb/src/main/java/org/openmbee/mms/rdb/config/DatabaseDefinitionService.java +++ b/rdb/src/main/java/org/openmbee/mms/rdb/config/DatabaseDefinitionService.java @@ -88,6 +88,9 @@ public void deleteProjectDatabase(Project project) throws SQLException { + databaseProjectString(project) + "';")); } statement.executeUpdate(connection.nativeSQL("DROP DATABASE " + databaseProjectString(project))); + + //TODO: if using PG 13, can use the following + //statement.executeUpdate(connection.nativeSQL("DROP DATABASE " + databaseProjectString(project) + " WITH (FORCE)")); } } diff --git a/rdb/src/main/java/org/openmbee/mms/rdb/repositories/BaseDAOImpl.java b/rdb/src/main/java/org/openmbee/mms/rdb/repositories/BaseDAOImpl.java index 8af828e7d..4a5e21365 100644 --- a/rdb/src/main/java/org/openmbee/mms/rdb/repositories/BaseDAOImpl.java +++ b/rdb/src/main/java/org/openmbee/mms/rdb/repositories/BaseDAOImpl.java @@ -9,6 +9,10 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.transaction.PlatformTransactionManager; +import java.util.regex.Pattern; + +import static org.openmbee.mms.core.config.Constants.BRANCH_ID_VALID_PATTERN; + public abstract class BaseDAOImpl { private CrudDataSources crudDataSources; @@ -36,6 +40,10 @@ public JdbcTemplate getConn() { public String getSuffix() { String refId = ContextHolder.getContext().getBranchId(); - return refId.equals(ContextObject.MASTER_BRANCH) ? "" : refId.toLowerCase(); + if(BRANCH_ID_VALID_PATTERN.matcher(refId).matches()) { + return refId.equals(ContextObject.MASTER_BRANCH) ? "" : refId.toLowerCase(); + } else { + throw new IllegalArgumentException("Bad branch id, aborting current operation."); + } } } diff --git a/twc/src/main/java/org/openmbee/mms/twc/config/TwcConfig.java b/twc/src/main/java/org/openmbee/mms/twc/config/TwcConfig.java index 228e3f379..9b573bfcd 100644 --- a/twc/src/main/java/org/openmbee/mms/twc/config/TwcConfig.java +++ b/twc/src/main/java/org/openmbee/mms/twc/config/TwcConfig.java @@ -8,6 +8,8 @@ import org.springframework.context.annotation.Configuration; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; @Configuration @ConfigurationProperties("twc") @@ -18,6 +20,8 @@ public class TwcConfig { private List instances; + private boolean useAuthDelegation = false; + public List getInstances() { return instances; } @@ -26,6 +30,14 @@ public void setInstances(List instances) { this.instances = instances; } + public boolean isUseAuthDelegation() { + return useAuthDelegation; + } + + public void setUseAuthDelegation(boolean useAuthDelegation) { + this.useAuthDelegation = useAuthDelegation; + } + public TwcAuthenticationProvider getAuthNProvider(String associatedTWC) { if(associatedTWC == null) return null; @@ -39,11 +51,21 @@ public TwcAuthenticationProvider getAuthNProvider(String associatedTWC) { } public TeamworkCloud getTeamworkCloud(String twcUrl) { + String host = stripHost(twcUrl); for(TeamworkCloud twc : getInstances()) { - if (twc.hasKnownName(twcUrl)) { + if (twc.hasKnownName(host)) { return twc; } } return null; } + + private static String stripHost(String url) { + Pattern pattern = Pattern.compile("(https?://)?([\\w-\\.]*)(:\\d+)?"); + Matcher matcher = pattern.matcher(url); + if(matcher.matches()) { + return matcher.group(2); + } + return url; + } } diff --git a/twc/src/main/java/org/openmbee/mms/twc/permissions/TwcPermissionsDelegateFactory.java b/twc/src/main/java/org/openmbee/mms/twc/permissions/TwcPermissionsDelegateFactory.java index bb06901dc..176261ad0 100644 --- a/twc/src/main/java/org/openmbee/mms/twc/permissions/TwcPermissionsDelegateFactory.java +++ b/twc/src/main/java/org/openmbee/mms/twc/permissions/TwcPermissionsDelegateFactory.java @@ -40,6 +40,9 @@ public void setTwcMetadataService(TwcMetadataService twcMetadataService) { @Override public PermissionsDelegate getPermissionsDelegate(Project project) { + if(!twcConfig.isUseAuthDelegation()) { + return null; + } TwcProjectDetails twcProjectDetails = getTwcDetails(project); if(twcProjectDetails != null) { @@ -52,12 +55,20 @@ public PermissionsDelegate getPermissionsDelegate(Project project) { @Override public PermissionsDelegate getPermissionsDelegate(Organization organization) { + if(!twcConfig.isUseAuthDelegation()) { + return null; + } + //TODO implement this once category-level permissions are implemented in TWC return null; } @Override public PermissionsDelegate getPermissionsDelegate(Branch branch) { + if(!twcConfig.isUseAuthDelegation()) { + return null; + } + TwcProjectDetails twcProjectDetails = getTwcDetails(branch.getProject()); if(twcProjectDetails != null) { return autowire(new TwcBranchPermissionsDelegate(branch, twcProjectDetails.getTeamworkCloud(), diff --git a/twc/src/test/java/org/openmbee/mms/twc/permissions/TwcPermissionsDelegateFactoryTest.java b/twc/src/test/java/org/openmbee/mms/twc/permissions/TwcPermissionsDelegateFactoryTest.java index c64e881f6..cbe38dec3 100644 --- a/twc/src/test/java/org/openmbee/mms/twc/permissions/TwcPermissionsDelegateFactoryTest.java +++ b/twc/src/test/java/org/openmbee/mms/twc/permissions/TwcPermissionsDelegateFactoryTest.java @@ -21,8 +21,19 @@ public class TwcPermissionsDelegateFactoryTest { @Test public void testTwcOrg() { + ApplicationContext applicationContext = mock(ApplicationContext.class); + TwcConfig twcConfig = mock(TwcConfig.class); + TwcMetadataService twcMetadataService = mock(TwcMetadataService.class); + + when(twcConfig.isUseAuthDelegation()).thenReturn(true); + + TwcPermissionsDelegateFactory twcPermissionsDelegateFactory = new TwcPermissionsDelegateFactory(); + twcPermissionsDelegateFactory.setApplicationContext(applicationContext); + twcPermissionsDelegateFactory.setTwcConfig(twcConfig); + twcPermissionsDelegateFactory.setTwcMetadataService(twcMetadataService); + //These are not supported - assertNull( new TwcPermissionsDelegateFactory().getPermissionsDelegate(new Organization())); + assertNull( twcPermissionsDelegateFactory.getPermissionsDelegate(new Organization())); } @Test @@ -32,6 +43,8 @@ public void testTwcProject() { TwcConfig twcConfig = mock(TwcConfig.class); TwcMetadataService twcMetadataService = mock(TwcMetadataService.class); + when(twcConfig.isUseAuthDelegation()).thenReturn(true); + TwcPermissionsDelegateFactory twcPermissionsDelegateFactory = new TwcPermissionsDelegateFactory(); twcPermissionsDelegateFactory.setApplicationContext(applicationContext); twcPermissionsDelegateFactory.setTwcConfig(twcConfig); @@ -70,6 +83,8 @@ public void testTwcProjectDoesNotMatchTwc() { TwcConfig twcConfig = mock(TwcConfig.class); TwcMetadataService twcMetadataService = mock(TwcMetadataService.class); + when(twcConfig.isUseAuthDelegation()).thenReturn(true); + TwcPermissionsDelegateFactory twcPermissionsDelegateFactory = new TwcPermissionsDelegateFactory(); twcPermissionsDelegateFactory.setApplicationContext(applicationContext); twcPermissionsDelegateFactory.setTwcConfig(twcConfig); @@ -143,6 +158,8 @@ public void testTwcBranch() { TwcConfig twcConfig = mock(TwcConfig.class); TwcMetadataService twcMetadataService = mock(TwcMetadataService.class); + when(twcConfig.isUseAuthDelegation()).thenReturn(true); + TwcPermissionsDelegateFactory twcPermissionsDelegateFactory = new TwcPermissionsDelegateFactory(); twcPermissionsDelegateFactory.setApplicationContext(applicationContext); twcPermissionsDelegateFactory.setTwcConfig(twcConfig); @@ -184,6 +201,8 @@ public void testTwcBranchDoesNotMatchTwc() { TwcConfig twcConfig = mock(TwcConfig.class); TwcMetadataService twcMetadataService = mock(TwcMetadataService.class); + when(twcConfig.isUseAuthDelegation()).thenReturn(true); + TwcPermissionsDelegateFactory twcPermissionsDelegateFactory = new TwcPermissionsDelegateFactory(); twcPermissionsDelegateFactory.setApplicationContext(applicationContext); twcPermissionsDelegateFactory.setTwcConfig(twcConfig);