Skip to content

Commit

Permalink
Adds ability for regular users to grant and revoke write permissions …
Browse files Browse the repository at this point in the history
…for their owned assets (#1273)

* Added services for listing, granting and revoking write permissions for an entity

* Revert pom change

* Fixed Source EntityType

* Revert warmCaches

* Renamed package

* Added role suggest

* Added access control to Cohort Characterizations

* Added note

* Migrations

* fixes query group by fields
  • Loading branch information
pavgra authored and wivern committed Aug 1, 2019
1 parent 2a81135 commit 93f0299
Show file tree
Hide file tree
Showing 32 changed files with 764 additions and 270 deletions.
2 changes: 2 additions & 0 deletions src/main/java/org/ohdsi/webapi/JerseyConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.ohdsi.webapi.cohortcharacterization.CcController;
import org.ohdsi.webapi.executionengine.controller.ScriptExecutionCallbackController;
import org.ohdsi.webapi.executionengine.controller.ScriptExecutionController;
import org.ohdsi.webapi.security.PermissionController;
import org.ohdsi.webapi.service.ActivityService;
import org.ohdsi.webapi.service.CDMResultsService;
import org.ohdsi.webapi.service.CohortAnalysisService;
Expand Down Expand Up @@ -79,6 +80,7 @@ public void afterPropertiesSet() throws Exception {
register(MultiPartFeature.class);
register(FeatureExtractionService.class);
register(CcController.class);
register(PermissionController.class);
register(new AbstractBinder() {
@Override
protected void configure() {
Expand Down
1 change: 1 addition & 0 deletions src/main/java/org/ohdsi/webapi/events/EntityName.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.ohdsi.webapi.events;

// NOTE: overlaps with EntityType, will be refactored in upcoming PR
public enum EntityName {
COHORT_CHARACTERIZATION("cohort characterization"),
FEATURE_ANALYSIS("feature analysis"),
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/org/ohdsi/webapi/security/AccessType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.ohdsi.webapi.security;

public enum AccessType {
READ,
WRITE
}
114 changes: 114 additions & 0 deletions src/main/java/org/ohdsi/webapi/security/PermissionController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package org.ohdsi.webapi.security;

import org.ohdsi.webapi.security.dto.AccessRequestDTO;
import org.ohdsi.webapi.security.dto.RoleDTO;
import org.ohdsi.webapi.security.model.EntityType;
import org.ohdsi.webapi.shiro.Entities.RoleEntity;
import org.ohdsi.webapi.shiro.PermissionManager;
import org.springframework.core.convert.ConversionService;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

@Controller
@Path(value = "/permission")
@Transactional
public class PermissionController {

private final PermissionService permissionService;
private final PermissionManager permissionManager;
private final ConversionService conversionService;

public PermissionController(PermissionService permissionService, PermissionManager permissionManager, ConversionService conversionService) {

this.permissionService = permissionService;
this.permissionManager = permissionManager;
this.conversionService = conversionService;
}

@GET
@Path("/access/suggest")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public List<RoleDTO> listAccessesForEntity(@QueryParam("roleSearch") String roleSearch) {

List<RoleEntity> roles = permissionService.suggestRoles(roleSearch);
return roles.stream().map(re -> conversionService.convert(re, RoleDTO.class)).collect(Collectors.toList());
}

@GET
@Path("/access/{entityType}/{entityId}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public List<RoleDTO> listAccessesForEntity(
@PathParam("entityType") EntityType entityType,
@PathParam("entityId") Integer entityId
) throws Exception {

permissionService.checkCommonEntityOwnership(entityType, entityId);

Set<String> permissionTemplates = permissionService.getTemplatesForType(entityType, AccessType.WRITE).keySet();

List<String> permissions = permissionTemplates
.stream()
.map(pt -> permissionService.getPermission(pt, entityId))
.collect(Collectors.toList());

List<RoleEntity> roles = permissionService.finaAllRolesHavingPermissions(permissions);

return roles.stream().map(re -> conversionService.convert(re, RoleDTO.class)).collect(Collectors.toList());
}


/**
* Grant group of permissions (READ / WRITE / ...) for the specified entity to the given role.
* Only owner of the entity can do that.
*/
@POST
@Path("/access/{entityType}/{entityId}/role/{roleId}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public void grantEntityPermissionsForRole(
@PathParam("entityType") EntityType entityType,
@PathParam("entityId") Integer entityId,
@PathParam("roleId") Long roleId,
AccessRequestDTO accessRequestDTO
) throws Exception {

permissionService.checkCommonEntityOwnership(entityType, entityId);

Map<String, String> permissionTemplates = permissionService.getTemplatesForType(entityType, accessRequestDTO.getAccessType());

RoleEntity role = permissionManager.getRole(roleId);
permissionManager.addPermissionsFromTemplate(role, permissionTemplates, entityId.toString());
}

@DELETE
@Path("/access/{entityType}/{entityId}/role/{roleId}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public void revokeEntityPermissionsFromRole(
@PathParam("entityType") EntityType entityType,
@PathParam("entityId") Integer entityId,
@PathParam("roleId") Long roleId,
AccessRequestDTO accessRequestDTO
) throws Exception {

permissionService.checkCommonEntityOwnership(entityType, entityId);
Map<String, String> permissionTemplates = permissionService.getTemplatesForType(entityType, accessRequestDTO.getAccessType());
permissionService.removePermissionsFromRole(permissionTemplates, entityId, roleId);
}
}
134 changes: 134 additions & 0 deletions src/main/java/org/ohdsi/webapi/security/PermissionService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package org.ohdsi.webapi.security;

import org.apache.shiro.authz.UnauthorizedException;
import org.ohdsi.webapi.model.CommonEntity;
import org.ohdsi.webapi.security.model.EntityPermissionSchema;
import org.ohdsi.webapi.security.model.EntityPermissionSchemaResolver;
import org.ohdsi.webapi.security.model.EntityType;
import org.ohdsi.webapi.shiro.Entities.PermissionEntity;
import org.ohdsi.webapi.shiro.Entities.PermissionRepository;
import org.ohdsi.webapi.shiro.Entities.RoleEntity;
import org.ohdsi.webapi.shiro.Entities.RolePermissionEntity;
import org.ohdsi.webapi.shiro.Entities.RolePermissionRepository;
import org.ohdsi.webapi.shiro.Entities.RoleRepository;
import org.ohdsi.webapi.shiro.Entities.UserEntity;
import org.ohdsi.webapi.shiro.PermissionManager;
import org.springframework.aop.framework.Advised;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.support.Repositories;
import org.springframework.stereotype.Service;
import org.springframework.web.context.WebApplicationContext;

import javax.annotation.PostConstruct;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;

@Service
public class PermissionService {

private final WebApplicationContext appContext;
private final PermissionManager permissionManager;
private final EntityPermissionSchemaResolver entityPermissionSchemaResolver;
private final RoleRepository roleRepository;
private final PermissionRepository permissionRepository;
private final RolePermissionRepository rolePermissionRepository;
private final ConversionService conversionService;

private Repositories repositories;

public PermissionService(
WebApplicationContext appContext,
PermissionManager permissionManager,
EntityPermissionSchemaResolver entityPermissionSchemaResolver,
RoleRepository roleRepository,
PermissionRepository permissionRepository,
RolePermissionRepository rolePermissionRepository,
ConversionService conversionService
) {

this.appContext = appContext;
this.permissionManager = permissionManager;
this.entityPermissionSchemaResolver = entityPermissionSchemaResolver;
this.roleRepository = roleRepository;
this.permissionRepository = permissionRepository;
this.rolePermissionRepository = rolePermissionRepository;
this.conversionService = conversionService;
}

@PostConstruct
private void postConstruct() {

this.repositories = new Repositories(appContext);
}

public List<RoleEntity> suggestRoles(String roleSearch) {

return roleRepository.findByNameIgnoreCaseContaining(roleSearch);
}

public Map<String, String> getTemplatesForType(EntityType entityType, AccessType accessType) {

EntityPermissionSchema entityPermissionSchema = entityPermissionSchemaResolver.getForType(entityType);
return getPermissionTemplates(entityPermissionSchema, accessType);
}

public void checkCommonEntityOwnership(EntityType entityType, Integer entityId) throws Exception {

JpaRepository entityRepository = (JpaRepository) (((Advised) repositories.getRepositoryFor(entityType.getEntityClass())).getTargetSource().getTarget());
Class idClazz = Arrays.stream(entityType.getEntityClass().getMethods())
.filter(m -> m.getName().equals("getId"))
.findFirst()
.orElseThrow(() -> new RuntimeException("Cannot retrieve common entity"))
.getReturnType();
CommonEntity entity = (CommonEntity) entityRepository.getOne((Serializable) conversionService.convert(entityId, idClazz));

if (!isCurrentUserOwnerOf(entity)) {
throw new UnauthorizedException();
}
}

public Map<String, String> getPermissionTemplates(EntityPermissionSchema permissionSchema, AccessType accessType) {

switch (accessType) {
case WRITE:
return permissionSchema.getWritePermissions();
default:
throw new UnsupportedOperationException();
}
}

public List<RoleEntity> finaAllRolesHavingPermissions(List<String> permissions) {

return roleRepository.finaAllRolesHavingPermissions(permissions, (long) permissions.size());
}

public void removePermissionsFromRole(Map<String, String> permissionTemplates, Integer entityId, Long roleId) {

RoleEntity role = roleRepository.findById(roleId);
permissionTemplates.keySet()
.forEach(pt -> {
String permission = getPermission(pt, entityId);
PermissionEntity permissionEntity = permissionRepository.findByValueIgnoreCase(permission);
if (permissionEntity != null) {
RolePermissionEntity rp = rolePermissionRepository.findByRoleAndPermission(role, permissionEntity);
rolePermissionRepository.delete(rp.getId());
}
});
}

public String getPermission(String template, Object entityId) {

return String.format(template, entityId);
}

private boolean isCurrentUserOwnerOf(CommonEntity entity) {

UserEntity owner = entity.getCreatedBy();
String loggedInUsername = permissionManager.getSubjectName();
return Objects.equals(owner.getLogin(), loggedInUsername);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.ohdsi.webapi.security.converter;

import org.ohdsi.webapi.converter.BaseConversionServiceAwareConverter;
import org.ohdsi.webapi.security.dto.RoleDTO;
import org.ohdsi.webapi.shiro.Entities.RoleEntity;
import org.springframework.stereotype.Component;

@Component
public class RoleEntityToRoleDTOConverter extends BaseConversionServiceAwareConverter<RoleEntity, RoleDTO> {

@Override
public RoleDTO convert(RoleEntity roleEntity) {

return new RoleDTO(roleEntity.getId(), roleEntity.getName());
}
}
18 changes: 18 additions & 0 deletions src/main/java/org/ohdsi/webapi/security/dto/AccessRequestDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.ohdsi.webapi.security.dto;

import org.ohdsi.webapi.security.AccessType;

public class AccessRequestDTO {

private AccessType accessType;

public AccessType getAccessType() {

return accessType;
}

public void setAccessType(AccessType accessType) {

this.accessType = accessType;
}
}
37 changes: 37 additions & 0 deletions src/main/java/org/ohdsi/webapi/security/dto/RoleDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.ohdsi.webapi.security.dto;

public class RoleDTO {

private Long id;
private String name;

public RoleDTO() {

}

public RoleDTO(Long id, String name) {

this.id = id;
this.name = name;
}

public Long getId() {

return id;
}

public void setId(Long id) {

this.id = id;
}

public String getName() {

return name;
}

public void setName(String name) {

this.name = name;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.ohdsi.webapi.security.model;

import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Component
public class CohortCharacterizationPermissionSchema extends EntityPermissionSchema {

private static Map<String, String> writePermissions = new HashMap<String, String>() {{
put("cohort-characterization:%s:put", "Update Cohort Characterization with ID = %s");
put("cohort-characterization:%s:delete", "Delete Cohort Characterization with ID = %s");
}};

public CohortCharacterizationPermissionSchema() {

super(EntityType.COHORT_CHARACTERIZATION, new HashMap<>(), writePermissions);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.ohdsi.webapi.security.model;

import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Component
public class CohortDefinitionPermissionSchema extends EntityPermissionSchema {

private static Map<String, String> writePermissions = new HashMap<String, String>() {{
put("cohortdefinition:%s:put", "Update Cohort Definition with ID = %s");
put("cohortdefinition:%s:delete", "Delete Cohort Definition with ID = %s");
put("cohortdefinition:%s:check:post", "Fix Cohort Definition with ID = %s");
}};

public CohortDefinitionPermissionSchema() {

super(EntityType.COHORT_DEFINITION, new HashMap<>(), writePermissions);
}
}
Loading

0 comments on commit 93f0299

Please sign in to comment.