Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f9ee83c
[BI-2255] - refactored ProgramSecuredRoleGroup enum
mlm483 Jul 26, 2024
2c5dce8
[BI-2255] - refactored ProgramSecuredRoleGroup enum
mlm483 Jul 26, 2024
f1279b7
[BI-2255] WIP
davedrp Aug 6, 2024
e9351a9
[BI-2255] WIP
davedrp Aug 7, 2024
e037a0c
Merge pull request #377 from Breeding-Insight/feature/BI-1692
HMS17 Aug 8, 2024
69e68fe
[autocommit] bumping build number
rob-ouser-bi Aug 8, 2024
6cabaed
[BI-2255] WIP
davedrp Aug 8, 2024
de9cad3
Merge branch 'develop' into feature/BI-2257
mlm483 Aug 8, 2024
16e3768
Merge pull request #383 from Breeding-Insight/feature/BI-2257
mlm483 Aug 8, 2024
1a2c6de
[autocommit] bumping build number
rob-ouser-bi Aug 8, 2024
1654b5e
[BI-2255] WIP
davedrp Aug 9, 2024
972848c
Merge branch 'develop' into feature/BI-2255
davedrp Aug 9, 2024
e65f791
[BI-2055] adjusted order of SecuredAnnotationRules
davedrp Aug 13, 2024
b7bc15b
[BI-2055] Removed parameters from ExperimentSecuredRule
davedrp Aug 13, 2024
ed72bc8
[BI-2055] renamed annotation from ExperimentSecured -> ExperimentColl…
davedrp Aug 13, 2024
bf6f0de
[BI-2055] created the extractExperimentId to make more readable
davedrp Aug 13, 2024
760f054
[BI-2255] cleaned up code
davedrp Aug 13, 2024
45fa5d4
[BI-2255] move initialization of variable to inside if-statement
davedrp Aug 13, 2024
b82badd
Merge branch 'develop' into feature/BI-2255
davedrp Aug 13, 2024
51adb85
[BI-2255]Addressed PR-comments from Matthew
davedrp Aug 15, 2024
a9d71ea
Merge branch 'develop' into feature/BI-2255
davedrp Aug 19, 2024
321f8ad
[BI-2255] fixed bug
davedrp Aug 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import lombok.Getter;
import lombok.Setter;
import org.breedinginsight.model.ProgramUser;
import org.breedinginsight.services.exceptions.DoesNotExistException;

import java.util.Collection;
import java.util.List;
Expand All @@ -38,4 +39,11 @@ public AuthenticatedUser(String username, Collection<String> roles, UUID id, Lis
this.id = id;
this.programRoles = programRoles;
}

