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 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.
* */
@@ -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