diff --git a/packages/plugins/apps/README.md b/packages/plugins/apps/README.md index 7a1ca0ccb..16aa87e23 100644 --- a/packages/plugins/apps/README.md +++ b/packages/plugins/apps/README.md @@ -72,13 +72,15 @@ Additional glob patterns (relative to the project root) to include in the upload ### apps.authOverrides.method -> default: `apiKey` +> default: `apiKey` when both `DD_API_KEY` and `DD_APP_KEY` are configured, otherwise `oauth` Authentication method for uploading app bundles. Use `apiKey` to send `DD_API_KEY`/`DD_APP_KEY` credentials from the shared `auth` config. Use `oauth` to complete a local Authorization Code + PKCE flow and upload with a short-lived bearer token instead. -You can also set `DATADOG_APPS_AUTH_METHOD=oauth` or `DD_APPS_AUTH_METHOD=oauth`. +When `apps.authOverrides.method` is not set, the plugin uses API/App-key auth if both keys are configured. If either key is missing, it uses OAuth by default. + +You can also set `DATADOG_APPS_AUTH_METHOD` or `DD_APPS_AUTH_METHOD` to `apiKey` or `oauth`. When the method is `oauth`, the plugin derives OAuth client settings from the resolved Datadog site. The plugin reads tokens from the OS credential store, refreshes expired access tokens when a refresh token is available, and only starts browser authorization when no usable stored token exists. diff --git a/packages/plugins/apps/src/validate.test.ts b/packages/plugins/apps/src/validate.test.ts index d9ad0c512..22e2e42de 100644 --- a/packages/plugins/apps/src/validate.test.ts +++ b/packages/plugins/apps/src/validate.test.ts @@ -4,13 +4,47 @@ import { validateOptions } from '@dd/apps-plugin/validate'; +const authEnvVars = [ + 'DATADOG_API_KEY', + 'DD_API_KEY', + 'DATADOG_APP_KEY', + 'DD_APP_KEY', + 'DATADOG_APPS_AUTH_METHOD', + 'DD_APPS_AUTH_METHOD', +] as const; + +const savedAuthEnv = Object.fromEntries( + authEnvVars.map((key) => [key, process.env[key]]), +) as Record<(typeof authEnvVars)[number], string | undefined>; + +const restoreAuthEnv = () => { + for (const key of authEnvVars) { + const value = savedAuthEnv[key]; + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } +}; + describe('Apps Plugin - validateOptions', () => { + beforeEach(() => { + for (const key of authEnvVars) { + delete process.env[key]; + } + }); + + afterEach(() => { + restoreAuthEnv(); + }); + describe('defaults', () => { - test('Should set defaults when nothing is provided', () => { + test('Should default to OAuth when API/App keys are not provided', () => { const result = validateOptions({}); expect(result).toEqual({ authOverrides: { - method: 'apiKey', + method: 'oauth', }, dryRun: true, include: [], @@ -19,6 +53,35 @@ describe('Apps Plugin - validateOptions', () => { }); }); + test('Should default to API-key auth when API/App keys are configured', () => { + const result = validateOptions({ + auth: { + apiKey: 'api-key', + appKey: 'app-key', + }, + }); + + expect(result.authOverrides.method).toBe('apiKey'); + }); + + test('Should default to API-key auth when API/App keys are configured through env vars', () => { + process.env.DATADOG_API_KEY = 'api-key'; + process.env.DATADOG_APP_KEY = 'app-key'; + + const result = validateOptions({}); + expect(result.authOverrides.method).toBe('apiKey'); + }); + + test('Should default to OAuth when API-key auth is incomplete', () => { + const result = validateOptions({ + auth: { + apiKey: 'api-key', + }, + }); + + expect(result.authOverrides.method).toBe('oauth'); + }); + test('Should set dryRun to false when DATADOG_APPS_UPLOAD_ASSETS is set', () => { process.env.DATADOG_APPS_UPLOAD_ASSETS = '1'; try { @@ -63,7 +126,7 @@ describe('Apps Plugin - validateOptions', () => { expect(result).toEqual({ authOverrides: { - method: 'apiKey', + method: 'oauth', }, dryRun: true, include: ['public/**/*', 'dist/**/*'], @@ -83,14 +146,44 @@ describe('Apps Plugin - validateOptions', () => { expect(result.authOverrides.method).toBe('oauth'); }); + test('Should prefer configured OAuth over available API/App keys', () => { + const result = validateOptions({ + auth: { + apiKey: 'api-key', + appKey: 'app-key', + }, + apps: { + enable: true, + authOverrides: { method: 'oauth' }, + }, + }); + + expect(result.authOverrides.method).toBe('oauth'); + }); + + test('Should enable API-key method when configured', () => { + const result = validateOptions({ + apps: { + enable: true, + authOverrides: { method: 'apiKey' }, + }, + }); + + expect(result.authOverrides.method).toBe('apiKey'); + }); + test('Should allow env vars to opt into OAuth', () => { process.env.DATADOG_APPS_AUTH_METHOD = 'oauth'; - try { - const result = validateOptions({ apps: {} }); - expect(result.authOverrides.method).toBe('oauth'); - } finally { - delete process.env.DATADOG_APPS_AUTH_METHOD; - } + + const result = validateOptions({ apps: {} }); + expect(result.authOverrides.method).toBe('oauth'); + }); + + test('Should allow env vars to opt into API-key auth', () => { + process.env.DATADOG_APPS_AUTH_METHOD = 'apiKey'; + + const result = validateOptions({ apps: {} }); + expect(result.authOverrides.method).toBe('apiKey'); }); }); }); diff --git a/packages/plugins/apps/src/validate.ts b/packages/plugins/apps/src/validate.ts index 4ceac46b3..026e9d9a0 100644 --- a/packages/plugins/apps/src/validate.ts +++ b/packages/plugins/apps/src/validate.ts @@ -22,12 +22,18 @@ const resolveAuthMethod = (value: string | undefined): AuthMethod | undefined => throw new Error(`apps.authOverrides.method must be one of: ${AUTH_METHODS.join(', ')}`); }; +const hasApiKeyAuth = (options: Options): boolean => + Boolean( + (getDDEnvValue('API_KEY') || options.auth?.apiKey) && + (getDDEnvValue('APP_KEY') || options.auth?.appKey), + ); + export const validateOptions = (options: Options): AppsOptionsWithDefaults => { const resolvedOptions = (options[CONFIG_KEY] || {}) as AppsOptions; const method = resolveAuthMethod( getDDEnvValue('APPS_AUTH_METHOD') || resolvedOptions.authOverrides?.method, - ) || 'apiKey'; + ) || (hasApiKeyAuth(options) ? 'apiKey' : 'oauth'); return { include: resolvedOptions.include || [], diff --git a/packages/plugins/apps/src/vite/dev-server.test.ts b/packages/plugins/apps/src/vite/dev-server.test.ts index 7a8620500..5de05e8d1 100644 --- a/packages/plugins/apps/src/vite/dev-server.test.ts +++ b/packages/plugins/apps/src/vite/dev-server.test.ts @@ -478,7 +478,7 @@ describe('Dev Server Middleware', () => { expect(apiScope.isDone()).toBe(true); }); - test('Should return 400 with auth guidance when default API-key auth is missing keys', async () => { + test('Should return 400 with auth guidance when explicit API-key auth is missing keys', async () => { const noKeyMiddleware = createDevServerMiddleware( mockViteBuild, () => mockFunctions,