diff options
author | Jon Marius Venstad <jonmv@users.noreply.github.com> | 2019-05-03 15:37:06 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-05-03 15:37:06 +0200 |
commit | 93f18a00c63c6cfc8d2e1aebf2b5f7b67b91224b (patch) | |
tree | 0eda93540033f8057d5c1ee53ec0028116f7c466 /controller-server | |
parent | 3d51f852911edc8a165188c2cadbb8b7db669d4a (diff) | |
parent | 0999cc21b472b3d302b47ab331c4b243ab098c84 (diff) |
Merge pull request #9270 from vespa-engine/jvenstad/submission-minors
Jvenstad/submission minors
Diffstat (limited to 'controller-server')
8 files changed, 100 insertions, 61 deletions
diff --git a/controller-server/pom.xml b/controller-server/pom.xml index 3e7247bd44b..c6c6acafe15 100644 --- a/controller-server/pom.xml +++ b/controller-server/pom.xml @@ -160,12 +160,6 @@ </dependency> <dependency> - <groupId>org.apache.httpcomponents</groupId> - <artifactId>httpmime</artifactId> - <scope>test</scope> - </dependency> - - <dependency> <groupId>com.github.tomakehurst</groupId> <artifactId>wiremock-standalone</artifactId> <scope>test</scope> diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java index 1b9bf28f395..5c5ed88d916 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java @@ -1,6 +1,7 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.restapi.application; +import ai.vespa.hosted.api.Signatures; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableSet; import com.google.inject.Inject; @@ -88,10 +89,13 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; +import java.security.DigestInputStream; import java.security.Principal; import java.time.DayOfWeek; import java.time.Duration; import java.time.Instant; +import java.util.Arrays; +import java.util.Base64; import java.util.List; import java.util.Map; import java.util.Optional; @@ -911,7 +915,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { ZoneId zone = ZoneId.from(environment, region); // Get deployOptions - Map<String, byte[]> dataParts = new MultipartParser().parse(request); + Map<String, byte[]> dataParts = parseDataParts(request); if ( ! dataParts.containsKey("deployOptions")) return ErrorResponse.badRequest("Missing required form part 'deployOptions'"); Inspector deployOptions = SlimeUtils.jsonToSlime(dataParts.get("deployOptions")).get(); @@ -1399,7 +1403,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { } private HttpResponse submit(String tenant, String application, HttpRequest request) { - Map<String, byte[]> dataParts = new MultipartParser().parse(request); + Map<String, byte[]> dataParts = parseDataParts(request); Inspector submitOptions = SlimeUtils.jsonToSlime(dataParts.get(EnvironmentResource.SUBMIT_OPTIONS)).get(); SourceRevision sourceRevision = toSourceRevision(submitOptions); String authorEmail = submitOptions.field("authorEmail").asString(); @@ -1420,4 +1424,17 @@ public class ApplicationApiHandler extends LoggingRequestHandler { dataParts.get(EnvironmentResource.APPLICATION_TEST_ZIP)); } + private static Map<String, byte[]> parseDataParts(HttpRequest request) { + String contentHash = request.getHeader("x-Content-Hash"); + if (contentHash == null) + return new MultipartParser().parse(request); + + DigestInputStream digester = Signatures.sha256Digester(request.getData()); + var dataParts = new MultipartParser().parse(request.getHeader("Content-Type"), digester, request.getUri()); + if ( ! Arrays.equals(digester.getMessageDigest().digest(), Base64.getDecoder().decode(contentHash))) + throw new IllegalArgumentException("Value of X-Content-Hash header does not match computed content hash"); + + return dataParts; + } + } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/MultipartParser.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/MultipartParser.java index 75f4ff68f1e..3125a8a363a 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/MultipartParser.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/MultipartParser.java @@ -7,6 +7,8 @@ import org.apache.commons.fileupload.ParameterParser; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.net.URI; import java.util.HashMap; import java.util.Map; @@ -24,18 +26,24 @@ public class MultipartParser { * @throws IllegalArgumentException if this request is not a well-formed request with Content-Type multipart/form-data */ public Map<String, byte[]> parse(HttpRequest request) { + return parse(request.getHeader("Content-Type"), request.getData(), request.getUri()); + } + + /** + * Parses the given data stream for the given uri using the provided content-type header to determine boundaries. + * + * @throws IllegalArgumentException if this is not a well-formed request with Content-Type multipart/form-data + */ + public Map<String, byte[]> parse(String contentTypeHeader, InputStream data, URI uri) { try { ParameterParser parameterParser = new ParameterParser(); - Map<String, String> contentType = parameterParser.parse(request.getHeader("Content-Type"), ';'); + Map<String, String> contentType = parameterParser.parse(contentTypeHeader, ';'); if ( ! contentType.containsKey("multipart/form-data")) - throw new IllegalArgumentException("Expected a multipart message, but got Content-Type: " + - request.getHeader("Content-Type")); + throw new IllegalArgumentException("Expected a multipart message, but got Content-Type: " + contentTypeHeader); String boundary = contentType.get("boundary"); if (boundary == null) throw new IllegalArgumentException("Missing boundary property in Content-Type header"); - MultipartStream multipartStream = new MultipartStream(request.getData(), boundary.getBytes(), - 1000 * 1000, - null); + MultipartStream multipartStream = new MultipartStream(data, boundary.getBytes(), 1 << 20, null); boolean nextPart = multipartStream.skipPreamble(); Map<String, byte[]> parts = new HashMap<>(); while (nextPart) { @@ -55,7 +63,7 @@ public class MultipartParser { throw new IllegalArgumentException("Malformed multipart/form-data request", e); } catch(IOException e) { - throw new IllegalArgumentException("IO error reading multipart request " + request.getUri(), e); + throw new IllegalArgumentException("IO error reading multipart request " + uri, e); } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java index 5cf29179d2a..214413441d3 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java @@ -1,3 +1,4 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.restapi.filter; import ai.vespa.hosted.api.Method; @@ -59,7 +60,8 @@ public class SignatureFilter extends JsonSecurityRequestFilterBase { if (verified) request.setAttribute(SecurityContext.ATTRIBUTE_NAME, new SecurityContext(() -> "buildService@" + id.tenant() + "." + id.application(), - Set.of(Role.buildService(id.tenant(), id.application())))); + Set.of(Role.buildService(id.tenant(), id.application()), + Role.applicationDeveloper(id.tenant(), id.application())))); } catch (Exception e) { logger.log(LogLevel.DEBUG, () -> "Exception verifying signed request: " + Exceptions.toMessageString(e)); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java index 7032112a860..3fcead5d0b6 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.hosted.controller.restapi.application; import ai.vespa.hosted.api.MultiPartStreamer; +import ai.vespa.hosted.api.Signatures; import com.yahoo.application.container.handler.Request; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; @@ -60,13 +61,9 @@ import com.yahoo.vespa.hosted.controller.restapi.ContainerTester; import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest; import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; import com.yahoo.yolean.Exceptions; -import org.apache.http.HttpEntity; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.mime.MultipartEntityBuilder; import org.junit.Before; import org.junit.Test; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; @@ -74,6 +71,7 @@ import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.ArrayList; +import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -207,10 +205,11 @@ public class ApplicationApiTest extends ControllerContainerTest { addUserToHostedOperatorRole(HostedAthenzIdentities.from(HOSTED_VESPA_OPERATOR)); - // POST (deploy) an application to a zone - manual user deployment - HttpEntity entity = createApplicationDeployData(applicationPackage, true); + // POST (deploy) an application to a zone - manual user deployment (includes a content hash for verification) + MultiPartStreamer entity = createApplicationDeployData(applicationPackage, true); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default/deploy", POST) .data(entity) + .header("X-Content-Hash", Base64.getEncoder().encodeToString(Signatures.sha256Digest(entity::data))) .userIdentity(USER_ID), new File("deploy-result.json")); @@ -568,6 +567,21 @@ public class ApplicationApiTest extends ControllerContainerTest { .data(createApplicationSubmissionData(packageWithService)), "{\"message\":\"Application package version: 1.0.44-d00d, source revision of repository 'repo', branch 'master' with commit 'd00d', by a@b, built against 6.1 at 1970-01-01T00:00:01Z\"}"); + // Fourth attempt has a wrong content hash in a header, and fails. + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/submit", POST) + .screwdriverIdentity(SCREWDRIVER_ID) + .header("X-Content-Hash", "not/the/right/hash") + .data(createApplicationSubmissionData(packageWithService)), + "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Value of X-Content-Hash header does not match computed content hash\"}", 400); + + // Fifth attempt has the right content hash in a header, and succeeds. + MultiPartStreamer streamer = createApplicationSubmissionData(packageWithService); + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/submit", POST) + .screwdriverIdentity(SCREWDRIVER_ID) + .header("X-Content-Hash", Base64.getEncoder().encodeToString(Signatures.sha256Digest(streamer::data))) + .data(streamer), + "{\"message\":\"Application package version: 1.0.45-d00d, source revision of repository 'repo', branch 'master' with commit 'd00d', by a@b, built against 6.1 at 1970-01-01T00:00:01Z\"}"); + ApplicationId app1 = ApplicationId.from("tenant1", "application1", "default"); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/jobreport", POST) .screwdriverIdentity(SCREWDRIVER_ID) @@ -672,7 +686,7 @@ public class ApplicationApiTest extends ControllerContainerTest { // Create tenant and deploy ApplicationId id = createTenantAndApplication(); long projectId = 1; - HttpEntity deployData = createApplicationDeployData(Optional.empty(), false); + MultiPartStreamer deployData = createApplicationDeployData(Optional.empty(), false); startAndTestChange(controllerTester, id, projectId, applicationPackage, deployData, 100); // us-west-1 @@ -754,14 +768,14 @@ public class ApplicationApiTest extends ControllerContainerTest { new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId("application1")); // POST (deploy) an application to a prod zone - allowed when project ID is not specified - HttpEntity entity = createApplicationDeployData(applicationPackage, true); + MultiPartStreamer entity = createApplicationDeployData(applicationPackage, true); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/default/deploy", POST) .data(entity) .screwdriverIdentity(SCREWDRIVER_ID), new File("deploy-result.json")); // POST (deploy) a system application with an application package - HttpEntity noAppEntity = createApplicationDeployData(Optional.empty(), true); + MultiPartStreamer noAppEntity = createApplicationDeployData(Optional.empty(), true); tester.assertResponse(request("/application/v4/tenant/hosted-vespa/application/routing/environment/prod/region/us-central-1/instance/default/deploy", POST) .data(noAppEntity) .userIdentity(HOSTED_VESPA_OPERATOR), @@ -790,7 +804,7 @@ public class ApplicationApiTest extends ControllerContainerTest { .build(); ApplicationId id = createTenantAndApplication(); long projectId = 1; - HttpEntity deployData = createApplicationDeployData(Optional.empty(), false); + MultiPartStreamer deployData = createApplicationDeployData(Optional.empty(), false); startAndTestChange(controllerTester, id, projectId, applicationPackage, deployData, 100); // us-east-3 @@ -935,7 +949,7 @@ public class ApplicationApiTest extends ControllerContainerTest { configServer.throwOnNextPrepare(new ConfigServerException(new URI("server-url"), "Failed to prepare application", ConfigServerException.ErrorCode.INVALID_APPLICATION_PACKAGE, null)); // POST (deploy) an application with an invalid application package - HttpEntity entity = createApplicationDeployData(applicationPackage, true); + MultiPartStreamer entity = createApplicationDeployData(applicationPackage, true); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default/deploy", POST) .data(entity) .userIdentity(USER_ID), @@ -1061,7 +1075,7 @@ public class ApplicationApiTest extends ControllerContainerTest { 200); // Deploy to an authorized zone by a user tenant is disallowed - HttpEntity entity = createApplicationDeployData(applicationPackage, true); + MultiPartStreamer entity = createApplicationDeployData(applicationPackage, true); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default/deploy", POST) .data(entity) .userIdentity(USER_ID), @@ -1187,7 +1201,7 @@ public class ApplicationApiTest extends ControllerContainerTest { // POST (deploy) an application to a dev zone String expectedResult="{\"error-code\":\"BAD_REQUEST\",\"message\":\"User user.new-user is not allowed to launch services in Athenz domain domain1. Please reach out to the domain admin.\"}"; - HttpEntity entity = createApplicationDeployData(applicationPackage, true); + MultiPartStreamer entity = createApplicationDeployData(applicationPackage, true); tester.assertResponse(request("/application/v4/tenant/by-new-user/application/application1/environment/dev/region/us-west-1/instance/default", POST) .data(entity) .userIdentity(userId), @@ -1220,7 +1234,7 @@ public class ApplicationApiTest extends ControllerContainerTest { .build(); // POST (deploy) an application to a dev zone - HttpEntity entity = createApplicationDeployData(applicationPackage, true); + MultiPartStreamer entity = createApplicationDeployData(applicationPackage, true); tester.assertResponse(request("/application/v4/tenant/by-new-user/application/application1/environment/dev/region/us-west-1/instance/default", POST) .data(entity) .userIdentity(tenantAdmin), @@ -1403,20 +1417,20 @@ public class ApplicationApiTest extends ControllerContainerTest { } } - private HttpEntity createApplicationDeployData(ApplicationPackage applicationPackage, boolean deployDirectly) { + private MultiPartStreamer createApplicationDeployData(ApplicationPackage applicationPackage, boolean deployDirectly) { return createApplicationDeployData(Optional.of(applicationPackage), deployDirectly); } - private HttpEntity createApplicationDeployData(Optional<ApplicationPackage> applicationPackage, boolean deployDirectly) { + private MultiPartStreamer createApplicationDeployData(Optional<ApplicationPackage> applicationPackage, boolean deployDirectly) { return createApplicationDeployData(applicationPackage, Optional.empty(), deployDirectly); } - private HttpEntity createApplicationDeployData(Optional<ApplicationPackage> applicationPackage, + private MultiPartStreamer createApplicationDeployData(Optional<ApplicationPackage> applicationPackage, Optional<ApplicationVersion> applicationVersion, boolean deployDirectly) { - MultipartEntityBuilder builder = MultipartEntityBuilder.create(); - builder.addTextBody("deployOptions", deployOptions(deployDirectly, applicationVersion), ContentType.APPLICATION_JSON); - applicationPackage.ifPresent(ap -> builder.addBinaryBody("applicationZip", ap.zippedContent())); - return builder.build(); + MultiPartStreamer streamer = new MultiPartStreamer(); + streamer.addJson("deployOptions", deployOptions(deployDirectly, applicationVersion)); + applicationPackage.ifPresent(ap -> streamer.addBytes("applicationZip", ap.zippedContent())); + return streamer; } private MultiPartStreamer createApplicationSubmissionData(ApplicationPackage applicationPackage) { @@ -1492,7 +1506,7 @@ public class ApplicationApiTest extends ControllerContainerTest { private void startAndTestChange(ContainerControllerTester controllerTester, ApplicationId application, long projectId, ApplicationPackage applicationPackage, - HttpEntity deployData, long buildNumber) { + MultiPartStreamer deployData, long buildNumber) { ContainerTester tester = controllerTester.containerTester(); // Trigger application change @@ -1618,6 +1632,7 @@ public class ApplicationApiTest extends ControllerContainerTest { private AthenzIdentity identity; private OktaAccessToken oktaAccessToken; private String contentType = "application/json"; + private Map<String, List<String>> headers = new HashMap<>(); private String recursive; private RequestBuilder(String path, Request.Method method) { @@ -1630,20 +1645,17 @@ public class ApplicationApiTest extends ControllerContainerTest { private RequestBuilder data(MultiPartStreamer streamer) { return Exceptions.uncheck(() -> data(streamer.data().readAllBytes()).contentType(streamer.contentType())); } - private RequestBuilder data(HttpEntity data) { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - try { - data.writeTo(out); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - return data(out.toByteArray()).contentType(data.getContentType().getValue()); - } + private RequestBuilder userIdentity(UserId userId) { this.identity = HostedAthenzIdentities.from(userId); return this; } private RequestBuilder screwdriverIdentity(ScrewdriverId screwdriverId) { this.identity = HostedAthenzIdentities.from(screwdriverId); return this; } private RequestBuilder oktaAccessToken(OktaAccessToken oktaAccessToken) { this.oktaAccessToken = oktaAccessToken; return this; } private RequestBuilder contentType(String contentType) { this.contentType = contentType; return this; } private RequestBuilder recursive(String recursive) { this.recursive = recursive; return this; } + private RequestBuilder header(String name, String value) { + this.headers.putIfAbsent(name, new ArrayList<>()); + this.headers.get(name).add(value); + return this; + } @Override public Request get() { @@ -1651,6 +1663,7 @@ public class ApplicationApiTest extends ControllerContainerTest { // user and domain parameters are translated to a Principal by MockAuthorizer as we do not run HTTP filters (recursive == null ? "" : "?recursive=" + recursive), data, method); + request.getHeaders().addAll(headers); request.getHeaders().put("Content-Type", contentType); if (identity != null) { addIdentityToRequest(request, identity); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/jobs.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/jobs.json index 5ac896e2753..72dd13474dc 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/jobs.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/jobs.json @@ -7,8 +7,8 @@ }, "application": { "application": { - "hash": "1.0.44-d00d", - "build": 44, + "hash": "1.0.45-d00d", + "build": 45, "source": { "gitRepository": "repo", "gitBranch": "master", @@ -21,8 +21,8 @@ }, "deploying": { "application": { - "hash": "1.0.44-d00d", - "build": 44, + "hash": "1.0.45-d00d", + "build": 45, "source": { "gitRepository": "repo", "gitBranch": "master", @@ -42,8 +42,8 @@ "start": (ignore), "wantedPlatform": "6.1", "wantedApplication": { - "hash": "1.0.44-d00d", - "build": 44, + "hash": "1.0.45-d00d", + "build": 45, "source": { "gitRepository": "repo", "gitBranch": "master", @@ -78,8 +78,8 @@ "start": (ignore), "wantedPlatform": "6.1", "wantedApplication": { - "hash": "1.0.44-d00d", - "build": 44, + "hash": "1.0.45-d00d", + "build": 45, "source": { "gitRepository": "repo", "gitBranch": "master", @@ -112,8 +112,8 @@ "status": "pending", "wantedPlatform": "6.1", "wantedApplication": { - "hash": "1.0.44-d00d", - "build": 44, + "hash": "1.0.45-d00d", + "build": 45, "source": { "gitRepository": "repo", "gitBranch": "master", diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-job.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-job.json index 2f5c6e489bd..107d969e8ad 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-job.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-job.json @@ -5,8 +5,8 @@ "start": (ignore), "wantedPlatform": "6.1", "wantedApplication": { - "hash": "1.0.44-d00d", - "build": 44, + "hash": "1.0.45-d00d", + "build": 45, "source": { "gitRepository": "repo", "gitBranch": "master", diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java index bf44481c110..0bddae11572 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java @@ -1,3 +1,4 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.restapi.filter; import ai.vespa.hosted.api.Method; @@ -83,7 +84,9 @@ public class SignatureFilterTest { assertTrue(filter.filter(signed).isEmpty()); SecurityContext securityContext = (SecurityContext) signed.getAttribute(SecurityContext.ATTRIBUTE_NAME); assertEquals("buildService@my-tenant.my-app", securityContext.principal().getName()); - assertEquals(Set.of(Role.buildService(id.tenant(), id.application())), securityContext.roles()); + assertEquals(Set.of(Role.buildService(id.tenant(), id.application()), + Role.applicationDeveloper(id.tenant(), id.application())), + securityContext.roles()); // Signed POST request also gets a build service role. byte[] hiBytes = new byte[]{0x48, 0x69}; @@ -91,7 +94,9 @@ public class SignatureFilterTest { filter.filter(signed); securityContext = (SecurityContext) signed.getAttribute(SecurityContext.ATTRIBUTE_NAME); assertEquals("buildService@my-tenant.my-app", securityContext.principal().getName()); - assertEquals(Set.of(Role.buildService(id.tenant(), id.application())), securityContext.roles()); + assertEquals(Set.of(Role.buildService(id.tenant(), id.application()), + Role.applicationDeveloper(id.tenant(), id.application())), + securityContext.roles()); // Unsigned requests still get no roles. filter.filter(unsigned); |