diff --git a/pom.xml b/pom.xml index 54e68aa2a116c86243f226bcf41bb7d5fc16a072..bd786c5b540a6cf71ac141d49ea5e9ee59d50ff4 100644 --- a/pom.xml +++ b/pom.xml @@ -60,6 +60,10 @@ <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy-jackson</artifactId> </dependency> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-oidc</artifactId> + </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-context-propagation</artifactId> @@ -115,6 +119,21 @@ <artifactId>rest-assured</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-test-security</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-test-security-oidc</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-test-oidc-server</artifactId> + <scope>test</scope> + </dependency> <dependency> <groupId>io.rest-assured</groupId> <artifactId>json-schema-validator</artifactId> diff --git a/spec/openapi.yaml b/spec/openapi.yaml index a902e0a27c9a72d9c33a6b56efa51b9e67d0f7ef..ac818bfb286fd49d4e05c1cc87d46ee167f679fd 100644 --- a/spec/openapi.yaml +++ b/spec/openapi.yaml @@ -131,6 +131,8 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + 401: + description: Unauthorized - user not allowed 500: description: Error while processing request diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/ReportsResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/ReportsResource.java index ad153d39b1e764b23101be97c218f586895349b3..5959731f1a0788e9be295cf45e55bab0c1588b5e 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/resource/ReportsResource.java +++ b/src/main/java/org/eclipsefoundation/git/eca/resource/ReportsResource.java @@ -12,6 +12,7 @@ package org.eclipsefoundation.git.eca.resource; import java.time.LocalDate; +import java.util.List; import javax.inject.Inject; import javax.ws.rs.BadRequestException; @@ -21,23 +22,39 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; +import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipsefoundation.core.model.RequestWrapper; import org.eclipsefoundation.git.eca.namespace.GitEcaParameterNames; import org.eclipsefoundation.git.eca.service.ReportsService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import io.quarkus.security.Authenticated; +import io.quarkus.security.identity.SecurityIdentity; + +@Authenticated @Path("/reports") public class ReportsResource { + private static final Logger LOGGER = LoggerFactory.getLogger(ReportsResource.class); + + @ConfigProperty(name = "eclipse.reports.allowed-users") + List<String> allowedUsers; @Inject RequestWrapper wrap; @Inject ReportsService reportsService; + @Inject + SecurityIdentity ident; @GET @Path("/gitlab/private-projects") public Response getPrivateProjectEvents(@QueryParam("status") String status, @QueryParam("since") LocalDate since, @QueryParam("until") LocalDate until) { - + if (!allowedUsers.contains(ident.getPrincipal().getName())) { + LOGGER.debug("User '{}' does not have access to the reports, access blocked", ident.getPrincipal().getName()); + return Response.status(401).build(); + } if (StringUtils.isNotBlank(status) && !isValidStatus(status)) { throw new BadRequestException(String.format("Invalid 'status' parameter: %s", status)); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 7f1acfd66b080cc69610fbd6ae489cbbd7eb00d0..c5c46d3a5b52f2a6f30ddadf76cf27788576d448 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,8 +6,12 @@ quarkus.rest-client."org.eclipsefoundation.git.eca.api.GitlabAPI".url=https://gi eclipse.noreply.email-patterns=@users.noreply.github.com\$ +## Base HTTP settings +quarkus.http.enable-compression=true +quarkus.http.port=8080 ## Expect to be mounted to '/git' to match current URL spec quarkus.http.root-path=/git + ## DATASOURCE CONFIG eclipse.db.default.limit=10 eclipse.db.default.limit.max=100 @@ -19,16 +23,17 @@ quarkus.datasource.jdbc.max-size = 15 quarkus.hibernate-orm.packages=org.eclipsefoundation.git.eca.dto quarkus.hibernate-orm.datasource=<default> -quarkus.http.enable-compression=true - -quarkus.http.port=8080 - ## OAUTH CONFIG quarkus.oauth2.enabled=false -quarkus.oidc.enabled=false +quarkus.oidc.application-type=web-app +quarkus.oidc.token.refresh-expired=true +quarkus.oidc.authentication.session-age-extension=60m +quarkus.oidc.discovery-enabled=true +quarkus.oidc.roles.source=accesstoken oauth2.scope=eclipsefdn_view_all_profiles oauth2.client-id=placeholder oauth2.client-secret=placeholder +eclipse.reports.allowed-users=mbarbaro,webdev quarkus.cache.caffeine."default".initial-capacity=1000 quarkus.cache.caffeine."default".expire-after-write=1H diff --git a/src/test/java/org/eclipsefoundation/git/eca/resource/ReportsResourceTest.java b/src/test/java/org/eclipsefoundation/git/eca/resource/ReportsResourceTest.java index 3e96bd0067cbd5fc03e02044ed93ed43ffecdac0..cd3983dcec825b1f7f6d6d8d485b3a2cdb73bf0b 100644 --- a/src/test/java/org/eclipsefoundation/git/eca/resource/ReportsResourceTest.java +++ b/src/test/java/org/eclipsefoundation/git/eca/resource/ReportsResourceTest.java @@ -13,12 +13,17 @@ package org.eclipsefoundation.git.eca.resource; import org.eclipsefoundation.git.eca.namespace.GitEcaParameterNames; import org.eclipsefoundation.git.eca.test.namespaces.SchemaNamespaceHelper; +import org.eclipsefoundation.testing.helpers.AuthHelper; import org.eclipsefoundation.testing.helpers.TestCaseHelper; import org.eclipsefoundation.testing.templates.RestAssuredTemplates; import org.eclipsefoundation.testing.templates.RestAssuredTemplates.EndpointTestCase; import org.junit.jupiter.api.Test; import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.quarkus.test.security.oidc.Claim; +import io.quarkus.test.security.oidc.ConfigMetadata; +import io.quarkus.test.security.oidc.OidcSecurity; import io.restassured.http.ContentType; @QuarkusTest @@ -31,104 +36,140 @@ class ReportsResourceTest { public static final String REPORTS_PROJECTS_RANGE_URL = REPORTS_PROJECTS_URL + "?since={start}&until={end}"; public static final EndpointTestCase GET_REPORT_SUCCESS_CASE = TestCaseHelper - .buildSuccessCase(REPORTS_PROJECTS_URL, new String[] {}, + .buildSuccessCase(REPORTS_PROJECTS_URL, new String[] {}, SchemaNamespaceHelper.PRIVATE_PROJECT_EVENTS_SCHEMA_PATH); + + public static final EndpointTestCase GET_REPORT_ACTIVE_SUCCESS_CASE = TestCaseHelper + .buildSuccessCase(REPORTS_PROJECTS_STATUS_URL, new String[] { GitEcaParameterNames.STATUS_ACTIVE.getName() }, SchemaNamespaceHelper.PRIVATE_PROJECT_EVENTS_SCHEMA_PATH); - public static final EndpointTestCase GET_REPORT_ACTIVE_SUCCESS_CASE = TestCaseHelper.buildSuccessCase( - REPORTS_PROJECTS_STATUS_URL, new String[] { GitEcaParameterNames.STATUS_ACTIVE.getName() }, - SchemaNamespaceHelper.PRIVATE_PROJECT_EVENTS_SCHEMA_PATH); + public static final EndpointTestCase GET_REPORT_DELETED_SUCCESS_CASE = TestCaseHelper + .buildSuccessCase(REPORTS_PROJECTS_STATUS_URL, new String[] { GitEcaParameterNames.STATUS_DELETED.getName() }, + SchemaNamespaceHelper.PRIVATE_PROJECT_EVENTS_SCHEMA_PATH); - public static final EndpointTestCase GET_REPORT_DELETED_SUCCESS_CASE = TestCaseHelper.buildSuccessCase( - REPORTS_PROJECTS_STATUS_URL, new String[] { GitEcaParameterNames.STATUS_DELETED.getName() }, - SchemaNamespaceHelper.PRIVATE_PROJECT_EVENTS_SCHEMA_PATH); + public static final EndpointTestCase GET_REPORT_SINCE_SUCCESS_CASE = TestCaseHelper + .buildSuccessCase(REPORTS_PROJECTS_SINCE_URL, new String[] { "2022-11-11" }, + SchemaNamespaceHelper.PRIVATE_PROJECT_EVENTS_SCHEMA_PATH); - public static final EndpointTestCase GET_REPORT_SINCE_SUCCESS_CASE = TestCaseHelper.buildSuccessCase( - REPORTS_PROJECTS_SINCE_URL, new String[] { "2022-11-11" }, - SchemaNamespaceHelper.PRIVATE_PROJECT_EVENTS_SCHEMA_PATH); + public static final EndpointTestCase GET_REPORT_UNTIL_SUCCESS_CASE = TestCaseHelper + .buildSuccessCase(REPORTS_PROJECTS_UNTIL_URL, new String[] { "2022-11-15" }, + SchemaNamespaceHelper.PRIVATE_PROJECT_EVENTS_SCHEMA_PATH); - public static final EndpointTestCase GET_REPORT_UNTIL_SUCCESS_CASE = TestCaseHelper.buildSuccessCase( - REPORTS_PROJECTS_UNTIL_URL, new String[] { "2022-11-15" }, - SchemaNamespaceHelper.PRIVATE_PROJECT_EVENTS_SCHEMA_PATH); + public static final EndpointTestCase GET_REPORT_RANGE_SUCCESS_CASE = TestCaseHelper + .buildSuccessCase(REPORTS_PROJECTS_RANGE_URL, new String[] { "2022-11-15", "2022-11-15" }, + SchemaNamespaceHelper.PRIVATE_PROJECT_EVENTS_SCHEMA_PATH); - public static final EndpointTestCase GET_REPORT_RANGE_SUCCESS_CASE = TestCaseHelper.buildSuccessCase( - REPORTS_PROJECTS_RANGE_URL, new String[] { "2022-11-15", "2022-11-15" }, - SchemaNamespaceHelper.PRIVATE_PROJECT_EVENTS_SCHEMA_PATH); + public static final EndpointTestCase GET_REPORT_BAD_STATUS_CASE = TestCaseHelper + .buildBadRequestCase(REPORTS_PROJECTS_STATUS_URL, new String[] { "nope" }, SchemaNamespaceHelper.ERROR_SCHEMA_PATH); - public static final EndpointTestCase GET_REPORT_BAD_STATUS_CASE = TestCaseHelper.buildBadRequestCase( - REPORTS_PROJECTS_STATUS_URL, new String[] { "nope" }, SchemaNamespaceHelper.ERROR_SCHEMA_PATH); + public static final EndpointTestCase GET_REPORT_BAD_UNTIL_CASE = TestCaseHelper + .buildBadRequestCase(REPORTS_PROJECTS_UNTIL_URL, new String[] { "nope" }, SchemaNamespaceHelper.ERROR_SCHEMA_PATH); - public static final EndpointTestCase GET_REPORT_BAD_UNTIL_CASE = TestCaseHelper.buildBadRequestCase( - REPORTS_PROJECTS_UNTIL_URL, new String[] { "nope" }, SchemaNamespaceHelper.ERROR_SCHEMA_PATH); + public static final EndpointTestCase GET_REPORT_BAD_SINCE_CASE = TestCaseHelper + .buildBadRequestCase(REPORTS_PROJECTS_SINCE_URL, new String[] { "nope" }, SchemaNamespaceHelper.ERROR_SCHEMA_PATH); - public static final EndpointTestCase GET_REPORT_BAD_SINCE_CASE = TestCaseHelper.buildBadRequestCase( - REPORTS_PROJECTS_SINCE_URL, new String[] { "nope" }, SchemaNamespaceHelper.ERROR_SCHEMA_PATH); + public static final EndpointTestCase GET_REPORT_UNAUTHORIZED_CASE = EndpointTestCase + .builder() + .setPath(REPORTS_PROJECTS_URL) + .setStatusCode(401) + .build(); /* - * GET /reports/webhooks/gitlab/system + * GET /reports/gitlab/private-projects */ @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) + @OidcSecurity(claims = { @Claim(key = AuthHelper.EMAIL_CLAIM_KEY, value = AuthHelper.EMAIL_CLAIM_VALUE) }, userinfo = {}, config = { + @ConfigMetadata(key = AuthHelper.ISSUER_FIELD_KEY, value = AuthHelper.ISSUER_FIELD_VALUE) }) void getPrivProjReport_success() { RestAssuredTemplates.testGet(GET_REPORT_SUCCESS_CASE); } @Test + @TestSecurity(user = "jwick", roles = AuthHelper.DEFAULT_ROLE) + void getPrivProjReport_success_multipleValidUsers() { + RestAssuredTemplates.testGet(GET_REPORT_SUCCESS_CASE); + } + + @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) void getPrivProjReport_success_validateSchema() { RestAssuredTemplates.testGet_validateSchema(GET_REPORT_SUCCESS_CASE); } @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) void getPrivProjReport_success_validateResponseFormat() { RestAssuredTemplates.testGet_validateResponseFormat(GET_REPORT_SUCCESS_CASE); } @Test + void getPrivProjReport_failure_unauthenticated() { + // Quarkus will apply 401 for failed authentication + RestAssuredTemplates.testGet(GET_REPORT_UNAUTHORIZED_CASE); + } + + @Test + @TestSecurity(user = "badActorz", roles = AuthHelper.DEFAULT_ROLE) + void getPrivProjReport_failure_unauthorizedUser() { + RestAssuredTemplates.testGet(GET_REPORT_UNAUTHORIZED_CASE); + } + + @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) void getPrivProjReport_failure_invalidRequestFormat() { - RestAssuredTemplates.testGet( - TestCaseHelper.buildInvalidFormatCase(REPORTS_PROJECTS_URL, new String[] {}, ContentType.TEXT)); + RestAssuredTemplates.testGet(TestCaseHelper.buildInvalidFormatCase(REPORTS_PROJECTS_URL, new String[] {}, ContentType.TEXT)); } /* - * GET /reports/webhooks/gitlab/system?status=active + * GET /reports/gitlab/private-projects?status=active */ @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) void getPrivProjReportActive_success() { RestAssuredTemplates.testGet(GET_REPORT_ACTIVE_SUCCESS_CASE); } @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) void getPrivProjReportActive_success_validateSchema() { RestAssuredTemplates.testGet_validateSchema(GET_REPORT_ACTIVE_SUCCESS_CASE); } @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) void getPrivProjReportActive_success_validateResponseFormat() { RestAssuredTemplates.testGet_validateResponseFormat(GET_REPORT_ACTIVE_SUCCESS_CASE); } @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) void getPrivProjReportStatus_failure_invalidStatus() { RestAssuredTemplates.testGet(GET_REPORT_BAD_STATUS_CASE); } @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) void getPrivProjReportStatus_failure_invalidStatus_validate_schema() { RestAssuredTemplates.testGet_validateSchema(GET_REPORT_BAD_STATUS_CASE); } /* - * GET /reports/webhooks/gitlab/system?status=deleted + * GET /reports/gitlab/private-projects?status=deleted */ @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) void getPrivProjReportDeleted_success() { RestAssuredTemplates.testGet(GET_REPORT_DELETED_SUCCESS_CASE); } @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) void getPrivProjReportDeleted_success_validateSchema() { RestAssuredTemplates.testGet_validateSchema(GET_REPORT_DELETED_SUCCESS_CASE); } @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) void getPrivProjReportDeleted_success_validateResponseFormat() { RestAssuredTemplates.testGet_validateResponseFormat(GET_REPORT_DELETED_SUCCESS_CASE); } @@ -137,54 +178,64 @@ class ReportsResourceTest { * GET /reports/webhooks/gitlab/system?since={date} */ @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) void getPrivProjReportSince_success() { RestAssuredTemplates.testGet(GET_REPORT_SINCE_SUCCESS_CASE); } @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) void getPrivProjReportSince_success_validateSchema() { RestAssuredTemplates.testGet_validateSchema(GET_REPORT_SINCE_SUCCESS_CASE); } @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) void getPrivProjReportSince_success_validateResponseFormat() { RestAssuredTemplates.testGet_validateResponseFormat(GET_REPORT_SINCE_SUCCESS_CASE); } @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) void getPrivProjReportSince_failure_invalidStatus() { RestAssuredTemplates.testGet(GET_REPORT_BAD_SINCE_CASE); } @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) void getPrivProjReportSince_failure_invalidStatus_validate_schema() { RestAssuredTemplates.testGet_validateSchema(GET_REPORT_BAD_SINCE_CASE); } /* - * GET /reports/webhooks/gitlab/system?until={date} + * GET /reports/gitlab/private-projects?until={date} */ @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) void getPrivProjReportUntil_success() { RestAssuredTemplates.testGet(GET_REPORT_UNTIL_SUCCESS_CASE); } @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) void getPrivProjReportUntil_success_validateSchema() { RestAssuredTemplates.testGet_validateSchema(GET_REPORT_UNTIL_SUCCESS_CASE); } @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) void getPrivProjReportUntil_success_validateResponseFormat() { RestAssuredTemplates.testGet_validateResponseFormat(GET_REPORT_UNTIL_SUCCESS_CASE); } @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) void getPrivProjReportUntil_failure_invalidStatus() { RestAssuredTemplates.testGet(GET_REPORT_BAD_UNTIL_CASE); } @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) void getPrivProjReportSUntil_failure_invalidStatus_validate_schema() { RestAssuredTemplates.testGet_validateSchema(GET_REPORT_BAD_UNTIL_CASE); } @@ -193,16 +244,19 @@ class ReportsResourceTest { * GET /reports/webhooks/gitlab/system?since={date}&until={date} */ @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) void getPrivProjReportRange_success() { RestAssuredTemplates.testGet(GET_REPORT_RANGE_SUCCESS_CASE); } @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) void getPrivProjReportRange_success_validateSchema() { RestAssuredTemplates.testGet_validateSchema(GET_REPORT_RANGE_SUCCESS_CASE); } @Test + @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) void getPrivProjReportRange_success_validateResponseFormat() { RestAssuredTemplates.testGet_validateResponseFormat(GET_REPORT_RANGE_SUCCESS_CASE); } diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 95c2d6c5838dc67215427431fd59d52d85e17be6..71bfbed292481d5b9290aa9ad184135c80c99ba9 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -4,6 +4,8 @@ org.eclipsefoundation.git.eca.api.BotsAPI/mp-rest/url=https://api.eclipse.org eclipse.noreply.email-patterns=@users.noreply.github.com\$ eclipse.mail.allowlist=noreply@github.com +eclipse.reports.allowed-users=opearson,jwick +eclipse.gitlab.access-token=token_val ## DATASOURCE CONFIG quarkus.datasource.db-kind=h2 @@ -17,12 +19,12 @@ quarkus.flyway.migrate-at-start=true ## Expect to be mounted to '/git' to match current URL spec quarkus.http.root-path=/git +quarkus.http.port=8080 ## OIDC Connection/Authentication Info -quarkus.oauth2.enabled=false quarkus.oidc.enabled=false quarkus.keycloak.devservices.enabled=false quarkus.oidc-client.enabled=false -quarkus.http.port=8080 +quarkus.oidc.client-id=quarkus-service-app +quarkus.oidc.application-type=service -eclipse.gitlab.access-token=token_val