mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-13 09:35:39 +00:00
Compare commits
155 Commits
infisical/
...
gcp-sync-h
Author | SHA1 | Date | |
---|---|---|---|
48943b4d78 | |||
fd1afc2cbe | |||
5ebf142e3e | |||
bdceea4c91 | |||
32fa6866e4 | |||
b4faef797c | |||
08732cab62 | |||
81d5f639ae | |||
25b83d4b86 | |||
a500f00a49 | |||
6842f7aa8b | |||
ad207786e2 | |||
ace8c37c25 | |||
4c82408b51 | |||
8146dcef16 | |||
2e90addbc5 | |||
427201a634 | |||
0b55ac141c | |||
aecfa268ae | |||
fdfc020efc | |||
62aa80a104 | |||
cf9d8035bd | |||
d0c9f1ca53 | |||
2ecc7424d9 | |||
c04b97c689 | |||
7600a86dfc | |||
8924eaf251 | |||
82e9504285 | |||
c4e10df754 | |||
ce60e96008 | |||
930b59cb4f | |||
ec363a5ad4 | |||
de7e92ccfc | |||
522d81ae1a | |||
02153ffb32 | |||
d9d62384e7 | |||
76f34501dc | |||
7415bb93b8 | |||
7a1c08a7f2 | |||
84f9eb5f9f | |||
87ac723fcb | |||
a6dab47552 | |||
08bac83bcc | |||
46c90f03f0 | |||
d7722f7587 | |||
a42bcb3393 | |||
192dba04a5 | |||
0cc3240956 | |||
667580546b | |||
9fd662b7f7 | |||
a56cbbc02f | |||
dc30465afb | |||
f1caab2d00 | |||
1d186b1950 | |||
9cf5908cc1 | |||
f1b6c3764f | |||
4e6c860c69 | |||
eda9ed257e | |||
38cf43176e | |||
f5c7943f2f | |||
3c59f7f350 | |||
84cc7bcd6c | |||
159c27ac67 | |||
de5a432745 | |||
387780aa94 | |||
3887ce800b | |||
1a06b3e1f5 | |||
5f0dd31334 | |||
7e14c58931 | |||
627e17b3ae | |||
39b7a4a111 | |||
e7c512999e | |||
c19016e6e6 | |||
20477ce2b0 | |||
e04b2220be | |||
edf6a37fe5 | |||
f5749e326a | |||
75e0a68b68 | |||
4dc56033b1 | |||
ed37b99756 | |||
6fa41a609b | |||
c9da8477c8 | |||
5e4b478b74 | |||
765be2d99d | |||
719a18c218 | |||
16d3bbb67a | |||
872a3fe48d | |||
c7414e00f9 | |||
ad1dd55b8b | |||
497761a0e5 | |||
483fb458dd | |||
17cf602a65 | |||
23f6f5dfd4 | |||
b9b76579ac | |||
761965696b | |||
ace2500885 | |||
4eff7d8ea5 | |||
c4512ae111 | |||
78c349c09a | |||
09df440613 | |||
a8fc0e540a | |||
46ce46b5a0 | |||
dc88115d43 | |||
955657e172 | |||
f1ba64aa66 | |||
d74197aeb4 | |||
97567d06d4 | |||
3986df8e8a | |||
3fcd84b592 | |||
29e39b558b | |||
9458c8b04f | |||
3b95c5d859 | |||
de8f315211 | |||
9960d58e1b | |||
0057404562 | |||
47ca1b3011 | |||
716cd090c4 | |||
e870bb3ade | |||
98c9e98082 | |||
a814f459ab | |||
66817a40db | |||
20bd2ca71c | |||
004a8b71a2 | |||
f0fce3086e | |||
a9e7db6fc0 | |||
2bd681d58f | |||
51fef3ce60 | |||
df9e7bf6ee | |||
04479bb70a | |||
cdc90411e5 | |||
dcb05a3093 | |||
b055cda64d | |||
f68602280e | |||
f9483afe95 | |||
d742534f6a | |||
99eb8eb8ed | |||
1dea024880 | |||
699e03c1a9 | |||
f6372249b4 | |||
0f42fcd688 | |||
ada04ed4fc | |||
795d9e4413 | |||
67f2e4671a | |||
214f837041 | |||
c48c9ae628 | |||
7003ad608a | |||
104edca6f1 | |||
75345d91c0 | |||
abc2ffca57 | |||
c54eafc128 | |||
757942aefc | |||
1d57629036 | |||
8061066e27 | |||
c993b1bbe3 | |||
2cbf33ac14 |
3
.envrc
Normal file
3
.envrc
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Learn more at https://direnv.net
|
||||||
|
# We instruct direnv to use our Nix flake for a consistent development environment.
|
||||||
|
use flake
|
@ -92,7 +92,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
- name: Install openapi-diff
|
- name: Install openapi-diff
|
||||||
run: go install github.com/tufin/oasdiff@latest
|
run: go install github.com/oasdiff/oasdiff@latest
|
||||||
- name: Running OpenAPI Spec diff action
|
- name: Running OpenAPI Spec diff action
|
||||||
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
|
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
|
||||||
- name: cleanup
|
- name: cleanup
|
||||||
|
8
.github/workflows/run-backend-tests.yml
vendored
8
.github/workflows/run-backend-tests.yml
vendored
@ -34,7 +34,10 @@ jobs:
|
|||||||
working-directory: backend
|
working-directory: backend
|
||||||
- name: Start postgres and redis
|
- name: Start postgres and redis
|
||||||
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
|
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
|
||||||
- name: Start integration test
|
- name: Run unit test
|
||||||
|
run: npm run test:unit
|
||||||
|
working-directory: backend
|
||||||
|
- name: Run integration test
|
||||||
run: npm run test:e2e
|
run: npm run test:e2e
|
||||||
working-directory: backend
|
working-directory: backend
|
||||||
env:
|
env:
|
||||||
@ -44,4 +47,5 @@ jobs:
|
|||||||
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
|
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
|
||||||
- name: cleanup
|
- name: cleanup
|
||||||
run: |
|
run: |
|
||||||
docker compose -f "docker-compose.dev.yml" down
|
docker compose -f "docker-compose.dev.yml" down
|
||||||
|
|
||||||
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -1,3 +1,5 @@
|
|||||||
|
.direnv/
|
||||||
|
|
||||||
# backend
|
# backend
|
||||||
node_modules
|
node_modules
|
||||||
.env
|
.env
|
||||||
@ -26,8 +28,6 @@ node_modules
|
|||||||
/.pnp
|
/.pnp
|
||||||
.pnp.js
|
.pnp.js
|
||||||
|
|
||||||
.env
|
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
coverage
|
coverage
|
||||||
reports
|
reports
|
||||||
@ -63,10 +63,12 @@ yarn-error.log*
|
|||||||
|
|
||||||
# Editor specific
|
# Editor specific
|
||||||
.vscode/*
|
.vscode/*
|
||||||
.idea/*
|
**/.idea/*
|
||||||
|
|
||||||
frontend-build
|
frontend-build
|
||||||
|
|
||||||
|
# cli
|
||||||
|
.go/
|
||||||
*.tgz
|
*.tgz
|
||||||
cli/infisical-merge
|
cli/infisical-merge
|
||||||
cli/test/infisical-merge
|
cli/test/infisical-merge
|
||||||
|
@ -120,4 +120,3 @@ export default {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
"type:check": "tsc --noEmit",
|
"type:check": "tsc --noEmit",
|
||||||
"lint:fix": "eslint --fix --ext js,ts ./src",
|
"lint:fix": "eslint --fix --ext js,ts ./src",
|
||||||
"lint": "eslint 'src/**/*.ts'",
|
"lint": "eslint 'src/**/*.ts'",
|
||||||
|
"test:unit": "vitest run -c vitest.unit.config.ts",
|
||||||
"test:e2e": "vitest run -c vitest.e2e.config.ts --bail=1",
|
"test:e2e": "vitest run -c vitest.e2e.config.ts --bail=1",
|
||||||
"test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1",
|
"test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1",
|
||||||
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
|
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
|
||||||
@ -70,6 +71,7 @@
|
|||||||
"migrate:org": "tsx ./scripts/migrate-organization.ts",
|
"migrate:org": "tsx ./scripts/migrate-organization.ts",
|
||||||
"seed:new": "tsx ./scripts/create-seed-file.ts",
|
"seed:new": "tsx ./scripts/create-seed-file.ts",
|
||||||
"seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run",
|
"seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run",
|
||||||
|
"seed-dev": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
|
||||||
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest"
|
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
|
@ -1,16 +1,11 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import {
|
import { SecretApprovalRequestsReviewersSchema, SecretApprovalRequestsSchema, UsersSchema } from "@app/db/schemas";
|
||||||
SecretApprovalRequestsReviewersSchema,
|
|
||||||
SecretApprovalRequestsSchema,
|
|
||||||
SecretTagsSchema,
|
|
||||||
UsersSchema
|
|
||||||
} from "@app/db/schemas";
|
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
|
import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||||
|
|
||||||
@ -250,14 +245,6 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const tagSchema = SecretTagsSchema.pick({
|
|
||||||
id: true,
|
|
||||||
slug: true,
|
|
||||||
color: true
|
|
||||||
})
|
|
||||||
.array()
|
|
||||||
.optional();
|
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:id",
|
url: "/:id",
|
||||||
@ -291,7 +278,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
.omit({ _id: true, environment: true, workspace: true, type: true, version: true })
|
.omit({ _id: true, environment: true, workspace: true, type: true, version: true })
|
||||||
.extend({
|
.extend({
|
||||||
op: z.string(),
|
op: z.string(),
|
||||||
tags: tagSchema,
|
tags: SanitizedTagSchema.array().optional(),
|
||||||
secretMetadata: ResourceMetadataSchema.nullish(),
|
secretMetadata: ResourceMetadataSchema.nullish(),
|
||||||
secret: z
|
secret: z
|
||||||
.object({
|
.object({
|
||||||
@ -310,7 +297,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
secretKey: z.string(),
|
secretKey: z.string(),
|
||||||
secretValue: z.string().optional(),
|
secretValue: z.string().optional(),
|
||||||
secretComment: z.string().optional(),
|
secretComment: z.string().optional(),
|
||||||
tags: tagSchema,
|
tags: SanitizedTagSchema.array().optional(),
|
||||||
secretMetadata: ResourceMetadataSchema.nullish()
|
secretMetadata: ResourceMetadataSchema.nullish()
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
import { ProjectPermissionActions } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
|
||||||
import { RAW_SECRETS } from "@app/lib/api-docs";
|
import { RAW_SECRETS } from "@app/lib/api-docs";
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
@ -9,7 +9,7 @@ import { AuthMode } from "@app/services/auth/auth-type";
|
|||||||
|
|
||||||
const AccessListEntrySchema = z
|
const AccessListEntrySchema = z
|
||||||
.object({
|
.object({
|
||||||
allowedActions: z.nativeEnum(ProjectPermissionActions).array(),
|
allowedActions: z.nativeEnum(ProjectPermissionSecretActions).array(),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
membershipId: z.string(),
|
membershipId: z.string(),
|
||||||
name: z.string()
|
name: z.string()
|
||||||
|
@ -22,7 +22,11 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
secretVersions: secretRawSchema.array()
|
secretVersions: secretRawSchema
|
||||||
|
.extend({
|
||||||
|
secretValueHidden: z.boolean()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -37,6 +41,7 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
|
|||||||
offset: req.query.offset,
|
offset: req.query.offset,
|
||||||
secretId: req.params.secretId
|
secretId: req.params.secretId
|
||||||
});
|
});
|
||||||
|
|
||||||
return { secretVersions };
|
return { secretVersions };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SecretSnapshotsSchema, SecretTagsSchema } from "@app/db/schemas";
|
import { SecretSnapshotsSchema } from "@app/db/schemas";
|
||||||
import { PROJECTS } from "@app/lib/api-docs";
|
import { PROJECTS } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
||||||
@ -31,12 +31,9 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
|||||||
secretVersions: secretRawSchema
|
secretVersions: secretRawSchema
|
||||||
.omit({ _id: true, environment: true, workspace: true, type: true })
|
.omit({ _id: true, environment: true, workspace: true, type: true })
|
||||||
.extend({
|
.extend({
|
||||||
|
secretValueHidden: z.boolean(),
|
||||||
secretId: z.string(),
|
secretId: z.string(),
|
||||||
tags: SecretTagsSchema.pick({
|
tags: SanitizedTagSchema.array()
|
||||||
id: true,
|
|
||||||
slug: true,
|
|
||||||
color: true
|
|
||||||
}).array()
|
|
||||||
})
|
})
|
||||||
.array(),
|
.array(),
|
||||||
folderVersion: z.object({ id: z.string(), name: z.string() }).array(),
|
folderVersion: z.object({ id: z.string(), name: z.string() }).array(),
|
||||||
@ -55,6 +52,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
id: req.params.secretSnapshotId
|
id: req.params.secretSnapshotId
|
||||||
});
|
});
|
||||||
|
|
||||||
return { secretSnapshot };
|
return { secretSnapshot };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -2,6 +2,7 @@ import slugify from "@sindresorhus/slugify";
|
|||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
|
||||||
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
||||||
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types";
|
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types";
|
||||||
import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
||||||
@ -23,7 +24,9 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
|
|||||||
body: z.object({
|
body: z.object({
|
||||||
projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId),
|
projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId),
|
||||||
slug: slugSchema({ min: 1, max: 60 }).optional().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
slug: slugSchema({ min: 1, max: 60 }).optional().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
||||||
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions),
|
permissions: ProjectPermissionV2Schema.array()
|
||||||
|
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions)
|
||||||
|
.refine(checkForInvalidPermissionCombination),
|
||||||
type: z.discriminatedUnion("isTemporary", [
|
type: z.discriminatedUnion("isTemporary", [
|
||||||
z.object({
|
z.object({
|
||||||
isTemporary: z.literal(false)
|
isTemporary: z.literal(false)
|
||||||
@ -81,7 +84,8 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
|
|||||||
slug: slugSchema({ min: 1, max: 60 }).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
|
slug: slugSchema({ min: 1, max: 60 }).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
|
||||||
permissions: ProjectPermissionV2Schema.array()
|
permissions: ProjectPermissionV2Schema.array()
|
||||||
.optional()
|
.optional()
|
||||||
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
|
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions)
|
||||||
|
.refine(checkForInvalidPermissionCombination),
|
||||||
type: z.discriminatedUnion("isTemporary", [
|
type: z.discriminatedUnion("isTemporary", [
|
||||||
z.object({ isTemporary: z.literal(false).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary) }),
|
z.object({ isTemporary: z.literal(false).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary) }),
|
||||||
z.object({
|
z.object({
|
||||||
|
@ -3,6 +3,7 @@ import ms from "ms";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types";
|
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types";
|
||||||
|
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
|
||||||
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
||||||
import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs";
|
import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
@ -30,7 +31,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.identityId),
|
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.identityId),
|
||||||
projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.projectId),
|
projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.projectId),
|
||||||
slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug),
|
slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug),
|
||||||
permissions: ProjectPermissionV2Schema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission),
|
permissions: ProjectPermissionV2Schema.array()
|
||||||
|
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission)
|
||||||
|
.refine(checkForInvalidPermissionCombination),
|
||||||
type: z.discriminatedUnion("isTemporary", [
|
type: z.discriminatedUnion("isTemporary", [
|
||||||
z.object({
|
z.object({
|
||||||
isTemporary: z.literal(false)
|
isTemporary: z.literal(false)
|
||||||
@ -94,7 +97,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug),
|
slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug),
|
||||||
permissions: ProjectPermissionV2Schema.array()
|
permissions: ProjectPermissionV2Schema.array()
|
||||||
.optional()
|
.optional()
|
||||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission),
|
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission)
|
||||||
|
.refine(checkForInvalidPermissionCombination),
|
||||||
type: z.discriminatedUnion("isTemporary", [
|
type: z.discriminatedUnion("isTemporary", [
|
||||||
z.object({ isTemporary: z.literal(false).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.isTemporary) }),
|
z.object({ isTemporary: z.literal(false).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.isTemporary) }),
|
||||||
z.object({
|
z.object({
|
||||||
|
@ -2,6 +2,7 @@ import { packRules } from "@casl/ability/extra";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
|
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
|
||||||
|
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
|
||||||
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
||||||
import { PROJECT_ROLE } from "@app/lib/api-docs";
|
import { PROJECT_ROLE } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
@ -37,7 +38,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
.describe(PROJECT_ROLE.CREATE.slug),
|
.describe(PROJECT_ROLE.CREATE.slug),
|
||||||
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
||||||
description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description),
|
description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description),
|
||||||
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
|
permissions: ProjectPermissionV2Schema.array()
|
||||||
|
.describe(PROJECT_ROLE.CREATE.permissions)
|
||||||
|
.refine(checkForInvalidPermissionCombination)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@ -92,7 +95,10 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
.describe(PROJECT_ROLE.UPDATE.slug),
|
.describe(PROJECT_ROLE.UPDATE.slug),
|
||||||
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
||||||
description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description),
|
description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description),
|
||||||
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
permissions: ProjectPermissionV2Schema.array()
|
||||||
|
.describe(PROJECT_ROLE.UPDATE.permissions)
|
||||||
|
.optional()
|
||||||
|
.superRefine(checkForInvalidPermissionCombination)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
@ -1,5 +1,16 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export type PasswordRequirements = {
|
||||||
|
length: number;
|
||||||
|
required: {
|
||||||
|
lowercase: number;
|
||||||
|
uppercase: number;
|
||||||
|
digits: number;
|
||||||
|
symbols: number;
|
||||||
|
};
|
||||||
|
allowedSymbols?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export enum SqlProviders {
|
export enum SqlProviders {
|
||||||
Postgres = "postgres",
|
Postgres = "postgres",
|
||||||
MySQL = "mysql2",
|
MySQL = "mysql2",
|
||||||
@ -100,6 +111,28 @@ export const DynamicSecretSqlDBSchema = z.object({
|
|||||||
database: z.string().trim(),
|
database: z.string().trim(),
|
||||||
username: z.string().trim(),
|
username: z.string().trim(),
|
||||||
password: z.string().trim(),
|
password: z.string().trim(),
|
||||||
|
passwordRequirements: z
|
||||||
|
.object({
|
||||||
|
length: z.number().min(1).max(250),
|
||||||
|
required: z
|
||||||
|
.object({
|
||||||
|
lowercase: z.number().min(0),
|
||||||
|
uppercase: z.number().min(0),
|
||||||
|
digits: z.number().min(0),
|
||||||
|
symbols: z.number().min(0)
|
||||||
|
})
|
||||||
|
.refine((data) => {
|
||||||
|
const total = Object.values(data).reduce((sum, count) => sum + count, 0);
|
||||||
|
return total <= 250;
|
||||||
|
}, "Sum of required characters cannot exceed 250"),
|
||||||
|
allowedSymbols: z.string().optional()
|
||||||
|
})
|
||||||
|
.refine((data) => {
|
||||||
|
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
|
||||||
|
return total <= data.length;
|
||||||
|
}, "Sum of required characters cannot exceed the total length")
|
||||||
|
.optional()
|
||||||
|
.describe("Password generation requirements"),
|
||||||
creationStatement: z.string().trim(),
|
creationStatement: z.string().trim(),
|
||||||
revocationStatement: z.string().trim(),
|
revocationStatement: z.string().trim(),
|
||||||
renewStatement: z.string().trim().optional(),
|
renewStatement: z.string().trim().optional(),
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
import { randomInt } from "crypto";
|
||||||
import handlebars from "handlebars";
|
import handlebars from "handlebars";
|
||||||
import knex from "knex";
|
import knex from "knex";
|
||||||
import { customAlphabet } from "nanoid";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { withGatewayProxy } from "@app/lib/gateway";
|
import { withGatewayProxy } from "@app/lib/gateway";
|
||||||
@ -8,16 +8,99 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
|
|||||||
|
|
||||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||||
import { DynamicSecretSqlDBSchema, SqlProviders, TDynamicProviderFns } from "./models";
|
import { DynamicSecretSqlDBSchema, PasswordRequirements, SqlProviders, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||||
|
|
||||||
const generatePassword = (provider: SqlProviders) => {
|
const DEFAULT_PASSWORD_REQUIREMENTS = {
|
||||||
// oracle has limit of 48 password length
|
length: 48,
|
||||||
const size = provider === SqlProviders.Oracle ? 30 : 48;
|
required: {
|
||||||
|
lowercase: 1,
|
||||||
|
uppercase: 1,
|
||||||
|
digits: 1,
|
||||||
|
symbols: 0
|
||||||
|
},
|
||||||
|
allowedSymbols: "-_.~!*"
|
||||||
|
};
|
||||||
|
|
||||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
|
const ORACLE_PASSWORD_REQUIREMENTS = {
|
||||||
return customAlphabet(charset, 48)(size);
|
...DEFAULT_PASSWORD_REQUIREMENTS,
|
||||||
|
length: 30
|
||||||
|
};
|
||||||
|
|
||||||
|
const generatePassword = (provider: SqlProviders, requirements?: PasswordRequirements) => {
|
||||||
|
const defaultReqs = provider === SqlProviders.Oracle ? ORACLE_PASSWORD_REQUIREMENTS : DEFAULT_PASSWORD_REQUIREMENTS;
|
||||||
|
const finalReqs = requirements || defaultReqs;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { length, required, allowedSymbols } = finalReqs;
|
||||||
|
|
||||||
|
const chars = {
|
||||||
|
lowercase: "abcdefghijklmnopqrstuvwxyz",
|
||||||
|
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
digits: "0123456789",
|
||||||
|
symbols: allowedSymbols || "-_.~!*"
|
||||||
|
};
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
if (required.lowercase > 0) {
|
||||||
|
parts.push(
|
||||||
|
...Array(required.lowercase)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => chars.lowercase[randomInt(chars.lowercase.length)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (required.uppercase > 0) {
|
||||||
|
parts.push(
|
||||||
|
...Array(required.uppercase)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => chars.uppercase[randomInt(chars.uppercase.length)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (required.digits > 0) {
|
||||||
|
parts.push(
|
||||||
|
...Array(required.digits)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => chars.digits[randomInt(chars.digits.length)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (required.symbols > 0) {
|
||||||
|
parts.push(
|
||||||
|
...Array(required.symbols)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => chars.symbols[randomInt(chars.symbols.length)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const requiredTotal = Object.values(required).reduce<number>((a, b) => a + b, 0);
|
||||||
|
const remainingLength = Math.max(length - requiredTotal, 0);
|
||||||
|
|
||||||
|
const allowedChars = Object.entries(chars)
|
||||||
|
.filter(([key]) => required[key as keyof typeof required] > 0)
|
||||||
|
.map(([, value]) => value)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
parts.push(
|
||||||
|
...Array(remainingLength)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => allowedChars[randomInt(allowedChars.length)])
|
||||||
|
);
|
||||||
|
|
||||||
|
// shuffle the array to mix up the characters
|
||||||
|
for (let i = parts.length - 1; i > 0; i -= 1) {
|
||||||
|
const j = randomInt(i + 1);
|
||||||
|
[parts[i], parts[j]] = [parts[j], parts[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join("");
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message = error instanceof Error ? error.message : "Unknown error";
|
||||||
|
throw new Error(`Failed to generate password: ${message}`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateUsername = (provider: SqlProviders) => {
|
const generateUsername = (provider: SqlProviders) => {
|
||||||
@ -115,7 +198,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
|||||||
const create = async (inputs: unknown, expireAt: number) => {
|
const create = async (inputs: unknown, expireAt: number) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const username = generateUsername(providerInputs.client);
|
const username = generateUsername(providerInputs.client);
|
||||||
const password = generatePassword(providerInputs.client);
|
const password = generatePassword(providerInputs.client, providerInputs.passwordRequirements);
|
||||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||||
const db = await $getClient({ ...providerInputs, port, host });
|
const db = await $getClient({ ...providerInputs, port, host });
|
||||||
try {
|
try {
|
||||||
|
@ -3,7 +3,7 @@ import slugify from "@sindresorhus/slugify";
|
|||||||
|
|
||||||
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
|
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
|
||||||
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
|
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
@ -87,9 +87,14 @@ export const groupServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
const isCustomRole = Boolean(customRole);
|
const isCustomRole = Boolean(customRole);
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
|
|
||||||
if (!hasRequiredPriviledges)
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" });
|
if (!permissionBoundary.isValid)
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to create a more privileged group",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const group = await groupDAL.transaction(async (tx) => {
|
const group = await groupDAL.transaction(async (tx) => {
|
||||||
const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name }, tx);
|
const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name }, tx);
|
||||||
@ -156,9 +161,13 @@ export const groupServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isCustomRole = Boolean(customOrgRole);
|
const isCustomRole = Boolean(customOrgRole);
|
||||||
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
if (!hasRequiredNewRolePermission)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update a more privileged group",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
if (isCustomRole) customRole = customOrgRole;
|
if (isCustomRole) customRole = customOrgRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,9 +338,13 @@ export const groupServiceFactory = ({
|
|||||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||||
|
|
||||||
// check if user has broader or equal to privileges than group
|
// check if user has broader or equal to privileges than group
|
||||||
const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, groupRolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, groupRolePermission);
|
||||||
if (!hasRequiredPrivileges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to add user to more privileged group",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const user = await userDAL.findOne({ username });
|
const user = await userDAL.findOne({ username });
|
||||||
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
|
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
|
||||||
@ -396,9 +409,13 @@ export const groupServiceFactory = ({
|
|||||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||||
|
|
||||||
// check if user has broader or equal to privileges than group
|
// check if user has broader or equal to privileges than group
|
||||||
const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, groupRolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, groupRolePermission);
|
||||||
if (!hasRequiredPrivileges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to delete user from more privileged group" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to delete user from more privileged group",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const user = await userDAL.findOne({ username });
|
const user = await userDAL.findOne({ username });
|
||||||
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
|
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
|
||||||
|
@ -3,7 +3,7 @@ import { packRules } from "@casl/ability/extra";
|
|||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
|
|
||||||
import { ActionProjectType, TableName } from "@app/db/schemas";
|
import { ActionProjectType, TableName } from "@app/db/schemas";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission";
|
import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
@ -79,9 +79,13 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
|||||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||||
targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission));
|
targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission));
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission);
|
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||||
slug,
|
slug,
|
||||||
@ -161,9 +165,13 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
|||||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||||
targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || []));
|
targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || []));
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission);
|
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
if (data?.slug) {
|
if (data?.slug) {
|
||||||
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||||
@ -239,9 +247,13 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.Any
|
actionProjectType: ActionProjectType.Any
|
||||||
});
|
});
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
|
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
|
||||||
return {
|
return {
|
||||||
|
@ -3,7 +3,7 @@ import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
|
|||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
|
|
||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
@ -88,9 +88,13 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
|||||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||||
targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission));
|
targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission));
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission);
|
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||||
slug,
|
slug,
|
||||||
@ -172,9 +176,13 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
|||||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||||
targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || []));
|
targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || []));
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission);
|
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
|
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||||
slug,
|
slug,
|
||||||
@ -268,9 +276,13 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.Any
|
actionProjectType: ActionProjectType.Any
|
||||||
});
|
});
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to edit more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to edit more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
|
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||||
slug,
|
slug,
|
||||||
|
@ -1,7 +1,109 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
|
import { ForbiddenError, MongoAbility, PureAbility, subject } from "@casl/ability";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { TOrganizations } from "@app/db/schemas";
|
import { TOrganizations } from "@app/db/schemas";
|
||||||
import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type";
|
import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ProjectPermissionSecretActions,
|
||||||
|
ProjectPermissionSet,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
ProjectPermissionV2Schema,
|
||||||
|
SecretSubjectFields
|
||||||
|
} from "./project-permission";
|
||||||
|
|
||||||
|
export function throwIfMissingSecretReadValueOrDescribePermission(
|
||||||
|
permission: MongoAbility<ProjectPermissionSet> | PureAbility,
|
||||||
|
action: Extract<
|
||||||
|
ProjectPermissionSecretActions,
|
||||||
|
ProjectPermissionSecretActions.ReadValue | ProjectPermissionSecretActions.DescribeSecret
|
||||||
|
>,
|
||||||
|
subjectFields?: SecretSubjectFields
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (subjectFields) {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||||
|
subject(ProjectPermissionSub.Secrets, subjectFields)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||||
|
ProjectPermissionSub.Secrets
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (subjectFields) {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, subjectFields));
|
||||||
|
} else {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(action, ProjectPermissionSub.Secrets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasSecretReadValueOrDescribePermission(
|
||||||
|
permission: MongoAbility<ProjectPermissionSet>,
|
||||||
|
action: Extract<
|
||||||
|
ProjectPermissionSecretActions,
|
||||||
|
ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue
|
||||||
|
>,
|
||||||
|
subjectFields?: SecretSubjectFields
|
||||||
|
) {
|
||||||
|
let canNewPermission = false;
|
||||||
|
let canOldPermission = false;
|
||||||
|
|
||||||
|
if (subjectFields) {
|
||||||
|
canNewPermission = permission.can(action, subject(ProjectPermissionSub.Secrets, subjectFields));
|
||||||
|
canOldPermission = permission.can(
|
||||||
|
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||||
|
subject(ProjectPermissionSub.Secrets, subjectFields)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
canNewPermission = permission.can(action, ProjectPermissionSub.Secrets);
|
||||||
|
canOldPermission = permission.can(
|
||||||
|
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||||
|
ProjectPermissionSub.Secrets
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return canNewPermission || canOldPermission;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OptionalArrayPermissionSchema = ProjectPermissionV2Schema.array().optional();
|
||||||
|
export function checkForInvalidPermissionCombination(permissions: z.infer<typeof OptionalArrayPermissionSchema>) {
|
||||||
|
if (!permissions) return;
|
||||||
|
|
||||||
|
for (const permission of permissions) {
|
||||||
|
if (permission.subject === ProjectPermissionSub.Secrets) {
|
||||||
|
if (permission.action.includes(ProjectPermissionSecretActions.DescribeAndReadValue)) {
|
||||||
|
const hasReadValue = permission.action.includes(ProjectPermissionSecretActions.ReadValue);
|
||||||
|
const hasDescribeSecret = permission.action.includes(ProjectPermissionSecretActions.DescribeSecret);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
if (!hasReadValue && !hasDescribeSecret) continue;
|
||||||
|
|
||||||
|
const hasBothDescribeAndReadValue = hasReadValue && hasDescribeSecret;
|
||||||
|
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `You have selected Read, and ${
|
||||||
|
hasBothDescribeAndReadValue
|
||||||
|
? "both Read Value and Describe Secret"
|
||||||
|
: hasReadValue
|
||||||
|
? "Read Value"
|
||||||
|
: hasDescribeSecret
|
||||||
|
? "Describe Secret"
|
||||||
|
: ""
|
||||||
|
}. You cannot select Read Value or Describe Secret if you have selected Read. The Read permission is a legacy action which has been replaced by Describe Secret and Read Value.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
|
function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
|
||||||
if (!actorAuthMethod) return false;
|
if (!actorAuthMethod) return false;
|
||||||
|
|
||||||
|
@ -5,22 +5,6 @@ import { PermissionConditionOperators } from "@app/lib/casl";
|
|||||||
|
|
||||||
export const PermissionConditionSchema = {
|
export const PermissionConditionSchema = {
|
||||||
[PermissionConditionOperators.$IN]: z.string().trim().min(1).array(),
|
[PermissionConditionOperators.$IN]: z.string().trim().min(1).array(),
|
||||||
[PermissionConditionOperators.$ALL]: z.string().trim().min(1).array(),
|
|
||||||
[PermissionConditionOperators.$REGEX]: z
|
|
||||||
.string()
|
|
||||||
.min(1)
|
|
||||||
.refine(
|
|
||||||
(el) => {
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line no-new
|
|
||||||
new RegExp(el);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ message: "Invalid regex pattern" }
|
|
||||||
),
|
|
||||||
[PermissionConditionOperators.$EQ]: z.string().min(1),
|
[PermissionConditionOperators.$EQ]: z.string().min(1),
|
||||||
[PermissionConditionOperators.$NEQ]: z.string().min(1),
|
[PermissionConditionOperators.$NEQ]: z.string().min(1),
|
||||||
[PermissionConditionOperators.$GLOB]: z
|
[PermissionConditionOperators.$GLOB]: z
|
||||||
|
@ -17,6 +17,15 @@ export enum ProjectPermissionActions {
|
|||||||
Delete = "delete"
|
Delete = "delete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProjectPermissionSecretActions {
|
||||||
|
DescribeAndReadValue = "read",
|
||||||
|
DescribeSecret = "describeSecret",
|
||||||
|
ReadValue = "readValue",
|
||||||
|
Create = "create",
|
||||||
|
Edit = "edit",
|
||||||
|
Delete = "delete"
|
||||||
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionCmekActions {
|
export enum ProjectPermissionCmekActions {
|
||||||
Read = "read",
|
Read = "read",
|
||||||
Create = "create",
|
Create = "create",
|
||||||
@ -115,7 +124,7 @@ export type IdentityManagementSubjectFields = {
|
|||||||
|
|
||||||
export type ProjectPermissionSet =
|
export type ProjectPermissionSet =
|
||||||
| [
|
| [
|
||||||
ProjectPermissionActions,
|
ProjectPermissionSecretActions,
|
||||||
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
|
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
|
||||||
]
|
]
|
||||||
| [
|
| [
|
||||||
@ -429,6 +438,7 @@ const GeneralPermissionSchema = [
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Do not update this schema anymore, as it's kept purely for backwards compatability. Update V2 schema only.
|
||||||
export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [
|
export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
|
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
|
||||||
@ -460,7 +470,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
|||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
|
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
|
||||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretActions).describe(
|
||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
),
|
),
|
||||||
conditions: SecretConditionV2Schema.describe(
|
conditions: SecretConditionV2Schema.describe(
|
||||||
@ -517,7 +527,6 @@ const buildAdminPermissionRules = () => {
|
|||||||
|
|
||||||
// Admins get full access to everything
|
// Admins get full access to everything
|
||||||
[
|
[
|
||||||
ProjectPermissionSub.Secrets,
|
|
||||||
ProjectPermissionSub.SecretFolders,
|
ProjectPermissionSub.SecretFolders,
|
||||||
ProjectPermissionSub.SecretImports,
|
ProjectPermissionSub.SecretImports,
|
||||||
ProjectPermissionSub.SecretApproval,
|
ProjectPermissionSub.SecretApproval,
|
||||||
@ -550,10 +559,22 @@ const buildAdminPermissionRules = () => {
|
|||||||
ProjectPermissionActions.Create,
|
ProjectPermissionActions.Create,
|
||||||
ProjectPermissionActions.Delete
|
ProjectPermissionActions.Delete
|
||||||
],
|
],
|
||||||
el as ProjectPermissionSub
|
el
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
can(
|
||||||
|
[
|
||||||
|
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||||
|
ProjectPermissionSecretActions.DescribeSecret,
|
||||||
|
ProjectPermissionSecretActions.ReadValue,
|
||||||
|
ProjectPermissionSecretActions.Create,
|
||||||
|
ProjectPermissionSecretActions.Edit,
|
||||||
|
ProjectPermissionSecretActions.Delete
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.Secrets
|
||||||
|
);
|
||||||
|
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||||
@ -613,10 +634,12 @@ const buildMemberPermissionRules = () => {
|
|||||||
|
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
ProjectPermissionActions.Read,
|
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionSecretActions.DescribeSecret,
|
||||||
ProjectPermissionActions.Create,
|
ProjectPermissionSecretActions.ReadValue,
|
||||||
ProjectPermissionActions.Delete
|
ProjectPermissionSecretActions.Edit,
|
||||||
|
ProjectPermissionSecretActions.Create,
|
||||||
|
ProjectPermissionSecretActions.Delete
|
||||||
],
|
],
|
||||||
ProjectPermissionSub.Secrets
|
ProjectPermissionSub.Secrets
|
||||||
);
|
);
|
||||||
@ -788,7 +811,9 @@ export const projectMemberPermissions = buildMemberPermissionRules();
|
|||||||
const buildViewerPermissionRules = () => {
|
const buildViewerPermissionRules = () => {
|
||||||
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
|
can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets);
|
||||||
|
can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets);
|
||||||
|
can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
|
||||||
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
|
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
|
||||||
@ -837,7 +862,6 @@ export const buildServiceTokenProjectPermission = (
|
|||||||
(subject) => {
|
(subject) => {
|
||||||
if (canWrite) {
|
if (canWrite) {
|
||||||
can(ProjectPermissionActions.Edit, subject, {
|
can(ProjectPermissionActions.Edit, subject, {
|
||||||
// TODO: @Akhi
|
|
||||||
// @ts-expect-error type
|
// @ts-expect-error type
|
||||||
secretPath: { $glob: secretPath },
|
secretPath: { $glob: secretPath },
|
||||||
environment
|
environment
|
||||||
@ -916,7 +940,17 @@ export const backfillPermissionV1SchemaToV2Schema = (
|
|||||||
subject: ProjectPermissionSub.SecretImports as const
|
subject: ProjectPermissionSub.SecretImports as const
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const secretPolicies = secretSubjects.map(({ subject, ...el }) => ({
|
||||||
|
subject: ProjectPermissionSub.Secrets as const,
|
||||||
|
...el,
|
||||||
|
action:
|
||||||
|
el.action.includes(ProjectPermissionActions.Read) && !el.action.includes(ProjectPermissionSecretActions.ReadValue)
|
||||||
|
? el.action.concat(ProjectPermissionSecretActions.ReadValue)
|
||||||
|
: el.action
|
||||||
|
}));
|
||||||
|
|
||||||
const secretFolderPolicies = secretSubjects
|
const secretFolderPolicies = secretSubjects
|
||||||
|
|
||||||
.map(({ subject, ...el }) => ({
|
.map(({ subject, ...el }) => ({
|
||||||
...el,
|
...el,
|
||||||
// read permission is not needed anymore
|
// read permission is not needed anymore
|
||||||
@ -958,6 +992,7 @@ export const backfillPermissionV1SchemaToV2Schema = (
|
|||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore-error this is valid ts
|
// @ts-ignore-error this is valid ts
|
||||||
secretImportPolicies,
|
secretImportPolicies,
|
||||||
|
secretPolicies,
|
||||||
dynamicSecretPolicies,
|
dynamicSecretPolicies,
|
||||||
hasReadOnlyFolder.length ? [] : secretFolderPolicies
|
hasReadOnlyFolder.length ? [] : secretFolderPolicies
|
||||||
);
|
);
|
||||||
|
@ -3,7 +3,7 @@ import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
|
|||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
|
|
||||||
import { ActionProjectType, TableName } from "@app/db/schemas";
|
import { ActionProjectType, TableName } from "@app/db/schemas";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
@ -76,9 +76,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
|||||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||||
targetUserPermission.update(targetUserPermission.rules.concat(customPermission));
|
targetUserPermission.update(targetUserPermission.rules.concat(customPermission));
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetUserPermission);
|
const permissionBoundary = validatePermissionBoundary(permission, targetUserPermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update more privileged user",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
|
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
|
||||||
slug,
|
slug,
|
||||||
@ -163,9 +167,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
|||||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||||
targetUserPermission.update(targetUserPermission.rules.concat(dto.permissions || []));
|
targetUserPermission.update(targetUserPermission.rules.concat(dto.permissions || []));
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetUserPermission);
|
const permissionBoundary = validatePermissionBoundary(permission, targetUserPermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
if (dto?.slug) {
|
if (dto?.slug) {
|
||||||
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
|
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
SecretEncryptionAlgo,
|
SecretEncryptionAlgo,
|
||||||
SecretKeyEncoding,
|
SecretKeyEncoding,
|
||||||
SecretType,
|
SecretType,
|
||||||
|
TableName,
|
||||||
TSecretApprovalRequestsSecretsInsert,
|
TSecretApprovalRequestsSecretsInsert,
|
||||||
TSecretApprovalRequestsSecretsV2Insert
|
TSecretApprovalRequestsSecretsV2Insert
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
@ -57,8 +58,9 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
|||||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
import { TLicenseServiceFactory } from "../license/license-service";
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
|
import { throwIfMissingSecretReadValueOrDescribePermission } from "../permission/permission-fns";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
import { ProjectPermissionSecretActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||||
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
|
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
|
||||||
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
|
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
|
||||||
import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal";
|
import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal";
|
||||||
@ -88,7 +90,12 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
|||||||
secretDAL: TSecretDALFactory;
|
secretDAL: TSecretDALFactory;
|
||||||
secretTagDAL: Pick<
|
secretTagDAL: Pick<
|
||||||
TSecretTagDALFactory,
|
TSecretTagDALFactory,
|
||||||
"findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret" | "saveTagsToSecretV2" | "deleteTagsToSecretV2"
|
| "findManyTagsById"
|
||||||
|
| "saveTagsToSecret"
|
||||||
|
| "deleteTagsManySecret"
|
||||||
|
| "saveTagsToSecretV2"
|
||||||
|
| "deleteTagsToSecretV2"
|
||||||
|
| "find"
|
||||||
>;
|
>;
|
||||||
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
|
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
|
||||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||||
@ -106,7 +113,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
|||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
|
||||||
secretV2BridgeDAL: Pick<
|
secretV2BridgeDAL: Pick<
|
||||||
TSecretV2BridgeDALFactory,
|
TSecretV2BridgeDALFactory,
|
||||||
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany"
|
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find"
|
||||||
>;
|
>;
|
||||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||||
@ -912,10 +919,11 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
environment,
|
||||||
);
|
secretPath
|
||||||
|
});
|
||||||
|
|
||||||
await projectDAL.checkProjectUpgradeStatus(projectId);
|
await projectDAL.checkProjectUpgradeStatus(projectId);
|
||||||
|
|
||||||
@ -1000,6 +1008,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
: keyName2BlindIndex[secretName];
|
: keyName2BlindIndex[secretName];
|
||||||
// add tags
|
// add tags
|
||||||
if (tagIds?.length) commitTagIds[keyName2BlindIndex[secretName]] = tagIds;
|
if (tagIds?.length) commitTagIds[keyName2BlindIndex[secretName]] = tagIds;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...latestSecretVersions[secretId],
|
...latestSecretVersions[secretId],
|
||||||
...el,
|
...el,
|
||||||
@ -1327,17 +1336,48 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
// deleted secrets
|
// deleted secrets
|
||||||
const deletedSecrets = data[SecretOperations.Delete];
|
const deletedSecrets = data[SecretOperations.Delete];
|
||||||
if (deletedSecrets && deletedSecrets.length) {
|
if (deletedSecrets && deletedSecrets.length) {
|
||||||
const secretsToDeleteInDB = await secretV2BridgeDAL.findBySecretKeys(
|
const secretsToDeleteInDB = await secretV2BridgeDAL.find({
|
||||||
folderId,
|
folderId,
|
||||||
deletedSecrets.map((el) => ({
|
$complex: {
|
||||||
key: el.secretKey,
|
operator: "and",
|
||||||
type: SecretType.Shared
|
value: [
|
||||||
}))
|
{
|
||||||
);
|
operator: "or",
|
||||||
|
value: deletedSecrets.map((el) => ({
|
||||||
|
operator: "and",
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
operator: "eq",
|
||||||
|
field: `${TableName.SecretV2}.key` as "key",
|
||||||
|
value: el.secretKey
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: "eq",
|
||||||
|
field: "type",
|
||||||
|
value: SecretType.Shared
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
if (secretsToDeleteInDB.length !== deletedSecrets.length)
|
if (secretsToDeleteInDB.length !== deletedSecrets.length)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: `Secret does not exist: ${secretsToDeleteInDB.map((el) => el.key).join(",")}`
|
message: `Secret does not exist: ${secretsToDeleteInDB.map((el) => el.key).join(",")}`
|
||||||
});
|
});
|
||||||
|
secretsToDeleteInDB.forEach((el) => {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionSecretActions.Delete,
|
||||||
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
secretName: el.key,
|
||||||
|
secretTags: el.tags?.map((i) => i.slug)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const secretsGroupedByKey = groupBy(secretsToDeleteInDB, (i) => i.key);
|
const secretsGroupedByKey = groupBy(secretsToDeleteInDB, (i) => i.key);
|
||||||
const deletedSecretIds = deletedSecrets.map((el) => secretsGroupedByKey[el.secretKey][0].id);
|
const deletedSecretIds = deletedSecrets.map((el) => secretsGroupedByKey[el.secretKey][0].id);
|
||||||
const latestSecretVersions = await secretVersionV2BridgeDAL.findLatestVersionMany(folderId, deletedSecretIds);
|
const latestSecretVersions = await secretVersionV2BridgeDAL.findLatestVersionMany(folderId, deletedSecretIds);
|
||||||
@ -1363,9 +1403,9 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
const tagsGroupById = groupBy(tags, (i) => i.id);
|
const tagsGroupById = groupBy(tags, (i) => i.id);
|
||||||
|
|
||||||
commits.forEach((commit) => {
|
commits.forEach((commit) => {
|
||||||
let action = ProjectPermissionActions.Create;
|
let action = ProjectPermissionSecretActions.Create;
|
||||||
if (commit.op === SecretOperations.Update) action = ProjectPermissionActions.Edit;
|
if (commit.op === SecretOperations.Update) action = ProjectPermissionSecretActions.Edit;
|
||||||
if (commit.op === SecretOperations.Delete) action = ProjectPermissionActions.Delete;
|
if (commit.op === SecretOperations.Delete) return; // we do the validation on top
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
action,
|
action,
|
||||||
|
@ -265,6 +265,7 @@ export const secretReplicationServiceFactory = ({
|
|||||||
folderDAL,
|
folderDAL,
|
||||||
secretImportDAL,
|
secretImportDAL,
|
||||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
|
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
|
||||||
|
viewSecretValue: true,
|
||||||
hasSecretAccess: () => true
|
hasSecretAccess: () => true
|
||||||
});
|
});
|
||||||
// secrets that gets replicated across imports
|
// secrets that gets replicated across imports
|
||||||
|
@ -15,7 +15,11 @@ import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret
|
|||||||
|
|
||||||
import { TLicenseServiceFactory } from "../license/license-service";
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSecretActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "../permission/project-permission";
|
||||||
import { TSecretRotationDALFactory } from "./secret-rotation-dal";
|
import { TSecretRotationDALFactory } from "./secret-rotation-dal";
|
||||||
import { TSecretRotationQueueFactory } from "./secret-rotation-queue";
|
import { TSecretRotationQueueFactory } from "./secret-rotation-queue";
|
||||||
import { TSecretRotationEncData } from "./secret-rotation-queue/secret-rotation-queue-types";
|
import { TSecretRotationEncData } from "./secret-rotation-queue/secret-rotation-queue-types";
|
||||||
@ -106,7 +110,7 @@ export const secretRotationServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionSecretActions.Edit,
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-argument */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-argument */
|
||||||
// akhilmhdh: I did this, quite strange bug with eslint. Everything do have a type stil has this error
|
// akhilmhdh: I did this, quite strange bug with eslint. Everything do have a type stil has this error
|
||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
import { ActionProjectType, TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas";
|
import { ActionProjectType, TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas";
|
||||||
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||||
@ -12,6 +12,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
|||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
|
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
|
||||||
|
import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "@app/services/secret/secret-fns";
|
||||||
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
|
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
|
||||||
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
|
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
|
||||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
@ -22,8 +23,16 @@ import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secre
|
|||||||
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
||||||
|
|
||||||
import { TLicenseServiceFactory } from "../license/license-service";
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
|
import {
|
||||||
|
hasSecretReadValueOrDescribePermission,
|
||||||
|
throwIfMissingSecretReadValueOrDescribePermission
|
||||||
|
} from "../permission/permission-fns";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSecretActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "../permission/project-permission";
|
||||||
import {
|
import {
|
||||||
TGetSnapshotDataDTO,
|
TGetSnapshotDataDTO,
|
||||||
TProjectSnapshotCountDTO,
|
TProjectSnapshotCountDTO,
|
||||||
@ -97,10 +106,10 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||||
|
|
||||||
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
|
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
|
||||||
ProjectPermissionActions.Read,
|
environment,
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
secretPath: path
|
||||||
);
|
});
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||||
if (!folder) {
|
if (!folder) {
|
||||||
@ -134,10 +143,10 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||||
|
|
||||||
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
|
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
|
||||||
ProjectPermissionActions.Read,
|
environment,
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
secretPath: path
|
||||||
);
|
});
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||||
if (!folder)
|
if (!folder)
|
||||||
@ -162,6 +171,7 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||||
|
|
||||||
const shouldUseBridge = snapshot.projectVersion === 3;
|
const shouldUseBridge = snapshot.projectVersion === 3;
|
||||||
let snapshotDetails;
|
let snapshotDetails;
|
||||||
if (shouldUseBridge) {
|
if (shouldUseBridge) {
|
||||||
@ -170,68 +180,112 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
projectId: snapshot.projectId
|
projectId: snapshot.projectId
|
||||||
});
|
});
|
||||||
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotV2DataById(id);
|
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotV2DataById(id);
|
||||||
|
|
||||||
|
const fullFolderPath = await getFullFolderPath({
|
||||||
|
folderDAL,
|
||||||
|
folderId: encryptedSnapshotDetails.folderId,
|
||||||
|
envId: encryptedSnapshotDetails.environment.id
|
||||||
|
});
|
||||||
|
|
||||||
snapshotDetails = {
|
snapshotDetails = {
|
||||||
...encryptedSnapshotDetails,
|
...encryptedSnapshotDetails,
|
||||||
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({
|
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => {
|
||||||
...el,
|
const canReadValue = hasSecretReadValueOrDescribePermission(
|
||||||
secretKey: el.key,
|
permission,
|
||||||
secretValue: el.encryptedValue
|
ProjectPermissionSecretActions.ReadValue,
|
||||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
{
|
||||||
: "",
|
environment: encryptedSnapshotDetails.environment.slug,
|
||||||
secretComment: el.encryptedComment
|
secretPath: fullFolderPath,
|
||||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
secretName: el.key,
|
||||||
: ""
|
secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined
|
||||||
}))
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let secretValue = "";
|
||||||
|
if (canReadValue) {
|
||||||
|
secretValue = el.encryptedValue
|
||||||
|
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||||
|
: "";
|
||||||
|
} else {
|
||||||
|
secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...el,
|
||||||
|
secretKey: el.key,
|
||||||
|
secretValueHidden: !canReadValue,
|
||||||
|
secretValue,
|
||||||
|
secretComment: el.encryptedComment
|
||||||
|
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||||
|
: ""
|
||||||
|
};
|
||||||
|
})
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotDataById(id);
|
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotDataById(id);
|
||||||
|
|
||||||
|
const fullFolderPath = await getFullFolderPath({
|
||||||
|
folderDAL,
|
||||||
|
folderId: encryptedSnapshotDetails.folderId,
|
||||||
|
envId: encryptedSnapshotDetails.environment.id
|
||||||
|
});
|
||||||
|
|
||||||
const { botKey } = await projectBotService.getBotKey(snapshot.projectId);
|
const { botKey } = await projectBotService.getBotKey(snapshot.projectId);
|
||||||
if (!botKey)
|
if (!botKey)
|
||||||
throw new NotFoundError({ message: `Project bot key not found for project with ID '${snapshot.projectId}'` });
|
throw new NotFoundError({ message: `Project bot key not found for project with ID '${snapshot.projectId}'` });
|
||||||
snapshotDetails = {
|
snapshotDetails = {
|
||||||
...encryptedSnapshotDetails,
|
...encryptedSnapshotDetails,
|
||||||
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({
|
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => {
|
||||||
...el,
|
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
||||||
secretKey: decryptSymmetric128BitHexKeyUTF8({
|
|
||||||
ciphertext: el.secretKeyCiphertext,
|
ciphertext: el.secretKeyCiphertext,
|
||||||
iv: el.secretKeyIV,
|
iv: el.secretKeyIV,
|
||||||
tag: el.secretKeyTag,
|
tag: el.secretKeyTag,
|
||||||
key: botKey
|
key: botKey
|
||||||
}),
|
});
|
||||||
secretValue: decryptSymmetric128BitHexKeyUTF8({
|
|
||||||
ciphertext: el.secretValueCiphertext,
|
const canReadValue = hasSecretReadValueOrDescribePermission(
|
||||||
iv: el.secretValueIV,
|
permission,
|
||||||
tag: el.secretValueTag,
|
ProjectPermissionSecretActions.ReadValue,
|
||||||
key: botKey
|
{
|
||||||
}),
|
environment: encryptedSnapshotDetails.environment.slug,
|
||||||
secretComment:
|
secretPath: fullFolderPath,
|
||||||
el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext
|
secretName: secretKey,
|
||||||
? decryptSymmetric128BitHexKeyUTF8({
|
secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined
|
||||||
ciphertext: el.secretCommentCiphertext,
|
}
|
||||||
iv: el.secretCommentIV,
|
);
|
||||||
tag: el.secretCommentTag,
|
|
||||||
key: botKey
|
let secretValue = "";
|
||||||
})
|
|
||||||
: ""
|
if (canReadValue) {
|
||||||
}))
|
secretValue = decryptSymmetric128BitHexKeyUTF8({
|
||||||
|
ciphertext: el.secretValueCiphertext,
|
||||||
|
iv: el.secretValueIV,
|
||||||
|
tag: el.secretValueTag,
|
||||||
|
key: botKey
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...el,
|
||||||
|
secretKey,
|
||||||
|
secretValueHidden: !canReadValue,
|
||||||
|
secretValue,
|
||||||
|
secretComment:
|
||||||
|
el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext
|
||||||
|
? decryptSymmetric128BitHexKeyUTF8({
|
||||||
|
ciphertext: el.secretCommentCiphertext,
|
||||||
|
iv: el.secretCommentIV,
|
||||||
|
tag: el.secretCommentTag,
|
||||||
|
key: botKey
|
||||||
|
})
|
||||||
|
: ""
|
||||||
|
};
|
||||||
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullFolderPath = await getFullFolderPath({
|
|
||||||
folderDAL,
|
|
||||||
folderId: snapshotDetails.folderId,
|
|
||||||
envId: snapshotDetails.environment.id
|
|
||||||
});
|
|
||||||
|
|
||||||
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment: snapshotDetails.environment.slug,
|
|
||||||
secretPath: fullFolderPath
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return snapshotDetails;
|
return snapshotDetails;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -459,7 +459,8 @@ export const PROJECTS = {
|
|||||||
workspaceId: "The ID of the project to update.",
|
workspaceId: "The ID of the project to update.",
|
||||||
name: "The new name of the project.",
|
name: "The new name of the project.",
|
||||||
projectDescription: "An optional description label for the project.",
|
projectDescription: "An optional description label for the project.",
|
||||||
autoCapitalization: "Disable or enable auto-capitalization for the project."
|
autoCapitalization: "Disable or enable auto-capitalization for the project.",
|
||||||
|
slug: "An optional slug for the project. (must be unique within the organization)"
|
||||||
},
|
},
|
||||||
GET_KEY: {
|
GET_KEY: {
|
||||||
workspaceId: "The ID of the project to get the key from."
|
workspaceId: "The ID of the project to get the key from."
|
||||||
@ -666,6 +667,7 @@ export const SECRETS = {
|
|||||||
secretPath: "The path of the secret to attach tags to.",
|
secretPath: "The path of the secret to attach tags to.",
|
||||||
type: "The type of the secret to attach tags to. (shared/personal)",
|
type: "The type of the secret to attach tags to. (shared/personal)",
|
||||||
environment: "The slug of the environment where the secret is located",
|
environment: "The slug of the environment where the secret is located",
|
||||||
|
viewSecretValue: "Whether or not to retrieve the secret value.",
|
||||||
projectSlug: "The slug of the project where the secret is located.",
|
projectSlug: "The slug of the project where the secret is located.",
|
||||||
tagSlugs: "An array of existing tag slugs to attach to the secret."
|
tagSlugs: "An array of existing tag slugs to attach to the secret."
|
||||||
},
|
},
|
||||||
@ -689,6 +691,7 @@ export const RAW_SECRETS = {
|
|||||||
"The slug of the project to list secrets from. This parameter is only applicable by machine identities.",
|
"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.",
|
environment: "The slug of the environment to list secrets from.",
|
||||||
secretPath: "The secret path to list secrets from.",
|
secretPath: "The secret path to list secrets from.",
|
||||||
|
viewSecretValue: "Whether or not to retrieve the secret value.",
|
||||||
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.",
|
tagSlugs: "The comma separated tag slugs to filter secrets.",
|
||||||
metadataFilter:
|
metadataFilter:
|
||||||
@ -717,6 +720,7 @@ export const RAW_SECRETS = {
|
|||||||
secretPath: "The path of the secret to get.",
|
secretPath: "The path of the secret to get.",
|
||||||
version: "The version of the secret to get.",
|
version: "The version of the secret to get.",
|
||||||
type: "The type of the secret to get.",
|
type: "The type of the secret to get.",
|
||||||
|
viewSecretValue: "Whether or not to retrieve the secret value.",
|
||||||
includeImports: "Weather to include imported secrets or not."
|
includeImports: "Weather to include imported secrets or not."
|
||||||
},
|
},
|
||||||
UPDATE: {
|
UPDATE: {
|
||||||
|
669
backend/src/lib/casl/boundary.test.ts
Normal file
669
backend/src/lib/casl/boundary.test.ts
Normal file
@ -0,0 +1,669 @@
|
|||||||
|
import { createMongoAbility } from "@casl/ability";
|
||||||
|
|
||||||
|
import { PermissionConditionOperators } from ".";
|
||||||
|
import { validatePermissionBoundary } from "./boundary";
|
||||||
|
|
||||||
|
describe("Validate Permission Boundary Function", () => {
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
title: "child with equal privilege",
|
||||||
|
parentPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit", "delete", "read"],
|
||||||
|
subject: "secrets"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit", "delete", "read"],
|
||||||
|
subject: "secrets"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
expectValid: true,
|
||||||
|
missingPermissions: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "child with less privilege",
|
||||||
|
parentPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit", "delete", "read"],
|
||||||
|
subject: "secrets"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit"],
|
||||||
|
subject: "secrets"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
expectValid: true,
|
||||||
|
missingPermissions: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "child with more privilege",
|
||||||
|
parentPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit"],
|
||||||
|
subject: "secrets"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
expectValid: false,
|
||||||
|
missingPermissions: [{ action: "edit", subject: "secrets" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "parent with multiple and child with multiple",
|
||||||
|
parentPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: ["create", "edit"],
|
||||||
|
subject: "members"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "members"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
expectValid: true,
|
||||||
|
missingPermissions: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Child with no access",
|
||||||
|
parentPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: ["create", "edit"],
|
||||||
|
subject: "members"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
childPermission: createMongoAbility([]),
|
||||||
|
expectValid: true,
|
||||||
|
missingPermissions: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Parent and child disjoint set",
|
||||||
|
parentPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit", "delete", "read"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit", "delete", "read"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
expectValid: false,
|
||||||
|
missingPermissions: ["create", "edit", "delete", "read"].map((el) => ({
|
||||||
|
action: el,
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Parent with inverted rules",
|
||||||
|
parentPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit", "delete", "read"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "read",
|
||||||
|
subject: "secrets",
|
||||||
|
inverted: true,
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" },
|
||||||
|
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: "read",
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" },
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "/" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
expectValid: true,
|
||||||
|
missingPermissions: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Parent with inverted rules - child accessing invalid one",
|
||||||
|
parentPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit", "delete", "read"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "read",
|
||||||
|
subject: "secrets",
|
||||||
|
inverted: true,
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" },
|
||||||
|
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: "read",
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" },
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "/hello/world" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
expectValid: false,
|
||||||
|
missingPermissions: [
|
||||||
|
{
|
||||||
|
action: "read",
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" },
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "/hello/world" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
])("Check permission: $title", ({ parentPermission, childPermission, expectValid, missingPermissions }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
if (expectValid) {
|
||||||
|
expect(permissionBoundary.isValid).toBeTruthy();
|
||||||
|
} else {
|
||||||
|
expect(permissionBoundary.isValid).toBeFalsy();
|
||||||
|
expect(permissionBoundary.missingPermissions).toEqual(expect.arrayContaining(missingPermissions));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Validate Permission Boundary: Checking Parent $eq operator", () => {
|
||||||
|
const parentPermission = createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "read"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$IN,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$IN]: ["dev"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$GLOB,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$GLOB]: "dev" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])("Child $operator truthy cases", ({ childPermission }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
expect(permissionBoundary.isValid).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "prod" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$IN,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$IN]: ["dev", "prod"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$GLOB,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$GLOB]: "dev**" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$NEQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$GLOB]: "staging" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])("Child $operator falsy cases", ({ childPermission }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
expect(permissionBoundary.isValid).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Validate Permission Boundary: Checking Parent $neq operator", () => {
|
||||||
|
const parentPermission = createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "read"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$NEQ]: "/hello" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "/" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$NEQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$NEQ]: "/hello" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$IN,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$IN]: ["/", "/staging"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$GLOB,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$GLOB]: "/dev**" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])("Child $operator truthy cases", ({ childPermission }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
expect(permissionBoundary.isValid).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "/hello" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$NEQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$NEQ]: "/" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$IN,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$IN]: ["/", "/hello"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$GLOB,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello**" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])("Child $operator falsy cases", ({ childPermission }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
expect(permissionBoundary.isValid).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Validate Permission Boundary: Checking Parent $IN operator", () => {
|
||||||
|
const parentPermission = createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$IN]: ["dev", "staging"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$IN,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$IN]: ["dev"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: `${PermissionConditionOperators.$IN} - 2`,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$IN]: ["dev", "staging"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$GLOB,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$GLOB]: "dev" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])("Child $operator truthy cases", ({ childPermission }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
expect(permissionBoundary.isValid).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "prod" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$NEQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$NEQ]: "dev" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$IN,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$IN]: ["dev", "prod"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$GLOB,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$GLOB]: "dev**" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])("Child $operator falsy cases", ({ childPermission }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
expect(permissionBoundary.isValid).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Validate Permission Boundary: Checking Parent $GLOB operator", () => {
|
||||||
|
const parentPermission = createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "read"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "/hello/world" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$IN,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$IN]: ["/hello/world", "/hello/world2"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$GLOB,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**/world" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])("Child $operator truthy cases", ({ childPermission }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
expect(permissionBoundary.isValid).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "/print" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$NEQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$NEQ]: "/hello/world" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$IN,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$IN]: ["/", "/hello"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$GLOB,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello**" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])("Child $operator falsy cases", ({ childPermission }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
expect(permissionBoundary.isValid).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
249
backend/src/lib/casl/boundary.ts
Normal file
249
backend/src/lib/casl/boundary.ts
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
import { MongoAbility } from "@casl/ability";
|
||||||
|
import { MongoQuery } from "@ucast/mongo2js";
|
||||||
|
import picomatch from "picomatch";
|
||||||
|
|
||||||
|
import { PermissionConditionOperators } from "./index";
|
||||||
|
|
||||||
|
type TMissingPermission = {
|
||||||
|
action: string;
|
||||||
|
subject: string;
|
||||||
|
conditions?: MongoQuery;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TPermissionConditionShape = {
|
||||||
|
[PermissionConditionOperators.$EQ]: string;
|
||||||
|
[PermissionConditionOperators.$NEQ]: string;
|
||||||
|
[PermissionConditionOperators.$GLOB]: string;
|
||||||
|
[PermissionConditionOperators.$IN]: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPermissionSetID = (action: string, subject: string) => `${action}:${subject}`;
|
||||||
|
const invertTheOperation = (shouldInvert: boolean, operation: boolean) => (shouldInvert ? !operation : operation);
|
||||||
|
const formatConditionOperator = (condition: TPermissionConditionShape | string) => {
|
||||||
|
return (
|
||||||
|
typeof condition === "string" ? { [PermissionConditionOperators.$EQ]: condition } : condition
|
||||||
|
) as TPermissionConditionShape;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isOperatorsASubset = (parentSet: TPermissionConditionShape, subset: TPermissionConditionShape) => {
|
||||||
|
// we compute each operator against each other in left hand side and right hand side
|
||||||
|
if (subset[PermissionConditionOperators.$EQ] || subset[PermissionConditionOperators.$NEQ]) {
|
||||||
|
const subsetOperatorValue = subset[PermissionConditionOperators.$EQ] || subset[PermissionConditionOperators.$NEQ];
|
||||||
|
const isInverted = !subset[PermissionConditionOperators.$EQ];
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$EQ] &&
|
||||||
|
invertTheOperation(isInverted, parentSet[PermissionConditionOperators.$EQ] !== subsetOperatorValue)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$NEQ] &&
|
||||||
|
invertTheOperation(isInverted, parentSet[PermissionConditionOperators.$NEQ] === subsetOperatorValue)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$IN] &&
|
||||||
|
invertTheOperation(isInverted, !parentSet[PermissionConditionOperators.$IN].includes(subsetOperatorValue))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// ne and glob cannot match each other
|
||||||
|
if (parentSet[PermissionConditionOperators.$GLOB] && isInverted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$GLOB] &&
|
||||||
|
!picomatch.isMatch(subsetOperatorValue, parentSet[PermissionConditionOperators.$GLOB], { strictSlashes: false })
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (subset[PermissionConditionOperators.$IN]) {
|
||||||
|
const subsetOperatorValue = subset[PermissionConditionOperators.$IN];
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$EQ] &&
|
||||||
|
(subsetOperatorValue.length !== 1 || subsetOperatorValue[0] !== parentSet[PermissionConditionOperators.$EQ])
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$NEQ] &&
|
||||||
|
subsetOperatorValue.includes(parentSet[PermissionConditionOperators.$NEQ])
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$IN] &&
|
||||||
|
!subsetOperatorValue.every((el) => parentSet[PermissionConditionOperators.$IN].includes(el))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$GLOB] &&
|
||||||
|
!subsetOperatorValue.every((el) =>
|
||||||
|
picomatch.isMatch(el, parentSet[PermissionConditionOperators.$GLOB], {
|
||||||
|
strictSlashes: false
|
||||||
|
})
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (subset[PermissionConditionOperators.$GLOB]) {
|
||||||
|
const subsetOperatorValue = subset[PermissionConditionOperators.$GLOB];
|
||||||
|
const { isGlob } = picomatch.scan(subsetOperatorValue);
|
||||||
|
// if it's glob, all other fixed operators would make this superset because glob is powerful. like eq
|
||||||
|
// example: $in [dev, prod] => glob: dev** could mean anything starting with dev: thus is bigger
|
||||||
|
if (
|
||||||
|
isGlob &&
|
||||||
|
Object.keys(parentSet).some(
|
||||||
|
(el) => el !== PermissionConditionOperators.$GLOB && el !== PermissionConditionOperators.$NEQ
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$EQ] &&
|
||||||
|
parentSet[PermissionConditionOperators.$EQ] !== subsetOperatorValue
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$NEQ] &&
|
||||||
|
picomatch.isMatch(parentSet[PermissionConditionOperators.$NEQ], subsetOperatorValue, {
|
||||||
|
strictSlashes: false
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// if parent set is IN, glob cannot be used for children - It's a bigger scope
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$IN] &&
|
||||||
|
!parentSet[PermissionConditionOperators.$IN].includes(subsetOperatorValue)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$GLOB] &&
|
||||||
|
!picomatch.isMatch(subsetOperatorValue, parentSet[PermissionConditionOperators.$GLOB], {
|
||||||
|
strictSlashes: false
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSubsetForSamePermissionSubjectAction = (
|
||||||
|
parentSetRules: ReturnType<MongoAbility["possibleRulesFor"]>,
|
||||||
|
subsetRules: ReturnType<MongoAbility["possibleRulesFor"]>,
|
||||||
|
appendToMissingPermission: (condition?: MongoQuery) => void
|
||||||
|
) => {
|
||||||
|
const isMissingConditionInParent = parentSetRules.every((el) => !el.conditions);
|
||||||
|
if (isMissingConditionInParent) return true;
|
||||||
|
|
||||||
|
// all subset rules must pass in comparison to parent rul
|
||||||
|
return subsetRules.every((subsetRule) => {
|
||||||
|
const subsetRuleConditions = subsetRule.conditions as Record<string, TPermissionConditionShape | string>;
|
||||||
|
// compare subset rule with all parent rules
|
||||||
|
const isSubsetOfNonInvertedParentSet = parentSetRules
|
||||||
|
.filter((el) => !el.inverted)
|
||||||
|
.some((parentSetRule) => {
|
||||||
|
// get conditions and iterate
|
||||||
|
const parentSetRuleConditions = parentSetRule?.conditions as Record<string, TPermissionConditionShape | string>;
|
||||||
|
if (!parentSetRuleConditions) return true;
|
||||||
|
return Object.keys(parentSetRuleConditions).every((parentConditionField) => {
|
||||||
|
// if parent condition is missing then it's never a subset
|
||||||
|
if (!subsetRuleConditions?.[parentConditionField]) return false;
|
||||||
|
|
||||||
|
// standardize the conditions plain string operator => $eq function
|
||||||
|
const parentRuleConditionOperators = formatConditionOperator(parentSetRuleConditions[parentConditionField]);
|
||||||
|
const selectedSubsetRuleCondition = subsetRuleConditions?.[parentConditionField];
|
||||||
|
const subsetRuleConditionOperators = formatConditionOperator(selectedSubsetRuleCondition);
|
||||||
|
return isOperatorsASubset(parentRuleConditionOperators, subsetRuleConditionOperators);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const invertedParentSetRules = parentSetRules.filter((el) => el.inverted);
|
||||||
|
const isNotSubsetOfInvertedParentSet = invertedParentSetRules.length
|
||||||
|
? !invertedParentSetRules.some((parentSetRule) => {
|
||||||
|
// get conditions and iterate
|
||||||
|
const parentSetRuleConditions = parentSetRule?.conditions as Record<
|
||||||
|
string,
|
||||||
|
TPermissionConditionShape | string
|
||||||
|
>;
|
||||||
|
if (!parentSetRuleConditions) return true;
|
||||||
|
return Object.keys(parentSetRuleConditions).every((parentConditionField) => {
|
||||||
|
// if parent condition is missing then it's never a subset
|
||||||
|
if (!subsetRuleConditions?.[parentConditionField]) return false;
|
||||||
|
|
||||||
|
// standardize the conditions plain string operator => $eq function
|
||||||
|
const parentRuleConditionOperators = formatConditionOperator(parentSetRuleConditions[parentConditionField]);
|
||||||
|
const selectedSubsetRuleCondition = subsetRuleConditions?.[parentConditionField];
|
||||||
|
const subsetRuleConditionOperators = formatConditionOperator(selectedSubsetRuleCondition);
|
||||||
|
return isOperatorsASubset(parentRuleConditionOperators, subsetRuleConditionOperators);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
: true;
|
||||||
|
const isSubset = isSubsetOfNonInvertedParentSet && isNotSubsetOfInvertedParentSet;
|
||||||
|
if (!isSubset) {
|
||||||
|
appendToMissingPermission(subsetRule.conditions);
|
||||||
|
}
|
||||||
|
return isSubset;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validatePermissionBoundary = (parentSetPermissions: MongoAbility, subsetPermissions: MongoAbility) => {
|
||||||
|
const checkedPermissionRules = new Set<string>();
|
||||||
|
const missingPermissions: TMissingPermission[] = [];
|
||||||
|
|
||||||
|
subsetPermissions.rules.forEach((subsetPermissionRules) => {
|
||||||
|
const subsetPermissionSubject = subsetPermissionRules.subject.toString();
|
||||||
|
let subsetPermissionActions: string[] = [];
|
||||||
|
|
||||||
|
// actions can be string or string[]
|
||||||
|
if (typeof subsetPermissionRules.action === "string") {
|
||||||
|
subsetPermissionActions.push(subsetPermissionRules.action);
|
||||||
|
} else {
|
||||||
|
subsetPermissionRules.action.forEach((subsetPermissionAction) => {
|
||||||
|
subsetPermissionActions.push(subsetPermissionAction);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// if action is already processed ignore
|
||||||
|
subsetPermissionActions = subsetPermissionActions.filter(
|
||||||
|
(el) => !checkedPermissionRules.has(getPermissionSetID(el, subsetPermissionSubject))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!subsetPermissionActions.length) return;
|
||||||
|
subsetPermissionActions.forEach((subsetPermissionAction) => {
|
||||||
|
const parentSetRulesOfSubset = parentSetPermissions.possibleRulesFor(
|
||||||
|
subsetPermissionAction,
|
||||||
|
subsetPermissionSubject
|
||||||
|
);
|
||||||
|
const nonInveretedOnes = parentSetRulesOfSubset.filter((el) => !el.inverted);
|
||||||
|
if (!nonInveretedOnes.length) {
|
||||||
|
missingPermissions.push({ action: subsetPermissionAction, subject: subsetPermissionSubject });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subsetRules = subsetPermissions.possibleRulesFor(subsetPermissionAction, subsetPermissionSubject);
|
||||||
|
isSubsetForSamePermissionSubjectAction(parentSetRulesOfSubset, subsetRules, (conditions) => {
|
||||||
|
missingPermissions.push({ action: subsetPermissionAction, subject: subsetPermissionSubject, conditions });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
subsetPermissionActions.forEach((el) =>
|
||||||
|
checkedPermissionRules.add(getPermissionSetID(el, subsetPermissionSubject))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (missingPermissions.length) {
|
||||||
|
return { isValid: false as const, missingPermissions };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isValid: true };
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import { buildMongoQueryMatcher, MongoAbility } from "@casl/ability";
|
import { buildMongoQueryMatcher } from "@casl/ability";
|
||||||
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
|
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
|
||||||
import picomatch from "picomatch";
|
import picomatch from "picomatch";
|
||||||
|
|
||||||
@ -20,45 +20,8 @@ const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
|
|||||||
|
|
||||||
export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
|
export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts and formats permissions from a CASL Ability object or a raw permission set.
|
|
||||||
*/
|
|
||||||
const extractPermissions = (ability: MongoAbility) => {
|
|
||||||
const permissions: string[] = [];
|
|
||||||
ability.rules.forEach((permission) => {
|
|
||||||
if (typeof permission.action === "string") {
|
|
||||||
permissions.push(`${permission.action}_${permission.subject as string}`);
|
|
||||||
} else {
|
|
||||||
permission.action.forEach((permissionAction) => {
|
|
||||||
permissions.push(`${permissionAction}_${permission.subject as string}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return permissions;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compares two sets of permissions to determine if the first set is at least as privileged as the second set.
|
|
||||||
* The function checks if all permissions in the second set are contained within the first set and if the first set has equal or more permissions.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const isAtLeastAsPrivileged = (permissions1: MongoAbility, permissions2: MongoAbility) => {
|
|
||||||
const set1 = new Set(extractPermissions(permissions1));
|
|
||||||
const set2 = new Set(extractPermissions(permissions2));
|
|
||||||
|
|
||||||
for (const perm of set2) {
|
|
||||||
if (!set1.has(perm)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return set1.size >= set2.size;
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum PermissionConditionOperators {
|
export enum PermissionConditionOperators {
|
||||||
$IN = "$in",
|
$IN = "$in",
|
||||||
$ALL = "$all",
|
|
||||||
$REGEX = "$regex",
|
|
||||||
$EQ = "$eq",
|
$EQ = "$eq",
|
||||||
$NEQ = "$ne",
|
$NEQ = "$ne",
|
||||||
$GLOB = "$glob"
|
$GLOB = "$glob"
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
/* eslint-disable max-classes-per-file */
|
/* eslint-disable max-classes-per-file */
|
||||||
|
|
||||||
export class DatabaseError extends Error {
|
export class DatabaseError extends Error {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@ -52,10 +53,18 @@ export class ForbiddenRequestError extends Error {
|
|||||||
|
|
||||||
error: unknown;
|
error: unknown;
|
||||||
|
|
||||||
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown } = {}) {
|
details?: unknown;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
name,
|
||||||
|
error,
|
||||||
|
message,
|
||||||
|
details
|
||||||
|
}: { message?: string; name?: string; error?: unknown; details?: unknown } = {}) {
|
||||||
super(message ?? "You are not allowed to access this resource");
|
super(message ?? "You are not allowed to access this resource");
|
||||||
this.name = name || "ForbiddenError";
|
this.name = name || "ForbiddenError";
|
||||||
this.error = error;
|
this.error = error;
|
||||||
|
this.details = details;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,6 +96,7 @@ export const pingGatewayAndVerify = async ({
|
|||||||
error: err as Error
|
error: err as Error
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= maxRetries; attempt += 1) {
|
for (let attempt = 1; attempt <= maxRetries; attempt += 1) {
|
||||||
try {
|
try {
|
||||||
const stream = quicClient.connection.newStream("bidi");
|
const stream = quicClient.connection.newStream("bidi");
|
||||||
@ -108,17 +109,13 @@ export const pingGatewayAndVerify = async ({
|
|||||||
const { value, done } = await reader.read();
|
const { value, done } = await reader.read();
|
||||||
|
|
||||||
if (done) {
|
if (done) {
|
||||||
throw new BadRequestError({
|
throw new Error("Gateway closed before receiving PONG");
|
||||||
message: "Gateway closed before receiving PONG"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = Buffer.from(value).toString();
|
const response = Buffer.from(value).toString();
|
||||||
|
|
||||||
if (response !== "PONG\n" && response !== "PONG") {
|
if (response !== "PONG\n" && response !== "PONG") {
|
||||||
throw new BadRequestError({
|
throw new Error(`Failed to Ping. Unexpected response: ${response}`);
|
||||||
message: `Failed to Ping. Unexpected response: ${response}`
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reader.releaseLock();
|
reader.releaseLock();
|
||||||
@ -146,6 +143,7 @@ interface TProxyServer {
|
|||||||
server: net.Server;
|
server: net.Server;
|
||||||
port: number;
|
port: number;
|
||||||
cleanup: () => Promise<void>;
|
cleanup: () => Promise<void>;
|
||||||
|
getProxyError: () => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const setupProxyServer = async ({
|
const setupProxyServer = async ({
|
||||||
@ -170,6 +168,7 @@ const setupProxyServer = async ({
|
|||||||
error: err as Error
|
error: err as Error
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
const proxyErrorMsg = [""];
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const server = net.createServer();
|
const server = net.createServer();
|
||||||
@ -185,31 +184,33 @@ const setupProxyServer = async ({
|
|||||||
const forwardWriter = stream.writable.getWriter();
|
const forwardWriter = stream.writable.getWriter();
|
||||||
await forwardWriter.write(Buffer.from(`FORWARD-TCP ${targetHost}:${targetPort}\n`));
|
await forwardWriter.write(Buffer.from(`FORWARD-TCP ${targetHost}:${targetPort}\n`));
|
||||||
forwardWriter.releaseLock();
|
forwardWriter.releaseLock();
|
||||||
/* eslint-disable @typescript-eslint/no-misused-promises */
|
|
||||||
// Set up bidirectional copy
|
// Set up bidirectional copy
|
||||||
const setupCopy = async () => {
|
const setupCopy = () => {
|
||||||
// Client to QUIC
|
// Client to QUIC
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
const writer = stream.writable.getWriter();
|
||||||
const writer = stream.writable.getWriter();
|
|
||||||
|
|
||||||
// Create a handler for client data
|
// Create a handler for client data
|
||||||
clientConn.on("data", async (chunk) => {
|
clientConn.on("data", (chunk) => {
|
||||||
await writer.write(chunk);
|
writer.write(chunk).catch((err) => {
|
||||||
|
proxyErrorMsg.push((err as Error)?.message);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Handle client connection close
|
// Handle client connection close
|
||||||
clientConn.on("end", async () => {
|
clientConn.on("end", () => {
|
||||||
await writer.close();
|
writer.close().catch((err) => {
|
||||||
|
logger.error(err);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
clientConn.on("error", async (err) => {
|
clientConn.on("error", (clientConnErr) => {
|
||||||
await writer.abort(err);
|
writer.abort(clientConnErr?.message).catch((err) => {
|
||||||
|
proxyErrorMsg.push((err as Error)?.message);
|
||||||
});
|
});
|
||||||
} catch (err) {
|
});
|
||||||
clientConn.destroy();
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// QUIC to Client
|
// QUIC to Client
|
||||||
@ -238,15 +239,18 @@ const setupProxyServer = async ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
proxyErrorMsg.push((err as Error)?.message);
|
||||||
clientConn.destroy();
|
clientConn.destroy();
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
};
|
};
|
||||||
await setupCopy();
|
|
||||||
//
|
setupCopy();
|
||||||
// Handle connection closure
|
// Handle connection closure
|
||||||
clientConn.on("close", async () => {
|
clientConn.on("close", () => {
|
||||||
await stream.destroy();
|
stream.destroy().catch((err) => {
|
||||||
|
proxyErrorMsg.push((err as Error)?.message);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const cleanup = async () => {
|
const cleanup = async () => {
|
||||||
@ -254,13 +258,18 @@ const setupProxyServer = async ({
|
|||||||
await stream.destroy();
|
await stream.destroy();
|
||||||
};
|
};
|
||||||
|
|
||||||
clientConn.on("error", (err) => {
|
clientConn.on("error", (clientConnErr) => {
|
||||||
logger.error(err, "Client socket error");
|
logger.error(clientConnErr, "Client socket error");
|
||||||
void cleanup();
|
cleanup().catch((err) => {
|
||||||
reject(err);
|
logger.error(err, "Client conn cleanup");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
clientConn.on("end", cleanup);
|
clientConn.on("end", () => {
|
||||||
|
cleanup().catch((err) => {
|
||||||
|
logger.error(err, "Client conn end");
|
||||||
|
});
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err, "Failed to establish target connection:");
|
logger.error(err, "Failed to establish target connection:");
|
||||||
clientConn.end();
|
clientConn.end();
|
||||||
@ -272,12 +281,12 @@ const setupProxyServer = async ({
|
|||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on("close", async () => {
|
server.on("close", () => {
|
||||||
await quicClient?.destroy();
|
quicClient?.destroy().catch((err) => {
|
||||||
|
logger.error(err, "Failed to destroy quic client");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/* eslint-enable */
|
|
||||||
|
|
||||||
server.listen(0, () => {
|
server.listen(0, () => {
|
||||||
const address = server.address();
|
const address = server.address();
|
||||||
if (!address || typeof address === "string") {
|
if (!address || typeof address === "string") {
|
||||||
@ -293,7 +302,8 @@ const setupProxyServer = async ({
|
|||||||
cleanup: async () => {
|
cleanup: async () => {
|
||||||
server.close();
|
server.close();
|
||||||
await quicClient?.destroy();
|
await quicClient?.destroy();
|
||||||
}
|
},
|
||||||
|
getProxyError: () => proxyErrorMsg.join(",")
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -316,7 +326,7 @@ export const withGatewayProxy = async (
|
|||||||
const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId } = options;
|
const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId } = options;
|
||||||
|
|
||||||
// Setup the proxy server
|
// Setup the proxy server
|
||||||
const { port, cleanup } = await setupProxyServer({
|
const { port, cleanup, getProxyError } = await setupProxyServer({
|
||||||
targetHost,
|
targetHost,
|
||||||
targetPort,
|
targetPort,
|
||||||
relayPort,
|
relayPort,
|
||||||
@ -330,8 +340,12 @@ export const withGatewayProxy = async (
|
|||||||
// Execute the callback with the allocated port
|
// Execute the callback with the allocated port
|
||||||
await callback(port);
|
await callback(port);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err, "Failed to proxy");
|
const proxyErrorMessage = getProxyError();
|
||||||
throw new BadRequestError({ message: (err as Error)?.message });
|
if (proxyErrorMessage) {
|
||||||
|
logger.error(new Error(proxyErrorMessage), "Failed to proxy");
|
||||||
|
}
|
||||||
|
logger.error(err, "Failed to do gateway");
|
||||||
|
throw new BadRequestError({ message: proxyErrorMessage || (err as Error)?.message });
|
||||||
} finally {
|
} finally {
|
||||||
// Ensure cleanup happens regardless of success or failure
|
// Ensure cleanup happens regardless of success or failure
|
||||||
await cleanup();
|
await cleanup();
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
const TURN_TOKEN_TTL = 60 * 60 * 1000; // 24 hours in milliseconds
|
const TURN_TOKEN_TTL = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
|
||||||
export const getTurnCredentials = (id: string, authSecret: string, ttl = TURN_TOKEN_TTL) => {
|
export const getTurnCredentials = (id: string, authSecret: string, ttl = TURN_TOKEN_TTL) => {
|
||||||
const timestamp = Math.floor((Date.now() + ttl) / 1000);
|
const timestamp = Math.floor((Date.now() + ttl) / 1000);
|
||||||
const username = `${timestamp}:${id}`;
|
const username = `${timestamp}:${id}`;
|
||||||
|
@ -21,3 +21,10 @@ export const slugSchema = ({ min = 1, max = 32, field = "Slug" }: SlugSchemaInpu
|
|||||||
message: `${field} field can only contain lowercase letters, numbers, and hyphens`
|
message: `${field} field can only contain lowercase letters, numbers, and hyphens`
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const GenericResourceNameSchema = z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, { message: "Name must be at least 1 character" })
|
||||||
|
.max(64, { message: "Name must be 64 or fewer characters" })
|
||||||
|
.regex(/^[a-zA-Z0-9\-_\s]+$/, "Name can only contain alphanumeric characters, dashes, underscores, and spaces");
|
||||||
|
@ -122,7 +122,8 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
|||||||
reqId: req.id,
|
reqId: req.id,
|
||||||
statusCode: HttpStatusCodes.Forbidden,
|
statusCode: HttpStatusCodes.Forbidden,
|
||||||
message: error.message,
|
message: error.message,
|
||||||
error: error.name
|
error: error.name,
|
||||||
|
details: error?.details
|
||||||
});
|
});
|
||||||
} else if (error instanceof RateLimitError) {
|
} else if (error instanceof RateLimitError) {
|
||||||
void res.status(HttpStatusCodes.TooManyRequests).send({
|
void res.status(HttpStatusCodes.TooManyRequests).send({
|
||||||
|
@ -635,6 +635,7 @@ export const registerRoutes = async (
|
|||||||
});
|
});
|
||||||
const superAdminService = superAdminServiceFactory({
|
const superAdminService = superAdminServiceFactory({
|
||||||
userDAL,
|
userDAL,
|
||||||
|
identityDAL,
|
||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
authService: loginService,
|
authService: loginService,
|
||||||
serverCfgDAL: superAdminDAL,
|
serverCfgDAL: superAdminDAL,
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
ProjectRolesSchema,
|
ProjectRolesSchema,
|
||||||
ProjectsSchema,
|
ProjectsSchema,
|
||||||
SecretApprovalPoliciesSchema,
|
SecretApprovalPoliciesSchema,
|
||||||
|
SecretTagsSchema,
|
||||||
UsersSchema
|
UsersSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
@ -241,3 +242,11 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({
|
|||||||
kmsCertificateKeyId: true,
|
kmsCertificateKeyId: true,
|
||||||
auditLogsRetentionDays: true
|
auditLogsRetentionDays: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const SanitizedTagSchema = SecretTagsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
slug: true,
|
||||||
|
color: true
|
||||||
|
}).extend({
|
||||||
|
name: z.string()
|
||||||
|
});
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import DOMPurify from "isomorphic-dompurify";
|
import DOMPurify from "isomorphic-dompurify";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
|
import { IdentitiesSchema, OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
@ -118,7 +118,12 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
searchTerm: z.string().default(""),
|
searchTerm: z.string().default(""),
|
||||||
offset: z.coerce.number().default(0),
|
offset: z.coerce.number().default(0),
|
||||||
limit: z.coerce.number().max(100).default(20)
|
limit: z.coerce.number().max(100).default(20),
|
||||||
|
// TODO: remove this once z.coerce.boolean() is supported
|
||||||
|
adminsOnly: z
|
||||||
|
.string()
|
||||||
|
.transform((val) => val === "true")
|
||||||
|
.default("false")
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@ -149,6 +154,43 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/identity-management/identities",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
searchTerm: z.string().default(""),
|
||||||
|
offset: z.coerce.number().default(0),
|
||||||
|
limit: z.coerce.number().max(100).default(20)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
identities: IdentitiesSchema.pick({
|
||||||
|
name: true,
|
||||||
|
id: true
|
||||||
|
}).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: (req, res, done) => {
|
||||||
|
verifyAuth([AuthMode.JWT])(req, res, () => {
|
||||||
|
verifySuperAdmin(req, res, done);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const identities = await server.services.superAdmin.getIdentities({
|
||||||
|
...req.query
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
identities
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/integrations/slack/config",
|
url: "/integrations/slack/config",
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas";
|
import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema } from "@app/db/schemas";
|
||||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionDynamicSecretActions,
|
ProjectPermissionDynamicSecretActions,
|
||||||
|
ProjectPermissionSecretActions,
|
||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { DASHBOARD } from "@app/lib/api-docs";
|
import { DASHBOARD } from "@app/lib/api-docs";
|
||||||
@ -15,7 +16,7 @@ import { secretsLimit } from "@app/server/config/rateLimiter";
|
|||||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { SanitizedDynamicSecretSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
import { SanitizedDynamicSecretSchema, SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
@ -116,16 +117,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
dynamicSecrets: SanitizedDynamicSecretSchema.extend({ environment: z.string() }).array().optional(),
|
dynamicSecrets: SanitizedDynamicSecretSchema.extend({ environment: z.string() }).array().optional(),
|
||||||
secrets: secretRawSchema
|
secrets: secretRawSchema
|
||||||
.extend({
|
.extend({
|
||||||
|
secretValueHidden: z.boolean(),
|
||||||
secretPath: z.string().optional(),
|
secretPath: z.string().optional(),
|
||||||
secretMetadata: ResourceMetadataSchema.optional(),
|
secretMetadata: ResourceMetadataSchema.optional(),
|
||||||
tags: SecretTagsSchema.pick({
|
tags: SanitizedTagSchema.array().optional()
|
||||||
id: true,
|
|
||||||
slug: true,
|
|
||||||
color: true
|
|
||||||
})
|
|
||||||
.extend({ name: z.string() })
|
|
||||||
.array()
|
|
||||||
.optional()
|
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.optional(),
|
.optional(),
|
||||||
@ -294,6 +289,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
|
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
|
||||||
secrets = await server.services.secret.getSecretsRawMultiEnv({
|
secrets = await server.services.secret.getSecretsRawMultiEnv({
|
||||||
|
viewSecretValue: true,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
@ -393,6 +389,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
.optional(),
|
.optional(),
|
||||||
search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(),
|
search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(),
|
||||||
tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(),
|
tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(),
|
||||||
|
viewSecretValue: booleanSchema.default(true),
|
||||||
includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
|
includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
|
||||||
includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
|
includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
|
||||||
includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
|
includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
|
||||||
@ -410,16 +407,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
dynamicSecrets: SanitizedDynamicSecretSchema.array().optional(),
|
dynamicSecrets: SanitizedDynamicSecretSchema.array().optional(),
|
||||||
secrets: secretRawSchema
|
secrets: secretRawSchema
|
||||||
.extend({
|
.extend({
|
||||||
|
secretValueHidden: z.boolean(),
|
||||||
secretPath: z.string().optional(),
|
secretPath: z.string().optional(),
|
||||||
secretMetadata: ResourceMetadataSchema.optional(),
|
secretMetadata: ResourceMetadataSchema.optional(),
|
||||||
tags: SecretTagsSchema.pick({
|
tags: SanitizedTagSchema.array().optional()
|
||||||
id: true,
|
|
||||||
slug: true,
|
|
||||||
color: true
|
|
||||||
})
|
|
||||||
.extend({ name: z.string() })
|
|
||||||
.array()
|
|
||||||
.optional()
|
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.optional(),
|
.optional(),
|
||||||
@ -601,23 +592,25 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
|
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
|
||||||
const secretsRaw = await server.services.secret.getSecretsRaw({
|
secrets = (
|
||||||
actorId: req.permission.id,
|
await server.services.secret.getSecretsRaw({
|
||||||
actor: req.permission.type,
|
actorId: req.permission.id,
|
||||||
actorOrgId: req.permission.orgId,
|
actor: req.permission.type,
|
||||||
environment,
|
viewSecretValue: req.query.viewSecretValue,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
throwOnMissingReadValuePermission: false,
|
||||||
projectId,
|
actorOrgId: req.permission.orgId,
|
||||||
path: secretPath,
|
environment,
|
||||||
orderBy,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
orderDirection,
|
projectId,
|
||||||
search,
|
path: secretPath,
|
||||||
limit: remainingLimit,
|
orderBy,
|
||||||
offset: adjustedOffset,
|
orderDirection,
|
||||||
tagSlugs: tags
|
search,
|
||||||
});
|
limit: remainingLimit,
|
||||||
|
offset: adjustedOffset,
|
||||||
secrets = secretsRaw.secrets;
|
tagSlugs: tags
|
||||||
|
})
|
||||||
|
).secrets;
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
projectId,
|
projectId,
|
||||||
@ -696,16 +689,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
.optional(),
|
.optional(),
|
||||||
secrets: secretRawSchema
|
secrets: secretRawSchema
|
||||||
.extend({
|
.extend({
|
||||||
|
secretValueHidden: z.boolean(),
|
||||||
secretPath: z.string().optional(),
|
secretPath: z.string().optional(),
|
||||||
secretMetadata: ResourceMetadataSchema.optional(),
|
secretMetadata: ResourceMetadataSchema.optional(),
|
||||||
tags: SecretTagsSchema.pick({
|
tags: SanitizedTagSchema.array().optional()
|
||||||
id: true,
|
|
||||||
slug: true,
|
|
||||||
color: true
|
|
||||||
})
|
|
||||||
.extend({ name: z.string() })
|
|
||||||
.array()
|
|
||||||
.optional()
|
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.optional()
|
.optional()
|
||||||
@ -749,6 +736,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
const secrets = await server.services.secret.getSecretsRawByFolderMappings(
|
const secrets = await server.services.secret.getSecretsRawByFolderMappings(
|
||||||
{
|
{
|
||||||
|
filterByAction: ProjectPermissionSecretActions.DescribeSecret,
|
||||||
projectId,
|
projectId,
|
||||||
folderMappings,
|
folderMappings,
|
||||||
filters: {
|
filters: {
|
||||||
@ -846,6 +834,52 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/accessible-secrets",
|
||||||
|
config: {
|
||||||
|
rateLimit: secretsLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
projectId: z.string().trim(),
|
||||||
|
environment: z.string().trim(),
|
||||||
|
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||||
|
filterByAction: z
|
||||||
|
.enum([ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue])
|
||||||
|
.default(ProjectPermissionSecretActions.ReadValue)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secrets: secretRawSchema
|
||||||
|
.extend({
|
||||||
|
secretPath: z.string().optional(),
|
||||||
|
secretValueHidden: z.boolean()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { projectId, environment, secretPath, filterByAction } = req.query;
|
||||||
|
|
||||||
|
const { secrets } = await server.services.secret.getAccessibleSecrets({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
projectId,
|
||||||
|
filterByAction
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secrets };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/secrets-by-keys",
|
url: "/secrets-by-keys",
|
||||||
@ -862,22 +896,17 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
projectId: z.string().trim(),
|
projectId: z.string().trim(),
|
||||||
environment: z.string().trim(),
|
environment: z.string().trim(),
|
||||||
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
|
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||||
keys: z.string().trim().transform(decodeURIComponent)
|
keys: z.string().trim().transform(decodeURIComponent),
|
||||||
|
viewSecretValue: booleanSchema.default(false)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
secrets: secretRawSchema
|
secrets: secretRawSchema
|
||||||
.extend({
|
.extend({
|
||||||
|
secretValueHidden: z.boolean(),
|
||||||
secretPath: z.string().optional(),
|
secretPath: z.string().optional(),
|
||||||
secretMetadata: ResourceMetadataSchema.optional(),
|
secretMetadata: ResourceMetadataSchema.optional(),
|
||||||
tags: SecretTagsSchema.pick({
|
tags: SanitizedTagSchema.array().optional()
|
||||||
id: true,
|
|
||||||
slug: true,
|
|
||||||
color: true
|
|
||||||
})
|
|
||||||
.extend({ name: z.string() })
|
|
||||||
.array()
|
|
||||||
.optional()
|
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.optional()
|
.optional()
|
||||||
@ -886,7 +915,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { secretPath, projectId, environment } = req.query;
|
const { secretPath, projectId, environment, viewSecretValue } = req.query;
|
||||||
|
|
||||||
const keys = req.query.keys?.split(",").filter((key) => Boolean(key.trim())) ?? [];
|
const keys = req.query.keys?.split(",").filter((key) => Boolean(key.trim())) ?? [];
|
||||||
if (!keys.length) throw new BadRequestError({ message: "One or more keys required" });
|
if (!keys.length) throw new BadRequestError({ message: "One or more keys required" });
|
||||||
@ -895,6 +924,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
|
viewSecretValue,
|
||||||
environment,
|
environment,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
projectId,
|
projectId,
|
||||||
|
@ -91,7 +91,6 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
await projectRouter.register(registerProjectMembershipRouter);
|
await projectRouter.register(registerProjectMembershipRouter);
|
||||||
await projectRouter.register(registerSecretTagRouter);
|
await projectRouter.register(registerSecretTagRouter);
|
||||||
},
|
},
|
||||||
|
|
||||||
{ prefix: "/workspace" }
|
{ prefix: "/workspace" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-t
|
|||||||
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
|
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
|
||||||
import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
|
import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { slugSchema } from "@app/server/lib/schemas";
|
import { GenericResourceNameSchema, slugSchema } from "@app/server/lib/schemas";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
||||||
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
|
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
|
||||||
@ -251,7 +251,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
params: z.object({ organizationId: z.string().trim() }),
|
params: z.object({ organizationId: z.string().trim() }),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().trim().max(64, { message: "Name must be 64 or fewer characters" }).optional(),
|
name: GenericResourceNameSchema.optional(),
|
||||||
slug: slugSchema({ max: 64 }).optional(),
|
slug: slugSchema({ max: 64 }).optional(),
|
||||||
authEnforced: z.boolean().optional(),
|
authEnforced: z.boolean().optional(),
|
||||||
scimEnabled: z.boolean().optional(),
|
scimEnabled: z.boolean().optional(),
|
||||||
|
@ -6,6 +6,7 @@ import { authRateLimit } from "@app/server/config/rateLimiter";
|
|||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { validateSignUpAuthorization } from "@app/services/auth/auth-fns";
|
import { validateSignUpAuthorization } from "@app/services/auth/auth-fns";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { UserEncryption } from "@app/services/user/user-types";
|
||||||
|
|
||||||
export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@ -113,20 +114,16 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
message: z.string(),
|
|
||||||
user: UsersSchema,
|
user: UsersSchema,
|
||||||
token: z.string()
|
token: z.string(),
|
||||||
|
userEncryptionVersion: z.nativeEnum(UserEncryption)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { token, user } = await server.services.password.verifyPasswordResetEmail(req.body.email, req.body.code);
|
const passwordReset = await server.services.password.verifyPasswordResetEmail(req.body.email, req.body.code);
|
||||||
|
|
||||||
return {
|
return passwordReset;
|
||||||
message: "Successfully verified email",
|
|
||||||
user,
|
|
||||||
token
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,10 +2,12 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
IntegrationsSchema,
|
IntegrationsSchema,
|
||||||
|
ProjectEnvironmentsSchema,
|
||||||
ProjectMembershipsSchema,
|
ProjectMembershipsSchema,
|
||||||
ProjectRolesSchema,
|
ProjectRolesSchema,
|
||||||
ProjectSlackConfigsSchema,
|
ProjectSlackConfigsSchema,
|
||||||
ProjectType,
|
ProjectType,
|
||||||
|
SecretFoldersSchema,
|
||||||
UserEncryptionKeysSchema,
|
UserEncryptionKeysSchema,
|
||||||
UsersSchema
|
UsersSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
@ -307,7 +309,17 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
.max(256, { message: "Description must be 256 or fewer characters" })
|
.max(256, { message: "Description must be 256 or fewer characters" })
|
||||||
.optional()
|
.optional()
|
||||||
.describe(PROJECTS.UPDATE.projectDescription),
|
.describe(PROJECTS.UPDATE.projectDescription),
|
||||||
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization)
|
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization),
|
||||||
|
slug: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.regex(
|
||||||
|
/^[a-z0-9]+(?:[_-][a-z0-9]+)*$/,
|
||||||
|
"Project slug can only contain lowercase letters and numbers, with optional single hyphens (-) or underscores (_) between words. Cannot start or end with a hyphen or underscore."
|
||||||
|
)
|
||||||
|
.max(64, { message: "Slug must be 64 characters or fewer" })
|
||||||
|
.optional()
|
||||||
|
.describe(PROJECTS.UPDATE.slug)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@ -325,7 +337,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
update: {
|
update: {
|
||||||
name: req.body.name,
|
name: req.body.name,
|
||||||
description: req.body.description,
|
description: req.body.description,
|
||||||
autoCapitalization: req.body.autoCapitalization
|
autoCapitalization: req.body.autoCapitalization,
|
||||||
|
slug: req.body.slug
|
||||||
},
|
},
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
@ -664,4 +677,31 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
return slackConfig;
|
return slackConfig;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:workspaceId/environment-folder-tree",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.record(
|
||||||
|
ProjectEnvironmentsSchema.extend({ folders: SecretFoldersSchema.extend({ path: z.string() }).array() })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const environmentsFolders = await server.services.folder.getProjectEnvironmentsFolders(
|
||||||
|
req.params.workspaceId,
|
||||||
|
req.permission
|
||||||
|
);
|
||||||
|
|
||||||
|
return environmentsFolders;
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -3,6 +3,7 @@ import { registerIdentityOrgRouter } from "./identity-org-router";
|
|||||||
import { registerIdentityProjectRouter } from "./identity-project-router";
|
import { registerIdentityProjectRouter } from "./identity-project-router";
|
||||||
import { registerMfaRouter } from "./mfa-router";
|
import { registerMfaRouter } from "./mfa-router";
|
||||||
import { registerOrgRouter } from "./organization-router";
|
import { registerOrgRouter } from "./organization-router";
|
||||||
|
import { registerPasswordRouter } from "./password-router";
|
||||||
import { registerProjectMembershipRouter } from "./project-membership-router";
|
import { registerProjectMembershipRouter } from "./project-membership-router";
|
||||||
import { registerProjectRouter } from "./project-router";
|
import { registerProjectRouter } from "./project-router";
|
||||||
import { registerServiceTokenRouter } from "./service-token-router";
|
import { registerServiceTokenRouter } from "./service-token-router";
|
||||||
@ -12,6 +13,7 @@ export const registerV2Routes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerMfaRouter, { prefix: "/auth" });
|
await server.register(registerMfaRouter, { prefix: "/auth" });
|
||||||
await server.register(registerUserRouter, { prefix: "/users" });
|
await server.register(registerUserRouter, { prefix: "/users" });
|
||||||
await server.register(registerServiceTokenRouter, { prefix: "/service-token" });
|
await server.register(registerServiceTokenRouter, { prefix: "/service-token" });
|
||||||
|
await server.register(registerPasswordRouter, { prefix: "/password" });
|
||||||
await server.register(
|
await server.register(
|
||||||
async (orgRouter) => {
|
async (orgRouter) => {
|
||||||
await orgRouter.register(registerOrgRouter);
|
await orgRouter.register(registerOrgRouter);
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
import { ORGANIZATIONS } from "@app/lib/api-docs";
|
import { ORGANIZATIONS } from "@app/lib/api-docs";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { GenericResourceNameSchema } from "@app/server/lib/schemas";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
@ -330,7 +331,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().trim()
|
name: GenericResourceNameSchema
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
53
backend/src/server/routes/v2/password-router.ts
Normal file
53
backend/src/server/routes/v2/password-router.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns";
|
||||||
|
import { ResetPasswordV2Type } from "@app/services/auth/auth-password-type";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/password-reset",
|
||||||
|
config: {
|
||||||
|
rateLimit: authRateLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
newPassword: z.string().trim()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const token = validatePasswordResetAuthorization(req.headers.authorization);
|
||||||
|
await server.services.password.resetPasswordV2({
|
||||||
|
type: ResetPasswordV2Type.Recovery,
|
||||||
|
newPassword: req.body.newPassword,
|
||||||
|
userId: token.userId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/user/password-reset",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
oldPassword: z.string().trim(),
|
||||||
|
newPassword: z.string().trim()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
rateLimit: authRateLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.password.resetPasswordV2({
|
||||||
|
type: ResetPasswordV2Type.LoggedInReset,
|
||||||
|
userId: req.permission.id,
|
||||||
|
newPassword: req.body.newPassword,
|
||||||
|
oldPassword: req.body.oldPassword
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -1,13 +1,7 @@
|
|||||||
import picomatch from "picomatch";
|
import picomatch from "picomatch";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import {
|
import { SecretApprovalRequestsSchema, SecretsSchema, SecretType, ServiceTokenScopes } from "@app/db/schemas";
|
||||||
SecretApprovalRequestsSchema,
|
|
||||||
SecretsSchema,
|
|
||||||
SecretTagsSchema,
|
|
||||||
SecretType,
|
|
||||||
ServiceTokenScopes
|
|
||||||
} from "@app/db/schemas";
|
|
||||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
|
import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
@ -23,7 +17,7 @@ import { SecretOperations, SecretProtectionType } from "@app/services/secret/sec
|
|||||||
import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
|
import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
|
||||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
import { secretRawSchema } from "../sanitizedSchemas";
|
import { SanitizedTagSchema, secretRawSchema } from "../sanitizedSchemas";
|
||||||
|
|
||||||
const SecretReferenceNode = z.object({
|
const SecretReferenceNode = z.object({
|
||||||
key: z.string(),
|
key: z.string(),
|
||||||
@ -31,6 +25,14 @@ const SecretReferenceNode = z.object({
|
|||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
secretPath: z.string()
|
secretPath: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const convertStringBoolean = (defaultValue: boolean = false) => {
|
||||||
|
return z
|
||||||
|
.enum(["true", "false"])
|
||||||
|
.default(defaultValue ? "true" : "false")
|
||||||
|
.transform((value) => value === "true");
|
||||||
|
};
|
||||||
|
|
||||||
type TSecretReferenceNode = z.infer<typeof SecretReferenceNode> & { children: TSecretReferenceNode[] };
|
type TSecretReferenceNode = z.infer<typeof SecretReferenceNode> & { children: TSecretReferenceNode[] };
|
||||||
|
|
||||||
const SecretReferenceNodeTree: z.ZodType<TSecretReferenceNode> = SecretReferenceNode.extend({
|
const SecretReferenceNodeTree: z.ZodType<TSecretReferenceNode> = SecretReferenceNode.extend({
|
||||||
@ -75,17 +77,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge(
|
secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({
|
||||||
z.object({
|
tags: SanitizedTagSchema.array()
|
||||||
tags: SecretTagsSchema.pick({
|
})
|
||||||
id: true,
|
|
||||||
slug: true,
|
|
||||||
color: true
|
|
||||||
})
|
|
||||||
.extend({ name: z.string() })
|
|
||||||
.array()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -139,13 +133,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({
|
secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({
|
||||||
tags: SecretTagsSchema.pick({
|
tags: SanitizedTagSchema.array()
|
||||||
id: true,
|
|
||||||
slug: true,
|
|
||||||
color: true
|
|
||||||
})
|
|
||||||
.extend({ name: z.string() })
|
|
||||||
.array()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -247,21 +235,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug),
|
workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug),
|
||||||
environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment),
|
environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment),
|
||||||
secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath),
|
secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath),
|
||||||
expandSecretReferences: z
|
viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.LIST.viewSecretValue),
|
||||||
.enum(["true", "false"])
|
expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.LIST.expand),
|
||||||
.default("false")
|
recursive: convertStringBoolean().describe(RAW_SECRETS.LIST.recursive),
|
||||||
.transform((value) => value === "true")
|
include_imports: convertStringBoolean().describe(RAW_SECRETS.LIST.includeImports),
|
||||||
.describe(RAW_SECRETS.LIST.expand),
|
|
||||||
recursive: z
|
|
||||||
.enum(["true", "false"])
|
|
||||||
.default("false")
|
|
||||||
.transform((value) => value === "true")
|
|
||||||
.describe(RAW_SECRETS.LIST.recursive),
|
|
||||||
include_imports: z
|
|
||||||
.enum(["true", "false"])
|
|
||||||
.default("false")
|
|
||||||
.transform((value) => value === "true")
|
|
||||||
.describe(RAW_SECRETS.LIST.includeImports),
|
|
||||||
tagSlugs: z
|
tagSlugs: z
|
||||||
.string()
|
.string()
|
||||||
.describe(RAW_SECRETS.LIST.tagSlugs)
|
.describe(RAW_SECRETS.LIST.tagSlugs)
|
||||||
@ -274,15 +251,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
secrets: secretRawSchema
|
secrets: secretRawSchema
|
||||||
.extend({
|
.extend({
|
||||||
secretPath: z.string().optional(),
|
secretPath: z.string().optional(),
|
||||||
|
secretValueHidden: z.boolean(),
|
||||||
secretMetadata: ResourceMetadataSchema.optional(),
|
secretMetadata: ResourceMetadataSchema.optional(),
|
||||||
tags: SecretTagsSchema.pick({
|
tags: SanitizedTagSchema.array().optional()
|
||||||
id: true,
|
|
||||||
slug: true,
|
|
||||||
color: true
|
|
||||||
})
|
|
||||||
.extend({ name: z.string() })
|
|
||||||
.array()
|
|
||||||
.optional()
|
|
||||||
})
|
})
|
||||||
.array(),
|
.array(),
|
||||||
imports: z
|
imports: z
|
||||||
@ -293,6 +264,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
secrets: secretRawSchema
|
secrets: secretRawSchema
|
||||||
.omit({ createdAt: true, updatedAt: true })
|
.omit({ createdAt: true, updatedAt: true })
|
||||||
.extend({
|
.extend({
|
||||||
|
secretValueHidden: z.boolean(),
|
||||||
secretMetadata: ResourceMetadataSchema.optional()
|
secretMetadata: ResourceMetadataSchema.optional()
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
@ -342,6 +314,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
expandSecretReferences: req.query.expandSecretReferences,
|
expandSecretReferences: req.query.expandSecretReferences,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
projectId: workspaceId,
|
projectId: workspaceId,
|
||||||
|
viewSecretValue: req.query.viewSecretValue,
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
metadataFilter: req.query.metadataFilter,
|
metadataFilter: req.query.metadataFilter,
|
||||||
includeImports: req.query.include_imports,
|
includeImports: req.query.include_imports,
|
||||||
@ -376,6 +349,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { secrets, imports };
|
return { secrets, imports };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -394,14 +368,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
200: z.object({
|
200: z.object({
|
||||||
secret: secretRawSchema.extend({
|
secret: secretRawSchema.extend({
|
||||||
secretPath: z.string(),
|
secretPath: z.string(),
|
||||||
tags: SecretTagsSchema.pick({
|
tags: SanitizedTagSchema.array().optional(),
|
||||||
id: true,
|
|
||||||
slug: true,
|
|
||||||
color: true
|
|
||||||
})
|
|
||||||
.extend({ name: z.string() })
|
|
||||||
.array()
|
|
||||||
.optional(),
|
|
||||||
secretMetadata: ResourceMetadataSchema.optional()
|
secretMetadata: ResourceMetadataSchema.optional()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -445,28 +412,15 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath),
|
secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath),
|
||||||
version: z.coerce.number().optional().describe(RAW_SECRETS.GET.version),
|
version: z.coerce.number().optional().describe(RAW_SECRETS.GET.version),
|
||||||
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.GET.type),
|
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.GET.type),
|
||||||
expandSecretReferences: z
|
viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.GET.viewSecretValue),
|
||||||
.enum(["true", "false"])
|
expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.GET.expand),
|
||||||
.default("false")
|
include_imports: convertStringBoolean().describe(RAW_SECRETS.GET.includeImports)
|
||||||
.transform((value) => value === "true")
|
|
||||||
.describe(RAW_SECRETS.GET.expand),
|
|
||||||
include_imports: z
|
|
||||||
.enum(["true", "false"])
|
|
||||||
.default("false")
|
|
||||||
.transform((value) => value === "true")
|
|
||||||
.describe(RAW_SECRETS.GET.includeImports)
|
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
secret: secretRawSchema.extend({
|
secret: secretRawSchema.extend({
|
||||||
tags: SecretTagsSchema.pick({
|
secretValueHidden: z.boolean(),
|
||||||
id: true,
|
tags: SanitizedTagSchema.array().optional(),
|
||||||
slug: true,
|
|
||||||
color: true
|
|
||||||
})
|
|
||||||
.extend({ name: z.string() })
|
|
||||||
.array()
|
|
||||||
.optional(),
|
|
||||||
secretMetadata: ResourceMetadataSchema.optional()
|
secretMetadata: ResourceMetadataSchema.optional()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -498,6 +452,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
expandSecretReferences: req.query.expandSecretReferences,
|
expandSecretReferences: req.query.expandSecretReferences,
|
||||||
environment,
|
environment,
|
||||||
projectId: workspaceId,
|
projectId: workspaceId,
|
||||||
|
viewSecretValue: req.query.viewSecretValue,
|
||||||
projectSlug: workspaceSlug,
|
projectSlug: workspaceSlug,
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
secretName: req.params.secretName,
|
secretName: req.params.secretName,
|
||||||
@ -704,7 +659,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
response: {
|
response: {
|
||||||
200: z.union([
|
200: z.union([
|
||||||
z.object({
|
z.object({
|
||||||
secret: secretRawSchema
|
secret: secretRawSchema.extend({
|
||||||
|
secretValueHidden: z.boolean()
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
|
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
|
||||||
])
|
])
|
||||||
@ -800,7 +757,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
response: {
|
response: {
|
||||||
200: z.union([
|
200: z.union([
|
||||||
z.object({
|
z.object({
|
||||||
secret: secretRawSchema
|
secret: secretRawSchema.extend({
|
||||||
|
secretValueHidden: z.boolean()
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
|
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
|
||||||
])
|
])
|
||||||
@ -822,6 +781,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
if (secretOperation.type === SecretProtectionType.Approval) {
|
if (secretOperation.type === SecretProtectionType.Approval) {
|
||||||
return { approval: secretOperation.approval };
|
return { approval: secretOperation.approval };
|
||||||
}
|
}
|
||||||
|
|
||||||
const { secret } = secretOperation;
|
const { secret } = secretOperation;
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
@ -884,13 +844,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
workspace: z.string(),
|
workspace: z.string(),
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
secretPath: z.string().optional(),
|
secretPath: z.string().optional(),
|
||||||
tags: SecretTagsSchema.pick({
|
tags: SanitizedTagSchema.array()
|
||||||
id: true,
|
|
||||||
slug: true,
|
|
||||||
color: true
|
|
||||||
})
|
|
||||||
.extend({ name: z.string() })
|
|
||||||
.array()
|
|
||||||
})
|
})
|
||||||
.array(),
|
.array(),
|
||||||
imports: z
|
imports: z
|
||||||
@ -986,10 +940,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
|
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||||
type: z.nativeEnum(SecretType).default(SecretType.Shared),
|
type: z.nativeEnum(SecretType).default(SecretType.Shared),
|
||||||
version: z.coerce.number().optional(),
|
version: z.coerce.number().optional(),
|
||||||
include_imports: z
|
include_imports: convertStringBoolean()
|
||||||
.enum(["true", "false"])
|
|
||||||
.default("false")
|
|
||||||
.transform((value) => value === "true")
|
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@ -1260,6 +1211,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
z.object({
|
z.object({
|
||||||
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge(
|
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge(
|
||||||
z.object({
|
z.object({
|
||||||
|
secretValueHidden: z.boolean(),
|
||||||
_id: z.string(),
|
_id: z.string(),
|
||||||
workspace: z.string(),
|
workspace: z.string(),
|
||||||
environment: z.string()
|
environment: z.string()
|
||||||
@ -1429,13 +1381,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
response: {
|
response: {
|
||||||
200: z.union([
|
200: z.union([
|
||||||
z.object({
|
z.object({
|
||||||
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge(
|
secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({
|
||||||
z.object({
|
_id: z.string(),
|
||||||
_id: z.string(),
|
secretValueHidden: z.boolean(),
|
||||||
workspace: z.string(),
|
workspace: z.string(),
|
||||||
environment: z.string()
|
environment: z.string()
|
||||||
})
|
})
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
|
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
|
||||||
])
|
])
|
||||||
@ -1747,7 +1698,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
response: {
|
response: {
|
||||||
200: z.union([
|
200: z.union([
|
||||||
z.object({
|
z.object({
|
||||||
secrets: SecretsSchema.omit({ secretBlindIndex: true }).array()
|
secrets: SecretsSchema.omit({ secretBlindIndex: true }).extend({ secretValueHidden: z.boolean() }).array()
|
||||||
}),
|
}),
|
||||||
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
|
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
|
||||||
])
|
])
|
||||||
@ -1862,7 +1813,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
response: {
|
response: {
|
||||||
200: z.union([
|
200: z.union([
|
||||||
z.object({
|
z.object({
|
||||||
secrets: SecretsSchema.omit({ secretBlindIndex: true }).array()
|
secrets: SecretsSchema.omit({ secretBlindIndex: true })
|
||||||
|
.extend({
|
||||||
|
secretValueHidden: z.boolean()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
}),
|
}),
|
||||||
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
|
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
|
||||||
])
|
])
|
||||||
@ -2124,7 +2079,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
response: {
|
response: {
|
||||||
200: z.union([
|
200: z.union([
|
||||||
z.object({
|
z.object({
|
||||||
secrets: secretRawSchema.array()
|
secrets: secretRawSchema.extend({ secretValueHidden: z.boolean() }).array()
|
||||||
}),
|
}),
|
||||||
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
|
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
|
||||||
])
|
])
|
||||||
@ -2246,7 +2201,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
response: {
|
response: {
|
||||||
200: z.union([
|
200: z.union([
|
||||||
z.object({
|
z.object({
|
||||||
secrets: secretRawSchema.array()
|
secrets: secretRawSchema
|
||||||
|
.extend({
|
||||||
|
secretValueHidden: z.boolean()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
}),
|
}),
|
||||||
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
|
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
|
||||||
])
|
])
|
||||||
|
@ -4,6 +4,7 @@ import { UsersSchema } from "@app/db/schemas";
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { ForbiddenRequestError } from "@app/lib/errors";
|
import { ForbiddenRequestError } from "@app/lib/errors";
|
||||||
import { authRateLimit } from "@app/server/config/rateLimiter";
|
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { GenericResourceNameSchema } from "@app/server/lib/schemas";
|
||||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
@ -100,7 +101,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
|||||||
encryptedPrivateKeyTag: z.string().trim(),
|
encryptedPrivateKeyTag: z.string().trim(),
|
||||||
salt: z.string().trim(),
|
salt: z.string().trim(),
|
||||||
verifier: z.string().trim(),
|
verifier: z.string().trim(),
|
||||||
organizationName: z.string().trim().min(1),
|
organizationName: GenericResourceNameSchema,
|
||||||
providerAuthToken: z.string().trim().optional().nullish(),
|
providerAuthToken: z.string().trim().optional().nullish(),
|
||||||
attributionSource: z.string().trim().optional(),
|
attributionSource: z.string().trim().optional(),
|
||||||
password: z.string()
|
password: z.string()
|
||||||
|
@ -45,6 +45,36 @@ export const validateSignUpAuthorization = (token: string, userId: string, valid
|
|||||||
if (decodedToken.userId !== userId) throw new UnauthorizedError();
|
if (decodedToken.userId !== userId) throw new UnauthorizedError();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const validatePasswordResetAuthorization = (token?: string) => {
|
||||||
|
if (!token) throw new UnauthorizedError();
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>token?.split(" ", 2) ?? [null, null];
|
||||||
|
if (AUTH_TOKEN_TYPE === null) {
|
||||||
|
throw new UnauthorizedError({ message: "Missing Authorization Header in the request header." });
|
||||||
|
}
|
||||||
|
if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") {
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (AUTH_TOKEN_VALUE === null) {
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: "Missing Authorization Body in the request header"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const decodedToken = jwt.verify(AUTH_TOKEN_VALUE, appCfg.AUTH_SECRET) as AuthModeProviderSignUpTokenPayload;
|
||||||
|
|
||||||
|
if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) {
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: `The provided authentication token type is not supported.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return decodedToken;
|
||||||
|
};
|
||||||
|
|
||||||
export const enforceUserLockStatus = (isLocked: boolean, temporaryLockDateEnd?: Date | null) => {
|
export const enforceUserLockStatus = (isLocked: boolean, temporaryLockDateEnd?: Date | null) => {
|
||||||
if (isLocked) {
|
if (isLocked) {
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
|
@ -4,7 +4,10 @@ import jwt from "jsonwebtoken";
|
|||||||
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||||
|
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
|
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
import { OrgServiceActor } from "@app/lib/types";
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
|
||||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||||
@ -12,10 +15,13 @@ import { TokenType } from "../auth-token/auth-token-types";
|
|||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
import { TTotpConfigDALFactory } from "../totp/totp-config-dal";
|
import { TTotpConfigDALFactory } from "../totp/totp-config-dal";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
|
import { UserEncryption } from "../user/user-types";
|
||||||
import { TAuthDALFactory } from "./auth-dal";
|
import { TAuthDALFactory } from "./auth-dal";
|
||||||
import {
|
import {
|
||||||
|
ResetPasswordV2Type,
|
||||||
TChangePasswordDTO,
|
TChangePasswordDTO,
|
||||||
TCreateBackupPrivateKeyDTO,
|
TCreateBackupPrivateKeyDTO,
|
||||||
|
TResetPasswordV2DTO,
|
||||||
TResetPasswordViaBackupKeyDTO,
|
TResetPasswordViaBackupKeyDTO,
|
||||||
TSetupPasswordViaBackupKeyDTO
|
TSetupPasswordViaBackupKeyDTO
|
||||||
} from "./auth-password-type";
|
} from "./auth-password-type";
|
||||||
@ -114,26 +120,31 @@ export const authPaswordServiceFactory = ({
|
|||||||
* Email password reset flow via email. Step 1 send email
|
* Email password reset flow via email. Step 1 send email
|
||||||
*/
|
*/
|
||||||
const sendPasswordResetEmail = async (email: string) => {
|
const sendPasswordResetEmail = async (email: string) => {
|
||||||
const user = await userDAL.findUserByUsername(email);
|
const sendEmail = async () => {
|
||||||
// ignore as user is not found to avoid an outside entity to identify infisical registered accounts
|
const user = await userDAL.findUserByUsername(email);
|
||||||
if (!user || (user && !user.isAccepted)) return;
|
|
||||||
|
|
||||||
const cfg = getConfig();
|
if (user && user.isAccepted) {
|
||||||
const token = await tokenService.createTokenForUser({
|
const cfg = getConfig();
|
||||||
type: TokenType.TOKEN_EMAIL_PASSWORD_RESET,
|
const token = await tokenService.createTokenForUser({
|
||||||
userId: user.id
|
type: TokenType.TOKEN_EMAIL_PASSWORD_RESET,
|
||||||
});
|
userId: user.id
|
||||||
|
});
|
||||||
|
|
||||||
await smtpService.sendMail({
|
await smtpService.sendMail({
|
||||||
template: SmtpTemplates.ResetPassword,
|
template: SmtpTemplates.ResetPassword,
|
||||||
recipients: [email],
|
recipients: [email],
|
||||||
subjectLine: "Infisical password reset",
|
subjectLine: "Infisical password reset",
|
||||||
substitutions: {
|
substitutions: {
|
||||||
email,
|
email,
|
||||||
token,
|
token,
|
||||||
callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : ""
|
callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : ""
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// note(daniel): run in background to prevent timing attacks
|
||||||
|
void sendEmail().catch((err) => logger.error(err, "Failed to send password reset email"));
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -142,6 +153,11 @@ export const authPaswordServiceFactory = ({
|
|||||||
const verifyPasswordResetEmail = async (email: string, code: string) => {
|
const verifyPasswordResetEmail = async (email: string, code: string) => {
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
const user = await userDAL.findUserByUsername(email);
|
const user = await userDAL.findUserByUsername(email);
|
||||||
|
|
||||||
|
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
|
||||||
|
|
||||||
|
if (!userEnc) throw new BadRequestError({ message: "Failed to find user encryption data" });
|
||||||
|
|
||||||
// ignore as user is not found to avoid an outside entity to identify infisical registered accounts
|
// ignore as user is not found to avoid an outside entity to identify infisical registered accounts
|
||||||
if (!user || (user && !user.isAccepted)) {
|
if (!user || (user && !user.isAccepted)) {
|
||||||
throw new Error("Failed email verification for pass reset");
|
throw new Error("Failed email verification for pass reset");
|
||||||
@ -162,8 +178,91 @@ export const authPaswordServiceFactory = ({
|
|||||||
{ expiresIn: cfg.JWT_SIGNUP_LIFETIME }
|
{ expiresIn: cfg.JWT_SIGNUP_LIFETIME }
|
||||||
);
|
);
|
||||||
|
|
||||||
return { token, user };
|
return { token, user, userEncryptionVersion: userEnc.encryptionVersion as UserEncryption };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetPasswordV2 = async ({ userId, newPassword, type, oldPassword }: TResetPasswordV2DTO) => {
|
||||||
|
const cfg = getConfig();
|
||||||
|
|
||||||
|
const user = await userDAL.findUserEncKeyByUserId(userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new BadRequestError({ message: `User encryption key not found for user with ID '${userId}'` });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.hashedPassword) {
|
||||||
|
throw new BadRequestError({ message: "Unable to reset password, no password is set" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.authMethods?.includes(AuthMethod.EMAIL)) {
|
||||||
|
throw new BadRequestError({ message: "Unable to reset password, no email authentication method is configured" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// we check the old password if the user is resetting their password while logged in
|
||||||
|
if (type === ResetPasswordV2Type.LoggedInReset) {
|
||||||
|
if (!oldPassword) {
|
||||||
|
throw new BadRequestError({ message: "Current password is required." });
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = await bcrypt.compare(oldPassword, user.hashedPassword);
|
||||||
|
if (!isValid) {
|
||||||
|
throw new BadRequestError({ message: "Incorrect current password." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newHashedPassword = await bcrypt.hash(newPassword, cfg.BCRYPT_SALT_ROUND);
|
||||||
|
|
||||||
|
// we need to get the original private key first for v2
|
||||||
|
let privateKey: string;
|
||||||
|
if (
|
||||||
|
user.serverEncryptedPrivateKey &&
|
||||||
|
user.serverEncryptedPrivateKeyTag &&
|
||||||
|
user.serverEncryptedPrivateKeyIV &&
|
||||||
|
user.serverEncryptedPrivateKeyEncoding &&
|
||||||
|
user.encryptionVersion === UserEncryption.V2
|
||||||
|
) {
|
||||||
|
privateKey = infisicalSymmetricDecrypt({
|
||||||
|
iv: user.serverEncryptedPrivateKeyIV,
|
||||||
|
tag: user.serverEncryptedPrivateKeyTag,
|
||||||
|
ciphertext: user.serverEncryptedPrivateKey,
|
||||||
|
keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Cannot reset password without current credentials or recovery method",
|
||||||
|
name: "Reset password"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const encKeys = await generateUserSrpKeys(user.username, newPassword, {
|
||||||
|
publicKey: user.publicKey,
|
||||||
|
privateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const { tag, iv, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey);
|
||||||
|
|
||||||
|
await userDAL.updateUserEncryptionByUserId(userId, {
|
||||||
|
hashedPassword: newHashedPassword,
|
||||||
|
|
||||||
|
// srp params
|
||||||
|
salt: encKeys.salt,
|
||||||
|
verifier: encKeys.verifier,
|
||||||
|
|
||||||
|
protectedKey: encKeys.protectedKey,
|
||||||
|
protectedKeyIV: encKeys.protectedKeyIV,
|
||||||
|
protectedKeyTag: encKeys.protectedKeyTag,
|
||||||
|
encryptedPrivateKey: encKeys.encryptedPrivateKey,
|
||||||
|
iv: encKeys.encryptedPrivateKeyIV,
|
||||||
|
tag: encKeys.encryptedPrivateKeyTag,
|
||||||
|
|
||||||
|
serverEncryptedPrivateKey: ciphertext,
|
||||||
|
serverEncryptedPrivateKeyIV: iv,
|
||||||
|
serverEncryptedPrivateKeyTag: tag,
|
||||||
|
serverEncryptedPrivateKeyEncoding: encoding
|
||||||
|
});
|
||||||
|
|
||||||
|
await tokenService.revokeAllMySessions(userId);
|
||||||
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Reset password of a user via backup key
|
* Reset password of a user via backup key
|
||||||
* */
|
* */
|
||||||
@ -391,6 +490,7 @@ export const authPaswordServiceFactory = ({
|
|||||||
createBackupPrivateKey,
|
createBackupPrivateKey,
|
||||||
getBackupPrivateKeyOfUser,
|
getBackupPrivateKeyOfUser,
|
||||||
sendPasswordSetupEmail,
|
sendPasswordSetupEmail,
|
||||||
setupPassword
|
setupPassword,
|
||||||
|
resetPasswordV2
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -13,6 +13,18 @@ export type TChangePasswordDTO = {
|
|||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum ResetPasswordV2Type {
|
||||||
|
Recovery = "recovery",
|
||||||
|
LoggedInReset = "logged-in-reset"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TResetPasswordV2DTO = {
|
||||||
|
type: ResetPasswordV2Type;
|
||||||
|
userId: string;
|
||||||
|
newPassword: string;
|
||||||
|
oldPassword?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TResetPasswordViaBackupKeyDTO = {
|
export type TResetPasswordViaBackupKeyDTO = {
|
||||||
userId: string;
|
userId: string;
|
||||||
protectedKey: string;
|
protectedKey: string;
|
||||||
|
@ -31,9 +31,9 @@ export type TImportDataIntoInfisicalDTO = {
|
|||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findLastEnvPosition" | "create" | "findOne">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findLastEnvPosition" | "create" | "findOne">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
|
||||||
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys">;
|
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "find">;
|
||||||
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "create">;
|
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "create">;
|
||||||
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create">;
|
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create" | "find">;
|
||||||
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">;
|
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">;
|
||||||
|
|
||||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">;
|
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">;
|
||||||
|
@ -27,9 +27,9 @@ export type TExternalMigrationQueueFactoryDep = {
|
|||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findLastEnvPosition" | "create" | "findOne">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findLastEnvPosition" | "create" | "findOne">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
|
||||||
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys">;
|
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "find">;
|
||||||
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "create">;
|
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "create">;
|
||||||
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create">;
|
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create" | "find">;
|
||||||
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">;
|
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">;
|
||||||
|
|
||||||
folderDAL: Pick<TSecretFolderDALFactory, "create" | "findBySecretPath" | "findOne" | "findById">;
|
folderDAL: Pick<TSecretFolderDALFactory, "create" | "findBySecretPath" | "findOne" | "findById">;
|
||||||
|
@ -4,7 +4,7 @@ import ms from "ms";
|
|||||||
import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas";
|
import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
||||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
@ -102,11 +102,13 @@ export const groupProjectServiceFactory = ({
|
|||||||
project.id
|
project.id
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, rolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
if (!hasRequiredPrivileges) {
|
throw new ForbiddenRequestError({
|
||||||
throw new ForbiddenRequestError({ message: "Failed to assign group to a more privileged role" });
|
name: "PermissionBoundaryError",
|
||||||
}
|
message: "Failed to assign group to a more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate custom roles input
|
// validate custom roles input
|
||||||
@ -267,12 +269,13 @@ export const groupProjectServiceFactory = ({
|
|||||||
requestedRoleChange,
|
requestedRoleChange,
|
||||||
project.id
|
project.id
|
||||||
);
|
);
|
||||||
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, rolePermission);
|
if (!permissionBoundary.isValid)
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
if (!hasRequiredPrivileges) {
|
name: "PermissionBoundaryError",
|
||||||
throw new ForbiddenRequestError({ message: "Failed to assign group to a more privileged role" });
|
message: "Failed to assign group to a more privileged role",
|
||||||
}
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate custom roles input
|
// validate custom roles input
|
||||||
|
@ -78,9 +78,7 @@ export const identityAccessTokenServiceFactory = ({
|
|||||||
const renewAccessToken = async ({ accessToken }: TRenewAccessTokenDTO) => {
|
const renewAccessToken = async ({ accessToken }: TRenewAccessTokenDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
const decodedToken = jwt.verify(accessToken, appCfg.AUTH_SECRET) as JwtPayload & {
|
const decodedToken = jwt.verify(accessToken, appCfg.AUTH_SECRET) as TIdentityAccessTokenJwtPayload;
|
||||||
identityAccessTokenId: string;
|
|
||||||
};
|
|
||||||
if (decodedToken.authTokenType !== AuthTokenType.IDENTITY_ACCESS_TOKEN) {
|
if (decodedToken.authTokenType !== AuthTokenType.IDENTITY_ACCESS_TOKEN) {
|
||||||
throw new BadRequestError({ message: "Only identity access tokens can be renewed" });
|
throw new BadRequestError({ message: "Only identity access tokens can be renewed" });
|
||||||
}
|
}
|
||||||
@ -127,7 +125,23 @@ export const identityAccessTokenServiceFactory = ({
|
|||||||
accessTokenLastRenewedAt: new Date()
|
accessTokenLastRenewedAt: new Date()
|
||||||
});
|
});
|
||||||
|
|
||||||
return { accessToken, identityAccessToken: updatedIdentityAccessToken };
|
const renewedToken = jwt.sign(
|
||||||
|
{
|
||||||
|
identityId: decodedToken.identityId,
|
||||||
|
clientSecretId: decodedToken.clientSecretId,
|
||||||
|
identityAccessTokenId: decodedToken.identityAccessTokenId,
|
||||||
|
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||||
|
} as TIdentityAccessTokenJwtPayload,
|
||||||
|
appCfg.AUTH_SECRET,
|
||||||
|
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
||||||
|
Number(identityAccessToken.accessTokenTTL) === 0
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return { accessToken: renewedToken, identityAccessToken: updatedIdentityAccessToken };
|
||||||
};
|
};
|
||||||
|
|
||||||
const revokeAccessToken = async (accessToken: string) => {
|
const revokeAccessToken = async (accessToken: string) => {
|
||||||
|
@ -7,7 +7,7 @@ import { IdentityAuthMethod } from "@app/db/schemas";
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
@ -339,9 +339,12 @@ export const identityAwsAuthServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isAtLeastAsPrivileged(permission, rolePermission))
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to revoke aws auth of identity with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to revoke aws auth of identity with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
|
|
||||||
const revokedIdentityAwsAuth = await identityAwsAuthDAL.transaction(async (tx) => {
|
const revokedIdentityAwsAuth = await identityAwsAuthDAL.transaction(async (tx) => {
|
||||||
|
@ -5,7 +5,7 @@ import { IdentityAuthMethod } from "@app/db/schemas";
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
@ -312,9 +312,12 @@ export const identityAzureAuthServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
if (!isAtLeastAsPrivileged(permission, rolePermission))
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to revoke azure auth of identity with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to revoke azure auth of identity with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
|
|
||||||
const revokedIdentityAzureAuth = await identityAzureAuthDAL.transaction(async (tx) => {
|
const revokedIdentityAzureAuth = await identityAzureAuthDAL.transaction(async (tx) => {
|
||||||
|
@ -5,7 +5,7 @@ import { IdentityAuthMethod } from "@app/db/schemas";
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
@ -358,9 +358,12 @@ export const identityGcpAuthServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
if (!isAtLeastAsPrivileged(permission, rolePermission))
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to revoke gcp auth of identity with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to revoke gcp auth of identity with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
|
|
||||||
const revokedIdentityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => {
|
const revokedIdentityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => {
|
||||||
|
@ -7,7 +7,7 @@ import { IdentityAuthMethod, TIdentityJwtAuthsUpdate } from "@app/db/schemas";
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
@ -78,14 +78,22 @@ export const identityJwtAuthServiceFactory = ({
|
|||||||
let tokenData: Record<string, string | boolean | number> = {};
|
let tokenData: Record<string, string | boolean | number> = {};
|
||||||
|
|
||||||
if (identityJwtAuth.configurationType === JwtConfigurationType.JWKS) {
|
if (identityJwtAuth.configurationType === JwtConfigurationType.JWKS) {
|
||||||
const decryptedJwksCaCert = orgDataKeyDecryptor({
|
let client: JwksClient;
|
||||||
cipherTextBlob: identityJwtAuth.encryptedJwksCaCert
|
if (identityJwtAuth.jwksUrl.includes("https:")) {
|
||||||
}).toString();
|
const decryptedJwksCaCert = orgDataKeyDecryptor({
|
||||||
const requestAgent = new https.Agent({ ca: decryptedJwksCaCert, rejectUnauthorized: !!decryptedJwksCaCert });
|
cipherTextBlob: identityJwtAuth.encryptedJwksCaCert
|
||||||
const client = new JwksClient({
|
}).toString();
|
||||||
jwksUri: identityJwtAuth.jwksUrl,
|
|
||||||
requestAgent
|
const requestAgent = new https.Agent({ ca: decryptedJwksCaCert, rejectUnauthorized: !!decryptedJwksCaCert });
|
||||||
});
|
client = new JwksClient({
|
||||||
|
jwksUri: identityJwtAuth.jwksUrl,
|
||||||
|
requestAgent
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
client = new JwksClient({
|
||||||
|
jwksUri: identityJwtAuth.jwksUrl
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { kid } = decodedToken.header;
|
const { kid } = decodedToken.header;
|
||||||
const jwtSigningKey = await client.getSigningKey(kid);
|
const jwtSigningKey = await client.getSigningKey(kid);
|
||||||
@ -508,11 +516,13 @@ export const identityJwtAuthServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isAtLeastAsPrivileged(permission, rolePermission)) {
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to revoke JWT auth of identity with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to revoke jwt auth of identity with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const revokedIdentityJwtAuth = await identityJwtAuthDAL.transaction(async (tx) => {
|
const revokedIdentityJwtAuth = await identityJwtAuthDAL.transaction(async (tx) => {
|
||||||
const deletedJwtAuth = await identityJwtAuthDAL.delete({ identityId }, tx);
|
const deletedJwtAuth = await identityJwtAuthDAL.delete({ identityId }, tx);
|
||||||
|
@ -7,7 +7,7 @@ import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/sche
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
@ -487,9 +487,12 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
if (!isAtLeastAsPrivileged(permission, rolePermission))
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to revoke kubernetes auth of identity with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to revoke kubernetes auth of identity with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
|
|
||||||
const revokedIdentityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => {
|
const revokedIdentityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => {
|
||||||
|
@ -8,7 +8,7 @@ import { IdentityAuthMethod, TIdentityOidcAuthsUpdate } from "@app/db/schemas";
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
@ -428,11 +428,13 @@ export const identityOidcAuthServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isAtLeastAsPrivileged(permission, rolePermission)) {
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to revoke OIDC auth of identity with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to revoke oidc auth of identity with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const revokedIdentityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => {
|
const revokedIdentityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => {
|
||||||
const deletedOidcAuth = await identityOidcAuthDAL.delete({ identityId }, tx);
|
const deletedOidcAuth = await identityOidcAuthDAL.delete({ identityId }, tx);
|
||||||
|
@ -4,7 +4,7 @@ import ms from "ms";
|
|||||||
import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas";
|
import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
|
|
||||||
@ -91,11 +91,13 @@ export const identityProjectServiceFactory = ({
|
|||||||
projectId
|
projectId
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
if (!hasRequiredPriviledges) {
|
throw new ForbiddenRequestError({
|
||||||
throw new ForbiddenRequestError({ message: "Failed to change to a more privileged role" });
|
name: "PermissionBoundaryError",
|
||||||
}
|
message: "Failed to assign to a more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate custom roles input
|
// validate custom roles input
|
||||||
@ -185,9 +187,13 @@ export const identityProjectServiceFactory = ({
|
|||||||
projectId
|
projectId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isAtLeastAsPrivileged(permission, rolePermission)) {
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
throw new ForbiddenRequestError({ message: "Failed to change to a more privileged role" });
|
if (!permissionBoundary.isValid)
|
||||||
}
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to change to a more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate custom roles input
|
// validate custom roles input
|
||||||
@ -277,8 +283,13 @@ export const identityProjectServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.Any
|
actionProjectType: ActionProjectType.Any
|
||||||
});
|
});
|
||||||
if (!isAtLeastAsPrivileged(permission, identityRolePermission))
|
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
|
||||||
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
|
if (!permissionBoundary.isValid)
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to remove more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const [deletedIdentity] = await identityProjectDAL.delete({ identityId, projectId });
|
const [deletedIdentity] = await identityProjectDAL.delete({ identityId, projectId });
|
||||||
return deletedIdentity;
|
return deletedIdentity;
|
||||||
|
@ -5,7 +5,7 @@ import { IdentityAuthMethod, TableName } from "@app/db/schemas";
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
@ -245,11 +245,13 @@ export const identityTokenAuthServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isAtLeastAsPrivileged(permission, rolePermission)) {
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to revoke Token Auth of identity with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to revoke token auth of identity with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const revokedIdentityTokenAuth = await identityTokenAuthDAL.transaction(async (tx) => {
|
const revokedIdentityTokenAuth = await identityTokenAuthDAL.transaction(async (tx) => {
|
||||||
const deletedTokenAuth = await identityTokenAuthDAL.delete({ identityId }, tx);
|
const deletedTokenAuth = await identityTokenAuthDAL.delete({ identityId }, tx);
|
||||||
@ -295,10 +297,12 @@ export const identityTokenAuthServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
if (!hasPriviledge)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to create token for identity with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to create token for identity with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
|
|
||||||
const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId });
|
const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId });
|
||||||
@ -415,10 +419,12 @@ export const identityTokenAuthServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
if (!hasPriviledge)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to update token for identity with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update token for identity with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
|
|
||||||
const [token] = await identityAccessTokenDAL.update(
|
const [token] = await identityAccessTokenDAL.update(
|
||||||
|
@ -8,7 +8,7 @@ import { IdentityAuthMethod } from "@app/db/schemas";
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip";
|
import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip";
|
||||||
@ -367,9 +367,12 @@ export const identityUaServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
if (!isAtLeastAsPrivileged(permission, rolePermission))
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to revoke universal auth of identity with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to revoke universal auth of identity with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
|
|
||||||
const revokedIdentityUniversalAuth = await identityUaDAL.transaction(async (tx) => {
|
const revokedIdentityUniversalAuth = await identityUaDAL.transaction(async (tx) => {
|
||||||
@ -414,10 +417,12 @@ export const identityUaServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
if (!hasPriviledge)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to add identity to project with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to create client secret for a more privileged identity.",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
|
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
@ -475,9 +480,12 @@ export const identityUaServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isAtLeastAsPrivileged(permission, rolePermission))
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to add identity to project with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to get identity client secret with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
|
|
||||||
const identityUniversalAuth = await identityUaDAL.findOne({
|
const identityUniversalAuth = await identityUaDAL.findOne({
|
||||||
@ -524,9 +532,12 @@ export const identityUaServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
if (!isAtLeastAsPrivileged(permission, rolePermission))
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to read identity client secret of project with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to read identity client secret of identity with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
|
|
||||||
const clientSecret = await identityUaClientSecretDAL.findById(clientSecretId);
|
const clientSecret = await identityUaClientSecretDAL.findById(clientSecretId);
|
||||||
@ -566,10 +577,12 @@ export const identityUaServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
if (!isAtLeastAsPrivileged(permission, rolePermission))
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to revoke identity client secret with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to revoke identity client secret with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
|
|
||||||
const clientSecret = await identityUaClientSecretDAL.updateById(clientSecretId, {
|
const clientSecret = await identityUaClientSecretDAL.updateById(clientSecretId, {
|
||||||
|
@ -1,10 +1,42 @@
|
|||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName } from "@app/db/schemas";
|
import { TableName, TIdentities } from "@app/db/schemas";
|
||||||
import { ormify } from "@app/lib/knex";
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
|
||||||
export type TIdentityDALFactory = ReturnType<typeof identityDALFactory>;
|
export type TIdentityDALFactory = ReturnType<typeof identityDALFactory>;
|
||||||
|
|
||||||
export const identityDALFactory = (db: TDbClient) => {
|
export const identityDALFactory = (db: TDbClient) => {
|
||||||
const identityOrm = ormify(db, TableName.Identity);
|
const identityOrm = ormify(db, TableName.Identity);
|
||||||
return identityOrm;
|
|
||||||
|
const getIdentitiesByFilter = async ({
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
searchTerm,
|
||||||
|
sortBy
|
||||||
|
}: {
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
searchTerm: string;
|
||||||
|
sortBy?: keyof TIdentities;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
let query = db.replicaNode()(TableName.Identity);
|
||||||
|
|
||||||
|
if (searchTerm) {
|
||||||
|
query = query.where((qb) => {
|
||||||
|
void qb.whereILike("name", `%${searchTerm}%`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortBy) {
|
||||||
|
query = query.orderBy(sortBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await query.limit(limit).offset(offset).select(selectAllTableCols(TableName.Identity));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Get identities by filter" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...identityOrm, getIdentitiesByFilter };
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,7 @@ import { OrgMembershipRole, TableName, TOrgRoles } from "@app/db/schemas";
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||||
|
|
||||||
@ -58,9 +58,13 @@ export const identityServiceFactory = ({
|
|||||||
orgId
|
orgId
|
||||||
);
|
);
|
||||||
const isCustomRole = Boolean(customRole);
|
const isCustomRole = Boolean(customRole);
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to create a more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to create a more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(orgId);
|
const plan = await licenseService.getPlan(orgId);
|
||||||
|
|
||||||
@ -129,9 +133,13 @@ export const identityServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update a more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
let customRole: TOrgRoles | undefined;
|
let customRole: TOrgRoles | undefined;
|
||||||
if (role) {
|
if (role) {
|
||||||
@ -141,9 +149,13 @@ export const identityServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isCustomRole = Boolean(customOrgRole);
|
const isCustomRole = Boolean(customOrgRole);
|
||||||
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission);
|
const appliedRolePermissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
if (!hasRequiredNewRolePermission)
|
if (!appliedRolePermissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to create a more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to create a more privileged identity",
|
||||||
|
details: { missingPermissions: appliedRolePermissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
if (isCustomRole) customRole = customOrgRole;
|
if (isCustomRole) customRole = customOrgRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,9 +228,13 @@ export const identityServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to delete more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const deletedIdentity = await identityDAL.deleteById(id);
|
const deletedIdentity = await identityDAL.deleteById(id);
|
||||||
|
|
||||||
|
@ -68,7 +68,8 @@ const getIntegrationSecretsV2 = async (
|
|||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
secretImportDAL,
|
secretImportDAL,
|
||||||
secretImports,
|
secretImports,
|
||||||
hasSecretAccess: () => true
|
hasSecretAccess: () => true,
|
||||||
|
viewSecretValue: true
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
|
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
|
import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSecretActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { NotFoundError } from "@app/lib/errors";
|
import { NotFoundError } from "@app/lib/errors";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
@ -91,13 +96,10 @@ export const integrationServiceFactory = ({
|
|||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||||
ProjectPermissionActions.Read,
|
environment: sourceEnvironment,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
secretPath
|
||||||
environment: sourceEnvironment,
|
});
|
||||||
secretPath
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(integrationAuth.projectId, sourceEnvironment, secretPath);
|
const folder = await folderDAL.findBySecretPath(integrationAuth.projectId, sourceEnvironment, secretPath);
|
||||||
if (!folder) {
|
if (!folder) {
|
||||||
@ -174,13 +176,10 @@ export const integrationServiceFactory = ({
|
|||||||
const newSecretPath = secretPath || integration.secretPath;
|
const newSecretPath = secretPath || integration.secretPath;
|
||||||
|
|
||||||
if (environment || secretPath) {
|
if (environment || secretPath) {
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||||
ProjectPermissionActions.Read,
|
environment: newEnvironment,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
secretPath: newSecretPath
|
||||||
environment: newEnvironment,
|
});
|
||||||
secretPath: newSecretPath
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(integration.projectId, newEnvironment, newSecretPath);
|
const folder = await folderDAL.findBySecretPath(integration.projectId, newEnvironment, newSecretPath);
|
||||||
|
@ -7,7 +7,7 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service
|
|||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
@ -274,13 +274,13 @@ export const projectMembershipServiceFactory = ({
|
|||||||
projectId
|
projectId
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
if (!hasRequiredPriviledges) {
|
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: `Failed to change to a more privileged role ${requestedRoleChange}`
|
name: "PermissionBoundaryError",
|
||||||
|
message: `Failed to change to a more privileged role ${requestedRoleChange}`,
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate custom roles input
|
// validate custom roles input
|
||||||
|
@ -10,8 +10,13 @@ import {
|
|||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
|
import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSecretActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
|
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
|
||||||
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
|
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
|
||||||
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
|
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
|
||||||
@ -563,11 +568,24 @@ export const projectServiceFactory = ({
|
|||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||||
|
|
||||||
|
if (update.slug) {
|
||||||
|
const existingProject = await projectDAL.findOne({
|
||||||
|
slug: update.slug,
|
||||||
|
orgId: actorOrgId
|
||||||
|
});
|
||||||
|
if (existingProject && existingProject.id !== project.id) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to update project slug. The project "${existingProject.name}" with the slug "${existingProject.slug}" already exists in your organization. Please choose a unique slug for your project.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updatedProject = await projectDAL.updateById(project.id, {
|
const updatedProject = await projectDAL.updateById(project.id, {
|
||||||
name: update.name,
|
name: update.name,
|
||||||
description: update.description,
|
description: update.description,
|
||||||
autoCapitalization: update.autoCapitalization,
|
autoCapitalization: update.autoCapitalization,
|
||||||
enforceCapitalization: update.autoCapitalization
|
enforceCapitalization: update.autoCapitalization,
|
||||||
|
slug: update.slug
|
||||||
});
|
});
|
||||||
|
|
||||||
return updatedProject;
|
return updatedProject;
|
||||||
@ -747,7 +765,7 @@ export const projectServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.Any
|
actionProjectType: ActionProjectType.Any
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
|
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret);
|
||||||
|
|
||||||
const project = await projectDAL.findProjectById(projectId);
|
const project = await projectDAL.findProjectById(projectId);
|
||||||
|
|
||||||
|
@ -82,6 +82,7 @@ export type TUpdateProjectDTO = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
autoCapitalization?: boolean;
|
autoCapitalization?: boolean;
|
||||||
|
slug?: string;
|
||||||
};
|
};
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
17
backend/src/services/secret-folder/secret-folder-fns.ts
Normal file
17
backend/src/services/secret-folder/secret-folder-fns.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { TSecretFolders } from "@app/db/schemas";
|
||||||
|
import { InternalServerError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
export const buildFolderPath = (
|
||||||
|
folder: TSecretFolders,
|
||||||
|
foldersMap: Record<string, TSecretFolders>,
|
||||||
|
depth: number = 0
|
||||||
|
): string => {
|
||||||
|
if (depth > 20) {
|
||||||
|
throw new InternalServerError({ message: "Maximum folder depth of 20 exceeded" });
|
||||||
|
}
|
||||||
|
if (!folder.parentId) {
|
||||||
|
return depth === 0 ? "/" : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${buildFolderPath(foldersMap[folder.parentId], foldersMap, depth + 1)}/${folder.name}`;
|
||||||
|
};
|
@ -8,6 +8,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services
|
|||||||
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
|
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
|
||||||
|
import { buildFolderPath } from "@app/services/secret-folder/secret-folder-fns";
|
||||||
|
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
@ -27,7 +28,7 @@ type TSecretFolderServiceFactoryDep = {
|
|||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||||
folderDAL: TSecretFolderDALFactory;
|
folderDAL: TSecretFolderDALFactory;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs" | "find">;
|
||||||
folderVersionDAL: TSecretFolderVersionDALFactory;
|
folderVersionDAL: TSecretFolderVersionDALFactory;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||||
};
|
};
|
||||||
@ -580,6 +581,44 @@ export const secretFolderServiceFactory = ({
|
|||||||
return folders;
|
return folders;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getProjectEnvironmentsFolders = async (projectId: string, actor: OrgServiceActor) => {
|
||||||
|
// folder list is allowed to be read by anyone
|
||||||
|
// permission is to check if user has access
|
||||||
|
await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorId: actor.id,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
|
});
|
||||||
|
|
||||||
|
const environments = await projectEnvDAL.find({ projectId });
|
||||||
|
|
||||||
|
const folders = await folderDAL.find({
|
||||||
|
$in: {
|
||||||
|
envId: environments.map((env) => env.id)
|
||||||
|
},
|
||||||
|
isReserved: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const environmentFolders = Object.fromEntries(
|
||||||
|
environments.map((env) => {
|
||||||
|
const relevantFolders = folders.filter((folder) => folder.envId === env.id);
|
||||||
|
const foldersMap = Object.fromEntries(relevantFolders.map((folder) => [folder.id, folder]));
|
||||||
|
|
||||||
|
const foldersWithPath = relevantFolders.map((folder) => ({
|
||||||
|
...folder,
|
||||||
|
path: buildFolderPath(folder, foldersMap)
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [env.slug, { ...env, folders: foldersWithPath }];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return environmentFolders;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createFolder,
|
createFolder,
|
||||||
updateFolder,
|
updateFolder,
|
||||||
@ -589,6 +628,7 @@ export const secretFolderServiceFactory = ({
|
|||||||
getFolderById,
|
getFolderById,
|
||||||
getProjectFolderCount,
|
getProjectFolderCount,
|
||||||
getFoldersMultiEnv,
|
getFoldersMultiEnv,
|
||||||
getFoldersDeepByEnvs
|
getFoldersDeepByEnvs,
|
||||||
|
getProjectEnvironmentsFolders
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -3,6 +3,7 @@ import { groupBy, unique } from "@app/lib/fn";
|
|||||||
|
|
||||||
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
|
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
|
||||||
import { TSecretDALFactory } from "../secret/secret-dal";
|
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||||
|
import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||||
import { TSecretImportDALFactory } from "./secret-import-dal";
|
import { TSecretImportDALFactory } from "./secret-import-dal";
|
||||||
@ -32,6 +33,12 @@ type TSecretImportSecretsV2 = {
|
|||||||
folderId: string | undefined;
|
folderId: string | undefined;
|
||||||
importFolderId: string;
|
importFolderId: string;
|
||||||
secrets: (TSecretsV2 & {
|
secrets: (TSecretsV2 & {
|
||||||
|
secretTags: {
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
color?: string | null;
|
||||||
|
id: string;
|
||||||
|
}[];
|
||||||
workspace: string;
|
workspace: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
_id: string;
|
_id: string;
|
||||||
@ -39,6 +46,7 @@ type TSecretImportSecretsV2 = {
|
|||||||
// akhilmhdh: yes i know you can put ?.
|
// akhilmhdh: yes i know you can put ?.
|
||||||
// But for somereason ts consider ? and undefined explicit as different just ts things
|
// But for somereason ts consider ? and undefined explicit as different just ts things
|
||||||
secretValue: string;
|
secretValue: string;
|
||||||
|
secretValueHidden: boolean;
|
||||||
secretComment: string;
|
secretComment: string;
|
||||||
secretMetadata?: ResourceMetadataDTO;
|
secretMetadata?: ResourceMetadataDTO;
|
||||||
})[];
|
})[];
|
||||||
@ -150,12 +158,14 @@ export const fnSecretsV2FromImports = async ({
|
|||||||
secretImportDAL,
|
secretImportDAL,
|
||||||
decryptor,
|
decryptor,
|
||||||
expandSecretReferences,
|
expandSecretReferences,
|
||||||
hasSecretAccess
|
hasSecretAccess,
|
||||||
|
viewSecretValue
|
||||||
}: {
|
}: {
|
||||||
secretImports: (Omit<TSecretImports, "importEnv"> & {
|
secretImports: (Omit<TSecretImports, "importEnv"> & {
|
||||||
importEnv: { id: string; slug: string; name: string };
|
importEnv: { id: string; slug: string; name: string };
|
||||||
})[];
|
})[];
|
||||||
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">;
|
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">;
|
||||||
|
viewSecretValue: boolean;
|
||||||
secretDAL: Pick<TSecretV2BridgeDALFactory, "find">;
|
secretDAL: Pick<TSecretV2BridgeDALFactory, "find">;
|
||||||
secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds">;
|
secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds">;
|
||||||
decryptor: (value?: Buffer | null) => string;
|
decryptor: (value?: Buffer | null) => string;
|
||||||
@ -168,9 +178,14 @@ export const fnSecretsV2FromImports = async ({
|
|||||||
hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean;
|
hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const cyclicDetector = new Set();
|
const cyclicDetector = new Set();
|
||||||
const stack: { secretImports: typeof rootSecretImports; depth: number; parentImportedSecrets: TSecretsV2[] }[] = [
|
const stack: {
|
||||||
{ secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] }
|
secretImports: typeof rootSecretImports;
|
||||||
];
|
depth: number;
|
||||||
|
parentImportedSecrets: (TSecretsV2 & {
|
||||||
|
secretValueHidden: boolean;
|
||||||
|
secretTags: { slug: string; name: string; id: string; color?: string | null }[];
|
||||||
|
})[];
|
||||||
|
}[] = [{ secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] }];
|
||||||
|
|
||||||
const processedImports: TSecretImportSecretsV2[] = [];
|
const processedImports: TSecretImportSecretsV2[] = [];
|
||||||
|
|
||||||
@ -229,7 +244,9 @@ export const fnSecretsV2FromImports = async ({
|
|||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
secretKey: item.key,
|
secretKey: item.key,
|
||||||
secretValue: decryptor(item.encryptedValue),
|
secretValue: viewSecretValue ? decryptor(item.encryptedValue) : INFISICAL_SECRET_VALUE_HIDDEN_MASK,
|
||||||
|
secretValueHidden: !viewSecretValue,
|
||||||
|
secretTags: item.tags,
|
||||||
secretComment: decryptor(item.encryptedComment),
|
secretComment: decryptor(item.encryptedComment),
|
||||||
environment: importEnv.slug,
|
environment: importEnv.slug,
|
||||||
workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend.
|
workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend.
|
||||||
@ -267,6 +284,8 @@ export const fnSecretsV2FromImports = async ({
|
|||||||
processedImport.secrets = unique(processedImport.secrets, (i) => i.key);
|
processedImport.secrets = unique(processedImport.secrets, (i) => i.key);
|
||||||
return Promise.allSettled(
|
return Promise.allSettled(
|
||||||
processedImport.secrets.map(async (decryptedSecret, index) => {
|
processedImport.secrets.map(async (decryptedSecret, index) => {
|
||||||
|
if (decryptedSecret.secretValueHidden) return;
|
||||||
|
|
||||||
const expandedSecretValue = await expandSecretReferences({
|
const expandedSecretValue = await expandSecretReferences({
|
||||||
value: decryptedSecret.secretValue,
|
value: decryptedSecret.secretValue,
|
||||||
secretPath: processedImport.secretPath,
|
secretPath: processedImport.secretPath,
|
||||||
|
@ -4,8 +4,16 @@ import { ForbiddenError, subject } from "@casl/ability";
|
|||||||
|
|
||||||
import { ActionProjectType, TableName } from "@app/db/schemas";
|
import { ActionProjectType, TableName } from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
|
import {
|
||||||
|
hasSecretReadValueOrDescribePermission,
|
||||||
|
throwIfMissingSecretReadValueOrDescribePermission
|
||||||
|
} from "@app/ee/services/permission/permission-fns";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSecretActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { getReplicationFolderName } from "@app/ee/services/secret-replication/secret-replication-service";
|
import { getReplicationFolderName } from "@app/ee/services/secret-replication/secret-replication-service";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
@ -89,13 +97,11 @@ export const secretImportServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// check if user has permission to import from target path
|
// check if user has permission to import from target path
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
|
||||||
ProjectPermissionActions.Read,
|
environment: data.environment,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
secretPath: data.path
|
||||||
environment: data.environment,
|
});
|
||||||
secretPath: data.path
|
|
||||||
})
|
|
||||||
);
|
|
||||||
if (isReplication) {
|
if (isReplication) {
|
||||||
const plan = await licenseService.getPlan(actorOrgId);
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
if (!plan.secretApproval) {
|
if (!plan.secretApproval) {
|
||||||
@ -401,13 +407,10 @@ export const secretImportServiceFactory = ({
|
|||||||
if (!secretImportDoc.isReplication) throw new BadRequestError({ message: "Import is not in replication mode" });
|
if (!secretImportDoc.isReplication) throw new BadRequestError({ message: "Import is not in replication mode" });
|
||||||
|
|
||||||
// check if user has permission to import from target path
|
// check if user has permission to import from target path
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
|
||||||
ProjectPermissionActions.Read,
|
environment: secretImportDoc.importEnv.slug,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
secretPath: secretImportDoc.importPath
|
||||||
environment: secretImportDoc.importEnv.slug,
|
});
|
||||||
secretPath: secretImportDoc.importPath
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
await projectDAL.checkProjectUpgradeStatus(projectId);
|
await projectDAL.checkProjectUpgradeStatus(projectId);
|
||||||
|
|
||||||
@ -595,14 +598,12 @@ export const secretImportServiceFactory = ({
|
|||||||
// so anything based on this order will also be in right position
|
// so anything based on this order will also be in right position
|
||||||
const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false });
|
const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false });
|
||||||
const allowedImports = secretImports.filter((el) =>
|
const allowedImports = secretImports.filter((el) =>
|
||||||
permission.can(
|
hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||||
ProjectPermissionActions.Read,
|
environment: el.importEnv.slug,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
secretPath: el.importPath
|
||||||
environment: el.importEnv.slug,
|
})
|
||||||
secretPath: el.importPath
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL });
|
return fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL });
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -642,20 +643,19 @@ export const secretImportServiceFactory = ({
|
|||||||
const importedSecrets = await fnSecretsV2FromImports({
|
const importedSecrets = await fnSecretsV2FromImports({
|
||||||
secretImports,
|
secretImports,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
|
viewSecretValue: true,
|
||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
secretImportDAL,
|
secretImportDAL,
|
||||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
|
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
|
||||||
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) =>
|
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) =>
|
||||||
permission.can(
|
hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||||
ProjectPermissionActions.Read,
|
environment: expandEnvironment,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
secretPath: expandSecretPath,
|
||||||
environment: expandEnvironment,
|
secretName: expandSecretKey,
|
||||||
secretPath: expandSecretPath,
|
secretTags: expandSecretTags
|
||||||
secretName: expandSecretKey,
|
})
|
||||||
secretTags: expandSecretTags
|
|
||||||
})
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return importedSecrets;
|
return importedSecrets;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -666,13 +666,10 @@ export const secretImportServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const allowedImports = secretImports.filter((el) =>
|
const allowedImports = secretImports.filter((el) =>
|
||||||
permission.can(
|
hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||||
ProjectPermissionActions.Read,
|
environment: el.importEnv.slug,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
secretPath: el.importPath
|
||||||
environment: el.importEnv.slug,
|
})
|
||||||
secretPath: el.importPath
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
const importedSecrets = await fnSecretsFromImports({
|
const importedSecrets = await fnSecretsFromImports({
|
||||||
allowedImports,
|
allowedImports,
|
||||||
@ -683,7 +680,10 @@ export const secretImportServiceFactory = ({
|
|||||||
return importedSecrets.map((el) => ({
|
return importedSecrets.map((el) => ({
|
||||||
...el,
|
...el,
|
||||||
secrets: el.secrets.map((encryptedSecret) =>
|
secrets: el.secrets.map((encryptedSecret) =>
|
||||||
decryptSecretRaw({ ...encryptedSecret, workspace: projectId, environment, secretPath }, botKey)
|
decryptSecretRaw(
|
||||||
|
{ ...encryptedSecret, workspace: projectId, environment, secretPath, secretValueHidden: false },
|
||||||
|
botKey
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
@ -71,8 +71,16 @@ const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCreden
|
|||||||
|
|
||||||
res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8");
|
res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// when a secret in GCP has no versions, we treat it as if it's a blank value
|
// when a secret in GCP has no versions, or is disabled/destroyed, we treat it as if it's a blank value
|
||||||
if (error instanceof AxiosError && error.response?.status === 404) {
|
if (
|
||||||
|
error instanceof AxiosError &&
|
||||||
|
(error.response?.status === 404 ||
|
||||||
|
(error.response?.status === 400 &&
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
error.response.data.error.status === "FAILED_PRECONDITION" &&
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call
|
||||||
|
error.response.data.error.message.match(/(?:disabled|destroyed)/i)))
|
||||||
|
) {
|
||||||
res[key] = "";
|
res[key] = "";
|
||||||
} else {
|
} else {
|
||||||
throw new SecretSyncError({
|
throw new SecretSyncError({
|
||||||
|
@ -249,7 +249,8 @@ export const secretSyncQueueFactory = ({
|
|||||||
expandSecretReferences,
|
expandSecretReferences,
|
||||||
secretImportDAL,
|
secretImportDAL,
|
||||||
secretImports,
|
secretImports,
|
||||||
hasSecretAccess: () => true
|
hasSecretAccess: () => true,
|
||||||
|
viewSecretValue: true
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
|
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
|
import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionSecretActions,
|
||||||
ProjectPermissionSecretSyncActions,
|
ProjectPermissionSecretSyncActions,
|
||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
@ -178,13 +179,10 @@ export const secretSyncServiceFactory = ({
|
|||||||
ProjectPermissionSub.SecretSyncs
|
ProjectPermissionSub.SecretSyncs
|
||||||
);
|
);
|
||||||
|
|
||||||
ForbiddenError.from(projectPermission).throwUnlessCan(
|
throwIfMissingSecretReadValueOrDescribePermission(projectPermission, ProjectPermissionSecretActions.ReadValue, {
|
||||||
ProjectPermissionActions.Read,
|
environment,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
secretPath
|
||||||
environment,
|
});
|
||||||
secretPath
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
|
|
||||||
@ -269,13 +267,10 @@ export const secretSyncServiceFactory = ({
|
|||||||
if (!updatedEnvironment || !updatedSecretPath)
|
if (!updatedEnvironment || !updatedSecretPath)
|
||||||
throw new BadRequestError({ message: "Must specify both source environment and secret path" });
|
throw new BadRequestError({ message: "Must specify both source environment and secret path" });
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||||
ProjectPermissionActions.Read,
|
environment: updatedEnvironment,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
secretPath: updatedSecretPath
|
||||||
environment: updatedEnvironment,
|
});
|
||||||
secretPath: updatedSecretPath
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const newFolder = await folderDAL.findBySecretPath(secretSync.projectId, updatedEnvironment, updatedSecretPath);
|
const newFolder = await folderDAL.findBySecretPath(secretSync.projectId, updatedEnvironment, updatedSecretPath);
|
||||||
|
|
||||||
|
@ -47,6 +47,7 @@ export const secretTagDALFactory = (db: TDbClient) => {
|
|||||||
throw new DatabaseError({ error, name: "Find all by ids" });
|
throw new DatabaseError({ error, name: "Find all by ids" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...secretTagOrm,
|
...secretTagOrm,
|
||||||
saveTagsToSecret: secretJnTagOrm.insertMany,
|
saveTagsToSecret: secretJnTagOrm.insertMany,
|
||||||
|
@ -8,6 +8,7 @@ import { logger } from "@app/lib/logger";
|
|||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
|
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
|
||||||
|
import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
|
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
|
||||||
import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types";
|
import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types";
|
||||||
@ -93,7 +94,7 @@ export const fnSecretBulkInsert = async ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const userActorId = actor && actor.type === ActorType.USER ? actor.actorId : undefined;
|
const userActorId = actor && actor.type === ActorType.USER ? actor.actorId : undefined;
|
||||||
const identityActorId = actor && actor.type !== ActorType.USER ? actor.actorId : undefined;
|
const identityActorId = actor && actor.type === ActorType.IDENTITY ? actor.actorId : undefined;
|
||||||
const actorType = actor?.type || ActorType.PLATFORM;
|
const actorType = actor?.type || ActorType.PLATFORM;
|
||||||
|
|
||||||
const newSecrets = await secretDAL.insertMany(
|
const newSecrets = await secretDAL.insertMany(
|
||||||
@ -108,6 +109,7 @@ export const fnSecretBulkInsert = async ({
|
|||||||
[`${TableName.SecretV2}Id` as const]: newSecretGroupedByKeyName[key][0].id
|
[`${TableName.SecretV2}Id` as const]: newSecretGroupedByKeyName[key][0].id
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
const secretVersions = await secretVersionDAL.insertMany(
|
const secretVersions = await secretVersionDAL.insertMany(
|
||||||
sanitizedInputSecrets.map((el) => ({
|
sanitizedInputSecrets.map((el) => ({
|
||||||
...el,
|
...el,
|
||||||
@ -146,6 +148,7 @@ export const fnSecretBulkInsert = async ({
|
|||||||
if (newSecretTags.length) {
|
if (newSecretTags.length) {
|
||||||
const secTags = await secretTagDAL.saveTagsToSecretV2(newSecretTags, tx);
|
const secTags = await secretTagDAL.saveTagsToSecretV2(newSecretTags, tx);
|
||||||
const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId);
|
const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId);
|
||||||
|
|
||||||
const newSecretVersionTags = secTags.flatMap(({ secrets_v2Id, secret_tagsId }) => ({
|
const newSecretVersionTags = secTags.flatMap(({ secrets_v2Id, secret_tagsId }) => ({
|
||||||
[`${TableName.SecretVersionV2}Id` as const]: secVersionsGroupBySecId[secrets_v2Id][0].id,
|
[`${TableName.SecretVersionV2}Id` as const]: secVersionsGroupBySecId[secrets_v2Id][0].id,
|
||||||
[`${TableName.SecretTag}Id` as const]: secret_tagsId
|
[`${TableName.SecretTag}Id` as const]: secret_tagsId
|
||||||
@ -154,7 +157,16 @@ export const fnSecretBulkInsert = async ({
|
|||||||
await secretVersionTagDAL.insertMany(newSecretVersionTags, tx);
|
await secretVersionTagDAL.insertMany(newSecretVersionTags, tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
return newSecrets.map((secret) => ({ ...secret, _id: secret.id }));
|
const secretsWithTags = await secretDAL.find(
|
||||||
|
{
|
||||||
|
$in: {
|
||||||
|
[`${TableName.SecretV2}.id` as "id"]: newSecrets.map((s) => s.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ tx }
|
||||||
|
);
|
||||||
|
|
||||||
|
return secretsWithTags.map((secret) => ({ ...secret, _id: secret.id }));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fnSecretBulkUpdate = async ({
|
export const fnSecretBulkUpdate = async ({
|
||||||
@ -170,7 +182,7 @@ export const fnSecretBulkUpdate = async ({
|
|||||||
actor
|
actor
|
||||||
}: TFnSecretBulkUpdate) => {
|
}: TFnSecretBulkUpdate) => {
|
||||||
const userActorId = actor && actor?.type === ActorType.USER ? actor?.actorId : undefined;
|
const userActorId = actor && actor?.type === ActorType.USER ? actor?.actorId : undefined;
|
||||||
const identityActorId = actor && actor?.type !== ActorType.USER ? actor?.actorId : undefined;
|
const identityActorId = actor && actor?.type === ActorType.IDENTITY ? actor?.actorId : undefined;
|
||||||
const actorType = actor?.type || ActorType.PLATFORM;
|
const actorType = actor?.type || ActorType.PLATFORM;
|
||||||
|
|
||||||
const sanitizedInputSecrets = inputSecrets.map(
|
const sanitizedInputSecrets = inputSecrets.map(
|
||||||
@ -300,7 +312,15 @@ export const fnSecretBulkUpdate = async ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
return newSecrets.map((secret) => ({ ...secret, _id: secret.id }));
|
const secretsWithTags = await secretDAL.find(
|
||||||
|
{
|
||||||
|
$in: {
|
||||||
|
[`${TableName.SecretV2}.id` as "id"]: newSecrets.map((s) => s.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ tx }
|
||||||
|
);
|
||||||
|
return secretsWithTags.map((secret) => ({ ...secret, _id: secret.id }));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fnSecretBulkDelete = async ({
|
export const fnSecretBulkDelete = async ({
|
||||||
@ -533,7 +553,7 @@ export const expandSecretReferencesFactory = ({
|
|||||||
const referredValue = await fetchSecret(environment, secretPath, secretKey);
|
const referredValue = await fetchSecret(environment, secretPath, secretKey);
|
||||||
if (!canExpandValue(environment, secretPath, secretKey, referredValue.tags))
|
if (!canExpandValue(environment, secretPath, secretKey, referredValue.tags))
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to.`
|
message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to read value on.`
|
||||||
});
|
});
|
||||||
|
|
||||||
const cacheKey = getCacheUniqueKey(environment, secretPath);
|
const cacheKey = getCacheUniqueKey(environment, secretPath);
|
||||||
@ -552,7 +572,7 @@ export const expandSecretReferencesFactory = ({
|
|||||||
const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey);
|
const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey);
|
||||||
if (!canExpandValue(secretReferenceEnvironment, secretReferencePath, secretReferenceKey, referedValue.tags))
|
if (!canExpandValue(secretReferenceEnvironment, secretReferencePath, secretReferenceKey, referedValue.tags))
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to.`
|
message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to read value on.`
|
||||||
});
|
});
|
||||||
|
|
||||||
const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath);
|
const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath);
|
||||||
@ -646,13 +666,13 @@ export const reshapeBridgeSecret = (
|
|||||||
name: string;
|
name: string;
|
||||||
}[];
|
}[];
|
||||||
secretMetadata?: ResourceMetadataDTO;
|
secretMetadata?: ResourceMetadataDTO;
|
||||||
}
|
},
|
||||||
|
secretValueHidden: boolean
|
||||||
) => ({
|
) => ({
|
||||||
secretKey: secret.key,
|
secretKey: secret.key,
|
||||||
secretPath,
|
secretPath,
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
environment,
|
environment,
|
||||||
secretValue: secret.value || "",
|
|
||||||
secretComment: secret.comment || "",
|
secretComment: secret.comment || "",
|
||||||
version: secret.version,
|
version: secret.version,
|
||||||
type: secret.type,
|
type: secret.type,
|
||||||
@ -674,5 +694,15 @@ export const reshapeBridgeSecret = (
|
|||||||
metadata: secret.metadata,
|
metadata: secret.metadata,
|
||||||
secretMetadata: secret.secretMetadata,
|
secretMetadata: secret.secretMetadata,
|
||||||
createdAt: secret.createdAt,
|
createdAt: secret.createdAt,
|
||||||
updatedAt: secret.updatedAt
|
updatedAt: secret.updatedAt,
|
||||||
|
|
||||||
|
...(secretValueHidden
|
||||||
|
? {
|
||||||
|
secretValue: INFISICAL_SECRET_VALUE_HIDDEN_MASK,
|
||||||
|
secretValueHidden: true
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
secretValue: secret.value || "",
|
||||||
|
secretValueHidden: false
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,7 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { SecretType, TSecretsV2, TSecretsV2Insert, TSecretsV2Update } from "@app/db/schemas";
|
import { SecretType, TSecretsV2, TSecretsV2Insert, TSecretsV2Update } from "@app/db/schemas";
|
||||||
|
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
|
||||||
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
@ -36,6 +37,8 @@ export type TGetSecretsDTO = {
|
|||||||
includeImports?: boolean;
|
includeImports?: boolean;
|
||||||
recursive?: boolean;
|
recursive?: boolean;
|
||||||
tagSlugs?: string[];
|
tagSlugs?: string[];
|
||||||
|
viewSecretValue: boolean;
|
||||||
|
throwOnMissingReadValuePermission?: boolean;
|
||||||
metadataFilter?: {
|
metadataFilter?: {
|
||||||
key?: string;
|
key?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
@ -48,6 +51,11 @@ export type TGetSecretsDTO = {
|
|||||||
keys?: string[];
|
keys?: string[];
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TGetSecretsMissingReadValuePermissionDTO = Omit<
|
||||||
|
TGetSecretsDTO,
|
||||||
|
"viewSecretValue" | "recursive" | "expandSecretReferences"
|
||||||
|
>;
|
||||||
|
|
||||||
export type TGetASecretDTO = {
|
export type TGetASecretDTO = {
|
||||||
secretName: string;
|
secretName: string;
|
||||||
path: string;
|
path: string;
|
||||||
@ -57,6 +65,7 @@ export type TGetASecretDTO = {
|
|||||||
includeImports?: boolean;
|
includeImports?: boolean;
|
||||||
version?: number;
|
version?: number;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
viewSecretValue: boolean;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TCreateSecretDTO = TProjectPermission & {
|
export type TCreateSecretDTO = TProjectPermission & {
|
||||||
@ -164,9 +173,9 @@ export type TFnSecretBulkInsert = {
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">;
|
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">;
|
||||||
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences">;
|
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "find">;
|
||||||
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
||||||
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2">;
|
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "find">;
|
||||||
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||||
actor?: {
|
actor?: {
|
||||||
type: string;
|
type: string;
|
||||||
@ -192,9 +201,9 @@ export type TFnSecretBulkUpdate = {
|
|||||||
data: TRequireReferenceIfValue & { tags?: string[]; secretMetadata?: ResourceMetadataDTO };
|
data: TRequireReferenceIfValue & { tags?: string[]; secretMetadata?: ResourceMetadataDTO };
|
||||||
}[];
|
}[];
|
||||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||||
secretDAL: Pick<TSecretV2BridgeDALFactory, "bulkUpdate" | "upsertSecretReferences">;
|
secretDAL: Pick<TSecretV2BridgeDALFactory, "bulkUpdate" | "upsertSecretReferences" | "find">;
|
||||||
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
||||||
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2">;
|
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2" | "find">;
|
||||||
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||||
actor?: {
|
actor?: {
|
||||||
type: string;
|
type: string;
|
||||||
@ -340,4 +349,12 @@ export type TGetSecretsRawByFolderMappingsDTO = {
|
|||||||
folderMappings: { folderId: string; path: string; environment: string }[];
|
folderMappings: { folderId: string; path: string; environment: string }[];
|
||||||
userId: string;
|
userId: string;
|
||||||
filters: TFindSecretsByFolderIdsFilter;
|
filters: TFindSecretsByFolderIdsFilter;
|
||||||
|
filterByAction?: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TGetAccessibleSecretsDTO = {
|
||||||
|
environment: string;
|
||||||
|
projectId: string;
|
||||||
|
secretPath: string;
|
||||||
|
filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
|
||||||
|
} & TProjectPermission;
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas";
|
import { SecretVersionsV2Schema, TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas";
|
||||||
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols, TFindOpt } from "@app/lib/knex";
|
import { ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { QueueName } from "@app/queue";
|
import { QueueName } from "@app/queue";
|
||||||
|
|
||||||
@ -13,6 +13,58 @@ export type TSecretVersionV2DALFactory = ReturnType<typeof secretVersionV2Bridge
|
|||||||
export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
||||||
const secretVersionV2Orm = ormify(db, TableName.SecretVersionV2);
|
const secretVersionV2Orm = ormify(db, TableName.SecretVersionV2);
|
||||||
|
|
||||||
|
const findBySecretId = async (secretId: string, { offset, limit, sort, tx }: TFindOpt<TSecretVersionsV2> = {}) => {
|
||||||
|
try {
|
||||||
|
const query = (tx || db.replicaNode())(TableName.SecretVersionV2)
|
||||||
|
.where(`${TableName.SecretVersionV2}.secretId`, secretId)
|
||||||
|
.leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SecretV2JnTag,
|
||||||
|
`${TableName.SecretV2}.id`,
|
||||||
|
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SecretTag,
|
||||||
|
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
|
||||||
|
`${TableName.SecretTag}.id`
|
||||||
|
)
|
||||||
|
.select(selectAllTableCols(TableName.SecretVersionV2))
|
||||||
|
.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"));
|
||||||
|
|
||||||
|
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 docs = await query;
|
||||||
|
|
||||||
|
const data = sqlNestRelationships({
|
||||||
|
data: docs,
|
||||||
|
key: "id",
|
||||||
|
parentMapper: (el) => ({ _id: el.id, ...SecretVersionsV2Schema.parse(el) }),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "tagId",
|
||||||
|
label: "tags" as const,
|
||||||
|
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
|
||||||
|
id,
|
||||||
|
color,
|
||||||
|
slug,
|
||||||
|
name: slug
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: `${TableName.SecretVersionV2}: FindBySecretId` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// This will fetch all latest secret versions from a folder
|
// This will fetch all latest secret versions from a folder
|
||||||
const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => {
|
const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
@ -135,6 +187,17 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SecretVersionV2}.userActorId`
|
`${TableName.SecretVersionV2}.userActorId`
|
||||||
)
|
)
|
||||||
.leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`)
|
.leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`)
|
||||||
|
.leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SecretV2JnTag,
|
||||||
|
`${TableName.SecretV2}.id`,
|
||||||
|
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SecretTag,
|
||||||
|
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
|
||||||
|
`${TableName.SecretTag}.id`
|
||||||
|
)
|
||||||
.where((qb) => {
|
.where((qb) => {
|
||||||
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
|
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
|
||||||
void qb.where(`${TableName.ProjectMembership}.projectId`, projectId);
|
void qb.where(`${TableName.ProjectMembership}.projectId`, projectId);
|
||||||
@ -145,9 +208,12 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
})
|
})
|
||||||
.select(
|
.select(
|
||||||
selectAllTableCols(TableName.SecretVersionV2),
|
selectAllTableCols(TableName.SecretVersionV2),
|
||||||
`${TableName.Users}.username as userActorName`,
|
db.ref("username").withSchema(TableName.Users).as("userActorName"),
|
||||||
`${TableName.Identity}.name as identityActorName`,
|
db.ref("name").withSchema(TableName.Identity).as("identityActorName"),
|
||||||
`${TableName.ProjectMembership}.id as membershipId`
|
db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"),
|
||||||
|
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||||
|
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||||
|
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
|
||||||
);
|
);
|
||||||
|
|
||||||
if (limit) void query.limit(limit);
|
if (limit) void query.limit(limit);
|
||||||
@ -162,14 +228,33 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const docs: Array<
|
const docs = await query;
|
||||||
TSecretVersionsV2 & {
|
|
||||||
userActorName: string | undefined | null;
|
const data = sqlNestRelationships({
|
||||||
identityActorName: string | undefined | null;
|
data: docs,
|
||||||
membershipId: string | undefined | null;
|
key: "id",
|
||||||
}
|
parentMapper: (el) => ({
|
||||||
> = await query;
|
_id: el.id,
|
||||||
return docs;
|
...SecretVersionsV2Schema.parse(el),
|
||||||
|
userActorName: el.userActorName,
|
||||||
|
identityActorName: el.identityActorName,
|
||||||
|
membershipId: el.membershipId
|
||||||
|
}),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "tagId",
|
||||||
|
label: "tags" as const,
|
||||||
|
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
|
||||||
|
id,
|
||||||
|
color,
|
||||||
|
slug,
|
||||||
|
name: slug
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "FindVersionsBySecretIdWithActors" });
|
throw new DatabaseError({ error, name: "FindVersionsBySecretIdWithActors" });
|
||||||
}
|
}
|
||||||
@ -181,6 +266,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
findLatestVersionMany,
|
findLatestVersionMany,
|
||||||
bulkUpdate,
|
bulkUpdate,
|
||||||
findLatestVersionByFolderId,
|
findLatestVersionByFolderId,
|
||||||
findVersionsBySecretIdWithActors
|
findVersionsBySecretIdWithActors,
|
||||||
|
findBySecretId
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -169,6 +169,48 @@ export const secretDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findManySecretsWithTags = async (
|
||||||
|
filter: {
|
||||||
|
secretIds: string[];
|
||||||
|
type: SecretType;
|
||||||
|
},
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const secrets = await (tx || db.replicaNode())(TableName.Secret)
|
||||||
|
.whereIn(`${TableName.Secret}.id` as "id", filter.secretIds)
|
||||||
|
.where("type", filter.type)
|
||||||
|
.leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`)
|
||||||
|
.leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`)
|
||||||
|
.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"));
|
||||||
|
|
||||||
|
const data = sqlNestRelationships({
|
||||||
|
data: secrets,
|
||||||
|
key: "id",
|
||||||
|
parentMapper: (el) => ({ _id: el.id, ...SecretsSchema.parse(el) }),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "tagId",
|
||||||
|
label: "tags" as const,
|
||||||
|
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
|
||||||
|
id,
|
||||||
|
color,
|
||||||
|
slug,
|
||||||
|
name: slug
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "get many secrets with tags" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => {
|
const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
|
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
|
||||||
@ -443,6 +485,7 @@ export const secretDALFactory = (db: TDbClient) => {
|
|||||||
upsertSecretReferences,
|
upsertSecretReferences,
|
||||||
findReferencedSecretReferences,
|
findReferencedSecretReferences,
|
||||||
findAllProjectSecretValues,
|
findAllProjectSecretValues,
|
||||||
pruneSecretReminders
|
pruneSecretReminders,
|
||||||
|
findManySecretsWithTags
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
import { subject } from "@casl/ability";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -12,8 +11,9 @@ import {
|
|||||||
TSecretFolders,
|
TSecretFolders,
|
||||||
TSecrets
|
TSecrets
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
|
import { hasSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import {
|
import {
|
||||||
buildSecretBlindIndexFromName,
|
buildSecretBlindIndexFromName,
|
||||||
@ -51,6 +51,8 @@ import {
|
|||||||
TUpdateManySecretsRawFnFactory
|
TUpdateManySecretsRawFnFactory
|
||||||
} from "./secret-types";
|
} from "./secret-types";
|
||||||
|
|
||||||
|
export const INFISICAL_SECRET_VALUE_HIDDEN_MASK = "<hidden-by-infisical>";
|
||||||
|
|
||||||
export const generateSecretBlindIndexBySalt = async (secretName: string, secretBlindIndexDoc: TSecretBlindIndexes) => {
|
export const generateSecretBlindIndexBySalt = async (secretName: string, secretBlindIndexDoc: TSecretBlindIndexes) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const secretBlindIndex = await buildSecretBlindIndexFromName({
|
const secretBlindIndex = await buildSecretBlindIndexFromName({
|
||||||
@ -189,13 +191,10 @@ export const recursivelyGetSecretPaths = ({
|
|||||||
// Filter out paths that the user does not have permission to access, and paths that are not in the current path
|
// Filter out paths that the user does not have permission to access, and paths that are not in the current path
|
||||||
const allowedPaths = paths.filter(
|
const allowedPaths = paths.filter(
|
||||||
(folder) =>
|
(folder) =>
|
||||||
permission.can(
|
hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||||
ProjectPermissionActions.Read,
|
environment,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
secretPath: folder.path
|
||||||
environment,
|
}) && folder.path.startsWith(currentPath === "/" ? "" : currentPath)
|
||||||
secretPath: folder.path
|
|
||||||
})
|
|
||||||
) && folder.path.startsWith(currentPath === "/" ? "" : currentPath)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return allowedPaths;
|
return allowedPaths;
|
||||||
@ -344,6 +343,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
|
|||||||
|
|
||||||
export const decryptSecretRaw = (
|
export const decryptSecretRaw = (
|
||||||
secret: TSecrets & {
|
secret: TSecrets & {
|
||||||
|
secretValueHidden: boolean;
|
||||||
workspace: string;
|
workspace: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
@ -362,12 +362,14 @@ export const decryptSecretRaw = (
|
|||||||
key
|
key
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretValue = decryptSymmetric128BitHexKeyUTF8({
|
const secretValue = !secret.secretValueHidden
|
||||||
ciphertext: secret.secretValueCiphertext,
|
? decryptSymmetric128BitHexKeyUTF8({
|
||||||
iv: secret.secretValueIV,
|
ciphertext: secret.secretValueCiphertext,
|
||||||
tag: secret.secretValueTag,
|
iv: secret.secretValueIV,
|
||||||
key
|
tag: secret.secretValueTag,
|
||||||
});
|
key
|
||||||
|
})
|
||||||
|
: INFISICAL_SECRET_VALUE_HIDDEN_MASK;
|
||||||
|
|
||||||
let secretComment = "";
|
let secretComment = "";
|
||||||
|
|
||||||
@ -385,6 +387,7 @@ export const decryptSecretRaw = (
|
|||||||
secretPath: secret.secretPath,
|
secretPath: secret.secretPath,
|
||||||
workspace: secret.workspace,
|
workspace: secret.workspace,
|
||||||
environment: secret.environment,
|
environment: secret.environment,
|
||||||
|
secretValueHidden: secret.secretValueHidden,
|
||||||
secretValue,
|
secretValue,
|
||||||
secretComment,
|
secretComment,
|
||||||
version: secret.version,
|
version: secret.version,
|
||||||
@ -1198,3 +1201,23 @@ export const fnDeleteProjectSecretReminders = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const conditionallyHideSecretValue = (
|
||||||
|
shouldHideValue: boolean,
|
||||||
|
{
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag
|
||||||
|
}: {
|
||||||
|
secretValueCiphertext: string;
|
||||||
|
secretValueIV: string;
|
||||||
|
secretValueTag: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
secretValueCiphertext: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueCiphertext,
|
||||||
|
secretValueIV: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueIV,
|
||||||
|
secretValueTag: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueTag,
|
||||||
|
secretValueHidden: shouldHideValue
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -403,7 +403,8 @@ export const secretQueueFactory = ({
|
|||||||
expandSecretReferences,
|
expandSecretReferences,
|
||||||
secretImportDAL,
|
secretImportDAL,
|
||||||
secretImports,
|
secretImports,
|
||||||
hasSecretAccess: () => true
|
hasSecretAccess: () => true,
|
||||||
|
viewSecretValue: true
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
|
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
|
||||||
|
@ -6,14 +6,23 @@ import {
|
|||||||
ActionProjectType,
|
ActionProjectType,
|
||||||
ProjectMembershipRole,
|
ProjectMembershipRole,
|
||||||
ProjectUpgradeStatus,
|
ProjectUpgradeStatus,
|
||||||
|
ProjectVersion,
|
||||||
SecretEncryptionAlgo,
|
SecretEncryptionAlgo,
|
||||||
SecretKeyEncoding,
|
SecretKeyEncoding,
|
||||||
SecretsSchema,
|
SecretsSchema,
|
||||||
SecretType
|
SecretType
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
|
import {
|
||||||
|
hasSecretReadValueOrDescribePermission,
|
||||||
|
throwIfMissingSecretReadValueOrDescribePermission
|
||||||
|
} from "@app/ee/services/permission/permission-fns";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSecretActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
||||||
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
|
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
|
||||||
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
|
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
|
||||||
@ -48,6 +57,7 @@ import { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bri
|
|||||||
import { TGetSecretReferencesTreeDTO } from "../secret-v2-bridge/secret-v2-bridge-types";
|
import { TGetSecretReferencesTreeDTO } from "../secret-v2-bridge/secret-v2-bridge-types";
|
||||||
import { TSecretDALFactory } from "./secret-dal";
|
import { TSecretDALFactory } from "./secret-dal";
|
||||||
import {
|
import {
|
||||||
|
conditionallyHideSecretValue,
|
||||||
decryptSecretRaw,
|
decryptSecretRaw,
|
||||||
fnSecretBlindIndexCheck,
|
fnSecretBlindIndexCheck,
|
||||||
fnSecretBulkDelete,
|
fnSecretBulkDelete,
|
||||||
@ -71,6 +81,7 @@ import {
|
|||||||
TDeleteManySecretRawDTO,
|
TDeleteManySecretRawDTO,
|
||||||
TDeleteSecretDTO,
|
TDeleteSecretDTO,
|
||||||
TDeleteSecretRawDTO,
|
TDeleteSecretRawDTO,
|
||||||
|
TGetAccessibleSecretsDTO,
|
||||||
TGetASecretByIdRawDTO,
|
TGetASecretByIdRawDTO,
|
||||||
TGetASecretDTO,
|
TGetASecretDTO,
|
||||||
TGetASecretRawDTO,
|
TGetASecretRawDTO,
|
||||||
@ -205,7 +216,7 @@ export const secretServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Create,
|
ProjectPermissionSecretActions.Create,
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -323,7 +334,7 @@ export const secretServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionSecretActions.Edit,
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -445,7 +456,23 @@ export const secretServiceFactory = ({
|
|||||||
environmentSlug: folder.environment.slug
|
environmentSlug: folder.environment.slug
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return { ...updatedSecret[0], workspace: projectId, environment, secretPath: path };
|
|
||||||
|
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
|
||||||
|
permission,
|
||||||
|
ProjectPermissionSecretActions.ReadValue,
|
||||||
|
{
|
||||||
|
environment,
|
||||||
|
secretPath: path
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...updatedSecret[0],
|
||||||
|
...conditionallyHideSecretValue(secretValueHidden, updatedSecret[0]),
|
||||||
|
workspace: projectId,
|
||||||
|
environment,
|
||||||
|
secretPath: path
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteSecret = async ({
|
const deleteSecret = async ({
|
||||||
@ -468,7 +495,7 @@ export const secretServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Delete,
|
ProjectPermissionSecretActions.Delete,
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -541,7 +568,23 @@ export const secretServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...deletedSecret[0], _id: deletedSecret[0].id, workspace: projectId, environment, secretPath: path };
|
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
|
||||||
|
permission,
|
||||||
|
ProjectPermissionSecretActions.ReadValue,
|
||||||
|
{
|
||||||
|
environment,
|
||||||
|
secretPath: path
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...deletedSecret[0],
|
||||||
|
...conditionallyHideSecretValue(secretValueHidden, deletedSecret[0]),
|
||||||
|
_id: deletedSecret[0].id,
|
||||||
|
workspace: projectId,
|
||||||
|
environment,
|
||||||
|
secretPath: path
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSecrets = async ({
|
const getSecrets = async ({
|
||||||
@ -589,10 +632,10 @@ export const secretServiceFactory = ({
|
|||||||
|
|
||||||
paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p }));
|
paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p }));
|
||||||
} else {
|
} else {
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||||
ProjectPermissionActions.Read,
|
environment,
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
secretPath: path
|
||||||
);
|
});
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||||
if (!folder) return { secrets: [], imports: [] };
|
if (!folder) return { secrets: [], imports: [] };
|
||||||
@ -614,13 +657,10 @@ export const secretServiceFactory = ({
|
|||||||
// if its service token allow full access over imported one
|
// if its service token allow full access over imported one
|
||||||
actor === ActorType.SERVICE
|
actor === ActorType.SERVICE
|
||||||
? true
|
? true
|
||||||
: permission.can(
|
: hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||||
ProjectPermissionActions.Read,
|
environment: importEnv.slug,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
secretPath: importPath
|
||||||
environment: importEnv.slug,
|
})
|
||||||
secretPath: importPath
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
const importedSecrets = await fnSecretsFromImports({
|
const importedSecrets = await fnSecretsFromImports({
|
||||||
allowedImports,
|
allowedImports,
|
||||||
@ -671,10 +711,11 @@ export const secretServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||||
ProjectPermissionActions.Read,
|
environment,
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
secretPath: path
|
||||||
);
|
});
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||||
if (!folder)
|
if (!folder)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
@ -721,14 +762,12 @@ export const secretServiceFactory = ({
|
|||||||
// if its service token allow full access over imported one
|
// if its service token allow full access over imported one
|
||||||
actor === ActorType.SERVICE
|
actor === ActorType.SERVICE
|
||||||
? true
|
? true
|
||||||
: permission.can(
|
: hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||||
ProjectPermissionActions.Read,
|
environment: importEnv.slug,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
secretPath: importPath
|
||||||
environment: importEnv.slug,
|
})
|
||||||
secretPath: importPath
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const importedSecrets = await fnSecretsFromImports({
|
const importedSecrets = await fnSecretsFromImports({
|
||||||
allowedImports,
|
allowedImports,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
@ -740,6 +779,7 @@ export const secretServiceFactory = ({
|
|||||||
if (secretBlindIndex === importedSecrets[i].secrets[j].secretBlindIndex) {
|
if (secretBlindIndex === importedSecrets[i].secrets[j].secretBlindIndex) {
|
||||||
return {
|
return {
|
||||||
...importedSecrets[i].secrets[j],
|
...importedSecrets[i].secrets[j],
|
||||||
|
secretValueHidden: false,
|
||||||
workspace: projectId,
|
workspace: projectId,
|
||||||
environment: importedSecrets[i].environment,
|
environment: importedSecrets[i].environment,
|
||||||
secretPath: importedSecrets[i].secretPath
|
secretPath: importedSecrets[i].secretPath
|
||||||
@ -750,7 +790,13 @@ export const secretServiceFactory = ({
|
|||||||
}
|
}
|
||||||
if (!secret) throw new NotFoundError({ message: `Secret with name '${secretName}' not found` });
|
if (!secret) throw new NotFoundError({ message: `Secret with name '${secretName}' not found` });
|
||||||
|
|
||||||
return { ...secret, workspace: projectId, environment, secretPath: path };
|
return {
|
||||||
|
...secret,
|
||||||
|
secretValueHidden: false, // Always false because we check permission at the beginning of the function
|
||||||
|
workspace: projectId,
|
||||||
|
environment,
|
||||||
|
secretPath: path
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const createManySecret = async ({
|
const createManySecret = async ({
|
||||||
@ -772,7 +818,7 @@ export const secretServiceFactory = ({
|
|||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Create,
|
ProjectPermissionSecretActions.Create,
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -860,7 +906,7 @@ export const secretServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionSecretActions.Edit,
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -902,8 +948,8 @@ export const secretServiceFactory = ({
|
|||||||
if (tagIds.length !== tags.length) throw new NotFoundError({ message: "One or more tags not found" });
|
if (tagIds.length !== tags.length) throw new NotFoundError({ message: "One or more tags not found" });
|
||||||
|
|
||||||
const references = await getSecretReference(projectId);
|
const references = await getSecretReference(projectId);
|
||||||
const secrets = await secretDAL.transaction(async (tx) =>
|
const secrets = await secretDAL.transaction(async (tx) => {
|
||||||
fnSecretBulkUpdate({
|
const updatedSecrets = await fnSecretBulkUpdate({
|
||||||
folderId,
|
folderId,
|
||||||
projectId,
|
projectId,
|
||||||
tx,
|
tx,
|
||||||
@ -933,8 +979,22 @@ export const secretServiceFactory = ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL
|
secretVersionTagDAL
|
||||||
})
|
});
|
||||||
);
|
|
||||||
|
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
|
||||||
|
permission,
|
||||||
|
ProjectPermissionSecretActions.ReadValue,
|
||||||
|
{
|
||||||
|
environment,
|
||||||
|
secretPath: path
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return updatedSecrets.map((secret) => ({
|
||||||
|
...secret,
|
||||||
|
...conditionallyHideSecretValue(secretValueHidden, secret)
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
await snapshotService.performSnapshot(folderId);
|
await snapshotService.performSnapshot(folderId);
|
||||||
await secretQueueService.syncSecrets({
|
await secretQueueService.syncSecrets({
|
||||||
@ -968,7 +1028,7 @@ export const secretServiceFactory = ({
|
|||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Delete,
|
ProjectPermissionSecretActions.Delete,
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1019,8 +1079,19 @@ export const secretServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
|
||||||
|
permission,
|
||||||
|
ProjectPermissionSecretActions.ReadValue,
|
||||||
|
{
|
||||||
|
environment,
|
||||||
|
secretPath: path
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return secrets;
|
return secrets.map((secret) => ({
|
||||||
|
...secret,
|
||||||
|
...conditionallyHideSecretValue(secretValueHidden, secret)
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
await snapshotService.performSnapshot(folderId);
|
await snapshotService.performSnapshot(folderId);
|
||||||
@ -1181,6 +1252,7 @@ export const secretServiceFactory = ({
|
|||||||
secretName,
|
secretName,
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
environment,
|
environment,
|
||||||
|
viewSecretValue: false,
|
||||||
type: "shared"
|
type: "shared"
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1195,12 +1267,25 @@ export const secretServiceFactory = ({
|
|||||||
| (typeof groupPermissions)[number]
|
| (typeof groupPermissions)[number]
|
||||||
) => {
|
) => {
|
||||||
const allowedActions = [
|
const allowedActions = [
|
||||||
ProjectPermissionActions.Read,
|
ProjectPermissionSecretActions.DescribeSecret,
|
||||||
ProjectPermissionActions.Delete,
|
ProjectPermissionSecretActions.ReadValue,
|
||||||
ProjectPermissionActions.Create,
|
ProjectPermissionSecretActions.Delete,
|
||||||
ProjectPermissionActions.Edit
|
ProjectPermissionSecretActions.Create,
|
||||||
].filter((action) =>
|
ProjectPermissionSecretActions.Edit
|
||||||
entityPermission.permission.can(
|
].filter((action) => {
|
||||||
|
if (
|
||||||
|
action === ProjectPermissionSecretActions.DescribeSecret ||
|
||||||
|
action === ProjectPermissionSecretActions.ReadValue
|
||||||
|
) {
|
||||||
|
return hasSecretReadValueOrDescribePermission(entityPermission.permission, action, {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
secretName,
|
||||||
|
secretTags: secret?.tags?.map((el) => el.slug)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return entityPermission.permission.can(
|
||||||
action,
|
action,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
environment,
|
environment,
|
||||||
@ -1208,8 +1293,8 @@ export const secretServiceFactory = ({
|
|||||||
secretName,
|
secretName,
|
||||||
secretTags: secret?.tags?.map((el) => el.slug)
|
secretTags: secret?.tags?.map((el) => el.slug)
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
);
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...entityPermission,
|
...entityPermission,
|
||||||
@ -1228,6 +1313,39 @@ export const secretServiceFactory = ({
|
|||||||
return { users: usersWithAccess, identities: identitiesWithAccess, groups: groupsWithAccess };
|
return { users: usersWithAccess, identities: identitiesWithAccess, groups: groupsWithAccess };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAccessibleSecrets = async ({
|
||||||
|
projectId,
|
||||||
|
secretPath,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
environment,
|
||||||
|
filterByAction
|
||||||
|
}: TGetAccessibleSecretsDTO) => {
|
||||||
|
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||||
|
|
||||||
|
if (!shouldUseSecretV2Bridge) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Project version does not support this endpoint.",
|
||||||
|
name: "ProjectVersionNotSupported"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const secrets = await secretV2BridgeService.getAccessibleSecrets({
|
||||||
|
projectId,
|
||||||
|
secretPath,
|
||||||
|
environment,
|
||||||
|
filterByAction,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod
|
||||||
|
});
|
||||||
|
|
||||||
|
return secrets;
|
||||||
|
};
|
||||||
|
|
||||||
const getSecretsRaw = async ({
|
const getSecretsRaw = async ({
|
||||||
projectId,
|
projectId,
|
||||||
path,
|
path,
|
||||||
@ -1235,11 +1353,13 @@ export const secretServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
|
viewSecretValue,
|
||||||
environment,
|
environment,
|
||||||
includeImports,
|
includeImports,
|
||||||
expandSecretReferences,
|
expandSecretReferences,
|
||||||
recursive,
|
recursive,
|
||||||
tagSlugs = [],
|
tagSlugs = [],
|
||||||
|
throwOnMissingReadValuePermission = true,
|
||||||
...paramsV2
|
...paramsV2
|
||||||
}: TGetSecretsRawDTO) => {
|
}: TGetSecretsRawDTO) => {
|
||||||
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||||
@ -1250,6 +1370,8 @@ export const secretServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
|
viewSecretValue,
|
||||||
|
throwOnMissingReadValuePermission,
|
||||||
environment,
|
environment,
|
||||||
path,
|
path,
|
||||||
recursive,
|
recursive,
|
||||||
@ -1258,6 +1380,7 @@ export const secretServiceFactory = ({
|
|||||||
tagSlugs,
|
tagSlugs,
|
||||||
...paramsV2
|
...paramsV2
|
||||||
});
|
});
|
||||||
|
|
||||||
return { secrets, imports };
|
return { secrets, imports };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1286,14 +1409,20 @@ export const secretServiceFactory = ({
|
|||||||
recursive
|
recursive
|
||||||
});
|
});
|
||||||
|
|
||||||
const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey));
|
const decryptedSecrets = secrets.map((el) => decryptSecretRaw({ ...el, secretValueHidden: false }, botKey));
|
||||||
const filteredSecrets = tagSlugs.length
|
const filteredSecrets = tagSlugs.length
|
||||||
? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug))))
|
? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug))))
|
||||||
: decryptedSecrets;
|
: decryptedSecrets;
|
||||||
const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => {
|
const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => {
|
||||||
const decryptedImportSecrets = importedSecrets.map((sec) =>
|
const decryptedImportSecrets = importedSecrets.map((sec) =>
|
||||||
decryptSecretRaw(
|
decryptSecretRaw(
|
||||||
{ ...sec, environment: el.environment, workspace: projectId, secretPath: el.secretPath },
|
{
|
||||||
|
...sec,
|
||||||
|
environment: el.environment,
|
||||||
|
workspace: projectId,
|
||||||
|
secretPath: el.secretPath,
|
||||||
|
secretValueHidden: false
|
||||||
|
},
|
||||||
botKey
|
botKey
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -1304,6 +1433,7 @@ export const secretServiceFactory = ({
|
|||||||
const importedEntries = decryptedImportSecrets.reduce(
|
const importedEntries = decryptedImportSecrets.reduce(
|
||||||
(
|
(
|
||||||
accum: {
|
accum: {
|
||||||
|
secretValueHidden: boolean;
|
||||||
secretKey: string;
|
secretKey: string;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
workspace: string;
|
workspace: string;
|
||||||
@ -1347,6 +1477,7 @@ export const secretServiceFactory = ({
|
|||||||
Object.keys(secretsGroupByPath).map((groupedPath) =>
|
Object.keys(secretsGroupByPath).map((groupedPath) =>
|
||||||
Promise.allSettled(
|
Promise.allSettled(
|
||||||
secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => {
|
secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => {
|
||||||
|
if (decryptedSecret.secretValueHidden) return;
|
||||||
const expandedSecretValue = await expandSecret({
|
const expandedSecretValue = await expandSecret({
|
||||||
value: decryptedSecret.secretValue,
|
value: decryptedSecret.secretValue,
|
||||||
secretPath: groupedPath,
|
secretPath: groupedPath,
|
||||||
@ -1363,6 +1494,7 @@ export const secretServiceFactory = ({
|
|||||||
processedImports.map((processedImport) =>
|
processedImports.map((processedImport) =>
|
||||||
Promise.allSettled(
|
Promise.allSettled(
|
||||||
processedImport.secrets.map(async (decryptedSecret, index) => {
|
processedImport.secrets.map(async (decryptedSecret, index) => {
|
||||||
|
if (decryptedSecret.secretValueHidden) return;
|
||||||
const expandedSecretValue = await expandSecret({
|
const expandedSecretValue = await expandSecret({
|
||||||
value: decryptedSecret.secretValue,
|
value: decryptedSecret.secretValue,
|
||||||
secretPath: path,
|
secretPath: path,
|
||||||
@ -1400,6 +1532,7 @@ export const secretServiceFactory = ({
|
|||||||
path,
|
path,
|
||||||
actor,
|
actor,
|
||||||
environment,
|
environment,
|
||||||
|
viewSecretValue,
|
||||||
projectId: workspaceId,
|
projectId: workspaceId,
|
||||||
expandSecretReferences,
|
expandSecretReferences,
|
||||||
projectSlug,
|
projectSlug,
|
||||||
@ -1419,6 +1552,7 @@ export const secretServiceFactory = ({
|
|||||||
includeImports,
|
includeImports,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
path,
|
path,
|
||||||
|
viewSecretValue,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@ -1449,6 +1583,7 @@ export const secretServiceFactory = ({
|
|||||||
message: `Project bot for project with ID '${projectId}' not found. Please upgrade your project.`,
|
message: `Project bot for project with ID '${projectId}' not found. Please upgrade your project.`,
|
||||||
name: "bot_not_found_error"
|
name: "bot_not_found_error"
|
||||||
});
|
});
|
||||||
|
|
||||||
const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey);
|
const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey);
|
||||||
|
|
||||||
if (expandSecretReferences) {
|
if (expandSecretReferences) {
|
||||||
@ -1467,7 +1602,10 @@ export const secretServiceFactory = ({
|
|||||||
decryptedSecret.secretValue = expandedSecretValue || "";
|
decryptedSecret.secretValue = expandedSecretValue || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return { secretMetadata: undefined, ...decryptedSecret };
|
return {
|
||||||
|
secretMetadata: undefined,
|
||||||
|
...decryptedSecret
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const createSecretRaw = async ({
|
const createSecretRaw = async ({
|
||||||
@ -1618,7 +1756,16 @@ export const secretServiceFactory = ({
|
|||||||
tags: tagIds
|
tags: tagIds
|
||||||
});
|
});
|
||||||
|
|
||||||
return { type: SecretProtectionType.Direct as const, secret: decryptSecretRaw(secret, botKey) };
|
return {
|
||||||
|
type: SecretProtectionType.Direct as const,
|
||||||
|
secret: decryptSecretRaw(
|
||||||
|
{
|
||||||
|
...secret,
|
||||||
|
secretValueHidden: false
|
||||||
|
},
|
||||||
|
botKey
|
||||||
|
)
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSecretRaw = async ({
|
const updateSecretRaw = async ({
|
||||||
@ -2014,7 +2161,7 @@ export const secretServiceFactory = ({
|
|||||||
return {
|
return {
|
||||||
type: SecretProtectionType.Direct as const,
|
type: SecretProtectionType.Direct as const,
|
||||||
secrets: secrets.map((secret) =>
|
secrets: secrets.map((secret) =>
|
||||||
decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath }, botKey)
|
decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath, secretValueHidden: false }, botKey)
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -2303,6 +2450,12 @@ export const secretServiceFactory = ({
|
|||||||
const folder = await folderDAL.findById(secret.folderId);
|
const folder = await folderDAL.findById(secret.folderId);
|
||||||
if (!folder) throw new NotFoundError({ message: `Folder with ID '${secret.folderId}' not found` });
|
if (!folder) throw new NotFoundError({ message: `Folder with ID '${secret.folderId}' not found` });
|
||||||
|
|
||||||
|
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]);
|
||||||
|
|
||||||
|
if (!folderWithPath) {
|
||||||
|
throw new NotFoundError({ message: `Folder with ID '${folder.id}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
const { botKey } = await projectBotService.getBotKey(folder.projectId);
|
const { botKey } = await projectBotService.getBotKey(folder.projectId);
|
||||||
if (!botKey)
|
if (!botKey)
|
||||||
throw new NotFoundError({ message: `Project bot for project with ID '${folder.projectId}' not found` });
|
throw new NotFoundError({ message: `Project bot for project with ID '${folder.projectId}' not found` });
|
||||||
@ -2316,18 +2469,43 @@ export const secretServiceFactory = ({
|
|||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||||
const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] });
|
const secretVersions = await secretVersionDAL.findBySecretId(secretId, {
|
||||||
return secretVersions.map((el) =>
|
offset,
|
||||||
decryptSecretRaw(
|
limit,
|
||||||
|
sort: [["createdAt", "desc"]]
|
||||||
|
});
|
||||||
|
return secretVersions.map((el) => {
|
||||||
|
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
||||||
|
ciphertext: secret.secretKeyCiphertext,
|
||||||
|
iv: secret.secretKeyIV,
|
||||||
|
tag: secret.secretKeyTag,
|
||||||
|
key: botKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
|
||||||
|
permission,
|
||||||
|
ProjectPermissionSecretActions.ReadValue,
|
||||||
{
|
{
|
||||||
|
environment: folder.environment.envSlug,
|
||||||
|
secretPath: folderWithPath.path,
|
||||||
|
secretName: secretKey,
|
||||||
|
...(el.tags?.length && {
|
||||||
|
secretTags: el.tags.map((tag) => tag.slug)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return decryptSecretRaw(
|
||||||
|
{
|
||||||
|
secretValueHidden,
|
||||||
...el,
|
...el,
|
||||||
workspace: folder.projectId,
|
workspace: folder.projectId,
|
||||||
environment: folder.environment.envSlug,
|
environment: folder.environment.envSlug,
|
||||||
secretPath: "/"
|
secretPath: folderWithPath.path
|
||||||
},
|
},
|
||||||
botKey
|
botKey
|
||||||
)
|
);
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const attachTags = async ({
|
const attachTags = async ({
|
||||||
@ -2353,7 +2531,7 @@ export const secretServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionSecretActions.Edit,
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -2459,7 +2637,7 @@ export const secretServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionSecretActions.Edit,
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -2625,7 +2803,7 @@ export const secretServiceFactory = ({
|
|||||||
message: `Project with slug '${projectSlug}' not found`
|
message: `Project with slug '${projectSlug}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (project.version === 3) {
|
if (project.version === ProjectVersion.V3) {
|
||||||
return secretV2BridgeService.moveSecrets({
|
return secretV2BridgeService.moveSecrets({
|
||||||
sourceEnvironment,
|
sourceEnvironment,
|
||||||
sourceSecretPath,
|
sourceSecretPath,
|
||||||
@ -2650,30 +2828,6 @@ export const secretServiceFactory = ({
|
|||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Delete,
|
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment: sourceEnvironment,
|
|
||||||
secretPath: sourceSecretPath
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Create,
|
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment: destinationEnvironment,
|
|
||||||
secretPath: destinationSecretPath
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment: destinationEnvironment,
|
|
||||||
secretPath: destinationSecretPath
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const { botKey } = await projectBotService.getBotKey(project.id);
|
const { botKey } = await projectBotService.getBotKey(project.id);
|
||||||
if (!botKey) {
|
if (!botKey) {
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
@ -2701,11 +2855,9 @@ export const secretServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceSecrets = await secretDAL.find({
|
const sourceSecrets = await secretDAL.findManySecretsWithTags({
|
||||||
type: SecretType.Shared,
|
type: SecretType.Shared,
|
||||||
$in: {
|
secretIds
|
||||||
id: secretIds
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (sourceSecrets.length !== secretIds.length) {
|
if (sourceSecrets.length !== secretIds.length) {
|
||||||
@ -2714,21 +2866,62 @@ export const secretServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptedSourceSecrets = sourceSecrets.map((secret) => ({
|
const sourceActions = [
|
||||||
...secret,
|
ProjectPermissionSecretActions.Delete,
|
||||||
secretKey: decryptSymmetric128BitHexKeyUTF8({
|
ProjectPermissionSecretActions.DescribeSecret,
|
||||||
|
ProjectPermissionSecretActions.ReadValue
|
||||||
|
] as const;
|
||||||
|
const destinationActions = [ProjectPermissionSecretActions.Create, ProjectPermissionSecretActions.Edit] as const;
|
||||||
|
|
||||||
|
const decryptedSourceSecrets = sourceSecrets.map((secret) => {
|
||||||
|
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext: secret.secretKeyCiphertext,
|
ciphertext: secret.secretKeyCiphertext,
|
||||||
iv: secret.secretKeyIV,
|
iv: secret.secretKeyIV,
|
||||||
tag: secret.secretKeyTag,
|
tag: secret.secretKeyTag,
|
||||||
key: botKey
|
key: botKey
|
||||||
}),
|
});
|
||||||
secretValue: decryptSymmetric128BitHexKeyUTF8({
|
|
||||||
ciphertext: secret.secretValueCiphertext,
|
for (const destinationAction of destinationActions) {
|
||||||
iv: secret.secretValueIV,
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
tag: secret.secretValueTag,
|
destinationAction,
|
||||||
key: botKey
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
})
|
environment: destinationEnvironment,
|
||||||
}));
|
secretPath: destinationSecretPath
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const sourceAction of sourceActions) {
|
||||||
|
if (
|
||||||
|
sourceAction === ProjectPermissionSecretActions.ReadValue ||
|
||||||
|
sourceAction === ProjectPermissionSecretActions.DescribeSecret
|
||||||
|
) {
|
||||||
|
throwIfMissingSecretReadValueOrDescribePermission(permission, sourceAction, {
|
||||||
|
environment: sourceEnvironment,
|
||||||
|
secretPath: sourceSecretPath
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
sourceAction,
|
||||||
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment: sourceEnvironment,
|
||||||
|
secretPath: sourceSecretPath
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...secret,
|
||||||
|
secretKey,
|
||||||
|
secretValue: decryptSymmetric128BitHexKeyUTF8({
|
||||||
|
ciphertext: secret.secretValueCiphertext,
|
||||||
|
iv: secret.secretValueIV,
|
||||||
|
tag: secret.secretValueTag,
|
||||||
|
key: botKey
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
let isSourceUpdated = false;
|
let isSourceUpdated = false;
|
||||||
let isDestinationUpdated = false;
|
let isDestinationUpdated = false;
|
||||||
@ -3102,6 +3295,7 @@ export const secretServiceFactory = ({
|
|||||||
getSecretReferenceTree,
|
getSecretReferenceTree,
|
||||||
getSecretsRawByFolderMappings,
|
getSecretsRawByFolderMappings,
|
||||||
getSecretAccessList,
|
getSecretAccessList,
|
||||||
getSecretByIdRaw
|
getSecretByIdRaw,
|
||||||
|
getAccessibleSecrets
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -2,6 +2,7 @@ import { Knex } from "knex";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SecretType, TSecretBlindIndexes, TSecrets, TSecretsInsert, TSecretsUpdate } from "@app/db/schemas";
|
import { SecretType, TSecretBlindIndexes, TSecrets, TSecretsInsert, TSecretsUpdate } from "@app/db/schemas";
|
||||||
|
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
|
||||||
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||||
@ -180,10 +181,18 @@ export enum SecretsOrderBy {
|
|||||||
Name = "name" // "key" for secrets but using name for use across resources
|
Name = "name" // "key" for secrets but using name for use across resources
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TGetAccessibleSecretsDTO = {
|
||||||
|
secretPath: string;
|
||||||
|
environment: string;
|
||||||
|
filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TGetSecretsRawDTO = {
|
export type TGetSecretsRawDTO = {
|
||||||
expandSecretReferences?: boolean;
|
expandSecretReferences?: boolean;
|
||||||
path: string;
|
path: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
|
viewSecretValue: boolean;
|
||||||
|
throwOnMissingReadValuePermission?: boolean;
|
||||||
includeImports?: boolean;
|
includeImports?: boolean;
|
||||||
recursive?: boolean;
|
recursive?: boolean;
|
||||||
tagSlugs?: string[];
|
tagSlugs?: string[];
|
||||||
@ -209,6 +218,7 @@ export type TGetASecretRawDTO = {
|
|||||||
secretName: string;
|
secretName: string;
|
||||||
path: string;
|
path: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
|
viewSecretValue: boolean;
|
||||||
expandSecretReferences?: boolean;
|
expandSecretReferences?: boolean;
|
||||||
type: "shared" | "personal";
|
type: "shared" | "personal";
|
||||||
includeImports?: boolean;
|
includeImports?: boolean;
|
||||||
@ -417,7 +427,7 @@ export type TCreateManySecretsRawFnFactory = {
|
|||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
secretV2BridgeDAL: Pick<
|
secretV2BridgeDAL: Pick<
|
||||||
TSecretV2BridgeDALFactory,
|
TSecretV2BridgeDALFactory,
|
||||||
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany"
|
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find"
|
||||||
>;
|
>;
|
||||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||||
@ -454,7 +464,7 @@ export type TUpdateManySecretsRawFnFactory = {
|
|||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
secretV2BridgeDAL: Pick<
|
secretV2BridgeDAL: Pick<
|
||||||
TSecretV2BridgeDALFactory,
|
TSecretV2BridgeDALFactory,
|
||||||
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany"
|
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find"
|
||||||
>;
|
>;
|
||||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas";
|
import { SecretVersionsSchema, TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas";
|
||||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
import { ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { QueueName } from "@app/queue";
|
import { QueueName } from "@app/queue";
|
||||||
|
|
||||||
@ -12,6 +12,50 @@ export type TSecretVersionDALFactory = ReturnType<typeof secretVersionDALFactory
|
|||||||
export const secretVersionDALFactory = (db: TDbClient) => {
|
export const secretVersionDALFactory = (db: TDbClient) => {
|
||||||
const secretVersionOrm = ormify(db, TableName.SecretVersion);
|
const secretVersionOrm = ormify(db, TableName.SecretVersion);
|
||||||
|
|
||||||
|
const findBySecretId = async (secretId: string, { offset, limit, sort, tx }: TFindOpt<TSecretVersions> = {}) => {
|
||||||
|
try {
|
||||||
|
const query = (tx || db.replicaNode())(TableName.SecretVersion)
|
||||||
|
.where(`${TableName.SecretVersion}.secretId`, secretId)
|
||||||
|
.leftJoin(TableName.Secret, `${TableName.SecretVersion}.secretId`, `${TableName.Secret}.id`)
|
||||||
|
.leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`)
|
||||||
|
.leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`)
|
||||||
|
.select(selectAllTableCols(TableName.SecretVersion))
|
||||||
|
.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"));
|
||||||
|
|
||||||
|
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 docs = await query;
|
||||||
|
|
||||||
|
const data = sqlNestRelationships({
|
||||||
|
data: docs,
|
||||||
|
key: "id",
|
||||||
|
parentMapper: (el) => ({ _id: el.id, ...SecretVersionsSchema.parse(el) }),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "tagId",
|
||||||
|
label: "tags" as const,
|
||||||
|
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
|
||||||
|
id,
|
||||||
|
color,
|
||||||
|
slug,
|
||||||
|
name: slug
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: `${TableName.SecretVersion}: FindBySecretId` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// This will fetch all latest secret versions from a folder
|
// This will fetch all latest secret versions from a folder
|
||||||
const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => {
|
const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
@ -149,6 +193,7 @@ export const secretVersionDALFactory = (db: TDbClient) => {
|
|||||||
findLatestVersionMany,
|
findLatestVersionMany,
|
||||||
bulkUpdate,
|
bulkUpdate,
|
||||||
findLatestVersionByFolderId,
|
findLatestVersionByFolderId,
|
||||||
|
findBySecretId,
|
||||||
bulkUpdateNoVersionIncrement
|
bulkUpdateNoVersionIncrement
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -5,7 +5,11 @@ import bcrypt from "bcrypt";
|
|||||||
|
|
||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSecretActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
|
|
||||||
@ -67,7 +71,7 @@ export const serviceTokenServiceFactory = ({
|
|||||||
|
|
||||||
scopes.forEach(({ environment, secretPath }) => {
|
scopes.forEach(({ environment, secretPath }) => {
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Create,
|
ProjectPermissionSecretActions.Create,
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -19,9 +19,11 @@ import { TUserDALFactory } from "../user/user-dal";
|
|||||||
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
|
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
|
||||||
import { UserAliasType } from "../user-alias/user-alias-types";
|
import { UserAliasType } from "../user-alias/user-alias-types";
|
||||||
import { TSuperAdminDALFactory } from "./super-admin-dal";
|
import { TSuperAdminDALFactory } from "./super-admin-dal";
|
||||||
import { LoginMethod, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types";
|
import { LoginMethod, TAdminGetIdentitiesDTO, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types";
|
||||||
|
import { TIdentityDALFactory } from "@app/services/identity/identity-dal";
|
||||||
|
|
||||||
type TSuperAdminServiceFactoryDep = {
|
type TSuperAdminServiceFactoryDep = {
|
||||||
|
identityDAL: Pick<TIdentityDALFactory, "getIdentitiesByFilter">;
|
||||||
serverCfgDAL: TSuperAdminDALFactory;
|
serverCfgDAL: TSuperAdminDALFactory;
|
||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
userAliasDAL: Pick<TUserAliasDALFactory, "findOne">;
|
userAliasDAL: Pick<TUserAliasDALFactory, "findOne">;
|
||||||
@ -51,6 +53,7 @@ const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000";
|
|||||||
export const superAdminServiceFactory = ({
|
export const superAdminServiceFactory = ({
|
||||||
serverCfgDAL,
|
serverCfgDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
|
identityDAL,
|
||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
authService,
|
authService,
|
||||||
orgService,
|
orgService,
|
||||||
@ -271,26 +274,30 @@ export const superAdminServiceFactory = ({
|
|||||||
return { token, user: userInfo, organization };
|
return { token, user: userInfo, organization };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUsers = ({ offset, limit, searchTerm }: TAdminGetUsersDTO) => {
|
const getUsers = ({ offset, limit, searchTerm, adminsOnly }: TAdminGetUsersDTO) => {
|
||||||
return userDAL.getUsersByFilter({
|
return userDAL.getUsersByFilter({
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
sortBy: "username"
|
sortBy: "username",
|
||||||
|
adminsOnly
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteUser = async (userId: string) => {
|
const deleteUser = async (userId: string) => {
|
||||||
if (!licenseService.onPremFeatures?.instanceUserManagement) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Failed to delete user due to plan restriction. Upgrade to Infisical's Pro plan."
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await userDAL.deleteById(userId);
|
const user = await userDAL.deleteById(userId);
|
||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getIdentities = ({ offset, limit, searchTerm }: TAdminGetIdentitiesDTO) => {
|
||||||
|
return identityDAL.getIdentitiesByFilter({
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
searchTerm,
|
||||||
|
sortBy: "name"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const grantServerAdminAccessToUser = async (userId: string) => {
|
const grantServerAdminAccessToUser = async (userId: string) => {
|
||||||
if (!licenseService.onPremFeatures?.instanceUserManagement) {
|
if (!licenseService.onPremFeatures?.instanceUserManagement) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@ -388,6 +395,7 @@ export const superAdminServiceFactory = ({
|
|||||||
adminSignUp,
|
adminSignUp,
|
||||||
getUsers,
|
getUsers,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
|
getIdentities,
|
||||||
getAdminSlackConfig,
|
getAdminSlackConfig,
|
||||||
updateRootEncryptionStrategy,
|
updateRootEncryptionStrategy,
|
||||||
getConfiguredEncryptionStrategies,
|
getConfiguredEncryptionStrategies,
|
||||||
|
@ -20,6 +20,13 @@ export type TAdminGetUsersDTO = {
|
|||||||
offset: number;
|
offset: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
|
adminsOnly: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAdminGetIdentitiesDTO = {
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
searchTerm: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum LoginMethod {
|
export enum LoginMethod {
|
||||||
|
@ -23,15 +23,18 @@ export const userDALFactory = (db: TDbClient) => {
|
|||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
sortBy
|
sortBy,
|
||||||
|
adminsOnly
|
||||||
}: {
|
}: {
|
||||||
limit: number;
|
limit: number;
|
||||||
offset: number;
|
offset: number;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
sortBy?: keyof TUsers;
|
sortBy?: keyof TUsers;
|
||||||
|
adminsOnly: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
let query = db.replicaNode()(TableName.Users).where("isGhost", "=", false);
|
let query = db.replicaNode()(TableName.Users).where("isGhost", "=", false);
|
||||||
|
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
query = query.where((qb) => {
|
query = query.where((qb) => {
|
||||||
void qb
|
void qb
|
||||||
@ -42,6 +45,10 @@ export const userDALFactory = (db: TDbClient) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (adminsOnly) {
|
||||||
|
query = query.where("superAdmin", true);
|
||||||
|
}
|
||||||
|
|
||||||
if (sortBy) {
|
if (sortBy) {
|
||||||
query = query.orderBy(sortBy);
|
query = query.orderBy(sortBy);
|
||||||
}
|
}
|
||||||
|
17
backend/vitest.unit.config.ts
Normal file
17
backend/vitest.unit.config.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import path from "path";
|
||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
env: {
|
||||||
|
NODE_ENV: "test"
|
||||||
|
},
|
||||||
|
include: ["./src/**/*.test.ts"]
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@app": path.resolve(__dirname, "./src")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
12
cli/go.mod
12
cli/go.mod
@ -20,6 +20,7 @@ require (
|
|||||||
github.com/muesli/reflow v0.3.0
|
github.com/muesli/reflow v0.3.0
|
||||||
github.com/muesli/roff v0.1.0
|
github.com/muesli/roff v0.1.0
|
||||||
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9
|
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9
|
||||||
|
github.com/pion/dtls/v3 v3.0.4
|
||||||
github.com/pion/logging v0.2.3
|
github.com/pion/logging v0.2.3
|
||||||
github.com/pion/turn/v4 v4.0.0
|
github.com/pion/turn/v4 v4.0.0
|
||||||
github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a
|
github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a
|
||||||
@ -29,9 +30,9 @@ require (
|
|||||||
github.com/spf13/cobra v1.6.1
|
github.com/spf13/cobra v1.6.1
|
||||||
github.com/spf13/viper v1.8.1
|
github.com/spf13/viper v1.8.1
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
golang.org/x/crypto v0.35.0
|
golang.org/x/crypto v0.36.0
|
||||||
golang.org/x/sys v0.30.0
|
golang.org/x/sys v0.31.0
|
||||||
golang.org/x/term v0.29.0
|
golang.org/x/term v0.30.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -90,7 +91,6 @@ require (
|
|||||||
github.com/oklog/ulid v1.3.1 // indirect
|
github.com/oklog/ulid v1.3.1 // indirect
|
||||||
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
|
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
|
||||||
github.com/pelletier/go-toml v1.9.3 // indirect
|
github.com/pelletier/go-toml v1.9.3 // indirect
|
||||||
github.com/pion/dtls/v3 v3.0.4 // indirect
|
|
||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||||
@ -115,8 +115,8 @@ require (
|
|||||||
golang.org/x/mod v0.23.0 // indirect
|
golang.org/x/mod v0.23.0 // indirect
|
||||||
golang.org/x/net v0.35.0 // indirect
|
golang.org/x/net v0.35.0 // indirect
|
||||||
golang.org/x/oauth2 v0.21.0 // indirect
|
golang.org/x/oauth2 v0.21.0 // indirect
|
||||||
golang.org/x/sync v0.11.0 // indirect
|
golang.org/x/sync v0.12.0 // indirect
|
||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.23.0 // indirect
|
||||||
golang.org/x/time v0.6.0 // indirect
|
golang.org/x/time v0.6.0 // indirect
|
||||||
golang.org/x/tools v0.30.0 // indirect
|
golang.org/x/tools v0.30.0 // indirect
|
||||||
google.golang.org/api v0.188.0 // indirect
|
google.golang.org/api v0.188.0 // indirect
|
||||||
|
22
cli/go.sum
22
cli/go.sum
@ -484,8 +484,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@ -590,8 +590,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@ -640,11 +640,11 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@ -654,8 +654,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
@ -858,4 +858,4 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
|
|||||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
@ -2,6 +2,12 @@ package api
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
|
type Environment struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
// Stores info for login one
|
// Stores info for login one
|
||||||
type LoginOneRequest struct {
|
type LoginOneRequest struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
@ -14,7 +20,6 @@ type LoginOneResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stores info for login two
|
// Stores info for login two
|
||||||
|
|
||||||
type LoginTwoRequest struct {
|
type LoginTwoRequest struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
ClientProof string `json:"clientProof"`
|
ClientProof string `json:"clientProof"`
|
||||||
@ -168,9 +173,10 @@ type Secret struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Project struct {
|
type Project struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Slug string `json:"slug"`
|
Slug string `json:"slug"`
|
||||||
|
Environments []Environment `json:"environments"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RawSecret struct {
|
type RawSecret struct {
|
||||||
|
@ -1,39 +1,34 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// "fmt"
|
|
||||||
|
|
||||||
// "github.com/Infisical/infisical-merge/packages/api"
|
|
||||||
// "github.com/Infisical/infisical-merge/packages/models"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Infisical/infisical-merge/packages/gateway"
|
"github.com/Infisical/infisical-merge/packages/gateway"
|
||||||
"github.com/Infisical/infisical-merge/packages/util"
|
"github.com/Infisical/infisical-merge/packages/util"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
|
|
||||||
// "github.com/Infisical/infisical-merge/packages/visualize"
|
|
||||||
// "github.com/rs/zerolog/log"
|
|
||||||
|
|
||||||
// "github.com/go-resty/resty/v2"
|
|
||||||
"github.com/posthog/posthog-go"
|
"github.com/posthog/posthog-go"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var gatewayCmd = &cobra.Command{
|
var gatewayCmd = &cobra.Command{
|
||||||
Example: `infisical gateway`,
|
|
||||||
Short: "Used to infisical gateway",
|
|
||||||
Use: "gateway",
|
Use: "gateway",
|
||||||
|
Short: "Run the Infisical gateway or manage its systemd service",
|
||||||
|
Long: "Run the Infisical gateway in the foreground or manage its systemd service installation. Use 'gateway install' to set up the systemd service.",
|
||||||
|
Example: `infisical gateway --token=<token>
|
||||||
|
sudo infisical gateway install --token=<token> --domain=<domain>`,
|
||||||
DisableFlagsInUseLine: true,
|
DisableFlagsInUseLine: true,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
token, err := util.GetInfisicalToken(cmd)
|
token, err := util.GetInfisicalToken(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse token flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
if token == nil {
|
if token == nil {
|
||||||
@ -109,6 +104,50 @@ var gatewayCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var gatewayInstallCmd = &cobra.Command{
|
||||||
|
Use: "install",
|
||||||
|
Short: "Install and enable systemd service for the gateway (requires sudo)",
|
||||||
|
Long: "Install and enable systemd service for the gateway. Must be run with sudo on Linux.",
|
||||||
|
Example: "sudo infisical gateway install --token=<token> --domain=<domain>",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if runtime.GOOS != "linux" {
|
||||||
|
util.HandleError(fmt.Errorf("systemd service installation is only supported on Linux"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Geteuid() != 0 {
|
||||||
|
util.HandleError(fmt.Errorf("systemd service installation requires root/sudo privileges"))
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := util.GetInfisicalToken(cmd)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
if token == nil {
|
||||||
|
util.HandleError(fmt.Errorf("Token not found"))
|
||||||
|
}
|
||||||
|
|
||||||
|
domain, err := cmd.Flags().GetString("domain")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse domain flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gateway.InstallGatewaySystemdService(token.Token, domain); err != nil {
|
||||||
|
util.HandleError(err, "Failed to install systemd service")
|
||||||
|
}
|
||||||
|
|
||||||
|
enableCmd := exec.Command("systemctl", "enable", "infisical-gateway")
|
||||||
|
if err := enableCmd.Run(); err != nil {
|
||||||
|
util.HandleError(err, "Failed to enable systemd service")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Msg("Successfully installed and enabled infisical-gateway service")
|
||||||
|
log.Info().Msg("To start the service, run: sudo systemctl start infisical-gateway")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
var gatewayRelayCmd = &cobra.Command{
|
var gatewayRelayCmd = &cobra.Command{
|
||||||
Example: `infisical gateway relay`,
|
Example: `infisical gateway relay`,
|
||||||
Short: "Used to run infisical gateway relay",
|
Short: "Used to run infisical gateway relay",
|
||||||
@ -138,9 +177,12 @@ var gatewayRelayCmd = &cobra.Command{
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
gatewayCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token")
|
gatewayCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token")
|
||||||
|
gatewayInstallCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token")
|
||||||
|
gatewayInstallCmd.Flags().String("domain", "", "Domain of your self-hosted Infisical instance")
|
||||||
|
|
||||||
gatewayRelayCmd.Flags().String("config", "", "Relay config yaml file path")
|
gatewayRelayCmd.Flags().String("config", "", "Relay config yaml file path")
|
||||||
|
|
||||||
|
gatewayCmd.AddCommand(gatewayInstallCmd)
|
||||||
gatewayCmd.AddCommand(gatewayRelayCmd)
|
gatewayCmd.AddCommand(gatewayRelayCmd)
|
||||||
rootCmd.AddCommand(gatewayCmd)
|
rootCmd.AddCommand(gatewayCmd)
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,9 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Infisical/infisical-merge/packages/api"
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
|
||||||
"github.com/Infisical/infisical-merge/packages/models"
|
"github.com/Infisical/infisical-merge/packages/models"
|
||||||
"github.com/Infisical/infisical-merge/packages/util"
|
"github.com/Infisical/infisical-merge/packages/util"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
@ -59,11 +62,11 @@ var runCmd = &cobra.Command{
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
environmentName, _ := cmd.Flags().GetString("env")
|
environmentSlug, _ := cmd.Flags().GetString("env")
|
||||||
if !cmd.Flags().Changed("env") {
|
if !cmd.Flags().Changed("env") {
|
||||||
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
|
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
|
||||||
if environmentFromWorkspace != "" {
|
if environmentFromWorkspace != "" {
|
||||||
environmentName = environmentFromWorkspace
|
environmentSlug = environmentFromWorkspace
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,8 +139,20 @@ var runCmd = &cobra.Command{
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("Confirming selected environment is valid: %s", environmentSlug)
|
||||||
|
|
||||||
|
hasEnvironment, err := confirmProjectHasEnvironment(environmentSlug, projectId, token)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Could not confirm project has environment")
|
||||||
|
}
|
||||||
|
if !hasEnvironment {
|
||||||
|
util.HandleError(fmt.Errorf("project does not have environment '%s'", environmentSlug))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("Project '%s' has environment '%s'", projectId, environmentSlug)
|
||||||
|
|
||||||
request := models.GetAllSecretsParameters{
|
request := models.GetAllSecretsParameters{
|
||||||
Environment: environmentName,
|
Environment: environmentSlug,
|
||||||
WorkspaceId: projectId,
|
WorkspaceId: projectId,
|
||||||
TagSlugs: tagSlugs,
|
TagSlugs: tagSlugs,
|
||||||
SecretsPath: secretsPath,
|
SecretsPath: secretsPath,
|
||||||
@ -308,7 +323,6 @@ func waitForExitCommand(cmd *exec.Cmd) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func executeCommandWithWatchMode(commandFlag string, args []string, watchModeInterval int, request models.GetAllSecretsParameters, projectConfigDir string, secretOverriding bool, token *models.TokenDetails) {
|
func executeCommandWithWatchMode(commandFlag string, args []string, watchModeInterval int, request models.GetAllSecretsParameters, projectConfigDir string, secretOverriding bool, token *models.TokenDetails) {
|
||||||
|
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
var err error
|
var err error
|
||||||
var lastSecretsFetch time.Time
|
var lastSecretsFetch time.Time
|
||||||
@ -439,8 +453,53 @@ func executeCommandWithWatchMode(commandFlag string, args []string, watchModeInt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchAndFormatSecretsForShell(request models.GetAllSecretsParameters, projectConfigDir string, secretOverriding bool, token *models.TokenDetails) (models.InjectableEnvironmentResult, error) {
|
func confirmProjectHasEnvironment(environmentSlug, projectId string, token *models.TokenDetails) (bool, error) {
|
||||||
|
var accessToken string
|
||||||
|
|
||||||
|
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
|
||||||
|
accessToken = token.Token
|
||||||
|
} else {
|
||||||
|
util.RequireLogin()
|
||||||
|
util.RequireLocalWorkspaceFile()
|
||||||
|
|
||||||
|
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to authenticate")
|
||||||
|
}
|
||||||
|
|
||||||
|
if loggedInUserDetails.LoginExpired {
|
||||||
|
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
||||||
|
}
|
||||||
|
accessToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if projectId == "" {
|
||||||
|
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to get local project details")
|
||||||
|
}
|
||||||
|
|
||||||
|
projectId = workspaceFile.WorkspaceId
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient := resty.New()
|
||||||
|
httpClient.SetAuthToken(accessToken).
|
||||||
|
SetHeader("Accept", "application/json")
|
||||||
|
|
||||||
|
project, err := api.CallGetProjectById(httpClient, projectId)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, env := range project.Environments {
|
||||||
|
if env.Slug == environmentSlug {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchAndFormatSecretsForShell(request models.GetAllSecretsParameters, projectConfigDir string, secretOverriding bool, token *models.TokenDetails) (models.InjectableEnvironmentResult, error) {
|
||||||
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
|
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
|
||||||
request.InfisicalToken = token.Token
|
request.InfisicalToken = token.Token
|
||||||
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
|
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/Infisical/infisical-merge/packages/api"
|
"github.com/Infisical/infisical-merge/packages/api"
|
||||||
"github.com/Infisical/infisical-merge/packages/systemd"
|
"github.com/Infisical/infisical-merge/packages/systemd"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
|
"github.com/pion/dtls/v3"
|
||||||
"github.com/pion/logging"
|
"github.com/pion/logging"
|
||||||
"github.com/pion/turn/v4"
|
"github.com/pion/turn/v4"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
@ -54,26 +55,6 @@ func (g *Gateway) ConnectWithRelay() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
relayAddress, relayPort := strings.Split(relayDetails.TurnServerAddress, ":")[0], strings.Split(relayDetails.TurnServerAddress, ":")[1]
|
relayAddress, relayPort := strings.Split(relayDetails.TurnServerAddress, ":")[0], strings.Split(relayDetails.TurnServerAddress, ":")[1]
|
||||||
var conn net.Conn
|
|
||||||
|
|
||||||
// Dial TURN Server
|
|
||||||
if relayPort == "5349" {
|
|
||||||
log.Info().Msgf("Provided relay port %s. Using TLS", relayPort)
|
|
||||||
conn, err = tls.Dial("tcp", relayDetails.TurnServerAddress, &tls.Config{
|
|
||||||
ServerName: relayAddress,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
log.Info().Msgf("Provided relay port %s. Using non TLS connection.", relayPort)
|
|
||||||
peerAddr, errPeer := net.ResolveTCPAddr("tcp", relayDetails.TurnServerAddress)
|
|
||||||
if errPeer != nil {
|
|
||||||
return fmt.Errorf("Failed to parse turn server address: %w", err)
|
|
||||||
}
|
|
||||||
conn, err = net.DialTCP("tcp", nil, peerAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to connect with relay server: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start a new TURN Client and wrap our net.Conn in a STUNConn
|
// Start a new TURN Client and wrap our net.Conn in a STUNConn
|
||||||
// This allows us to simulate datagram based communication over a net.Conn
|
// This allows us to simulate datagram based communication over a net.Conn
|
||||||
@ -81,17 +62,42 @@ func (g *Gateway) ConnectWithRelay() error {
|
|||||||
if os.Getenv("LOG_LEVEL") == "debug" {
|
if os.Getenv("LOG_LEVEL") == "debug" {
|
||||||
logger.DefaultLogLevel = logging.LogLevelDebug
|
logger.DefaultLogLevel = logging.LogLevelDebug
|
||||||
}
|
}
|
||||||
cfg := &turn.ClientConfig{
|
|
||||||
|
turnClientCfg := &turn.ClientConfig{
|
||||||
STUNServerAddr: relayDetails.TurnServerAddress,
|
STUNServerAddr: relayDetails.TurnServerAddress,
|
||||||
TURNServerAddr: relayDetails.TurnServerAddress,
|
TURNServerAddr: relayDetails.TurnServerAddress,
|
||||||
Conn: turn.NewSTUNConn(conn),
|
|
||||||
Username: relayDetails.TurnServerUsername,
|
Username: relayDetails.TurnServerUsername,
|
||||||
Password: relayDetails.TurnServerPassword,
|
Password: relayDetails.TurnServerPassword,
|
||||||
Realm: relayDetails.TurnServerRealm,
|
Realm: relayDetails.TurnServerRealm,
|
||||||
LoggerFactory: logger,
|
LoggerFactory: logger,
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := turn.NewClient(cfg)
|
turnAddr, err := net.ResolveUDPAddr("udp4", relayDetails.TurnServerAddress)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to parse turn server address: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial TURN Server
|
||||||
|
if relayPort == "5349" {
|
||||||
|
log.Info().Msgf("Provided relay port %s. Using TLS", relayPort)
|
||||||
|
conn, err := dtls.Dial("udp", turnAddr, &dtls.Config{
|
||||||
|
ServerName: relayAddress,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to connect with relay server: %w", err)
|
||||||
|
}
|
||||||
|
turnClientCfg.Conn = turn.NewSTUNConn(conn)
|
||||||
|
} else {
|
||||||
|
log.Info().Msgf("Provided relay port %s. Using non TLS connection.", relayPort)
|
||||||
|
conn, err := net.ListenPacket("udp4", turnAddr.String())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to connect with relay server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
turnClientCfg.Conn = conn
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := turn.NewClient(turnClientCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to create relay client: %w", err)
|
return fmt.Errorf("Failed to create relay client: %w", err)
|
||||||
}
|
}
|
||||||
@ -168,7 +174,6 @@ func (g *Gateway) Listen(ctx context.Context) error {
|
|||||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||||
NextProtos: []string{"infisical-gateway"},
|
NextProtos: []string{"infisical-gateway"},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup QUIC listener on the relayConn
|
// Setup QUIC listener on the relayConn
|
||||||
quicConfig := &quic.Config{
|
quicConfig := &quic.Config{
|
||||||
EnableDatagrams: true,
|
EnableDatagrams: true,
|
||||||
@ -176,7 +181,6 @@ func (g *Gateway) Listen(ctx context.Context) error {
|
|||||||
KeepAlivePeriod: 2 * time.Second,
|
KeepAlivePeriod: 2 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
g.registerRelayIsActive(ctx, errCh)
|
|
||||||
quicListener, err := quic.Listen(relayUdpConnection, tlsConfig, quicConfig)
|
quicListener, err := quic.Listen(relayUdpConnection, tlsConfig, quicConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to listen for QUIC: %w", err)
|
return fmt.Errorf("Failed to listen for QUIC: %w", err)
|
||||||
@ -185,6 +189,8 @@ func (g *Gateway) Listen(ctx context.Context) error {
|
|||||||
|
|
||||||
log.Printf("Listener started on %s", quicListener.Addr())
|
log.Printf("Listener started on %s", quicListener.Addr())
|
||||||
|
|
||||||
|
g.registerRelayIsActive(ctx, errCh)
|
||||||
|
|
||||||
log.Info().Msg("Gateway started successfully")
|
log.Info().Msg("Gateway started successfully")
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
@ -326,7 +332,6 @@ func (g *Gateway) registerRelayIsActive(ctx context.Context, errCh chan error) e
|
|||||||
failures := 0
|
failures := 0
|
||||||
|
|
||||||
log.Info().Msg("Starting relay connection health check")
|
log.Info().Msg("Starting relay connection health check")
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
for {
|
for {
|
||||||
@ -335,36 +340,17 @@ func (g *Gateway) registerRelayIsActive(ctx context.Context, errCh chan error) e
|
|||||||
log.Info().Msg("Stopping relay connection health check")
|
log.Info().Msg("Stopping relay connection health check")
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
func() {
|
log.Debug().Msg("Performing relay connection health check")
|
||||||
log.Debug().Msg("Performing relay connection health check")
|
err := g.createPermissionForStaticIps(g.config.InfisicalStaticIp)
|
||||||
|
if err != nil && !strings.Contains(err.Error(), "tls:") {
|
||||||
if g.client == nil {
|
failures++
|
||||||
failures++
|
log.Warn().Err(err).Int("failures", failures).Msg("Failed to refresh TURN permissions")
|
||||||
log.Warn().Int("failures", failures).Msg("TURN client is nil")
|
if failures >= maxFailures {
|
||||||
if failures >= maxFailures {
|
errCh <- fmt.Errorf("relay connection check failed: %w", err)
|
||||||
errCh <- fmt.Errorf("relay connection check failed: TURN client is nil")
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
continue
|
||||||
// we try to refresh permissions - this is a lightweight operation
|
}
|
||||||
// that will fail immediately if the UDP connection is broken. good for health check
|
|
||||||
log.Debug().Msg("Refreshing TURN permissions to verify connection")
|
|
||||||
if err := g.createPermissionForStaticIps(g.config.InfisicalStaticIp); err != nil {
|
|
||||||
failures++
|
|
||||||
log.Warn().Err(err).Int("failures", failures).Msg("Failed to refresh TURN permissions")
|
|
||||||
if failures >= maxFailures {
|
|
||||||
errCh <- fmt.Errorf("relay connection check failed: %w", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Msg("Successfully refreshed TURN permissions - connection is healthy")
|
|
||||||
if failures > 0 {
|
|
||||||
log.Info().Int("previous_failures", failures).Msg("Relay connection restored")
|
|
||||||
failures = 0
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user