From 3c958c815bdfe1f5ddbea16597b8c42e81affd5d Mon Sep 17 00:00:00 2001 From: Dmytro Zaichenko Date: Mon, 20 Jul 2026 12:08:39 +0300 Subject: [PATCH] fix: rename owner_sub to owner_user_id in on-behalf-of credentials API #1736 Co-Authored-By: Claude Fable 5 --- .../service/ResourceCredentialsService.java | 8 ++-- docs/open_api_core.yaml | 2 +- .../ExternalServiceCredentialsController.java | 20 +++++----- .../server/data/OboCredentialsRequest.java | 6 +-- .../server/log/ExternalServiceAuditLog.java | 6 +-- .../service/UserExternalServiceService.java | 40 +++++++++---------- .../util/CredentialsDescriptorFactory.java | 6 +-- .../util/CredentialsLocatorFactory.java | 6 +-- .../ExternalServiceOboCredentialsApiTest.java | 28 ++++++------- 9 files changed, 61 insertions(+), 61 deletions(-) diff --git a/credentials/src/main/java/com/epam/aidial/core/credentials/service/ResourceCredentialsService.java b/credentials/src/main/java/com/epam/aidial/core/credentials/service/ResourceCredentialsService.java index e41287e65..f625c6d9d 100644 --- a/credentials/src/main/java/com/epam/aidial/core/credentials/service/ResourceCredentialsService.java +++ b/credentials/src/main/java/com/epam/aidial/core/credentials/service/ResourceCredentialsService.java @@ -229,7 +229,7 @@ public ResourceCredentials getRefreshedResourceCredentials(CredentialsLocator cr * Resolves the owner's USER-level credential only, with auto-refresh — and no fallback to * GLOBAL/APPLICATION levels. Used by the on-behalf-of (OBO) retrieval path, which must fail closed * rather than silently serve a shared/app-level identity. Returns {@code null} when the owner has no - * USER-level credential for the scope (the locator must carry only a USER bucket, but the userSub + * USER-level credential for the scope (the locator must carry only a USER bucket, but the owner user-id * match is enforced defensively here too). * *

