From ce9c37617d8f519c7a67b4d4ac54d455601c81e5 Mon Sep 17 00:00:00 2001 From: Gcolon021 <34667267+Gcolon021@users.noreply.github.com> Date: Tue, 28 May 2024 09:17:31 -0400 Subject: [PATCH] [ALS-6235] Investigate: BDC Auth login issue: Explore delays creating the user roles (#173) [ALS-6235] Investigate: BDC Auth login issue: Explore delays creating the user roles FenceAuthorization service has been refactored to improve readability and modularity. Fence mapping utility now loads the fence mapping json data on application startup and initializes two maps used by the application. This reduced execution time from ~13.25 minutes to ~9 minutes. AccessRules now uses a cache when creating new AccessRules. This reduces the time spent querying the database as many access rules are reused by privileges. This change reduced execution time from ~9 minutes down to 2.25 - 2.5 minutes. Refactor Role creation to allow for parallelization. This reduced execution time from 2.25 - 2.5 minutes down to ~30seconds. Roles are no longer removed from the user on login. We now update the user roles in place. --- .../auth/data/repository/RoleRepository.java | 11 + .../auth/rest/StudyAccessService.java | 6 +- .../auth/FENCEAuthenticationService.java | 1317 ++++++++--------- .../auth/utils/FenceMappingUtility.java | 88 ++ .../dbmi/avillach/StudyAccessServiceTest.java | 8 +- 5 files changed, 748 insertions(+), 682 deletions(-) create mode 100644 pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/utils/FenceMappingUtility.java diff --git a/pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/data/repository/RoleRepository.java b/pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/data/repository/RoleRepository.java index 63aa7f92d..7e87c6618 100644 --- a/pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/data/repository/RoleRepository.java +++ b/pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/data/repository/RoleRepository.java @@ -5,6 +5,7 @@ import javax.enterprise.context.ApplicationScoped; import javax.transaction.Transactional; +import java.util.List; import java.util.UUID; /** @@ -18,4 +19,14 @@ public class RoleRepository extends BaseRepository { protected RoleRepository() { super(Role.class); } + + public void persistAll(List newRoles) { + for (Role newRole : newRoles) { + if (newRole.getUuid() == null) { + em.persist(newRole); + } else { + em.merge(newRole); + } + } + } } diff --git a/pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/rest/StudyAccessService.java b/pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/rest/StudyAccessService.java index 3fe0db33b..efd963254 100644 --- a/pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/rest/StudyAccessService.java +++ b/pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/rest/StudyAccessService.java @@ -3,6 +3,7 @@ import edu.harvard.hms.dbmi.avillach.auth.data.entity.Role; import edu.harvard.hms.dbmi.avillach.auth.data.repository.RoleRepository; import edu.harvard.hms.dbmi.avillach.auth.service.auth.FENCEAuthenticationService; +import edu.harvard.hms.dbmi.avillach.auth.utils.FenceMappingUtility; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; @@ -37,6 +38,9 @@ public class StudyAccessService { public static final String STUDY_IDENTIFIER = "study_identifier"; public static final String CONSENT_GROUP_CODE = "consent_group_code"; + @Inject + private FenceMappingUtility fenceUtilityMapping; + @Inject FENCEAuthenticationService fenceAuthenticationService; @@ -55,7 +59,7 @@ public Response addStudyAccess(@ApiParam(value="The Study Identifier of the new Map fenceMappingForStudy = null; try { - Map fenceMapping = fenceAuthenticationService.getFENCEMapping(); + Map fenceMapping = this.fenceUtilityMapping.getFENCEMapping(); if (fenceMapping == null) { throw new Exception("Fence mapping is null"); } diff --git a/pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/service/auth/FENCEAuthenticationService.java b/pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/service/auth/FENCEAuthenticationService.java index 6fb59d38e..926196572 100644 --- a/pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/service/auth/FENCEAuthenticationService.java +++ b/pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/service/auth/FENCEAuthenticationService.java @@ -6,8 +6,8 @@ import static edu.harvard.hms.dbmi.avillach.auth.JAXRSConfiguration.fence_standard_access_rules; import static edu.harvard.hms.dbmi.avillach.auth.JAXRSConfiguration.fence_topmed_consent_group_concept_path; -import java.io.File; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.node.ObjectNode; +import edu.harvard.hms.dbmi.avillach.auth.utils.FenceMappingUtility; import org.apache.commons.lang3.StringUtils; import org.apache.http.Header; import org.apache.http.entity.StringEntity; @@ -39,7 +40,7 @@ import org.springframework.util.CollectionUtils; public class FENCEAuthenticationService { - private Logger logger = LoggerFactory.getLogger(FENCEAuthenticationService.class); + private final Logger logger = LoggerFactory.getLogger(FENCEAuthenticationService.class); @Inject UserRepository userRepo; @@ -67,17 +68,14 @@ public class FENCEAuthenticationService { private Application picSureApp; private Connection fenceConnection; - - //read the fence_mapping.json into this object to improve lookup speeds - private static Map _projectMap; - + private static final String parentAccessionField = "\\\\_Parent Study Accession with Subject ID\\\\"; private static final String topmedAccessionField = "\\\\_Topmed Study Accession with Subject ID\\\\"; public static final String fence_open_access_role_name = "FENCE_ROLE_OPEN_ACCESS"; private final Set openAccessIdpValues = Set.of("fence", "ras"); - + private static final String[] underscoreFields = new String[] { parentAccessionField, topmedAccessionField, @@ -91,6 +89,12 @@ public class FENCEAuthenticationService { "\\\\_Consents\\\\" ///old _Consents\Short Study... path no longer used, but still present in examples. }; + @Inject + private FenceMappingUtility fenceMappingUtility; + + private final ConcurrentHashMap accessRuleCache = new ConcurrentHashMap<>(); + private Set allowQueryTypeRules; + @PostConstruct public void initializeFenceService() { picSureApp = applicationRepo.getUniqueResultByColumn("name", "PICSURE"); @@ -102,7 +106,7 @@ private JsonNode getFENCEUserProfile(String access_token) { List
headers = new ArrayList<>(); headers.add(new BasicHeader("Authorization", "Bearer " + access_token)); - logger.debug("getFENCEUserProfile() getting user profile from uri:"+JAXRSConfiguration.idp_provider_uri+"/user/user"); + logger.debug("getFENCEUserProfile() getting user profile from uri:{}/user/user", JAXRSConfiguration.idp_provider_uri); JsonNode fence_user_profile_response = HttpClientUtil.simpleGet( JAXRSConfiguration.idp_provider_uri+"/user/user", JAXRSConfiguration.client, @@ -164,20 +168,20 @@ public Response getFENCEProfile(String callback_url, Map authReq try { logger.debug("getFENCEProfile() query FENCE for user profile with code"); fence_user_profile = getFENCEUserProfile(getFENCEAccessToken(callback_url, fence_code).get("access_token").asText()); - + if(logger.isTraceEnabled()){ // create object mapper instance ObjectMapper mapper = new ObjectMapper(); // `JsonNode` to JSON string String prettyString = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(fence_user_profile); - - logger.trace("getFENCEProfile() user profile structure:"+ prettyString); + + logger.trace("getFENCEProfile() user profile structure:{}", prettyString); } - logger.debug("getFENCEProfile() .username:" + fence_user_profile.get("username")); - logger.debug("getFENCEProfile() .user_id:" + fence_user_profile.get("user_id")); - logger.debug("getFENCEProfile() .email:" + fence_user_profile.get("email")); + logger.debug("getFENCEProfile() .username:{}", fence_user_profile.get("username")); + logger.debug("getFENCEProfile() .user_id:{}", fence_user_profile.get("user_id")); + logger.debug("getFENCEProfile() .email:{}", fence_user_profile.get("email")); } catch (Exception ex) { - logger.error("getFENCEProfile() could not retrieve the user profile from the auth provider, because "+ex.getMessage(), ex); + logger.error("getFENCEProfile() could not retrieve the user profile from the auth provider, because {}", ex.getMessage(), ex); throw new NotAuthorizedException("Could not get the user profile "+ "from the Gen3 authentication provider."+ex.getMessage()); } @@ -187,11 +191,8 @@ public Response getFENCEProfile(String callback_url, Map authReq // Create or retrieve the user profile from our database, based on the the key // in the Gen3/FENCE profile current_user = createUserFromFENCEProfile(fence_user_profile); - logger.info("getFENCEProfile() saved details for user with e-mail:" - +current_user.getEmail() - +" and subject:" - +current_user.getSubject()); - + logger.info("getFENCEProfile() saved details for user with e-mail:{} and subject:{}", current_user.getEmail(), current_user.getSubject()); + //clear some cache entries if we register a new login AuthorizationService.clearCache(current_user); UserService.clearCache(current_user); @@ -201,17 +202,58 @@ public Response getFENCEProfile(String callback_url, Map authReq throw new NotAuthorizedException("The user details could not be persisted. Please contact the administrator."); } + if (fence_harmonized_concept_path != null && !fence_harmonized_concept_path.contains("\\\\")) { + fence_harmonized_concept_path = fence_harmonized_concept_path.replaceAll("\\\\", "\\\\\\\\"); + logger.debug("Escaped harmonized consent path: {}", fence_harmonized_concept_path); + } + // Update the user's roles (or create them if none exists) //Set actual_user_roles = u.getRoles(); Iterator project_access_names = fence_user_profile.get("authz").fieldNames(); - while (project_access_names.hasNext()) { - String access_role_name = project_access_names.next(); - createAndUpsertRole(access_role_name, current_user); + + // I want to parallelize this, but I'm not sure if it's safe to do so. + Set roleNames = new HashSet<>(); + project_access_names.forEachRemaining(roleName -> { + // We need to add/remove the users roles based on what is in the project_access_names list + Map projectMetadata = this.fenceMappingUtility.getFenceMappingByAuthZ().get(roleName); + + if (projectMetadata == null) { + logger.error("getFENCEProfile() -> createAndUpsertRole could not find study in FENCE mapping SKIPPING: {}", roleName); + return; + } + + String projectId = (String) projectMetadata.get("study_identifier"); + String consentCode = (String) projectMetadata.get("consent_group_code"); + String newRoleName = StringUtils.isNotBlank(consentCode) ? "FENCE_"+projectId+"_"+consentCode : "FENCE_"+projectId; + + roleNames.add(newRoleName); + }); + + // find roles that are in the user's roles but not in the project_access_names. These are the roles that need to be removed. + // exclude userRole -> "PIC-SURE Top Admin".equals(userRole.getName()) || "Admin".equals(userRole.getName()) || userRole.getName().startsWith("MANUAL_") + Set rolesToRemove = current_user.getRoles().parallelStream() + .filter(role -> !roleNames.contains(role.getName()) && !role.getName().equals(fence_open_access_role_name) && !role.getName().startsWith("MANUAL_") && !role.getName().equals("PIC-SURE Top Admin") && !role.getName().equals("Admin")) + .collect(Collectors.toSet()); + + if (!rolesToRemove.isEmpty()) { + current_user.getRoles().removeAll(rolesToRemove); + logger.debug("upsertRole() removed {} roles from user", rolesToRemove.size()); + logger.debug("User roles after removal: {}", current_user.getRoles().size()); } - final String idp = extractIdp(current_user); + // find roles that are in the project_access_names but not in the user's roles. These are the roles that need to be added. + List newRoles = roleNames.parallelStream() + .map(roleName -> createRole(roleName, "FENCE role " + roleName)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); - if (current_user.getRoles() != null && (current_user.getRoles().size() > 0 || openAccessIdpValues.contains(idp))) { + if (!newRoles.isEmpty()) { + roleRepo.persistAll(newRoles); + current_user.getRoles().addAll(newRoles); + } + + final String idp = extractIdp(current_user); + if (current_user.getRoles() != null && (!current_user.getRoles().isEmpty() || openAccessIdpValues.contains(idp))) { Role openAccessRole = roleRepo.getUniqueResultByColumn("name", fence_open_access_role_name); if (openAccessRole != null) { current_user.getRoles().add(openAccessRole); @@ -219,62 +261,63 @@ public Response getFENCEProfile(String callback_url, Map authReq logger.warn("Unable to find fence OPEN ACCESS role"); } } - - + + try { userRepo.changeRole(current_user, current_user.getRoles()); - logger.debug("upsertRole() updated user, who now has "+current_user.getRoles().size()+" roles."); + logger.debug("upsertRole() updated user, who now has {} roles.", current_user.getRoles().size()); } catch (Exception ex) { - logger.error("upsertRole() Could not add roles to user, because "+ex.getMessage()); + logger.error("upsertRole() Could not add roles to user, because {}", ex.getMessage()); } HashMap claims = new HashMap(); claims.put("name", fence_user_profile.get("name")); claims.put("email", current_user.getEmail()); claims.put("sub", current_user.getSubject()); HashMap responseMap = authUtil.getUserProfileResponse(claims); - logger.info("LOGIN SUCCESS ___ " + current_user.getEmail() + ":" + current_user.getUuid().toString() + ":" + current_user.getSubject() + " ___ Authorization will expire at ___ " + responseMap.get("expirationDate") + "___"); + logger.info("LOGIN SUCCESS ___ {}:{}:{} ___ Authorization will expire at ___ {}___", current_user.getEmail(), current_user.getUuid().toString(), current_user.getSubject(), responseMap.get("expirationDate")); logger.debug("getFENCEProfile() UserProfile response object has been generated"); logger.debug("getFENCEToken() finished"); + return PICSUREResponse.success(responseMap); } - private void createAndUpsertRole(String access_role_name, User current_user) { - logger.debug("createAndUpsertRole() starting..."); - Map projectMetadata = getFENCEMapping().values().stream() - .filter(map -> access_role_name.equals( - map.get("authZ").toString().replace("\\/", "/")) - ).findFirst().orElse(null); - - if (projectMetadata == null) { - logger.error("getFENCEProfile() -> createAndUpsertRole could not find study in FENCE mapping SKIPPING: " + access_role_name); - return; + private Role createRole(String roleName, String roleDescription) { + if (roleName.isEmpty()) { + logger.error("createRole() roleName is empty"); + return null; } - - String projectId = (String) projectMetadata.get("study_identifier"); - String consentCode = (String) projectMetadata.get("consent_group_code"); - String newRoleName = StringUtils.isNotBlank(consentCode) ? "FENCE_"+projectId+"_"+consentCode : "FENCE_"+projectId; - - logger.info("getFENCEProfile() New PSAMA role name:"+newRoleName); - - if (upsertRole(current_user, newRoleName, "FENCE role "+newRoleName)) { - logger.info("getFENCEProfile() Updated user role. Now it includes `"+newRoleName+"`"); + logger.info("getFENCEProfile() New PSAMA role name:{}", roleName); + Role r; + // Create the Role in the repository, if it does not exist. Otherwise, add it. + Role existing_role = roleRepo.getUniqueResultByColumn("name", roleName); + if (existing_role != null) { + // Role already exists + logger.info("upsertRole() role already exists"); + r = existing_role; } else { - logger.error("getFENCEProfile() could not add roles to user's profile"); + // This is a new Role + r = new Role(); + r.setName(roleName); + r.setDescription(roleDescription); + // Since this is a new Role, we need to ensure that the + // corresponding Privilege (with gates) and AccessRule is added. + r.setPrivileges(addFENCEPrivileges(r)); + logger.info("upsertRole() created new role"); } + + return r; } - private String extractIdp(User current_user) { + private static String extractIdp(User current_user) { try { final ObjectNode node; node = new ObjectMapper().readValue(current_user.getGeneralMetadata(), ObjectNode.class); return node.get("idp").asText(); } catch (JsonProcessingException e) { - logger.warn("Error parsing idp value from medatada", e); return ""; } } - /** * Create or update a user record, based on the FENCE user profile, which is in JSON format. * @@ -286,7 +329,7 @@ private User createUserFromFENCEProfile(JsonNode node) { User new_user = new User(); new_user.setSubject("fence|"+node.get("user_id").asText()); - // This is not always an email address, but it is the only attribute other than the sub claim + // This is not always an email address, but it is the only attribute other than the sub claim // that is guaranteed to be populated by Fence and which makes sense as a display name for a // user. new_user.setEmail(node.get("username").asText()); @@ -297,20 +340,16 @@ private User createUserFromFENCEProfile(JsonNode node) { logger.debug("createUserFromFENCEProfile() finished setting fields"); User actual_user = userRepo.findOrCreate(new_user); - - Set roles = new HashSet<>(); - if (actual_user != null && !CollectionUtils.isEmpty(actual_user.getRoles())) { - roles = actual_user.getRoles().stream() - .filter(userRole -> "PIC-SURE Top Admin".equals(userRole.getName()) || "Admin".equals(userRole.getName()) || userRole.getName().startsWith("MANUAL_")) - .collect(Collectors.toSet()); + if (actual_user.getRoles() == null) { + actual_user.setRoles(new HashSet<>()); } - // Clear current set of roles every time we create or retrieve a user but persist admin status - actual_user.setRoles(roles); +// .filter(userRole -> "PIC-SURE Top Admin".equals(userRole.getName()) || "Admin".equals(userRole.getName()) || userRole.getName().startsWith("MANUAL_")) +// .collect(Collectors.toSet()); logger.debug("createUserFromFENCEProfile() cleared roles"); - userRepo.persist(actual_user); +// userRepo.persist(actual_user); logger.debug("createUserFromFENCEProfile() finished, user record inserted"); return actual_user; } @@ -344,7 +383,7 @@ public boolean upsertRole(User u, String roleName, String roleDescription) { r.setDescription(roleDescription); // Since this is a new Role, we need to ensure that the // corresponding Privilege (with gates) and AccessRule is added. - r.setPrivileges(addFENCEPrivileges(u, r)); + r.setPrivileges(addFENCEPrivileges(r)); roleRepo.persist(r); logger.info("upsertRole() created new role"); } @@ -353,7 +392,7 @@ public boolean upsertRole(User u, String roleName, String roleDescription) { } status = true; } catch (Exception ex) { - logger.error("upsertRole() Could not inser/update role "+roleName+" to repo", ex); + logger.error("upsertRole() Could not inser/update role {} to repo", roleName, ex); } @@ -361,43 +400,43 @@ public boolean upsertRole(User u, String roleName, String roleDescription) { return status; } - private Set addFENCEPrivileges(User u, Role r) { + private Set addFENCEPrivileges(Role r) { String roleName = r.getName(); - logger.info("addFENCEPrivileges() starting, adding privilege(s) to role "+roleName); + logger.info("addFENCEPrivileges() starting, adding privilege(s) to role {}", roleName); //each project can have up to three privileges: Parent | Harmonized | Topmed //harmonized has 2 ARs for parent + harminized and harmonized only - //Topmed has up to three ARs for topmed / topmed + parent / topmed + harmonized + //Topmed has up to three ARs for topmed / topmed + parent / topmed + harmonized Set privs = r.getPrivileges(); if (privs == null) { privs = new HashSet();} //e.g. FENCE_phs0000xx_c2 or FENCE_tutorial-biolinc_camp String project_name = extractProject(roleName); if (project_name.length() <= 0) { - logger.warn("addFENCEPrivileges() role name: "+roleName+" returned an empty project name"); + logger.warn("addFENCEPrivileges() role name: {} returned an empty project name", roleName); } String consent_group = extractConsentGroup(roleName); if (consent_group.length() <= 0) { - logger.warn("addFENCEPrivileges() role name: "+roleName+" returned an empty consent group"); + logger.warn("addFENCEPrivileges() role name: {} returned an empty consent group", roleName); } - logger.info("addFENCEPrivileges() project name: "+project_name+" consent group: "+consent_group); + logger.info("addFENCEPrivileges() project name: {} consent group: {}", project_name, consent_group); // Look up the metadata by consent group. Map projectMetadata = getFENCEMappingforProjectAndConsent(project_name, consent_group); - - if(projectMetadata == null || projectMetadata.size() == 0) { + + if(projectMetadata == null || projectMetadata.isEmpty()) { //no privileges means no access to this project. just return existing set of privs. - logger.warn("No metadata available for project " + project_name + "." + consent_group); + logger.warn("No metadata available for project {}.{}", project_name, consent_group); return privs; } - + logger.info("addPrivileges() This is a new privilege"); - + String dataType = (String) projectMetadata.get("data_type"); Boolean isHarmonized = "Y".equals(projectMetadata.get("is_harmonized")); String concept_path = (String) projectMetadata.get("top_level_path"); String projectAlias = (String) projectMetadata.get("abbreviated_name"); - + //we need to add escape sequence back in to the path for parsing later (also need to double escape the regex) // // OK... so, we need to do this for the query Template and scopes, but should NOT do this for the rules. @@ -412,353 +451,313 @@ private Set addFENCEPrivileges(User u, Role r) { //insert genomic/topmed privs - this will also add rules for including harmonized & parent data if applicable privs.add(upsertTopmedPrivilege(project_name, projectAlias, consent_group, concept_path, isHarmonized)); } - + if(dataType != null && dataType.contains("P")) { //insert clinical privs - logger.info("addPrivileges() project:"+project_name+" consent_group:"+consent_group+" concept_path:"+concept_path); + logger.info("addPrivileges() project:{} consent_group:{} concept_path:{}", project_name, consent_group, concept_path); privs.add(upsertClinicalPrivilege(project_name, projectAlias, consent_group, concept_path, false)); - + //if harmonized study, also create harmonized privileges if(Boolean.TRUE.equals(isHarmonized)) { privs.add(upsertClinicalPrivilege(project_name, projectAlias, consent_group, concept_path, true)); } } - + //projects without G or P in data_type are skipped if(dataType == null || (!dataType.contains("P") && !dataType.contains("G"))){ - logger.warn("Missing study type for " + project_name + " " + consent_group + ". Skipping."); + logger.warn("Missing study type for {} {}. Skipping.", project_name, consent_group); } - + logger.info("addPrivileges() Finished"); return privs; } - private static String extractProject(String roleName) { - String projectPattern = "FENCE_(.*?)(?:_c\\d+)?$"; - if (roleName.startsWith("MANUAL_")) { - projectPattern = "MANUAL_(.*?)(?:_c\\d+)?$"; - } - Pattern projectRegex = Pattern.compile(projectPattern); - Matcher projectMatcher = projectRegex.matcher(roleName); - String project = ""; - if (projectMatcher.find()) { - project = projectMatcher.group(1).trim(); - } else { - String[] parts = roleName.split("_", 1); - if (parts.length > 0) { - project = parts[1]; - } - } - return project; - } - - private static String extractConsentGroup(String roleName) { - String consentPattern = "FENCE_.*?_c(\\d+)$"; - if (roleName.startsWith("MANUAL_")) { - consentPattern = "MANUAL_.*?_c(\\d+)$"; - } - Pattern consentRegex = Pattern.compile(consentPattern); - Matcher consentMatcher = consentRegex.matcher(roleName); - String consentGroup = ""; - if (consentMatcher.find()) { - consentGroup = "c" + consentMatcher.group(1).trim(); - } - return consentGroup; - } - /** - * Creates a privilege with a set of access rules that allow queries containing a consent group to pass if the query only contains valid entries that match conceptPath. If the study is harmonized, + * Creates a privilege with a set of access rules that allow queries containing a consent group to pass if the query only contains valid entries that match conceptPath. If the study is harmonized, * this also creates an access rule to allow access when using the harmonized consent concept path. - * * Privileges created with this method will deny access if any genomic filters (topmed data) are included. - * - * @param studyIdentifier - * @param consent_group - * @param conceptPath - * @param isHarmonized - * @return + * + * @param studyIdentifier The study identifier + * @param consent_group The consent group + * @param conceptPath The concept path + * @param isHarmonized Whether the study is harmonized + * @return The created privilege */ private Privilege upsertClinicalPrivilege(String studyIdentifier, String projectAlias, String consent_group, String conceptPath, boolean isHarmonized) { - - String privilegeName = (consent_group != null && consent_group != "") ? "PRIV_FENCE_"+studyIdentifier+"_"+consent_group+(isHarmonized?"_HARMONIZED":"") : "PRIV_FENCE_"+studyIdentifier+(isHarmonized?"_HARMONIZED":"") ; + // Construct the privilege name + String privilegeName = (consent_group != null && !consent_group.isEmpty()) ? + "PRIV_FENCE_" + studyIdentifier + "_" + consent_group + (isHarmonized ? "_HARMONIZED" : "") : + "PRIV_FENCE_" + studyIdentifier + (isHarmonized ? "_HARMONIZED" : ""); + + // Check if the Privilege already exists Privilege priv = privilegeRepo.getUniqueResultByColumn("name", privilegeName); - if(priv != null) { - logger.info("upsertClinicalPrivilege() " + privilegeName + " already exists"); - return priv; - } - - priv = new Privilege(); + if (priv != null) { + logger.info("{} already exists", privilegeName); + return priv; + } + priv = new Privilege(); try { priv.setApplication(picSureApp); priv.setName(privilegeName); + // Set consent concept path String consent_concept_path = isHarmonized ? fence_harmonized_consent_group_concept_path : fence_parent_consent_group_concept_path; - - if(!consent_concept_path.contains("\\\\")){ - //these have to be escaped again so that jaxson can convert it correctly - consent_concept_path = consent_concept_path.replaceAll("\\\\", "\\\\\\\\"); - logger.debug(consent_concept_path); + if (!consent_concept_path.contains("\\\\")) { + consent_concept_path = consent_concept_path.replaceAll("\\\\", "\\\\\\\\"); + logger.debug("Escaped consent concept path: {}", consent_concept_path); } - if(fence_harmonized_concept_path != null && !fence_harmonized_concept_path.contains("\\\\")){ - //these have to be escaped again so that jaxson can convert it correctly - fence_harmonized_concept_path = fence_harmonized_concept_path.replaceAll("\\\\", "\\\\\\\\"); - logger.debug("upsertTopmedPrivilege(): escaped harmonized consent path" + fence_harmonized_concept_path); - } - - - // TOOD: Change this to a mustache template - String studyIdentifierField = (consent_group != null && consent_group != "") ? studyIdentifier+"."+consent_group: studyIdentifier; - String queryTemplateText = "{\"categoryFilters\": {\"" - +consent_concept_path - +"\":[\"" - +studyIdentifierField - +"\"]}," - +"\"numericFilters\":{},\"requiredFields\":[],"; - - if("fence".equalsIgnoreCase(JAXRSConfiguration.idp_provider)) { - queryTemplateText += "\"fields\":[\"" + parentAccessionField + "\"],"; - } else { - queryTemplateText += "\"fields\":[],"; - } - queryTemplateText+="\"variantInfoFilters\":[{\"categoryVariantInfoFilters\":{},\"numericVariantInfoFilters\":{}}]," - +"\"expectedResultType\": \"COUNT\"" - +"}"; + + String studyIdentifierField = (consent_group != null && !consent_group.isEmpty()) ? studyIdentifier + "." + consent_group : studyIdentifier; + String queryTemplateText = String.format( + "{\"categoryFilters\": {\"%s\":[\"%s\"]},\"numericFilters\":{},\"requiredFields\":[],\"fields\":[],\"variantInfoFilters\":[{\"categoryVariantInfoFilters\":{},\"numericVariantInfoFilters\":{}}],\"expectedResultType\": \"COUNT\"}", + consent_concept_path, studyIdentifierField + ); priv.setQueryTemplate(queryTemplateText); - if(isHarmonized) { - priv.setQueryScope("[\"" + conceptPath + "\",\"_\",\"" + fence_harmonized_concept_path + "\"]"); - } else { - priv.setQueryScope("[\"" + conceptPath + "\",\"_\"]"); - } + priv.setQueryScope(isHarmonized ? String.format("[\"%s\",\"_\",\"%s\"]", conceptPath, fence_harmonized_concept_path) : String.format("[\"%s\",\"_\"]", conceptPath)); + + // Initialize the set of AccessRules + Set accessrules = new HashSet<>(); - Set accessrules = new HashSet(); - - //just need one AR for parent study + // Create and add the parent consent access rule AccessRule ar = createConsentAccessRule(studyIdentifier, consent_group, "PARENT", fence_parent_consent_group_concept_path); - - //if this is a new rule, we need to populate it - if(ar.getGates() == null) { - ar.setGates(new HashSet()); - ar.getGates().addAll(getGates(true, false, false)); - - if(ar.getSubAccessRule() == null) { - ar.setSubAccessRule(new HashSet()); - } - ar.getSubAccessRule().addAll(getAllowedQueryTypeRules()); - ar.getSubAccessRule().addAll(getPhenotypeSubRules(studyIdentifier, conceptPath, projectAlias)); - ar.getSubAccessRule().addAll(getTopmedRestrictedSubRules()); - accessruleRepo.merge(ar); - } - + configureAccessRule(ar, studyIdentifier, consent_group, conceptPath, projectAlias, true, false, false); accessrules.add(ar); - - // here we add a rule to allow querying a parent study if genomic filters are included. This goes on all studies; - // if the study has no genomic data or the user is not authorizzed for genomic studies, there will be 0 patients returned. - ar = upsertTopmedAccessRule(studyIdentifier, consent_group, "TOPMED+PARENT"); - - //if this is a new rule, we need to populate it - if(ar.getGates() == null) { - ar.setGates(new HashSet()); - ar.getGates().addAll(getGates(true, false, true)); - if(ar.getSubAccessRule() == null) { - ar.setSubAccessRule(new HashSet()); - } - ar.getSubAccessRule().addAll(getAllowedQueryTypeRules()); - ar.getSubAccessRule().addAll(getPhenotypeSubRules(studyIdentifier, conceptPath, projectAlias)); - //this is added in the 'getPhenotypeRestrictedSubRules()' which is not called in this path - ar.getSubAccessRule().add(createPhenotypeSubRule(fence_topmed_consent_group_concept_path, "ALLOW_TOPMED_CONSENT", "$.query.query.categoryFilters", AccessRule.TypeNaming.ALL_CONTAINS, "", true)); - - accessruleRepo.merge(ar); - } + + // Create and add the Topmed+Parent access rule + ar = upsertTopmedAccessRule(studyIdentifier, consent_group, "TOPMED+PARENT"); + configureAccessRule(ar, studyIdentifier, consent_group, conceptPath, projectAlias, true, false, true); accessrules.add(ar); - - if(isHarmonized) { - //add a rule for accessing only harmonized data - ar = createConsentAccessRule(studyIdentifier, consent_group, "HARMONIZED", fence_harmonized_consent_group_concept_path); - - //if this is a new rule, we need to populate it - if(ar.getGates() == null) { - ar.setGates(new HashSet()); - ar.getGates().add(upsertConsentGate("HARMONIZED_CONSENT", "$.query.query.categoryFilters." + fence_harmonized_consent_group_concept_path + "[*]", true, "harmonized data")); - - if(ar.getSubAccessRule() == null) { - ar.setSubAccessRule(new HashSet()); - } - ar.getSubAccessRule().addAll(getAllowedQueryTypeRules()); - ar.getSubAccessRule().addAll(getHarmonizedSubRules()); - ar.getSubAccessRule().addAll(getPhenotypeSubRules(studyIdentifier, conceptPath, projectAlias)); - accessruleRepo.merge(ar); - } - accessrules.add(ar); - } - - // Add additional access rules; (these are still created through that SQL script) - for(String arName: fence_standard_access_rules.split(",")) { - if (arName.startsWith("AR_")) { - logger.info("upsertClinicalPrivilege() Adding AccessRule "+arName+" to privilege "+priv.getName()); - ar = accessruleRepo.getUniqueResultByColumn("name",arName); - if(ar != null) { - accessrules.add(ar); - } - else { - logger.warn("upsertClinicalPrivilege() unable to find an access rule with name " + arName); - } - } + + // If harmonized, create and add the harmonized access rule + if (isHarmonized) { + ar = createConsentAccessRule(studyIdentifier, consent_group, "HARMONIZED", fence_harmonized_consent_group_concept_path); + configureHarmonizedAccessRule(ar, studyIdentifier, consent_group, conceptPath, projectAlias); + accessrules.add(ar); } + + // Add standard access rules + addStandardAccessRules(accessrules); + priv.setAccessRules(accessrules); - logger.info("createNewPrivilege() Added "+accessrules.size()+" access_rules to privilege"); + logger.info("Added {} access_rules to privilege", accessrules.size()); privilegeRepo.persist(priv); - logger.info("createNewPrivilege() Added new privilege "+priv.getName()+" to DB"); + logger.info("Added new privilege {} to DB", priv.getName()); } catch (Exception ex) { - ex.printStackTrace(); - logger.error("createNewPrivilege() could not save privilege"); + logger.error("Could not save privilege", ex); } return priv; } - - private Set getAllowedQueryTypeRules() { - Set rules = new HashSet(); - String[] allowedTypes = JAXRSConfiguration.fence_allowed_query_types.split(","); - for(String queryType : allowedTypes) { - - String ar_name = "AR_ALLOW_" + queryType ; - - AccessRule ar = accessruleRepo.getUniqueResultByColumn("name", ar_name); - if(ar != null) { - logger.debug("createTopmedRestrictedSubRule() Found existing rule: " + ar.getName()); - rules.add(ar); - continue; + + /** + * Configures the AccessRule with gates and sub-rules. + * + * @param ar The AccessRule to configure. + * @param studyIdentifier The study identifier. + * @param consent_group The consent group. + * @param conceptPath The concept path. + * @param projectAlias The project alias. + * @param parent Whether to include parent gates. + * @param harmonized Whether to include harmonized gates. + * @param topmed Whether to include Topmed gates. + */ + private void configureAccessRule(AccessRule ar, String studyIdentifier, String consent_group, String conceptPath, String projectAlias, boolean parent, boolean harmonized, boolean topmed) { + if (ar.getGates() == null) { + ar.setGates(new HashSet<>()); + ar.getGates().addAll(getGates(parent, harmonized, topmed)); + + if (ar.getSubAccessRule() == null) { + ar.setSubAccessRule(new HashSet<>()); } - - logger.info("upsertPhenotypeSubRule() Creating new access rule "+ar_name); - ar = new AccessRule(); - ar.setName(ar_name); - ar.setDescription("FENCE SUB AR to allow " + queryType + " Queries"); - ar.setRule( "$.query.query.expectedResultType"); - ar.setType(AccessRule.TypeNaming.ALL_EQUALS); - ar.setValue(queryType); - ar.setCheckMapKeyOnly(false); - ar.setCheckMapNode(false); - ar.setEvaluateOnlyByGates(false); - ar.setGateAnyRelation(false); - - accessruleRepo.persist(ar); - rules.add(ar); - - } - return rules; - } + ar.getSubAccessRule().addAll(getAllowedQueryTypeRules()); + ar.getSubAccessRule().addAll(getPhenotypeSubRules(studyIdentifier, conceptPath, projectAlias)); + ar.getSubAccessRule().addAll(getTopmedRestrictedSubRules()); + accessruleRepo.merge(ar); + } + } - private Collection getTopmedRestrictedSubRules() { + /** + * Configures the harmonized AccessRule with gates and sub-rules. + * + * @param ar The AccessRule to configure. + * @param studyIdentifier The study identifier. + * @param consent_group The consent group. + * @param conceptPath The concept path. + * @param projectAlias The project alias. + */ + private void configureHarmonizedAccessRule(AccessRule ar, String studyIdentifier, String consent_group, String conceptPath, String projectAlias) { + if (ar.getGates() == null) { + ar.setGates(new HashSet<>()); + ar.getGates().add(upsertConsentGate("HARMONIZED_CONSENT", "$.query.query.categoryFilters." + fence_harmonized_consent_group_concept_path + "[*]", true, "harmonized data")); + + if (ar.getSubAccessRule() == null) { + ar.setSubAccessRule(new HashSet<>()); + } + ar.getSubAccessRule().addAll(getAllowedQueryTypeRules()); + ar.getSubAccessRule().addAll(getHarmonizedSubRules()); + ar.getSubAccessRule().addAll(getPhenotypeSubRules(studyIdentifier, conceptPath, projectAlias)); + accessruleRepo.merge(ar); + } + } + + private Set getAllowedQueryTypeRules() { + if (allowQueryTypeRules == null) { + allowQueryTypeRules = loadAllowedQueryTypeRules(); + } + + return allowQueryTypeRules; + } + + /** + * Retrieves or creates AccessRules for allowed query types. + * + * @return A set of AccessRules for allowed query types. + */ + private Set loadAllowedQueryTypeRules() { + // Initialize a set to hold the AccessRules + Set rules = new HashSet<>(); + // Split the allowed query types from the configuration + String[] allowedTypes = JAXRSConfiguration.fence_allowed_query_types.split(","); + + // Iterate over each allowed query type + for (String queryType : allowedTypes) { + // Construct the AccessRule name + String ar_name = "AR_ALLOW_" + queryType; + + // Log the creation of a new AccessRule + AccessRule ar = getOrCreateAccessRule( + ar_name, + "FENCE SUB AR to allow " + queryType + " Queries", + "$.query.query.expectedResultType", + AccessRule.TypeNaming.ALL_EQUALS, + queryType, + false, + false, + false, + false + ); + + // Add the newly created rule to the set + rules.add(ar); + } + // Return the set of AccessRules + return rules; + } + + private Collection getTopmedRestrictedSubRules() { Set rules = new HashSet(); rules.add(upsertTopmedRestrictedSubRule("CATEGORICAL", "$.query.query.variantInfoFilters[*].categoryVariantInfoFilters.*")); rules.add(upsertTopmedRestrictedSubRule("NUMERIC", "$.query.query.variantInfoFilters[*].numericVariantInfoFilters.*")); - + return rules; } - - //topmed restriction rules don't need much configuration. Just deny all access. + + /** + * Creates and returns a restricted sub-rule AccessRule for Topmed. + * topmed restriction rules don't need much configuration. Just deny all access. + * @param type The type of the Topmed restriction. + * @param rule The rule expression. + * @return The created AccessRule. + */ private AccessRule upsertTopmedRestrictedSubRule(String type, String rule) { + // Construct the AccessRule name String ar_name = "AR_TOPMED_RESTRICTED_" + type; - + // Check if the AccessRule already exists AccessRule ar = accessruleRepo.getUniqueResultByColumn("name", ar_name); - if(ar != null) { - logger.debug("createTopmedRestrictedSubRule() Found existing rule: " + ar.getName()); - return ar; + if (ar != null) { + // Log and return the existing rule + logger.debug("Found existing rule: {}", ar.getName()); + return ar; } - - logger.info("upsertTopmedRestrictedSubRule() Creating new access rule "+ar_name); - ar = new AccessRule(); - ar.setName(ar_name); - ar.setDescription("FENCE SUB AR for retricting " + type + " genomic concepts"); - ar.setRule(rule); - ar.setType(AccessRule.TypeNaming.IS_EMPTY); - ar.setValue(null); - ar.setCheckMapKeyOnly(false); - ar.setCheckMapNode(false); - ar.setEvaluateOnlyByGates(false); - ar.setGateAnyRelation(false); - - accessruleRepo.persist(ar); - - return ar; + + // Log the creation of a new AccessRule + // Create the AccessRule using the createAccessRule method + return getOrCreateAccessRule( + ar_name, + "FENCE SUB AR for restricting " + type + " genomic concepts", + rule, + AccessRule.TypeNaming.IS_EMPTY, + null, + false, + false, + false, + false + ); } - private Collection getPhenotypeSubRules(String studyIdentifier, String conceptPath, String alias) { - + private Collection getPhenotypeSubRules(String studyIdentifier, String conceptPath, String alias) { + Set rules = new HashSet(); //categorical filters will always contain at least one entry (for the consent groups); it will never be empty rules.add(createPhenotypeSubRule(fence_parent_consent_group_concept_path, "ALLOW_PARENT_CONSENT", "$.query.query.categoryFilters", AccessRule.TypeNaming.ALL_CONTAINS, "", true)); - - for(String underscorePath : underscoreFields ) { + + for(String underscorePath : underscoreFields) { rules.add(createPhenotypeSubRule(underscorePath, "ALLOW " + underscorePath, "$.query.query.fields.[*]", AccessRule.TypeNaming.ALL_CONTAINS_OR_EMPTY, "FIELDS", false)); rules.add(createPhenotypeSubRule(underscorePath, "ALLOW " + underscorePath, "$.query.query.categoryFilters", AccessRule.TypeNaming.ALL_CONTAINS, "CATEGORICAL", true)); rules.add(createPhenotypeSubRule(underscorePath, "ALLOW " + underscorePath, "$.query.query.requiredFields.[*]", AccessRule.TypeNaming.ALL_CONTAINS_OR_EMPTY, "REQ_FIELDS", false)); } - + rules.add(createPhenotypeSubRule(conceptPath, alias+ "_" + studyIdentifier, "$.query.query.categoryFilters", AccessRule.TypeNaming.ALL_CONTAINS, "CATEGORICAL", true)); rules.add(createPhenotypeSubRule(conceptPath, alias+ "_" + studyIdentifier, "$.query.query.numericFilters", AccessRule.TypeNaming.ALL_CONTAINS_OR_EMPTY, "NUMERIC", true)); rules.add(createPhenotypeSubRule(conceptPath, alias+ "_" + studyIdentifier, "$.query.query.fields.[*]", AccessRule.TypeNaming.ALL_CONTAINS_OR_EMPTY, "FIELDS", false)); rules.add(createPhenotypeSubRule(conceptPath, alias+ "_" + studyIdentifier, "$.query.query.requiredFields.[*]", AccessRule.TypeNaming.ALL_CONTAINS_OR_EMPTY, "REQUIRED_FIELDS", false)); - + return rules; } - + /** - * Harmonized rules should allow the user to supply paretn and top med consent groups; this allows a single harmonized - * rules instead of splitting between a topmed+harmonized and parent+harmonized - * + * Harmonized rules should allow the user to supply paretn and top med consent groups; this allows a single harmonized + * rules instead of splitting between a topmed+harmonized and parent+harmonized + * * @return */ private Collection getHarmonizedSubRules() { - + Set rules = new HashSet(); //categorical filters will always contain at least one entry (for the consent groups); it will never be empty rules.add(createPhenotypeSubRule(fence_parent_consent_group_concept_path, "ALLOW_PARENT_CONSENT", "$.query.query.categoryFilters", AccessRule.TypeNaming.ALL_CONTAINS, "", true)); rules.add(createPhenotypeSubRule(fence_harmonized_consent_group_concept_path, "ALLOW_HARMONIZED_CONSENT", "$.query.query.categoryFilters", AccessRule.TypeNaming.ALL_CONTAINS, "", true)); rules.add(createPhenotypeSubRule(fence_topmed_consent_group_concept_path, "ALLOW_TOPMED_CONSENT", "$.query.query.categoryFilters", AccessRule.TypeNaming.ALL_CONTAINS, "", true)); - - for(String underscorePath : underscoreFields ) { + + for(String underscorePath : underscoreFields) { rules.add(createPhenotypeSubRule(underscorePath, "ALLOW " + underscorePath, "$.query.query.fields.[*]", AccessRule.TypeNaming.ALL_CONTAINS_OR_EMPTY, "FIELDS", false)); rules.add(createPhenotypeSubRule(underscorePath, "ALLOW " + underscorePath, "$.query.query.categoryFilters", AccessRule.TypeNaming.ALL_CONTAINS, "CATEGORICAL", true)); rules.add(createPhenotypeSubRule(underscorePath, "ALLOW " + underscorePath, "$.query.query.requiredFields.[*]", AccessRule.TypeNaming.ALL_CONTAINS_OR_EMPTY, "REQ_FIELDS", false)); } - + rules.add(createPhenotypeSubRule(fence_harmonized_concept_path, "HARMONIZED", "$.query.query.categoryFilters", AccessRule.TypeNaming.ALL_CONTAINS, "CATEGORICAL", true)); rules.add(createPhenotypeSubRule(fence_harmonized_concept_path, "HARMONIZED", "$.query.query.numericFilters", AccessRule.TypeNaming.ALL_CONTAINS_OR_EMPTY, "NUMERIC", true)); rules.add(createPhenotypeSubRule(fence_harmonized_concept_path, "HARMONIZED", "$.query.query.fields.[*]", AccessRule.TypeNaming.ALL_CONTAINS_OR_EMPTY, "FIELDS", false)); rules.add(createPhenotypeSubRule(fence_harmonized_concept_path, "HARMONIZED", "$.query.query.requiredFields.[*]", AccessRule.TypeNaming.ALL_CONTAINS_OR_EMPTY, "REQUIRED_FIELDS", false)); - + return rules; } - + /** * generate and return a set of rules that disallow access to phenotype data (only genomic filters allowed) * @return */ private Collection getPhenotypeRestrictedSubRules(String studyIdentifier, String consentCode, String alias) { - Set rules = new HashSet(); //categorical filters will always contain at least one entry (for the consent groups); it will never be empty rules.add(createPhenotypeSubRule(fence_topmed_consent_group_concept_path, "ALLOW_TOPMED_CONSENT", "$.query.query.categoryFilters", AccessRule.TypeNaming.ALL_CONTAINS, "", true)); - + for(String underscorePath : underscoreFields ) { rules.add(createPhenotypeSubRule(underscorePath, "ALLOW " + underscorePath, "$.query.query.fields.[*]", AccessRule.TypeNaming.ALL_CONTAINS_OR_EMPTY, "FIELDS", false)); rules.add(createPhenotypeSubRule(underscorePath, "ALLOW " + underscorePath, "$.query.query.categoryFilters", AccessRule.TypeNaming.ALL_CONTAINS, "CATEGORICAL", true)); rules.add(createPhenotypeSubRule(underscorePath, "ALLOW " + underscorePath, "$.query.query.requiredFields.[*]", AccessRule.TypeNaming.ALL_CONTAINS_OR_EMPTY, "REQ_FIELDS", false)); } - + rules.add(createPhenotypeSubRule(null, alias + "_" + studyIdentifier+ "_" + consentCode, "$.query.query.numericFilters.[*]", AccessRule.TypeNaming.IS_EMPTY, "DISALLOW_NUMERIC", false)); -// rules.add(createPhenotypeSubRule(null, alias + "_" + studyIdentifier+ "_" + consentCode, "$.query.query.fields.[*]", AccessRule.TypeNaming.IS_EMPTY, "DISALLOW FIELDS", false, parentRule)); rules.add(createPhenotypeSubRule(null, alias + "_" + studyIdentifier+ "_" + consentCode, "$.query.query.requiredFields.[*]", AccessRule.TypeNaming.IS_EMPTY, "DISALLOW_REQUIRED_FIELDS", false)); - + return rules; } - + /** * Return a set of gates that identify which consent values have been provided. the boolean parameters indicate * if a value in the specified consent location should allow this gate to pass. @@ -768,399 +767,359 @@ private Collection getPhenotypeRestrictedSubRules(String s * @return */ private Collection getGates(boolean parent, boolean harmonized, boolean topmed) { - Set gates = new HashSet(); gates.add(upsertConsentGate("PARENT_CONSENT", "$.query.query.categoryFilters." + fence_parent_consent_group_concept_path + "[*]", parent, "parent study data")); gates.add(upsertConsentGate("HARMONIZED_CONSENT", "$.query.query.categoryFilters." + fence_harmonized_consent_group_concept_path + "[*]", harmonized, "harmonized data")); gates.add(upsertConsentGate("TOPMED_CONSENT", "$.query.query.categoryFilters." + fence_topmed_consent_group_concept_path + "[*]", topmed, "Topmed data")); - + return gates; } - - private AccessRule createPhenotypeSubRule(String conceptPath, String alias, String rule, int ruleType, String label, boolean useMapKey) { - - //remove double escape sequence from path evaluation expression - if(conceptPath != null && conceptPath.contains("\\\\")) { - //replaceall regex needs to be double escaped (again) - conceptPath = conceptPath.replaceAll("\\\\\\\\", "\\\\"); - } - - String ar_name = "AR_PHENO_"+alias + "_" + label; - - AccessRule ar = accessruleRepo.getUniqueResultByColumn("name", ar_name); - if(ar != null) { - logger.debug("createPhenotypeSubRule() Found existing rule: " + ar.getName()); - return ar; - } - - logger.info("createPhenotypeSubRule() Creating new access rule "+ar_name); - ar = new AccessRule(); - ar.setName(ar_name); - ar.setDescription("FENCE SUB AR for " + alias + " " + label + " clinical concepts"); - ar.setRule(rule); - ar.setType(ruleType); - ar.setValue(ruleType == AccessRule.TypeNaming.IS_NOT_EMPTY ? null : conceptPath); - ar.setCheckMapKeyOnly(useMapKey); - ar.setCheckMapNode(useMapKey); - ar.setEvaluateOnlyByGates(false); - ar.setGateAnyRelation(false); - - accessruleRepo.persist(ar); - - return ar; - } - - /** - * Creates a privilege for Topmed access. This has (up to) three access rules - 1) topmed only 2) topmed + parent 3) topmed+ hermonized (??) - * @param studyIdentifier - * @param consent_group - * @return - */ - private Privilege upsertTopmedPrivilege(String studyIdentifier, String projectAlias, String consent_group, String parentConceptPath, boolean isHarmonized) { - - String privilegeName = "PRIV_FENCE_"+studyIdentifier+"_"+consent_group + "_TOPMED"; - Privilege priv = privilegeRepo.getUniqueResultByColumn("name", privilegeName); - if(priv != null) { - logger.info("upsertTopmedPrivilege() " + privilegeName + " already exists"); - return priv; - } - - priv = new Privilege(); + /** + * Creates a privilege for Topmed access. This has (up to) three access rules: + * 1) topmed only 2) topmed + parent 3) topmed + harmonized. + * @param studyIdentifier + * @param projectAlias + * @param consentGroup + * @param parentConceptPath + * @param isHarmonized + * @return Privilege + */ + private Privilege upsertTopmedPrivilege(String studyIdentifier, String projectAlias, String consentGroup, String parentConceptPath, boolean isHarmonized) { + String privilegeName = "PRIV_FENCE_" + studyIdentifier + "_" + consentGroup + "_TOPMED"; + Privilege priv = privilegeRepo.getUniqueResultByColumn("name", privilegeName); + if (priv != null) { + logger.info("upsertTopmedPrivilege() {} already exists", privilegeName); + return priv; + } + + priv = new Privilege(); - // Build Privilege Object try { - priv.setApplication(picSureApp); - priv.setName(privilegeName); - priv.setDescription("FENCE privilege for Topmed "+studyIdentifier+"."+consent_group); - - String consent_concept_path = fence_topmed_consent_group_concept_path; - if(!consent_concept_path.contains("\\\\")){ - //these have to be escaped again so that jaxson can convert it correctly - consent_concept_path = consent_concept_path.replaceAll("\\\\", "\\\\\\\\"); - logger.debug("upsertTopmedPrivilege(): escaped parent consent path" + consent_concept_path); - } - - if(fence_harmonized_concept_path != null && !fence_harmonized_concept_path.contains("\\\\")){ - //these have to be escaped again so that jaxson can convert it correctly - fence_harmonized_concept_path = fence_harmonized_concept_path.replaceAll("\\\\", "\\\\\\\\"); - logger.debug("upsertTopmedPrivilege(): escaped harmonized consent path" + fence_harmonized_concept_path); - } - - // TODO: Change this to a mustache template - String queryTemplateText = "{\"categoryFilters\": {\"" - +consent_concept_path - +"\":[\"" - +studyIdentifier+"."+consent_group - +"\"]}," - +"\"numericFilters\":{},\"requiredFields\":[]," - +"\"fields\":[\"" + topmedAccessionField + "\"]," - +"\"variantInfoFilters\":[{\"categoryVariantInfoFilters\":{},\"numericVariantInfoFilters\":{}}]," - +"\"expectedResultType\": \"COUNT\"" - +"}"; - priv.setQueryTemplate(queryTemplateText); - //need a non-null scope that is a list of strings - - - - String variantColumns = JAXRSConfiguration.variantAnnotationColumns; - if(variantColumns == null || variantColumns.isEmpty()) { - priv.setQueryScope("[\"_\"]"); - } else { - StringBuilder builder = new StringBuilder(); - for(String annotationPath : variantColumns.split(",")) { - if(builder.length() == 0) { - builder.append("["); - } else { - builder.append(","); - } - builder.append("\""+annotationPath+"\""); - } - builder.append(",\"_\""); - builder.append("]"); - priv.setQueryScope(builder.toString()); - } - - - Set accessrules = new HashSet(); - - AccessRule ar = upsertTopmedAccessRule(studyIdentifier, consent_group, "TOPMED"); - - //if this is a new rule, we need to populate it - if(ar.getGates() == null) { - ar.setGates(new HashSet()); - ar.getGates().addAll(getGates(false, false, true)); - - if(ar.getSubAccessRule() == null) { - ar.setSubAccessRule(new HashSet()); - } - ar.getSubAccessRule().addAll(getAllowedQueryTypeRules()); - ar.getSubAccessRule().addAll(getPhenotypeRestrictedSubRules(studyIdentifier, consent_group, projectAlias)); - accessruleRepo.merge(ar); - } - accessrules.add(ar); - - if(parentConceptPath != null) { - - ar = upsertTopmedAccessRule(studyIdentifier, consent_group, "TOPMED+PARENT"); - - //if this is a new rule, we need to populate it - if(ar.getGates() == null) { - ar.setGates(new HashSet()); - ar.getGates().addAll(getGates(true, false, true)); - if(ar.getSubAccessRule() == null) { - ar.setSubAccessRule(new HashSet()); - } - ar.getSubAccessRule().addAll(getAllowedQueryTypeRules()); - ar.getSubAccessRule().addAll(getPhenotypeSubRules(studyIdentifier, parentConceptPath, projectAlias)); - //this is added in the 'getPhenotypeRestrictedSubRules()' which is not called in this path - ar.getSubAccessRule().add(createPhenotypeSubRule(fence_topmed_consent_group_concept_path, "ALLOW_TOPMED_CONSENT", "$.query.query.categoryFilters", AccessRule.TypeNaming.ALL_CONTAINS, "", true)); - - accessruleRepo.merge(ar); - } - accessrules.add(ar); - - - if(isHarmonized) { - ar = upsertHarmonizedAccessRule(studyIdentifier, consent_group, "HARMONIZED"); - - //if this is a new rule, we need to populate it - if(ar.getGates() == null) { - ar.setGates(new HashSet()); -// ar.getGates().addAll(getGates(true, true, false)); - ar.getGates().add(upsertConsentGate("HARMONIZED_CONSENT", "$.query.query.categoryFilters." + fence_harmonized_consent_group_concept_path + "[*]", true, "harmonized data")); - - if(ar.getSubAccessRule() == null) { - ar.setSubAccessRule(new HashSet()); - } - ar.getSubAccessRule().addAll(getAllowedQueryTypeRules()); - ar.getSubAccessRule().addAll(getHarmonizedSubRules()); - ar.getSubAccessRule().addAll(getPhenotypeSubRules(studyIdentifier, parentConceptPath, projectAlias)); - accessruleRepo.merge(ar); - } - accessrules.add(ar); - - -// ar = upsertHarmonizedAccessRule(studyIdentifier, consent_group, "TOPMED+HARMONIZED+PARENT"); -// -// //if this is a new rule, we need to populate it -// if(ar.getGates() == null) { -// ar.setGates(new HashSet()); -// ar.getGates().addAll(getGates(true, true, true)); -// -// if(ar.getSubAccessRule() == null) { -// ar.setSubAccessRule(new HashSet()); -// } -// ar.getSubAccessRule().addAll(getAllowedQueryTypeRules()); -// ar.getSubAccessRule().addAll(getPhenotypeSubRules(studyIdentifier, parentConceptPath, projectAlias)); -// ar.getSubAccessRule().addAll(getHarmonizedSubRules()); -// ar.getSubAccessRule().add(createPhenotypeSubRule(fence_topmed_consent_group_concept_path, "ALLOW_TOPMED_CONSENT", "$.query.query.categoryFilters", AccessRule.TypeNaming.ALL_CONTAINS, "", true)); -// -// accessruleRepo.merge(ar); -// } -// accessrules.add(ar); - } - - } - - // Add additional access rules; - for(String arName: fence_standard_access_rules.split(",")) { - if (arName.startsWith("AR_")) { - logger.info("upsertTopmedPrivilege() Adding AccessRule "+arName+" to privilege "+priv.getName()); - ar = accessruleRepo.getUniqueResultByColumn("name",arName); - if(ar != null) { - accessrules.add(ar); - } - else { - logger.warn("uupsertTopmedPrivilege() nable to find an access rule with name " + arName); - } + buildPrivilegeObject(priv, privilegeName, studyIdentifier, consentGroup); + + Set accessRules = new HashSet<>(); + AccessRule topmedRule = upsertTopmedAccessRule(studyIdentifier, consentGroup, "TOPMED"); + + populateAccessRule(topmedRule, false, false, true); + topmedRule.getSubAccessRule().addAll(getPhenotypeRestrictedSubRules(studyIdentifier, consentGroup, projectAlias)); + accessRules.add(topmedRule); + + if (parentConceptPath != null) { + AccessRule topmedParentRule = upsertTopmedAccessRule(studyIdentifier, consentGroup, "TOPMED+PARENT"); + populateAccessRule(topmedParentRule, true, false, true); + topmedParentRule.getSubAccessRule().addAll(getPhenotypeSubRules(studyIdentifier, parentConceptPath, projectAlias)); + accessRules.add(topmedParentRule); + + if (isHarmonized) { + AccessRule harmonizedRule = upsertHarmonizedAccessRule(studyIdentifier, consentGroup, "HARMONIZED"); + populateHarmonizedAccessRule(harmonizedRule, parentConceptPath, studyIdentifier, projectAlias); + accessRules.add(harmonizedRule); } } - priv.setAccessRules(accessrules); - logger.info("upsertTopmedPrivilege() Added "+accessrules.size()+" access_rules to privilege"); + + addStandardAccessRules(accessRules); + + priv.setAccessRules(accessRules); + logger.info("upsertTopmedPrivilege() Added {} access_rules to privilege", accessRules.size()); privilegeRepo.persist(priv); - logger.info("upsertTopmedPrivilege() Added new privilege "+priv.getName()+" to DB"); + logger.info("upsertTopmedPrivilege() Added new privilege {} to DB", priv.getName()); } catch (Exception ex) { - ex.printStackTrace(); - logger.error("upsertTopmedPrivilege() could not save privilege"); + logger.error("upsertTopmedPrivilege() could not save privilege", ex); } + return priv; } - //Generates Main rule only; gates & sub rules attached after calling this - // prentRule should be null if this is the main rule, or the appropriate value if this is a sub rule - private AccessRule createConsentAccessRule(String studyIdentifier, String consent_group, String label, String consent_path) { - logger.debug("upsertConsentAccessRule() starting"); - String ar_name = (consent_group != null && consent_group != "") ? "AR_CONSENT_" + studyIdentifier+"_"+consent_group+ "_" +label : "AR_CONSENT_" + studyIdentifier; + private void buildPrivilegeObject(Privilege priv, String privilegeName, String studyIdentifier, String consentGroup) { + priv.setApplication(picSureApp); + priv.setName(privilegeName); + priv.setDescription("FENCE privilege for Topmed " + studyIdentifier + "." + consentGroup); - AccessRule ar = accessruleRepo.getUniqueResultByColumn("name", ar_name); - if(ar != null) { - logger.debug("upsertConsentAccessRule() Found existing rule: " + ar.getName()); - return ar; + String consentConceptPath = escapePath(fence_topmed_consent_group_concept_path); + fence_harmonized_concept_path = escapePath(fence_harmonized_concept_path); + + String queryTemplateText = "{\"categoryFilters\": {\"" + consentConceptPath + "\":[\"" + studyIdentifier + "." + consentGroup + "\"]}," + + "\"numericFilters\":{},\"requiredFields\":[]," + + "\"fields\":[\"" + topmedAccessionField + "\"]," + + "\"variantInfoFilters\":[{\"categoryVariantInfoFilters\":{},\"numericVariantInfoFilters\":{}}]," + + "\"expectedResultType\": \"COUNT\"" + + "}"; + + priv.setQueryTemplate(queryTemplateText); + + String variantColumns = JAXRSConfiguration.variantAnnotationColumns; + priv.setQueryScope(buildQueryScope(variantColumns)); + } + + private String escapePath(String path) { + if (path != null && !path.contains("\\\\")) { + return path.replaceAll("\\\\", "\\\\\\\\"); } - - - - logger.info("upsertConsentAccessRule() Creating new access rule "+ar_name); - ar = new AccessRule(); - ar.setName(ar_name); - String description = (consent_group != null && consent_group != "") ? "FENCE AR for "+studyIdentifier+"."+consent_group + " clinical concepts" : "FENCE AR for "+studyIdentifier+" clinical concepts"; - ar.setDescription(description); - StringBuilder ruleText = new StringBuilder(); - ruleText.append("$.query.query.categoryFilters."); - ruleText.append(consent_path); - ruleText.append("[*]"); - ar.setRule(ruleText.toString()); - ar.setType(AccessRule.TypeNaming.ALL_CONTAINS); - String arValue = (consent_group != null && consent_group != "") ? studyIdentifier+"."+consent_group : studyIdentifier; - ar.setValue(arValue); - ar.setCheckMapKeyOnly(false); - ar.setCheckMapNode(false); - ar.setEvaluateOnlyByGates(false); - ar.setGateAnyRelation(false); - - accessruleRepo.persist(ar); - - logger.debug("upsertConsentAccessRule() finished"); - return ar; + return path; } - - // Generates Main Rule only; gates & sub rules attached by calling method - private AccessRule upsertTopmedAccessRule(String project_name, String consent_group, String label ) { - logger.debug("upsertTopmedAccessRule() starting"); - String ar_name = (consent_group != null && consent_group != "") ? "AR_TOPMED_"+project_name+"_"+consent_group + "_" + label : "AR_TOPMED_"+project_name+"_"+label; - AccessRule ar = accessruleRepo.getUniqueResultByColumn("name", ar_name); - if (ar != null) { - logger.info("upsertTopmedAccessRule() AccessRule "+ar_name+" already exists."); - return ar; + + private String buildQueryScope(String variantColumns) { + if (variantColumns == null || variantColumns.isEmpty()) { + return "[\"_\"]"; } - logger.info("upsertTopmedAccessRule() Creating new access rule "+ar_name); - ar = new AccessRule(); - ar.setName(ar_name); - ar.setDescription("FENCE AR for "+project_name+"."+consent_group + " Topmed data"); - StringBuilder ruleText = new StringBuilder(); - ruleText.append("$.query.query.categoryFilters."); - ruleText.append(fence_topmed_consent_group_concept_path); - ruleText.append("[*]"); - ar.setRule(ruleText.toString()); - ar.setType(AccessRule.TypeNaming.ALL_CONTAINS); - String arValue = (consent_group != null && consent_group != "") ? project_name+"."+consent_group : project_name; - ar.setValue(arValue); - ar.setCheckMapKeyOnly(false); - ar.setCheckMapNode(false); - ar.setEvaluateOnlyByGates(false); - ar.setGateAnyRelation(false); - - accessruleRepo.persist(ar); - - logger.debug("upsertTopmedAccessRule() finished"); - return ar; + return Arrays.stream(variantColumns.split(",")) + .map(path -> "\"" + path + "\"") + .collect(Collectors.joining(",", "[", ",\"_\"]")); } - - - // Generates Main Rule only; gates & sub rules attached by calling method - private AccessRule upsertHarmonizedAccessRule(String project_name, String consent_group, String label ) { - logger.debug("upsertTopmedAccessRule() starting"); - String ar_name = "AR_TOPMED_"+project_name+"_"+consent_group + "_" + label; - AccessRule ar = accessruleRepo.getUniqueResultByColumn("name", ar_name); - if (ar != null) { - logger.info("upsertTopmedAccessRule() AccessRule "+ar_name+" already exists."); - return ar; + + private void populateAccessRule(AccessRule rule, boolean includeParent, boolean includeHarmonized, boolean includeTopmed) { + if (rule.getGates() == null) { + rule.setGates(new HashSet<>(getGates(includeParent, includeHarmonized, includeTopmed))); + } + + if (rule.getSubAccessRule() == null) { + rule.setSubAccessRule(new HashSet<>(getAllowedQueryTypeRules())); + } + + accessruleRepo.merge(rule); + } + + private void populateHarmonizedAccessRule(AccessRule rule, String parentConceptPath, String studyIdentifier, String projectAlias) { + if (rule.getGates() == null) { + rule.setGates(new HashSet<>(Collections.singletonList( + upsertConsentGate("HARMONIZED_CONSENT", "$.query.query.categoryFilters." + fence_harmonized_consent_group_concept_path + "[*]", true, "harmonized data") + ))); } - logger.info("upsertTopmedAccessRule() Creating new access rule "+ar_name); - ar = new AccessRule(); - ar.setName(ar_name); - ar.setDescription("FENCE AR for "+project_name+"."+consent_group + " Topmed data"); - StringBuilder ruleText = new StringBuilder(); - ruleText.append("$.query.query.categoryFilters."); - ruleText.append(fence_harmonized_consent_group_concept_path); - ruleText.append("[*]"); - ar.setRule(ruleText.toString()); - ar.setType(AccessRule.TypeNaming.ALL_CONTAINS); - ar.setValue(project_name+"."+consent_group); - ar.setCheckMapKeyOnly(false); - ar.setCheckMapNode(false); - ar.setEvaluateOnlyByGates(false); - ar.setGateAnyRelation(false); - - accessruleRepo.persist(ar); - - logger.debug("upsertTopmedAccessRule() finished"); - return ar; + if (rule.getSubAccessRule() == null) { + rule.setSubAccessRule(new HashSet<>(getAllowedQueryTypeRules())); + rule.getSubAccessRule().addAll(getHarmonizedSubRules()); + rule.getSubAccessRule().addAll(getPhenotypeSubRules(studyIdentifier, parentConceptPath, projectAlias)); + } + + accessruleRepo.merge(rule); + } + + // A set of standard access rules that are added to all privileges + // to cache the standard access rules + private Set standardAccessRules; + + private void addStandardAccessRules(Set accessRules) { + if (standardAccessRules != null && !standardAccessRules.isEmpty()) { + accessRules.addAll(standardAccessRules); + } else { + standardAccessRules = new HashSet<>(); + for (String arName : fence_standard_access_rules.split(",")) { + if (arName.startsWith("AR_")) { + logger.info("Adding AccessRule {} to privilege", arName); + AccessRule ar = accessruleRepo.getUniqueResultByColumn("name", arName); + if (ar != null) { + standardAccessRules.add(ar); + } else { + logger.warn("Unable to find an access rule with name {}", arName); + } + } + } + + accessRules.addAll(standardAccessRules); + } + } + + + /** + * Creates and returns a consent access rule AccessRule. + * Generates Main rule only; gates & sub-rules attached after calling this + * prentRule should be null if this is the main rule, or the appropriate value if this is a sub-rule + * + * @param studyIdentifier The study identifier. + * @param consent_group The consent group. + * @param label The label for the rule. + * @param consent_path The consent path. + * @return The created AccessRule. + */ + private AccessRule createConsentAccessRule(String studyIdentifier, String consent_group, String label, String consent_path) { + String ar_name = (consent_group != null && !consent_group.isEmpty()) ? "AR_CONSENT_" + studyIdentifier + "_" + consent_group + "_" + label : "AR_CONSENT_" + studyIdentifier; + String description = (consent_group != null && !consent_group.isEmpty()) ? "FENCE AR for " + studyIdentifier + "." + consent_group + " clinical concepts" : "FENCE AR for " + studyIdentifier + " clinical concepts"; + String ruleText = "$.query.query.categoryFilters." + consent_path + "[*]"; + String arValue = (consent_group != null && !consent_group.isEmpty()) ? studyIdentifier + "." + consent_group : studyIdentifier; + + return getOrCreateAccessRule( + ar_name, + description, + ruleText, + AccessRule.TypeNaming.ALL_CONTAINS, + arValue, + false, + false, + false, + false + ); + } + + /** + * Creates and returns a Topmed access rule AccessRule. + * Generates Main Rule only; gates & sub-rules attached by calling method + * @param project_name The name of the project. + * @param consent_group The consent group. + * @param label The label for the rule. + * @return The created AccessRule. + */ + private AccessRule upsertTopmedAccessRule(String project_name, String consent_group, String label) { + String ar_name = (consent_group != null && !consent_group.isEmpty()) ? "AR_TOPMED_" + project_name + "_" + consent_group + "_" + label : "AR_TOPMED_" + project_name + "_" + label; + String description = "FENCE AR for " + project_name + "." + consent_group + " Topmed data"; + String ruleText = "$.query.query.categoryFilters." + fence_topmed_consent_group_concept_path + "[*]"; + String arValue = (consent_group != null && !consent_group.isEmpty()) ? project_name + "." + consent_group : project_name; + + return getOrCreateAccessRule( + ar_name, + description, + ruleText, + AccessRule.TypeNaming.ALL_CONTAINS, + arValue, + false, + false, + false, + false + ); } /** - * Insert a new gate (if it doesn't exist yet) to identify if consent values are present in the query. + * Creates and returns a harmonized access rule AccessRule for Topmed. + * Generates Main Rule only; gates & sub rules attached by calling method + * + * @param project_name The name of the project. + * @param consent_group The consent group. + * @param label The label for the rule. + * @return The created AccessRule. + */ + private AccessRule upsertHarmonizedAccessRule(String project_name, String consent_group, String label) { + String ar_name = "AR_TOPMED_" + project_name + "_" + consent_group + "_" + label; + logger.info("upsertHarmonizedAccessRule() Creating new access rule {}", ar_name); + String description = "FENCE AR for " + project_name + "." + consent_group + " Topmed data"; + String ruleText = "$.query.query.categoryFilters." + fence_harmonized_consent_group_concept_path + "[*]"; + String arValue = project_name + "." + consent_group; + + return getOrCreateAccessRule( + ar_name, + description, + ruleText, + AccessRule.TypeNaming.ALL_CONTAINS, + arValue, + false, + false, + false, + false + ); + } + + /** + * Creates and returns a consent gate AccessRule. + * Insert a new gate (if it doesn't exist yet) to identify if consent values are present in the query. * return an existing gate named GATE_{gateName}_(PRESENT|MISSING) if it exists. + * + * @param gateName The name of the gate. + * @param rule The rule expression. + * @param is_present Whether the gate is for present or missing consent. + * @param description The description of the gate. + * @return The created AccessRule. */ - private AccessRule upsertConsentGate(String gateName, String rule, boolean is_present, String description) { - - gateName = "GATE_" + gateName + "_" + (is_present ? "PRESENT": "MISSING"); - - AccessRule gate = accessruleRepo.getUniqueResultByColumn("name", gateName); - if (gate != null) { - logger.info("upsertConsentGate() AccessRule "+gateName+" already exists."); - return gate; + private AccessRule upsertConsentGate(String gateName, String rule, boolean is_present, String description) { + gateName = "GATE_" + gateName + "_" + (is_present ? "PRESENT" : "MISSING"); + return getOrCreateAccessRule( + gateName, + "FENCE GATE for " + description + " consent " + (is_present ? "present" : "missing"), + rule, + is_present ? AccessRule.TypeNaming.IS_NOT_EMPTY : AccessRule.TypeNaming.IS_EMPTY, + null, + false, + false, + false, + false + ); + } + + private AccessRule createPhenotypeSubRule(String conceptPath, String alias, String rule, int ruleType, String label, boolean useMapKey) { + String ar_name = "AR_PHENO_" + alias + "_" + label; + logger.info("createPhenotypeSubRule() Creating new access rule {}", ar_name); + return getOrCreateAccessRule( + ar_name, + "FENCE SUB AR for " + alias + " " + label + " clinical concepts", + rule, + ruleType, + ruleType == AccessRule.TypeNaming.IS_NOT_EMPTY ? null : conceptPath, + useMapKey, + useMapKey, + false, + false + ); + } + + private AccessRule getOrCreateAccessRule(String name, String description, String rule, int type, String value, boolean checkMapKeyOnly, boolean checkMapNode, boolean evaluateOnlyByGates, boolean gateAnyRelation) { + return accessRuleCache.computeIfAbsent(name, key -> { + AccessRule ar = accessruleRepo.getUniqueResultByColumn("name", key); + if (ar == null) { + logger.info("Creating new access rule {}", key); + ar = new AccessRule(); + ar.setName(name); + ar.setDescription(description); + ar.setRule(rule); + ar.setType(type); + ar.setValue(value); + ar.setCheckMapKeyOnly(checkMapKeyOnly); + ar.setCheckMapNode(checkMapNode); + ar.setEvaluateOnlyByGates(evaluateOnlyByGates); + ar.setGateAnyRelation(gateAnyRelation); + accessruleRepo.persist(ar); + } + + return ar; + }); + } + + private String extractProject(String roleName) { + String projectPattern = "FENCE_(.*?)(?:_c\\d+)?$"; + if (roleName.startsWith("MANUAL_")) { + projectPattern = "MANUAL_(.*?)(?:_c\\d+)?$"; + } + Pattern projectRegex = Pattern.compile(projectPattern); + Matcher projectMatcher = projectRegex.matcher(roleName); + String project = ""; + if (projectMatcher.find()) { + project = projectMatcher.group(1).trim(); + } else { + logger.info("extractProject() Could not extract project from role name: {}", roleName); + String[] parts = roleName.split("_", 1); + if (parts.length > 0) { + project = parts[1]; + } } + return project; + } + + private static String extractConsentGroup(String roleName) { + String consentPattern = "FENCE_.*?_c(\\d+)$"; + if (roleName.startsWith("MANUAL_")) { + consentPattern = "MANUAL_.*?_c(\\d+)$"; + } + Pattern consentRegex = Pattern.compile(consentPattern); + Matcher consentMatcher = consentRegex.matcher(roleName); + String consentGroup = ""; + if (consentMatcher.find()) { + consentGroup = "c" + consentMatcher.group(1).trim(); + } + return consentGroup; + } + + private Map getFENCEMappingforProjectAndConsent(String projectId, String consent_group) { + String consentVal = (consent_group != null && !consent_group.isEmpty()) ? projectId + "." + consent_group : projectId; + logger.info("getFENCEMappingforProjectAndConsent() looking up {}", consentVal); + + Object projectMetadata = this.fenceMappingUtility.getFENCEMapping().get(consentVal); + if(projectMetadata instanceof Map) { + return (Map)projectMetadata; + } else if (projectMetadata != null) { + logger.info("getFENCEMappingforProjectAndConsent() Obj instance of " + projectMetadata.getClass().getCanonicalName()); + } + return null; + } - logger.info("upsertClinicalGate() Creating new access rule "+gateName); - gate = new AccessRule(); - gate.setName(gateName); - gate.setDescription("FENCE GATE for " + description + " consent " + (is_present ? "present" : "missing")); - gate.setRule(rule); - gate.setType(is_present ? AccessRule.TypeNaming.IS_NOT_EMPTY : AccessRule.TypeNaming.IS_EMPTY ); - gate.setValue(null); - gate.setCheckMapKeyOnly(false); - gate.setCheckMapNode(false); - gate.setEvaluateOnlyByGates(false); - gate.setGateAnyRelation(false); - - accessruleRepo.persist(gate); - return gate; - } - - private Map getFENCEMappingforProjectAndConsent(String projectId, String consent_group) { - - String consentVal = (consent_group != null && consent_group != "") ? projectId + "." + consent_group : projectId; - logger.info("getFENCEMappingforProjectAndConsent() looking up "+consentVal); - - Object projectMetadata = getFENCEMapping().get(consentVal); - if( projectMetadata instanceof Map) { - return (Map)projectMetadata; - } else if (projectMetadata != null) { - logger.info("getFENCEMappingforProjectAndConsent() Obj instance of " + projectMetadata.getClass().getCanonicalName()); - } - return null; - } - - public Map getFENCEMapping(){ - if(_projectMap == null || _projectMap.isEmpty()) { - try { - Map fenceMapping = JAXRSConfiguration.objectMapper.readValue( - new File(String.join(File.separator, - new String[] {JAXRSConfiguration.templatePath ,"fence_mapping.json"})) - , Map.class); - List projects = (List) fenceMapping.get("bio_data_catalyst"); - logger.debug("getFENCEMapping: found FENCE mapping with " + projects.size() + " entries"); - _projectMap = new HashMap(projects.size()); - for(Map project : projects) { - String consentVal = (project.get("consent_group_code") != null && project.get("consent_group_code") != "") ? - "" + project.get("study_identifier") + "." + project.get("consent_group_code") : - "" + project.get("study_identifier"); - logger.debug("adding study " + consentVal); - _projectMap.put(consentVal, project); - } - - } catch (Exception e) { - logger.error("getFENCEMapping: Non-fatal error parsing fence_mapping.json: "+JAXRSConfiguration.templatePath, e); - return new HashMap(); - } - } - - return _projectMap; - } } diff --git a/pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/utils/FenceMappingUtility.java b/pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/utils/FenceMappingUtility.java new file mode 100644 index 000000000..53a240c67 --- /dev/null +++ b/pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/utils/FenceMappingUtility.java @@ -0,0 +1,88 @@ +package edu.harvard.hms.dbmi.avillach.auth.utils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.PostConstruct; +import javax.ejb.Singleton; +import javax.ejb.Startup; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Singleton +@Startup +public class FenceMappingUtility { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + private Map fenceMappingByConsent; + private Map fenceMappingByAuthZ; + private static String templatePath; + private ObjectMapper objectMapper; + + @PostConstruct + public void init() { + try { + Context ctx = new InitialContext(); + templatePath = (String) ctx.lookup("java:global/templatePath"); + objectMapper = new ObjectMapper(); + + initializeFENCEMappings(); + } catch (NamingException e) { + throw new RuntimeException(e); + } + } + + private void initializeFENCEMappings() { + if (fenceMappingByConsent == null || fenceMappingByAuthZ == null) { + ArrayList studies = loadBioDataCatalystFenceMappingData(); + ConcurrentHashMap tempFenceMappingByConsent = new ConcurrentHashMap<>(); + ConcurrentHashMap tempFenceMappingByAuthZ = new ConcurrentHashMap<>(); + + studies.parallelStream().forEach(study -> { + String consentVal = (study.get("consent_group_code") != null && !study.get("consent_group_code").toString().isEmpty()) ? + study.get("study_identifier") + "." + study.get("consent_group_code") : + study.get("study_identifier").toString(); + tempFenceMappingByConsent.put(consentVal, study); + tempFenceMappingByAuthZ.put(study.get("authZ").toString().replace("\\/", "/"), study); + }); + + fenceMappingByConsent = Collections.unmodifiableMap(tempFenceMappingByConsent); + fenceMappingByAuthZ = Collections.unmodifiableMap(tempFenceMappingByAuthZ); + } + } + + public Map getFENCEMapping() { + return fenceMappingByConsent; + } + + public Map getFenceMappingByAuthZ() { + return fenceMappingByAuthZ; + } + + private ArrayList loadBioDataCatalystFenceMappingData() { + Map fenceMapping; + ArrayList studies; + try { + logger.debug("getFENCEMapping: loading FENCE mapping from {}", templatePath); + fenceMapping = objectMapper.readValue( + new File(String.join(File.separator, + new String[]{templatePath, "fence_mapping.json"})) + , Map.class); + + studies = (ArrayList) fenceMapping.get("bio_data_catalyst"); + logger.debug("getFENCEMapping: found FENCE mapping with {} entries", studies.size()); + } catch (Exception e) { + logger.error("loadFenceMappingData: Non-fatal error parsing fence_mapping.json: {}", templatePath, e); + return new ArrayList<>(); + } + return studies; + } + +} diff --git a/pic-sure-auth-services/src/test/java/edu/harvard/hms/dbmi/avillach/StudyAccessServiceTest.java b/pic-sure-auth-services/src/test/java/edu/harvard/hms/dbmi/avillach/StudyAccessServiceTest.java index a3811e3ee..0058a21ad 100644 --- a/pic-sure-auth-services/src/test/java/edu/harvard/hms/dbmi/avillach/StudyAccessServiceTest.java +++ b/pic-sure-auth-services/src/test/java/edu/harvard/hms/dbmi/avillach/StudyAccessServiceTest.java @@ -3,6 +3,7 @@ import edu.harvard.hms.dbmi.avillach.auth.data.repository.RoleRepository; import edu.harvard.hms.dbmi.avillach.auth.rest.StudyAccessService; import edu.harvard.hms.dbmi.avillach.auth.service.auth.FENCEAuthenticationService; +import edu.harvard.hms.dbmi.avillach.auth.utils.FenceMappingUtility; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; @@ -25,6 +26,9 @@ public class StudyAccessServiceTest { @Mock private FENCEAuthenticationService fenceAuthenticationService; + @Mock + private FenceMappingUtility fenceUtilityMapping; + @Before public void init() { MockitoAnnotations.initMocks(this); @@ -42,7 +46,7 @@ public void testAddStudyAccessWithBlankIdentifier() { @Test public void testAddStudyAccess() { String studyIdentifier = "testStudy"; - when(fenceAuthenticationService.getFENCEMapping()).thenReturn(Map.of(studyIdentifier, Map.of(StudyAccessService.STUDY_IDENTIFIER, studyIdentifier,StudyAccessService.CONSENT_GROUP_CODE, ""))); + when(fenceUtilityMapping.getFENCEMapping()).thenReturn(Map.of(studyIdentifier, Map.of(StudyAccessService.STUDY_IDENTIFIER, studyIdentifier,StudyAccessService.CONSENT_GROUP_CODE, ""))); when(fenceAuthenticationService.upsertRole(null, "MANUAL_testStudy", "MANUAL_ role MANUAL_testStudy")).thenReturn(true); Response response = studyAccessService.addStudyAccess(studyIdentifier); @@ -53,7 +57,7 @@ public void testAddStudyAccess() { @Test public void testAddStudyAccessWithConsent() { String studyIdentifier = "testStudy2.c2"; - when(fenceAuthenticationService.getFENCEMapping()).thenReturn(Map.of(studyIdentifier, Map.of(StudyAccessService.STUDY_IDENTIFIER, "testStudy2", StudyAccessService.CONSENT_GROUP_CODE, "c2"))); + when(fenceUtilityMapping.getFENCEMapping()).thenReturn(Map.of(studyIdentifier, Map.of(StudyAccessService.STUDY_IDENTIFIER, "testStudy2", StudyAccessService.CONSENT_GROUP_CODE, "c2"))); when(fenceAuthenticationService.upsertRole(null, "MANUAL_testStudy2_c2", "MANUAL_ role MANUAL_testStudy2_c2")).thenReturn(true); Response response = studyAccessService.addStudyAccess(studyIdentifier);