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

fix(schema): Add versions field to project secrets and variables response #590

Merged
merged 4 commits into from
Dec 16, 2024
Merged
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
111 changes: 101 additions & 10 deletions apps/api/src/secret/secret.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -619,14 +619,28 @@ describe('Secret Controller Tests', () => {
expect(response.json().items.length).toBe(1)

const { secret, values } = response.json().items[0]
expect(secret.id).toBeDefined()
expect(secret.name).toBeDefined()
expect(secret.note).toBeDefined()
expect(secret.projectId).toBeDefined()
expect(secret).toStrictEqual({
id: secret1.id,
name: secret1.name,
slug: secret1.slug,
note: secret1.note,
projectId: project1.id,
lastUpdatedById: secret1.lastUpdatedById,
lastUpdatedBy: {
id: user1.id,
name: user1.name
},
createdAt: secret1.createdAt.toISOString(),
updatedAt: secret1.updatedAt.toISOString(),
rotateAt: secret1.rotateAt.toISOString()
})
expect(values.length).toBe(1)

const value = values[0]
expect(value.environment).toBeDefined()
expect(value.version).toBe(1)
expect(value.environment.id).toBe(environment1.id)
expect(value.environment.slug).toBe(environment1.slug)
expect(value.environment.name).toBe(environment1.name)
expect(value.value).not.toEqual('Secret 1 value')

//check metadata
Expand All @@ -645,6 +659,69 @@ describe('Secret Controller Tests', () => {
)
})

it('should be able to fetch only new versions of secrets', async () => {
// Update secret1
await secretService.updateSecret(user1, secret1.slug, {
entries: [
{
value: 'Secret new 1 value',
environmentSlug: environment1.slug
}
]
})

const response = await app.inject({
method: 'GET',
url: `/secret/${project1.slug}?page=0&limit=10`,
headers: {
'x-e2e-user-email': user1.email
}
})

expect(response.statusCode).toBe(200)
expect(response.json().items.length).toBe(1)

const { secret, values } = response.json().items[0]
expect(secret).toStrictEqual({
id: secret1.id,
name: secret1.name,
slug: secret1.slug,
note: secret1.note,
projectId: project1.id,
lastUpdatedById: secret1.lastUpdatedById,
lastUpdatedBy: {
id: user1.id,
name: user1.name
},
createdAt: secret1.createdAt.toISOString(),
updatedAt: expect.any(String),
rotateAt: secret1.rotateAt.toISOString()
})
expect(values.length).toBe(1)

const value = values[0]
expect(value.version).toBe(2)
expect(value.environment.id).toBe(environment1.id)
expect(value.environment.slug).toBe(environment1.slug)
expect(value.environment.name).toBe(environment1.name)
expect(value.value).not.toEqual('Secret 1 new value')

//check metadata
const metadata = response.json().metadata
expect(metadata.totalCount).toEqual(1)
expect(metadata.links.self).toEqual(
`/secret/${project1.slug}?decryptValue=false&page=0&limit=10&sort=name&order=asc&search=`
)
expect(metadata.links.first).toEqual(
`/secret/${project1.slug}?decryptValue=false&page=0&limit=10&sort=name&order=asc&search=`
)
expect(metadata.links.previous).toBeNull()
expect(metadata.links.next).toBeNull()
expect(metadata.links.last).toEqual(
`/secret/${project1.slug}?decryptValue=false&page=0&limit=10&sort=name&order=asc&search=`
)
})

it('should be able to fetch all secrets decrypted', async () => {
const response = await app.inject({
method: 'GET',
Expand All @@ -658,14 +735,28 @@ describe('Secret Controller Tests', () => {
expect(response.json().items.length).toBe(1)

const { secret, values } = response.json().items[0]
expect(secret.id).toBeDefined()
expect(secret.name).toBeDefined()
expect(secret.note).toBeDefined()
expect(secret.projectId).toBeDefined()
expect(secret).toStrictEqual({
id: secret1.id,
name: secret1.name,
slug: secret1.slug,
note: secret1.note,
projectId: project1.id,
lastUpdatedById: secret1.lastUpdatedById,
lastUpdatedBy: {
id: user1.id,
name: user1.name
},
createdAt: secret1.createdAt.toISOString(),
updatedAt: secret1.updatedAt.toISOString(),
rotateAt: secret1.rotateAt.toISOString()
})
expect(values.length).toBe(1)

const value = values[0]
expect(value.environment).toBeDefined()
expect(value.version).toBe(1)
expect(value.environment.id).toBe(environment1.id)
expect(value.environment.slug).toBe(environment1.slug)
expect(value.environment.name).toBe(environment1.name)
expect(value.value).toEqual('Secret 1 value')

//check metadata
Expand Down
135 changes: 58 additions & 77 deletions apps/api/src/secret/service/secret.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,8 @@ export class SecretService {
},
versions: {
select: {
value: true,
version: true,
environment: {
select: {
name: true,
Expand All @@ -687,93 +689,72 @@ export class SecretService {
}
})

const secretsWithEnvironmentalValues = new Map<
Secret['id'],
{
secret: Secret
values: {
environment: {
name: Environment['name']
id: Environment['id']
}
value: SecretVersion['value']
version: SecretVersion['version']
}[]
}
>()

// Find all the environments for this project
const environments = await this.prisma.environment.findMany({
where: {
projectId
}
})
const environmentIds = new Map(
environments.map((env) => [env.id, env.name])
)
const secretsWithEnvironmentalValues = new Set<{
secret: Partial<Secret>
values: {
environment: {
name: Environment['name']
id: Environment['id']
slug: Environment['slug']
}
value: SecretVersion['value']
version: SecretVersion['version']
}[]
}>()

for (const secret of secrets) {
// Make a copy of the environment IDs
const envIds = new Map(environmentIds)
let iterations = envIds.size

// Find the latest version for each environment
while (iterations--) {
const latestVersion = await this.prisma.secretVersion.findFirst({
where: {
secretId: secret.id,
environmentId: {
in: Array.from(envIds.keys())
}
},
orderBy: {
version: 'desc'
},
include: {
environment: {
select: {
id: true,
slug: true
}
}
// Logic to update the map:
// 1. If the environment ID is not present in the key, insert the environment ID and the secret version
// 2. If the environment ID is already present, check if the existing secret version is lesser than the new secret version.
// If it is, update the secret version
const envIdToSecretVersionMap = new Map<
Environment['id'],
Partial<SecretVersion> & {
environment: {
id: Environment['id']
slug: Environment['slug']
name: Environment['name']
}
})
}
>()

if (!latestVersion) continue
for (const secretVersion of secret.versions) {
const environmentId = secretVersion.environment.id
const existingSecretVersion = envIdToSecretVersionMap.get(environmentId)

if (secretsWithEnvironmentalValues.has(secret.id)) {
secretsWithEnvironmentalValues.get(secret.id).values.push({
environment: {
id: latestVersion.environmentId,
name: envIds.get(latestVersion.environmentId)
},
value: decryptValue
? await decrypt(project.privateKey, latestVersion.value)
: latestVersion.value,
version: latestVersion.version
})
if (!existingSecretVersion) {
envIdToSecretVersionMap.set(environmentId, secretVersion)
} else {
secretsWithEnvironmentalValues.set(secret.id, {
secret,
values: [
{
environment: {
id: latestVersion.environmentId,
name: envIds.get(latestVersion.environmentId)
},
value: decryptValue
? await decrypt(project.privateKey, latestVersion.value)
: latestVersion.value,
version: latestVersion.version
}
]
})
if (existingSecretVersion.version < secretVersion.version) {
envIdToSecretVersionMap.set(environmentId, secretVersion)
}
}

envIds.delete(latestVersion.environmentId)
}

delete secret.versions

// Add the secret to the map
secretsWithEnvironmentalValues.add({
secret,
values: await Promise.all(
Array.from(envIdToSecretVersionMap.values()).map(
async (secretVersion) => ({
environment: {
id: secretVersion.environment.id,
name: secretVersion.environment.name,
slug: secretVersion.environment.slug
},
value: decryptValue
? await decrypt(project.privateKey, secretVersion.value)
: secretVersion.value,
version: secretVersion.version
})
)
)
})
}

// console.log(secretsWithEnvironmentalValues)
const items = Array.from(secretsWithEnvironmentalValues.values())

// Calculate pagination metadata
Expand Down
Loading
Loading