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 35 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
1900236
Extend graphql schema to show relevant entity roles
assadriaz Sep 25, 2024
6eb2467
Add test coverage in TiamatAuthorizationServiceTest
assadriaz Sep 26, 2024
df3f6d4
Refactoring add test
assadriaz Oct 8, 2024
b778d56
Update Test to cover more case of role assignment
assadriaz Oct 18, 2024
c269cfc
Update pom testcontainers dependency
assadriaz Oct 18, 2024
de2fa23
Update Test to cover more case of role assignment
assadriaz Oct 18, 2024
9d52508
Add Test to cover usecase user with multiple roles
assadriaz Oct 18, 2024
f025b01
update javadocs
assadriaz Oct 28, 2024
132e39f
adds entity permission in group of stop places in graphql query
assadriaz Oct 28, 2024
0381bab
Refactoring updated DefaultAuthorizationService, to show correct perm…
assadriaz Oct 29, 2024
05e506e
Refactoring updated DefaultAuthorizationService, to show correct perm…
assadriaz Oct 29, 2024
3696810
Refactoring updated DefaultAuthorizationService, to show correct perm…
assadriaz Nov 5, 2024
c6f60a5
Add entityFilter on allowStops,bannedStops and submodes.
assadriaz Nov 18, 2024
fa4143a
change port in docker compose.yaml
assadriaz Nov 18, 2024
a83b0e2
use camel case in GraphQLNames
assadriaz Nov 18, 2024
4a17420
use camel case in GraphQLNames
assadriaz Nov 18, 2024
936e1f3
update filter by type, allow all type attribute
assadriaz Nov 19, 2024
da41e1a
Revert entityFilter changes
assadriaz Nov 19, 2024
c8cf143
Revert entityFilter changes
assadriaz Nov 19, 2024
68d96d4
implement filterByRole in getStopTypesOrSubmode method
assadriaz Nov 22, 2024
a0969f8
fix bugs
assadriaz Nov 26, 2024
67faa9f
update groupOfStops in DefaultAuthorizationService
assadriaz Nov 29, 2024
0e2808e
LocationPermissionsFetcher wip
assadriaz Dec 3, 2024
5f0f905
LocationPermissionsFetcher implementation
assadriaz Dec 3, 2024
3082ecc
fix gosp entity permission
assadriaz Dec 12, 2024
b4a433d
fix bug not auth error on guest user
assadriaz Jan 27, 2025
5972bb1
add test to test no auth entity permision on stopplace graphql
assadriaz Jan 27, 2025
e350f20
Update EntityPermissionsFetcher, filter allowed stop places and submo…
assadriaz Jan 28, 2025
b4ac566
Update EntityPermissionsFetcher, filter allowed stop places and submo…
assadriaz Feb 3, 2025
0f48247
Refactoring removed unused method
assadriaz Feb 26, 2025
2030a71
Update DefaultAuthorizationService to check all child stop authorizat…
assadriaz Feb 27, 2025
7926e53
Refactoring, removes AuthorizationCheck endpoint in graphql and imple…
assadriaz Feb 27, 2025
b044cff
Refactor AuthorizationService, remove canEditEntity(RoleAssignment ro…
assadriaz Feb 27, 2025
10eefd0
Refactor EntityPermissions, replace string with StopPlace Submode Enum.
assadriaz Mar 3, 2025
5c80226
Refactor EntityPermissions and AuthorizationService
assadriaz Mar 3, 2025
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.23</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>
<rutebanken-storage.version>4.7</rutebanken-storage.version>
</properties>

Expand Down
35 changes: 27 additions & 8 deletions src/main/java/org/rutebanken/tiamat/auth/AuthorizationService.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.rutebanken.tiamat.auth;

import org.rutebanken.helper.organisation.RoleAssignment;
import org.locationtech.jts.geom.Point;
import org.rutebanken.tiamat.diff.generic.SubmodeEnumuration;
import org.rutebanken.tiamat.model.EntityStructure;
import org.rutebanken.tiamat.model.StopTypeEnumeration;
import org.springframework.security.access.AccessDeniedException;

import java.util.Collection;
Expand All @@ -15,7 +17,7 @@ public interface AuthorizationService {
/**
* Verify that the current user have right to edit any entity?
*/
void verifyCanEditAllEntities();
boolean verifyCanEditAllEntities();


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

/**
* Return the subset of the roles that the current user holds that apply to this entity.
* */
<T extends EntityStructure> Set<String> getRelevantRolesForEntity(T entity);
* Verify that the current user has right to delete the given entity.
*/
boolean canDeleteEntity(EntityStructure entity);

/**
* Does the role assignment give edit right on the given entity?
* (for unit tests only)
* Verify that the current user has right to edit the given entity.
*/
<T extends EntityStructure> boolean canEditEntity(RoleAssignment roleAssignment, T entity);
boolean canEditEntity(EntityStructure entity);

boolean canEditEntity(Point point);

Set<StopTypeEnumeration> getAllowedStopPlaceTypes(EntityStructure entity);

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

Set<StopTypeEnumeration> getBannedStopPlaceTypes(EntityStructure entity);

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

Set<SubmodeEnumuration> getAllowedSubmodes(EntityStructure entity);

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

Set<SubmodeEnumuration> getBannedSubmodes(EntityStructure entity);

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


boolean isGuest();
}
Original file line number Diff line number Diff line change
@@ -1,55 +1,76 @@
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.diff.generic.SubmodeEnumuration;
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.model.StopTypeEnumeration;
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.Arrays;
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
public boolean canEditEntities(Collection<? extends EntityStructure> entities) {
return dataScopedAuthorizationService.isAuthorized(ROLE_EDIT_STOPS, entities);
}

@Override
public <T extends EntityStructure> boolean canEditEntity(RoleAssignment roleAssignment, T entity) {
return dataScopedAuthorizationService.authorized(roleAssignment, entity, ROLE_EDIT_STOPS);
}

@Override
public void verifyCanEditEntities(Collection<? extends EntityStructure> entities) {
Expand All @@ -63,9 +84,178 @@ public void verifyCanDeleteEntities(Collection<? extends EntityStructure> entiti
}

@Override
public <T extends EntityStructure> Set<String> getRelevantRolesForEntity(T entity) {
return dataScopedAuthorizationService.getRelevantRolesForEntity(entity);
public boolean canDeleteEntity(EntityStructure entity) {
return canEditDeleteEntity(entity, ROLE_DELETE_STOPS);
}

@Override
public boolean canEditEntity(EntityStructure entity) {
return canEditDeleteEntity(entity, ROLE_EDIT_STOPS);
}

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

}

@Override
public Set<StopTypeEnumeration> getAllowedStopPlaceTypes(EntityStructure entity){
final Set<String> allowedStopTypes = getStopTypesOrSubmode(STOP_PLACE_TYPE, true, entity);

return convertToStopTypeEnumeration(allowedStopTypes);

}

private Set<StopTypeEnumeration> convertToStopTypeEnumeration(Set<String> stopTypes) {

if(stopTypes.contains("*")){
return Set.of();
}
return stopTypes.stream()
.map(StopTypeEnumeration::fromValue)
.collect(Collectors.toSet());
}

private Set<SubmodeEnumuration> convertToSubmodeEnumeration(Set<String> submodes) {

if(submodes.contains("*")) {
return Set.of();
}
return submodes.stream()
.map(SubmodeEnumuration::fromValue)
.collect(Collectors.toSet());
}

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

@Override
public Set<StopTypeEnumeration> getBannedStopPlaceTypes(EntityStructure entity) {


if(hasNoAuthentications() || !dataScopedAuthorizationService.isAuthorized(ROLE_EDIT_STOPS, List.of(entity))) {
return convertToStopTypeEnumeration(Set.of("*"));
}
final Set<String> stopType = getStopTypesOrSubmode(STOP_PLACE_TYPE, false, entity);
return convertToStopTypeEnumeration(stopType);
}

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

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

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

@Override
public Set<SubmodeEnumuration> getBannedSubmodes(EntityStructure entity) {
if(hasNoAuthentications() || !dataScopedAuthorizationService.isAuthorized(ROLE_EDIT_STOPS, List.of(entity))) {
return convertToSubmodeEnumeration(Set.of("*"));
}
final Set<String> submode = getStopTypesOrSubmode(SUBMODE, false, entity);
return convertToSubmodeEnumeration(submode);
}

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

@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("*");
}
Set<String> stopTypesSubmodes = 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());
if (canEdit && stopTypesSubmodes.isEmpty() && isAllowed) {
stopTypesSubmodes.add(ENTITY_CLASSIFIER_ALL_ATTRIBUTES);
}
return stopTypesSubmodes;
}

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 {
if(entity instanceof StopPlace stopPlace) {
if(!stopPlace.getChildren().isEmpty()) {
return stopPlace.getChildren().stream()
.allMatch(child -> dataScopedAuthorizationService.isAuthorized(role, List.of(child)));
} else {
return dataScopedAuthorizationService.isAuthorized(role, List.of(stopPlace));
}

}
return dataScopedAuthorizationService.isAuthorized(role, List.of(entity));
}
}
}
Loading
Loading