mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-02 08:27:38 +00:00
Compare commits
396 Commits
fix/addres
...
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 | ||
|
f8e1ed09d2 | ||
|
5c71116be6 | ||
|
07cc4fd1ab | ||
|
74bdbc0724 | ||
|
a0d5c67456 | ||
|
db4f4d8f28 | ||
|
d6f6f51d16 | ||
|
79a0f3d701 | ||
|
46912c4c3c | ||
|
6636377cb5 | ||
|
26320ddce4 | ||
|
f5964040d7 | ||
|
dcaa7f1fce | ||
|
a4119ee1bb | ||
|
667f696d26 | ||
|
5c0e5a8ae0 |
65
.env.example
Normal file
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
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
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
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
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
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
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
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
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
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
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
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
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" });
|
||||
};
|
@@ -36,7 +36,7 @@ export const writeLimit: RateLimitOptions = {
|
||||
export const secretsLimit: RateLimitOptions = {
|
||||
// secrets, folders, secret imports
|
||||
timeWindow: 60 * 1000,
|
||||
max: 600,
|
||||
max: 1000,
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
|
@@ -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,
|
||||
|
97
company/documentation/getting-started/introduction.mdx
Normal file
97
company/documentation/getting-started/introduction.mdx
Normal file
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: "What is Infisical?"
|
||||
sidebarTitle: "What is Infisical?"
|
||||
description: "An Introduction to the Infisical secret management platform."
|
||||
---
|
||||
|
||||
Infisical is an [open-source](https://github.com/infisical/infisical) secret management platform for developers.
|
||||
It provides capabilities for storing, managing, and syncing application configuration and secrets like API keys, database
|
||||
credentials, and certificates across infrastructure. In addition, Infisical prevents secrets leaks to git and enables secure
|
||||
sharing of secrets among engineers.
|
||||
|
||||
Start managing secrets securely with [Infisical Cloud](https://app.infisical.com) or learn how to [host Infisical](/self-hosting/overview) yourself.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card
|
||||
title="Infisical Cloud"
|
||||
href="https://app.infisical.com/signup"
|
||||
icon="cloud"
|
||||
color="#000000"
|
||||
>
|
||||
Get started with Infisical Cloud in just a few minutes.
|
||||
</Card>
|
||||
<Card
|
||||
href="/self-hosting/overview"
|
||||
title="Self-hosting"
|
||||
icon="server"
|
||||
color="#000000"
|
||||
>
|
||||
Self-host Infisical on your own infrastructure.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Why Infisical?
|
||||
|
||||
Infisical helps developers achieve secure centralized secret management and provides all the tools to easily manage secrets in various environments and infrastructure components. In particular, here are some of the most common points that developers mention after adopting Infisical:
|
||||
- Streamlined **local development** processes (switching .env files to [Infisical CLI](/cli/commands/run) and removing secrets from developer machines).
|
||||
- **Best-in-class developer experience** with an easy-to-use [Web Dashboard](/documentation/platform/project).
|
||||
- Simple secret management inside **[CI/CD pipelines](/integrations/cicd/githubactions)** and staging environments.
|
||||
- Secure and compliant secret management practices in **[production environments](/sdks/overview)**.
|
||||
- **Facilitated workflows** around [secret change management](/documentation/platform/pr-workflows), [access requests](/documentation/platform/access-controls/access-requests), [temporary access provisioning](/documentation/platform/access-controls/temporary-access), and more.
|
||||
- **Improved security posture** thanks to [secret scanning](/cli/scanning-overview), [granular access control policies](/documentation/platform/access-controls/overview), [automated secret rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview), and [dynamic secrets](/documentation/platform/dynamic-secrets/overview) capabilities.
|
||||
|
||||
## How does Infisical work?
|
||||
|
||||
To make secret management effortless and secure, Infisical follows a certain structure for enabling secret management workflows as defined below.
|
||||
|
||||
**Identities** in Infisical are users or machine which have a certain set of roles and permissions assigned to them. Such identities are able to manage secrets in various **Clients** throughout the entire infrastructure. To do that, identities have to verify themselves through one of the available **Authentication Methods**.
|
||||
|
||||
As a result, the 3 main concepts that are important to understand are:
|
||||
- **[Identities](/documentation/platform/identities/overview)**: users or machines with a set permissions assigned to them.
|
||||
- **[Clients](/integrations/platforms/kubernetes)**: Infisical-developed tools for managing secrets in various infrastructure components (e.g., [Kubernetes Operator](/integrations/platforms/kubernetes), [Infisical Agent](/integrations/platforms/infisical-agent), [CLI](/cli/usage), [SDKs](/sdks/overview), [API](/api-reference/overview/introduction), [Web Dashboard](/documentation/platform/organization)).
|
||||
- **[Authentication Methods](/documentation/platform/identities/universal-auth)**: ways for Identities to authenticate inside different clients (e.g., SAML SSO for Web Dashboard, Universal Auth for Infisical Agent, etc.).
|
||||
|
||||
## How to get started with Infisical?
|
||||
|
||||
Depending on your use case, it might be helpful to look into some of the resources and guides provided below.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card href="../../cli/overview" title="Command Line Interface (CLI)" icon="square-terminal" color="#000000">
|
||||
Inject secrets into any application process/environment.
|
||||
</Card>
|
||||
<Card
|
||||
title="SDKs"
|
||||
href="/documentation/getting-started/sdks"
|
||||
icon="boxes-stacked"
|
||||
color="#000000"
|
||||
>
|
||||
Fetch secrets with any programming language on demand.
|
||||
</Card>
|
||||
<Card href="../../integrations/platforms/docker-intro" title="Docker" icon="docker" color="#000000">
|
||||
Inject secrets into Docker containers.
|
||||
</Card>
|
||||
<Card
|
||||
href="../../integrations/platforms/kubernetes"
|
||||
title="Kubernetes"
|
||||
icon="server"
|
||||
color="#000000"
|
||||
>
|
||||
Fetch and save secrets as native Kubernetes secrets.
|
||||
</Card>
|
||||
<Card
|
||||
href="/documentation/getting-started/api"
|
||||
title="REST API"
|
||||
icon="cloud"
|
||||
color="#000000"
|
||||
>
|
||||
Fetch secrets via HTTP request.
|
||||
</Card>
|
||||
<Card
|
||||
href="/integrations/overview"
|
||||
title="Native Integrations"
|
||||
icon="clouds"
|
||||
color="#000000"
|
||||
>
|
||||
Explore integrations for GitHub, Vercel, AWS, and more.
|
||||
</Card>
|
||||
</CardGroup>
|
BIN
company/favicon.png
Normal file
BIN
company/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
5
company/logo/dark.svg
Normal file
5
company/logo/dark.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 6.8 KiB |
5
company/logo/light.svg
Normal file
5
company/logo/light.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 6.8 KiB |
80
company/mint.json
Normal file
80
company/mint.json
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"name": "Infisical",
|
||||
"openapi": "https://app.infisical.com/api/docs/json",
|
||||
"logo": {
|
||||
"dark": "/logo/dark.svg",
|
||||
"light": "/logo/light.svg",
|
||||
"href": "https://infisical.com"
|
||||
},
|
||||
"favicon": "/favicon.png",
|
||||
"colors": {
|
||||
"primary": "#26272b",
|
||||
"light": "#97b31d",
|
||||
"dark": "#A1B659",
|
||||
"ultraLight": "#E7F256",
|
||||
"ultraDark": "#8D9F4C",
|
||||
"background": {
|
||||
"light": "#ffffff",
|
||||
"dark": "#0D1117"
|
||||
},
|
||||
"anchors": {
|
||||
"from": "#000000",
|
||||
"to": "#707174"
|
||||
}
|
||||
},
|
||||
"modeToggle": {
|
||||
"default": "light",
|
||||
"isHidden": true
|
||||
},
|
||||
"feedback": {
|
||||
"suggestEdit": true,
|
||||
"raiseIssue": true,
|
||||
"thumbsRating": true
|
||||
},
|
||||
"api": {
|
||||
"baseUrl": ["https://app.infisical.com", "http://localhost:8080"]
|
||||
},
|
||||
"topbarLinks": [
|
||||
{
|
||||
"name": "Log In",
|
||||
"url": "https://app.infisical.com/login"
|
||||
}
|
||||
],
|
||||
"topbarCtaButton": {
|
||||
"name": "Start for Free",
|
||||
"url": "https://app.infisical.com/signup"
|
||||
},
|
||||
"tabs": [
|
||||
{
|
||||
"name": "Integrations",
|
||||
"url": "integrations"
|
||||
},
|
||||
{
|
||||
"name": "CLI",
|
||||
"url": "cli"
|
||||
},
|
||||
{
|
||||
"name": "API Reference",
|
||||
"url": "api-reference"
|
||||
},
|
||||
{
|
||||
"name": "SDKs",
|
||||
"url": "sdks"
|
||||
},
|
||||
{
|
||||
"name": "Changelog",
|
||||
"url": "changelog"
|
||||
}
|
||||
],
|
||||
"navigation": [
|
||||
{
|
||||
"group": "Getting Started",
|
||||
"pages": [
|
||||
"documentation/getting-started/introduction"
|
||||
]
|
||||
}
|
||||
],
|
||||
"integrations": {
|
||||
"intercom": "hsg644ru"
|
||||
}
|
||||
}
|
142
company/style.css
Normal file
142
company/style.css
Normal file
@@ -0,0 +1,142 @@
|
||||
#navbar .max-w-8xl {
|
||||
max-width: 100%;
|
||||
border-bottom: 1px solid #ebebeb;
|
||||
background-color: #fcfcfc;
|
||||
}
|
||||
|
||||
.max-w-8xl {
|
||||
/* background-color: #f5f5f5; */
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
left: 0;
|
||||
padding-left: 48px;
|
||||
padding-right: 30px;
|
||||
border-right: 1px;
|
||||
border-color: #cdd64b;
|
||||
background-color: #fcfcfc;
|
||||
border-right: 1px solid #ebebeb;
|
||||
}
|
||||
|
||||
#sidebar .relative .sticky {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#sidebar li > div.mt-2 {
|
||||
border-radius: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#sidebar li > a.mt-2 {
|
||||
border-radius: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#sidebar li > a.leading-6 {
|
||||
border-radius: 0;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
/* #sidebar ul > div.mt-12 {
|
||||
padding-top: 30px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#sidebar ul > div.mt-12 h5 {
|
||||
position: absolute;
|
||||
left: -12px;
|
||||
top: -0px;
|
||||
} */
|
||||
|
||||
#header {
|
||||
border-left: 1px solid #26272b;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
background-color: #f5f5f5;
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
#content-area .mt-8 .block{
|
||||
border-radius: 0;
|
||||
border-width: 1px;
|
||||
border-color: #ebebeb;
|
||||
}
|
||||
|
||||
#content-area .mt-8 .rounded-xl{
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#content-area .mt-8 .rounded-lg{
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#content-area .mt-6 .rounded-xl{
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#content-area .mt-6 .rounded-lg{
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#content-area .mt-6 .rounded-md{
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#content-area .mt-8 .rounded-md{
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#content-area div.my-4{
|
||||
border-radius: 0;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
#content-area div.flex-1 {
|
||||
/* text-transform: uppercase; */
|
||||
opacity: 0.8;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
#content-area button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#content-area a {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#content-area .not-prose {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* .eyebrow {
|
||||
text-transform: uppercase;
|
||||
font-weight: 400;
|
||||
color: red;
|
||||
} */
|
||||
|
||||
#content-container {
|
||||
/* background-color: #f5f5f5; */
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
#topbar-cta-button .group .absolute {
|
||||
background-color: black;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
/* #topbar-cta-button .group .absolute:hover {
|
||||
background-color: white;
|
||||
border-radius: 0px;
|
||||
} */
|
||||
|
||||
#topbar-cta-button .group .flex {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
.flex-1 .flex .items-center {
|
||||
/* background-color: #f5f5f5; */
|
||||
}
|
@@ -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
|
||||
@@ -103,6 +99,8 @@ services:
|
||||
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
|
||||
@@ -119,6 +117,8 @@ services:
|
||||
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
|
||||
@@ -135,6 +135,8 @@ services:
|
||||
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
|
||||
@@ -256,4 +258,4 @@ configs:
|
||||
|
||||
secrets:
|
||||
env_file:
|
||||
file: .env
|
||||
file: .env
|
||||
|
@@ -4,7 +4,7 @@ openapi: "GET /api/v2/service-token"
|
||||
---
|
||||
|
||||
<Warning>
|
||||
This endpoint will be deprecated in the near future with the removal of service tokens in Q1/Q2 2024.
|
||||
This endpoint is deprecated and will be removed in the future.
|
||||
|
||||
We recommend switching to using [identities](/documentation/platform/identities/overview) if your client supports it.
|
||||
We recommend switching to using [Machine Identities](/documentation/platform/identities/machine-identities).
|
||||
</Warning>
|
||||
|
@@ -16,36 +16,48 @@ Export environment variables from the platform into a file format.
|
||||
<Accordion title="infisical export" defaultOpen="true">
|
||||
Use this command to export environment variables from the platform into a raw file formats
|
||||
|
||||
```bash
|
||||
$ infisical export
|
||||
```bash
|
||||
$ infisical export
|
||||
|
||||
# Export variables to a .env file
|
||||
infisical export > .env
|
||||
# Export variables to a .env file
|
||||
infisical export > .env
|
||||
|
||||
# Export variables to a .env file (with export keyword)
|
||||
infisical export --format=dotenv-export > .env
|
||||
# Export variables to a .env file (with export keyword)
|
||||
infisical export --format=dotenv-export > .env
|
||||
|
||||
# Export variables to a CSV file
|
||||
infisical export --format=csv > secrets.csv
|
||||
# Export variables to a CSV file
|
||||
infisical export --format=csv > secrets.csv
|
||||
|
||||
# Export variables to a JSON file
|
||||
infisical export --format=json > secrets.json
|
||||
# Export variables to a JSON file
|
||||
infisical export --format=json > secrets.json
|
||||
|
||||
# Export variables to a YAML file
|
||||
infisical export --format=yaml > secrets.yaml
|
||||
# Export variables to a YAML file
|
||||
infisical export --format=yaml > secrets.yaml
|
||||
|
||||
# Render secrets using a custom template file
|
||||
infisical export --template=<path to template>
|
||||
```
|
||||
# Render secrets using a custom template file
|
||||
infisical export --template=<path to template>
|
||||
```
|
||||
|
||||
### Environment variables
|
||||
|
||||
### Environment variables
|
||||
<Accordion title="INFISICAL_TOKEN">
|
||||
Used to fetch secrets via a [service token](/documentation/platform/token) apposed to logged in credentials. Simply, export this variable in the terminal before running this command.
|
||||
Used to fetch secrets via a [machine identities](/documentation/platform/identities/machine-identities) apposed to logged in credentials. Simply, export this variable in the terminal before running this command.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
export INFISICAL_TOKEN=st.63e03c4a97cb4a747186c71e.ed5b46a34c078a8f94e8228f4ab0ff97.4f7f38034811995997d72badf44b42ec
|
||||
# Example
|
||||
export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=<identity-client-id> --client-secret=<identity-client-secret> --silent --plain) # --plain flag will output only the token, so it can be fed to an environment variable. --silent will disable any update messages.
|
||||
```
|
||||
|
||||
<Info>
|
||||
Alternatively, you may use service tokens.
|
||||
|
||||
Please note, however, that service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities). They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys).
|
||||
```bash
|
||||
# Example
|
||||
export INFISICAL_TOKEN=<service-token>
|
||||
```
|
||||
|
||||
</Info>
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="INFISICAL_DISABLE_UPDATE_CHECK">
|
||||
@@ -54,16 +66,18 @@ Export environment variables from the platform into a file format.
|
||||
To use, simply export this variable in the terminal before running this command.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
# Example
|
||||
export INFISICAL_DISABLE_UPDATE_CHECK=true
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
### flags
|
||||
### flags
|
||||
|
||||
<Accordion title="--template">
|
||||
The `--template` flag specifies the path to the template file used for rendering secrets. When using templates, you can omit the other format flags.
|
||||
|
||||
```text my-template-file
|
||||
```text my-template-file
|
||||
{{$secrets := secret "<infisical-project-id>" "<environment-slug>" "<folder-path>"}}
|
||||
{{$length := len $secrets}}
|
||||
{{- "{"}}
|
||||
@@ -73,24 +87,26 @@ Export environment variables from the platform into a file format.
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{ "}" -}}
|
||||
```
|
||||
```
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical export --template="/path/to/template/file"
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="--env">
|
||||
Used to set the environment that secrets are pulled from.
|
||||
Used to set the environment that secrets are pulled from.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical export --env=prod
|
||||
# Example
|
||||
infisical export --env=prod
|
||||
```
|
||||
|
||||
Note: this flag only accepts environment slug names not the fully qualified name. To view the slug name of an environment, visit the project settings page.
|
||||
|
||||
default value: `dev`
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--projectId">
|
||||
@@ -98,28 +114,32 @@ Export environment variables from the platform into a file format.
|
||||
This flag allows you to override this behavior by explicitly defining the project to fetch your secrets from.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
|
||||
# Example
|
||||
|
||||
infisical export --projectId=XXXXXXXXXXXXXX
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--expand">
|
||||
Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`)
|
||||
|
||||
Default value: `true`
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--format">
|
||||
Format of the output file. Accepted values: `dotenv`, `dotenv-export`, `csv`, `json` and `yaml`
|
||||
Format of the output file. Accepted values: `dotenv`, `dotenv-export`, `csv`, `json` and `yaml`
|
||||
|
||||
Default value: `dotenv`
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--secret-overriding">
|
||||
Prioritizes personal secrets with the same name over shared secrets
|
||||
|
||||
Default value: `true`
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--path">
|
||||
@@ -129,19 +149,21 @@ Export environment variables from the platform into a file format.
|
||||
# Example
|
||||
infisical export --path="/path/to/folder" --env=dev
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--tags">
|
||||
When working with tags, you can use this flag to filter and retrieve only secrets that are associated with a specific tag(s).
|
||||
|
||||
```bash
|
||||
# Example
|
||||
# Example
|
||||
infisical run --tags=tag1,tag2,tag3 -- npm run dev
|
||||
```
|
||||
|
||||
Note: you must reference the tag by its slug name not its fully qualified name. Go to project settings to view all tag slugs.
|
||||
|
||||
By default, all secrets are fetched
|
||||
|
||||
</Accordion>
|
||||
|
||||
</Accordion>
|
||||
|
@@ -11,6 +11,7 @@ description: "The command that injects your secrets into local environment"
|
||||
# Example
|
||||
infisical run [options] -- npm run dev
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="Chained commands">
|
||||
@@ -20,6 +21,7 @@ description: "The command that injects your secrets into local environment"
|
||||
# Example
|
||||
infisical run [options] --command "npm run bootstrap && npm run dev start; other-bash-command"
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -27,27 +29,38 @@ description: "The command that injects your secrets into local environment"
|
||||
|
||||
Inject secrets from Infisical into your application process.
|
||||
|
||||
|
||||
## Subcommands & flags
|
||||
|
||||
<Accordion title="infisical run" defaultOpen="true">
|
||||
Use this command to inject secrets into your applications process
|
||||
|
||||
```bash
|
||||
$ infisical run -- <your application command>
|
||||
```bash
|
||||
$ infisical run -- <your application command>
|
||||
|
||||
# Example
|
||||
$ infisical run -- npm run dev
|
||||
```
|
||||
# Example
|
||||
$ infisical run -- npm run dev
|
||||
```
|
||||
|
||||
### Environment variables
|
||||
|
||||
### Environment variables
|
||||
<Accordion title="INFISICAL_TOKEN">
|
||||
Used to fetch secrets via a [service token](/documentation/platform/token) apposed to logged in credentials. Simply, export this variable in the terminal before running this command.
|
||||
Used to fetch secrets via a [machine identity](/documentation/platform/identities/machine-identities) apposed to logged in credentials. Simply, export this variable in the terminal before running this command.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
export INFISICAL_TOKEN=st.63e03c4a97cb4a747186c71e.ed5b46a34c078a8f94e8228f4ab0ff97.4f7f38034811995997d72badf44b42ec
|
||||
# Example
|
||||
export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=<identity-client-id> --client-secret=<identity-client-secret> --silent --plain) # --plain flag will output only the token, so it can be fed to an environment variable. --silent will disable any update messages.
|
||||
```
|
||||
|
||||
<Info>
|
||||
Alternatively, you may use service tokens.
|
||||
|
||||
Please note, however, that service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities). They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys).
|
||||
```bash
|
||||
# Example
|
||||
export INFISICAL_TOKEN=<service-token>
|
||||
```
|
||||
|
||||
</Info>
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="INFISICAL_DISABLE_UPDATE_CHECK">
|
||||
@@ -56,71 +69,90 @@ Inject secrets from Infisical into your application process.
|
||||
To use, simply export this variable in the terminal before running this command.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
# Example
|
||||
export INFISICAL_DISABLE_UPDATE_CHECK=true
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
### Flags
|
||||
|
||||
### Flags
|
||||
|
||||
<Accordion title="--project-config-dir">
|
||||
Explicitly set the directory where the .infisical.json resides. This is useful for some monorepo setups.
|
||||
Explicitly set the directory where the .infisical.json resides. This is useful for some monorepo setups.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
# Example
|
||||
infisical run --project-config-dir=/some-dir -- printenv
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--command">
|
||||
Pass secrets into multiple commands at once
|
||||
|
||||
```bash
|
||||
# Example
|
||||
# Example
|
||||
infisical run --command="npm run build && npm run dev; more-commands..."
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--projectId">
|
||||
The project ID to fetch secrets from. This is required when using a machine identity to authenticate.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical run --projectId=<project-id> -- npm run dev
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--token">
|
||||
If you are using a [service token](/documentation/platform/token) to authenticate, you can pass the token as a flag
|
||||
If you are using a [machine identity](/documentation/platform/identities/machine-identities) to authenticate, you can pass the token as a flag
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical run --token="st.63e03c4a97cb4a747186c71e.ed5b46a34c078a8f94e8228f4ab0ff97.4f7f38034811995997d72badf44b42ec" -- npm run start
|
||||
# Example
|
||||
infisical run --token="<universal-auth-access-token>" --projectId=<project-id> -- npm run start
|
||||
```
|
||||
|
||||
You may also expose the token to the CLI by setting the environment variable `INFISICAL_TOKEN` before executing the run command. This will have the same effect as setting the token with `--token` flag
|
||||
You may also expose the token to the CLI by setting the environment variable `INFISICAL_TOKEN` before executing the run command. This will have the same effect as setting the token with `--token` flag
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--expand">
|
||||
Turn on or off the shell parameter expansion in your secrets. If you have used shell parameters in your secret(s), activating this feature will populate them before injecting them into your application process.
|
||||
|
||||
Default value: `true`
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--env">
|
||||
This is used to specify the environment from which secrets should be retrieved. The accepted values are the environment slugs defined for your project, such as `dev`, `staging`, `test`, and `prod`.
|
||||
|
||||
Default value: `dev`
|
||||
</Accordion>
|
||||
{" "}
|
||||
|
||||
<Accordion title="--env">
|
||||
This is used to specify the environment from which secrets should be
|
||||
retrieved. The accepted values are the environment slugs defined for your
|
||||
project, such as `dev`, `staging`, `test`, and `prod`. Default value: `dev`
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--secret-overriding">
|
||||
Prioritizes personal secrets with the same name over shared secrets
|
||||
|
||||
Default value: `true`
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--tags">
|
||||
When working with tags, you can use this flag to filter and retrieve only secrets that are associated with a specific tag(s).
|
||||
|
||||
```bash
|
||||
# Example
|
||||
# Example
|
||||
infisical run --tags=tag1,tag2,tag3 -- npm run dev
|
||||
```
|
||||
|
||||
Note: you must reference the tag by its slug name not its fully qualified name. Go to project settings to view all tag slugs.
|
||||
|
||||
By default, all secrets are fetched
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--path">
|
||||
|
@@ -23,13 +23,23 @@ $ infisical secrets
|
||||
### Environment variables
|
||||
|
||||
<Accordion title="INFISICAL_TOKEN">
|
||||
Used to fetch secrets via a [service token](/documentation/platform/token) apposed to logged in credentials. Simply, export this variable in the terminal before running this command.
|
||||
Used to fetch secrets via a [machine identity](/documentation/platform/identities/machine-identities) apposed to logged in credentials. Simply, export this variable in the terminal before running this command.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
export INFISICAL_TOKEN=st.63e03c4a97cb4a747186c71e.ed5b46a34c078a8f94e8228f4ab0ff97.4f7f38034811995997d72badf44b42ec
|
||||
export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=<identity-client-id> --client-secret=<identity-client-secret> --silent --plain) # --plain flag will output only the token, so it can be fed to an environment variable. --silent will disable any update messages.
|
||||
```
|
||||
|
||||
<Info>
|
||||
Alternatively, you may use service tokens.
|
||||
|
||||
Please note, however, that service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities). They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys).
|
||||
```bash
|
||||
# Example
|
||||
export INFISICAL_TOKEN=<service-token>
|
||||
```
|
||||
</Info>
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="INFISICAL_DISABLE_UPDATE_CHECK">
|
||||
@@ -53,6 +63,16 @@ $ infisical secrets
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--projectId">
|
||||
The project ID to fetch secrets from. This is required when using a machine identity to authenticate.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical secrets --projectId=<project-id>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--env">
|
||||
Used to select the environment name on which actions should be taken on
|
||||
|
||||
@@ -186,7 +206,7 @@ $ infisical secrets folders
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--token">
|
||||
Fetch folders using the Infisical service token
|
||||
Fetch folders using a [machine identity](/documentation/platform/identities/machine-identities) access token.
|
||||
|
||||
Default value: ``
|
||||
</Accordion>
|
||||
|
@@ -3,37 +3,47 @@ title: "infisical service-token"
|
||||
description: "Manage Infisical service tokens"
|
||||
---
|
||||
|
||||
```bash
|
||||
<Warning>
|
||||
This command is deprecated and will be removed in the near future. Please
|
||||
switch to using [Machine
|
||||
Identities](/documentation/platform/identities/machine-identities) for
|
||||
authenticating with Infisical.
|
||||
</Warning>
|
||||
|
||||
```bash
|
||||
infisical service-token create --scope=dev:/global --scope=dev:/backend --access-level=read --access-level=write
|
||||
```
|
||||
|
||||
## Description
|
||||
The Infisical `service-token` command allows you to manage service tokens for a given Infisical project.
|
||||
|
||||
The Infisical `service-token` command allows you to manage service tokens for a given Infisical project.
|
||||
With this command, you can create, view, and delete service tokens.
|
||||
|
||||
<Accordion title="service-token create" defaultOpen="true">
|
||||
Use this command to create a service token
|
||||
|
||||
```bash
|
||||
$ infisical service-token create --scope=dev:/backend/** --access-level=read --access-level=write
|
||||
```
|
||||
```bash
|
||||
$ infisical service-token create --scope=dev:/backend/** --access-level=read --access-level=write
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
### Flags
|
||||
<Accordion title="--scope">
|
||||
```bash
|
||||
infisical service-token create --scope=dev:/global --scope=dev:/backend/** --access-level=read
|
||||
```
|
||||
|
||||
Use the scope flag to define which environments and paths your service token should be authorized to access.
|
||||
|
||||
The value of your scope flag should be in the following `<environment slug>:<path>`.
|
||||
|
||||
The value of your scope flag should be in the following `<environment slug>:<path>`.
|
||||
Here, `environment slug` refers to the slug name of the environment, and `path` indicates the folder path where your secrets are stored.
|
||||
|
||||
For specifying multiple scopes, you can use multiple --scope flags.
|
||||
|
||||
|
||||
<Info>
|
||||
The `path` can be a Glob pattern
|
||||
</Info>
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--projectId">
|
||||
@@ -41,8 +51,9 @@ With this command, you can create, view, and delete service tokens.
|
||||
infisical service-token create --scope=dev:/global --access-level=read --projectId=63cefb15c8d3175601cfa989
|
||||
```
|
||||
|
||||
The project ID you'd like to create the service token for.
|
||||
The project ID you'd like to create the service token for.
|
||||
By default, the CLI will attempt to use the linked Infisical project in `.infisical.json` generated by `infisical init` command.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="--name">
|
||||
```bash
|
||||
@@ -52,6 +63,7 @@ With this command, you can create, view, and delete service tokens.
|
||||
Service token name
|
||||
|
||||
Default: `Service token generated via CLI`
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="--expiry-seconds">
|
||||
```bash
|
||||
@@ -61,6 +73,7 @@ With this command, you can create, view, and delete service tokens.
|
||||
Set the service token's expiration time in seconds from now. To never expire set to zero.
|
||||
|
||||
Default: `1 day`
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="--access-level">
|
||||
```bash
|
||||
@@ -68,6 +81,7 @@ With this command, you can create, view, and delete service tokens.
|
||||
```
|
||||
|
||||
The type of access the service token should have. Can be `read` and or `write`
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="--token-only">
|
||||
```bash
|
||||
@@ -77,5 +91,6 @@ With this command, you can create, view, and delete service tokens.
|
||||
When true, only the service token will be printed
|
||||
|
||||
Default: `false`
|
||||
|
||||
</Accordion>
|
||||
</Accordion>
|
||||
|
@@ -1,22 +0,0 @@
|
||||
---
|
||||
title: "Infisical Token"
|
||||
description: "How to use Infisical service token within the CLI."
|
||||
---
|
||||
|
||||
Prerequisite: [Infisical Token and How to Generate One](/documentation/platform/token).
|
||||
|
||||
It's possible to use the CLI to sync environment variables without manually entering login credentials by using a service token in the prerequisite link above.
|
||||
|
||||
## Feeding Infisical Token to the CLI
|
||||
|
||||
The CLI looks out for an environment variable called the `INFISICAL_TOKEN` which you can set depending on where you run the CLI. If `INFISICAL_TOKEN` is detected by the CLI, it will authenticate and retrieve the environment variables which the token is authorized for.
|
||||
|
||||
A common use-case is to use the Infisical Token to fetch environment variables with Docker. More specifically, a token can be passed to a container as an environment variable for the CLI to authenticate and pull its corresponding secrets. Check out the integration guides for that:
|
||||
|
||||
- [Docker](../../integrations/platforms/docker)
|
||||
- [Docker Compose](../../integrations/platforms/docker-compose)
|
||||
|
||||
<Info>
|
||||
Once the token is expired, the CLI using it will no longer be able to make
|
||||
requests with it.
|
||||
</Info>
|
@@ -1,141 +1,125 @@
|
||||
---
|
||||
title: "Quick usage"
|
||||
title: "Quickstart"
|
||||
description: "Manage secrets with Infisical CLI"
|
||||
---
|
||||
|
||||
The CLI is designed for a variety of applications, ranging from local secret management to CI/CD and production scenarios.
|
||||
The distinguishing factor, however, is the authentication method used.
|
||||
The CLI is designed for a variety of secret management applications ranging from local development to CI/CD and production scenarios.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Local development only">
|
||||
To use the Infisical CLI in your local development environment, simply run the command below and follow the interactive guide.
|
||||
<Tab title="Local development">
|
||||
In the following steps, we explore how to use the Infisical CLI to fetch back environment variables from Infisical
|
||||
and inject them into your local development process.
|
||||
|
||||
<Steps>
|
||||
<Step title="Log in with the CLI">
|
||||
Start by running the `infisical login` command to authenticate with Infisical.
|
||||
|
||||
```bash
|
||||
infisical login
|
||||
```
|
||||
<Note>
|
||||
If you are in a containerized environment such as WSL 2 or Codespaces, run `infisical login -i` to avoid browser based login
|
||||
</Note>
|
||||
</Step>
|
||||
<Step title="Initialize Infisical for your project">
|
||||
Next, navigate to your project and initialize Infisical.
|
||||
|
||||
```bash
|
||||
# navigate to your project
|
||||
cd /path/to/project
|
||||
|
||||
```bash
|
||||
infisical login
|
||||
```
|
||||
# initialize infisical
|
||||
infisical init
|
||||
```
|
||||
|
||||
<Note>
|
||||
If you are in a containerized environment such as WSL 2 or Codespaces, run `infisical login -i` to avoid browser based login
|
||||
</Note>
|
||||
The `infisical init` command creates a `.infisical.json` file, containing [local project settings](./project-config), at the location where the command is executed.
|
||||
|
||||
## Initialize Infisical for your project
|
||||
<Note>
|
||||
The `.infisical.json` file does not contain any sensitive data, so you may commit it to your git repository.
|
||||
</Note>
|
||||
</Step>
|
||||
<Step title="Inject environment variables">
|
||||
Finally, pass environment variables from Infisical into your application.
|
||||
|
||||
```bash
|
||||
# navigate to your project
|
||||
cd /path/to/project
|
||||
<Tabs>
|
||||
<Tab title="Feed secrets to your application">
|
||||
```bash
|
||||
infisical run --env=dev --path=/apps/firefly -- [your application start command] # e.g. npm run dev
|
||||
|
||||
# initialize infisical
|
||||
infisical init
|
||||
```
|
||||
# example with node (nodemon)
|
||||
infisical run --env=staging --path=/apps/spotify -- nodemon index.js
|
||||
|
||||
# example with flask
|
||||
infisical run --env=prod --path=/apps/backend -- flask run
|
||||
|
||||
# example with spring boot - maven
|
||||
infisical run --env=dev --path=/apps/ -- ./mvnw spring-boot:run --quiet
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="Feed secrets via custom aliases (advanced)">
|
||||
Custom aliases can utilize secrets from Infisical. Suppose there is a custom alias `yd` in `custom.sh` that runs `yarn dev` and needs the secrets provided by Infisical.
|
||||
```bash
|
||||
#!/bin/sh
|
||||
|
||||
yd() {
|
||||
yarn dev
|
||||
}
|
||||
```
|
||||
|
||||
To make the secrets available from Infisical to `yd`, you can run the following command:
|
||||
|
||||
```bash
|
||||
infisical run --env=prod --path=/apps/reddit --command="source custom.sh && yd"
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
View all available options for `run` command [here](./commands/run)
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
This will create `.infisical.json` file at the location the command was executed. This file contains your [local project settings](./project-config). It does not contain any sensitive data.
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="Staging, production & all other use case">
|
||||
To use Infisical for non local development scenarios, please create a [service token](../documentation/platform/token). The service token will allow you to authenticate and interact with Infisical.
|
||||
Once you have created a service token with the required permissions, you'll need to feed the token to the CLI.
|
||||
<Tab title="Staging, production & all other use cases">
|
||||
In the following steps, we explore how to use the Infisical CLI in a non-local development scenario
|
||||
to fetch back environment variables and export them to a file.
|
||||
<Steps>
|
||||
<Step title="Create a machine identity and obtain credentials for it">
|
||||
Follow the steps listed [here](/documentation/platform/identities/universal-auth) to create a machine identity and obtain a **client ID** and **client secret** for it.
|
||||
</Step>
|
||||
<Step title="Obtain a machine identity access token">
|
||||
Run the following command to authenticate with Infisical using the **client ID** and **client secret** credentials from step 1 and set the `INFISICAL_TOKEN` environment variable to the retrieved access token.
|
||||
|
||||
```bash
|
||||
export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=<identity-client-id> --client-secret=<identity-client-secret> --silent --plain) # --plain flag will output only the token, so it can be fed to an environment variable. --silent will disable any update messages.
|
||||
```
|
||||
|
||||
#### Pass as flag
|
||||
You may use the --token flag to set the token
|
||||
The CLI is configured to look out for the `INFISICAL_TOKEN` environment variable, so going forward any command used will be authenticated.
|
||||
|
||||
```
|
||||
infisical export --token=<>
|
||||
infisical secrets --token=<>
|
||||
infisical run --token=<> -- npm run dev
|
||||
```
|
||||
Alternatively, assuming you have an access token on hand, you can also pass it directly to the CLI using the `--token` flag in conjunction with other CLI commands.
|
||||
|
||||
#### Pass via shell environment variable
|
||||
The CLI is configured to look for an environment variable named `INFISICAL_TOKEN`. If set, it'll attempt to use it for authentication.
|
||||
<Info>
|
||||
Keep in mind that the machine identity access token has a limited lifetime. It is recommended to use it only for the duration of the task at hand.
|
||||
You can [refresh the token](./commands/token) if needed.
|
||||
</Info>
|
||||
</Step>
|
||||
<Step title="Export environment variables back into a file">
|
||||
Finally, export the environment variables from Infisical to a file of choice.
|
||||
|
||||
```
|
||||
export INFISICAL_TOKEN=<>
|
||||
```
|
||||
|
||||
```bash
|
||||
# export variables to a .env file (with export keyword)
|
||||
infisical export --format=dotenv-export > .env
|
||||
|
||||
# export variables to a YAML file
|
||||
infisical export --format=yaml > secrets.yaml
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
## Inject environment variables
|
||||
<Tabs>
|
||||
<Tab title="Feed secrets to your application">
|
||||
```bash
|
||||
infisical run --env=dev --path=/apps/firefly -- [your application start command]
|
||||
|
||||
# example with node (nodemon)
|
||||
infisical run --env=staging --path=/apps/spotify -- nodemon index.js
|
||||
|
||||
# example with flask
|
||||
infisical run --env=prod --path=/apps/backend -- flask run
|
||||
|
||||
# example with spring boot - maven
|
||||
infisical run --env=dev --path=/apps/ -- ./mvnw spring-boot:run --quiet
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Feed secrets via custom aliases (advanced)">
|
||||
Custom aliases can utilize secrets from Infisical. Suppose there is a custom alias `yd` in `custom.sh` that runs `yarn dev` and needs the secrets provided by Infisical.
|
||||
```bash
|
||||
#!/bin/sh
|
||||
|
||||
yd() {
|
||||
yarn dev
|
||||
}
|
||||
```
|
||||
|
||||
To make the secrets available from Infisical to `yd`, you can run the following command:
|
||||
|
||||
```bash
|
||||
infisical run --env=prod --path=/apps/reddit --command="source custom.sh && yd"
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
View all available options for `run` command [here](./commands/run)
|
||||
|
||||
## Connect CLI to self hosted Infisical
|
||||
|
||||
<Accordion title="Optional: point CLI to self-hosted">
|
||||
The CLI is set to connect to Infisical Cloud by default, but if you're running your own instance of Infisical, you can direct the CLI to it using one of the methods provided below.
|
||||
|
||||
#### Method 1: Use the updated CLI
|
||||
Beginning with CLI version V0.4.0, it is now possible to choose between logging in through the Infisical cloud or your own self-hosted instance. Simply execute the `infisical login` command and follow the on-screen instructions.
|
||||
|
||||
#### Method 2: Export environment variable
|
||||
You can point the CLI to the self hosted Infisical instance by exporting the environment variable `INFISICAL_API_URL` in your terminal.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Linux/MacOs">
|
||||
```bash
|
||||
# Set backend host
|
||||
export INFISICAL_API_URL="https://your-self-hosted-infisical.com/api"
|
||||
|
||||
# Remove backend host
|
||||
unset INFISICAL_API_URL
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Windows Powershell">
|
||||
```bash
|
||||
# Set backend host
|
||||
setx INFISICAL_API_URL "https://your-self-hosted-infisical.com/api"
|
||||
|
||||
# Remove backend host
|
||||
setx INFISICAL_API_URL ""
|
||||
|
||||
# NOTE: Once set or removed, please restart powershell for the change to take effect
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
#### Method 3: Set manually on every command
|
||||
Another option to point the CLI to your self hosted Infisical instance is to set it via a flag on every command you run.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical <any-command> --domain="https://your-self-hosted-infisical.com/api"
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
## History
|
||||
|
||||
Your terminal keeps a history with the commands you run. When you create Infisical secrets directly from your terminal, they'll stay there for a while.
|
||||
@@ -143,30 +127,101 @@ Your terminal keeps a history with the commands you run. When you create Infisic
|
||||
For security and privacy concerns, we recommend you to configure your terminal to ignore those specific Infisical commands.
|
||||
|
||||
<Accordion title="Ignore commands">
|
||||
<Tabs>
|
||||
<Tab title="Unix/Linux">
|
||||
<Tip>
|
||||
`$HOME/.profile` is pretty common but, you could place it under `$HOME/.profile.d/infisical.sh` or any profile file run at login
|
||||
</Tip>
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Unix/Linux">
|
||||
<Tip>
|
||||
`$HOME/.profile` is pretty common but, you could place it under `$HOME/.profile.d/infisical.sh` or any profile file run at login
|
||||
</Tip>
|
||||
```bash
|
||||
cat <<EOF >> $HOME/.profile && source $HOME/.profile
|
||||
|
||||
# Ignoring specific Infisical CLI commands
|
||||
DEFAULT_HISTIGNORE=$HISTIGNORE
|
||||
export HISTIGNORE="*infisical secrets set*:$DEFAULT_HISTIGNORE"
|
||||
EOF
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="Windows">
|
||||
If you're on WSL, then you can use the Unix/Linux method.
|
||||
|
||||
<Tip>
|
||||
Here's some [documentation](https://superuser.com/a/1658331) about how to clear the terminal history, in PowerShell and CMD
|
||||
</Tip>
|
||||
|
||||
</Tab>
|
||||
|
||||
</Tabs>
|
||||
</Accordion>
|
||||
|
||||
## FAQ
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Can I connect the CLI to my self-hosted Infisical instance?">
|
||||
Yes. The CLI is set to connect to Infisical Cloud by default, but if you're running your own instance of Infisical, you can direct the CLI to it using one of the methods provided below.
|
||||
|
||||
#### Method 1: Use the updated CLI
|
||||
|
||||
Beginning with CLI version V0.4.0, it is now possible to choose between logging in through the Infisical cloud or your own self-hosted instance. Simply execute the `infisical login` command and follow the on-screen instructions.
|
||||
|
||||
#### Method 2: Export environment variable
|
||||
|
||||
You can point the CLI to the self hosted Infisical instance by exporting the environment variable `INFISICAL_API_URL` in your terminal.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Linux/MacOs">
|
||||
```bash
|
||||
cat <<EOF >> $HOME/.profile && source $HOME/.profile
|
||||
# set backend host
|
||||
export INFISICAL_API_URL="https://your-self-hosted-infisical.com/api"
|
||||
|
||||
# Ignoring specific Infisical CLI commands
|
||||
DEFAULT_HISTIGNORE=$HISTIGNORE
|
||||
export HISTIGNORE="*infisical secrets set*:$DEFAULT_HISTIGNORE"
|
||||
EOF
|
||||
# remove backend host
|
||||
unset INFISICAL_API_URL
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="Windows">
|
||||
If you're on WSL, then you can use the Unix/Linux method.
|
||||
</Tab>
|
||||
<Tab title="Windows Powershell">
|
||||
```bash
|
||||
# set backend host
|
||||
setx INFISICAL_API_URL "https://your-self-hosted-infisical.com/api"
|
||||
|
||||
<Tip>
|
||||
Here's some [documentation](https://superuser.com/a/1658331) about how to clear the terminal history, in PowerShell and CMD
|
||||
</Tip>
|
||||
# remove backend host
|
||||
setx INFISICAL_API_URL ""
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Accordion>
|
||||
# NOTE: Once set or removed, please restart powershell for the change to take effect
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
</Tabs>
|
||||
|
||||
#### Method 3: Set manually on every command
|
||||
|
||||
Another option to point the CLI to your self hosted Infisical instance is to set it via a flag on every command you run.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical <any-command> --domain="https://your-self-hosted-infisical.com/api"
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Can I use the CLI with service tokens?">
|
||||
Yes. Please note, however, that service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities). They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys).
|
||||
|
||||
To use Infisical for non local development scenarios, please create a service token. The service token will allow you to authenticate and interact with Infisical. Once you have created a service token with the required permissions, you’ll need to feed the token to the CLI.
|
||||
|
||||
```bash
|
||||
infisical export --token=<service-token>
|
||||
infisical secrets --token=<service-token>
|
||||
infisical run --token=<service-token> -- npm run dev
|
||||
```
|
||||
|
||||
#### Pass via shell environment variable
|
||||
The CLI is configured to look for an environment variable named `INFISICAL_TOKEN`. If set, it’ll attempt to use it for authentication.
|
||||
|
||||
```bash
|
||||
export INFISICAL_TOKEN=<service-token>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
@@ -1,87 +0,0 @@
|
||||
---
|
||||
title: "Kubernetes"
|
||||
---
|
||||
|
||||
The Infisical Secrets Operator fetches secrets from Infisical and saves them as Kubernetes secrets using the custom `InfisicalSecret` resource to define authentication and storage methods.
|
||||
The operator updates secrets continuously and can reload dependent deployments automatically on secret changes.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Connected to your cluster via kubectl
|
||||
- Have a project with secrets ready in [Infisical Cloud](https://app.infisical.com).
|
||||
- Create an [Infisical Token](/documentation/platform/token) scoped to an environment in your project in Infisical.
|
||||
|
||||
## Installation
|
||||
|
||||
Follow the instructions for either [Helm](https://helm.sh/) or [kubectl](https://github.com/kubernetes/kubectl) to install the Infisical Secrets Operator.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Helm">
|
||||
Install the Infisical Helm repository
|
||||
|
||||
```console
|
||||
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
|
||||
|
||||
helm repo update
|
||||
```
|
||||
|
||||
Install the Helm chart
|
||||
```console
|
||||
helm install --generate-name infisical-helm-charts/secrets-operator
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="Kubectl">
|
||||
The operator will be installed in `infisical-operator-system` namespace
|
||||
```
|
||||
kubectl apply -f https://raw.githubusercontent.com/Infisical/infisical/main/k8-operator/kubectl-install/install-secrets-operator.yaml
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
**Step 1: Create Kubernetes secret containing service token**
|
||||
|
||||
Once you have generated the service token, create a Kubernetes secret containing the service token you generated by running the command below.
|
||||
|
||||
``` bash
|
||||
kubectl create secret generic service-token --from-literal=infisicalToken=<your-service-token-here>
|
||||
```
|
||||
|
||||
**Step 2: Fill out the InfisicalSecrets CRD and apply it to your cluster**
|
||||
|
||||
```yaml infisical-secrets-config.yaml
|
||||
apiVersion: secrets.infisical.com/v1alpha1
|
||||
kind: InfisicalSecret
|
||||
metadata:
|
||||
# Name of of this InfisicalSecret resource
|
||||
name: infisicalsecret-sample
|
||||
spec:
|
||||
# The host that should be used to pull secrets from. If left empty, the value specified in Global configuration will be used
|
||||
hostAPI: https://app.infisical.com/api
|
||||
resyncInterval:
|
||||
authentication:
|
||||
serviceToken:
|
||||
serviceTokenSecretReference:
|
||||
secretName: service-token
|
||||
secretNamespace: option
|
||||
secretsScope:
|
||||
envSlug: dev
|
||||
secretsPath: "/"
|
||||
managedSecretReference:
|
||||
secretName: managed-secret # <-- the name of kubernetes secret that will be created
|
||||
secretNamespace: default # <-- where the kubernetes secret should be created
|
||||
```
|
||||
|
||||
```
|
||||
kubectl apply -f infisical-secrets-config.yaml
|
||||
```
|
||||
|
||||
You should now see a new kubernetes secret automatically created in the namespace you defined in the `managedSecretReference` property above.
|
||||
|
||||
See also:
|
||||
|
||||
- [Documentation for the Infisical Kubernetes Operator](../../integrations/platforms/kubernetes)
|
||||
|
82
docs/documentation/platform/audit-log-streams.mdx
Normal file
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>
|
@@ -24,7 +24,7 @@ This approach offers several advantages in terms of security and management:
|
||||
|
||||
- **Scalability**: Dynamic secret management systems can scale more effectively to handle a large number of services and applications, as they automate much of the overhead associated with manual secret management.
|
||||
|
||||
Dynamic secrets are particularly useful in environments with stringent security requirements, such as cloud environments, distributed systems, and microservices architectures, where they help to manage database credentials, API keys, service tokens, and other types of secrets.
|
||||
Dynamic secrets are particularly useful in environments with stringent security requirements, such as cloud environments, distributed systems, and microservices architectures, where they help to manage database credentials, API keys, tokens, and other types of secrets.
|
||||
|
||||
## Infisical Dynamic Secret Templates
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Machine Identities
|
||||
title: Machine Identities
|
||||
description: "Learn how to use Machine Identities to programmatically interact with Infisical."
|
||||
---
|
||||
|
||||
@@ -21,18 +21,17 @@ Key Features:
|
||||
A typical workflow for using identities consists of four steps:
|
||||
|
||||
1. Creating the identity with a name and [role](/documentation/platform/role-based-access-controls) in Organization Access Control > Machine Identities.
|
||||
This step also involves configuring an authentication method for it such as [Universal Auth](/documentation/platform/identities/universal-auth).
|
||||
This step also involves configuring an authentication method for it such as [Universal Auth](/documentation/platform/identities/universal-auth).
|
||||
2. Adding the identity to the project(s) you want it to have access to.
|
||||
3. Authenticating the identity with the Infisical API based on the configured authentication method on it and receiving a short-lived access token back.
|
||||
4. Authenticating subsequent requests with the Infisical API using the short-lived access token.
|
||||
|
||||
|
||||
<Note>
|
||||
Currently, identities can only be used to make authenticated requests to the Infisical API, SDKs, Terraform, Kubernetes Operator, and Infisical Agent. They do not work with clients such as CLI, Ansible look up plugin, etc.
|
||||
|
||||
Machine Identity support for the rest of the clients is planned to be released in the current quarter.
|
||||
</Note>
|
||||
Machine Identity support for the rest of the clients is planned to be released in the current quarter.
|
||||
|
||||
</Note>
|
||||
|
||||
## Authentication Methods
|
||||
|
||||
@@ -43,8 +42,16 @@ To interact with various resources in Infisical, Machine Identities are able to
|
||||
## FAQ
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Can I use machine identities with the CLI?">
|
||||
|
||||
Yes - Identities can be used with the CLI.
|
||||
|
||||
You can learn more about how to do this in the CLI quickstart [here](/cli/usage).
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="What is the difference between an identity and service token?">
|
||||
A service token is a project-level authentication method that is being phased out in favor of identities.
|
||||
A service token is a project-level authentication method that is being deprecated in favor of identities. The service token method will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys).
|
||||
|
||||
Amongst many differences, identities provide broader access over the Infisical API, utilizes the same
|
||||
permission system as user identities, and come with a significantly larger number of configurable authentication and security features.
|
||||
|
@@ -17,7 +17,6 @@ Upon being added to an organization and projects, users assume a certain set of
|
||||
|
||||
To interact with various resources in Infisical, users are able to utilize a number of authentication methods:
|
||||
- **Email & Password**: the most common authentication method that is used for authentication into Web Dashboard and Infisical CLI. It is recommended to utilize [Multi-factor Authentication](/documentation/platform/mfa) in addition to it.
|
||||
- **Service Tokens**: Service tokens allow users authenticate into CLI and other clients under their own identity. For the majority of use cases, it is not a recommended approach. Instead, it is often a good idea to utilize [Machine Identities](./machine-identities) with [Universal Authentication](/documentation/platform/identities/universal-auth).
|
||||
- **SSO**: Infisical natively integrates with a number of SSO identity providers like [Google](/documentation/platform/sso/google), [GitHub](/documentation/platform/sso/github), and [GitLab](/documentation/platform/sso/gitlab).
|
||||
- **SAML SSO**: It is also possible to set up SAML SSO integration with identity providers like [Okta](/documentation/platform/sso/okta), [Microsoft Entra ID](/documentation/platform/sso/azure) (formerly known as Azure AD), [JumpCloud](/documentation/platform/sso/jumpcloud), [Google](/documentation/platform/sso/google-saml), and more.
|
||||
- **LDAP**: For organizations with more advanced needs, Infisical also provides user authentication with [LDAP](/documentation/platform/ldap/overview) that includes a number of LDAP providers.
|
||||
|
@@ -1,38 +0,0 @@
|
||||
---
|
||||
title: "IP Allowlisting"
|
||||
description: "Restrict access to your secrets in Infisical using trusted IPs"
|
||||
---
|
||||
|
||||
<Warning>
|
||||
IP allowlisting at the project-level is being replaced with IP allowlisting at the token-level now available with the Service Token V3 authentication method.
|
||||
|
||||
Instead of providing trusted IPs (specific IPs and CIDR ranges) to be applied across all service tokens,
|
||||
you can now specify trusted IPs at the token-level.
|
||||
|
||||
</Warning>
|
||||
<Info>
|
||||
Note that IP Allowlisting is a paid feature.
|
||||
|
||||
If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical,
|
||||
then you should contact sales@infisical.com to purchase an enterprise license to use it.
|
||||
</Info>
|
||||
|
||||
Projects in Infisical can be configured to restrict client access to specific IP addresses or CIDR ranges. This applies to any client using service tokens and
|
||||
can be useful, for example, for limiting access to traffic coming from corporate networks.
|
||||
|
||||
By default, each project is initialized with the `0.0.0.0/0` entry, representing all possible IPv4 addresses.
|
||||
For enhanced security, we strongly recommend replacing the default entry with your client IPs to tighten access to your secrets.
|
||||
|
||||
<Note>
|
||||
You must be a project `admin` to manage your project's IP whitelist.
|
||||
</Note>
|
||||
|
||||

|
||||
|
||||
## Creating a trusted IP entry
|
||||
|
||||
To create a trusted IP entry, head over to the **IP Whitelist** tab in your project. When creating an entry,
|
||||
you can specify either a specific IP address like `192.0.2.1` or a CIDR range like `2001:db8::/32`; both IPv4 and IPv6
|
||||
formats are accepted.
|
||||
|
||||

|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user