public ProgramUser extractProgramUser(UUID programId) throws DoesNotExistException {
return this.programRoles.stream()
.filter(pu -> programId.equals( pu.getProgramId() ) )
.findFirst()
.orElseThrow( () -> new DoesNotExistException( String.format("No program user found for program %s", this.id) ) );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.breedinginsight.api.auth;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExperimentCollaboratorSecured {
//The only role is EXPERIMENTAL_COLLABORATOR
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,10 @@

package org.breedinginsight.api.auth;

import org.apache.commons.collections4.ListUtils;

import java.util.List;

public enum ProgramSecuredRoleGroup {
ALL_PROGRAM_ROLES(List.of(ProgramSecuredRole.READ_ONLY, ProgramSecuredRole.PROGRAM_ADMIN)),
ALL(ListUtils.union(ALL_PROGRAM_ROLES.getProgramRoles(), List.of(ProgramSecuredRole.SYSTEM_ADMIN)));
PROGRAM_SCOPED_ROLES(List.of(ProgramSecuredRole.SYSTEM_ADMIN, ProgramSecuredRole.READ_ONLY, ProgramSecuredRole.PROGRAM_ADMIN));

private List<ProgramSecuredRole> programRoles;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.breedinginsight.api.auth.rules;

import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.exceptions.HttpStatusException;
import io.micronaut.http.server.exceptions.HttpServerException;
import io.micronaut.security.rules.SecuredAnnotationRule;
import io.micronaut.security.rules.SecurityRuleResult;
import io.micronaut.security.token.RolesFinder;
import io.micronaut.web.router.MethodBasedRouteMatch;
import io.micronaut.web.router.RouteMatch;
import org.brapi.client.v2.model.exceptions.ApiException;
import org.brapi.v2.model.core.BrAPITrial;
import org.breedinginsight.api.auth.*;
import org.breedinginsight.brapi.v2.dao.BrAPITrialDAO;
import org.breedinginsight.daos.ExperimentalCollaboratorDAO;
import org.breedinginsight.daos.ProgramDAO;
import org.breedinginsight.model.ProgramUser;
import org.breedinginsight.model.Role;
import org.breedinginsight.services.exceptions.DoesNotExistException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.*;

@Singleton
public class ExperimentCollaboratorSecuredAnnotationRule extends SecuredAnnotationRule {

// Executes before the ProgramSecuredAnnotationRule, and if the annotation exists, will return before the ProgramSecuredAnnotationRule can execute
public static final Integer ORDER = ProgramSecuredAnnotationRule.ORDER -2;

public ExperimentCollaboratorSecuredAnnotationRule(RolesFinder rolesFinder) {
super(rolesFinder);
}

@Inject
private SecurityService securityService;
@Inject
private ProgramDAO programDAO;
@Inject
private BrAPITrialDAO brAPITrialDAO;
@Inject
private ExperimentalCollaboratorDAO experimentalCollaboratorDAO;

@Override
public SecurityRuleResult check(HttpRequest<?> request, @Nullable RouteMatch<?> routeMatch, @Nullable Map<String, Object> claims) {
// Does not approve request so that checks after it can check. Only rejects on fail.

if (routeMatch instanceof MethodBasedRouteMatch) {
MethodBasedRouteMatch methodRoute = ((MethodBasedRouteMatch) routeMatch);

if (methodRoute.hasAnnotation(ExperimentCollaboratorSecured.class)) {
String programId = (String) routeMatch.getVariableValues()
.get("programId");
String experimentId = extractExperimentId(routeMatch);
if (programId == null) {
throw new HttpServerException("Endpoint does not have program id to check roles against");
}
if (experimentId == null) {
throw new HttpServerException("Endpoint does not have experiment id to check roles against");
}

if (!programDAO.existsById(UUID.fromString(programId))) {
throw new HttpStatusException(HttpStatus.NOT_FOUND, "Program does not exist");
}
Optional<BrAPITrial> trial;
try {
trial = brAPITrialDAO.getTrialById(UUID.fromString(programId), UUID.fromString(experimentId));
} catch (ApiException e) {
throw new RuntimeException(e);
} catch (DoesNotExistException e) {
throw new HttpStatusException(HttpStatus.NOT_FOUND, "Experiment does not exist");
}
if( trial.isEmpty()) {
throw new HttpStatusException(HttpStatus.NOT_FOUND, "Experiment does not exist");
}

if (claims != null){
AuthenticatedUser user = securityService.getUser();

return checkAuthorization(user, experimentId, programId);
}

// Rejects if no claims
return SecurityRuleResult.REJECTED;
}
}

return SecurityRuleResult.UNKNOWN;
}

private static String extractExperimentId(@NotNull RouteMatch<?> routeMatch) {
//The endpoints can use either the "experimentId" or "trialId" parameter to pass the experiment ID
String experimentId = (String) routeMatch.getVariableValues()
.get("experimentId");
if( experimentId==null) {
experimentId = (String) routeMatch.getVariableValues()
.get("trialId");
}
return experimentId;
}

private SecurityRuleResult checkAuthorization(AuthenticatedUser authenticatedUser, String experimentId, String programId) {
ProgramUser programUserRole;
try {
programUserRole = authenticatedUser.extractProgramUser(UUID.fromString(programId));
} catch (DoesNotExistException e) {
return SecurityRuleResult.UNKNOWN;
}
if(this.isExperimentCoordinator(programUserRole)){
List<UUID> collaborativeExperimentIds = experimentalCollaboratorDAO.getExperimentIds(programUserRole.getId(), true);
if(collaborativeExperimentIds.contains( UUID.fromString(experimentId)) ){
return SecurityRuleResult.ALLOWED;
}
}
else {
//Allow the next Secured Annotation to be run
return SecurityRuleResult.UNKNOWN;
}
return SecurityRuleResult.REJECTED;
}

private boolean isExperimentCoordinator(ProgramUser programUser){
List<Role> roles = programUser.getRoles();
if( roles.size()!=1 ){ return false; }
String primaryRole = roles.get(0).getDomain();
return (primaryRole != null &&
primaryRole.equals( ProgramSecuredRole.EXPERIMENTAL_COLLABORATOR.toString() )
);
}

@Override
public int getOrder() {
return ORDER;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public HttpResponse<?> createProgramBreedingMethod(@PathVariable UUID programId,
@Get("programs/{programId}/breeding-methods{?inUse}")
@Produces(MediaType.APPLICATION_JSON)
@AddMetadata
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL})
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.PROGRAM_SCOPED_ROLES})
public HttpResponse<Response<DataResponse<ProgramBreedingMethodEntity>>> getProgramBreedingMethods(@PathVariable UUID programId, @QueryValue(defaultValue = "false") Boolean inUse) {
log.debug(String.format("fetching breeding methods for program: %s", programId));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
import io.micronaut.security.rules.SecurityRule;
import lombok.extern.slf4j.Slf4j;
import org.brapi.client.v2.model.exceptions.ApiException;
import org.breedinginsight.api.auth.ProgramSecured;
import org.breedinginsight.api.auth.ProgramSecuredRoleGroup;
import org.breedinginsight.api.auth.*;
import org.breedinginsight.api.model.v1.request.SubEntityDatasetRequest;
import org.breedinginsight.api.model.v1.response.Response;
import org.breedinginsight.brapi.v2.model.request.query.ExperimentExportQuery;
Expand All @@ -22,7 +21,6 @@
import org.breedinginsight.model.Program;
import org.breedinginsight.services.ProgramService;
import org.breedinginsight.services.exceptions.DoesNotExistException;
import org.breedinginsight.services.exceptions.UnprocessableEntityException;
import org.breedinginsight.utilities.response.mappers.ExperimentQueryMapper;

import javax.inject.Inject;
Expand All @@ -47,7 +45,8 @@ public ExperimentController(BrAPITrialService experimentService, ExperimentQuery
}

@Get("/${micronaut.bi.api.version}/programs/{programId}/experiments/{experimentId}/export{?queryParams*}")
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL})
@ExperimentCollaboratorSecured
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.PROGRAM_SCOPED_ROLES})
@Produces(value={"text/csv", "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/octet-stream"})
public HttpResponse<StreamedFile> datasetExport(
@PathVariable("programId") UUID programId, @PathVariable("experimentId") UUID experimentId,
Expand Down Expand Up @@ -75,7 +74,8 @@ public HttpResponse<StreamedFile> datasetExport(
}

@Get("/${micronaut.bi.api.version}/programs/{programId}/experiments/{experimentId}/dataset/{datasetId}{?stats}")
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL})
@ExperimentCollaboratorSecured
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.PROGRAM_SCOPED_ROLES})
@Produces(MediaType.APPLICATION_JSON)
public HttpResponse<Response<Dataset>> getDatasetData(
@PathVariable("programId") UUID programId,
Expand All @@ -102,7 +102,7 @@ public HttpResponse<Response<Dataset>> getDatasetData(
* @return An HttpResponse with a Response object containing the newly created Dataset.
*/
@Post("/${micronaut.bi.api.version}/programs/{programId}/experiments/{experimentId}/dataset")
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL})
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.PROGRAM_SCOPED_ROLES})
@Produces(MediaType.APPLICATION_JSON)
public HttpResponse<Response<Dataset>> createSubEntityDataset(
@PathVariable("programId") UUID programId,
Expand Down Expand Up @@ -132,7 +132,8 @@ public HttpResponse<Response<Dataset>> createSubEntityDataset(
* @throws ApiException if an error occurs while retrieving the datasets.
*/
@Get("/${micronaut.bi.api.version}/programs/{programId}/experiments/{experimentId}/datasets")
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL})
@ExperimentCollaboratorSecured
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.PROGRAM_SCOPED_ROLES})
@Produces(MediaType.APPLICATION_JSON)
public HttpResponse<Response<List<DatasetMetadata>>> getDatasets(
@PathVariable("programId") UUID programId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public JobController(SecurityService securityService, JobService jobService) {
@Get("programs/{programId}/jobs")
@Produces(MediaType.APPLICATION_JSON)
@AddMetadata
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL})
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.PROGRAM_SCOPED_ROLES})
public HttpResponse<Response<DataResponse<Job>>> getProgramJobs(@PathVariable UUID programId) {
log.debug(String.format("fetching jobs for program: %s", programId));
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ public HttpResponse<Response<DataResponse<Program>>> postProgramsSearch(

@Get("/programs/{programId}")
@Produces(MediaType.APPLICATION_JSON)
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL})
@ProgramSecured(roles = {ProgramSecuredRole.SYSTEM_ADMIN, ProgramSecuredRole.READ_ONLY, ProgramSecuredRole.PROGRAM_ADMIN
,ProgramSecuredRole.EXPERIMENTAL_COLLABORATOR })
@AddMetadata
public HttpResponse<Response<Program>> getProgram(@PathVariable UUID programId) {

Expand Down Expand Up @@ -178,7 +179,7 @@ public HttpResponse archiveProgram(@PathVariable UUID programId) {

@Get("/programs/{programId}/users{?queryParams*}")
@Produces(MediaType.APPLICATION_JSON)
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL})
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.PROGRAM_SCOPED_ROLES})
public HttpResponse<Response<DataResponse<ProgramUser>>> getProgramUsers(
@PathVariable UUID programId,
@QueryValue @QueryValid(using = ProgramUserQueryMapper.class) @Valid QueryParams queryParams) {
Expand All @@ -194,7 +195,7 @@ public HttpResponse<Response<DataResponse<ProgramUser>>> getProgramUsers(

@Post("/programs/{programId}/users/search{?queryParams*}")
@Produces(MediaType.APPLICATION_JSON)
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL})
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.PROGRAM_SCOPED_ROLES})
public HttpResponse<Response<DataResponse<ProgramUser>>> searchProgramUsers(
@PathVariable UUID programId,
@QueryValue @QueryValid(using = ProgramUserQueryMapper.class) @Valid QueryParams queryParams,
Expand All @@ -211,7 +212,7 @@ public HttpResponse<Response<DataResponse<ProgramUser>>> searchProgramUsers(

@Get("/programs/{programId}/users/{userId}")
@Produces(MediaType.APPLICATION_JSON)
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL})
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.PROGRAM_SCOPED_ROLES})
@AddMetadata
public HttpResponse<Response<ProgramUser>> getProgramUser(@PathVariable UUID programId, @PathVariable UUID userId) {

Expand Down Expand Up @@ -292,7 +293,7 @@ public HttpResponse archiveProgramUser(@PathVariable UUID programId, @PathVariab

@Get("/programs/{programId}/locations{?queryParams*}")
@Produces(MediaType.APPLICATION_JSON)
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL})
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.PROGRAM_SCOPED_ROLES})
public HttpResponse<Response<DataResponse<ProgramLocation>>> getProgramLocations(
@PathVariable UUID programId,
@QueryValue @QueryValid(using= ProgramLocationQueryMapper.class) @Valid QueryParams queryParams) {
Expand All @@ -311,7 +312,7 @@ public HttpResponse<Response<DataResponse<ProgramLocation>>> getProgramLocations

@Post("/programs/{programId}/locations/search{?queryParams*}")
@Produces(MediaType.APPLICATION_JSON)
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL})
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.PROGRAM_SCOPED_ROLES})
public HttpResponse<Response<DataResponse<ProgramLocation>>> postProgramLocationsSearch(
@PathVariable UUID programId,
@QueryValue @QueryValid(using= ProgramLocationQueryMapper.class) @Valid QueryParams queryParams,
Expand All @@ -335,7 +336,7 @@ public HttpResponse<Response<DataResponse<ProgramLocation>>> postProgramLocation
@Get("/programs/{programId}/locations/{locationId}")
@Produces(MediaType.APPLICATION_JSON)
@AddMetadata
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL})
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.PROGRAM_SCOPED_ROLES})
public HttpResponse<Response<ProgramLocation>> getProgramLocations(@PathVariable UUID programId,
@PathVariable UUID locationId) {

Expand Down Expand Up @@ -423,7 +424,7 @@ public HttpResponse archiveProgramLocation(@PathVariable UUID programId,

@Get("/programs/{programId}/observation-levels")
@Produces(MediaType.APPLICATION_JSON)
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL})
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.PROGRAM_SCOPED_ROLES})
public HttpResponse<Response<DataResponse<ProgramObservationLevel>>> getProgramObservationLevels(@PathVariable UUID programId)
throws DoesNotExistException {
List<ProgramObservationLevel> programObservationLevels = programObservationLevelService.getByProgramId(programId);
Expand Down
Loading