Skip to content

Commit ab83dd2

Browse files
committed
chore(vault): migrate Vault plugin to NestJS
Signed-off-by: William Phetsinorath <william.phetsinorath-open@interieur.gouv.fr>
1 parent f9a38ee commit ab83dd2

File tree

5 files changed

+157
-6
lines changed

5 files changed

+157
-6
lines changed

apps/server-nestjs/.envrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
source_up
2+
dotenv

apps/server-nestjs/src/modules/vault/vault-client.service.ts

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Inject, Injectable, Logger } from '@nestjs/common'
22
import { ConfigurationService } from '@/cpin-module/infrastructure/configuration/configuration.service'
3-
import { generateKVConfigUpdate } from './vault.utils'
43

54
export interface VaultMetadata {
65
created_time: string
@@ -28,15 +27,15 @@ export class VaultClientService {
2827
) {
2928
}
3029

31-
private async request<T = any>(method: string, path: string, options: { body?: any } = {}): Promise<T | null> {
30+
private async fetch<T = any>(path: string, options: { method?: string, body?: any } = {}): Promise<T | null> {
3231
const url = `${this.config.vaultInternalUrl}${path}`
3332
const headers: Record<string, string> = {
3433
'Content-Type': 'application/json',
3534
'X-Vault-Token': this.config.vaultToken,
3635
}
3736

3837
const response = await fetch(url, {
39-
method,
38+
method: options.method,
4039
headers,
4140
body: options.body ? JSON.stringify(options.body) : undefined,
4241
})
@@ -53,7 +52,9 @@ export class VaultClientService {
5352
async read<T = any>(path: string): Promise<VaultSecret<T> | null> {
5453
if (path.startsWith('/')) path = path.slice(1)
5554
try {
56-
const data = await this.request<VaultResponse<T>>('GET', `/v1/${this.config.vaultKvName}/data/${path}`)
55+
const data = await this.fetch<VaultResponse<T>>(`/v1/${this.config.vaultKvName}/data/${path}`, {
56+
method: 'GET',
57+
})
5758
if (!data) return null
5859
return data.data
5960
} catch (error) {
@@ -65,7 +66,8 @@ export class VaultClientService {
6566
async write<T = any>(data: T, path: string): Promise<void> {
6667
if (path.startsWith('/')) path = path.slice(1)
6768
try {
68-
await this.request('POST', `/v1/${this.config.vaultKvName}/data/${path}`, {
69+
await this.fetch(`/v1/${this.config.vaultKvName}/data/${path}`, {
70+
method: 'POST',
6971
body: { data },
7072
})
7173
} catch (error) {
@@ -77,10 +79,48 @@ export class VaultClientService {
7779
async destroy(path: string): Promise<void> {
7880
if (path.startsWith('/')) path = path.slice(1)
7981
try {
80-
await this.request('DELETE', `/v1/${this.config.vaultKvName}/metadata/${path}`)
82+
await this.fetch(`/v1/${this.config.vaultKvName}/metadata/${path}`, {
83+
method: 'DELETE',
84+
})
8185
} catch (error) {
8286
this.logger.error(`Failed to destroy vault path ${path}: ${error}`)
8387
throw error
8488
}
8589
}
90+
91+
async upsertPolicyAcl(policyName: string, data: any) {
92+
await this.fetch(`/v1/sys/policies/acl/${policyName}`, {
93+
method: 'POST',
94+
body: data,
95+
})
96+
}
97+
98+
async createMount(name: string, data: any) {
99+
this.fetch(`/v1/sys/mounts/${name}/tune`, {
100+
method: 'POST',
101+
body: data,
102+
})
103+
}
104+
105+
async updateMount(name: string, data: any) {
106+
this.fetch(`/v1/sys/mounts/${name}/tune`, {
107+
method: 'PUT',
108+
body: data,
109+
})
110+
}
111+
112+
async upsertRole(roleName: string, policies: string[]) {
113+
await this.fetch(`/v1/auth/approle/role/${roleName}`, {
114+
method: 'POST',
115+
body: {
116+
secret_id_num_uses: '0',
117+
secret_id_ttl: '0',
118+
token_max_ttl: '0',
119+
token_num_uses: '0',
120+
token_ttl: '0',
121+
token_type: 'batch',
122+
token_policies: policies,
123+
},
124+
})
125+
}
86126
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Inject, Injectable } from '@nestjs/common'
2+
import type { Prisma } from '@prisma/client'
3+
import { PrismaService } from '@/cpin-module/infrastructure/database/prisma.service'
4+
5+
export const projectSelect = {
6+
id: true,
7+
name: true,
8+
slug: true,
9+
description: true,
10+
environments: {
11+
select: {
12+
id: true,
13+
name: true,
14+
clusterId: true,
15+
cpu: true,
16+
gpu: true,
17+
memory: true,
18+
autosync: true,
19+
},
20+
},
21+
} satisfies Prisma.ProjectSelect
22+
23+
export type ProjectWithDetails = Prisma.ProjectGetPayload<{
24+
select: typeof projectSelect
25+
}>
26+
27+
@Injectable()
28+
export class VaultDatastoreService {
29+
constructor(@Inject(PrismaService) private readonly prisma: PrismaService) {}
30+
31+
async getAllProjects(): Promise<ProjectWithDetails[]> {
32+
return this.prisma.project.findMany({
33+
select: projectSelect,
34+
})
35+
}
36+
37+
async getProject(id: string): Promise<ProjectWithDetails | null> {
38+
return this.prisma.project.findUnique({
39+
where: { id },
40+
select: projectSelect,
41+
})
42+
}
43+
}

apps/server-nestjs/src/modules/vault/vault.service.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { Inject, Injectable, Logger } from '@nestjs/common'
22
import { ConfigurationService } from '@/cpin-module/infrastructure/configuration/configuration.service'
33
import { VaultClientService } from './vault-client.service'
44
import type { VaultSecret } from './vault-client.service'
5+
import { generateAppAdminPolicyName, generateTechnicalReadOnlyPolicyName, generateZoneName } from './vault.utils'
6+
import type { ProjectWithDetails } from './vault-datastore.service'
57

68
@Injectable()
79
export class VaultService {
@@ -32,4 +34,55 @@ export class VaultService {
3234
async destroy(path: string): Promise<void> {
3335
await this.vaultClientService.destroy(path)
3436
}
37+
38+
async upsertMount(kvName: string) {
39+
const body = {
40+
type: 'kv',
41+
config: {
42+
force_no_cache: true,
43+
},
44+
options: {
45+
version: 2,
46+
},
47+
}
48+
try {
49+
await this.vaultClientService.updateMount(kvName, body)
50+
} catch (_error) {
51+
await this.vaultClientService.createMount(kvName, body)
52+
}
53+
}
54+
55+
async createZone(project: ProjectWithDetails, environment: ProjectWithDetails['environments'][number]) {
56+
const kvName = generateZoneName(environment.name)
57+
await this.upsertMount(kvName)
58+
const techName = generateTechnicalReadOnlyPolicyName(project)
59+
const appName = generateAppAdminPolicyName(project)
60+
await Promise.all([
61+
this.createTechnicalReadOnlyPolicy(techName, project.slug),
62+
this.createAppAdminPolicy(appName, project.slug),
63+
])
64+
await this.vaultClientService.upsertRole(kvName, [
65+
techName,
66+
appName,
67+
])
68+
}
69+
70+
async createTechnicalReadOnlyPolicy(name: string, projectSlug: string) {
71+
await this.vaultClientService.upsertPolicyAcl(
72+
name,
73+
{
74+
policy: `path "${projectSlug}/*" { capabilities = ["create", "read", "update", "delete", "list"] }`,
75+
},
76+
)
77+
}
78+
79+
async createAppAdminPolicy(name: string, projectSlug: string) {
80+
await this.vaultClientService.upsertPolicyAcl(
81+
name,
82+
{
83+
policy:
84+
`path "${this.config.vaultKvName}/data/${this.config.projectRootPath}/${projectSlug}/REGISTRY/ro-robot" { capabilities = ["read"] }`,
85+
},
86+
)
87+
}
3588
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { ProjectWithDetails } from './vault-datastore.service'
2+
3+
export function generateTechnicalReadOnlyPolicyName(project: ProjectWithDetails) {
4+
return `tech--${project.slug}--ro`
5+
}
6+
7+
export function generateAppAdminPolicyName(project: ProjectWithDetails) {
8+
return `app--${project.slug}--admin`
9+
}
10+
11+
export function generateZoneName(name: string) {
12+
return `zone-${name}`
13+
}

0 commit comments

Comments
 (0)