mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-02 16:55:02 +00:00
Compare commits
346 Commits
daniel/upd
...
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 |
@ -3,3 +3,4 @@ frontend/src/views/Project/MembersPage/components/IdentityTab/components/Identit
|
|||||||
frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:304
|
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/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
|
||||||
|
4
backend/src/@types/fastify.d.ts
vendored
4
backend/src/@types/fastify.d.ts
vendored
@ -1,6 +1,8 @@
|
|||||||
import "fastify";
|
import "fastify";
|
||||||
|
|
||||||
import { TUsers } from "@app/db/schemas";
|
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 { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
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 { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
|
||||||
@ -113,6 +115,8 @@ declare module "fastify" {
|
|||||||
identityAccessToken: TIdentityAccessTokenServiceFactory;
|
identityAccessToken: TIdentityAccessTokenServiceFactory;
|
||||||
identityProject: TIdentityProjectServiceFactory;
|
identityProject: TIdentityProjectServiceFactory;
|
||||||
identityUa: TIdentityUaServiceFactory;
|
identityUa: TIdentityUaServiceFactory;
|
||||||
|
accessApprovalPolicy: TAccessApprovalPolicyServiceFactory;
|
||||||
|
accessApprovalRequest: TAccessApprovalRequestServiceFactory;
|
||||||
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
|
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
|
||||||
secretApprovalRequest: TSecretApprovalRequestServiceFactory;
|
secretApprovalRequest: TSecretApprovalRequestServiceFactory;
|
||||||
secretRotation: TSecretRotationServiceFactory;
|
secretRotation: TSecretRotationServiceFactory;
|
||||||
|
45
backend/src/@types/knex.d.ts
vendored
45
backend/src/@types/knex.d.ts
vendored
@ -2,6 +2,18 @@ import { Knex } from "knex";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
TableName,
|
TableName,
|
||||||
|
TAccessApprovalPolicies,
|
||||||
|
TAccessApprovalPoliciesApprovers,
|
||||||
|
TAccessApprovalPoliciesApproversInsert,
|
||||||
|
TAccessApprovalPoliciesApproversUpdate,
|
||||||
|
TAccessApprovalPoliciesInsert,
|
||||||
|
TAccessApprovalPoliciesUpdate,
|
||||||
|
TAccessApprovalRequests,
|
||||||
|
TAccessApprovalRequestsInsert,
|
||||||
|
TAccessApprovalRequestsReviewers,
|
||||||
|
TAccessApprovalRequestsReviewersInsert,
|
||||||
|
TAccessApprovalRequestsReviewersUpdate,
|
||||||
|
TAccessApprovalRequestsUpdate,
|
||||||
TApiKeys,
|
TApiKeys,
|
||||||
TApiKeysInsert,
|
TApiKeysInsert,
|
||||||
TApiKeysUpdate,
|
TApiKeysUpdate,
|
||||||
@ -38,6 +50,9 @@ import {
|
|||||||
TGroupProjectMemberships,
|
TGroupProjectMemberships,
|
||||||
TGroupProjectMembershipsInsert,
|
TGroupProjectMembershipsInsert,
|
||||||
TGroupProjectMembershipsUpdate,
|
TGroupProjectMembershipsUpdate,
|
||||||
|
TGroupProjectUserAdditionalPrivilege,
|
||||||
|
TGroupProjectUserAdditionalPrivilegeInsert,
|
||||||
|
TGroupProjectUserAdditionalPrivilegeUpdate,
|
||||||
TGroups,
|
TGroups,
|
||||||
TGroupsInsert,
|
TGroupsInsert,
|
||||||
TGroupsUpdate,
|
TGroupsUpdate,
|
||||||
@ -278,6 +293,11 @@ declare module "knex/types/tables" {
|
|||||||
TProjectUserMembershipRolesInsert,
|
TProjectUserMembershipRolesInsert,
|
||||||
TProjectUserMembershipRolesUpdate
|
TProjectUserMembershipRolesUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.GroupProjectUserAdditionalPrivilege]: Knex.CompositeTableType<
|
||||||
|
TGroupProjectUserAdditionalPrivilege,
|
||||||
|
TGroupProjectUserAdditionalPrivilegeInsert,
|
||||||
|
TGroupProjectUserAdditionalPrivilegeUpdate
|
||||||
|
>;
|
||||||
[TableName.ProjectRoles]: Knex.CompositeTableType<TProjectRoles, TProjectRolesInsert, TProjectRolesUpdate>;
|
[TableName.ProjectRoles]: Knex.CompositeTableType<TProjectRoles, TProjectRolesInsert, TProjectRolesUpdate>;
|
||||||
[TableName.ProjectUserAdditionalPrivilege]: Knex.CompositeTableType<
|
[TableName.ProjectUserAdditionalPrivilege]: Knex.CompositeTableType<
|
||||||
TProjectUserAdditionalPrivilege,
|
TProjectUserAdditionalPrivilege,
|
||||||
@ -344,6 +364,31 @@ declare module "knex/types/tables" {
|
|||||||
TIdentityProjectAdditionalPrivilegeInsert,
|
TIdentityProjectAdditionalPrivilegeInsert,
|
||||||
TIdentityProjectAdditionalPrivilegeUpdate
|
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.ScimToken]: Knex.CompositeTableType<TScimTokens, TScimTokensInsert, TScimTokensUpdate>;
|
||||||
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
|
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
|
||||||
TSecretApprovalPolicies,
|
TSecretApprovalPolicies,
|
||||||
|
@ -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>
|
||||||
|
>;
|
@ -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,3 +1,7 @@
|
|||||||
|
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 "./api-keys";
|
||||||
export * from "./audit-log-streams";
|
export * from "./audit-log-streams";
|
||||||
export * from "./audit-logs";
|
export * from "./audit-logs";
|
||||||
@ -10,6 +14,7 @@ export * from "./git-app-install-sessions";
|
|||||||
export * from "./git-app-org";
|
export * from "./git-app-org";
|
||||||
export * from "./group-project-membership-roles";
|
export * from "./group-project-membership-roles";
|
||||||
export * from "./group-project-memberships";
|
export * from "./group-project-memberships";
|
||||||
|
export * from "./group-project-user-additional-privilege";
|
||||||
export * from "./groups";
|
export * from "./groups";
|
||||||
export * from "./identities";
|
export * from "./identities";
|
||||||
export * from "./identity-access-tokens";
|
export * from "./identity-access-tokens";
|
||||||
|
@ -25,6 +25,7 @@ export enum TableName {
|
|||||||
ProjectMembership = "project_memberships",
|
ProjectMembership = "project_memberships",
|
||||||
ProjectRoles = "project_roles",
|
ProjectRoles = "project_roles",
|
||||||
ProjectUserAdditionalPrivilege = "project_user_additional_privilege",
|
ProjectUserAdditionalPrivilege = "project_user_additional_privilege",
|
||||||
|
GroupProjectUserAdditionalPrivilege = "group_project_user_additional_privilege",
|
||||||
ProjectUserMembershipRole = "project_user_membership_roles",
|
ProjectUserMembershipRole = "project_user_membership_roles",
|
||||||
ProjectKeys = "project_keys",
|
ProjectKeys = "project_keys",
|
||||||
Secret = "secrets",
|
Secret = "secrets",
|
||||||
@ -50,6 +51,10 @@ export enum TableName {
|
|||||||
IdentityProjectMembershipRole = "identity_project_membership_role",
|
IdentityProjectMembershipRole = "identity_project_membership_role",
|
||||||
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
|
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
|
||||||
ScimToken = "scim_tokens",
|
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",
|
SecretApprovalPolicy = "secret_approval_policies",
|
||||||
SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
|
SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
|
||||||
SecretApprovalRequest = "secret_approval_requests",
|
SecretApprovalRequest = "secret_approval_requests",
|
||||||
|
@ -9,10 +9,11 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
|
|
||||||
export const SecretApprovalPoliciesApproversSchema = z.object({
|
export const SecretApprovalPoliciesApproversSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
approverId: z.string().uuid(),
|
approverId: z.string().uuid().nullable().optional(),
|
||||||
policyId: z.string().uuid(),
|
policyId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
approverUserId: z.string().uuid().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretApprovalPoliciesApprovers = z.infer<typeof SecretApprovalPoliciesApproversSchema>;
|
export type TSecretApprovalPoliciesApprovers = z.infer<typeof SecretApprovalPoliciesApproversSchema>;
|
||||||
|
@ -9,11 +9,12 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
|
|
||||||
export const SecretApprovalRequestsReviewersSchema = z.object({
|
export const SecretApprovalRequestsReviewersSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
member: z.string().uuid(),
|
member: z.string().uuid().nullable().optional(),
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
requestId: z.string().uuid(),
|
requestId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
memberUserId: z.string().uuid().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretApprovalRequestsReviewers = z.infer<typeof SecretApprovalRequestsReviewersSchema>;
|
export type TSecretApprovalRequestsReviewers = z.infer<typeof SecretApprovalRequestsReviewersSchema>;
|
||||||
|
@ -16,9 +16,11 @@ export const SecretApprovalRequestsSchema = z.object({
|
|||||||
slug: z.string(),
|
slug: z.string(),
|
||||||
folderId: z.string().uuid(),
|
folderId: z.string().uuid(),
|
||||||
statusChangeBy: z.string().uuid().nullable().optional(),
|
statusChangeBy: z.string().uuid().nullable().optional(),
|
||||||
committerId: z.string().uuid(),
|
committerId: z.string().uuid().nullable().optional(),
|
||||||
createdAt: z.date(),
|
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>;
|
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 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -1,4 +1,6 @@
|
|||||||
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
|
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 { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
||||||
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
||||||
import { registerGroupRouter } from "./group-router";
|
import { registerGroupRouter } from "./group-router";
|
||||||
@ -41,6 +43,9 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
prefix: "/secret-rotation-providers"
|
prefix: "/secret-rotation-providers"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await server.register(registerAccessApprovalPolicyRouter, { prefix: "/access-approvals/policies" });
|
||||||
|
await server.register(registerAccessApprovalRequestRouter, { prefix: "/access-approvals/requests" });
|
||||||
|
|
||||||
await server.register(
|
await server.register(
|
||||||
async (dynamicSecretRouter) => {
|
async (dynamicSecretRouter) => {
|
||||||
await dynamicSecretRouter.register(registerDynamicSecretRouter);
|
await dynamicSecretRouter.register(registerDynamicSecretRouter);
|
||||||
|
@ -130,7 +130,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
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: {
|
response: {
|
||||||
200: z.object({
|
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,
|
type: isClosing ? EventType.SECRET_APPROVAL_CLOSED : EventType.SECRET_APPROVAL_REOPENED,
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
metadata: {
|
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,
|
secretApprovalRequestId: approval.id,
|
||||||
secretApprovalRequestSlug: approval.slug
|
secretApprovalRequestSlug: approval.slug
|
||||||
// eslint-disable-next-line
|
// 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">;
|
@ -625,9 +625,9 @@ interface SecretApprovalReopened {
|
|||||||
interface SecretApprovalRequest {
|
interface SecretApprovalRequest {
|
||||||
type: EventType.SECRET_APPROVAL_REQUEST;
|
type: EventType.SECRET_APPROVAL_REQUEST;
|
||||||
metadata: {
|
metadata: {
|
||||||
committedBy: string;
|
|
||||||
secretApprovalRequestSlug: string;
|
secretApprovalRequestSlug: string;
|
||||||
secretApprovalRequestId: 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 { DatabaseError } from "@app/lib/errors";
|
||||||
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex";
|
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex";
|
||||||
|
|
||||||
|
import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal";
|
||||||
|
|
||||||
export type TGroupDALFactory = ReturnType<typeof groupDALFactory>;
|
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 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> = {}) => {
|
const findGroups = async (filter: TFindFilter<TGroups>, { offset, limit, sort, tx }: TFindOpt<TGroups> = {}) => {
|
||||||
try {
|
try {
|
||||||
@ -122,9 +190,10 @@ export const groupDALFactory = (db: TDbClient) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
...groupOrm,
|
||||||
findGroups,
|
findGroups,
|
||||||
findByOrgId,
|
findByOrgId,
|
||||||
findAllGroupMembers,
|
findAllGroupMembers,
|
||||||
...groupOrm
|
delete: deleteMany
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -266,6 +266,9 @@ export const removeUsersFromGroupByUserIds = async ({
|
|||||||
userIds,
|
userIds,
|
||||||
userDAL,
|
userDAL,
|
||||||
userGroupMembershipDAL,
|
userGroupMembershipDAL,
|
||||||
|
accessApprovalRequestDAL,
|
||||||
|
secretApprovalRequestDAL,
|
||||||
|
secretApprovalPolicyDAL,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
tx: outerTx
|
tx: outerTx
|
||||||
@ -322,20 +325,16 @@ export const removeUsersFromGroupByUserIds = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (membersToRemoveFromGroupNonPending.length) {
|
if (membersToRemoveFromGroupNonPending.length) {
|
||||||
// check which projects the group is part of
|
const groupProjectMemberships = await groupProjectDAL.find(
|
||||||
const projectIds = Array.from(
|
|
||||||
new Set(
|
|
||||||
(
|
|
||||||
await groupProjectDAL.find(
|
|
||||||
{
|
{
|
||||||
groupId: group.id
|
groupId: group.id
|
||||||
},
|
},
|
||||||
{ tx }
|
{ tx }
|
||||||
)
|
|
||||||
).map((gp) => gp.projectId)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 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
|
// TODO: this part can be optimized
|
||||||
for await (const userId of userIds) {
|
for await (const userId of userIds) {
|
||||||
const t = await userGroupMembershipDAL.filterProjectsByUserMembership(userId, group.id, projectIds, tx);
|
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(
|
await userGroupMembershipDAL.delete(
|
||||||
{
|
{
|
||||||
groupId: group.id,
|
groupId: group.id,
|
||||||
userId
|
$in: {
|
||||||
|
userId: membersToRemoveFromGroupNonPending.map((member) => member.id)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@ -364,12 +388,15 @@ export const removeUsersFromGroupByUserIds = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (membersToRemoveFromGroupPending.length) {
|
if (membersToRemoveFromGroupPending.length) {
|
||||||
await userGroupMembershipDAL.delete({
|
await userGroupMembershipDAL.delete(
|
||||||
|
{
|
||||||
groupId: group.id,
|
groupId: group.id,
|
||||||
$in: {
|
$in: {
|
||||||
userId: membersToRemoveFromGroupPending.map((member) => member.id)
|
userId: membersToRemoveFromGroupPending.map((member) => member.id)
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return membersToRemoveFromGroupNonPending.concat(membersToRemoveFromGroupPending);
|
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 { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||||
import { TUserDALFactory } from "@app/services/user/user-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 { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
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 { TGroupDALFactory } from "./group-dal";
|
||||||
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "./group-fns";
|
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "./group-fns";
|
||||||
import {
|
import {
|
||||||
@ -41,6 +44,9 @@ type TGroupServiceFactoryDep = {
|
|||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "delete">;
|
||||||
|
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "delete">;
|
||||||
|
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findByProjectIds">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGroupServiceFactory = ReturnType<typeof groupServiceFactory>;
|
export type TGroupServiceFactory = ReturnType<typeof groupServiceFactory>;
|
||||||
@ -50,6 +56,9 @@ export const groupServiceFactory = ({
|
|||||||
groupDAL,
|
groupDAL,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
|
secretApprovalRequestDAL,
|
||||||
|
secretApprovalPolicyDAL,
|
||||||
|
accessApprovalRequestDAL,
|
||||||
userGroupMembershipDAL,
|
userGroupMembershipDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
@ -328,6 +337,9 @@ export const groupServiceFactory = ({
|
|||||||
group,
|
group,
|
||||||
userIds: [user.id],
|
userIds: [user.id],
|
||||||
userDAL,
|
userDAL,
|
||||||
|
accessApprovalRequestDAL,
|
||||||
|
secretApprovalPolicyDAL,
|
||||||
|
secretApprovalRequestDAL,
|
||||||
userGroupMembershipDAL,
|
userGroupMembershipDAL,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
projectKeyDAL
|
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 { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||||
import { TUserDALFactory } from "@app/services/user/user-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 = {
|
export type TCreateGroupDTO = {
|
||||||
name: string;
|
name: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
@ -77,6 +81,9 @@ export type TRemoveUsersFromGroupByUserIds = {
|
|||||||
group: TGroups;
|
group: TGroups;
|
||||||
userIds: string[];
|
userIds: string[];
|
||||||
userDAL: Pick<TUserDALFactory, "find" | "transaction">;
|
userDAL: Pick<TUserDALFactory, "find" | "transaction">;
|
||||||
|
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "delete">;
|
||||||
|
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "delete">;
|
||||||
|
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findByProjectIds">;
|
||||||
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "filterProjectsByUserMembership" | "delete">;
|
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "filterProjectsByUserMembership" | "delete">;
|
||||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "delete">;
|
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 { normalizeUsername } from "@app/services/user/user-fns";
|
||||||
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
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 { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
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 { TLdapConfigDALFactory } from "./ldap-config-dal";
|
||||||
import {
|
import {
|
||||||
TCreateLdapCfgDTO,
|
TCreateLdapCfgDTO,
|
||||||
@ -67,6 +70,9 @@ type TLdapConfigServiceFactoryDep = {
|
|||||||
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "delete">;
|
||||||
|
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "delete">;
|
||||||
|
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findByProjectIds">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TLdapConfigServiceFactory = ReturnType<typeof ldapConfigServiceFactory>;
|
export type TLdapConfigServiceFactory = ReturnType<typeof ldapConfigServiceFactory>;
|
||||||
@ -78,6 +84,9 @@ export const ldapConfigServiceFactory = ({
|
|||||||
orgBotDAL,
|
orgBotDAL,
|
||||||
groupDAL,
|
groupDAL,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
|
accessApprovalRequestDAL,
|
||||||
|
secretApprovalPolicyDAL,
|
||||||
|
secretApprovalRequestDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
@ -524,7 +533,10 @@ export const ldapConfigServiceFactory = ({
|
|||||||
group,
|
group,
|
||||||
userIds: [newUser.id],
|
userIds: [newUser.id],
|
||||||
userDAL,
|
userDAL,
|
||||||
|
secretApprovalRequestDAL,
|
||||||
|
accessApprovalRequestDAL,
|
||||||
userGroupMembershipDAL,
|
userGroupMembershipDAL,
|
||||||
|
secretApprovalPolicyDAL,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
tx
|
tx
|
||||||
|
@ -121,8 +121,8 @@ export const licenseServiceFactory = ({
|
|||||||
|
|
||||||
if (isValidOfflineLicense) {
|
if (isValidOfflineLicense) {
|
||||||
onPremFeatures = contents.license.features;
|
onPremFeatures = contents.license.features;
|
||||||
instanceType = InstanceType.EnterpriseOnPrem;
|
instanceType = InstanceType.EnterpriseOnPremOffline;
|
||||||
logger.info(`Instance type: ${InstanceType.EnterpriseOnPrem}`);
|
logger.info(`Instance type: ${InstanceType.EnterpriseOnPremOffline}`);
|
||||||
isValidLicense = true;
|
isValidLicense = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import { TOrgPermission } from "@app/lib/types";
|
|||||||
export enum InstanceType {
|
export enum InstanceType {
|
||||||
OnPrem = "self-hosted",
|
OnPrem = "self-hosted",
|
||||||
EnterpriseOnPrem = "enterprise-self-hosted",
|
EnterpriseOnPrem = "enterprise-self-hosted",
|
||||||
|
EnterpriseOnPremOffline = "enterprise-self-hosted-offline",
|
||||||
Cloud = "cloud"
|
Cloud = "cloud"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,6 +62,11 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
|
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
|
||||||
`${TableName.GroupProjectMembership}.id`
|
`${TableName.GroupProjectMembership}.id`
|
||||||
)
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.GroupProjectUserAdditionalPrivilege,
|
||||||
|
`${TableName.GroupProjectUserAdditionalPrivilege}.groupProjectMembershipId`,
|
||||||
|
`${TableName.GroupProjectMembership}.id`
|
||||||
|
)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.ProjectRoles,
|
TableName.ProjectRoles,
|
||||||
`${TableName.GroupProjectMembershipRole}.customRoleId`,
|
`${TableName.GroupProjectMembershipRole}.customRoleId`,
|
||||||
@ -77,11 +82,34 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("projectId").withSchema(TableName.GroupProjectMembership),
|
db.ref("projectId").withSchema(TableName.GroupProjectMembership),
|
||||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||||
db.ref("orgId").withSchema(TableName.Project),
|
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(
|
.join(
|
||||||
TableName.ProjectUserMembershipRole,
|
TableName.ProjectUserMembershipRole,
|
||||||
`${TableName.ProjectUserMembershipRole}.projectMembershipId`,
|
`${TableName.ProjectUserMembershipRole}.projectMembershipId`,
|
||||||
@ -127,7 +155,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const permission = sqlNestRelationships({
|
const permission = sqlNestRelationships({
|
||||||
data: docs,
|
data: projectMemberDocs,
|
||||||
key: "projectId",
|
key: "projectId",
|
||||||
parentMapper: ({ orgId, orgAuthEnforced, membershipId, membershipCreatedAt, membershipUpdatedAt }) => ({
|
parentMapper: ({ orgId, orgAuthEnforced, membershipId, membershipCreatedAt, membershipUpdatedAt }) => ({
|
||||||
orgId,
|
orgId,
|
||||||
@ -194,6 +222,33 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
permissions: z.unknown(),
|
permissions: z.unknown(),
|
||||||
customRoleSlug: z.string().optional().nullable()
|
customRoleSlug: z.string().optional().nullable()
|
||||||
}).parse(data)
|
}).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)
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
) ?? [];
|
) ?? [];
|
||||||
|
|
||||||
const activeAdditionalPrivileges = permission?.[0]?.additionalPrivileges?.filter(
|
const activeAdditionalPrivileges =
|
||||||
|
permission?.[0]?.additionalPrivileges?.filter(
|
||||||
({ isTemporary, temporaryAccessEndTime }) =>
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < 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 {
|
return {
|
||||||
...(permission[0] || groupPermission[0]),
|
...(permission[0] || groupPermission[0]),
|
||||||
roles: [...activeRoles, ...activeGroupRoles],
|
roles: [...activeRoles, ...activeGroupRoles],
|
||||||
additionalPrivileges: activeAdditionalPrivileges
|
additionalPrivileges: [...activeAdditionalPrivileges, ...activeGroupAdditionalPrivileges]
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "GetProjectPermission" });
|
throw new DatabaseError({ error, name: "GetProjectPermission" });
|
||||||
|
@ -90,6 +90,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
|||||||
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
|
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
|
||||||
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
|
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);
|
const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
|
||||||
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
||||||
|
|
||||||
@ -138,6 +142,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
|||||||
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
|
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
|
||||||
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
|
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);
|
const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
|
||||||
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
||||||
|
|
||||||
@ -164,6 +172,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
|||||||
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
|
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
|
||||||
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
|
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);
|
const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
|
||||||
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
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 { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
|
import { TAccessApprovalRequestDALFactory } from "../access-approval-request/access-approval-request-dal";
|
||||||
import { TLicenseServiceFactory } from "../license/license-service";
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
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 { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList } from "./scim-fns";
|
||||||
import {
|
import {
|
||||||
TCreateScimGroupDTO,
|
TCreateScimGroupDTO,
|
||||||
@ -64,6 +67,9 @@ type TScimServiceFactoryDep = {
|
|||||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
|
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "delete">;
|
||||||
|
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "delete">;
|
||||||
|
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findByProjectIds">;
|
||||||
smtpService: TSmtpService;
|
smtpService: TSmtpService;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -81,6 +87,9 @@ export const scimServiceFactory = ({
|
|||||||
userGroupMembershipDAL,
|
userGroupMembershipDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
|
accessApprovalRequestDAL,
|
||||||
|
secretApprovalRequestDAL,
|
||||||
|
secretApprovalPolicyDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
smtpService
|
smtpService
|
||||||
}: TScimServiceFactoryDep) => {
|
}: TScimServiceFactoryDep) => {
|
||||||
@ -710,6 +719,9 @@ export const scimServiceFactory = ({
|
|||||||
userIds: toRemoveUserIds,
|
userIds: toRemoveUserIds,
|
||||||
userDAL,
|
userDAL,
|
||||||
userGroupMembershipDAL,
|
userGroupMembershipDAL,
|
||||||
|
secretApprovalPolicyDAL,
|
||||||
|
accessApprovalRequestDAL,
|
||||||
|
secretApprovalRequestDAL,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
tx
|
tx
|
||||||
|
@ -20,7 +20,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SecretApprovalPolicy}.id`,
|
`${TableName.SecretApprovalPolicy}.id`,
|
||||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
`${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("name").withSchema(TableName.Environment).as("envName"))
|
||||||
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
|
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
|
||||||
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
|
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
|
||||||
@ -33,18 +33,18 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
const doc = await sapFindQuery(tx || db, {
|
const doc = await sapFindQuery(tx || db, {
|
||||||
[`${TableName.SecretApprovalPolicy}.id` as "id"]: id
|
[`${TableName.SecretApprovalPolicy}.id` as "id"]: id
|
||||||
});
|
});
|
||||||
const formatedDoc = mergeOneToManyRelation(
|
const formattedDoc = mergeOneToManyRelation(
|
||||||
doc,
|
doc,
|
||||||
"id",
|
"id",
|
||||||
({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({
|
({ approverUserId, envId, envName: name, envSlug: slug, ...el }) => ({
|
||||||
...el,
|
...el,
|
||||||
envId,
|
envId,
|
||||||
environment: { id: envId, name, slug }
|
environment: { id: envId, name, slug }
|
||||||
}),
|
}),
|
||||||
({ approverId }) => approverId,
|
({ approverUserId }) => approverUserId,
|
||||||
"approvers"
|
"approvers"
|
||||||
);
|
);
|
||||||
return formatedDoc?.[0];
|
return formattedDoc?.[0];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "FindById" });
|
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) => {
|
const find = async (filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const docs = await sapFindQuery(tx || db, filter);
|
const docs = await sapFindQuery(tx || db, filter);
|
||||||
const formatedDoc = mergeOneToManyRelation(
|
const formattedDoc = mergeOneToManyRelation(
|
||||||
docs,
|
docs,
|
||||||
"id",
|
"id",
|
||||||
({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({
|
({ approverUserId, envId, envName: name, envSlug: slug, ...el }) => ({
|
||||||
...el,
|
...el,
|
||||||
envId,
|
envId,
|
||||||
environment: { id: envId, name, slug }
|
environment: { id: envId, name, slug }
|
||||||
}),
|
}),
|
||||||
({ approverId }) => approverId,
|
({ approverUserId }) => approverUserId,
|
||||||
"approvers"
|
"approvers"
|
||||||
);
|
);
|
||||||
return formatedDoc;
|
return formattedDoc;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find" });
|
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 { containsGlobPatterns } from "@app/lib/picomatch";
|
||||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-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 { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal";
|
||||||
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
|
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
|
||||||
@ -29,6 +30,7 @@ type TSecretApprovalPolicyServiceFactoryDep = {
|
|||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||||
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
|
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
||||||
|
userDAL: Pick<TUserDALFactory, "findUsersByProjectId" | "findUserByProjectId">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretApprovalPolicyServiceFactory = ReturnType<typeof secretApprovalPolicyServiceFactory>;
|
export type TSecretApprovalPolicyServiceFactory = ReturnType<typeof secretApprovalPolicyServiceFactory>;
|
||||||
@ -38,7 +40,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
permissionService,
|
permissionService,
|
||||||
secretApprovalPolicyApproverDAL,
|
secretApprovalPolicyApproverDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
projectMembershipDAL
|
userDAL
|
||||||
}: TSecretApprovalPolicyServiceFactoryDep) => {
|
}: TSecretApprovalPolicyServiceFactoryDep) => {
|
||||||
const createSecretApprovalPolicy = async ({
|
const createSecretApprovalPolicy = async ({
|
||||||
name,
|
name,
|
||||||
@ -69,11 +71,12 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
const env = await projectEnvDAL.findOne({ slug: environment, projectId });
|
const env = await projectEnvDAL.findOne({ slug: environment, projectId });
|
||||||
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
||||||
|
|
||||||
const secretApprovers = await projectMembershipDAL.find({
|
const secretApproverUsers = await userDAL.findUsersByProjectId(
|
||||||
projectId,
|
projectId,
|
||||||
$in: { id: approvers }
|
approvers.map((approverUserId) => approverUserId)
|
||||||
});
|
);
|
||||||
if (secretApprovers.length !== approvers.length)
|
|
||||||
|
if (secretApproverUsers.length !== approvers.length)
|
||||||
throw new BadRequestError({ message: "Approver not found in project" });
|
throw new BadRequestError({ message: "Approver not found in project" });
|
||||||
|
|
||||||
const secretApproval = await secretApprovalPolicyDAL.transaction(async (tx) => {
|
const secretApproval = await secretApprovalPolicyDAL.transaction(async (tx) => {
|
||||||
@ -87,8 +90,8 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
await secretApprovalPolicyApproverDAL.insertMany(
|
await secretApprovalPolicyApproverDAL.insertMany(
|
||||||
secretApprovers.map(({ id }) => ({
|
secretApproverUsers.map(({ id }) => ({
|
||||||
approverId: id,
|
approverUserId: id,
|
||||||
policyId: doc.id
|
policyId: doc.id
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
@ -132,21 +135,19 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
if (approvers) {
|
if (approvers) {
|
||||||
const secretApprovers = await projectMembershipDAL.find(
|
const secretApproverUsers = await userDAL.findUsersByProjectId(
|
||||||
{
|
secretApprovalPolicy.projectId,
|
||||||
projectId: secretApprovalPolicy.projectId,
|
approvers.map((approverUserId) => approverUserId)
|
||||||
$in: { id: approvers }
|
|
||||||
},
|
|
||||||
{ tx }
|
|
||||||
);
|
);
|
||||||
if (secretApprovers.length !== approvers.length)
|
|
||||||
|
if (secretApproverUsers.length !== approvers.length)
|
||||||
throw new BadRequestError({ message: "Approver not found in project" });
|
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" });
|
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||||
await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
||||||
await secretApprovalPolicyApproverDAL.insertMany(
|
await secretApprovalPolicyApproverDAL.insertMany(
|
||||||
secretApprovers.map(({ id }) => ({
|
secretApproverUsers.map((user) => ({
|
||||||
approverId: id,
|
approverUserId: user.id,
|
||||||
policyId: doc.id
|
policyId: doc.id
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
|
@ -16,7 +16,7 @@ export type TSecretApprovalRequestDALFactory = ReturnType<typeof secretApprovalR
|
|||||||
|
|
||||||
type TFindQueryFilter = {
|
type TFindQueryFilter = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
membershipId: string;
|
actorId: string;
|
||||||
status?: RequestState;
|
status?: RequestState;
|
||||||
environment?: string;
|
environment?: string;
|
||||||
committer?: string;
|
committer?: string;
|
||||||
@ -49,7 +49,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
)
|
)
|
||||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||||
.select(
|
.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("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
|
||||||
tx.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"),
|
tx.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"),
|
||||||
tx.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"),
|
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("slug").withSchema(TableName.Environment).as("environment"),
|
||||||
tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
||||||
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
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) => {
|
const findById = async (id: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const sql = findQuery({ [`${TableName.SecretApprovalRequest}.id` as "id"]: id }, tx || db);
|
const sql = findQuery({ [`${TableName.SecretApprovalRequest}.id` as "id"]: id }, tx || db);
|
||||||
const docs = await sql;
|
const docs = await sql;
|
||||||
const formatedDoc = sqlNestRelationships({
|
const formattedDoc = sqlNestRelationships({
|
||||||
data: docs,
|
data: docs,
|
||||||
key: "id",
|
key: "id",
|
||||||
parentMapper: (el) => ({
|
parentMapper: (el) => ({
|
||||||
@ -84,20 +84,20 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
label: "reviewers" as const,
|
label: "reviewers" as const,
|
||||||
mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined)
|
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 {
|
return {
|
||||||
...formatedDoc[0],
|
...formattedDoc[0],
|
||||||
policy: { ...formatedDoc[0].policy, approvers: formatedDoc[0].approvers }
|
policy: { ...formattedDoc[0].policy, approvers: formattedDoc[0].approvers }
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "FindByIdSAR" });
|
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 {
|
try {
|
||||||
const docs = await (tx || db)
|
const docs = await (tx || db)
|
||||||
.with(
|
.with(
|
||||||
@ -110,12 +110,12 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SecretApprovalRequest}.policyId`,
|
`${TableName.SecretApprovalRequest}.policyId`,
|
||||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
||||||
)
|
)
|
||||||
.where({ projectId })
|
.where({ [`${TableName.Environment}.projectId` as "projectId"]: projectId })
|
||||||
.andWhere(
|
.andWhere(
|
||||||
(bd) =>
|
(bd) =>
|
||||||
void bd
|
void bd
|
||||||
.where(`${TableName.SecretApprovalPolicyApprover}.approverId`, membershipId)
|
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, approverUserId)
|
||||||
.orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId)
|
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, approverUserId)
|
||||||
)
|
)
|
||||||
.select("status", `${TableName.SecretApprovalRequest}.id`)
|
.select("status", `${TableName.SecretApprovalRequest}.id`)
|
||||||
.groupBy(`${TableName.SecretApprovalRequest}.id`, "status")
|
.groupBy(`${TableName.SecretApprovalRequest}.id`, "status")
|
||||||
@ -142,7 +142,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const findByProjectId = async (
|
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
|
tx?: Knex
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
@ -173,21 +173,21 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
stripUndefinedInWhere({
|
stripUndefinedInWhere({
|
||||||
projectId,
|
[`${TableName.Environment}.projectId`]: projectId,
|
||||||
[`${TableName.Environment}.slug` as "slug"]: environment,
|
[`${TableName.Environment}.slug` as "slug"]: environment,
|
||||||
[`${TableName.SecretApprovalRequest}.status`]: status,
|
[`${TableName.SecretApprovalRequest}.status`]: status,
|
||||||
committerId: committer
|
committerUserId: committer
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.andWhere(
|
.andWhere(
|
||||||
(bd) =>
|
(bd) =>
|
||||||
void bd
|
void bd
|
||||||
.where(`${TableName.SecretApprovalPolicyApprover}.approverId`, membershipId)
|
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, actorId)
|
||||||
.orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId)
|
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, actorId)
|
||||||
)
|
)
|
||||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||||
.select(
|
.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("slug").withSchema(TableName.Environment).as("environment"),
|
||||||
db.ref("id").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerMemberId"),
|
db.ref("id").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerMemberId"),
|
||||||
db.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
|
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("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
||||||
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
||||||
db.ref("approverId").withSchema(TableName.SecretApprovalPolicyApprover)
|
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover)
|
||||||
)
|
)
|
||||||
.orderBy("createdAt", "desc");
|
.orderBy("createdAt", "desc");
|
||||||
|
|
||||||
@ -217,7 +217,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
parentMapper: (el) => ({
|
parentMapper: (el) => ({
|
||||||
...SecretApprovalRequestsSchema.parse(el),
|
...SecretApprovalRequestsSchema.parse(el),
|
||||||
environment: el.environment,
|
environment: el.environment,
|
||||||
projectId: el.projectId,
|
projectId: el.envProjectId,
|
||||||
policy: {
|
policy: {
|
||||||
id: el.policyId,
|
id: el.policyId,
|
||||||
name: el.policyName,
|
name: el.policyName,
|
||||||
@ -232,9 +232,9 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
mapper: ({ reviewerMemberId: member, reviewerStatus: s }) => (member ? { member, status: s } : undefined)
|
mapper: ({ reviewerMemberId: member, reviewerStatus: s }) => (member ? { member, status: s } : undefined)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "approverId",
|
key: "approverUserId",
|
||||||
label: "approvers" as const,
|
label: "approvers" as const,
|
||||||
mapper: ({ approverId }) => approverId
|
mapper: ({ approverUserId }) => approverUserId
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "commitId",
|
key: "commitId",
|
||||||
|
@ -113,7 +113,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("secretCommentTag").withSchema(TableName.SecretVersion).as("secVerCommentTag"),
|
db.ref("secretCommentTag").withSchema(TableName.SecretVersion).as("secVerCommentTag"),
|
||||||
db.ref("secretCommentCiphertext").withSchema(TableName.SecretVersion).as("secVerCommentCiphertext")
|
db.ref("secretCommentCiphertext").withSchema(TableName.SecretVersion).as("secVerCommentCiphertext")
|
||||||
);
|
);
|
||||||
const formatedDoc = sqlNestRelationships({
|
const formattedDoc = sqlNestRelationships({
|
||||||
data: doc,
|
data: doc,
|
||||||
key: "id",
|
key: "id",
|
||||||
parentMapper: (data) => SecretApprovalRequestsSecretsSchema.omit({ secretVersion: true }).parse(data),
|
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,
|
...el,
|
||||||
secret: secret?.[0],
|
secret: secret?.[0],
|
||||||
secretVersion: secretVersion?.[0]
|
secretVersion: secretVersion?.[0]
|
||||||
|
@ -85,7 +85,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
||||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||||
|
|
||||||
const { membership } = await permissionService.getProjectPermission(
|
await permissionService.getProjectPermission(
|
||||||
actor as ActorType.USER,
|
actor as ActorType.USER,
|
||||||
actorId,
|
actorId,
|
||||||
projectId,
|
projectId,
|
||||||
@ -93,7 +93,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
|
||||||
const count = await secretApprovalRequestDAL.findProjectRequestCount(projectId, membership.id);
|
const count = await secretApprovalRequestDAL.findProjectRequestCount(projectId, actorId);
|
||||||
return count;
|
return count;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -111,19 +111,14 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
}: TListApprovalsDTO) => {
|
}: TListApprovalsDTO) => {
|
||||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||||
|
|
||||||
const { membership } = await permissionService.getProjectPermission(
|
await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
|
||||||
actor,
|
|
||||||
actorId,
|
|
||||||
projectId,
|
|
||||||
actorAuthMethod,
|
|
||||||
actorOrgId
|
|
||||||
);
|
|
||||||
const approvals = await secretApprovalRequestDAL.findByProjectId({
|
const approvals = await secretApprovalRequestDAL.findByProjectId({
|
||||||
projectId,
|
projectId,
|
||||||
committer,
|
committer,
|
||||||
environment,
|
environment,
|
||||||
status,
|
status,
|
||||||
membershipId: membership.id,
|
actorId,
|
||||||
limit,
|
limit,
|
||||||
offset
|
offset
|
||||||
});
|
});
|
||||||
@ -143,7 +138,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||||
|
|
||||||
const { policy } = secretApprovalRequest;
|
const { policy } = secretApprovalRequest;
|
||||||
const { membership, hasRole } = await permissionService.getProjectPermission(
|
const { hasRole } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
secretApprovalRequest.projectId,
|
secretApprovalRequest.projectId,
|
||||||
@ -152,8 +147,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
!hasRole(ProjectMembershipRole.Admin) &&
|
!hasRole(ProjectMembershipRole.Admin) &&
|
||||||
secretApprovalRequest.committerId !== membership.id &&
|
secretApprovalRequest.committerUserId !== actorId &&
|
||||||
!policy.approvers.find((approverId) => approverId === membership.id)
|
!policy.approvers.find((approverUserId) => approverUserId === actorId)
|
||||||
) {
|
) {
|
||||||
throw new UnauthorizedError({ message: "User has no access" });
|
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" });
|
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||||
|
|
||||||
const { policy } = secretApprovalRequest;
|
const { policy } = secretApprovalRequest;
|
||||||
const { membership, hasRole } = await permissionService.getProjectPermission(
|
const { hasRole } = await permissionService.getProjectPermission(
|
||||||
ActorType.USER,
|
ActorType.USER,
|
||||||
actorId,
|
actorId,
|
||||||
secretApprovalRequest.projectId,
|
secretApprovalRequest.projectId,
|
||||||
@ -187,8 +182,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
!hasRole(ProjectMembershipRole.Admin) &&
|
!hasRole(ProjectMembershipRole.Admin) &&
|
||||||
secretApprovalRequest.committerId !== membership.id &&
|
secretApprovalRequest.committerUserId !== actorId &&
|
||||||
!policy.approvers.find((approverId) => approverId === membership.id)
|
!policy.approvers.find((approverUserId) => approverUserId === actorId)
|
||||||
) {
|
) {
|
||||||
throw new UnauthorizedError({ message: "User has no access" });
|
throw new UnauthorizedError({ message: "User has no access" });
|
||||||
}
|
}
|
||||||
@ -196,7 +191,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
const review = await secretApprovalRequestReviewerDAL.findOne(
|
const review = await secretApprovalRequestReviewerDAL.findOne(
|
||||||
{
|
{
|
||||||
requestId: secretApprovalRequest.id,
|
requestId: secretApprovalRequest.id,
|
||||||
member: membership.id
|
memberUserId: actorId
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@ -205,7 +200,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
{
|
{
|
||||||
status,
|
status,
|
||||||
requestId: secretApprovalRequest.id,
|
requestId: secretApprovalRequest.id,
|
||||||
member: membership.id
|
memberUserId: actorId
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@ -228,7 +223,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||||
|
|
||||||
const { policy } = secretApprovalRequest;
|
const { policy } = secretApprovalRequest;
|
||||||
const { membership, hasRole } = await permissionService.getProjectPermission(
|
const { hasRole } = await permissionService.getProjectPermission(
|
||||||
ActorType.USER,
|
ActorType.USER,
|
||||||
actorId,
|
actorId,
|
||||||
secretApprovalRequest.projectId,
|
secretApprovalRequest.projectId,
|
||||||
@ -237,8 +232,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
!hasRole(ProjectMembershipRole.Admin) &&
|
!hasRole(ProjectMembershipRole.Admin) &&
|
||||||
secretApprovalRequest.committerId !== membership.id &&
|
secretApprovalRequest.committerUserId !== actorId &&
|
||||||
!policy.approvers.find((approverId) => approverId === membership.id)
|
!policy.approvers.find((approverUserId) => approverUserId === actorId)
|
||||||
) {
|
) {
|
||||||
throw new UnauthorizedError({ message: "User has no access" });
|
throw new UnauthorizedError({ message: "User has no access" });
|
||||||
}
|
}
|
||||||
@ -251,8 +246,9 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
|
|
||||||
const updatedRequest = await secretApprovalRequestDAL.updateById(secretApprovalRequest.id, {
|
const updatedRequest = await secretApprovalRequestDAL.updateById(secretApprovalRequest.id, {
|
||||||
status,
|
status,
|
||||||
statusChangeBy: membership.id
|
statusChangeByUserId: actorId
|
||||||
});
|
});
|
||||||
|
|
||||||
return { ...secretApprovalRequest, ...updatedRequest };
|
return { ...secretApprovalRequest, ...updatedRequest };
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -268,7 +264,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||||
|
|
||||||
const { policy, folderId, projectId } = secretApprovalRequest;
|
const { policy, folderId, projectId } = secretApprovalRequest;
|
||||||
const { membership, hasRole } = await permissionService.getProjectPermission(
|
const { hasRole } = await permissionService.getProjectPermission(
|
||||||
ActorType.USER,
|
ActorType.USER,
|
||||||
actorId,
|
actorId,
|
||||||
projectId,
|
projectId,
|
||||||
@ -278,8 +274,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!hasRole(ProjectMembershipRole.Admin) &&
|
!hasRole(ProjectMembershipRole.Admin) &&
|
||||||
secretApprovalRequest.committerId !== membership.id &&
|
secretApprovalRequest.committerUserId !== actorId &&
|
||||||
!policy.approvers.find((approverId) => approverId === membership.id)
|
!policy.approvers.find((approverUserId) => approverUserId === actorId)
|
||||||
) {
|
) {
|
||||||
throw new UnauthorizedError({ message: "User has no access" });
|
throw new UnauthorizedError({ message: "User has no access" });
|
||||||
}
|
}
|
||||||
@ -290,7 +286,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
const hasMinApproval =
|
const hasMinApproval =
|
||||||
secretApprovalRequest.policy.approvals <=
|
secretApprovalRequest.policy.approvals <=
|
||||||
secretApprovalRequest.policy.approvers.filter(
|
secretApprovalRequest.policy.approvers.filter(
|
||||||
(approverId) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
|
(approverUserId) => reviewers[approverUserId.toString()] === ApprovalStatus.APPROVED
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
if (!hasMinApproval) throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
|
if (!hasMinApproval) throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
|
||||||
@ -445,7 +441,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
conflicts: JSON.stringify(conflicts),
|
conflicts: JSON.stringify(conflicts),
|
||||||
hasMerged: true,
|
hasMerged: true,
|
||||||
status: RequestState.Closed,
|
status: RequestState.Closed,
|
||||||
statusChangeBy: membership.id
|
statusChangeByUserId: actorId
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@ -480,7 +476,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
}: TGenerateSecretApprovalRequestDTO) => {
|
}: TGenerateSecretApprovalRequestDTO) => {
|
||||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||||
|
|
||||||
const { permission, membership } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
projectId,
|
projectId,
|
||||||
@ -634,7 +630,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
policyId: policy.id,
|
policyId: policy.id,
|
||||||
status: "open",
|
status: "open",
|
||||||
hasMerged: false,
|
hasMerged: false,
|
||||||
committerId: membership.id
|
committerUserId: actorId
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
@ -108,6 +108,7 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
|||||||
if (req.url.includes("/api/v3/auth/")) {
|
if (req.url.includes("/api/v3/auth/")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!authMode) return;
|
if (!authMode) return;
|
||||||
|
|
||||||
switch (authMode) {
|
switch (authMode) {
|
||||||
|
@ -2,6 +2,12 @@ import { Knex } from "knex";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { registerV1EERoutes } from "@app/ee/routes/v1";
|
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 { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
|
||||||
import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue";
|
import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue";
|
||||||
import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||||
@ -16,6 +22,7 @@ import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secre
|
|||||||
import { groupDALFactory } from "@app/ee/services/group/group-dal";
|
import { groupDALFactory } from "@app/ee/services/group/group-dal";
|
||||||
import { groupServiceFactory } from "@app/ee/services/group/group-service";
|
import { groupServiceFactory } from "@app/ee/services/group/group-service";
|
||||||
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
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 { 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 { 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";
|
import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal";
|
||||||
@ -205,6 +212,14 @@ export const registerRoutes = async (
|
|||||||
const scimDAL = scimDALFactory(db);
|
const scimDAL = scimDALFactory(db);
|
||||||
const ldapConfigDAL = ldapConfigDALFactory(db);
|
const ldapConfigDAL = ldapConfigDALFactory(db);
|
||||||
const ldapGroupMapDAL = ldapGroupMapDALFactory(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 sapApproverDAL = secretApprovalPolicyApproverDALFactory(db);
|
||||||
const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db);
|
const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db);
|
||||||
const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db);
|
const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db);
|
||||||
@ -218,10 +233,10 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const gitAppInstallSessionDAL = gitAppInstallSessionDALFactory(db);
|
const gitAppInstallSessionDAL = gitAppInstallSessionDALFactory(db);
|
||||||
const gitAppOrgDAL = gitAppDALFactory(db);
|
const gitAppOrgDAL = gitAppDALFactory(db);
|
||||||
const groupDAL = groupDALFactory(db);
|
const userGroupMembershipDAL = userGroupMembershipDALFactory(db);
|
||||||
|
const groupDAL = groupDALFactory(db, userGroupMembershipDAL);
|
||||||
const groupProjectDAL = groupProjectDALFactory(db);
|
const groupProjectDAL = groupProjectDALFactory(db);
|
||||||
const groupProjectMembershipRoleDAL = groupProjectMembershipRoleDALFactory(db);
|
const groupProjectMembershipRoleDAL = groupProjectMembershipRoleDALFactory(db);
|
||||||
const userGroupMembershipDAL = userGroupMembershipDALFactory(db);
|
|
||||||
const secretScanningDAL = secretScanningDALFactory(db);
|
const secretScanningDAL = secretScanningDALFactory(db);
|
||||||
const licenseDAL = licenseDALFactory(db);
|
const licenseDAL = licenseDALFactory(db);
|
||||||
const dynamicSecretDAL = dynamicSecretDALFactory(db);
|
const dynamicSecretDAL = dynamicSecretDALFactory(db);
|
||||||
@ -259,9 +274,11 @@ export const registerRoutes = async (
|
|||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
secretApprovalPolicyApproverDAL: sapApproverDAL,
|
secretApprovalPolicyApproverDAL: sapApproverDAL,
|
||||||
|
userDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
secretApprovalPolicyDAL
|
secretApprovalPolicyDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const samlService = samlConfigServiceFactory({
|
const samlService = samlConfigServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
orgBotDAL,
|
orgBotDAL,
|
||||||
@ -275,10 +292,13 @@ export const registerRoutes = async (
|
|||||||
groupDAL,
|
groupDAL,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
|
secretApprovalPolicyDAL,
|
||||||
userGroupMembershipDAL,
|
userGroupMembershipDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
|
secretApprovalRequestDAL,
|
||||||
|
accessApprovalRequestDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService
|
licenseService
|
||||||
});
|
});
|
||||||
@ -286,7 +306,10 @@ export const registerRoutes = async (
|
|||||||
groupDAL,
|
groupDAL,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
groupProjectMembershipRoleDAL,
|
groupProjectMembershipRoleDAL,
|
||||||
|
secretApprovalPolicyDAL,
|
||||||
|
secretApprovalRequestDAL,
|
||||||
userGroupMembershipDAL,
|
userGroupMembershipDAL,
|
||||||
|
accessApprovalRequestDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
@ -301,7 +324,10 @@ export const registerRoutes = async (
|
|||||||
projectDAL,
|
projectDAL,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
groupDAL,
|
groupDAL,
|
||||||
|
secretApprovalPolicyDAL,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
|
secretApprovalRequestDAL,
|
||||||
|
accessApprovalRequestDAL,
|
||||||
userGroupMembershipDAL,
|
userGroupMembershipDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
@ -314,7 +340,10 @@ export const registerRoutes = async (
|
|||||||
ldapGroupMapDAL,
|
ldapGroupMapDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
orgBotDAL,
|
orgBotDAL,
|
||||||
|
secretApprovalPolicyDAL,
|
||||||
groupDAL,
|
groupDAL,
|
||||||
|
secretApprovalRequestDAL,
|
||||||
|
accessApprovalRequestDAL,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
@ -406,6 +435,7 @@ export const registerRoutes = async (
|
|||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
|
groupProjectDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
@ -580,6 +610,31 @@ export const registerRoutes = async (
|
|||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
secretQueueService
|
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({
|
const secretRotationQueue = secretRotationQueueFactory({
|
||||||
telemetryService,
|
telemetryService,
|
||||||
secretRotationDAL,
|
secretRotationDAL,
|
||||||
@ -716,6 +771,8 @@ export const registerRoutes = async (
|
|||||||
identityProject: identityProjectService,
|
identityProject: identityProjectService,
|
||||||
identityUa: identityUaService,
|
identityUa: identityUaService,
|
||||||
secretApprovalPolicy: sapService,
|
secretApprovalPolicy: sapService,
|
||||||
|
accessApprovalPolicy: accessApprovalPolicyService,
|
||||||
|
accessApprovalRequest: accessApprovalRequestService,
|
||||||
secretApprovalRequest: sarService,
|
secretApprovalRequest: sarService,
|
||||||
secretRotation: secretRotationService,
|
secretRotation: secretRotationService,
|
||||||
dynamicSecret: dynamicSecretService,
|
dynamicSecret: dynamicSecretService,
|
||||||
|
@ -68,9 +68,16 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
workspaceId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
includeGroupMembers: z
|
||||||
|
.enum(["true", "false"])
|
||||||
|
.default("false")
|
||||||
|
.transform((value) => value === "true")
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
users: ProjectMembershipsSchema.extend({
|
users: ProjectMembershipsSchema.extend({
|
||||||
|
isGroupMember: z.boolean(),
|
||||||
user: UsersSchema.pick({
|
user: UsersSchema.pick({
|
||||||
email: true,
|
email: true,
|
||||||
username: true,
|
username: true,
|
||||||
@ -104,6 +111,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
includeGroupMembers: req.query.includeGroupMembers,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.workspaceId,
|
||||||
actorOrgId: req.permission.orgId
|
actorOrgId: req.permission.orgId
|
||||||
});
|
});
|
||||||
|
@ -915,7 +915,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
event: {
|
event: {
|
||||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||||
metadata: {
|
metadata: {
|
||||||
committedBy: approval.committerId,
|
committedByUser: approval.committerUserId,
|
||||||
secretApprovalRequestId: approval.id,
|
secretApprovalRequestId: approval.id,
|
||||||
secretApprovalRequestSlug: approval.slug
|
secretApprovalRequestSlug: approval.slug
|
||||||
}
|
}
|
||||||
@ -1099,7 +1099,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
event: {
|
event: {
|
||||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||||
metadata: {
|
metadata: {
|
||||||
committedBy: approval.committerId,
|
committedByUser: approval.committerUserId,
|
||||||
secretApprovalRequestId: approval.id,
|
secretApprovalRequestId: approval.id,
|
||||||
secretApprovalRequestSlug: approval.slug
|
secretApprovalRequestSlug: approval.slug
|
||||||
}
|
}
|
||||||
@ -1230,14 +1230,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
projectId: req.body.workspaceId,
|
projectId: req.body.workspaceId,
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||||
metadata: {
|
metadata: {
|
||||||
committedBy: approval.committerId,
|
committedByUser: approval.committerUserId,
|
||||||
secretApprovalRequestId: approval.id,
|
secretApprovalRequestId: approval.id,
|
||||||
secretApprovalRequestSlug: approval.slug
|
secretApprovalRequestSlug: approval.slug
|
||||||
}
|
}
|
||||||
@ -1363,7 +1362,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
event: {
|
event: {
|
||||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||||
metadata: {
|
metadata: {
|
||||||
committedBy: approval.committerId,
|
committedByUser: approval.committerUserId,
|
||||||
secretApprovalRequestId: approval.id,
|
secretApprovalRequestId: approval.id,
|
||||||
secretApprovalRequestSlug: approval.slug
|
secretApprovalRequestSlug: approval.slug
|
||||||
}
|
}
|
||||||
@ -1490,7 +1489,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
event: {
|
event: {
|
||||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||||
metadata: {
|
metadata: {
|
||||||
committedBy: approval.committerId,
|
committedByUser: approval.committerUserId,
|
||||||
secretApprovalRequestId: approval.id,
|
secretApprovalRequestId: approval.id,
|
||||||
secretApprovalRequestSlug: approval.slug
|
secretApprovalRequestSlug: approval.slug
|
||||||
}
|
}
|
||||||
@ -1604,7 +1603,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
event: {
|
event: {
|
||||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||||
metadata: {
|
metadata: {
|
||||||
committedBy: approval.committerId,
|
committedByUser: approval.committerUserId,
|
||||||
secretApprovalRequestId: approval.id,
|
secretApprovalRequestId: approval.id,
|
||||||
secretApprovalRequestSlug: approval.slug
|
secretApprovalRequestSlug: approval.slug
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
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 { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, sqlNestRelationships } from "@app/lib/knex";
|
import { ormify, sqlNestRelationships } from "@app/lib/knex";
|
||||||
|
|
||||||
@ -10,6 +10,100 @@ export type TGroupProjectDALFactory = ReturnType<typeof groupProjectDALFactory>;
|
|||||||
export const groupProjectDALFactory = (db: TDbClient) => {
|
export const groupProjectDALFactory = (db: TDbClient) => {
|
||||||
const groupProjectOrm = ormify(db, TableName.GroupProjectMembership);
|
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) => {
|
const findByProjectId = async (projectId: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const docs = await (tx || db)(TableName.GroupProjectMembership)
|
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 ms from "ms";
|
||||||
|
|
||||||
import { ProjectMembershipRole, SecretKeyEncoding } from "@app/db/schemas";
|
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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { 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 { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
||||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
@ -39,6 +42,9 @@ type TGroupProjectServiceFactoryDep = {
|
|||||||
projectBotDAL: TProjectBotDALFactory;
|
projectBotDAL: TProjectBotDALFactory;
|
||||||
groupDAL: Pick<TGroupDALFactory, "findOne">;
|
groupDAL: Pick<TGroupDALFactory, "findOne">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissionByRole">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissionByRole">;
|
||||||
|
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "delete">;
|
||||||
|
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findByProjectIds">;
|
||||||
|
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "delete">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGroupProjectServiceFactory = ReturnType<typeof groupProjectServiceFactory>;
|
export type TGroupProjectServiceFactory = ReturnType<typeof groupProjectServiceFactory>;
|
||||||
@ -48,6 +54,9 @@ export const groupProjectServiceFactory = ({
|
|||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
groupProjectMembershipRoleDAL,
|
groupProjectMembershipRoleDAL,
|
||||||
userGroupMembershipDAL,
|
userGroupMembershipDAL,
|
||||||
|
secretApprovalRequestDAL,
|
||||||
|
secretApprovalPolicyDAL,
|
||||||
|
accessApprovalRequestDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
@ -277,7 +286,8 @@ export const groupProjectServiceFactory = ({
|
|||||||
if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
|
if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
|
||||||
|
|
||||||
const groupProjectMembership = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
|
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(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@ -289,8 +299,34 @@ export const groupProjectServiceFactory = ({
|
|||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Groups);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Groups);
|
||||||
|
|
||||||
const deletedProjectGroup = await groupProjectDAL.transaction(async (tx) => {
|
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);
|
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) {
|
if (groupMembers.length) {
|
||||||
await projectKeyDAL.delete(
|
await projectKeyDAL.delete(
|
||||||
{
|
{
|
||||||
|
@ -1,12 +1,74 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
import { Tables } from "knex/types/tables";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
|
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
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 type TProjectMembershipDALFactory = ReturnType<typeof projectMembershipDALFactory>;
|
||||||
|
|
||||||
export const projectMembershipDALFactory = (db: TDbClient) => {
|
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
|
// special query
|
||||||
const findAllProjectMembers = async (projectId: string) => {
|
const findAllProjectMembers = async (projectId: string) => {
|
||||||
@ -54,6 +116,7 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
|||||||
const members = sqlNestRelationships({
|
const members = sqlNestRelationships({
|
||||||
data: docs,
|
data: docs,
|
||||||
parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, userId }) => ({
|
parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, userId }) => ({
|
||||||
|
isGroupMember: false,
|
||||||
id,
|
id,
|
||||||
userId,
|
userId,
|
||||||
projectId,
|
projectId,
|
||||||
@ -152,8 +215,9 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...projectMemberOrm,
|
...projectMembershipOrm,
|
||||||
findAllProjectMembers,
|
findAllProjectMembers,
|
||||||
|
delete: deleteMany,
|
||||||
findProjectGhostUser,
|
findProjectGhostUser,
|
||||||
findMembershipsByUsername,
|
findMembershipsByUsername,
|
||||||
findProjectMembershipsByUserId
|
findProjectMembershipsByUserId
|
||||||
|
@ -19,6 +19,7 @@ import { groupBy } from "@app/lib/fn";
|
|||||||
|
|
||||||
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
|
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
||||||
@ -52,6 +53,7 @@ type TProjectMembershipServiceFactoryDep = {
|
|||||||
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction">;
|
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction">;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
groupProjectDAL: TGroupProjectDALFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TProjectMembershipServiceFactory = ReturnType<typeof projectMembershipServiceFactory>;
|
export type TProjectMembershipServiceFactory = ReturnType<typeof projectMembershipServiceFactory>;
|
||||||
@ -61,6 +63,7 @@ export const projectMembershipServiceFactory = ({
|
|||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
|
groupProjectDAL,
|
||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
@ -74,6 +77,7 @@ export const projectMembershipServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
|
includeGroupMembers,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
projectId
|
projectId
|
||||||
}: TGetProjectMembershipDTO) => {
|
}: TGetProjectMembershipDTO) => {
|
||||||
@ -86,7 +90,20 @@ export const projectMembershipServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
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 ({
|
const addUsersToProject = async ({
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
export type TGetProjectMembershipDTO = TProjectPermission;
|
export type TGetProjectMembershipDTO = {
|
||||||
|
includeGroupMembers?: boolean;
|
||||||
|
} & TProjectPermission;
|
||||||
export enum ProjectUserMembershipTemporaryMode {
|
export enum ProjectUserMembershipTemporaryMode {
|
||||||
Relative = "relative"
|
Relative = "relative"
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ export enum SmtpTemplates {
|
|||||||
EmailVerification = "emailVerification.handlebars",
|
EmailVerification = "emailVerification.handlebars",
|
||||||
SecretReminder = "secretReminder.handlebars",
|
SecretReminder = "secretReminder.handlebars",
|
||||||
EmailMfa = "emailMfa.handlebars",
|
EmailMfa = "emailMfa.handlebars",
|
||||||
|
AccessApprovalRequest = "accessApprovalRequest.handlebars",
|
||||||
HistoricalSecretList = "historicalSecretLeakIncident.handlebars",
|
HistoricalSecretList = "historicalSecretLeakIncident.handlebars",
|
||||||
NewDeviceJoin = "newDevice.handlebars",
|
NewDeviceJoin = "newDevice.handlebars",
|
||||||
OrgInvite = "organizationInvitation.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
|
TUserEncryptionKeysUpdate
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
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>;
|
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) => {
|
const findUserByProjectMembershipId = async (projectMembershipId: string) => {
|
||||||
try {
|
try {
|
||||||
return await db(TableName.ProjectMembership)
|
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) => {
|
const createUserEncryption = async (data: TUserEncryptionKeysInsert, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const [userEnc] = await (tx || db)(TableName.UserEncryptionKey).insert(data).returning("*");
|
const [userEnc] = await (tx || db)(TableName.UserEncryptionKey).insert(data).returning("*");
|
||||||
@ -135,11 +239,14 @@ export const userDALFactory = (db: TDbClient) => {
|
|||||||
return {
|
return {
|
||||||
...userOrm,
|
...userOrm,
|
||||||
findUserByUsername,
|
findUserByUsername,
|
||||||
|
findUsersByProjectId,
|
||||||
|
findUserByProjectId,
|
||||||
findUserEncKeyByUsername,
|
findUserEncKeyByUsername,
|
||||||
findUserEncKeyByUserIdsBatch,
|
findUserEncKeyByUserIdsBatch,
|
||||||
findUserEncKeyByUserId,
|
findUserEncKeyByUserId,
|
||||||
updateUserEncryptionByUserId,
|
updateUserEncryptionByUserId,
|
||||||
findUserByProjectMembershipId,
|
findUserByProjectMembershipId,
|
||||||
|
findUsersByProjectMembershipIds,
|
||||||
upsertUserEncryptionKey,
|
upsertUserEncryptionKey,
|
||||||
createUserEncryption,
|
createUserEncryption,
|
||||||
findOneUserAction,
|
findOneUserAction,
|
||||||
|
@ -17,12 +17,8 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children
|
|||||||
containerClassName
|
containerClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div className={twMerge("rounded-md bg-mineshaft-800 p-16 text-bunker-300", className)}>
|
||||||
className={twMerge(
|
<div className="flex items-end space-x-12">
|
||||||
"flex items-end space-x-12 rounded-md bg-mineshaft-800 p-16 text-bunker-300",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<FontAwesomeIcon icon={faLock} size="6x" />
|
<FontAwesomeIcon icon={faLock} size="6x" />
|
||||||
</div>
|
</div>
|
||||||
@ -37,5 +33,6 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
32
frontend/src/components/v2/Badge/Badge.tsx
Normal file
32
frontend/src/components/v2/Badge/Badge.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { cva, VariantProps } from "cva";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
[
|
||||||
|
"inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-xs text-yellow opacity-80 hover:opacity-100"
|
||||||
|
],
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
primary: "bg-yellow/20 text-yellow",
|
||||||
|
danger: "bg-red/20 text-red",
|
||||||
|
success: "bg-green/20 text-green"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export type BadgeProps = VariantProps<typeof badgeVariants> & IProps;
|
||||||
|
|
||||||
|
export const Badge = ({ children, className, variant }: BadgeProps) => {
|
||||||
|
return (
|
||||||
|
<div className={twMerge(badgeVariants({ variant: variant || "primary" }), className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
1
frontend/src/components/v2/Badge/index.tsx
Normal file
1
frontend/src/components/v2/Badge/index.tsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { Badge } from "./Badge";
|
@ -29,7 +29,7 @@ const buttonVariants = cva(
|
|||||||
colorSchema: {
|
colorSchema: {
|
||||||
primary: ["bg-primary", "text-black", "border-primary bg-opacity-90 hover:bg-opacity-100"],
|
primary: ["bg-primary", "text-black", "border-primary bg-opacity-90 hover:bg-opacity-100"],
|
||||||
secondary: ["bg-mineshaft", "text-gray-300", "border-mineshaft hover:bg-opacity-80"],
|
secondary: ["bg-mineshaft", "text-gray-300", "border-mineshaft hover:bg-opacity-80"],
|
||||||
danger: ["bg-red", "text-white", "border-red hover:bg-opacity-90"],
|
danger: ["!bg-red", "!text-white", "!border-red hover:!bg-opacity-90"],
|
||||||
gray: ["bg-bunker-500", "text-bunker-200"]
|
gray: ["bg-bunker-500", "text-bunker-200"]
|
||||||
},
|
},
|
||||||
variant: {
|
variant: {
|
||||||
|
13
frontend/src/components/v2/Divider/Divider.tsx
Normal file
13
frontend/src/components/v2/Divider/Divider.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Divider = ({ className }: IProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div className={twMerge("flex items-center px-2 opacity-50", className)}>
|
||||||
|
<div aria-hidden="true" className="h-5 w-full grow border border-t border-mineshaft-200" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
1
frontend/src/components/v2/Divider/index.tsx
Normal file
1
frontend/src/components/v2/Divider/index.tsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { Divider } from "./Divider";
|
@ -41,18 +41,22 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
`inline-flex items-center justify-between rounded-md
|
`inline-flex items-center justify-between rounded-md
|
||||||
bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200 focus:bg-mineshaft-700/80`,
|
bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none focus:bg-mineshaft-700/80 data-[placeholder]:text-mineshaft-200`,
|
||||||
className
|
className,
|
||||||
|
isDisabled && "cursor-not-allowed opacity-50"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SelectPrimitive.Value placeholder={placeholder}>
|
<SelectPrimitive.Value placeholder={placeholder}>
|
||||||
{props.icon ? <FontAwesomeIcon icon={props.icon} /> : placeholder}
|
{props.icon ? <FontAwesomeIcon icon={props.icon} /> : placeholder}
|
||||||
</SelectPrimitive.Value>
|
</SelectPrimitive.Value>
|
||||||
{!isDisabled && (
|
|
||||||
<SelectPrimitive.Icon className="ml-3">
|
<SelectPrimitive.Icon className="ml-3">
|
||||||
<FontAwesomeIcon icon={faCaretDown} size="sm" />
|
<FontAwesomeIcon
|
||||||
|
icon={faCaretDown}
|
||||||
|
size="sm"
|
||||||
|
className={twMerge(isDisabled && "opacity-30")}
|
||||||
|
/>
|
||||||
</SelectPrimitive.Icon>
|
</SelectPrimitive.Icon>
|
||||||
)}
|
|
||||||
</SelectPrimitive.Trigger>
|
</SelectPrimitive.Trigger>
|
||||||
<SelectPrimitive.Portal>
|
<SelectPrimitive.Portal>
|
||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
|
14
frontend/src/hooks/api/accessApproval/index.tsx
Normal file
14
frontend/src/hooks/api/accessApproval/index.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export {
|
||||||
|
useCreateAccessApprovalPolicy,
|
||||||
|
useCreateAccessRequest,
|
||||||
|
useDeleteAccessApprovalPolicy,
|
||||||
|
useDeleteAccessApprovalRequest,
|
||||||
|
useReviewAccessRequest,
|
||||||
|
useUpdateAccessApprovalPolicy
|
||||||
|
} from "./mutation";
|
||||||
|
export {
|
||||||
|
useGetAccessApprovalPolicies,
|
||||||
|
useGetAccessApprovalRequests,
|
||||||
|
useGetAccessPolicyApprovalCount,
|
||||||
|
useGetAccessRequestsCount
|
||||||
|
} from "./queries";
|
142
frontend/src/hooks/api/accessApproval/mutation.tsx
Normal file
142
frontend/src/hooks/api/accessApproval/mutation.tsx
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import { packRules } from "@casl/ability/extra";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { accessApprovalKeys } from "./queries";
|
||||||
|
import {
|
||||||
|
TAccessApproval,
|
||||||
|
TCreateAccessPolicyDTO,
|
||||||
|
TCreateAccessRequestDTO,
|
||||||
|
TDeleteSecretPolicyDTO,
|
||||||
|
TUpdateAccessPolicyDTO
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export const useCreateAccessApprovalPolicy = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, TCreateAccessPolicyDTO>({
|
||||||
|
mutationFn: async ({ environment, projectSlug, approvals, approvers, name, secretPath }) => {
|
||||||
|
const { data } = await apiRequest.post("/api/v1/access-approvals/policies", {
|
||||||
|
environment,
|
||||||
|
projectSlug,
|
||||||
|
approvals,
|
||||||
|
approvers,
|
||||||
|
secretPath,
|
||||||
|
name
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { projectSlug }) => {
|
||||||
|
queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(projectSlug));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateAccessApprovalPolicy = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, TUpdateAccessPolicyDTO>({
|
||||||
|
mutationFn: async ({ id, approvers, approvals, name, secretPath }) => {
|
||||||
|
const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, {
|
||||||
|
approvals,
|
||||||
|
approvers,
|
||||||
|
secretPath,
|
||||||
|
name
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { projectSlug }) => {
|
||||||
|
queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(projectSlug));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteAccessApprovalPolicy = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, TDeleteSecretPolicyDTO>({
|
||||||
|
mutationFn: async ({ id }) => {
|
||||||
|
const { data } = await apiRequest.delete(`/api/v1/access-approvals/policies/${id}`);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { projectSlug }) => {
|
||||||
|
queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(projectSlug));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteAccessApprovalRequest = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, { requestId: string; projectSlug: string }>({
|
||||||
|
mutationFn: async ({ requestId, projectSlug }) => {
|
||||||
|
const { data } = await apiRequest.delete(`/api/v1/access-approvals/requests/${requestId}`, {
|
||||||
|
params: {
|
||||||
|
projectSlug
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { projectSlug }) => {
|
||||||
|
queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalRequests(projectSlug));
|
||||||
|
queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalRequestCount(projectSlug));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateAccessRequest = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<{}, {}, TCreateAccessRequestDTO>({
|
||||||
|
mutationFn: async ({ projectSlug, ...request }) => {
|
||||||
|
const { data } = await apiRequest.post<TAccessApproval>(
|
||||||
|
"/api/v1/access-approvals/requests",
|
||||||
|
{
|
||||||
|
...request,
|
||||||
|
permissions: request.permissions ? packRules(request.permissions) : undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
projectSlug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { projectSlug }) => {
|
||||||
|
queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalRequestCount(projectSlug));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useReviewAccessRequest = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
requestId: string;
|
||||||
|
status: "approved" | "rejected";
|
||||||
|
projectSlug: string;
|
||||||
|
envSlug?: string;
|
||||||
|
requestedBy?: string;
|
||||||
|
}
|
||||||
|
>({
|
||||||
|
mutationFn: async ({ requestId, status }) => {
|
||||||
|
const { data } = await apiRequest.post(
|
||||||
|
`/api/v1/access-approvals/requests/${requestId}/review`,
|
||||||
|
{
|
||||||
|
status
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { projectSlug, envSlug, requestedBy }) => {
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
accessApprovalKeys.getAccessApprovalRequests(projectSlug, envSlug, requestedBy)
|
||||||
|
);
|
||||||
|
queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalRequestCount(projectSlug));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
155
frontend/src/hooks/api/accessApproval/queries.tsx
Normal file
155
frontend/src/hooks/api/accessApproval/queries.tsx
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||||
|
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { TProjectPermission } from "../roles/types";
|
||||||
|
import {
|
||||||
|
TAccessApprovalPolicy,
|
||||||
|
TAccessApprovalRequest,
|
||||||
|
TAccessRequestCount,
|
||||||
|
TGetAccessApprovalRequestsDTO,
|
||||||
|
TGetAccessPolicyApprovalCountDTO
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export const accessApprovalKeys = {
|
||||||
|
getAccessApprovalPolicies: (projectSlug: string) =>
|
||||||
|
[{ projectSlug }, "access-approval-policies"] as const,
|
||||||
|
getAccessApprovalPolicyOfABoard: (workspaceId: string, environment: string) =>
|
||||||
|
[{ workspaceId, environment }, "access-approval-policy"] as const,
|
||||||
|
|
||||||
|
getAccessApprovalRequests: (projectSlug: string, envSlug?: string, requestedBy?: string) =>
|
||||||
|
[{ projectSlug, envSlug, requestedBy }, "access-approvals-requests"] as const,
|
||||||
|
getAccessApprovalRequestCount: (projectSlug: string) =>
|
||||||
|
[{ projectSlug }, "access-approval-request-count"] as const
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchPolicyApprovalCount = async ({
|
||||||
|
projectSlug,
|
||||||
|
envSlug
|
||||||
|
}: TGetAccessPolicyApprovalCountDTO) => {
|
||||||
|
const { data } = await apiRequest.get<{ count: number }>(
|
||||||
|
"/api/v1/access-approvals/policies/count",
|
||||||
|
{
|
||||||
|
params: { projectSlug, envSlug }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return data.count;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetAccessPolicyApprovalCount = ({
|
||||||
|
projectSlug,
|
||||||
|
envSlug,
|
||||||
|
options = {}
|
||||||
|
}: TGetAccessPolicyApprovalCountDTO & {
|
||||||
|
options?: UseQueryOptions<
|
||||||
|
number,
|
||||||
|
unknown,
|
||||||
|
number,
|
||||||
|
ReturnType<typeof accessApprovalKeys.getAccessApprovalPolicies>
|
||||||
|
>;
|
||||||
|
}) =>
|
||||||
|
useQuery({
|
||||||
|
queryFn: () => fetchPolicyApprovalCount({ projectSlug, envSlug }),
|
||||||
|
...options,
|
||||||
|
enabled: Boolean(projectSlug) && (options?.enabled ?? true)
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchApprovalPolicies = async ({ projectSlug }: TGetAccessApprovalRequestsDTO) => {
|
||||||
|
const { data } = await apiRequest.get<{ approvals: TAccessApprovalPolicy[] }>(
|
||||||
|
"/api/v1/access-approvals/policies",
|
||||||
|
{ params: { projectSlug } }
|
||||||
|
);
|
||||||
|
return data.approvals;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchApprovalRequests = async ({
|
||||||
|
projectSlug,
|
||||||
|
envSlug,
|
||||||
|
authorUserId
|
||||||
|
}: TGetAccessApprovalRequestsDTO) => {
|
||||||
|
const { data } = await apiRequest.get<{ requests: TAccessApprovalRequest[] }>(
|
||||||
|
"/api/v1/access-approvals/requests",
|
||||||
|
{ params: { projectSlug, envSlug, authorUserId } }
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.requests.map((request) => ({
|
||||||
|
...request,
|
||||||
|
|
||||||
|
privilege: request.privilege
|
||||||
|
? {
|
||||||
|
...request.privilege,
|
||||||
|
permissions: unpackRules(
|
||||||
|
request.privilege.permissions as unknown as PackRule<TProjectPermission>[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
permissions: unpackRules(request.permissions as unknown as PackRule<TProjectPermission>[])
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchAccessRequestsCount = async (projectSlug: string) => {
|
||||||
|
const { data } = await apiRequest.get<TAccessRequestCount>(
|
||||||
|
"/api/v1/access-approvals/requests/count",
|
||||||
|
{ params: { projectSlug } }
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetAccessRequestsCount = ({
|
||||||
|
projectSlug,
|
||||||
|
options = {}
|
||||||
|
}: TGetAccessApprovalRequestsDTO & {
|
||||||
|
options?: UseQueryOptions<
|
||||||
|
TAccessRequestCount,
|
||||||
|
unknown,
|
||||||
|
{ pendingCount: number; finalizedCount: number },
|
||||||
|
ReturnType<typeof accessApprovalKeys.getAccessApprovalRequestCount>
|
||||||
|
>;
|
||||||
|
}) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: accessApprovalKeys.getAccessApprovalRequestCount(projectSlug),
|
||||||
|
queryFn: () => fetchAccessRequestsCount(projectSlug),
|
||||||
|
...options,
|
||||||
|
enabled: Boolean(projectSlug) && (options?.enabled ?? true)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useGetAccessApprovalPolicies = ({
|
||||||
|
projectSlug,
|
||||||
|
envSlug,
|
||||||
|
authorUserId,
|
||||||
|
options = {}
|
||||||
|
}: TGetAccessApprovalRequestsDTO & {
|
||||||
|
options?: UseQueryOptions<
|
||||||
|
TAccessApprovalPolicy[],
|
||||||
|
unknown,
|
||||||
|
TAccessApprovalPolicy[],
|
||||||
|
ReturnType<typeof accessApprovalKeys.getAccessApprovalPolicies>
|
||||||
|
>;
|
||||||
|
}) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: accessApprovalKeys.getAccessApprovalPolicies(projectSlug),
|
||||||
|
queryFn: () => fetchApprovalPolicies({ projectSlug, envSlug, authorUserId }),
|
||||||
|
...options,
|
||||||
|
enabled: Boolean(projectSlug) && (options?.enabled ?? true)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useGetAccessApprovalRequests = ({
|
||||||
|
projectSlug,
|
||||||
|
envSlug,
|
||||||
|
authorUserId,
|
||||||
|
options = {}
|
||||||
|
}: TGetAccessApprovalRequestsDTO & {
|
||||||
|
options?: UseQueryOptions<
|
||||||
|
TAccessApprovalRequest[],
|
||||||
|
unknown,
|
||||||
|
TAccessApprovalRequest[],
|
||||||
|
ReturnType<typeof accessApprovalKeys.getAccessApprovalRequests>
|
||||||
|
>;
|
||||||
|
}) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: accessApprovalKeys.getAccessApprovalRequests(projectSlug, envSlug, authorUserId),
|
||||||
|
queryFn: () => fetchApprovalRequests({ projectSlug, envSlug, authorUserId }),
|
||||||
|
...options,
|
||||||
|
enabled: Boolean(projectSlug) && (options?.enabled ?? true)
|
||||||
|
});
|
142
frontend/src/hooks/api/accessApproval/types.ts
Normal file
142
frontend/src/hooks/api/accessApproval/types.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import { TProjectPermission } from "../roles/types";
|
||||||
|
import { WorkspaceEnv } from "../workspace/types";
|
||||||
|
|
||||||
|
export type TAccessApprovalPolicy = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
approvals: number;
|
||||||
|
secretPath: string;
|
||||||
|
envId: string;
|
||||||
|
workspace: string;
|
||||||
|
environment: WorkspaceEnv;
|
||||||
|
projectId: string;
|
||||||
|
approvers: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAccessApprovalRequest = {
|
||||||
|
id: string;
|
||||||
|
policyId: string;
|
||||||
|
privilegeId: string | null;
|
||||||
|
requestedByUserId: string;
|
||||||
|
groupMembershipId: string | null;
|
||||||
|
projectMembershipId: string | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
isTemporary: boolean;
|
||||||
|
temporaryRange: string | null | undefined;
|
||||||
|
|
||||||
|
permissions: TProjectPermission[] | null;
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
environmentName: string;
|
||||||
|
isApproved: boolean;
|
||||||
|
|
||||||
|
privilege: {
|
||||||
|
groupMembershipId: string | null;
|
||||||
|
projectMembershipId: string | null;
|
||||||
|
isTemporary: boolean;
|
||||||
|
temporaryMode?: string | null;
|
||||||
|
temporaryRange?: string | null;
|
||||||
|
temporaryAccessStartTime?: Date | null;
|
||||||
|
temporaryAccessEndTime?: Date | null;
|
||||||
|
permissions: TProjectPermission[];
|
||||||
|
isApproved: boolean;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
policy: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
approvals: number;
|
||||||
|
approvers: string[];
|
||||||
|
secretPath?: string | null;
|
||||||
|
envId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
reviewers: {
|
||||||
|
member: string;
|
||||||
|
status: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAccessApproval = {
|
||||||
|
id: string;
|
||||||
|
policyId: string;
|
||||||
|
privilegeId: string;
|
||||||
|
requestedBy: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAccessRequestCount = {
|
||||||
|
pendingCount: number;
|
||||||
|
finalizedCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TProjectUserPrivilege = {
|
||||||
|
projectMembershipId: string;
|
||||||
|
slug: string;
|
||||||
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
permissions?: TProjectPermission[];
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
isTemporary: true;
|
||||||
|
temporaryMode: string;
|
||||||
|
temporaryRange: string;
|
||||||
|
temporaryAccessStartTime: string;
|
||||||
|
temporaryAccessEndTime?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
isTemporary: false;
|
||||||
|
temporaryMode?: null;
|
||||||
|
temporaryRange?: null;
|
||||||
|
temporaryAccessStartTime?: null;
|
||||||
|
temporaryAccessEndTime?: null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export type TCreateAccessRequestDTO = {
|
||||||
|
projectSlug: string;
|
||||||
|
} & Omit<TProjectUserPrivilege, "id" | "createdAt" | "updatedAt" | "slug" | "projectMembershipId">;
|
||||||
|
|
||||||
|
export type TGetAccessApprovalRequestsDTO = {
|
||||||
|
projectSlug: string;
|
||||||
|
envSlug?: string;
|
||||||
|
authorUserId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetAccessPolicyApprovalCountDTO = {
|
||||||
|
projectSlug: string;
|
||||||
|
envSlug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetSecretApprovalPolicyOfBoardDTO = {
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateAccessPolicyDTO = {
|
||||||
|
projectSlug: string;
|
||||||
|
name?: string;
|
||||||
|
environment: string;
|
||||||
|
approvers?: string[];
|
||||||
|
approvals?: number;
|
||||||
|
secretPath?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateAccessPolicyDTO = {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
approvers?: string[];
|
||||||
|
secretPath?: string;
|
||||||
|
environment?: string;
|
||||||
|
approvals?: number;
|
||||||
|
// for invalidating list
|
||||||
|
projectSlug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteSecretPolicyDTO = {
|
||||||
|
id: string;
|
||||||
|
// for invalidating list
|
||||||
|
projectSlug: string;
|
||||||
|
};
|
@ -26,7 +26,7 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
|||||||
[EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET]: "Create universal auth client secret",
|
[EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET]: "Create universal auth client secret",
|
||||||
[EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET]: "Revoke universal auth client secret",
|
[EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET]: "Revoke universal auth client secret",
|
||||||
[EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS]: "Get universal auth client secrets",
|
[EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS]: "Get universal auth client secrets",
|
||||||
[EventType.GET_IDENTITY_UNIVERSAL_AUTH]: "Get universal auth",
|
[EventType.SECRET_APPROVAL_REQUEST_CREATED]: "Secret approval request created",
|
||||||
[EventType.CREATE_ENVIRONMENT]: "Create environment",
|
[EventType.CREATE_ENVIRONMENT]: "Create environment",
|
||||||
[EventType.UPDATE_ENVIRONMENT]: "Update environment",
|
[EventType.UPDATE_ENVIRONMENT]: "Update environment",
|
||||||
[EventType.DELETE_ENVIRONMENT]: "Delete environment",
|
[EventType.DELETE_ENVIRONMENT]: "Delete environment",
|
||||||
|
@ -56,5 +56,6 @@ export enum EventType {
|
|||||||
UPDATE_SECRET_IMPORT = "update-secret-import",
|
UPDATE_SECRET_IMPORT = "update-secret-import",
|
||||||
DELETE_SECRET_IMPORT = "delete-secret-import",
|
DELETE_SECRET_IMPORT = "delete-secret-import",
|
||||||
UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role",
|
UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role",
|
||||||
UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions"
|
UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions",
|
||||||
|
SECRET_APPROVAL_REQUEST_CREATED = "secret-approval-request"
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,16 @@ interface GetSecretsEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SecretApprovalRequestCreatedEvent {
|
||||||
|
type: EventType.SECRET_APPROVAL_REQUEST_CREATED;
|
||||||
|
metadata: {
|
||||||
|
secretApprovalRequestId: string;
|
||||||
|
secretApprovalRequestSlug: string;
|
||||||
|
committedByUser?: string;
|
||||||
|
committedBy?: undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface GetSecretEvent {
|
interface GetSecretEvent {
|
||||||
type: EventType.GET_SECRET;
|
type: EventType.GET_SECRET;
|
||||||
metadata: {
|
metadata: {
|
||||||
@ -465,6 +475,7 @@ interface UpdateUserDeniedPermissions {
|
|||||||
export type Event =
|
export type Event =
|
||||||
| GetSecretsEvent
|
| GetSecretsEvent
|
||||||
| GetSecretEvent
|
| GetSecretEvent
|
||||||
|
| SecretApprovalRequestCreatedEvent
|
||||||
| CreateSecretEvent
|
| CreateSecretEvent
|
||||||
| UpdateSecretEvent
|
| UpdateSecretEvent
|
||||||
| DeleteSecretEvent
|
| DeleteSecretEvent
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
export * from "./accessApproval";
|
||||||
export * from "./admin";
|
export * from "./admin";
|
||||||
export * from "./apiKeys";
|
export * from "./apiKeys";
|
||||||
export * from "./auditLogs";
|
export * from "./auditLogs";
|
||||||
|
@ -46,7 +46,7 @@ export type TSecretApprovalRequest<J extends unknown = EncryptedSecret> = {
|
|||||||
id: string;
|
id: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
committerId: string;
|
committerUserId: string;
|
||||||
reviewers: {
|
reviewers: {
|
||||||
member: string;
|
member: string;
|
||||||
status: ApprovalStatus;
|
status: ApprovalStatus;
|
||||||
@ -58,7 +58,7 @@ export type TSecretApprovalRequest<J extends unknown = EncryptedSecret> = {
|
|||||||
hasMerged: boolean;
|
hasMerged: boolean;
|
||||||
status: "open" | "close";
|
status: "open" | "close";
|
||||||
policy: TSecretApprovalPolicy;
|
policy: TSecretApprovalPolicy;
|
||||||
statusChangeBy: string;
|
statusChangeByUserId: string;
|
||||||
conflicts: Array<{ secretId: string; op: CommitType.UPDATE }>;
|
conflicts: Array<{ secretId: string; op: CommitType.UPDATE }>;
|
||||||
commits: ({
|
commits: ({
|
||||||
// if there is no secret means it was creation
|
// if there is no secret means it was creation
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ZodIssue } from "zod";
|
import { ZodIssue } from "zod";
|
||||||
|
|
||||||
|
export type { TAccessApprovalPolicy } from "./accessApproval/types";
|
||||||
export type { TAuditLogStream } from "./auditLogStreams/types";
|
export type { TAuditLogStream } from "./auditLogStreams/types";
|
||||||
export type { GetAuthTokenAPI } from "./auth/types";
|
export type { GetAuthTokenAPI } from "./auth/types";
|
||||||
export type { IncidentContact } from "./incidentContacts/types";
|
export type { IncidentContact } from "./incidentContacts/types";
|
||||||
|
@ -75,6 +75,7 @@ export type TWorkspaceUser = {
|
|||||||
id: string;
|
id: string;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
};
|
};
|
||||||
|
isGroupMember?: boolean;
|
||||||
inviteEmail: string;
|
inviteEmail: string;
|
||||||
organization: string;
|
organization: string;
|
||||||
roles: (
|
roles: (
|
||||||
|
@ -307,14 +307,19 @@ export const useDeleteWsEnvironment = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetWorkspaceUsers = (workspaceId: string) => {
|
export const useGetWorkspaceUsers = (workspaceId: string, includeGroupMembers?: boolean) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: workspaceKeys.getWorkspaceUsers(workspaceId),
|
queryKey: workspaceKeys.getWorkspaceUsers(workspaceId),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const {
|
const {
|
||||||
data: { users }
|
data: { users }
|
||||||
} = await apiRequest.get<{ users: TWorkspaceUser[] }>(
|
} = await apiRequest.get<{ users: TWorkspaceUser[] }>(
|
||||||
`/api/v1/workspace/${workspaceId}/users`
|
`/api/v1/workspace/${workspaceId}/users`,
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
includeGroupMembers
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
return users;
|
return users;
|
||||||
},
|
},
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
/* eslint-disable no-var */
|
/* eslint-disable no-var */
|
||||||
/* eslint-disable func-names */
|
/* eslint-disable func-names */
|
||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
@ -64,6 +64,7 @@ import {
|
|||||||
fetchOrgUsers,
|
fetchOrgUsers,
|
||||||
useAddUserToWsNonE2EE,
|
useAddUserToWsNonE2EE,
|
||||||
useCreateWorkspace,
|
useCreateWorkspace,
|
||||||
|
useGetAccessRequestsCount,
|
||||||
useGetOrgTrialUrl,
|
useGetOrgTrialUrl,
|
||||||
useGetSecretApprovalRequestCount,
|
useGetSecretApprovalRequestCount,
|
||||||
useGetUserAction,
|
useGetUserAction,
|
||||||
@ -124,9 +125,15 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
|
const projectSlug = currentWorkspace?.slug || "";
|
||||||
const { data: updateClosed } = useGetUserAction("december_update_closed");
|
const { data: updateClosed } = useGetUserAction("december_update_closed");
|
||||||
|
|
||||||
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId });
|
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId });
|
||||||
|
const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug });
|
||||||
|
|
||||||
|
const pendingRequestsCount = useMemo(() => {
|
||||||
|
return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
||||||
|
}, [secretApprovalReqCount, accessApprovalRequestCount]);
|
||||||
|
|
||||||
const isAddingProjectsAllowed = subscription?.workspaceLimit
|
const isAddingProjectsAllowed = subscription?.workspaceLimit
|
||||||
? subscription.workspacesUsed < subscription.workspaceLimit
|
? subscription.workspacesUsed < subscription.workspaceLimit
|
||||||
@ -554,10 +561,13 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
}
|
}
|
||||||
icon="system-outline-189-domain-verification"
|
icon="system-outline-189-domain-verification"
|
||||||
>
|
>
|
||||||
Secret Approvals
|
Approvals
|
||||||
{Boolean(secretApprovalReqCount?.open) && (
|
{Boolean(
|
||||||
|
secretApprovalReqCount?.open ||
|
||||||
|
accessApprovalRequestCount?.pendingCount
|
||||||
|
) && (
|
||||||
<span className="ml-2 rounded border border-primary-400 bg-primary-600 py-0.5 px-1 text-xs font-semibold text-black">
|
<span className="ml-2 rounded border border-primary-400 bg-primary-600 py-0.5 px-1 text-xs font-semibold text-black">
|
||||||
{secretApprovalReqCount?.open}
|
{pendingRequestsCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -35,8 +35,6 @@ export default function LoginPage() {
|
|||||||
const selectOrg = useSelectOrganization();
|
const selectOrg = useSelectOrganization();
|
||||||
const { user, isLoading: userLoading } = useUser();
|
const { user, isLoading: userLoading } = useUser();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const queryParams = new URLSearchParams(window.location.search);
|
const queryParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
const logout = useLogoutUser(true);
|
const logout = useLogoutUser(true);
|
||||||
@ -153,7 +151,7 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-md text-center text-gray-500">
|
<p className="text-md text-center text-gray-500">
|
||||||
You‘re currently logged in as <strong>{user.email}</strong>
|
You‘re currently logged in as <strong>{user.username}</strong>
|
||||||
</p>
|
</p>
|
||||||
<p className="text-md text-center text-gray-500">
|
<p className="text-md text-center text-gray-500">
|
||||||
Not you?{" "}
|
Not you?{" "}
|
||||||
|
@ -38,6 +38,13 @@ export const LogsTableRow = ({ auditLog }: Props) => {
|
|||||||
|
|
||||||
const renderMetadata = (event: Event) => {
|
const renderMetadata = (event: Event) => {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
|
case EventType.SECRET_APPROVAL_REQUEST_CREATED:
|
||||||
|
return (
|
||||||
|
<Td>
|
||||||
|
<p>{`Request ID: ${event.metadata.secretApprovalRequestId}`}</p>
|
||||||
|
<p>{`Request slug: ${event.metadata.secretApprovalRequestSlug}`}</p>
|
||||||
|
</Td>
|
||||||
|
);
|
||||||
case EventType.GET_SECRETS:
|
case EventType.GET_SECRETS:
|
||||||
return (
|
return (
|
||||||
<Td>
|
<Td>
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import {
|
import {
|
||||||
faArrowRotateLeft,
|
faArrowRotateLeft,
|
||||||
faCaretDown,
|
faCaretDown,
|
||||||
faCheck,
|
faCheck,
|
||||||
faClock,
|
faClock,
|
||||||
|
faLockOpen,
|
||||||
faPlus,
|
faPlus,
|
||||||
faTrash
|
faTrash
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
@ -44,11 +46,13 @@ import {
|
|||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
TProjectUserPrivilege,
|
TProjectUserPrivilege,
|
||||||
|
useCreateAccessRequest,
|
||||||
useCreateProjectUserAdditionalPrivilege,
|
useCreateProjectUserAdditionalPrivilege,
|
||||||
useDeleteProjectUserAdditionalPrivilege,
|
useDeleteProjectUserAdditionalPrivilege,
|
||||||
useListProjectUserPrivileges,
|
useListProjectUserPrivileges,
|
||||||
useUpdateProjectUserAdditionalPrivilege
|
useUpdateProjectUserAdditionalPrivilege
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
|
import { TAccessApprovalPolicy } from "@app/hooks/api/types";
|
||||||
|
|
||||||
const secretPermissionSchema = z.object({
|
const secretPermissionSchema = z.object({
|
||||||
secretPath: z.string().optional(),
|
secretPath: z.string().optional(),
|
||||||
@ -70,23 +74,34 @@ const secretPermissionSchema = z.object({
|
|||||||
])
|
])
|
||||||
});
|
});
|
||||||
type TSecretPermissionForm = z.infer<typeof secretPermissionSchema>;
|
type TSecretPermissionForm = z.infer<typeof secretPermissionSchema>;
|
||||||
const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPrivilege }) => {
|
export const SpecificPrivilegeSecretForm = ({
|
||||||
|
privilege,
|
||||||
|
policies,
|
||||||
|
onClose
|
||||||
|
}: {
|
||||||
|
privilege?: TProjectUserPrivilege;
|
||||||
|
policies?: TAccessApprovalPolicy[];
|
||||||
|
onClose?: () => void;
|
||||||
|
}) => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||||
"deletePrivilege"
|
"deletePrivilege",
|
||||||
|
"requestAccess"
|
||||||
] as const);
|
] as const);
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
const isMemberEditDisabled = permission.cannot(
|
const isMemberEditDisabled =
|
||||||
ProjectPermissionActions.Edit,
|
permission.cannot(ProjectPermissionActions.Edit, ProjectPermissionSub.Member) && !!privilege;
|
||||||
ProjectPermissionSub.Member
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateUserPrivilege = useUpdateProjectUserAdditionalPrivilege();
|
const updateUserPrivilege = useUpdateProjectUserAdditionalPrivilege();
|
||||||
const deleteUserPrivilege = useDeleteProjectUserAdditionalPrivilege();
|
const deleteUserPrivilege = useDeleteProjectUserAdditionalPrivilege();
|
||||||
|
const requestAccess = useCreateAccessRequest();
|
||||||
|
|
||||||
const privilegeForm = useForm<TSecretPermissionForm>({
|
const privilegeForm = useForm<TSecretPermissionForm>({
|
||||||
resolver: zodResolver(secretPermissionSchema),
|
resolver: zodResolver(secretPermissionSchema),
|
||||||
values: {
|
values: {
|
||||||
|
...(privilege
|
||||||
|
? {
|
||||||
environmentSlug: privilege.permissions?.[0]?.conditions?.environment,
|
environmentSlug: privilege.permissions?.[0]?.conditions?.environment,
|
||||||
// secret path will be inside $glob operator
|
// secret path will be inside $glob operator
|
||||||
secretPath: privilege.permissions?.[0]?.conditions?.secretPath?.$glob || "",
|
secretPath: privilege.permissions?.[0]?.conditions?.secretPath?.$glob || "",
|
||||||
@ -105,16 +120,59 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
|
|||||||
// zod will pick it
|
// zod will pick it
|
||||||
temporaryAccess: privilege
|
temporaryAccess: privilege
|
||||||
}
|
}
|
||||||
|
: {
|
||||||
|
environmentSlug: currentWorkspace?.environments?.[0].slug!,
|
||||||
|
read: false,
|
||||||
|
edit: false,
|
||||||
|
create: false,
|
||||||
|
delete: false,
|
||||||
|
temporaryAccess: {
|
||||||
|
isTemporary: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const temporaryAccessField = privilegeForm.watch("temporaryAccess");
|
const temporaryAccessField = privilegeForm.watch("temporaryAccess");
|
||||||
const selectedEnvironmentSlug = privilegeForm.watch("environmentSlug");
|
const selectedEnvironment = privilegeForm.watch("environmentSlug");
|
||||||
|
const secretPath = privilegeForm.watch("secretPath");
|
||||||
|
|
||||||
|
const readAccess = privilegeForm.watch("read");
|
||||||
|
const createAccess = privilegeForm.watch("create");
|
||||||
|
const editAccess = privilegeForm.watch("edit");
|
||||||
|
const deleteAccess = privilegeForm.watch("delete");
|
||||||
|
|
||||||
|
const accessSelected = readAccess || createAccess || editAccess || deleteAccess;
|
||||||
|
|
||||||
|
const selectablePaths = useMemo(() => {
|
||||||
|
if (!policies) return [];
|
||||||
|
const environmentPolicies = policies.filter(
|
||||||
|
(policy) => policy.environment.slug === selectedEnvironment
|
||||||
|
);
|
||||||
|
|
||||||
|
privilegeForm.setValue("secretPath", "", {
|
||||||
|
shouldValidate: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...environmentPolicies.map((policy) => policy.secretPath)];
|
||||||
|
}, [policies, selectedEnvironment]);
|
||||||
|
|
||||||
const isTemporary = temporaryAccessField?.isTemporary;
|
const isTemporary = temporaryAccessField?.isTemporary;
|
||||||
const isExpired =
|
const isExpired =
|
||||||
temporaryAccessField.isTemporary &&
|
temporaryAccessField.isTemporary &&
|
||||||
new Date() > new Date(temporaryAccessField.temporaryAccessEndTime || "");
|
new Date() > new Date(temporaryAccessField.temporaryAccessEndTime || "");
|
||||||
|
|
||||||
const handleUpdatePrivilege = async (data: TSecretPermissionForm) => {
|
const handleUpdatePrivilege = async (data: TSecretPermissionForm) => {
|
||||||
|
if (!privilege) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "No privilege to update found.",
|
||||||
|
title: "Error"
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (updateUserPrivilege.isLoading) return;
|
if (updateUserPrivilege.isLoading) return;
|
||||||
try {
|
try {
|
||||||
const actions = [
|
const actions = [
|
||||||
@ -152,6 +210,15 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDeletePrivilege = async () => {
|
const handleDeletePrivilege = async () => {
|
||||||
|
if (!privilege) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "No privilege to delete found.",
|
||||||
|
title: "Error"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (deleteUserPrivilege.isLoading) return;
|
if (deleteUserPrivilege.isLoading) return;
|
||||||
try {
|
try {
|
||||||
await deleteUserPrivilege.mutateAsync({
|
await deleteUserPrivilege.mutateAsync({
|
||||||
@ -170,35 +237,100 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// This is used for requesting access additional privileges, not directly creating a privilege!
|
||||||
|
const handleRequestAccess = async (data: TSecretPermissionForm) => {
|
||||||
|
if (!policies) return;
|
||||||
|
if (!currentWorkspace) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "No workspace found.",
|
||||||
|
title: "Error"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.secretPath) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Please select a secret path",
|
||||||
|
title: "Error"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = [
|
||||||
|
{ action: ProjectPermissionActions.Read, allowed: data.read },
|
||||||
|
{ action: ProjectPermissionActions.Create, allowed: data.create },
|
||||||
|
{ action: ProjectPermissionActions.Delete, allowed: data.delete },
|
||||||
|
{ action: ProjectPermissionActions.Edit, allowed: data.edit }
|
||||||
|
];
|
||||||
|
const conditions: Record<string, any> = { environment: data.environmentSlug };
|
||||||
|
if (data.secretPath) {
|
||||||
|
conditions.secretPath = { $glob: data.secretPath };
|
||||||
|
}
|
||||||
|
await requestAccess.mutateAsync({
|
||||||
|
...data,
|
||||||
|
...(data.temporaryAccess.isTemporary && {
|
||||||
|
temporaryRange: data.temporaryAccess.temporaryRange
|
||||||
|
}),
|
||||||
|
projectSlug: currentWorkspace.slug,
|
||||||
|
isTemporary: data.temporaryAccess.isTemporary,
|
||||||
|
permissions: actions
|
||||||
|
.filter(({ allowed }) => allowed)
|
||||||
|
.map(({ action }) => ({
|
||||||
|
action,
|
||||||
|
subject: [ProjectPermissionSub.Secrets],
|
||||||
|
conditions
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully requested access"
|
||||||
|
});
|
||||||
|
privilegeForm.reset();
|
||||||
|
if (onClose) onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (data: TSecretPermissionForm) => {
|
||||||
|
if (privilege) {
|
||||||
|
handleUpdatePrivilege(data);
|
||||||
|
} else {
|
||||||
|
handleRequestAccess(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getAccessLabel = (exactTime = false) => {
|
const getAccessLabel = (exactTime = false) => {
|
||||||
if (isExpired) return "Access expired";
|
if (isExpired) return "Access expired";
|
||||||
if (!temporaryAccessField?.isTemporary) return "Permanent";
|
if (!temporaryAccessField?.isTemporary) return "Permanent";
|
||||||
if (exactTime)
|
|
||||||
|
if (exactTime && !policies) {
|
||||||
return `Until ${format(
|
return `Until ${format(
|
||||||
new Date(temporaryAccessField.temporaryAccessEndTime || ""),
|
new Date(temporaryAccessField.temporaryAccessEndTime || ""),
|
||||||
"yyyy-MM-dd HH:mm:ss"
|
"yyyy-MM-dd HH:mm:ss"
|
||||||
)}`;
|
)}`;
|
||||||
|
}
|
||||||
return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date());
|
return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date());
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4">
|
<div className="mt-4 w-full">
|
||||||
<form onSubmit={privilegeForm.handleSubmit(handleUpdatePrivilege)}>
|
<form onSubmit={privilegeForm.handleSubmit(handleSubmit)}>
|
||||||
<div className="flex items-start space-x-4">
|
<div className={twMerge("flex items-start gap-4", !privilege && "flex-wrap")}>
|
||||||
<Controller
|
<Controller
|
||||||
control={privilegeForm.control}
|
control={privilegeForm.control}
|
||||||
name="environmentSlug"
|
name="environmentSlug"
|
||||||
render={({ field: { onChange, ...field } }) => (
|
render={({ field: { onChange, ...field } }) => (
|
||||||
<FormControl label="Env">
|
<FormControl label="Environment">
|
||||||
<Select
|
<Select
|
||||||
{...field}
|
{...field}
|
||||||
isDisabled={isMemberEditDisabled}
|
isDisabled={isMemberEditDisabled}
|
||||||
className="bg-mineshaft-600 hover:bg-mineshaft-500"
|
className="bg-mineshaft-600 hover:bg-mineshaft-500"
|
||||||
onValueChange={(e) => onChange(e)}
|
onValueChange={(e) => onChange(e)}
|
||||||
>
|
>
|
||||||
{currentWorkspace?.environments?.map(({ slug, id }) => (
|
{currentWorkspace?.environments?.map(({ slug, id, name }) => (
|
||||||
<SelectItem value={slug} key={id}>
|
<SelectItem value={slug} key={id}>
|
||||||
{slug}
|
{name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
@ -208,16 +340,43 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
|
|||||||
<Controller
|
<Controller
|
||||||
control={privilegeForm.control}
|
control={privilegeForm.control}
|
||||||
name="secretPath"
|
name="secretPath"
|
||||||
render={({ field }) => (
|
render={({ field }) => {
|
||||||
|
if (policies) {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
isDisabled={!!selectablePaths.length}
|
||||||
|
content="The selected environment doesn't have any policies."
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<FormControl label="Secret Path">
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
isDisabled={isMemberEditDisabled || !selectablePaths.length}
|
||||||
|
className="w-48"
|
||||||
|
onValueChange={(e) => field.onChange(e)}
|
||||||
|
>
|
||||||
|
{selectablePaths.map((path) => (
|
||||||
|
<SelectItem value={path} key={path}>
|
||||||
|
{path}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
<FormControl label="Secret Path">
|
<FormControl label="Secret Path">
|
||||||
<SecretPathInput
|
<SecretPathInput
|
||||||
{...field}
|
{...field}
|
||||||
isDisabled={isMemberEditDisabled}
|
isDisabled={isMemberEditDisabled}
|
||||||
containerClassName="w-48"
|
containerClassName="w-48"
|
||||||
environment={selectedEnvironmentSlug}
|
environment={selectedEnvironment}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-grow justify-between">
|
<div className="flex flex-grow justify-between">
|
||||||
<Controller
|
<Controller
|
||||||
@ -285,7 +444,7 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-7 flex items-center space-x-2">
|
<div className="mt-6 flex items-center space-x-2">
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger disabled={isMemberEditDisabled}>
|
<PopoverTrigger disabled={isMemberEditDisabled}>
|
||||||
<div>
|
<div>
|
||||||
@ -301,7 +460,7 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
|
|||||||
isExpired && "text-red-600"
|
isExpired && "text-red-600"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{getAccessLabel()}
|
{getAccessLabel(false)}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
@ -362,8 +521,9 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{temporaryAccessField.isTemporary ? "Restart" : "Grant"}
|
{temporaryAccessField.isTemporary && !policies ? "Restart" : "Grant"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{temporaryAccessField.isTemporary && (
|
{temporaryAccessField.isTemporary && (
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
@ -375,14 +535,15 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Revoke Access
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
{privilegeForm.formState.isDirty ? (
|
{/* eslint-disable-next-line no-nested-ternary */}
|
||||||
|
{privilegeForm.formState.isDirty && privilege ? (
|
||||||
<>
|
<>
|
||||||
<Tooltip content="Cancel" className="mr-4">
|
<Tooltip content="Cancel" className="mr-4">
|
||||||
<IconButton
|
<IconButton
|
||||||
@ -413,7 +574,8 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : // eslint-disable-next-line no-nested-ternary
|
||||||
|
privilege ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={isMemberEditDisabled ? "Access restricted" : "Delete"}
|
content={isMemberEditDisabled ? "Access restricted" : "Delete"}
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
@ -428,9 +590,28 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
|
|||||||
<FontAwesomeIcon icon={faTrash} />
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{!!policies && (
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
isLoading={privilegeForm.formState.isSubmitting || requestAccess.isLoading}
|
||||||
|
isDisabled={
|
||||||
|
isMemberEditDisabled ||
|
||||||
|
!policies.length ||
|
||||||
|
!privilegeForm.formState.isValid ||
|
||||||
|
!secretPath ||
|
||||||
|
!accessSelected
|
||||||
|
}
|
||||||
|
className="mt-4"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faLockOpen} />}
|
||||||
|
>
|
||||||
|
Request access
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.deletePrivilege.isOpen}
|
isOpen={popUp.deletePrivilege.isOpen}
|
||||||
|
@ -3,25 +3,31 @@ import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
|
|||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||||
|
import { Divider } from "@app/components/v2/Divider";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
|
|
||||||
|
import { AccessApprovalPolicyList } from "./components/AccessApprovalPolicyList";
|
||||||
|
import { AccessApprovalRequest } from "./components/AccessApprovalRequest";
|
||||||
import { SecretApprovalPolicyList } from "./components/SecretApprovalPolicyList";
|
import { SecretApprovalPolicyList } from "./components/SecretApprovalPolicyList";
|
||||||
import { SecretApprovalRequest } from "./components/SecretApprovalRequest";
|
import { SecretApprovalRequest } from "./components/SecretApprovalRequest";
|
||||||
|
|
||||||
enum TabSection {
|
enum TabSection {
|
||||||
ApprovalRequests = "approval-requests",
|
SecretApprovalRequests = "approval-requests",
|
||||||
Rules = "approval-rules"
|
SecretPolicies = "approval-rules",
|
||||||
|
ResourcePolicies = "resource-rules",
|
||||||
|
ResourceApprovalRequests = "resource-requests"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SecretApprovalPage = () => {
|
export const SecretApprovalPage = () => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const projectId = currentWorkspace?.id || "";
|
||||||
|
const projectSlug = currentWorkspace?.slug || "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto h-full w-full max-w-7xl bg-bunker-800 px-6 text-white">
|
<div className="container mx-auto h-full w-full max-w-7xl bg-bunker-800 px-6 text-white">
|
||||||
<div className="flex items-center justify-between py-6">
|
<div className="flex items-center justify-between py-6">
|
||||||
<div className="flex w-full flex-col">
|
<div className="flex w-full flex-col">
|
||||||
<h2 className="text-3xl font-semibold text-gray-200">Secret Approval Workflows</h2>
|
<h2 className="text-3xl font-semibold text-gray-200">Approval Workflows</h2>
|
||||||
<p className="text-bunker-300">
|
<p className="text-bunker-300">
|
||||||
Create approval policies for any modifications to secrets in sensitive environments and
|
Create approval policies for any modifications to secrets in sensitive environments and
|
||||||
folders.
|
folders.
|
||||||
@ -39,16 +45,26 @@ export const SecretApprovalPage = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Tabs defaultValue={TabSection.ApprovalRequests}>
|
<Tabs defaultValue={TabSection.SecretApprovalRequests}>
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab value={TabSection.ApprovalRequests}>Secret PRs</Tab>
|
<Tab value={TabSection.SecretApprovalRequests}>Secret Requests</Tab>
|
||||||
<Tab value={TabSection.Rules}>Policies</Tab>
|
<Tab value={TabSection.SecretPolicies}>Secret Policies</Tab>
|
||||||
|
<Divider />
|
||||||
|
<Tab value={TabSection.ResourceApprovalRequests}>Access Requests</Tab>
|
||||||
|
<Tab value={TabSection.ResourcePolicies}>Access Request Policies</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel value={TabSection.ApprovalRequests}>
|
<TabPanel value={TabSection.SecretApprovalRequests}>
|
||||||
<SecretApprovalRequest />
|
<SecretApprovalRequest />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel value={TabSection.Rules}>
|
<TabPanel value={TabSection.SecretPolicies}>
|
||||||
<SecretApprovalPolicyList workspaceId={workspaceId} />
|
<SecretApprovalPolicyList workspaceId={projectId} />
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
<TabPanel value={TabSection.ResourceApprovalRequests}>
|
||||||
|
<AccessApprovalRequest projectId={projectId} projectSlug={projectSlug} />
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={TabSection.ResourcePolicies}>
|
||||||
|
<AccessApprovalPolicyList workspaceId={projectId} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,174 @@
|
|||||||
|
import { faFileShield, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DeleteActionModal,
|
||||||
|
EmptyState,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr,
|
||||||
|
UpgradePlanModal
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
useProjectPermission,
|
||||||
|
useSubscription,
|
||||||
|
useWorkspace
|
||||||
|
} from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { useDeleteAccessApprovalPolicy, useGetWorkspaceUsers } from "@app/hooks/api";
|
||||||
|
import { useGetAccessApprovalPolicies } from "@app/hooks/api/accessApproval/queries";
|
||||||
|
import { TAccessApprovalPolicy } from "@app/hooks/api/types";
|
||||||
|
|
||||||
|
import { AccessApprovalPolicyRow } from "./components/AccessApprovalPolicyRow";
|
||||||
|
import { AccessPolicyForm } from "./components/AccessPolicyModal";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
workspaceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AccessApprovalPolicyList = ({ workspaceId }: IProps) => {
|
||||||
|
const { handlePopUpToggle, handlePopUpOpen, handlePopUpClose, popUp } = usePopUp([
|
||||||
|
"secretPolicyForm",
|
||||||
|
"deletePolicy",
|
||||||
|
"upgradePlan"
|
||||||
|
] as const);
|
||||||
|
const { permission } = useProjectPermission();
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const { data: members } = useGetWorkspaceUsers(workspaceId);
|
||||||
|
const { data: policies, isLoading: isPoliciesLoading } = useGetAccessApprovalPolicies({
|
||||||
|
projectSlug: currentWorkspace?.slug as string,
|
||||||
|
options: {
|
||||||
|
enabled:
|
||||||
|
permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) &&
|
||||||
|
!!currentWorkspace?.slug
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteAccessApprovalPolicy();
|
||||||
|
|
||||||
|
const handleDeletePolicy = async () => {
|
||||||
|
const { id } = popUp.deletePolicy.data as TAccessApprovalPolicy;
|
||||||
|
if (!currentWorkspace?.slug) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteSecretApprovalPolicy({
|
||||||
|
projectSlug: currentWorkspace?.slug,
|
||||||
|
id
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully deleted policy"
|
||||||
|
});
|
||||||
|
handlePopUpClose("deletePolicy");
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to delete policy"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-6 flex items-end justify-between">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-xl font-semibold text-mineshaft-100">Access Request Policies</span>
|
||||||
|
<div className="mt-2 text-sm text-bunker-300">
|
||||||
|
Implement secret request policies for specific secrets and environments.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Create}
|
||||||
|
a={ProjectPermissionSub.SecretApproval}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (subscription && !subscription?.secretApproval) {
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handlePopUpOpen("secretPolicyForm");
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
Create policy
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th>Environment</Th>
|
||||||
|
<Th>Secret Path</Th>
|
||||||
|
<Th>Eligible Approvers</Th>
|
||||||
|
<Th>Approval Required</Th>
|
||||||
|
<Th />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isPoliciesLoading && (
|
||||||
|
<TableSkeleton columns={6} innerKey="secret-policies" className="bg-mineshaft-700" />
|
||||||
|
)}
|
||||||
|
{!isPoliciesLoading && !policies?.length && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={6}>
|
||||||
|
<EmptyState title="No policies found" icon={faFileShield} />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
{!!currentWorkspace &&
|
||||||
|
policies?.map((policy) => (
|
||||||
|
<AccessApprovalPolicyRow
|
||||||
|
projectSlug={currentWorkspace.slug}
|
||||||
|
policy={policy}
|
||||||
|
key={policy.id}
|
||||||
|
members={members}
|
||||||
|
onEdit={() => handlePopUpOpen("secretPolicyForm", policy)}
|
||||||
|
onDelete={() => handlePopUpOpen("deletePolicy", policy)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
<AccessPolicyForm
|
||||||
|
projectSlug={currentWorkspace?.slug!}
|
||||||
|
isOpen={popUp.secretPolicyForm.isOpen}
|
||||||
|
onToggle={(isOpen) => handlePopUpToggle("secretPolicyForm", isOpen)}
|
||||||
|
members={members}
|
||||||
|
editValues={popUp.secretPolicyForm.data as TAccessApprovalPolicy}
|
||||||
|
/>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deletePolicy.isOpen}
|
||||||
|
deleteKey="remove"
|
||||||
|
title="Do you want to remove this policy?"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deletePolicy", isOpen)}
|
||||||
|
onDeleteApproved={handleDeletePolicy}
|
||||||
|
/>
|
||||||
|
<UpgradePlanModal
|
||||||
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
|
text="You can add secret approval policy if you switch to Infisical's Enterprise plan."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,146 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { faCheckCircle, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Td,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||||
|
import { useUpdateAccessApprovalPolicy } from "@app/hooks/api";
|
||||||
|
import { TAccessApprovalPolicy } from "@app/hooks/api/types";
|
||||||
|
import { TWorkspaceUser } from "@app/hooks/api/users/types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
policy: TAccessApprovalPolicy;
|
||||||
|
members?: TWorkspaceUser[];
|
||||||
|
projectSlug: string;
|
||||||
|
onEdit: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AccessApprovalPolicyRow = ({
|
||||||
|
policy,
|
||||||
|
members = [],
|
||||||
|
projectSlug,
|
||||||
|
onEdit,
|
||||||
|
onDelete
|
||||||
|
}: Props) => {
|
||||||
|
const [selectedApprovers, setSelectedApprovers] = useState<string[]>([]);
|
||||||
|
const { mutate: updateAccessApprovalPolicy, isLoading } = useUpdateAccessApprovalPolicy();
|
||||||
|
const { permission } = useProjectPermission();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr>
|
||||||
|
<Td>{policy.name}</Td>
|
||||||
|
<Td>{policy.environment.slug}</Td>
|
||||||
|
<Td>{policy.secretPath || "*"}</Td>
|
||||||
|
<Td>
|
||||||
|
<DropdownMenu
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
updateAccessApprovalPolicy(
|
||||||
|
{
|
||||||
|
projectSlug,
|
||||||
|
id: policy.id,
|
||||||
|
approvers: selectedApprovers
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSettled: () => {
|
||||||
|
setSelectedApprovers([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedApprovers(policy.approvers);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
asChild
|
||||||
|
disabled={
|
||||||
|
isLoading ||
|
||||||
|
permission.cannot(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
isReadOnly
|
||||||
|
value={policy.approvers?.length ? `${policy.approvers.length} selected` : "None"}
|
||||||
|
className="text-left"
|
||||||
|
/>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }}
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel>
|
||||||
|
Select members that are allowed to approve changes
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
{members?.map(({ user }) => {
|
||||||
|
const isChecked = selectedApprovers.includes(user.id);
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
setSelectedApprovers((state) =>
|
||||||
|
isChecked ? state.filter((el) => el !== user.id) : [...state, user.id]
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
key={`create-policy-members-${user.id}`}
|
||||||
|
iconPos="right"
|
||||||
|
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
|
>
|
||||||
|
{user.username}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Td>
|
||||||
|
<Td>{policy.approvals}</Td>
|
||||||
|
<Td>
|
||||||
|
<div className="flex items-center justify-end space-x-4">
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={ProjectPermissionSub.SecretApproval}
|
||||||
|
renderTooltip
|
||||||
|
allowedLabel="Edit"
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<IconButton variant="plain" ariaLabel="edit" onClick={onEdit} isDisabled={!isAllowed}>
|
||||||
|
<FontAwesomeIcon icon={faPencil} size="lg" />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Delete}
|
||||||
|
a={ProjectPermissionSub.SecretApproval}
|
||||||
|
renderTooltip
|
||||||
|
allowedLabel="Delete"
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="danger"
|
||||||
|
size="lg"
|
||||||
|
ariaLabel="edit"
|
||||||
|
onClick={onDelete}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,268 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import {
|
||||||
|
useCreateAccessApprovalPolicy,
|
||||||
|
useUpdateAccessApprovalPolicy
|
||||||
|
} from "@app/hooks/api/accessApproval";
|
||||||
|
import { TAccessApprovalPolicy } from "@app/hooks/api/accessApproval/types";
|
||||||
|
import { TWorkspaceUser } from "@app/hooks/api/users/types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen?: boolean;
|
||||||
|
onToggle: (isOpen: boolean) => void;
|
||||||
|
members?: TWorkspaceUser[];
|
||||||
|
projectSlug: string;
|
||||||
|
editValues?: TAccessApprovalPolicy;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formSchema = z
|
||||||
|
.object({
|
||||||
|
environment: z.string(),
|
||||||
|
name: z.string().optional(),
|
||||||
|
secretPath: z.string().optional(),
|
||||||
|
approvals: z.number().min(1),
|
||||||
|
approvers: z.string().array().min(1)
|
||||||
|
})
|
||||||
|
.refine((data) => data.approvals <= data.approvers.length, {
|
||||||
|
path: ["approvals"],
|
||||||
|
message: "The number of approvals should be lower than the number of approvers."
|
||||||
|
});
|
||||||
|
|
||||||
|
type TFormSchema = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export const AccessPolicyForm = ({
|
||||||
|
isOpen,
|
||||||
|
onToggle,
|
||||||
|
members = [],
|
||||||
|
projectSlug,
|
||||||
|
editValues
|
||||||
|
}: Props) => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<TFormSchema>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
values: editValues ? { ...editValues, environment: editValues.environment.slug } : undefined
|
||||||
|
});
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const environments = currentWorkspace?.environments || [];
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) reset({});
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const isEditMode = Boolean(editValues);
|
||||||
|
|
||||||
|
const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy();
|
||||||
|
const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy();
|
||||||
|
|
||||||
|
const handleCreatePolicy = async (data: TFormSchema) => {
|
||||||
|
if (!projectSlug) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createAccessApprovalPolicy({
|
||||||
|
...data,
|
||||||
|
projectSlug
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully created policy"
|
||||||
|
});
|
||||||
|
onToggle(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to create policy"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdatePolicy = async (data: TFormSchema) => {
|
||||||
|
if (!projectSlug) return;
|
||||||
|
if (!editValues?.id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateAccessApprovalPolicy({
|
||||||
|
id: editValues?.id,
|
||||||
|
...data,
|
||||||
|
projectSlug
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully updated policy"
|
||||||
|
});
|
||||||
|
onToggle(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "failed to update policy"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormSubmit = async (data: TFormSchema) => {
|
||||||
|
if (isEditMode) {
|
||||||
|
await handleUpdatePolicy(data);
|
||||||
|
} else {
|
||||||
|
await handleCreatePolicy(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
||||||
|
<ModalContent title={isEditMode ? "Edit Access Policy" : "Create Access Policy"}>
|
||||||
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="name"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Policy Name" isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Input {...field} value={field.value || ""} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="environment"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Environment"
|
||||||
|
isRequired
|
||||||
|
className="mt-4"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
isDisabled={isEditMode}
|
||||||
|
value={value}
|
||||||
|
onValueChange={(val) => onChange(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem
|
||||||
|
value={sourceEnvironment.slug}
|
||||||
|
key={`azure-key-vault-environment-${sourceEnvironment.slug}`}
|
||||||
|
>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretPath"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Secret Path" isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Input {...field} value={field.value || ""} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="approvers"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Approvers Required"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Input
|
||||||
|
isReadOnly
|
||||||
|
value={value?.length ? `${value.length} selected` : "None"}
|
||||||
|
className="text-left"
|
||||||
|
/>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }}
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel>
|
||||||
|
Select members that are allowed to approve changes
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
{members.map(({ user }) => {
|
||||||
|
const isChecked = value?.includes(user.id);
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
onChange(
|
||||||
|
isChecked
|
||||||
|
? value?.filter((el) => el !== user.id)
|
||||||
|
: [...(value || []), user.id]
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
key={`create-policy-members-${user.id}`}
|
||||||
|
iconPos="right"
|
||||||
|
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
|
>
|
||||||
|
{user.username}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="approvals"
|
||||||
|
defaultValue={1}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Approvals Required"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
onChange={(el) => field.onChange(parseInt(el.target.value, 10))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="mt-8 flex items-center space-x-4">
|
||||||
|
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => onToggle(false)} variant="outline_bg">
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export { AccessApprovalPolicyList } from "./AccessApprovalPolicyList";
|
@ -0,0 +1,484 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
|
/* eslint-disable react/jsx-no-useless-fragment */
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
faCheck,
|
||||||
|
faCheckCircle,
|
||||||
|
faChevronDown,
|
||||||
|
faLock,
|
||||||
|
faPlus,
|
||||||
|
faXmark
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { formatDistance } from "date-fns";
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DeleteActionModal,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
EmptyState,
|
||||||
|
Tooltip,
|
||||||
|
UpgradePlanModal
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { Badge } from "@app/components/v2/Badge";
|
||||||
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
useProjectPermission,
|
||||||
|
useSubscription,
|
||||||
|
useUser,
|
||||||
|
useWorkspace
|
||||||
|
} from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { useDeleteAccessApprovalRequest, useGetWorkspaceUsers } from "@app/hooks/api";
|
||||||
|
import {
|
||||||
|
accessApprovalKeys,
|
||||||
|
useGetAccessApprovalPolicies,
|
||||||
|
useGetAccessApprovalRequests,
|
||||||
|
useGetAccessRequestsCount
|
||||||
|
} from "@app/hooks/api/accessApproval/queries";
|
||||||
|
import { TAccessApprovalRequest } from "@app/hooks/api/accessApproval/types";
|
||||||
|
import { ApprovalStatus, TWorkspaceUser } from "@app/hooks/api/types";
|
||||||
|
import { queryClient } from "@app/reactQuery";
|
||||||
|
|
||||||
|
import { RequestAccessModal } from "./components/RequestAccessModal";
|
||||||
|
import { ReviewAccessRequestModal } from "./components/ReviewAccessModal";
|
||||||
|
|
||||||
|
const generateRequestText = (request: TAccessApprovalRequest, membershipId: string) => {
|
||||||
|
const { isTemporary } = request;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full items-center justify-between text-sm">
|
||||||
|
<div>
|
||||||
|
Requested {isTemporary ? "temporary" : "permanent"} access to{" "}
|
||||||
|
<code className="mx-1 rounded-sm bg-primary-500/20 px-1.5 py-0.5 font-mono text-xs text-primary">
|
||||||
|
{request.policy.secretPath}
|
||||||
|
</code>
|
||||||
|
in
|
||||||
|
<code className="mx-1 rounded-sm bg-primary-500/20 px-1.5 py-0.5 font-mono text-xs text-primary">
|
||||||
|
{request.environmentName}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{request.requestedByUserId === membershipId && (
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
<Badge className="ml-1">Requested By You</Badge>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AccessApprovalRequest = ({
|
||||||
|
projectSlug,
|
||||||
|
projectId
|
||||||
|
}: {
|
||||||
|
projectSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
}) => {
|
||||||
|
const [selectedRequest, setSelectedRequest] = useState<
|
||||||
|
(TAccessApprovalRequest & { user: TWorkspaceUser["user"] | null }) | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
|
const { handlePopUpOpen, popUp, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
|
"requestAccess",
|
||||||
|
"deleteRequest",
|
||||||
|
"reviewRequest",
|
||||||
|
"upgradePlan"
|
||||||
|
] as const);
|
||||||
|
const { permission } = useProjectPermission();
|
||||||
|
const { user: currentUser } = useUser();
|
||||||
|
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const { data: members } = useGetWorkspaceUsers(projectId, true);
|
||||||
|
|
||||||
|
const membersGroupById = members?.reduce<Record<string, TWorkspaceUser>>(
|
||||||
|
(prev, curr) => ({ ...prev, [curr.user.id]: curr }),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const [statusFilter, setStatusFilter] = useState<"open" | "close">("open");
|
||||||
|
const [requestedByFilter, setRequestedByFilter] = useState<string | undefined>(undefined);
|
||||||
|
const [envFilter, setEnvFilter] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const { data: requestCount } = useGetAccessRequestsCount({
|
||||||
|
projectSlug
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: policies, isLoading: policiesLoading } = useGetAccessApprovalPolicies({
|
||||||
|
projectSlug
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: requests } = useGetAccessApprovalRequests({
|
||||||
|
projectSlug,
|
||||||
|
authorUserId: requestedByFilter,
|
||||||
|
envSlug: envFilter
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: deleteAccess, isLoading: deleteAccessIsLoading } =
|
||||||
|
useDeleteAccessApprovalRequest();
|
||||||
|
|
||||||
|
const filteredRequests = useMemo(() => {
|
||||||
|
if (statusFilter === "open") {
|
||||||
|
return requests?.filter(
|
||||||
|
(request) =>
|
||||||
|
!request.isApproved &&
|
||||||
|
!request.reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (statusFilter === "close") {
|
||||||
|
console.log(requests);
|
||||||
|
return requests?.filter(
|
||||||
|
(request) =>
|
||||||
|
request.isApproved ||
|
||||||
|
request.reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return requests;
|
||||||
|
}, [requests, statusFilter, requestedByFilter, envFilter]);
|
||||||
|
|
||||||
|
const generateRequestDetails = (request: TAccessApprovalRequest) => {
|
||||||
|
const isReviewedByUser =
|
||||||
|
request.reviewers.findIndex(({ member }) => member === currentUser.id) !== -1;
|
||||||
|
const isRejectedByAnyone = request.reviewers.some(
|
||||||
|
({ status }) => status === ApprovalStatus.REJECTED
|
||||||
|
);
|
||||||
|
const isApprover = request.policy.approvers.indexOf(currentUser.id || "") !== -1;
|
||||||
|
const isAccepted = request.isApproved;
|
||||||
|
|
||||||
|
const userReviewStatus = request.reviewers.find(
|
||||||
|
({ member }) => member === currentUser.id
|
||||||
|
)?.status;
|
||||||
|
|
||||||
|
let displayData: { label: string; type: "primary" | "danger" | "success" } = {
|
||||||
|
label: "",
|
||||||
|
type: "primary"
|
||||||
|
};
|
||||||
|
|
||||||
|
const isExpired =
|
||||||
|
request.privilege &&
|
||||||
|
request.isApproved &&
|
||||||
|
new Date() > new Date(request.privilege.temporaryAccessEndTime || ("" as string));
|
||||||
|
|
||||||
|
if (isExpired) displayData = { label: "Access Expired", type: "danger" };
|
||||||
|
else if (isAccepted) displayData = { label: "Access Granted", type: "success" };
|
||||||
|
else if (isRejectedByAnyone) displayData = { label: "Rejected", type: "danger" };
|
||||||
|
else if (userReviewStatus === ApprovalStatus.APPROVED) {
|
||||||
|
displayData = {
|
||||||
|
label: `Pending ${request.policy.approvals - request.reviewers.length} review${
|
||||||
|
request.policy.approvals - request.reviewers.length > 1 ? "s" : ""
|
||||||
|
}`,
|
||||||
|
type: "primary"
|
||||||
|
};
|
||||||
|
} else if (!isReviewedByUser)
|
||||||
|
displayData = {
|
||||||
|
label: "Review Required",
|
||||||
|
type: "primary"
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
displayData,
|
||||||
|
isReviewedByUser,
|
||||||
|
isRejectedByAnyone,
|
||||||
|
isApprover,
|
||||||
|
userReviewStatus,
|
||||||
|
isAccepted
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-6 flex items-end justify-between">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-xl font-semibold text-mineshaft-100">Access Requests</span>
|
||||||
|
<div className="mt-2 text-sm text-bunker-300">
|
||||||
|
Request access to secrets in sensitive environments and folders.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Tooltip
|
||||||
|
content="To submit Access Requests, your project needs to create Access Request policies first."
|
||||||
|
isDisabled={policiesLoading || !!policies?.length}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (subscription && !subscription?.secretApproval) {
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handlePopUpOpen("requestAccess");
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
isDisabled={policiesLoading || !policies?.length}
|
||||||
|
>
|
||||||
|
Request access
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
<motion.div
|
||||||
|
key="approval-changes-list"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
className="rounded-md text-gray-300"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-8 rounded-t-md border-x border-t border-mineshaft-600 bg-mineshaft-800 p-4 px-8">
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => setStatusFilter("open")}
|
||||||
|
onKeyDown={(evt) => {
|
||||||
|
if (evt.key === "Enter") setStatusFilter("open");
|
||||||
|
}}
|
||||||
|
className={
|
||||||
|
statusFilter === "close" ? "text-gray-500 duration-100 hover:text-gray-400" : ""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faLock} className="mr-2" />
|
||||||
|
{!!requestCount && requestCount?.pendingCount} Pending
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
statusFilter === "open" ? "text-gray-500 duration-100 hover:text-gray-400" : ""
|
||||||
|
}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => setStatusFilter("close")}
|
||||||
|
onKeyDown={(evt) => {
|
||||||
|
if (evt.key === "Enter") setStatusFilter("close");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCheck} className="mr-2" />
|
||||||
|
{!!requestCount && requestCount.finalizedCount} Completed
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-grow justify-end space-x-8">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="text-bunker-300"
|
||||||
|
rightIcon={<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />}
|
||||||
|
>
|
||||||
|
Environments
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuLabel>Select an environment</DropdownMenuLabel>
|
||||||
|
{currentWorkspace?.environments.map(({ slug, name }) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => setEnvFilter((state) => (state === slug ? undefined : slug))}
|
||||||
|
key={`request-filter-${slug}`}
|
||||||
|
icon={envFilter === slug && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
|
iconPos="right"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
{!!permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Member) && (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className={requestedByFilter ? "text-white" : "text-bunker-300"}
|
||||||
|
rightIcon={
|
||||||
|
<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Requested By
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuLabel>Select an author</DropdownMenuLabel>
|
||||||
|
{members?.map(({ user }) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
setRequestedByFilter((state) => (state === user.id ? undefined : user.id))
|
||||||
|
}
|
||||||
|
key={`request-filter-member-${user.id}`}
|
||||||
|
icon={
|
||||||
|
requestedByFilter === user.id && <FontAwesomeIcon icon={faCheckCircle} />
|
||||||
|
}
|
||||||
|
iconPos="right"
|
||||||
|
>
|
||||||
|
{user.username}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col rounded-b-md border-x border-t border-b border-mineshaft-600 bg-mineshaft-800">
|
||||||
|
{filteredRequests?.length === 0 && (
|
||||||
|
<div className="py-12">
|
||||||
|
<EmptyState title="No more access requests pending." />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!!filteredRequests?.length &&
|
||||||
|
filteredRequests?.map((request) => {
|
||||||
|
const details = generateRequestDetails(request);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
aria-disabled={
|
||||||
|
details.isReviewedByUser || details.isRejectedByAnyone || details.isAccepted
|
||||||
|
}
|
||||||
|
key={request.id}
|
||||||
|
className="flex w-full cursor-pointer px-8 py-4 hover:bg-mineshaft-700 aria-disabled:opacity-80"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
!details.isApprover ||
|
||||||
|
details.isReviewedByUser ||
|
||||||
|
details.isRejectedByAnyone ||
|
||||||
|
details.isAccepted
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
setSelectedRequest({
|
||||||
|
...request,
|
||||||
|
user: membersGroupById?.[request.requestedByUserId].user!
|
||||||
|
});
|
||||||
|
handlePopUpOpen("reviewRequest");
|
||||||
|
}}
|
||||||
|
onKeyDown={(evt) => {
|
||||||
|
if (
|
||||||
|
!details.isApprover ||
|
||||||
|
details.isAccepted ||
|
||||||
|
details.isReviewedByUser ||
|
||||||
|
details.isRejectedByAnyone
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
if (evt.key === "Enter") {
|
||||||
|
setSelectedRequest({
|
||||||
|
...request,
|
||||||
|
user: membersGroupById?.[request.requestedByUserId].user!
|
||||||
|
});
|
||||||
|
handlePopUpOpen("reviewRequest");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="flex w-full flex-col justify-between">
|
||||||
|
<div className="mb-1 flex w-full items-center">
|
||||||
|
<FontAwesomeIcon icon={faLock} className="mr-2" />
|
||||||
|
{generateRequestText(request, currentUser.id)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{membersGroupById?.[request.requestedByUserId]?.user && (
|
||||||
|
<>
|
||||||
|
Requested {formatDistance(new Date(request.createdAt), new Date())}{" "}
|
||||||
|
ago by{" "}
|
||||||
|
{membersGroupById?.[request.requestedByUserId]?.user?.firstName}{" "}
|
||||||
|
{membersGroupById?.[request.requestedByUserId]?.user?.lastName} (
|
||||||
|
{membersGroupById?.[request.requestedByUserId]?.user?.email}){" "}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{details.isApprover && (
|
||||||
|
<Badge variant={details.displayData.type}>
|
||||||
|
{details.displayData.label}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{details.isApprover && details.isAccepted && (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
onClick={async () => {
|
||||||
|
if (deleteAccessIsLoading) return;
|
||||||
|
|
||||||
|
handlePopUpOpen("deleteRequest", request.id);
|
||||||
|
}}
|
||||||
|
icon={faXmark}
|
||||||
|
className="ml-2 cursor-pointer opacity-70 hover:opacity-100"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{!!policies && (
|
||||||
|
<RequestAccessModal
|
||||||
|
policies={policies}
|
||||||
|
isOpen={popUp.requestAccess.isOpen}
|
||||||
|
onOpenChange={() => {
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
accessApprovalKeys.getAccessApprovalRequests(
|
||||||
|
projectSlug,
|
||||||
|
envFilter,
|
||||||
|
requestedByFilter
|
||||||
|
)
|
||||||
|
);
|
||||||
|
handlePopUpClose("requestAccess");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!!selectedRequest && (
|
||||||
|
<ReviewAccessRequestModal
|
||||||
|
selectedEnvSlug={envFilter}
|
||||||
|
selectedRequester={requestedByFilter}
|
||||||
|
projectSlug={projectSlug}
|
||||||
|
request={selectedRequest}
|
||||||
|
isOpen={popUp.reviewRequest.isOpen}
|
||||||
|
onOpenChange={() => {
|
||||||
|
handlePopUpClose("reviewRequest");
|
||||||
|
setSelectedRequest(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deleteRequest.isOpen}
|
||||||
|
title="Are you sure you want to delete the access request?"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deleteRequest", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={async () => {
|
||||||
|
await deleteAccess({
|
||||||
|
requestId: popUp.deleteRequest.data as string,
|
||||||
|
projectSlug
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Access request deleted successfully"
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpClose("deleteRequest");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UpgradePlanModal
|
||||||
|
text="You need to upgrade your plan to access this feature"
|
||||||
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
|
onOpenChange={() => handlePopUpClose("upgradePlan")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,25 @@
|
|||||||
|
import { Modal, ModalContent } from "@app/components/v2";
|
||||||
|
import { TAccessApprovalPolicy } from "@app/hooks/api/types";
|
||||||
|
import { SpecificPrivilegeSecretForm } from "@app/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection";
|
||||||
|
|
||||||
|
export const RequestAccessModal = ({
|
||||||
|
isOpen,
|
||||||
|
onOpenChange,
|
||||||
|
policies
|
||||||
|
}: {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
|
policies: TAccessApprovalPolicy[];
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<ModalContent
|
||||||
|
className="max-w-4xl"
|
||||||
|
title="Request Access"
|
||||||
|
subTitle="Your role has limited permissions, please contact your administrator to gain access"
|
||||||
|
>
|
||||||
|
<SpecificPrivilegeSecretForm onClose={() => onOpenChange(false)} policies={policies} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,158 @@
|
|||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import ms from "ms";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Button, Modal, ModalContent } from "@app/components/v2";
|
||||||
|
import { Badge } from "@app/components/v2/Badge";
|
||||||
|
import { ProjectPermissionActions } from "@app/context";
|
||||||
|
import { useReviewAccessRequest } from "@app/hooks/api";
|
||||||
|
import { TAccessApprovalRequest } from "@app/hooks/api/accessApproval/types";
|
||||||
|
import { TWorkspaceUser } from "@app/hooks/api/types";
|
||||||
|
|
||||||
|
export const ReviewAccessRequestModal = ({
|
||||||
|
isOpen,
|
||||||
|
onOpenChange,
|
||||||
|
request,
|
||||||
|
projectSlug,
|
||||||
|
selectedRequester,
|
||||||
|
selectedEnvSlug
|
||||||
|
}: {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
|
request: TAccessApprovalRequest & { user: TWorkspaceUser["user"] | null };
|
||||||
|
projectSlug: string;
|
||||||
|
selectedRequester: string | undefined;
|
||||||
|
selectedEnvSlug: string | undefined;
|
||||||
|
}) => {
|
||||||
|
const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null);
|
||||||
|
|
||||||
|
const accessDetails = {
|
||||||
|
env: request.environmentName,
|
||||||
|
// secret path will be inside $glob operator
|
||||||
|
secretPath: request.policy.secretPath,
|
||||||
|
read: request.permissions?.some(({ action }) => action.includes(ProjectPermissionActions.Read)),
|
||||||
|
edit: request.permissions?.some(({ action }) => action.includes(ProjectPermissionActions.Edit)),
|
||||||
|
create: request.permissions?.some(({ action }) =>
|
||||||
|
action.includes(ProjectPermissionActions.Create)
|
||||||
|
),
|
||||||
|
delete: request.permissions?.some(({ action }) =>
|
||||||
|
action.includes(ProjectPermissionActions.Delete)
|
||||||
|
),
|
||||||
|
|
||||||
|
temporaryAccess: {
|
||||||
|
isTemporary: request.isTemporary,
|
||||||
|
temporaryRange: request.temporaryRange
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestedAccess = useMemo(() => {
|
||||||
|
const access: string[] = [];
|
||||||
|
if (accessDetails.read) access.push("Read");
|
||||||
|
if (accessDetails.edit) access.push("Edit");
|
||||||
|
if (accessDetails.create) access.push("Create");
|
||||||
|
if (accessDetails.delete) access.push("Delete");
|
||||||
|
|
||||||
|
return access.join(", ");
|
||||||
|
}, [accessDetails]);
|
||||||
|
|
||||||
|
const getAccessLabel = () => {
|
||||||
|
if (!accessDetails.temporaryAccess.isTemporary || !accessDetails.temporaryAccess.temporaryRange)
|
||||||
|
return "Permanent";
|
||||||
|
|
||||||
|
// convert the range to human readable format
|
||||||
|
ms(ms(accessDetails.temporaryAccess.temporaryRange), { long: true });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge>
|
||||||
|
{`Valid for ${ms(ms(accessDetails.temporaryAccess.temporaryRange), {
|
||||||
|
long: true
|
||||||
|
})} after approval`}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const reviewAccessRequest = useReviewAccessRequest();
|
||||||
|
|
||||||
|
const handleReview = useCallback(async (status: "approved" | "rejected") => {
|
||||||
|
setIsLoading(status);
|
||||||
|
try {
|
||||||
|
await reviewAccessRequest.mutateAsync({
|
||||||
|
requestId: request.id,
|
||||||
|
status,
|
||||||
|
projectSlug,
|
||||||
|
envSlug: selectedEnvSlug,
|
||||||
|
requestedBy: selectedRequester
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
setIsLoading(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
title: `Request ${status}`,
|
||||||
|
text: `The request has been ${status}`,
|
||||||
|
type: status === "approved" ? "success" : "info"
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(null);
|
||||||
|
onOpenChange(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<ModalContent
|
||||||
|
className="max-w-4xl"
|
||||||
|
title="Review Request"
|
||||||
|
subTitle="Review the request and approve or deny access."
|
||||||
|
>
|
||||||
|
<div className="text-sm">
|
||||||
|
<span>
|
||||||
|
<span className="font-bold">
|
||||||
|
{request.user?.firstName} {request.user?.lastName} ({request.user?.email})
|
||||||
|
</span>{" "}
|
||||||
|
is requesting access to the following resource:
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="mt-4 mb-2 border-l border-blue-500 bg-blue-500/20 px-3 py-2 text-mineshaft-200">
|
||||||
|
<div className="mb-1 lowercase">
|
||||||
|
<span className="font-bold capitalize">Requested path: </span>
|
||||||
|
<Badge>{accessDetails.env + accessDetails.secretPath || ""}</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-1">
|
||||||
|
<span className="font-bold">Permissions: </span>
|
||||||
|
<Badge>{requestedAccess}</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="font-bold">Access Type: </span>
|
||||||
|
<span>{getAccessLabel()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-x-2">
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading === "approved"}
|
||||||
|
isDisabled={!!isLoading}
|
||||||
|
onClick={() => handleReview("approved")}
|
||||||
|
className="mt-4"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Approve Request
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading === "rejected"}
|
||||||
|
isDisabled={!!isLoading}
|
||||||
|
onClick={() => handleReview("rejected")}
|
||||||
|
className="mt-4 border-transparent bg-transparent text-mineshaft-200 hover:border-red hover:bg-red/20 hover:text-mineshaft-200"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Reject Request
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export { AccessApprovalRequest } from "./AccessApprovalRequest";
|
@ -47,7 +47,6 @@ export const SecretApprovalPolicyList = ({ workspaceId }: Props) => {
|
|||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
|
|
||||||
const { data: members } = useGetWorkspaceUsers(workspaceId);
|
const { data: members } = useGetWorkspaceUsers(workspaceId);
|
||||||
const { data: policies, isLoading: isPoliciesLoading } = useGetSecretApprovalPolicies({
|
const { data: policies, isLoading: isPoliciesLoading } = useGetSecretApprovalPolicies({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@ -120,7 +119,6 @@ export const SecretApprovalPolicyList = ({ workspaceId }: Props) => {
|
|||||||
<Th>Secret Path</Th>
|
<Th>Secret Path</Th>
|
||||||
<Th>Eligible Approvers</Th>
|
<Th>Eligible Approvers</Th>
|
||||||
<Th>Approval Required</Th>
|
<Th>Approval Required</Th>
|
||||||
<Th />
|
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
|
@ -84,21 +84,21 @@ export const SecretApprovalPolicyRow = ({
|
|||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
Select members that are allowed to approve changes
|
Select members that are allowed to approve changes
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{members?.map(({ id, user }) => {
|
{members?.map(({ user }) => {
|
||||||
const isChecked = selectedApprovers.includes(id);
|
const isChecked = selectedApprovers.includes(user.id);
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(evt) => {
|
onClick={(evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
setSelectedApprovers((state) =>
|
setSelectedApprovers((state) =>
|
||||||
isChecked ? state.filter((el) => el !== id) : [...state, id]
|
isChecked ? state.filter((el) => el !== user.id) : [...state, user.id]
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
key={`create-policy-members-${id}`}
|
key={`create-policy-members-${user.id}`}
|
||||||
iconPos="right"
|
iconPos="right"
|
||||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
>
|
>
|
||||||
{user.email}
|
{user.username}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -208,21 +208,23 @@ export const SecretPolicyForm = ({
|
|||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
Select members that are allowed to approve changes
|
Select members that are allowed to approve changes
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{members.map(({ id, user }) => {
|
{members.map(({ user }) => {
|
||||||
const isChecked = value?.includes(id);
|
const isChecked = value?.includes(user.id);
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(evt) => {
|
onClick={(evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
onChange(
|
onChange(
|
||||||
isChecked ? value?.filter((el) => el !== id) : [...(value || []), id]
|
isChecked
|
||||||
|
? value?.filter((el) => el !== user.id)
|
||||||
|
: [...(value || []), user.id]
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
key={`create-policy-members-${id}`}
|
key={`create-policy-members-${user.id}`}
|
||||||
iconPos="right"
|
iconPos="right"
|
||||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
>
|
>
|
||||||
{user.email}
|
{user.username}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -19,7 +19,13 @@ import {
|
|||||||
EmptyState,
|
EmptyState,
|
||||||
Skeleton
|
Skeleton
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useUser, useWorkspace } from "@app/context";
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
useProjectPermission,
|
||||||
|
useUser,
|
||||||
|
useWorkspace
|
||||||
|
} from "@app/context";
|
||||||
import {
|
import {
|
||||||
useGetSecretApprovalRequestCount,
|
useGetSecretApprovalRequestCount,
|
||||||
useGetSecretApprovalRequests,
|
useGetSecretApprovalRequests,
|
||||||
@ -58,9 +64,10 @@ export const SecretApprovalRequest = () => {
|
|||||||
const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } =
|
const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } =
|
||||||
useGetSecretApprovalRequestCount({ workspaceId });
|
useGetSecretApprovalRequestCount({ workspaceId });
|
||||||
const { user: presentUser } = useUser();
|
const { user: presentUser } = useUser();
|
||||||
const { data: members } = useGetWorkspaceUsers(workspaceId);
|
const { permission } = useProjectPermission();
|
||||||
|
const { data: members } = useGetWorkspaceUsers(workspaceId, true);
|
||||||
const membersGroupById = members?.reduce<Record<string, TWorkspaceUser>>(
|
const membersGroupById = members?.reduce<Record<string, TWorkspaceUser>>(
|
||||||
(prev, curr) => ({ ...prev, [curr.id]: curr }),
|
(prev, curr) => ({ ...prev, [curr.user.id]: curr }),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
const myMembershipId = members?.find(({ user }) => user.id === presentUser?.id)?.id;
|
const myMembershipId = members?.find(({ user }) => user.id === presentUser?.id)?.id;
|
||||||
@ -89,7 +96,7 @@ export const SecretApprovalRequest = () => {
|
|||||||
members={membersGroupById}
|
members={membersGroupById}
|
||||||
approvalRequestId={selectedApproval?.id || ""}
|
approvalRequestId={selectedApproval?.id || ""}
|
||||||
onGoBack={handleGoBackSecretRequestDetail}
|
onGoBack={handleGoBackSecretRequestDetail}
|
||||||
committer={membersGroupById?.[selectedApproval?.committerId || ""]}
|
committer={membersGroupById?.[selectedApproval?.committerUserId || ""]}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
@ -156,34 +163,42 @@ export const SecretApprovalRequest = () => {
|
|||||||
))}
|
))}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
{!!permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Member) && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger>
|
||||||
<Button
|
<Button
|
||||||
variant="plain"
|
variant="plain"
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
className={committerFilter ? "text-white" : "text-bunker-300"}
|
className={committerFilter ? "text-white" : "text-bunker-300"}
|
||||||
rightIcon={<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />}
|
rightIcon={
|
||||||
|
<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Author
|
Author
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuLabel>Select an author</DropdownMenuLabel>
|
<DropdownMenuLabel>Select an author</DropdownMenuLabel>
|
||||||
{members?.map(({ user, id }) => (
|
{members?.map(({ user }) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => setCommitterFilter((state) => (state === id ? undefined : id))}
|
onClick={() =>
|
||||||
key={`request-filter-member-${id}`}
|
setCommitterFilter((state) => (state === user.id ? undefined : user.id))
|
||||||
icon={committerFilter === id && <FontAwesomeIcon icon={faCheckCircle} />}
|
}
|
||||||
|
key={`request-filter-member-${user.id}`}
|
||||||
|
icon={
|
||||||
|
committerFilter === user.id && <FontAwesomeIcon icon={faCheckCircle} />
|
||||||
|
}
|
||||||
iconPos="right"
|
iconPos="right"
|
||||||
>
|
>
|
||||||
{user.email}
|
{user.username}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
))}
|
))}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col rounded-b-md border-x border-t border-b border-mineshaft-600 border-mineshaft-600 bg-mineshaft-800">
|
<div className="flex flex-col rounded-b-md border-x border-t border-b border-mineshaft-600 bg-mineshaft-800">
|
||||||
{isRequestListEmpty && (
|
{isRequestListEmpty && (
|
||||||
<div className="py-12">
|
<div className="py-12">
|
||||||
<EmptyState title="No more requests pending." />
|
<EmptyState title="No more requests pending." />
|
||||||
@ -195,7 +210,7 @@ export const SecretApprovalRequest = () => {
|
|||||||
const {
|
const {
|
||||||
id: reqId,
|
id: reqId,
|
||||||
commits,
|
commits,
|
||||||
committerId,
|
committerUserId,
|
||||||
createdAt,
|
createdAt,
|
||||||
policy,
|
policy,
|
||||||
reviewers,
|
reviewers,
|
||||||
@ -225,9 +240,9 @@ export const SecretApprovalRequest = () => {
|
|||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-xs text-gray-500">
|
||||||
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "}
|
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "}
|
||||||
{membersGroupById?.[committerId]?.user?.firstName}{" "}
|
{membersGroupById?.[committerUserId]?.user?.firstName}{" "}
|
||||||
{membersGroupById?.[committerId]?.user?.lastName} (
|
{membersGroupById?.[committerUserId]?.user?.lastName} (
|
||||||
{membersGroupById?.[committerId]?.user?.email}){" "}
|
{membersGroupById?.[committerUserId]?.user?.email}){" "}
|
||||||
{isApprover && !isReviewed && status === "open" && "- Review required"}
|
{isApprover && !isReviewed && status === "open" && "- Review required"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,7 +37,6 @@ export const SecretApprovalRequestAction = ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
canApprove
|
canApprove
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
|
||||||
const { mutateAsync: performSecretApprovalMerge, isLoading: isMerging } =
|
const { mutateAsync: performSecretApprovalMerge, isLoading: isMerging } =
|
||||||
usePerformSecretApprovalRequestMerge();
|
usePerformSecretApprovalRequestMerge();
|
||||||
|
|
||||||
@ -136,7 +135,7 @@ export const SecretApprovalRequestAction = ({
|
|||||||
<div className="flex items-start space-x-4">
|
<div className="flex items-start space-x-4">
|
||||||
<FontAwesomeIcon icon={faCheck} className="pt-1 text-2xl text-primary" />
|
<FontAwesomeIcon icon={faCheck} className="pt-1 text-2xl text-primary" />
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
Change request merged
|
Secret approval merged
|
||||||
<span className="inline-block text-xs text-bunker-200">
|
<span className="inline-block text-xs text-bunker-200">
|
||||||
Merged by {statusChangeByEmail}
|
Merged by {statusChangeByEmail}
|
||||||
</span>
|
</span>
|
||||||
@ -150,7 +149,7 @@ export const SecretApprovalRequestAction = ({
|
|||||||
<div className="flex items-start space-x-4">
|
<div className="flex items-start space-x-4">
|
||||||
<FontAwesomeIcon icon={faUserLock} className="pt-1 text-2xl text-primary" />
|
<FontAwesomeIcon icon={faUserLock} className="pt-1 text-2xl text-primary" />
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
Change request has been closed
|
Secret approval has been closed
|
||||||
<span className="inline-block text-xs text-bunker-200">
|
<span className="inline-block text-xs text-bunker-200">
|
||||||
Closed by {statusChangeByEmail}
|
Closed by {statusChangeByEmail}
|
||||||
</span>
|
</span>
|
||||||
|
@ -83,7 +83,6 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
members = {}
|
members = {}
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
|
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
|
||||||
const {
|
const {
|
||||||
@ -94,7 +93,6 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
id: approvalRequestId,
|
id: approvalRequestId,
|
||||||
decryptKey: decryptFileKey!
|
decryptKey: decryptFileKey!
|
||||||
});
|
});
|
||||||
console.log(secretApprovalRequestDetails);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mutateAsync: updateSecretApprovalRequestStatus,
|
mutateAsync: updateSecretApprovalRequestStatus,
|
||||||
@ -106,11 +104,12 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
const isRejecting = variables?.status === ApprovalStatus.REJECTED && isUpdatingRequestStatus;
|
const isRejecting = variables?.status === ApprovalStatus.REJECTED && isUpdatingRequestStatus;
|
||||||
|
|
||||||
// membership of present user
|
// membership of present user
|
||||||
const myMembership = Object.values(members).find(
|
const myUser = Object.values(members).find(
|
||||||
({ user: membershipUser }) => membershipUser.email === user.email
|
({ user: membershipUser }) => membershipUser.email === user.email
|
||||||
);
|
)?.user;
|
||||||
const myMembershipId = myMembership?.id || "";
|
|
||||||
const canApprove = secretApprovalRequestDetails?.policy?.approvers?.includes(myMembershipId);
|
const myUserId = myUser?.id || "";
|
||||||
|
const canApprove = secretApprovalRequestDetails?.policy?.approvers?.includes(myUserId);
|
||||||
const reviewedMembers = secretApprovalRequestDetails?.reviewers?.reduce<
|
const reviewedMembers = secretApprovalRequestDetails?.reviewers?.reduce<
|
||||||
Record<string, ApprovalStatus>
|
Record<string, ApprovalStatus>
|
||||||
>(
|
>(
|
||||||
@ -120,8 +119,8 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
}),
|
}),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
const hasApproved = reviewedMembers?.[myMembershipId] === ApprovalStatus.APPROVED;
|
const hasApproved = reviewedMembers?.[myUserId] === ApprovalStatus.APPROVED;
|
||||||
const hasRejected = reviewedMembers?.[myMembershipId] === ApprovalStatus.REJECTED;
|
const hasRejected = reviewedMembers?.[myUserId] === ApprovalStatus.REJECTED;
|
||||||
|
|
||||||
const handleSecretApprovalStatusUpdate = async (status: ApprovalStatus) => {
|
const handleSecretApprovalStatusUpdate = async (status: ApprovalStatus) => {
|
||||||
try {
|
try {
|
||||||
@ -251,7 +250,7 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
status={secretApprovalRequestDetails.status}
|
status={secretApprovalRequestDetails.status}
|
||||||
isMergable={isMergable}
|
isMergable={isMergable}
|
||||||
statusChangeByEmail={
|
statusChangeByEmail={
|
||||||
members[secretApprovalRequestDetails?.statusChangeBy || ""]?.user?.email || ""
|
members[secretApprovalRequestDetails?.statusChangeByUserId || ""]?.user?.email || ""
|
||||||
}
|
}
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
/>
|
/>
|
||||||
|
Reference in New Issue
Block a user