Skip to content
Draft
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
10 changes: 5 additions & 5 deletions apps/server-nestjs/src/modules/argocd/argocd.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ describe('argoCDService', () => {
gitlab.getOrCreateInfraGroupRepoPublicUrl.mockResolvedValue('https://gitlab.internal/infra-repo')
gitlab.listFiles.mockResolvedValue([])
vault.getAuthApproleRoleRoleId.mockResolvedValue('role-id')
vault.createAuthApproleRoleSecretId.mockResolvedValue('secret-id')
vault.ensureAuthApproleRoleSecretId.mockResolvedValue('secret-id')
gitlab.generateCreateOrUpdateAction.mockImplementation(async (_repoId, _ref, filePath: string, content: string) => {
return makeCommitAction({ filePath, content })
})
Expand Down Expand Up @@ -447,7 +447,7 @@ describe('argoCDService', () => {
),
])
vault.getAuthApproleRoleRoleId.mockResolvedValue('role-id')
vault.createAuthApproleRoleSecretId.mockResolvedValue('secret-id')
vault.ensureAuthApproleRoleSecretId.mockResolvedValue('secret-id')
gitlab.generateCreateOrUpdateAction.mockImplementation(async (_repoId, _ref, filePath: string, content: string) => {
return makeCommitAction({ filePath, content })
})
Expand Down Expand Up @@ -538,7 +538,7 @@ describe('argoCDService', () => {
gitlab.getOrCreateInfraGroupRepoPublicUrl.mockResolvedValue('https://gitlab.internal/infra-repo')
gitlab.listFiles.mockResolvedValue([])
vault.getAuthApproleRoleRoleId.mockResolvedValue('role-id')
vault.createAuthApproleRoleSecretId.mockResolvedValue('secret-id')
vault.ensureAuthApproleRoleSecretId.mockResolvedValue('secret-id')

gitlab.generateCreateOrUpdateAction.mockResolvedValue(null)

Expand Down Expand Up @@ -583,7 +583,7 @@ describe('argoCDService', () => {
gitlab.getOrCreateInfraGroupRepoPublicUrl.mockResolvedValue('https://gitlab.internal/infra-repo')
gitlab.listFiles.mockResolvedValue([])
vault.getAuthApproleRoleRoleId.mockResolvedValue('role-id')
vault.createAuthApproleRoleSecretId.mockResolvedValue('secret-id')
vault.ensureAuthApproleRoleSecretId.mockResolvedValue('secret-id')
gitlab.generateCreateOrUpdateAction.mockImplementation(async (_repoId, _ref, filePath: string, content: string) => {
return makeCommitAction({ filePath, content })
})
Expand Down Expand Up @@ -738,7 +738,7 @@ describe('argoCDService', () => {
gitlab.getOrCreateInfraGroupRepoPublicUrl.mockResolvedValue('https://gitlab.internal/infra-repo')
gitlab.listFiles.mockResolvedValue([])
vault.getAuthApproleRoleRoleId.mockResolvedValue('role-id')
vault.createAuthApproleRoleSecretId.mockResolvedValue('secret-id')
vault.ensureAuthApproleRoleSecretId.mockResolvedValue('secret-id')
gitlab.generateCreateOrUpdateAction.mockImplementation(async (_repoId, _ref, filePath: string, content: string) => {
return makeCommitAction({ filePath, content })
})
Expand Down
2 changes: 1 addition & 1 deletion apps/server-nestjs/src/modules/argocd/argocd.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ export class ArgoCDService {
this.logger.warn(`Couldn't find app role (project=${projectSlug})`)
return undefined
})
const secretId = await this.vault.createAuthApproleRoleSecretId(projectSlug).catch(() => {
const secretId = await this.vault.ensureAuthApproleRoleSecretId(projectSlug).catch(() => {
this.logger.warn(`Couldn't find secret (project=${projectSlug})`)
return undefined
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,48 @@ describe('vault', () => {
expect(capturedPath).toBe('forge/my-project/GITLAB')
})
})

describe('ensureAuthApproleRoleSecretId', () => {
it('mints and persists a secret-id on first sync', async () => {
let minted = false
let persisted: unknown
server.use(
http.get(`${vaultUrl}/v1/kv/data/*`, () => {
return HttpResponse.json({}, { status: HttpStatus.NOT_FOUND })
}),
http.post(`${vaultUrl}/v1/auth/approle/role/*/secret-id`, () => {
minted = true
return HttpResponse.json({ data: { secret_id: 'minted-secret' } })
}),
http.post(`${vaultUrl}/v1/kv/data/*`, async ({ request }) => {
persisted = await request.json()
return HttpResponse.json({})
}),
)

const secretId = await service.ensureAuthApproleRoleSecretId('my-project')

expect(secretId).toBe('minted-secret')
expect(minted).toBe(true)
expect(persisted).toEqual({ data: { secret_id: 'minted-secret' } })
})

it('reuses a persisted secret-id on subsequent syncs without minting', async () => {
let minted = false
server.use(
http.get(`${vaultUrl}/v1/kv/data/*`, () => {
return HttpResponse.json({ data: { data: { secret_id: 'persisted-secret' }, metadata: { created_time: '2023-01-01T00:00:00.000Z', version: 1 } } })
}),
http.post(`${vaultUrl}/v1/auth/approle/role/*/secret-id`, () => {
minted = true
return HttpResponse.json({ data: { secret_id: 'unexpected' } })
}),
)

const secretId = await service.ensureAuthApproleRoleSecretId('my-project')

expect(secretId).toBe('persisted-secret')
expect(minted).toBe(false)
})
})
})
11 changes: 9 additions & 2 deletions apps/server-nestjs/src/modules/vault/vault-client.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { baseConfigFactory } from '../../config/base.config'
import { vaultConfigFactory } from '../../config/vault.config'
import { StartActiveSpan } from '../infrastructure/telemetry/telemetry.decorator'
import { VaultError, VaultHttpClientService } from './vault-http-client.service'
import { generateGitlabMirrorCredPath, generateSecretGroupPath, generateSonarqubeCredPath, generateTechReadOnlyCredPath, isVaultNotFound } from './vault.utils'
import { generateAppRoleSecretIdPath, generateGitlabMirrorCredPath, generateSecretGroupPath, generateSonarqubeCredPath, generateTechReadOnlyCredPath, isVaultNotFound } from './vault.utils'

export interface VaultSysPoliciesAclUpsertRequest {
policy: string
Expand Down Expand Up @@ -401,14 +401,21 @@ export class VaultClientService {
}

@StartActiveSpan()
async createAuthApproleRoleSecretId(roleName: string) {
async ensureAuthApproleRoleSecretId(roleName: string) {
const kvPath = generateAppRoleSecretIdPath(this.baseConfig.projectsRootDir, roleName)
const existing = await this.read<{ secret_id: string }>(kvPath).catch(() => null)
if (existing?.data?.secret_id) {
this.logger.verbose(`Reusing Vault AppRole secret-id for ${roleName}`)
return existing.data.secret_id
}
const path = `auth/approle/role/${roleName}/secret-id`
this.logger.verbose(`Creating Vault AppRole secret-id for ${roleName}`)
const response = await this.http.fetch<VaultSecretIdResponse>(path, { method: 'POST' })
const secretId = response?.data?.secret_id
if (!secretId) {
throw new VaultError('InvalidResponse', `Vault secret-id not generated for role ${roleName}`, { method: 'POST', path })
}
await this.write({ secret_id: secretId }, kvPath)
return secretId
}

Expand Down
4 changes: 4 additions & 0 deletions apps/server-nestjs/src/modules/vault/vault.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ export function isVaultNotFound(error: unknown): error is VaultError {
export function isVaultBadRequest(error: unknown): error is VaultError {
return error instanceof VaultError && error.kind === 'HttpError' && error.status === 400
}

export function generateAppRoleSecretIdPath(projectRootDir: string, projectSlug: string) {
return `${generateProjectPath(projectRootDir, projectSlug)}/APPROLE_SECRET_ID`
}