Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/core/src/helpers/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const yellow = chalk.bold.yellow;
// - DATADOG_APPS_AUTH_METHOD
// - DD_SITE
// - DATADOG_SITE
const OVERRIDE_VARIABLES = [
export const OVERRIDE_VARIABLES = [
'API_KEY',
'APP_KEY',
'SOURCEMAP_INTAKE_URL',
Expand Down
6 changes: 4 additions & 2 deletions packages/plugins/apps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if OAuth is loaded when nothing is configured, then I think the default is oauth now.

Suggested change
> default: `apiKey` when both `DD_API_KEY` and `DD_APP_KEY` are configured, otherwise `oauth`
> default: `oauth`

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be when DD_API_KEY and DD_APP_KEY are set, the default is apiKey, otherwise the default is 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.

Expand Down
10 changes: 7 additions & 3 deletions packages/plugins/apps/src/upload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ import { createRequestData, getOriginHeaders, NB_RETRIES } from '@dd/core/helper
import { getMockLogger, mockLogFn } from '@dd/tests/_jest/helpers/mocks';
import stripAnsi from 'strip-ansi';

jest.mock('@dd/core/helpers/env', () => ({
getDDEnvValue: jest.fn(),
}));
jest.mock('@dd/core/helpers/env', () => {
const actual = jest.requireActual('@dd/core/helpers/env');
return {
...actual,
getDDEnvValue: jest.fn(),
};
});

jest.mock('@dd/core/helpers/fs', () => {
const actual = jest.requireActual('@dd/core/helpers/fs');
Expand Down
88 changes: 79 additions & 9 deletions packages/plugins/apps/src/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,25 @@
// Copyright 2019-Present Datadog, Inc.

import { validateOptions } from '@dd/apps-plugin/validate';
import { cleanEnv } from '@dd/tests/_jest/helpers/env';

describe('Apps Plugin - validateOptions', () => {
let restoreEnv: () => void;

beforeEach(() => {
restoreEnv = cleanEnv();
});

afterEach(() => {
restoreEnv();
});

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: [],
Expand All @@ -19,6 +30,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 {
Expand Down Expand Up @@ -63,7 +103,7 @@ describe('Apps Plugin - validateOptions', () => {

expect(result).toEqual({
authOverrides: {
method: 'apiKey',
method: 'oauth',
},
dryRun: true,
include: ['public/**/*', 'dist/**/*'],
Expand All @@ -83,14 +123,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');
});
});
});
8 changes: 7 additions & 1 deletion packages/plugins/apps/src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 || [],
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/apps/src/vite/dev-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
49 changes: 23 additions & 26 deletions packages/tests/src/_jest/helpers/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Copyright 2019-Present Datadog, Inc.

import { SUPPORTED_BUNDLERS } from '@dd/core/constants';
import { OVERRIDE_VARIABLES } from '@dd/core/helpers/env';
import { mkdirSync } from '@dd/core/helpers/fs';
import type { BundlerName } from '@dd/core/types';
import { bgYellow, dim, green, red } from '@dd/tools/helpers';
Expand All @@ -12,6 +13,14 @@ import path from 'path';

const fsp = fs.promises;

type EnvOverrideVariable =
| `DATADOG_${(typeof OVERRIDE_VARIABLES)[number]}`
| `DD_${(typeof OVERRIDE_VARIABLES)[number]}`;

const ENV_OVERRIDE_VARIABLES = OVERRIDE_VARIABLES.flatMap(
(key) => [`DATADOG_${key}`, `DD_${key}`] as const,
) as EnvOverrideVariable[];

type TestEnv = {
NO_CLEANUP: boolean;
NEED_BUILD: boolean;
Expand Down Expand Up @@ -66,35 +75,23 @@ export const setupEnv = (env: TestEnv): void => {
};

export const cleanEnv = () => {
const previousEnv = {
DATADOG_API_KEY: process.env.DATADOG_API_KEY,
DD_API_KEY: process.env.DD_API_KEY,
DATADOG_APP_KEY: process.env.DATADOG_APP_KEY,
DD_APP_KEY: process.env.DD_APP_KEY,
DATADOG_APPS_UPLOAD_ASSETS: process.env.DATADOG_APPS_UPLOAD_ASSETS,
DD_APPS_UPLOAD_ASSETS: process.env.DD_APPS_UPLOAD_ASSETS,
DATADOG_SITE: process.env.DATADOG_SITE,
DD_SITE: process.env.DD_SITE,
};
const previousEnv = Object.fromEntries(
ENV_OVERRIDE_VARIABLES.map((key) => [key, process.env[key]]),
) as Record<EnvOverrideVariable, string | undefined>;

delete process.env.DATADOG_API_KEY;
delete process.env.DD_API_KEY;
delete process.env.DATADOG_APP_KEY;
delete process.env.DD_APP_KEY;
delete process.env.DATADOG_APPS_UPLOAD_ASSETS;
delete process.env.DD_APPS_UPLOAD_ASSETS;
delete process.env.DATADOG_SITE;
delete process.env.DD_SITE;
for (const key of ENV_OVERRIDE_VARIABLES) {
delete process.env[key];
}

return () => {
process.env.DATADOG_API_KEY = previousEnv.DATADOG_API_KEY;
process.env.DD_API_KEY = previousEnv.DD_API_KEY;
process.env.DATADOG_APP_KEY = previousEnv.DATADOG_APP_KEY;
process.env.DD_APP_KEY = previousEnv.DD_APP_KEY;
process.env.DATADOG_APPS_UPLOAD_ASSETS = previousEnv.DATADOG_APPS_UPLOAD_ASSETS;
process.env.DD_APPS_UPLOAD_ASSETS = previousEnv.DD_APPS_UPLOAD_ASSETS;
process.env.DATADOG_SITE = previousEnv.DATADOG_SITE;
process.env.DD_SITE = previousEnv.DD_SITE;
for (const key of ENV_OVERRIDE_VARIABLES) {
const value = previousEnv[key];
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
};
};

Expand Down
Loading