diff --git a/server/src/main/java/access/api/InvitationController.java b/server/src/main/java/access/api/InvitationController.java index 14d6b459..183d7832 100644 --- a/server/src/main/java/access/api/InvitationController.java +++ b/server/src/main/java/access/api/InvitationController.java @@ -41,6 +41,7 @@ import java.util.*; import java.util.stream.Collectors; +import static access.SwaggerOpenIdConfig.API_TOKENS_SCHEME_NAME; import static access.SwaggerOpenIdConfig.OPEN_ID_SCHEME_NAME; import static java.util.stream.Collectors.toSet; @@ -48,6 +49,7 @@ @RequestMapping(value = {"/api/v1/invitations", "/api/external/v1/invitations"}, produces = MediaType.APPLICATION_JSON_VALUE) @Transactional @SecurityRequirement(name = OPEN_ID_SCHEME_NAME, scopes = {"openid"}) +@SecurityRequirement(name = API_TOKENS_SCHEME_NAME) @EnableConfigurationProperties(SuperAdmin.class) public class InvitationController implements HasManage { diff --git a/server/src/main/java/access/api/ManageController.java b/server/src/main/java/access/api/ManageController.java index 433aab21..bfb8c690 100644 --- a/server/src/main/java/access/api/ManageController.java +++ b/server/src/main/java/access/api/ManageController.java @@ -24,12 +24,14 @@ import java.util.*; import java.util.stream.Collectors; +import static access.SwaggerOpenIdConfig.API_TOKENS_SCHEME_NAME; import static access.SwaggerOpenIdConfig.OPEN_ID_SCHEME_NAME; @RestController @RequestMapping(value = {"/api/v1/manage", "/api/external/v1/manage"}, produces = MediaType.APPLICATION_JSON_VALUE) @Transactional @SecurityRequirement(name = OPEN_ID_SCHEME_NAME, scopes = {"openid"}) +@SecurityRequirement(name = API_TOKENS_SCHEME_NAME) @EnableConfigurationProperties(Config.class) @SuppressWarnings("unchecked") public class ManageController { diff --git a/server/src/main/java/access/api/RoleController.java b/server/src/main/java/access/api/RoleController.java index 1bce98ab..f040e589 100644 --- a/server/src/main/java/access/api/RoleController.java +++ b/server/src/main/java/access/api/RoleController.java @@ -31,12 +31,14 @@ import java.util.*; import java.util.stream.Collectors; +import static access.SwaggerOpenIdConfig.API_TOKENS_SCHEME_NAME; import static access.SwaggerOpenIdConfig.OPEN_ID_SCHEME_NAME; @RestController @RequestMapping(value = {"/api/v1/roles", "/api/external/v1/roles", }, produces = MediaType.APPLICATION_JSON_VALUE) @Transactional @SecurityRequirement(name = OPEN_ID_SCHEME_NAME, scopes = {"openid"}) +@SecurityRequirement(name = API_TOKENS_SCHEME_NAME) @EnableConfigurationProperties(Config.class) public class RoleController { private static final Log LOG = LogFactory.getLog(RoleController.class); diff --git a/server/src/main/java/access/api/UserController.java b/server/src/main/java/access/api/UserController.java index f9d4eebf..697422bd 100644 --- a/server/src/main/java/access/api/UserController.java +++ b/server/src/main/java/access/api/UserController.java @@ -40,6 +40,7 @@ import java.util.*; import java.util.concurrent.atomic.AtomicReference; +import static access.SwaggerOpenIdConfig.API_TOKENS_SCHEME_NAME; import static access.SwaggerOpenIdConfig.OPEN_ID_SCHEME_NAME; @RestController @@ -47,6 +48,7 @@ @Transactional @SecurityRequirement(name = OPEN_ID_SCHEME_NAME, scopes = {"openid"}) @EnableConfigurationProperties(Config.class) +@SecurityRequirement(name = API_TOKENS_SCHEME_NAME) public class UserController { private static final Log LOG = LogFactory.getLog(UserController.class); diff --git a/server/src/main/java/access/api/UserRoleController.java b/server/src/main/java/access/api/UserRoleController.java index 2c4bb2d3..9e762dd6 100644 --- a/server/src/main/java/access/api/UserRoleController.java +++ b/server/src/main/java/access/api/UserRoleController.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; +import static access.SwaggerOpenIdConfig.API_TOKENS_SCHEME_NAME; import static access.SwaggerOpenIdConfig.OPEN_ID_SCHEME_NAME; @RestController @@ -31,6 +32,7 @@ @Transactional @SecurityRequirement(name = OPEN_ID_SCHEME_NAME, scopes = {"openid"}) @EnableConfigurationProperties(Config.class) +@SecurityRequirement(name = API_TOKENS_SCHEME_NAME) public class UserRoleController { private static final Log LOG = LogFactory.getLog(UserRoleController.class); diff --git a/server/src/main/java/access/cron/IdentityProvider.java b/server/src/main/java/access/cron/IdentityProvider.java index 0a109f66..6839a527 100644 --- a/server/src/main/java/access/cron/IdentityProvider.java +++ b/server/src/main/java/access/cron/IdentityProvider.java @@ -9,10 +9,8 @@ import java.io.Serializable; -@NoArgsConstructor @AllArgsConstructor @Getter -@Setter public class IdentityProvider implements Serializable { private String displayNameEn; @@ -21,7 +19,7 @@ public class IdentityProvider implements Serializable { public String getName() { String language = LocaleContextHolder.getLocale().getLanguage(); - if ("en".equals(language.toLowerCase())) { + if ("en".equalsIgnoreCase(language)) { return StringUtils.hasText(displayNameEn) ? displayNameEn : displayNameNl; } return StringUtils.hasText(displayNameNl) ? displayNameNl : displayNameEn; diff --git a/server/src/main/java/access/security/SecurityConfig.java b/server/src/main/java/access/security/SecurityConfig.java index 90e084e9..5f436568 100644 --- a/server/src/main/java/access/security/SecurityConfig.java +++ b/server/src/main/java/access/security/SecurityConfig.java @@ -129,12 +129,10 @@ SecurityFilterChain sessionSecurityFilterChain(HttpSecurity http, UserRepository userRepository, @Value("${institution-admin.entitlement}") String entitlement, @Value("${institution-admin.organization-guid-prefix}") String organizationGuidPrefix) throws Exception { - final RequestHeaderRequestMatcher apiTokenRequestMatcher = new RequestHeaderRequestMatcher(API_TOKEN_HEADER); http .csrf(c -> c .ignoringRequestMatchers("/login/oauth2/code/oidcng") - .ignoringRequestMatchers("/api/v1/validations/**") - .ignoringRequestMatchers(apiTokenRequestMatcher)) + .ignoringRequestMatchers("/api/v1/validations/**")) .securityMatcher("/login/oauth2/**", "/oauth2/authorization/**", "/api/v1/**") .authorizeHttpRequests(c -> c .requestMatchers( @@ -148,9 +146,6 @@ SecurityFilterChain sessionSecurityFilterChain(HttpSecurity http, "/ui/**", "internal/**") .permitAll() - //The API token is secured in the UserHandlerMethodArgumentResolver - .requestMatchers(apiTokenRequestMatcher) - .permitAll() .anyRequest() .authenticated() ) @@ -180,7 +175,8 @@ private OAuth2AuthorizationRequestResolver authorizationRequestResolver( @Order(2) SecurityFilterChain basicAuthenticationSecurityFilterChain(HttpSecurity http) throws Exception { http.csrf(c -> c.disable()) - .securityMatcher("/api/voot/**", + .securityMatcher( + "/api/voot/**", "/api/external/v1/voot/**", "/api/aa/**", "/api/external/v1/aa/**", @@ -200,11 +196,15 @@ SecurityFilterChain basicAuthenticationSecurityFilterChain(HttpSecurity http) th @Bean @Order(3) SecurityFilterChain jwtSecurityFilterChain(HttpSecurity http) throws Exception { + final RequestHeaderRequestMatcher apiTokenRequestMatcher = new RequestHeaderRequestMatcher(API_TOKEN_HEADER); http.csrf(c -> c.disable()) .securityMatcher("/api/external/v1/**") .authorizeHttpRequests(c -> c .requestMatchers("/api/external/v1/validations/**") .permitAll() + //The API token is secured in the UserHandlerMethodArgumentResolver + .requestMatchers(apiTokenRequestMatcher) + .permitAll() .anyRequest() .authenticated() ) diff --git a/server/src/main/resources/manage/saml20_idp.json b/server/src/main/resources/manage/saml20_idp.json index 0b8899db..5b0d4344 100644 --- a/server/src/main/resources/manage/saml20_idp.json +++ b/server/src/main/resources/manage/saml20_idp.json @@ -67,7 +67,8 @@ "name:en": "Idp UU EN", "name:nl": "IdP UU NL", "OrganizationName:en": "SURF bv", - "logo:0:url": "https://static.surfconext.nl/media/idp/surfconext.png" + "logo:0:url": "https://static.surfconext.nl/media/idp/surfconext.png", + "coin:institution_guid": "test_institution_guid" } } } diff --git a/server/src/test/java/access/AbstractTest.java b/server/src/test/java/access/AbstractTest.java index 74b87187..aeb1d6d0 100644 --- a/server/src/test/java/access/AbstractTest.java +++ b/server/src/test/java/access/AbstractTest.java @@ -56,7 +56,6 @@ import java.util.function.Consumer; import java.util.function.UnaryOperator; -import static access.Seed.ORGANISATION_GUID; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static io.restassured.RestAssured.given; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -114,14 +113,6 @@ public abstract class AbstractTest { @LocalServerPort protected int port; - protected static final UnaryOperator> institutionalAdminEntitlementOperator = m -> { - m.put("eduperson_entitlement", - List.of( - "urn:mace:surfnet.nl:surfnet.nl:sab:role:SURFconextverantwoordelijke", - "urn:mace:surfnet.nl:surfnet.nl:sab:organizationGUID:" + ORGANISATION_GUID - )); - return m; - }; @BeforeAll protected static void beforeAll() { RestAssured.config = RestAssuredConfig.config() @@ -475,4 +466,16 @@ protected void stubForUpdateScimRolePatch() throws JsonProcessingException { )); } + protected UnaryOperator> institutionalAdminEntitlementOperator(String organisationGuid) { + return m -> { + m.put("eduperson_entitlement", + List.of( + "urn:mace:surfnet.nl:surfnet.nl:sab:role:SURFconextverantwoordelijke", + "urn:mace:surfnet.nl:surfnet.nl:sab:organizationGUID:" + organisationGuid + )); + return m; + }; + } + + } diff --git a/server/src/test/java/access/api/APITokenControllerTest.java b/server/src/test/java/access/api/APITokenControllerTest.java index 532d60ae..001e8a48 100644 --- a/server/src/test/java/access/api/APITokenControllerTest.java +++ b/server/src/test/java/access/api/APITokenControllerTest.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.Map; +import java.util.UUID; import static access.Seed.*; import static io.restassured.RestAssured.given; @@ -21,7 +22,8 @@ class APITokenControllerTest extends AbstractTest { @Test void apiTokensByInstitution() throws Exception { super.stubForManageProviderByOrganisationGUID(ORGANISATION_GUID); - AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/me", INSTITUTION_ADMIN, institutionalAdminEntitlementOperator); + AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/me", INSTITUTION_ADMIN, + institutionalAdminEntitlementOperator(ORGANISATION_GUID)); List tokens = given() .when() @@ -38,7 +40,8 @@ void apiTokensByInstitution() throws Exception { @Test void create() throws Exception { super.stubForManageProviderByOrganisationGUID(ORGANISATION_GUID); - AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/me", INSTITUTION_ADMIN, institutionalAdminEntitlementOperator); + AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/me", INSTITUTION_ADMIN, + institutionalAdminEntitlementOperator(ORGANISATION_GUID)); //First get the value, otherwise the creation will fail Map res = given() .when() @@ -69,11 +72,29 @@ void create() throws Exception { assertEquals(HashGenerator.hashToken(token), apiTokenFromDB.getHashedValue()); } + @Test + void createWithFaultyToken() throws Exception { + super.stubForManageProviderByOrganisationGUID(ORGANISATION_GUID); + AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/me", INSTITUTION_ADMIN, + institutionalAdminEntitlementOperator(ORGANISATION_GUID)); + given() + .when() + .filter(accessCookieFilter.cookieFilter()) + .accept(ContentType.JSON) + .header(accessCookieFilter.csrfToken().getHeaderName(), accessCookieFilter.csrfToken().getToken()) + .contentType(ContentType.JSON) + .body(new APIToken("wrong", "wrong", "wrong")) + .post("/api/v1/tokens") + .then() + .statusCode(403); + } + @Test void deleteToken() throws Exception { super.stubForManageProviderByOrganisationGUID(ORGANISATION_GUID); APIToken apiToken = apiTokenRepository.findByHashedValue(HashGenerator.hashToken(API_TOKEN_HASH)).get(); - AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/me", INSTITUTION_ADMIN, institutionalAdminEntitlementOperator); + AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/me", INSTITUTION_ADMIN, + institutionalAdminEntitlementOperator(ORGANISATION_GUID)); given() .when() @@ -87,4 +108,24 @@ void deleteToken() throws Exception { .statusCode(204); assertEquals(0, apiTokenRepository.count()); } + + @Test + void deleteOtherToken() throws Exception { + String organisationGUID = "test_institution_guid"; + super.stubForManageProviderByOrganisationGUID(organisationGUID); + APIToken apiToken = apiTokenRepository.findByHashedValue(HashGenerator.hashToken(API_TOKEN_HASH)).get(); + AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/me", INSTITUTION_ADMIN, + institutionalAdminEntitlementOperator(organisationGUID)); + + given() + .when() + .filter(accessCookieFilter.cookieFilter()) + .accept(ContentType.JSON) + .header(accessCookieFilter.csrfToken().getHeaderName(), accessCookieFilter.csrfToken().getToken()) + .contentType(ContentType.JSON) + .pathParams("id", apiToken.getId()) + .delete("/api/v1/tokens/{id}") + .then() + .statusCode(403); + } } \ No newline at end of file diff --git a/server/src/test/java/access/api/RoleControllerTest.java b/server/src/test/java/access/api/RoleControllerTest.java index 92ac1448..f003e960 100644 --- a/server/src/test/java/access/api/RoleControllerTest.java +++ b/server/src/test/java/access/api/RoleControllerTest.java @@ -2,21 +2,17 @@ import access.AbstractTest; import access.AccessCookieFilter; -import access.Seed; import access.manage.EntityType; import access.model.RemoteProvisionedGroup; import access.model.Role; import access.model.RoleExists; import io.restassured.common.mapper.TypeRef; import io.restassured.http.ContentType; -import io.restassured.http.Headers; import org.junit.jupiter.api.Test; -import org.springframework.http.HttpHeaders; import java.util.List; import java.util.Map; import java.util.UUID; -import java.util.function.UnaryOperator; import static access.Seed.*; import static access.security.SecurityConfig.API_TOKEN_HEADER; @@ -197,7 +193,7 @@ void rolesByApplicationInstitutionAdmin() throws Exception { super.stubForManageProviderByOrganisationGUID(ORGANISATION_GUID); AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/me", INSTITUTION_ADMIN, - institutionalAdminEntitlementOperator); + institutionalAdminEntitlementOperator(ORGANISATION_GUID)); List roles = given() .when() .filter(accessCookieFilter.cookieFilter()) @@ -324,6 +320,23 @@ void roleNotFound() throws Exception { } + @Test + void rolesByApplicationInstitutionAdminByAPI() throws Exception { + super.stubForManageProviderByEntityID(EntityType.SAML20_SP, "https://wiki"); + super.stubForManageProviderByEntityID(EntityType.OIDC10_RP, "https://wiki"); + super.stubForManageProviderByOrganisationGUID(ORGANISATION_GUID); + + List roles = given() + .when() + .header(API_TOKEN_HEADER, API_TOKEN_HASH) + .accept(ContentType.JSON) + .contentType(ContentType.JSON) + .get("/api/external/v1/roles") + .as(new TypeRef<>() { + }); + assertEquals(4, roles.size()); + } + @Test void deleteRoleWithAPI() throws Exception { Role role = roleRepository.search("wiki", 1).get(0); @@ -341,7 +354,7 @@ void deleteRoleWithAPI() throws Exception { .header(API_TOKEN_HEADER, API_TOKEN_HASH) .contentType(ContentType.JSON) .pathParams("id", role.getId()) - .delete("/api/v1/roles/{id}") + .delete("/api/external/v1/roles/{id}") .then() .statusCode(204); assertEquals(0, roleRepository.search("wiki", 1).size()); diff --git a/server/src/test/java/access/api/UserControllerTest.java b/server/src/test/java/access/api/UserControllerTest.java index 9743f5dc..9015aaf9 100644 --- a/server/src/test/java/access/api/UserControllerTest.java +++ b/server/src/test/java/access/api/UserControllerTest.java @@ -97,7 +97,8 @@ void meWithOauth2Login() throws Exception { void institutionAdminProvision() throws Exception { super.stubForManageProviderByOrganisationGUID(ORGANISATION_GUID); - AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/me", "new_institution_admin", institutionalAdminEntitlementOperator); + AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/me", "new_institution_admin", + institutionalAdminEntitlementOperator(ORGANISATION_GUID)); User user = given() .when() diff --git a/server/src/test/java/access/cron/IdentityProviderTest.java b/server/src/test/java/access/cron/IdentityProviderTest.java new file mode 100644 index 00000000..ab32f385 --- /dev/null +++ b/server/src/test/java/access/cron/IdentityProviderTest.java @@ -0,0 +1,21 @@ +package access.cron; + +import org.junit.jupiter.api.Test; +import org.springframework.context.i18n.LocaleContextHolder; + +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.*; + +class IdentityProviderTest { + + @Test + void getName() { + IdentityProvider identityProvider = new IdentityProvider(null, "nl", "logo"); + assertEquals("nl", identityProvider.getName()); + + LocaleContextHolder.setLocale(Locale.forLanguageTag("nl")); + identityProvider = new IdentityProvider("en", "nl", "logo"); + assertEquals("nl", identityProvider.getName()); + } +} \ No newline at end of file