diff --git a/docker-compose/compose.yaml b/docker-compose/compose.yaml index cadd1beaa..ef705a42b 100644 --- a/docker-compose/compose.yaml +++ b/docker-compose/compose.yaml @@ -23,7 +23,7 @@ services: volumes: - ./spring:/etc/application-config ports: - - "8777:8777" + - "1888:1888" db: image: 'postgis/postgis:13-master' diff --git a/docker-compose/spring/application.properties b/docker-compose/spring/application.properties index d10abc1e5..e97a00a99 100644 --- a/docker-compose/spring/application.properties +++ b/docker-compose/spring/application.properties @@ -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 diff --git a/pom.xml b/pom.xml index ab225fd72..eab4c7d5b 100644 --- a/pom.xml +++ b/pom.xml @@ -58,7 +58,7 @@ 5.2.4 2.2.20 2.0.14 - 2.26 + 2.31 1.19.0 4.0.17 5.4.0 @@ -66,7 +66,7 @@ 2.12.2 --add-opens java.base/java.lang=ALL-UNNAMED - 1.19.2 + 1.20.2 diff --git a/src/main/java/org/rutebanken/tiamat/auth/AuthorizationService.java b/src/main/java/org/rutebanken/tiamat/auth/AuthorizationService.java index 9ce5317fe..8f036ac6d 100644 --- a/src/main/java/org/rutebanken/tiamat/auth/AuthorizationService.java +++ b/src/main/java/org/rutebanken/tiamat/auth/AuthorizationService.java @@ -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; @@ -15,7 +16,7 @@ public interface AuthorizationService { /** * Verify that the current user have right to edit any entity? */ - void verifyCanEditAllEntities(); + boolean verifyCanEditAllEntities(); /** @@ -35,6 +36,18 @@ public interface AuthorizationService { */ void verifyCanDeleteEntities(Collection 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. * */ @@ -46,6 +59,22 @@ public interface AuthorizationService { */ boolean canEditEntity(RoleAssignment roleAssignment, T entity); + Set getAllowedStopPlaceTypes(Object entity); + + Set getLocationAllowedStopPlaceTypes(boolean canEdit, Point point); + + Set getBannedStopPlaceTypes(Object entity); + + Set getLocationBannedStopPlaceTypes(boolean canEdit,Point point); + + Set getAllowedSubmodes(Object entity); + + Set getLocationAllowedSubmodes(boolean canEdit,Point point); + + Set getBannedSubmodes(Object entity); + + Set getLocationBannedSubmodes(boolean canEdit,Point point); + boolean isGuest(); } diff --git a/src/main/java/org/rutebanken/tiamat/auth/DefaultAuthorizationService.java b/src/main/java/org/rutebanken/tiamat/auth/DefaultAuthorizationService.java index 515046595..57576b021 100644 --- a/src/main/java/org/rutebanken/tiamat/auth/DefaultAuthorizationService.java +++ b/src/main/java/org/rutebanken/tiamat/auth/DefaultAuthorizationService.java @@ -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 roleAssignments) { - if (roleAssignments + boolean verifyCanEditAllEntities(List 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 @@ -67,5 +89,138 @@ public Set getRelevantRolesForEntity(T entit return dataScopedAuthorizationService.getRelevantRolesForEntity(entity); } + @Override + 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) { + return roleAssignmentExtractor.getRoleAssignmentsForUser().stream() + .filter(roleAssignment -> roleAssignment.getRole().equals(ROLE_EDIT_STOPS)) + .anyMatch(roleAssignment -> topographicPlaceChecker.pointMatchesAdministrativeZone(roleAssignment, point)); + + } + + @Override + public Set getAllowedStopPlaceTypes(Object entity){ + return getStopTypesOrSubmode(STOP_PLACE_TYPE, true, entity); + } + @Override + public Set getLocationAllowedStopPlaceTypes(boolean canEdit, Point point) { + return getLocationStopTypesOrSubmode(canEdit,STOP_PLACE_TYPE, true, point); + } + + @Override + public Set getBannedStopPlaceTypes(Object 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 getLocationBannedStopPlaceTypes(boolean canEdit, Point point) { + return getLocationStopTypesOrSubmode(canEdit,STOP_PLACE_TYPE, false, point); + } + + @Override + public Set getAllowedSubmodes(Object entity) { + return getStopTypesOrSubmode(SUBMODE, true, entity); + } + + @Override + public Set getLocationAllowedSubmodes(boolean canEdit, Point point) { + return getLocationStopTypesOrSubmode(canEdit,SUBMODE, true, point); + } + + @Override + public Set getBannedSubmodes(Object 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 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 getStopTypesOrSubmode(String type, boolean isAllowed, Object entity) { + if (hasNoAuthentications()) { + return Set.of(); + } + return roleAssignmentExtractor.getRoleAssignmentsForUser().stream() + .filter(roleAssignment -> filterByRole(roleAssignment,entity)) + .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 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) { + 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 gospMembers = groupOfStopPlacesMembersResolver.resolve(groupOfStopPlaces); + return gospMembers.stream() + .allMatch(stopPlace -> dataScopedAuthorizationService.isAuthorized(role, List.of(stopPlace))); + } else { + return dataScopedAuthorizationService.isAuthorized(role, List.of(entity)); + } + } } diff --git a/src/main/java/org/rutebanken/tiamat/auth/check/TopographicPlaceChecker.java b/src/main/java/org/rutebanken/tiamat/auth/check/TopographicPlaceChecker.java index ab60968c7..cfb285e38 100644 --- a/src/main/java/org/rutebanken/tiamat/auth/check/TopographicPlaceChecker.java +++ b/src/main/java/org/rutebanken/tiamat/auth/check/TopographicPlaceChecker.java @@ -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; @@ -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; + } } diff --git a/src/main/java/org/rutebanken/tiamat/config/AuthorizationServiceConfig.java b/src/main/java/org/rutebanken/tiamat/config/AuthorizationServiceConfig.java index e0a68fda2..a762766e0 100644 --- a/src/main/java/org/rutebanken/tiamat/config/AuthorizationServiceConfig.java +++ b/src/main/java/org/rutebanken/tiamat/config/AuthorizationServiceConfig.java @@ -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; @@ -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 diff --git a/src/main/java/org/rutebanken/tiamat/importer/PublicationDeliveryImporter.java b/src/main/java/org/rutebanken/tiamat/importer/PublicationDeliveryImporter.java index 1637e4920..dd942eb02 100644 --- a/src/main/java/org/rutebanken/tiamat/importer/PublicationDeliveryImporter.java +++ b/src/main/java/org/rutebanken/tiamat/importer/PublicationDeliveryImporter.java @@ -34,6 +34,8 @@ import org.slf4j.LoggerFactory; import org.slf4j.MDC; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Service; import java.util.Timer; @@ -59,6 +61,7 @@ public class PublicationDeliveryImporter { private final TopographicPlaceImportHandler topographicPlaceImportHandler; private final BackgroundJobs backgroundJobs; private final AuthorizationService authorizationService; + private final boolean authorizationEnabled; @Autowired public PublicationDeliveryImporter(PublicationDeliveryHelper publicationDeliveryHelper, NetexMapper netexMapper, @@ -69,7 +72,9 @@ public PublicationDeliveryImporter(PublicationDeliveryHelper publicationDelivery GroupOfTariffZonesImportHandler groupOfTariffZonesImportHandler, StopPlaceImportHandler stopPlaceImportHandler, ParkingsImportHandler parkingsImportHandler, - BackgroundJobs backgroundJobs, AuthorizationService authorizationService) { + BackgroundJobs backgroundJobs, + AuthorizationService authorizationService, + @Value("${authorization.enabled:true}") boolean authorizationEnabled) { this.publicationDeliveryHelper = publicationDeliveryHelper; this.parkingsImportHandler = parkingsImportHandler; this.publicationDeliveryCreator = publicationDeliveryCreator; @@ -80,6 +85,7 @@ public PublicationDeliveryImporter(PublicationDeliveryHelper publicationDelivery this.stopPlaceImportHandler = stopPlaceImportHandler; this.backgroundJobs = backgroundJobs; this.authorizationService = authorizationService; + this.authorizationEnabled = authorizationEnabled; } @@ -89,8 +95,10 @@ public PublicationDeliveryStructure importPublicationDelivery(PublicationDeliver @SuppressWarnings("unchecked") public PublicationDeliveryStructure importPublicationDelivery(PublicationDeliveryStructure incomingPublicationDelivery, ImportParams importParams) { + if(authorizationEnabled && !authorizationService.verifyCanEditAllEntities()){ + throw new AccessDeniedException("Insufficient privileges for operation"); + } - authorizationService.verifyCanEditAllEntities(); if (incomingPublicationDelivery.getDataObjects() == null) { String responseMessage = "Received publication delivery but it does not contain any data objects."; diff --git a/src/main/java/org/rutebanken/tiamat/model/authorization/EntityPermissions.java b/src/main/java/org/rutebanken/tiamat/model/authorization/EntityPermissions.java new file mode 100644 index 000000000..f69ab5120 --- /dev/null +++ b/src/main/java/org/rutebanken/tiamat/model/authorization/EntityPermissions.java @@ -0,0 +1,23 @@ +package org.rutebanken.tiamat.model.authorization; + +import java.util.Collections; +import java.util.Set; + +public class EntityPermissions { + private final Set allowedStopPlaceTypes; + private final Set bannedStopPlaceTypes; + private final Set allowedSubmodes; + private final Set bannedSubmodes; + private boolean canEdit; + private boolean canDelete; + + public EntityPermissions(boolean canEdit, boolean canDelete, Set allowedStopPlaceTypes, Set bannedStopPlaceTypes, Set allowedSubmodes, Set bannedSubmodes) { + this.canEdit = canEdit; + this.canDelete = canDelete; + this.allowedStopPlaceTypes = allowedStopPlaceTypes == null ? Collections.emptySet() : allowedStopPlaceTypes; + this.bannedStopPlaceTypes = bannedStopPlaceTypes == null ? Collections.emptySet() : bannedStopPlaceTypes; + this.allowedSubmodes = allowedSubmodes == null ? Collections.emptySet() : allowedSubmodes; + this.bannedSubmodes = bannedSubmodes == null ? Collections.emptySet() : bannedSubmodes; + } + +} diff --git a/src/main/java/org/rutebanken/tiamat/model/authorization/UserPermissions.java b/src/main/java/org/rutebanken/tiamat/model/authorization/UserPermissions.java new file mode 100644 index 000000000..3adeafb64 --- /dev/null +++ b/src/main/java/org/rutebanken/tiamat/model/authorization/UserPermissions.java @@ -0,0 +1,4 @@ +package org.rutebanken.tiamat.model.authorization; + +public record UserPermissions(boolean isGuest, boolean allowNewStopEverywhere) { +} diff --git a/src/main/java/org/rutebanken/tiamat/rest/graphql/GraphQLNames.java b/src/main/java/org/rutebanken/tiamat/rest/graphql/GraphQLNames.java index e857131ff..38fdee08b 100644 --- a/src/main/java/org/rutebanken/tiamat/rest/graphql/GraphQLNames.java +++ b/src/main/java/org/rutebanken/tiamat/rest/graphql/GraphQLNames.java @@ -158,6 +158,16 @@ public class GraphQLNames { public static final String GROUP_OF_STOP_PLACES = "groupOfStopPlaces"; public static final String STOP_PLACE_GROUPS = "groups"; + + public static final String PERMISSIONS = "permissions"; + public static final String ENTITY_PERMISSIONS= "entityPermissions"; + public static final String OUTPUT_TYPE_ENTITY_PERMISSIONS= "EntityPermissions"; + public static final String USER_PERMISSIONS= "userPermissions"; + public static final String OUTPUT_TYPE_USER_PERMISSIONS= "UserPermissions"; + public static final String USER_CONTEXT= "userContext"; + public static final String LOCATION_PERMISSIONS= "locationPermissions"; + + public static final String GROUP_OF_TARIFF_ZONES = "groupOfTariffZones"; public static final String GROUP_OF_TARIFF_ZONES_MEMBERS = "members"; public static final String OUTPUT_TYPE_GROUP_OF_TARIFF_ZONES ="GroupOfTariffZones"; @@ -344,6 +354,9 @@ public class GraphQLNames { public static final String ONLY_MONOMODAL_STOPPLACES = "onlyMonomodalStopPlaces"; public static final String ONLY_MONOMODAL_STOPPLACES_DESCRIPTION = "Set to true to only return mono modal stop places."; + public static final String LONGITUDE = "longitude"; + public static final String LATITUDE = "latitude"; + public static final String LONGITUDE_MIN = "lonMin"; public static final String LATITUDE_MIN = "latMin"; public static final String LONGITUDE_MAX = "lonMax"; diff --git a/src/main/java/org/rutebanken/tiamat/rest/graphql/StopPlaceRegisterGraphQLSchema.java b/src/main/java/org/rutebanken/tiamat/rest/graphql/StopPlaceRegisterGraphQLSchema.java index ef399292a..4925e55e1 100644 --- a/src/main/java/org/rutebanken/tiamat/rest/graphql/StopPlaceRegisterGraphQLSchema.java +++ b/src/main/java/org/rutebanken/tiamat/rest/graphql/StopPlaceRegisterGraphQLSchema.java @@ -61,14 +61,17 @@ import org.rutebanken.tiamat.model.identification.IdentifiedEntity; import org.rutebanken.tiamat.repository.TopographicPlaceRepository; import org.rutebanken.tiamat.rest.graphql.fetchers.AuthorizationCheckDataFetcher; +import org.rutebanken.tiamat.rest.graphql.fetchers.EntityPermissionsFetcher; import org.rutebanken.tiamat.rest.graphql.fetchers.FareZoneAuthoritiesFetcher; import org.rutebanken.tiamat.rest.graphql.fetchers.GroupOfStopPlacesMembersFetcher; import org.rutebanken.tiamat.rest.graphql.fetchers.GroupOfStopPlacesPurposeOfGroupingFetcher; import org.rutebanken.tiamat.rest.graphql.fetchers.KeyValuesDataFetcher; +import org.rutebanken.tiamat.rest.graphql.fetchers.LocationPermissionsFetcher; import org.rutebanken.tiamat.rest.graphql.fetchers.PolygonFetcher; import org.rutebanken.tiamat.rest.graphql.fetchers.StopPlaceFareZoneFetcher; import org.rutebanken.tiamat.rest.graphql.fetchers.StopPlaceTariffZoneFetcher; import org.rutebanken.tiamat.rest.graphql.fetchers.TagFetcher; +import org.rutebanken.tiamat.rest.graphql.fetchers.UserPermissionsFetcher; import org.rutebanken.tiamat.rest.graphql.mappers.GeometryMapper; import org.rutebanken.tiamat.rest.graphql.mappers.ValidBetweenMapper; import org.rutebanken.tiamat.rest.graphql.operations.MultiModalityOperationsBuilder; @@ -255,6 +258,12 @@ public class StopPlaceRegisterGraphQLSchema { @Autowired private StopPlaceTariffZoneFetcher stopPlaceTariffZoneFetcher; + @Autowired + private EntityPermissionsFetcher entityPermissionsFetcher; + + @Autowired + private LocationPermissionsFetcher locationPermissionsFetcher; + @Autowired private StopPlaceFareZoneFetcher stopPlaceFareZoneFetcher; @@ -351,6 +360,9 @@ public class StopPlaceRegisterGraphQLSchema { @Autowired private TopographicPlaceRepository topographicPlaceRepository; + @Autowired + private UserPermissionsFetcher userPermissionsFetcher; + @PostConstruct public void init() { @@ -382,6 +394,46 @@ public void init() { ); GraphQLObjectType validBetweenObjectType = createValidBetweenObjectType(); + GraphQLObjectType userPermissionsObjectType = newObject() + .name(OUTPUT_TYPE_USER_PERMISSIONS) + .field(newFieldDefinition() + .name("isGuest") + .type(GraphQLBoolean) + .build()) + .field(newFieldDefinition() + .name("allowNewStopEverywhere") + .type(GraphQLBoolean) + .build()) + .build(); + + GraphQLObjectType entityPermissionObjectType = newObject() + .name(OUTPUT_TYPE_ENTITY_PERMISSIONS) + .field(newFieldDefinition() + .name("canEdit") + .type(GraphQLBoolean) + .build()) + .field(newFieldDefinition() + .name("canDelete") + .type(GraphQLBoolean) + .build()) + + .field(newFieldDefinition() + .name("allowedStopPlaceTypes") + .type(new GraphQLList(GraphQLString)) + .build()) + .field(newFieldDefinition() + .name("bannedStopPlaceTypes") + .type(new GraphQLList(GraphQLString)) + .build()) + .field(newFieldDefinition() + .name("allowedSubmodes") + .type(new GraphQLList(GraphQLString)) + .build()) + .field(newFieldDefinition() + .name("bannedSubmodes") + .type(new GraphQLList(GraphQLString)) + .build()) + .build(); List zoneCommandFieldList = zoneCommonFieldListCreator.create(validBetweenObjectType); @@ -397,7 +449,7 @@ public void init() { MutableTypeResolver stopPlaceTypeResolver = new MutableTypeResolver(); - List stopPlaceInterfaceFields = stopPlaceInterfaceCreator.createCommonInterfaceFields(tariffZoneObjectType,fareZoneObjectType, topographicPlaceObjectType, validBetweenObjectType); + List stopPlaceInterfaceFields = stopPlaceInterfaceCreator.createCommonInterfaceFields(tariffZoneObjectType,fareZoneObjectType, topographicPlaceObjectType, validBetweenObjectType, entityPermissionObjectType); GraphQLInterfaceType stopPlaceInterface = stopPlaceInterfaceCreator.createInterface(stopPlaceInterfaceFields, commonFieldsList); GraphQLObjectType stopPlaceObjectType = stopPlaceObjectTypeCreator.create(stopPlaceInterface, stopPlaceInterfaceFields, commonFieldsList, quayObjectType); @@ -415,7 +467,7 @@ public void init() { }); GraphQLObjectType purposeOfGroupingType =purposeOfGroupingTypeCreator.create(); - GraphQLObjectType groupOfStopPlacesObjectType = groupOfStopPlaceObjectTypeCreator.create(stopPlaceInterface, purposeOfGroupingType); + GraphQLObjectType groupOfStopPlacesObjectType = groupOfStopPlaceObjectTypeCreator.create(stopPlaceInterface, purposeOfGroupingType, entityPermissionObjectType); GraphQLObjectType groupOfTariffZonesObjectType = groupOfTariffZonesObjectTypeCreator.create(); GraphQLObjectType addressablePlaceObjectType = createAddressablePlaceObjectType(commonFieldsList); @@ -434,6 +486,7 @@ public void init() { .description(ALL_VERSIONS_ARG_DESCRIPTION) .build(); + GraphQLObjectType stopPlaceRegisterQuery = newObject() .name(STOPPLACES_REGISTER) .description("Query and search for data") @@ -521,6 +574,17 @@ public void init() { .type(new GraphQLList(GraphQLString)) .description("List all fare zone authorities.") .build()) + .field(newFieldDefinition() + .name(USER_PERMISSIONS) + .description("User permissions") + .type(userPermissionsObjectType) + + .build()) + .field(newFieldDefinition() + .name(LOCATION_PERMISSIONS) + .description("Location permissions") + .type(entityPermissionObjectType) + .arguments(createLocationArguments())) .build(); @@ -629,6 +693,10 @@ public GraphQLCodeRegistry buildCodeRegistry(TypeResolver stopPlaceTypeResolver) registerDataFetcher(codeRegistryBuilder, OUTPUT_TYPE_QUAY, IMPORTED_ID, getOriginalIdsFetcher()); registerDataFetcher(codeRegistryBuilder, OUTPUT_TYPE_STOPPLACE, ID, getNetexIdFetcher()); + registerDataFetcher(codeRegistryBuilder, OUTPUT_TYPE_STOPPLACE, PERMISSIONS, entityPermissionsFetcher); + registerDataFetcher(codeRegistryBuilder, OUTPUT_TYPE_PARENT_STOPPLACE, PERMISSIONS, entityPermissionsFetcher); + registerDataFetcher(codeRegistryBuilder, OUTPUT_TYPE_GROUP_OF_STOPPLACES, PERMISSIONS, entityPermissionsFetcher); + registerDataFetcher(codeRegistryBuilder, OUTPUT_TYPE_STOPPLACE, TARIFF_ZONES, stopPlaceTariffZoneFetcher); registerDataFetcher(codeRegistryBuilder, OUTPUT_TYPE_PARENT_STOPPLACE, TARIFF_ZONES, stopPlaceTariffZoneFetcher); @@ -735,12 +803,7 @@ public GraphQLCodeRegistry buildCodeRegistry(TypeResolver stopPlaceTypeResolver) return null; }); - registerDataFetcher(codeRegistryBuilder,OUTPUT_TYPE_SHELTER_EQUIPMENT,ID,getNetexIdFetcher()); - registerDataFetcher(codeRegistryBuilder,OUTPUT_TYPE_SANITARY_EQUIPMENT,ID,getNetexIdFetcher()); - registerDataFetcher(codeRegistryBuilder,OUTPUT_TYPE_CYCLE_STORAGE_EQUIPMENT,ID,getNetexIdFetcher()); - registerDataFetcher(codeRegistryBuilder,OUTPUT_TYPE_GENERAL_SIGN_EQUIPMENT,ID,getNetexIdFetcher()); - registerDataFetcher(codeRegistryBuilder,OUTPUT_TYPE_TICKETING_EQUIPMENT,ID,getNetexIdFetcher()); - registerDataFetcher(codeRegistryBuilder,OUTPUT_TYPE_WAITING_ROOM_EQUIPMENT,ID,getNetexIdFetcher()); + mapNetexId(codeRegistryBuilder, OUTPUT_TYPE_SHELTER_EQUIPMENT, OUTPUT_TYPE_SANITARY_EQUIPMENT, OUTPUT_TYPE_CYCLE_STORAGE_EQUIPMENT, OUTPUT_TYPE_GENERAL_SIGN_EQUIPMENT, OUTPUT_TYPE_TICKETING_EQUIPMENT, OUTPUT_TYPE_WAITING_ROOM_EQUIPMENT); registerDataFetcher(codeRegistryBuilder,OUTPUT_TYPE_BOARDING_POSITION,ID,getNetexIdFetcher()); registerDataFetcher(codeRegistryBuilder,OUTPUT_TYPE_ENTITY_REF,ADDRESSABLE_PLACE,referenceFetcher); @@ -789,7 +852,8 @@ public GraphQLCodeRegistry buildCodeRegistry(TypeResolver stopPlaceTypeResolver) return null; }); - + registerDataFetcher(codeRegistryBuilder,STOPPLACES_REGISTER,USER_PERMISSIONS,userPermissionsFetcher); + registerDataFetcher(codeRegistryBuilder,STOPPLACES_REGISTER,LOCATION_PERMISSIONS,locationPermissionsFetcher); @@ -876,6 +940,15 @@ public GraphQLCodeRegistry buildCodeRegistry(TypeResolver stopPlaceTypeResolver) return codeRegistryBuilder.build(); } + private void mapNetexId(GraphQLCodeRegistry.Builder codeRegistryBuilder, String outputTypeShelterEquipment, String outputTypeSanitaryEquipment, String outputTypeCycleStorageEquipment, String outputTypeGeneralSignEquipment, String outputTypeTicketingEquipment, String outputTypeWaitingRoomEquipment) { + registerDataFetcher(codeRegistryBuilder, outputTypeShelterEquipment,ID,getNetexIdFetcher()); + registerDataFetcher(codeRegistryBuilder, outputTypeSanitaryEquipment,ID,getNetexIdFetcher()); + registerDataFetcher(codeRegistryBuilder, outputTypeCycleStorageEquipment,ID,getNetexIdFetcher()); + registerDataFetcher(codeRegistryBuilder, outputTypeGeneralSignEquipment,ID,getNetexIdFetcher()); + registerDataFetcher(codeRegistryBuilder, outputTypeTicketingEquipment,ID,getNetexIdFetcher()); + registerDataFetcher(codeRegistryBuilder, outputTypeWaitingRoomEquipment,ID,getNetexIdFetcher()); + } + private void dataFetcherPlaceEquipments(GraphQLCodeRegistry.Builder codeRegistryBuilder, String source) { registerDataFetcher(codeRegistryBuilder, source, PLACE_EQUIPMENTS, env -> { if (env.getSource() instanceof StopPlace stopPlace) { @@ -1206,6 +1279,23 @@ private List createFindStopPlaceArguments(GraphQLArgument allVe return arguments; } + private List createLocationArguments() { + List arguments = new ArrayList<>(); + arguments.add(GraphQLArgument.newArgument() + .name(LONGITUDE) + .description("longitude") + .type(new GraphQLNonNull(GraphQLBigDecimal)) + .build()); + arguments.add(GraphQLArgument.newArgument() + .name(LATITUDE) + .description("latitude") + .type(new GraphQLNonNull(GraphQLBigDecimal)) + .build()); + + return arguments; + + } + private List createBboxArguments() { List arguments = createPageAndSizeArguments(); //BoundingBox diff --git a/src/main/java/org/rutebanken/tiamat/rest/graphql/fetchers/EntityPermissionsFetcher.java b/src/main/java/org/rutebanken/tiamat/rest/graphql/fetchers/EntityPermissionsFetcher.java new file mode 100644 index 000000000..3f40bd5f4 --- /dev/null +++ b/src/main/java/org/rutebanken/tiamat/rest/graphql/fetchers/EntityPermissionsFetcher.java @@ -0,0 +1,59 @@ +package org.rutebanken.tiamat.rest.graphql.fetchers; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import org.rutebanken.tiamat.auth.AuthorizationService; +import org.rutebanken.tiamat.model.EntityInVersionStructure; +import org.rutebanken.tiamat.model.GroupOfStopPlaces; +import org.rutebanken.tiamat.model.StopPlace; +import org.rutebanken.tiamat.model.authorization.EntityPermissions; +import org.rutebanken.tiamat.netex.id.TypeFromIdResolver; +import org.rutebanken.tiamat.repository.generic.GenericEntityInVersionRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Set; + +@Component +public class EntityPermissionsFetcher implements DataFetcher { + + @Autowired + private GenericEntityInVersionRepository genericEntityInVersionRepository; + + @Autowired + private TypeFromIdResolver typeFromIdResolver; + + + @Autowired + private AuthorizationService authorizationService; + + @Override + public Object get(DataFetchingEnvironment environment) throws Exception { + String netexId = null; + if(environment.getSource() instanceof StopPlace stopPlace) { + netexId= stopPlace.getNetexId(); + } else if (environment.getSource() instanceof GroupOfStopPlaces groupOfStopPlaces) { + netexId = groupOfStopPlaces.getNetexId(); + } else { + throw new IllegalArgumentException("Cannot find entity with ID: " + netexId); + } + + + Class clazz = typeFromIdResolver.resolveClassFromId(netexId); + EntityInVersionStructure entityInVersionStructure = genericEntityInVersionRepository.findFirstByNetexIdOrderByVersionDesc(netexId, clazz); + + if (entityInVersionStructure == null) { + throw new IllegalArgumentException("Cannot find entity with ID: " + netexId); + } + + final boolean canEditEntities = authorizationService.canEditEntity(entityInVersionStructure); + final boolean canDeleteEntity = authorizationService.canDeleteEntity(entityInVersionStructure); + final Set allowedStopPlaceTypes = authorizationService.getAllowedStopPlaceTypes(entityInVersionStructure); + final Set bannedStopPlaceTypes = authorizationService.getBannedStopPlaceTypes(entityInVersionStructure); + final Set allowedSubmode = authorizationService.getAllowedSubmodes(entityInVersionStructure); + final Set bannedSubmode = authorizationService.getBannedSubmodes(entityInVersionStructure); + + + return new EntityPermissions(canEditEntities, canDeleteEntity, allowedStopPlaceTypes, bannedStopPlaceTypes, allowedSubmode, bannedSubmode); + } +} diff --git a/src/main/java/org/rutebanken/tiamat/rest/graphql/fetchers/LocationPermissionsFetcher.java b/src/main/java/org/rutebanken/tiamat/rest/graphql/fetchers/LocationPermissionsFetcher.java new file mode 100644 index 000000000..29196f30d --- /dev/null +++ b/src/main/java/org/rutebanken/tiamat/rest/graphql/fetchers/LocationPermissionsFetcher.java @@ -0,0 +1,68 @@ +package org.rutebanken.tiamat.rest.graphql.fetchers; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.rutebanken.tiamat.auth.AuthorizationService; +import org.rutebanken.tiamat.model.authorization.EntityPermissions; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.HashSet; +import java.util.Set; + +import static org.rutebanken.tiamat.rest.graphql.GraphQLNames.LATITUDE; +import static org.rutebanken.tiamat.rest.graphql.GraphQLNames.LONGITUDE; + +@Component +public class LocationPermissionsFetcher implements DataFetcher { + + @Autowired + private AuthorizationService authorizationService; + + @Autowired + private GeometryFactory geometryFactory; + + @Override + public Object get(DataFetchingEnvironment environment) throws Exception { + final double latitude = ((BigDecimal) environment.getArgument(LATITUDE)).doubleValue(); + final double longitude = ((BigDecimal)environment.getArgument(LONGITUDE)).doubleValue(); + + Point point = geometryFactory.createPoint(new Coordinate(longitude, latitude)); + + final boolean canEditEntity = authorizationService.canEditEntity(point); + final Set locationAllowedStopPlaceTypes = authorizationService.getLocationAllowedStopPlaceTypes(canEditEntity, point); + final Set locationBannedStopPlaceTypes = authorizationService.getLocationBannedStopPlaceTypes(canEditEntity, point); + final Set locationAllowedSubmodes = authorizationService.getLocationAllowedSubmodes(canEditEntity, point); + final Set locationBannedSubmodes = authorizationService.getLocationBannedSubmodes(canEditEntity, point); + + + Set allowedStopPlaceTypeCopy = new HashSet<>(locationAllowedStopPlaceTypes); + Set bannedStopPlaceTypeCopy = new HashSet<>(locationBannedStopPlaceTypes); + Set allowedSubmodeCopy = new HashSet<>(locationAllowedSubmodes); + Set bannedSubmodeCopy = new HashSet<>(locationBannedSubmodes); + + Set duplicateStopPlaceTypes = new HashSet<>(locationAllowedStopPlaceTypes); + duplicateStopPlaceTypes.retainAll(locationBannedStopPlaceTypes); + Set duplicateSubmodes = new HashSet<>(locationAllowedSubmodes); + duplicateSubmodes.retainAll(locationBannedSubmodes); + + allowedStopPlaceTypeCopy.removeAll(duplicateStopPlaceTypes); + bannedStopPlaceTypeCopy.removeAll(duplicateStopPlaceTypes); + allowedSubmodeCopy.removeAll(duplicateSubmodes); + bannedSubmodeCopy.removeAll(duplicateSubmodes); + if(allowedStopPlaceTypeCopy.isEmpty()) { + allowedStopPlaceTypeCopy.add("*"); + + } + if(allowedSubmodeCopy.isEmpty()) { + allowedSubmodeCopy.add("*"); + } + + return new EntityPermissions(canEditEntity,false, allowedStopPlaceTypeCopy, bannedStopPlaceTypeCopy, allowedSubmodeCopy, bannedSubmodeCopy); + + } +} diff --git a/src/main/java/org/rutebanken/tiamat/rest/graphql/fetchers/UserPermissionsFetcher.java b/src/main/java/org/rutebanken/tiamat/rest/graphql/fetchers/UserPermissionsFetcher.java new file mode 100644 index 000000000..e6a8745e6 --- /dev/null +++ b/src/main/java/org/rutebanken/tiamat/rest/graphql/fetchers/UserPermissionsFetcher.java @@ -0,0 +1,47 @@ +/* + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the "Licence"); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + */ + +package org.rutebanken.tiamat.rest.graphql.fetchers; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import org.rutebanken.tiamat.auth.AuthorizationService; +import org.rutebanken.tiamat.model.authorization.UserPermissions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + + +@Component +public class UserPermissionsFetcher implements DataFetcher { + + private static final Logger logger = LoggerFactory.getLogger(UserPermissionsFetcher.class); + + + @Autowired + private AuthorizationService authorizationService; + + @Override + public Object get(DataFetchingEnvironment dataFetchingEnvironment) { + + final boolean isGuest = authorizationService.isGuest(); + final boolean allowNewStopEverywhere = authorizationService.verifyCanEditAllEntities(); + + logger.debug("isGuest: {}, allowNewStopEverywhere: {} " , allowNewStopEverywhere, isGuest); + + return new UserPermissions(isGuest, allowNewStopEverywhere); + } +} diff --git a/src/main/java/org/rutebanken/tiamat/rest/graphql/types/GroupOfStopPlacesObjectTypeCreator.java b/src/main/java/org/rutebanken/tiamat/rest/graphql/types/GroupOfStopPlacesObjectTypeCreator.java index a9fa6031f..b19affda4 100644 --- a/src/main/java/org/rutebanken/tiamat/rest/graphql/types/GroupOfStopPlacesObjectTypeCreator.java +++ b/src/main/java/org/rutebanken/tiamat/rest/graphql/types/GroupOfStopPlacesObjectTypeCreator.java @@ -42,6 +42,7 @@ import static org.rutebanken.tiamat.rest.graphql.GraphQLNames.GROUP_OF_STOP_PLACES_MEMBERS; import static org.rutebanken.tiamat.rest.graphql.GraphQLNames.NAME; import static org.rutebanken.tiamat.rest.graphql.GraphQLNames.OUTPUT_TYPE_GROUP_OF_STOPPLACES; +import static org.rutebanken.tiamat.rest.graphql.GraphQLNames.PERMISSIONS; import static org.rutebanken.tiamat.rest.graphql.GraphQLNames.PURPOSE_OF_GROUPING; import static org.rutebanken.tiamat.rest.graphql.GraphQLNames.SHORT_NAME; import static org.rutebanken.tiamat.rest.graphql.GraphQLNames.VERSION; @@ -52,7 +53,7 @@ @Component public class GroupOfStopPlacesObjectTypeCreator { - public GraphQLObjectType create(GraphQLInterfaceType stopPlaceInterface, GraphQLObjectType purposeOfGroupingType) { + public GraphQLObjectType create(GraphQLInterfaceType stopPlaceInterface, GraphQLObjectType purposeOfGroupingType, GraphQLObjectType entityPermissionObjectType) { return newObject() .name(OUTPUT_TYPE_GROUP_OF_STOPPLACES) @@ -78,6 +79,9 @@ public GraphQLObjectType create(GraphQLInterfaceType stopPlaceInterface, GraphQL .field(newFieldDefinition() .name(GROUP_OF_STOP_PLACES_MEMBERS) .type(new GraphQLList(stopPlaceInterface))) + .field(newFieldDefinition() + .name(PERMISSIONS) + .type(entityPermissionObjectType)) .build(); } } diff --git a/src/main/java/org/rutebanken/tiamat/rest/graphql/types/StopPlaceInterfaceCreator.java b/src/main/java/org/rutebanken/tiamat/rest/graphql/types/StopPlaceInterfaceCreator.java index 720aff8f3..87a3059b6 100644 --- a/src/main/java/org/rutebanken/tiamat/rest/graphql/types/StopPlaceInterfaceCreator.java +++ b/src/main/java/org/rutebanken/tiamat/rest/graphql/types/StopPlaceInterfaceCreator.java @@ -35,6 +35,7 @@ import static org.rutebanken.tiamat.rest.graphql.GraphQLNames.FARE_ZONES; import static org.rutebanken.tiamat.rest.graphql.GraphQLNames.OUTPUT_TYPE_GROUP_OF_STOPPLACES; import static org.rutebanken.tiamat.rest.graphql.GraphQLNames.OUTPUT_TYPE_STOPPLACE_INTERFACE; +import static org.rutebanken.tiamat.rest.graphql.GraphQLNames.PERMISSIONS; import static org.rutebanken.tiamat.rest.graphql.GraphQLNames.STOP_PLACE_GROUPS; import static org.rutebanken.tiamat.rest.graphql.GraphQLNames.TAGS; import static org.rutebanken.tiamat.rest.graphql.GraphQLNames.TARIFF_ZONES; @@ -55,8 +56,9 @@ public class StopPlaceInterfaceCreator { public List createCommonInterfaceFields(GraphQLObjectType tariffZoneObjectType, GraphQLObjectType fareZoneObjectType, - GraphQLObjectType topographicPlaceObjectType, - GraphQLObjectType validBetweenObjectType) { + GraphQLObjectType topographicPlaceObjectType, + GraphQLObjectType validBetweenObjectType, + GraphQLObjectType entityPermissionObjectType) { List stopPlaceInterfaceFields = new ArrayList<>(); stopPlaceInterfaceFields.add(newFieldDefinition() .name(VERSION_COMMENT) @@ -93,6 +95,10 @@ public List createCommonInterfaceFields(GraphQLObjectTyp .name(STOP_PLACE_GROUPS) .type(new GraphQLList(new GraphQLTypeReference(OUTPUT_TYPE_GROUP_OF_STOPPLACES))) .build()); + stopPlaceInterfaceFields.add(newFieldDefinition() + .name(PERMISSIONS) + .type(entityPermissionObjectType) + .build()); return stopPlaceInterfaceFields; } diff --git a/src/test/java/org/rutebanken/tiamat/auth/DefaultAuthorizationServiceTest.java b/src/test/java/org/rutebanken/tiamat/auth/DefaultAuthorizationServiceTest.java index 248d48d9e..78f377b1d 100644 --- a/src/test/java/org/rutebanken/tiamat/auth/DefaultAuthorizationServiceTest.java +++ b/src/test/java/org/rutebanken/tiamat/auth/DefaultAuthorizationServiceTest.java @@ -3,7 +3,6 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.rutebanken.helper.organisation.RoleAssignment; -import org.springframework.security.access.AccessDeniedException; import java.util.List; @@ -12,14 +11,15 @@ class DefaultAuthorizationServiceTest { @Test void verifyCanEditAllEntities() { List roleAssignments = RoleAssignmentListBuilder.builder().withAccessAllAreas().build(); - DefaultAuthorizationService defaultAuthorizationService = new DefaultAuthorizationService(null, null); - Assertions.assertDoesNotThrow(() -> defaultAuthorizationService.verifyCanEditAllEntities(roleAssignments)); + DefaultAuthorizationService defaultAuthorizationService = new DefaultAuthorizationService(null,false, null, null, null); + Assertions.assertTrue(defaultAuthorizationService.verifyCanEditAllEntities(roleAssignments)); } @Test void verifyCanEditAllEntitiesMissingRoleAssignment() { List roleAssignments = RoleAssignmentListBuilder.builder().build(); - DefaultAuthorizationService defaultAuthorizationService = new DefaultAuthorizationService(null, null); - Assertions.assertThrows(AccessDeniedException.class, () -> defaultAuthorizationService.verifyCanEditAllEntities(roleAssignments)); + DefaultAuthorizationService defaultAuthorizationService = new DefaultAuthorizationService(null,false, null, null, null); + Assertions.assertFalse(defaultAuthorizationService.verifyCanEditAllEntities(roleAssignments)); + } } \ No newline at end of file diff --git a/src/test/java/org/rutebanken/tiamat/auth/StopPlaceAuthorizationServiceTest.java b/src/test/java/org/rutebanken/tiamat/auth/StopPlaceAuthorizationServiceTest.java index 67dbe1f20..a958c0102 100644 --- a/src/test/java/org/rutebanken/tiamat/auth/StopPlaceAuthorizationServiceTest.java +++ b/src/test/java/org/rutebanken/tiamat/auth/StopPlaceAuthorizationServiceTest.java @@ -31,6 +31,7 @@ import org.rutebanken.tiamat.model.StopPlace; import org.rutebanken.tiamat.model.StopTypeEnumeration; import org.rutebanken.tiamat.model.ValidBetween; +import org.rutebanken.tiamat.service.groupofstopplaces.GroupOfStopPlacesMembersResolver; import org.rutebanken.tiamat.service.stopplace.MultiModalStopPlaceEditor; import org.rutebanken.tiamat.versioning.VersionCreator; import org.springframework.beans.factory.annotation.Autowired; @@ -86,6 +87,9 @@ public class StopPlaceAuthorizationServiceTest extends TiamatIntegrationTest { @Autowired private TiamatEntityResolver tiamatEntityResolver; + @Autowired + private GroupOfStopPlacesMembersResolver groupOfStopPlacesMembersResolver; + @Autowired private TiamatOriganisationChecker tiamatOriganisationChecker; @@ -122,7 +126,7 @@ public void StopPlaceAuthorizationServiceTest() { tiamatOriganisationChecker, topographicPlaceChecker, tiamatEntityResolver); - this.authorizationService = authorizationServiceConfig.authorizationService(dataScopedAuthorizationService, roleAssignmentExtractor); + this.authorizationService = authorizationServiceConfig.authorizationService(dataScopedAuthorizationService,false, roleAssignmentExtractor,topographicPlaceChecker,groupOfStopPlacesMembersResolver); diff --git a/src/test/java/org/rutebanken/tiamat/auth/TiamatAuthorizationServiceTest.java b/src/test/java/org/rutebanken/tiamat/auth/TiamatAuthorizationServiceTest.java index 7a6d3010a..742f1bc69 100644 --- a/src/test/java/org/rutebanken/tiamat/auth/TiamatAuthorizationServiceTest.java +++ b/src/test/java/org/rutebanken/tiamat/auth/TiamatAuthorizationServiceTest.java @@ -15,24 +15,44 @@ package org.rutebanken.tiamat.auth; +import org.junit.Ignore; import org.junit.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.geom.impl.CoordinateArraySequence; import org.rutebanken.helper.organisation.RoleAssignment; import org.rutebanken.tiamat.TiamatIntegrationTest; import org.rutebanken.tiamat.model.BusSubmodeEnumeration; import org.rutebanken.tiamat.model.Quay; import org.rutebanken.tiamat.model.StopPlace; import org.rutebanken.tiamat.model.StopTypeEnumeration; +import org.rutebanken.tiamat.model.TopographicPlace; import org.rutebanken.tiamat.model.WaterSubmodeEnumeration; import org.rutebanken.tiamat.repository.StopPlaceRepository; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.transaction.annotation.Transactional; +import java.time.Instant; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Set; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; +import static org.rutebanken.helper.organisation.AuthorizationConstants.ENTITY_CLASSIFIER_ALL_TYPES; import static org.rutebanken.helper.organisation.AuthorizationConstants.ENTITY_TYPE; +import static org.rutebanken.helper.organisation.AuthorizationConstants.ROLE_DELETE_STOPS; import static org.rutebanken.helper.organisation.AuthorizationConstants.ROLE_EDIT_STOPS; @Transactional // Because of the authorization service logs entities which could read lazy loaded fields @@ -143,6 +163,130 @@ public void authorizedWithSubmodeAndType() { boolean authorized = authorizationService.canEditEntity(roleAssignment, stopPlace); assertThat("Should be authorized as both type and submode are allowed", authorized, is(true)); } + /** + * Test allowed stop place types + * EntityType=StopPlace, StopPlaceType=railStation,railReplacementBus + */ + + @Test + public void authorizedGetAllowedStopPlaceTypesTest() { + setUpSecurityContext(); + final List roleAssignments = roleAssignmentsForRailAndRailReplacementMocked(ROLE_EDIT_STOPS); + mockedRoleAssignmentExtractor.setNextReturnedRoleAssignment(roleAssignments); + + StopPlace stopPlace = new StopPlace(); + stopPlace.setStopPlaceType(StopTypeEnumeration.RAIL_STATION); + stopPlace.setBusSubmode(BusSubmodeEnumeration.RAIL_REPLACEMENT_BUS); + + final Set allowedStopPlaceTypes = authorizationService.getAllowedStopPlaceTypes(stopPlace); + assertThat("Should contain allowed StopPlaceType", allowedStopPlaceTypes.contains("railStation"), is(true)); + } + + + @Test + public void authorizedGetBannedStopPlaceTypesEntityFilterTest() { + setUpSecurityContext(); + final List roleAssignments = roleAssignmentsMultipleRoles(); + mockedRoleAssignmentExtractor.setNextReturnedRoleAssignment(roleAssignments); + + StopPlace stopPlace = new StopPlace(); + stopPlace.setStopPlaceType(StopTypeEnumeration.BUS_STATION); + + final Set bannedStopPlaceTypes = authorizationService.getBannedStopPlaceTypes(stopPlace); + assertThat("Should contain allowed StopPlaceType", bannedStopPlaceTypes.contains(StopTypeEnumeration.RAIL_STATION.value()), is(false)); + assertThat("Should contain allowed StopPlaceType", bannedStopPlaceTypes.contains(StopTypeEnumeration.AIRPORT.value()), is(false)); + } + + + /** + * Test banned stop place types + * EntityType=StopPlace, StopPlaceType=railStation,railReplacementBus + * Test ignored as it is not working as expected + */ + @Test + @Ignore + public void authorizedGetBannedStopPlaceTypesTest() { + setUpSecurityContext(); + RoleAssignment roleAssignment = RoleAssignment.builder() + .withRole(ROLE_EDIT_STOPS) + .withOrganisation("OST") + .withAdministrativeZone("KVE:TopographicalPlace:01") + .withEntityClassification(ENTITY_TYPE, "StopPlace") + .withEntityClassification("StopPlaceType", "!airport") + .withEntityClassification("StopPlaceType", "!railStation") + .withEntityClassification("Submode", "!railReplacementBus") + .build(); + + mockedRoleAssignmentExtractor.setNextReturnedRoleAssignment(roleAssignment); + + Point point = geometryFactory.createPoint(new Coordinate(9.536819, 61.772281)); + Point point2 = geometryFactory.createPoint(new Coordinate(23.682516, 70.664175)); + + + TopographicPlace municipality = new TopographicPlace(); + municipality.setNetexId("KVE:TopographicalPlace:01"); + municipality.setVersion(1); + municipality.setPolygon(createPolygon(point)); + topographicPlaceRepository.saveAndFlush(municipality); + + TopographicPlace municipality2 = new TopographicPlace(); + municipality2.setNetexId("KVE:TopographicalPlace:02"); + municipality2.setVersion(1); + municipality2.setPolygon(createPolygon(point2)); + topographicPlaceRepository.saveAndFlush(municipality2); + + + + StopPlace stopPlace = new StopPlace(); + stopPlace.setStopPlaceType(StopTypeEnumeration.BUS_STATION); + stopPlace.setBusSubmode(BusSubmodeEnumeration.REGIONAL_BUS); + stopPlace.setTopographicPlace(municipality); + stopPlace.setCentroid(point); + stopPlaceRepository.saveAndFlush(stopPlace); + + StopPlace railStation = new StopPlace(); + railStation.setStopPlaceType(StopTypeEnumeration.RAIL_STATION); + railStation.setTopographicPlace(municipality); + railStation.setCentroid(point); + stopPlaceRepository.saveAndFlush(railStation); + + StopPlace outsideStopPlace = new StopPlace(); + outsideStopPlace.setStopPlaceType(StopTypeEnumeration.BUS_STATION); + outsideStopPlace.setBusSubmode(BusSubmodeEnumeration.REGIONAL_BUS); + outsideStopPlace.setTopographicPlace(municipality2); + outsideStopPlace.setCentroid(point2); + stopPlaceRepository.saveAndFlush(outsideStopPlace); + + final Set bannedStopPlaceTypes = authorizationService.getBannedStopPlaceTypes(stopPlace); + + mockedRoleAssignmentExtractor.setNextReturnedRoleAssignment(roleAssignment); + final Set bannedSubmodes = authorizationService.getBannedSubmodes(stopPlace); + + assertThat("Should contain banned StopPlaceType", bannedStopPlaceTypes.contains("airport"), is(true)); + assertThat("Should contain banned StopPlaceType", bannedStopPlaceTypes.contains("railStation"), is(true)); + assertThat("Should contain banned Submode", bannedSubmodes.contains("railReplacementBus"), is(true)); + + mockedRoleAssignmentExtractor.setNextReturnedRoleAssignment(roleAssignment); + boolean authorized = authorizationService.canEditEntity(roleAssignment, stopPlace); + assertThat("Should be authorized as both type and submode are allowed", authorized, is(true)); + + mockedRoleAssignmentExtractor.setNextReturnedRoleAssignment(roleAssignment); + boolean authorizedRail = authorizationService.canEditEntity(roleAssignment, railStation); + assertThat("Should not be authorized as rail station is banned", authorizedRail, is(false)); + + + mockedRoleAssignmentExtractor.setNextReturnedRoleAssignment(roleAssignment); + boolean authorizedOutside = authorizationService.canEditEntity(roleAssignment, outsideStopPlace); + assertThat("Should be authorized as outside stop place is not in the same topographical place", authorizedOutside, is(false)); + + mockedRoleAssignmentExtractor.setNextReturnedRoleAssignment(roleAssignment); + final Set bannedSubmodesOutside = authorizationService.getBannedSubmodes(outsideStopPlace); + assertThat("Should contain all banned Submode", bannedSubmodesOutside.contains(ENTITY_CLASSIFIER_ALL_TYPES), is(true)); + + mockedRoleAssignmentExtractor.setNextReturnedRoleAssignment(roleAssignment); + final Set bannedStopPlaceTypesOutside = authorizationService.getBannedSubmodes(outsideStopPlace); + assertThat("Should not contain banned stop place type", bannedStopPlaceTypesOutside.isEmpty(), is(true)); + } /** * Test real life example from ninkasi @@ -193,6 +337,57 @@ public void testNSBEditStopsRoleAssignmentsBusStation() { assertThat("bus station not allowed when sub mode not set", authorized, is(false)); } + /** + * Test if user has multiple roles + * A User has roles for editing stops TopographicPlace 1 and 2 + */ + @Test + public void testUserWithMultipleRoles() { + setUpSecurityContext(); + final List roleAssignments = roleAssignmentsMultipleRoles(); + + Point point = geometryFactory.createPoint(new Coordinate(9.536819, 61.772281)); + Point point2 = geometryFactory.createPoint(new Coordinate(23.682516, 70.664175)); + + TopographicPlace municipality = new TopographicPlace(); + municipality.setNetexId("KVE:TopographicalPlace:01"); + municipality.setVersion(1); + municipality.setPolygon(createPolygon(point)); + topographicPlaceRepository.saveAndFlush(municipality); + + TopographicPlace municipality2 = new TopographicPlace(); + municipality2.setNetexId("KVE:TopographicalPlace:02"); + municipality2.setVersion(1); + municipality2.setPolygon(createPolygon(point2)); + topographicPlaceRepository.saveAndFlush(municipality2); + + StopPlace stopPlace = new StopPlace(); + stopPlace.setStopPlaceType(StopTypeEnumeration.BUS_STATION); + stopPlace.setBusSubmode(BusSubmodeEnumeration.REGIONAL_BUS); + stopPlace.setTopographicPlace(municipality); + stopPlace.setCentroid(point); + stopPlaceRepository.saveAndFlush(stopPlace); + + StopPlace stopPlace2 = new StopPlace(); + stopPlace2.setStopPlaceType(StopTypeEnumeration.BUS_STATION); + stopPlace2.setBusSubmode(BusSubmodeEnumeration.REGIONAL_BUS); + stopPlace2.setTopographicPlace(municipality2); + stopPlace2.setCentroid(point2); + + stopPlaceRepository.saveAndFlush(stopPlace2); + + mockedRoleAssignmentExtractor.setNextReturnedRoleAssignment(roleAssignments); + assertThat("Should be authorized as both type and submode are allowed", authorizationService.canEditEntity(stopPlace), is(true)); + mockedRoleAssignmentExtractor.setNextReturnedRoleAssignment(roleAssignments); + assertThat("Should be authorized as both type and submode are allowed", authorizationService.canDeleteEntity(stopPlace), is(true)); + + mockedRoleAssignmentExtractor.setNextReturnedRoleAssignment(roleAssignments); + assertThat("Should be authorized as both type and submode are allowed", authorizationService.canEditEntity(stopPlace2), is(true)); + mockedRoleAssignmentExtractor.setNextReturnedRoleAssignment(roleAssignments); + assertThat("Should be authorized as both type and submode are allowed", authorizationService.canDeleteEntity(stopPlace2), is(false)); + + } + private List roleAssignmentsForRailAndRailReplacementMocked(String role) { List roleAssignments = Arrays.asList(RoleAssignment.builder().withRole(role) .withOrganisation("NSB") @@ -208,4 +403,61 @@ private List roleAssignmentsForRailAndRailReplacementMocked(Stri return roleAssignments; } + private List roleAssignmentsMultipleRoles() { + return Arrays.asList(RoleAssignment.builder() + .withRole(ROLE_EDIT_STOPS) + .withOrganisation("OST") + .withAdministrativeZone("KVE:TopographicalPlace:01") + .withEntityClassification(ENTITY_TYPE, "StopPlace") + .withEntityClassification("StopPlaceType", "!airport") + .withEntityClassification("StopPlaceType", "!railStation") + .withEntityClassification("Submode", "!railReplacementBus") + .build(), + RoleAssignment.builder() + .withRole(ROLE_DELETE_STOPS) + .withOrganisation("OST") + .withAdministrativeZone("KVE:TopographicalPlace:01") + .withEntityClassification(ENTITY_TYPE, "StopPlace") + .withEntityClassification("StopPlaceType", "!airport") + .withEntityClassification("StopPlaceType", "!railStation") + .withEntityClassification("Submode", "!railReplacementBus") + .build(), + RoleAssignment.builder() + .withRole(ROLE_EDIT_STOPS) + .withOrganisation("OST") + .withAdministrativeZone("KVE:TopographicalPlace:02") + .withEntityClassification(ENTITY_TYPE, "StopPlace") + .withEntityClassification("StopPlaceType", "!airport") + .withEntityClassification("StopPlaceType", "!railStation") + .withEntityClassification("Submode", "!railReplacementBus") + .build() + ); + + } + + private Polygon createPolygon(Point point) { + Geometry bufferedPoint = point.buffer(10); + LinearRing linearRing = new LinearRing(new CoordinateArraySequence(bufferedPoint.getCoordinates()), geometryFactory); + return geometryFactory.createPolygon(linearRing, null); + } + + private void setUpSecurityContext() { + // Create a Jwt with claims + Map claims = new HashMap<>(); + claims.put("sub", "testuser"); + claims.put("scope", "ROLE_USER"); // Or other relevant scopes/roles + + // Create a Jwt instance + Jwt jwt = new Jwt( + "tokenValue", + Instant.now(), + Instant.now().plusSeconds(3600), + Map.of("alg", "none"), + claims + ); + + final AbstractAuthenticationToken authToken = new JwtAuthenticationToken(jwt, Collections.singleton(new SimpleGrantedAuthority("ROLE_EDIT_STOPS"))); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } \ No newline at end of file