Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ✨ build project kv with config for projects #1408

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
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
98 changes: 85 additions & 13 deletions plugins/vault/src/class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@ interface readOptions {
}
interface AppRoleCredentials {
url: string
kvName: string
coreKvName: string
roleId: string
secretId: string
}

export class VaultProjectApi extends PluginApi {
private token: string | undefined = undefined
private axios: AxiosInstance
private kvName: string = 'forge-dso'
private coreKvName: string = 'forge-dso'
private projectKvName: string
private groupName: string
private policyName: string
private basePath: string
private roleName: string
private projectRootDir: string | undefined
Expand All @@ -27,6 +30,9 @@ export class VaultProjectApi extends PluginApi {
super()
this.basePath = `${project.organization.name}/${project.name}`
this.roleName = `${project.organization.name}-${project.name}`
this.projectKvName = `${project.organization.name}-${project.name}`
this.groupName = `${project.organization.name}-${project.name}`
this.policyName = `${project.organization.name}-${project.name}`
this.projectRootDir = removeTrailingSlash(requiredEnv('PROJECTS_ROOT_DIR'))
this.axios = axios.create({
baseURL: requiredEnv('VAULT_URL'),
Expand All @@ -36,7 +42,7 @@ export class VaultProjectApi extends PluginApi {
})
this.defaultAppRoleCredentials = {
url: removeTrailingSlash(requiredEnv('VAULT_URL')),
kvName: this.kvName,
coreKvName: this.coreKvName,
roleId: 'none',
secretId: 'none',
}
Expand All @@ -54,7 +60,7 @@ export class VaultProjectApi extends PluginApi {

const listSecretPath: string[] = []
const response = await this.axios({
url: `/v1/${this.kvName}/metadata/${this.projectRootDir}/${this.basePath}${path}/`,
url: `/v1/${this.coreKvName}/metadata/${this.projectRootDir}/${this.basePath}${path}/`,
headers: {
'X-Vault-Token': await this.getToken(),
},
Expand All @@ -80,7 +86,7 @@ export class VaultProjectApi extends PluginApi {
if (path.startsWith('/'))
path = path.slice(1)
const response = await this.axios.get(
`/v1/${this.kvName}/data/${this.projectRootDir}/${this.basePath}/${path}`,
`/v1/${this.coreKvName}/data/${this.projectRootDir}/${this.basePath}/${path}`,
{
headers: { 'X-Vault-Token': await this.getToken() },
validateStatus: status => (options.throwIfNoEntry ? [200] : [200, 404]).includes(status),
Expand All @@ -93,7 +99,7 @@ export class VaultProjectApi extends PluginApi {
if (path.startsWith('/'))
path = path.slice(1)
const response = await this.axios.post(
`/v1/${this.kvName}/data/${this.projectRootDir}/${this.basePath}/${path}`,
`/v1/${this.coreKvName}/data/${this.projectRootDir}/${this.basePath}/${path}`,
{
headers: { 'X-Vault-Token': await this.getToken() },
data: body,
Expand All @@ -102,10 +108,10 @@ export class VaultProjectApi extends PluginApi {
return await response.data
}

async upsertPolicy() {
async upsertPolicy(policyName: string, policy: string) {
await this.axios.put(
`/v1/sys/policies/acl/${this.roleName}`,
{ policy: `path "${this.kvName}/data/${this.projectRootDir}/${this.basePath}/*" { capabilities = ["create", "read", "update", "delete", "list"] }` },
`/v1/sys/policies/acl/${policyName}`,
{ policy },
{
headers: {
'X-Vault-Token': await this.getToken(),
Expand All @@ -115,20 +121,28 @@ export class VaultProjectApi extends PluginApi {
)
}

async isAppRoleEnabled() {
async getAuthMethod() {
const response = await this.axios.get(
'/v1/sys/auth',
{
headers: { 'X-Vault-Token': await this.getToken() },
},
)
return Object.keys(response.data).includes('approle/')
return response.data
}

async isAppRoleEnabled() {
const methods = await this.getAuthMethod()
return Object.keys(methods).includes('approle/')
}

public async upsertRole() {
const appRoleEnabled = await this.isAppRoleEnabled()
if (!appRoleEnabled) return
this.upsertPolicy()
this.upsertPolicy(
this.roleName,
`path "${this.coreKvName}/data/${this.projectRootDir}/${this.basePath}/*" { capabilities = ["create", "read", "update", "delete", "list"] }`,
)
this.axios.post(
`/v1/auth/approle/role/${this.roleName}`,
{
Expand Down Expand Up @@ -173,7 +187,7 @@ export class VaultProjectApi extends PluginApi {
if (path.startsWith('/'))
path = path.slice(1)
return this.axios.delete(
`/v1/${this.kvName}/metadata/${this.projectRootDir}/${this.basePath}/${path}`,
`/v1/${this.coreKvName}/metadata/${this.projectRootDir}/${this.basePath}/${path}`,
{
headers: { 'X-Vault-Token': await this.getToken() },
},
Expand All @@ -194,4 +208,62 @@ export class VaultProjectApi extends PluginApi {
},
)
}

public async upsertKv() {
const appRoleEnabled = await this.isAppRoleEnabled()
if (!appRoleEnabled) return
this.createKv(this.projectKvName)
this.upsertPolicy(
this.policyName,
`path "${this.projectKvName}/*" { capabilities = ["create", "read", "update", "delete", "list"] }\n\npath "/v1/sys/policies/acl/${this.policyName}/*" { capabilities = ["create", "read", "update", "list"] }`,
)
const group = await this.createGroup(this.groupName, this.policyName)
this.createGroupAlias(this.groupName, group.id)
}

public async createKv(kvName: string) {
await this.axios.post(
`/v1/sys/mounts/${kvName}`,
{
headers: { 'X-Vault-Token': await this.getToken() },
data: {
type: 'kv',
options: {
version: 2,
},
},
},
)
}

public async createGroup(group: string, policyName: string) {
const response = await this.axios.post(
`/v1/identity/group/${group}`,
{
headers: { 'X-Vault-Token': await this.getToken() },
data: {
name: group,
type: 'external',
policies: [policyName],
},
},
)
return response.data
}

public async createGroupAlias(groupAlias: string, groupId: string) {
const methods = await this.getAuthMethod()
const response = await this.axios.post(
`/v1/identity/group/${groupAlias}`,
{
headers: { 'X-Vault-Token': await this.getToken() },
data: {
name: groupAlias,
mount_accessor: methods['oidc/'].accessor,
canonical_id: groupId,
},
},
)
return response.data
}
}
2 changes: 1 addition & 1 deletion plugins/vault/src/functions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type Project, type StepCall, parseError } from '@cpn-console/hooks'

export const upsertProjectAppRole: StepCall<Project> = async (payload) => {
export const upsertProject: StepCall<Project> = async (payload) => {
try {
if (!payload.apis.vault) {
throw new Error('no Vault available')
Expand Down
4 changes: 2 additions & 2 deletions plugins/vault/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { DefaultArgs, Plugin, Project, ProjectLite } from '@cpn-console/hooks'
import { archiveDsoProject, upsertProjectAppRole } from './functions.js'
import { archiveDsoProject, upsertProject } from './functions.js'
import infos from './infos.js'
import monitor from './monitor.js'
import { VaultProjectApi } from './class.js'
Expand All @@ -12,7 +12,7 @@ export const plugin: Plugin = {
getProjectSecrets: onlyApi,
upsertProject: {
...onlyApi,
steps: { main: upsertProjectAppRole },
steps: { main: upsertProject },
},
deleteProject: {
...onlyApi,
Expand Down
3 changes: 1 addition & 2 deletions plugins/vault/src/infos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ export const vaultUrl = process.env.VAULT_URL

const infos: ServiceInfos = {
name: 'vault',
// TODO wait for vault to be connected to oidc
// to: ({ project, organization }) => `${vaultUrl}/ui/vault/secrets/forge-dso/list/${projectRootDir}/${organization}/${project}`,
to: ({ project, organization }) => `${vaultUrl}/ui/vault/secrets/${organization}-${project}`,
title: 'Vault',
imgSrc: '/img/vault.svg',
description: 'Vault s\'intègre profondément avec les identités de confiance pour automatiser l\'accès aux secrets, aux données et aux systèmes',
Expand Down