Compare commits
381 Commits
infisical/
...
daniel/fix
Author | SHA1 | Date | |
---|---|---|---|
|
53983d13f3 | ||
|
5d9e47aec6 | ||
|
be968be813 | ||
|
dc60d59e2e | ||
|
e3f48e72b0 | ||
|
3c6b7aee9a | ||
|
a183e94ff4 | ||
|
b54e780443 | ||
|
5376bb72b3 | ||
|
56d0d59ddc | ||
|
ef9d4a4eee | ||
|
873c6eea18 | ||
|
8d8e0bb794 | ||
|
348cf1c50c | ||
|
05669efdd8 | ||
|
c302630551 | ||
|
2a4c9100be | ||
|
9ced5717ac | ||
|
b2f2541d0b | ||
|
3a9ad8d306 | ||
|
10207d03dd | ||
|
832dd62158 | ||
|
df29f3499f | ||
|
0d4c05f537 | ||
|
fb0407fec8 | ||
|
7d899463b4 | ||
|
cfaf076352 | ||
|
a875489172 | ||
|
8634f8348b | ||
|
ad5b16d448 | ||
|
62e6acb7dc | ||
|
bf50eed8b0 | ||
|
dcd69b5d99 | ||
|
dda98a0036 | ||
|
24ab66f61f | ||
|
9b97afad1c | ||
|
dca3832fd4 | ||
|
c3458a9d34 | ||
|
76d371f13c | ||
|
7e9bcc5ce1 | ||
|
029f2fa3af | ||
|
0e9b2a8045 | ||
|
514cf07ba5 | ||
|
55efc58566 | ||
|
9b03f4984a | ||
|
a04f938b6c | ||
|
596a22e9eb | ||
|
2ea01537c0 | ||
|
ab57c658a8 | ||
|
89030c92c7 | ||
|
d842aef714 | ||
|
53089e26b9 | ||
|
be890b4189 | ||
|
cd0f126cf2 | ||
|
a092b9a19f | ||
|
2f043849c9 | ||
|
d15fa9f176 | ||
|
61508ec90a | ||
|
9d84bfa69e | ||
|
6a1d465778 | ||
|
5eff705486 | ||
|
cd532bc20d | ||
|
18cdaaf024 | ||
|
74e1dbdf9b | ||
|
64ab75748c | ||
|
f7b689158d | ||
|
19b9a31f0b | ||
|
0568cdcec6 | ||
|
a4bc459576 | ||
|
b0b73acc21 | ||
|
07d66cbb65 | ||
|
ee97782860 | ||
|
c856de534b | ||
|
eefd71f4cc | ||
|
77e9609d0c | ||
|
afbbe5b7ba | ||
|
54d5cdedab | ||
|
9e12935a9f | ||
|
101fa56d83 | ||
|
9bceb99110 | ||
|
ca7a0a73be | ||
|
3632361f3c | ||
|
f5c0274844 | ||
|
36a11387dd | ||
|
a82c94472a | ||
|
508f9610ca | ||
|
59065c0648 | ||
|
6443c94283 | ||
|
26611881bc | ||
|
2852989ac1 | ||
|
124bb7c205 | ||
|
697445cb1f | ||
|
04108907ba | ||
|
411cac2a31 | ||
|
afb9920fca | ||
|
ccf99d2465 | ||
|
bca84f74c5 | ||
|
6c93973db7 | ||
|
8d3f8c94fb | ||
|
2eeb7dbc41 | ||
|
f18624d2e4 | ||
|
42a49da17b | ||
|
5d87ce866c | ||
|
02d7f90ec2 | ||
|
03564fc59b | ||
|
8669f5c39a | ||
|
c2bd2e6963 | ||
|
eb23d114a2 | ||
|
dec2cd465b | ||
|
4cdec49751 | ||
|
43967ef848 | ||
|
55046d4144 | ||
|
124acfd279 | ||
|
62e12269b8 | ||
|
f03d8b718e | ||
|
acf13df0f3 | ||
|
cb8ec57177 | ||
|
b543f2ce50 | ||
|
f852e629ef | ||
|
58b74d97bb | ||
|
ba12aab65a | ||
|
952c4a3931 | ||
|
4a1bae07ca | ||
|
c24f72435a | ||
|
4bf378c28d | ||
|
407c8e17d3 | ||
|
67b7fb819a | ||
|
edfccb2ae2 | ||
|
df8dc43bcf | ||
|
0d610f2644 | ||
|
a422d211fe | ||
|
f66d5e3d28 | ||
|
2c4e951fe2 | ||
|
e23d2dff64 | ||
|
e7de6ad5d9 | ||
|
ca0d79d664 | ||
|
adc0552df0 | ||
|
cff79e7c8c | ||
|
450e653005 | ||
|
c866e55d1b | ||
|
d66c2a85f4 | ||
|
3b8c0a5cb1 | ||
|
b77f0fed45 | ||
|
e8bc47b573 | ||
|
c6785eff3a | ||
|
bc1a9055ee | ||
|
dbe1f2bcff | ||
|
15107ebfaa | ||
|
435a395a15 | ||
|
fe829af054 | ||
|
bd9dc44a69 | ||
|
765dd84d19 | ||
|
ac100e17f4 | ||
|
e349f9aa3b | ||
|
29c3c41ebb | ||
|
e4af0759b8 | ||
|
c681774709 | ||
|
f63f2d9c69 | ||
|
044662901a | ||
|
8cdb2082d9 | ||
|
52d0f5e1be | ||
|
be1e7be0d5 | ||
|
e1b0bc1b97 | ||
|
f05d1b9d95 | ||
|
fa2bd6a75e | ||
|
2402ce2a12 | ||
|
f770a18d41 | ||
|
8ab7470f74 | ||
|
eb56c23db1 | ||
|
14812adade | ||
|
99b1efffc7 | ||
|
af6189c82b | ||
|
b6ca18af5d | ||
|
ee7bb6d60d | ||
|
bfde867ba7 | ||
|
1a20f3148c | ||
|
ce5b14222f | ||
|
74a43d55f7 | ||
|
85cce4274e | ||
|
9eb88836e9 | ||
|
d6c9658747 | ||
|
f9967c0cc8 | ||
|
bd8dfe4089 | ||
|
03fcaadab2 | ||
|
d3a0a84815 | ||
|
49ae146470 | ||
|
f73b362c84 | ||
|
d9043fa9e0 | ||
|
98f6dc8df9 | ||
|
12c67d921d | ||
|
7dea2ba916 | ||
|
ace27a3605 | ||
|
e85ea1a458 | ||
|
fb16464fda | ||
|
c6b636bb42 | ||
|
034ac68b58 | ||
|
33e2c52f14 | ||
|
b435a06a92 | ||
|
48c23db3f9 | ||
|
3159972ec3 | ||
|
8a5c293a6e | ||
|
1d9c18d155 | ||
|
13945bb31d | ||
|
9df9197cac | ||
|
3809729e31 | ||
|
03d29a4afc | ||
|
a4264335fe | ||
|
7752bab0f0 | ||
|
56a20dc397 | ||
|
6f79d8bb6c | ||
|
044ac01100 | ||
|
641c0308f9 | ||
|
ecfb833797 | ||
|
256f14cf6a | ||
|
32c28227b2 | ||
|
3be6402727 | ||
|
90c09c64cb | ||
|
d0da69b999 | ||
|
7fb3730b22 | ||
|
49e154ddd1 | ||
|
3742976bcb | ||
|
5695137f24 | ||
|
13d7cfd41b | ||
|
81fc5d3c18 | ||
|
8e8f44895d | ||
|
45570490a0 | ||
|
1add5d6a24 | ||
|
7ac0536236 | ||
|
89e9f46ae5 | ||
|
e3728b8a61 | ||
|
92bbabde3c | ||
|
11b4c5381a | ||
|
97496c1b3c | ||
|
3cac1acf08 | ||
|
c3756b8cc0 | ||
|
8678c79c02 | ||
|
d2f010d17d | ||
|
5c8d5e8430 | ||
|
7c8d99875a | ||
|
ab30b0803f | ||
|
e2d68f07d1 | ||
|
07ced66538 | ||
|
9cb0ec231b | ||
|
8b169b2b9e | ||
|
b9c02264c7 | ||
|
9f96a9d188 | ||
|
55f232a642 | ||
|
34ff65d09c | ||
|
fe38c79f68 | ||
|
a8aecc378b | ||
|
9ce7161aea | ||
|
1951ca723c | ||
|
416f85f7e2 | ||
|
75bef6fc8b | ||
|
5fa6e8bcf2 | ||
|
f4a5d9c391 | ||
|
3c6b976d8f | ||
|
787d2287a0 | ||
|
92f73d66f0 | ||
|
3cd8670064 | ||
|
4e3dd15d67 | ||
|
4c97ba1221 | ||
|
b89128fb32 | ||
|
c788c0cb80 | ||
|
e3fde17622 | ||
|
2eb9f30ef5 | ||
|
9432f3ce4a | ||
|
5393afbd05 | ||
|
cb304d9a10 | ||
|
a5f29db670 | ||
|
009b49685c | ||
|
90d3f4d643 | ||
|
cc08d31300 | ||
|
71deb7c62a | ||
|
efec1c0a96 | ||
|
efa4b7a4b6 | ||
|
65e0077d6c | ||
|
1be311ffd9 | ||
|
3994962d0b | ||
|
0b9334f34c | ||
|
2b4396547d | ||
|
761ec8dcc0 | ||
|
56e69bc5e9 | ||
|
067faef6a2 | ||
|
026b934a87 | ||
|
90eef0495e | ||
|
119fe97b14 | ||
|
dab3daee86 | ||
|
f2e344c11d | ||
|
df1a879e73 | ||
|
fb21d4e13d | ||
|
ca6f50a257 | ||
|
26f0adbf7e | ||
|
456d9ca5ce | ||
|
e652fd962c | ||
|
bc16484f3f | ||
|
4e87cc7c28 | ||
|
d9e2b99338 | ||
|
9bac996c7a | ||
|
089d57ea59 | ||
|
14a17d638d | ||
|
5d9755b332 | ||
|
4f6b73518e | ||
|
2f4965659c | ||
|
2dc12693b0 | ||
|
5305139a55 | ||
|
db72c07e81 | ||
|
2f3ae5429a | ||
|
56e216c37c | ||
|
8db3544885 | ||
|
f196c6a0ce | ||
|
246eecc23c | ||
|
013b744706 | ||
|
fe68328aeb | ||
|
1dcfd14431 | ||
|
f232f00f77 | ||
|
82517477cb | ||
|
4e149cce81 | ||
|
5894cb4049 | ||
|
4938dda303 | ||
|
62112447a6 | ||
|
bead911e0f | ||
|
49987ca1e5 | ||
|
cec083aa9b | ||
|
9146079317 | ||
|
243ffc9904 | ||
|
b07d29faa2 | ||
|
60fcc42d8c | ||
|
fabf7181fa | ||
|
c9a7b6abb6 | ||
|
3c53befb3e | ||
|
1f0cf6cc9b | ||
|
84c19a7554 | ||
|
d3ea91c54b | ||
|
ecb58b8680 | ||
|
1972a3c6ed | ||
|
a0b1fb23df | ||
|
5910c11d88 | ||
|
a768496c5e | ||
|
e59cc138d9 | ||
|
7a7d41ca83 | ||
|
bd8b56a224 | ||
|
aa5bd117e6 | ||
|
e66e6a7490 | ||
|
e54f499026 | ||
|
7a5e0e9463 | ||
|
87b571d6ff | ||
|
1e6af8ad8f | ||
|
a771ddf859 | ||
|
c4cd6909bb | ||
|
49642480d3 | ||
|
b667dccc0d | ||
|
fdda247120 | ||
|
ee8a88d062 | ||
|
33349839cd | ||
|
8f3883c7d4 | ||
|
38cfb7fd41 | ||
|
a331eb8dc4 | ||
|
2dcb409d3b | ||
|
39bcb73f3d | ||
|
52189111d7 | ||
|
f369761920 | ||
|
8eb22630b6 | ||
|
d650fd68c0 | ||
|
387c899193 | ||
|
37882e6344 | ||
|
68a1aa6f46 | ||
|
fa18ca41ac | ||
|
8485fdc1cd | ||
|
49ae2386c0 | ||
|
f2b1f3f0e7 | ||
|
69aa20e35c | ||
|
524c7ae78f | ||
|
e13f7a7486 | ||
|
1867fb2fc4 | ||
|
5dd144b97b | ||
|
b1b430e003 | ||
|
fb09980413 | ||
|
3b36cb8b3d | ||
|
be6a98d0bb | ||
|
dcaa7f1fce |
65
.env.example
Normal file
@@ -0,0 +1,65 @@
|
||||
# Keys
|
||||
# Required key for platform encryption/decryption ops
|
||||
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
||||
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
|
||||
|
||||
# JWT
|
||||
# Required secrets to sign JWT tokens
|
||||
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
||||
AUTH_SECRET=5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE=
|
||||
|
||||
# Postgres creds
|
||||
POSTGRES_PASSWORD=infisical
|
||||
POSTGRES_USER=infisical
|
||||
POSTGRES_DB=infisical
|
||||
|
||||
# Required
|
||||
DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://redis:6379
|
||||
|
||||
# Website URL
|
||||
# Required
|
||||
SITE_URL=http://localhost:8080
|
||||
|
||||
# Mail/SMTP
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=
|
||||
SMTP_NAME=
|
||||
SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
|
||||
# Integration
|
||||
# Optional only if integration is used
|
||||
CLIENT_ID_HEROKU=
|
||||
CLIENT_ID_VERCEL=
|
||||
CLIENT_ID_NETLIFY=
|
||||
CLIENT_ID_GITHUB=
|
||||
CLIENT_ID_GITLAB=
|
||||
CLIENT_ID_BITBUCKET=
|
||||
CLIENT_SECRET_HEROKU=
|
||||
CLIENT_SECRET_VERCEL=
|
||||
CLIENT_SECRET_NETLIFY=
|
||||
CLIENT_SECRET_GITHUB=
|
||||
CLIENT_SECRET_GITLAB=
|
||||
CLIENT_SECRET_BITBUCKET=
|
||||
CLIENT_SLUG_VERCEL=
|
||||
|
||||
# Sentry (optional) for monitoring errors
|
||||
SENTRY_DSN=
|
||||
|
||||
# Infisical Cloud-specific configs
|
||||
# Ignore - Not applicable for self-hosted version
|
||||
POSTHOG_HOST=
|
||||
POSTHOG_PROJECT_API_KEY=
|
||||
|
||||
# SSO-specific variables
|
||||
CLIENT_ID_GOOGLE_LOGIN=
|
||||
CLIENT_SECRET_GOOGLE_LOGIN=
|
||||
|
||||
CLIENT_ID_GITHUB_LOGIN=
|
||||
CLIENT_SECRET_GITHUB_LOGIN=
|
||||
|
||||
CLIENT_ID_GITLAB_LOGIN=
|
||||
CLIENT_SECRET_GITLAB_LOGIN=
|
@@ -2,4 +2,5 @@
|
||||
frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx:generic-api-key:206
|
||||
frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:304
|
||||
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx:generic-api-key:206
|
||||
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:292
|
||||
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:292
|
||||
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451
|
||||
|
6
backend/src/@types/fastify.d.ts
vendored
@@ -1,8 +1,11 @@
|
||||
import "fastify";
|
||||
|
||||
import { TUsers } from "@app/db/schemas";
|
||||
import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service";
|
||||
import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service";
|
||||
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
|
||||
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
||||
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
||||
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
|
||||
@@ -112,6 +115,8 @@ declare module "fastify" {
|
||||
identityAccessToken: TIdentityAccessTokenServiceFactory;
|
||||
identityProject: TIdentityProjectServiceFactory;
|
||||
identityUa: TIdentityUaServiceFactory;
|
||||
accessApprovalPolicy: TAccessApprovalPolicyServiceFactory;
|
||||
accessApprovalRequest: TAccessApprovalRequestServiceFactory;
|
||||
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
|
||||
secretApprovalRequest: TSecretApprovalRequestServiceFactory;
|
||||
secretRotation: TSecretRotationServiceFactory;
|
||||
@@ -120,6 +125,7 @@ declare module "fastify" {
|
||||
scim: TScimServiceFactory;
|
||||
ldap: TLdapConfigServiceFactory;
|
||||
auditLog: TAuditLogServiceFactory;
|
||||
auditLogStream: TAuditLogStreamServiceFactory;
|
||||
secretScanning: TSecretScanningServiceFactory;
|
||||
license: TLicenseServiceFactory;
|
||||
trustedIp: TTrustedIpServiceFactory;
|
||||
|
53
backend/src/@types/knex.d.ts
vendored
@@ -2,11 +2,26 @@ import { Knex } from "knex";
|
||||
|
||||
import {
|
||||
TableName,
|
||||
TAccessApprovalPolicies,
|
||||
TAccessApprovalPoliciesApprovers,
|
||||
TAccessApprovalPoliciesApproversInsert,
|
||||
TAccessApprovalPoliciesApproversUpdate,
|
||||
TAccessApprovalPoliciesInsert,
|
||||
TAccessApprovalPoliciesUpdate,
|
||||
TAccessApprovalRequests,
|
||||
TAccessApprovalRequestsInsert,
|
||||
TAccessApprovalRequestsReviewers,
|
||||
TAccessApprovalRequestsReviewersInsert,
|
||||
TAccessApprovalRequestsReviewersUpdate,
|
||||
TAccessApprovalRequestsUpdate,
|
||||
TApiKeys,
|
||||
TApiKeysInsert,
|
||||
TApiKeysUpdate,
|
||||
TAuditLogs,
|
||||
TAuditLogsInsert,
|
||||
TAuditLogStreams,
|
||||
TAuditLogStreamsInsert,
|
||||
TAuditLogStreamsUpdate,
|
||||
TAuditLogsUpdate,
|
||||
TAuthTokens,
|
||||
TAuthTokenSessions,
|
||||
@@ -35,6 +50,9 @@ import {
|
||||
TGroupProjectMemberships,
|
||||
TGroupProjectMembershipsInsert,
|
||||
TGroupProjectMembershipsUpdate,
|
||||
TGroupProjectUserAdditionalPrivilege,
|
||||
TGroupProjectUserAdditionalPrivilegeInsert,
|
||||
TGroupProjectUserAdditionalPrivilegeUpdate,
|
||||
TGroups,
|
||||
TGroupsInsert,
|
||||
TGroupsUpdate,
|
||||
@@ -275,6 +293,11 @@ declare module "knex/types/tables" {
|
||||
TProjectUserMembershipRolesInsert,
|
||||
TProjectUserMembershipRolesUpdate
|
||||
>;
|
||||
[TableName.GroupProjectUserAdditionalPrivilege]: Knex.CompositeTableType<
|
||||
TGroupProjectUserAdditionalPrivilege,
|
||||
TGroupProjectUserAdditionalPrivilegeInsert,
|
||||
TGroupProjectUserAdditionalPrivilegeUpdate
|
||||
>;
|
||||
[TableName.ProjectRoles]: Knex.CompositeTableType<TProjectRoles, TProjectRolesInsert, TProjectRolesUpdate>;
|
||||
[TableName.ProjectUserAdditionalPrivilege]: Knex.CompositeTableType<
|
||||
TProjectUserAdditionalPrivilege,
|
||||
@@ -341,6 +364,31 @@ declare module "knex/types/tables" {
|
||||
TIdentityProjectAdditionalPrivilegeInsert,
|
||||
TIdentityProjectAdditionalPrivilegeUpdate
|
||||
>;
|
||||
|
||||
[TableName.AccessApprovalPolicy]: Knex.CompositeTableType<
|
||||
TAccessApprovalPolicies,
|
||||
TAccessApprovalPoliciesInsert,
|
||||
TAccessApprovalPoliciesUpdate
|
||||
>;
|
||||
|
||||
[TableName.AccessApprovalPolicyApprover]: Knex.CompositeTableType<
|
||||
TAccessApprovalPoliciesApprovers,
|
||||
TAccessApprovalPoliciesApproversInsert,
|
||||
TAccessApprovalPoliciesApproversUpdate
|
||||
>;
|
||||
|
||||
[TableName.AccessApprovalRequest]: Knex.CompositeTableType<
|
||||
TAccessApprovalRequests,
|
||||
TAccessApprovalRequestsInsert,
|
||||
TAccessApprovalRequestsUpdate
|
||||
>;
|
||||
|
||||
[TableName.AccessApprovalRequestReviewer]: Knex.CompositeTableType<
|
||||
TAccessApprovalRequestsReviewers,
|
||||
TAccessApprovalRequestsReviewersInsert,
|
||||
TAccessApprovalRequestsReviewersUpdate
|
||||
>;
|
||||
|
||||
[TableName.ScimToken]: Knex.CompositeTableType<TScimTokens, TScimTokensInsert, TScimTokensUpdate>;
|
||||
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
|
||||
TSecretApprovalPolicies,
|
||||
@@ -404,6 +452,11 @@ declare module "knex/types/tables" {
|
||||
[TableName.LdapGroupMap]: Knex.CompositeTableType<TLdapGroupMaps, TLdapGroupMapsInsert, TLdapGroupMapsUpdate>;
|
||||
[TableName.OrgBot]: Knex.CompositeTableType<TOrgBots, TOrgBotsInsert, TOrgBotsUpdate>;
|
||||
[TableName.AuditLog]: Knex.CompositeTableType<TAuditLogs, TAuditLogsInsert, TAuditLogsUpdate>;
|
||||
[TableName.AuditLogStream]: Knex.CompositeTableType<
|
||||
TAuditLogStreams,
|
||||
TAuditLogStreamsInsert,
|
||||
TAuditLogStreamsUpdate
|
||||
>;
|
||||
[TableName.GitAppInstallSession]: Knex.CompositeTableType<
|
||||
TGitAppInstallSessions,
|
||||
TGitAppInstallSessionsInsert,
|
||||
|
28
backend/src/db/migrations/20240503101144_audit-log-stream.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.AuditLogStream))) {
|
||||
await knex.schema.createTable(TableName.AuditLogStream, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("url").notNullable();
|
||||
t.text("encryptedHeadersCiphertext");
|
||||
t.text("encryptedHeadersIV");
|
||||
t.text("encryptedHeadersTag");
|
||||
t.string("encryptedHeadersAlgorithm");
|
||||
t.string("encryptedHeadersKeyEncoding");
|
||||
t.uuid("orgId").notNullable();
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.AuditLogStream);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await dropOnUpdateTrigger(knex, TableName.AuditLogStream);
|
||||
await knex.schema.dropTableIfExists(TableName.AuditLogStream);
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.AccessApprovalPolicy))) {
|
||||
await knex.schema.createTable(TableName.AccessApprovalPolicy, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("name").notNullable();
|
||||
t.integer("approvals").defaultTo(1).notNullable();
|
||||
t.uuid("envId").notNullable();
|
||||
t.string("secretPath");
|
||||
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicy);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover))) {
|
||||
await knex.schema.createTable(TableName.AccessApprovalPolicyApprover, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.uuid("approverUserId").nullable();
|
||||
t.foreign("approverUserId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||
|
||||
t.uuid("policyId").notNullable();
|
||||
t.foreign("policyId").references("id").inTable(TableName.AccessApprovalPolicy).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover);
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyApprover);
|
||||
await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicy);
|
||||
|
||||
await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover);
|
||||
await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicy);
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.GroupProjectUserAdditionalPrivilege))) {
|
||||
await knex.schema.createTable(TableName.GroupProjectUserAdditionalPrivilege, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("slug", 60).notNullable();
|
||||
|
||||
t.uuid("groupProjectMembershipId").notNullable();
|
||||
t.foreign("groupProjectMembershipId")
|
||||
.references("id")
|
||||
.inTable(TableName.GroupProjectMembership)
|
||||
.onDelete("CASCADE");
|
||||
|
||||
t.uuid("requestedByUserId").notNullable();
|
||||
t.foreign("requestedByUserId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||
|
||||
t.boolean("isTemporary").notNullable().defaultTo(false);
|
||||
t.string("temporaryMode");
|
||||
t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc
|
||||
t.datetime("temporaryAccessStartTime");
|
||||
t.datetime("temporaryAccessEndTime");
|
||||
t.jsonb("permissions").notNullable();
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.GroupProjectUserAdditionalPrivilege);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await dropOnUpdateTrigger(knex, TableName.GroupProjectUserAdditionalPrivilege);
|
||||
await knex.schema.dropTableIfExists(TableName.GroupProjectUserAdditionalPrivilege);
|
||||
}
|
@@ -0,0 +1,69 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.AccessApprovalRequest))) {
|
||||
await knex.schema.createTable(TableName.AccessApprovalRequest, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.uuid("policyId").notNullable();
|
||||
t.foreign("policyId").references("id").inTable(TableName.AccessApprovalPolicy).onDelete("CASCADE");
|
||||
|
||||
t.uuid("projectUserPrivilegeId").nullable();
|
||||
t.foreign("projectUserPrivilegeId")
|
||||
.references("id")
|
||||
.inTable(TableName.ProjectUserAdditionalPrivilege)
|
||||
.onDelete("CASCADE");
|
||||
|
||||
t.uuid("groupProjectUserPrivilegeId").nullable();
|
||||
t.foreign("groupProjectUserPrivilegeId")
|
||||
.references("id")
|
||||
.inTable(TableName.GroupProjectUserAdditionalPrivilege)
|
||||
.onDelete("CASCADE");
|
||||
|
||||
t.uuid("requestedByUserId").notNullable();
|
||||
t.foreign("requestedByUserId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||
|
||||
t.uuid("projectMembershipId").nullable();
|
||||
t.foreign("projectMembershipId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
|
||||
|
||||
t.uuid("groupMembershipId").nullable();
|
||||
t.foreign("groupMembershipId").references("id").inTable(TableName.GroupProjectMembership).onDelete("CASCADE");
|
||||
|
||||
// We use these values to create the actual privilege at a later point in time.
|
||||
t.boolean("isTemporary").notNullable();
|
||||
t.string("temporaryRange").nullable();
|
||||
|
||||
t.jsonb("permissions").notNullable();
|
||||
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
await createOnUpdateTrigger(knex, TableName.AccessApprovalRequest);
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.AccessApprovalRequestReviewer))) {
|
||||
await knex.schema.createTable(TableName.AccessApprovalRequestReviewer, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.uuid("memberUserId").notNullable();
|
||||
t.foreign("memberUserId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||
|
||||
t.string("status").notNullable();
|
||||
t.uuid("requestId").notNullable();
|
||||
t.foreign("requestId").references("id").inTable(TableName.AccessApprovalRequest).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.AccessApprovalRequestReviewer);
|
||||
await knex.schema.dropTableIfExists(TableName.AccessApprovalRequest);
|
||||
|
||||
await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer);
|
||||
await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequest);
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
// SecretApprovalPolicyApprover, approverUserId
|
||||
if (!(await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverUserId"))) {
|
||||
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (t) => {
|
||||
t.uuid("approverId").nullable().alter();
|
||||
|
||||
t.uuid("approverUserId").nullable();
|
||||
t.foreign("approverUserId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
// SecretApprovalRequest, statusChangeByUserId
|
||||
if (!(await knex.schema.hasColumn(TableName.SecretApprovalRequest, "statusChangeByUserId"))) {
|
||||
await knex.schema.alterTable(TableName.SecretApprovalRequest, (t) => {
|
||||
t.uuid("statusChangeBy").nullable().alter();
|
||||
|
||||
t.uuid("statusChangeByUserId").nullable();
|
||||
t.foreign("statusChangeByUserId").references("id").inTable(TableName.Users).onDelete("SET NULL");
|
||||
});
|
||||
}
|
||||
|
||||
// SecretApprovalRequest, committerUserId
|
||||
if (!(await knex.schema.hasColumn(TableName.SecretApprovalRequest, "committerUserId"))) {
|
||||
await knex.schema.alterTable(TableName.SecretApprovalRequest, (t) => {
|
||||
t.uuid("committerId").nullable().alter();
|
||||
|
||||
t.uuid("committerUserId").nullable();
|
||||
t.foreign("committerUserId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
// SecretApprovalRequestReviewer, memberUserId
|
||||
if (!(await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "memberUserId"))) {
|
||||
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (t) => {
|
||||
t.uuid("member").nullable().alter();
|
||||
|
||||
t.uuid("memberUserId").nullable();
|
||||
t.foreign("memberUserId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverUserId")) {
|
||||
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (t) => {
|
||||
t.dropColumn("approverUserId");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.SecretApprovalRequest, "statusChangeByUserId")) {
|
||||
await knex.schema.alterTable(TableName.SecretApprovalRequest, (t) => {
|
||||
t.dropColumn("statusChangeByUserId");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.SecretApprovalRequest, "committerUserId")) {
|
||||
await knex.schema.alterTable(TableName.SecretApprovalRequest, (t) => {
|
||||
t.dropColumn("committerUserId");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "memberUserId")) {
|
||||
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (t) => {
|
||||
t.dropColumn("memberUserId");
|
||||
});
|
||||
}
|
||||
}
|
25
backend/src/db/schemas/access-approval-policies-approvers.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const AccessApprovalPoliciesApproversSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
approverUserId: z.string().uuid().nullable().optional(),
|
||||
policyId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>;
|
||||
export type TAccessApprovalPoliciesApproversInsert = Omit<
|
||||
z.input<typeof AccessApprovalPoliciesApproversSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TAccessApprovalPoliciesApproversUpdate = Partial<
|
||||
Omit<z.input<typeof AccessApprovalPoliciesApproversSchema>, TImmutableDBKeys>
|
||||
>;
|
24
backend/src/db/schemas/access-approval-policies.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const AccessApprovalPoliciesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
approvals: z.number().default(1),
|
||||
envId: z.string().uuid(),
|
||||
secretPath: z.string().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TAccessApprovalPolicies = z.infer<typeof AccessApprovalPoliciesSchema>;
|
||||
export type TAccessApprovalPoliciesInsert = Omit<z.input<typeof AccessApprovalPoliciesSchema>, TImmutableDBKeys>;
|
||||
export type TAccessApprovalPoliciesUpdate = Partial<
|
||||
Omit<z.input<typeof AccessApprovalPoliciesSchema>, TImmutableDBKeys>
|
||||
>;
|
26
backend/src/db/schemas/access-approval-requests-reviewers.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const AccessApprovalRequestsReviewersSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
memberUserId: z.string().uuid(),
|
||||
status: z.string(),
|
||||
requestId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TAccessApprovalRequestsReviewers = z.infer<typeof AccessApprovalRequestsReviewersSchema>;
|
||||
export type TAccessApprovalRequestsReviewersInsert = Omit<
|
||||
z.input<typeof AccessApprovalRequestsReviewersSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TAccessApprovalRequestsReviewersUpdate = Partial<
|
||||
Omit<z.input<typeof AccessApprovalRequestsReviewersSchema>, TImmutableDBKeys>
|
||||
>;
|
29
backend/src/db/schemas/access-approval-requests.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const AccessApprovalRequestsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
policyId: z.string().uuid(),
|
||||
projectUserPrivilegeId: z.string().uuid().nullable().optional(),
|
||||
groupProjectUserPrivilegeId: z.string().uuid().nullable().optional(),
|
||||
requestedByUserId: z.string().uuid(),
|
||||
projectMembershipId: z.string().uuid().nullable().optional(),
|
||||
groupMembershipId: z.string().uuid().nullable().optional(),
|
||||
isTemporary: z.boolean(),
|
||||
temporaryRange: z.string().nullable().optional(),
|
||||
permissions: z.unknown(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TAccessApprovalRequests = z.infer<typeof AccessApprovalRequestsSchema>;
|
||||
export type TAccessApprovalRequestsInsert = Omit<z.input<typeof AccessApprovalRequestsSchema>, TImmutableDBKeys>;
|
||||
export type TAccessApprovalRequestsUpdate = Partial<
|
||||
Omit<z.input<typeof AccessApprovalRequestsSchema>, TImmutableDBKeys>
|
||||
>;
|
25
backend/src/db/schemas/audit-log-streams.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const AuditLogStreamsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
url: z.string(),
|
||||
encryptedHeadersCiphertext: z.string().nullable().optional(),
|
||||
encryptedHeadersIV: z.string().nullable().optional(),
|
||||
encryptedHeadersTag: z.string().nullable().optional(),
|
||||
encryptedHeadersAlgorithm: z.string().nullable().optional(),
|
||||
encryptedHeadersKeyEncoding: z.string().nullable().optional(),
|
||||
orgId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TAuditLogStreams = z.infer<typeof AuditLogStreamsSchema>;
|
||||
export type TAuditLogStreamsInsert = Omit<z.input<typeof AuditLogStreamsSchema>, TImmutableDBKeys>;
|
||||
export type TAuditLogStreamsUpdate = Partial<Omit<z.input<typeof AuditLogStreamsSchema>, TImmutableDBKeys>>;
|
@@ -0,0 +1,32 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const GroupProjectUserAdditionalPrivilegeSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
slug: z.string(),
|
||||
groupProjectMembershipId: z.string().uuid(),
|
||||
requestedByUserId: z.string().uuid(),
|
||||
isTemporary: z.boolean().default(false),
|
||||
temporaryMode: z.string().nullable().optional(),
|
||||
temporaryRange: z.string().nullable().optional(),
|
||||
temporaryAccessStartTime: z.date().nullable().optional(),
|
||||
temporaryAccessEndTime: z.date().nullable().optional(),
|
||||
permissions: z.unknown(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TGroupProjectUserAdditionalPrivilege = z.infer<typeof GroupProjectUserAdditionalPrivilegeSchema>;
|
||||
export type TGroupProjectUserAdditionalPrivilegeInsert = Omit<
|
||||
z.input<typeof GroupProjectUserAdditionalPrivilegeSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TGroupProjectUserAdditionalPrivilegeUpdate = Partial<
|
||||
Omit<z.input<typeof GroupProjectUserAdditionalPrivilegeSchema>, TImmutableDBKeys>
|
||||
>;
|
@@ -1,4 +1,9 @@
|
||||
export * from "./access-approval-policies";
|
||||
export * from "./access-approval-policies-approvers";
|
||||
export * from "./access-approval-requests";
|
||||
export * from "./access-approval-requests-reviewers";
|
||||
export * from "./api-keys";
|
||||
export * from "./audit-log-streams";
|
||||
export * from "./audit-logs";
|
||||
export * from "./auth-token-sessions";
|
||||
export * from "./auth-tokens";
|
||||
@@ -9,6 +14,7 @@ export * from "./git-app-install-sessions";
|
||||
export * from "./git-app-org";
|
||||
export * from "./group-project-membership-roles";
|
||||
export * from "./group-project-memberships";
|
||||
export * from "./group-project-user-additional-privilege";
|
||||
export * from "./groups";
|
||||
export * from "./identities";
|
||||
export * from "./identity-access-tokens";
|
||||
|
@@ -25,6 +25,7 @@ export enum TableName {
|
||||
ProjectMembership = "project_memberships",
|
||||
ProjectRoles = "project_roles",
|
||||
ProjectUserAdditionalPrivilege = "project_user_additional_privilege",
|
||||
GroupProjectUserAdditionalPrivilege = "group_project_user_additional_privilege",
|
||||
ProjectUserMembershipRole = "project_user_membership_roles",
|
||||
ProjectKeys = "project_keys",
|
||||
Secret = "secrets",
|
||||
@@ -50,6 +51,10 @@ export enum TableName {
|
||||
IdentityProjectMembershipRole = "identity_project_membership_role",
|
||||
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
|
||||
ScimToken = "scim_tokens",
|
||||
AccessApprovalPolicy = "access_approval_policies",
|
||||
AccessApprovalPolicyApprover = "access_approval_policies_approvers",
|
||||
AccessApprovalRequest = "access_approval_requests",
|
||||
AccessApprovalRequestReviewer = "access_approval_requests_reviewers",
|
||||
SecretApprovalPolicy = "secret_approval_policies",
|
||||
SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
|
||||
SecretApprovalRequest = "secret_approval_requests",
|
||||
@@ -62,6 +67,7 @@ export enum TableName {
|
||||
LdapConfig = "ldap_configs",
|
||||
LdapGroupMap = "ldap_group_maps",
|
||||
AuditLog = "audit_logs",
|
||||
AuditLogStream = "audit_log_streams",
|
||||
GitAppInstallSession = "git_app_install_sessions",
|
||||
GitAppOrg = "git_app_org",
|
||||
SecretScanningGitRisk = "secret_scanning_git_risks",
|
||||
|
@@ -9,10 +9,11 @@ import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SecretApprovalPoliciesApproversSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
approverId: z.string().uuid(),
|
||||
approverId: z.string().uuid().nullable().optional(),
|
||||
policyId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
approverUserId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSecretApprovalPoliciesApprovers = z.infer<typeof SecretApprovalPoliciesApproversSchema>;
|
||||
|
@@ -9,11 +9,12 @@ import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SecretApprovalRequestsReviewersSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
member: z.string().uuid(),
|
||||
member: z.string().uuid().nullable().optional(),
|
||||
status: z.string(),
|
||||
requestId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
memberUserId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSecretApprovalRequestsReviewers = z.infer<typeof SecretApprovalRequestsReviewersSchema>;
|
||||
|
@@ -16,9 +16,11 @@ export const SecretApprovalRequestsSchema = z.object({
|
||||
slug: z.string(),
|
||||
folderId: z.string().uuid(),
|
||||
statusChangeBy: z.string().uuid().nullable().optional(),
|
||||
committerId: z.string().uuid(),
|
||||
committerId: z.string().uuid().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
statusChangeByUserId: z.string().uuid().nullable().optional(),
|
||||
committerUserId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSecretApprovalRequests = z.infer<typeof SecretApprovalRequestsSchema>;
|
||||
|
170
backend/src/ee/routes/v1/access-approval-policy-router.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "POST",
|
||||
schema: {
|
||||
body: z
|
||||
.object({
|
||||
projectSlug: z.string().trim(),
|
||||
name: z.string().optional(),
|
||||
secretPath: z.string().trim().default("/"),
|
||||
environment: z.string(),
|
||||
approvers: z.string().array().min(1),
|
||||
approvals: z.number().min(1).default(1)
|
||||
})
|
||||
.refine((data) => data.approvals <= data.approvers.length, {
|
||||
path: ["approvals"],
|
||||
message: "The number of approvals should be lower than the number of approvers."
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.accessApprovalPolicy.createAccessApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body,
|
||||
projectSlug: req.body.projectSlug,
|
||||
name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`
|
||||
});
|
||||
return { approval };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "GET",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectSlug: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approvals: sapPubSchema
|
||||
.extend({ approvers: z.string().nullish().array(), secretPath: z.string().optional() })
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const approvals = await server.services.accessApprovalPolicy.getAccessApprovalPolicyByProjectSlug({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectSlug: req.query.projectSlug
|
||||
});
|
||||
return { approvals };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/count",
|
||||
method: "GET",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectSlug: z.string(),
|
||||
envSlug: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
count: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { count } = await server.services.accessApprovalPolicy.getAccessPolicyCountByEnvSlug({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectSlug: req.query.projectSlug,
|
||||
actorOrgId: req.permission.orgId,
|
||||
envSlug: req.query.envSlug
|
||||
});
|
||||
return { count };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:policyId",
|
||||
method: "PATCH",
|
||||
schema: {
|
||||
params: z.object({
|
||||
policyId: z.string()
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
secretPath: z
|
||||
.string()
|
||||
.trim()
|
||||
.optional()
|
||||
.transform((val) => (val === "" ? "/" : val)),
|
||||
approvers: z.string().array().min(1),
|
||||
approvals: z.number().min(1).default(1)
|
||||
})
|
||||
.refine((data) => data.approvals <= data.approvers.length, {
|
||||
path: ["approvals"],
|
||||
message: "The number of approvals should be lower than the number of approvers."
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
await server.services.accessApprovalPolicy.updateAccessApprovalPolicy({
|
||||
policyId: req.params.policyId,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
...req.body
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:policyId",
|
||||
method: "DELETE",
|
||||
schema: {
|
||||
params: z.object({
|
||||
policyId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.accessApprovalPolicy.deleteAccessApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
policyId: req.params.policyId
|
||||
});
|
||||
return { approval };
|
||||
}
|
||||
});
|
||||
};
|
192
backend/src/ee/routes/v1/access-approval-request-router.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema } from "@app/db/schemas";
|
||||
import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerAccessApprovalRequestRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "POST",
|
||||
schema: {
|
||||
body: z.object({
|
||||
permissions: z.any().array(),
|
||||
isTemporary: z.boolean(),
|
||||
temporaryRange: z.string().optional()
|
||||
}),
|
||||
querystring: z.object({
|
||||
projectSlug: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: AccessApprovalRequestsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { request } = await server.services.accessApprovalRequest.createAccessApprovalRequest({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
permissions: req.body.permissions,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectSlug: req.query.projectSlug,
|
||||
temporaryRange: req.body.temporaryRange,
|
||||
isTemporary: req.body.isTemporary
|
||||
});
|
||||
return { approval: request };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/count",
|
||||
method: "GET",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectSlug: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
pendingCount: z.number(),
|
||||
finalizedCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { count } = await server.services.accessApprovalRequest.getCount({
|
||||
projectSlug: req.query.projectSlug,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod
|
||||
});
|
||||
|
||||
return { ...count };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "GET",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectSlug: z.string().trim(),
|
||||
authorUserId: z.string().trim().optional(),
|
||||
envSlug: z.string().trim().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
requests: AccessApprovalRequestsSchema.extend({
|
||||
environmentName: z.string(),
|
||||
isApproved: z.boolean(),
|
||||
privilege: z
|
||||
.object({
|
||||
projectMembershipId: z.string().nullish(),
|
||||
groupMembershipId: z.string().nullish(),
|
||||
isTemporary: z.boolean(),
|
||||
temporaryMode: z.string().nullish(),
|
||||
temporaryRange: z.string().nullish(),
|
||||
temporaryAccessStartTime: z.date().nullish(),
|
||||
temporaryAccessEndTime: z.date().nullish(),
|
||||
permissions: z.unknown()
|
||||
})
|
||||
.nullable(),
|
||||
policy: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
approvals: z.number(),
|
||||
approvers: z.string().array(),
|
||||
secretPath: z.string().nullish(),
|
||||
envId: z.string()
|
||||
}),
|
||||
reviewers: z
|
||||
.object({
|
||||
member: z.string(),
|
||||
status: z.string()
|
||||
})
|
||||
.array()
|
||||
}).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { requests } = await server.services.accessApprovalRequest.listApprovalRequests({
|
||||
projectSlug: req.query.projectSlug,
|
||||
envSlug: req.query.envSlug,
|
||||
authorUserId: req.query.authorUserId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod
|
||||
});
|
||||
|
||||
return { requests };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:requestId",
|
||||
method: "DELETE",
|
||||
schema: {
|
||||
params: z.object({
|
||||
requestId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
projectSlug: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
request: AccessApprovalRequestsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { request } = await server.services.accessApprovalRequest.deleteAccessApprovalRequest({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
requestId: req.params.requestId,
|
||||
projectSlug: req.query.projectSlug
|
||||
});
|
||||
|
||||
return { request };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:requestId/review",
|
||||
method: "POST",
|
||||
schema: {
|
||||
params: z.object({
|
||||
requestId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED])
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
review: AccessApprovalRequestsReviewersSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const review = await server.services.accessApprovalRequest.reviewAccessRequest({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
requestId: req.params.requestId,
|
||||
status: req.body.status
|
||||
});
|
||||
|
||||
return { review };
|
||||
}
|
||||
});
|
||||
};
|
215
backend/src/ee/routes/v1/audit-log-stream-router.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AUDIT_LOG_STREAMS } from "@app/lib/api-docs";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { SanitizedAuditLogStreamSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerAuditLogStreamRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Create an Audit Log Stream.",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
body: z.object({
|
||||
url: z.string().min(1).describe(AUDIT_LOG_STREAMS.CREATE.url),
|
||||
headers: z
|
||||
.object({
|
||||
key: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.CREATE.headers.key),
|
||||
value: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.CREATE.headers.value)
|
||||
})
|
||||
.describe(AUDIT_LOG_STREAMS.CREATE.headers.desc)
|
||||
.array()
|
||||
.optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
auditLogStream: SanitizedAuditLogStreamSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const auditLogStream = await server.services.auditLogStream.create({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
url: req.body.url,
|
||||
headers: req.body.headers
|
||||
});
|
||||
|
||||
return { auditLogStream };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Update an Audit Log Stream by ID.",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
id: z.string().describe(AUDIT_LOG_STREAMS.UPDATE.id)
|
||||
}),
|
||||
body: z.object({
|
||||
url: z.string().optional().describe(AUDIT_LOG_STREAMS.UPDATE.url),
|
||||
headers: z
|
||||
.object({
|
||||
key: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.UPDATE.headers.key),
|
||||
value: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.UPDATE.headers.value)
|
||||
})
|
||||
.describe(AUDIT_LOG_STREAMS.UPDATE.headers.desc)
|
||||
.array()
|
||||
.optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
auditLogStream: SanitizedAuditLogStreamSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const auditLogStream = await server.services.auditLogStream.updateById({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
id: req.params.id,
|
||||
url: req.body.url,
|
||||
headers: req.body.headers
|
||||
});
|
||||
|
||||
return { auditLogStream };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Delete an Audit Log Stream by ID.",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
id: z.string().describe(AUDIT_LOG_STREAMS.DELETE.id)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
auditLogStream: SanitizedAuditLogStreamSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const auditLogStream = await server.services.auditLogStream.deleteById({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
id: req.params.id
|
||||
});
|
||||
|
||||
return { auditLogStream };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Get an Audit Log Stream by ID.",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
id: z.string().describe(AUDIT_LOG_STREAMS.GET_BY_ID.id)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
auditLogStream: SanitizedAuditLogStreamSchema.extend({
|
||||
headers: z
|
||||
.object({
|
||||
key: z.string(),
|
||||
value: z.string()
|
||||
})
|
||||
.array()
|
||||
.optional()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const auditLogStream = await server.services.auditLogStream.getById({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
id: req.params.id
|
||||
});
|
||||
|
||||
return { auditLogStream };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "List Audit Log Streams.",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
response: {
|
||||
200: z.object({
|
||||
auditLogStreams: SanitizedAuditLogStreamSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const auditLogStreams = await server.services.auditLogStream.list({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod
|
||||
});
|
||||
|
||||
return { auditLogStreams };
|
||||
}
|
||||
});
|
||||
};
|
@@ -1,3 +1,6 @@
|
||||
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
|
||||
import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router";
|
||||
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
|
||||
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
||||
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
||||
import { registerGroupRouter } from "./group-router";
|
||||
@@ -40,6 +43,9 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
prefix: "/secret-rotation-providers"
|
||||
});
|
||||
|
||||
await server.register(registerAccessApprovalPolicyRouter, { prefix: "/access-approvals/policies" });
|
||||
await server.register(registerAccessApprovalRequestRouter, { prefix: "/access-approvals/requests" });
|
||||
|
||||
await server.register(
|
||||
async (dynamicSecretRouter) => {
|
||||
await dynamicSecretRouter.register(registerDynamicSecretRouter);
|
||||
@@ -55,6 +61,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
|
||||
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
|
||||
await server.register(registerGroupRouter, { prefix: "/groups" });
|
||||
await server.register(registerAuditLogStreamRouter, { prefix: "/audit-log-streams" });
|
||||
await server.register(
|
||||
async (privilegeRouter) => {
|
||||
await privilegeRouter.register(registerUserAdditionalPrivilegeRouter, { prefix: "/users" });
|
||||
|
@@ -130,7 +130,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approvals: sapPubSchema.merge(z.object({ approvers: z.string().array() })).array()
|
||||
approvals: sapPubSchema.merge(z.object({ approvers: z.string().nullish().array() })).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -161,7 +161,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
policy: sapPubSchema.merge(z.object({ approvers: z.string().array() })).optional()
|
||||
policy: sapPubSchema.merge(z.object({ approvers: z.string().nullish().array() })).optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
@@ -197,7 +197,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
type: isClosing ? EventType.SECRET_APPROVAL_CLOSED : EventType.SECRET_APPROVAL_REOPENED,
|
||||
// eslint-disable-next-line
|
||||
metadata: {
|
||||
[isClosing ? ("closedBy" as const) : ("reopenedBy" as const)]: approval.statusChangeBy as string,
|
||||
[isClosing ? ("closedBy" as const) : ("reopenedBy" as const)]: approval.statusChangeByUserId as string,
|
||||
secretApprovalRequestId: approval.id,
|
||||
secretApprovalRequestSlug: approval.slug
|
||||
// eslint-disable-next-line
|
||||
|
@@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TAccessApprovalPolicyApproverDALFactory = ReturnType<typeof accessApprovalPolicyApproverDALFactory>;
|
||||
|
||||
export const accessApprovalPolicyApproverDALFactory = (db: TDbClient) => {
|
||||
const accessApprovalPolicyApproverOrm = ormify(db, TableName.AccessApprovalPolicyApprover);
|
||||
return { ...accessApprovalPolicyApproverOrm };
|
||||
};
|
@@ -0,0 +1,76 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TAccessApprovalPolicies } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { buildFindFilter, mergeOneToManyRelation, ormify, selectAllTableCols, TFindFilter } from "@app/lib/knex";
|
||||
|
||||
export type TAccessApprovalPolicyDALFactory = ReturnType<typeof accessApprovalPolicyDALFactory>;
|
||||
|
||||
export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
const accessApprovalPolicyOrm = ormify(db, TableName.AccessApprovalPolicy);
|
||||
|
||||
const accessApprovalPolicyFindQuery = async (tx: Knex, filter: TFindFilter<TAccessApprovalPolicies>) => {
|
||||
const result = await tx(TableName.AccessApprovalPolicy)
|
||||
// eslint-disable-next-line
|
||||
.where(buildFindFilter(filter))
|
||||
.join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||
.join(
|
||||
TableName.AccessApprovalPolicyApprover,
|
||||
`${TableName.AccessApprovalPolicy}.id`,
|
||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
.select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
|
||||
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
|
||||
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
|
||||
.select(tx.ref("projectId").withSchema(TableName.Environment))
|
||||
.select(selectAllTableCols(TableName.AccessApprovalPolicy));
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await accessApprovalPolicyFindQuery(tx || db, {
|
||||
[`${TableName.AccessApprovalPolicy}.id` as "id"]: id
|
||||
});
|
||||
const formatedDoc = mergeOneToManyRelation(
|
||||
doc,
|
||||
"id",
|
||||
({ approverUserId, envId, envName: name, envSlug: slug, ...el }) => ({
|
||||
...el,
|
||||
envId,
|
||||
environment: { id: envId, name, slug }
|
||||
}),
|
||||
({ approverUserId }) => approverUserId,
|
||||
"approvers"
|
||||
);
|
||||
return formatedDoc?.[0];
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindById" });
|
||||
}
|
||||
};
|
||||
|
||||
const find = async (filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>, tx?: Knex) => {
|
||||
try {
|
||||
const docs = await accessApprovalPolicyFindQuery(tx || db, filter);
|
||||
const formatedDoc = mergeOneToManyRelation(
|
||||
docs,
|
||||
"id",
|
||||
({ approverUserId, envId, envName: name, envSlug: slug, ...el }) => ({
|
||||
...el,
|
||||
envId,
|
||||
environment: { id: envId, name, slug }
|
||||
}),
|
||||
({ approverUserId }) => approverUserId,
|
||||
"approvers"
|
||||
);
|
||||
return formatedDoc.map((policy) => ({ ...policy, secretPath: policy.secretPath || undefined }));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...accessApprovalPolicyOrm, find, findById };
|
||||
};
|
@@ -0,0 +1,36 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TVerifyApprovers } from "./access-approval-policy-types";
|
||||
|
||||
export const verifyApprovers = async ({
|
||||
userIds,
|
||||
projectId,
|
||||
orgId,
|
||||
envSlug,
|
||||
actorAuthMethod,
|
||||
secretPath,
|
||||
permissionService
|
||||
}: TVerifyApprovers) => {
|
||||
for await (const userId of userIds) {
|
||||
try {
|
||||
const { permission: approverPermission } = await permissionService.getProjectPermission(
|
||||
ActorType.USER,
|
||||
userId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(approverPermission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: envSlug, secretPath })
|
||||
);
|
||||
} catch (err) {
|
||||
throw new BadRequestError({ message: "One or more approvers doesn't have access to be specified secret path" });
|
||||
}
|
||||
}
|
||||
};
|
@@ -0,0 +1,271 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal";
|
||||
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
|
||||
import { verifyApprovers } from "./access-approval-policy-fns";
|
||||
import {
|
||||
TCreateAccessApprovalPolicy,
|
||||
TDeleteAccessApprovalPolicy,
|
||||
TGetAccessPolicyCountByEnvironmentDTO,
|
||||
TListAccessApprovalPoliciesDTO,
|
||||
TUpdateAccessApprovalPolicy
|
||||
} from "./access-approval-policy-types";
|
||||
|
||||
type TSecretApprovalPolicyServiceFactoryDep = {
|
||||
projectDAL: TProjectDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
accessApprovalPolicyDAL: TAccessApprovalPolicyDALFactory;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findOne">;
|
||||
accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory;
|
||||
userDAL: Pick<TUserDALFactory, "findUsersByProjectId">;
|
||||
};
|
||||
|
||||
export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>;
|
||||
|
||||
export const accessApprovalPolicyServiceFactory = ({
|
||||
accessApprovalPolicyDAL,
|
||||
accessApprovalPolicyApproverDAL,
|
||||
permissionService,
|
||||
projectEnvDAL,
|
||||
userDAL,
|
||||
projectDAL
|
||||
}: TSecretApprovalPolicyServiceFactoryDep) => {
|
||||
const createAccessApprovalPolicy = async ({
|
||||
name,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
secretPath,
|
||||
actorAuthMethod,
|
||||
approvals,
|
||||
approvers,
|
||||
projectSlug,
|
||||
environment
|
||||
}: TCreateAccessApprovalPolicy) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
if (approvals > approvers.length)
|
||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
|
||||
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
||||
|
||||
// We need to get the users by project ID to ensure they are part of the project.
|
||||
const accessApproverUsers = await userDAL.findUsersByProjectId(
|
||||
project.id,
|
||||
approvers.map((approverUserId) => approverUserId)
|
||||
);
|
||||
|
||||
if (accessApproverUsers.length !== approvers.length) {
|
||||
throw new BadRequestError({ message: "Approver not found in project" });
|
||||
}
|
||||
|
||||
await verifyApprovers({
|
||||
projectId: project.id,
|
||||
orgId: actorOrgId,
|
||||
envSlug: environment,
|
||||
secretPath,
|
||||
actorAuthMethod,
|
||||
permissionService,
|
||||
userIds: accessApproverUsers.map((user) => user.id)
|
||||
});
|
||||
|
||||
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
|
||||
const doc = await accessApprovalPolicyDAL.create(
|
||||
{
|
||||
envId: env.id,
|
||||
approvals,
|
||||
secretPath,
|
||||
name
|
||||
},
|
||||
tx
|
||||
);
|
||||
await accessApprovalPolicyApproverDAL.insertMany(
|
||||
accessApproverUsers.map((user) => ({
|
||||
approverUserId: user.id,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
return doc;
|
||||
});
|
||||
return { ...accessApproval, environment: env, projectId: project.id };
|
||||
};
|
||||
|
||||
const getAccessApprovalPolicyByProjectSlug = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectSlug
|
||||
}: TListAccessApprovalPoliciesDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
// Anyone in the project should be able to get the policies.
|
||||
/* const { permission } = */ await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
// ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
|
||||
const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id });
|
||||
return accessApprovalPolicies;
|
||||
};
|
||||
|
||||
const updateAccessApprovalPolicy = async ({
|
||||
policyId,
|
||||
approvers,
|
||||
secretPath,
|
||||
name,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
approvals
|
||||
}: TUpdateAccessApprovalPolicy) => {
|
||||
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
|
||||
if (!accessApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
accessApprovalPolicy.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
|
||||
|
||||
const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => {
|
||||
const doc = await accessApprovalPolicyDAL.updateById(
|
||||
accessApprovalPolicy.id,
|
||||
{
|
||||
approvals,
|
||||
secretPath,
|
||||
name
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (approvers) {
|
||||
// Find the workspace project memberships of the users passed in the approvers array
|
||||
const secretApproverUsers = await userDAL.findUsersByProjectId(
|
||||
accessApprovalPolicy.projectId,
|
||||
approvers.map((approverUserId) => approverUserId)
|
||||
);
|
||||
|
||||
await verifyApprovers({
|
||||
projectId: accessApprovalPolicy.projectId,
|
||||
orgId: actorOrgId,
|
||||
envSlug: accessApprovalPolicy.environment.slug,
|
||||
secretPath: doc.secretPath!,
|
||||
actorAuthMethod,
|
||||
permissionService,
|
||||
userIds: secretApproverUsers.map((user) => user.id)
|
||||
});
|
||||
|
||||
if (secretApproverUsers.length !== approvers.length)
|
||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
||||
await accessApprovalPolicyApproverDAL.insertMany(
|
||||
secretApproverUsers.map((user) => ({
|
||||
approverUserId: user.id,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
return doc;
|
||||
});
|
||||
return {
|
||||
...updatedPolicy,
|
||||
environment: accessApprovalPolicy.environment,
|
||||
projectId: accessApprovalPolicy.projectId
|
||||
};
|
||||
};
|
||||
|
||||
const deleteAccessApprovalPolicy = async ({
|
||||
policyId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TDeleteAccessApprovalPolicy) => {
|
||||
const policy = await accessApprovalPolicyDAL.findById(policyId);
|
||||
if (!policy) throw new BadRequestError({ message: "Secret approval policy not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
policy.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
await accessApprovalPolicyDAL.deleteById(policyId);
|
||||
return policy;
|
||||
};
|
||||
|
||||
const getAccessPolicyCountByEnvSlug = async ({
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectSlug,
|
||||
actorId,
|
||||
envSlug
|
||||
}: TGetAccessPolicyCountByEnvironmentDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!membership) throw new BadRequestError({ message: "User not found in project" });
|
||||
|
||||
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
|
||||
if (!environment) throw new BadRequestError({ message: "Environment not found" });
|
||||
|
||||
const policies = await accessApprovalPolicyDAL.find({ envId: environment.id, projectId: project.id });
|
||||
if (!policies) throw new BadRequestError({ message: "No policies found" });
|
||||
|
||||
return { count: policies.length };
|
||||
};
|
||||
|
||||
return {
|
||||
getAccessPolicyCountByEnvSlug,
|
||||
createAccessApprovalPolicy,
|
||||
deleteAccessApprovalPolicy,
|
||||
updateAccessApprovalPolicy,
|
||||
getAccessApprovalPolicyByProjectSlug
|
||||
};
|
||||
};
|
@@ -0,0 +1,44 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { ActorAuthMethod } from "@app/services/auth/auth-type";
|
||||
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
|
||||
export type TVerifyApprovers = {
|
||||
userIds: string[];
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
envSlug: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
secretPath: string;
|
||||
projectId: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TCreateAccessApprovalPolicy = {
|
||||
approvals: number;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
approvers: string[];
|
||||
projectSlug: string;
|
||||
name: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateAccessApprovalPolicy = {
|
||||
policyId: string;
|
||||
approvals?: number;
|
||||
approvers?: string[];
|
||||
secretPath?: string;
|
||||
name?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteAccessApprovalPolicy = {
|
||||
policyId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetAccessPolicyCountByEnvironmentDTO = {
|
||||
envSlug: string;
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListAccessApprovalPoliciesDTO = {
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
@@ -0,0 +1,378 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { AccessApprovalRequestsSchema, TableName, TAccessApprovalRequests } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
|
||||
|
||||
import { ApprovalStatus } from "./access-approval-request-types";
|
||||
|
||||
export type TAccessApprovalRequestDALFactory = ReturnType<typeof accessApprovalRequestDALFactory>;
|
||||
|
||||
export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
const accessApprovalRequestOrm = ormify(db, TableName.AccessApprovalRequest);
|
||||
const projectUserAdditionalPrivilegeOrm = ormify(db, TableName.ProjectUserAdditionalPrivilege);
|
||||
const groupProjectUserAdditionalPrivilegeOrm = ormify(db, TableName.GroupProjectUserAdditionalPrivilege);
|
||||
|
||||
const deleteMany = async (filter: TFindFilter<TAccessApprovalRequests>, tx?: Knex) => {
|
||||
const transaction = tx || (await db.transaction());
|
||||
|
||||
try {
|
||||
const accessApprovalRequests = await accessApprovalRequestOrm.find(filter, { tx: transaction });
|
||||
|
||||
await projectUserAdditionalPrivilegeOrm.delete(
|
||||
{
|
||||
$in: {
|
||||
id: accessApprovalRequests
|
||||
.filter((req) => Boolean(req.projectUserPrivilegeId))
|
||||
.map((req) => req.projectUserPrivilegeId!)
|
||||
}
|
||||
},
|
||||
transaction
|
||||
);
|
||||
|
||||
await groupProjectUserAdditionalPrivilegeOrm.delete(
|
||||
{
|
||||
$in: {
|
||||
id: accessApprovalRequests
|
||||
.filter((req) => Boolean(req.groupProjectUserPrivilegeId))
|
||||
.map((req) => req.groupProjectUserPrivilegeId!)
|
||||
}
|
||||
},
|
||||
transaction
|
||||
);
|
||||
|
||||
return await accessApprovalRequestOrm.delete(filter, transaction);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "DeleteManyAccessApprovalRequest" });
|
||||
}
|
||||
};
|
||||
|
||||
const findRequestsWithPrivilegeByPolicyIds = async (policyIds: string[]) => {
|
||||
try {
|
||||
const docs = await db(TableName.AccessApprovalRequest)
|
||||
.whereIn(`${TableName.AccessApprovalRequest}.policyId`, policyIds)
|
||||
|
||||
.leftJoin(
|
||||
TableName.ProjectUserAdditionalPrivilege,
|
||||
`${TableName.AccessApprovalRequest}.projectUserPrivilegeId`,
|
||||
`${TableName.ProjectUserAdditionalPrivilege}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.GroupProjectUserAdditionalPrivilege,
|
||||
`${TableName.AccessApprovalRequest}.groupProjectUserPrivilegeId`,
|
||||
`${TableName.GroupProjectUserAdditionalPrivilege}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalPolicy,
|
||||
`${TableName.AccessApprovalRequest}.policyId`,
|
||||
`${TableName.AccessApprovalPolicy}.id`
|
||||
)
|
||||
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalRequestReviewer,
|
||||
`${TableName.AccessApprovalRequest}.id`,
|
||||
`${TableName.AccessApprovalRequestReviewer}.requestId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalPolicyApprover,
|
||||
`${TableName.AccessApprovalPolicy}.id`,
|
||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
|
||||
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||
|
||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.AccessApprovalPolicy).as("policyId"),
|
||||
db.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"),
|
||||
db.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
|
||||
db.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
|
||||
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId")
|
||||
)
|
||||
|
||||
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||
|
||||
.select(
|
||||
db.ref("projectId").withSchema(TableName.Environment),
|
||||
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||
db.ref("name").withSchema(TableName.Environment).as("envName")
|
||||
)
|
||||
|
||||
.select(
|
||||
db.ref("memberUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"),
|
||||
db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus")
|
||||
)
|
||||
|
||||
// Project user additional privilege
|
||||
.select(
|
||||
db
|
||||
.ref("projectMembershipId")
|
||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||
.as("projectPrivilegeProjectMembershipId"),
|
||||
|
||||
db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("projectPrivilegeIsTemporary"),
|
||||
|
||||
db
|
||||
.ref("temporaryMode")
|
||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||
.as("projectPrivilegeTemporaryMode"),
|
||||
|
||||
db
|
||||
.ref("temporaryRange")
|
||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||
.as("projectPrivilegeTemporaryRange"),
|
||||
|
||||
db
|
||||
.ref("temporaryAccessStartTime")
|
||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||
.as("projectPrivilegeTemporaryAccessStartTime"),
|
||||
|
||||
db
|
||||
.ref("temporaryAccessEndTime")
|
||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||
.as("projectPrivilegeTemporaryAccessEndTime"),
|
||||
|
||||
db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("projectPrivilegePermissions")
|
||||
)
|
||||
// Group project user additional privilege
|
||||
.select(
|
||||
db
|
||||
.ref("groupProjectMembershipId")
|
||||
.withSchema(TableName.GroupProjectUserAdditionalPrivilege)
|
||||
.as("groupPrivilegeGroupProjectMembershipId"),
|
||||
|
||||
db
|
||||
.ref("requestedByUserId")
|
||||
.withSchema(TableName.GroupProjectUserAdditionalPrivilege)
|
||||
.as("groupPrivilegeRequestedByUserId"),
|
||||
|
||||
db
|
||||
.ref("isTemporary")
|
||||
.withSchema(TableName.GroupProjectUserAdditionalPrivilege)
|
||||
.as("groupPrivilegeIsTemporary"),
|
||||
|
||||
db
|
||||
.ref("temporaryMode")
|
||||
.withSchema(TableName.GroupProjectUserAdditionalPrivilege)
|
||||
.as("groupPrivilegeTemporaryMode"),
|
||||
|
||||
db
|
||||
.ref("temporaryRange")
|
||||
.withSchema(TableName.GroupProjectUserAdditionalPrivilege)
|
||||
.as("groupPrivilegeTemporaryRange"),
|
||||
|
||||
db
|
||||
.ref("temporaryAccessStartTime")
|
||||
.withSchema(TableName.GroupProjectUserAdditionalPrivilege)
|
||||
.as("groupPrivilegeTemporaryAccessStartTime"),
|
||||
|
||||
db
|
||||
.ref("temporaryAccessEndTime")
|
||||
.withSchema(TableName.GroupProjectUserAdditionalPrivilege)
|
||||
.as("groupPrivilegeTemporaryAccessEndTime"),
|
||||
db
|
||||
.ref("permissions")
|
||||
.withSchema(TableName.GroupProjectUserAdditionalPrivilege)
|
||||
.as("groupPrivilegePermissions")
|
||||
)
|
||||
.orderBy(`${TableName.AccessApprovalRequest}.createdAt`, "desc");
|
||||
|
||||
const projectUserFormattedDocs = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: (doc) => ({
|
||||
...AccessApprovalRequestsSchema.parse(doc),
|
||||
projectId: doc.projectId,
|
||||
environment: doc.envSlug,
|
||||
environmentName: doc.envName,
|
||||
policy: {
|
||||
id: doc.policyId,
|
||||
name: doc.policyName,
|
||||
approvals: doc.policyApprovals,
|
||||
secretPath: doc.policySecretPath,
|
||||
envId: doc.policyEnvId
|
||||
},
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
privilege: doc.projectUserPrivilegeId
|
||||
? {
|
||||
projectMembershipId: doc.projectMembershipId,
|
||||
groupMembershipId: null,
|
||||
requestedByUserId: null,
|
||||
isTemporary: doc.projectPrivilegeIsTemporary,
|
||||
temporaryMode: doc.projectPrivilegeTemporaryMode,
|
||||
temporaryRange: doc.projectPrivilegeTemporaryRange,
|
||||
temporaryAccessStartTime: doc.projectPrivilegeTemporaryAccessStartTime,
|
||||
temporaryAccessEndTime: doc.projectPrivilegeTemporaryAccessEndTime,
|
||||
permissions: doc.projectPrivilegePermissions
|
||||
}
|
||||
: doc.groupProjectUserPrivilegeId
|
||||
? {
|
||||
groupMembershipId: doc.groupPrivilegeGroupProjectMembershipId,
|
||||
requestedByUserId: doc.groupPrivilegeRequestedByUserId,
|
||||
projectMembershipId: null,
|
||||
isTemporary: doc.groupPrivilegeIsTemporary,
|
||||
temporaryMode: doc.groupPrivilegeTemporaryMode,
|
||||
temporaryRange: doc.groupPrivilegeTemporaryRange,
|
||||
temporaryAccessStartTime: doc.groupPrivilegeTemporaryAccessStartTime,
|
||||
temporaryAccessEndTime: doc.groupPrivilegeTemporaryAccessEndTime,
|
||||
permissions: doc.groupPrivilegePermissions
|
||||
}
|
||||
: null,
|
||||
|
||||
isApproved: Boolean(doc.projectUserPrivilegeId || doc.groupProjectUserPrivilegeId)
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "reviewerUserId",
|
||||
label: "reviewers" as const,
|
||||
mapper: ({ reviewerUserId, reviewerStatus: status }) =>
|
||||
reviewerUserId ? { member: reviewerUserId, status } : undefined
|
||||
},
|
||||
{ key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId }
|
||||
]
|
||||
});
|
||||
|
||||
if (!projectUserFormattedDocs) return [];
|
||||
|
||||
return projectUserFormattedDocs.map((doc) => ({
|
||||
...doc,
|
||||
policy: { ...doc.policy, approvers: doc.approvers }
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindRequestsWithPrivilege" });
|
||||
}
|
||||
};
|
||||
|
||||
const findQuery = (filter: TFindFilter<TAccessApprovalRequests>, tx: Knex) =>
|
||||
tx(TableName.AccessApprovalRequest)
|
||||
.where(filter)
|
||||
.join(
|
||||
TableName.AccessApprovalPolicy,
|
||||
`${TableName.AccessApprovalRequest}.policyId`,
|
||||
`${TableName.AccessApprovalPolicy}.id`
|
||||
)
|
||||
|
||||
.join(
|
||||
TableName.AccessApprovalPolicyApprover,
|
||||
`${TableName.AccessApprovalPolicy}.id`,
|
||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalRequestReviewer,
|
||||
`${TableName.AccessApprovalRequest}.id`,
|
||||
`${TableName.AccessApprovalRequestReviewer}.requestId`
|
||||
)
|
||||
|
||||
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||
.select(
|
||||
tx.ref("memberUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"),
|
||||
tx.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"),
|
||||
tx.ref("id").withSchema(TableName.AccessApprovalPolicy).as("policyId"),
|
||||
tx.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"),
|
||||
tx.ref("projectId").withSchema(TableName.Environment),
|
||||
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
|
||||
tx.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
|
||||
tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
|
||||
tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover)
|
||||
);
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const sql = findQuery({ [`${TableName.AccessApprovalRequest}.id` as "id"]: id }, tx || db);
|
||||
const docs = await sql;
|
||||
const formatedDoc = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: (el) => ({
|
||||
...AccessApprovalRequestsSchema.parse(el),
|
||||
projectId: el.projectId,
|
||||
environment: el.environment,
|
||||
policy: {
|
||||
id: el.policyId,
|
||||
name: el.policyName,
|
||||
approvals: el.policyApprovals,
|
||||
secretPath: el.policySecretPath
|
||||
}
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "reviewerUserId",
|
||||
label: "reviewers" as const,
|
||||
mapper: ({ reviewerUserId, reviewerStatus: status }) =>
|
||||
reviewerUserId ? { member: reviewerUserId, status } : undefined
|
||||
},
|
||||
{ key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId }
|
||||
]
|
||||
});
|
||||
if (!formatedDoc?.[0]) return;
|
||||
return {
|
||||
...formatedDoc[0],
|
||||
policy: { ...formatedDoc[0].policy, approvers: formatedDoc[0].approvers }
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByIdAccessApprovalRequest" });
|
||||
}
|
||||
};
|
||||
|
||||
const getCount = async ({ projectId }: { projectId: string }) => {
|
||||
try {
|
||||
const accessRequests = await db(TableName.AccessApprovalRequest)
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalPolicy,
|
||||
`${TableName.AccessApprovalRequest}.policyId`,
|
||||
`${TableName.AccessApprovalPolicy}.id`
|
||||
)
|
||||
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalRequestReviewer,
|
||||
`${TableName.AccessApprovalRequest}.id`,
|
||||
`${TableName.AccessApprovalRequestReviewer}.requestId`
|
||||
)
|
||||
|
||||
.where(`${TableName.Environment}.projectId`, projectId)
|
||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||
.select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"))
|
||||
.select(db.ref("memberUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("memberUserId"));
|
||||
|
||||
const formattedRequests = sqlNestRelationships({
|
||||
data: accessRequests,
|
||||
key: "id",
|
||||
parentMapper: (doc) => ({
|
||||
...AccessApprovalRequestsSchema.parse(doc)
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "memberUserId",
|
||||
label: "reviewers" as const,
|
||||
mapper: ({ memberUserId, reviewerStatus: status }) =>
|
||||
memberUserId ? { member: memberUserId, status } : undefined
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// an approval is pending if there is no reviewer rejections and no privilege ID is set
|
||||
const pendingApprovals = formattedRequests.filter(
|
||||
(req) =>
|
||||
!req.projectUserPrivilegeId &&
|
||||
!req.groupProjectUserPrivilegeId &&
|
||||
!req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED)
|
||||
);
|
||||
|
||||
// an approval is finalized if there are any rejections or a privilege ID is set
|
||||
const finalizedApprovals = formattedRequests.filter(
|
||||
(req) =>
|
||||
req.projectUserPrivilegeId ||
|
||||
req.groupProjectUserPrivilegeId ||
|
||||
req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED)
|
||||
);
|
||||
|
||||
return { pendingCount: pendingApprovals.length, finalizedCount: finalizedApprovals.length };
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "GetCountAccessApprovalRequest" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...accessApprovalRequestOrm, findById, findRequestsWithPrivilegeByPolicyIds, getCount, delete: deleteMany };
|
||||
};
|
@@ -0,0 +1,53 @@
|
||||
import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
|
||||
import { TVerifyPermission } from "./access-approval-request-types";
|
||||
|
||||
function filterUnique(value: string, index: number, array: string[]) {
|
||||
return array.indexOf(value) === index;
|
||||
}
|
||||
|
||||
export const verifyRequestedPermissions = ({ permissions }: TVerifyPermission) => {
|
||||
const permission = unpackRules(
|
||||
permissions as PackRule<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
conditions?: Record<string, any>;
|
||||
action: string;
|
||||
subject: [string];
|
||||
}>[]
|
||||
);
|
||||
|
||||
if (!permission || !permission.length) {
|
||||
throw new UnauthorizedError({ message: "No permission provided" });
|
||||
}
|
||||
|
||||
const requestedPermissions: string[] = [];
|
||||
|
||||
for (const p of permission) {
|
||||
if (p.action[0] === "read") requestedPermissions.push("Read Access");
|
||||
if (p.action[0] === "create") requestedPermissions.push("Create Access");
|
||||
if (p.action[0] === "delete") requestedPermissions.push("Delete Access");
|
||||
if (p.action[0] === "edit") requestedPermissions.push("Edit Access");
|
||||
}
|
||||
|
||||
const firstPermission = permission[0];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
const permissionSecretPath = firstPermission.conditions?.secretPath?.$glob;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-unsafe-assignment
|
||||
const permissionEnv = firstPermission.conditions?.environment;
|
||||
|
||||
if (!permissionEnv || typeof permissionEnv !== "string") {
|
||||
throw new UnauthorizedError({ message: "Permission environment is not a string" });
|
||||
}
|
||||
if (!permissionSecretPath || typeof permissionSecretPath !== "string") {
|
||||
throw new UnauthorizedError({ message: "Permission path is not a string" });
|
||||
}
|
||||
|
||||
return {
|
||||
envSlug: permissionEnv,
|
||||
secretPath: permissionSecretPath,
|
||||
accessTypes: requestedPermissions.filter(filterUnique)
|
||||
};
|
||||
};
|
@@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TAccessApprovalRequestReviewerDALFactory = ReturnType<typeof accessApprovalRequestReviewerDALFactory>;
|
||||
|
||||
export const accessApprovalRequestReviewerDALFactory = (db: TDbClient) => {
|
||||
const secretApprovalRequestReviewerOrm = ormify(db, TableName.AccessApprovalRequestReviewer);
|
||||
return secretApprovalRequestReviewerOrm;
|
||||
};
|
@@ -0,0 +1,502 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import ms from "ms";
|
||||
|
||||
import { ProjectMembershipRole, TProjectUserAdditionalPrivilege } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal";
|
||||
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
|
||||
import { verifyApprovers } from "../access-approval-policy/access-approval-policy-fns";
|
||||
import { TGroupProjectUserAdditionalPrivilegeDALFactory } from "../group-project-user-additional-privilege/group-project-user-additional-privilege-dal";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types";
|
||||
import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal";
|
||||
import { verifyRequestedPermissions } from "./access-approval-request-fns";
|
||||
import { TAccessApprovalRequestReviewerDALFactory } from "./access-approval-request-reviewer-dal";
|
||||
import {
|
||||
ApprovalStatus,
|
||||
TCreateAccessApprovalRequestDTO,
|
||||
TDeleteApprovalRequestDTO,
|
||||
TGetAccessRequestCountDTO,
|
||||
TListApprovalRequestsDTO,
|
||||
TReviewAccessRequestDTO
|
||||
} from "./access-approval-request-types";
|
||||
|
||||
type TAccessApprovalRequestServiceFactoryDep = {
|
||||
additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "create" | "findById" | "deleteById">;
|
||||
groupAdditionalPrivilegeDAL: TGroupProjectUserAdditionalPrivilegeDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
accessApprovalPolicyApproverDAL: Pick<TAccessApprovalPolicyApproverDALFactory, "find">;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug">;
|
||||
accessApprovalRequestDAL: Pick<
|
||||
TAccessApprovalRequestDALFactory,
|
||||
| "create"
|
||||
| "find"
|
||||
| "findRequestsWithPrivilegeByPolicyIds"
|
||||
| "findById"
|
||||
| "transaction"
|
||||
| "updateById"
|
||||
| "findOne"
|
||||
| "getCount"
|
||||
| "deleteById"
|
||||
>;
|
||||
accessApprovalPolicyDAL: Pick<TAccessApprovalPolicyDALFactory, "findOne" | "find">;
|
||||
accessApprovalRequestReviewerDAL: Pick<
|
||||
TAccessApprovalRequestReviewerDALFactory,
|
||||
"create" | "find" | "findOne" | "transaction"
|
||||
>;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
userDAL: Pick<
|
||||
TUserDALFactory,
|
||||
"findUserByProjectMembershipId" | "findUsersByProjectMembershipIds" | "findUsersByProjectId" | "findUserByProjectId"
|
||||
>;
|
||||
};
|
||||
|
||||
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
|
||||
|
||||
export const accessApprovalRequestServiceFactory = ({
|
||||
projectDAL,
|
||||
projectEnvDAL,
|
||||
permissionService,
|
||||
accessApprovalRequestDAL,
|
||||
groupAdditionalPrivilegeDAL,
|
||||
accessApprovalRequestReviewerDAL,
|
||||
projectMembershipDAL,
|
||||
accessApprovalPolicyDAL,
|
||||
accessApprovalPolicyApproverDAL,
|
||||
additionalPrivilegeDAL,
|
||||
smtpService,
|
||||
userDAL
|
||||
}: TAccessApprovalRequestServiceFactoryDep) => {
|
||||
const createAccessApprovalRequest = async ({
|
||||
isTemporary,
|
||||
temporaryRange,
|
||||
actorId,
|
||||
permissions: requestedPermissions,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectSlug
|
||||
}: TCreateAccessApprovalRequestDTO) => {
|
||||
const cfg = getConfig();
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new UnauthorizedError({ message: "Project not found" });
|
||||
|
||||
// Anyone can create an access approval request.
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" });
|
||||
|
||||
await projectDAL.checkProjectUpgradeStatus(project.id);
|
||||
|
||||
const { envSlug, secretPath, accessTypes } = verifyRequestedPermissions({ permissions: requestedPermissions });
|
||||
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
|
||||
|
||||
if (!environment) throw new UnauthorizedError({ message: "Environment not found" });
|
||||
|
||||
const policy = await accessApprovalPolicyDAL.findOne({
|
||||
envId: environment.id,
|
||||
secretPath
|
||||
});
|
||||
if (!policy) throw new UnauthorizedError({ message: "No policy matching criteria was found." });
|
||||
|
||||
const approvers = await accessApprovalPolicyApproverDAL.find({
|
||||
policyId: policy.id
|
||||
});
|
||||
|
||||
if (approvers.some((approver) => !approver.approverUserId)) {
|
||||
throw new BadRequestError({ message: "Policy approvers must be assigned to users" });
|
||||
}
|
||||
|
||||
const approverUsers = await userDAL.findUsersByProjectId(
|
||||
project.id,
|
||||
approvers.map((approver) => approver.approverUserId!)
|
||||
);
|
||||
|
||||
const requestedByUser = await userDAL.findUserByProjectId(project.id, actorId);
|
||||
|
||||
if (!requestedByUser) throw new BadRequestError({ message: "User not found in project" });
|
||||
|
||||
const duplicateRequests = await accessApprovalRequestDAL.find({
|
||||
policyId: policy.id,
|
||||
requestedByUserId: actorId,
|
||||
permissions: JSON.stringify(requestedPermissions),
|
||||
isTemporary
|
||||
});
|
||||
|
||||
if (duplicateRequests?.length > 0) {
|
||||
for await (const duplicateRequest of duplicateRequests) {
|
||||
let foundPrivilege: Pick<
|
||||
TProjectUserAdditionalPrivilege,
|
||||
"temporaryAccessEndTime" | "isTemporary" | "id"
|
||||
> | null = null;
|
||||
|
||||
if (duplicateRequest.projectUserPrivilegeId) {
|
||||
foundPrivilege = await additionalPrivilegeDAL.findById(duplicateRequest.projectUserPrivilegeId);
|
||||
} else if (duplicateRequest.groupProjectUserPrivilegeId) {
|
||||
foundPrivilege = await groupAdditionalPrivilegeDAL.findById(duplicateRequest.groupProjectUserPrivilegeId);
|
||||
}
|
||||
|
||||
if (foundPrivilege) {
|
||||
const isExpired = new Date() > new Date(foundPrivilege.temporaryAccessEndTime || ("" as string));
|
||||
|
||||
if (!isExpired || !foundPrivilege.isTemporary) {
|
||||
throw new BadRequestError({ message: "You already have an active privilege with the same criteria" });
|
||||
}
|
||||
} else {
|
||||
const reviewers = await accessApprovalRequestReviewerDAL.find({
|
||||
requestId: duplicateRequest.id
|
||||
});
|
||||
|
||||
const isRejected = reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED);
|
||||
|
||||
if (!isRejected) {
|
||||
throw new BadRequestError({ message: "You already have a pending access request with the same criteria" });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const approval = await accessApprovalRequestDAL.transaction(async (tx) => {
|
||||
const requesterUser = await userDAL.findUserByProjectId(project.id, actorId);
|
||||
|
||||
if (!requesterUser?.projectMembershipId && !requesterUser?.groupProjectMembershipId) {
|
||||
throw new BadRequestError({ message: "You don't have a membership for this project" });
|
||||
}
|
||||
|
||||
const approvalRequest = await accessApprovalRequestDAL.create(
|
||||
{
|
||||
projectMembershipId: requesterUser.projectMembershipId || null,
|
||||
groupMembershipId: requesterUser.groupProjectMembershipId || null,
|
||||
policyId: policy.id,
|
||||
requestedByUserId: actorId, // This is the user ID of the person who made the request
|
||||
temporaryRange: temporaryRange || null,
|
||||
permissions: JSON.stringify(requestedPermissions),
|
||||
isTemporary
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!),
|
||||
subjectLine: "Access Approval Request",
|
||||
|
||||
substitutions: {
|
||||
projectName: project.name,
|
||||
requesterFullName: `${requestedByUser.firstName} ${requestedByUser.lastName}`,
|
||||
requesterEmail: requestedByUser.email,
|
||||
isTemporary,
|
||||
...(isTemporary && {
|
||||
expiresIn: ms(ms(temporaryRange || ""), { long: true })
|
||||
}),
|
||||
secretPath,
|
||||
environment: envSlug,
|
||||
permissions: accessTypes,
|
||||
approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval`
|
||||
},
|
||||
template: SmtpTemplates.AccessApprovalRequest
|
||||
});
|
||||
|
||||
return approvalRequest;
|
||||
});
|
||||
|
||||
return { request: approval };
|
||||
};
|
||||
|
||||
const deleteAccessApprovalRequest = async ({
|
||||
projectSlug,
|
||||
actor,
|
||||
requestId,
|
||||
actorOrgId,
|
||||
actorId,
|
||||
actorAuthMethod
|
||||
}: TDeleteApprovalRequestDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new UnauthorizedError({ message: "Project not found" });
|
||||
|
||||
const { membership, permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" });
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId);
|
||||
|
||||
if (!accessApprovalRequest?.projectUserPrivilegeId && !accessApprovalRequest?.groupProjectUserPrivilegeId) {
|
||||
throw new BadRequestError({ message: "Access request must be approved to be deleted" });
|
||||
}
|
||||
|
||||
if (accessApprovalRequest?.projectId !== project.id) {
|
||||
throw new UnauthorizedError({ message: "Request not found in project" });
|
||||
}
|
||||
|
||||
const approvers = await accessApprovalPolicyApproverDAL.find({
|
||||
policyId: accessApprovalRequest.policyId
|
||||
});
|
||||
|
||||
// make sure the actor (actorId) is an approver
|
||||
if (!approvers.some((approver) => approver.approverUserId === actorId)) {
|
||||
throw new UnauthorizedError({ message: "Only policy approvers can delete access requests" });
|
||||
}
|
||||
|
||||
if (accessApprovalRequest.projectUserPrivilegeId) {
|
||||
await additionalPrivilegeDAL.deleteById(accessApprovalRequest.projectUserPrivilegeId);
|
||||
} else if (accessApprovalRequest.groupProjectUserPrivilegeId) {
|
||||
await groupAdditionalPrivilegeDAL.deleteById(accessApprovalRequest.groupProjectUserPrivilegeId);
|
||||
}
|
||||
|
||||
return { request: accessApprovalRequest };
|
||||
};
|
||||
|
||||
const listApprovalRequests = async ({
|
||||
projectSlug,
|
||||
authorUserId,
|
||||
envSlug,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorId,
|
||||
actorAuthMethod
|
||||
}: TListApprovalRequestsDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new UnauthorizedError({ message: "Project not found" });
|
||||
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" });
|
||||
|
||||
const policies = await accessApprovalPolicyDAL.find({ projectId: project.id });
|
||||
let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id));
|
||||
|
||||
if (authorUserId) requests = requests.filter((request) => request.requestedByUserId === authorUserId);
|
||||
if (envSlug) requests = requests.filter((request) => request.environment === envSlug);
|
||||
|
||||
return { requests };
|
||||
};
|
||||
|
||||
const reviewAccessRequest = async ({
|
||||
requestId,
|
||||
actor,
|
||||
status,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TReviewAccessRequestDTO) => {
|
||||
const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId);
|
||||
if (!accessApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||
|
||||
const { policy } = accessApprovalRequest;
|
||||
const { membership, hasRole } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
accessApprovalRequest.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" });
|
||||
|
||||
if (
|
||||
!hasRole(ProjectMembershipRole.Admin) &&
|
||||
accessApprovalRequest.requestedByUserId !== actorId && // The request wasn't made by the current user
|
||||
!policy.approvers.find((approverUserId) => approverUserId === membership.id) // The request isn't performed by an assigned approver
|
||||
) {
|
||||
throw new UnauthorizedError({ message: "You are not authorized to approve this request" });
|
||||
}
|
||||
|
||||
const reviewerProjectMembership = await projectMembershipDAL.findById(membership.id);
|
||||
|
||||
await verifyApprovers({
|
||||
projectId: accessApprovalRequest.projectId,
|
||||
orgId: actorOrgId,
|
||||
envSlug: accessApprovalRequest.environment,
|
||||
secretPath: accessApprovalRequest.policy.secretPath!,
|
||||
actorAuthMethod,
|
||||
permissionService,
|
||||
userIds: [reviewerProjectMembership.userId]
|
||||
});
|
||||
|
||||
const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id });
|
||||
if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) {
|
||||
throw new BadRequestError({ message: "The request has already been rejected by another reviewer" });
|
||||
}
|
||||
|
||||
const reviewStatus = await accessApprovalRequestReviewerDAL.transaction(async (tx) => {
|
||||
const review = await accessApprovalRequestReviewerDAL.findOne(
|
||||
{
|
||||
requestId: accessApprovalRequest.id,
|
||||
memberUserId: actorId
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (!review) {
|
||||
const newReview = await accessApprovalRequestReviewerDAL.create(
|
||||
{
|
||||
status,
|
||||
requestId: accessApprovalRequest.id,
|
||||
memberUserId: actorId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const allReviews = [...existingReviews, newReview];
|
||||
|
||||
const approvedReviews = allReviews.filter((r) => r.status === ApprovalStatus.APPROVED);
|
||||
|
||||
// approvals is the required number of approvals. If the number of approved reviews is equal to the number of required approvals, then the request is approved.
|
||||
if (approvedReviews.length === policy.approvals) {
|
||||
if (accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) {
|
||||
throw new BadRequestError({ message: "Temporary range is required for temporary access" });
|
||||
}
|
||||
|
||||
let projectUserPrivilegeId: string | null = null;
|
||||
let groupProjectMembershipId: string | null = null;
|
||||
|
||||
if (!accessApprovalRequest.groupMembershipId && !accessApprovalRequest.projectMembershipId) {
|
||||
throw new BadRequestError({ message: "Project membership or group membership is required" });
|
||||
}
|
||||
|
||||
// Permanent access
|
||||
if (!accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) {
|
||||
if (accessApprovalRequest.groupMembershipId) {
|
||||
// Group user privilege
|
||||
const groupProjectUserAdditionalPrivilege = await groupAdditionalPrivilegeDAL.create(
|
||||
{
|
||||
groupProjectMembershipId: accessApprovalRequest.groupMembershipId,
|
||||
requestedByUserId: accessApprovalRequest.requestedByUserId,
|
||||
slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`,
|
||||
permissions: JSON.stringify(accessApprovalRequest.permissions)
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
groupProjectMembershipId = groupProjectUserAdditionalPrivilege.id;
|
||||
} else {
|
||||
// Project user privilege
|
||||
const privilege = await additionalPrivilegeDAL.create(
|
||||
{
|
||||
projectMembershipId: accessApprovalRequest.projectMembershipId!,
|
||||
slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`,
|
||||
permissions: JSON.stringify(accessApprovalRequest.permissions)
|
||||
},
|
||||
tx
|
||||
);
|
||||
projectUserPrivilegeId = privilege.id;
|
||||
}
|
||||
} else {
|
||||
// Temporary access
|
||||
const relativeTempAllocatedTimeInMs = ms(accessApprovalRequest.temporaryRange!);
|
||||
const startTime = new Date();
|
||||
|
||||
if (accessApprovalRequest.groupMembershipId) {
|
||||
// Group user privilege
|
||||
const groupProjectUserAdditionalPrivilege = await groupAdditionalPrivilegeDAL.create(
|
||||
{
|
||||
groupProjectMembershipId: accessApprovalRequest.groupMembershipId,
|
||||
requestedByUserId: accessApprovalRequest.requestedByUserId,
|
||||
slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`,
|
||||
permissions: JSON.stringify(accessApprovalRequest.permissions),
|
||||
isTemporary: true,
|
||||
temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative,
|
||||
temporaryRange: accessApprovalRequest.temporaryRange!,
|
||||
temporaryAccessStartTime: startTime,
|
||||
temporaryAccessEndTime: new Date(new Date(startTime).getTime() + relativeTempAllocatedTimeInMs)
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
groupProjectMembershipId = groupProjectUserAdditionalPrivilege.id;
|
||||
} else {
|
||||
const privilege = await additionalPrivilegeDAL.create(
|
||||
{
|
||||
projectMembershipId: accessApprovalRequest.projectMembershipId!,
|
||||
slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`,
|
||||
permissions: JSON.stringify(accessApprovalRequest.permissions),
|
||||
isTemporary: true,
|
||||
temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative,
|
||||
temporaryRange: accessApprovalRequest.temporaryRange!,
|
||||
temporaryAccessStartTime: startTime,
|
||||
temporaryAccessEndTime: new Date(new Date(startTime).getTime() + relativeTempAllocatedTimeInMs)
|
||||
},
|
||||
tx
|
||||
);
|
||||
projectUserPrivilegeId = privilege.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (projectUserPrivilegeId) {
|
||||
await accessApprovalRequestDAL.updateById(accessApprovalRequest.id, { projectUserPrivilegeId }, tx);
|
||||
} else if (groupProjectMembershipId) {
|
||||
await accessApprovalRequestDAL.updateById(
|
||||
accessApprovalRequest.id,
|
||||
{ groupProjectUserPrivilegeId: groupProjectMembershipId },
|
||||
tx
|
||||
);
|
||||
} else {
|
||||
throw new BadRequestError({ message: "No privilege was created" });
|
||||
}
|
||||
}
|
||||
|
||||
return newReview;
|
||||
}
|
||||
throw new BadRequestError({ message: "You have already reviewed this request" });
|
||||
});
|
||||
|
||||
return reviewStatus;
|
||||
};
|
||||
|
||||
const getCount = async ({ projectSlug, actor, actorAuthMethod, actorId, actorOrgId }: TGetAccessRequestCountDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new UnauthorizedError({ message: "Project not found" });
|
||||
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!membership) throw new BadRequestError({ message: "User not found in project" });
|
||||
|
||||
const count = await accessApprovalRequestDAL.getCount({ projectId: project.id });
|
||||
|
||||
return { count };
|
||||
};
|
||||
|
||||
return {
|
||||
createAccessApprovalRequest,
|
||||
listApprovalRequests,
|
||||
reviewAccessRequest,
|
||||
deleteAccessApprovalRequest,
|
||||
getCount
|
||||
};
|
||||
};
|
@@ -0,0 +1,38 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export enum ApprovalStatus {
|
||||
PENDING = "pending",
|
||||
APPROVED = "approved",
|
||||
REJECTED = "rejected"
|
||||
}
|
||||
|
||||
export type TVerifyPermission = {
|
||||
permissions: unknown;
|
||||
};
|
||||
|
||||
export type TGetAccessRequestCountDTO = {
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TReviewAccessRequestDTO = {
|
||||
requestId: string;
|
||||
status: ApprovalStatus;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TCreateAccessApprovalRequestDTO = {
|
||||
projectSlug: string;
|
||||
permissions: unknown;
|
||||
isTemporary: boolean;
|
||||
temporaryRange?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListApprovalRequestsDTO = {
|
||||
projectSlug: string;
|
||||
authorUserId?: string;
|
||||
envSlug?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteApprovalRequestDTO = {
|
||||
requestId: string;
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
@@ -0,0 +1,11 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TAuditLogStreamDALFactory = ReturnType<typeof auditLogStreamDALFactory>;
|
||||
|
||||
export const auditLogStreamDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.AuditLogStream);
|
||||
|
||||
return orm;
|
||||
};
|
@@ -0,0 +1,233 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { RawAxiosRequestHeaders } from "axios";
|
||||
|
||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { validateLocalIps } from "@app/lib/validator";
|
||||
|
||||
import { AUDIT_LOG_STREAM_TIMEOUT } from "../audit-log/audit-log-queue";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TAuditLogStreamDALFactory } from "./audit-log-stream-dal";
|
||||
import {
|
||||
LogStreamHeaders,
|
||||
TCreateAuditLogStreamDTO,
|
||||
TDeleteAuditLogStreamDTO,
|
||||
TGetDetailsAuditLogStreamDTO,
|
||||
TListAuditLogStreamDTO,
|
||||
TUpdateAuditLogStreamDTO
|
||||
} from "./audit-log-stream-types";
|
||||
|
||||
type TAuditLogStreamServiceFactoryDep = {
|
||||
auditLogStreamDAL: TAuditLogStreamDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
export type TAuditLogStreamServiceFactory = ReturnType<typeof auditLogStreamServiceFactory>;
|
||||
|
||||
export const auditLogStreamServiceFactory = ({
|
||||
auditLogStreamDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
}: TAuditLogStreamServiceFactoryDep) => {
|
||||
const create = async ({
|
||||
url,
|
||||
actor,
|
||||
headers = [],
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TCreateAuditLogStreamDTO) => {
|
||||
if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" });
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.auditLogStreams)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to create audit log streams due to plan restriction. Upgrade plan to create group."
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||
|
||||
validateLocalIps(url);
|
||||
|
||||
const totalStreams = await auditLogStreamDAL.find({ orgId: actorOrgId });
|
||||
if (totalStreams.length >= plan.auditLogStreamLimit) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to create audit log streams due to plan limit reached. Kindly contact Infisical to add more streams."
|
||||
});
|
||||
}
|
||||
|
||||
// testing connection first
|
||||
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
|
||||
if (headers.length)
|
||||
headers.forEach(({ key, value }) => {
|
||||
streamHeaders[key] = value;
|
||||
});
|
||||
await request
|
||||
.post(
|
||||
url,
|
||||
{ ping: "ok" },
|
||||
{
|
||||
headers: streamHeaders,
|
||||
// request timeout
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
// connection timeout
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
}
|
||||
)
|
||||
.catch((err) => {
|
||||
throw new Error(`Failed to connect with the source ${(err as Error)?.message}`);
|
||||
});
|
||||
const encryptedHeaders = headers ? infisicalSymmetricEncypt(JSON.stringify(headers)) : undefined;
|
||||
const logStream = await auditLogStreamDAL.create({
|
||||
orgId: actorOrgId,
|
||||
url,
|
||||
...(encryptedHeaders
|
||||
? {
|
||||
encryptedHeadersCiphertext: encryptedHeaders.ciphertext,
|
||||
encryptedHeadersIV: encryptedHeaders.iv,
|
||||
encryptedHeadersTag: encryptedHeaders.tag,
|
||||
encryptedHeadersAlgorithm: encryptedHeaders.algorithm,
|
||||
encryptedHeadersKeyEncoding: encryptedHeaders.encoding
|
||||
}
|
||||
: {})
|
||||
});
|
||||
return logStream;
|
||||
};
|
||||
|
||||
const updateById = async ({
|
||||
id,
|
||||
url,
|
||||
actor,
|
||||
headers = [],
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TUpdateAuditLogStreamDTO) => {
|
||||
if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" });
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.auditLogStreams)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update audit log streams due to plan restriction. Upgrade plan to create group."
|
||||
});
|
||||
|
||||
const logStream = await auditLogStreamDAL.findById(id);
|
||||
if (!logStream) throw new BadRequestError({ message: "Audit log stream not found" });
|
||||
|
||||
const { orgId } = logStream;
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||
|
||||
if (url) validateLocalIps(url);
|
||||
|
||||
// testing connection first
|
||||
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
|
||||
if (headers.length)
|
||||
headers.forEach(({ key, value }) => {
|
||||
streamHeaders[key] = value;
|
||||
});
|
||||
|
||||
await request
|
||||
.post(
|
||||
url || logStream.url,
|
||||
{ ping: "ok" },
|
||||
{
|
||||
headers: streamHeaders,
|
||||
// request timeout
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
// connection timeout
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
}
|
||||
)
|
||||
.catch((err) => {
|
||||
throw new Error(`Failed to connect with the source ${(err as Error)?.message}`);
|
||||
});
|
||||
|
||||
const encryptedHeaders = headers ? infisicalSymmetricEncypt(JSON.stringify(headers)) : undefined;
|
||||
const updatedLogStream = await auditLogStreamDAL.updateById(id, {
|
||||
url,
|
||||
...(encryptedHeaders
|
||||
? {
|
||||
encryptedHeadersCiphertext: encryptedHeaders.ciphertext,
|
||||
encryptedHeadersIV: encryptedHeaders.iv,
|
||||
encryptedHeadersTag: encryptedHeaders.tag,
|
||||
encryptedHeadersAlgorithm: encryptedHeaders.algorithm,
|
||||
encryptedHeadersKeyEncoding: encryptedHeaders.encoding
|
||||
}
|
||||
: {})
|
||||
});
|
||||
return updatedLogStream;
|
||||
};
|
||||
|
||||
const deleteById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TDeleteAuditLogStreamDTO) => {
|
||||
if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" });
|
||||
|
||||
const logStream = await auditLogStreamDAL.findById(id);
|
||||
if (!logStream) throw new BadRequestError({ message: "Audit log stream not found" });
|
||||
|
||||
const { orgId } = logStream;
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings);
|
||||
|
||||
const deletedLogStream = await auditLogStreamDAL.deleteById(id);
|
||||
return deletedLogStream;
|
||||
};
|
||||
|
||||
const getById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TGetDetailsAuditLogStreamDTO) => {
|
||||
const logStream = await auditLogStreamDAL.findById(id);
|
||||
if (!logStream) throw new BadRequestError({ message: "Audit log stream not found" });
|
||||
|
||||
const { orgId } = logStream;
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
|
||||
const headers =
|
||||
logStream?.encryptedHeadersCiphertext && logStream?.encryptedHeadersIV && logStream?.encryptedHeadersTag
|
||||
? (JSON.parse(
|
||||
infisicalSymmetricDecrypt({
|
||||
tag: logStream.encryptedHeadersTag,
|
||||
iv: logStream.encryptedHeadersIV,
|
||||
ciphertext: logStream.encryptedHeadersCiphertext,
|
||||
keyEncoding: logStream.encryptedHeadersKeyEncoding as SecretKeyEncoding
|
||||
})
|
||||
) as LogStreamHeaders[])
|
||||
: undefined;
|
||||
|
||||
return { ...logStream, headers };
|
||||
};
|
||||
|
||||
const list = async ({ actor, actorId, actorOrgId, actorAuthMethod }: TListAuditLogStreamDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
|
||||
const logStreams = await auditLogStreamDAL.find({ orgId: actorOrgId });
|
||||
return logStreams;
|
||||
};
|
||||
|
||||
return {
|
||||
create,
|
||||
updateById,
|
||||
deleteById,
|
||||
getById,
|
||||
list
|
||||
};
|
||||
};
|
@@ -0,0 +1,27 @@
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
export type LogStreamHeaders = {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type TCreateAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
|
||||
url: string;
|
||||
headers?: LogStreamHeaders[];
|
||||
};
|
||||
|
||||
export type TUpdateAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
|
||||
id: string;
|
||||
url?: string;
|
||||
headers?: LogStreamHeaders[];
|
||||
};
|
||||
|
||||
export type TDeleteAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type TListAuditLogStreamDTO = Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TGetDetailsAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
|
||||
id: string;
|
||||
};
|
@@ -1,13 +1,21 @@
|
||||
import { RawAxiosRequestHeaders } from "axios";
|
||||
|
||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { TAuditLogStreamDALFactory } from "../audit-log-stream/audit-log-stream-dal";
|
||||
import { LogStreamHeaders } from "../audit-log-stream/audit-log-stream-types";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { TAuditLogDALFactory } from "./audit-log-dal";
|
||||
import { TCreateAuditLogDTO } from "./audit-log-types";
|
||||
|
||||
type TAuditLogQueueServiceFactoryDep = {
|
||||
auditLogDAL: TAuditLogDALFactory;
|
||||
auditLogStreamDAL: Pick<TAuditLogStreamDALFactory, "find">;
|
||||
queueService: TQueueServiceFactory;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
@@ -15,11 +23,15 @@ type TAuditLogQueueServiceFactoryDep = {
|
||||
|
||||
export type TAuditLogQueueServiceFactory = ReturnType<typeof auditLogQueueServiceFactory>;
|
||||
|
||||
// keep this timeout 5s it must be fast because else the queue will take time to finish
|
||||
// audit log is a crowded queue thus needs to be fast
|
||||
export const AUDIT_LOG_STREAM_TIMEOUT = 5 * 1000;
|
||||
export const auditLogQueueServiceFactory = ({
|
||||
auditLogDAL,
|
||||
queueService,
|
||||
projectDAL,
|
||||
licenseService
|
||||
licenseService,
|
||||
auditLogStreamDAL
|
||||
}: TAuditLogQueueServiceFactoryDep) => {
|
||||
const pushToLog = async (data: TCreateAuditLogDTO) => {
|
||||
await queueService.queue(QueueName.AuditLog, QueueJobs.AuditLog, data, {
|
||||
@@ -47,7 +59,7 @@ export const auditLogQueueServiceFactory = ({
|
||||
// skip inserting if audit log retention is 0 meaning its not supported
|
||||
if (ttl === 0) return;
|
||||
|
||||
await auditLogDAL.create({
|
||||
const auditLog = await auditLogDAL.create({
|
||||
actor: actor.type,
|
||||
actorMetadata: actor.metadata,
|
||||
userAgent,
|
||||
@@ -59,6 +71,46 @@ export const auditLogQueueServiceFactory = ({
|
||||
eventMetadata: event.metadata,
|
||||
userAgentType
|
||||
});
|
||||
|
||||
const logStreams = orgId ? await auditLogStreamDAL.find({ orgId }) : [];
|
||||
await Promise.allSettled(
|
||||
logStreams.map(
|
||||
async ({
|
||||
url,
|
||||
encryptedHeadersTag,
|
||||
encryptedHeadersIV,
|
||||
encryptedHeadersKeyEncoding,
|
||||
encryptedHeadersCiphertext
|
||||
}) => {
|
||||
const streamHeaders =
|
||||
encryptedHeadersIV && encryptedHeadersCiphertext && encryptedHeadersTag
|
||||
? (JSON.parse(
|
||||
infisicalSymmetricDecrypt({
|
||||
keyEncoding: encryptedHeadersKeyEncoding as SecretKeyEncoding,
|
||||
iv: encryptedHeadersIV,
|
||||
tag: encryptedHeadersTag,
|
||||
ciphertext: encryptedHeadersCiphertext
|
||||
})
|
||||
) as LogStreamHeaders[])
|
||||
: [];
|
||||
|
||||
const headers: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
|
||||
|
||||
if (streamHeaders.length)
|
||||
streamHeaders.forEach(({ key, value }) => {
|
||||
headers[key] = value;
|
||||
});
|
||||
|
||||
return request.post(url, auditLog, {
|
||||
headers,
|
||||
// request timeout
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
// connection timeout
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
queueService.start(QueueName.AuditLogPrune, async () => {
|
||||
|
@@ -625,9 +625,9 @@ interface SecretApprovalReopened {
|
||||
interface SecretApprovalRequest {
|
||||
type: EventType.SECRET_APPROVAL_REQUEST;
|
||||
metadata: {
|
||||
committedBy: string;
|
||||
secretApprovalRequestSlug: string;
|
||||
secretApprovalRequestId: string;
|
||||
committedByUser?: string | null; // Needs to be nullable for backward compatibility
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,12 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TGroupProjectUserAdditionalPrivilegeDALFactory = ReturnType<
|
||||
typeof groupProjectUserAdditionalPrivilegeDALFactory
|
||||
>;
|
||||
|
||||
export const groupProjectUserAdditionalPrivilegeDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.GroupProjectUserAdditionalPrivilege);
|
||||
return orm;
|
||||
};
|
@@ -5,10 +5,78 @@ import { TableName, TGroups } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex";
|
||||
|
||||
import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal";
|
||||
|
||||
export type TGroupDALFactory = ReturnType<typeof groupDALFactory>;
|
||||
|
||||
export const groupDALFactory = (db: TDbClient) => {
|
||||
export const groupDALFactory = (db: TDbClient, userGroupMembershipDAL: TUserGroupMembershipDALFactory) => {
|
||||
const groupOrm = ormify(db, TableName.Groups);
|
||||
const groupMembershipOrm = ormify(db, TableName.GroupProjectMembership);
|
||||
const accessApprovalRequestOrm = ormify(db, TableName.AccessApprovalRequest);
|
||||
const secretApprovalRequestOrm = ormify(db, TableName.SecretApprovalRequest);
|
||||
|
||||
const deleteMany = async (filterQuery: TFindFilter<TGroups>, tx?: Knex) => {
|
||||
const transaction = tx || (await db.transaction());
|
||||
|
||||
// Find all memberships
|
||||
const groups = await groupOrm.find(filterQuery, { tx: transaction });
|
||||
|
||||
for await (const group of groups) {
|
||||
// Find all the group memberships of the groups (a group membership is which projects the group is a part of)
|
||||
const groupProjectMemberships = await groupMembershipOrm.find(
|
||||
{ groupId: group.id },
|
||||
{
|
||||
tx: transaction
|
||||
}
|
||||
);
|
||||
|
||||
// For each of those group memberships, we need to find all the members of the group that don't have a regular membership in the project
|
||||
for await (const groupMembership of groupProjectMemberships) {
|
||||
const members = await userGroupMembershipDAL.findGroupMembersNotInProject(
|
||||
group.id,
|
||||
groupMembership.projectId,
|
||||
transaction
|
||||
);
|
||||
|
||||
// We then delete all the access approval requests and secret approval requests associated with these members
|
||||
await accessApprovalRequestOrm.delete(
|
||||
{
|
||||
groupMembershipId: groupMembership.id,
|
||||
$in: {
|
||||
requestedByUserId: members.map(({ user }) => user.id)
|
||||
}
|
||||
},
|
||||
transaction
|
||||
);
|
||||
|
||||
const policies = await (tx || db)(TableName.SecretApprovalPolicy)
|
||||
.join(TableName.Environment, `${TableName.SecretApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||
.where(`${TableName.Environment}.projectId`, groupMembership.projectId)
|
||||
.select(selectAllTableCols(TableName.SecretApprovalPolicy));
|
||||
|
||||
await secretApprovalRequestOrm.delete(
|
||||
{
|
||||
$in: {
|
||||
policyId: policies.map(({ id }) => id),
|
||||
committerUserId: members.map(({ user }) => user.id)
|
||||
}
|
||||
},
|
||||
transaction
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await groupOrm.delete(
|
||||
{
|
||||
$in: {
|
||||
id: groups.map((group) => group.id)
|
||||
}
|
||||
},
|
||||
transaction
|
||||
);
|
||||
|
||||
return groups;
|
||||
};
|
||||
|
||||
const findGroups = async (filter: TFindFilter<TGroups>, { offset, limit, sort, tx }: TFindOpt<TGroups> = {}) => {
|
||||
try {
|
||||
@@ -122,9 +190,10 @@ export const groupDALFactory = (db: TDbClient) => {
|
||||
};
|
||||
|
||||
return {
|
||||
...groupOrm,
|
||||
findGroups,
|
||||
findByOrgId,
|
||||
findAllGroupMembers,
|
||||
...groupOrm
|
||||
delete: deleteMany
|
||||
};
|
||||
};
|
||||
|
@@ -266,6 +266,9 @@ export const removeUsersFromGroupByUserIds = async ({
|
||||
userIds,
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
accessApprovalRequestDAL,
|
||||
secretApprovalRequestDAL,
|
||||
secretApprovalPolicyDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
tx: outerTx
|
||||
@@ -322,20 +325,16 @@ export const removeUsersFromGroupByUserIds = async ({
|
||||
});
|
||||
|
||||
if (membersToRemoveFromGroupNonPending.length) {
|
||||
// check which projects the group is part of
|
||||
const projectIds = Array.from(
|
||||
new Set(
|
||||
(
|
||||
await groupProjectDAL.find(
|
||||
{
|
||||
groupId: group.id
|
||||
},
|
||||
{ tx }
|
||||
)
|
||||
).map((gp) => gp.projectId)
|
||||
)
|
||||
const groupProjectMemberships = await groupProjectDAL.find(
|
||||
{
|
||||
groupId: group.id
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
// check which projects the group is part of
|
||||
const projectIds = Array.from(new Set(groupProjectMemberships.map((gp) => gp.projectId)));
|
||||
|
||||
// TODO: this part can be optimized
|
||||
for await (const userId of userIds) {
|
||||
const t = await userGroupMembershipDAL.filterProjectsByUserMembership(userId, group.id, projectIds, tx);
|
||||
@@ -353,10 +352,35 @@ export const removeUsersFromGroupByUserIds = async ({
|
||||
);
|
||||
}
|
||||
|
||||
await accessApprovalRequestDAL.delete(
|
||||
{
|
||||
$in: {
|
||||
groupMembershipId: groupProjectMemberships
|
||||
.filter((gp) => projectsToDeleteKeyFor.includes(gp.projectId))
|
||||
.map((gp) => gp.id)
|
||||
},
|
||||
requestedByUserId: userId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const projectSecretApprovalPolicies = await secretApprovalPolicyDAL.findByProjectIds(projectIds);
|
||||
await secretApprovalRequestDAL.delete(
|
||||
{
|
||||
committerUserId: userId,
|
||||
$in: {
|
||||
policyId: projectSecretApprovalPolicies.map((p) => p.id)
|
||||
}
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await userGroupMembershipDAL.delete(
|
||||
{
|
||||
groupId: group.id,
|
||||
userId
|
||||
$in: {
|
||||
userId: membersToRemoveFromGroupNonPending.map((member) => member.id)
|
||||
}
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -364,12 +388,15 @@ export const removeUsersFromGroupByUserIds = async ({
|
||||
}
|
||||
|
||||
if (membersToRemoveFromGroupPending.length) {
|
||||
await userGroupMembershipDAL.delete({
|
||||
groupId: group.id,
|
||||
$in: {
|
||||
userId: membersToRemoveFromGroupPending.map((member) => member.id)
|
||||
}
|
||||
});
|
||||
await userGroupMembershipDAL.delete(
|
||||
{
|
||||
groupId: group.id,
|
||||
$in: {
|
||||
userId: membersToRemoveFromGroupPending.map((member) => member.id)
|
||||
}
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return membersToRemoveFromGroupNonPending.concat(membersToRemoveFromGroupPending);
|
||||
|
@@ -12,9 +12,12 @@ import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal
|
||||
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TAccessApprovalRequestDALFactory } from "../access-approval-request/access-approval-request-dal";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
|
||||
import { TSecretApprovalRequestDALFactory } from "../secret-approval-request/secret-approval-request-dal";
|
||||
import { TGroupDALFactory } from "./group-dal";
|
||||
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "./group-fns";
|
||||
import {
|
||||
@@ -41,6 +44,9 @@ type TGroupServiceFactoryDep = {
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "delete">;
|
||||
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "delete">;
|
||||
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findByProjectIds">;
|
||||
};
|
||||
|
||||
export type TGroupServiceFactory = ReturnType<typeof groupServiceFactory>;
|
||||
@@ -50,6 +56,9 @@ export const groupServiceFactory = ({
|
||||
groupDAL,
|
||||
groupProjectDAL,
|
||||
orgDAL,
|
||||
secretApprovalRequestDAL,
|
||||
secretApprovalPolicyDAL,
|
||||
accessApprovalRequestDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectDAL,
|
||||
projectBotDAL,
|
||||
@@ -328,6 +337,9 @@ export const groupServiceFactory = ({
|
||||
group,
|
||||
userIds: [user.id],
|
||||
userDAL,
|
||||
accessApprovalRequestDAL,
|
||||
secretApprovalPolicyDAL,
|
||||
secretApprovalRequestDAL,
|
||||
userGroupMembershipDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL
|
||||
|
@@ -10,6 +10,10 @@ import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal
|
||||
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TAccessApprovalRequestDALFactory } from "../access-approval-request/access-approval-request-dal";
|
||||
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
|
||||
import { TSecretApprovalRequestDALFactory } from "../secret-approval-request/secret-approval-request-dal";
|
||||
|
||||
export type TCreateGroupDTO = {
|
||||
name: string;
|
||||
slug?: string;
|
||||
@@ -77,6 +81,9 @@ export type TRemoveUsersFromGroupByUserIds = {
|
||||
group: TGroups;
|
||||
userIds: string[];
|
||||
userDAL: Pick<TUserDALFactory, "find" | "transaction">;
|
||||
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "delete">;
|
||||
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "delete">;
|
||||
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findByProjectIds">;
|
||||
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "filterProjectsByUserMembership" | "delete">;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "delete">;
|
||||
|
@@ -26,9 +26,12 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
import { normalizeUsername } from "@app/services/user/user-fns";
|
||||
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
||||
|
||||
import { TAccessApprovalRequestDALFactory } from "../access-approval-request/access-approval-request-dal";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
|
||||
import { TSecretApprovalRequestDALFactory } from "../secret-approval-request/secret-approval-request-dal";
|
||||
import { TLdapConfigDALFactory } from "./ldap-config-dal";
|
||||
import {
|
||||
TCreateLdapCfgDTO,
|
||||
@@ -67,6 +70,9 @@ type TLdapConfigServiceFactoryDep = {
|
||||
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "delete">;
|
||||
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "delete">;
|
||||
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findByProjectIds">;
|
||||
};
|
||||
|
||||
export type TLdapConfigServiceFactory = ReturnType<typeof ldapConfigServiceFactory>;
|
||||
@@ -78,6 +84,9 @@ export const ldapConfigServiceFactory = ({
|
||||
orgBotDAL,
|
||||
groupDAL,
|
||||
groupProjectDAL,
|
||||
accessApprovalRequestDAL,
|
||||
secretApprovalPolicyDAL,
|
||||
secretApprovalRequestDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL,
|
||||
@@ -524,7 +533,10 @@ export const ldapConfigServiceFactory = ({
|
||||
group,
|
||||
userIds: [newUser.id],
|
||||
userDAL,
|
||||
secretApprovalRequestDAL,
|
||||
accessApprovalRequestDAL,
|
||||
userGroupMembershipDAL,
|
||||
secretApprovalPolicyDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
tx
|
||||
|
@@ -24,6 +24,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
customAlerts: false,
|
||||
auditLogs: false,
|
||||
auditLogsRetentionDays: 0,
|
||||
auditLogStreams: false,
|
||||
auditLogStreamLimit: 3,
|
||||
samlSSO: false,
|
||||
scim: false,
|
||||
ldap: false,
|
||||
|
@@ -121,8 +121,8 @@ export const licenseServiceFactory = ({
|
||||
|
||||
if (isValidOfflineLicense) {
|
||||
onPremFeatures = contents.license.features;
|
||||
instanceType = InstanceType.EnterpriseOnPrem;
|
||||
logger.info(`Instance type: ${InstanceType.EnterpriseOnPrem}`);
|
||||
instanceType = InstanceType.EnterpriseOnPremOffline;
|
||||
logger.info(`Instance type: ${InstanceType.EnterpriseOnPremOffline}`);
|
||||
isValidLicense = true;
|
||||
return;
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ import { TOrgPermission } from "@app/lib/types";
|
||||
export enum InstanceType {
|
||||
OnPrem = "self-hosted",
|
||||
EnterpriseOnPrem = "enterprise-self-hosted",
|
||||
EnterpriseOnPremOffline = "enterprise-self-hosted-offline",
|
||||
Cloud = "cloud"
|
||||
}
|
||||
|
||||
@@ -40,6 +41,8 @@ export type TFeatureSet = {
|
||||
customAlerts: false;
|
||||
auditLogs: false;
|
||||
auditLogsRetentionDays: 0;
|
||||
auditLogStreams: false;
|
||||
auditLogStreamLimit: 3;
|
||||
samlSSO: false;
|
||||
scim: false;
|
||||
ldap: false;
|
||||
|
@@ -62,6 +62,11 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
|
||||
`${TableName.GroupProjectMembership}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.GroupProjectUserAdditionalPrivilege,
|
||||
`${TableName.GroupProjectUserAdditionalPrivilege}.groupProjectMembershipId`,
|
||||
`${TableName.GroupProjectMembership}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.ProjectRoles,
|
||||
`${TableName.GroupProjectMembershipRole}.customRoleId`,
|
||||
@@ -77,11 +82,34 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
db.ref("projectId").withSchema(TableName.GroupProjectMembership),
|
||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||
db.ref("orgId").withSchema(TableName.Project),
|
||||
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug")
|
||||
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
||||
db.ref("permissions").withSchema(TableName.ProjectRoles)
|
||||
)
|
||||
.select("permissions");
|
||||
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
|
||||
.select(
|
||||
db.ref("projectId").withSchema(TableName.GroupProjectMembership).as("groupMembershipProjectId"),
|
||||
db.ref("id").withSchema(TableName.GroupProjectUserAdditionalPrivilege).as("userApId"),
|
||||
db.ref("permissions").withSchema(TableName.GroupProjectUserAdditionalPrivilege).as("userApPermissions"),
|
||||
db.ref("temporaryMode").withSchema(TableName.GroupProjectUserAdditionalPrivilege).as("userApTemporaryMode"),
|
||||
db.ref("isTemporary").withSchema(TableName.GroupProjectUserAdditionalPrivilege).as("userApIsTemporary"),
|
||||
db.ref("temporaryRange").withSchema(TableName.GroupProjectUserAdditionalPrivilege).as("userApTemporaryRange"),
|
||||
db.ref("groupProjectMembershipId").withSchema(TableName.GroupProjectUserAdditionalPrivilege),
|
||||
db
|
||||
.ref("requestedByUserId")
|
||||
.withSchema(TableName.GroupProjectUserAdditionalPrivilege)
|
||||
.as("userApRequestedByUserId"),
|
||||
|
||||
const docs = await db(TableName.ProjectMembership)
|
||||
db
|
||||
.ref("temporaryAccessStartTime")
|
||||
.withSchema(TableName.GroupProjectUserAdditionalPrivilege)
|
||||
.as("userApTemporaryAccessStartTime"),
|
||||
db
|
||||
.ref("temporaryAccessEndTime")
|
||||
.withSchema(TableName.GroupProjectUserAdditionalPrivilege)
|
||||
.as("userApTemporaryAccessEndTime")
|
||||
);
|
||||
|
||||
const projectMemberDocs = await db(TableName.ProjectMembership)
|
||||
.join(
|
||||
TableName.ProjectUserMembershipRole,
|
||||
`${TableName.ProjectUserMembershipRole}.projectMembershipId`,
|
||||
@@ -127,7 +155,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
);
|
||||
|
||||
const permission = sqlNestRelationships({
|
||||
data: docs,
|
||||
data: projectMemberDocs,
|
||||
key: "projectId",
|
||||
parentMapper: ({ orgId, orgAuthEnforced, membershipId, membershipCreatedAt, membershipUpdatedAt }) => ({
|
||||
orgId,
|
||||
@@ -194,6 +222,33 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
permissions: z.unknown(),
|
||||
customRoleSlug: z.string().optional().nullable()
|
||||
}).parse(data)
|
||||
},
|
||||
{
|
||||
key: "userApId",
|
||||
label: "additionalPrivileges" as const,
|
||||
mapper: ({
|
||||
groupMembershipProjectId,
|
||||
groupProjectMembershipId,
|
||||
userApId,
|
||||
userApPermissions,
|
||||
userApRequestedByUserId,
|
||||
userApIsTemporary,
|
||||
userApTemporaryMode,
|
||||
userApTemporaryRange,
|
||||
userApTemporaryAccessEndTime,
|
||||
userApTemporaryAccessStartTime
|
||||
}) => ({
|
||||
groupProjectMembershipId,
|
||||
groupMembershipProjectId,
|
||||
id: userApId,
|
||||
userId: userApRequestedByUserId,
|
||||
permissions: userApPermissions,
|
||||
temporaryRange: userApTemporaryRange,
|
||||
temporaryMode: userApTemporaryMode,
|
||||
temporaryAccessEndTime: userApTemporaryAccessEndTime,
|
||||
temporaryAccessStartTime: userApTemporaryAccessStartTime,
|
||||
isTemporary: userApIsTemporary
|
||||
})
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -214,15 +269,24 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||
) ?? [];
|
||||
|
||||
const activeAdditionalPrivileges = permission?.[0]?.additionalPrivileges?.filter(
|
||||
({ isTemporary, temporaryAccessEndTime }) =>
|
||||
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||
);
|
||||
const activeAdditionalPrivileges =
|
||||
permission?.[0]?.additionalPrivileges?.filter(
|
||||
({ isTemporary, temporaryAccessEndTime }) =>
|
||||
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||
) ?? [];
|
||||
const activeGroupAdditionalPrivileges =
|
||||
groupPermission?.[0]?.additionalPrivileges?.filter(
|
||||
({ isTemporary, temporaryAccessEndTime, groupProjectMembershipId, groupMembershipProjectId, userId: user }) =>
|
||||
groupMembershipProjectId === projectId &&
|
||||
!!groupProjectMembershipId &&
|
||||
user === userId &&
|
||||
(!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime))
|
||||
) ?? [];
|
||||
|
||||
return {
|
||||
...(permission[0] || groupPermission[0]),
|
||||
roles: [...activeRoles, ...activeGroupRoles],
|
||||
additionalPrivileges: activeAdditionalPrivileges
|
||||
additionalPrivileges: [...activeAdditionalPrivileges, ...activeGroupAdditionalPrivileges]
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "GetProjectPermission" });
|
||||
|
@@ -90,6 +90,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
|
||||
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
|
||||
|
||||
// This is fine. This service is only used for direct user privileges, not group-based privileges
|
||||
if (!userPrivilege.projectMembershipId)
|
||||
throw new BadRequestError({ message: "Operation not supported for groups" });
|
||||
|
||||
const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
|
||||
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
||||
|
||||
@@ -138,6 +142,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
|
||||
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
|
||||
|
||||
// This is fine. This service is only used for direct user privileges, not group-based privileges
|
||||
if (!userPrivilege.projectMembershipId)
|
||||
throw new BadRequestError({ message: "Operation not supported for groups" });
|
||||
|
||||
const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
|
||||
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
||||
|
||||
@@ -164,6 +172,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
|
||||
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
|
||||
|
||||
// This is fine. This service is only used for direct user privileges, not group-based privileges
|
||||
if (!userPrivilege.projectMembershipId)
|
||||
throw new BadRequestError({ message: "Operation not supported for groups" });
|
||||
|
||||
const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
|
||||
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
||||
|
||||
|
@@ -22,9 +22,12 @@ import { TProjectMembershipDALFactory } from "@app/services/project-membership/p
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TAccessApprovalRequestDALFactory } from "../access-approval-request/access-approval-request-dal";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
|
||||
import { TSecretApprovalRequestDALFactory } from "../secret-approval-request/secret-approval-request-dal";
|
||||
import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList } from "./scim-fns";
|
||||
import {
|
||||
TCreateScimGroupDTO,
|
||||
@@ -64,6 +67,9 @@ type TScimServiceFactoryDep = {
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "delete">;
|
||||
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "delete">;
|
||||
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findByProjectIds">;
|
||||
smtpService: TSmtpService;
|
||||
};
|
||||
|
||||
@@ -81,6 +87,9 @@ export const scimServiceFactory = ({
|
||||
userGroupMembershipDAL,
|
||||
projectKeyDAL,
|
||||
projectBotDAL,
|
||||
accessApprovalRequestDAL,
|
||||
secretApprovalRequestDAL,
|
||||
secretApprovalPolicyDAL,
|
||||
permissionService,
|
||||
smtpService
|
||||
}: TScimServiceFactoryDep) => {
|
||||
@@ -710,6 +719,9 @@ export const scimServiceFactory = ({
|
||||
userIds: toRemoveUserIds,
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
secretApprovalPolicyDAL,
|
||||
accessApprovalRequestDAL,
|
||||
secretApprovalRequestDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
tx
|
||||
|
@@ -20,7 +20,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretApprovalPolicy}.id`,
|
||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
.select(tx.ref("approverId").withSchema(TableName.SecretApprovalPolicyApprover))
|
||||
.select(tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover))
|
||||
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
|
||||
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
|
||||
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
|
||||
@@ -33,18 +33,18 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
const doc = await sapFindQuery(tx || db, {
|
||||
[`${TableName.SecretApprovalPolicy}.id` as "id"]: id
|
||||
});
|
||||
const formatedDoc = mergeOneToManyRelation(
|
||||
const formattedDoc = mergeOneToManyRelation(
|
||||
doc,
|
||||
"id",
|
||||
({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({
|
||||
({ approverUserId, envId, envName: name, envSlug: slug, ...el }) => ({
|
||||
...el,
|
||||
envId,
|
||||
environment: { id: envId, name, slug }
|
||||
}),
|
||||
({ approverId }) => approverId,
|
||||
({ approverUserId }) => approverUserId,
|
||||
"approvers"
|
||||
);
|
||||
return formatedDoc?.[0];
|
||||
return formattedDoc?.[0];
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindById" });
|
||||
}
|
||||
@@ -53,22 +53,31 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
const find = async (filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>, tx?: Knex) => {
|
||||
try {
|
||||
const docs = await sapFindQuery(tx || db, filter);
|
||||
const formatedDoc = mergeOneToManyRelation(
|
||||
const formattedDoc = mergeOneToManyRelation(
|
||||
docs,
|
||||
"id",
|
||||
({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({
|
||||
({ approverUserId, envId, envName: name, envSlug: slug, ...el }) => ({
|
||||
...el,
|
||||
envId,
|
||||
environment: { id: envId, name, slug }
|
||||
}),
|
||||
({ approverId }) => approverId,
|
||||
({ approverUserId }) => approverUserId,
|
||||
"approvers"
|
||||
);
|
||||
return formatedDoc;
|
||||
return formattedDoc;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...secretApprovalPolicyOrm, findById, find };
|
||||
const findByProjectIds = async (projectIds: string[], tx?: Knex) => {
|
||||
const policies = await (tx || db)(TableName.SecretApprovalPolicy)
|
||||
.join(TableName.Environment, `${TableName.SecretApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||
.whereIn(`${TableName.Environment}.projectId`, projectIds)
|
||||
.select(selectAllTableCols(TableName.SecretApprovalPolicy));
|
||||
|
||||
return policies;
|
||||
};
|
||||
|
||||
return { ...secretApprovalPolicyOrm, findById, find, findByProjectIds };
|
||||
};
|
||||
|
@@ -7,6 +7,7 @@ import { BadRequestError } from "@app/lib/errors";
|
||||
import { containsGlobPatterns } from "@app/lib/picomatch";
|
||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal";
|
||||
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
|
||||
@@ -29,6 +30,7 @@ type TSecretApprovalPolicyServiceFactoryDep = {
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
||||
userDAL: Pick<TUserDALFactory, "findUsersByProjectId" | "findUserByProjectId">;
|
||||
};
|
||||
|
||||
export type TSecretApprovalPolicyServiceFactory = ReturnType<typeof secretApprovalPolicyServiceFactory>;
|
||||
@@ -38,7 +40,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
permissionService,
|
||||
secretApprovalPolicyApproverDAL,
|
||||
projectEnvDAL,
|
||||
projectMembershipDAL
|
||||
userDAL
|
||||
}: TSecretApprovalPolicyServiceFactoryDep) => {
|
||||
const createSecretApprovalPolicy = async ({
|
||||
name,
|
||||
@@ -69,11 +71,12 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
const env = await projectEnvDAL.findOne({ slug: environment, projectId });
|
||||
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
||||
|
||||
const secretApprovers = await projectMembershipDAL.find({
|
||||
const secretApproverUsers = await userDAL.findUsersByProjectId(
|
||||
projectId,
|
||||
$in: { id: approvers }
|
||||
});
|
||||
if (secretApprovers.length !== approvers.length)
|
||||
approvers.map((approverUserId) => approverUserId)
|
||||
);
|
||||
|
||||
if (secretApproverUsers.length !== approvers.length)
|
||||
throw new BadRequestError({ message: "Approver not found in project" });
|
||||
|
||||
const secretApproval = await secretApprovalPolicyDAL.transaction(async (tx) => {
|
||||
@@ -87,8 +90,8 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
await secretApprovalPolicyApproverDAL.insertMany(
|
||||
secretApprovers.map(({ id }) => ({
|
||||
approverId: id,
|
||||
secretApproverUsers.map(({ id }) => ({
|
||||
approverUserId: id,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
@@ -132,21 +135,19 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
if (approvers) {
|
||||
const secretApprovers = await projectMembershipDAL.find(
|
||||
{
|
||||
projectId: secretApprovalPolicy.projectId,
|
||||
$in: { id: approvers }
|
||||
},
|
||||
{ tx }
|
||||
const secretApproverUsers = await userDAL.findUsersByProjectId(
|
||||
secretApprovalPolicy.projectId,
|
||||
approvers.map((approverUserId) => approverUserId)
|
||||
);
|
||||
if (secretApprovers.length !== approvers.length)
|
||||
|
||||
if (secretApproverUsers.length !== approvers.length)
|
||||
throw new BadRequestError({ message: "Approver not found in project" });
|
||||
if (doc.approvals > secretApprovers.length)
|
||||
if (doc.approvals > secretApproverUsers.length)
|
||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||
await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
||||
await secretApprovalPolicyApproverDAL.insertMany(
|
||||
secretApprovers.map(({ id }) => ({
|
||||
approverId: id,
|
||||
secretApproverUsers.map((user) => ({
|
||||
approverUserId: user.id,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
|
@@ -16,7 +16,7 @@ export type TSecretApprovalRequestDALFactory = ReturnType<typeof secretApprovalR
|
||||
|
||||
type TFindQueryFilter = {
|
||||
projectId: string;
|
||||
membershipId: string;
|
||||
actorId: string;
|
||||
status?: RequestState;
|
||||
environment?: string;
|
||||
committer?: string;
|
||||
@@ -49,7 +49,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||
.select(
|
||||
tx.ref("member").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerMemberId"),
|
||||
tx.ref("memberUserId").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerMemberId"),
|
||||
tx.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
|
||||
tx.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"),
|
||||
tx.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"),
|
||||
@@ -57,14 +57,14 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
|
||||
tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
||||
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
||||
tx.ref("approverId").withSchema(TableName.SecretApprovalPolicyApprover)
|
||||
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover)
|
||||
);
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const sql = findQuery({ [`${TableName.SecretApprovalRequest}.id` as "id"]: id }, tx || db);
|
||||
const docs = await sql;
|
||||
const formatedDoc = sqlNestRelationships({
|
||||
const formattedDoc = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: (el) => ({
|
||||
@@ -84,20 +84,20 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
label: "reviewers" as const,
|
||||
mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined)
|
||||
},
|
||||
{ key: "approverId", label: "approvers" as const, mapper: ({ approverId }) => approverId }
|
||||
{ key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId }
|
||||
]
|
||||
});
|
||||
if (!formatedDoc?.[0]) return;
|
||||
if (!formattedDoc?.[0]) return;
|
||||
return {
|
||||
...formatedDoc[0],
|
||||
policy: { ...formatedDoc[0].policy, approvers: formatedDoc[0].approvers }
|
||||
...formattedDoc[0],
|
||||
policy: { ...formattedDoc[0].policy, approvers: formattedDoc[0].approvers }
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByIdSAR" });
|
||||
}
|
||||
};
|
||||
|
||||
const findProjectRequestCount = async (projectId: string, membershipId: string, tx?: Knex) => {
|
||||
const findProjectRequestCount = async (projectId: string, approverUserId: string, tx?: Knex) => {
|
||||
try {
|
||||
const docs = await (tx || db)
|
||||
.with(
|
||||
@@ -110,12 +110,12 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretApprovalRequest}.policyId`,
|
||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
.where({ projectId })
|
||||
.where({ [`${TableName.Environment}.projectId` as "projectId"]: projectId })
|
||||
.andWhere(
|
||||
(bd) =>
|
||||
void bd
|
||||
.where(`${TableName.SecretApprovalPolicyApprover}.approverId`, membershipId)
|
||||
.orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId)
|
||||
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, approverUserId)
|
||||
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, approverUserId)
|
||||
)
|
||||
.select("status", `${TableName.SecretApprovalRequest}.id`)
|
||||
.groupBy(`${TableName.SecretApprovalRequest}.id`, "status")
|
||||
@@ -142,7 +142,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
};
|
||||
|
||||
const findByProjectId = async (
|
||||
{ status, limit = 20, offset = 0, projectId, committer, environment, membershipId }: TFindQueryFilter,
|
||||
{ status, limit = 20, offset = 0, projectId, committer, environment, actorId }: TFindQueryFilter,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
@@ -173,21 +173,21 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
)
|
||||
.where(
|
||||
stripUndefinedInWhere({
|
||||
projectId,
|
||||
[`${TableName.Environment}.projectId`]: projectId,
|
||||
[`${TableName.Environment}.slug` as "slug"]: environment,
|
||||
[`${TableName.SecretApprovalRequest}.status`]: status,
|
||||
committerId: committer
|
||||
committerUserId: committer
|
||||
})
|
||||
)
|
||||
.andWhere(
|
||||
(bd) =>
|
||||
void bd
|
||||
.where(`${TableName.SecretApprovalPolicyApprover}.approverId`, membershipId)
|
||||
.orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId)
|
||||
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, actorId)
|
||||
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, actorId)
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||
.select(
|
||||
db.ref("projectId").withSchema(TableName.Environment),
|
||||
db.ref("projectId").withSchema(TableName.Environment).as("envProjectId"),
|
||||
db.ref("slug").withSchema(TableName.Environment).as("environment"),
|
||||
db.ref("id").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerMemberId"),
|
||||
db.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
|
||||
@@ -201,7 +201,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
),
|
||||
db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
||||
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
||||
db.ref("approverId").withSchema(TableName.SecretApprovalPolicyApprover)
|
||||
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover)
|
||||
)
|
||||
.orderBy("createdAt", "desc");
|
||||
|
||||
@@ -217,7 +217,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
parentMapper: (el) => ({
|
||||
...SecretApprovalRequestsSchema.parse(el),
|
||||
environment: el.environment,
|
||||
projectId: el.projectId,
|
||||
projectId: el.envProjectId,
|
||||
policy: {
|
||||
id: el.policyId,
|
||||
name: el.policyName,
|
||||
@@ -232,9 +232,9 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
mapper: ({ reviewerMemberId: member, reviewerStatus: s }) => (member ? { member, status: s } : undefined)
|
||||
},
|
||||
{
|
||||
key: "approverId",
|
||||
key: "approverUserId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverId }) => approverId
|
||||
mapper: ({ approverUserId }) => approverUserId
|
||||
},
|
||||
{
|
||||
key: "commitId",
|
||||
|
@@ -113,7 +113,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
db.ref("secretCommentTag").withSchema(TableName.SecretVersion).as("secVerCommentTag"),
|
||||
db.ref("secretCommentCiphertext").withSchema(TableName.SecretVersion).as("secVerCommentCiphertext")
|
||||
);
|
||||
const formatedDoc = sqlNestRelationships({
|
||||
const formattedDoc = sqlNestRelationships({
|
||||
data: doc,
|
||||
key: "id",
|
||||
parentMapper: (data) => SecretApprovalRequestsSecretsSchema.omit({ secretVersion: true }).parse(data),
|
||||
@@ -212,7 +212,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
]
|
||||
});
|
||||
return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({
|
||||
return formattedDoc?.map(({ secret, secretVersion, ...el }) => ({
|
||||
...el,
|
||||
secret: secret?.[0],
|
||||
secretVersion: secretVersion?.[0]
|
||||
|
@@ -85,7 +85,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
await permissionService.getProjectPermission(
|
||||
actor as ActorType.USER,
|
||||
actorId,
|
||||
projectId,
|
||||
@@ -93,7 +93,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
const count = await secretApprovalRequestDAL.findProjectRequestCount(projectId, membership.id);
|
||||
const count = await secretApprovalRequestDAL.findProjectRequestCount(projectId, actorId);
|
||||
return count;
|
||||
};
|
||||
|
||||
@@ -111,19 +111,14 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
}: TListApprovalsDTO) => {
|
||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
|
||||
|
||||
const approvals = await secretApprovalRequestDAL.findByProjectId({
|
||||
projectId,
|
||||
committer,
|
||||
environment,
|
||||
status,
|
||||
membershipId: membership.id,
|
||||
actorId,
|
||||
limit,
|
||||
offset
|
||||
});
|
||||
@@ -143,7 +138,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||
|
||||
const { policy } = secretApprovalRequest;
|
||||
const { membership, hasRole } = await permissionService.getProjectPermission(
|
||||
const { hasRole } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
secretApprovalRequest.projectId,
|
||||
@@ -152,8 +147,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
);
|
||||
if (
|
||||
!hasRole(ProjectMembershipRole.Admin) &&
|
||||
secretApprovalRequest.committerId !== membership.id &&
|
||||
!policy.approvers.find((approverId) => approverId === membership.id)
|
||||
secretApprovalRequest.committerUserId !== actorId &&
|
||||
!policy.approvers.find((approverUserId) => approverUserId === actorId)
|
||||
) {
|
||||
throw new UnauthorizedError({ message: "User has no access" });
|
||||
}
|
||||
@@ -178,7 +173,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||
|
||||
const { policy } = secretApprovalRequest;
|
||||
const { membership, hasRole } = await permissionService.getProjectPermission(
|
||||
const { hasRole } = await permissionService.getProjectPermission(
|
||||
ActorType.USER,
|
||||
actorId,
|
||||
secretApprovalRequest.projectId,
|
||||
@@ -187,8 +182,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
);
|
||||
if (
|
||||
!hasRole(ProjectMembershipRole.Admin) &&
|
||||
secretApprovalRequest.committerId !== membership.id &&
|
||||
!policy.approvers.find((approverId) => approverId === membership.id)
|
||||
secretApprovalRequest.committerUserId !== actorId &&
|
||||
!policy.approvers.find((approverUserId) => approverUserId === actorId)
|
||||
) {
|
||||
throw new UnauthorizedError({ message: "User has no access" });
|
||||
}
|
||||
@@ -196,7 +191,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
const review = await secretApprovalRequestReviewerDAL.findOne(
|
||||
{
|
||||
requestId: secretApprovalRequest.id,
|
||||
member: membership.id
|
||||
memberUserId: actorId
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -205,7 +200,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
{
|
||||
status,
|
||||
requestId: secretApprovalRequest.id,
|
||||
member: membership.id
|
||||
memberUserId: actorId
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -228,7 +223,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||
|
||||
const { policy } = secretApprovalRequest;
|
||||
const { membership, hasRole } = await permissionService.getProjectPermission(
|
||||
const { hasRole } = await permissionService.getProjectPermission(
|
||||
ActorType.USER,
|
||||
actorId,
|
||||
secretApprovalRequest.projectId,
|
||||
@@ -237,8 +232,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
);
|
||||
if (
|
||||
!hasRole(ProjectMembershipRole.Admin) &&
|
||||
secretApprovalRequest.committerId !== membership.id &&
|
||||
!policy.approvers.find((approverId) => approverId === membership.id)
|
||||
secretApprovalRequest.committerUserId !== actorId &&
|
||||
!policy.approvers.find((approverUserId) => approverUserId === actorId)
|
||||
) {
|
||||
throw new UnauthorizedError({ message: "User has no access" });
|
||||
}
|
||||
@@ -251,8 +246,9 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
|
||||
const updatedRequest = await secretApprovalRequestDAL.updateById(secretApprovalRequest.id, {
|
||||
status,
|
||||
statusChangeBy: membership.id
|
||||
statusChangeByUserId: actorId
|
||||
});
|
||||
|
||||
return { ...secretApprovalRequest, ...updatedRequest };
|
||||
};
|
||||
|
||||
@@ -268,7 +264,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||
|
||||
const { policy, folderId, projectId } = secretApprovalRequest;
|
||||
const { membership, hasRole } = await permissionService.getProjectPermission(
|
||||
const { hasRole } = await permissionService.getProjectPermission(
|
||||
ActorType.USER,
|
||||
actorId,
|
||||
projectId,
|
||||
@@ -278,8 +274,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
|
||||
if (
|
||||
!hasRole(ProjectMembershipRole.Admin) &&
|
||||
secretApprovalRequest.committerId !== membership.id &&
|
||||
!policy.approvers.find((approverId) => approverId === membership.id)
|
||||
secretApprovalRequest.committerUserId !== actorId &&
|
||||
!policy.approvers.find((approverUserId) => approverUserId === actorId)
|
||||
) {
|
||||
throw new UnauthorizedError({ message: "User has no access" });
|
||||
}
|
||||
@@ -290,7 +286,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
const hasMinApproval =
|
||||
secretApprovalRequest.policy.approvals <=
|
||||
secretApprovalRequest.policy.approvers.filter(
|
||||
(approverId) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
|
||||
(approverUserId) => reviewers[approverUserId.toString()] === ApprovalStatus.APPROVED
|
||||
).length;
|
||||
|
||||
if (!hasMinApproval) throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
|
||||
@@ -445,7 +441,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
conflicts: JSON.stringify(conflicts),
|
||||
hasMerged: true,
|
||||
status: RequestState.Closed,
|
||||
statusChangeBy: membership.id
|
||||
statusChangeByUserId: actorId
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -480,7 +476,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
}: TGenerateSecretApprovalRequestDTO) => {
|
||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||
|
||||
const { permission, membership } = await permissionService.getProjectPermission(
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
@@ -634,7 +630,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
policyId: policy.id,
|
||||
status: "open",
|
||||
hasMerged: false,
|
||||
committerId: membership.id
|
||||
committerUserId: actorId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
@@ -614,3 +614,29 @@ export const INTEGRATION = {
|
||||
integrationId: "The ID of the integration object."
|
||||
}
|
||||
};
|
||||
|
||||
export const AUDIT_LOG_STREAMS = {
|
||||
CREATE: {
|
||||
url: "The HTTP URL to push logs to.",
|
||||
headers: {
|
||||
desc: "The HTTP headers attached for the external prrovider requests.",
|
||||
key: "The HTTP header key name.",
|
||||
value: "The HTTP header value."
|
||||
}
|
||||
},
|
||||
UPDATE: {
|
||||
id: "The ID of the audit log stream to update.",
|
||||
url: "The HTTP URL to push logs to.",
|
||||
headers: {
|
||||
desc: "The HTTP headers attached for the external prrovider requests.",
|
||||
key: "The HTTP header key name.",
|
||||
value: "The HTTP header value."
|
||||
}
|
||||
},
|
||||
DELETE: {
|
||||
id: "The ID of the audit log stream to delete."
|
||||
},
|
||||
GET_BY_ID: {
|
||||
id: "The ID of the audit log stream to get details."
|
||||
}
|
||||
};
|
||||
|
@@ -119,6 +119,7 @@ const envSchema = z
|
||||
})
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
isCloud: Boolean(data.LICENSE_SERVER_KEY),
|
||||
isSmtpConfigured: Boolean(data.SMTP_HOST),
|
||||
isRedisConfigured: Boolean(data.REDIS_URL),
|
||||
isDevelopmentMode: data.NODE_ENV === "development",
|
||||
|
@@ -17,7 +17,7 @@ export type TOrgPermission = {
|
||||
actorId: string;
|
||||
orgId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string | undefined;
|
||||
actorOrgId: string;
|
||||
};
|
||||
|
||||
export type TProjectPermission = {
|
||||
|
@@ -1 +1,2 @@
|
||||
export { isDisposableEmail } from "./validate-email";
|
||||
export { validateLocalIps } from "./validate-url";
|
||||
|
18
backend/src/lib/validator/validate-url.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { getConfig } from "../config/env";
|
||||
import { BadRequestError } from "../errors";
|
||||
|
||||
export const validateLocalIps = (url: string) => {
|
||||
const validUrl = new URL(url);
|
||||
const appCfg = getConfig();
|
||||
// on cloud local ips are not allowed
|
||||
if (
|
||||
appCfg.isCloud &&
|
||||
(validUrl.host === "host.docker.internal" ||
|
||||
validUrl.host.match(/^10\.\d+\.\d+\.\d+/) ||
|
||||
validUrl.host.match(/^192\.168\.\d+\.\d+/))
|
||||
)
|
||||
throw new BadRequestError({ message: "Local IPs not allowed as URL" });
|
||||
|
||||
if (validUrl.host === "localhost" || validUrl.host === "127.0.0.1")
|
||||
throw new BadRequestError({ message: "Localhost not allowed" });
|
||||
};
|
@@ -108,6 +108,7 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
||||
if (req.url.includes("/api/v3/auth/")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!authMode) return;
|
||||
|
||||
switch (authMode) {
|
||||
|
@@ -2,9 +2,17 @@ import { Knex } from "knex";
|
||||
import { z } from "zod";
|
||||
|
||||
import { registerV1EERoutes } from "@app/ee/routes/v1";
|
||||
import { accessApprovalPolicyApproverDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal";
|
||||
import { accessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal";
|
||||
import { accessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service";
|
||||
import { accessApprovalRequestDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-dal";
|
||||
import { accessApprovalRequestReviewerDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-reviewer-dal";
|
||||
import { accessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service";
|
||||
import { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
|
||||
import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue";
|
||||
import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||
import { auditLogStreamDALFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-dal";
|
||||
import { auditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
|
||||
import { dynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal";
|
||||
import { dynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
||||
import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/providers";
|
||||
@@ -14,6 +22,7 @@ import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secre
|
||||
import { groupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
import { groupServiceFactory } from "@app/ee/services/group/group-service";
|
||||
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
import { groupProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/group-project-user-additional-privilege/group-project-user-additional-privilege-dal";
|
||||
import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal";
|
||||
import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
||||
import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal";
|
||||
@@ -193,6 +202,7 @@ export const registerRoutes = async (
|
||||
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
|
||||
|
||||
const auditLogDAL = auditLogDALFactory(db);
|
||||
const auditLogStreamDAL = auditLogStreamDALFactory(db);
|
||||
const trustedIpDAL = trustedIpDALFactory(db);
|
||||
const telemetryDAL = telemetryDALFactory(db);
|
||||
|
||||
@@ -202,6 +212,14 @@ export const registerRoutes = async (
|
||||
const scimDAL = scimDALFactory(db);
|
||||
const ldapConfigDAL = ldapConfigDALFactory(db);
|
||||
const ldapGroupMapDAL = ldapGroupMapDALFactory(db);
|
||||
|
||||
const accessApprovalPolicyDAL = accessApprovalPolicyDALFactory(db);
|
||||
const accessApprovalRequestDAL = accessApprovalRequestDALFactory(db);
|
||||
const accessApprovalPolicyApproverDAL = accessApprovalPolicyApproverDALFactory(db);
|
||||
const accessApprovalRequestReviewerDAL = accessApprovalRequestReviewerDALFactory(db);
|
||||
|
||||
const groupProjectUserAdditionalPrivilegeDAL = groupProjectUserAdditionalPrivilegeDALFactory(db);
|
||||
|
||||
const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db);
|
||||
const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db);
|
||||
const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db);
|
||||
@@ -215,10 +233,10 @@ export const registerRoutes = async (
|
||||
|
||||
const gitAppInstallSessionDAL = gitAppInstallSessionDALFactory(db);
|
||||
const gitAppOrgDAL = gitAppDALFactory(db);
|
||||
const groupDAL = groupDALFactory(db);
|
||||
const userGroupMembershipDAL = userGroupMembershipDALFactory(db);
|
||||
const groupDAL = groupDALFactory(db, userGroupMembershipDAL);
|
||||
const groupProjectDAL = groupProjectDALFactory(db);
|
||||
const groupProjectMembershipRoleDAL = groupProjectMembershipRoleDALFactory(db);
|
||||
const userGroupMembershipDAL = userGroupMembershipDALFactory(db);
|
||||
const secretScanningDAL = secretScanningDALFactory(db);
|
||||
const licenseDAL = licenseDALFactory(db);
|
||||
const dynamicSecretDAL = dynamicSecretDALFactory(db);
|
||||
@@ -243,16 +261,24 @@ export const registerRoutes = async (
|
||||
auditLogDAL,
|
||||
queueService,
|
||||
projectDAL,
|
||||
licenseService
|
||||
licenseService,
|
||||
auditLogStreamDAL
|
||||
});
|
||||
const auditLogService = auditLogServiceFactory({ auditLogDAL, permissionService, auditLogQueue });
|
||||
const auditLogStreamService = auditLogStreamServiceFactory({
|
||||
licenseService,
|
||||
permissionService,
|
||||
auditLogStreamDAL
|
||||
});
|
||||
const sapService = secretApprovalPolicyServiceFactory({
|
||||
projectMembershipDAL,
|
||||
projectEnvDAL,
|
||||
secretApprovalPolicyApproverDAL: sapApproverDAL,
|
||||
userDAL,
|
||||
permissionService,
|
||||
secretApprovalPolicyDAL
|
||||
});
|
||||
|
||||
const samlService = samlConfigServiceFactory({
|
||||
permissionService,
|
||||
orgBotDAL,
|
||||
@@ -266,10 +292,13 @@ export const registerRoutes = async (
|
||||
groupDAL,
|
||||
groupProjectDAL,
|
||||
orgDAL,
|
||||
secretApprovalPolicyDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectDAL,
|
||||
projectBotDAL,
|
||||
projectKeyDAL,
|
||||
secretApprovalRequestDAL,
|
||||
accessApprovalRequestDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
});
|
||||
@@ -277,7 +306,10 @@ export const registerRoutes = async (
|
||||
groupDAL,
|
||||
groupProjectDAL,
|
||||
groupProjectMembershipRoleDAL,
|
||||
secretApprovalPolicyDAL,
|
||||
secretApprovalRequestDAL,
|
||||
userGroupMembershipDAL,
|
||||
accessApprovalRequestDAL,
|
||||
projectDAL,
|
||||
projectKeyDAL,
|
||||
projectBotDAL,
|
||||
@@ -292,7 +324,10 @@ export const registerRoutes = async (
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
groupDAL,
|
||||
secretApprovalPolicyDAL,
|
||||
groupProjectDAL,
|
||||
secretApprovalRequestDAL,
|
||||
accessApprovalRequestDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectKeyDAL,
|
||||
projectBotDAL,
|
||||
@@ -305,7 +340,10 @@ export const registerRoutes = async (
|
||||
ldapGroupMapDAL,
|
||||
orgDAL,
|
||||
orgBotDAL,
|
||||
secretApprovalPolicyDAL,
|
||||
groupDAL,
|
||||
secretApprovalRequestDAL,
|
||||
accessApprovalRequestDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
@@ -397,6 +435,7 @@ export const registerRoutes = async (
|
||||
projectUserMembershipRoleDAL,
|
||||
projectDAL,
|
||||
permissionService,
|
||||
groupProjectDAL,
|
||||
projectBotDAL,
|
||||
orgDAL,
|
||||
userDAL,
|
||||
@@ -571,6 +610,31 @@ export const registerRoutes = async (
|
||||
secretVersionTagDAL,
|
||||
secretQueueService
|
||||
});
|
||||
|
||||
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
|
||||
accessApprovalPolicyDAL,
|
||||
accessApprovalPolicyApproverDAL,
|
||||
permissionService,
|
||||
projectEnvDAL,
|
||||
userDAL,
|
||||
projectDAL
|
||||
});
|
||||
|
||||
const accessApprovalRequestService = accessApprovalRequestServiceFactory({
|
||||
projectDAL,
|
||||
permissionService,
|
||||
accessApprovalRequestReviewerDAL,
|
||||
additionalPrivilegeDAL: projectUserAdditionalPrivilegeDAL,
|
||||
groupAdditionalPrivilegeDAL: groupProjectUserAdditionalPrivilegeDAL,
|
||||
projectMembershipDAL,
|
||||
accessApprovalPolicyDAL,
|
||||
accessApprovalRequestDAL,
|
||||
projectEnvDAL,
|
||||
userDAL,
|
||||
smtpService,
|
||||
accessApprovalPolicyApproverDAL
|
||||
});
|
||||
|
||||
const secretRotationQueue = secretRotationQueueFactory({
|
||||
telemetryService,
|
||||
secretRotationDAL,
|
||||
@@ -707,6 +771,8 @@ export const registerRoutes = async (
|
||||
identityProject: identityProjectService,
|
||||
identityUa: identityUaService,
|
||||
secretApprovalPolicy: sapService,
|
||||
accessApprovalPolicy: accessApprovalPolicyService,
|
||||
accessApprovalRequest: accessApprovalRequestService,
|
||||
secretApprovalRequest: sarService,
|
||||
secretRotation: secretRotationService,
|
||||
dynamicSecret: dynamicSecretService,
|
||||
@@ -715,6 +781,7 @@ export const registerRoutes = async (
|
||||
saml: samlService,
|
||||
ldap: ldapService,
|
||||
auditLog: auditLogService,
|
||||
auditLogStream: auditLogStreamService,
|
||||
secretScanning: secretScanningService,
|
||||
license: licenseService,
|
||||
trustedIp: trustedIpService,
|
||||
|
@@ -69,3 +69,10 @@ export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
|
||||
keyEncoding: true,
|
||||
algorithm: true
|
||||
});
|
||||
|
||||
export const SanitizedAuditLogStreamSchema = z.object({
|
||||
id: z.string(),
|
||||
url: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
@@ -68,9 +68,16 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
includeGroupMembers: z
|
||||
.enum(["true", "false"])
|
||||
.default("false")
|
||||
.transform((value) => value === "true")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
users: ProjectMembershipsSchema.extend({
|
||||
isGroupMember: z.boolean(),
|
||||
user: UsersSchema.pick({
|
||||
email: true,
|
||||
username: true,
|
||||
@@ -104,6 +111,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
includeGroupMembers: req.query.includeGroupMembers,
|
||||
projectId: req.params.workspaceId,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
@@ -76,6 +76,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
organization: z.string(),
|
||||
environments: z
|
||||
.object({
|
||||
|
@@ -915,7 +915,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
event: {
|
||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||
metadata: {
|
||||
committedBy: approval.committerId,
|
||||
committedByUser: approval.committerUserId,
|
||||
secretApprovalRequestId: approval.id,
|
||||
secretApprovalRequestSlug: approval.slug
|
||||
}
|
||||
@@ -1099,7 +1099,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
event: {
|
||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||
metadata: {
|
||||
committedBy: approval.committerId,
|
||||
committedByUser: approval.committerUserId,
|
||||
secretApprovalRequestId: approval.id,
|
||||
secretApprovalRequestSlug: approval.slug
|
||||
}
|
||||
@@ -1230,14 +1230,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: req.body.workspaceId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||
metadata: {
|
||||
committedBy: approval.committerId,
|
||||
committedByUser: approval.committerUserId,
|
||||
secretApprovalRequestId: approval.id,
|
||||
secretApprovalRequestSlug: approval.slug
|
||||
}
|
||||
@@ -1363,7 +1362,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
event: {
|
||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||
metadata: {
|
||||
committedBy: approval.committerId,
|
||||
committedByUser: approval.committerUserId,
|
||||
secretApprovalRequestId: approval.id,
|
||||
secretApprovalRequestSlug: approval.slug
|
||||
}
|
||||
@@ -1490,7 +1489,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
event: {
|
||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||
metadata: {
|
||||
committedBy: approval.committerId,
|
||||
committedByUser: approval.committerUserId,
|
||||
secretApprovalRequestId: approval.id,
|
||||
secretApprovalRequestSlug: approval.slug
|
||||
}
|
||||
@@ -1604,7 +1603,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
event: {
|
||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||
metadata: {
|
||||
committedBy: approval.committerId,
|
||||
committedByUser: approval.committerUserId,
|
||||
secretApprovalRequestId: approval.id,
|
||||
secretApprovalRequestSlug: approval.slug
|
||||
}
|
||||
|
@@ -82,7 +82,7 @@ export const authSignupServiceFactory = ({
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.EmailVerification,
|
||||
subjectLine: "Infisical confirmation code",
|
||||
recipients: [email],
|
||||
recipients: [user.email as string],
|
||||
substitutions: {
|
||||
code: token
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, sqlNestRelationships } from "@app/lib/knex";
|
||||
|
||||
@@ -10,6 +10,100 @@ export type TGroupProjectDALFactory = ReturnType<typeof groupProjectDALFactory>;
|
||||
export const groupProjectDALFactory = (db: TDbClient) => {
|
||||
const groupProjectOrm = ormify(db, TableName.GroupProjectMembership);
|
||||
|
||||
// The GroupProjectMembership table has a reference to the project (projectId) AND the group (groupId).
|
||||
// We need to join the GroupProjectMembership table with the Groups table to get the group name and slug.
|
||||
// We also need to join the GroupProjectMembershipRole table to get the role of the group in the project.
|
||||
const findAllProjectGroupMembers = async (projectId: string) => {
|
||||
const docs = await db(TableName.UserGroupMembership)
|
||||
// Join the GroupProjectMembership table with the Groups table to get the group name and slug.
|
||||
.join(
|
||||
TableName.GroupProjectMembership,
|
||||
`${TableName.UserGroupMembership}.groupId`,
|
||||
`${TableName.GroupProjectMembership}.groupId` // this gives us access to the project id in the group membership
|
||||
)
|
||||
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
|
||||
|
||||
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
||||
.join<TUserEncryptionKeys>(
|
||||
TableName.UserEncryptionKey,
|
||||
`${TableName.UserEncryptionKey}.userId`,
|
||||
`${TableName.Users}.id`
|
||||
)
|
||||
.join(
|
||||
TableName.GroupProjectMembershipRole,
|
||||
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
|
||||
`${TableName.GroupProjectMembership}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.ProjectRoles,
|
||||
`${TableName.GroupProjectMembershipRole}.customRoleId`,
|
||||
`${TableName.ProjectRoles}.id`
|
||||
)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.GroupProjectMembership),
|
||||
db.ref("isGhost").withSchema(TableName.Users),
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("email").withSchema(TableName.Users),
|
||||
db.ref("publicKey").withSchema(TableName.UserEncryptionKey),
|
||||
db.ref("firstName").withSchema(TableName.Users),
|
||||
db.ref("lastName").withSchema(TableName.Users),
|
||||
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||
db.ref("role").withSchema(TableName.GroupProjectMembershipRole),
|
||||
db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("membershipRoleId"),
|
||||
db.ref("customRoleId").withSchema(TableName.GroupProjectMembershipRole),
|
||||
db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"),
|
||||
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
||||
db.ref("temporaryMode").withSchema(TableName.GroupProjectMembershipRole),
|
||||
db.ref("isTemporary").withSchema(TableName.GroupProjectMembershipRole),
|
||||
db.ref("temporaryRange").withSchema(TableName.GroupProjectMembershipRole),
|
||||
db.ref("temporaryAccessStartTime").withSchema(TableName.GroupProjectMembershipRole),
|
||||
db.ref("temporaryAccessEndTime").withSchema(TableName.GroupProjectMembershipRole)
|
||||
)
|
||||
.where({ isGhost: false });
|
||||
|
||||
const members = sqlNestRelationships({
|
||||
data: docs,
|
||||
parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, userId }) => ({
|
||||
isGroupMember: true,
|
||||
id,
|
||||
userId,
|
||||
projectId,
|
||||
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost }
|
||||
}),
|
||||
key: "id",
|
||||
childrenMapper: [
|
||||
{
|
||||
label: "roles" as const,
|
||||
key: "membershipRoleId",
|
||||
mapper: ({
|
||||
role,
|
||||
customRoleId,
|
||||
customRoleName,
|
||||
customRoleSlug,
|
||||
membershipRoleId,
|
||||
temporaryRange,
|
||||
temporaryMode,
|
||||
temporaryAccessEndTime,
|
||||
temporaryAccessStartTime,
|
||||
isTemporary
|
||||
}) => ({
|
||||
id: membershipRoleId,
|
||||
role,
|
||||
customRoleId,
|
||||
customRoleName,
|
||||
customRoleSlug,
|
||||
temporaryRange,
|
||||
temporaryMode,
|
||||
temporaryAccessEndTime,
|
||||
temporaryAccessStartTime,
|
||||
isTemporary
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
return members;
|
||||
};
|
||||
|
||||
const findByProjectId = async (projectId: string, tx?: Knex) => {
|
||||
try {
|
||||
const docs = await (tx || db)(TableName.GroupProjectMembership)
|
||||
@@ -95,5 +189,5 @@ export const groupProjectDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
return { ...groupProjectOrm, findByProjectId };
|
||||
return { ...groupProjectOrm, findByProjectId, findAllProjectGroupMembers };
|
||||
};
|
||||
|
@@ -2,8 +2,11 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import ms from "ms";
|
||||
|
||||
import { ProjectMembershipRole, SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { TAccessApprovalRequestDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-dal";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { TSecretApprovalPolicyDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-dal";
|
||||
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
|
||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
@@ -39,6 +42,9 @@ type TGroupProjectServiceFactoryDep = {
|
||||
projectBotDAL: TProjectBotDALFactory;
|
||||
groupDAL: Pick<TGroupDALFactory, "findOne">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissionByRole">;
|
||||
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "delete">;
|
||||
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findByProjectIds">;
|
||||
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "delete">;
|
||||
};
|
||||
|
||||
export type TGroupProjectServiceFactory = ReturnType<typeof groupProjectServiceFactory>;
|
||||
@@ -48,6 +54,9 @@ export const groupProjectServiceFactory = ({
|
||||
groupProjectDAL,
|
||||
groupProjectMembershipRoleDAL,
|
||||
userGroupMembershipDAL,
|
||||
secretApprovalRequestDAL,
|
||||
secretApprovalPolicyDAL,
|
||||
accessApprovalRequestDAL,
|
||||
projectDAL,
|
||||
projectKeyDAL,
|
||||
projectBotDAL,
|
||||
@@ -277,7 +286,8 @@ export const groupProjectServiceFactory = ({
|
||||
if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
|
||||
|
||||
const groupProjectMembership = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
|
||||
if (!groupProjectMembership) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
|
||||
if (!groupProjectMembership.id)
|
||||
throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@@ -289,8 +299,34 @@ export const groupProjectServiceFactory = ({
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Groups);
|
||||
|
||||
const deletedProjectGroup = await groupProjectDAL.transaction(async (tx) => {
|
||||
// This is group members that do not have individual access to the project (A.K.A members that don't have a normal project membership)
|
||||
const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, project.id, tx);
|
||||
|
||||
// Delete all access approvals by the group members
|
||||
|
||||
await accessApprovalRequestDAL.delete(
|
||||
{
|
||||
groupMembershipId: groupProjectMembership.id,
|
||||
$in: {
|
||||
requestedByUserId: groupMembers.map((member) => member.user.id)
|
||||
}
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const secretApprovalPolicies = await secretApprovalPolicyDAL.findByProjectIds([project.id], tx);
|
||||
|
||||
// Delete any secret approvals by the group members
|
||||
await secretApprovalRequestDAL.delete(
|
||||
{
|
||||
$in: {
|
||||
policyId: secretApprovalPolicies.map((policy) => policy.id),
|
||||
committerUserId: groupMembers.map((member) => member.user.id)
|
||||
}
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (groupMembers.length) {
|
||||
await projectKeyDAL.delete(
|
||||
{
|
||||
|
@@ -1,12 +1,74 @@
|
||||
import { Knex } from "knex";
|
||||
import { Tables } from "knex/types/tables";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||
import { ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
|
||||
|
||||
export type TProjectMembershipDALFactory = ReturnType<typeof projectMembershipDALFactory>;
|
||||
|
||||
export const projectMembershipDALFactory = (db: TDbClient) => {
|
||||
const projectMemberOrm = ormify(db, TableName.ProjectMembership);
|
||||
const projectMembershipOrm = ormify(db, TableName.ProjectMembership);
|
||||
const accessApprovalRequestOrm = ormify(db, TableName.AccessApprovalRequest);
|
||||
const secretApprovalRequestOrm = ormify(db, TableName.SecretApprovalRequest);
|
||||
|
||||
const deleteMany = async (filter: TFindFilter<Tables[TableName.ProjectMembership]["base"]>, tx?: Knex) => {
|
||||
const handleDeletion = async (processedTx: Knex) => {
|
||||
// Find all memberships
|
||||
const memberships = await projectMembershipOrm.find(filter, {
|
||||
tx: processedTx
|
||||
});
|
||||
|
||||
// Delete all access approvals in this project from the users attached to these memberships
|
||||
await accessApprovalRequestOrm.delete(
|
||||
{
|
||||
$in: {
|
||||
projectMembershipId: memberships.map((membership) => membership.id)
|
||||
}
|
||||
},
|
||||
processedTx
|
||||
);
|
||||
|
||||
for await (const membership of memberships) {
|
||||
const allPoliciesInProject = await (tx || db)(TableName.SecretApprovalRequest)
|
||||
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
|
||||
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||
.join(
|
||||
TableName.SecretApprovalPolicy,
|
||||
`${TableName.SecretApprovalRequest}.policyId`,
|
||||
`${TableName.SecretApprovalPolicy}.id`
|
||||
)
|
||||
.where({ [`${TableName.Environment}.projectId` as "projectId"]: membership.projectId })
|
||||
.where({ [`${TableName.SecretApprovalRequest}.committerUserId` as "committerUserId"]: membership.userId })
|
||||
.select(db.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"));
|
||||
|
||||
await secretApprovalRequestOrm.delete(
|
||||
{
|
||||
$in: {
|
||||
policyId: allPoliciesInProject.map((policy) => policy.policyId)
|
||||
},
|
||||
committerUserId: membership.userId
|
||||
},
|
||||
processedTx
|
||||
);
|
||||
// Delete the actual project memberships
|
||||
await projectMembershipOrm.delete(
|
||||
{
|
||||
id: membership.id
|
||||
},
|
||||
processedTx
|
||||
);
|
||||
}
|
||||
|
||||
return memberships;
|
||||
};
|
||||
|
||||
if (tx) {
|
||||
return handleDeletion(tx);
|
||||
}
|
||||
return db.transaction(handleDeletion);
|
||||
};
|
||||
|
||||
// special query
|
||||
const findAllProjectMembers = async (projectId: string) => {
|
||||
@@ -54,6 +116,7 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
||||
const members = sqlNestRelationships({
|
||||
data: docs,
|
||||
parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, userId }) => ({
|
||||
isGroupMember: false,
|
||||
id,
|
||||
userId,
|
||||
projectId,
|
||||
@@ -152,8 +215,9 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
||||
};
|
||||
|
||||
return {
|
||||
...projectMemberOrm,
|
||||
...projectMembershipOrm,
|
||||
findAllProjectMembers,
|
||||
delete: deleteMany,
|
||||
findProjectGhostUser,
|
||||
findMembershipsByUsername,
|
||||
findProjectMembershipsByUserId
|
||||
|
@@ -19,6 +19,7 @@ import { groupBy } from "@app/lib/fn";
|
||||
|
||||
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
||||
@@ -52,6 +53,7 @@ type TProjectMembershipServiceFactoryDep = {
|
||||
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
groupProjectDAL: TGroupProjectDALFactory;
|
||||
};
|
||||
|
||||
export type TProjectMembershipServiceFactory = ReturnType<typeof projectMembershipServiceFactory>;
|
||||
@@ -61,6 +63,7 @@ export const projectMembershipServiceFactory = ({
|
||||
projectMembershipDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
smtpService,
|
||||
groupProjectDAL,
|
||||
projectRoleDAL,
|
||||
projectBotDAL,
|
||||
orgDAL,
|
||||
@@ -74,6 +77,7 @@ export const projectMembershipServiceFactory = ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
includeGroupMembers,
|
||||
actorAuthMethod,
|
||||
projectId
|
||||
}: TGetProjectMembershipDTO) => {
|
||||
@@ -86,7 +90,20 @@ export const projectMembershipServiceFactory = ({
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||
|
||||
return projectMembershipDAL.findAllProjectMembers(projectId);
|
||||
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
|
||||
|
||||
if (includeGroupMembers) {
|
||||
const groupMembers = await groupProjectDAL.findAllProjectGroupMembers(projectId);
|
||||
const allMembers = [...projectMembers, ...groupMembers];
|
||||
|
||||
// Ensure the userId is unique
|
||||
const membersIds = new Set(allMembers.map((entity) => entity.user.id));
|
||||
const uniqueMembers = allMembers.filter((entity) => membersIds.has(entity.user.id));
|
||||
|
||||
return uniqueMembers;
|
||||
}
|
||||
|
||||
return projectMembers;
|
||||
};
|
||||
|
||||
const addUsersToProject = async ({
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TGetProjectMembershipDTO = TProjectPermission;
|
||||
export type TGetProjectMembershipDTO = {
|
||||
includeGroupMembers?: boolean;
|
||||
} & TProjectPermission;
|
||||
export enum ProjectUserMembershipTemporaryMode {
|
||||
Relative = "relative"
|
||||
}
|
||||
|
@@ -20,6 +20,7 @@ export enum SmtpTemplates {
|
||||
EmailVerification = "emailVerification.handlebars",
|
||||
SecretReminder = "secretReminder.handlebars",
|
||||
EmailMfa = "emailMfa.handlebars",
|
||||
AccessApprovalRequest = "accessApprovalRequest.handlebars",
|
||||
HistoricalSecretList = "historicalSecretLeakIncident.handlebars",
|
||||
NewDeviceJoin = "newDevice.handlebars",
|
||||
OrgInvite = "organizationInvitation.handlebars",
|
||||
|
@@ -0,0 +1,50 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Access Approval Request</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Infisical</h2>
|
||||
<h2>New access approval request pending your review</h2>
|
||||
<p>You have a new access approval request pending review in project "{{projectName}}".</p>
|
||||
|
||||
<p>
|
||||
{{requesterFullName}}
|
||||
({{requesterEmail}}) has requested
|
||||
{{#if isTemporary}}
|
||||
temporary
|
||||
{{else}}
|
||||
permanent
|
||||
{{/if}}
|
||||
access to
|
||||
{{secretPath}}
|
||||
in the
|
||||
{{environment}}
|
||||
environment.
|
||||
|
||||
{{#if isTemporary}}
|
||||
<br />
|
||||
This access will expire
|
||||
{{expiresIn}}
|
||||
after it has been approved.
|
||||
{{/if}}
|
||||
</p>
|
||||
<p>
|
||||
The following permissions are requested:
|
||||
<ul>
|
||||
{{#each permissions}}
|
||||
<li>{{this}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
View the request and approve or deny it
|
||||
<a href="{{approvalUrl}}">here</a>.
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
@@ -10,7 +10,7 @@ import {
|
||||
TUserEncryptionKeysUpdate
|
||||
} from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TUserDALFactory = ReturnType<typeof userDALFactory>;
|
||||
|
||||
@@ -63,6 +63,99 @@ export const userDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findUsersByProjectId = async (projectId: string, userIds: string[]) => {
|
||||
try {
|
||||
const projectMembershipQuery = await db(TableName.ProjectMembership)
|
||||
.where({ projectId })
|
||||
.whereIn("userId", userIds)
|
||||
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||
.select(selectAllTableCols(TableName.Users))
|
||||
.select(db.ref("id").withSchema(TableName.ProjectMembership).as("projectMembershipId"));
|
||||
|
||||
const groupMembershipQuery = await db(TableName.UserGroupMembership)
|
||||
.whereIn("userId", userIds)
|
||||
.join(
|
||||
TableName.GroupProjectMembership,
|
||||
`${TableName.UserGroupMembership}.groupId`,
|
||||
`${TableName.GroupProjectMembership}.groupId` // this gives us access to the project id in the group membership
|
||||
)
|
||||
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
|
||||
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
||||
.select(selectAllTableCols(TableName.Users))
|
||||
.select(db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupProjectMembershipId"));
|
||||
|
||||
const projectMembershipUsers = projectMembershipQuery.map((user) => ({
|
||||
...user,
|
||||
projectMembershipId: user.projectMembershipId,
|
||||
userGroupMembershipId: null
|
||||
}));
|
||||
|
||||
const groupMembershipUsers = groupMembershipQuery.map((user) => ({
|
||||
...user,
|
||||
projectMembershipId: null,
|
||||
groupProjectMembershipId: user.groupProjectMembershipId
|
||||
}));
|
||||
|
||||
// return [...projectMembershipUsers, ...groupMembershipUsers];
|
||||
|
||||
// There may be duplicates in the results since a user can have both a project membership, and access through a group, so we need to filter out potential duplicates.
|
||||
// We should prioritize project memberships over group memberships.
|
||||
const memberships = [...projectMembershipUsers, ...groupMembershipUsers];
|
||||
|
||||
const uniqueMemberships = memberships.filter((user, index) => {
|
||||
const firstIndex = memberships.findIndex((u) => u.id === user.id);
|
||||
return firstIndex === index;
|
||||
});
|
||||
|
||||
return uniqueMemberships;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find users by project id" });
|
||||
}
|
||||
};
|
||||
|
||||
// if its a group membership, it should have a isGroupMembership flag
|
||||
const findUserByProjectId = async (projectId: string, userId: string) => {
|
||||
try {
|
||||
const projectMembership = await db(TableName.ProjectMembership)
|
||||
.where({ projectId, userId })
|
||||
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||
.select(selectAllTableCols(TableName.Users))
|
||||
.select(db.ref("id").withSchema(TableName.ProjectMembership).as("projectMembershipId"))
|
||||
.first();
|
||||
|
||||
const groupProjectMembership = await db(TableName.UserGroupMembership)
|
||||
.where({ userId })
|
||||
.join(
|
||||
TableName.GroupProjectMembership,
|
||||
`${TableName.UserGroupMembership}.groupId`,
|
||||
`${TableName.GroupProjectMembership}.groupId` // this gives us access to the project id in the group membership
|
||||
)
|
||||
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
|
||||
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
||||
.select(selectAllTableCols(TableName.Users))
|
||||
.select(db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupProjectMembershipId"))
|
||||
.first();
|
||||
|
||||
if (projectMembership) {
|
||||
return {
|
||||
...projectMembership,
|
||||
projectMembershipId: projectMembership.projectMembershipId,
|
||||
groupProjectMembershipId: null
|
||||
};
|
||||
}
|
||||
|
||||
if (groupProjectMembership) {
|
||||
return {
|
||||
...groupProjectMembership,
|
||||
projectMembershipId: null,
|
||||
groupProjectMembershipId: groupProjectMembership.groupProjectMembershipId
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find user by project id" });
|
||||
}
|
||||
};
|
||||
|
||||
const findUserByProjectMembershipId = async (projectMembershipId: string) => {
|
||||
try {
|
||||
return await db(TableName.ProjectMembership)
|
||||
@@ -74,6 +167,17 @@ export const userDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findUsersByProjectMembershipIds = async (projectMembershipIds: string[]) => {
|
||||
try {
|
||||
return await db(TableName.ProjectMembership)
|
||||
.whereIn(`${TableName.ProjectMembership}.id`, projectMembershipIds)
|
||||
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||
.select("*");
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find users by project membership ids" });
|
||||
}
|
||||
};
|
||||
|
||||
const createUserEncryption = async (data: TUserEncryptionKeysInsert, tx?: Knex) => {
|
||||
try {
|
||||
const [userEnc] = await (tx || db)(TableName.UserEncryptionKey).insert(data).returning("*");
|
||||
@@ -135,11 +239,14 @@ export const userDALFactory = (db: TDbClient) => {
|
||||
return {
|
||||
...userOrm,
|
||||
findUserByUsername,
|
||||
findUsersByProjectId,
|
||||
findUserByProjectId,
|
||||
findUserEncKeyByUsername,
|
||||
findUserEncKeyByUserIdsBatch,
|
||||
findUserEncKeyByUserId,
|
||||
updateUserEncryptionByUserId,
|
||||
findUserByProjectMembershipId,
|
||||
findUsersByProjectMembershipIds,
|
||||
upsertUserEncryptionKey,
|
||||
createUserEncryption,
|
||||
findOneUserAction,
|
||||
|
@@ -24,16 +24,16 @@ resolvers hostdns
|
||||
timeout retry 1s
|
||||
hold valid 5s
|
||||
|
||||
frontend master
|
||||
frontend postgres_master
|
||||
bind *:5433
|
||||
default_backend master_backend
|
||||
default_backend postgres_master_backend
|
||||
|
||||
frontend replicas
|
||||
frontend postgres_replicas
|
||||
bind *:5434
|
||||
default_backend replica_backend
|
||||
default_backend postgres_replica_backend
|
||||
|
||||
|
||||
backend master_backend
|
||||
backend postgres_master_backend
|
||||
option httpchk GET /master
|
||||
http-check expect status 200
|
||||
default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
|
||||
@@ -41,7 +41,7 @@ backend master_backend
|
||||
server postgres-2 postgres-2:5432 check port 8008 resolvers hostdns
|
||||
server postgres-3 postgres-3:5432 check port 8008 resolvers hostdns
|
||||
|
||||
backend replica_backend
|
||||
backend postgres_replica_backend
|
||||
option httpchk GET /replica
|
||||
http-check expect status 200
|
||||
default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
|
||||
@@ -50,11 +50,11 @@ backend replica_backend
|
||||
server postgres-3 postgres-3:5432 check port 8008 resolvers hostdns
|
||||
|
||||
|
||||
frontend redis_frontend
|
||||
frontend redis_master_frontend
|
||||
bind *:6379
|
||||
default_backend redis_backend
|
||||
default_backend redis_master_backend
|
||||
|
||||
backend redis_backend
|
||||
backend redis_master_backend
|
||||
option tcp-check
|
||||
tcp-check send AUTH\ 123456\r\n
|
||||
tcp-check expect string +OK
|
||||
|
@@ -5,8 +5,8 @@ services:
|
||||
image: haproxy:latest
|
||||
ports:
|
||||
- '7001:7000'
|
||||
- '5002:5433'
|
||||
- '5003:5434'
|
||||
- '5002:5433' # Postgres master
|
||||
- '5003:5434' # Postgres read
|
||||
- '6379:6379'
|
||||
- '8080:8080'
|
||||
networks:
|
||||
@@ -15,22 +15,18 @@ services:
|
||||
- source: haproxy-config
|
||||
target: /usr/local/etc/haproxy/haproxy.cfg
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node1
|
||||
mode: global
|
||||
|
||||
infisical:
|
||||
container_name: infisical-backend
|
||||
image: infisical/infisical:latest-postgres
|
||||
image: infisical/infisical:v0.60.1-postgres
|
||||
env_file: .env
|
||||
ports:
|
||||
- 80:8080
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
networks:
|
||||
- infisical
|
||||
secrets:
|
||||
- env_file
|
||||
deploy:
|
||||
replicas: 5
|
||||
|
||||
etcd1:
|
||||
image: ghcr.io/zalando/spilo-16:3.2-p2
|
||||
|
82
docs/documentation/platform/audit-log-streams.mdx
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: "Audit Log Streams"
|
||||
description: "Learn how to stream Infisical Audit Logs to external logging providers."
|
||||
---
|
||||
|
||||
<Info>
|
||||
Audit log streams is a paid feature.
|
||||
|
||||
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
|
||||
then you should contact team@infisical.com to purchase an enterprise license to use it.
|
||||
</Info>
|
||||
|
||||
Infisical Audit Log Streaming enables you to transmit your organization's Audit Logs to external logging providers for monitoring and analysis.
|
||||
|
||||
The logs are formatted in JSON, requiring your logging provider to support JSON-based log parsing.
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to Organization Settings in your sidebar." />
|
||||
<Step title="Select Audit Log Streams Tab.">
|
||||

|
||||
</Step>
|
||||
<Step title="Click on Create">
|
||||

|
||||
|
||||
Provide the following values
|
||||
<ParamField path="Endpoint URL" type="string" required>
|
||||
The HTTPS endpoint URL of the logging provider that collects the JSON stream.
|
||||
</ParamField>
|
||||
<ParamField path="Headers" type="string" >
|
||||
The HTTP headers for the logging provider for identification and authentication.
|
||||
</ParamField>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||

|
||||
Your Audit Logs are now ready to be streamed.
|
||||
|
||||
## Example Providers
|
||||
|
||||
### Better Stack
|
||||
|
||||
<Steps>
|
||||
<Step title="Select Connect Source">
|
||||

|
||||
</Step>
|
||||
<Step title="Provide a name and select platform"/>
|
||||
<Step title="Provide Audit Log Stream inputs">
|
||||

|
||||
|
||||
1. Copy the **endpoint** from Better Stack to the **Endpoint URL** field.
|
||||
3. Create a new header with key **Authorization** and set the value as **Bearer \<source token from betterstack\>**.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Datadog
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to API Keys section">
|
||||

|
||||
</Step>
|
||||
<Step title="Select New Key and provide a key name">
|
||||

|
||||

|
||||
</Step>
|
||||
<Step title="Find your Datadog region specific logging endpoint.">
|
||||

|
||||
|
||||
1. Navigate to the [Datadog Send Logs API documentation](https://docs.datadoghq.com/api/latest/logs/?code-lang=curl&site=us5#send-logs).
|
||||
2. Pick your Datadog account region.
|
||||
3. Obtain your Datadog logging endpoint URL.
|
||||
</Step>
|
||||
<Step title="Provide audit log stream inputs">
|
||||

|
||||
|
||||
1. Copy the **logging endpoint** from Datadog to the **Endpoint URL** field.
|
||||
2. Copy the **API Key** from previous step
|
||||
3. Create a new header with key **DD-API-KEY** and set the value as **API Key**.
|
||||
</Step>
|
||||
</Steps>
|
After Width: | Height: | Size: 126 KiB |
After Width: | Height: | Size: 257 KiB |
BIN
docs/images/platform/audit-log-streams/data-create-api-key.png
Normal file
After Width: | Height: | Size: 90 KiB |
BIN
docs/images/platform/audit-log-streams/data-dog-api-key.png
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
docs/images/platform/audit-log-streams/datadog-api-sidebar.png
Normal file
After Width: | Height: | Size: 119 KiB |
After Width: | Height: | Size: 74 KiB |
After Width: | Height: | Size: 38 KiB |
BIN
docs/images/platform/audit-log-streams/stream-create.png
Normal file
After Width: | Height: | Size: 361 KiB |
BIN
docs/images/platform/audit-log-streams/stream-inputs.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
docs/images/platform/audit-log-streams/stream-list.png
Normal file
After Width: | Height: | Size: 112 KiB |
After Width: | Height: | Size: 722 KiB |
@@ -143,7 +143,8 @@
|
||||
"documentation/platform/dynamic-secrets/aws-iam"
|
||||
]
|
||||
},
|
||||
"documentation/platform/groups"
|
||||
"documentation/platform/groups",
|
||||
"documentation/platform/audit-log-streams"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -190,7 +191,6 @@
|
||||
"group": "Self-host Infisical",
|
||||
"pages": [
|
||||
"self-hosting/overview",
|
||||
"self-hosting/configuration/requirements",
|
||||
{
|
||||
"group": "Installation methods",
|
||||
"pages": [
|
||||
@@ -201,6 +201,7 @@
|
||||
]
|
||||
},
|
||||
"self-hosting/configuration/envars",
|
||||
"self-hosting/configuration/requirements",
|
||||
{
|
||||
"group": "Guides",
|
||||
"pages": [
|
||||
@@ -479,6 +480,9 @@
|
||||
"api-reference/endpoints/secrets/read",
|
||||
"api-reference/endpoints/secrets/update",
|
||||
"api-reference/endpoints/secrets/delete",
|
||||
"api-reference/endpoints/secrets/create-many",
|
||||
"api-reference/endpoints/secrets/update-many",
|
||||
"api-reference/endpoints/secrets/delete-many",
|
||||
"api-reference/endpoints/secrets/attach-tags",
|
||||
"api-reference/endpoints/secrets/detach-tags"
|
||||
]
|
||||
|
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "Requirements"
|
||||
title: "Hardware requirements"
|
||||
description: "Find out the minimal requirements for operating Infisical."
|
||||
---
|
||||
|
||||
|
@@ -63,426 +63,9 @@ For the sake of simplicity, the example in this guide only contains one manager
|
||||
It's important to note that while the cluster can tolerate the failure of one node in a three-node setup, it's recommended to have a minimum of three nodes to ensure high availability.
|
||||
With two nodes, the failure of a single node can result in a loss of quorum and potential downtime.
|
||||
|
||||
## Docker Deployment Stack
|
||||
## Docker Deployment Stack Overview
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Docker Swarm stack">
|
||||
```yaml infisical-stack.yaml
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
haproxy:
|
||||
image: haproxy:latest
|
||||
ports:
|
||||
- '7001:7000'
|
||||
- '5002:5433'
|
||||
- '5003:5434'
|
||||
- '6379:6379'
|
||||
- '8080:8080'
|
||||
networks:
|
||||
- infisical
|
||||
configs:
|
||||
- source: haproxy-config
|
||||
target: /usr/local/etc/haproxy/haproxy.cfg
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node1
|
||||
|
||||
infisical:
|
||||
container_name: infisical-backend
|
||||
image: infisical/infisical:v0.60.0-postgres
|
||||
env_file: .env
|
||||
ports:
|
||||
- 80:8080
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
networks:
|
||||
- infisical
|
||||
secrets:
|
||||
- env_file
|
||||
|
||||
etcd1:
|
||||
image: ghcr.io/zalando/spilo-16:3.2-p2
|
||||
networks:
|
||||
- infisical
|
||||
environment:
|
||||
ETCD_UNSUPPORTED_ARCH: arm64
|
||||
container_name: demo-etcd1
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node1
|
||||
hostname: etcd1
|
||||
command: |
|
||||
etcd --name etcd1
|
||||
--listen-client-urls http://0.0.0.0:2379
|
||||
--listen-peer-urls=http://0.0.0.0:2380
|
||||
--advertise-client-urls http://etcd1:2379
|
||||
--initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
|
||||
--initial-advertise-peer-urls=http://etcd1:2380
|
||||
--initial-cluster-state=new
|
||||
|
||||
etcd2:
|
||||
image: ghcr.io/zalando/spilo-16:3.2-p2
|
||||
networks:
|
||||
- infisical
|
||||
environment:
|
||||
ETCD_UNSUPPORTED_ARCH: arm64
|
||||
container_name: demo-etcd2
|
||||
hostname: etcd2
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node2
|
||||
command: |
|
||||
etcd --name etcd2
|
||||
--listen-client-urls http://0.0.0.0:2379
|
||||
--listen-peer-urls=http://0.0.0.0:2380
|
||||
--advertise-client-urls http://etcd2:2379
|
||||
--initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
|
||||
--initial-advertise-peer-urls=http://etcd2:2380
|
||||
--initial-cluster-state=new
|
||||
|
||||
etcd3:
|
||||
image: ghcr.io/zalando/spilo-16:3.2-p2
|
||||
networks:
|
||||
- infisical
|
||||
environment:
|
||||
ETCD_UNSUPPORTED_ARCH: arm64
|
||||
container_name: demo-etcd3
|
||||
hostname: etcd3
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node3
|
||||
command: |
|
||||
etcd --name etcd3
|
||||
--listen-client-urls http://0.0.0.0:2379
|
||||
--listen-peer-urls=http://0.0.0.0:2380
|
||||
--advertise-client-urls http://etcd3:2379
|
||||
--initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
|
||||
--initial-advertise-peer-urls=http://etcd3:2380
|
||||
--initial-cluster-state=new
|
||||
|
||||
spolo1:
|
||||
image: ghcr.io/zalando/spilo-16:3.2-p2
|
||||
container_name: postgres-1
|
||||
networks:
|
||||
- infisical
|
||||
hostname: postgres-1
|
||||
environment:
|
||||
ETCD_HOSTS: etcd1:2379,etcd2:2379,etcd3:2379
|
||||
PGPASSWORD_SUPERUSER: "postgres"
|
||||
PGUSER_SUPERUSER: "postgres"
|
||||
SCOPE: infisical
|
||||
volumes:
|
||||
- postgres_data1:/home/postgres/pgdata
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node1
|
||||
|
||||
spolo2:
|
||||
image: ghcr.io/zalando/spilo-16:3.2-p2
|
||||
container_name: postgres-2
|
||||
networks:
|
||||
- infisical
|
||||
hostname: postgres-2
|
||||
environment:
|
||||
ETCD_HOSTS: etcd1:2379,etcd2:2379,etcd3:2379
|
||||
PGPASSWORD_SUPERUSER: "postgres"
|
||||
PGUSER_SUPERUSER: "postgres"
|
||||
SCOPE: infisical
|
||||
volumes:
|
||||
- postgres_data2:/home/postgres/pgdata
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node2
|
||||
|
||||
spolo3:
|
||||
image: ghcr.io/zalando/spilo-16:3.2-p2
|
||||
container_name: postgres-3
|
||||
networks:
|
||||
- infisical
|
||||
hostname: postgres-3
|
||||
environment:
|
||||
ETCD_HOSTS: etcd1:2379,etcd2:2379,etcd3:2379
|
||||
PGPASSWORD_SUPERUSER: "postgres"
|
||||
PGUSER_SUPERUSER: "postgres"
|
||||
SCOPE: infisical
|
||||
volumes:
|
||||
- postgres_data3:/home/postgres/pgdata
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node3
|
||||
|
||||
|
||||
redis_replica0:
|
||||
image: bitnami/redis:6.2.10
|
||||
environment:
|
||||
- REDIS_REPLICATION_MODE=master
|
||||
- REDIS_PASSWORD=123456
|
||||
networks:
|
||||
- infisical
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node1
|
||||
|
||||
redis_replica1:
|
||||
image: bitnami/redis:6.2.10
|
||||
environment:
|
||||
- REDIS_REPLICATION_MODE=slave
|
||||
- REDIS_MASTER_HOST=redis_replica0
|
||||
- REDIS_MASTER_PORT_NUMBER=6379
|
||||
- REDIS_MASTER_PASSWORD=123456
|
||||
- REDIS_PASSWORD=123456
|
||||
networks:
|
||||
- infisical
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node2
|
||||
|
||||
redis_replica2:
|
||||
image: bitnami/redis:6.2.10
|
||||
environment:
|
||||
- REDIS_REPLICATION_MODE=slave
|
||||
- REDIS_MASTER_HOST=redis_replica0
|
||||
- REDIS_MASTER_PORT_NUMBER=6379
|
||||
- REDIS_MASTER_PASSWORD=123456
|
||||
- REDIS_PASSWORD=123456
|
||||
networks:
|
||||
- infisical
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node3
|
||||
|
||||
redis_sentinel1:
|
||||
image: bitnami/redis-sentinel:6.2.10
|
||||
environment:
|
||||
- REDIS_SENTINEL_QUORUM=2
|
||||
- REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=5000
|
||||
- REDIS_SENTINEL_FAILOVER_TIMEOUT=60000
|
||||
- REDIS_SENTINEL_PORT_NUMBER=26379
|
||||
- REDIS_MASTER_HOST=redis_replica1
|
||||
- REDIS_MASTER_PORT_NUMBER=6379
|
||||
- REDIS_MASTER_PASSWORD=123456
|
||||
networks:
|
||||
- infisical
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node1
|
||||
|
||||
redis_sentinel2:
|
||||
image: bitnami/redis-sentinel:6.2.10
|
||||
environment:
|
||||
- REDIS_SENTINEL_QUORUM=2
|
||||
- REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=5000
|
||||
- REDIS_SENTINEL_FAILOVER_TIMEOUT=60000
|
||||
- REDIS_SENTINEL_PORT_NUMBER=26379
|
||||
- REDIS_MASTER_HOST=redis_replica1
|
||||
- REDIS_MASTER_PORT_NUMBER=6379
|
||||
- REDIS_MASTER_PASSWORD=123456
|
||||
networks:
|
||||
- infisical
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node2
|
||||
|
||||
redis_sentinel3:
|
||||
image: bitnami/redis-sentinel:6.2.10
|
||||
environment:
|
||||
- REDIS_SENTINEL_QUORUM=2
|
||||
- REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=5000
|
||||
- REDIS_SENTINEL_FAILOVER_TIMEOUT=60000
|
||||
- REDIS_SENTINEL_PORT_NUMBER=26379
|
||||
- REDIS_MASTER_HOST=redis_replica1
|
||||
- REDIS_MASTER_PORT_NUMBER=6379
|
||||
- REDIS_MASTER_PASSWORD=123456
|
||||
networks:
|
||||
- infisical
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.name == node3
|
||||
|
||||
networks:
|
||||
infisical:
|
||||
|
||||
|
||||
volumes:
|
||||
postgres_data1:
|
||||
postgres_data2:
|
||||
postgres_data3:
|
||||
postgres_data4:
|
||||
redis0:
|
||||
redis1:
|
||||
redis2:
|
||||
|
||||
configs:
|
||||
haproxy-config:
|
||||
file: ./haproxy.cfg
|
||||
|
||||
secrets:
|
||||
env_file:
|
||||
file: .env
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="HA Proxy config">
|
||||
```text haproxy.cfg
|
||||
global
|
||||
maxconn 10000
|
||||
log stdout format raw local0
|
||||
|
||||
defaults
|
||||
log global
|
||||
mode tcp
|
||||
retries 3
|
||||
timeout client 30m
|
||||
timeout connect 10s
|
||||
timeout server 30m
|
||||
timeout check 5s
|
||||
|
||||
listen stats
|
||||
mode http
|
||||
bind *:7000
|
||||
stats enable
|
||||
stats uri /
|
||||
|
||||
resolvers hostdns
|
||||
nameserver dns 127.0.0.11:53
|
||||
resolve_retries 3
|
||||
timeout resolve 1s
|
||||
timeout retry 1s
|
||||
hold valid 5s
|
||||
|
||||
frontend master
|
||||
bind *:5433
|
||||
default_backend master_backend
|
||||
|
||||
frontend replicas
|
||||
bind *:5434
|
||||
default_backend replica_backend
|
||||
|
||||
|
||||
backend master_backend
|
||||
option httpchk GET /master
|
||||
http-check expect status 200
|
||||
default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
|
||||
server postgres-1 postgres-1:5432 check port 8008 resolvers hostdns
|
||||
server postgres-2 postgres-2:5432 check port 8008 resolvers hostdns
|
||||
server postgres-3 postgres-3:5432 check port 8008 resolvers hostdns
|
||||
|
||||
backend replica_backend
|
||||
option httpchk GET /replica
|
||||
http-check expect status 200
|
||||
default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
|
||||
server postgres-1 postgres-1:5432 check port 8008 resolvers hostdns
|
||||
server postgres-2 postgres-2:5432 check port 8008 resolvers hostdns
|
||||
server postgres-3 postgres-3:5432 check port 8008 resolvers hostdns
|
||||
|
||||
|
||||
frontend redis_frontend
|
||||
bind *:6379
|
||||
default_backend redis_backend
|
||||
|
||||
backend redis_backend
|
||||
option tcp-check
|
||||
tcp-check send AUTH\ 123456\r\n
|
||||
tcp-check expect string +OK
|
||||
tcp-check send PING\r\n
|
||||
tcp-check expect string +PONG
|
||||
tcp-check send info\ replication\r\n
|
||||
tcp-check expect string role:master
|
||||
tcp-check send QUIT\r\n
|
||||
tcp-check expect string +OK
|
||||
server redis_master redis_replica0:6379 check inter 1s
|
||||
server redis_replica1 redis_replica1:6379 check inter 1s
|
||||
server redis_replica2 redis_replica2:6379 check inter 1s
|
||||
|
||||
frontend infisical_frontend
|
||||
bind *:8080
|
||||
default_backend infisical_backend
|
||||
|
||||
backend infisical_backend
|
||||
option httpchk GET /api/status
|
||||
http-check expect status 200
|
||||
server infisical infisical:8080 check inter 1s
|
||||
```
|
||||
</Tab>
|
||||
<Tab title=".example-env">
|
||||
```env .env
|
||||
# Keys
|
||||
# Required key for platform encryption/decryption ops
|
||||
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
||||
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
|
||||
|
||||
# JWT
|
||||
# Required secrets to sign JWT tokens
|
||||
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
||||
AUTH_SECRET=5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE=
|
||||
|
||||
DB_CONNECTION_URI=postgres://infisical:infisical@haproxy:5433/infisical?sslmode=no-verify
|
||||
# Redis
|
||||
REDIS_URL=redis://:123456@haproxy:6379
|
||||
|
||||
|
||||
# Website URL
|
||||
# Required
|
||||
SITE_URL=http://localhost:8080
|
||||
|
||||
# Mail/SMTP
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=
|
||||
SMTP_NAME=
|
||||
SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
|
||||
# Integration
|
||||
# Optional only if integration is used
|
||||
CLIENT_ID_HEROKU=
|
||||
CLIENT_ID_VERCEL=
|
||||
CLIENT_ID_NETLIFY=
|
||||
CLIENT_ID_GITHUB=
|
||||
CLIENT_ID_GITLAB=
|
||||
CLIENT_ID_BITBUCKET=
|
||||
CLIENT_SECRET_HEROKU=
|
||||
CLIENT_SECRET_VERCEL=
|
||||
CLIENT_SECRET_NETLIFY=
|
||||
CLIENT_SECRET_GITHUB=
|
||||
CLIENT_SECRET_GITLAB=
|
||||
CLIENT_SECRET_BITBUCKET=
|
||||
CLIENT_SLUG_VERCEL=
|
||||
|
||||
# Sentry (optional) for monitoring errors
|
||||
SENTRY_DSN=
|
||||
|
||||
# Infisical Cloud-specific configs
|
||||
# Ignore - Not applicable for self-hosted version
|
||||
POSTHOG_HOST=
|
||||
POSTHOG_PROJECT_API_KEY=
|
||||
|
||||
# SSO-specific variables
|
||||
CLIENT_ID_GOOGLE_LOGIN=
|
||||
CLIENT_SECRET_GOOGLE_LOGIN=
|
||||
|
||||
CLIENT_ID_GITHUB_LOGIN=
|
||||
CLIENT_SECRET_GITHUB_LOGIN=
|
||||
|
||||
CLIENT_ID_GITLAB_LOGIN=
|
||||
CLIENT_SECRET_GITLAB_LOGIN=
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
The provided Docker stack YAML file defines the services and their configurations for deploying Infisical with high availability. The main components of this stack are as follows.
|
||||
The [Docker stack file](https://github.com/Infisical/infisical/tree/main/docker-swarm) used in this guide defines the services and their configurations for deploying Infisical in a highly available manner. The main components of this stack are as follows.
|
||||
|
||||
1. **HAProxy**: The HAProxy service is configured to expose ports for accessing PostgreSQL (5433 for the master, 5434 for replicas), Redis master (6379), and the Infisical backend (8080). It uses a config file (`haproxy.cfg`) to define the load balancing and health check rules.
|
||||
|
||||
@@ -496,42 +79,34 @@ The provided Docker stack YAML file defines the services and their configuration
|
||||
|
||||
6. **Redis Sentinel**: Three Redis Sentinel instances (redis_sentinel1, redis_sentinel2, redis_sentinel3) are deployed, one on each node, to monitor and manage the Redis instances. They are connected to the `infisical` network.
|
||||
|
||||
## HAProxy Configuration
|
||||
## Deployment instructions
|
||||
|
||||
The HAProxy configuration file (`haproxy.cfg`) defines the load balancing and health check rules for the PostgreSQL and Redis instances.
|
||||
|
||||
1. **Stats**: This section enables the HAProxy statistics dashboard, accessible at port 7000.
|
||||
|
||||
2. **Resolvers**: This section defines the DNS resolver for service discovery, using the Docker embedded DNS server.
|
||||
|
||||
3. **Frontend**: There are separate frontend sections for the PostgreSQL master (port 5433), PostgreSQL replicas (port 5434), Redis (port 6379), and the Infisical backend (port 8080). Each frontend binds to the respective port and defines the default backend.
|
||||
|
||||
4. **Backend**: The backend sections define the servers and health check rules for each service.
|
||||
- For PostgreSQL, there are separate backends for the master and replicas. The health check is performed using an HTTP request to the `/master` or `/replica` endpoint, expecting a 200 status code.
|
||||
- For Redis, the backend uses a TCP health check with authentication and expects the role to be `master` for the Redis master instance.
|
||||
- For the Infisical backend, the health check is performed using an HTTP request to the `/api/status` endpoint, expecting a 200 status code.
|
||||
|
||||
## Setting Up Docker Nodes
|
||||
|
||||
1. Initialize Docker Swarm on one of the VMs by running the following command:
|
||||
|
||||
```
|
||||
docker swarm init --advertise-addr <MANAGER_NODE_IP>
|
||||
<Steps>
|
||||
<Step title="Initialize Docker Swarm on one of the VMs by running the following command">
|
||||
```
|
||||
docker swarm init
|
||||
```
|
||||
|
||||
Replace `<MANAGER_NODE_IP>` with the IP address of the VM that will serve as the manager node. Remember to copy the join token returned by the this init command.
|
||||
|
||||
<Tip>
|
||||
For the sake of simplicity, we only use one manager node in this example deployment. However, in production settings, we recommended you have at least 3 manager nodes.
|
||||
</Tip>
|
||||
</Step>
|
||||
|
||||
2. On the other VMs, join the Docker Swarm by running the command provided by the manager node:
|
||||
|
||||
```
|
||||
<Step title="On the other VMs, join the Docker Swarm by running the command provided by the manager node">
|
||||
```
|
||||
docker swarm join --token <JOIN_TOKEN> <MANAGER_NODE_IP>:2377
|
||||
```
|
||||
|
||||
Replace `<JOIN_TOKEN>` with the token provided by the manager node during initialization.
|
||||
|
||||
3. Label the nodes with `node.labels.name` to specify their roles. For example:
|
||||
</Step>
|
||||
|
||||
```
|
||||
<Step title="Label the nodes with `node.labels.name` to specify their roles.">
|
||||
Labels on nodes will help us select where stateful components such as Postgres and Redis are deployed on. To label nodes, follow the steps below.
|
||||
|
||||
```
|
||||
docker node update --label-add name=node1 <NODE1_ID>
|
||||
docker node update --label-add name=node2 <NODE2_ID>
|
||||
docker node update --label-add name=node3 <NODE3_ID>
|
||||
@@ -540,32 +115,102 @@ The HAProxy configuration file (`haproxy.cfg`) defines the load balancing and he
|
||||
Replace `<NODE1_ID>`, `<NODE2_ID>`, and `<NODE3_ID>` with the respective node IDs.
|
||||
To view the list of nodes and their ids, run the following on the manager node `docker node ls`.
|
||||
|
||||
## Deploying the Docker Stack
|
||||
</Step>
|
||||
|
||||
1. Copy the provided Docker stack YAML file and the HAProxy configuration file to the manager node.
|
||||
<Step title="Copy deployment assets to manager node">
|
||||
Copy the Docker stack YAML file, HAProxy configuration file and example `.env` file to the manager node. Ensure that all 3 files are placed in the same file directory.
|
||||
- [Docker stack file](https://github.com/Infisical/infisical/blob/main/docker-swarm/stack.yaml) (rename to infisical-stack.yaml)
|
||||
- [HA configuration file](https://github.com/Infisical/infisical/blob/main/docker-swarm/haproxy.cfg) (rename to haproxy.cfg)
|
||||
- [Example .env file](https://github.com/Infisical/infisical/blob/main/docker-swarm/.env-example) (rename to .env)
|
||||
</Step>
|
||||
|
||||
2. Deploy the stack using the following command:
|
||||
<Step title="Deploy stack">
|
||||
|
||||
```
|
||||
docker stack deploy -c infisical-stack.yaml infisical
|
||||
```
|
||||
</Step>
|
||||
|
||||
This command deploys the stack with the specified configuration.
|
||||
3. Run the [schema migration](/self-hosting/configuration/schema-migrations) to initialize the database.
|
||||
To connect to the Postgres database, use the following default credentials: username: `postgres` and password: `postgres`.
|
||||
<Step title="Check service status">
|
||||
```plain
|
||||
$ docker service ls
|
||||
ID NAME MODE REPLICAS IMAGE PORTS
|
||||
4kzq3ub8qgn9 infisical_etcd1 replicated 1/1 ghcr.io/zalando/spilo-16:3.2-p2
|
||||
tqx9t82bn8d9 infisical_etcd2 replicated 1/1 ghcr.io/zalando/spilo-16:3.2-p2
|
||||
t8vbkrasy8fz infisical_etcd3 replicated 1/1 ghcr.io/zalando/spilo-16:3.2-p2
|
||||
77iei42fcf6q infisical_haproxy global 4/4 haproxy:latest *:5002-5003->5433-5434/tcp, *:6379->6379/tcp, *:7001->7000/tcp, *:8080->8080/tcp
|
||||
jaewzqy8md56 infisical_infisical replicated 5/5 infisical/infisical:v0.60.1-postgres
|
||||
58w4zablfbtb infisical_redis_replica0 replicated 1/1 bitnami/redis:6.2.10
|
||||
w4yag2whq0un infisical_redis_replica1 replicated 1/1 bitnami/redis:6.2.10
|
||||
w03mriy0jave infisical_redis_replica2 replicated 1/1 bitnami/redis:6.2.10
|
||||
ppo6rk47hc9t infisical_redis_sentinel1 replicated 1/1 bitnami/redis-sentinel:6.2.10
|
||||
ub29vd0lnq7f infisical_redis_sentinel2 replicated 1/1 bitnami/redis-sentinel:6.2.10
|
||||
szg3yky7yji2 infisical_redis_sentinel3 replicated 1/1 bitnami/redis-sentinel:6.2.10
|
||||
eqtocpf5tiy0 infisical_spolo1 replicated 1/1 ghcr.io/zalando/spilo-16:3.2-p2
|
||||
3lznscvk7k5t infisical_spolo2 replicated 1/1 ghcr.io/zalando/spilo-16:3.2-p2
|
||||
v04ml7rz2j5q infisical_spolo3 replicated 1/1 ghcr.io/zalando/spilo-16:3.2-p2
|
||||
```
|
||||
|
||||
## Scaling and Resilience
|
||||
<Note>
|
||||
You'll notice that service `infisical_infisical` will not be in running state.
|
||||
This is expected as the database does not yet have the desired schemas.
|
||||
Once the database schema migrations have been successfully applied, this issue should be resolved.
|
||||
</Note>
|
||||
</Step>
|
||||
|
||||
To further scale and make the system more resilient, you can add more nodes to the Docker Swarm and update the stack configuration accordingly:
|
||||
<Step title="Run schema migrations">
|
||||
Run the schema migration to initialize the database. Follow the [guide here](/self-hosting/configuration/schema-migrations) to learn how.
|
||||
|
||||
1. Add new VMs and join them to the Docker Swarm as worker nodes.
|
||||
To connect to the Postgres database, use the following default credentials defined in the Docker swarm: username: `postgres`, password: `postgres` and database: `postgres`.
|
||||
</Step>
|
||||
|
||||
2. Update the Docker stack YAML file to include the new nodes in the `deploy` section of the relevant services, specifying the appropriate `node.labels.name` constraints.
|
||||
<Step title="View service status">
|
||||

|
||||
To view the health of services in your Infisical cluster, visit port `<NODE-IP>:7001` of any node in your Docker swarm.
|
||||
This port will expose the HA Proxy stats.
|
||||
|
||||
3. Update the HAProxy configuration file (`haproxy.cfg`) to include the new nodes in the backend sections for PostgreSQL and Redis.
|
||||
Run the following command to view the IPs of the nodes in your docker swarm.
|
||||
|
||||
4. Redeploy the updated stack using the `docker stack deploy` command.
|
||||
```plain
|
||||
$ docker node ls
|
||||
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
|
||||
0jnegl4gpo235l66nglcwc07t localhost Ready Active 26.0.2
|
||||
no1a7zwj88057k73m196ulkq6 * localhost Ready Active Leader 26.0.2
|
||||
wcb2x27w3tq7ht4v1h7ke49qk localhost Ready Active 26.0.2
|
||||
zov5q7uop7wpxc2ndz712v9oa localhost Ready Active 26.0.2
|
||||
```
|
||||
|
||||
Note that the database containers (PostgreSQL) are stateful and cannot be simply replicated. Instead, one database instance is deployed per node to ensure data consistency and avoid conflicts.
|
||||
<Info>
|
||||
The stats page may take 1-2 minutes to become accessible.
|
||||
</Info>
|
||||
</Step>
|
||||
|
||||
<Check>Once all services are running as expected, you may visit the IP address of the node where the HA Proxy was deployed. This should take you to the Infisical installation wizard.</Check>
|
||||
<Step title="Initialize Infisical">
|
||||

|
||||
Once all expected services are up and running, visit `<NODE-IP>:8080` of any node in the swarm. This will take you to the Infisical configuration page.
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## FAQ
|
||||
<Accordion title="How do I scale Infisical cluster further?" defaultOpen="true">
|
||||
To further scale and make the system more resilient, you can add more nodes to the Docker Swarm and update the stack configuration accordingly:
|
||||
|
||||
1. Add new VMs and join them to the Docker Swarm as worker nodes.
|
||||
|
||||
2. Update the Docker stack YAML file to include the new nodes in the `deploy` section of the relevant services, specifying the appropriate `node.labels.name` constraints.
|
||||
|
||||
3. Update the HAProxy configuration file (`haproxy.cfg`) to include the new nodes in the backend sections for PostgreSQL and Redis.
|
||||
|
||||
4. Redeploy the updated stack using the `docker stack deploy` command.
|
||||
|
||||
Note that the database containers (PostgreSQL) are stateful and cannot be simply replicated. Instead, one database instance is deployed per node to ensure data consistency and avoid conflicts.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="How do I configure backups for Postgres and Redis?">
|
||||
Native tooling for scheduled backups of Postgres and Redis is currently in development.
|
||||
In the meantime, we recommend using a variety of open-source tools available for this purpose.
|
||||
For Postgres, [Spilo](https://github.com/zalando/spilo) provides built-in support for scheduled data dumps.
|
||||
You can explore other third party tools for managing db backups, one such tool is [docker-db-backup](https://github.com/tiredofit/docker-db-backup).
|
||||
</Accordion>
|
||||
|
@@ -17,23 +17,20 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children
|
||||
containerClassName
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex items-end space-x-12 rounded-md bg-mineshaft-800 p-16 text-bunker-300",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faLock} size="6x" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-2 text-4xl font-medium">Access Restricted</div>
|
||||
{children || (
|
||||
<div className="text-sm">
|
||||
Your role has limited permissions, please <br /> contact your administrator to gain
|
||||
access
|
||||
</div>
|
||||
)}
|
||||
<div className={twMerge("rounded-md bg-mineshaft-800 p-16 text-bunker-300", className)}>
|
||||
<div className="flex items-end space-x-12">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faLock} size="6x" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-2 text-4xl font-medium">Access Restricted</div>
|
||||
{children || (
|
||||
<div className="text-sm">
|
||||
Your role has limited permissions, please <br /> contact your administrator to gain
|
||||
access
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
32
frontend/src/components/v2/Badge/Badge.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { cva, VariantProps } from "cva";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface IProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const badgeVariants = cva(
|
||||
[
|
||||
"inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-xs text-yellow opacity-80 hover:opacity-100"
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: "bg-yellow/20 text-yellow",
|
||||
danger: "bg-red/20 text-red",
|
||||
success: "bg-green/20 text-green"
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export type BadgeProps = VariantProps<typeof badgeVariants> & IProps;
|
||||
|
||||
export const Badge = ({ children, className, variant }: BadgeProps) => {
|
||||
return (
|
||||
<div className={twMerge(badgeVariants({ variant: variant || "primary" }), className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
1
frontend/src/components/v2/Badge/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { Badge } from "./Badge";
|
@@ -29,7 +29,7 @@ const buttonVariants = cva(
|
||||
colorSchema: {
|
||||
primary: ["bg-primary", "text-black", "border-primary bg-opacity-90 hover:bg-opacity-100"],
|
||||
secondary: ["bg-mineshaft", "text-gray-300", "border-mineshaft hover:bg-opacity-80"],
|
||||
danger: ["bg-red", "text-white", "border-red hover:bg-opacity-90"],
|
||||
danger: ["!bg-red", "!text-white", "!border-red hover:!bg-opacity-90"],
|
||||
gray: ["bg-bunker-500", "text-bunker-200"]
|
||||
},
|
||||
variant: {
|
||||
|
13
frontend/src/components/v2/Divider/Divider.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface IProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Divider = ({ className }: IProps): JSX.Element => {
|
||||
return (
|
||||
<div className={twMerge("flex items-center px-2 opacity-50", className)}>
|
||||
<div aria-hidden="true" className="h-5 w-full grow border border-t border-mineshaft-200" />
|
||||
</div>
|
||||
);
|
||||
};
|