Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/entity permisions graphql #187

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a54720f
Extend graphql schema to show relevant entity roles
assadriaz Sep 25, 2024
6df41a0
Add test coverage in TiamatAuthorizationServiceTest
assadriaz Sep 26, 2024
2db1415
Refactor PublicationDeliveryImporter makes AuthorizationService confi…
assadriaz Oct 8, 2024
bc9c1b2
Refactoring add test
assadriaz Oct 8, 2024
a6ccd0d
Update Test to cover more case of role assignment
assadriaz Oct 18, 2024
f2598f8
Update pom testcontainers dependency
assadriaz Oct 18, 2024
932a0a9
Update Test to cover more case of role assignment
assadriaz Oct 18, 2024
0ff27b4
Add Test to cover usecase user with multiple roles
assadriaz Oct 18, 2024
24ac5d3
update javadocs
assadriaz Oct 28, 2024
673643c
adds entity permission in group of stop places in graphql query
assadriaz Oct 28, 2024
66f3b2f
Refactoring updated DefaultAuthorizationService, to show correct perm…
assadriaz Oct 29, 2024
ddab310
Refactoring updated DefaultAuthorizationService, to show correct perm…
assadriaz Oct 29, 2024
7dcd562
Refactoring updated DefaultAuthorizationService, to show correct perm…
assadriaz Nov 5, 2024
505d6c5
Add entityFilter on allowStops,bannedStops and submodes.
assadriaz Nov 18, 2024
d9515b3
change port in docker compose.yaml
assadriaz Nov 18, 2024
759d27a
use camel case in GraphQLNames
assadriaz Nov 18, 2024
ea9d3c5
use camel case in GraphQLNames
assadriaz Nov 18, 2024
9f2f4be
update filter by type, allow all type attribute
assadriaz Nov 19, 2024
288408a
Revert entityFilter changes
assadriaz Nov 19, 2024
e493a30
Revert entityFilter changes
assadriaz Nov 19, 2024
76d034b
implement filterByRole in getStopTypesOrSubmode method
assadriaz Nov 22, 2024
d7b0443
fix bugs
assadriaz Nov 26, 2024
d4a4c7c
update groupOfStops in DefaultAuthorizationService
assadriaz Nov 29, 2024
4ec1d7f
LocationPermissionsFetcher wip
assadriaz Dec 3, 2024
4b4685b
LocationPermissionsFetcher implementation
assadriaz Dec 3, 2024
f4d292c
fix gosp entity permission
assadriaz Dec 12, 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
2 changes: 1 addition & 1 deletion docker-compose/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ services:
volumes:
- ./spring:/etc/application-config
ports:
- "8777:8777"
- "1888:1888"

db:
image: 'postgis/postgis:13-master'
Expand Down
2 changes: 1 addition & 1 deletion docker-compose/spring/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ changelog.gcp.publish.enabled=false
changelog.publish.enabled=false
authorization.enabled = false
netex.id.valid.prefix.list={TopographicPlace:{'KVE','WOF','OSM','ENT','LAN'},TariffZone:{'*'},FareZone:{'*'},GroupOfTariffZones:{'*'}}
server.port=8777
server.port=1888
tariffzoneLookupService.resetReferences=true
netex.import.enabled.types=MERGE,INITIAL,ID_MATCH,MATCH
jettyMaxThreads=10
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,15 @@
<hazelcast.version>5.2.4</hazelcast.version>
<swagger-jersey2.version>2.2.20</swagger-jersey2.version>
<netex-java-model.version>2.0.14</netex-java-model.version>
<entur.helpers.version>2.26</entur.helpers.version>
<entur.helpers.version>2.31</entur.helpers.version>
<jts-core.version>1.19.0</jts-core.version>
<groovy-all.version>4.0.17</groovy-all.version>
<rest-assured.version>5.4.0</rest-assured.version>
<argLine/>

<xercesImpl.version>2.12.2</xercesImpl.version>
<maven.compiler.args>--add-opens java.base/java.lang=ALL-UNNAMED</maven.compiler.args>
<testcontainers.version>1.19.2</testcontainers.version>
<testcontainers.version>1.20.2</testcontainers.version>
</properties>