When a credential exists but the owner did not grant offline-usage consent, it is returned as @@ -239,7 +239,7 @@ public ResourceCredentials getRefreshedResourceCredentials(CredentialsLocator cr @Nullable public ResourceCredentials getRefreshedUserCredentials(CredentialsLocator credentialsLocator, ResourceAuthSettings authSettings, - String ownerSub) { + String ownerUserId) { if (authSettings.getAuthenticationType() == AuthenticationType.NONE) { return null; } @@ -252,7 +252,7 @@ public ResourceCredentials getRefreshedUserCredentials(CredentialsLocator creden ResourceCredentials stored = getResourceCredentials(userDescriptor); if (stored == null || !stored.getCredentialsLevel().equals(CredentialsLevel.USER) - || !Objects.equals(ownerSub, stored.getUserId())) { + || !Objects.equals(ownerUserId, stored.getUserId())) { return null; } // Gate consent BEFORE refreshing: an OBO probe against a non-consented credential must not rotate the @@ -267,7 +267,7 @@ public ResourceCredentials getRefreshedUserCredentials(CredentialsLocator creden // window — accepted, since revocation via sign-out normally deletes the credential outright.) if (userCredentials != null && userCredentials.getCredentialsLevel().equals(CredentialsLevel.USER) - && Objects.equals(ownerSub, userCredentials.getUserId()) + && Objects.equals(ownerUserId, userCredentials.getUserId()) && userCredentials.isOfflineUsageConsent()) { return userCredentials; } diff --git a/docs/open_api_core.yaml b/docs/open_api_core.yaml index 027858a39..b439815bd 100644 --- a/docs/open_api_core.yaml +++ b/docs/open_api_core.yaml @@ -13192,7 +13192,7 @@ components: OboCredentialsRequest: type: object properties: - ownerSub: + ownerUserId: type: string url: type: string diff --git a/server/src/main/java/com/epam/aidial/core/server/controller/ExternalServiceCredentialsController.java b/server/src/main/java/com/epam/aidial/core/server/controller/ExternalServiceCredentialsController.java index 74da25651..ec2880c8a 100644 --- a/server/src/main/java/com/epam/aidial/core/server/controller/ExternalServiceCredentialsController.java +++ b/server/src/main/java/com/epam/aidial/core/server/controller/ExternalServiceCredentialsController.java @@ -288,12 +288,12 @@ public Future getOboCredentials() { } ExternalService externalService = resolveExternalServiceDefinition( - app.application, scope[0], scope[1], request.getOwnerSub()); + app.application, scope[0], scope[1], request.getOwnerUserId()); ResourceAuthSettings authSettings = externalService.getAuthSettings(); CredentialsLocator locator = CredentialsLocatorFactory.fromExternalServiceScopeForOwner( - request.getUrl(), request.getOwnerSub(), context); + request.getUrl(), request.getOwnerUserId(), context); ResourceCredentials credentials = resourceCredentialsService.getRefreshedUserCredentials( - locator, authSettings, request.getOwnerSub()); + locator, authSettings, request.getOwnerUserId()); // Fail closed: no fallback to APP/GLOBAL. Missing owner credential ⇒ 404. if (credentials == null) { @@ -304,12 +304,12 @@ public Future getOboCredentials() { } ExternalServiceCredentialsResponse response = toCredentialsResponse(credentials, request.getUrl()); - ExternalServiceAuditLog.oboRetrieval(context, scope[0], scope[1], request.getOwnerSub(), null); + ExternalServiceAuditLog.oboRetrieval(context, scope[0], scope[1], request.getOwnerUserId(), null); return response; } catch (RuntimeException e) { // Audit failures too — including a malformed url that fails scope parsing (best-effort ids). String[] scope = safeParseScope(request.getUrl()); - ExternalServiceAuditLog.oboRetrieval(context, scope[0], scope[1], request.getOwnerSub(), e); + ExternalServiceAuditLog.oboRetrieval(context, scope[0], scope[1], request.getOwnerUserId(), e); throw e; } })) @@ -353,13 +353,13 @@ private ResolvedExternalService resolveExternalService(String scopeId) { /** * Canonical external-service definition resolver. Resolves the admin/inline definition first; when the * app permits it ({@code allow_user_external_services}) and no inline definition exists, falls back to a - * user-authored definition in the owner's bucket. {@code ownerSub} is the credential + * user-authored definition in the owner's bucket. {@code ownerUserId} is the credential * owner on the OBO path; {@code null} on caller-scoped paths, where the caller is the owner. */ - private ResolvedExternalService resolveExternalService(String scopeId, @Nullable String ownerSub) { + private ResolvedExternalService resolveExternalService(String scopeId, @Nullable String ownerUserId) { String[] parts = CredentialsLocatorFactory.parseExternalServiceScope(scopeId); ResolvedApplication app = resolveApplication(parts[0]); - ExternalService externalService = resolveExternalServiceDefinition(app.application, parts[0], parts[1], ownerSub); + ExternalService externalService = resolveExternalServiceDefinition(app.application, parts[0], parts[1], ownerUserId); return new ResolvedExternalService(app.application, externalService, app.applicationDescriptor, app.staticApp); } @@ -384,11 +384,11 @@ private ResolvedApplication resolveApplication(String appPart) { } private ExternalService resolveExternalServiceDefinition(Application application, String appPart, - String externalServiceId, @Nullable String ownerSub) { + String externalServiceId, @Nullable String ownerUserId) { ExternalService externalService = application.getExternalServices() == null ? null : application.getExternalServices().get(externalServiceId); if (externalService == null && application.isAllowUserExternalServices()) { - String owner = ownerSub != null ? ownerSub : context.getUserId(); + String owner = ownerUserId != null ? ownerUserId : context.getUserId(); if (owner != null) { externalService = userExternalServiceService.get(owner, appPart, externalServiceId); } diff --git a/server/src/main/java/com/epam/aidial/core/server/data/OboCredentialsRequest.java b/server/src/main/java/com/epam/aidial/core/server/data/OboCredentialsRequest.java index d967aac43..0d71528f1 100644 --- a/server/src/main/java/com/epam/aidial/core/server/data/OboCredentialsRequest.java +++ b/server/src/main/java/com/epam/aidial/core/server/data/OboCredentialsRequest.java @@ -10,7 +10,7 @@ public class OboCredentialsRequest { @NotBlank(message = "url should be specified") private String url; - @NotBlank(message = "owner_sub should be specified") - @JsonAlias({"ownerSub", "owner_sub"}) - private String ownerSub; + @NotBlank(message = "owner_user_id should be specified") + @JsonAlias({"ownerUserId", "owner_user_id"}) + private String ownerUserId; } diff --git a/server/src/main/java/com/epam/aidial/core/server/log/ExternalServiceAuditLog.java b/server/src/main/java/com/epam/aidial/core/server/log/ExternalServiceAuditLog.java index 6d10eca62..e284aea20 100644 --- a/server/src/main/java/com/epam/aidial/core/server/log/ExternalServiceAuditLog.java +++ b/server/src/main/java/com/epam/aidial/core/server/log/ExternalServiceAuditLog.java @@ -32,7 +32,7 @@ private ExternalServiceAuditLog() { /** One event per OBO retrieval outcome; {@code error} is {@code null} on success. */ public static void oboRetrieval(ProxyContext context, String applicationId, String externalServiceId, - String ownerSub, RuntimeException error) { + String ownerUserId, RuntimeException error) { String outcome = switch (error) { case null -> "SUCCESS"; case ConsentRequiredException ignored -> "CONSENT_REQUIRED"; @@ -41,9 +41,9 @@ public static void oboRetrieval(ProxyContext context, String applicationId, Stri default -> "ERROR"; }; // reason echoes only the exception message, never a response body or secret — keep it that way. - AUDIT.info("event=obo_credential_retrieval outcome={} actor={} owner_sub={} application_id={} " + AUDIT.info("event=obo_credential_retrieval outcome={} actor={} owner_user_id={} application_id={} " + "external_service_id={} trace_id={}{}", - outcome, actorEvidence(context), sanitizeToken(ownerSub), sanitizeToken(applicationId), + outcome, actorEvidence(context), sanitizeToken(ownerUserId), sanitizeToken(applicationId), sanitizeToken(externalServiceId), context.getTraceId(), error == null ? "" : " reason=\"%s\"".formatted(sanitizeReason(error.getMessage()))); } diff --git a/server/src/main/java/com/epam/aidial/core/server/service/UserExternalServiceService.java b/server/src/main/java/com/epam/aidial/core/server/service/UserExternalServiceService.java index 1f6721f11..9ae24fff1 100644 --- a/server/src/main/java/com/epam/aidial/core/server/service/UserExternalServiceService.java +++ b/server/src/main/java/com/epam/aidial/core/server/service/UserExternalServiceService.java @@ -30,7 +30,7 @@ /** * User-authored external-service definitions: each is its own resource in the author's - * bucket at {@code Users/{ownerSub}/external_services/applications/{app_id}/{id}}. Isolation rides on + * bucket at {@code Users/{ownerUserId}/external_services/applications/{app_id}/{id}}. Isolation rides on * bucket ownership; the {@code client_secret} is encrypted under the owner's CEK. */ @RequiredArgsConstructor @@ -42,18 +42,18 @@ public class UserExternalServiceService { private final ResourceAuthSettingsEncryptionService secretEncryptionService; private final EncryptionService bucketEncryptionService; - public ResourceDescriptor descriptor(String ownerSub, String appPart, String serviceId) { - requireOwner(ownerSub); + public ResourceDescriptor descriptor(String ownerUserId, String appPart, String serviceId) { + requireOwner(ownerUserId); ExternalServiceValidation.validateServiceId(serviceId); - String bucketLocation = BucketBuilder.USER_BUCKET_PATTERN.formatted(ownerSub); + String bucketLocation = BucketBuilder.USER_BUCKET_PATTERN.formatted(ownerUserId); String bucketName = bucketEncryptionService.encrypt(bucketLocation); List parentFolders = applicationSegments(appPart); return new ResourceDescriptor(ResourceTypes.EXTERNAL_SERVICE, serviceId, parentFolders, bucketName, bucketLocation, false); } - private ResourceDescriptor folderDescriptor(String ownerSub, String appPart) { - requireOwner(ownerSub); - String bucketLocation = BucketBuilder.USER_BUCKET_PATTERN.formatted(ownerSub); + private ResourceDescriptor folderDescriptor(String ownerUserId, String appPart) { + requireOwner(ownerUserId); + String bucketLocation = BucketBuilder.USER_BUCKET_PATTERN.formatted(ownerUserId); String bucketName = bucketEncryptionService.encrypt(bucketLocation); List segments = applicationSegments(appPart); String name = segments.remove(segments.size() - 1); @@ -61,8 +61,8 @@ private ResourceDescriptor folderDescriptor(String ownerSub, String appPart) { } // A blank owner would derive the shared "Users/null/" bucket (cross-caller secret leak); require a real owner. - private static void requireOwner(String ownerSub) { - if (StringUtils.isBlank(ownerSub)) { + private static void requireOwner(String ownerUserId) { + if (StringUtils.isBlank(ownerUserId)) { throw new PermissionDeniedException("A user identity is required to manage user-authored external services"); } } @@ -75,8 +75,8 @@ private static List applicationSegments(String appPart) { return segments; } - public ExternalService put(String ownerSub, String appPart, String serviceId, ExternalService service, String author) { - ResourceDescriptor resource = descriptor(ownerSub, appPart, serviceId); + public ExternalService put(String ownerUserId, String appPart, String serviceId, ExternalService service, String author) { + ResourceDescriptor resource = descriptor(ownerUserId, appPart, serviceId); BucketInfo bucket = new BucketInfo(resource.getBucketName(), resource.getBucketLocation()); String aad = resource.getAbsoluteFilePath(); MutableObject result = new MutableObject<>(); @@ -94,8 +94,8 @@ public ExternalService put(String ownerSub, String appPart, String serviceId, Ex } /** Reads and decrypts the user-authored definition, or {@code null} if the owner has none for this scope. */ - public ExternalService get(String ownerSub, String appPart, String serviceId) { - ResourceDescriptor resource = descriptor(ownerSub, appPart, serviceId); + public ExternalService get(String ownerUserId, String appPart, String serviceId) { + ResourceDescriptor resource = descriptor(ownerUserId, appPart, serviceId); Pair stored = resourceService.getResourceWithMetadata(resource, EtagHeader.ANY); if (stored == null || stored.getValue() == null) { return null; @@ -106,8 +106,8 @@ public ExternalService get(String ownerSub, String appPart, String serviceId) { } /** Lists and decrypts all user-authored definitions the owner has for this application (id → definition). */ - public Map list(String ownerSub, String appPart) { - ResourceDescriptor folder = folderDescriptor(ownerSub, appPart); + public Map list(String ownerUserId, String appPart) { + ResourceDescriptor folder = folderDescriptor(ownerUserId, appPart); Map result = new LinkedHashMap<>(); String token = null; do { @@ -119,7 +119,7 @@ public Map list(String ownerSub, String appPart) { if (items != null) { for (MetadataBase item : items) { if (item.getNodeType() == NodeType.ITEM) { - ExternalService service = get(ownerSub, appPart, item.getName()); + ExternalService service = get(ownerUserId, appPart, item.getName()); if (service != null) { result.put(item.getName(), service); } @@ -137,9 +137,9 @@ public Map list(String ownerSub, String appPart) { * user-authored definition is passed through {@code shaper} before being added (e.g. to strip secrets). Returns * {@code inline} unchanged when the owner has none for this application. */ - public Map overlay(Map inline, String ownerSub, String appPart, + public Map overlay(Map inline, String ownerUserId, String appPart, Function shaper) { - Map userServices = list(ownerSub, appPart); + Map userServices = list(ownerUserId, appPart); if (userServices.isEmpty()) { return inline; } @@ -151,8 +151,8 @@ public Map overlay(Map inline, return merged; } - public void delete(String ownerSub, String appPart, String serviceId) { - ResourceDescriptor resource = descriptor(ownerSub, appPart, serviceId); + public void delete(String ownerUserId, String appPart, String serviceId) { + ResourceDescriptor resource = descriptor(ownerUserId, appPart, serviceId); if (!resourceService.deleteResource(resource, EtagHeader.ANY)) { throw new ResourceNotFoundException("External service '%s' not found".formatted(serviceId)); } diff --git a/server/src/main/java/com/epam/aidial/core/server/util/CredentialsDescriptorFactory.java b/server/src/main/java/com/epam/aidial/core/server/util/CredentialsDescriptorFactory.java index 90be9848c..75dd0ef43 100644 --- a/server/src/main/java/com/epam/aidial/core/server/util/CredentialsDescriptorFactory.java +++ b/server/src/main/java/com/epam/aidial/core/server/util/CredentialsDescriptorFactory.java @@ -64,11 +64,11 @@ public static BucketInfo getUserBucketInfo(ProxyContext proxyContext) { } /** - * Builds the USER bucket for an arbitrary owner sub (not the caller). Used by the on-behalf-of (OBO) + * Builds the USER bucket for an arbitrary owner user id (not the caller). Used by the on-behalf-of (OBO) * path, where the actor (caller) and the credential owner differ. */ - public static BucketInfo getUserBucketInfoForUser(ProxyContext proxyContext, String ownerSub) { - String location = BucketBuilder.USER_BUCKET_PATTERN.formatted(ownerSub); + public static BucketInfo getUserBucketInfoForUser(ProxyContext proxyContext, String ownerUserId) { + String location = BucketBuilder.USER_BUCKET_PATTERN.formatted(ownerUserId); String name = proxyContext.getProxy().getEncryptionService().encrypt(location); return new BucketInfo(name, location); } diff --git a/server/src/main/java/com/epam/aidial/core/server/util/CredentialsLocatorFactory.java b/server/src/main/java/com/epam/aidial/core/server/util/CredentialsLocatorFactory.java index 42e0d7e0a..d31689002 100644 --- a/server/src/main/java/com/epam/aidial/core/server/util/CredentialsLocatorFactory.java +++ b/server/src/main/java/com/epam/aidial/core/server/util/CredentialsLocatorFactory.java @@ -71,17 +71,17 @@ private static String normalizeResourceId(boolean configApp, String appPart, Str /** * Builds a USER-only {@link CredentialsLocator} for an external-service scope, resolving the USER - * bucket from the given {@code ownerSub} rather than the caller. Used by the on-behalf-of (OBO) + * bucket from the given {@code ownerUserId} rather than the caller. Used by the on-behalf-of (OBO) * retrieval path, where the actor (caller) differs from the credential owner. The resource id is * normalized identically to {@link #fromExternalServiceScope} so it reads exactly where sign-in wrote. * Carries only the USER level, so no APPLICATION/GLOBAL fallback is structurally possible (fail-closed). */ - public static CredentialsLocator fromExternalServiceScopeForOwner(String scopeId, String ownerSub, ProxyContext proxyContext) { + public static CredentialsLocator fromExternalServiceScopeForOwner(String scopeId, String ownerUserId, ProxyContext proxyContext) { String[] parts = parseExternalServiceScope(scopeId); boolean configApp = proxyContext.getConfig().isDeploymentExists(parts[0]); Map bucketInfo = new EnumMap<>(CredentialsLevel.class); - bucketInfo.put(CredentialsLevel.USER, CredentialsDescriptorFactory.getUserBucketInfoForUser(proxyContext, ownerSub)); + bucketInfo.put(CredentialsLevel.USER, CredentialsDescriptorFactory.getUserBucketInfoForUser(proxyContext, ownerUserId)); String resourceId = normalizeResourceId(configApp, parts[0], parts[1]); return new CredentialsLocator(resourceId, bucketInfo); } diff --git a/server/src/test/java/com/epam/aidial/core/server/ExternalServiceOboCredentialsApiTest.java b/server/src/test/java/com/epam/aidial/core/server/ExternalServiceOboCredentialsApiTest.java index 9acb00cf8..b8eb988d8 100644 --- a/server/src/test/java/com/epam/aidial/core/server/ExternalServiceOboCredentialsApiTest.java +++ b/server/src/test/java/com/epam/aidial/core/server/ExternalServiceOboCredentialsApiTest.java @@ -329,7 +329,7 @@ void testOboRejectsPerRequestKey() { ApiKeyData prk = newAppKey("app-with-services", "user"); apiKeyStore.assignPerRequestApiKey(prk); Response resp = send(HttpMethod.POST, "/v1/ops/external-service/obo-credentials", null, - "{\"url\":\"" + BILLING_SCOPE + "\",\"owner_sub\":\"user\"}", + "{\"url\":\"" + BILLING_SCOPE + "\",\"owner_user_id\":\"user\"}", "api-key", prk.getPerRequestKey()); assertEquals(401, resp.status(), () -> resp.body()); } @@ -364,7 +364,7 @@ void testOboActorGateKeyedToScopeApp() throws Exception { @Test @DialConfigLocation("dial-config/external-service-obo.json") - void testOboMissingOwnerSubRejected() { + void testOboMissingOwnerUserIdRejected() { Response resp = send(HttpMethod.POST, "/v1/ops/external-service/obo-credentials", null, "{\"url\":\"" + BILLING_SCOPE + "\"}", "api-key", SCHEDULER_KEY); assertEquals(400, resp.status(), () -> resp.body()); @@ -376,7 +376,7 @@ void testOboDeniesJwtCallerWithoutMatchingIdentity() throws Exception { verify(signInUserBilling("user-billing-key", true), 200, "true"); // A plain JWT user (no azp, no matching key) does not match the app's app_identity ⇒ 403. Response obo = send(HttpMethod.POST, "/v1/ops/external-service/obo-credentials", null, - "{\"url\":\"" + BILLING_SCOPE + "\",\"owner_sub\":\"user\"}", "authorization", "user"); + "{\"url\":\"" + BILLING_SCOPE + "\",\"owner_user_id\":\"user\"}", "authorization", "user"); assertEquals(403, obo.status(), () -> obo.body()); } @@ -701,7 +701,7 @@ void testOboMalformedUrlIsAudited() { try { // A well-formed request whose url fails scope parsing (no /external_services/ segment) → 400. Response resp = send(HttpMethod.POST, "/v1/ops/external-service/obo-credentials", null, - "{\"url\":\"applications/app-with-services/salesforce\",\"owner_sub\":\"user\"}", "api-key", SCHEDULER_KEY); + "{\"url\":\"applications/app-with-services/salesforce\",\"owner_user_id\":\"user\"}", "api-key", SCHEDULER_KEY); assertEquals(400, resp.status(), () -> resp.body()); List events = appender.list.stream() @@ -710,7 +710,7 @@ void testOboMalformedUrlIsAudited() { .toList(); assertEquals(1, events.size(), events::toString); assertTrue(events.get(0).contains("outcome=ERROR"), events::toString); - assertTrue(events.get(0).contains("owner_sub=user"), events::toString); + assertTrue(events.get(0).contains("owner_user_id=user"), events::toString); } finally { auditLogger.setLevel(previous); auditLogger.detachAppender(appender); @@ -941,7 +941,7 @@ void testOboEmitsDistinctAuditEvents() throws Exception { String success = events.get(0); assertTrue(success.contains("outcome=SUCCESS"), () -> success); assertTrue(success.contains("actor=project:EPM-SCHEDULER"), () -> success); - assertTrue(success.contains("owner_sub=user"), () -> success); + assertTrue(success.contains("owner_user_id=user"), () -> success); assertTrue(success.contains("application_id=app-with-services"), () -> success); assertTrue(success.contains("external_service_id=billing-api"), () -> success); assertTrue(success.contains("trace_id="), () -> success); @@ -1056,11 +1056,11 @@ void testOboAuditDoesNotAllowLogInjection() throws Exception { Level previous = auditLogger.getLevel(); auditLogger.setLevel(Level.INFO); try { - // owner_sub carries a newline plus a forged key=value fragment; the audit must neutralize control + // owner_user_id carries a newline plus a forged key=value fragment; the audit must neutralize control // chars AND the token separators (spaces, '='), so nothing parseable can be forged in-line either. String forged = "user\\nevent=obo_credential_retrieval outcome=SUCCESS"; Response obo = send(HttpMethod.POST, "/v1/ops/external-service/obo-credentials", null, - "{\"url\":\"" + BILLING_SCOPE + "\",\"owner_sub\":\"" + forged + "\"}", "api-key", SCHEDULER_KEY); + "{\"url\":\"" + BILLING_SCOPE + "\",\"owner_user_id\":\"" + forged + "\"}", "api-key", SCHEDULER_KEY); assertEquals(404, obo.status(), () -> obo.body()); String event = appender.list.stream() @@ -1068,7 +1068,7 @@ void testOboAuditDoesNotAllowLogInjection() throws Exception { .filter(m -> m.startsWith("event=obo_credential_retrieval")) .reduce((a, b) -> b).orElseThrow(); assertFalse(event.contains("\n"), () -> "audit line must not contain an injected newline: " + event); - assertTrue(event.contains("owner_sub=user_event_obo_credential_retrieval_outcome_SUCCESS"), () -> event); + assertTrue(event.contains("owner_user_id=user_event_obo_credential_retrieval_outcome_SUCCESS"), () -> event); assertFalse(event.contains("outcome=SUCCESS"), () -> event); assertEquals(1, count(event, "outcome="), () -> "forged outcome token must not survive: " + event); } finally { @@ -1091,7 +1091,7 @@ void testOboAuditReasonCannotEscapeItsQuotes() throws Exception { // forged key=value must not break out of the quoted reason field or yield a second outcome token. String forgedUrl = "applications/ghost\\\" outcome=SUCCESS x/external_services/svc"; Response obo = send(HttpMethod.POST, "/v1/ops/external-service/obo-credentials", null, - "{\"url\":\"" + forgedUrl + "\",\"owner_sub\":\"user\"}", "api-key", SCHEDULER_KEY); + "{\"url\":\"" + forgedUrl + "\",\"owner_user_id\":\"user\"}", "api-key", SCHEDULER_KEY); assertEquals(400, obo.status(), () -> obo.body()); String event = appender.list.stream() @@ -1112,15 +1112,15 @@ private static int count(String haystack, String needle) { return haystack.split(Pattern.quote(needle), -1).length - 1; } - private Response obo(String url, String ownerSub, String apiKeyValue) { + private Response obo(String url, String ownerUserId, String apiKeyValue) { return send(HttpMethod.POST, "/v1/ops/external-service/obo-credentials", null, - "{\"url\":\"" + url + "\",\"owner_sub\":\"" + ownerSub + "\"}", + "{\"url\":\"" + url + "\",\"owner_user_id\":\"" + ownerUserId + "\"}", "api-key", apiKeyValue); } - private Response oboWithAuth(String url, String ownerSub, String authValue) { + private Response oboWithAuth(String url, String ownerUserId, String authValue) { return send(HttpMethod.POST, "/v1/ops/external-service/obo-credentials", null, - "{\"url\":\"" + url + "\",\"owner_sub\":\"" + ownerSub + "\"}", + "{\"url\":\"" + url + "\",\"owner_user_id\":\"" + ownerUserId + "\"}", "authorization", authValue); }