From 7db1aea26a65a8b46c2649798f72716653bff1c4 Mon Sep 17 00:00:00 2001 From: Gokhan Date: Mon, 20 Jul 2026 21:51:39 -0700 Subject: [PATCH 1/2] Kanban 1855: Add SecretProvider.getAppName() for per-app cost attribution SecretProvider gains a default getAppName() (null by default), and SecretService exposes it by querying active providers in priority order. This gives any SecretProvider implementation a standard way to report the application it's scoped to, without callers needing to know which concrete provider is active or how it derives that name. Consumed by premiumModules' SsmSecretProvider (LabKey/premiumModules#665) to attribute Vertex AI request cost per app via GCP billing labels, without professional needing a direct dependency on cloudServices. --- .../labkey/api/secrets/SecretProvider.java | 10 ++++ .../org/labkey/api/secrets/SecretService.java | 6 +++ .../core/secrets/SecretServiceImpl.java | 47 +++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/api/src/org/labkey/api/secrets/SecretProvider.java b/api/src/org/labkey/api/secrets/SecretProvider.java index 928d5268cc9..6401c06f5ac 100644 --- a/api/src/org/labkey/api/secrets/SecretProvider.java +++ b/api/src/org/labkey/api/secrets/SecretProvider.java @@ -30,4 +30,14 @@ public interface SecretProvider /** Human-readable name for this source, shown on the admin secrets page. */ @NotNull String getDescription(); + + /** + * Returns the name of the application this source is scoped to, or {@code null} if this + * source has no notion of an application name (the default). Used for attributing + * externally-billed resources (e.g., Vertex AI requests) back to the owning app. + */ + default @Nullable String getAppName() + { + return null; + } } diff --git a/api/src/org/labkey/api/secrets/SecretService.java b/api/src/org/labkey/api/secrets/SecretService.java index 8dd22cdd7d4..1ca7d08cd7f 100644 --- a/api/src/org/labkey/api/secrets/SecretService.java +++ b/api/src/org/labkey/api/secrets/SecretService.java @@ -90,4 +90,10 @@ static void setInstance(SecretService service) * The external provider takes priority over startup-property and environment-variable sources. */ @Nullable String getExternalProviderDescription(); + + /** + * Returns the application name reported by the highest-priority provider that has one + * (see {@link SecretProvider#getAppName()}), or {@code null} if no active provider reports one. + */ + @Nullable String getAppName(); } diff --git a/core/src/org/labkey/core/secrets/SecretServiceImpl.java b/core/src/org/labkey/core/secrets/SecretServiceImpl.java index b5d4cd064ae..4af583846d7 100644 --- a/core/src/org/labkey/core/secrets/SecretServiceImpl.java +++ b/core/src/org/labkey/core/secrets/SecretServiceImpl.java @@ -142,6 +142,18 @@ public void setExternalProvider(@NotNull SecretProvider provider) return provider != null ? provider.getDescription() : null; } + @Override + public @Nullable String getAppName() + { + for (SecretProvider provider : activeProviders()) + { + String appName = provider.getAppName(); + if (appName != null) + return appName; + } + return null; + } + public void shutdown() { SecretProvider external = _externalProvider; @@ -237,6 +249,41 @@ public void testExternalProviderPriority() assertEquals("from-external", svc.getSecret(prop)); } + @Test + public void testGetAppNameFromExternalProvider() + { + SecretServiceImpl svc = new SecretServiceImpl(); + svc.setExternalProvider(new SecretProvider() + { + @Override + public @Nullable String getSecret(String propertyName) + { + return null; + } + + @Override + public @NotNull String getDescription() + { + return "Test provider"; + } + + @Override + public @Nullable String getAppName() + { + return "MyApp"; + } + }); + + assertEquals("MyApp", svc.getAppName()); + } + + @Test + public void testGetAppNameNullWhenNoProviderReportsOne() + { + SecretServiceImpl svc = new SecretServiceImpl(); + assertNull(svc.getAppName()); + } + private StartupPropertySecretProvider startupProviderWith(String name, String value) { StartupPropertySecretProvider provider = new StartupPropertySecretProvider(); From a2f723a925923b5524991e7d4080ddfe292ce1e0 Mon Sep 17 00:00:00 2001 From: Gokhan Date: Tue, 21 Jul 2026 15:58:01 -0700 Subject: [PATCH 2/2] Kanban 1855: Add AppProps.getAppName() delegating to SecretService Exposes app name where a dev would naturally look first (on AppProps), while keeping SecretService as the actual source of truth so no new syseng/DevOps-managed startup property is needed. Addresses PR review feedback from Matthew and Adam on #7867. --- api/src/org/labkey/api/settings/AppProps.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/api/src/org/labkey/api/settings/AppProps.java b/api/src/org/labkey/api/settings/AppProps.java index 3ae76372530..064127076d6 100644 --- a/api/src/org/labkey/api/settings/AppProps.java +++ b/api/src/org/labkey/api/settings/AppProps.java @@ -21,6 +21,7 @@ import org.labkey.api.data.ContainerManager; import org.labkey.api.module.DefaultModule; import org.labkey.api.module.SupportedDatabase; +import org.labkey.api.secrets.SecretService; import org.labkey.api.util.ExceptionReportingLevel; import org.labkey.api.util.Path; import org.labkey.api.util.UsageReportingLevel; @@ -75,6 +76,17 @@ static WriteableAppProps getWriteableInstance() @Nullable String getEnlistmentId(); + /** + * Returns the DevOps-assigned name of the tenant/customer this deployment belongs to (e.g., "Hooli", + * "WNPRC"), or {@code null} if none is configured. Not customer-facing; currently sourced from + * whichever active {@link org.labkey.api.secrets.SecretProvider} reports one. + * @see SecretService#getAppName() + */ + default @Nullable String getAppName() + { + return SecretService.get().getAppName(); + } + boolean isCachingAllowed(); boolean isRecompileJspEnabled();