Skip to content
Closed
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: 3 additions & 3 deletions docs/keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ Retrieve SDK keys from the Management API.
USAGE
$ dvc keys get [--config-path <value>] [--auth-path <value>] [--repo-config-path <value>] [--client-id
<value>] [--client-secret <value>] [--project <value>] [--no-api] [--headless] [--env <value>] [--type
mobile|client|server]
mobile|client|server|all]

FLAGS
--env=<value> Environment to fetch a key for
--type=<option> The type of SDK key to retrieve
<options: mobile|client|server>
<options: mobile|client|server|all>

GLOBAL FLAGS
--auth-path=<value> Override the default location to look for an auth.yml file
Expand All @@ -37,7 +37,7 @@ DESCRIPTION
EXAMPLES
$ dvc keys get

$ dvc keys get --keys=environment-one,environment-two
$ dvc keys get --env=production --type=server
```

_See code: [src/commands/keys/get.ts](https://github.com/DevCycleHQ/cli/blob/v6.3.2/src/commands/keys/get.ts)_
5 changes: 3 additions & 2 deletions oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2526,7 +2526,7 @@
"hiddenAliases": [],
"examples": [
"<%= config.bin %> <%= command.id %>",
"<%= config.bin %> <%= command.id %> --keys=environment-one,environment-two"
"<%= config.bin %> <%= command.id %> --env=production --type=server"
],
"flags": {
"config-path": {
Expand Down Expand Up @@ -2625,7 +2625,8 @@
"options": [
"mobile",
"client",
"server"
"server",
"all"
]
}
},
Expand Down
164 changes: 164 additions & 0 deletions src/commands/keys/get.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { expect } from 'vitest'
import { dvcTest } from '../../../test-utils'
import { BASE_URL } from '../../api/common'

describe('keys get', () => {
const projectKey = 'test-project'
const authFlags = [
'--client-id',
'test-client-id',
'--client-secret',
'test-client-secret',
]

const serverKeys = [
{
key: 'dvc_server_old',
createdAt: '2024-01-01T12:00:00.000Z',
compromised: false,
},
{
key: 'dvc_server_new',
createdAt: '2025-01-01T12:00:00.000Z',
compromised: false,
},
]
const environment = (
sdkKeys: Partial<Record<'mobile' | 'client' | 'server', unknown[]>>,
) => ({
key: 'production',
name: 'Production',
_id: '61450f3daec96f5cf4a49961',
sdkKeys: { mobile: [], client: [], server: [], ...sdkKeys },
})

const nockEnvironment = (sdkKeys: Record<string, unknown[]>) =>
dvcTest().nock(BASE_URL, (api) =>
api
.get(`/v1/projects/${projectKey}/environments/production`)
.reply(200, environment(sdkKeys)),
)

nockEnvironment({ server: serverKeys })
.stdout()
.command([
'keys get',
'--project',
projectKey,
'--env',
'production',
'--type',
'server',
'--headless',
...authFlags,
])
.it('prints every key, most recent first', (ctx) => {
expect(ctx.stdout.trim().split('\n')).to.eql([
'dvc_server_new',
'dvc_server_old',
])
})

nockEnvironment({ server: serverKeys })
.stdout()
.command([
'keys get',
'--project',
projectKey,
'--env',
'production',
'--type',
'all',
'--headless',
...authFlags,
])
.it('supports --type all', (ctx) => {
expect(JSON.parse(ctx.stdout)).to.eql({
mobile: [],
client: [],
server: serverKeys,
})
})

nockEnvironment({
server: [{ ...serverKeys[0], compromised: true }, serverKeys[1]],
})
.stdout()
.command([
'keys get',
'--project',
projectKey,
'--env',
'production',
'--type',
'server',
'--headless',
...authFlags,
])
.it('marks compromised keys', (ctx) => {
expect(ctx.stdout.trim().split('\n')).to.eql([
'dvc_server_new',
'dvc_server_old (compromised)',
])
})

nockEnvironment({
server: [
{ key: '', createdAt: serverKeys[0].createdAt, compromised: false },
],
})
.stderr()
.command([
'keys get',
'--project',
projectKey,
'--env',
'production',
'--type',
'server',
'--headless',
...authFlags,
])
.catch((err) =>
expect(err.message).to.contain(
'do not have permission to view server SDK keys',
),
)
.it('errors when the key is hidden by permissions')

nockEnvironment({ server: [] })
.stderr()
.command([
'keys get',
'--project',
projectKey,
'--env',
'production',
'--type',
'server',
'--headless',
...authFlags,
])
.catch((err) =>
expect(err.message).to.contain('No server SDK keys found'),
)
.it('errors when there are no keys of that type')

dvcTest()
.stderr()
.command([
'keys get',
'--project',
projectKey,
'--env',
'production',
'--headless',
...authFlags,
])
.catch((err) =>
expect(err.message).to.contain(
'In headless mode, the env and type flags are required',
),
)
.it('requires the type flag in headless mode')
})
49 changes: 32 additions & 17 deletions src/commands/keys/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ export default class GetEnvironmentKey extends Base {
static description = 'Retrieve SDK keys from the Management API.'
static examples = [
'<%= config.bin %> <%= command.id %>',
'<%= config.bin %> <%= command.id %> --keys=environment-one,environment-two',
'<%= config.bin %> <%= command.id %> --env=production --type=server',
]
static flags = {
...Base.flags,
env: Flags.string({
description: 'Environment to fetch a key for',
}),
type: Flags.string({
options: ['mobile', 'client', 'server'],
options: ['mobile', 'client', 'server', 'all'],
description: 'The type of SDK key to retrieve',
}),
}
Expand All @@ -33,8 +33,10 @@ export default class GetEnvironmentKey extends Base {
const { project, headless } = flags
await this.requireProject(project, headless)

if (flags.headless && !flags.env) {
throw new Error('In headless mode, the env flag is required')
if (flags.headless && (!flags.env || !flags.type)) {
throw new Error(
'In headless mode, the env and type flags are required',
)
}

const environmentKey = await this.getEnvironmentKey()
Expand All @@ -43,22 +45,35 @@ export default class GetEnvironmentKey extends Base {
this.projectKey,
environmentKey,
)
if (!environment) {
return
}
const sdkType = await this.getSdkType()
if (sdkType && sdkType !== 'all') {
const activeKeys = environment.sdkKeys[sdkType] as APIKey[]
const currentKey = activeKeys[activeKeys.length - 1]
if (currentKey.compromised) {
this.writer.warningMessage(
`The most recent key for ${environmentKey} ${sdkType} has been compromised}`,
)
}
this.writer.showRawResults(currentKey.key)
} else {
if (sdkType === 'all') {
this.writer.showResults(environment.sdkKeys)
return
}

const activeKeys = environment.sdkKeys[sdkType] as APIKey[]
if (!activeKeys?.length) {
throw new Error(
`No ${sdkType} SDK keys found for environment ${environmentKey}`,
)
}

// the API appends new keys, so reverse to put the most recent first
const keysNewestFirst = [...activeKeys].reverse()
if (keysNewestFirst.every(({ key }) => !key)) {
throw new Error(
`Your credentials do not have permission to view ${sdkType} SDK keys for environment ` +
`${environmentKey}. Publisher permissions are required for protected environments.`,
)
}

Comment on lines +61 to +69

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.

why are we reversing the order here? if we're inverting the API response would that not confuse people on which is accurate? We don't have a createdat timestamp anywhere.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is still an open question, since the dashboard is being updated to show the latest token on the landing page with the details being shown in another view.

this.writer.showRawResults(
keysNewestFirst
.map(({ key, compromised }) =>
compromised ? `${key} (compromised)` : key,
)
.join('\n'),
)
}

private async getEnvironmentKey(): Promise<string> {
Expand Down
Loading