Skip to content
Open
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
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
111 changes: 102 additions & 9 deletions packages/plugins/apps/src/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
};
Comment on lines +7 to +29

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We have this cleanEnv() system here:

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,
};
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;
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;
};
};

Currently, we only call it in beforeAll/afterAll but maybe we could extend/modify it to suit your use-case?
Not sure if it would have a real impact if we were to do it on beforeEach/afterEach instead.

(And also make it leverage OVERRIDE_VARIABLES instead of duplicating them)


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

expect(result).toEqual({
authOverrides: {
method: 'apiKey',
method: 'oauth',
},
dryRun: true,
include: ['public/**/*', 'dist/**/*'],
Expand All @@ -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');
});
});
});
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
Loading