<dependencies>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.rutebanken.tiamat.auth;

import org.locationtech.jts.geom.Point;
import org.rutebanken.helper.organisation.RoleAssignment;
import org.rutebanken.tiamat.model.EntityStructure;
import org.springframework.security.access.AccessDeniedException;
Expand All @@ -15,7 +16,7 @@ public interface AuthorizationService {
/**
* Verify that the current user have right to edit any entity?
*/
void verifyCanEditAllEntities();
boolean verifyCanEditAllEntities();


/**
Expand All @@ -35,6 +36,18 @@ public interface AuthorizationService {
*/
void verifyCanDeleteEntities(Collection<? extends EntityStructure> entities);

/**
* Verify that the current user has right to delete the given entity.
*/
boolean canDeleteEntity(EntityStructure entity);

/**
* Verify that the current user has right to edit the given entity.
*/
boolean canEditEntity(EntityStructure entity);

boolean canEditEntity(Point point);

/**
* Return the subset of the roles that the current user holds that apply to this entity.
* */
Expand All @@ -46,6 +59,22 @@ public interface AuthorizationService {
*/
<T extends EntityStructure> boolean canEditEntity(RoleAssignment roleAssignment, T entity);

Set<String> getAllowedStopPlaceTypes(EntityStructure entity);

Set<String> getLocationAllowedStopPlaceTypes(boolean canEdit, Point point);

Set<String> getBannedStopPlaceTypes(EntityStructure entity);

Set<String> getLocationBannedStopPlaceTypes(boolean canEdit,Point point);

Set<String> getAllowedSubmodes(EntityStructure entity);

Set<String> getLocationAllowedSubmodes(boolean canEdit,Point point);

Set<String> getBannedSubmodes(EntityStructure entity);

Set<String> getLocationBannedSubmodes(boolean canEdit,Point point);


boolean isGuest();
}
Original file line number Diff line number Diff line change
@@ -1,44 +1,66 @@
package org.rutebanken.tiamat.auth;

import org.apache.commons.lang3.StringUtils;
import org.locationtech.jts.geom.Point;
import org.rutebanken.helper.organisation.AuthorizationConstants;
import org.rutebanken.helper.organisation.DataScopedAuthorizationService;
import org.rutebanken.helper.organisation.RoleAssignment;
import org.rutebanken.helper.organisation.RoleAssignmentExtractor;
import org.rutebanken.tiamat.auth.check.TopographicPlaceChecker;
import org.rutebanken.tiamat.model.EntityStructure;
import org.springframework.security.access.AccessDeniedException;
import org.rutebanken.tiamat.model.GroupOfStopPlaces;
import org.rutebanken.tiamat.model.StopPlace;
import org.rutebanken.tiamat.service.groupofstopplaces.GroupOfStopPlacesMembersResolver;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;

import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import static org.rutebanken.helper.organisation.AuthorizationConstants.*;
import static org.rutebanken.helper.organisation.AuthorizationConstants.ENTITY_CLASSIFIER_ALL_ATTRIBUTES;
import static org.rutebanken.helper.organisation.AuthorizationConstants.ROLE_DELETE_STOPS;
import static org.rutebanken.helper.organisation.AuthorizationConstants.ROLE_EDIT_STOPS;

public class DefaultAuthorizationService implements AuthorizationService {
private final DataScopedAuthorizationService dataScopedAuthorizationService;
private final boolean authorizationEnabled;
private final RoleAssignmentExtractor roleAssignmentExtractor;
private static final String STOP_PLACE_TYPE = "StopPlaceType";
private static final String SUBMODE = "Submode";
private final TopographicPlaceChecker topographicPlaceChecker;
private final GroupOfStopPlacesMembersResolver groupOfStopPlacesMembersResolver;

public DefaultAuthorizationService(DataScopedAuthorizationService dataScopedAuthorizationService, RoleAssignmentExtractor roleAssignmentExtractor) {
public DefaultAuthorizationService(DataScopedAuthorizationService dataScopedAuthorizationService,
boolean authorizationEnabled,
RoleAssignmentExtractor roleAssignmentExtractor,
TopographicPlaceChecker topographicPlaceChecker, GroupOfStopPlacesMembersResolver groupOfStopPlacesMembersResolver) {
this.dataScopedAuthorizationService = dataScopedAuthorizationService;
this.authorizationEnabled = authorizationEnabled;
this.roleAssignmentExtractor = roleAssignmentExtractor;
}
this.topographicPlaceChecker = topographicPlaceChecker;
this.groupOfStopPlacesMembersResolver = groupOfStopPlacesMembersResolver;
}

@Override
public void verifyCanEditAllEntities() {
verifyCanEditAllEntities(roleAssignmentExtractor.getRoleAssignmentsForUser());
public boolean verifyCanEditAllEntities() {
if(hasNoAuthentications()) {
return false;
}
return verifyCanEditAllEntities(roleAssignmentExtractor.getRoleAssignmentsForUser());
}

void verifyCanEditAllEntities(List<RoleAssignment> roleAssignments) {
if (roleAssignments
boolean verifyCanEditAllEntities(List<RoleAssignment> roleAssignments) {
return roleAssignments
.stream()
.noneMatch(roleAssignment -> ROLE_EDIT_STOPS.equals(roleAssignment.getRole())
&& roleAssignment.getEntityClassifications() != null
&& roleAssignment.getEntityClassifications().get(AuthorizationConstants.ENTITY_TYPE) != null
&& roleAssignment.getEntityClassifications().get(AuthorizationConstants.ENTITY_TYPE).contains(ENTITY_CLASSIFIER_ALL_ATTRIBUTES)
&& StringUtils.isEmpty(roleAssignment.getAdministrativeZone())
)) {
throw new AccessDeniedException("Insufficient privileges for operation");
}
.anyMatch(roleAssignment -> ROLE_EDIT_STOPS.equals(roleAssignment.getRole())
&& roleAssignment.getEntityClassifications() != null
&& roleAssignment.getEntityClassifications().get(AuthorizationConstants.ENTITY_TYPE) != null
&& roleAssignment.getEntityClassifications().get(AuthorizationConstants.ENTITY_TYPE).contains(ENTITY_CLASSIFIER_ALL_ATTRIBUTES)
&& StringUtils.isEmpty(roleAssignment.getAdministrativeZone())
);
}

@Override
Expand Down Expand Up @@ -67,5 +89,143 @@ public <T extends EntityStructure> Set<String> getRelevantRolesForEntity(T entit
return dataScopedAuthorizationService.getRelevantRolesForEntity(entity);
}

@Override
public boolean canDeleteEntity(EntityStructure entity) {
testower marked this conversation as resolved.
Show resolved Hide resolved
return canEditDeleteEntity(entity, ROLE_DELETE_STOPS);
}

@Override
public boolean canEditEntity(EntityStructure entity) {
testower marked this conversation as resolved.
Show resolved Hide resolved
return canEditDeleteEntity(entity, ROLE_EDIT_STOPS);
}

@Override
public boolean canEditEntity(Point point) {
return roleAssignmentExtractor.getRoleAssignmentsForUser().stream()
.filter(roleAssignment -> roleAssignment.getRole().equals(ROLE_EDIT_STOPS))
.anyMatch(roleAssignment -> topographicPlaceChecker.pointMatchesAdministrativeZone(roleAssignment, point));

}

@Override
public Set<String> getAllowedStopPlaceTypes(EntityStructure entity){
return getStopTypesOrSubmode(STOP_PLACE_TYPE, true, entity);
}

@Override
public Set<String> getLocationAllowedStopPlaceTypes(boolean canEdit, Point point) {
return getLocationStopTypesOrSubmode(canEdit,STOP_PLACE_TYPE, true, point);
}

@Override
public Set<String> getBannedStopPlaceTypes(EntityStructure entity) {
if(!dataScopedAuthorizationService.isAuthorized(ROLE_EDIT_STOPS, List.of(entity))) {
return Set.of(ENTITY_CLASSIFIER_ALL_ATTRIBUTES);
}
return getStopTypesOrSubmode(STOP_PLACE_TYPE, false, entity);
}

@Override
public Set<String> getLocationBannedStopPlaceTypes(boolean canEdit, Point point) {
return getLocationStopTypesOrSubmode(canEdit,STOP_PLACE_TYPE, false, point);
}

@Override
public Set<String> getAllowedSubmodes(EntityStructure entity) {
return getStopTypesOrSubmode(SUBMODE, true, entity);
}

@Override
public Set<String> getLocationAllowedSubmodes(boolean canEdit, Point point) {
return getLocationStopTypesOrSubmode(canEdit,SUBMODE, true, point);
}

@Override
public Set<String> getBannedSubmodes(EntityStructure entity) {
if(!dataScopedAuthorizationService.isAuthorized(ROLE_EDIT_STOPS, List.of(entity))) {
return Set.of(ENTITY_CLASSIFIER_ALL_ATTRIBUTES);
}
return getStopTypesOrSubmode(SUBMODE, false, entity);
}

@Override
public Set<String> getLocationBannedSubmodes(boolean canEdit, Point point) {
return getLocationStopTypesOrSubmode(canEdit,SUBMODE, false, point);
}

@Override
public boolean isGuest() {
if (hasNoAuthentications()) {
return true;
}
return roleAssignmentExtractor.getRoleAssignmentsForUser().isEmpty();
}

private Set<String> getStopTypesOrSubmode(String type, boolean isAllowed, EntityStructure entity) {
if (hasNoAuthentications()) {
return Set.of();
}
return roleAssignmentExtractor.getRoleAssignmentsForUser().stream()
.filter(roleAssignment -> canEditDeleteEntity(entity,roleAssignment.getRole()))
.filter(roleAssignment -> roleAssignment.getEntityClassifications() != null)
.filter(roleAssignment -> topographicPlaceChecker.entityMatchesAdministrativeZone(roleAssignment, entity))
.filter(roleAssignment -> roleAssignment.getEntityClassifications().get(type) != null)
.map(roleAssignment -> roleAssignment.getEntityClassifications().get(type))
.flatMap(List::stream)
.filter(types -> isAllowed != types.startsWith("!"))
.map(types -> isAllowed ? types : types.substring(1))
.collect(Collectors.toSet());
}

private Set<String> getLocationStopTypesOrSubmode(boolean canEdit, String type, boolean isAllowed, Point point) {
if (hasNoAuthentications()) {
return Set.of();
}
if (!canEdit && !isAllowed) {
return Set.of(ENTITY_CLASSIFIER_ALL_ATTRIBUTES);
}
return roleAssignmentExtractor.getRoleAssignmentsForUser().stream()
.filter(roleAssignment -> roleAssignment.getEntityClassifications() != null)
.filter(roleAssignment -> topographicPlaceChecker.entityMatchesAdministrativeZone(roleAssignment,point ))
.filter(roleAssignment -> roleAssignment.getEntityClassifications().get(type) != null)
.map(roleAssignment -> roleAssignment.getEntityClassifications().get(type))
.flatMap(List::stream)
.filter(types -> isAllowed != types.startsWith("!"))
.map(types -> isAllowed ? types : types.substring(1))
.collect(Collectors.toSet());
}

private boolean filterByRole(RoleAssignment roleAssignment,Object entity) {
if (entity instanceof GroupOfStopPlaces groupOfStopPlaces) {
final List<StopPlace> gospMembers = groupOfStopPlacesMembersResolver.resolve(groupOfStopPlaces);
dataScopedAuthorizationService.isAuthorized(roleAssignment.getRole(), gospMembers);
}

return dataScopedAuthorizationService.authorized(roleAssignment, entity, ROLE_EDIT_STOPS)
|| dataScopedAuthorizationService.authorized(roleAssignment, entity, ROLE_DELETE_STOPS);

}


private boolean hasNoAuthentications() {
if(!authorizationEnabled) {
return true;
}
final Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return !(auth instanceof JwtAuthenticationToken);
}

private boolean canEditDeleteEntity(EntityStructure entity, String role) {
if (hasNoAuthentications()) {
return false;
}

if (entity instanceof GroupOfStopPlaces groupOfStopPlaces) {
final List<StopPlace> gospMembers = groupOfStopPlacesMembersResolver.resolve(groupOfStopPlaces);
return gospMembers.stream()
.allMatch(stopPlace -> dataScopedAuthorizationService.isAuthorized(role, List.of(stopPlace)));
} else {
return dataScopedAuthorizationService.isAuthorized(role, List.of(entity));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

package org.rutebanken.tiamat.auth.check;

import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.rutebanken.helper.organisation.AdministrativeZoneChecker;
import org.rutebanken.helper.organisation.RoleAssignment;
Expand Down Expand Up @@ -71,4 +72,26 @@ public boolean entityMatchesAdministrativeZone(RoleAssignment roleAssignment, Ob
logger.warn("Cannot look for matches in topographic place for entity {} ({})", entity, entity.getClass().getSimpleName());
return true;
}

public boolean pointMatchesAdministrativeZone(RoleAssignment roleAssignment, Point point) {
if (roleAssignment.getAdministrativeZone() != null) {
TopographicPlace topographicPlace = topographicPlaceRepository.findFirstByNetexIdOrderByVersionDesc(roleAssignment.getAdministrativeZone());
if (topographicPlace == null) {
logger.warn("RoleAssignment contains unknown adminZone reference: {}. Will not allow authorization", roleAssignment.getAdministrativeZone());
return false;
}
Polygon polygon = topographicPlace.getPolygon();

if (polygon.contains(point)) {
logger.debug("Polygon for topographic place {}-{} contains point for {}", topographicPlace.getNetexId(), topographicPlace.getVersion(), point);
return true;
} else {
logger.warn("No polygon match for topographic place {}-{} and point {}", topographicPlace.getNetexId(), topographicPlace.getVersion(), point);
return false;
}

}
logger.warn("Cannot look for matches in topographic place for point {} ({})", point, point.getClass().getSimpleName());
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@
import org.rutebanken.helper.organisation.DataScopedAuthorizationService;
import org.rutebanken.helper.organisation.ReflectionAuthorizationService;
import org.rutebanken.helper.organisation.RoleAssignmentExtractor;
import org.rutebanken.tiamat.auth.AuthorizationService;
import org.rutebanken.tiamat.auth.DefaultAuthorizationService;
import org.rutebanken.tiamat.auth.TiamatEntityResolver;
import org.rutebanken.tiamat.auth.check.TiamatOriganisationChecker;
import org.rutebanken.tiamat.auth.check.TopographicPlaceChecker;
import org.rutebanken.tiamat.auth.AuthorizationService;
import org.rutebanken.tiamat.auth.DefaultAuthorizationService;
import org.rutebanken.tiamat.service.groupofstopplaces.GroupOfStopPlacesMembersResolver;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -39,8 +40,16 @@ public class AuthorizationServiceConfig {


@Bean
public AuthorizationService authorizationService(DataScopedAuthorizationService dataScopedAuthorizationService, RoleAssignmentExtractor roleAssignmentExtractor) {
return new DefaultAuthorizationService(dataScopedAuthorizationService, roleAssignmentExtractor);
public AuthorizationService authorizationService(DataScopedAuthorizationService dataScopedAuthorizationService,
@Value("${authorization.enabled:true}") boolean authorizationEnabled,
RoleAssignmentExtractor roleAssignmentExtractor,
TopographicPlaceChecker topographicPlaceChecker,
GroupOfStopPlacesMembersResolver groupOfStopPlacesMembersResolver) {
return new DefaultAuthorizationService(dataScopedAuthorizationService,
authorizationEnabled,
roleAssignmentExtractor,
topographicPlaceChecker,
groupOfStopPlacesMembersResolver);
}

@Bean
Expand Down
Loading