mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-28 02:53:22 +00:00
Compare commits
135 Commits
misc/add-o
...
doc/add-dy
Author | SHA1 | Date | |
---|---|---|---|
|
b176f13392 | ||
|
4570de09ae | ||
|
4feff5b4ca | ||
|
6081e2927e | ||
|
0b42f29916 | ||
|
b60d0992f4 | ||
|
a8a68f600c | ||
|
742f5f6621 | ||
|
f3cd7efe0e | ||
|
2b16c19b70 | ||
|
943b540383 | ||
|
e180021aa6 | ||
|
8e08c443ad | ||
|
dae26daeeb | ||
|
170f8d9add | ||
|
8d41ef198a | ||
|
69d60a227a | ||
|
c8eefcfbf9 | ||
|
53cec754cc | ||
|
5db3e177eb | ||
|
3fcc3ccff4 | ||
|
df07d7b6d7 | ||
|
28a655bef1 | ||
|
5f2cd04f46 | ||
|
897ce1f267 | ||
|
6afc17b84b | ||
|
9017a5e838 | ||
|
cb8e4d884e | ||
|
16807c3dd6 | ||
|
61791e385c | ||
|
bbd7bfb0f5 | ||
|
4de8c48b2c | ||
|
a4bbe2c612 | ||
|
541a2e7d05 | ||
|
ea4e51d826 | ||
|
3bc920c593 | ||
|
df38c761ad | ||
|
32a84471f2 | ||
|
ea14df2cbd | ||
|
6bd6cac366 | ||
|
45294253aa | ||
|
635fbdc80b | ||
|
d20c48b7cf | ||
|
1fc18fe23b | ||
|
99403e122b | ||
|
5176e70437 | ||
|
82b2b0af97 | ||
|
e313c866a2 | ||
|
2d81606049 | ||
|
718f4ef129 | ||
|
a42f3b3763 | ||
|
f7d882a6fc | ||
|
385afdfcf8 | ||
|
281d703cc3 | ||
|
6f56ed5474 | ||
|
809e4eeba1 | ||
|
254446c895 | ||
|
bb52e2beb4 | ||
|
2739b08e59 | ||
|
ba5e877a3b | ||
|
d2752216f6 | ||
|
d91fb0db02 | ||
|
4892eea009 | ||
|
09c6fcb73b | ||
|
79181a1e3d | ||
|
bb934ef7b1 | ||
|
cd9316537d | ||
|
942e5f2f65 | ||
|
353d231a4e | ||
|
68e05b7198 | ||
|
4f998e3940 | ||
|
1248840dc8 | ||
|
64c8125e4b | ||
|
c109fbab3e | ||
|
15fb01089b | ||
|
6f4be3e25a | ||
|
8d33647739 | ||
|
d1c142e5b1 | ||
|
bb1cad0c5b | ||
|
2a1cfe15b4 | ||
|
881d70bc64 | ||
|
14c1b4f07b | ||
|
3028bdd424 | ||
|
902a0b0ed4 | ||
|
c1decab912 | ||
|
216c073290 | ||
|
8626bce632 | ||
|
c5a2b0321f | ||
|
cc689d3178 | ||
|
e6848828f2 | ||
|
c8b93e4467 | ||
|
0bca24bb00 | ||
|
c563ada50f | ||
|
26d1616e22 | ||
|
5fd071d1de | ||
|
a6ac78356b | ||
|
e4a2137991 | ||
|
9721d7a15e | ||
|
93db5c4555 | ||
|
ad4393fdef | ||
|
cd06e4e7f3 | ||
|
711a4179ce | ||
|
b4a2a477d3 | ||
|
8e53a1b171 | ||
|
71af463ad8 | ||
|
7abd18b11c | ||
|
1aee50a751 | ||
|
e9b37a1f98 | ||
|
43fded2350 | ||
|
7b6f4d810d | ||
|
b97bbe5beb | ||
|
cf5260b383 | ||
|
13e0dd8e0f | ||
|
7467a05fc4 | ||
|
afba636850 | ||
|
891cb06de0 | ||
|
02e8f20cbf | ||
|
dbe771dba0 | ||
|
273fd6c98f | ||
|
d5f4ce4376 | ||
|
18aac6508b | ||
|
85653a90d5 | ||
|
879ef2c178 | ||
|
8777cfe680 | ||
|
2b630f75aa | ||
|
91cee20cc8 | ||
|
4249ec6030 | ||
|
e7a95e6af2 | ||
|
a9f04a3c1f | ||
|
3d380710ee | ||
|
2177ec6bcc | ||
|
070eb2aacd | ||
|
e619cfa313 | ||
|
c3038e3ca1 | ||
|
ff0e7feeee |
2
.github/workflows/run-cli-tests.yml
vendored
2
.github/workflows/run-cli-tests.yml
vendored
@@ -50,6 +50,6 @@ jobs:
|
||||
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
|
||||
CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
|
||||
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
|
||||
INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
|
||||
# INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
|
||||
|
||||
run: go test -v -count=1 ./test
|
||||
|
13
Makefile
13
Makefile
@@ -15,3 +15,16 @@ up-prod:
|
||||
|
||||
down:
|
||||
docker compose -f docker-compose.dev.yml down
|
||||
|
||||
reviewable-ui:
|
||||
cd frontend && \
|
||||
npm run lint:fix && \
|
||||
npm run type:check
|
||||
|
||||
reviewable-api:
|
||||
cd backend && \
|
||||
npm run lint:fix && \
|
||||
npm run type:check
|
||||
|
||||
reviewable: reviewable-ui reviewable-api
|
||||
|
||||
|
55
backend/package-lock.json
generated
55
backend/package-lock.json
generated
@@ -25,6 +25,7 @@
|
||||
"@fastify/swagger": "^8.14.0",
|
||||
"@fastify/swagger-ui": "^2.1.0",
|
||||
"@node-saml/passport-saml": "^4.0.4",
|
||||
"@octokit/plugin-retry": "^5.0.5",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@octokit/webhooks-types": "^7.3.1",
|
||||
"@peculiar/asn1-schema": "^2.3.8",
|
||||
@@ -7812,19 +7813,45 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/plugin-retry": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz",
|
||||
"integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==",
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-5.0.5.tgz",
|
||||
"integrity": "sha512-sB1RWMhSrre02Atv95K6bhESlJ/sPdZkK/wE/w1IdSCe0yM6FxSjksLa6T7aAvxvxlLKzQEC4KIiqpqyov1Tbg==",
|
||||
"dependencies": {
|
||||
"@octokit/request-error": "^5.0.0",
|
||||
"@octokit/types": "^12.0.0",
|
||||
"@octokit/request-error": "^4.0.1",
|
||||
"@octokit/types": "^10.0.0",
|
||||
"bottleneck": "^2.15.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@octokit/core": ">=5"
|
||||
"@octokit/core": ">=3"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/plugin-retry/node_modules/@octokit/openapi-types": {
|
||||
"version": "18.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz",
|
||||
"integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw=="
|
||||
},
|
||||
"node_modules/@octokit/plugin-retry/node_modules/@octokit/request-error": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-4.0.2.tgz",
|
||||
"integrity": "sha512-uqwUEmZw3x4I9DGYq9fODVAAvcLsPQv97NRycP6syEFu5916M189VnNBW2zANNwqg3OiligNcAey7P0SET843w==",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^10.0.0",
|
||||
"deprecation": "^2.0.0",
|
||||
"once": "^1.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/plugin-retry/node_modules/@octokit/types": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-10.0.0.tgz",
|
||||
"integrity": "sha512-Vm8IddVmhCgU1fxC1eyinpwqzXPEYu0NrYzD3YZjlGjyftdLBTeqNblRC0jmJmgxbJIsQlyogVeGnrNaaMVzIg==",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/plugin-throttling": {
|
||||
@@ -17396,6 +17423,22 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/probot/node_modules/@octokit/plugin-retry": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz",
|
||||
"integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==",
|
||||
"dependencies": {
|
||||
"@octokit/request-error": "^5.0.0",
|
||||
"@octokit/types": "^12.0.0",
|
||||
"bottleneck": "^2.15.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@octokit/core": ">=5"
|
||||
}
|
||||
},
|
||||
"node_modules/probot/node_modules/commander": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
|
||||
|
@@ -121,6 +121,7 @@
|
||||
"@fastify/swagger": "^8.14.0",
|
||||
"@fastify/swagger-ui": "^2.1.0",
|
||||
"@node-saml/passport-saml": "^4.0.4",
|
||||
"@octokit/plugin-retry": "^5.0.5",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@octokit/webhooks-types": "^7.3.1",
|
||||
"@peculiar/asn1-schema": "^2.3.8",
|
||||
|
@@ -7,14 +7,33 @@ const prompt = promptSync({
|
||||
sigint: true
|
||||
});
|
||||
|
||||
type ComponentType = 1 | 2 | 3;
|
||||
|
||||
console.log(`
|
||||
Component List
|
||||
--------------
|
||||
0. Exit
|
||||
1. Service component
|
||||
2. DAL component
|
||||
3. Router component
|
||||
`);
|
||||
const componentType = parseInt(prompt("Select a component: "), 10);
|
||||
|
||||
function getComponentType(): ComponentType {
|
||||
while (true) {
|
||||
const input = prompt("Select a component (0-3): ");
|
||||
const componentType = parseInt(input, 10);
|
||||
|
||||
if (componentType === 0) {
|
||||
console.log("Exiting the program. Goodbye!");
|
||||
process.exit(0);
|
||||
} else if (componentType === 1 || componentType === 2 || componentType === 3) {
|
||||
return componentType;
|
||||
} else {
|
||||
console.log("Invalid input. Please enter 0, 1, 2, or 3.");
|
||||
}
|
||||
}
|
||||
}
|
||||
const componentType = getComponentType();
|
||||
|
||||
if (componentType === 1) {
|
||||
const componentName = prompt("Enter service name: ");
|
||||
|
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@@ -51,6 +51,7 @@ import { TIntegrationServiceFactory } from "@app/services/integration/integratio
|
||||
import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
||||
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
|
||||
import { TOrgServiceFactory } from "@app/services/org/org-service";
|
||||
import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
|
||||
import { TProjectServiceFactory } from "@app/services/project/project-service";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
|
||||
@@ -167,6 +168,7 @@ declare module "fastify" {
|
||||
rateLimit: TRateLimitServiceFactory;
|
||||
userEngagement: TUserEngagementServiceFactory;
|
||||
externalKms: TExternalKmsServiceFactory;
|
||||
orgAdmin: TOrgAdminServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
21
backend/src/db/migrations/20240806185442_drop-tag-name.ts
Normal file
21
backend/src/db/migrations/20240806185442_drop-tag-name.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasNameField = await knex.schema.hasColumn(TableName.SecretTag, "name");
|
||||
if (hasNameField) {
|
||||
await knex.schema.alterTable(TableName.SecretTag, (t) => {
|
||||
t.dropColumn("name");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasNameField = await knex.schema.hasColumn(TableName.SecretTag, "name");
|
||||
if (!hasNameField) {
|
||||
await knex.schema.alterTable(TableName.SecretTag, (t) => {
|
||||
t.string("name");
|
||||
});
|
||||
}
|
||||
}
|
@@ -9,7 +9,6 @@ import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SecretTagsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
color: z.string().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
|
@@ -131,7 +131,7 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
|
||||
.default("/")
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(DYNAMIC_SECRET_LEASES.RENEW.path),
|
||||
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.ttl)
|
||||
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.environmentSlug)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@@ -75,15 +75,16 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
.del()
|
||||
.returning("id");
|
||||
numberOfRetryOnFailure = 0; // reset
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 100); // time to breathe for db
|
||||
});
|
||||
} catch (error) {
|
||||
numberOfRetryOnFailure += 1;
|
||||
logger.error(error, "Failed to delete audit log on pruning");
|
||||
} finally {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10); // time to breathe for db
|
||||
});
|
||||
}
|
||||
} while (deletedAuditLogIds.length > 0 && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE);
|
||||
} while (deletedAuditLogIds.length > 0 || numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE);
|
||||
};
|
||||
|
||||
return { ...auditLogOrm, pruneAuditLog, find };
|
||||
|
@@ -147,7 +147,8 @@ export enum EventType {
|
||||
GET_KMS = "get-kms",
|
||||
UPDATE_PROJECT_KMS = "update-project-kms",
|
||||
GET_PROJECT_KMS_BACKUP = "get-project-kms-backup",
|
||||
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup"
|
||||
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup",
|
||||
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project"
|
||||
}
|
||||
|
||||
interface UserActorMetadata {
|
||||
@@ -337,6 +338,7 @@ interface DeleteIntegrationEvent {
|
||||
targetServiceId?: string;
|
||||
path?: string;
|
||||
region?: string;
|
||||
shouldDeleteIntegrationSecrets?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1245,6 +1247,16 @@ interface LoadProjectKmsBackupEvent {
|
||||
metadata: Record<string, string>; // no metadata yet
|
||||
}
|
||||
|
||||
interface OrgAdminAccessProjectEvent {
|
||||
type: EventType.ORG_ADMIN_ACCESS_PROJECT;
|
||||
metadata: {
|
||||
userId: string;
|
||||
username: string;
|
||||
email: string;
|
||||
projectId: string;
|
||||
}; // no metadata yet
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@@ -1354,4 +1366,5 @@ export type Event =
|
||||
| GetKmsEvent
|
||||
| UpdateProjectKmsEvent
|
||||
| GetProjectKmsBackupEvent
|
||||
| LoadProjectKmsBackupEvent;
|
||||
| LoadProjectKmsBackupEvent
|
||||
| OrgAdminAccessProjectEvent;
|
||||
|
@@ -9,6 +9,10 @@ export enum OrgPermissionActions {
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum OrgPermissionAdminConsoleAction {
|
||||
AccessAllProjects = "access-all-projects"
|
||||
}
|
||||
|
||||
export enum OrgPermissionSubjects {
|
||||
Workspace = "workspace",
|
||||
Role = "role",
|
||||
@@ -22,7 +26,8 @@ export enum OrgPermissionSubjects {
|
||||
Billing = "billing",
|
||||
SecretScanning = "secret-scanning",
|
||||
Identity = "identity",
|
||||
Kms = "kms"
|
||||
Kms = "kms",
|
||||
AdminConsole = "organization-admin-console"
|
||||
}
|
||||
|
||||
export type OrgPermissionSet =
|
||||
@@ -39,7 +44,8 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms];
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||
|
||||
const buildAdminPermission = () => {
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||
@@ -107,6 +113,8 @@ const buildAdminPermission = () => {
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
||||
|
||||
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||
|
||||
return build({ conditionsMatcher });
|
||||
};
|
||||
|
||||
|
@@ -81,15 +81,13 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
.select({
|
||||
secVerTagId: "secVerTag.id",
|
||||
secVerTagColor: "secVerTag.color",
|
||||
secVerTagSlug: "secVerTag.slug",
|
||||
secVerTagName: "secVerTag.name"
|
||||
secVerTagSlug: "secVerTag.slug"
|
||||
})
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTag).as("tagJnId"),
|
||||
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
|
||||
)
|
||||
.select(
|
||||
db.ref("secretBlindIndex").withSchema(TableName.Secret).as("orgSecBlindIndex"),
|
||||
@@ -124,9 +122,9 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagJnId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({
|
||||
mapper: ({ tagId: id, tagSlug: slug, tagColor: color }) => ({
|
||||
id,
|
||||
name,
|
||||
name: slug,
|
||||
slug,
|
||||
color
|
||||
})
|
||||
@@ -200,11 +198,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "secVerTagId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({
|
||||
mapper: ({ secVerTagId: id, secVerTagSlug: slug, secVerTagColor: color }) => ({
|
||||
// eslint-disable-next-line
|
||||
id,
|
||||
// eslint-disable-next-line
|
||||
name,
|
||||
name: slug,
|
||||
// eslint-disable-next-line
|
||||
slug,
|
||||
// eslint-disable-next-line
|
||||
@@ -262,15 +260,13 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
.select({
|
||||
secVerTagId: "secVerTag.id",
|
||||
secVerTagColor: "secVerTag.color",
|
||||
secVerTagSlug: "secVerTag.slug",
|
||||
secVerTagName: "secVerTag.name"
|
||||
secVerTagSlug: "secVerTag.slug"
|
||||
})
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTagV2).as("tagJnId"),
|
||||
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
|
||||
)
|
||||
.select(
|
||||
db.ref("version").withSchema(TableName.SecretV2).as("orgSecVersion"),
|
||||
@@ -292,9 +288,9 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagJnId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({
|
||||
mapper: ({ tagId: id, tagSlug: slug, tagColor: color }) => ({
|
||||
id,
|
||||
name,
|
||||
name: slug,
|
||||
slug,
|
||||
color
|
||||
})
|
||||
@@ -330,11 +326,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "secVerTagId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({
|
||||
mapper: ({ secVerTagId: id, secVerTagSlug: slug, secVerTagColor: color }) => ({
|
||||
// eslint-disable-next-line
|
||||
id,
|
||||
// eslint-disable-next-line
|
||||
name,
|
||||
name: slug,
|
||||
// eslint-disable-next-line
|
||||
slug,
|
||||
// eslint-disable-next-line
|
||||
|
@@ -224,12 +224,10 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretKey: el.key,
|
||||
id: el.id,
|
||||
version: el.version,
|
||||
secretValue: el.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||
: undefined,
|
||||
secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
||||
secretComment: el.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||
: undefined,
|
||||
: "",
|
||||
secret: el.secret
|
||||
? {
|
||||
secretKey: el.secret.key,
|
||||
@@ -237,10 +235,10 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
version: el.secret.version,
|
||||
secretValue: el.secret.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
|
||||
: undefined,
|
||||
: "",
|
||||
secretComment: el.secret.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedComment }).toString()
|
||||
: undefined
|
||||
: ""
|
||||
}
|
||||
: undefined,
|
||||
secretVersion: el.secretVersion
|
||||
@@ -250,10 +248,10 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
version: el.secretVersion.version,
|
||||
secretValue: el.secretVersion.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
|
||||
: undefined,
|
||||
: "",
|
||||
secretComment: el.secretVersion.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
|
||||
: undefined
|
||||
: ""
|
||||
}
|
||||
: undefined
|
||||
}));
|
||||
|
@@ -257,7 +257,7 @@ export const secretReplicationServiceFactory = ({
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
folderDAL,
|
||||
secretImportDAL,
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
|
||||
});
|
||||
// secrets that gets replicated across imports
|
||||
const sourceDecryptedLocalSecrets = sourceLocalSecrets.map((el) => ({
|
||||
@@ -449,7 +449,7 @@ export const secretReplicationServiceFactory = ({
|
||||
});
|
||||
}
|
||||
if (locallyDeletedSecrets.length) {
|
||||
await secretDAL.delete(
|
||||
await secretV2BridgeDAL.delete(
|
||||
{
|
||||
$in: {
|
||||
id: locallyDeletedSecrets.map(({ id }) => id)
|
||||
|
@@ -164,10 +164,10 @@ export const secretSnapshotServiceFactory = ({
|
||||
secretKey: el.key,
|
||||
secretValue: el.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||
: undefined,
|
||||
: "",
|
||||
secretComment: el.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||
: undefined
|
||||
: ""
|
||||
}))
|
||||
};
|
||||
} else {
|
||||
|
@@ -100,8 +100,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||
db.ref("id").withSchema(TableName.SecretVersionTag).as("tagVersionId"),
|
||||
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
|
||||
);
|
||||
return sqlNestRelationships({
|
||||
data,
|
||||
@@ -132,9 +131,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagVersionId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
|
||||
mapper: ({ tagId: id, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
|
||||
id,
|
||||
name,
|
||||
name: slug,
|
||||
slug,
|
||||
color,
|
||||
vId
|
||||
@@ -195,8 +194,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
|
||||
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
|
||||
);
|
||||
return sqlNestRelationships({
|
||||
data,
|
||||
@@ -227,9 +225,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagVersionId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
|
||||
mapper: ({ tagId: id, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
|
||||
id,
|
||||
name,
|
||||
name: slug,
|
||||
slug,
|
||||
color,
|
||||
vId
|
||||
@@ -353,8 +351,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||
db.ref("id").withSchema(TableName.SecretVersionTag).as("tagVersionId"),
|
||||
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
|
||||
);
|
||||
|
||||
const formated = sqlNestRelationships({
|
||||
@@ -377,9 +374,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagVersionId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
|
||||
mapper: ({ tagId: id, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
|
||||
id,
|
||||
name,
|
||||
name: slug,
|
||||
slug,
|
||||
color,
|
||||
vId
|
||||
@@ -508,8 +505,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
|
||||
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
|
||||
);
|
||||
|
||||
const formated = sqlNestRelationships({
|
||||
@@ -532,9 +528,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagVersionId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
|
||||
mapper: ({ tagId: id, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
|
||||
id,
|
||||
name,
|
||||
name: slug,
|
||||
slug,
|
||||
color,
|
||||
vId
|
||||
|
@@ -596,7 +596,8 @@ export const RAW_SECRETS = {
|
||||
"The slug of the project to list secrets from. This parameter is only applicable by machine identities.",
|
||||
environment: "The slug of the environment to list secrets from.",
|
||||
secretPath: "The secret path to list secrets from.",
|
||||
includeImports: "Weather to include imported secrets or not."
|
||||
includeImports: "Weather to include imported secrets or not.",
|
||||
tagSlugs: "The comma separated tag slugs to filter secrets"
|
||||
},
|
||||
CREATE: {
|
||||
secretName: "The name of the secret to create.",
|
||||
|
@@ -19,23 +19,43 @@ export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
|
||||
|
||||
export type TFindFilter<R extends object = object> = Partial<R> & {
|
||||
$in?: Partial<{ [k in keyof R]: R[k][] }>;
|
||||
$search?: Partial<{ [k in keyof R]: R[k] }>;
|
||||
};
|
||||
export const buildFindFilter =
|
||||
<R extends object = object>({ $in, ...filter }: TFindFilter<R>) =>
|
||||
<R extends object = object>({ $in, $search, ...filter }: TFindFilter<R>) =>
|
||||
(bd: Knex.QueryBuilder<R, R>) => {
|
||||
void bd.where(filter);
|
||||
if ($in) {
|
||||
Object.entries($in).forEach(([key, val]) => {
|
||||
void bd.whereIn(key as never, val as never);
|
||||
if (val) {
|
||||
void bd.whereIn(key as never, val as never);
|
||||
}
|
||||
});
|
||||
}
|
||||
if ($search) {
|
||||
Object.entries($search).forEach(([key, val]) => {
|
||||
if (val) {
|
||||
void bd.whereILike(key as never, val as never);
|
||||
}
|
||||
});
|
||||
}
|
||||
return bd;
|
||||
};
|
||||
|
||||
export type TFindOpt<R extends object = object> = {
|
||||
export type TFindReturn<TQuery extends Knex.QueryBuilder, TCount extends boolean = false> = Array<
|
||||
Awaited<TQuery>[0] &
|
||||
(TCount extends true
|
||||
? {
|
||||
count: string;
|
||||
}
|
||||
: unknown)
|
||||
>;
|
||||
|
||||
export type TFindOpt<R extends object = object, TCount extends boolean = boolean> = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
sort?: Array<[keyof R, "asc" | "desc"] | [keyof R, "asc" | "desc", "first" | "last"]>;
|
||||
count?: TCount;
|
||||
tx?: Knex;
|
||||
};
|
||||
|
||||
@@ -66,18 +86,22 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
|
||||
throw new DatabaseError({ error, name: "Find one" });
|
||||
}
|
||||
},
|
||||
find: async (
|
||||
find: async <TCount extends boolean = false>(
|
||||
filter: TFindFilter<Tables[Tname]["base"]>,
|
||||
{ offset, limit, sort, tx }: TFindOpt<Tables[Tname]["base"]> = {}
|
||||
{ offset, limit, sort, count, tx }: TFindOpt<Tables[Tname]["base"], TCount> = {}
|
||||
) => {
|
||||
try {
|
||||
const query = (tx || db.replicaNode())(tableName).where(buildFindFilter(filter));
|
||||
if (count) {
|
||||
void query.select(db.raw("COUNT(*) OVER() AS count"));
|
||||
void query.select("*");
|
||||
}
|
||||
if (limit) void query.limit(limit);
|
||||
if (offset) void query.offset(offset);
|
||||
if (sort) {
|
||||
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
|
||||
}
|
||||
const res = await query;
|
||||
const res = (await query) as TFindReturn<typeof query, TCount>;
|
||||
return res;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find one" });
|
||||
@@ -104,6 +128,16 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
|
||||
throw new DatabaseError({ error, name: "Create" });
|
||||
}
|
||||
},
|
||||
// This spilit the insert into multiple chunk
|
||||
batchInsert: async (data: readonly Tables[Tname]["insert"][], tx?: Knex) => {
|
||||
try {
|
||||
if (!data.length) return [];
|
||||
const res = await (tx || db).batchInsert(tableName, data as never).returning("*");
|
||||
return res as Tables[Tname]["base"][];
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "batchInsert" });
|
||||
}
|
||||
},
|
||||
upsert: async (data: readonly Tables[Tname]["insert"][], onConflictField: keyof Tables[Tname]["base"], tx?: Knex) => {
|
||||
try {
|
||||
if (!data.length) return [];
|
||||
|
@@ -129,6 +129,7 @@ import { orgDALFactory } from "@app/services/org/org-dal";
|
||||
import { orgRoleDALFactory } from "@app/services/org/org-role-dal";
|
||||
import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
|
||||
import { orgServiceFactory } from "@app/services/org/org-service";
|
||||
import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
|
||||
import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||
import { projectDALFactory } from "@app/services/project/project-dal";
|
||||
import { projectQueueFactory } from "@app/services/project/project-queue";
|
||||
@@ -499,6 +500,16 @@ export const registerRoutes = async (
|
||||
keyStore,
|
||||
licenseService
|
||||
});
|
||||
const orgAdminService = orgAdminServiceFactory({
|
||||
projectDAL,
|
||||
permissionService,
|
||||
projectUserMembershipRoleDAL,
|
||||
userDAL,
|
||||
projectBotDAL,
|
||||
projectKeyDAL,
|
||||
projectMembershipDAL
|
||||
});
|
||||
|
||||
const rateLimitService = rateLimitServiceFactory({
|
||||
rateLimitDAL,
|
||||
licenseService
|
||||
@@ -886,8 +897,15 @@ export const registerRoutes = async (
|
||||
folderDAL,
|
||||
integrationDAL,
|
||||
integrationAuthDAL,
|
||||
secretQueueService
|
||||
secretQueueService,
|
||||
integrationAuthService,
|
||||
projectBotService,
|
||||
secretV2BridgeDAL,
|
||||
secretImportDAL,
|
||||
secretDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const serviceTokenService = serviceTokenServiceFactory({
|
||||
projectEnvDAL,
|
||||
serviceTokenDAL,
|
||||
@@ -1019,7 +1037,8 @@ export const registerRoutes = async (
|
||||
snapshotDAL,
|
||||
identityAccessTokenDAL,
|
||||
secretSharingDAL,
|
||||
secretVersionV2DAL: secretVersionV2BridgeDAL
|
||||
secretVersionV2DAL: secretVersionV2BridgeDAL,
|
||||
identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL
|
||||
});
|
||||
|
||||
const oidcService = oidcConfigServiceFactory({
|
||||
@@ -1114,7 +1133,8 @@ export const registerRoutes = async (
|
||||
identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService,
|
||||
secretSharing: secretSharingService,
|
||||
userEngagement: userEngagementService,
|
||||
externalKms: externalKmsService
|
||||
externalKms: externalKmsService,
|
||||
orgAdmin: orgAdminService
|
||||
});
|
||||
|
||||
const cronJobs: CronJob[] = [];
|
||||
|
@@ -63,8 +63,8 @@ export const secretRawSchema = z.object({
|
||||
version: z.number(),
|
||||
type: z.string(),
|
||||
secretKey: z.string(),
|
||||
secretValue: z.string().optional(),
|
||||
secretComment: z.string().optional(),
|
||||
secretValue: z.string(),
|
||||
secretComment: z.string(),
|
||||
secretReminderNote: z.string().nullable().optional(),
|
||||
secretReminderRepeatDays: z.number().nullable().optional(),
|
||||
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
|
||||
|
@@ -15,6 +15,7 @@ import { registerIdentityUaRouter } from "./identity-universal-auth-router";
|
||||
import { registerIntegrationAuthRouter } from "./integration-auth-router";
|
||||
import { registerIntegrationRouter } from "./integration-router";
|
||||
import { registerInviteOrgRouter } from "./invite-org-router";
|
||||
import { registerOrgAdminRouter } from "./org-admin-router";
|
||||
import { registerOrgRouter } from "./organization-router";
|
||||
import { registerPasswordRouter } from "./password-router";
|
||||
import { registerProjectEnvRouter } from "./project-env-router";
|
||||
@@ -50,6 +51,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerPasswordRouter, { prefix: "/password" });
|
||||
await server.register(registerOrgRouter, { prefix: "/organization" });
|
||||
await server.register(registerAdminRouter, { prefix: "/admin" });
|
||||
await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" });
|
||||
await server.register(registerUserRouter, { prefix: "/user" });
|
||||
await server.register(registerInviteOrgRouter, { prefix: "/invite-org" });
|
||||
await server.register(registerUserActionRouter, { prefix: "/user-action" });
|
||||
|
@@ -170,6 +170,12 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
params: z.object({
|
||||
integrationId: z.string().trim().describe(INTEGRATION.DELETE.integrationId)
|
||||
}),
|
||||
querystring: z.object({
|
||||
shouldDeleteIntegrationSecrets: z
|
||||
.enum(["true", "false"])
|
||||
.optional()
|
||||
.transform((val) => val === "true")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
integration: IntegrationsSchema
|
||||
@@ -183,7 +189,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.integrationId
|
||||
id: req.params.integrationId,
|
||||
shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
@@ -205,7 +212,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
targetService: integration.targetService,
|
||||
targetServiceId: integration.targetServiceId,
|
||||
path: integration.path,
|
||||
region: integration.region
|
||||
region: integration.region,
|
||||
shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets
|
||||
// eslint-disable-next-line
|
||||
}) as any
|
||||
}
|
||||
|
90
backend/src/server/routes/v1/org-admin-router.ts
Normal file
90
backend/src/server/routes/v1/org-admin-router.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectMembershipsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { SanitizedProjectSchema } from "../sanitizedSchemas";
|
||||
|
||||
export const registerOrgAdminRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/projects",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
search: z.string().optional(),
|
||||
offset: z.coerce.number().default(0),
|
||||
limit: z.coerce.number().max(100).default(50)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
projects: SanitizedProjectSchema.array(),
|
||||
count: z.coerce.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { projects, count } = await server.services.orgAdmin.listOrgProjects({
|
||||
limit: req.query.limit,
|
||||
offset: req.query.offset,
|
||||
search: req.query.search,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type
|
||||
});
|
||||
return { projects, count };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/projects/:projectId/grant-admin-access",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
projectId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
membership: ProjectMembershipsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { membership } = await server.services.orgAdmin.grantProjectAdminAccess({
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
projectId: req.params.projectId
|
||||
});
|
||||
if (req.auth.authMode === AuthMode.JWT) {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.projectId,
|
||||
event: {
|
||||
type: EventType.ORG_ADMIN_ACCESS_PROJECT,
|
||||
metadata: {
|
||||
projectId: req.params.projectId,
|
||||
username: req.auth.user.username,
|
||||
email: req.auth.user.email || "",
|
||||
userId: req.auth.userId
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { membership };
|
||||
}
|
||||
});
|
||||
};
|
@@ -1,3 +1,4 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretTagsSchema } from "@app/db/schemas";
|
||||
@@ -49,7 +50,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
workspaceTag: SecretTagsSchema
|
||||
// akhilmhdh: for terraform backward compatiability
|
||||
workspaceTag: SecretTagsSchema.extend({ name: z.string() })
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -79,7 +81,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
workspaceTag: SecretTagsSchema
|
||||
// akhilmhdh: for terraform backward compatiability
|
||||
workspaceTag: SecretTagsSchema.extend({ name: z.string() })
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -108,8 +111,14 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
||||
projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId)
|
||||
}),
|
||||
body: z.object({
|
||||
name: z.string().trim().describe(SECRET_TAGS.CREATE.name),
|
||||
slug: z.string().trim().describe(SECRET_TAGS.CREATE.slug),
|
||||
slug: z
|
||||
.string()
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.describe(SECRET_TAGS.CREATE.slug)
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
|
||||
}),
|
||||
color: z.string().trim().describe(SECRET_TAGS.CREATE.color)
|
||||
}),
|
||||
response: {
|
||||
@@ -144,8 +153,14 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
||||
tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId)
|
||||
}),
|
||||
body: z.object({
|
||||
name: z.string().trim().describe(SECRET_TAGS.UPDATE.name),
|
||||
slug: z.string().trim().describe(SECRET_TAGS.UPDATE.slug),
|
||||
slug: z
|
||||
.string()
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.describe(SECRET_TAGS.UPDATE.slug)
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
|
||||
}),
|
||||
color: z.string().trim().describe(SECRET_TAGS.UPDATE.color)
|
||||
}),
|
||||
response: {
|
||||
|
@@ -59,9 +59,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
color: true
|
||||
}).array()
|
||||
})
|
||||
.extend({ name: z.string() })
|
||||
.array()
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -116,16 +117,15 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge(
|
||||
z.object({
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
color: true
|
||||
}).array()
|
||||
secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
color: true
|
||||
})
|
||||
)
|
||||
.extend({ name: z.string() })
|
||||
.array()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -180,7 +180,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
.enum(["true", "false"])
|
||||
.default("false")
|
||||
.transform((value) => value === "true")
|
||||
.describe(RAW_SECRETS.LIST.includeImports)
|
||||
.describe(RAW_SECRETS.LIST.includeImports),
|
||||
tagSlugs: z
|
||||
.string()
|
||||
.describe(RAW_SECRETS.LIST.tagSlugs)
|
||||
.optional()
|
||||
// split by comma and trim the strings
|
||||
.transform((el) => (el ? el.split(",").map((i) => i.trim()) : []))
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -190,9 +196,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
color: true
|
||||
})
|
||||
.extend({ name: z.string() })
|
||||
.array()
|
||||
.optional()
|
||||
})
|
||||
@@ -251,7 +257,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
projectId: workspaceId,
|
||||
path: secretPath,
|
||||
includeImports: req.query.include_imports,
|
||||
recursive: req.query.recursive
|
||||
recursive: req.query.recursive,
|
||||
tagSlugs: req.query.tagSlugs
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
@@ -325,9 +332,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
color: true
|
||||
})
|
||||
.extend({ name: z.string() })
|
||||
.array()
|
||||
.optional()
|
||||
})
|
||||
@@ -731,9 +738,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
color: true
|
||||
}).array()
|
||||
})
|
||||
.extend({ name: z.string() })
|
||||
.array()
|
||||
})
|
||||
.array(),
|
||||
imports: z
|
||||
|
@@ -4,6 +4,7 @@ import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
export type TIdentityUaClientSecretDALFactory = ReturnType<typeof identityUaClientSecretDALFactory>;
|
||||
|
||||
@@ -23,5 +24,55 @@ export const identityUaClientSecretDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
return { ...uaClientSecretOrm, incrementUsage };
|
||||
const removeExpiredClientSecrets = async (tx?: Knex) => {
|
||||
const BATCH_SIZE = 10000;
|
||||
const MAX_RETRY_ON_FAILURE = 3;
|
||||
|
||||
let deletedClientSecret: { id: string }[] = [];
|
||||
let numberOfRetryOnFailure = 0;
|
||||
|
||||
do {
|
||||
try {
|
||||
const findExpiredClientSecretQuery = (tx || db)(TableName.IdentityUaClientSecret)
|
||||
.where({
|
||||
isClientSecretRevoked: true
|
||||
})
|
||||
.orWhere((qb) => {
|
||||
void qb
|
||||
.where("clientSecretNumUses", ">", 0)
|
||||
.andWhere(
|
||||
"clientSecretNumUses",
|
||||
">=",
|
||||
db.ref("clientSecretNumUsesLimit").withSchema(TableName.IdentityUaClientSecret)
|
||||
);
|
||||
})
|
||||
.orWhere((qb) => {
|
||||
void qb
|
||||
.where("clientSecretTTL", ">", 0)
|
||||
.andWhereRaw(
|
||||
`"${TableName.IdentityUaClientSecret}"."createdAt" + make_interval(secs => "${TableName.IdentityUaClientSecret}"."clientSecretTTL") < NOW()`
|
||||
);
|
||||
})
|
||||
.select("id")
|
||||
.limit(BATCH_SIZE);
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
deletedClientSecret = await (tx || db)(TableName.IdentityUaClientSecret)
|
||||
.whereIn("id", findExpiredClientSecretQuery)
|
||||
.del()
|
||||
.returning("id");
|
||||
numberOfRetryOnFailure = 0; // reset
|
||||
} catch (error) {
|
||||
numberOfRetryOnFailure += 1;
|
||||
logger.error(error, "Failed to delete client secret on pruning");
|
||||
} finally {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10); // time to breathe for db
|
||||
});
|
||||
}
|
||||
} while (deletedClientSecret.length > 0 || numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE);
|
||||
};
|
||||
|
||||
return { ...uaClientSecretOrm, incrementUsage, removeExpiredClientSecrets };
|
||||
};
|
||||
|
@@ -0,0 +1,357 @@
|
||||
import { retry } from "@octokit/plugin-retry";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
|
||||
import { TIntegrationAuths, TIntegrations } from "@app/db/schemas";
|
||||
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { IntegrationMetadataSchema } from "../integration/integration-schema";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
||||
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
|
||||
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||
import { TIntegrationAuthServiceFactory } from "./integration-auth-service";
|
||||
import { Integrations } from "./integration-list";
|
||||
|
||||
const MAX_SYNC_SECRET_DEPTH = 5;
|
||||
|
||||
/**
|
||||
* Return the secrets in a given [folderId] including secrets from
|
||||
* nested imported folders recursively.
|
||||
*/
|
||||
const getIntegrationSecretsV2 = async (
|
||||
dto: {
|
||||
projectId: string;
|
||||
environment: string;
|
||||
folderId: string;
|
||||
depth: number;
|
||||
decryptor: (value: Buffer | null | undefined) => string;
|
||||
},
|
||||
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">,
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">,
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">
|
||||
) => {
|
||||
const content: Record<string, boolean> = {};
|
||||
if (dto.depth > MAX_SYNC_SECRET_DEPTH) {
|
||||
logger.info(
|
||||
`getIntegrationSecrets: secret depth exceeded for [projectId=${dto.projectId}] [folderId=${dto.folderId}] [depth=${dto.depth}]`
|
||||
);
|
||||
return content;
|
||||
}
|
||||
|
||||
// process secrets in current folder
|
||||
const secrets = await secretV2BridgeDAL.findByFolderId(dto.folderId);
|
||||
|
||||
secrets.forEach((secret) => {
|
||||
const secretKey = secret.key;
|
||||
content[secretKey] = true;
|
||||
});
|
||||
|
||||
// check if current folder has any imports from other folders
|
||||
const secretImports = await secretImportDAL.find({ folderId: dto.folderId, isReplication: false });
|
||||
|
||||
// if no imports then return secrets in the current folder
|
||||
if (!secretImports.length) return content;
|
||||
const importedSecrets = await fnSecretsV2FromImports({
|
||||
decryptor: dto.decryptor,
|
||||
folderDAL,
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
secretImportDAL,
|
||||
allowedImports: secretImports
|
||||
});
|
||||
|
||||
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
|
||||
for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) {
|
||||
const importedSecret = importedSecrets[i].secrets[j];
|
||||
if (!content[importedSecret.key]) {
|
||||
content[importedSecret.key] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the secrets in a given [folderId] including secrets from
|
||||
* nested imported folders recursively.
|
||||
*/
|
||||
const getIntegrationSecretsV1 = async (
|
||||
dto: {
|
||||
projectId: string;
|
||||
environment: string;
|
||||
folderId: string;
|
||||
key: string;
|
||||
depth: number;
|
||||
},
|
||||
secretDAL: Pick<TSecretDALFactory, "findByFolderId">,
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">,
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">
|
||||
) => {
|
||||
let content: Record<string, boolean> = {};
|
||||
if (dto.depth > MAX_SYNC_SECRET_DEPTH) {
|
||||
logger.info(
|
||||
`getIntegrationSecrets: secret depth exceeded for [projectId=${dto.projectId}] [folderId=${dto.folderId}] [depth=${dto.depth}]`
|
||||
);
|
||||
return content;
|
||||
}
|
||||
|
||||
// process secrets in current folder
|
||||
const secrets = await secretDAL.findByFolderId(dto.folderId);
|
||||
secrets.forEach((secret) => {
|
||||
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: secret.secretKeyCiphertext,
|
||||
iv: secret.secretKeyIV,
|
||||
tag: secret.secretKeyTag,
|
||||
key: dto.key
|
||||
});
|
||||
|
||||
content[secretKey] = true;
|
||||
});
|
||||
|
||||
// check if current folder has any imports from other folders
|
||||
const secretImport = await secretImportDAL.find({ folderId: dto.folderId, isReplication: false });
|
||||
|
||||
// if no imports then return secrets in the current folder
|
||||
if (!secretImport) return content;
|
||||
|
||||
const importedFolders = await folderDAL.findByManySecretPath(
|
||||
secretImport.map(({ importEnv, importPath }) => ({
|
||||
envId: importEnv.id,
|
||||
secretPath: importPath
|
||||
}))
|
||||
);
|
||||
|
||||
for await (const folder of importedFolders) {
|
||||
if (folder) {
|
||||
// get secrets contained in each imported folder by recursively calling
|
||||
// this function against the imported folder
|
||||
const importedSecrets = await getIntegrationSecretsV1(
|
||||
{
|
||||
environment: dto.environment,
|
||||
projectId: dto.projectId,
|
||||
folderId: folder.id,
|
||||
key: dto.key,
|
||||
depth: dto.depth + 1
|
||||
},
|
||||
secretDAL,
|
||||
folderDAL,
|
||||
secretImportDAL
|
||||
);
|
||||
|
||||
// add the imported secrets to the current folder secrets
|
||||
content = { ...importedSecrets, ...content };
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
export const deleteGithubSecrets = async ({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
}: {
|
||||
integration: Omit<TIntegrations, "envId">;
|
||||
secrets: Record<string, boolean>;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
interface GitHubSecret {
|
||||
name: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
visibility?: "all" | "private" | "selected";
|
||||
selected_repositories_url?: string | undefined;
|
||||
}
|
||||
|
||||
const OctokitWithRetry = Octokit.plugin(retry);
|
||||
const octokit = new OctokitWithRetry({
|
||||
auth: accessToken
|
||||
});
|
||||
|
||||
enum GithubScope {
|
||||
Repo = "github-repo",
|
||||
Org = "github-org",
|
||||
Env = "github-env"
|
||||
}
|
||||
|
||||
let encryptedGithubSecrets: GitHubSecret[];
|
||||
|
||||
switch (integration.scope) {
|
||||
case GithubScope.Org: {
|
||||
encryptedGithubSecrets = (
|
||||
await octokit.request("GET /orgs/{org}/actions/secrets", {
|
||||
org: integration.owner as string
|
||||
})
|
||||
).data.secrets;
|
||||
break;
|
||||
}
|
||||
case GithubScope.Env: {
|
||||
encryptedGithubSecrets = (
|
||||
await octokit.request("GET /repositories/{repository_id}/environments/{environment_name}/secrets", {
|
||||
repository_id: Number(integration.appId),
|
||||
environment_name: integration.targetEnvironmentId as string
|
||||
})
|
||||
).data.secrets;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
encryptedGithubSecrets = (
|
||||
await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", {
|
||||
owner: integration.owner as string,
|
||||
repo: integration.app as string
|
||||
})
|
||||
).data.secrets;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for await (const encryptedSecret of encryptedGithubSecrets) {
|
||||
if (encryptedSecret.name in secrets) {
|
||||
switch (integration.scope) {
|
||||
case GithubScope.Org: {
|
||||
await octokit.request("DELETE /orgs/{org}/actions/secrets/{secret_name}", {
|
||||
org: integration.owner as string,
|
||||
secret_name: encryptedSecret.name
|
||||
});
|
||||
break;
|
||||
}
|
||||
case GithubScope.Env: {
|
||||
await octokit.request(
|
||||
"DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}",
|
||||
{
|
||||
repository_id: Number(integration.appId),
|
||||
environment_name: integration.targetEnvironmentId as string,
|
||||
secret_name: encryptedSecret.name
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
|
||||
owner: integration.owner as string,
|
||||
repo: integration.app as string,
|
||||
secret_name: encryptedSecret.name
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// small delay to prevent hitting API rate limits
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 50);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteIntegrationSecrets = async ({
|
||||
integration,
|
||||
integrationAuth,
|
||||
integrationAuthService,
|
||||
projectBotService,
|
||||
secretV2BridgeDAL,
|
||||
folderDAL,
|
||||
secretDAL,
|
||||
secretImportDAL,
|
||||
kmsService
|
||||
}: {
|
||||
integration: Omit<TIntegrations, "envId"> & {
|
||||
projectId: string;
|
||||
environment: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
secretPath: string;
|
||||
};
|
||||
integrationAuth: TIntegrationAuths;
|
||||
integrationAuthService: Pick<TIntegrationAuthServiceFactory, "getIntegrationAccessToken" | "getIntegrationAuth">;
|
||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath" | "findBySecretPath">;
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
||||
secretDAL: Pick<TSecretDALFactory, "findByFolderId">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}) => {
|
||||
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integration.projectId);
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: integration.projectId
|
||||
});
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(
|
||||
integration.projectId,
|
||||
integration.environment.slug,
|
||||
integration.secretPath
|
||||
);
|
||||
|
||||
if (!folder) {
|
||||
throw new NotFoundError({
|
||||
message: "Folder not found."
|
||||
});
|
||||
}
|
||||
|
||||
const { accessToken } = await integrationAuthService.getIntegrationAccessToken(
|
||||
integrationAuth,
|
||||
shouldUseSecretV2Bridge,
|
||||
botKey
|
||||
);
|
||||
|
||||
const secrets = shouldUseSecretV2Bridge
|
||||
? await getIntegrationSecretsV2(
|
||||
{
|
||||
environment: integration.environment.id,
|
||||
projectId: integration.projectId,
|
||||
folderId: folder.id,
|
||||
depth: 1,
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
|
||||
},
|
||||
secretV2BridgeDAL,
|
||||
folderDAL,
|
||||
secretImportDAL
|
||||
)
|
||||
: await getIntegrationSecretsV1(
|
||||
{
|
||||
environment: integration.environment.id,
|
||||
projectId: integration.projectId,
|
||||
folderId: folder.id,
|
||||
key: botKey as string,
|
||||
depth: 1
|
||||
},
|
||||
secretDAL,
|
||||
folderDAL,
|
||||
secretImportDAL
|
||||
);
|
||||
|
||||
const suffixedSecrets: typeof secrets = {};
|
||||
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
|
||||
|
||||
if (metadata) {
|
||||
Object.keys(secrets).forEach((key) => {
|
||||
const prefix = metadata?.secretPrefix || "";
|
||||
const suffix = metadata?.secretSuffix || "";
|
||||
const newKey = prefix + key + suffix;
|
||||
suffixedSecrets[newKey] = secrets[key];
|
||||
});
|
||||
}
|
||||
|
||||
switch (integration.integration) {
|
||||
case Integrations.GITHUB: {
|
||||
await deleteGithubSecrets({
|
||||
integration,
|
||||
accessToken,
|
||||
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new BadRequestError({
|
||||
message: "Invalid integration"
|
||||
});
|
||||
}
|
||||
};
|
@@ -538,19 +538,20 @@ const syncSecretsAWSParameterStore = async ({
|
||||
integration,
|
||||
secrets,
|
||||
accessId,
|
||||
accessToken
|
||||
accessToken,
|
||||
projectId
|
||||
}: {
|
||||
integration: TIntegrations;
|
||||
integration: TIntegrations & { secretPath: string; environment: { slug: string } };
|
||||
secrets: Record<string, { value: string; comment?: string }>;
|
||||
accessId: string | null;
|
||||
accessToken: string;
|
||||
projectId?: string;
|
||||
}) => {
|
||||
let response: { isSynced: boolean; syncMessage: string } | null = null;
|
||||
|
||||
if (!accessId) {
|
||||
throw new Error("AWS access ID is required");
|
||||
}
|
||||
|
||||
const config = new AWS.Config({
|
||||
region: integration.region as string,
|
||||
credentials: {
|
||||
@@ -567,7 +568,9 @@ const syncSecretsAWSParameterStore = async ({
|
||||
|
||||
const metadata = z.record(z.any()).parse(integration.metadata || {});
|
||||
const awsParameterStoreSecretsObj: Record<string, AWS.SSM.Parameter> = {};
|
||||
|
||||
logger.info(
|
||||
`getIntegrationSecrets: integration sync triggered for ssm with [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [shouldDisableDelete=${metadata.shouldDisableDelete}]`
|
||||
);
|
||||
// now fetch all aws parameter store secrets
|
||||
let hasNext = true;
|
||||
let nextToken: string | undefined;
|
||||
@@ -594,6 +597,18 @@ const syncSecretsAWSParameterStore = async ({
|
||||
nextToken = parameters.NextToken;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`getIntegrationSecrets: all fetched keys from AWS SSM [projectId=${projectId}] [environment=${
|
||||
integration.environment.slug
|
||||
}] [secretPath=${integration.secretPath}] [awsParameterStoreSecretsObj=${Object.keys(
|
||||
awsParameterStoreSecretsObj
|
||||
).join(",")}]`
|
||||
);
|
||||
logger.info(
|
||||
`getIntegrationSecrets: all secrets from Infisical to send to AWS SSM [projectId=${projectId}] [environment=${
|
||||
integration.environment.slug
|
||||
}] [secretPath=${integration.secretPath}] [secrets=${Object.keys(secrets).join(",")}]`
|
||||
);
|
||||
// Identify secrets to create
|
||||
// don't use Promise.all() and promise map here
|
||||
// it will cause rate limit
|
||||
@@ -603,24 +618,56 @@ const syncSecretsAWSParameterStore = async ({
|
||||
// case: secret does not exist in AWS parameter store
|
||||
// -> create secret
|
||||
if (secrets[key].value) {
|
||||
logger.info(
|
||||
`getIntegrationSecrets: create secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
|
||||
);
|
||||
await ssm
|
||||
.putParameter({
|
||||
Name: `${integration.path}${key}`,
|
||||
Type: "SecureString",
|
||||
Value: secrets[key].value,
|
||||
...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId }),
|
||||
// Overwrite: true,
|
||||
Tags: metadata.secretAWSTag
|
||||
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({
|
||||
Key: tag.key,
|
||||
Value: tag.value
|
||||
}))
|
||||
: []
|
||||
Overwrite: true
|
||||
})
|
||||
.promise();
|
||||
if (metadata.secretAWSTag?.length) {
|
||||
try {
|
||||
await ssm
|
||||
.addTagsToResource({
|
||||
ResourceType: "Parameter",
|
||||
ResourceId: `${integration.path}${key}`,
|
||||
Tags: metadata.secretAWSTag
|
||||
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({
|
||||
Key: tag.key,
|
||||
Value: tag.value
|
||||
}))
|
||||
: []
|
||||
})
|
||||
.promise();
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
err,
|
||||
`getIntegrationSecrets: create secret in AWS SSM for failed [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((err as any).code === "AccessDeniedException") {
|
||||
logger.error(
|
||||
`AWS Parameter Store Error [integration=${integration.id}]: double check AWS account permissions (refer to the Infisical docs)`
|
||||
);
|
||||
}
|
||||
|
||||
response = {
|
||||
isSynced: false,
|
||||
syncMessage: (err as AWSError)?.message || "Error syncing with AWS Parameter Store"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
// case: secret exists in AWS parameter store
|
||||
} else {
|
||||
logger.info(
|
||||
`getIntegrationSecrets: update secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
|
||||
);
|
||||
// -> update secret
|
||||
if (awsParameterStoreSecretsObj[key].Value !== secrets[key].value) {
|
||||
await ssm
|
||||
@@ -648,6 +695,10 @@ const syncSecretsAWSParameterStore = async ({
|
||||
})
|
||||
.promise();
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
err,
|
||||
`getIntegrationSecrets: update secret in AWS SSM for failed [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((err as any).code === "AccessDeniedException") {
|
||||
logger.error(
|
||||
@@ -670,9 +721,18 @@ const syncSecretsAWSParameterStore = async ({
|
||||
}
|
||||
|
||||
if (!metadata.shouldDisableDelete) {
|
||||
logger.info(
|
||||
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [step=1]`
|
||||
);
|
||||
for (const key in awsParameterStoreSecretsObj) {
|
||||
if (Object.hasOwn(awsParameterStoreSecretsObj, key)) {
|
||||
logger.info(
|
||||
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=2]`
|
||||
);
|
||||
if (!(key in secrets)) {
|
||||
logger.info(
|
||||
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=3]`
|
||||
);
|
||||
// case:
|
||||
// -> delete secret
|
||||
await ssm
|
||||
@@ -680,6 +740,9 @@ const syncSecretsAWSParameterStore = async ({
|
||||
Name: awsParameterStoreSecretsObj[key].Name as string
|
||||
})
|
||||
.promise();
|
||||
logger.info(
|
||||
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=4]`
|
||||
);
|
||||
}
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 50);
|
||||
@@ -3656,7 +3719,8 @@ export const syncIntegrationSecrets = async ({
|
||||
integration,
|
||||
secrets,
|
||||
accessId,
|
||||
accessToken
|
||||
accessToken,
|
||||
projectId
|
||||
});
|
||||
break;
|
||||
case Integrations.AWS_SECRET_MANAGER:
|
||||
|
@@ -6,8 +6,15 @@ import { BadRequestError } from "@app/lib/errors";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
|
||||
import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service";
|
||||
import { deleteIntegrationSecrets } from "../integration-auth/integration-delete-secret";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||
import { TSecretQueueFactory } from "../secret/secret-queue";
|
||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
||||
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||
import { TIntegrationDALFactory } from "./integration-dal";
|
||||
import {
|
||||
TCreateIntegrationDTO,
|
||||
@@ -19,9 +26,15 @@ import {
|
||||
type TIntegrationServiceFactoryDep = {
|
||||
integrationDAL: TIntegrationDALFactory;
|
||||
integrationAuthDAL: TIntegrationAuthDALFactory;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
||||
integrationAuthService: TIntegrationAuthServiceFactory;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findByManySecretPath">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
projectBotService: TProjectBotServiceFactory;
|
||||
secretQueueService: Pick<TSecretQueueFactory, "syncIntegrations">;
|
||||
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">;
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
secretDAL: Pick<TSecretDALFactory, "findByFolderId">;
|
||||
};
|
||||
|
||||
export type TIntegrationServiceFactory = ReturnType<typeof integrationServiceFactory>;
|
||||
@@ -31,7 +44,13 @@ export const integrationServiceFactory = ({
|
||||
integrationAuthDAL,
|
||||
folderDAL,
|
||||
permissionService,
|
||||
secretQueueService
|
||||
secretQueueService,
|
||||
integrationAuthService,
|
||||
projectBotService,
|
||||
secretV2BridgeDAL,
|
||||
secretImportDAL,
|
||||
kmsService,
|
||||
secretDAL
|
||||
}: TIntegrationServiceFactoryDep) => {
|
||||
const createIntegration = async ({
|
||||
app,
|
||||
@@ -161,7 +180,14 @@ export const integrationServiceFactory = ({
|
||||
return updatedIntegration;
|
||||
};
|
||||
|
||||
const deleteIntegration = async ({ actorId, id, actor, actorAuthMethod, actorOrgId }: TDeleteIntegrationDTO) => {
|
||||
const deleteIntegration = async ({
|
||||
actorId,
|
||||
id,
|
||||
actor,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
shouldDeleteIntegrationSecrets
|
||||
}: TDeleteIntegrationDTO) => {
|
||||
const integration = await integrationDAL.findById(id);
|
||||
if (!integration) throw new BadRequestError({ message: "Integration auth not found" });
|
||||
|
||||
@@ -174,6 +200,22 @@ export const integrationServiceFactory = ({
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
|
||||
|
||||
const integrationAuth = await integrationAuthDAL.findById(integration.integrationAuthId);
|
||||
|
||||
if (shouldDeleteIntegrationSecrets) {
|
||||
await deleteIntegrationSecrets({
|
||||
integration,
|
||||
integrationAuth,
|
||||
projectBotService,
|
||||
integrationAuthService,
|
||||
secretV2BridgeDAL,
|
||||
folderDAL,
|
||||
secretImportDAL,
|
||||
secretDAL,
|
||||
kmsService
|
||||
});
|
||||
}
|
||||
|
||||
const deletedIntegration = await integrationDAL.transaction(async (tx) => {
|
||||
// delete integration
|
||||
const deletedIntegrationResult = await integrationDAL.deleteById(id, tx);
|
||||
|
@@ -63,6 +63,7 @@ export type TUpdateIntegrationDTO = {
|
||||
|
||||
export type TDeleteIntegrationDTO = {
|
||||
id: string;
|
||||
shouldDeleteIntegrationSecrets?: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TSyncIntegrationDTO = {
|
||||
|
5
backend/src/services/org-admin/org-admin-dal.ts
Normal file
5
backend/src/services/org-admin/org-admin-dal.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type TOrgAdminDALFactory = ReturnType<typeof orgAdminDALFactory>;
|
||||
|
||||
export const orgAdminDALFactory = () => {
|
||||
return {};
|
||||
};
|
191
backend/src/services/org-admin/org-admin-service.ts
Normal file
191
backend/src/services/org-admin/org-admin-service.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ProjectMembershipRole, ProjectVersion, SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TAccessProjectDTO, TListOrgProjectsDTO } from "./org-admin-types";
|
||||
|
||||
type TOrgAdminServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
projectDAL: Pick<TProjectDALFactory, "find" | "findById" | "findProjectGhostUser">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findOne" | "create" | "transaction" | "delete">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "create">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserId">;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create" | "delete">;
|
||||
};
|
||||
|
||||
export type TOrgAdminServiceFactory = ReturnType<typeof orgAdminServiceFactory>;
|
||||
|
||||
export const orgAdminServiceFactory = ({
|
||||
permissionService,
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
projectKeyDAL,
|
||||
projectBotDAL,
|
||||
userDAL,
|
||||
projectUserMembershipRoleDAL
|
||||
}: TOrgAdminServiceFactoryDep) => {
|
||||
const listOrgProjects = async ({
|
||||
actor,
|
||||
limit,
|
||||
actorId,
|
||||
offset,
|
||||
search,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TListOrgProjectsDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionAdminConsoleAction.AccessAllProjects,
|
||||
OrgPermissionSubjects.AdminConsole
|
||||
);
|
||||
const projects = await projectDAL.find(
|
||||
{
|
||||
orgId: actorOrgId,
|
||||
$search: {
|
||||
name: search ? `%${search}%` : undefined
|
||||
}
|
||||
},
|
||||
{ offset, limit, sort: [["name", "asc"]], count: true }
|
||||
);
|
||||
|
||||
const count = projects?.[0]?.count ? parseInt(projects?.[0]?.count, 10) : 0;
|
||||
return { projects, count };
|
||||
};
|
||||
|
||||
const grantProjectAdminAccess = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId
|
||||
}: TAccessProjectDTO) => {
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionAdminConsoleAction.AccessAllProjects,
|
||||
OrgPermissionSubjects.AdminConsole
|
||||
);
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
if (project.version === ProjectVersion.V1) {
|
||||
throw new BadRequestError({ message: "Please upgrade your project on your dashboard" });
|
||||
}
|
||||
|
||||
// check already there exist a membership if there return it
|
||||
const projectMembership = await projectMembershipDAL.findOne({
|
||||
projectId,
|
||||
userId: actorId
|
||||
});
|
||||
if (projectMembership) {
|
||||
// reset and make the user admin
|
||||
await projectMembershipDAL.transaction(async (tx) => {
|
||||
await projectUserMembershipRoleDAL.delete({ projectMembershipId: projectMembership.id }, tx);
|
||||
await projectUserMembershipRoleDAL.create(
|
||||
{
|
||||
projectMembershipId: projectMembership.id,
|
||||
role: ProjectMembershipRole.Admin
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
return { isExistingMember: true, membership: projectMembership };
|
||||
}
|
||||
|
||||
// missing membership thus add admin back as admin to project
|
||||
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
|
||||
if (!ghostUser) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find sudo user"
|
||||
});
|
||||
}
|
||||
|
||||
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
|
||||
if (!ghostUserLatestKey) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find sudo user latest key"
|
||||
});
|
||||
}
|
||||
|
||||
const bot = await projectBotDAL.findOne({ projectId });
|
||||
if (!bot) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find bot"
|
||||
});
|
||||
}
|
||||
|
||||
const botPrivateKey = infisicalSymmetricDecrypt({
|
||||
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
|
||||
iv: bot.iv,
|
||||
tag: bot.tag,
|
||||
ciphertext: bot.encryptedPrivateKey
|
||||
});
|
||||
|
||||
const userEncryptionKey = await userDAL.findUserEncKeyByUserId(actorId);
|
||||
if (!userEncryptionKey) throw new BadRequestError({ message: "user encryption key not found" });
|
||||
const [newWsMember] = assignWorkspaceKeysToMembers({
|
||||
decryptKey: ghostUserLatestKey,
|
||||
userPrivateKey: botPrivateKey,
|
||||
members: [
|
||||
{
|
||||
orgMembershipId: membership.id,
|
||||
projectMembershipRole: ProjectMembershipRole.Admin,
|
||||
userPublicKey: userEncryptionKey.publicKey
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const updatedMembership = await projectMembershipDAL.transaction(async (tx) => {
|
||||
const newProjectMembership = await projectMembershipDAL.create(
|
||||
{
|
||||
projectId,
|
||||
userId: actorId
|
||||
},
|
||||
tx
|
||||
);
|
||||
await projectUserMembershipRoleDAL.create(
|
||||
{ projectMembershipId: newProjectMembership.id, role: ProjectMembershipRole.Admin },
|
||||
tx
|
||||
);
|
||||
|
||||
await projectKeyDAL.create(
|
||||
{
|
||||
encryptedKey: newWsMember.workspaceEncryptedKey,
|
||||
nonce: newWsMember.workspaceEncryptedNonce,
|
||||
senderId: ghostUser.id,
|
||||
receiverId: actorId,
|
||||
projectId
|
||||
},
|
||||
tx
|
||||
);
|
||||
return newProjectMembership;
|
||||
});
|
||||
return { isExistingMember: false, membership: updatedMembership };
|
||||
};
|
||||
|
||||
return { listOrgProjects, grantProjectAdminAccess };
|
||||
};
|
11
backend/src/services/org-admin/org-admin-types.ts
Normal file
11
backend/src/services/org-admin/org-admin-types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
export type TListOrgProjectsDTO = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TAccessProjectDTO = {
|
||||
projectId: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
@@ -46,6 +46,7 @@ export const projectBotDALFactory = (db: TDbClient) => {
|
||||
const doc = await db
|
||||
.replicaNode()(TableName.ProjectMembership)
|
||||
.where(`${TableName.ProjectMembership}.projectId` as "projectId", projectId)
|
||||
.where(`${TableName.ProjectKeys}.projectId` as "projectId", projectId)
|
||||
.where(`${TableName.Users}.isGhost` as "isGhost", false)
|
||||
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||
.join(TableName.ProjectKeys, `${TableName.ProjectMembership}.userId`, `${TableName.ProjectKeys}.receiverId`)
|
||||
|
@@ -66,10 +66,10 @@ export const getBotKeyFnFactory = (
|
||||
await projectBotDAL.create({
|
||||
name: "Infisical Bot (Ghost)",
|
||||
projectId,
|
||||
isActive: true,
|
||||
tag,
|
||||
iv,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
isActive: true,
|
||||
publicKey: botKey.publicKey,
|
||||
algorithm,
|
||||
keyEncoding: encoding,
|
||||
@@ -80,6 +80,12 @@ export const getBotKeyFnFactory = (
|
||||
} else {
|
||||
await projectBotDAL.updateById(bot.id, {
|
||||
isActive: true,
|
||||
tag,
|
||||
iv,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
publicKey: botKey.publicKey,
|
||||
algorithm,
|
||||
keyEncoding: encoding,
|
||||
encryptedProjectKey: encryptedWorkspaceKey.ciphertext,
|
||||
encryptedProjectKeyNonce: encryptedWorkspaceKey.nonce,
|
||||
senderId: projectV1Keys.userId
|
||||
@@ -89,7 +95,6 @@ export const getBotKeyFnFactory = (
|
||||
}
|
||||
|
||||
const botPrivateKey = getBotPrivateKey({ bot });
|
||||
|
||||
const botKey = decryptAsymmetric({
|
||||
ciphertext: bot.encryptedProjectKey,
|
||||
privateKey: botPrivateKey,
|
||||
|
@@ -256,7 +256,6 @@ export const projectMembershipServiceFactory = ({
|
||||
}
|
||||
|
||||
const bot = await projectBotDAL.findOne({ projectId });
|
||||
|
||||
if (!bot) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find bot"
|
||||
|
@@ -4,6 +4,7 @@ import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
|
||||
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
||||
import { TIdentityUaClientSecretDALFactory } from "../identity-ua/identity-ua-client-secret-dal";
|
||||
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
|
||||
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
|
||||
import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal";
|
||||
@@ -12,6 +13,7 @@ import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-d
|
||||
type TDailyResourceCleanUpQueueServiceFactoryDep = {
|
||||
auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">;
|
||||
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "removeExpiredTokens">;
|
||||
identityUniversalAuthClientSecretDAL: Pick<TIdentityUaClientSecretDALFactory, "removeExpiredClientSecrets">;
|
||||
secretVersionDAL: Pick<TSecretVersionDALFactory, "pruneExcessVersions">;
|
||||
secretVersionV2DAL: Pick<TSecretVersionV2DALFactory, "pruneExcessVersions">;
|
||||
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
|
||||
@@ -30,12 +32,14 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
||||
secretFolderVersionDAL,
|
||||
identityAccessTokenDAL,
|
||||
secretSharingDAL,
|
||||
secretVersionV2DAL
|
||||
secretVersionV2DAL,
|
||||
identityUniversalAuthClientSecretDAL
|
||||
}: TDailyResourceCleanUpQueueServiceFactoryDep) => {
|
||||
queueService.start(QueueName.DailyResourceCleanUp, async () => {
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
|
||||
await auditLogDAL.pruneAuditLog();
|
||||
await identityAccessTokenDAL.removeExpiredTokens();
|
||||
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
|
||||
await secretSharingDAL.pruneExpiredSharedSecrets();
|
||||
await snapshotDAL.pruneExcessSnapshots();
|
||||
await secretVersionDAL.pruneExcessVersions();
|
||||
|
@@ -36,8 +36,8 @@ type TSecretImportSecretsV2 = {
|
||||
secretKey: string;
|
||||
// akhilmhdh: yes i know you can put ?.
|
||||
// But for somereason ts consider ? and undefined explicit as different just ts things
|
||||
secretValue: string | undefined;
|
||||
secretComment: string | undefined;
|
||||
secretValue: string;
|
||||
secretComment: string;
|
||||
})[];
|
||||
};
|
||||
|
||||
@@ -157,7 +157,7 @@ export const fnSecretsV2FromImports = async ({
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds">;
|
||||
depth?: number;
|
||||
cyclicDetector?: Set<string>;
|
||||
decryptor: (value?: Buffer | null) => string | undefined;
|
||||
decryptor: (value?: Buffer | null) => string;
|
||||
expandSecretReferences?: (
|
||||
secrets: Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>
|
||||
) => Promise<Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>>;
|
||||
@@ -231,6 +231,7 @@ export const fnSecretsV2FromImports = async ({
|
||||
_id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend.
|
||||
}))
|
||||
.concat(folderDeeperImportSecrets);
|
||||
|
||||
return {
|
||||
secretPath: importPath,
|
||||
environment: importEnv.slug,
|
||||
@@ -254,7 +255,7 @@ export const fnSecretsV2FromImports = async ({
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>
|
||||
{} as Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean | null }>
|
||||
);
|
||||
// eslint-disable-next-line
|
||||
await expandSecretReferences(secretsGroupByKey);
|
||||
|
@@ -507,7 +507,7 @@ export const secretImportServiceFactory = ({
|
||||
folderDAL,
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
secretImportDAL,
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
|
||||
});
|
||||
return importedSecrets;
|
||||
}
|
||||
|
@@ -51,7 +51,7 @@ export const secretTagDALFactory = (db: TDbClient) => {
|
||||
...secretTagOrm,
|
||||
saveTagsToSecret: secretJnTagOrm.insertMany,
|
||||
deleteTagsToSecret: secretJnTagOrm.delete,
|
||||
saveTagsToSecretV2: secretV2JnTagOrm.insertMany,
|
||||
saveTagsToSecretV2: secretV2JnTagOrm.batchInsert,
|
||||
deleteTagsToSecretV2: secretV2JnTagOrm.delete,
|
||||
findSecretTagsByProjectId,
|
||||
deleteTagsManySecret,
|
||||
|
@@ -22,16 +22,7 @@ type TSecretTagServiceFactoryDep = {
|
||||
export type TSecretTagServiceFactory = ReturnType<typeof secretTagServiceFactory>;
|
||||
|
||||
export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSecretTagServiceFactoryDep) => {
|
||||
const createTag = async ({
|
||||
name,
|
||||
slug,
|
||||
actor,
|
||||
color,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId
|
||||
}: TCreateTagDTO) => {
|
||||
const createTag = async ({ slug, actor, color, actorId, actorOrgId, actorAuthMethod, projectId }: TCreateTagDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@@ -46,7 +37,6 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
|
||||
const newTag = await secretTagDAL.create({
|
||||
projectId,
|
||||
name,
|
||||
slug,
|
||||
color,
|
||||
createdBy: actorId,
|
||||
@@ -55,7 +45,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
return newTag;
|
||||
};
|
||||
|
||||
const updateTag = async ({ actorId, actor, actorOrgId, actorAuthMethod, id, name, color, slug }: TUpdateTagDTO) => {
|
||||
const updateTag = async ({ actorId, actor, actorOrgId, actorAuthMethod, id, color, slug }: TUpdateTagDTO) => {
|
||||
const tag = await secretTagDAL.findById(id);
|
||||
if (!tag) throw new BadRequestError({ message: "Tag doesn't exist" });
|
||||
|
||||
@@ -73,7 +63,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
|
||||
|
||||
const updatedTag = await secretTagDAL.updateById(tag.id, { name, color, slug });
|
||||
const updatedTag = await secretTagDAL.updateById(tag.id, { color, slug });
|
||||
return updatedTag;
|
||||
};
|
||||
|
||||
@@ -107,7 +97,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
||||
|
||||
return tag;
|
||||
return { ...tag, name: tag.slug };
|
||||
};
|
||||
|
||||
const getTagBySlug = async ({ actorId, actor, actorOrgId, actorAuthMethod, slug, projectId }: TGetTagBySlugDTO) => {
|
||||
@@ -123,7 +113,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
||||
|
||||
return tag;
|
||||
return { ...tag, name: tag.slug };
|
||||
};
|
||||
|
||||
const getProjectTags = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TListProjectTagsDTO) => {
|
||||
|
@@ -1,14 +1,12 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TCreateTagDTO = {
|
||||
name: string;
|
||||
color: string;
|
||||
slug: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TUpdateTagDTO = {
|
||||
id: string;
|
||||
name?: string;
|
||||
slug?: string;
|
||||
color?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
@@ -136,7 +136,6 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
|
||||
.orderBy("id", "asc");
|
||||
|
||||
const data = sqlNestRelationships({
|
||||
@@ -147,11 +146,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
|
||||
id,
|
||||
color,
|
||||
slug,
|
||||
name
|
||||
name: slug
|
||||
})
|
||||
}
|
||||
]
|
||||
@@ -169,14 +168,13 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
.where({ [`${TableName.SecretV2}Id` as const]: secretId })
|
||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
|
||||
|
||||
return tags.map((el) => ({
|
||||
id: el.tagId,
|
||||
color: el.tagColor,
|
||||
slug: el.tagSlug,
|
||||
name: el.tagName
|
||||
name: el.tagSlug
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "get secret tags" });
|
||||
@@ -210,7 +208,6 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
|
||||
.orderBy("id", "asc");
|
||||
|
||||
const data = sqlNestRelationships({
|
||||
@@ -221,11 +218,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
|
||||
id,
|
||||
color,
|
||||
slug,
|
||||
name
|
||||
name: slug
|
||||
})
|
||||
}
|
||||
]
|
||||
@@ -290,7 +287,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
}))
|
||||
);
|
||||
if (!newSecretReferences.length) return;
|
||||
const secretReferences = await (tx || db)(TableName.SecretReferenceV2).insert(newSecretReferences);
|
||||
const secretReferences = await (tx || db).batchInsert(TableName.SecretReferenceV2, newSecretReferences);
|
||||
return secretReferences;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "UpsertSecretReference" });
|
||||
@@ -350,8 +347,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
.select(selectAllTableCols(TableName.SecretV2))
|
||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
|
||||
const docs = sqlNestRelationships({
|
||||
data: rawDocs,
|
||||
key: "id",
|
||||
@@ -360,11 +356,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
|
||||
id,
|
||||
color,
|
||||
slug,
|
||||
name
|
||||
name: slug
|
||||
})
|
||||
}
|
||||
]
|
||||
|
@@ -528,8 +528,8 @@ export const reshapeBridgeSecret = (
|
||||
environment: string,
|
||||
secretPath: string,
|
||||
secret: Omit<TSecretsV2, "encryptedValue" | "encryptedComment"> & {
|
||||
value?: string;
|
||||
comment?: string;
|
||||
value: string;
|
||||
comment: string;
|
||||
tags?: {
|
||||
id: string;
|
||||
slug: string;
|
||||
@@ -542,8 +542,8 @@ export const reshapeBridgeSecret = (
|
||||
secretPath,
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
secretValue: secret.value,
|
||||
secretComment: secret.comment,
|
||||
secretValue: secret.value || "",
|
||||
secretComment: secret.comment || "",
|
||||
version: secret.version,
|
||||
type: secret.type,
|
||||
_id: secret.id,
|
||||
|
@@ -196,7 +196,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
return reshapeBridgeSecret(projectId, environment, secretPath, {
|
||||
...secret[0],
|
||||
value: inputSecret.secretValue,
|
||||
comment: inputSecret.secretComment
|
||||
comment: inputSecret.secretComment || ""
|
||||
});
|
||||
};
|
||||
|
||||
@@ -339,8 +339,8 @@ export const secretV2BridgeServiceFactory = ({
|
||||
});
|
||||
return reshapeBridgeSecret(projectId, environment, secretPath, {
|
||||
...updatedSecret[0],
|
||||
value: inputSecret.secretValue,
|
||||
comment: inputSecret.secretComment
|
||||
value: inputSecret.secretValue || "",
|
||||
comment: inputSecret.secretComment || ""
|
||||
});
|
||||
};
|
||||
|
||||
@@ -378,6 +378,18 @@ export const secretV2BridgeServiceFactory = ({
|
||||
throw new BadRequestError({ message: "Must be user to delete personal secret" });
|
||||
}
|
||||
|
||||
const secretToDelete = await secretDAL.findOne({
|
||||
key: inputSecret.secretName,
|
||||
folderId,
|
||||
...(inputSecret.type === SecretType.Shared
|
||||
? {}
|
||||
: {
|
||||
type: SecretType.Personal,
|
||||
userId: actorId
|
||||
})
|
||||
});
|
||||
if (!secretToDelete) throw new NotFoundError({ message: "Secret not found" });
|
||||
|
||||
const deletedSecret = await secretDAL.transaction(async (tx) =>
|
||||
fnSecretBulkDelete({
|
||||
projectId,
|
||||
@@ -412,10 +424,10 @@ export const secretV2BridgeServiceFactory = ({
|
||||
...deletedSecret[0],
|
||||
value: deletedSecret[0].encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString()
|
||||
: undefined,
|
||||
: "",
|
||||
comment: deletedSecret[0].encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString()
|
||||
: undefined
|
||||
: ""
|
||||
});
|
||||
};
|
||||
|
||||
@@ -429,6 +441,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
includeImports,
|
||||
recursive,
|
||||
tagSlugs = [],
|
||||
expandSecretReferences: shouldExpandSecretReferences
|
||||
}: TGetSecretsDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
@@ -490,12 +503,15 @@ export const secretV2BridgeServiceFactory = ({
|
||||
...secret,
|
||||
value: secret.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
|
||||
: undefined,
|
||||
: "",
|
||||
comment: secret.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
|
||||
: undefined
|
||||
: ""
|
||||
})
|
||||
);
|
||||
const filteredSecrets = tagSlugs.length
|
||||
? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug))))
|
||||
: decryptedSecrets;
|
||||
const expandSecretReferences = expandSecretReferencesFactory({
|
||||
projectId,
|
||||
folderDAL,
|
||||
@@ -504,7 +520,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
});
|
||||
|
||||
if (shouldExpandSecretReferences) {
|
||||
const secretsGroupByPath = groupBy(decryptedSecrets, (i) => i.secretPath);
|
||||
const secretsGroupByPath = groupBy(filteredSecrets, (i) => i.secretPath);
|
||||
for (const secretPathKey in secretsGroupByPath) {
|
||||
if (Object.hasOwn(secretsGroupByPath, secretPathKey)) {
|
||||
const secretsGroupByKey = secretsGroupByPath[secretPathKey].reduce(
|
||||
@@ -522,7 +538,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
await expandSecretReferences(secretsGroupByKey);
|
||||
secretsGroupByPath[secretPathKey].forEach((decryptedSecret) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value;
|
||||
decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value || "";
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -530,7 +546,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
|
||||
if (!includeImports) {
|
||||
return {
|
||||
secrets: decryptedSecrets
|
||||
secrets: filteredSecrets
|
||||
};
|
||||
}
|
||||
|
||||
@@ -554,11 +570,11 @@ export const secretV2BridgeServiceFactory = ({
|
||||
folderDAL,
|
||||
secretImportDAL,
|
||||
expandSecretReferences,
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
|
||||
});
|
||||
|
||||
return {
|
||||
secrets: decryptedSecrets,
|
||||
secrets: filteredSecrets,
|
||||
imports: importedSecrets
|
||||
};
|
||||
};
|
||||
@@ -654,7 +670,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretDAL,
|
||||
folderDAL,
|
||||
secretImportDAL,
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined),
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
|
||||
expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined
|
||||
});
|
||||
|
||||
@@ -662,12 +678,11 @@ export const secretV2BridgeServiceFactory = ({
|
||||
for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) {
|
||||
const importedSecret = importedSecrets[i].secrets[j];
|
||||
if (secretName === importedSecret.key) {
|
||||
return reshapeBridgeSecret(
|
||||
projectId,
|
||||
importedSecrets[i].environment,
|
||||
importedSecrets[i].secretPath,
|
||||
importedSecret
|
||||
);
|
||||
return reshapeBridgeSecret(projectId, importedSecrets[i].environment, importedSecrets[i].secretPath, {
|
||||
...importedSecret,
|
||||
value: importedSecret.secretValue || "",
|
||||
comment: importedSecret.secretComment || ""
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -676,7 +691,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
|
||||
let secretValue = secret.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
|
||||
: undefined;
|
||||
: "";
|
||||
if (shouldExpandSecretReferences && secretValue) {
|
||||
const secretReferenceExpandedRecord = {
|
||||
[secret.key]: { value: secretValue }
|
||||
@@ -691,7 +706,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
value: secretValue,
|
||||
comment: secret.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
|
||||
: undefined
|
||||
: ""
|
||||
});
|
||||
};
|
||||
|
||||
@@ -781,10 +796,8 @@ export const secretV2BridgeServiceFactory = ({
|
||||
return newSecrets.map((el) =>
|
||||
reshapeBridgeSecret(projectId, environment, secretPath, {
|
||||
...el,
|
||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined,
|
||||
comment: el.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||
: undefined
|
||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
||||
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -902,10 +915,8 @@ export const secretV2BridgeServiceFactory = ({
|
||||
return secrets.map((el) =>
|
||||
reshapeBridgeSecret(projectId, environment, secretPath, {
|
||||
...el,
|
||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined,
|
||||
comment: el.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||
: undefined
|
||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
||||
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -981,10 +992,8 @@ export const secretV2BridgeServiceFactory = ({
|
||||
return secretsDeleted.map((el) =>
|
||||
reshapeBridgeSecret(projectId, environment, secretPath, {
|
||||
...el,
|
||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined,
|
||||
comment: el.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||
: undefined
|
||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
||||
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -1020,10 +1029,8 @@ export const secretV2BridgeServiceFactory = ({
|
||||
return secretVersions.map((el) =>
|
||||
reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", {
|
||||
...el,
|
||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined,
|
||||
comment: el.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||
: undefined
|
||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
||||
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
|
||||
})
|
||||
);
|
||||
};
|
||||
|
@@ -20,6 +20,7 @@ export type TGetSecretsDTO = {
|
||||
environment: string;
|
||||
includeImports?: boolean;
|
||||
recursive?: boolean;
|
||||
tagSlugs?: string[];
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetASecretDTO = {
|
||||
|
@@ -123,7 +123,6 @@ export const secretDALFactory = (db: TDbClient) => {
|
||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
|
||||
.orderBy("id", "asc");
|
||||
const data = sqlNestRelationships({
|
||||
data: secs,
|
||||
@@ -133,11 +132,11 @@ export const secretDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
|
||||
id,
|
||||
color,
|
||||
slug,
|
||||
name
|
||||
name: slug
|
||||
})
|
||||
}
|
||||
]
|
||||
@@ -155,14 +154,13 @@ export const secretDALFactory = (db: TDbClient) => {
|
||||
.where({ [`${TableName.Secret}Id` as const]: secretId })
|
||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
|
||||
|
||||
return tags.map((el) => ({
|
||||
id: el.tagId,
|
||||
color: el.tagColor,
|
||||
slug: el.tagSlug,
|
||||
name: el.tagName
|
||||
name: el.tagSlug
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "get secret tags" });
|
||||
@@ -188,7 +186,6 @@ export const secretDALFactory = (db: TDbClient) => {
|
||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
|
||||
.orderBy("id", "asc");
|
||||
const data = sqlNestRelationships({
|
||||
data: secs,
|
||||
@@ -198,11 +195,11 @@ export const secretDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
|
||||
id,
|
||||
color,
|
||||
slug,
|
||||
name
|
||||
name: slug
|
||||
})
|
||||
}
|
||||
]
|
||||
@@ -318,8 +315,7 @@ export const secretDALFactory = (db: TDbClient) => {
|
||||
.select(selectAllTableCols(TableName.Secret))
|
||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
|
||||
const docs = sqlNestRelationships({
|
||||
data: rawDocs,
|
||||
key: "id",
|
||||
@@ -328,11 +324,11 @@ export const secretDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
|
||||
id,
|
||||
color,
|
||||
slug,
|
||||
name
|
||||
name: slug
|
||||
})
|
||||
}
|
||||
]
|
||||
|
@@ -370,7 +370,6 @@ export const decryptSecretRaw = (
|
||||
id: string;
|
||||
slug: string;
|
||||
color?: string | null;
|
||||
name: string;
|
||||
}[];
|
||||
},
|
||||
key: string
|
||||
@@ -412,7 +411,7 @@ export const decryptSecretRaw = (
|
||||
_id: secret.id,
|
||||
id: secret.id,
|
||||
user: secret.userId,
|
||||
tags: secret.tags,
|
||||
tags: secret.tags?.map((el) => ({ ...el, name: el.slug })),
|
||||
skipMultilineEncoding: secret.skipMultilineEncoding,
|
||||
secretReminderRepeatDays: secret.secretReminderRepeatDays,
|
||||
secretReminderNote: secret.secretReminderNote,
|
||||
|
@@ -73,12 +73,12 @@ type TSecretQueueFactoryDep = {
|
||||
secretVersionTagDAL: TSecretVersionTagDALFactory;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
secretV2BridgeDAL: TSecretV2BridgeDALFactory;
|
||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "batchInsert" | "insertMany" | "findLatestVersionMany">;
|
||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "batchInsert">;
|
||||
secretRotationDAL: Pick<TSecretRotationDALFactory, "secretOutputV2InsertMany" | "find">;
|
||||
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "deleteByProjectId">;
|
||||
snapshotDAL: Pick<TSnapshotDALFactory, "findNSecretV1SnapshotByFolderId" | "deleteSnapshotsAboveLimit">;
|
||||
snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany">;
|
||||
snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany" | "batchInsert">;
|
||||
};
|
||||
|
||||
export type TGetSecrets = {
|
||||
@@ -728,7 +728,10 @@ export const secretQueueFactory = ({
|
||||
isSynced: response?.isSynced ?? true
|
||||
});
|
||||
} catch (err) {
|
||||
logger.info("Secret integration sync error: %o", err);
|
||||
logger.error(
|
||||
err,
|
||||
`Secret integration sync error [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}]`
|
||||
);
|
||||
|
||||
const message =
|
||||
(err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) ||
|
||||
@@ -828,7 +831,7 @@ export const secretQueueFactory = ({
|
||||
secretId: string;
|
||||
references: { environment: string; secretPath: string; secretKey: string }[];
|
||||
}[] = [];
|
||||
await secretV2BridgeDAL.insertMany(
|
||||
await secretV2BridgeDAL.batchInsert(
|
||||
projectV1Secrets.map((el) => {
|
||||
const key = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: el.secretKeyCiphertext,
|
||||
@@ -1004,14 +1007,14 @@ export const secretQueueFactory = ({
|
||||
|
||||
const projectV3SecretVersions = Object.values(projectV3SecretVersionsGroupById);
|
||||
if (projectV3SecretVersions.length) {
|
||||
await secretVersionV2BridgeDAL.insertMany(projectV3SecretVersions, tx);
|
||||
await secretVersionV2BridgeDAL.batchInsert(projectV3SecretVersions, tx);
|
||||
}
|
||||
if (projectV3SecretVersionTags.length) {
|
||||
await secretVersionTagV2BridgeDAL.insertMany(projectV3SecretVersionTags, tx);
|
||||
await secretVersionTagV2BridgeDAL.batchInsert(projectV3SecretVersionTags, tx);
|
||||
}
|
||||
|
||||
if (projectV3SnapshotSecrets.length) {
|
||||
await snapshotSecretV2BridgeDAL.insertMany(projectV3SnapshotSecrets, tx);
|
||||
await snapshotSecretV2BridgeDAL.batchInsert(projectV3SnapshotSecrets, tx);
|
||||
}
|
||||
await snapshotDAL.deleteSnapshotsAboveLimit(folderId, SNAPSHOT_BATCH_SIZE, tx);
|
||||
}
|
||||
|
@@ -964,7 +964,8 @@ export const secretServiceFactory = ({
|
||||
environment,
|
||||
includeImports,
|
||||
expandSecretReferences,
|
||||
recursive
|
||||
recursive,
|
||||
tagSlugs = []
|
||||
}: TGetSecretsRawDTO) => {
|
||||
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
@@ -978,7 +979,8 @@ export const secretServiceFactory = ({
|
||||
path,
|
||||
recursive,
|
||||
actorAuthMethod,
|
||||
includeImports
|
||||
includeImports,
|
||||
tagSlugs
|
||||
});
|
||||
return { secrets, imports };
|
||||
}
|
||||
@@ -998,6 +1000,9 @@ export const secretServiceFactory = ({
|
||||
});
|
||||
|
||||
const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey));
|
||||
const filteredSecrets = tagSlugs.length
|
||||
? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug))))
|
||||
: decryptedSecrets;
|
||||
const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => {
|
||||
const decryptedImportSecrets = importedSecrets.map((sec) =>
|
||||
decryptSecretRaw(
|
||||
@@ -1106,14 +1111,14 @@ export const secretServiceFactory = ({
|
||||
};
|
||||
|
||||
// expand secrets
|
||||
await batchSecretsExpand(decryptedSecrets);
|
||||
await batchSecretsExpand(filteredSecrets);
|
||||
|
||||
// expand imports by batch
|
||||
await Promise.all(processedImports.map((processedImport) => batchSecretsExpand(processedImport.secrets)));
|
||||
}
|
||||
|
||||
return {
|
||||
secrets: decryptedSecrets,
|
||||
secrets: filteredSecrets,
|
||||
imports: processedImports
|
||||
};
|
||||
};
|
||||
@@ -1149,6 +1154,7 @@ export const secretServiceFactory = ({
|
||||
type,
|
||||
secretName
|
||||
});
|
||||
|
||||
return secret;
|
||||
}
|
||||
|
||||
@@ -2081,7 +2087,7 @@ export const secretServiceFactory = ({
|
||||
|
||||
return {
|
||||
...updatedSecret[0],
|
||||
tags: [...existingSecretTags, ...tags].map((t) => ({ id: t.id, slug: t.slug, name: t.name, color: t.color }))
|
||||
tags: [...existingSecretTags, ...tags].map((t) => ({ id: t.id, slug: t.slug, name: t.slug, color: t.color }))
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -149,6 +149,7 @@ export type TGetSecretsRawDTO = {
|
||||
environment: string;
|
||||
includeImports?: boolean;
|
||||
recursive?: boolean;
|
||||
tagSlugs?: string[];
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetASecretRawDTO = {
|
||||
|
@@ -404,6 +404,10 @@ func CallGetRawSecretsV3(httpClient *resty.Client, request GetRawSecretsV3Reques
|
||||
SetQueryParam("environment", request.Environment).
|
||||
SetQueryParam("secretPath", request.SecretPath)
|
||||
|
||||
if request.TagSlugs != "" {
|
||||
req.SetQueryParam("tagSlugs", request.TagSlugs)
|
||||
}
|
||||
|
||||
if request.IncludeImport {
|
||||
req.SetQueryParam("include_imports", "true")
|
||||
}
|
||||
|
@@ -574,6 +574,7 @@ type GetRawSecretsV3Request struct {
|
||||
SecretPath string `json:"secretPath"`
|
||||
IncludeImport bool `json:"include_imports"`
|
||||
Recursive bool `json:"recursive"`
|
||||
TagSlugs string `json:"tagSlugs,omitempty"`
|
||||
}
|
||||
|
||||
type GetRawSecretsV3Response struct {
|
||||
|
@@ -312,7 +312,7 @@ func ParseAgentConfig(configFile []byte) (*Config, error) {
|
||||
|
||||
func secretTemplateFunction(accessToken string, existingEtag string, currentEtag *string) func(string, string, string) ([]models.SingleEnvironmentVariable, error) {
|
||||
return func(projectID, envSlug, secretPath string) ([]models.SingleEnvironmentVariable, error) {
|
||||
res, err := util.GetPlainTextSecretsV3(accessToken, projectID, envSlug, secretPath, false, false)
|
||||
res, err := util.GetPlainTextSecretsV3(accessToken, projectID, envSlug, secretPath, false, false, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -188,7 +189,7 @@ func formatEnvs(envs []models.SingleEnvironmentVariable, format string) (string,
|
||||
case FormatCSV:
|
||||
return formatAsCSV(envs), nil
|
||||
case FormatYaml:
|
||||
return formatAsYaml(envs), nil
|
||||
return formatAsYaml(envs)
|
||||
default:
|
||||
return "", fmt.Errorf("invalid format type: %s. Available format types are [%s]", format, []string{FormatDotenv, FormatJson, FormatCSV, FormatYaml, FormatDotEnvExport})
|
||||
}
|
||||
@@ -224,12 +225,18 @@ func formatAsDotEnvExport(envs []models.SingleEnvironmentVariable) string {
|
||||
return dotenv
|
||||
}
|
||||
|
||||
func formatAsYaml(envs []models.SingleEnvironmentVariable) string {
|
||||
var dotenv string
|
||||
func formatAsYaml(envs []models.SingleEnvironmentVariable) (string, error) {
|
||||
m := make(map[string]string)
|
||||
for _, env := range envs {
|
||||
dotenv += fmt.Sprintf("%s: %s\n", env.Key, env.Value)
|
||||
m[env.Key] = env.Value
|
||||
}
|
||||
return dotenv
|
||||
|
||||
yamlBytes, err := yaml.Marshal(m)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to format environment variables as YAML: %w", err)
|
||||
}
|
||||
|
||||
return string(yamlBytes), nil
|
||||
}
|
||||
|
||||
// Format environment variables as a JSON file
|
||||
|
79
cli/packages/cmd/export_test.go
Normal file
79
cli/packages/cmd/export_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func TestFormatAsYaml(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []models.SingleEnvironmentVariable
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Empty input",
|
||||
input: []models.SingleEnvironmentVariable{},
|
||||
expected: "{}\n",
|
||||
},
|
||||
{
|
||||
name: "Single environment variable",
|
||||
input: []models.SingleEnvironmentVariable{
|
||||
{Key: "KEY1", Value: "VALUE1"},
|
||||
},
|
||||
expected: "KEY1: VALUE1\n",
|
||||
},
|
||||
{
|
||||
name: "Multiple environment variables",
|
||||
input: []models.SingleEnvironmentVariable{
|
||||
{Key: "KEY1", Value: "VALUE1"},
|
||||
{Key: "KEY2", Value: "VALUE2"},
|
||||
{Key: "KEY3", Value: "VALUE3"},
|
||||
},
|
||||
expected: "KEY1: VALUE1\nKEY2: VALUE2\nKEY3: VALUE3\n",
|
||||
},
|
||||
{
|
||||
name: "Overwriting duplicate keys",
|
||||
input: []models.SingleEnvironmentVariable{
|
||||
{Key: "KEY1", Value: "VALUE1"},
|
||||
{Key: "KEY1", Value: "VALUE2"},
|
||||
},
|
||||
expected: "KEY1: VALUE2\n",
|
||||
},
|
||||
{
|
||||
name: "Special characters in values",
|
||||
input: []models.SingleEnvironmentVariable{
|
||||
{Key: "KEY1", Value: "Value with spaces"},
|
||||
{Key: "KEY2", Value: "Value:with:colons"},
|
||||
{Key: "KEY3", Value: "Value\nwith\nnewlines"},
|
||||
},
|
||||
expected: "KEY1: Value with spaces\nKEY2: Value:with:colons\nKEY3: |-\n Value\n with\n newlines\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := formatAsYaml(tt.input)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Compare the result with the expected output
|
||||
assert.Equal(t, tt.expected, result)
|
||||
|
||||
// Additionally, parse the result back into a map to ensure it's valid YAML
|
||||
var resultMap map[string]string
|
||||
err = yaml.Unmarshal([]byte(result), &resultMap)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create an expected map from the input
|
||||
expectedMap := make(map[string]string)
|
||||
for _, env := range tt.input {
|
||||
expectedMap[env.Key] = env.Value
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedMap, resultMap)
|
||||
})
|
||||
}
|
||||
}
|
@@ -155,22 +155,24 @@ var secretsSetCmd = &cobra.Command{
|
||||
DisableFlagsInUseLine: true,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
util.RequireLocalWorkspaceFile()
|
||||
|
||||
environmentName, _ := cmd.Flags().GetString("env")
|
||||
if !cmd.Flags().Changed("env") {
|
||||
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
|
||||
if environmentFromWorkspace != "" {
|
||||
environmentName = environmentFromWorkspace
|
||||
}
|
||||
}
|
||||
|
||||
token, err := util.GetInfisicalToken(cmd)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
projectId, err := cmd.Flags().GetString("projectId")
|
||||
if (token == nil) {
|
||||
util.RequireLocalWorkspaceFile()
|
||||
}
|
||||
|
||||
environmentName, _ := cmd.Flags().GetString("env")
|
||||
if !cmd.Flags().Changed("env") {
|
||||
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
|
||||
if environmentFromWorkspace != "" {
|
||||
environmentName = environmentFromWorkspace
|
||||
}
|
||||
}
|
||||
|
||||
projectId, err := cmd.Flags().GetString("projectId")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
@@ -374,6 +376,11 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
secretOverriding, err := cmd.Flags().GetBool("secret-overriding")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
request := models.GetAllSecretsParameters{
|
||||
Environment: environmentName,
|
||||
WorkspaceId: projectId,
|
||||
@@ -394,6 +401,12 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
|
||||
util.HandleError(err, "To fetch all secrets")
|
||||
}
|
||||
|
||||
if secretOverriding {
|
||||
secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_PERSONAL)
|
||||
} else {
|
||||
secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_SHARED)
|
||||
}
|
||||
|
||||
if shouldExpand {
|
||||
authParams := models.ExpandSecretsAuthentication{}
|
||||
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
|
||||
@@ -413,11 +426,13 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
|
||||
if value, ok := secretsMap[secretKeyFromArg]; ok {
|
||||
requestedSecrets = append(requestedSecrets, value)
|
||||
} else {
|
||||
requestedSecrets = append(requestedSecrets, models.SingleEnvironmentVariable{
|
||||
Key: secretKeyFromArg,
|
||||
Type: "*not found*",
|
||||
Value: "*not found*",
|
||||
})
|
||||
if !(plainOutput || showOnlyValue) {
|
||||
requestedSecrets = append(requestedSecrets, models.SingleEnvironmentVariable{
|
||||
Key: secretKeyFromArg,
|
||||
Type: "*not found*",
|
||||
Value: "*not found*",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -688,6 +703,7 @@ func init() {
|
||||
secretsGetCmd.Flags().Bool("include-imports", true, "Imported linked secrets ")
|
||||
secretsGetCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets, and process your referenced secrets")
|
||||
secretsGetCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders")
|
||||
secretsGetCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
|
||||
secretsCmd.AddCommand(secretsGetCmd)
|
||||
secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
|
||||
secretsCmd.AddCommand(secretsSetCmd)
|
||||
|
@@ -4,6 +4,7 @@ Copyright (c) 2023 Infisical Inc.
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -13,13 +14,26 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var AvailableVaultsAndDescriptions = []string{"auto (automatically select native vault on system)", "file (encrypted file vault)"}
|
||||
var AvailableVaults = []string{"auto", "file"}
|
||||
type VaultBackendType struct {
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
var AvailableVaults = []VaultBackendType{
|
||||
{
|
||||
Name: "auto",
|
||||
Description: "automatically select the system keyring",
|
||||
},
|
||||
{
|
||||
Name: "file",
|
||||
Description: "encrypted file vault",
|
||||
},
|
||||
}
|
||||
|
||||
var vaultSetCmd = &cobra.Command{
|
||||
Example: `infisical vault set pass`,
|
||||
Use: "set [vault-name]",
|
||||
Short: "Used to set the vault backend to store your login details securely at rest",
|
||||
Example: `infisical vault set file`,
|
||||
Use: "set [file|auto]",
|
||||
Short: "Used to configure the vault backends",
|
||||
DisableFlagsInUseLine: true,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
@@ -35,15 +49,16 @@ var vaultSetCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
if wantedVaultTypeName == "auto" || wantedVaultTypeName == "file" {
|
||||
if wantedVaultTypeName == util.VAULT_BACKEND_AUTO_MODE || wantedVaultTypeName == util.VAULT_BACKEND_FILE_MODE {
|
||||
configFile, err := util.GetConfigFile()
|
||||
if err != nil {
|
||||
log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
|
||||
return
|
||||
}
|
||||
|
||||
configFile.VaultBackendType = wantedVaultTypeName // save selected vault
|
||||
configFile.LoggedInUserEmail = "" // reset the logged in user to prompt them to re login
|
||||
configFile.VaultBackendType = wantedVaultTypeName
|
||||
configFile.LoggedInUserEmail = ""
|
||||
configFile.VaultBackendPassphrase = base64.StdEncoding.EncodeToString([]byte(util.GenerateRandomString(10)))
|
||||
|
||||
err = util.WriteConfigFile(&configFile)
|
||||
if err != nil {
|
||||
@@ -55,7 +70,11 @@ var vaultSetCmd = &cobra.Command{
|
||||
|
||||
Telemetry.CaptureEvent("cli-command:vault set", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("wantedVault", wantedVaultTypeName).Set("version", util.CLI_VERSION))
|
||||
} else {
|
||||
log.Error().Msgf("The requested vault type [%s] is not available on this system. Only the following vault backends are available for you system: %s", wantedVaultTypeName, strings.Join(AvailableVaults, ", "))
|
||||
var availableVaultsNames []string
|
||||
for _, vault := range AvailableVaults {
|
||||
availableVaultsNames = append(availableVaultsNames, vault.Name)
|
||||
}
|
||||
log.Error().Msgf("The requested vault type [%s] is not available on this system. Only the following vault backends are available for you system: %s", wantedVaultTypeName, strings.Join(availableVaultsNames, ", "))
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -73,8 +92,8 @@ var vaultCmd = &cobra.Command{
|
||||
|
||||
func printAvailableVaultBackends() {
|
||||
fmt.Printf("Vaults are used to securely store your login details locally. Available vaults:")
|
||||
for _, backend := range AvailableVaultsAndDescriptions {
|
||||
fmt.Printf("\n- %s", backend)
|
||||
for _, vaultType := range AvailableVaults {
|
||||
fmt.Printf("\n- %s (%s)", vaultType.Name, vaultType.Description)
|
||||
}
|
||||
|
||||
currentVaultBackend, err := util.GetCurrentVaultBackend()
|
||||
@@ -89,5 +108,6 @@ func printAvailableVaultBackends() {
|
||||
|
||||
func init() {
|
||||
vaultCmd.AddCommand(vaultSetCmd)
|
||||
|
||||
rootCmd.AddCommand(vaultCmd)
|
||||
}
|
||||
|
@@ -11,10 +11,11 @@ type UserCredentials struct {
|
||||
|
||||
// The file struct for Infisical config file
|
||||
type ConfigFile struct {
|
||||
LoggedInUserEmail string `json:"loggedInUserEmail"`
|
||||
LoggedInUserDomain string `json:"LoggedInUserDomain,omitempty"`
|
||||
LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"`
|
||||
VaultBackendType string `json:"vaultBackendType,omitempty"`
|
||||
LoggedInUserEmail string `json:"loggedInUserEmail"`
|
||||
LoggedInUserDomain string `json:"LoggedInUserDomain,omitempty"`
|
||||
LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"`
|
||||
VaultBackendType string `json:"vaultBackendType,omitempty"`
|
||||
VaultBackendPassphrase string `json:"vaultBackendPassphrase,omitempty"`
|
||||
}
|
||||
|
||||
type LoggedInUser struct {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -50,10 +51,11 @@ func WriteInitalConfig(userCredentials *models.UserCredentials) error {
|
||||
}
|
||||
|
||||
configFile := models.ConfigFile{
|
||||
LoggedInUserEmail: userCredentials.Email,
|
||||
LoggedInUserDomain: config.INFISICAL_URL,
|
||||
LoggedInUsers: existingConfigFile.LoggedInUsers,
|
||||
VaultBackendType: existingConfigFile.VaultBackendType,
|
||||
LoggedInUserEmail: userCredentials.Email,
|
||||
LoggedInUserDomain: config.INFISICAL_URL,
|
||||
LoggedInUsers: existingConfigFile.LoggedInUsers,
|
||||
VaultBackendType: existingConfigFile.VaultBackendType,
|
||||
VaultBackendPassphrase: existingConfigFile.VaultBackendPassphrase,
|
||||
}
|
||||
|
||||
configFileMarshalled, err := json.Marshal(configFile)
|
||||
@@ -215,6 +217,14 @@ func GetConfigFile() (models.ConfigFile, error) {
|
||||
return models.ConfigFile{}, err
|
||||
}
|
||||
|
||||
if configFile.VaultBackendPassphrase != "" {
|
||||
decodedPassphrase, err := base64.StdEncoding.DecodeString(configFile.VaultBackendPassphrase)
|
||||
if err != nil {
|
||||
return models.ConfigFile{}, fmt.Errorf("GetConfigFile: Unable to decode base64 passphrase [err=%s]", err)
|
||||
}
|
||||
os.Setenv("INFISICAL_VAULT_FILE_PASSPHRASE", string(decodedPassphrase))
|
||||
}
|
||||
|
||||
return configFile, nil
|
||||
}
|
||||
|
||||
|
@@ -8,6 +8,10 @@ const (
|
||||
INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json"
|
||||
INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN"
|
||||
INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN"
|
||||
INFISICAL_VAULT_FILE_PASSPHRASE_ENV_NAME = "INFISICAL_VAULT_FILE_PASSPHRASE" // This works because we've forked the keyring package and added support for this env variable. This explains why you won't find any occurrences of it in the CLI codebase.
|
||||
|
||||
VAULT_BACKEND_AUTO_MODE = "auto"
|
||||
VAULT_BACKEND_FILE_MODE = "file"
|
||||
|
||||
// Universal Auth
|
||||
INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME = "INFISICAL_UNIVERSAL_AUTH_CLIENT_ID"
|
||||
@@ -34,7 +38,8 @@ const (
|
||||
SERVICE_TOKEN_IDENTIFIER = "service-token"
|
||||
UNIVERSAL_AUTH_TOKEN_IDENTIFIER = "universal-auth-token"
|
||||
|
||||
INFISICAL_BACKUP_SECRET = "infisical-backup-secrets"
|
||||
INFISICAL_BACKUP_SECRET = "infisical-backup-secrets" // akhilmhdh: @depreciated remove in version v0.30
|
||||
INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY = "infisical-backup-secret-encryption-key"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@@ -71,7 +71,7 @@ func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) {
|
||||
if strings.Contains(err.Error(), "credentials not found in system keyring") {
|
||||
return LoggedInUserDetails{}, errors.New("we couldn't find your logged in details, try running [infisical login] then try again")
|
||||
} else {
|
||||
return LoggedInUserDetails{}, fmt.Errorf("failed to fetch creditnals from keyring because [err=%s]", err)
|
||||
return LoggedInUserDetails{}, fmt.Errorf("failed to fetch credentials from keyring because [err=%s]", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
@@ -25,6 +26,8 @@ type DecodedSymmetricEncryptionDetails = struct {
|
||||
Key []byte
|
||||
}
|
||||
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV string, tag string) (DecodedSymmetricEncryptionDetails, error) {
|
||||
cipherx, err := base64.StdEncoding.DecodeString(cipher)
|
||||
if err != nil {
|
||||
@@ -287,3 +290,11 @@ func GetCmdFlagOrEnv(cmd *cobra.Command, flag, envName string) (string, error) {
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func GenerateRandomString(length int) string {
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = charset[rand.Intn(len(charset))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
@@ -1,6 +1,10 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
@@ -20,16 +24,39 @@ func SetValueInKeyring(key, value string) error {
|
||||
PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical rest] then try again")
|
||||
}
|
||||
|
||||
return keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value)
|
||||
err = keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value)
|
||||
|
||||
if err != nil {
|
||||
log.Debug().Msg(fmt.Sprintf("Error while setting default keyring: %v", err))
|
||||
configFile, _ := GetConfigFile()
|
||||
|
||||
if configFile.VaultBackendPassphrase == "" {
|
||||
encodedPassphrase := base64.StdEncoding.EncodeToString([]byte(GenerateRandomString(10))) // generate random passphrase
|
||||
configFile.VaultBackendPassphrase = encodedPassphrase
|
||||
configFile.VaultBackendType = VAULT_BACKEND_FILE_MODE
|
||||
err = WriteConfigFile(&configFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We call this function at last to trigger the environment variable to be set
|
||||
GetConfigFile()
|
||||
}
|
||||
|
||||
err = keyring.Set(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key, value)
|
||||
log.Debug().Msg(fmt.Sprintf("Error while setting file keyring: %v", err))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func GetValueInKeyring(key string) (string, error) {
|
||||
currentVaultBackend, err := GetCurrentVaultBackend()
|
||||
if err != nil {
|
||||
PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical rest] then try again")
|
||||
PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical reset] then try again")
|
||||
}
|
||||
|
||||
return keyring.Get(currentVaultBackend, MAIN_KEYRING_SERVICE, key)
|
||||
|
||||
}
|
||||
|
||||
func DeleteValueInKeyring(key string) error {
|
||||
|
@@ -1,14 +1,15 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
@@ -20,7 +21,7 @@ import (
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool) ([]models.SingleEnvironmentVariable, error) {
|
||||
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool, tagSlugs string) ([]models.SingleEnvironmentVariable, error) {
|
||||
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
|
||||
if len(serviceTokenParts) < 4 {
|
||||
return nil, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
|
||||
@@ -53,6 +54,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
|
||||
SecretPath: secretPath,
|
||||
IncludeImport: includeImports,
|
||||
Recursive: recursive,
|
||||
TagSlugs: tagSlugs,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -76,7 +78,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
|
||||
|
||||
}
|
||||
|
||||
func GetPlainTextSecretsV3(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool, recursive bool) (models.PlaintextSecretResult, error) {
|
||||
func GetPlainTextSecretsV3(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool, recursive bool, tagSlugs string) (models.PlaintextSecretResult, error) {
|
||||
httpClient := resty.New()
|
||||
httpClient.SetAuthToken(accessToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
@@ -86,7 +88,7 @@ func GetPlainTextSecretsV3(accessToken string, workspaceId string, environmentNa
|
||||
Environment: environmentName,
|
||||
IncludeImport: includeImports,
|
||||
Recursive: recursive,
|
||||
// TagSlugs: tagSlugs,
|
||||
TagSlugs: tagSlugs,
|
||||
}
|
||||
|
||||
if secretsPath != "" {
|
||||
@@ -281,29 +283,36 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
|
||||
}
|
||||
|
||||
res, err := GetPlainTextSecretsV3(loggedInUserDetails.UserCredentials.JTWToken, infisicalDotJson.WorkspaceId,
|
||||
params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive)
|
||||
params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive, params.TagSlugs)
|
||||
log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", err)
|
||||
|
||||
if err == nil {
|
||||
WriteBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath, res.Secrets)
|
||||
backupEncryptionKey, err := GetBackupEncryptionKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
WriteBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath, backupEncryptionKey, res.Secrets)
|
||||
}
|
||||
|
||||
secretsToReturn = res.Secrets
|
||||
errorToReturn = err
|
||||
// only attempt to serve cached secrets if no internet connection and if at least one secret cached
|
||||
if !isConnected {
|
||||
backedSecrets, err := ReadBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath)
|
||||
if len(backedSecrets) > 0 {
|
||||
PrintWarning("Unable to fetch latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug")
|
||||
secretsToReturn = backedSecrets
|
||||
errorToReturn = err
|
||||
backupEncryptionKey, _ := GetBackupEncryptionKey()
|
||||
if backupEncryptionKey != nil {
|
||||
backedUpSecrets, err := ReadBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath, backupEncryptionKey)
|
||||
if len(backedUpSecrets) > 0 {
|
||||
PrintWarning("Unable to fetch the latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug")
|
||||
secretsToReturn = backedUpSecrets
|
||||
errorToReturn = err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
if params.InfisicalToken != "" {
|
||||
log.Debug().Msg("Trying to fetch secrets using service token")
|
||||
secretsToReturn, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive)
|
||||
secretsToReturn, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive, params.TagSlugs)
|
||||
} else if params.UniversalAuthAccessToken != "" {
|
||||
|
||||
if params.WorkspaceId == "" {
|
||||
@@ -311,7 +320,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
|
||||
}
|
||||
|
||||
log.Debug().Msg("Trying to fetch secrets using universal auth")
|
||||
res, err := GetPlainTextSecretsV3(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive)
|
||||
res, err := GetPlainTextSecretsV3(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive, params.TagSlugs)
|
||||
|
||||
errorToReturn = err
|
||||
secretsToReturn = res.Secrets
|
||||
@@ -476,71 +485,99 @@ func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType stri
|
||||
return secretsToReturn
|
||||
}
|
||||
|
||||
func WriteBackupSecrets(workspace string, environment string, secretsPath string, secrets []models.SingleEnvironmentVariable) error {
|
||||
var backedUpSecrets []models.BackupSecretKeyRing
|
||||
secretValueInKeyRing, err := GetValueInKeyring(INFISICAL_BACKUP_SECRET)
|
||||
func GetBackupEncryptionKey() ([]byte, error) {
|
||||
encryptionKey, err := GetValueInKeyring(INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY)
|
||||
if err != nil {
|
||||
if err == keyring.ErrUnsupportedPlatform {
|
||||
return errors.New("your OS does not support keyring. Consider using a service token https://infisical.com/docs/documentation/platform/token")
|
||||
} else if err != keyring.ErrNotFound {
|
||||
return fmt.Errorf("something went wrong, failed to retrieve value from system keyring [error=%v]", err)
|
||||
return nil, errors.New("your OS does not support keyring. Consider using a service token https://infisical.com/docs/documentation/platform/token")
|
||||
} else if err == keyring.ErrNotFound {
|
||||
// generate a new key
|
||||
randomizedKey := make([]byte, 16)
|
||||
rand.Read(randomizedKey)
|
||||
encryptionKey = hex.EncodeToString(randomizedKey)
|
||||
if err := SetValueInKeyring(INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY, encryptionKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(encryptionKey), nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("something went wrong, failed to retrieve value from system keyring [error=%v]", err)
|
||||
}
|
||||
}
|
||||
_ = json.Unmarshal([]byte(secretValueInKeyRing), &backedUpSecrets)
|
||||
return []byte(encryptionKey), nil
|
||||
}
|
||||
|
||||
backedUpSecrets = slices.DeleteFunc(backedUpSecrets, func(e models.BackupSecretKeyRing) bool {
|
||||
return e.SecretPath == secretsPath && e.ProjectID == workspace && e.Environment == environment
|
||||
})
|
||||
newBackupSecret := models.BackupSecretKeyRing{
|
||||
ProjectID: workspace,
|
||||
Environment: environment,
|
||||
SecretPath: secretsPath,
|
||||
Secrets: secrets,
|
||||
}
|
||||
backedUpSecrets = append(backedUpSecrets, newBackupSecret)
|
||||
func WriteBackupSecrets(workspace string, environment string, secretsPath string, encryptionKey []byte, secrets []models.SingleEnvironmentVariable) error {
|
||||
formattedPath := strings.ReplaceAll(secretsPath, "/", "-")
|
||||
fileName := fmt.Sprintf("project_secrets_%s_%s_%s.json", workspace, environment, formattedPath)
|
||||
secrets_backup_folder_name := "secrets-backup"
|
||||
|
||||
listOfSecretsMarshalled, err := json.Marshal(backedUpSecrets)
|
||||
_, fullConfigFileDirPath, err := GetFullConfigFilePath()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("WriteBackupSecrets: unable to get full config folder path [err=%s]", err)
|
||||
}
|
||||
|
||||
err = SetValueInKeyring(INFISICAL_BACKUP_SECRET, string(listOfSecretsMarshalled))
|
||||
// create secrets backup directory
|
||||
fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
|
||||
if _, err := os.Stat(fullPathToSecretsBackupFolder); errors.Is(err, os.ErrNotExist) {
|
||||
err := os.Mkdir(fullPathToSecretsBackupFolder, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
marshaledSecrets, _ := json.Marshal(secrets)
|
||||
result, err := crypto.EncryptSymmetric(marshaledSecrets, encryptionKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("StoreUserCredsInKeyRing: unable to store user credentials because [err=%s]", err)
|
||||
return fmt.Errorf("WriteBackupSecrets: Unable to encrypt local secret backup to file [err=%s]", err)
|
||||
}
|
||||
listOfSecretsMarshalled, _ := json.Marshal(result)
|
||||
err = os.WriteFile(fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName), listOfSecretsMarshalled, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("WriteBackupSecrets: Unable to write backup secrets to file [err=%s]", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReadBackupSecrets(workspace string, environment string, secretsPath string) ([]models.SingleEnvironmentVariable, error) {
|
||||
secretValueInKeyRing, err := GetValueInKeyring(INFISICAL_BACKUP_SECRET)
|
||||
func ReadBackupSecrets(workspace string, environment string, secretsPath string, encryptionKey []byte) ([]models.SingleEnvironmentVariable, error) {
|
||||
formattedPath := strings.ReplaceAll(secretsPath, "/", "-")
|
||||
fileName := fmt.Sprintf("project_secrets_%s_%s_%s.json", workspace, environment, formattedPath)
|
||||
secrets_backup_folder_name := "secrets-backup"
|
||||
|
||||
_, fullConfigFileDirPath, err := GetFullConfigFilePath()
|
||||
if err != nil {
|
||||
if err == keyring.ErrUnsupportedPlatform {
|
||||
return nil, errors.New("your OS does not support keyring. Consider using a service token https://infisical.com/docs/documentation/platform/token")
|
||||
} else if err == keyring.ErrNotFound {
|
||||
return nil, errors.New("credentials not found in system keyring")
|
||||
} else {
|
||||
return nil, fmt.Errorf("something went wrong, failed to retrieve value from system keyring [error=%v]", err)
|
||||
}
|
||||
return nil, fmt.Errorf("ReadBackupSecrets: unable to write config file because an error occurred when getting config file path [err=%s]", err)
|
||||
}
|
||||
|
||||
var backedUpSecrets []models.BackupSecretKeyRing
|
||||
err = json.Unmarshal([]byte(secretValueInKeyRing), &backedUpSecrets)
|
||||
fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
|
||||
if _, err := os.Stat(fullPathToSecretsBackupFolder); errors.Is(err, os.ErrNotExist) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
encryptedBackupSecretsFilePath := fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName)
|
||||
|
||||
encryptedBackupSecretsAsBytes, err := os.ReadFile(encryptedBackupSecretsFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getUserCredsFromKeyRing: Something went wrong when unmarshalling user creds [err=%s]", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, backupSecret := range backedUpSecrets {
|
||||
if backupSecret.Environment == environment && backupSecret.ProjectID == workspace && backupSecret.SecretPath == secretsPath {
|
||||
return backupSecret.Secrets, nil
|
||||
}
|
||||
var encryptedBackUpSecrets models.SymmetricEncryptionResult
|
||||
err = json.Unmarshal(encryptedBackupSecretsAsBytes, &encryptedBackUpSecrets)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ReadBackupSecrets: unable to parse encrypted backup secrets. The secrets backup may be malformed [err=%s]", err)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
result, err := crypto.DecryptSymmetric(encryptionKey, encryptedBackUpSecrets.CipherText, encryptedBackUpSecrets.AuthTag, encryptedBackUpSecrets.Nonce)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ReadBackupSecrets: unable to decrypt encrypted backup secrets [err=%s]", err)
|
||||
}
|
||||
var plainTextSecrets []models.SingleEnvironmentVariable
|
||||
_ = json.Unmarshal(result, &plainTextSecrets)
|
||||
|
||||
return plainTextSecrets, nil
|
||||
|
||||
}
|
||||
|
||||
func DeleteBackupSecrets() error {
|
||||
// keeping this logic for now. Need to remove it later as more users migrate keyring would be used and this folder will be removed completely by then
|
||||
secrets_backup_folder_name := "secrets-backup"
|
||||
|
||||
_, fullConfigFileDirPath, err := GetFullConfigFilePath()
|
||||
@@ -549,8 +586,8 @@ func DeleteBackupSecrets() error {
|
||||
}
|
||||
|
||||
fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
|
||||
|
||||
DeleteValueInKeyring(INFISICAL_BACKUP_SECRET)
|
||||
DeleteValueInKeyring(INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY)
|
||||
|
||||
return os.RemoveAll(fullPathToSecretsBackupFolder)
|
||||
}
|
||||
|
@@ -11,11 +11,11 @@ func GetCurrentVaultBackend() (string, error) {
|
||||
}
|
||||
|
||||
if configFile.VaultBackendType == "" {
|
||||
return "auto", nil
|
||||
return VAULT_BACKEND_AUTO_MODE, nil
|
||||
}
|
||||
|
||||
if configFile.VaultBackendType != "auto" && configFile.VaultBackendType != "file" {
|
||||
return "auto", nil
|
||||
if configFile.VaultBackendType != VAULT_BACKEND_AUTO_MODE && configFile.VaultBackendType != VAULT_BACKEND_FILE_MODE {
|
||||
return VAULT_BACKEND_AUTO_MODE, nil
|
||||
}
|
||||
|
||||
return configFile.VaultBackendType, nil
|
||||
|
@@ -1,4 +1,4 @@
|
||||
Warning: Unable to fetch latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug
|
||||
Warning: Unable to fetch the latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug
|
||||
┌───────────────┬──────────────┬─────────────┐
|
||||
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
|
||||
├───────────────┼──────────────┼─────────────┤
|
||||
|
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/bradleyjkemp/cupaloy/v2"
|
||||
)
|
||||
|
||||
|
||||
func TestServiceToken_SecretsGetWithImportsAndRecursiveCmd(t *testing.T) {
|
||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent")
|
||||
|
||||
@@ -94,7 +93,7 @@ func TestUserAuth_SecretsGetAll(t *testing.T) {
|
||||
}
|
||||
|
||||
// explicitly called here because it should happen directly after successful secretsGetAll
|
||||
testUserAuth_SecretsGetAllWithoutConnection(t)
|
||||
// testUserAuth_SecretsGetAllWithoutConnection(t)
|
||||
}
|
||||
|
||||
func testUserAuth_SecretsGetAllWithoutConnection(t *testing.T) {
|
||||
@@ -107,7 +106,7 @@ func testUserAuth_SecretsGetAllWithoutConnection(t *testing.T) {
|
||||
// set it to a URL that will always be unreachable
|
||||
newConfigFile.LoggedInUserDomain = "http://localhost:4999"
|
||||
util.WriteConfigFile(&newConfigFile)
|
||||
|
||||
|
||||
// restore config file
|
||||
defer util.WriteConfigFile(&originalConfigFile)
|
||||
|
||||
@@ -121,4 +120,4 @@ func testUserAuth_SecretsGetAllWithoutConnection(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
15
company/handbook/meetings.mdx
Normal file
15
company/handbook/meetings.mdx
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: "Meetings"
|
||||
sidebarTitle: "Meetings"
|
||||
description: "The guide to meetings at Infisical."
|
||||
---
|
||||
|
||||
## "Let's schedule a meeting about this"
|
||||
|
||||
Being a remote-first company, we try to be as async as possible. When an issue arises, it's best to create a public Slack thread and tag all the necessary team members. Otherwise, if you were to "put a meeting on a calendar", the decision making process will inevitable slow down by at least a day (e.g., trying to find the right time for folks in different time zones is not always straightforward).
|
||||
|
||||
In other words, we have almost no (recurring) meetings and prefer written communication or quick Slack huddles.
|
||||
|
||||
## Weekly All-hands
|
||||
|
||||
All-hands is the single recurring meeting that we run every Monday at 8:30am PT. Typically, we would discuss everything important that happened during the previous week and plan out the week ahead. This is also an opportunity to bring up any important topics in front of the whole company (but feel free to post those in Slack too).
|
@@ -1,14 +1,16 @@
|
||||
---
|
||||
title: "Spenging Money"
|
||||
title: "Spending Money"
|
||||
sidebarTitle: "Spending Money"
|
||||
description: "The guide to spending money at Infisical."
|
||||
---
|
||||
|
||||
Fairly frequently, you might run into situations when you need to spend company money.
|
||||
|
||||
**Please spend money in a way that you think is in the best interest of the company.**
|
||||
<Note>
|
||||
Please spend money in a way that you think is in the best interest of the company.
|
||||
</Note>
|
||||
|
||||
## Trivial expenses
|
||||
# Trivial expenses
|
||||
|
||||
We don't want you to be slowed down because you're waiting for an approval to purchase some SaaS. For trivial expenses – **Just do it**.
|
||||
|
||||
@@ -22,6 +24,35 @@ Make sure you keep copies for all receipts. If you expense something on a compan
|
||||
|
||||
You should default to using your company card in all cases - it has no transaction fees. If using your personal card is unavoidable, please reach out to Maidul to get it reimbursed manually.
|
||||
|
||||
|
||||
# Equipment
|
||||
|
||||
Infisical is a remote first company so we understand the importance of having a comfortable work setup. To support this, we provide allowances for essential office equipment.
|
||||
|
||||
### Desk & Chair
|
||||
|
||||
Most people already have a comfortable desk and chair, but if you need an upgrade, we offer the following allowances.
|
||||
While we're not yet able to provide the latest and greatest, we strive to be reasonable given the stage of our company.
|
||||
|
||||
**Desk**: $150 USD
|
||||
|
||||
**Chair**: $150 USD
|
||||
|
||||
### Laptop
|
||||
Each team member will receive a company-issued Macbook Pro before they start their first day.
|
||||
|
||||
### Notes
|
||||
|
||||
1. All equipment purchased using company allowances remains the property of Infisical.
|
||||
2. Keep all receipts for equipment purchases and submit them for reimbursement.
|
||||
3. If you leave Infisical, you may be required to return company-owned equipment.
|
||||
|
||||
Please note that we're unable to offer a split payment option where the Infisical pays half and you pay half for equipment exceeding the allowance.
|
||||
This is because we don't yet have a formal HR department to handle such logistics.
|
||||
|
||||
For any equipment related questions, please reach out to Maidul.
|
||||
|
||||
|
||||
## Brex
|
||||
|
||||
We use Brex as our primary credit card provider. Don't have a company card yet? Reach out to Maidul.
|
@@ -59,7 +59,8 @@
|
||||
"handbook/onboarding",
|
||||
"handbook/spending-money",
|
||||
"handbook/time-off",
|
||||
"handbook/hiring"
|
||||
"handbook/hiring",
|
||||
"handbook/meetings"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create Lease"
|
||||
openapi: "POST /api/v1/dynamic-secrets/leases"
|
||||
---
|
4
docs/api-reference/endpoints/dynamic-secrets/create.mdx
Normal file
4
docs/api-reference/endpoints/dynamic-secrets/create.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/dynamic-secrets"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete Lease"
|
||||
openapi: "DELETE /api/v1/dynamic-secrets/leases/{leaseId}"
|
||||
---
|
4
docs/api-reference/endpoints/dynamic-secrets/delete.mdx
Normal file
4
docs/api-reference/endpoints/dynamic-secrets/delete.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/dynamic-secrets/{name}"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get Lease"
|
||||
openapi: "GET /api/v1/dynamic-secrets/leases/{leaseId}"
|
||||
---
|
4
docs/api-reference/endpoints/dynamic-secrets/get.mdx
Normal file
4
docs/api-reference/endpoints/dynamic-secrets/get.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get"
|
||||
openapi: "GET /api/v1/dynamic-secrets/{name}"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List Leases"
|
||||
openapi: "GET /api/v1/dynamic-secrets/{name}/leases"
|
||||
---
|
4
docs/api-reference/endpoints/dynamic-secrets/list.mdx
Normal file
4
docs/api-reference/endpoints/dynamic-secrets/list.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/dynamic-secrets"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Renew Lease"
|
||||
openapi: "POST /api/v1/dynamic-secrets/leases/{leaseId}/renew"
|
||||
---
|
4
docs/api-reference/endpoints/dynamic-secrets/update.mdx
Normal file
4
docs/api-reference/endpoints/dynamic-secrets/update.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/dynamic-secrets/{name}"
|
||||
---
|
@@ -30,8 +30,5 @@ description: "Change the vault type in Infisical"
|
||||
|
||||
## Description
|
||||
|
||||
To safeguard your login details when using the CLI, Infisical places them in a system vault or an encrypted text file, protected by a passphrase that only the user knows.
|
||||
|
||||
<Tip>To avoid constantly entering your passphrase when using the `file` vault type, set the `INFISICAL_VAULT_FILE_PASSPHRASE` environment variable with your password in your shell</Tip>
|
||||
|
||||
To safeguard your login details when using the CLI, Infisical attempts to store them in a system keyring. If a system keyring cannot be found on your machine, the data is stored in a config file.
|
||||
|
||||
|
@@ -16,7 +16,7 @@ Before you begin, you'll first need to choose a method of authentication with AW
|
||||
<Steps>
|
||||
<Step title="Create the Managing User IAM Role">
|
||||
1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console.
|
||||

|
||||

|
||||
|
||||
2. Select **AWS Account** as the **Trusted Entity Type**.
|
||||
3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If you are self-hosting, provide the AWS account number where Infisical is hosted.
|
||||
|
@@ -4,10 +4,10 @@ description: "Learn how to configure Google SAML for Infisical SSO."
|
||||
---
|
||||
|
||||
<Info>
|
||||
Google SAML SSO feature is a paid feature.
|
||||
|
||||
If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical,
|
||||
then you should contact sales@infisical.com to purchase an enterprise license to use it.
|
||||
Google SAML SSO feature is a paid feature. If you're using Infisical Cloud,
|
||||
then it is available under the **Pro Tier**. If you're self-hosting Infisical,
|
||||
then you should contact sales@infisical.com to purchase an enterprise license
|
||||
to use it.
|
||||
</Info>
|
||||
|
||||
<Steps>
|
||||
@@ -15,8 +15,9 @@ description: "Learn how to configure Google SAML for Infisical SSO."
|
||||
In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**.
|
||||
|
||||
Next, note the **ACS URL** and **SP Entity ID** to use when configuring the Google SAML application.
|
||||
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
<Step title="Create a SAML application in Google">
|
||||
2.1. In your [Google Admin console](https://support.google.com/a/answer/182076), head to Menu > Apps > Web and mobile apps and
|
||||
@@ -32,7 +33,7 @@ description: "Learn how to configure Google SAML for Infisical SSO."
|
||||
|
||||

|
||||
|
||||
2.4. Back in Infisical, set **SSO URL**, **IdP Entity ID**, and **Certificate** to the corresponding items from step 2.3.
|
||||
2.4. Back in Infisical, set **SSO URL** and **Certificate** to the corresponding items from step 2.3.
|
||||
|
||||

|
||||
|
||||
@@ -41,7 +42,7 @@ description: "Learn how to configure Google SAML for Infisical SSO."
|
||||
Also, check the **Signed response** checkbox.
|
||||
|
||||

|
||||
|
||||
|
||||
2.6. In the **Attribute mapping** tab, configure the following map:
|
||||
|
||||
- **First name** -> **firstName**
|
||||
@@ -49,7 +50,7 @@ description: "Learn how to configure Google SAML for Infisical SSO."
|
||||
- **Primary email** -> **email**
|
||||
|
||||

|
||||
|
||||
|
||||
Click **Finish**.
|
||||
</Step>
|
||||
<Step title="Assign users in Google Workspace to the application">
|
||||
@@ -57,11 +58,11 @@ description: "Learn how to configure Google SAML for Infisical SSO."
|
||||
and press on **User access**.
|
||||
|
||||

|
||||
|
||||
|
||||
To assign everyone in your organization to the application, click **On for everyone** or **Off for everyone** and then click **Save**.
|
||||
|
||||
|
||||
You can also assign an organizational unit or set of users to an application; you can learn more about that [here](https://support.google.com/a/answer/6087519?hl=en#add_custom_saml&turn_on&verify_sso&&zippy=%2Cstep-add-the-custom-saml-app%2Cstep-turn-on-your-saml-app%2Cstep-verify-that-sso-is-working-with-your-custom-app).
|
||||
|
||||
|
||||

|
||||
</Step>
|
||||
<Step title="Enable SAML SSO in Infisical">
|
||||
@@ -75,21 +76,24 @@ description: "Learn how to configure Google SAML for Infisical SSO."
|
||||
|
||||
To enforce SAML SSO, you're required to test out the SAML connection by successfully authenticating at least one Google user with Infisical;
|
||||
Once you've completed this requirement, you can toggle the **Enforce SAML SSO** button to enforce SAML SSO.
|
||||
|
||||
|
||||
<Warning>
|
||||
We recommend ensuring that your account is provisioned the application in Google
|
||||
prior to enforcing SAML SSO to prevent any unintended issues.
|
||||
</Warning>
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
<Note>
|
||||
If you're configuring SAML SSO on a self-hosted instance of Infisical, make sure to
|
||||
set the `AUTH_SECRET` and `SITE_URL` environment variable for it to work:
|
||||
|
||||
- `AUTH_SECRET`: A secret key used for signing and verifying JWT. This can be a random 32-byte base64 string generated with `openssl rand -base64 32`.
|
||||
- `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com)
|
||||
If you're configuring SAML SSO on a self-hosted instance of Infisical, make
|
||||
sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to
|
||||
work: - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This
|
||||
can be a random 32-byte base64 string generated with `openssl rand -base64
|
||||
32`. - `SITE_URL`: The URL of your self-hosted instance of Infisical - should
|
||||
be an absolute URL including the protocol (e.g. https://app.infisical.com)
|
||||
</Note>
|
||||
|
||||
References:
|
||||
- Google's guide to [set up your own custom SAML app](https://support.google.com/a/answer/6087519?hl=en#add_custom_saml&turn_on&verify_sso&&zippy=%2Cstep-add-the-custom-saml-app%2Cstep-turn-on-your-saml-app%2Cstep-verify-that-sso-is-working-with-your-custom-app).
|
||||
|
||||
- Google's guide to [set up your own custom SAML app](https://support.google.com/a/answer/6087519?hl=en#add_custom_saml&turn_on&verify_sso&&zippy=%2Cstep-add-the-custom-saml-app%2Cstep-turn-on-your-saml-app%2Cstep-verify-that-sso-is-working-with-your-custom-app).
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 605 KiB After Width: | Height: | Size: 219 KiB |
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "Kubernetes"
|
||||
title: "Kubernetes Operator"
|
||||
description: "How to use Infisical to inject secrets into Kubernetes clusters."
|
||||
---
|
||||
|
||||
@@ -9,6 +9,10 @@ The Infisical Secrets Operator is a Kubernetes controller that retrieves secrets
|
||||
It uses an `InfisicalSecret` resource to specify authentication and storage methods.
|
||||
The operator continuously updates secrets and can also reload dependent deployments automatically.
|
||||
|
||||
<Note>
|
||||
If you are already using the External Secrets operator, you can view the integration documentation for it [here](https://external-secrets.io/latest/provider/infisical/).
|
||||
</Note>
|
||||
|
||||
## Install Operator
|
||||
|
||||
The operator can be install via [Helm](https://helm.sh) or [kubectl](https://github.com/kubernetes/kubectl)
|
||||
|
@@ -155,7 +155,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Key Management",
|
||||
"group": "Key Management (KMS)",
|
||||
"pages": [
|
||||
"documentation/platform/kms/overview",
|
||||
"documentation/platform/kms/aws-kms",
|
||||
@@ -617,6 +617,21 @@
|
||||
"api-reference/endpoints/secrets/detach-tags"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Dynamic Secrets",
|
||||
"pages": [
|
||||
"api-reference/endpoints/dynamic-secrets/create",
|
||||
"api-reference/endpoints/dynamic-secrets/update",
|
||||
"api-reference/endpoints/dynamic-secrets/delete",
|
||||
"api-reference/endpoints/dynamic-secrets/get",
|
||||
"api-reference/endpoints/dynamic-secrets/list",
|
||||
"api-reference/endpoints/dynamic-secrets/list-leases",
|
||||
"api-reference/endpoints/dynamic-secrets/create-lease",
|
||||
"api-reference/endpoints/dynamic-secrets/delete-lease",
|
||||
"api-reference/endpoints/dynamic-secrets/renew-lease",
|
||||
"api-reference/endpoints/dynamic-secrets/get-lease"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Secret Imports",
|
||||
"pages": [
|
||||
|
@@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import { faCheck } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -87,7 +88,13 @@ type Props = {
|
||||
};
|
||||
|
||||
const createTagSchema = z.object({
|
||||
name: z.string().trim(),
|
||||
slug: z
|
||||
.string()
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
|
||||
}),
|
||||
color: z.string().trim()
|
||||
});
|
||||
|
||||
@@ -110,7 +117,7 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(createTagSchema)
|
||||
});
|
||||
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
|
||||
@@ -123,13 +130,12 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
|
||||
if (!isOpen) reset();
|
||||
}, [isOpen]);
|
||||
|
||||
const onFormSubmit = async ({ name, color }: FormData) => {
|
||||
const onFormSubmit = async ({ slug, color }: FormData) => {
|
||||
try {
|
||||
await createWsTag({
|
||||
workspaceID: workspaceId,
|
||||
tagName: name,
|
||||
tagColor: color,
|
||||
tagSlug: name.replace(" ", "_")
|
||||
tagSlug: slug
|
||||
});
|
||||
onToggle(false);
|
||||
reset();
|
||||
@@ -155,11 +161,11 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
name="slug"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Tag Name" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="Type your tag name" />
|
||||
<FormControl label="Tag Slug" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="Type your tag slug" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
@@ -16,6 +16,7 @@ type Props = {
|
||||
subTitle?: string;
|
||||
onDeleteApproved: () => Promise<void>;
|
||||
buttonText?: string;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const DeleteActionModal = ({
|
||||
@@ -26,7 +27,8 @@ export const DeleteActionModal = ({
|
||||
onDeleteApproved,
|
||||
title,
|
||||
subTitle = "This action is irreversible.",
|
||||
buttonText = "Delete"
|
||||
buttonText = "Delete",
|
||||
children
|
||||
}: Props): JSX.Element => {
|
||||
const [inputData, setInputData] = useState("");
|
||||
const [isLoading, setIsLoading] = useToggle();
|
||||
@@ -94,9 +96,10 @@ export const DeleteActionModal = ({
|
||||
<Input
|
||||
value={inputData}
|
||||
onChange={(e) => setInputData(e.target.value)}
|
||||
placeholder="Type confirm..."
|
||||
placeholder={`Type ${deleteKey} here`}
|
||||
/>
|
||||
</FormControl>
|
||||
{children}
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
@@ -50,7 +50,7 @@ export const Pagination = ({
|
||||
>
|
||||
<div className="mr-6 flex items-center space-x-2">
|
||||
<div className="text-xs">
|
||||
{(page - 1) * perPage} - {(page - 1) * perPage + perPage} of {count}
|
||||
{(page - 1) * perPage} - {Math.min((page - 1) * perPage + perPage, count)} of {count}
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
@@ -20,7 +20,12 @@ export enum OrgPermissionSubjects {
|
||||
Billing = "billing",
|
||||
SecretScanning = "secret-scanning",
|
||||
Identity = "identity",
|
||||
Kms = "kms"
|
||||
Kms = "kms",
|
||||
AdminConsole = "organization-admin-console"
|
||||
}
|
||||
|
||||
export enum OrgPermissionAdminConsoleAction {
|
||||
AccessAllProjects = "access-all-projects"
|
||||
}
|
||||
|
||||
export type OrgPermissionSet =
|
||||
@@ -37,6 +42,7 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms];
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||
|
||||
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
|
||||
|
14
frontend/src/helpers/parseEnvVar.ts
Normal file
14
frontend/src/helpers/parseEnvVar.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/** Extracts the key and value from a passed in env string based on the provided delimiters. */
|
||||
export const getKeyValue = (pastedContent: string, delimiters: string[]) => {
|
||||
const foundDelimiter = delimiters.find((delimiter) => pastedContent.includes(delimiter));
|
||||
|
||||
if (!foundDelimiter) {
|
||||
return { key: pastedContent.trim(), value: "" };
|
||||
}
|
||||
|
||||
const [key, value] = pastedContent.split(foundDelimiter);
|
||||
return {
|
||||
key: key.trim(),
|
||||
value: (value ?? "").trim()
|
||||
};
|
||||
};
|
@@ -56,7 +56,8 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.GET_CERT]: "Get certificate",
|
||||
[EventType.DELETE_CERT]: "Delete certificate",
|
||||
[EventType.REVOKE_CERT]: "Revoke certificate",
|
||||
[EventType.GET_CERT_BODY]: "Get certificate body"
|
||||
[EventType.GET_CERT_BODY]: "Get certificate body",
|
||||
[EventType.ORG_ADMIN_ACCESS_PROJECT]: "Org admin accessed project"
|
||||
};
|
||||
|
||||
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
|
||||
|
@@ -70,5 +70,6 @@ export enum EventType {
|
||||
GET_CERT = "get-cert",
|
||||
DELETE_CERT = "delete-cert",
|
||||
REVOKE_CERT = "revoke-cert",
|
||||
GET_CERT_BODY = "get-cert-body"
|
||||
GET_CERT_BODY = "get-cert-body",
|
||||
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project"
|
||||
}
|
||||
|
@@ -579,6 +579,16 @@ interface GetCertBody {
|
||||
};
|
||||
}
|
||||
|
||||
interface OrgAdminAccessProjectEvent {
|
||||
type: EventType.ORG_ADMIN_ACCESS_PROJECT;
|
||||
metadata: {
|
||||
userId: string;
|
||||
username: string;
|
||||
email: string;
|
||||
projectId: string;
|
||||
}; // no metadata yet
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@@ -635,7 +645,8 @@ export type Event =
|
||||
| GetCert
|
||||
| DeleteCert
|
||||
| RevokeCert
|
||||
| GetCertBody;
|
||||
| GetCertBody
|
||||
| OrgAdminAccessProjectEvent;
|
||||
|
||||
export type AuditLog = {
|
||||
id: string;
|
||||
|
@@ -19,6 +19,7 @@ export * from "./keys";
|
||||
export * from "./kms";
|
||||
export * from "./ldapConfig";
|
||||
export * from "./oidcConfig";
|
||||
export * from "./orgAdmin";
|
||||
export * from "./organization";
|
||||
export * from "./projectUserAdditionalPrivilege";
|
||||
export * from "./rateLimit";
|
||||
|
@@ -110,8 +110,15 @@ export const useCreateIntegration = () => {
|
||||
export const useDeleteIntegration = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, { id: string; workspaceId: string }>({
|
||||
mutationFn: ({ id }) => apiRequest.delete(`/api/v1/integration/${id}`),
|
||||
return useMutation<
|
||||
{},
|
||||
{},
|
||||
{ id: string; workspaceId: string; shouldDeleteIntegrationSecrets: boolean }
|
||||
>({
|
||||
mutationFn: ({ id, shouldDeleteIntegrationSecrets }) =>
|
||||
apiRequest.delete(
|
||||
`/api/v1/integration/${id}?shouldDeleteIntegrationSecrets=${shouldDeleteIntegrationSecrets}`
|
||||
),
|
||||
onSuccess: (_, { workspaceId }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId));
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceAuthorization(workspaceId));
|
||||
|
2
frontend/src/hooks/api/orgAdmin/index.tsx
Normal file
2
frontend/src/hooks/api/orgAdmin/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export { useOrgAdminAccessProject } from "./mutation";
|
||||
export { useOrgAdminGetProjects } from "./queries";
|
15
frontend/src/hooks/api/orgAdmin/mutation.tsx
Normal file
15
frontend/src/hooks/api/orgAdmin/mutation.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TOrgAdminAccessProjectDTO } from "./types";
|
||||
|
||||
export const useOrgAdminAccessProject = () =>
|
||||
useMutation({
|
||||
mutationFn: async ({ projectId }: TOrgAdminAccessProjectDTO) => {
|
||||
const { data } = await apiRequest.post(
|
||||
`/api/v1/organization-admin/projects/${projectId}/grant-admin-access`
|
||||
);
|
||||
return data;
|
||||
}
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user