mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-22 02:12:16 +00:00
Compare commits
300 Commits
daniel/bet
...
daniel/inc
Author | SHA1 | Date | |
---|---|---|---|
acf054d992 | |||
f50b0876e4 | |||
c30763c98f | |||
6fc95c3ff8 | |||
eef1f2b6ef | |||
128b1cf856 | |||
6b9944001e | |||
1cc22a6195 | |||
af643468fd | |||
f8358a0807 | |||
3eefb98f30 | |||
8f39f953f8 | |||
5e4af7e568 | |||
24bd13403a | |||
4149cbdf07 | |||
ced3ab97e8 | |||
20bcf8aab8 | |||
0814245ce6 | |||
2d2f27ea46 | |||
4aeb2bf65e | |||
24da76db19 | |||
3c49936eee | |||
b416e79d63 | |||
92c529587b | |||
3b74c232dc | |||
6164dc32d7 | |||
37e7040eea | |||
a7ebb4b241 | |||
2fc562ff2d | |||
b5c83fea4d | |||
b586f98926 | |||
e6205c086f | |||
2ca34099ed | |||
5da6c12941 | |||
e2612b75fc | |||
ca5edb95f1 | |||
724e2b3692 | |||
2c93561a3b | |||
0b24cc8631 | |||
6c6e932899 | |||
c66a711890 | |||
787f8318fe | |||
9a27873af5 | |||
0abab57d83 | |||
d5662dfef4 | |||
ee2ee48b47 | |||
896d977b95 | |||
d1966b60a8 | |||
e3cbcf5853 | |||
bdf1f7c601 | |||
24b23d4f90 | |||
09c1a5f778 | |||
73a9cf01f3 | |||
97e860cf21 | |||
25b55087cf | |||
25f694bbdb | |||
7cd85cf84a | |||
cf5c886b6f | |||
e667c7c988 | |||
fd254fbeec | |||
859c556425 | |||
a3cad030e5 | |||
342e9f99d3 | |||
8ed04d0b75 | |||
5b5a8ff03f | |||
e0199084ad | |||
67a6deed72 | |||
355113e15d | |||
40c589eced | |||
ec4f175f73 | |||
2273c21eb2 | |||
97c2b15e29 | |||
2f90ee067b | |||
7b64288019 | |||
e6e1ed7ca9 | |||
73838190fd | |||
d32fad87d1 | |||
67db9679fa | |||
3edd48a8b3 | |||
a4091bfcdd | |||
24483631a0 | |||
0f74a1a011 | |||
62d6e3763b | |||
39ea7a032f | |||
3ac125f9c7 | |||
7667a7e665 | |||
d7499fc5c5 | |||
f6885b239b | |||
4928322cdb | |||
77e191d63e | |||
15c98a1d2e | |||
ed757bdeff | |||
65241ad8bf | |||
6a7760f33f | |||
fdc62e21ef | |||
32f866f834 | |||
fbf52850e8 | |||
ab9b207f96 | |||
5532b9cfea | |||
449d3f0304 | |||
f0210c2607 | |||
ad88aaf17f | |||
0485b56e8d | |||
b65842f5c1 | |||
22b6e0afcd | |||
b0e536e576 | |||
54e4314e88 | |||
d00b1847cc | |||
be02617855 | |||
b5065f13c9 | |||
659b6d5d19 | |||
9c33251c44 | |||
1a0896475c | |||
7e820745a4 | |||
fa63c150dd | |||
1a2495a95c | |||
d79099946a | |||
27afad583b | |||
acde0867a0 | |||
d44f99bac2 | |||
2b35e20b1d | |||
da15957c3f | |||
208fc3452d | |||
ba1db870a4 | |||
7885a3b0ff | |||
66485f0464 | |||
0741058c1d | |||
3a6e79c575 | |||
70aa73482e | |||
2fa30bdd0e | |||
b28fe30bba | |||
9ba39e99c6 | |||
0e6aed7497 | |||
7e11fbe7a3 | |||
23abab987f | |||
a44b3efeb7 | |||
1992a09ac2 | |||
efa54e0c46 | |||
bde2d5e0a6 | |||
4090c894fc | |||
221bde01f8 | |||
b191a3c2f4 | |||
032197ee9f | |||
d5a4eb609a | |||
e7f1980b80 | |||
d430293c66 | |||
180d2692cd | |||
433e58655a | |||
5ffb6b7232 | |||
55ca9149d5 | |||
4ea57ca9a0 | |||
7ac4b0b79f | |||
2d51ed317f | |||
02c51b05b6 | |||
cd09f03f0b | |||
bc475e0f08 | |||
441b008709 | |||
4d81a0251e | |||
59da513481 | |||
c17047a193 | |||
f50a881273 | |||
afd6dd5257 | |||
3a43d7c5d5 | |||
65375886bd | |||
8495107849 | |||
c011d99b8b | |||
adc3542750 | |||
82e3241f1b | |||
2bca46886a | |||
971987c786 | |||
cd71a13bb7 | |||
98290fe31b | |||
9f15fb1474 | |||
301a867f8b | |||
658a044e85 | |||
2c1e29445d | |||
3f4c4f7418 | |||
592cc13b1f | |||
e70c2f3d10 | |||
bac865eab1 | |||
3d8fbc0a58 | |||
1fcfab7efa | |||
499334eef1 | |||
9fd76b8729 | |||
80d450e980 | |||
a1f2629366 | |||
bf8e1f2bfd | |||
f7d10ceeda | |||
095883a94e | |||
51638b7c71 | |||
adaddad370 | |||
cf6ff58f16 | |||
3e3f42a8f7 | |||
974e21d856 | |||
da86338bfe | |||
3a9a6767a0 | |||
fe8a1e6ce6 | |||
55aa3f7b58 | |||
59f3581370 | |||
ccae63936c | |||
6733349af0 | |||
f63c6b725b | |||
50b51f1810 | |||
fc39b3b0dd | |||
5964976e47 | |||
677a87150b | |||
2469c8d0c6 | |||
dafb89d1dd | |||
8da01445e5 | |||
6b2273d314 | |||
b886e66ee9 | |||
3afcb19727 | |||
06d2480f30 | |||
fd7d8ddf2d | |||
1dc0f4e5b8 | |||
fa64a88c24 | |||
385ec05e57 | |||
3a38e1e413 | |||
7f04e9e97d | |||
839f0c7e1c | |||
2352e29902 | |||
fcbc7fcece | |||
c2252c65a4 | |||
e150673de4 | |||
4f5c49a529 | |||
7107089ad3 | |||
967818f57d | |||
14c89c9be5 | |||
02111c2dc2 | |||
ebea74b607 | |||
5bbe5421bf | |||
279289989f | |||
bb4a16cf7c | |||
309db49f1b | |||
62a582ef17 | |||
d6b389760d | |||
449e7672f9 | |||
31ff6d3c17 | |||
cfcc32271f | |||
e2ea84f28a | |||
6885ef2e54 | |||
8fa9f476e3 | |||
1cf8d1e3fa | |||
9f61177b62 | |||
59b8e83476 | |||
eee4d00a08 | |||
51c0598b50 | |||
69311f058b | |||
0f70c3ea9a | |||
b5660c87a0 | |||
2bb0386220 | |||
526605a0bb | |||
5b9903a226 | |||
3fc60bf596 | |||
7815d6538f | |||
4c4d525655 | |||
e87656631c | |||
e102ccf9f0 | |||
63af75a330 | |||
8a10af9b62 | |||
18308950d1 | |||
86a9676a9c | |||
aa12a71ff3 | |||
8d71b295ea | |||
f72cedae10 | |||
864cf23416 | |||
10574bfe26 | |||
02085ce902 | |||
4eeea0b27c | |||
93b7f56337 | |||
12ecefa832 | |||
dd9a00679d | |||
081502848d | |||
0fa9fa20bc | |||
0a1f25a659 | |||
bc74c44f97 | |||
998bbe92f7 | |||
009be0ded8 | |||
c9f6207e32 | |||
0564d06923 | |||
d0656358a2 | |||
040fa511f6 | |||
75099f159f | |||
e4a83ad2e2 | |||
760f9d487c | |||
a02e73e2a4 | |||
fbebeaf38f | |||
97245c740e | |||
5a40b5a1cf | |||
19e4a6de4d | |||
0daca059c7 | |||
0fd193f8e0 | |||
342c713805 | |||
0df80c5b2d | |||
613b97c93d | |||
c577f51c19 | |||
335f3f7d37 | |||
b3f0d36ddc | |||
24d121ab59 | |||
dbb8617180 |
.github
README.mdbackend
e2e-test/routes
package-lock.jsonpackage.jsonsrc
@types
db
migrations
20240918005344_add-group-approvals.ts20240924100329_identity-metadata.ts20240925100349_managed-secret-sharing.ts20240930072738_add-oidc-auth-enforced-to-org.ts20241003220151_kms-key-cmek-alterations.ts20241005170802_kms-keys-temp-slug-col.ts
utils
schemas
ee
routes/v1
access-approval-policy-router.tsdynamic-secret-router.tsexternal-kms-router.tsgroup-router.tsidentity-project-additional-privilege-router.tsproject-role-router.tsproject-router.tsrate-limit-router.tssaml-router.tssecret-approval-policy-router.tssecret-approval-request-router.ts
services
access-approval-policy
access-approval-policy-dal.tsaccess-approval-policy-fns.tsaccess-approval-policy-service.tsaccess-approval-policy-types.ts
access-approval-request
audit-log-stream
audit-log
certificate-authority-crl
dynamic-secret-lease
dynamic-secret
external-kms
group
identity-project-additional-privilege
ldap-config
license
oidc
permission
org-permission.tspermission-dal.tspermission-fns.tspermission-service-types.tspermission-service.tspermission-types.tsproject-permission.ts
project-user-additional-privilege
saml-config
scim
secret-approval-policy
secret-approval-request
secret-approval-request-dal.tssecret-approval-request-secret-dal.tssecret-approval-request-service.ts
secret-replication
secret-rotation
secret-scanning
secret-snapshot
keystore
lib
api-docs
base64
casl
errors
fn
ip
knex
queue
server
app.ts
plugins
routes
index.tssanitizedSchemas.ts
v1
admin-router.tsauth-router.tscmek-router.tsdashboard-router.tsidentity-aws-iam-auth-router.tsidentity-azure-auth-router.tsidentity-gcp-auth-router.tsidentity-router.tsindex.tsintegration-router.tsorganization-router.tsproject-env-router.tssecret-folder-router.tssecret-import-router.tssecret-sharing-router.tssso-router.ts
v2
v3
services
api-key
auth-token
auth
certificate-authority
cmek
external-migration
group-project
identity-access-token
identity-aws-auth
identity-azure-auth
identity-gcp-auth
identity-kubernetes-auth
identity-oidc-auth
identity-project
identity-token-auth
identity-ua
identity
integration-auth
integration-app-list.tsintegration-auth-service.tsintegration-list.tsintegration-sync-secret.tsintegration-token.ts
integration
kms
org-admin
org-membership
org
pki-alert
project-bot
project-env
project-membership
project-role
project
secret-blind-index
secret-folder
secret-import
secret-sharing
secret-tag
secret-v2-bridge
secret
service-token
slack
smtp
super-admin
user
webhook
docs
api-reference/endpoints
audit-logs
groups
add-group-user.mdxcreate.mdxdelete.mdxget-by-id.mdxget.mdxlist-group-users.mdxremove-group-user.mdxupdate.mdx
kms/keys
project-groups
cli
documentation
guides
platform
images
guides/import-envkey
copy-encryption-key.pngenvkey-dashboard.pngenvkey-export.pnginfisical-import-dashboard.pnginfisical-import-envkey.png
integrations/databricks
integrations-databricks-auth.pngintegrations-databricks-create.pngintegrations-databricks.pngpat-token.png
platform
dynamic-secrets
dynamic-secret-ad-add-assignments.pngdynamic-secret-ad-add-client-secret.pngdynamic-secret-ad-add-permission.pngdynamic-secret-ad-admin-consent.pngdynamic-secret-ad-copy-app-id.pngdynamic-secret-ad-lease.pngdynamic-secret-ad-modal.pngdynamic-secret-ad-new-registration.pngdynamic-secret-ad-select-graph.pngdynamic-secret-ad-select-perms.pngdynamic-secret-ad-show-more.pngdynamic-secret-ad-tenant-id.pngdynamic-secret-ad-user-admin.pngdynamic-secret-ldap-lease.pngdynamic-secret-ldap-select.png
kms/infisical-kms
kms-add-key-modal.pngkms-add-key.pngkms-decrypt-data.pngkms-decrypt-options.pngkms-decrypted-data.pngkms-encrypt-data.pngkms-encrypted-data.pngkms-key-options.png
mfa/entra
sso
auth0-oidc
general-oidc
keycloak-oidc
integrations
cicd
cloud
aws-amplify.mdxaws-parameter-store.mdxcheckly.mdxcloud-66.mdxcloudflare-pages.mdxcloudflare-workers.mdxdatabricks.mdxdigital-ocean-app-platform.mdxflyio.mdxgcp-secret-manager.mdxhasura-cloud.mdxheroku.mdxlaravel-forge.mdxnetlify.mdxnorthflank.mdxqovery.mdxrailway.mdxrender.mdxsupabase.mdxteamcity.mdxwindmill.mdx
platforms
internals
mint.jsonsdks
frontend
public
src
components
features
navigation
signup
v2
Dropdown
InfisicalSecretInput
Pagination
SecretPathInput
Select
Switch
Tooltip
context
hooks
api
accessApproval
auditLogs
cmeks
dashboard
dynamicSecret
groups
identities
kms
migration
oidcConfig
organization
roles
secretApproval
secretFolders
secretImports
secretSharing
secrets
users
workspace
utils
layouts/AppLayout
pages
views
IntegrationsPage
Login/components/InitialStep
Org
AuditLogsPage
IdentityPage/components
MembersPage/components
OrgGroupsTab/components/OrgGroupsSection
OrgIdentityTab/components/IdentitySection
IdentityAuthMethodModal.tsxIdentityAwsAuthForm.tsxIdentityAzureAuthForm.tsxIdentityGcpAuthForm.tsxIdentityKubernetesAuthForm.tsxIdentityModal.tsxIdentityOidcAuthForm.tsxIdentityTable.tsxIdentityTokenAuthForm.tsxIdentityUniversalAuthForm.tsx
OrgMembersTab/components/OrgMembersSection
OrgRoleTabSection
RolePage/components
UserPage/components
OrgAdminPage/components/OrgAdminProjects
Project
AuditLogsPage
CertificatesPage
KmsPage
components
index.tsxMembersPage/components
GroupsTab/components/GroupsSection
IdentityTab
RolePage
RolePage.tsx
components/RolePermissionsSection
SecretApprovalPage/components/ApprovalPolicyList
SecretMainPage
SecretMainPage.tsxSecretMainPage.types.ts
components
ActionBar
ActionBar.tsx
CreateDynamicSecretForm
DynamicSecretListView
FolderListView
SecretDropzone
SecretImportListView
SecretListView
SecretOverviewPage
SecretOverviewPage.tsx
components
CreateSecretForm
FolderBreadCrumbs
SecretOverviewTableRow
SecretTableResourceCount
SecretV2MigrationSection
Settings
BillingSettingsPage/components/BillingCloudTab
OrgSettingsPage/components
ImportTab
OrgAuthTab
OrgEncryptionTab
OrgTabGroup
ProjectSettingsPage/components
ShareSecretPublicPage/components
ViewSecretPublicPage
admin/DashboardPage
helm-charts/secrets-operator
k8-operator/controllers
1
.github/pull_request_template.md
vendored
1
.github/pull_request_template.md
vendored
@ -6,6 +6,7 @@
|
||||
|
||||
- [ ] Bug fix
|
||||
- [ ] New feature
|
||||
- [ ] Improvement
|
||||
- [ ] Breaking change
|
||||
- [ ] Documentation
|
||||
|
||||
|
@ -73,6 +73,11 @@ We're on a mission to make security tooling more accessible to everyone, not jus
|
||||
- **[Infisical PKI Issuer for Kubernetes](https://infisical.com/docs/documentation/platform/pki/pki-issuer)**: Deliver TLS certificates to your Kubernetes workloads with automatic renewal.
|
||||
- **[Enrollment over Secure Transport](https://infisical.com/docs/documentation/platform/pki/est)**: Enroll and manage certificates via EST protocol.
|
||||
|
||||
### Key Management (KMS):
|
||||
|
||||
- **[Cryptograhic Keys](https://infisical.com/docs/documentation/platform/kms)**: Centrally manage keys across projects through a user-friendly interface or via the API.
|
||||
- **[Encrypt and Decrypt Data](https://infisical.com/docs/documentation/platform/kms#guide-to-encrypting-data)**: Use symmetric keys to encrypt and decrypt data.
|
||||
|
||||
### General Platform:
|
||||
- **Authentication Methods**: Authenticate machine identities with Infisical using a cloud-native or platform agnostic authentication method ([Kubernetes Auth](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth), [GCP Auth](https://infisical.com/docs/documentation/platform/identities/gcp-auth), [Azure Auth](https://infisical.com/docs/documentation/platform/identities/azure-auth), [AWS Auth](https://infisical.com/docs/documentation/platform/identities/aws-auth), [OIDC Auth](https://infisical.com/docs/documentation/platform/identities/oidc-auth/general), [Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth)).
|
||||
- **[Access Controls](https://infisical.com/docs/documentation/platform/access-controls/overview)**: Define advanced authorization controls for users and machine identities with [RBAC](https://infisical.com/docs/documentation/platform/access-controls/role-based-access-controls), [additional privileges](https://infisical.com/docs/documentation/platform/access-controls/additional-privileges), [temporary access](https://infisical.com/docs/documentation/platform/access-controls/temporary-access), [access requests](https://infisical.com/docs/documentation/platform/access-controls/access-requests), [approval workflows](https://infisical.com/docs/documentation/platform/pr-workflows), and more.
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { seedData1 } from "@app/db/seed-data";
|
||||
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
||||
|
||||
const createPolicy = async (dto: { name: string; secretPath: string; approvers: string[]; approvals: number }) => {
|
||||
const createPolicy = async (dto: { name: string; secretPath: string; approvers: {type: ApproverType.User, id: string}[]; approvals: number }) => {
|
||||
const res = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v1/secret-approvals`,
|
||||
@ -26,7 +27,7 @@ describe("Secret approval policy router", async () => {
|
||||
const policy = await createPolicy({
|
||||
secretPath: "/",
|
||||
approvals: 1,
|
||||
approvers: [seedData1.id],
|
||||
approvers: [{id:seedData1.id, type: ApproverType.User}],
|
||||
name: "test-policy"
|
||||
});
|
||||
|
||||
|
@ -510,7 +510,7 @@ describe("Service token fail cases", async () => {
|
||||
authorization: `Bearer ${serviceToken}`
|
||||
}
|
||||
});
|
||||
expect(fetchSecrets.statusCode).toBe(401);
|
||||
expect(fetchSecrets.statusCode).toBe(403);
|
||||
expect(fetchSecrets.json().error).toBe("PermissionDenied");
|
||||
await deleteServiceToken();
|
||||
});
|
||||
@ -532,7 +532,7 @@ describe("Service token fail cases", async () => {
|
||||
authorization: `Bearer ${serviceToken}`
|
||||
}
|
||||
});
|
||||
expect(fetchSecrets.statusCode).toBe(401);
|
||||
expect(fetchSecrets.statusCode).toBe(403);
|
||||
expect(fetchSecrets.json().error).toBe("PermissionDenied");
|
||||
await deleteServiceToken();
|
||||
});
|
||||
@ -557,7 +557,7 @@ describe("Service token fail cases", async () => {
|
||||
authorization: `Bearer ${serviceToken}`
|
||||
}
|
||||
});
|
||||
expect(writeSecrets.statusCode).toBe(401);
|
||||
expect(writeSecrets.statusCode).toBe(403);
|
||||
expect(writeSecrets.json().error).toBe("PermissionDenied");
|
||||
|
||||
// but read access should still work fine
|
||||
|
@ -1075,7 +1075,7 @@ describe("Secret V3 Raw Router Without E2EE enabled", async () => {
|
||||
},
|
||||
body: createSecretReqBody
|
||||
});
|
||||
expect(createSecRes.statusCode).toBe(400);
|
||||
expect(createSecRes.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
test("Update secret raw", async () => {
|
||||
@ -1093,7 +1093,7 @@ describe("Secret V3 Raw Router Without E2EE enabled", async () => {
|
||||
},
|
||||
body: updateSecretReqBody
|
||||
});
|
||||
expect(updateSecRes.statusCode).toBe(400);
|
||||
expect(updateSecRes.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
test("Delete secret raw", async () => {
|
||||
@ -1110,6 +1110,6 @@ describe("Secret V3 Raw Router Without E2EE enabled", async () => {
|
||||
},
|
||||
body: deletedSecretReqBody
|
||||
});
|
||||
expect(deletedSecRes.statusCode).toBe(400);
|
||||
expect(deletedSecRes.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
|
31
backend/package-lock.json
generated
31
backend/package-lock.json
generated
@ -61,6 +61,7 @@
|
||||
"jwks-rsa": "^3.1.0",
|
||||
"knex": "^3.0.1",
|
||||
"ldapjs": "^3.0.7",
|
||||
"ldif": "^0.5.1",
|
||||
"libsodium-wrappers": "^0.7.13",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"mongodb": "^6.8.1",
|
||||
@ -85,6 +86,7 @@
|
||||
"safe-regex": "^2.1.1",
|
||||
"scim-patch": "^0.8.3",
|
||||
"scim2-parse-filter": "^0.2.10",
|
||||
"sjcl": "^1.0.8",
|
||||
"smee-client": "^2.0.0",
|
||||
"tedious": "^18.2.1",
|
||||
"tweetnacl": "^1.0.3",
|
||||
@ -117,6 +119,7 @@
|
||||
"@types/prompt-sync": "^4.2.3",
|
||||
"@types/resolve": "^1.20.6",
|
||||
"@types/safe-regex": "^1.1.6",
|
||||
"@types/sjcl": "^1.0.34",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||
"@typescript-eslint/parser": "^6.20.0",
|
||||
@ -7296,6 +7299,13 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/sjcl": {
|
||||
"version": "1.0.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/sjcl/-/sjcl-1.0.34.tgz",
|
||||
"integrity": "sha512-bQHEeK5DTQRunIfQeUMgtpPsNNCcZyQ9MJuAfW1I7iN0LDunTc78Fu17STbLMd7KiEY/g2zHVApippa70h6HoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "9.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz",
|
||||
@ -13008,6 +13018,12 @@
|
||||
"verror": "^1.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ldif": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/ldif/-/ldif-0.5.1.tgz",
|
||||
"integrity": "sha512-8s46m/r2lSFO2+DqMxqWiJ10iiL4tuR5LC/KndV+E5//OAOzOx5s3HS5O34PJ5+kyaCA+K2oCaEPaDRfXUnQow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/leven": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz",
|
||||
@ -16397,6 +16413,15 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/sjcl": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/sjcl/-/sjcl-1.0.8.tgz",
|
||||
"integrity": "sha512-LzIjEQ0S0DpIgnxMEayM1rq9aGwGRG4OnZhCdjx7glTaJtf4zRfpg87ImfjSJjoW9vKpagd82McDOwbRT5kQKQ==",
|
||||
"license": "(BSD-2-Clause OR GPL-2.0-only)",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/slash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||
@ -17874,12 +17899,14 @@
|
||||
"node_modules/tweetnacl": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
||||
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
|
||||
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/tweetnacl-util": {
|
||||
"version": "0.15.1",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz",
|
||||
"integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw=="
|
||||
"integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
|
@ -80,6 +80,7 @@
|
||||
"@types/prompt-sync": "^4.2.3",
|
||||
"@types/resolve": "^1.20.6",
|
||||
"@types/safe-regex": "^1.1.6",
|
||||
"@types/sjcl": "^1.0.34",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||
"@typescript-eslint/parser": "^6.20.0",
|
||||
@ -158,6 +159,7 @@
|
||||
"jwks-rsa": "^3.1.0",
|
||||
"knex": "^3.0.1",
|
||||
"ldapjs": "^3.0.7",
|
||||
"ldif": "0.5.1",
|
||||
"libsodium-wrappers": "^0.7.13",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"mongodb": "^6.8.1",
|
||||
@ -182,6 +184,7 @@
|
||||
"safe-regex": "^2.1.1",
|
||||
"scim-patch": "^0.8.3",
|
||||
"scim2-parse-filter": "^0.2.10",
|
||||
"sjcl": "^1.0.8",
|
||||
"smee-client": "^2.0.0",
|
||||
"tedious": "^18.2.1",
|
||||
"tweetnacl": "^1.0.3",
|
||||
|
4
backend/src/@types/fastify.d.ts
vendored
4
backend/src/@types/fastify.d.ts
vendored
@ -38,6 +38,8 @@ import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-se
|
||||
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
|
||||
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||
import { TCmekServiceFactory } from "@app/services/cmek/cmek-service";
|
||||
import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
|
||||
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
||||
@ -181,6 +183,8 @@ declare module "fastify" {
|
||||
orgAdmin: TOrgAdminServiceFactory;
|
||||
slack: TSlackServiceFactory;
|
||||
workflowIntegration: TWorkflowIntegrationServiceFactory;
|
||||
cmek: TCmekServiceFactory;
|
||||
migration: TExternalMigrationServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
8
backend/src/@types/knex.d.ts
vendored
8
backend/src/@types/knex.d.ts
vendored
@ -101,6 +101,9 @@ import {
|
||||
TIdentityKubernetesAuths,
|
||||
TIdentityKubernetesAuthsInsert,
|
||||
TIdentityKubernetesAuthsUpdate,
|
||||
TIdentityMetadata,
|
||||
TIdentityMetadataInsert,
|
||||
TIdentityMetadataUpdate,
|
||||
TIdentityOidcAuths,
|
||||
TIdentityOidcAuthsInsert,
|
||||
TIdentityOidcAuthsUpdate,
|
||||
@ -546,6 +549,11 @@ declare module "knex/types/tables" {
|
||||
TIdentityUniversalAuthsInsert,
|
||||
TIdentityUniversalAuthsUpdate
|
||||
>;
|
||||
[TableName.IdentityMetadata]: KnexOriginal.CompositeTableType<
|
||||
TIdentityMetadata,
|
||||
TIdentityMetadataInsert,
|
||||
TIdentityMetadataUpdate
|
||||
>;
|
||||
[TableName.IdentityKubernetesAuth]: KnexOriginal.CompositeTableType<
|
||||
TIdentityKubernetesAuths,
|
||||
TIdentityKubernetesAuthsInsert,
|
||||
|
4
backend/src/@types/ldif.d.ts
vendored
Normal file
4
backend/src/@types/ldif.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
declare module "ldif" {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Untyped, the function returns `any`.
|
||||
function parse(input: string, ...args: any[]): any;
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasAccessApproverGroupId = await knex.schema.hasColumn(
|
||||
TableName.AccessApprovalPolicyApprover,
|
||||
"approverGroupId"
|
||||
);
|
||||
const hasAccessApproverUserId = await knex.schema.hasColumn(TableName.AccessApprovalPolicyApprover, "approverUserId");
|
||||
const hasSecretApproverGroupId = await knex.schema.hasColumn(
|
||||
TableName.SecretApprovalPolicyApprover,
|
||||
"approverGroupId"
|
||||
);
|
||||
const hasSecretApproverUserId = await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverUserId");
|
||||
if (await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover)) {
|
||||
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (table) => {
|
||||
// add column approverGroupId to AccessApprovalPolicyApprover
|
||||
if (!hasAccessApproverGroupId) {
|
||||
table.uuid("approverGroupId").nullable().references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||
}
|
||||
|
||||
// make approverUserId nullable
|
||||
if (hasAccessApproverUserId) {
|
||||
table.uuid("approverUserId").nullable().alter();
|
||||
}
|
||||
});
|
||||
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (table) => {
|
||||
// add column approverGroupId to SecretApprovalPolicyApprover
|
||||
if (!hasSecretApproverGroupId) {
|
||||
table.uuid("approverGroupId").nullable().references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||
}
|
||||
|
||||
// make approverUserId nullable
|
||||
if (hasSecretApproverUserId) {
|
||||
table.uuid("approverUserId").nullable().alter();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasAccessApproverGroupId = await knex.schema.hasColumn(
|
||||
TableName.AccessApprovalPolicyApprover,
|
||||
"approverGroupId"
|
||||
);
|
||||
const hasAccessApproverUserId = await knex.schema.hasColumn(TableName.AccessApprovalPolicyApprover, "approverUserId");
|
||||
const hasSecretApproverGroupId = await knex.schema.hasColumn(
|
||||
TableName.SecretApprovalPolicyApprover,
|
||||
"approverGroupId"
|
||||
);
|
||||
const hasSecretApproverUserId = await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverUserId");
|
||||
|
||||
if (await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover)) {
|
||||
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (table) => {
|
||||
if (hasAccessApproverGroupId) {
|
||||
table.dropColumn("approverGroupId");
|
||||
}
|
||||
// make approverUserId not nullable
|
||||
if (hasAccessApproverUserId) {
|
||||
table.uuid("approverUserId").notNullable().alter();
|
||||
}
|
||||
});
|
||||
|
||||
// remove
|
||||
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (table) => {
|
||||
if (hasSecretApproverGroupId) {
|
||||
table.dropColumn("approverGroupId");
|
||||
}
|
||||
// make approverUserId not nullable
|
||||
if (hasSecretApproverUserId) {
|
||||
table.uuid("approverUserId").notNullable().alter();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.IdentityMetadata))) {
|
||||
await knex.schema.createTable(TableName.IdentityMetadata, (tb) => {
|
||||
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
tb.string("key").notNullable();
|
||||
tb.string("value").notNullable();
|
||||
tb.uuid("orgId").notNullable();
|
||||
tb.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
tb.uuid("userId");
|
||||
tb.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||
tb.uuid("identityId");
|
||||
tb.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||
tb.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.IdentityMetadata);
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
t.string("iv").nullable().alter();
|
||||
t.string("tag").nullable().alter();
|
||||
t.string("encryptedValue").nullable().alter();
|
||||
|
||||
t.binary("encryptedSecret").nullable();
|
||||
t.string("hashedHex").nullable().alter();
|
||||
|
||||
t.string("identifier", 64).nullable();
|
||||
t.unique("identifier");
|
||||
t.index("identifier");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
t.dropColumn("encryptedSecret");
|
||||
|
||||
t.dropColumn("identifier");
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.OidcConfig, "lastUsed"))) {
|
||||
await knex.schema.alterTable(TableName.OidcConfig, (tb) => {
|
||||
tb.datetime("lastUsed");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.OidcConfig, "lastUsed")) {
|
||||
await knex.schema.alterTable(TableName.OidcConfig, (tb) => {
|
||||
tb.dropColumn("lastUsed");
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintIfExists";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.KmsKey)) {
|
||||
const hasOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId");
|
||||
const hasSlug = await knex.schema.hasColumn(TableName.KmsKey, "slug");
|
||||
|
||||
// drop constraint if exists (won't exist if rolled back, see below)
|
||||
await dropConstraintIfExists(TableName.KmsKey, "kms_keys_orgid_slug_unique", knex);
|
||||
|
||||
// projectId for CMEK functionality
|
||||
await knex.schema.alterTable(TableName.KmsKey, (table) => {
|
||||
table.string("projectId").nullable().references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
|
||||
if (hasOrgId) {
|
||||
table.unique(["orgId", "projectId", "slug"]);
|
||||
}
|
||||
|
||||
if (hasSlug) {
|
||||
table.renameColumn("slug", "name");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.KmsKey)) {
|
||||
const hasOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId");
|
||||
const hasName = await knex.schema.hasColumn(TableName.KmsKey, "name");
|
||||
|
||||
// remove projectId for CMEK functionality
|
||||
await knex.schema.alterTable(TableName.KmsKey, (table) => {
|
||||
if (hasName) {
|
||||
table.renameColumn("name", "slug");
|
||||
}
|
||||
|
||||
if (hasOrgId) {
|
||||
table.dropUnique(["orgId", "projectId", "slug"]);
|
||||
}
|
||||
table.dropColumn("projectId");
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.KmsKey)) {
|
||||
const hasSlug = await knex.schema.hasColumn(TableName.KmsKey, "slug");
|
||||
|
||||
if (!hasSlug) {
|
||||
// add slug back temporarily and set value equal to name
|
||||
await knex.schema
|
||||
.alterTable(TableName.KmsKey, (table) => {
|
||||
table.string("slug", 32);
|
||||
})
|
||||
.then(() => knex(TableName.KmsKey).update("slug", knex.ref("name")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.KmsKey)) {
|
||||
const hasSlug = await knex.schema.hasColumn(TableName.KmsKey, "slug");
|
||||
|
||||
if (hasSlug) {
|
||||
await knex.schema.alterTable(TableName.KmsKey, (table) => {
|
||||
table.dropColumn("slug");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
|
||||
export const dropConstraintIfExists = (tableName: TableName, constraintName: string, knex: Knex) =>
|
||||
knex.raw(`ALTER TABLE ${tableName} DROP CONSTRAINT IF EXISTS ${constraintName};`);
|
@ -54,7 +54,7 @@ export const getSecretManagerDataKey = async (knex: Knex, projectId: string) =>
|
||||
} else {
|
||||
const [kmsDoc] = await knex(TableName.KmsKey)
|
||||
.insert({
|
||||
slug: slugify(alphaNumericNanoId(8).toLowerCase()),
|
||||
name: slugify(alphaNumericNanoId(8).toLowerCase()),
|
||||
orgId: project.orgId,
|
||||
isReserved: false
|
||||
})
|
||||
|
@ -12,7 +12,8 @@ export const AccessApprovalPoliciesApproversSchema = z.object({
|
||||
policyId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
approverUserId: z.string().uuid()
|
||||
approverUserId: z.string().uuid().nullable().optional(),
|
||||
approverGroupId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>;
|
||||
|
23
backend/src/db/schemas/identity-metadata.ts
Normal file
23
backend/src/db/schemas/identity-metadata.ts
Normal file
@ -0,0 +1,23 @@
|
||||
// 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 IdentityMetadataSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
orgId: z.string().uuid(),
|
||||
userId: z.string().uuid().nullable().optional(),
|
||||
identityId: z.string().uuid().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TIdentityMetadata = z.infer<typeof IdentityMetadataSchema>;
|
||||
export type TIdentityMetadataInsert = Omit<z.input<typeof IdentityMetadataSchema>, TImmutableDBKeys>;
|
||||
export type TIdentityMetadataUpdate = Partial<Omit<z.input<typeof IdentityMetadataSchema>, TImmutableDBKeys>>;
|
@ -31,6 +31,7 @@ export * from "./identity-aws-auths";
|
||||
export * from "./identity-azure-auths";
|
||||
export * from "./identity-gcp-auths";
|
||||
export * from "./identity-kubernetes-auths";
|
||||
export * from "./identity-metadata";
|
||||
export * from "./identity-oidc-auths";
|
||||
export * from "./identity-org-memberships";
|
||||
export * from "./identity-project-additional-privilege";
|
||||
|
@ -13,9 +13,11 @@ export const KmsKeysSchema = z.object({
|
||||
isDisabled: z.boolean().default(false).nullable().optional(),
|
||||
isReserved: z.boolean().default(true).nullable().optional(),
|
||||
orgId: z.string().uuid(),
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
projectId: z.string().nullable().optional(),
|
||||
slug: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
|
||||
|
@ -70,6 +70,8 @@ export enum TableName {
|
||||
IdentityProjectMembership = "identity_project_memberships",
|
||||
IdentityProjectMembershipRole = "identity_project_membership_role",
|
||||
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
|
||||
// used by both identity and users
|
||||
IdentityMetadata = "identity_metadata",
|
||||
ScimToken = "scim_tokens",
|
||||
AccessApprovalPolicy = "access_approval_policies",
|
||||
AccessApprovalPolicyApprover = "access_approval_policies_approvers",
|
||||
|
@ -26,7 +26,8 @@ export const OidcConfigsSchema = z.object({
|
||||
isActive: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
orgId: z.string().uuid()
|
||||
orgId: z.string().uuid(),
|
||||
lastUsed: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>;
|
||||
|
@ -12,7 +12,8 @@ export const SecretApprovalPoliciesApproversSchema = z.object({
|
||||
policyId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
approverUserId: z.string().uuid()
|
||||
approverUserId: z.string().uuid().nullable().optional(),
|
||||
approverGroupId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSecretApprovalPoliciesApprovers = z.infer<typeof SecretApprovalPoliciesApproversSchema>;
|
||||
|
@ -5,14 +5,16 @@
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SecretSharingSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
encryptedValue: z.string(),
|
||||
iv: z.string(),
|
||||
tag: z.string(),
|
||||
hashedHex: z.string(),
|
||||
encryptedValue: z.string().nullable().optional(),
|
||||
iv: z.string().nullable().optional(),
|
||||
tag: z.string().nullable().optional(),
|
||||
hashedHex: z.string().nullable().optional(),
|
||||
expiresAt: z.date(),
|
||||
userId: z.string().uuid().nullable().optional(),
|
||||
orgId: z.string().uuid().nullable().optional(),
|
||||
@ -22,7 +24,9 @@ export const SecretSharingSchema = z.object({
|
||||
accessType: z.string().default("anyone"),
|
||||
name: z.string().nullable().optional(),
|
||||
lastViewedAt: z.date().nullable().optional(),
|
||||
password: z.string().nullable().optional()
|
||||
password: z.string().nullable().optional(),
|
||||
encryptedSecret: zodBuffer.nullable().optional(),
|
||||
identifier: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
||||
import { EnforcementLevel } from "@app/lib/types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@ -10,28 +12,32 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "POST",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
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),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||
})
|
||||
.refine((data) => data.approvals <= data.approvers.length, {
|
||||
path: ["approvals"],
|
||||
message: "The number of approvals should be lower than the number of approvers."
|
||||
}),
|
||||
body: z.object({
|
||||
projectSlug: z.string().trim(),
|
||||
name: z.string().optional(),
|
||||
secretPath: z.string().trim().default("/"),
|
||||
environment: z.string(),
|
||||
approvers: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
|
||||
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() })
|
||||
])
|
||||
.array()
|
||||
.min(1, { message: "At least one approver should be provided" }),
|
||||
approvals: z.number().min(1).default(1),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.accessApprovalPolicy.createAccessApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
@ -50,6 +56,9 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectSlug: z.string().trim()
|
||||
@ -58,14 +67,15 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
200: z.object({
|
||||
approvals: sapPubSchema
|
||||
.extend({
|
||||
userApprovers: z
|
||||
.object({
|
||||
userId: z.string()
|
||||
})
|
||||
.array(),
|
||||
secretPath: z.string().optional().nullable()
|
||||
approvers: z
|
||||
.object({ type: z.nativeEnum(ApproverType), id: z.string().nullable().optional() })
|
||||
.array()
|
||||
.nullable()
|
||||
.optional()
|
||||
})
|
||||
.array()
|
||||
.nullable()
|
||||
.optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -115,33 +125,37 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
server.route({
|
||||
url: "/:policyId",
|
||||
method: "PATCH",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
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),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||
})
|
||||
.refine((data) => data.approvals <= data.approvers.length, {
|
||||
path: ["approvals"],
|
||||
message: "The number of approvals should be lower than the number of approvers."
|
||||
}),
|
||||
body: z.object({
|
||||
name: z.string().optional(),
|
||||
secretPath: z
|
||||
.string()
|
||||
.trim()
|
||||
.optional()
|
||||
.transform((val) => (val === "" ? "/" : val)),
|
||||
approvers: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
|
||||
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() })
|
||||
])
|
||||
.array()
|
||||
.min(1, { message: "At least one approver should be provided" }),
|
||||
approvals: z.number().min(1).optional(),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
await server.services.accessApprovalPolicy.updateAccessApprovalPolicy({
|
||||
policyId: req.params.policyId,
|
||||
@ -157,6 +171,9 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
server.route({
|
||||
url: "/:policyId",
|
||||
method: "DELETE",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
policyId: z.string()
|
||||
@ -167,7 +184,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.accessApprovalPolicy.deleteAccessApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
@ -179,4 +196,44 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
return { approval };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:policyId",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
policyId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema.extend({
|
||||
approvers: z
|
||||
.object({
|
||||
type: z.nativeEnum(ApproverType),
|
||||
id: z.string().nullable().optional(),
|
||||
name: z.string().nullable().optional()
|
||||
})
|
||||
.array()
|
||||
.nullable()
|
||||
.optional()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.accessApprovalPolicy.getAccessApprovalPolicyById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.params
|
||||
});
|
||||
|
||||
return { approval };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -77,6 +77,39 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/entra-id/users",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
tenantId: z.string().min(1).describe("The tenant ID of the Azure Entra ID"),
|
||||
applicationId: z.string().min(1).describe("The application ID of the Azure Entra ID App Registration"),
|
||||
clientSecret: z.string().min(1).describe("The client secret of the Azure Entra ID App Registration")
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
name: z.string().min(1).describe("The name of the user"),
|
||||
id: z.string().min(1).describe("The ID of the user"),
|
||||
email: z.string().min(1).describe("The email of the user")
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const data = await server.services.dynamicSecret.fetchAzureEntraIdUsers({
|
||||
tenantId: req.body.tenantId,
|
||||
applicationId: req.body.applicationId,
|
||||
clientSecret: req.body.clientSecret
|
||||
});
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:name",
|
||||
@ -237,7 +270,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const dynamicSecretCfgs = await server.services.dynamicSecret.list({
|
||||
const dynamicSecretCfgs = await server.services.dynamicSecret.listDynamicSecretsByEnv({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
|
@ -26,7 +26,7 @@ const sanitizedExternalSchemaForGetAll = KmsKeysSchema.pick({
|
||||
isDisabled: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
slug: true
|
||||
name: true
|
||||
})
|
||||
.extend({
|
||||
externalKms: ExternalKmsSchema.pick({
|
||||
@ -57,7 +57,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
slug: z.string().min(1).trim().toLowerCase(),
|
||||
name: z.string().min(1).trim().toLowerCase(),
|
||||
description: z.string().trim().optional(),
|
||||
provider: ExternalKmsInputSchema
|
||||
}),
|
||||
@ -74,7 +74,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
slug: req.body.slug,
|
||||
name: req.body.name,
|
||||
provider: req.body.provider,
|
||||
description: req.body.description
|
||||
});
|
||||
@ -87,7 +87,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
||||
metadata: {
|
||||
kmsId: externalKms.id,
|
||||
provider: req.body.provider.type,
|
||||
slug: req.body.slug,
|
||||
name: req.body.name,
|
||||
description: req.body.description
|
||||
}
|
||||
}
|
||||
@ -108,7 +108,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
||||
id: z.string().trim().min(1)
|
||||
}),
|
||||
body: z.object({
|
||||
slug: z.string().min(1).trim().toLowerCase().optional(),
|
||||
name: z.string().min(1).trim().toLowerCase().optional(),
|
||||
description: z.string().trim().optional(),
|
||||
provider: ExternalKmsInputUpdateSchema
|
||||
}),
|
||||
@ -125,7 +125,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
slug: req.body.slug,
|
||||
name: req.body.name,
|
||||
provider: req.body.provider,
|
||||
description: req.body.description,
|
||||
id: req.params.id
|
||||
@ -139,7 +139,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
||||
metadata: {
|
||||
kmsId: externalKms.id,
|
||||
provider: req.body.provider.type,
|
||||
slug: req.body.slug,
|
||||
name: req.body.name,
|
||||
description: req.body.description
|
||||
}
|
||||
}
|
||||
@ -182,7 +182,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
||||
type: EventType.DELETE_KMS,
|
||||
metadata: {
|
||||
kmsId: externalKms.id,
|
||||
slug: externalKms.slug
|
||||
name: externalKms.name
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -224,7 +224,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
||||
type: EventType.GET_KMS,
|
||||
metadata: {
|
||||
kmsId: externalKms.id,
|
||||
slug: externalKms.slug
|
||||
name: externalKms.name
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -260,13 +260,13 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/slug/:slug",
|
||||
url: "/name/:name",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: z.string().trim().min(1)
|
||||
name: z.string().trim().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -276,12 +276,12 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const externalKms = await server.services.externalKms.findBySlug({
|
||||
const externalKms = await server.services.externalKms.findByName({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
slug: req.params.slug
|
||||
name: req.params.name
|
||||
});
|
||||
return { externalKms };
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "POST",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
body: z.object({
|
||||
name: z.string().trim().min(1).max(50).describe(GROUPS.CREATE.name),
|
||||
@ -43,12 +43,59 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:currentSlug",
|
||||
method: "PATCH",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
url: "/:id",
|
||||
method: "GET",
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
currentSlug: z.string().trim().describe(GROUPS.UPDATE.currentSlug)
|
||||
id: z.string().trim().describe(GROUPS.GET_BY_ID.id)
|
||||
}),
|
||||
response: {
|
||||
200: GroupsSchema
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const group = await server.services.group.getGroupById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.id
|
||||
});
|
||||
|
||||
return group;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "GET",
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
response: {
|
||||
200: GroupsSchema.array()
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const groups = await server.services.org.getOrgGroups({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
orgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return groups;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:id",
|
||||
method: "PATCH",
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string().trim().describe(GROUPS.UPDATE.id)
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
@ -70,7 +117,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
handler: async (req) => {
|
||||
const group = await server.services.group.updateGroup({
|
||||
currentSlug: req.params.currentSlug,
|
||||
id: req.params.id,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
@ -83,12 +130,12 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:slug",
|
||||
url: "/:id",
|
||||
method: "DELETE",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: z.string().trim().describe(GROUPS.DELETE.slug)
|
||||
id: z.string().trim().describe(GROUPS.DELETE.id)
|
||||
}),
|
||||
response: {
|
||||
200: GroupsSchema
|
||||
@ -96,7 +143,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
handler: async (req) => {
|
||||
const group = await server.services.group.deleteGroup({
|
||||
groupSlug: req.params.slug,
|
||||
id: req.params.id,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
@ -109,11 +156,11 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:slug/users",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
url: "/:id/users",
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: z.string().trim().describe(GROUPS.LIST_USERS.slug)
|
||||
id: z.string().trim().describe(GROUPS.LIST_USERS.id)
|
||||
}),
|
||||
querystring: z.object({
|
||||
offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset),
|
||||
@ -141,24 +188,25 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { users, totalCount } = await server.services.group.listGroupUsers({
|
||||
groupSlug: req.params.slug,
|
||||
id: req.params.id,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.query
|
||||
});
|
||||
|
||||
return { users, totalCount };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:slug/users/:username",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
url: "/:id/users/:username",
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: z.string().trim().describe(GROUPS.ADD_USER.slug),
|
||||
id: z.string().trim().describe(GROUPS.ADD_USER.id),
|
||||
username: z.string().trim().describe(GROUPS.ADD_USER.username)
|
||||
}),
|
||||
response: {
|
||||
@ -173,7 +221,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
handler: async (req) => {
|
||||
const user = await server.services.group.addUserToGroup({
|
||||
groupSlug: req.params.slug,
|
||||
id: req.params.id,
|
||||
username: req.params.username,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@ -187,11 +235,11 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:slug/users/:username",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
url: "/:id/users/:username",
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: z.string().trim().describe(GROUPS.DELETE_USER.slug),
|
||||
id: z.string().trim().describe(GROUPS.DELETE_USER.id),
|
||||
username: z.string().trim().describe(GROUPS.DELETE_USER.username)
|
||||
}),
|
||||
response: {
|
||||
@ -206,7 +254,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
handler: async (req) => {
|
||||
const user = await server.services.group.removeUserFromGroup({
|
||||
groupSlug: req.params.slug,
|
||||
id: req.params.id,
|
||||
username: req.params.username,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
|
@ -5,7 +5,7 @@ import { z } from "zod";
|
||||
|
||||
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types";
|
||||
import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@ -61,7 +61,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
handler: async (req) => {
|
||||
const { permissions, privilegePermission } = req.body;
|
||||
if (!permissions && !privilegePermission) {
|
||||
throw new BadRequestError({ message: "Permission or privilegePermission must be provided" });
|
||||
throw new UnauthorizedError({ message: "Permission or privilegePermission must be provided" });
|
||||
}
|
||||
|
||||
const permission = privilegePermission
|
||||
@ -140,7 +140,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
handler: async (req) => {
|
||||
const { permissions, privilegePermission } = req.body;
|
||||
if (!permissions && !privilegePermission) {
|
||||
throw new BadRequestError({ message: "Permission or privilegePermission must be provided" });
|
||||
throw new UnauthorizedError({ message: "Permission or privilegePermission must be provided" });
|
||||
}
|
||||
|
||||
const permission = privilegePermission
|
||||
@ -224,7 +224,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
handler: async (req) => {
|
||||
const { permissions, privilegePermission, ...updatedInfo } = req.body.privilegeDetails;
|
||||
if (!permissions && !privilegePermission) {
|
||||
throw new BadRequestError({ message: "Permission or privilegePermission must be provided" });
|
||||
throw new UnauthorizedError({ message: "Permission or privilegePermission must be provided" });
|
||||
}
|
||||
|
||||
const permission = privilegePermission
|
||||
|
@ -3,10 +3,11 @@ import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
|
||||
import { ProjectPermissionSchema } from "@app/ee/services/permission/project-permission";
|
||||
import { PROJECT_ROLE } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { ProjectPermissionSchema, SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
|
@ -87,6 +87,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Daniel: This endpoint is no longer is use.
|
||||
* We are keeping it for now because it has been exposed in our public api docs for a while, so by removing it we are likely to break users workflows.
|
||||
*
|
||||
* Please refer to the new endpoint, GET /api/v1/organization/audit-logs, for the same (and more) functionality.
|
||||
*/
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/audit-logs",
|
||||
@ -101,7 +107,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.workspaceId)
|
||||
workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.projectId)
|
||||
}),
|
||||
querystring: z.object({
|
||||
eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType),
|
||||
@ -122,10 +128,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
.merge(
|
||||
z.object({
|
||||
project: z.object({
|
||||
name: z.string(),
|
||||
slug: z.string()
|
||||
}),
|
||||
project: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
slug: z.string()
|
||||
})
|
||||
.optional(),
|
||||
event: z.object({
|
||||
type: z.string(),
|
||||
metadata: z.any()
|
||||
@ -195,7 +203,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
200: z.object({
|
||||
secretManagerKmsKey: z.object({
|
||||
id: z.string(),
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
isExternal: z.boolean()
|
||||
})
|
||||
})
|
||||
@ -235,7 +243,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
200: z.object({
|
||||
secretManagerKmsKey: z.object({
|
||||
id: z.string(),
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
isExternal: z.boolean()
|
||||
})
|
||||
})
|
||||
@ -260,7 +268,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
metadata: {
|
||||
secretManagerKmsKey: {
|
||||
id: secretManagerKmsKey.id,
|
||||
slug: secretManagerKmsKey.slug
|
||||
name: secretManagerKmsKey.name
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -328,7 +336,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
200: z.object({
|
||||
secretManagerKmsKey: z.object({
|
||||
id: z.string(),
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
isExternal: z.boolean()
|
||||
})
|
||||
})
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { RateLimitSchema } from "@app/db/schemas";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@ -29,7 +29,7 @@ export const registerRateLimitRouter = async (server: FastifyZodProvider) => {
|
||||
handler: async () => {
|
||||
const rateLimit = await server.services.rateLimit.getRateLimits();
|
||||
if (!rateLimit) {
|
||||
throw new BadRequestError({
|
||||
throw new NotFoundError({
|
||||
name: "Get Rate Limit Error",
|
||||
message: "Rate limit configuration does not exist."
|
||||
});
|
||||
|
@ -61,7 +61,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
id: samlConfigId
|
||||
};
|
||||
} else {
|
||||
throw new BadRequestError({ message: "Missing sso identitier or org slug" });
|
||||
throw new BadRequestError({ message: "Missing sso identifier or org slug" });
|
||||
}
|
||||
|
||||
const ssoConfig = await server.services.saml.getSaml(ssoLookupDetails);
|
||||
@ -100,6 +100,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
async (req, profile, cb) => {
|
||||
try {
|
||||
if (!profile) throw new BadRequestError({ message: "Missing profile" });
|
||||
|
||||
const email =
|
||||
profile?.email ??
|
||||
// entra sends data in this format
|
||||
@ -123,6 +124,14 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
);
|
||||
}
|
||||
|
||||
const userMetadata = Object.keys(profile.attributes || {})
|
||||
.map((key) => {
|
||||
// for the ones like in format: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email
|
||||
const formatedKey = key.startsWith("http") ? key.split("/").at(-1) || "" : key;
|
||||
return { key: formatedKey, value: String((profile.attributes as Record<string, string>)[key]) };
|
||||
})
|
||||
.filter((el) => el.key && !["email", "firstName", "lastName"].includes(el.key));
|
||||
|
||||
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
|
||||
externalId: profile.nameID,
|
||||
email,
|
||||
@ -130,7 +139,8 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
lastName: lastName as string,
|
||||
relayState: (req.body as { RelayState?: string }).RelayState,
|
||||
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string,
|
||||
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string
|
||||
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string,
|
||||
metadata: userMetadata
|
||||
});
|
||||
cb(null, { isUserCompleted, providerAuthToken });
|
||||
} catch (error) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { EnforcementLevel } from "@app/lib/types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
@ -16,32 +17,33 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z
|
||||
.object({
|
||||
workspaceId: z.string(),
|
||||
name: z.string().optional(),
|
||||
environment: z.string(),
|
||||
secretPath: z
|
||||
.string()
|
||||
.optional()
|
||||
.nullable()
|
||||
.default("/")
|
||||
.transform((val) => (val ? removeTrailingSlash(val) : val)),
|
||||
approvers: z.string().array().min(1),
|
||||
approvals: z.number().min(1).default(1),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||
})
|
||||
.refine((data) => data.approvals <= data.approvers.length, {
|
||||
path: ["approvals"],
|
||||
message: "The number of approvals should be lower than the number of approvers."
|
||||
}),
|
||||
body: z.object({
|
||||
workspaceId: z.string(),
|
||||
name: z.string().optional(),
|
||||
environment: z.string(),
|
||||
secretPath: z
|
||||
.string()
|
||||
.optional()
|
||||
.nullable()
|
||||
.default("/")
|
||||
.transform((val) => (val ? removeTrailingSlash(val) : val)),
|
||||
approvers: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
|
||||
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() })
|
||||
])
|
||||
.array()
|
||||
.min(1, { message: "At least one approver should be provided" }),
|
||||
approvals: z.number().min(1).default(1),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.secretApprovalPolicy.createSecretApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
@ -67,30 +69,31 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
params: z.object({
|
||||
sapId: z.string()
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
approvers: z.string().array().min(1),
|
||||
approvals: z.number().min(1).default(1),
|
||||
secretPath: z
|
||||
.string()
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((val) => (val ? removeTrailingSlash(val) : val))
|
||||
.transform((val) => (val === "" ? "/" : val)),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).optional()
|
||||
})
|
||||
.refine((data) => data.approvals <= data.approvers.length, {
|
||||
path: ["approvals"],
|
||||
message: "The number of approvals should be lower than the number of approvers."
|
||||
}),
|
||||
body: z.object({
|
||||
name: z.string().optional(),
|
||||
approvers: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
|
||||
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() })
|
||||
])
|
||||
.array()
|
||||
.min(1, { message: "At least one approver should be provided" }),
|
||||
approvals: z.number().min(1).default(1),
|
||||
secretPath: z
|
||||
.string()
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((val) => (val ? removeTrailingSlash(val) : val))
|
||||
.transform((val) => (val === "" ? "/" : val)),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.secretApprovalPolicy.updateSecretApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
@ -120,7 +123,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.secretApprovalPolicy.deleteSecretApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
@ -147,9 +150,10 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
200: z.object({
|
||||
approvals: sapPubSchema
|
||||
.extend({
|
||||
userApprovers: z
|
||||
approvers: z
|
||||
.object({
|
||||
userId: z.string()
|
||||
id: z.string().nullable().optional(),
|
||||
type: z.nativeEnum(ApproverType)
|
||||
})
|
||||
.array()
|
||||
})
|
||||
@ -170,6 +174,44 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:sapId",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
sapId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema.extend({
|
||||
approvers: z
|
||||
.object({
|
||||
id: z.string().nullable().optional(),
|
||||
type: z.nativeEnum(ApproverType),
|
||||
name: z.string().nullable().optional()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.secretApprovalPolicy.getSecretApprovalPolicyById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.params
|
||||
});
|
||||
|
||||
return { approval };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/board",
|
||||
method: "GET",
|
||||
@ -186,7 +228,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
200: z.object({
|
||||
policy: sapPubSchema
|
||||
.extend({
|
||||
userApprovers: z.object({ userId: z.string() }).array()
|
||||
userApprovers: z.object({ userId: z.string().nullable().optional() }).array()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
|
@ -13,7 +13,7 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const approvalRequestUser = z.object({ userId: z.string() }).merge(
|
||||
const approvalRequestUser = z.object({ userId: z.string().nullable().optional() }).merge(
|
||||
UsersSchema.pick({
|
||||
email: true,
|
||||
firstName: true,
|
||||
@ -46,7 +46,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
approvals: z.number(),
|
||||
approvers: z.string().array(),
|
||||
approvers: z
|
||||
.object({
|
||||
userId: z.string().nullable().optional()
|
||||
})
|
||||
.array(),
|
||||
secretPath: z.string().optional().nullable(),
|
||||
enforcementLevel: z.string()
|
||||
}),
|
||||
@ -54,7 +58,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(),
|
||||
environment: z.string(),
|
||||
reviewers: z.object({ userId: z.string(), status: z.string() }).array(),
|
||||
approvers: z.string().array()
|
||||
approvers: z
|
||||
.object({
|
||||
userId: z.string().nullable().optional()
|
||||
})
|
||||
.array()
|
||||
}).array()
|
||||
})
|
||||
}
|
||||
|
@ -5,22 +5,38 @@ import { AccessApprovalPoliciesSchema, TableName, TAccessApprovalPolicies } from
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
|
||||
|
||||
import { ApproverType } from "./access-approval-policy-types";
|
||||
|
||||
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 accessApprovalPolicyFindQuery = async (
|
||||
tx: Knex,
|
||||
filter: TFindFilter<TAccessApprovalPolicies>,
|
||||
customFilter?: {
|
||||
policyId?: string;
|
||||
}
|
||||
) => {
|
||||
const result = await tx(TableName.AccessApprovalPolicy)
|
||||
// eslint-disable-next-line
|
||||
.where(buildFindFilter(filter))
|
||||
.where((qb) => {
|
||||
if (customFilter?.policyId) {
|
||||
void qb.where(`${TableName.AccessApprovalPolicy}.id`, "=", customFilter.policyId);
|
||||
}
|
||||
})
|
||||
.join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalPolicyApprover,
|
||||
`${TableName.AccessApprovalPolicy}.id`,
|
||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
.leftJoin(TableName.Users, `${TableName.AccessApprovalPolicyApprover}.approverUserId`, `${TableName.Users}.id`)
|
||||
.select(tx.ref("username").withSchema(TableName.Users).as("approverUsername"))
|
||||
.select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||
.select(tx.ref("approverGroupId").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"))
|
||||
@ -30,10 +46,10 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
const findById = async (policyId: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), {
|
||||
[`${TableName.AccessApprovalPolicy}.id` as "id"]: id
|
||||
[`${TableName.AccessApprovalPolicy}.id` as "id"]: policyId
|
||||
});
|
||||
const formattedDoc = sqlNestRelationships({
|
||||
data: doc,
|
||||
@ -50,9 +66,18 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "approverUserId",
|
||||
label: "userApprovers" as const,
|
||||
mapper: ({ approverUserId }) => ({
|
||||
userId: approverUserId
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverUserId: id }) => ({
|
||||
id,
|
||||
type: "user"
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "approverGroupId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverGroupId: id }) => ({
|
||||
id,
|
||||
type: "group"
|
||||
})
|
||||
}
|
||||
]
|
||||
@ -64,9 +89,15 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const find = async (filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>, tx?: Knex) => {
|
||||
const find = async (
|
||||
filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>,
|
||||
customFilter?: {
|
||||
policyId?: string;
|
||||
},
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const docs = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), filter);
|
||||
const docs = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), filter, customFilter);
|
||||
|
||||
const formattedDocs = sqlNestRelationships({
|
||||
data: docs,
|
||||
@ -84,9 +115,19 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "approverUserId",
|
||||
label: "userApprovers" as const,
|
||||
mapper: ({ approverUserId }) => ({
|
||||
userId: approverUserId
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverUserId: id, approverUsername }) => ({
|
||||
id,
|
||||
type: ApproverType.User,
|
||||
name: approverUsername
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "approverGroupId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverGroupId: id }) => ({
|
||||
id,
|
||||
type: ApproverType.Group
|
||||
})
|
||||
}
|
||||
]
|
||||
|
@ -1,12 +1,11 @@
|
||||
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";
|
||||
import { TIsApproversValid } from "./access-approval-policy-types";
|
||||
|
||||
export const verifyApprovers = async ({
|
||||
export const isApproversValid = async ({
|
||||
userIds,
|
||||
projectId,
|
||||
orgId,
|
||||
@ -14,9 +13,9 @@ export const verifyApprovers = async ({
|
||||
actorAuthMethod,
|
||||
secretPath,
|
||||
permissionService
|
||||
}: TVerifyApprovers) => {
|
||||
for await (const userId of userIds) {
|
||||
try {
|
||||
}: TIsApproversValid) => {
|
||||
try {
|
||||
for await (const userId of userIds) {
|
||||
const { permission: approverPermission } = await permissionService.getProjectPermission(
|
||||
ActorType.USER,
|
||||
userId,
|
||||
@ -29,8 +28,9 @@ export const verifyApprovers = async ({
|
||||
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" });
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
@ -2,17 +2,21 @@ 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 { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
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 { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TGroupDALFactory } from "../group/group-dal";
|
||||
import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal";
|
||||
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
|
||||
import { verifyApprovers } from "./access-approval-policy-fns";
|
||||
import { isApproversValid } from "./access-approval-policy-fns";
|
||||
import {
|
||||
ApproverType,
|
||||
TCreateAccessApprovalPolicy,
|
||||
TDeleteAccessApprovalPolicy,
|
||||
TGetAccessApprovalPolicyByIdDTO,
|
||||
TGetAccessPolicyCountByEnvironmentDTO,
|
||||
TListAccessApprovalPoliciesDTO,
|
||||
TUpdateAccessApprovalPolicy
|
||||
@ -25,6 +29,8 @@ type TSecretApprovalPolicyServiceFactoryDep = {
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findOne">;
|
||||
accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
||||
groupDAL: TGroupDALFactory;
|
||||
userDAL: Pick<TUserDALFactory, "find">;
|
||||
};
|
||||
|
||||
export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>;
|
||||
@ -32,9 +38,11 @@ export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprov
|
||||
export const accessApprovalPolicyServiceFactory = ({
|
||||
accessApprovalPolicyDAL,
|
||||
accessApprovalPolicyApproverDAL,
|
||||
groupDAL,
|
||||
permissionService,
|
||||
projectEnvDAL,
|
||||
projectDAL
|
||||
projectDAL,
|
||||
userDAL
|
||||
}: TSecretApprovalPolicyServiceFactoryDep) => {
|
||||
const createAccessApprovalPolicy = async ({
|
||||
name,
|
||||
@ -50,9 +58,23 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
enforcementLevel
|
||||
}: TCreateAccessApprovalPolicy) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
if (!project) throw new NotFoundError({ message: "Project not found" });
|
||||
|
||||
if (approvals > approvers.length)
|
||||
// If there is a group approver people might be added to the group later to meet the approvers quota
|
||||
const groupApprovers = approvers
|
||||
.filter((approver) => approver.type === ApproverType.Group)
|
||||
.map((approver) => approver.id) as string[];
|
||||
|
||||
const userApprovers = approvers
|
||||
.filter((approver) => approver.type === ApproverType.User)
|
||||
.map((approver) => approver.id)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const userApproverNames = approvers
|
||||
.map((approver) => (approver.type === ApproverType.User ? approver.name : undefined))
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
if (!groupApprovers && approvals > userApprovers.length + userApproverNames.length)
|
||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
@ -67,18 +89,65 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
|
||||
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
||||
if (!env) throw new NotFoundError({ message: "Environment not found" });
|
||||
|
||||
await verifyApprovers({
|
||||
let approverUserIds = userApprovers;
|
||||
if (userApproverNames.length) {
|
||||
const approverUsers = await userDAL.find({
|
||||
$in: {
|
||||
username: userApproverNames
|
||||
}
|
||||
});
|
||||
|
||||
const approverNamesFromDb = approverUsers.map((user) => user.username);
|
||||
const invalidUsernames = userApproverNames.filter((username) => !approverNamesFromDb.includes(username));
|
||||
|
||||
if (invalidUsernames.length) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid approver user: ${invalidUsernames.join(", ")}`
|
||||
});
|
||||
}
|
||||
|
||||
approverUserIds = approverUserIds.concat(approverUsers.map((user) => user.id));
|
||||
}
|
||||
|
||||
const usersPromises: Promise<
|
||||
{
|
||||
id: string;
|
||||
email: string | null | undefined;
|
||||
username: string;
|
||||
firstName: string | null | undefined;
|
||||
lastName: string | null | undefined;
|
||||
isPartOfGroup: boolean;
|
||||
}[]
|
||||
>[] = [];
|
||||
const verifyAllApprovers = [...approverUserIds];
|
||||
|
||||
for (const groupId of groupApprovers) {
|
||||
usersPromises.push(groupDAL.findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 }));
|
||||
}
|
||||
const verifyGroupApprovers = (await Promise.all(usersPromises))
|
||||
.flat()
|
||||
.filter((user) => user.isPartOfGroup)
|
||||
.map((user) => user.id);
|
||||
verifyAllApprovers.push(...verifyGroupApprovers);
|
||||
|
||||
const approversValid = await isApproversValid({
|
||||
projectId: project.id,
|
||||
orgId: actorOrgId,
|
||||
envSlug: environment,
|
||||
secretPath,
|
||||
actorAuthMethod,
|
||||
permissionService,
|
||||
userIds: approvers
|
||||
userIds: verifyAllApprovers
|
||||
});
|
||||
|
||||
if (!approversValid) {
|
||||
throw new BadRequestError({
|
||||
message: "One or more approvers doesn't have access to be specified secret path"
|
||||
});
|
||||
}
|
||||
|
||||
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
|
||||
const doc = await accessApprovalPolicyDAL.create(
|
||||
{
|
||||
@ -90,13 +159,26 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
await accessApprovalPolicyApproverDAL.insertMany(
|
||||
approvers.map((userId) => ({
|
||||
approverUserId: userId,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
if (approverUserIds.length) {
|
||||
await accessApprovalPolicyApproverDAL.insertMany(
|
||||
approverUserIds.map((userId) => ({
|
||||
approverUserId: userId,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
if (groupApprovers) {
|
||||
await accessApprovalPolicyApproverDAL.insertMany(
|
||||
groupApprovers.map((groupId) => ({
|
||||
approverGroupId: groupId,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return doc;
|
||||
});
|
||||
return { ...accessApproval, environment: env, projectId: project.id };
|
||||
@ -110,7 +192,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
projectSlug
|
||||
}: TListAccessApprovalPoliciesDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
if (!project) throw new NotFoundError({ message: "Project not found" });
|
||||
|
||||
// Anyone in the project should be able to get the policies.
|
||||
/* const { permission } = */ await permissionService.getProjectPermission(
|
||||
@ -138,8 +220,30 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
approvals,
|
||||
enforcementLevel
|
||||
}: TUpdateAccessApprovalPolicy) => {
|
||||
const groupApprovers = approvers
|
||||
.filter((approver) => approver.type === ApproverType.Group)
|
||||
.map((approver) => approver.id) as string[];
|
||||
|
||||
const userApprovers = approvers
|
||||
.filter((approver) => approver.type === ApproverType.User)
|
||||
.map((approver) => approver.id)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const userApproverNames = approvers
|
||||
.map((approver) => (approver.type === ApproverType.User ? approver.name : undefined))
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
|
||||
if (!accessApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
|
||||
const currentAppovals = approvals || accessApprovalPolicy.approvals;
|
||||
if (
|
||||
groupApprovers?.length === 0 &&
|
||||
userApprovers &&
|
||||
currentAppovals > userApprovers.length + userApproverNames.length
|
||||
) {
|
||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||
}
|
||||
|
||||
if (!accessApprovalPolicy) throw new NotFoundError({ message: "Secret approval policy not found" });
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@ -161,26 +265,100 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (approvers) {
|
||||
await verifyApprovers({
|
||||
|
||||
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
||||
|
||||
if (userApprovers.length || userApproverNames.length) {
|
||||
let userApproverIds = userApprovers;
|
||||
if (userApproverNames.length) {
|
||||
const approverUsers = await userDAL.find({
|
||||
$in: {
|
||||
username: userApproverNames
|
||||
}
|
||||
});
|
||||
|
||||
const approverNamesFromDb = approverUsers.map((user) => user.username);
|
||||
const invalidUsernames = userApproverNames.filter((username) => !approverNamesFromDb.includes(username));
|
||||
|
||||
if (invalidUsernames.length) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid approver user: ${invalidUsernames.join(", ")}`
|
||||
});
|
||||
}
|
||||
|
||||
userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id));
|
||||
}
|
||||
|
||||
const approversValid = await isApproversValid({
|
||||
projectId: accessApprovalPolicy.projectId,
|
||||
orgId: actorOrgId,
|
||||
envSlug: accessApprovalPolicy.environment.slug,
|
||||
secretPath: doc.secretPath!,
|
||||
actorAuthMethod,
|
||||
permissionService,
|
||||
userIds: approvers
|
||||
userIds: userApproverIds
|
||||
});
|
||||
|
||||
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
||||
if (!approversValid) {
|
||||
throw new BadRequestError({
|
||||
message: "One or more approvers doesn't have access to be specified secret path"
|
||||
});
|
||||
}
|
||||
|
||||
await accessApprovalPolicyApproverDAL.insertMany(
|
||||
approvers.map((userId) => ({
|
||||
userApproverIds.map((userId) => ({
|
||||
approverUserId: userId,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
if (groupApprovers) {
|
||||
const usersPromises: Promise<
|
||||
{
|
||||
id: string;
|
||||
email: string | null | undefined;
|
||||
username: string;
|
||||
firstName: string | null | undefined;
|
||||
lastName: string | null | undefined;
|
||||
isPartOfGroup: boolean;
|
||||
}[]
|
||||
>[] = [];
|
||||
|
||||
for (const groupId of groupApprovers) {
|
||||
usersPromises.push(groupDAL.findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 }));
|
||||
}
|
||||
const verifyGroupApprovers = (await Promise.all(usersPromises))
|
||||
.flat()
|
||||
.filter((user) => user.isPartOfGroup)
|
||||
.map((user) => user.id);
|
||||
|
||||
const approversValid = await isApproversValid({
|
||||
projectId: accessApprovalPolicy.projectId,
|
||||
orgId: actorOrgId,
|
||||
envSlug: accessApprovalPolicy.environment.slug,
|
||||
secretPath: doc.secretPath!,
|
||||
actorAuthMethod,
|
||||
permissionService,
|
||||
userIds: verifyGroupApprovers
|
||||
});
|
||||
|
||||
if (!approversValid) {
|
||||
throw new BadRequestError({
|
||||
message: "One or more approvers doesn't have access to be specified secret path"
|
||||
});
|
||||
}
|
||||
|
||||
await accessApprovalPolicyApproverDAL.insertMany(
|
||||
groupApprovers.map((groupId) => ({
|
||||
approverGroupId: groupId,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return doc;
|
||||
});
|
||||
return {
|
||||
@ -198,7 +376,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
actorOrgId
|
||||
}: TDeleteAccessApprovalPolicy) => {
|
||||
const policy = await accessApprovalPolicyDAL.findById(policyId);
|
||||
if (!policy) throw new BadRequestError({ message: "Secret approval policy not found" });
|
||||
if (!policy) throw new NotFoundError({ message: "Secret approval policy not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@ -226,7 +404,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
}: TGetAccessPolicyCountByEnvironmentDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
if (!project) throw new NotFoundError({ message: "Project not found" });
|
||||
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@ -235,22 +413,53 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!membership) throw new BadRequestError({ message: "User not found in project" });
|
||||
if (!membership) {
|
||||
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
|
||||
}
|
||||
|
||||
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
|
||||
if (!environment) throw new BadRequestError({ message: "Environment not found" });
|
||||
if (!environment) throw new NotFoundError({ message: "Environment not found" });
|
||||
|
||||
const policies = await accessApprovalPolicyDAL.find({ envId: environment.id, projectId: project.id });
|
||||
if (!policies) throw new BadRequestError({ message: "No policies found" });
|
||||
if (!policies) throw new NotFoundError({ message: "No policies found" });
|
||||
|
||||
return { count: policies.length };
|
||||
};
|
||||
|
||||
const getAccessApprovalPolicyById = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
policyId
|
||||
}: TGetAccessApprovalPolicyByIdDTO) => {
|
||||
const [policy] = await accessApprovalPolicyDAL.find({}, { policyId });
|
||||
|
||||
if (!policy) {
|
||||
throw new NotFoundError({
|
||||
message: "Cannot find access approval policy"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
policy.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
|
||||
return policy;
|
||||
};
|
||||
|
||||
return {
|
||||
getAccessPolicyCountByEnvSlug,
|
||||
createAccessApprovalPolicy,
|
||||
deleteAccessApprovalPolicy,
|
||||
updateAccessApprovalPolicy,
|
||||
getAccessApprovalPolicyByProjectSlug
|
||||
getAccessApprovalPolicyByProjectSlug,
|
||||
getAccessApprovalPolicyById
|
||||
};
|
||||
};
|
||||
|
@ -3,7 +3,7 @@ import { ActorAuthMethod } from "@app/services/auth/auth-type";
|
||||
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
|
||||
export type TVerifyApprovers = {
|
||||
export type TIsApproversValid = {
|
||||
userIds: string[];
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
envSlug: string;
|
||||
@ -13,11 +13,16 @@ export type TVerifyApprovers = {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export enum ApproverType {
|
||||
Group = "group",
|
||||
User = "user"
|
||||
}
|
||||
|
||||
export type TCreateAccessApprovalPolicy = {
|
||||
approvals: number;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
approvers: string[];
|
||||
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
|
||||
projectSlug: string;
|
||||
name: string;
|
||||
enforcementLevel: EnforcementLevel;
|
||||
@ -26,7 +31,7 @@ export type TCreateAccessApprovalPolicy = {
|
||||
export type TUpdateAccessApprovalPolicy = {
|
||||
policyId: string;
|
||||
approvals?: number;
|
||||
approvers?: string[];
|
||||
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
|
||||
secretPath?: string;
|
||||
name?: string;
|
||||
enforcementLevel?: EnforcementLevel;
|
||||
@ -41,6 +46,10 @@ export type TGetAccessPolicyCountByEnvironmentDTO = {
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetAccessApprovalPolicyByIdDTO = {
|
||||
policyId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListAccessApprovalPoliciesDTO = {
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
@ -39,6 +39,12 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
`${TableName.AccessApprovalPolicy}.id`,
|
||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.UserGroupMembership,
|
||||
`${TableName.AccessApprovalPolicyApprover}.approverGroupId`,
|
||||
`${TableName.UserGroupMembership}.groupId`
|
||||
)
|
||||
.leftJoin(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
||||
|
||||
.join<TUsers>(
|
||||
db(TableName.Users).as("requestedByUser"),
|
||||
@ -59,6 +65,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
)
|
||||
|
||||
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||
.select(db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"))
|
||||
|
||||
.select(
|
||||
db.ref("projectId").withSchema(TableName.Environment),
|
||||
@ -142,7 +149,12 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
label: "reviewers" as const,
|
||||
mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined)
|
||||
},
|
||||
{ key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId }
|
||||
{ key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId },
|
||||
{
|
||||
key: "approverGroupUserId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverGroupUserId }) => approverGroupUserId
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
@ -172,17 +184,28 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
`requestedByUser.id`
|
||||
)
|
||||
|
||||
.join(
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalPolicyApprover,
|
||||
`${TableName.AccessApprovalPolicy}.id`,
|
||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
|
||||
.join<TUsers>(
|
||||
.leftJoin<TUsers>(
|
||||
db(TableName.Users).as("accessApprovalPolicyApproverUser"),
|
||||
`${TableName.AccessApprovalPolicyApprover}.approverUserId`,
|
||||
"accessApprovalPolicyApproverUser.id"
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.UserGroupMembership,
|
||||
`${TableName.AccessApprovalPolicyApprover}.approverGroupId`,
|
||||
`${TableName.UserGroupMembership}.groupId`
|
||||
)
|
||||
|
||||
.leftJoin<TUsers>(
|
||||
db(TableName.Users).as("accessApprovalPolicyGroupApproverUser"),
|
||||
`${TableName.UserGroupMembership}.userId`,
|
||||
"accessApprovalPolicyGroupApproverUser.id"
|
||||
)
|
||||
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalRequestReviewer,
|
||||
@ -200,10 +223,15 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||
.select(
|
||||
tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover),
|
||||
tx.ref("userId").withSchema(TableName.UserGroupMembership),
|
||||
tx.ref("email").withSchema("accessApprovalPolicyApproverUser").as("approverEmail"),
|
||||
tx.ref("email").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupEmail"),
|
||||
tx.ref("username").withSchema("accessApprovalPolicyApproverUser").as("approverUsername"),
|
||||
tx.ref("username").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupUsername"),
|
||||
tx.ref("firstName").withSchema("accessApprovalPolicyApproverUser").as("approverFirstName"),
|
||||
tx.ref("firstName").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupFirstName"),
|
||||
tx.ref("lastName").withSchema("accessApprovalPolicyApproverUser").as("approverLastName"),
|
||||
tx.ref("lastName").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupLastName"),
|
||||
tx.ref("email").withSchema("requestedByUser").as("requestedByUserEmail"),
|
||||
tx.ref("username").withSchema("requestedByUser").as("requestedByUserUsername"),
|
||||
tx.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"),
|
||||
@ -282,6 +310,23 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
lastName,
|
||||
username
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "userId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({
|
||||
userId,
|
||||
approverGroupEmail: email,
|
||||
approverGroupUsername: username,
|
||||
approverGroupLastName: lastName,
|
||||
approverFirstName: firstName
|
||||
}) => ({
|
||||
userId,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
username
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { TVerifyPermission } from "./access-approval-request-types";
|
||||
|
||||
@ -19,7 +19,7 @@ export const verifyRequestedPermissions = ({ permissions }: TVerifyPermission) =
|
||||
);
|
||||
|
||||
if (!permission || !permission.length) {
|
||||
throw new UnauthorizedError({ message: "No permission provided" });
|
||||
throw new BadRequestError({ message: "No permission provided" });
|
||||
}
|
||||
|
||||
const requestedPermissions: string[] = [];
|
||||
@ -39,10 +39,10 @@ export const verifyRequestedPermissions = ({ permissions }: TVerifyPermission) =
|
||||
const permissionEnv = firstPermission.conditions?.environment;
|
||||
|
||||
if (!permissionEnv || typeof permissionEnv !== "string") {
|
||||
throw new UnauthorizedError({ message: "Permission environment is not a string" });
|
||||
throw new BadRequestError({ message: "Permission environment is not a string" });
|
||||
}
|
||||
if (!permissionSecretPath || typeof permissionSecretPath !== "string") {
|
||||
throw new UnauthorizedError({ message: "Permission path is not a string" });
|
||||
throw new BadRequestError({ message: "Permission path is not a string" });
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -3,7 +3,7 @@ import ms from "ms";
|
||||
|
||||
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
@ -17,7 +17,8 @@ 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 { isApproversValid } from "../access-approval-policy/access-approval-policy-fns";
|
||||
import { TGroupDALFactory } from "../group/group-dal";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types";
|
||||
@ -57,6 +58,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
TAccessApprovalRequestReviewerDALFactory,
|
||||
"create" | "find" | "findOne" | "transaction"
|
||||
>;
|
||||
groupDAL: Pick<TGroupDALFactory, "findAllGroupPossibleMembers">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
userDAL: Pick<
|
||||
@ -70,6 +72,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
|
||||
|
||||
export const accessApprovalRequestServiceFactory = ({
|
||||
groupDAL,
|
||||
projectDAL,
|
||||
projectEnvDAL,
|
||||
permissionService,
|
||||
@ -96,7 +99,7 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
}: TCreateAccessApprovalRequestDTO) => {
|
||||
const cfg = getConfig();
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new UnauthorizedError({ message: "Project not found" });
|
||||
if (!project) throw new NotFoundError({ message: "Project not found" });
|
||||
|
||||
// Anyone can create an access approval request.
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
@ -106,31 +109,56 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" });
|
||||
if (!membership) {
|
||||
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
|
||||
}
|
||||
|
||||
const requestedByUser = await userDAL.findById(actorId);
|
||||
if (!requestedByUser) throw new UnauthorizedError({ message: "User not found" });
|
||||
if (!requestedByUser) throw new ForbiddenRequestError({ message: "User not found" });
|
||||
|
||||
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" });
|
||||
if (!environment) throw new NotFoundError({ 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." });
|
||||
if (!policy) throw new NotFoundError({ message: "No policy matching criteria was found." });
|
||||
|
||||
const approverIds: string[] = [];
|
||||
const approverGroupIds: string[] = [];
|
||||
|
||||
const approvers = await accessApprovalPolicyApproverDAL.find({
|
||||
policyId: policy.id
|
||||
});
|
||||
|
||||
approvers.forEach((approver) => {
|
||||
if (approver.approverUserId) {
|
||||
approverIds.push(approver.approverUserId);
|
||||
} else if (approver.approverGroupId) {
|
||||
approverGroupIds.push(approver.approverGroupId);
|
||||
}
|
||||
});
|
||||
|
||||
const groupUsers = (
|
||||
await Promise.all(
|
||||
approverGroupIds.map((groupApproverId) =>
|
||||
groupDAL.findAllGroupPossibleMembers({
|
||||
orgId: actorOrgId,
|
||||
groupId: groupApproverId
|
||||
})
|
||||
)
|
||||
)
|
||||
).flat();
|
||||
approverIds.push(...groupUsers.filter((user) => user.isPartOfGroup).map((user) => user.id));
|
||||
|
||||
const approverUsers = await userDAL.find({
|
||||
$in: {
|
||||
id: approvers.map((approver) => approver.approverUserId)
|
||||
id: [...new Set(approverIds)]
|
||||
}
|
||||
});
|
||||
|
||||
@ -236,7 +264,7 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
actorAuthMethod
|
||||
}: TListApprovalRequestsDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new UnauthorizedError({ message: "Project not found" });
|
||||
if (!project) throw new NotFoundError({ message: "Project not found" });
|
||||
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@ -245,7 +273,9 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" });
|
||||
if (!membership) {
|
||||
throw new ForbiddenRequestError({ 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));
|
||||
@ -270,7 +300,7 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
actorOrgId
|
||||
}: TReviewAccessRequestDTO) => {
|
||||
const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId);
|
||||
if (!accessApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||
if (!accessApprovalRequest) throw new NotFoundError({ message: "Secret approval request not found" });
|
||||
|
||||
const { policy } = accessApprovalRequest;
|
||||
const { membership, hasRole } = await permissionService.getProjectPermission(
|
||||
@ -281,19 +311,21 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" });
|
||||
if (!membership) {
|
||||
throw new ForbiddenRequestError({ 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((approver) => approver.userId === actorId) // The request isn't performed by an assigned approver
|
||||
) {
|
||||
throw new UnauthorizedError({ message: "You are not authorized to approve this request" });
|
||||
throw new ForbiddenRequestError({ message: "You are not authorized to approve this request" });
|
||||
}
|
||||
|
||||
const reviewerProjectMembership = await projectMembershipDAL.findById(membership.id);
|
||||
|
||||
await verifyApprovers({
|
||||
const approversValid = await isApproversValid({
|
||||
projectId: accessApprovalRequest.projectId,
|
||||
orgId: actorOrgId,
|
||||
envSlug: accessApprovalRequest.environment,
|
||||
@ -303,6 +335,10 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
userIds: [reviewerProjectMembership.userId]
|
||||
});
|
||||
|
||||
if (!approversValid) {
|
||||
throw new ForbiddenRequestError({ message: "You don't have access to approve this request" });
|
||||
}
|
||||
|
||||
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" });
|
||||
@ -385,7 +421,7 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
|
||||
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" });
|
||||
if (!project) throw new NotFoundError({ message: "Project not found" });
|
||||
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@ -394,7 +430,9 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!membership) throw new BadRequestError({ message: "User not found in project" });
|
||||
if (!membership) {
|
||||
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
|
||||
}
|
||||
|
||||
const count = await accessApprovalRequestDAL.getCount({ projectId: project.id });
|
||||
|
||||
|
@ -5,7 +5,7 @@ import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
|
||||
import { AUDIT_LOG_STREAM_TIMEOUT } from "../audit-log/audit-log-queue";
|
||||
@ -43,14 +43,15 @@ export const auditLogStreamServiceFactory = ({
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TCreateAuditLogStreamDTO) => {
|
||||
if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" });
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" });
|
||||
|
||||
const appCfg = getConfig();
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.auditLogStreams)
|
||||
if (!plan.auditLogStreams) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to create audit log streams due to plan restriction. Upgrade plan to create group."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
@ -120,7 +121,7 @@ export const auditLogStreamServiceFactory = ({
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TUpdateAuditLogStreamDTO) => {
|
||||
if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" });
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" });
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.auditLogStreams)
|
||||
@ -129,7 +130,7 @@ export const auditLogStreamServiceFactory = ({
|
||||
});
|
||||
|
||||
const logStream = await auditLogStreamDAL.findById(id);
|
||||
if (!logStream) throw new BadRequestError({ message: "Audit log stream not found" });
|
||||
if (!logStream) throw new NotFoundError({ message: "Audit log stream not found" });
|
||||
|
||||
const { orgId } = logStream;
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
@ -178,10 +179,10 @@ export const auditLogStreamServiceFactory = ({
|
||||
};
|
||||
|
||||
const deleteById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TDeleteAuditLogStreamDTO) => {
|
||||
if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" });
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" });
|
||||
|
||||
const logStream = await auditLogStreamDAL.findById(id);
|
||||
if (!logStream) throw new BadRequestError({ message: "Audit log stream not found" });
|
||||
if (!logStream) throw new NotFoundError({ message: "Audit log stream not found" });
|
||||
|
||||
const { orgId } = logStream;
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
@ -193,7 +194,7 @@ export const auditLogStreamServiceFactory = ({
|
||||
|
||||
const getById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TGetDetailsAuditLogStreamDTO) => {
|
||||
const logStream = await auditLogStreamDAL.findById(id);
|
||||
if (!logStream) throw new BadRequestError({ message: "Audit log stream not found" });
|
||||
if (!logStream) throw new NotFoundError({ message: "Audit log stream not found" });
|
||||
|
||||
const { orgId } = logStream;
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
|
@ -3,7 +3,7 @@ import { Knex } from "knex";
|
||||
import { TDbClient } from "@app/db";
|
||||
import { AuditLogsSchema, TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, stripUndefinedInWhere } from "@app/lib/knex";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueName } from "@app/queue";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
@ -48,47 +48,61 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
},
|
||||
tx?: Knex
|
||||
) => {
|
||||
if (!orgId && !projectId) {
|
||||
throw new Error("Either orgId or projectId must be provided");
|
||||
}
|
||||
|
||||
try {
|
||||
// Find statements
|
||||
const sqlQuery = (tx || db.replicaNode())(TableName.AuditLog)
|
||||
.where(
|
||||
stripUndefinedInWhere({
|
||||
projectId,
|
||||
[`${TableName.AuditLog}.orgId`]: orgId,
|
||||
userAgentType
|
||||
})
|
||||
)
|
||||
|
||||
.leftJoin(TableName.Project, `${TableName.AuditLog}.projectId`, `${TableName.Project}.id`)
|
||||
// eslint-disable-next-line func-names
|
||||
.where(function () {
|
||||
if (orgId) {
|
||||
void this.where(`${TableName.Project}.orgId`, orgId).orWhere(`${TableName.AuditLog}.orgId`, orgId);
|
||||
} else if (projectId) {
|
||||
void this.where(`${TableName.AuditLog}.projectId`, projectId);
|
||||
}
|
||||
});
|
||||
|
||||
if (userAgentType) {
|
||||
void sqlQuery.where("userAgentType", userAgentType);
|
||||
}
|
||||
|
||||
// Select statements
|
||||
void sqlQuery
|
||||
.select(selectAllTableCols(TableName.AuditLog))
|
||||
|
||||
.select(
|
||||
db.ref("name").withSchema(TableName.Project).as("projectName"),
|
||||
db.ref("slug").withSchema(TableName.Project).as("projectSlug")
|
||||
)
|
||||
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.orderBy(`${TableName.AuditLog}.createdAt`, "desc");
|
||||
|
||||
// Special case: Filter by actor ID
|
||||
if (actorId) {
|
||||
void sqlQuery.whereRaw(`"actorMetadata"->>'userId' = ?`, [actorId]);
|
||||
}
|
||||
|
||||
// Special case: Filter by key/value pairs in eventMetadata field
|
||||
if (eventMetadata && Object.keys(eventMetadata).length) {
|
||||
Object.entries(eventMetadata).forEach(([key, value]) => {
|
||||
void sqlQuery.whereRaw(`"eventMetadata"->>'${key}' = ?`, [value]);
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by actor type
|
||||
if (actorType) {
|
||||
void sqlQuery.where("actor", actorType);
|
||||
}
|
||||
|
||||
// Filter by event types
|
||||
if (eventType?.length) {
|
||||
void sqlQuery.whereIn("eventType", eventType);
|
||||
}
|
||||
|
||||
// Filter by date range
|
||||
if (startDate) {
|
||||
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, ">=", startDate);
|
||||
}
|
||||
@ -97,13 +111,21 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
const docs = await sqlQuery;
|
||||
|
||||
return docs.map((doc) => ({
|
||||
...AuditLogsSchema.parse(doc),
|
||||
project: {
|
||||
name: doc.projectName,
|
||||
slug: doc.projectSlug
|
||||
}
|
||||
}));
|
||||
return docs.map((doc) => {
|
||||
// Our type system refuses to acknowledge that the project name and slug are present in the doc, due to the disjointed query structure above.
|
||||
// This is a quick and dirty way to get around the types.
|
||||
const projectDoc = doc as unknown as { projectName: string; projectSlug: string };
|
||||
|
||||
return {
|
||||
...AuditLogsSchema.parse(doc),
|
||||
...(projectDoc?.projectSlug && {
|
||||
project: {
|
||||
name: projectDoc.projectName,
|
||||
slug: projectDoc.projectSlug
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error });
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ export const auditLogServiceFactory = ({
|
||||
permissionService
|
||||
}: TAuditLogServiceFactoryDep) => {
|
||||
const listAuditLogs = async ({ actorAuthMethod, actorId, actorOrgId, actor, filter }: TListProjectAuditLogDTO) => {
|
||||
// Filter logs for specific project
|
||||
if (filter.projectId) {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@ -34,6 +35,7 @@ export const auditLogServiceFactory = ({
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||
} else {
|
||||
// Organization-wide logs
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@ -44,13 +46,12 @@ export const auditLogServiceFactory = ({
|
||||
|
||||
/**
|
||||
* NOTE (dangtony98): Update this to organization-level audit log permission check once audit logs are moved
|
||||
* to the organization level
|
||||
* to the organization level ✅
|
||||
*/
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||
}
|
||||
|
||||
// If project ID is not provided, then we need to return all the audit logs for the organization itself.
|
||||
|
||||
const auditLogs = await auditLogDAL.find({
|
||||
startDate: filter.startDate,
|
||||
endDate: filter.endDate,
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||
@ -122,6 +123,7 @@ export enum EventType {
|
||||
UPDATE_WEBHOOK_STATUS = "update-webhook-status",
|
||||
DELETE_WEBHOOK = "delete-webhook",
|
||||
GET_SECRET_IMPORTS = "get-secret-imports",
|
||||
GET_SECRET_IMPORT = "get-secret-import",
|
||||
CREATE_SECRET_IMPORT = "create-secret-import",
|
||||
UPDATE_SECRET_IMPORT = "update-secret-import",
|
||||
DELETE_SECRET_IMPORT = "delete-secret-import",
|
||||
@ -182,7 +184,13 @@ export enum EventType {
|
||||
DELETE_SLACK_INTEGRATION = "delete-slack-integration",
|
||||
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
||||
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
|
||||
INTEGRATION_SYNCED = "integration-synced"
|
||||
INTEGRATION_SYNCED = "integration-synced",
|
||||
CREATE_CMEK = "create-cmek",
|
||||
UPDATE_CMEK = "update-cmek",
|
||||
DELETE_CMEK = "delete-cmek",
|
||||
GET_CMEKS = "get-cmeks",
|
||||
CMEK_ENCRYPT = "cmek-encrypt",
|
||||
CMEK_DECRYPT = "cmek-decrypt"
|
||||
}
|
||||
|
||||
interface UserActorMetadata {
|
||||
@ -1004,6 +1012,14 @@ interface GetSecretImportsEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetSecretImportEvent {
|
||||
type: EventType.GET_SECRET_IMPORT;
|
||||
metadata: {
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateSecretImportEvent {
|
||||
type: EventType.CREATE_SECRET_IMPORT;
|
||||
metadata: {
|
||||
@ -1350,7 +1366,7 @@ interface CreateKmsEvent {
|
||||
metadata: {
|
||||
kmsId: string;
|
||||
provider: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
}
|
||||
@ -1359,7 +1375,7 @@ interface DeleteKmsEvent {
|
||||
type: EventType.DELETE_KMS;
|
||||
metadata: {
|
||||
kmsId: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -1368,7 +1384,7 @@ interface UpdateKmsEvent {
|
||||
metadata: {
|
||||
kmsId: string;
|
||||
provider: string;
|
||||
slug?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
};
|
||||
}
|
||||
@ -1377,7 +1393,7 @@ interface GetKmsEvent {
|
||||
type: EventType.GET_KMS;
|
||||
metadata: {
|
||||
kmsId: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -1386,7 +1402,7 @@ interface UpdateProjectKmsEvent {
|
||||
metadata: {
|
||||
secretManagerKmsKey: {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
@ -1541,6 +1557,53 @@ interface IntegrationSyncedEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateCmekEvent {
|
||||
type: EventType.CREATE_CMEK;
|
||||
metadata: {
|
||||
keyId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
encryptionAlgorithm: SymmetricEncryption;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteCmekEvent {
|
||||
type: EventType.DELETE_CMEK;
|
||||
metadata: {
|
||||
keyId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateCmekEvent {
|
||||
type: EventType.UPDATE_CMEK;
|
||||
metadata: {
|
||||
keyId: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCmeksEvent {
|
||||
type: EventType.GET_CMEKS;
|
||||
metadata: {
|
||||
keyIds: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface CmekEncryptEvent {
|
||||
type: EventType.CMEK_ENCRYPT;
|
||||
metadata: {
|
||||
keyId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CmekDecryptEvent {
|
||||
type: EventType.CMEK_DECRYPT;
|
||||
metadata: {
|
||||
keyId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@ -1620,6 +1683,7 @@ export type Event =
|
||||
| UpdateWebhookStatusEvent
|
||||
| DeleteWebhookEvent
|
||||
| GetSecretImportsEvent
|
||||
| GetSecretImportEvent
|
||||
| CreateSecretImportEvent
|
||||
| UpdateSecretImportEvent
|
||||
| DeleteSecretImportEvent
|
||||
@ -1680,4 +1744,10 @@ export type Event =
|
||||
| GetSlackIntegration
|
||||
| UpdateProjectSlackConfig
|
||||
| GetProjectSlackConfig
|
||||
| IntegrationSyncedEvent;
|
||||
| IntegrationSyncedEvent
|
||||
| CreateCmekEvent
|
||||
| UpdateCmekEvent
|
||||
| DeleteCmekEvent
|
||||
| GetCmeksEvent
|
||||
| CmekEncryptEvent
|
||||
| CmekDecryptEvent;
|
||||
|
@ -2,10 +2,9 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
|
||||
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||
// import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
@ -19,7 +18,6 @@ type TCertificateAuthorityCrlServiceFactoryDep = {
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
// licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
export type TCertificateAuthorityCrlServiceFactory = ReturnType<typeof certificateAuthorityCrlServiceFactory>;
|
||||
@ -66,7 +64,7 @@ export const certificateAuthorityCrlServiceFactory = ({
|
||||
*/
|
||||
const getCaCrls = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCrlsDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||
if (!ca) throw new NotFoundError({ message: "CA not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@ -81,13 +79,6 @@ export const certificateAuthorityCrlServiceFactory = ({
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
// const plan = await licenseService.getPlan(actorOrgId);
|
||||
// if (!plan.caCrl)
|
||||
// throw new BadRequestError({
|
||||
// message:
|
||||
// "Failed to get CA certificate revocation lists (CRLs) due to plan restriction. Upgrade plan to get the CA CRL."
|
||||
// });
|
||||
|
||||
const caCrls = await certificateAuthorityCrlDAL.find({ caId: ca.id }, { sort: [["createdAt", "desc"]] });
|
||||
|
||||
const keyId = await getProjectKmsCertificateKeyId({
|
||||
|
@ -7,7 +7,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
@ -61,7 +61,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
}: TCreateDynamicSecretLeaseDTO) => {
|
||||
const appCfg = getConfig();
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
if (!project) throw new NotFoundError({ message: "Project not found" });
|
||||
|
||||
const projectId = project.id;
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
@ -84,10 +84,10 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
}
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
if (!folder) throw new NotFoundError({ message: "Folder not found" });
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
||||
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
|
||||
if (!dynamicSecretCfg) throw new NotFoundError({ message: "Dynamic secret not found" });
|
||||
|
||||
const totalLeasesTaken = await dynamicSecretLeaseDAL.countLeasesForDynamicSecret(dynamicSecretCfg.id);
|
||||
if (totalLeasesTaken >= appCfg.MAX_LEASE_LIMIT)
|
||||
@ -134,7 +134,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
leaseId
|
||||
}: TRenewDynamicSecretLeaseDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
if (!project) throw new NotFoundError({ message: "Project not found" });
|
||||
|
||||
const projectId = project.id;
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
@ -157,10 +157,10 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
}
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
if (!folder) throw new NotFoundError({ message: "Folder not found" });
|
||||
|
||||
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
|
||||
if (!dynamicSecretLease) throw new BadRequestError({ message: "Dynamic secret lease not found" });
|
||||
if (!dynamicSecretLease) throw new NotFoundError({ message: "Dynamic secret lease not found" });
|
||||
|
||||
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||
@ -208,7 +208,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
isForced
|
||||
}: TDeleteDynamicSecretLeaseDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
if (!project) throw new NotFoundError({ message: "Project not found" });
|
||||
|
||||
const projectId = project.id;
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
@ -224,10 +224,10 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
if (!folder) throw new NotFoundError({ message: "Folder not found" });
|
||||
|
||||
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
|
||||
if (!dynamicSecretLease) throw new BadRequestError({ message: "Dynamic secret lease not found" });
|
||||
if (!dynamicSecretLease) throw new NotFoundError({ message: "Dynamic secret lease not found" });
|
||||
|
||||
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||
@ -273,7 +273,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
actorAuthMethod
|
||||
}: TListDynamicSecretLeasesDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
if (!project) throw new NotFoundError({ message: "Project not found" });
|
||||
|
||||
const projectId = project.id;
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
@ -289,10 +289,10 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
if (!folder) throw new NotFoundError({ message: "Folder not found" });
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
||||
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
|
||||
if (!dynamicSecretCfg) throw new NotFoundError({ message: "Dynamic secret not found" });
|
||||
|
||||
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
|
||||
return dynamicSecretLeases;
|
||||
@ -309,7 +309,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
actorAuthMethod
|
||||
}: TDetailsDynamicSecretLeaseDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
if (!project) throw new NotFoundError({ message: "Project not found" });
|
||||
|
||||
const projectId = project.id;
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
@ -325,10 +325,10 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
if (!folder) throw new NotFoundError({ message: "Folder not found" });
|
||||
|
||||
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
|
||||
if (!dynamicSecretLease) throw new BadRequestError({ message: "Dynamic secret lease not found" });
|
||||
if (!dynamicSecretLease) throw new NotFoundError({ message: "Dynamic secret lease not found" });
|
||||
|
||||
return dynamicSecretLease;
|
||||
};
|
||||
|
@ -1,10 +1,70 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||
|
||||
export type TDynamicSecretDALFactory = ReturnType<typeof dynamicSecretDALFactory>;
|
||||
|
||||
export const dynamicSecretDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.DynamicSecret);
|
||||
return orm;
|
||||
|
||||
// find dynamic secrets for multiple environments (folder IDs are cross env, thus need to rank for pagination)
|
||||
const listDynamicSecretsByFolderIds = async (
|
||||
{
|
||||
folderIds,
|
||||
search,
|
||||
limit,
|
||||
offset = 0,
|
||||
orderBy = SecretsOrderBy.Name,
|
||||
orderDirection = OrderByDirection.ASC
|
||||
}: {
|
||||
folderIds: string[];
|
||||
search?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
orderBy?: SecretsOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
},
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const query = (tx || db.replicaNode())(TableName.DynamicSecret)
|
||||
.whereIn("folderId", folderIds)
|
||||
.where((bd) => {
|
||||
if (search) {
|
||||
void bd.whereILike(`${TableName.DynamicSecret}.name`, `%${search}%`);
|
||||
}
|
||||
})
|
||||
.leftJoin(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`)
|
||||
.leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||
.select(
|
||||
selectAllTableCols(TableName.DynamicSecret),
|
||||
db.ref("slug").withSchema(TableName.Environment).as("environment"),
|
||||
db.raw(`DENSE_RANK() OVER (ORDER BY ${TableName.DynamicSecret}."name" ${orderDirection}) as rank`)
|
||||
)
|
||||
.orderBy(`${TableName.DynamicSecret}.${orderBy}`, orderDirection);
|
||||
|
||||
if (limit) {
|
||||
const rankOffset = offset + 1;
|
||||
return await (tx || db)
|
||||
.with("w", query)
|
||||
.select("*")
|
||||
.from<Awaited<typeof query>[number]>("w")
|
||||
.where("w.rank", ">=", rankOffset)
|
||||
.andWhere("w.rank", "<", rankOffset + limit);
|
||||
}
|
||||
|
||||
const dynamicSecrets = await query;
|
||||
|
||||
return dynamicSecrets;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "List dynamic secret multi env" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...orm, listDynamicSecretsByFolderIds };
|
||||
};
|
||||
|
@ -5,7 +5,8 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
|
||||
@ -17,9 +18,12 @@ import {
|
||||
TCreateDynamicSecretDTO,
|
||||
TDeleteDynamicSecretDTO,
|
||||
TDetailsDynamicSecretDTO,
|
||||
TGetDynamicSecretsCountDTO,
|
||||
TListDynamicSecretsDTO,
|
||||
TListDynamicSecretsMultiEnvDTO,
|
||||
TUpdateDynamicSecretDTO
|
||||
} from "./dynamic-secret-types";
|
||||
import { AzureEntraIDProvider } from "./providers/azure-entra-id";
|
||||
import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models";
|
||||
|
||||
type TDynamicSecretServiceFactoryDep = {
|
||||
@ -31,7 +35,7 @@ type TDynamicSecretServiceFactoryDep = {
|
||||
"pruneDynamicSecret" | "unsetLeaseRevocation"
|
||||
>;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
@ -62,7 +66,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
actorAuthMethod
|
||||
}: TCreateDynamicSecretDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
if (!project) throw new NotFoundError({ message: "Project not found" });
|
||||
|
||||
const projectId = project.id;
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
@ -85,7 +89,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
}
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
if (!folder) throw new NotFoundError({ message: "Folder not found" });
|
||||
|
||||
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
||||
if (existingDynamicSecret)
|
||||
@ -130,7 +134,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
actorAuthMethod
|
||||
}: TUpdateDynamicSecretDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
if (!project) throw new NotFoundError({ message: "Project not found" });
|
||||
|
||||
const projectId = project.id;
|
||||
|
||||
@ -154,10 +158,10 @@ export const dynamicSecretServiceFactory = ({
|
||||
}
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
if (!folder) throw new NotFoundError({ message: "Folder not found" });
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
||||
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
|
||||
if (!dynamicSecretCfg) throw new NotFoundError({ message: "Dynamic secret not found" });
|
||||
|
||||
if (newName) {
|
||||
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
|
||||
@ -209,7 +213,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
isForced
|
||||
}: TDeleteDynamicSecretDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
if (!project) throw new NotFoundError({ message: "Project not found" });
|
||||
|
||||
const projectId = project.id;
|
||||
|
||||
@ -226,7 +230,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
if (!folder) throw new NotFoundError({ message: "Folder not found" });
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
||||
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
|
||||
@ -267,7 +271,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
actor
|
||||
}: TDetailsDynamicSecretDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
if (!project) throw new NotFoundError({ message: "Project not found" });
|
||||
|
||||
const projectId = project.id;
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
@ -283,10 +287,10 @@ export const dynamicSecretServiceFactory = ({
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
if (!folder) throw new NotFoundError({ message: "Folder not found" });
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
||||
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
|
||||
if (!dynamicSecretCfg) throw new NotFoundError({ message: "Dynamic secret not found" });
|
||||
const decryptedStoredInput = JSON.parse(
|
||||
infisicalSymmetricDecrypt({
|
||||
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||
@ -300,19 +304,58 @@ export const dynamicSecretServiceFactory = ({
|
||||
return { ...dynamicSecretCfg, inputs: providerInputs };
|
||||
};
|
||||
|
||||
const list = async ({
|
||||
// get unique dynamic secret count across multiple envs
|
||||
const getCountMultiEnv = async ({
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actorId,
|
||||
actor,
|
||||
projectSlug,
|
||||
projectId,
|
||||
path,
|
||||
environmentSlug
|
||||
}: TListDynamicSecretsDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
environmentSlugs,
|
||||
search,
|
||||
isInternal
|
||||
}: TListDynamicSecretsMultiEnvDTO) => {
|
||||
if (!isInternal) {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
const projectId = project.id;
|
||||
// verify user has access to each env in request
|
||||
environmentSlugs.forEach((environmentSlug) =>
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
|
||||
if (!folders.length) throw new NotFoundError({ message: "Folders not found" });
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.find(
|
||||
{ $in: { folderId: folders.map((folder) => folder.id) }, $search: search ? { name: `%${search}%` } : undefined },
|
||||
{ countDistinct: "name" }
|
||||
);
|
||||
|
||||
return Number(dynamicSecretCfg[0]?.count ?? 0);
|
||||
};
|
||||
|
||||
// get dynamic secret count for a single env
|
||||
const getDynamicSecretCount = async ({
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actorId,
|
||||
actor,
|
||||
path,
|
||||
environmentSlug,
|
||||
search,
|
||||
projectId
|
||||
}: TGetDynamicSecretsCountDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@ -326,17 +369,132 @@ export const dynamicSecretServiceFactory = ({
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
if (!folder) throw new NotFoundError({ message: "Folder not found" });
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.find({ folderId: folder.id });
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.find(
|
||||
{ folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined },
|
||||
{ count: true }
|
||||
);
|
||||
return Number(dynamicSecretCfg[0]?.count ?? 0);
|
||||
};
|
||||
|
||||
const listDynamicSecretsByEnv = async ({
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actorId,
|
||||
actor,
|
||||
projectSlug,
|
||||
path,
|
||||
environmentSlug,
|
||||
limit,
|
||||
offset,
|
||||
orderBy,
|
||||
orderDirection = OrderByDirection.ASC,
|
||||
search,
|
||||
...params
|
||||
}: TListDynamicSecretsDTO) => {
|
||||
let { projectId } = params;
|
||||
|
||||
if (!projectId) {
|
||||
if (!projectSlug) throw new BadRequestError({ message: "Project ID or slug required" });
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new NotFoundError({ message: "Project not found" });
|
||||
projectId = project.id;
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder) throw new NotFoundError({ message: "Folder not found" });
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.find(
|
||||
{ folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined },
|
||||
{
|
||||
limit,
|
||||
offset,
|
||||
sort: orderBy ? [[orderBy, orderDirection]] : undefined
|
||||
}
|
||||
);
|
||||
return dynamicSecretCfg;
|
||||
};
|
||||
|
||||
// get dynamic secrets for multiple envs
|
||||
const listDynamicSecretsByFolderIds = async ({
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actorId,
|
||||
actor,
|
||||
path,
|
||||
environmentSlugs,
|
||||
projectId,
|
||||
isInternal,
|
||||
...params
|
||||
}: TListDynamicSecretsMultiEnvDTO) => {
|
||||
if (!isInternal) {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
// verify user has access to each env in request
|
||||
environmentSlugs.forEach((environmentSlug) =>
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
|
||||
if (!folders.length) throw new NotFoundError({ message: "Folders not found" });
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.listDynamicSecretsByFolderIds({
|
||||
folderIds: folders.map((folder) => folder.id),
|
||||
...params
|
||||
});
|
||||
|
||||
return dynamicSecretCfg;
|
||||
};
|
||||
|
||||
const fetchAzureEntraIdUsers = async ({
|
||||
tenantId,
|
||||
applicationId,
|
||||
clientSecret
|
||||
}: {
|
||||
tenantId: string;
|
||||
applicationId: string;
|
||||
clientSecret: string;
|
||||
}) => {
|
||||
const azureEntraIdUsers = await AzureEntraIDProvider().fetchAzureEntraIdUsers(
|
||||
tenantId,
|
||||
applicationId,
|
||||
clientSecret
|
||||
);
|
||||
return azureEntraIdUsers;
|
||||
};
|
||||
|
||||
return {
|
||||
create,
|
||||
updateByName,
|
||||
deleteByName,
|
||||
getDetails,
|
||||
list
|
||||
listDynamicSecretsByEnv,
|
||||
listDynamicSecretsByFolderIds,
|
||||
getDynamicSecretCount,
|
||||
getCountMultiEnv,
|
||||
fetchAzureEntraIdUsers
|
||||
};
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||
|
||||
import { DynamicSecretProviderSchema } from "./providers/models";
|
||||
|
||||
@ -50,5 +51,20 @@ export type TDetailsDynamicSecretDTO = {
|
||||
export type TListDynamicSecretsDTO = {
|
||||
path: string;
|
||||
environmentSlug: string;
|
||||
projectSlug: string;
|
||||
projectSlug?: string;
|
||||
projectId?: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
orderBy?: SecretsOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
search?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListDynamicSecretsMultiEnvDTO = Omit<
|
||||
TListDynamicSecretsDTO,
|
||||
"projectId" | "environmentSlug" | "projectSlug"
|
||||
> & { projectId: string; environmentSlugs: string[]; isInternal?: boolean };
|
||||
|
||||
export type TGetDynamicSecretsCountDTO = Omit<TListDynamicSecretsDTO, "projectSlug" | "projectId"> & {
|
||||
projectId: string;
|
||||
};
|
||||
|
@ -0,0 +1,138 @@
|
||||
import axios from "axios";
|
||||
import { customAlphabet } from "nanoid";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { AzureEntraIDSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
const MSFT_GRAPH_API_URL = "https://graph.microsoft.com/v1.0/";
|
||||
const MSFT_LOGIN_URL = "https://login.microsoftonline.com";
|
||||
|
||||
const generatePassword = () => {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||
return customAlphabet(charset, 64)();
|
||||
};
|
||||
|
||||
type User = { name: string; id: string; email: string };
|
||||
|
||||
export const AzureEntraIDProvider = (): TDynamicProviderFns & {
|
||||
fetchAzureEntraIdUsers: (tenantId: string, applicationId: string, clientSecret: string) => Promise<User[]>;
|
||||
} => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await AzureEntraIDSchema.parseAsync(inputs);
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getToken = async (
|
||||
tenantId: string,
|
||||
applicationId: string,
|
||||
clientSecret: string
|
||||
): Promise<{ token?: string; success: boolean }> => {
|
||||
const response = await axios.post<{ access_token: string }>(
|
||||
`${MSFT_LOGIN_URL}/${tenantId}/oauth2/v2.0/token`,
|
||||
{
|
||||
grant_type: "client_credentials",
|
||||
client_id: applicationId,
|
||||
client_secret: clientSecret,
|
||||
scope: "https://graph.microsoft.com/.default"
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 200) {
|
||||
return { token: response.data.access_token, success: true };
|
||||
}
|
||||
return { success: false };
|
||||
};
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||
return data.success;
|
||||
};
|
||||
|
||||
const renew = async (inputs: unknown, entityId: string) => {
|
||||
// Do nothing
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||
if (!data.success) {
|
||||
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
|
||||
}
|
||||
|
||||
const password = generatePassword();
|
||||
|
||||
const response = await axios.patch(
|
||||
`${MSFT_GRAPH_API_URL}/users/${providerInputs.userId}`,
|
||||
{
|
||||
passwordProfile: {
|
||||
forceChangePasswordNextSignIn: false,
|
||||
password
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${data.token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
if (response.status !== 204) {
|
||||
throw new BadRequestError({ message: "Failed to update password" });
|
||||
}
|
||||
|
||||
return { entityId: providerInputs.userId, data: { email: providerInputs.email, password } };
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
// Creates a new password
|
||||
await create(inputs);
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
const fetchAzureEntraIdUsers = async (tenantId: string, applicationId: string, clientSecret: string) => {
|
||||
const data = await getToken(tenantId, applicationId, clientSecret);
|
||||
if (!data.success) {
|
||||
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
|
||||
}
|
||||
|
||||
const response = await axios.get<{ value: [{ id: string; displayName: string; userPrincipalName: string }] }>(
|
||||
`${MSFT_GRAPH_API_URL}/users`,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Authorization: `Bearer ${data.token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new BadRequestError({ message: "Failed to fetch users" });
|
||||
}
|
||||
|
||||
const users = response.data.value.map((user) => {
|
||||
return {
|
||||
name: user.displayName,
|
||||
id: user.id,
|
||||
email: user.userPrincipalName
|
||||
};
|
||||
});
|
||||
return users;
|
||||
};
|
||||
|
||||
return {
|
||||
validateProviderInputs,
|
||||
validateConnection,
|
||||
create,
|
||||
revoke,
|
||||
renew,
|
||||
fetchAzureEntraIdUsers
|
||||
};
|
||||
};
|
@ -1,7 +1,9 @@
|
||||
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
||||
import { AwsIamProvider } from "./aws-iam";
|
||||
import { AzureEntraIDProvider } from "./azure-entra-id";
|
||||
import { CassandraProvider } from "./cassandra";
|
||||
import { ElasticSearchProvider } from "./elastic-search";
|
||||
import { LdapProvider } from "./ldap";
|
||||
import { DynamicSecretProviders } from "./models";
|
||||
import { MongoAtlasProvider } from "./mongo-atlas";
|
||||
import { MongoDBProvider } from "./mongo-db";
|
||||
@ -18,5 +20,7 @@ export const buildDynamicSecretProviders = () => ({
|
||||
[DynamicSecretProviders.MongoAtlas]: MongoAtlasProvider(),
|
||||
[DynamicSecretProviders.MongoDB]: MongoDBProvider(),
|
||||
[DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(),
|
||||
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider()
|
||||
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider(),
|
||||
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(),
|
||||
[DynamicSecretProviders.Ldap]: LdapProvider()
|
||||
});
|
||||
|
235
backend/src/ee/services/dynamic-secret/providers/ldap.ts
Normal file
235
backend/src/ee/services/dynamic-secret/providers/ldap.ts
Normal file
@ -0,0 +1,235 @@
|
||||
import handlebars from "handlebars";
|
||||
import ldapjs from "ldapjs";
|
||||
import ldif from "ldif";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { LdapSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
const generatePassword = () => {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||
return customAlphabet(charset, 64)();
|
||||
};
|
||||
|
||||
const encodePassword = (password?: string) => {
|
||||
const quotedPassword = `"${password}"`;
|
||||
const utf16lePassword = Buffer.from(quotedPassword, "utf16le");
|
||||
const base64Password = utf16lePassword.toString("base64");
|
||||
return base64Password;
|
||||
};
|
||||
|
||||
const generateUsername = () => {
|
||||
return alphaNumericNanoId(20);
|
||||
};
|
||||
|
||||
const generateLDIF = ({
|
||||
username,
|
||||
password,
|
||||
ldifTemplate
|
||||
}: {
|
||||
username: string;
|
||||
password?: string;
|
||||
ldifTemplate: string;
|
||||
}): string => {
|
||||
const data = {
|
||||
Username: username,
|
||||
Password: password,
|
||||
EncodedPassword: encodePassword(password)
|
||||
};
|
||||
|
||||
const renderTemplate = handlebars.compile(ldifTemplate);
|
||||
const renderedLdif = renderTemplate(data);
|
||||
|
||||
return renderedLdif;
|
||||
};
|
||||
|
||||
export const LdapProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await LdapSchema.parseAsync(inputs);
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getClient = async (providerInputs: z.infer<typeof LdapSchema>): Promise<ldapjs.Client> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = ldapjs.createClient({
|
||||
url: providerInputs.url,
|
||||
tlsOptions: {
|
||||
ca: providerInputs.ca ? providerInputs.ca : null,
|
||||
rejectUnauthorized: !!providerInputs.ca
|
||||
},
|
||||
reconnect: true,
|
||||
bindDN: providerInputs.binddn,
|
||||
bindCredentials: providerInputs.bindpass
|
||||
});
|
||||
|
||||
client.on("error", (err: Error) => {
|
||||
client.unbind();
|
||||
reject(new BadRequestError({ message: err.message }));
|
||||
});
|
||||
|
||||
client.bind(providerInputs.binddn, providerInputs.bindpass, (err) => {
|
||||
if (err) {
|
||||
client.unbind();
|
||||
reject(new BadRequestError({ message: err.message }));
|
||||
} else {
|
||||
resolve(client);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
return client.connected;
|
||||
};
|
||||
|
||||
const executeLdif = async (client: ldapjs.Client, ldif_file: string) => {
|
||||
type TEntry = {
|
||||
dn: string;
|
||||
type: string;
|
||||
|
||||
changes: {
|
||||
operation?: string;
|
||||
attribute: {
|
||||
attribute: string;
|
||||
};
|
||||
value: {
|
||||
value: string;
|
||||
};
|
||||
values: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Untyped, can be any for ldapjs.Change.modification.values
|
||||
value: any;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
|
||||
let parsedEntries: TEntry[];
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
parsedEntries = ldif.parse(ldif_file).entries as TEntry[];
|
||||
} catch (err) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid LDIF format, refer to the documentation at Dynamic secrets > LDAP > LDIF Entries."
|
||||
});
|
||||
}
|
||||
|
||||
const dnArray: string[] = [];
|
||||
|
||||
for await (const entry of parsedEntries) {
|
||||
const { dn } = entry;
|
||||
let responseDn: string;
|
||||
|
||||
if (entry.type === "add") {
|
||||
const attributes: Record<string, string | string[]> = {};
|
||||
|
||||
entry.changes.forEach((change) => {
|
||||
const attrName = change.attribute.attribute;
|
||||
const attrValue = change.value.value;
|
||||
|
||||
attributes[attrName] = Array.isArray(attrValue) ? attrValue : [attrValue];
|
||||
});
|
||||
|
||||
responseDn = await new Promise((resolve, reject) => {
|
||||
client.add(dn, attributes, (err) => {
|
||||
if (err) {
|
||||
reject(new BadRequestError({ message: err.message }));
|
||||
} else {
|
||||
resolve(dn);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (entry.type === "modify") {
|
||||
const changes: ldapjs.Change[] = [];
|
||||
|
||||
entry.changes.forEach((change) => {
|
||||
changes.push(
|
||||
new ldapjs.Change({
|
||||
operation: change.operation || "replace",
|
||||
modification: {
|
||||
type: change.attribute.attribute,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
values: change.values.map((value) => value.value)
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
responseDn = await new Promise((resolve, reject) => {
|
||||
client.modify(dn, changes, (err) => {
|
||||
if (err) {
|
||||
reject(new BadRequestError({ message: err.message }));
|
||||
} else {
|
||||
resolve(dn);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (entry.type === "delete") {
|
||||
responseDn = await new Promise((resolve, reject) => {
|
||||
client.del(dn, (err) => {
|
||||
if (err) {
|
||||
reject(new BadRequestError({ message: err.message }));
|
||||
} else {
|
||||
resolve(dn);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
client.unbind();
|
||||
throw new BadRequestError({ message: `Unsupported operation type ${entry.type}` });
|
||||
}
|
||||
|
||||
dnArray.push(responseDn);
|
||||
}
|
||||
client.unbind();
|
||||
return dnArray;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const password = generatePassword();
|
||||
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.creationLdif });
|
||||
|
||||
try {
|
||||
const dnArray = await executeLdif(client, generatedLdif);
|
||||
|
||||
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
|
||||
} catch (err) {
|
||||
if (providerInputs.rollbackLdif) {
|
||||
const rollbackLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rollbackLdif });
|
||||
await executeLdif(client, rollbackLdif);
|
||||
}
|
||||
throw new BadRequestError({ message: (err as Error).message });
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await getClient(providerInputs);
|
||||
const revocationLdif = generateLDIF({ username: entityId, ldifTemplate: providerInputs.revocationLdif });
|
||||
|
||||
await executeLdif(connection, revocationLdif);
|
||||
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
const renew = async (inputs: unknown, entityId: string) => {
|
||||
// Do nothing
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
return {
|
||||
validateProviderInputs,
|
||||
validateConnection,
|
||||
create,
|
||||
revoke,
|
||||
renew
|
||||
};
|
||||
};
|
@ -166,6 +166,25 @@ export const DynamicSecretMongoDBSchema = z.object({
|
||||
)
|
||||
});
|
||||
|
||||
export const AzureEntraIDSchema = z.object({
|
||||
tenantId: z.string().trim().min(1),
|
||||
userId: z.string().trim().min(1),
|
||||
email: z.string().trim().min(1),
|
||||
applicationId: z.string().trim().min(1),
|
||||
clientSecret: z.string().trim().min(1)
|
||||
});
|
||||
|
||||
export const LdapSchema = z.object({
|
||||
url: z.string().trim().min(1),
|
||||
binddn: z.string().trim().min(1),
|
||||
bindpass: z.string().trim().min(1),
|
||||
ca: z.string().optional(),
|
||||
|
||||
creationLdif: z.string().min(1),
|
||||
revocationLdif: z.string().min(1),
|
||||
rollbackLdif: z.string().optional()
|
||||
});
|
||||
|
||||
export enum DynamicSecretProviders {
|
||||
SqlDatabase = "sql-database",
|
||||
Cassandra = "cassandra",
|
||||
@ -175,7 +194,9 @@ export enum DynamicSecretProviders {
|
||||
MongoAtlas = "mongo-db-atlas",
|
||||
ElasticSearch = "elastic-search",
|
||||
MongoDB = "mongo-db",
|
||||
RabbitMq = "rabbit-mq"
|
||||
RabbitMq = "rabbit-mq",
|
||||
AzureEntraID = "azure-entra-id",
|
||||
Ldap = "ldap"
|
||||
}
|
||||
|
||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||
@ -187,7 +208,9 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(DynamicSecretProviders.MongoAtlas), inputs: DynamicSecretMongoAtlasSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema })
|
||||
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema })
|
||||
]);
|
||||
|
||||
export type TDynamicProviderFns = {
|
||||
|
@ -30,7 +30,7 @@ export const externalKmsDALFactory = (db: TDbClient) => {
|
||||
isDisabled: el.isDisabled,
|
||||
isReserved: el.isReserved,
|
||||
orgId: el.orgId,
|
||||
slug: el.slug,
|
||||
name: el.name,
|
||||
createdAt: el.createdAt,
|
||||
updatedAt: el.updatedAt,
|
||||
externalKms: {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
@ -43,7 +43,7 @@ export const externalKmsServiceFactory = ({
|
||||
provider,
|
||||
description,
|
||||
actor,
|
||||
slug,
|
||||
name,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
@ -64,7 +64,7 @@ export const externalKmsServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const kmsSlug = slug ? slugify(slug) : slugify(alphaNumericNanoId(8).toLowerCase());
|
||||
const kmsName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase());
|
||||
|
||||
let sanitizedProviderInput = "";
|
||||
switch (provider.type) {
|
||||
@ -96,7 +96,7 @@ export const externalKmsServiceFactory = ({
|
||||
{
|
||||
isReserved: false,
|
||||
description,
|
||||
slug: kmsSlug,
|
||||
name: kmsName,
|
||||
orgId: actorOrgId
|
||||
},
|
||||
tx
|
||||
@ -120,7 +120,7 @@ export const externalKmsServiceFactory = ({
|
||||
description,
|
||||
actor,
|
||||
id: kmsId,
|
||||
slug,
|
||||
name,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
@ -142,10 +142,10 @@ export const externalKmsServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const kmsSlug = slug ? slugify(slug) : undefined;
|
||||
const kmsName = name ? slugify(name) : undefined;
|
||||
|
||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
||||
if (!externalKmsDoc) throw new NotFoundError({ message: "External kms not found" });
|
||||
|
||||
let sanitizedProviderInput = "";
|
||||
const { encryptor: orgDataKeyEncryptor, decryptor: orgDataKeyDecryptor } =
|
||||
@ -188,7 +188,7 @@ export const externalKmsServiceFactory = ({
|
||||
kmsDoc.id,
|
||||
{
|
||||
description,
|
||||
slug: kmsSlug
|
||||
name: kmsName
|
||||
},
|
||||
tx
|
||||
);
|
||||
@ -220,7 +220,7 @@ export const externalKmsServiceFactory = ({
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
||||
|
||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
||||
if (!externalKmsDoc) throw new NotFoundError({ message: "External kms not found" });
|
||||
|
||||
const externalKms = await externalKmsDAL.transaction(async (tx) => {
|
||||
const kms = await kmsDAL.deleteById(kmsDoc.id, tx);
|
||||
@ -258,7 +258,7 @@ export const externalKmsServiceFactory = ({
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
|
||||
|
||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
||||
if (!externalKmsDoc) throw new NotFoundError({ message: "External kms not found" });
|
||||
|
||||
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
@ -280,14 +280,14 @@ export const externalKmsServiceFactory = ({
|
||||
}
|
||||
};
|
||||
|
||||
const findBySlug = async ({
|
||||
const findByName = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
slug: kmsSlug
|
||||
name: kmsName
|
||||
}: TGetExternalKmsBySlugDTO) => {
|
||||
const kmsDoc = await kmsDAL.findOne({ slug: kmsSlug, orgId: actorOrgId });
|
||||
const kmsDoc = await kmsDAL.findOne({ name: kmsName, orgId: actorOrgId });
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@ -298,7 +298,7 @@ export const externalKmsServiceFactory = ({
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
|
||||
|
||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
||||
if (!externalKmsDoc) throw new NotFoundError({ message: "External kms not found" });
|
||||
|
||||
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
@ -327,6 +327,6 @@ export const externalKmsServiceFactory = ({
|
||||
deleteById,
|
||||
list,
|
||||
findById,
|
||||
findBySlug
|
||||
findByName
|
||||
};
|
||||
};
|
||||
|
@ -3,14 +3,14 @@ import { TOrgPermission } from "@app/lib/types";
|
||||
import { TExternalKmsInputSchema, TExternalKmsInputUpdateSchema } from "./providers/model";
|
||||
|
||||
export type TCreateExternalKmsDTO = {
|
||||
slug?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
provider: TExternalKmsInputSchema;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TUpdateExternalKmsDTO = {
|
||||
id: string;
|
||||
slug?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
provider?: TExternalKmsInputUpdateSchema;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
@ -26,5 +26,5 @@ export type TGetExternalKmsByIdDTO = {
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TGetExternalKmsBySlugDTO = {
|
||||
slug: string;
|
||||
name: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
@ -60,7 +60,7 @@ export const groupDALFactory = (db: TDbClient) => {
|
||||
};
|
||||
|
||||
// special query
|
||||
const findAllGroupMembers = async ({
|
||||
const findAllGroupPossibleMembers = async ({
|
||||
orgId,
|
||||
groupId,
|
||||
offset = 0,
|
||||
@ -125,7 +125,7 @@ export const groupDALFactory = (db: TDbClient) => {
|
||||
return {
|
||||
findGroups,
|
||||
findByOrgId,
|
||||
findAllGroupMembers,
|
||||
findAllGroupPossibleMembers,
|
||||
...groupOrm
|
||||
};
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ import { Knex } from "knex";
|
||||
|
||||
import { SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
|
||||
import { decryptAsymmetric, encryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError, ScimRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, ScimRequestError } from "@app/lib/errors";
|
||||
|
||||
import {
|
||||
TAddUsersToGroup,
|
||||
@ -73,24 +73,24 @@ const addAcceptedUsersToGroup = async ({
|
||||
const ghostUser = await projectDAL.findProjectGhostUser(projectId, tx);
|
||||
|
||||
if (!ghostUser) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find sudo user"
|
||||
throw new NotFoundError({
|
||||
message: "Failed to find project owner"
|
||||
});
|
||||
}
|
||||
|
||||
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId, tx);
|
||||
|
||||
if (!ghostUserLatestKey) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find sudo user latest key"
|
||||
throw new NotFoundError({
|
||||
message: "Failed to find project owner's latest key"
|
||||
});
|
||||
}
|
||||
|
||||
const bot = await projectBotDAL.findOne({ projectId }, tx);
|
||||
|
||||
if (!bot) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find bot"
|
||||
throw new NotFoundError({
|
||||
message: "Failed to find project bot"
|
||||
});
|
||||
}
|
||||
|
||||
@ -200,7 +200,7 @@ export const addUsersToGroupByUserIds = async ({
|
||||
|
||||
userIds.forEach((userId) => {
|
||||
if (!existingUserOrgMembershipsUserIdsSet.has(userId))
|
||||
throw new BadRequestError({
|
||||
throw new ForbiddenRequestError({
|
||||
message: `User with id ${userId} is not part of the organization`
|
||||
});
|
||||
});
|
||||
@ -303,7 +303,7 @@ export const removeUsersFromGroupByUserIds = async ({
|
||||
|
||||
userIds.forEach((userId) => {
|
||||
if (!existingUserGroupMembershipsUserIdsSet.has(userId))
|
||||
throw new BadRequestError({
|
||||
throw new ForbiddenRequestError({
|
||||
message: `User(s) are not part of the group ${group.slug}`
|
||||
});
|
||||
});
|
||||
@ -415,7 +415,7 @@ export const convertPendingGroupAdditionsToGroupMemberships = async ({
|
||||
const usersUserIdsSet = new Set(users.map((u) => u.id));
|
||||
userIds.forEach((userId) => {
|
||||
if (!usersUserIdsSet.has(userId)) {
|
||||
throw new BadRequestError({
|
||||
throw new NotFoundError({
|
||||
message: `Failed to find user with id ${userId}`
|
||||
});
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import slugify from "@sindresorhus/slugify";
|
||||
|
||||
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
|
||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
@ -21,6 +21,7 @@ import {
|
||||
TAddUserToGroupDTO,
|
||||
TCreateGroupDTO,
|
||||
TDeleteGroupDTO,
|
||||
TGetGroupByIdDTO,
|
||||
TListGroupUsersDTO,
|
||||
TRemoveUserFromGroupDTO,
|
||||
TUpdateGroupDTO
|
||||
@ -29,7 +30,10 @@ import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal";
|
||||
|
||||
type TGroupServiceFactoryDep = {
|
||||
userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findOne">;
|
||||
groupDAL: Pick<TGroupDALFactory, "create" | "findOne" | "update" | "delete" | "findAllGroupMembers">;
|
||||
groupDAL: Pick<
|
||||
TGroupDALFactory,
|
||||
"create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById"
|
||||
>;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findMembership" | "countAllOrgMembers">;
|
||||
userGroupMembershipDAL: Pick<
|
||||
@ -58,7 +62,7 @@ export const groupServiceFactory = ({
|
||||
licenseService
|
||||
}: TGroupServiceFactoryDep) => {
|
||||
const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => {
|
||||
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
@ -81,7 +85,8 @@ export const groupServiceFactory = ({
|
||||
);
|
||||
const isCustomRole = Boolean(customRole);
|
||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
|
||||
if (!hasRequiredPriviledges) throw new BadRequestError({ message: "Failed to create a more privileged group" });
|
||||
if (!hasRequiredPriviledges)
|
||||
throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" });
|
||||
|
||||
const group = await groupDAL.create({
|
||||
name,
|
||||
@ -95,7 +100,7 @@ export const groupServiceFactory = ({
|
||||
};
|
||||
|
||||
const updateGroup = async ({
|
||||
currentSlug,
|
||||
id,
|
||||
name,
|
||||
slug,
|
||||
role,
|
||||
@ -104,7 +109,7 @@ export const groupServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TUpdateGroupDTO) => {
|
||||
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
@ -121,8 +126,10 @@ export const groupServiceFactory = ({
|
||||
message: "Failed to update group due to plan restrictio Upgrade plan to update group."
|
||||
});
|
||||
|
||||
const group = await groupDAL.findOne({ orgId: actorOrgId, slug: currentSlug });
|
||||
if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${currentSlug}` });
|
||||
const group = await groupDAL.findOne({ orgId: actorOrgId, id });
|
||||
if (!group) {
|
||||
throw new NotFoundError({ message: `Failed to find group with ID ${id}` });
|
||||
}
|
||||
|
||||
let customRole: TOrgRoles | undefined;
|
||||
if (role) {
|
||||
@ -134,14 +141,13 @@ export const groupServiceFactory = ({
|
||||
const isCustomRole = Boolean(customOrgRole);
|
||||
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission);
|
||||
if (!hasRequiredNewRolePermission)
|
||||
throw new BadRequestError({ message: "Failed to create a more privileged group" });
|
||||
throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" });
|
||||
if (isCustomRole) customRole = customOrgRole;
|
||||
}
|
||||
|
||||
const [updatedGroup] = await groupDAL.update(
|
||||
{
|
||||
orgId: actorOrgId,
|
||||
slug: currentSlug
|
||||
id: group.id
|
||||
},
|
||||
{
|
||||
name,
|
||||
@ -158,8 +164,8 @@ export const groupServiceFactory = ({
|
||||
return updatedGroup;
|
||||
};
|
||||
|
||||
const deleteGroup = async ({ groupSlug, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => {
|
||||
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
||||
const deleteGroup = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
@ -178,15 +184,37 @@ export const groupServiceFactory = ({
|
||||
});
|
||||
|
||||
const [group] = await groupDAL.delete({
|
||||
orgId: actorOrgId,
|
||||
slug: groupSlug
|
||||
id,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
return group;
|
||||
};
|
||||
|
||||
const getGroupById = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TGetGroupByIdDTO) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
||||
|
||||
const group = await groupDAL.findById(id);
|
||||
if (!group) {
|
||||
throw new NotFoundError({
|
||||
message: `Cannot find group with ID ${id}`
|
||||
});
|
||||
}
|
||||
|
||||
return group;
|
||||
};
|
||||
|
||||
const listGroupUsers = async ({
|
||||
groupSlug,
|
||||
id,
|
||||
offset,
|
||||
limit,
|
||||
username,
|
||||
@ -195,7 +223,7 @@ export const groupServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TListGroupUsersDTO) => {
|
||||
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
@ -208,15 +236,15 @@ export const groupServiceFactory = ({
|
||||
|
||||
const group = await groupDAL.findOne({
|
||||
orgId: actorOrgId,
|
||||
slug: groupSlug
|
||||
id
|
||||
});
|
||||
|
||||
if (!group)
|
||||
throw new BadRequestError({
|
||||
message: `Failed to find group with slug ${groupSlug}`
|
||||
throw new NotFoundError({
|
||||
message: `Failed to find group with ID ${id}`
|
||||
});
|
||||
|
||||
const users = await groupDAL.findAllGroupMembers({
|
||||
const users = await groupDAL.findAllGroupPossibleMembers({
|
||||
orgId: group.orgId,
|
||||
groupId: group.id,
|
||||
offset,
|
||||
@ -229,15 +257,8 @@ export const groupServiceFactory = ({
|
||||
return { users, totalCount: count };
|
||||
};
|
||||
|
||||
const addUserToGroup = async ({
|
||||
groupSlug,
|
||||
username,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TAddUserToGroupDTO) => {
|
||||
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
||||
const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
@ -251,12 +272,12 @@ export const groupServiceFactory = ({
|
||||
// check if group with slug exists
|
||||
const group = await groupDAL.findOne({
|
||||
orgId: actorOrgId,
|
||||
slug: groupSlug
|
||||
id
|
||||
});
|
||||
|
||||
if (!group)
|
||||
throw new BadRequestError({
|
||||
message: `Failed to find group with slug ${groupSlug}`
|
||||
throw new NotFoundError({
|
||||
message: `Failed to find group with ID ${id}`
|
||||
});
|
||||
|
||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||
@ -267,7 +288,7 @@ export const groupServiceFactory = ({
|
||||
throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" });
|
||||
|
||||
const user = await userDAL.findOne({ username });
|
||||
if (!user) throw new BadRequestError({ message: `Failed to find user with username ${username}` });
|
||||
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
|
||||
|
||||
const users = await addUsersToGroupByUserIds({
|
||||
group,
|
||||
@ -285,14 +306,14 @@ export const groupServiceFactory = ({
|
||||
};
|
||||
|
||||
const removeUserFromGroup = async ({
|
||||
groupSlug,
|
||||
id,
|
||||
username,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TRemoveUserFromGroupDTO) => {
|
||||
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
@ -306,12 +327,12 @@ export const groupServiceFactory = ({
|
||||
// check if group with slug exists
|
||||
const group = await groupDAL.findOne({
|
||||
orgId: actorOrgId,
|
||||
slug: groupSlug
|
||||
id
|
||||
});
|
||||
|
||||
if (!group)
|
||||
throw new BadRequestError({
|
||||
message: `Failed to find group with slug ${groupSlug}`
|
||||
throw new NotFoundError({
|
||||
message: `Failed to find group with ID ${id}`
|
||||
});
|
||||
|
||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||
@ -322,7 +343,7 @@ export const groupServiceFactory = ({
|
||||
throw new ForbiddenRequestError({ message: "Failed to delete user from more privileged group" });
|
||||
|
||||
const user = await userDAL.findOne({ username });
|
||||
if (!user) throw new BadRequestError({ message: `Failed to find user with username ${username}` });
|
||||
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
|
||||
|
||||
const users = await removeUsersFromGroupByUserIds({
|
||||
group,
|
||||
@ -342,6 +363,7 @@ export const groupServiceFactory = ({
|
||||
deleteGroup,
|
||||
listGroupUsers,
|
||||
addUserToGroup,
|
||||
removeUserFromGroup
|
||||
removeUserFromGroup,
|
||||
getGroupById
|
||||
};
|
||||
};
|
||||
|
@ -17,7 +17,7 @@ export type TCreateGroupDTO = {
|
||||
} & TGenericPermission;
|
||||
|
||||
export type TUpdateGroupDTO = {
|
||||
currentSlug: string;
|
||||
id: string;
|
||||
} & Partial<{
|
||||
name: string;
|
||||
slug: string;
|
||||
@ -26,23 +26,27 @@ export type TUpdateGroupDTO = {
|
||||
TGenericPermission;
|
||||
|
||||
export type TDeleteGroupDTO = {
|
||||
groupSlug: string;
|
||||
id: string;
|
||||
} & TGenericPermission;
|
||||
|
||||
export type TGetGroupByIdDTO = {
|
||||
id: string;
|
||||
} & TGenericPermission;
|
||||
|
||||
export type TListGroupUsersDTO = {
|
||||
groupSlug: string;
|
||||
id: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
username?: string;
|
||||
} & TGenericPermission;
|
||||
|
||||
export type TAddUserToGroupDTO = {
|
||||
groupSlug: string;
|
||||
id: string;
|
||||
username: string;
|
||||
} & TGenericPermission;
|
||||
|
||||
export type TRemoveUserFromGroupDTO = {
|
||||
groupSlug: string;
|
||||
id: string;
|
||||
username: string;
|
||||
} & TGenericPermission;
|
||||
|
||||
|
@ -4,7 +4,7 @@ import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
@ -34,18 +34,12 @@ export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType<
|
||||
|
||||
// TODO(akhilmhdh): move this to more centralized
|
||||
export const UnpackedPermissionSchema = z.object({
|
||||
subject: z.union([z.string().min(1), z.string().array()]).optional(),
|
||||
action: z.union([z.string().min(1), z.string().array()]),
|
||||
conditions: z
|
||||
.object({
|
||||
environment: z.string().optional(),
|
||||
secretPath: z
|
||||
.object({
|
||||
$glob: z.string().min(1)
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
.optional()
|
||||
subject: z
|
||||
.union([z.string().min(1), z.string().array()])
|
||||
.transform((el) => (typeof el !== "string" ? el[0] : el))
|
||||
.optional(),
|
||||
action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)),
|
||||
conditions: z.unknown().optional()
|
||||
});
|
||||
|
||||
const unpackPermissions = (permissions: unknown) =>
|
||||
@ -71,12 +65,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
...dto
|
||||
}: TCreateIdentityPrivilegeDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
if (!project) throw new NotFoundError({ message: "Project not found" });
|
||||
const projectId = project.id;
|
||||
|
||||
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
|
||||
if (!identityProjectMembership)
|
||||
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
|
||||
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@ -143,12 +137,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
actorAuthMethod
|
||||
}: TUpdateIdentityPrivilegeDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
if (!project) throw new NotFoundError({ message: "Project not found" });
|
||||
const projectId = project.id;
|
||||
|
||||
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
|
||||
if (!identityProjectMembership)
|
||||
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
|
||||
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@ -173,7 +167,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
slug,
|
||||
projectMembershipId: identityProjectMembership.id
|
||||
});
|
||||
if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" });
|
||||
if (!identityPrivilege) throw new NotFoundError({ message: "Identity additional privilege not found" });
|
||||
if (data?.slug) {
|
||||
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||
slug: data.slug,
|
||||
@ -224,12 +218,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
actorAuthMethod
|
||||
}: TDeleteIdentityPrivilegeDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
if (!project) throw new NotFoundError({ message: "Project not found" });
|
||||
const projectId = project.id;
|
||||
|
||||
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
|
||||
if (!identityProjectMembership)
|
||||
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
|
||||
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@ -254,7 +248,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
slug,
|
||||
projectMembershipId: identityProjectMembership.id
|
||||
});
|
||||
if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" });
|
||||
if (!identityPrivilege) throw new NotFoundError({ message: "Identity additional privilege not found" });
|
||||
|
||||
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
|
||||
return {
|
||||
@ -274,12 +268,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
actorAuthMethod
|
||||
}: TGetIdentityPrivilegeDetailsDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
if (!project) throw new NotFoundError({ message: "Project not found" });
|
||||
const projectId = project.id;
|
||||
|
||||
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
|
||||
if (!identityProjectMembership)
|
||||
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
|
||||
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@ -293,7 +287,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
slug,
|
||||
projectMembershipId: identityProjectMembership.id
|
||||
});
|
||||
if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" });
|
||||
if (!identityPrivilege) throw new NotFoundError({ message: "Identity additional privilege not found" });
|
||||
|
||||
return {
|
||||
...identityPrivilege,
|
||||
@ -310,12 +304,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
projectSlug
|
||||
}: TListIdentityPrivilegesDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
if (!project) throw new NotFoundError({ message: "Project not found" });
|
||||
const projectId = project.id;
|
||||
|
||||
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
|
||||
if (!identityProjectMembership)
|
||||
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
|
||||
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
|
@ -21,7 +21,7 @@ import {
|
||||
infisicalSymmetricDecrypt,
|
||||
infisicalSymmetricEncypt
|
||||
} from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
||||
@ -253,7 +253,7 @@ export const ldapConfigServiceFactory = ({
|
||||
};
|
||||
|
||||
const orgBot = await orgBotDAL.findOne({ orgId });
|
||||
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
|
||||
if (!orgBot) throw new NotFoundError({ message: "Organization bot not found", name: "OrgBotNotFound" });
|
||||
const key = infisicalSymmetricDecrypt({
|
||||
ciphertext: orgBot.encryptedSymmetricKey,
|
||||
iv: orgBot.symmetricKeyIV,
|
||||
@ -289,10 +289,10 @@ export const ldapConfigServiceFactory = ({
|
||||
|
||||
const getLdapCfg = async (filter: { orgId: string; isActive?: boolean; id?: string }) => {
|
||||
const ldapConfig = await ldapConfigDAL.findOne(filter);
|
||||
if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" });
|
||||
if (!ldapConfig) throw new NotFoundError({ message: "Failed to find organization LDAP data" });
|
||||
|
||||
const orgBot = await orgBotDAL.findOne({ orgId: ldapConfig.orgId });
|
||||
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
|
||||
if (!orgBot) throw new NotFoundError({ message: "Organization bot not found", name: "OrgBotNotFound" });
|
||||
|
||||
const key = infisicalSymmetricDecrypt({
|
||||
ciphertext: orgBot.encryptedSymmetricKey,
|
||||
@ -375,7 +375,7 @@ export const ldapConfigServiceFactory = ({
|
||||
|
||||
const bootLdap = async (organizationSlug: string) => {
|
||||
const organization = await orgDAL.findOne({ slug: organizationSlug });
|
||||
if (!organization) throw new BadRequestError({ message: "Org not found" });
|
||||
if (!organization) throw new NotFoundError({ message: "Organization not found" });
|
||||
|
||||
const ldapConfig = await getLdapCfg({
|
||||
orgId: organization.id,
|
||||
@ -420,7 +420,7 @@ export const ldapConfigServiceFactory = ({
|
||||
const serverCfg = await getServerCfg();
|
||||
|
||||
if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.LDAP)) {
|
||||
throw new BadRequestError({
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Login with LDAP is disabled by administrator."
|
||||
});
|
||||
}
|
||||
@ -432,7 +432,7 @@ export const ldapConfigServiceFactory = ({
|
||||
});
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
if (!organization) throw new BadRequestError({ message: "Org not found" });
|
||||
if (!organization) throw new NotFoundError({ message: "Organization not found" });
|
||||
|
||||
if (userAlias) {
|
||||
await userDAL.transaction(async (tx) => {
|
||||
@ -700,7 +700,7 @@ export const ldapConfigServiceFactory = ({
|
||||
orgId
|
||||
});
|
||||
|
||||
if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" });
|
||||
if (!ldapConfig) throw new NotFoundError({ message: "Failed to find organization LDAP data" });
|
||||
|
||||
const groupMaps = await ldapGroupMapDAL.findLdapGroupMapsByLdapConfigId(ldapConfigId);
|
||||
|
||||
@ -741,13 +741,13 @@ export const ldapConfigServiceFactory = ({
|
||||
const groups = await searchGroups(ldapConfig, groupSearchFilter, ldapConfig.groupSearchBase);
|
||||
|
||||
if (!groups.some((g) => g.cn === ldapGroupCN)) {
|
||||
throw new BadRequestError({
|
||||
throw new NotFoundError({
|
||||
message: "Failed to find LDAP Group CN"
|
||||
});
|
||||
}
|
||||
|
||||
const group = await groupDAL.findOne({ slug: groupSlug, orgId });
|
||||
if (!group) throw new BadRequestError({ message: "Failed to find group" });
|
||||
if (!group) throw new NotFoundError({ message: "Failed to find group" });
|
||||
|
||||
const groupMap = await ldapGroupMapDAL.create({
|
||||
ldapConfigId,
|
||||
@ -781,7 +781,7 @@ export const ldapConfigServiceFactory = ({
|
||||
orgId
|
||||
});
|
||||
|
||||
if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" });
|
||||
if (!ldapConfig) throw new NotFoundError({ message: "Failed to find organization LDAP data" });
|
||||
|
||||
const [deletedGroupMap] = await ldapGroupMapDAL.delete({
|
||||
ldapConfigId: ldapConfig.id,
|
||||
|
@ -10,7 +10,7 @@ import { Knex } from "knex";
|
||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { verifyOfflineLicense } from "@app/lib/crypto";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
|
||||
@ -145,7 +145,7 @@ export const licenseServiceFactory = ({
|
||||
if (cachedPlan) return JSON.parse(cachedPlan) as TFeatureSet;
|
||||
|
||||
const org = await orgDAL.findOrgById(orgId);
|
||||
if (!org) throw new BadRequestError({ message: "Org not found" });
|
||||
if (!org) throw new NotFoundError({ message: "Organization not found" });
|
||||
const {
|
||||
data: { currentPlan }
|
||||
} = await licenseServerCloudApi.request.get<{ currentPlan: TFeatureSet }>(
|
||||
@ -204,7 +204,7 @@ export const licenseServiceFactory = ({
|
||||
const updateSubscriptionOrgMemberCount = async (orgId: string, tx?: Knex) => {
|
||||
if (instanceType === InstanceType.Cloud) {
|
||||
const org = await orgDAL.findOrgById(orgId);
|
||||
if (!org) throw new BadRequestError({ message: "Org not found" });
|
||||
if (!org) throw new NotFoundError({ message: "Organization not found" });
|
||||
|
||||
const quantity = await licenseDAL.countOfOrgMembers(orgId, tx);
|
||||
const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(orgId, tx);
|
||||
@ -266,8 +266,8 @@ export const licenseServiceFactory = ({
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
throw new NotFoundError({
|
||||
message: "Organization not found"
|
||||
});
|
||||
}
|
||||
|
||||
@ -294,8 +294,8 @@ export const licenseServiceFactory = ({
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
throw new NotFoundError({
|
||||
message: "Organization not found"
|
||||
});
|
||||
}
|
||||
|
||||
@ -340,8 +340,8 @@ export const licenseServiceFactory = ({
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
throw new NotFoundError({
|
||||
message: "Organization not found"
|
||||
});
|
||||
}
|
||||
const { data } = await licenseServerCloudApi.request.get(
|
||||
@ -357,8 +357,8 @@ export const licenseServiceFactory = ({
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
throw new NotFoundError({
|
||||
message: "Organization not found"
|
||||
});
|
||||
}
|
||||
const { data } = await licenseServerCloudApi.request.get(
|
||||
@ -373,8 +373,8 @@ export const licenseServiceFactory = ({
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
throw new NotFoundError({
|
||||
message: "Organization not found"
|
||||
});
|
||||
}
|
||||
|
||||
@ -398,8 +398,8 @@ export const licenseServiceFactory = ({
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
throw new NotFoundError({
|
||||
message: "Organization not found"
|
||||
});
|
||||
}
|
||||
const { data } = await licenseServerCloudApi.request.patch(
|
||||
@ -418,8 +418,8 @@ export const licenseServiceFactory = ({
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
throw new NotFoundError({
|
||||
message: "Organization not found"
|
||||
});
|
||||
}
|
||||
|
||||
@ -445,8 +445,8 @@ export const licenseServiceFactory = ({
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
throw new NotFoundError({
|
||||
message: "Organization not found"
|
||||
});
|
||||
}
|
||||
const {
|
||||
@ -474,8 +474,8 @@ export const licenseServiceFactory = ({
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
throw new NotFoundError({
|
||||
message: "Organization not found"
|
||||
});
|
||||
}
|
||||
|
||||
@ -491,8 +491,8 @@ export const licenseServiceFactory = ({
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
throw new NotFoundError({
|
||||
message: "Organization not found"
|
||||
});
|
||||
}
|
||||
const {
|
||||
@ -509,8 +509,8 @@ export const licenseServiceFactory = ({
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
throw new NotFoundError({
|
||||
message: "Organization not found"
|
||||
});
|
||||
}
|
||||
|
||||
@ -530,8 +530,8 @@ export const licenseServiceFactory = ({
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
throw new NotFoundError({
|
||||
message: "Organization not found"
|
||||
});
|
||||
}
|
||||
|
||||
@ -547,8 +547,8 @@ export const licenseServiceFactory = ({
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
throw new NotFoundError({
|
||||
message: "Organization not found"
|
||||
});
|
||||
}
|
||||
|
||||
@ -564,8 +564,8 @@ export const licenseServiceFactory = ({
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
throw new NotFoundError({
|
||||
message: "Organization not found"
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TOidcConfigDALFactory = ReturnType<typeof oidcConfigDALFactory>;
|
||||
@ -7,5 +8,22 @@ export type TOidcConfigDALFactory = ReturnType<typeof oidcConfigDALFactory>;
|
||||
export const oidcConfigDALFactory = (db: TDbClient) => {
|
||||
const oidcCfgOrm = ormify(db, TableName.OidcConfig);
|
||||
|
||||
return { ...oidcCfgOrm };
|
||||
const findEnforceableOidcCfg = async (orgId: string) => {
|
||||
try {
|
||||
const oidcCfg = await db
|
||||
.replicaNode()(TableName.OidcConfig)
|
||||
.where({
|
||||
orgId,
|
||||
isActive: true
|
||||
})
|
||||
.whereNotNull("lastUsed")
|
||||
.first();
|
||||
|
||||
return oidcCfg;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find org by id" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...oidcCfgOrm, findEnforceableOidcCfg };
|
||||
};
|
||||
|
@ -17,7 +17,7 @@ import {
|
||||
infisicalSymmetricDecrypt,
|
||||
infisicalSymmetricEncypt
|
||||
} from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
||||
@ -77,7 +77,7 @@ export const oidcConfigServiceFactory = ({
|
||||
const getOidc = async (dto: TGetOidcCfgDTO) => {
|
||||
const org = await orgDAL.findOne({ slug: dto.orgSlug });
|
||||
if (!org) {
|
||||
throw new BadRequestError({
|
||||
throw new NotFoundError({
|
||||
message: "Organization not found",
|
||||
name: "OrgNotFound"
|
||||
});
|
||||
@ -98,7 +98,7 @@ export const oidcConfigServiceFactory = ({
|
||||
});
|
||||
|
||||
if (!oidcCfg) {
|
||||
throw new BadRequestError({
|
||||
throw new NotFoundError({
|
||||
message: "Failed to find organization OIDC configuration"
|
||||
});
|
||||
}
|
||||
@ -106,7 +106,7 @@ export const oidcConfigServiceFactory = ({
|
||||
// decrypt and return cfg
|
||||
const orgBot = await orgBotDAL.findOne({ orgId: oidcCfg.orgId });
|
||||
if (!orgBot) {
|
||||
throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
|
||||
throw new NotFoundError({ message: "Organization bot not found", name: "OrgBotNotFound" });
|
||||
}
|
||||
|
||||
const key = infisicalSymmetricDecrypt({
|
||||
@ -160,7 +160,7 @@ export const oidcConfigServiceFactory = ({
|
||||
const serverCfg = await getServerCfg();
|
||||
|
||||
if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.OIDC)) {
|
||||
throw new BadRequestError({
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Login with OIDC is disabled by administrator."
|
||||
});
|
||||
}
|
||||
@ -173,7 +173,7 @@ export const oidcConfigServiceFactory = ({
|
||||
});
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
if (!organization) throw new BadRequestError({ message: "Org not found" });
|
||||
if (!organization) throw new NotFoundError({ message: "Organization not found" });
|
||||
|
||||
let user: TUsers;
|
||||
if (userAlias) {
|
||||
@ -314,6 +314,8 @@ export const oidcConfigServiceFactory = ({
|
||||
}
|
||||
);
|
||||
|
||||
await oidcConfigDAL.update({ orgId }, { lastUsed: new Date() });
|
||||
|
||||
if (user.email && !user.isEmailVerified) {
|
||||
const token = await tokenService.createTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
||||
@ -356,7 +358,7 @@ export const oidcConfigServiceFactory = ({
|
||||
});
|
||||
|
||||
if (!org) {
|
||||
throw new BadRequestError({
|
||||
throw new NotFoundError({
|
||||
message: "Organization not found"
|
||||
});
|
||||
}
|
||||
@ -378,7 +380,7 @@ export const oidcConfigServiceFactory = ({
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
|
||||
|
||||
const orgBot = await orgBotDAL.findOne({ orgId: org.id });
|
||||
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
|
||||
if (!orgBot) throw new NotFoundError({ message: "Organization bot not found", name: "OrgBotNotFound" });
|
||||
const key = infisicalSymmetricDecrypt({
|
||||
ciphertext: orgBot.encryptedSymmetricKey,
|
||||
iv: orgBot.symmetricKeyIV,
|
||||
@ -395,7 +397,8 @@ export const oidcConfigServiceFactory = ({
|
||||
tokenEndpoint,
|
||||
userinfoEndpoint,
|
||||
jwksUri,
|
||||
isActive
|
||||
isActive,
|
||||
lastUsed: null
|
||||
};
|
||||
|
||||
if (clientId !== undefined) {
|
||||
@ -418,6 +421,7 @@ export const oidcConfigServiceFactory = ({
|
||||
}
|
||||
|
||||
const [ssoConfig] = await oidcConfigDAL.update({ orgId: org.id }, updateQuery);
|
||||
await orgDAL.updateById(org.id, { authEnforced: false, scimEnabled: false });
|
||||
return ssoConfig;
|
||||
};
|
||||
|
||||
@ -443,7 +447,7 @@ export const oidcConfigServiceFactory = ({
|
||||
slug: orgSlug
|
||||
});
|
||||
if (!org) {
|
||||
throw new BadRequestError({
|
||||
throw new NotFoundError({
|
||||
message: "Organization not found"
|
||||
});
|
||||
}
|
||||
@ -549,7 +553,7 @@ export const oidcConfigServiceFactory = ({
|
||||
});
|
||||
|
||||
if (!org) {
|
||||
throw new BadRequestError({
|
||||
throw new NotFoundError({
|
||||
message: "Organization not found."
|
||||
});
|
||||
}
|
||||
@ -560,7 +564,7 @@ export const oidcConfigServiceFactory = ({
|
||||
});
|
||||
|
||||
if (!oidcCfg || !oidcCfg.isActive) {
|
||||
throw new BadRequestError({
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Failed to authenticate with OIDC SSO"
|
||||
});
|
||||
}
|
||||
@ -617,7 +621,7 @@ export const oidcConfigServiceFactory = ({
|
||||
if (oidcCfg.allowedEmailDomains) {
|
||||
const allowedDomains = oidcCfg.allowedEmailDomains.split(", ");
|
||||
if (!allowedDomains.includes(claims.email.split("@")[1])) {
|
||||
throw new BadRequestError({
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Email not allowed."
|
||||
});
|
||||
}
|
||||
|
@ -25,7 +25,8 @@ export enum OrgPermissionSubjects {
|
||||
SecretScanning = "secret-scanning",
|
||||
Identity = "identity",
|
||||
Kms = "kms",
|
||||
AdminConsole = "organization-admin-console"
|
||||
AdminConsole = "organization-admin-console",
|
||||
AuditLogs = "audit-logs"
|
||||
}
|
||||
|
||||
export type OrgPermissionSet =
|
||||
@ -43,6 +44,7 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||
|
||||
const buildAdminPermission = () => {
|
||||
@ -111,6 +113,11 @@ const buildAdminPermission = () => {
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.AuditLogs);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.AuditLogs);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.AuditLogs);
|
||||
|
||||
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||
|
||||
return rules;
|
||||
@ -140,6 +147,8 @@ const buildMemberPermission = () => {
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
|
@ -50,6 +50,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
.select(
|
||||
selectAllTableCols(TableName.OrgMembership),
|
||||
db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"),
|
||||
db.ref("permissions").withSchema(TableName.OrgRoles),
|
||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||
db.ref("groupId").withSchema("userGroups"),
|
||||
db.ref("groupOrgId").withSchema("userGroups"),
|
||||
@ -167,8 +168,14 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
})
|
||||
.join<TProjects>(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId]))
|
||||
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
||||
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||
void queryBuilder
|
||||
.on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`)
|
||||
.andOn(`${TableName.Organization}.id`, `${TableName.IdentityMetadata}.orgId`);
|
||||
})
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||
db.ref("username").withSchema(TableName.Users).as("username"),
|
||||
// groups specific
|
||||
db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"),
|
||||
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"),
|
||||
@ -256,6 +263,9 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||
.as("userAdditionalPrivilegesTemporaryAccessEndTime"),
|
||||
// general
|
||||
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
|
||||
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
||||
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"),
|
||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||
db.ref("orgId").withSchema(TableName.Project),
|
||||
db.ref("id").withSchema(TableName.Project).as("projectId")
|
||||
@ -266,6 +276,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
key: "projectId",
|
||||
parentMapper: ({
|
||||
orgId,
|
||||
username,
|
||||
orgAuthEnforced,
|
||||
membershipId,
|
||||
groupMembershipId,
|
||||
@ -278,6 +289,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
orgAuthEnforced,
|
||||
userId,
|
||||
projectId,
|
||||
username,
|
||||
id: membershipId || groupMembershipId,
|
||||
createdAt: membershipCreatedAt || groupMembershipCreatedAt,
|
||||
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
|
||||
@ -353,6 +365,15 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime,
|
||||
isTemporary: userAdditionalPrivilegesIsTemporary
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "metadataId",
|
||||
label: "metadata" as const,
|
||||
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||
id: metadataId,
|
||||
key: metadataKey,
|
||||
value: metadataValue
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
@ -398,6 +419,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
`${TableName.IdentityProjectMembershipRole}.projectMembershipId`,
|
||||
`${TableName.IdentityProjectMembership}.id`
|
||||
)
|
||||
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityProjectMembership}.identityId`)
|
||||
.leftJoin(
|
||||
TableName.ProjectRoles,
|
||||
`${TableName.IdentityProjectMembershipRole}.customRoleId`,
|
||||
@ -414,11 +436,17 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
`${TableName.IdentityProjectMembership}.projectId`,
|
||||
`${TableName.Project}.id`
|
||||
)
|
||||
.where("identityId", identityId)
|
||||
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||
void queryBuilder
|
||||
.on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`)
|
||||
.andOn(`${TableName.Project}.orgId`, `${TableName.IdentityMetadata}.orgId`);
|
||||
})
|
||||
.where(`${TableName.IdentityProjectMembership}.identityId`, identityId)
|
||||
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
|
||||
.select(selectAllTableCols(TableName.IdentityProjectMembershipRole))
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
|
||||
db.ref("name").withSchema(TableName.Identity).as("identityName"),
|
||||
db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project
|
||||
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
|
||||
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
|
||||
@ -442,15 +470,19 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
db
|
||||
.ref("temporaryAccessEndTime")
|
||||
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
|
||||
.as("identityApTemporaryAccessEndTime")
|
||||
.as("identityApTemporaryAccessEndTime"),
|
||||
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
|
||||
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
||||
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue")
|
||||
);
|
||||
|
||||
const permission = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "membershipId",
|
||||
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId }) => ({
|
||||
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId, identityName }) => ({
|
||||
id: membershipId,
|
||||
identityId,
|
||||
username: identityName,
|
||||
projectId,
|
||||
createdAt: membershipCreatedAt,
|
||||
updatedAt: membershipUpdatedAt,
|
||||
@ -488,6 +520,15 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
temporaryAccessStartTime: identityApTemporaryAccessStartTime,
|
||||
isTemporary: identityApIsTemporary
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "metadataId",
|
||||
label: "metadata" as const,
|
||||
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||
id: metadataId,
|
||||
key: metadataKey,
|
||||
value: metadataValue
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { TOrganizations } from "@app/db/schemas";
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type";
|
||||
|
||||
function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
|
||||
@ -14,14 +14,19 @@ function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
|
||||
].includes(actorAuthMethod);
|
||||
}
|
||||
|
||||
function validateOrgSAML(actorAuthMethod: ActorAuthMethod, isSamlEnforced: TOrganizations["authEnforced"]) {
|
||||
function validateOrgSSO(actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrganizations["authEnforced"]) {
|
||||
if (actorAuthMethod === undefined) {
|
||||
throw new UnauthorizedError({ name: "No auth method defined" });
|
||||
}
|
||||
|
||||
if (isSamlEnforced && actorAuthMethod !== null && !isAuthMethodSaml(actorAuthMethod)) {
|
||||
throw new UnauthorizedError({ name: "Cannot access org-scoped resource" });
|
||||
if (
|
||||
isOrgSsoEnforced &&
|
||||
actorAuthMethod !== null &&
|
||||
!isAuthMethodSaml(actorAuthMethod) &&
|
||||
actorAuthMethod !== AuthMethod.OIDC
|
||||
) {
|
||||
throw new ForbiddenRequestError({ name: "Org auth enforced. Cannot access org-scoped resource" });
|
||||
}
|
||||
}
|
||||
|
||||
export { isAuthMethodSaml, validateOrgSAML };
|
||||
export { isAuthMethodSaml, validateOrgSSO };
|
||||
|
@ -0,0 +1,9 @@
|
||||
export type TBuildProjectPermissionDTO = {
|
||||
permissions?: unknown;
|
||||
role: string;
|
||||
}[];
|
||||
|
||||
export type TBuildOrgPermissionDTO = {
|
||||
permissions?: unknown;
|
||||
role: string;
|
||||
}[];
|
@ -1,6 +1,7 @@
|
||||
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
|
||||
import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||
import { MongoQuery } from "@ucast/mongo2js";
|
||||
import handlebars from "handlebars";
|
||||
|
||||
import {
|
||||
OrgMembershipRole,
|
||||
@ -10,7 +11,8 @@ import {
|
||||
TProjectMemberships
|
||||
} from "@app/db/schemas";
|
||||
import { conditionsMatcher } from "@app/lib/casl";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { objectify } from "@app/lib/fn";
|
||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
@ -19,8 +21,8 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok
|
||||
|
||||
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
|
||||
import { TPermissionDALFactory } from "./permission-dal";
|
||||
import { validateOrgSAML } from "./permission-fns";
|
||||
import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-types";
|
||||
import { validateOrgSSO } from "./permission-fns";
|
||||
import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-service-types";
|
||||
import {
|
||||
buildServiceTokenProjectPermission,
|
||||
projectAdminPermissions,
|
||||
@ -62,7 +64,7 @@ export const permissionServiceFactory = ({
|
||||
permissions as PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[]
|
||||
);
|
||||
default:
|
||||
throw new BadRequestError({ name: "OrgRoleInvalid", message: "Org role not found" });
|
||||
throw new NotFoundError({ name: "OrgRoleInvalid", message: "Organization role not found" });
|
||||
}
|
||||
})
|
||||
.reduce((curr, prev) => prev.concat(curr), []);
|
||||
@ -72,7 +74,7 @@ export const permissionServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const buildProjectPermission = (projectUserRoles: TBuildProjectPermissionDTO) => {
|
||||
const buildProjectPermissionRules = (projectUserRoles: TBuildProjectPermissionDTO) => {
|
||||
const rules = projectUserRoles
|
||||
.map(({ role, permissions }) => {
|
||||
switch (role) {
|
||||
@ -90,7 +92,7 @@ export const permissionServiceFactory = ({
|
||||
);
|
||||
}
|
||||
default:
|
||||
throw new BadRequestError({
|
||||
throw new NotFoundError({
|
||||
name: "ProjectRoleInvalid",
|
||||
message: "Project role not found"
|
||||
});
|
||||
@ -98,9 +100,7 @@ export const permissionServiceFactory = ({
|
||||
})
|
||||
.reduce((curr, prev) => prev.concat(curr), []);
|
||||
|
||||
return createMongoAbility<ProjectPermissionSet>(rules, {
|
||||
conditionsMatcher
|
||||
});
|
||||
return rules;
|
||||
};
|
||||
|
||||
/*
|
||||
@ -114,11 +114,11 @@ export const permissionServiceFactory = ({
|
||||
) => {
|
||||
// when token is scoped, ensure the passed org id is same as user org id
|
||||
if (userOrgId && userOrgId !== orgId)
|
||||
throw new BadRequestError({ message: "Invalid user token. Scoped to different organization." });
|
||||
throw new ForbiddenRequestError({ message: "Invalid user token. Scoped to different organization." });
|
||||
const membership = await permissionDAL.getOrgPermission(userId, orgId);
|
||||
if (!membership) throw new UnauthorizedError({ name: "User not in org" });
|
||||
if (!membership) throw new ForbiddenRequestError({ name: "You are not apart of this organization" });
|
||||
if (membership.role === OrgMembershipRole.Custom && !membership.permissions) {
|
||||
throw new BadRequestError({ name: "Custom permission not found" });
|
||||
throw new BadRequestError({ name: "Custom organization permission not found" });
|
||||
}
|
||||
|
||||
// If the org ID is API_KEY, the request is being made with an API Key.
|
||||
@ -127,10 +127,10 @@ export const permissionServiceFactory = ({
|
||||
// Extra: This means that when users are using API keys to make requests, they can't use slug-based routes.
|
||||
// Slug-based routes depend on the organization ID being present on the request, since project slugs aren't globally unique, and we need a way to filter by organization.
|
||||
if (userOrgId !== "API_KEY" && membership.orgId !== userOrgId) {
|
||||
throw new UnauthorizedError({ name: "You are not logged into this organization" });
|
||||
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
|
||||
}
|
||||
|
||||
validateOrgSAML(authMethod, membership.orgAuthEnforced);
|
||||
validateOrgSSO(authMethod, membership.orgAuthEnforced);
|
||||
|
||||
const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat(
|
||||
membership?.groups?.map(({ role, customRolePermission }) => ({
|
||||
@ -143,9 +143,9 @@ export const permissionServiceFactory = ({
|
||||
|
||||
const getIdentityOrgPermission = async (identityId: string, orgId: string) => {
|
||||
const membership = await permissionDAL.getOrgIdentityPermission(identityId, orgId);
|
||||
if (!membership) throw new UnauthorizedError({ name: "Identity not in org" });
|
||||
if (!membership) throw new ForbiddenRequestError({ name: "Identity is not apart of this organization" });
|
||||
if (membership.role === OrgMembershipRole.Custom && !membership.permissions) {
|
||||
throw new BadRequestError({ name: "Custom permission not found" });
|
||||
throw new NotFoundError({ name: "Custom organization permission not found" });
|
||||
}
|
||||
return {
|
||||
permission: buildOrgPermission([{ role: membership.role, permissions: membership.permissions }]),
|
||||
@ -166,8 +166,8 @@ export const permissionServiceFactory = ({
|
||||
case ActorType.IDENTITY:
|
||||
return getIdentityOrgPermission(id, orgId);
|
||||
default:
|
||||
throw new UnauthorizedError({
|
||||
message: "Permission not defined",
|
||||
throw new BadRequestError({
|
||||
message: "Invalid actor provided",
|
||||
name: "Get org permission"
|
||||
});
|
||||
}
|
||||
@ -179,7 +179,7 @@ export const permissionServiceFactory = ({
|
||||
const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole);
|
||||
if (isCustomRole) {
|
||||
const orgRole = await orgRoleDAL.findOne({ slug: role, orgId });
|
||||
if (!orgRole) throw new BadRequestError({ message: "Role not found" });
|
||||
if (!orgRole) throw new NotFoundError({ message: "Specified role was not found" });
|
||||
return {
|
||||
permission: buildOrgPermission([{ role: OrgMembershipRole.Custom, permissions: orgRole.permissions }]),
|
||||
role: orgRole
|
||||
@ -196,12 +196,12 @@ export const permissionServiceFactory = ({
|
||||
userOrgId?: string
|
||||
): Promise<TProjectPermissionRT<ActorType.USER>> => {
|
||||
const userProjectPermission = await permissionDAL.getProjectPermission(userId, projectId);
|
||||
if (!userProjectPermission) throw new UnauthorizedError({ name: "User not in project" });
|
||||
if (!userProjectPermission) throw new ForbiddenRequestError({ name: "User not a part of the specified project" });
|
||||
|
||||
if (
|
||||
userProjectPermission.roles.some(({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions)
|
||||
) {
|
||||
throw new BadRequestError({ name: "Custom permission not found" });
|
||||
throw new NotFoundError({ name: "The permission was not found" });
|
||||
}
|
||||
|
||||
// If the org ID is API_KEY, the request is being made with an API Key.
|
||||
@ -210,10 +210,10 @@ export const permissionServiceFactory = ({
|
||||
// Extra: This means that when users are using API keys to make requests, they can't use slug-based routes.
|
||||
// Slug-based routes depend on the organization ID being present on the request, since project slugs aren't globally unique, and we need a way to filter by organization.
|
||||
if (userOrgId !== "API_KEY" && userProjectPermission.orgId !== userOrgId) {
|
||||
throw new UnauthorizedError({ name: "You are not logged into this organization" });
|
||||
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
|
||||
}
|
||||
|
||||
validateOrgSAML(authMethod, userProjectPermission.orgAuthEnforced);
|
||||
validateOrgSSO(authMethod, userProjectPermission.orgAuthEnforced);
|
||||
|
||||
// join two permissions and pass to build the final permission set
|
||||
const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
|
||||
@ -223,8 +223,32 @@ export const permissionServiceFactory = ({
|
||||
permissions
|
||||
})) || [];
|
||||
|
||||
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false, strict: true });
|
||||
const metadataKeyValuePair = objectify(
|
||||
userProjectPermission.metadata,
|
||||
(i) => i.key,
|
||||
(i) => i.value
|
||||
);
|
||||
const interpolateRules = templatedRules(
|
||||
{
|
||||
identity: {
|
||||
id: userProjectPermission.userId,
|
||||
username: userProjectPermission.username,
|
||||
metadata: metadataKeyValuePair
|
||||
}
|
||||
},
|
||||
{ data: false }
|
||||
);
|
||||
const permission = createMongoAbility<ProjectPermissionSet>(
|
||||
JSON.parse(interpolateRules) as RawRuleOf<MongoAbility<ProjectPermissionSet>>[],
|
||||
{
|
||||
conditionsMatcher
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)),
|
||||
permission,
|
||||
membership: userProjectPermission,
|
||||
hasRole: (role: string) =>
|
||||
userProjectPermission.roles.findIndex(
|
||||
@ -239,18 +263,19 @@ export const permissionServiceFactory = ({
|
||||
identityOrgId: string | undefined
|
||||
): Promise<TProjectPermissionRT<ActorType.IDENTITY>> => {
|
||||
const identityProjectPermission = await permissionDAL.getProjectIdentityPermission(identityId, projectId);
|
||||
if (!identityProjectPermission) throw new UnauthorizedError({ name: "Identity not in project" });
|
||||
if (!identityProjectPermission)
|
||||
throw new ForbiddenRequestError({ name: "Identity is not a member of the specified project" });
|
||||
|
||||
if (
|
||||
identityProjectPermission.roles.some(
|
||||
({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions
|
||||
)
|
||||
) {
|
||||
throw new BadRequestError({ name: "Custom permission not found" });
|
||||
throw new NotFoundError({ name: "Custom permission not found" });
|
||||
}
|
||||
|
||||
if (identityProjectPermission.orgId !== identityOrgId) {
|
||||
throw new UnauthorizedError({ name: "You are not a member of this organization" });
|
||||
throw new ForbiddenRequestError({ name: "Identity is not a member of the specified organization" });
|
||||
}
|
||||
|
||||
const rolePermissions =
|
||||
@ -261,8 +286,32 @@ export const permissionServiceFactory = ({
|
||||
permissions
|
||||
})) || [];
|
||||
|
||||
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false, strict: true });
|
||||
const metadataKeyValuePair = objectify(
|
||||
identityProjectPermission.metadata,
|
||||
(i) => i.key,
|
||||
(i) => i.value
|
||||
);
|
||||
const interpolateRules = templatedRules(
|
||||
{
|
||||
identity: {
|
||||
id: identityProjectPermission.identityId,
|
||||
username: identityProjectPermission.username,
|
||||
metadata: metadataKeyValuePair
|
||||
}
|
||||
},
|
||||
{ data: false }
|
||||
);
|
||||
const permission = createMongoAbility<ProjectPermissionSet>(
|
||||
JSON.parse(interpolateRules) as RawRuleOf<MongoAbility<ProjectPermissionSet>>[],
|
||||
{
|
||||
conditionsMatcher
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)),
|
||||
permission,
|
||||
membership: identityProjectPermission,
|
||||
hasRole: (role: string) =>
|
||||
identityProjectPermission.roles.findIndex(
|
||||
@ -277,25 +326,23 @@ export const permissionServiceFactory = ({
|
||||
actorOrgId: string | undefined
|
||||
) => {
|
||||
const serviceToken = await serviceTokenDAL.findById(serviceTokenId);
|
||||
if (!serviceToken) throw new BadRequestError({ message: "Service token not found" });
|
||||
if (!serviceToken) throw new NotFoundError({ message: "Service token not found" });
|
||||
|
||||
const serviceTokenProject = await projectDAL.findById(serviceToken.projectId);
|
||||
|
||||
if (!serviceTokenProject) throw new BadRequestError({ message: "Service token not linked to a project" });
|
||||
|
||||
if (serviceTokenProject.orgId !== actorOrgId) {
|
||||
throw new UnauthorizedError({ message: "Service token not a part of this organization" });
|
||||
throw new ForbiddenRequestError({ message: "Service token not a part of the specified organization" });
|
||||
}
|
||||
|
||||
if (serviceToken.projectId !== projectId)
|
||||
throw new UnauthorizedError({
|
||||
message: "Failed to find service authorization for given project"
|
||||
});
|
||||
if (serviceToken.projectId !== projectId) {
|
||||
throw new ForbiddenRequestError({ name: "Service token not a part of the specified project" });
|
||||
}
|
||||
|
||||
if (serviceTokenProject.orgId !== actorOrgId)
|
||||
throw new UnauthorizedError({
|
||||
message: "Failed to find service authorization for given project"
|
||||
});
|
||||
if (serviceTokenProject.orgId !== actorOrgId) {
|
||||
throw new ForbiddenRequestError({ message: "Service token not a part of the specified organization" });
|
||||
}
|
||||
|
||||
const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []);
|
||||
return {
|
||||
@ -335,8 +382,8 @@ export const permissionServiceFactory = ({
|
||||
case ActorType.IDENTITY:
|
||||
return getIdentityProjectPermission(id, projectId, actorOrgId) as Promise<TProjectPermissionRT<T>>;
|
||||
default:
|
||||
throw new UnauthorizedError({
|
||||
message: "Permission not defined",
|
||||
throw new BadRequestError({
|
||||
message: "Invalid actor provided",
|
||||
name: "Get project permission"
|
||||
});
|
||||
}
|
||||
@ -346,15 +393,23 @@ export const permissionServiceFactory = ({
|
||||
const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole);
|
||||
if (isCustomRole) {
|
||||
const projectRole = await projectRoleDAL.findOne({ slug: role, projectId });
|
||||
if (!projectRole) throw new BadRequestError({ message: "Role not found" });
|
||||
if (!projectRole) throw new NotFoundError({ message: `Specified role was not found: ${role}` });
|
||||
const rules = buildProjectPermissionRules([
|
||||
{ role: ProjectMembershipRole.Custom, permissions: projectRole.permissions }
|
||||
]);
|
||||
return {
|
||||
permission: buildProjectPermission([
|
||||
{ role: ProjectMembershipRole.Custom, permissions: projectRole.permissions }
|
||||
]),
|
||||
permission: createMongoAbility<ProjectPermissionSet>(rules, {
|
||||
conditionsMatcher
|
||||
}),
|
||||
role: projectRole
|
||||
};
|
||||
}
|
||||
return { permission: buildProjectPermission([{ role, permissions: [] }]) };
|
||||
|
||||
const rules = buildProjectPermissionRules([{ role, permissions: [] }]);
|
||||
const permission = createMongoAbility<ProjectPermissionSet>(rules, {
|
||||
conditionsMatcher
|
||||
});
|
||||
return { permission };
|
||||
};
|
||||
|
||||
return {
|
||||
@ -365,6 +420,6 @@ export const permissionServiceFactory = ({
|
||||
getOrgPermissionByRole,
|
||||
getProjectPermissionByRole,
|
||||
buildOrgPermission,
|
||||
buildProjectPermission
|
||||
buildProjectPermissionRules
|
||||
};
|
||||
};
|
||||
|
@ -1,9 +1,47 @@
|
||||
export type TBuildProjectPermissionDTO = {
|
||||
permissions?: unknown;
|
||||
role: string;
|
||||
}[];
|
||||
import picomatch from "picomatch";
|
||||
import { z } from "zod";
|
||||
|
||||
export type TBuildOrgPermissionDTO = {
|
||||
permissions?: unknown;
|
||||
role: string;
|
||||
}[];
|
||||
export enum PermissionConditionOperators {
|
||||
$IN = "$in",
|
||||
$ALL = "$all",
|
||||
$REGEX = "$regex",
|
||||
$EQ = "$eq",
|
||||
$NEQ = "$ne",
|
||||
$GLOB = "$glob"
|
||||
}
|
||||
|
||||
export const PermissionConditionSchema = {
|
||||
[PermissionConditionOperators.$IN]: z.string().min(1).array(),
|
||||
[PermissionConditionOperators.$ALL]: z.string().min(1).array(),
|
||||
[PermissionConditionOperators.$REGEX]: z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine(
|
||||
(el) => {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new RegExp(el);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{ message: "Invalid regex pattern" }
|
||||
),
|
||||
[PermissionConditionOperators.$EQ]: z.string().min(1),
|
||||
[PermissionConditionOperators.$NEQ]: z.string().min(1),
|
||||
[PermissionConditionOperators.$GLOB]: z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine(
|
||||
(el) => {
|
||||
try {
|
||||
picomatch.parse([el]);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{ message: "Invalid glob pattern" }
|
||||
)
|
||||
};
|
||||
|
@ -1,8 +1,12 @@
|
||||
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { conditionsMatcher } from "@app/lib/casl";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { PermissionConditionOperators, PermissionConditionSchema } from "./permission-types";
|
||||
|
||||
export enum ProjectPermissionActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
@ -10,6 +14,15 @@ export enum ProjectPermissionActions {
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionCmekActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
Encrypt = "encrypt",
|
||||
Decrypt = "decrypt"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSub {
|
||||
Role = "role",
|
||||
Member = "member",
|
||||
@ -34,10 +47,29 @@ export enum ProjectPermissionSub {
|
||||
CertificateTemplates = "certificate-templates",
|
||||
PkiAlerts = "pki-alerts",
|
||||
PkiCollections = "pki-collections",
|
||||
Kms = "kms"
|
||||
Kms = "kms",
|
||||
Cmek = "cmek"
|
||||
}
|
||||
|
||||
type SubjectFields = {
|
||||
export type SecretSubjectFields = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
// secretName: string;
|
||||
// secretTags: string[];
|
||||
};
|
||||
|
||||
export const CaslSecretsV2SubjectKnexMapper = (field: string) => {
|
||||
switch (field) {
|
||||
case "secretName":
|
||||
return `${TableName.SecretV2}.key`;
|
||||
case "secretTags":
|
||||
return `${TableName.SecretTag}.slug`;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
export type SecretFolderSubjectFields = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
};
|
||||
@ -45,11 +77,14 @@ type SubjectFields = {
|
||||
export type ProjectPermissionSet =
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
|
||||
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
|
||||
]
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub.SecretFolders | (ForcedSubject<ProjectPermissionSub.SecretFolders> & SubjectFields)
|
||||
(
|
||||
| ProjectPermissionSub.SecretFolders
|
||||
| (ForcedSubject<ProjectPermissionSub.SecretFolders> & SecretFolderSubjectFields)
|
||||
)
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
||||
@ -70,132 +105,254 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
|
||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
|
||||
|
||||
export const fullProjectPermissionSet: [ProjectPermissionActions, ProjectPermissionSub][] = [
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Secrets],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Secrets],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets],
|
||||
const CASL_ACTION_SCHEMA_NATIVE_ENUM = <ACTION extends z.EnumLike>(actions: ACTION) =>
|
||||
z
|
||||
.union([z.nativeEnum(actions), z.nativeEnum(actions).array().min(1)])
|
||||
.transform((el) => (typeof el === "string" ? [el] : el));
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval],
|
||||
const CASL_ACTION_SCHEMA_ENUM = <ACTION extends z.EnumValues>(actions: ACTION) =>
|
||||
z.union([z.enum(actions), z.enum(actions).array().min(1)]).transform((el) => (typeof el === "string" ? [el] : el));
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretRotation],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.SecretRotation],
|
||||
const SecretConditionSchema = z
|
||||
.object({
|
||||
environment: z.union([
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
||||
})
|
||||
.partial()
|
||||
]),
|
||||
secretPath: z.union([
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
|
||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
||||
})
|
||||
.partial()
|
||||
])
|
||||
})
|
||||
.partial();
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Member],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Member],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Member],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Member],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Groups],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Groups],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Groups],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Groups],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Role],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Role],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Role],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Role],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Integrations],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Integrations],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Identity],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Identity],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Identity],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Identity],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Settings],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Settings],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Settings],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Settings],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Environments],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Environments],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Environments],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Environments],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Tags],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Tags],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Tags],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Tags],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.AuditLogs],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList],
|
||||
|
||||
// double check if all CRUD are needed for CA and Certificates
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.CertificateAuthorities],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateAuthorities],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateAuthorities],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Certificates],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Certificates],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.CertificateTemplates],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateTemplates],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateTemplates],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.PkiCollections],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.PkiCollections],
|
||||
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Project],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Project],
|
||||
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]
|
||||
];
|
||||
export const ProjectPermissionSchema = z.discriminatedUnion("subject", [
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
),
|
||||
conditions: SecretConditionSchema.describe(
|
||||
"When specified, only matching conditions will be allowed to access given resource."
|
||||
).optional()
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.SecretRollback).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read, ProjectPermissionActions.Create]).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Member).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Groups).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Role).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Integrations).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Webhooks).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Identity).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.ServiceTokens).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Settings).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Environments).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Tags).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.AuditLogs).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.IpAllowList).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.CertificateAuthorities).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Certificates).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.CertificateTemplates).describe("The entity this permission pertains to. "),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.PkiAlerts).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.PkiCollections).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Project).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete]).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Kms).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Edit]).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.SecretFolders).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read]).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Cmek).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCmekActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
const buildAdminPermissionRules = () => {
|
||||
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||
|
||||
// Admins get full access to everything
|
||||
fullProjectPermissionSet.forEach((permission) => {
|
||||
const [action, subject] = permission;
|
||||
can(action, subject);
|
||||
[
|
||||
ProjectPermissionSub.Secrets,
|
||||
ProjectPermissionSub.SecretApproval,
|
||||
ProjectPermissionSub.SecretRotation,
|
||||
ProjectPermissionSub.Member,
|
||||
ProjectPermissionSub.Groups,
|
||||
ProjectPermissionSub.Role,
|
||||
ProjectPermissionSub.Integrations,
|
||||
ProjectPermissionSub.Webhooks,
|
||||
ProjectPermissionSub.Identity,
|
||||
ProjectPermissionSub.ServiceTokens,
|
||||
ProjectPermissionSub.Settings,
|
||||
ProjectPermissionSub.Environments,
|
||||
ProjectPermissionSub.Tags,
|
||||
ProjectPermissionSub.AuditLogs,
|
||||
ProjectPermissionSub.IpAllowList,
|
||||
ProjectPermissionSub.CertificateAuthorities,
|
||||
ProjectPermissionSub.Certificates,
|
||||
ProjectPermissionSub.CertificateTemplates,
|
||||
ProjectPermissionSub.PkiAlerts,
|
||||
ProjectPermissionSub.PkiCollections
|
||||
].forEach((el) => {
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
el as ProjectPermissionSub
|
||||
);
|
||||
});
|
||||
|
||||
can([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete], ProjectPermissionSub.Project);
|
||||
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
|
||||
can([ProjectPermissionActions.Edit], ProjectPermissionSub.Kms);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionCmekActions.Create,
|
||||
ProjectPermissionCmekActions.Edit,
|
||||
ProjectPermissionCmekActions.Delete,
|
||||
ProjectPermissionCmekActions.Read,
|
||||
ProjectPermissionCmekActions.Encrypt,
|
||||
ProjectPermissionCmekActions.Decrypt
|
||||
],
|
||||
ProjectPermissionSub.Cmek
|
||||
);
|
||||
return rules;
|
||||
};
|
||||
|
||||
@ -204,73 +361,128 @@ export const projectAdminPermissions = buildAdminPermissionRules();
|
||||
const buildMemberPermissionRules = () => {
|
||||
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Secrets
|
||||
);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretRotation);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
|
||||
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
||||
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.Member);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.Groups);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Webhooks
|
||||
);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Identity);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Identity
|
||||
);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.ServiceTokens
|
||||
);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Settings);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Settings
|
||||
);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Environments
|
||||
);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Tags);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Tags
|
||||
);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.Role);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.AuditLogs);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.IpAllowList);
|
||||
|
||||
// double check if all CRUD are needed for CA and Certificates
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateTemplates);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionCmekActions.Create,
|
||||
ProjectPermissionCmekActions.Edit,
|
||||
ProjectPermissionCmekActions.Delete,
|
||||
ProjectPermissionCmekActions.Read,
|
||||
ProjectPermissionCmekActions.Encrypt,
|
||||
ProjectPermissionCmekActions.Decrypt
|
||||
],
|
||||
ProjectPermissionSub.Cmek
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
@ -298,6 +510,7 @@ const buildViewerPermissionRules = () => {
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||
|
||||
return rules;
|
||||
};
|
||||
@ -380,32 +593,19 @@ export const isAtLeastAsPrivilegedWorkspace = (
|
||||
|
||||
return set1.size >= set2.size;
|
||||
};
|
||||
/* eslint-enable */
|
||||
|
||||
/*
|
||||
* Case: The user requests to create a role with permissions that are not valid and not supposed to be used ever.
|
||||
* If we don't check for this, we can run into issues where functions like the `isAtLeastAsPrivileged` will not work as expected, because we compare the size of each permission set.
|
||||
* If the permission set contains invalid permissions, the size will be different, and result in incorrect results.
|
||||
*/
|
||||
export const validateProjectPermissions = (permissions: unknown) => {
|
||||
const parsedPermissions =
|
||||
typeof permissions === "string" ? (JSON.parse(permissions) as string[]) : (permissions as string[]);
|
||||
|
||||
const flattenedPermissions = [...parsedPermissions];
|
||||
|
||||
for (const perm of flattenedPermissions) {
|
||||
const [action, subject] = perm;
|
||||
|
||||
if (
|
||||
!fullProjectPermissionSet.find(
|
||||
(currentPermission) => currentPermission[0] === action && currentPermission[1] === subject
|
||||
)
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: `Permission action ${action} on subject ${subject} is not valid`,
|
||||
name: "Create Role"
|
||||
});
|
||||
}
|
||||
export const SecretV2SubjectFieldMapper = (arg: string) => {
|
||||
switch (arg) {
|
||||
case "environment":
|
||||
return null;
|
||||
case "secretPath":
|
||||
return null;
|
||||
case "secretName":
|
||||
return `${TableName.SecretV2}.key`;
|
||||
case "secretTags":
|
||||
return `${TableName.SecretTag}.slug`;
|
||||
default:
|
||||
throw new BadRequestError({ message: `Invalid dynamic knex operator field: ${arg}` });
|
||||
}
|
||||
};
|
||||
|
||||
/* eslint-enable */
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import ms from "ms";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
@ -42,7 +42,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
...dto
|
||||
}: TCreateUserPrivilegeDTO) => {
|
||||
const projectMembership = await projectMembershipDAL.findById(projectMembershipId);
|
||||
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
||||
if (!projectMembership) throw new NotFoundError({ message: "Project membership not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@ -94,14 +94,14 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
...dto
|
||||
}: TUpdateUserPrivilegeDTO) => {
|
||||
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
|
||||
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
|
||||
if (!userPrivilege) throw new NotFoundError({ message: "User additional privilege not found" });
|
||||
|
||||
const projectMembership = await projectMembershipDAL.findOne({
|
||||
userId: userPrivilege.userId,
|
||||
projectId: userPrivilege.projectId
|
||||
});
|
||||
|
||||
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
||||
if (!projectMembership) throw new NotFoundError({ message: "Project membership not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@ -147,13 +147,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
|
||||
const deleteById = async ({ actorId, actor, actorOrgId, actorAuthMethod, privilegeId }: TDeleteUserPrivilegeDTO) => {
|
||||
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
|
||||
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
|
||||
if (!userPrivilege) throw new NotFoundError({ message: "User additional privilege not found" });
|
||||
|
||||
const projectMembership = await projectMembershipDAL.findOne({
|
||||
userId: userPrivilege.userId,
|
||||
projectId: userPrivilege.projectId
|
||||
});
|
||||
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
||||
if (!projectMembership) throw new NotFoundError({ message: "Project membership not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@ -176,13 +176,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
actorAuthMethod
|
||||
}: TGetUserPrivilegeDetailsDTO) => {
|
||||
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
|
||||
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
|
||||
if (!userPrivilege) throw new NotFoundError({ message: "User additional privilege not found" });
|
||||
|
||||
const projectMembership = await projectMembershipDAL.findOne({
|
||||
userId: userPrivilege.userId,
|
||||
projectId: userPrivilege.projectId
|
||||
});
|
||||
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
||||
if (!projectMembership) throw new NotFoundError({ message: "Project membership not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@ -204,7 +204,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
actorAuthMethod
|
||||
}: TListUserPrivilegesDTO) => {
|
||||
const projectMembership = await projectMembershipDAL.findById(projectMembershipId);
|
||||
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
||||
if (!projectMembership) throw new NotFoundError({ message: "Project membership not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
|
@ -19,10 +19,11 @@ import {
|
||||
infisicalSymmetricDecrypt,
|
||||
infisicalSymmetricEncypt
|
||||
} from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { AuthTokenType } from "@app/services/auth/auth-type";
|
||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
||||
import { TIdentityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal";
|
||||
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||
@ -51,6 +52,8 @@ type TSamlConfigServiceFactoryDep = {
|
||||
TOrgDALFactory,
|
||||
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
|
||||
>;
|
||||
|
||||
identityMetadataDAL: Pick<TIdentityMetadataDALFactory, "delete" | "insertMany" | "transaction">;
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
|
||||
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
@ -71,7 +74,8 @@ export const samlConfigServiceFactory = ({
|
||||
permissionService,
|
||||
licenseService,
|
||||
tokenService,
|
||||
smtpService
|
||||
smtpService,
|
||||
identityMetadataDAL
|
||||
}: TSamlConfigServiceFactoryDep) => {
|
||||
const createSamlCfg = async ({
|
||||
cert,
|
||||
@ -187,7 +191,7 @@ export const samlConfigServiceFactory = ({
|
||||
|
||||
const updateQuery: TSamlConfigsUpdate = { authProvider, isActive, lastUsed: null };
|
||||
const orgBot = await orgBotDAL.findOne({ orgId });
|
||||
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
|
||||
if (!orgBot) throw new NotFoundError({ message: "Organization bot not found", name: "OrgBotNotFound" });
|
||||
const key = infisicalSymmetricDecrypt({
|
||||
ciphertext: orgBot.encryptedSymmetricKey,
|
||||
iv: orgBot.symmetricKeyIV,
|
||||
@ -253,7 +257,7 @@ export const samlConfigServiceFactory = ({
|
||||
|
||||
ssoConfig = await samlConfigDAL.findById(id);
|
||||
}
|
||||
if (!ssoConfig) throw new BadRequestError({ message: "Failed to find organization SSO data" });
|
||||
if (!ssoConfig) throw new NotFoundError({ message: "Failed to find organization SSO data" });
|
||||
|
||||
// when dto is type id means it's internally used
|
||||
if (dto.type === "org") {
|
||||
@ -279,7 +283,7 @@ export const samlConfigServiceFactory = ({
|
||||
} = ssoConfig;
|
||||
|
||||
const orgBot = await orgBotDAL.findOne({ orgId: ssoConfig.orgId });
|
||||
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
|
||||
if (!orgBot) throw new NotFoundError({ message: "Organization bot not found", name: "OrgBotNotFound" });
|
||||
const key = infisicalSymmetricDecrypt({
|
||||
ciphertext: orgBot.encryptedSymmetricKey,
|
||||
iv: orgBot.symmetricKeyIV,
|
||||
@ -332,13 +336,14 @@ export const samlConfigServiceFactory = ({
|
||||
lastName,
|
||||
authProvider,
|
||||
orgId,
|
||||
relayState
|
||||
relayState,
|
||||
metadata
|
||||
}: TSamlLoginDTO) => {
|
||||
const appCfg = getConfig();
|
||||
const serverCfg = await getServerCfg();
|
||||
|
||||
if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.SAML)) {
|
||||
throw new BadRequestError({
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Login with SAML is disabled by administrator."
|
||||
});
|
||||
}
|
||||
@ -350,7 +355,7 @@ export const samlConfigServiceFactory = ({
|
||||
});
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
if (!organization) throw new BadRequestError({ message: "Org not found" });
|
||||
if (!organization) throw new NotFoundError({ message: "Organization not found" });
|
||||
|
||||
let user: TUsers;
|
||||
if (userAlias) {
|
||||
@ -386,6 +391,21 @@ export const samlConfigServiceFactory = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (metadata && foundUser.id) {
|
||||
await identityMetadataDAL.delete({ userId: foundUser.id, orgId }, tx);
|
||||
if (metadata.length) {
|
||||
await identityMetadataDAL.insertMany(
|
||||
metadata.map(({ key, value }) => ({
|
||||
userId: foundUser.id,
|
||||
orgId,
|
||||
key,
|
||||
value
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return foundUser;
|
||||
});
|
||||
} else {
|
||||
@ -474,6 +494,20 @@ export const samlConfigServiceFactory = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (metadata && newUser.id) {
|
||||
await identityMetadataDAL.delete({ userId: newUser.id, orgId }, tx);
|
||||
if (metadata.length) {
|
||||
await identityMetadataDAL.insertMany(
|
||||
metadata.map(({ key, value }) => ({
|
||||
userId: newUser?.id,
|
||||
orgId,
|
||||
key,
|
||||
value
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
}
|
||||
return newUser;
|
||||
});
|
||||
}
|
||||
|
@ -53,4 +53,5 @@ export type TSamlLoginDTO = {
|
||||
orgId: string;
|
||||
// saml thingy
|
||||
relayState?: string;
|
||||
metadata?: { key: string; value: string }[];
|
||||
};
|
||||
|
@ -9,7 +9,7 @@ import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee
|
||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
import { AuthTokenType } from "@app/services/auth/auth-type";
|
||||
@ -75,7 +75,14 @@ type TScimServiceFactoryDep = {
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">;
|
||||
groupDAL: Pick<
|
||||
TGroupDALFactory,
|
||||
"create" | "findOne" | "findAllGroupMembers" | "delete" | "findGroups" | "transaction" | "updateById" | "update"
|
||||
| "create"
|
||||
| "findOne"
|
||||
| "findAllGroupPossibleMembers"
|
||||
| "delete"
|
||||
| "findGroups"
|
||||
| "transaction"
|
||||
| "updateById"
|
||||
| "update"
|
||||
>;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
userGroupMembershipDAL: Pick<
|
||||
@ -169,7 +176,7 @@ export const scimServiceFactory = ({
|
||||
|
||||
const deleteScimToken = async ({ scimTokenId, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteScimTokenDTO) => {
|
||||
let scimToken = await scimDAL.findById(scimTokenId);
|
||||
if (!scimToken) throw new BadRequestError({ message: "Failed to find SCIM token to delete" });
|
||||
if (!scimToken) throw new NotFoundError({ message: "Failed to find SCIM token to delete" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
@ -775,7 +782,7 @@ export const scimServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const users = await groupDAL.findAllGroupMembers({
|
||||
const users = await groupDAL.findAllGroupPossibleMembers({
|
||||
orgId: group.orgId,
|
||||
groupId: group.id
|
||||
});
|
||||
|
@ -1,33 +1,62 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { SecretApprovalPoliciesSchema, TableName, TSecretApprovalPolicies } from "@app/db/schemas";
|
||||
import { SecretApprovalPoliciesSchema, TableName, TSecretApprovalPolicies, TUsers } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
|
||||
|
||||
import { ApproverType } from "../access-approval-policy/access-approval-policy-types";
|
||||
|
||||
export type TSecretApprovalPolicyDALFactory = ReturnType<typeof secretApprovalPolicyDALFactory>;
|
||||
|
||||
export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
const secretApprovalPolicyOrm = ormify(db, TableName.SecretApprovalPolicy);
|
||||
|
||||
const secretApprovalPolicyFindQuery = (tx: Knex, filter: TFindFilter<TSecretApprovalPolicies>) =>
|
||||
const secretApprovalPolicyFindQuery = (
|
||||
tx: Knex,
|
||||
filter: TFindFilter<TSecretApprovalPolicies>,
|
||||
customFilter?: {
|
||||
sapId?: string;
|
||||
}
|
||||
) =>
|
||||
tx(TableName.SecretApprovalPolicy)
|
||||
// eslint-disable-next-line
|
||||
.where(buildFindFilter(filter))
|
||||
.where((qb) => {
|
||||
if (customFilter?.sapId) {
|
||||
void qb.where(`${TableName.SecretApprovalPolicy}.id`, "=", customFilter.sapId);
|
||||
}
|
||||
})
|
||||
.join(TableName.Environment, `${TableName.SecretApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||
.leftJoin(
|
||||
TableName.SecretApprovalPolicyApprover,
|
||||
`${TableName.SecretApprovalPolicy}.id`,
|
||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
|
||||
.leftJoin(TableName.Users, `${TableName.SecretApprovalPolicyApprover}.approverUserId`, `${TableName.Users}.id`)
|
||||
|
||||
.leftJoin(
|
||||
TableName.UserGroupMembership,
|
||||
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
|
||||
`${TableName.UserGroupMembership}.groupId`
|
||||
)
|
||||
.leftJoin<TUsers>(
|
||||
db(TableName.Users).as("secretApprovalPolicyApproverUser"),
|
||||
`${TableName.SecretApprovalPolicyApprover}.approverUserId`,
|
||||
"secretApprovalPolicyApproverUser.id"
|
||||
)
|
||||
.leftJoin<TUsers>(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
||||
.select(
|
||||
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||
tx.ref("email").withSchema(TableName.Users).as("approverEmail"),
|
||||
tx.ref("firstName").withSchema(TableName.Users).as("approverFirstName"),
|
||||
tx.ref("lastName").withSchema(TableName.Users).as("approverLastName")
|
||||
tx.ref("id").withSchema("secretApprovalPolicyApproverUser").as("approverUserId"),
|
||||
tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"),
|
||||
tx.ref("firstName").withSchema("secretApprovalPolicyApproverUser").as("approverFirstName"),
|
||||
tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"),
|
||||
tx.ref("lastName").withSchema("secretApprovalPolicyApproverUser").as("approverLastName")
|
||||
)
|
||||
.select(
|
||||
tx.ref("approverGroupId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||
tx.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
|
||||
tx.ref("email").withSchema(TableName.Users).as("approverGroupEmail"),
|
||||
tx.ref("firstName").withSchema(TableName.Users).as("approverGroupFirstName"),
|
||||
tx.ref("lastName").withSchema(TableName.Users).as("approverGroupLastName")
|
||||
)
|
||||
.select(
|
||||
tx.ref("name").withSchema(TableName.Environment).as("envName"),
|
||||
@ -55,11 +84,31 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "approverUserId",
|
||||
label: "userApprovers" as const,
|
||||
mapper: ({ approverUserId, approverEmail, approverFirstName, approverLastName }) => ({
|
||||
userId: approverUserId,
|
||||
email: approverEmail,
|
||||
firstName: approverFirstName,
|
||||
lastName: approverLastName
|
||||
mapper: ({
|
||||
approverUserId: userId,
|
||||
approverEmail: email,
|
||||
approverFirstName: firstName,
|
||||
approverLastName: lastName
|
||||
}) => ({
|
||||
userId,
|
||||
email,
|
||||
firstName,
|
||||
lastName
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "approverGroupUserId",
|
||||
label: "userApprovers" as const,
|
||||
mapper: ({
|
||||
approverGroupUserId: userId,
|
||||
approverGroupEmail: email,
|
||||
approverGroupFirstName: firstName,
|
||||
approverGroupLastName: lastName
|
||||
}) => ({
|
||||
userId,
|
||||
email,
|
||||
firstName,
|
||||
lastName
|
||||
})
|
||||
}
|
||||
]
|
||||
@ -71,9 +120,15 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const find = async (filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>, tx?: Knex) => {
|
||||
const find = async (
|
||||
filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>,
|
||||
customFilter?: {
|
||||
sapId?: string;
|
||||
},
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const docs = await secretApprovalPolicyFindQuery(tx || db.replicaNode(), filter);
|
||||
const docs = await secretApprovalPolicyFindQuery(tx || db.replicaNode(), filter, customFilter);
|
||||
const formatedDoc = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
@ -83,11 +138,35 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
...SecretApprovalPoliciesSchema.parse(data)
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "approverUserId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverUserId: id, approverUsername }) => ({
|
||||
type: ApproverType.User,
|
||||
name: approverUsername,
|
||||
id
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "approverGroupId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverGroupId: id }) => ({
|
||||
type: ApproverType.Group,
|
||||
id
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "approverUserId",
|
||||
label: "userApprovers" as const,
|
||||
mapper: ({ approverUserId }) => ({
|
||||
userId: approverUserId
|
||||
mapper: ({ approverUserId: userId }) => ({
|
||||
userId
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "approverGroupUserId",
|
||||
label: "userApprovers" as const,
|
||||
mapper: ({ approverGroupUserId: userId }) => ({
|
||||
userId
|
||||
})
|
||||
}
|
||||
]
|
||||
|
@ -3,11 +3,13 @@ import picomatch from "picomatch";
|
||||
|
||||
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 { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { containsGlobPatterns } from "@app/lib/picomatch";
|
||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { ApproverType } from "../access-approval-policy/access-approval-policy-types";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal";
|
||||
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
|
||||
@ -15,6 +17,7 @@ import {
|
||||
TCreateSapDTO,
|
||||
TDeleteSapDTO,
|
||||
TGetBoardSapDTO,
|
||||
TGetSapByIdDTO,
|
||||
TListSapDTO,
|
||||
TUpdateSapDTO
|
||||
} from "./secret-approval-policy-types";
|
||||
@ -28,6 +31,7 @@ type TSecretApprovalPolicyServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||
userDAL: Pick<TUserDALFactory, "find">;
|
||||
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
@ -39,6 +43,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
permissionService,
|
||||
secretApprovalPolicyApproverDAL,
|
||||
projectEnvDAL,
|
||||
userDAL,
|
||||
licenseService
|
||||
}: TSecretApprovalPolicyServiceFactoryDep) => {
|
||||
const createSecretApprovalPolicy = async ({
|
||||
@ -54,7 +59,19 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
environment,
|
||||
enforcementLevel
|
||||
}: TCreateSapDTO) => {
|
||||
if (approvals > approvers.length)
|
||||
const groupApprovers = approvers
|
||||
?.filter((approver) => approver.type === ApproverType.Group)
|
||||
.map((approver) => approver.id);
|
||||
const userApprovers = approvers
|
||||
?.filter((approver) => approver.type === ApproverType.User)
|
||||
.map((approver) => approver.id)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const userApproverNames = approvers
|
||||
.map((approver) => (approver.type === ApproverType.User ? approver.name : undefined))
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
if (!groupApprovers.length && approvals > approvers.length)
|
||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
@ -78,7 +95,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
}
|
||||
|
||||
const env = await projectEnvDAL.findOne({ slug: environment, projectId });
|
||||
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
||||
if (!env) throw new NotFoundError({ message: "Environment not found" });
|
||||
|
||||
const secretApproval = await secretApprovalPolicyDAL.transaction(async (tx) => {
|
||||
const doc = await secretApprovalPolicyDAL.create(
|
||||
@ -91,15 +108,48 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
let userApproverIds = userApprovers;
|
||||
if (userApproverNames.length) {
|
||||
const approverUsers = await userDAL.find(
|
||||
{
|
||||
$in: {
|
||||
username: userApproverNames
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
const approverNamesFromDb = approverUsers.map((user) => user.username);
|
||||
const invalidUsernames = userApproverNames?.filter((username) => !approverNamesFromDb.includes(username));
|
||||
|
||||
if (invalidUsernames?.length) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid approver user: ${invalidUsernames.join(", ")}`
|
||||
});
|
||||
}
|
||||
|
||||
userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id));
|
||||
}
|
||||
|
||||
await secretApprovalPolicyApproverDAL.insertMany(
|
||||
approvers.map((approverUserId) => ({
|
||||
userApproverIds.map((approverUserId) => ({
|
||||
approverUserId,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
|
||||
await secretApprovalPolicyApproverDAL.insertMany(
|
||||
groupApprovers.map((approverGroupId) => ({
|
||||
approverGroupId,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
return doc;
|
||||
});
|
||||
|
||||
return { ...secretApproval, environment: env, projectId };
|
||||
};
|
||||
|
||||
@ -115,8 +165,20 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
secretPolicyId,
|
||||
enforcementLevel
|
||||
}: TUpdateSapDTO) => {
|
||||
const groupApprovers = approvers
|
||||
?.filter((approver) => approver.type === ApproverType.Group)
|
||||
.map((approver) => approver.id);
|
||||
const userApprovers = approvers
|
||||
?.filter((approver) => approver.type === ApproverType.User)
|
||||
.map((approver) => approver.id)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const userApproverNames = approvers
|
||||
.map((approver) => (approver.type === ApproverType.User ? approver.name : undefined))
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const secretApprovalPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId);
|
||||
if (!secretApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
|
||||
if (!secretApprovalPolicy) throw new NotFoundError({ message: "Secret approval policy not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@ -146,16 +208,52 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
||||
|
||||
if (approvers) {
|
||||
await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
||||
let userApproverIds = userApprovers;
|
||||
if (userApproverNames) {
|
||||
const approverUsers = await userDAL.find(
|
||||
{
|
||||
$in: {
|
||||
username: userApproverNames
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
const approverNamesFromDb = approverUsers.map((user) => user.username);
|
||||
const invalidUsernames = userApproverNames?.filter((username) => !approverNamesFromDb.includes(username));
|
||||
|
||||
if (invalidUsernames?.length) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid approver user: ${invalidUsernames.join(", ")}`
|
||||
});
|
||||
}
|
||||
|
||||
userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id));
|
||||
}
|
||||
|
||||
await secretApprovalPolicyApproverDAL.insertMany(
|
||||
approvers.map((approverUserId) => ({
|
||||
userApproverIds.map((approverUserId) => ({
|
||||
approverUserId,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
if (groupApprovers) {
|
||||
await secretApprovalPolicyApproverDAL.insertMany(
|
||||
groupApprovers.map((approverGroupId) => ({
|
||||
approverGroupId,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return doc;
|
||||
});
|
||||
return {
|
||||
@ -173,7 +271,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
actorOrgId
|
||||
}: TDeleteSapDTO) => {
|
||||
const sapPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId);
|
||||
if (!sapPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
|
||||
if (!sapPolicy) throw new NotFoundError({ message: "Secret approval policy not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@ -222,7 +320,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
const getSecretApprovalPolicy = async (projectId: string, environment: string, path: string) => {
|
||||
const secretPath = removeTrailingSlash(path);
|
||||
const env = await projectEnvDAL.findOne({ slug: environment, projectId });
|
||||
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
||||
if (!env) throw new NotFoundError({ message: "Environment not found" });
|
||||
|
||||
const policies = await secretApprovalPolicyDAL.find({ envId: env.id });
|
||||
if (!policies.length) return;
|
||||
@ -260,12 +358,41 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
return getSecretApprovalPolicy(projectId, environment, secretPath);
|
||||
};
|
||||
|
||||
const getSecretApprovalPolicyById = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
sapId
|
||||
}: TGetSapByIdDTO) => {
|
||||
const [sapPolicy] = await secretApprovalPolicyDAL.find({}, { sapId });
|
||||
|
||||
if (!sapPolicy) {
|
||||
throw new NotFoundError({
|
||||
message: "Cannot find secret approval policy"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
sapPolicy.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
|
||||
return sapPolicy;
|
||||
};
|
||||
|
||||
return {
|
||||
createSecretApprovalPolicy,
|
||||
updateSecretApprovalPolicy,
|
||||
deleteSecretApprovalPolicy,
|
||||
getSecretApprovalPolicy,
|
||||
getSecretApprovalPolicyByProjectId,
|
||||
getSecretApprovalPolicyOfFolder
|
||||
getSecretApprovalPolicyOfFolder,
|
||||
getSecretApprovalPolicyById
|
||||
};
|
||||
};
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { EnforcementLevel, TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { ApproverType } from "../access-approval-policy/access-approval-policy-types";
|
||||
|
||||
export type TCreateSapDTO = {
|
||||
approvals: number;
|
||||
secretPath?: string | null;
|
||||
environment: string;
|
||||
approvers: string[];
|
||||
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
|
||||
projectId: string;
|
||||
name: string;
|
||||
enforcementLevel: EnforcementLevel;
|
||||
@ -14,7 +16,7 @@ export type TUpdateSapDTO = {
|
||||
secretPolicyId: string;
|
||||
approvals?: number;
|
||||
secretPath?: string | null;
|
||||
approvers: string[];
|
||||
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
|
||||
name?: string;
|
||||
enforcementLevel?: EnforcementLevel;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
@ -25,6 +27,8 @@ export type TDeleteSapDTO = {
|
||||
|
||||
export type TListSapDTO = TProjectPermission;
|
||||
|
||||
export type TGetSapByIdDTO = Omit<TProjectPermission, "projectId"> & { sapId: string };
|
||||
|
||||
export type TGetBoardSapDTO = {
|
||||
projectId: string;
|
||||
environment: string;
|
||||
|
@ -48,16 +48,26 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretApprovalRequest}.committerUserId`,
|
||||
`committerUser.id`
|
||||
)
|
||||
.join(
|
||||
.leftJoin(
|
||||
TableName.SecretApprovalPolicyApprover,
|
||||
`${TableName.SecretApprovalPolicy}.id`,
|
||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
.join<TUsers>(
|
||||
.leftJoin<TUsers>(
|
||||
db(TableName.Users).as("secretApprovalPolicyApproverUser"),
|
||||
`${TableName.SecretApprovalPolicyApprover}.approverUserId`,
|
||||
"secretApprovalPolicyApproverUser.id"
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.UserGroupMembership,
|
||||
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
|
||||
`${TableName.UserGroupMembership}.groupId`
|
||||
)
|
||||
.leftJoin<TUsers>(
|
||||
db(TableName.Users).as("secretApprovalPolicyGroupApproverUser"),
|
||||
`${TableName.UserGroupMembership}.userId`,
|
||||
`secretApprovalPolicyGroupApproverUser.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SecretApprovalRequestReviewer,
|
||||
`${TableName.SecretApprovalRequest}.id`,
|
||||
@ -71,10 +81,15 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||
.select(
|
||||
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||
tx.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
|
||||
tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"),
|
||||
tx.ref("email").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupEmail"),
|
||||
tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"),
|
||||
tx.ref("username").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupUsername"),
|
||||
tx.ref("firstName").withSchema("secretApprovalPolicyApproverUser").as("approverFirstName"),
|
||||
tx.ref("firstName").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupFirstName"),
|
||||
tx.ref("lastName").withSchema("secretApprovalPolicyApproverUser").as("approverLastName"),
|
||||
tx.ref("lastName").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupLastName"),
|
||||
tx.ref("email").withSchema("statusChangedByUser").as("statusChangedByUserEmail"),
|
||||
tx.ref("username").withSchema("statusChangedByUser").as("statusChangedByUserUsername"),
|
||||
tx.ref("firstName").withSchema("statusChangedByUser").as("statusChangedByUserFirstName"),
|
||||
@ -152,13 +167,30 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
key: "approverUserId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({
|
||||
approverUserId,
|
||||
approverUserId: userId,
|
||||
approverEmail: email,
|
||||
approverUsername: username,
|
||||
approverLastName: lastName,
|
||||
approverFirstName: firstName
|
||||
}) => ({
|
||||
userId: approverUserId,
|
||||
userId,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
username
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "approverGroupUserId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({
|
||||
approverGroupUserId: userId,
|
||||
approverGroupEmail: email,
|
||||
approverGroupUsername: username,
|
||||
approverGroupLastName: lastName,
|
||||
approverGroupFirstName: firstName
|
||||
}) => ({
|
||||
userId,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
@ -236,11 +268,16 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretApprovalRequest}.policyId`,
|
||||
`${TableName.SecretApprovalPolicy}.id`
|
||||
)
|
||||
.join(
|
||||
.leftJoin(
|
||||
TableName.SecretApprovalPolicyApprover,
|
||||
`${TableName.SecretApprovalPolicy}.id`,
|
||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.UserGroupMembership,
|
||||
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
|
||||
`${TableName.UserGroupMembership}.groupId`
|
||||
)
|
||||
.join<TUsers>(
|
||||
db(TableName.Users).as("committerUser"),
|
||||
`${TableName.SecretApprovalRequest}.committerUserId`,
|
||||
@ -269,6 +306,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
void bd
|
||||
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
|
||||
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
|
||||
.orWhere(`${TableName.UserGroupMembership}.userId`, userId)
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||
.select(
|
||||
@ -289,6 +327,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
||||
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
||||
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||
db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
|
||||
db.ref("email").withSchema("committerUser").as("committerUserEmail"),
|
||||
db.ref("username").withSchema("committerUser").as("committerUserUsername"),
|
||||
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
|
||||
@ -334,7 +373,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "approverUserId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverUserId }) => approverUserId
|
||||
mapper: ({ approverUserId }) => ({ userId: approverUserId })
|
||||
},
|
||||
{
|
||||
key: "commitId",
|
||||
@ -344,6 +383,11 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
id,
|
||||
secretId
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "approverGroupUserId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverGroupUserId }) => ({ userId: approverGroupUserId })
|
||||
}
|
||||
]
|
||||
});
|
||||
@ -371,11 +415,16 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretApprovalRequest}.policyId`,
|
||||
`${TableName.SecretApprovalPolicy}.id`
|
||||
)
|
||||
.join(
|
||||
.leftJoin(
|
||||
TableName.SecretApprovalPolicyApprover,
|
||||
`${TableName.SecretApprovalPolicy}.id`,
|
||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.UserGroupMembership,
|
||||
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
|
||||
`${TableName.UserGroupMembership}.groupId`
|
||||
)
|
||||
.join<TUsers>(
|
||||
db(TableName.Users).as("committerUser"),
|
||||
`${TableName.SecretApprovalRequest}.committerUserId`,
|
||||
@ -404,6 +453,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
void bd
|
||||
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
|
||||
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
|
||||
.orWhere(`${TableName.UserGroupMembership}.userId`, userId)
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||
.select(
|
||||
@ -424,6 +474,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
||||
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
||||
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||
db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
|
||||
db.ref("email").withSchema("committerUser").as("committerUserEmail"),
|
||||
db.ref("username").withSchema("committerUser").as("committerUserUsername"),
|
||||
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
|
||||
@ -469,7 +520,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "approverUserId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverUserId }) => approverUserId
|
||||
mapper: ({ approverUserId }) => ({ userId: approverUserId })
|
||||
},
|
||||
{
|
||||
key: "commitId",
|
||||
@ -479,6 +530,13 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
id,
|
||||
secretId
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "approverGroupUserId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverGroupUserId }) => ({
|
||||
userId: approverGroupUserId
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
TSecretApprovalRequestsSecrets,
|
||||
TSecretTags
|
||||
} from "@app/db/schemas";
|
||||
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||
import { DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||
|
||||
export type TSecretApprovalRequestSecretDALFactory = ReturnType<typeof secretApprovalRequestSecretDALFactory>;
|
||||
@ -31,7 +31,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
);
|
||||
|
||||
if (existingApprovalSecrets.length !== data.length) {
|
||||
throw new BadRequestError({ message: "Some of the secret approvals do not exist" });
|
||||
throw new NotFoundError({ message: "Some of the secret approvals do not exist" });
|
||||
}
|
||||
|
||||
if (data.length === 0) return [];
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
} from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { groupBy, pick, unique } from "@app/lib/fn";
|
||||
import { setKnexStringValue } from "@app/lib/knex";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
@ -204,7 +204,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||
|
||||
const secretApprovalRequest = await secretApprovalRequestDAL.findById(id);
|
||||
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||
if (!secretApprovalRequest) throw new NotFoundError({ message: "Secret approval request not found" });
|
||||
|
||||
const { projectId } = secretApprovalRequest;
|
||||
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||
@ -222,7 +222,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretApprovalRequest.committerUserId !== actorId &&
|
||||
!policy.approvers.find(({ userId }) => userId === actorId)
|
||||
) {
|
||||
throw new UnauthorizedError({ message: "User has no access" });
|
||||
throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
|
||||
}
|
||||
|
||||
let secrets;
|
||||
@ -271,7 +271,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
: undefined
|
||||
}));
|
||||
} else {
|
||||
if (!botKey) throw new BadRequestError({ message: "Bot key not found" });
|
||||
if (!botKey) throw new NotFoundError({ message: "Project bot key not found" });
|
||||
const encrypedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
|
||||
secrets = encrypedSecrets.map((el) => ({
|
||||
...el,
|
||||
@ -307,7 +307,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
actorOrgId
|
||||
}: TReviewRequestDTO) => {
|
||||
const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId);
|
||||
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||
if (!secretApprovalRequest) throw new NotFoundError({ message: "Secret approval request not found" });
|
||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
@ -331,7 +331,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretApprovalRequest.committerUserId !== actorId &&
|
||||
!policy.approvers.find(({ userId }) => userId === actorId)
|
||||
) {
|
||||
throw new UnauthorizedError({ message: "User has no access" });
|
||||
throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
|
||||
}
|
||||
const reviewStatus = await secretApprovalRequestReviewerDAL.transaction(async (tx) => {
|
||||
const review = await secretApprovalRequestReviewerDAL.findOne(
|
||||
@ -365,7 +365,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
actorAuthMethod
|
||||
}: TStatusChangeDTO) => {
|
||||
const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId);
|
||||
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||
if (!secretApprovalRequest) throw new NotFoundError({ message: "Secret approval request not found" });
|
||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
@ -389,7 +389,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretApprovalRequest.committerUserId !== actorId &&
|
||||
!policy.approvers.find(({ userId }) => userId === actorId)
|
||||
) {
|
||||
throw new UnauthorizedError({ message: "User has no access" });
|
||||
throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
|
||||
}
|
||||
|
||||
if (secretApprovalRequest.hasMerged) throw new BadRequestError({ message: "Approval request has been merged" });
|
||||
@ -414,7 +414,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
bypassReason
|
||||
}: TMergeSecretApprovalRequestDTO) => {
|
||||
const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId);
|
||||
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||
if (!secretApprovalRequest) throw new NotFoundError({ message: "Secret approval request not found" });
|
||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
@ -439,7 +439,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretApprovalRequest.committerUserId !== actorId &&
|
||||
!policy.approvers.find(({ userId }) => userId === actorId)
|
||||
) {
|
||||
throw new UnauthorizedError({ message: "User has no access" });
|
||||
throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
|
||||
}
|
||||
const reviewers = secretApprovalRequest.reviewers.reduce<Record<string, ApprovalStatus>>(
|
||||
(prev, curr) => ({ ...prev, [curr.userId.toString()]: curr.status as ApprovalStatus }),
|
||||
@ -447,8 +447,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
);
|
||||
const hasMinApproval =
|
||||
secretApprovalRequest.policy.approvals <=
|
||||
secretApprovalRequest.policy.approvers.filter(
|
||||
({ userId: approverId }) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
|
||||
secretApprovalRequest.policy.approvers.filter(({ userId: approverId }) =>
|
||||
approverId ? reviewers[approverId] === ApprovalStatus.APPROVED : false
|
||||
).length;
|
||||
const isSoftEnforcement = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft;
|
||||
|
||||
@ -462,7 +462,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
const secretApprovalSecrets = await secretApprovalRequestSecretDAL.findByRequestIdBridgeSecretV2(
|
||||
secretApprovalRequest.id
|
||||
);
|
||||
if (!secretApprovalSecrets) throw new BadRequestError({ message: "No secrets found" });
|
||||
if (!secretApprovalSecrets) throw new NotFoundError({ message: "No secrets found" });
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
@ -602,7 +602,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
});
|
||||
} else {
|
||||
const secretApprovalSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
|
||||
if (!secretApprovalSecrets) throw new BadRequestError({ message: "No secrets found" });
|
||||
if (!secretApprovalSecrets) throw new NotFoundError({ message: "No secrets found" });
|
||||
|
||||
const conflicts: Array<{ secretId: string; op: SecretOperations }> = [];
|
||||
let secretCreationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Create);
|
||||
@ -612,8 +612,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretDAL,
|
||||
inputSecrets: secretCreationCommits.map(({ secretBlindIndex }) => {
|
||||
if (!secretBlindIndex) {
|
||||
throw new BadRequestError({
|
||||
message: "Missing secret blind index"
|
||||
throw new NotFoundError({
|
||||
message: "Secret blind index not found"
|
||||
});
|
||||
}
|
||||
return { secretBlindIndex };
|
||||
@ -639,8 +639,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
.filter(({ secretBlindIndex, secret }) => secret && secret.secretBlindIndex !== secretBlindIndex)
|
||||
.map(({ secretBlindIndex }) => {
|
||||
if (!secretBlindIndex) {
|
||||
throw new BadRequestError({
|
||||
message: "Missing secret blind index"
|
||||
throw new NotFoundError({
|
||||
message: "Secret blind index not found"
|
||||
});
|
||||
}
|
||||
return { secretBlindIndex };
|
||||
@ -762,8 +762,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretQueueService,
|
||||
inputSecrets: secretDeletionCommits.map(({ secretBlindIndex }) => {
|
||||
if (!secretBlindIndex) {
|
||||
throw new BadRequestError({
|
||||
message: "Missing secret blind index"
|
||||
throw new NotFoundError({
|
||||
message: "Secret blind index not found"
|
||||
});
|
||||
}
|
||||
return { secretBlindIndex, type: SecretType.Shared };
|
||||
@ -789,7 +789,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
|
||||
await snapshotService.performSnapshot(folderId);
|
||||
const [folder] = await folderDAL.findSecretPathByFolderIds(projectId, [folderId]);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
if (!folder) throw new NotFoundError({ message: "Folder not found" });
|
||||
await secretQueueService.syncSecrets({
|
||||
projectId,
|
||||
secretPath: folder.path,
|
||||
@ -805,7 +805,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
const requestedByUser = await userDAL.findOne({ id: actorId });
|
||||
const approverUsers = await userDAL.find({
|
||||
$in: {
|
||||
id: policy.approvers.map((approver: { userId: string }) => approver.userId)
|
||||
id: policy.approvers.map((approver: { userId: string | null | undefined }) => approver.userId!)
|
||||
}
|
||||
});
|
||||
|
||||
@ -860,14 +860,14 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder)
|
||||
throw new BadRequestError({
|
||||
throw new NotFoundError({
|
||||
message: "Folder not found for the given environment slug & secret path",
|
||||
name: "GenSecretApproval"
|
||||
});
|
||||
const folderId = folder.id;
|
||||
|
||||
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
|
||||
if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Update secret" });
|
||||
if (!blindIndexCfg) throw new NotFoundError({ message: "Blind index not found", name: "Update secret" });
|
||||
|
||||
const commits: Omit<TSecretApprovalRequestsSecretsInsert, "requestId">[] = [];
|
||||
const commitTagIds: Record<string, string[]> = {};
|
||||
@ -961,7 +961,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretDAL
|
||||
});
|
||||
const secretsGroupedByBlindIndex = groupBy(secrets, (i) => {
|
||||
if (!i.secretBlindIndex) throw new BadRequestError({ message: "Missing secret blind index" });
|
||||
if (!i.secretBlindIndex) throw new NotFoundError({ message: "Secret blind index not found" });
|
||||
return i.secretBlindIndex;
|
||||
});
|
||||
const deletedSecretIds = deletedSecrets.map(
|
||||
@ -972,7 +972,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
...deletedSecrets.map((el) => {
|
||||
const secretId = secretsGroupedByBlindIndex[keyName2BlindIndex[el.secretName]][0].id;
|
||||
if (!latestSecretVersions[secretId].secretBlindIndex)
|
||||
throw new BadRequestError({ message: "Failed to find secret blind index" });
|
||||
throw new NotFoundError({ message: "Secret blind index not found" });
|
||||
return {
|
||||
op: SecretOperations.Delete as const,
|
||||
...latestSecretVersions[secretId],
|
||||
@ -988,7 +988,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
|
||||
const tagIds = unique(Object.values(commitTagIds).flat());
|
||||
const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : [];
|
||||
if (tagIds.length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
|
||||
if (tagIds.length !== tags.length) throw new NotFoundError({ message: "Tag not found" });
|
||||
|
||||
const secretApprovalRequest = await secretApprovalRequestDAL.transaction(async (tx) => {
|
||||
const doc = await secretApprovalRequestDAL.create(
|
||||
@ -1054,7 +1054,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
|
||||
const commitsGroupByBlindIndex = groupBy(approvalCommits, (i) => {
|
||||
if (!i.secretBlindIndex) {
|
||||
throw new BadRequestError({ message: "Missing secret blind index" });
|
||||
throw new NotFoundError({ message: "Secret blind index not found" });
|
||||
}
|
||||
return i.secretBlindIndex;
|
||||
});
|
||||
@ -1132,7 +1132,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder)
|
||||
throw new BadRequestError({
|
||||
throw new NotFoundError({
|
||||
message: "Folder not found for the given environment slug & secret path",
|
||||
name: "GenSecretApproval"
|
||||
});
|
||||
@ -1191,8 +1191,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
}))
|
||||
);
|
||||
if (secretsToUpdateStoredInDB.length !== secretsToUpdate.length)
|
||||
throw new BadRequestError({
|
||||
message: `Secret not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}`
|
||||
throw new NotFoundError({
|
||||
message: `Secret does not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}`
|
||||
});
|
||||
|
||||
// now find any secret that needs to update its name
|
||||
@ -1207,8 +1207,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
}))
|
||||
);
|
||||
if (secrets.length)
|
||||
throw new BadRequestError({
|
||||
message: `Secret not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}`
|
||||
throw new NotFoundError({
|
||||
message: `Secret does not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}`
|
||||
});
|
||||
}
|
||||
|
||||
@ -1267,8 +1267,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
}))
|
||||
);
|
||||
if (secretsToDeleteInDB.length !== deletedSecrets.length)
|
||||
throw new BadRequestError({
|
||||
message: `Secret not exist: ${secretsToDeleteInDB.map((el) => el.key).join(",")}`
|
||||
throw new NotFoundError({
|
||||
message: `Secret does not exist: ${secretsToDeleteInDB.map((el) => el.key).join(",")}`
|
||||
});
|
||||
const secretsGroupedByKey = groupBy(secretsToDeleteInDB, (i) => i.key);
|
||||
const deletedSecretIds = deletedSecrets.map((el) => secretsGroupedByKey[el.secretKey][0].id);
|
||||
@ -1291,7 +1291,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
|
||||
const tagIds = unique(Object.values(commitTagIds).flat());
|
||||
const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : [];
|
||||
if (tagIds.length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
|
||||
if (tagIds.length !== tags.length) throw new NotFoundError({ message: "Tag not found" });
|
||||
|
||||
const secretApprovalRequest = await secretApprovalRequestDAL.transaction(async (tx) => {
|
||||
const doc = await secretApprovalRequestDAL.create(
|
||||
|
@ -4,7 +4,7 @@ import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approv
|
||||
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
|
||||
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
import { groupBy, unique } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
@ -295,7 +295,7 @@ export const secretReplicationServiceFactory = ({
|
||||
const [destinationFolder] = await folderDAL.findSecretPathByFolderIds(projectId, [
|
||||
destinationSecretImport.folderId
|
||||
]);
|
||||
if (!destinationFolder) throw new BadRequestError({ message: "Imported folder not found" });
|
||||
if (!destinationFolder) throw new NotFoundError({ message: "Imported folder not found" });
|
||||
|
||||
let destinationReplicationFolder = await folderDAL.findOne({
|
||||
parentId: destinationFolder.id,
|
||||
@ -506,7 +506,7 @@ export const secretReplicationServiceFactory = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!botKey) throw new BadRequestError({ message: "Bot not found" });
|
||||
if (!botKey) throw new NotFoundError({ message: "Project bot not found" });
|
||||
// these are the secrets to be added in replicated folders
|
||||
const sourceLocalSecrets = await secretDAL.find({ folderId: folder.id, type: SecretType.Shared });
|
||||
const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id });
|
||||
@ -545,7 +545,7 @@ export const secretReplicationServiceFactory = ({
|
||||
const [destinationFolder] = await folderDAL.findSecretPathByFolderIds(projectId, [
|
||||
destinationSecretImport.folderId
|
||||
]);
|
||||
if (!destinationFolder) throw new BadRequestError({ message: "Imported folder not found" });
|
||||
if (!destinationFolder) throw new NotFoundError({ message: "Imported folder not found" });
|
||||
|
||||
let destinationReplicationFolder = await folderDAL.findOne({
|
||||
parentId: destinationFolder.id,
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
infisicalSymmetricEncypt
|
||||
} from "@app/lib/crypto/encryption";
|
||||
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
@ -332,7 +332,7 @@ export const secretRotationQueueFactory = ({
|
||||
);
|
||||
});
|
||||
} else {
|
||||
if (!botKey) throw new BadRequestError({ message: "Bot not found" });
|
||||
if (!botKey) throw new NotFoundError({ message: "Project bot not found" });
|
||||
const encryptedSecrets = rotationOutputs.map(({ key: outputKey, secretId }) => ({
|
||||
secretId,
|
||||
value: encryptSymmetric128BitHexKeyUTF8(
|
||||
@ -372,7 +372,7 @@ export const secretRotationQueueFactory = ({
|
||||
);
|
||||
await secretVersionDAL.insertMany(
|
||||
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => {
|
||||
if (!el.secretBlindIndex) throw new BadRequestError({ message: "Missing blind index" });
|
||||
if (!el.secretBlindIndex) throw new NotFoundError({ message: "Secret blind index not found" });
|
||||
return {
|
||||
...el,
|
||||
secretId: id,
|
||||
|
@ -3,7 +3,7 @@ import Ajv from "ajv";
|
||||
|
||||
import { ProjectVersion } from "@app/db/schemas";
|
||||
import { decryptSymmetric128BitHexKeyUTF8, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
@ -94,7 +94,7 @@ export const secretRotationServiceFactory = ({
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder) throw new BadRequestError({ message: "Secret path not found" });
|
||||
if (!folder) throw new NotFoundError({ message: "Secret path not found" });
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
@ -108,14 +108,14 @@ export const secretRotationServiceFactory = ({
|
||||
$in: { id: Object.values(outputs) }
|
||||
});
|
||||
if (selectedSecrets.length !== Object.values(outputs).length)
|
||||
throw new BadRequestError({ message: "Secrets not found" });
|
||||
throw new NotFoundError({ message: "Secrets not found" });
|
||||
} else {
|
||||
const selectedSecrets = await secretDAL.find({
|
||||
folderId: folder.id,
|
||||
$in: { id: Object.values(outputs) }
|
||||
});
|
||||
if (selectedSecrets.length !== Object.values(outputs).length)
|
||||
throw new BadRequestError({ message: "Secrets not found" });
|
||||
throw new NotFoundError({ message: "Secrets not found" });
|
||||
}
|
||||
|
||||
const plan = await licenseService.getPlan(project.orgId);
|
||||
@ -125,7 +125,7 @@ export const secretRotationServiceFactory = ({
|
||||
});
|
||||
|
||||
const selectedTemplate = rotationTemplates.find(({ name }) => name === provider);
|
||||
if (!selectedTemplate) throw new BadRequestError({ message: "Provider not found" });
|
||||
if (!selectedTemplate) throw new NotFoundError({ message: "Provider not found" });
|
||||
const formattedInputs: Record<string, unknown> = {};
|
||||
Object.entries(inputs).forEach(([key, value]) => {
|
||||
const { type } = selectedTemplate.template.inputs.properties[key];
|
||||
@ -198,7 +198,7 @@ export const secretRotationServiceFactory = ({
|
||||
return docs;
|
||||
}
|
||||
|
||||
if (!botKey) throw new BadRequestError({ message: "bot not found" });
|
||||
if (!botKey) throw new NotFoundError({ message: "Project bot not found" });
|
||||
const docs = await secretRotationDAL.find({ projectId });
|
||||
return docs.map((el) => ({
|
||||
...el,
|
||||
@ -220,7 +220,7 @@ export const secretRotationServiceFactory = ({
|
||||
|
||||
const restartById = async ({ actor, actorId, actorOrgId, actorAuthMethod, rotationId }: TRestartDTO) => {
|
||||
const doc = await secretRotationDAL.findById(rotationId);
|
||||
if (!doc) throw new BadRequestError({ message: "Rotation not found" });
|
||||
if (!doc) throw new NotFoundError({ message: "Rotation not found" });
|
||||
|
||||
const project = await projectDAL.findById(doc.projectId);
|
||||
const plan = await licenseService.getPlan(project.orgId);
|
||||
@ -244,7 +244,7 @@ export const secretRotationServiceFactory = ({
|
||||
|
||||
const deleteById = async ({ actor, actorId, actorOrgId, actorAuthMethod, rotationId }: TDeleteDTO) => {
|
||||
const doc = await secretRotationDAL.findById(rotationId);
|
||||
if (!doc) throw new BadRequestError({ message: "Rotation not found" });
|
||||
if (!doc) throw new NotFoundError({ message: "Rotation not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
|
@ -7,7 +7,7 @@ import { ProbotOctokit } from "probot";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { TGitAppDALFactory } from "./git-app-dal";
|
||||
import { TGitAppInstallSessionDALFactory } from "./git-app-install-session-dal";
|
||||
@ -63,7 +63,7 @@ export const secretScanningServiceFactory = ({
|
||||
actorOrgId
|
||||
}: TLinkInstallSessionDTO) => {
|
||||
const session = await gitAppInstallSessionDAL.findOne({ sessionId });
|
||||
if (!session) throw new UnauthorizedError({ message: "Session not found" });
|
||||
if (!session) throw new NotFoundError({ message: "Session was not found" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
|
@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas";
|
||||
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||
import { InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
@ -99,7 +99,7 @@ export const secretSnapshotServiceFactory = ({
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
if (!folder) throw new NotFoundError({ message: "Folder not found" });
|
||||
|
||||
return snapshotDAL.countOfSnapshotsByFolderId(folder.id);
|
||||
};
|
||||
@ -131,7 +131,7 @@ export const secretSnapshotServiceFactory = ({
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
if (!folder) throw new NotFoundError({ message: "Folder not found" });
|
||||
|
||||
const snapshots = await snapshotDAL.find({ folderId: folder.id }, { limit, offset, sort: [["createdAt", "desc"]] });
|
||||
return snapshots;
|
||||
@ -139,7 +139,7 @@ export const secretSnapshotServiceFactory = ({
|
||||
|
||||
const getSnapshotData = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TGetSnapshotDataDTO) => {
|
||||
const snapshot = await snapshotDAL.findById(id);
|
||||
if (!snapshot) throw new BadRequestError({ message: "Snapshot not found" });
|
||||
if (!snapshot) throw new NotFoundError({ message: "Snapshot not found" });
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@ -173,7 +173,7 @@ export const secretSnapshotServiceFactory = ({
|
||||
} else {
|
||||
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotDataById(id);
|
||||
const { botKey } = await projectBotService.getBotKey(snapshot.projectId);
|
||||
if (!botKey) throw new BadRequestError({ message: "bot not found" });
|
||||
if (!botKey) throw new NotFoundError({ message: "Project bot not found" });
|
||||
snapshotDetails = {
|
||||
...encryptedSnapshotDetails,
|
||||
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({
|
||||
@ -225,7 +225,7 @@ export const secretSnapshotServiceFactory = ({
|
||||
try {
|
||||
if (!licenseService.isValidLicense) throw new InternalServerError({ message: "Invalid license" });
|
||||
const folder = await folderDAL.findById(folderId);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
if (!folder) throw new NotFoundError({ message: "Folder not found" });
|
||||
const shouldUseSecretV2Bridge = folder.projectVersion === 3;
|
||||
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
@ -309,7 +309,7 @@ export const secretSnapshotServiceFactory = ({
|
||||
actorOrgId
|
||||
}: TRollbackSnapshotDTO) => {
|
||||
const snapshot = await snapshotDAL.findById(snapshotId);
|
||||
if (!snapshot) throw new BadRequestError({ message: "Snapshot not found" });
|
||||
if (!snapshot) throw new NotFoundError({ message: "Snapshot not found" });
|
||||
const shouldUseBridge = snapshot.projectVersion === 3;
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
|
@ -16,6 +16,9 @@ export const KeyStorePrefixes = {
|
||||
WaitUntilReadyKmsOrgKeyCreation: "wait-until-ready-kms-org-key-creation-",
|
||||
WaitUntilReadyKmsOrgDataKeyCreation: "wait-until-ready-kms-org-data-key-creation-",
|
||||
|
||||
WaitUntilReadyProjectEnvironmentOperation: (projectId: string) =>
|
||||
`wait-until-ready-project-environments-operation-${projectId}`,
|
||||
ProjectEnvironmentLock: (projectId: string) => `project-environment-lock-${projectId}` as const,
|
||||
SyncSecretIntegrationLock: (projectId: string, environmentSlug: string, secretPath: string) =>
|
||||
`sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const,
|
||||
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
|
||||
|
@ -5,26 +5,30 @@ export const GROUPS = {
|
||||
role: "The role of the group to create."
|
||||
},
|
||||
UPDATE: {
|
||||
currentSlug: "The current slug of the group to update.",
|
||||
id: "The id of the group to update",
|
||||
name: "The new name of the group to update to.",
|
||||
slug: "The new slug of the group to update to.",
|
||||
role: "The new role of the group to update to."
|
||||
},
|
||||
DELETE: {
|
||||
id: "The id of the group to delete",
|
||||
slug: "The slug of the group to delete"
|
||||
},
|
||||
LIST_USERS: {
|
||||
slug: "The slug of the group to list users for",
|
||||
id: "The id of the group to list users for",
|
||||
offset: "The offset to start from. If you enter 10, it will start from the 10th user.",
|
||||
limit: "The number of users to return.",
|
||||
username: "The username to search for."
|
||||
},
|
||||
ADD_USER: {
|
||||
slug: "The slug of the group to add the user to.",
|
||||
id: "The id of the group to add the user to.",
|
||||
username: "The username of the user to add to the group."
|
||||
},
|
||||
GET_BY_ID: {
|
||||
id: "The id of the group to fetch"
|
||||
},
|
||||
DELETE_USER: {
|
||||
slug: "The slug of the group to remove the user from.",
|
||||
id: "The id of the group to remove the user from.",
|
||||
username: "The username of the user to remove from the group."
|
||||
}
|
||||
} as const;
|
||||
@ -356,7 +360,11 @@ export const ORGANIZATIONS = {
|
||||
organizationId: "The ID of the organization to update the membership for.",
|
||||
membershipId: "The ID of the membership to update.",
|
||||
role: "The new role of the membership.",
|
||||
isActive: "The active status of the membership"
|
||||
isActive: "The active status of the membership",
|
||||
metadata: {
|
||||
key: "The key for user metadata tag.",
|
||||
value: "The value for user metadata tag."
|
||||
}
|
||||
},
|
||||
DELETE_USER_MEMBERSHIP: {
|
||||
organizationId: "The ID of the organization to delete the membership from.",
|
||||
@ -409,21 +417,21 @@ export const PROJECTS = {
|
||||
secretSnapshotId: "The ID of the snapshot to rollback to."
|
||||
},
|
||||
ADD_GROUP_TO_PROJECT: {
|
||||
projectSlug: "The slug of the project to add the group to.",
|
||||
groupSlug: "The slug of the group to add to the project.",
|
||||
projectId: "The ID of the project to add the group to.",
|
||||
groupId: "The ID of the group to add to the project.",
|
||||
role: "The role for the group to assume in the project."
|
||||
},
|
||||
UPDATE_GROUP_IN_PROJECT: {
|
||||
projectSlug: "The slug of the project to update the group in.",
|
||||
groupSlug: "The slug of the group to update in the project.",
|
||||
projectId: "The ID of the project to update the group in.",
|
||||
groupId: "The ID of the group to update in the project.",
|
||||
roles: "A list of roles to update the group to."
|
||||
},
|
||||
REMOVE_GROUP_FROM_PROJECT: {
|
||||
projectSlug: "The slug of the project to delete the group from.",
|
||||
groupSlug: "The slug of the group to delete from the project."
|
||||
projectId: "The ID of the project to delete the group from.",
|
||||
groupId: "The ID of the group to delete from the project."
|
||||
},
|
||||
LIST_GROUPS_IN_PROJECT: {
|
||||
projectSlug: "The slug of the project to list groups for."
|
||||
projectId: "The ID of the project to list groups for."
|
||||
},
|
||||
LIST_INTEGRATION: {
|
||||
workspaceId: "The ID of the project to list integrations for."
|
||||
@ -667,6 +675,9 @@ export const SECRET_IMPORTS = {
|
||||
environment: "The slug of the environment to list secret imports from.",
|
||||
path: "The path to list secret imports from."
|
||||
},
|
||||
GET: {
|
||||
secretImportId: "The ID of the secret import to fetch."
|
||||
},
|
||||
CREATE: {
|
||||
environment: "The slug of the environment to import into.",
|
||||
path: "The path to import into.",
|
||||
@ -697,11 +708,46 @@ export const SECRET_IMPORTS = {
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const DASHBOARD = {
|
||||
SECRET_OVERVIEW_LIST: {
|
||||
projectId: "The ID of the project to list secrets/folders from.",
|
||||
environments:
|
||||
"The slugs of the environments to list secrets/folders from (comma separated, ie 'environments=dev,staging,prod').",
|
||||
secretPath: "The secret path to list secrets/folders from.",
|
||||
offset: "The offset to start from. If you enter 10, it will start from the 10th secret/folder.",
|
||||
limit: "The number of secrets/folders to return.",
|
||||
orderBy: "The column to order secrets/folders by.",
|
||||
orderDirection: "The direction to order secrets/folders in.",
|
||||
search: "The text string to filter secret keys and folder names by.",
|
||||
includeSecrets: "Whether to include project secrets in the response.",
|
||||
includeFolders: "Whether to include project folders in the response.",
|
||||
includeDynamicSecrets: "Whether to include dynamic project secrets in the response."
|
||||
},
|
||||
SECRET_DETAILS_LIST: {
|
||||
projectId: "The ID of the project to list secrets/folders from.",
|
||||
environment: "The slug of the environment to list secrets/folders from.",
|
||||
secretPath: "The secret path to list secrets/folders from.",
|
||||
offset: "The offset to start from. If you enter 10, it will start from the 10th secret/folder.",
|
||||
limit: "The number of secrets/folders to return.",
|
||||
orderBy: "The column to order secrets/folders by.",
|
||||
orderDirection: "The direction to order secrets/folders in.",
|
||||
search: "The text string to filter secret keys and folder names by.",
|
||||
tags: "The tags to filter secrets by (comma separated, ie 'tags=billing,engineering').",
|
||||
includeSecrets: "Whether to include project secrets in the response.",
|
||||
includeFolders: "Whether to include project folders in the response.",
|
||||
includeImports: "Whether to include project secret imports in the response.",
|
||||
includeDynamicSecrets: "Whether to include dynamic project secrets in the response."
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const AUDIT_LOGS = {
|
||||
EXPORT: {
|
||||
workspaceId: "The ID of the project to export audit logs from.",
|
||||
projectId:
|
||||
"Optionally filter logs by project ID. If not provided, logs from the entire organization will be returned.",
|
||||
eventType: "The type of the event to export.",
|
||||
userAgentType: "Choose which consuming application to export audit logs for.",
|
||||
eventMetadata:
|
||||
"Filter by event metadata key-value pairs. Formatted as `key1=value1,key2=value2`, with comma-separation.",
|
||||
startDate: "The date to start the export from.",
|
||||
endDate: "The date to end the export at.",
|
||||
offset: "The offset to start from. If you enter 10, it will start from the 10th audit log.",
|
||||
@ -1304,3 +1350,37 @@ export const PROJECT_ROLE = {
|
||||
projectSlug: "The slug of the project to list the roles of."
|
||||
}
|
||||
};
|
||||
|
||||
export const KMS = {
|
||||
CREATE_KEY: {
|
||||
projectId: "The ID of the project to create the key in.",
|
||||
name: "The name of the key to be created. Must be slug-friendly.",
|
||||
description: "An optional description of the key.",
|
||||
encryptionAlgorithm: "The algorithm to use when performing cryptographic operations with the key."
|
||||
},
|
||||
UPDATE_KEY: {
|
||||
keyId: "The ID of the key to be updated.",
|
||||
name: "The updated name of this key. Must be slug-friendly.",
|
||||
description: "The updated description of this key.",
|
||||
isDisabled: "The flag to enable or disable this key."
|
||||
},
|
||||
DELETE_KEY: {
|
||||
keyId: "The ID of the key to be deleted."
|
||||
},
|
||||
LIST_KEYS: {
|
||||
projectId: "The ID of the project to list keys from.",
|
||||
offset: "The offset to start from. If you enter 10, it will start from the 10th key.",
|
||||
limit: "The number of keys to return.",
|
||||
orderBy: "The column to order keys by.",
|
||||
orderDirection: "The direction to order keys in.",
|
||||
search: "The text string to filter key names by."
|
||||
},
|
||||
ENCRYPT: {
|
||||
keyId: "The ID of the key to encrypt the data with.",
|
||||
plaintext: "The plaintext to be encrypted (base64 encoded)."
|
||||
},
|
||||
DECRYPT: {
|
||||
keyId: "The ID of the key to decrypt the data with.",
|
||||
ciphertext: "The ciphertext to be decrypted (base64 encoded)."
|
||||
}
|
||||
};
|
||||
|
28
backend/src/lib/base64/index.ts
Normal file
28
backend/src/lib/base64/index.ts
Normal file
@ -0,0 +1,28 @@
|
||||
// Credit: https://github.com/miguelmota/is-base64
|
||||
export const isBase64 = (
|
||||
v: string,
|
||||
opts = { allowEmpty: false, mimeRequired: false, allowMime: true, paddingRequired: false }
|
||||
) => {
|
||||
if (opts.allowEmpty === false && v === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
let regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+/]{3}=)?";
|
||||
const mimeRegex = "(data:\\w+\\/[a-zA-Z\\+\\-\\.]+;base64,)";
|
||||
|
||||
if (opts.mimeRequired === true) {
|
||||
regex = mimeRegex + regex;
|
||||
} else if (opts.allowMime === true) {
|
||||
regex = `${mimeRegex}?${regex}`;
|
||||
}
|
||||
|
||||
if (opts.paddingRequired === false) {
|
||||
regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}(==)?|[A-Za-z0-9+\\/]{3}=?)?";
|
||||
}
|
||||
|
||||
return new RegExp(`^${regex}$`, "gi").test(v);
|
||||
};
|
||||
|
||||
export const getBase64SizeInBytes = (base64String: string) => {
|
||||
return Buffer.from(base64String, "base64").length;
|
||||
};
|
@ -23,8 +23,19 @@ export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
|
||||
/**
|
||||
* Extracts and formats permissions from a CASL Ability object or a raw permission set.
|
||||
*/
|
||||
const extractPermissions = (ability: MongoAbility) =>
|
||||
ability.rules.map((permission) => `${permission.action as string}_${permission.subject as string}`);
|
||||
const extractPermissions = (ability: MongoAbility) => {
|
||||
const permissions: string[] = [];
|
||||
ability.rules.forEach((permission) => {
|
||||
if (typeof permission.action === "string") {
|
||||
permissions.push(`${permission.action}_${permission.subject as string}`);
|
||||
} else {
|
||||
permission.action.forEach((permissionAction) => {
|
||||
permissions.push(`${permissionAction}_${permission.subject as string}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
return permissions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compares two sets of permissions to determine if the first set is at least as privileged as the second set.
|
||||
|
111
backend/src/lib/casl/knex.ts
Normal file
111
backend/src/lib/casl/knex.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { AnyAbility, ExtractSubjectType } from "@casl/ability";
|
||||
import { AbilityQuery, rulesToQuery } from "@casl/ability/extra";
|
||||
import { Tables } from "knex/types/tables";
|
||||
|
||||
import { BadRequestError, UnauthorizedError } from "../errors";
|
||||
import { TKnexDynamicOperator } from "../knex/dynamic";
|
||||
|
||||
type TBuildKnexQueryFromCaslDTO<K extends AnyAbility> = {
|
||||
ability: K;
|
||||
subject: ExtractSubjectType<Parameters<K["rulesFor"]>[1]>;
|
||||
action: Parameters<K["rulesFor"]>[0];
|
||||
};
|
||||
|
||||
export const buildKnexQueryFromCaslOperators = <K extends AnyAbility>({
|
||||
ability,
|
||||
subject,
|
||||
action
|
||||
}: TBuildKnexQueryFromCaslDTO<K>) => {
|
||||
const query = rulesToQuery(ability, action, subject, (rule) => {
|
||||
if (!rule.ast) throw new Error("Ast not defined");
|
||||
return rule.ast;
|
||||
});
|
||||
|
||||
if (query === null) throw new UnauthorizedError({ message: `You don't have permission to do ${action} ${subject}` });
|
||||
return query;
|
||||
};
|
||||
|
||||
type TFieldMapper<T extends keyof Tables> = {
|
||||
[K in T]: `${K}.${Exclude<keyof Tables[K]["base"], symbol>}`;
|
||||
}[T];
|
||||
|
||||
type TFormatCaslFieldsWithTableNames<T extends keyof Tables> = {
|
||||
// handle if any missing operator else throw error let the app break because this is executing again the db
|
||||
missingOperatorCallback?: (operator: string) => void;
|
||||
fieldMapping: (arg: string) => TFieldMapper<T> | null;
|
||||
dynamicQuery: TKnexDynamicOperator;
|
||||
};
|
||||
|
||||
export const formatCaslOperatorFieldsWithTableNames = <T extends keyof Tables>({
|
||||
missingOperatorCallback = (arg) => {
|
||||
throw new BadRequestError({ message: `Unknown permission operator: ${arg}` });
|
||||
},
|
||||
dynamicQuery: dynamicQueryAst,
|
||||
fieldMapping
|
||||
}: TFormatCaslFieldsWithTableNames<T>) => {
|
||||
const stack: [TKnexDynamicOperator, TKnexDynamicOperator | null][] = [[dynamicQueryAst, null]];
|
||||
|
||||
while (stack.length) {
|
||||
const [filterAst, parentAst] = stack.pop()!;
|
||||
|
||||
if (filterAst.operator === "and" || filterAst.operator === "or" || filterAst.operator === "not") {
|
||||
filterAst.value.forEach((el) => {
|
||||
stack.push([el, filterAst]);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
filterAst.operator === "eq" ||
|
||||
filterAst.operator === "ne" ||
|
||||
filterAst.operator === "in" ||
|
||||
filterAst.operator === "endsWith" ||
|
||||
filterAst.operator === "startsWith"
|
||||
) {
|
||||
const attrPath = fieldMapping(filterAst.field);
|
||||
if (attrPath) {
|
||||
filterAst.field = attrPath;
|
||||
} else if (parentAst && Array.isArray(parentAst.value)) {
|
||||
parentAst.value = parentAst.value.filter((childAst) => childAst !== filterAst) as string[];
|
||||
} else throw new Error("Unknown casl field");
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parentAst && Array.isArray(parentAst.value)) {
|
||||
parentAst.value = parentAst.value.filter((childAst) => childAst !== filterAst) as string[];
|
||||
} else {
|
||||
missingOperatorCallback?.(filterAst.operator);
|
||||
}
|
||||
}
|
||||
return dynamicQueryAst;
|
||||
};
|
||||
|
||||
export const convertCaslOperatorToKnexOperator = <T extends keyof Tables>(
|
||||
caslKnexOperators: AbilityQuery,
|
||||
fieldMapping: (arg: string) => TFieldMapper<T> | null
|
||||
) => {
|
||||
const value = [];
|
||||
if (caslKnexOperators.$and) {
|
||||
value.push({
|
||||
operator: "not" as const,
|
||||
value: caslKnexOperators.$and as TKnexDynamicOperator[]
|
||||
});
|
||||
}
|
||||
if (caslKnexOperators.$or) {
|
||||
value.push({
|
||||
operator: "or" as const,
|
||||
value: caslKnexOperators.$or as TKnexDynamicOperator[]
|
||||
});
|
||||
}
|
||||
|
||||
return formatCaslOperatorFieldsWithTableNames({
|
||||
dynamicQuery: {
|
||||
operator: "and",
|
||||
value
|
||||
},
|
||||
fieldMapping
|
||||
});
|
||||
};
|
@ -40,9 +40,9 @@ export class ForbiddenRequestError extends Error {
|
||||
|
||||
error: unknown;
|
||||
|
||||
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown }) {
|
||||
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown } = {}) {
|
||||
super(message ?? "You are not allowed to access this resource");
|
||||
this.name = name || "ForbideenError";
|
||||
this.name = name || "ForbiddenError";
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
|
@ -52,3 +52,21 @@ export const unique = <T, K extends string | number | symbol>(array: readonly T[
|
||||
);
|
||||
return Object.values(valueMap);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert an array to a dictionary by mapping each item
|
||||
* into a dictionary key & value
|
||||
*/
|
||||
export const objectify = <T, Key extends string | number | symbol, Value = T>(
|
||||
array: readonly T[],
|
||||
getKey: (item: T) => Key,
|
||||
getValue: (item: T) => Value = (item) => item as unknown as Value
|
||||
): Record<Key, Value> => {
|
||||
return array.reduce(
|
||||
(acc, item) => {
|
||||
acc[getKey(item)] = getValue(item);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<Key, Value>
|
||||
);
|
||||
};
|
||||
|
@ -9,3 +9,8 @@ export const removeTrailingSlash = (str: string) => {
|
||||
|
||||
return str.endsWith("/") ? str.slice(0, -1) : str;
|
||||
};
|
||||
|
||||
export const prefixWithSlash = (str: string) => {
|
||||
if (str.startsWith("/")) return str;
|
||||
return `/${str}`;
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import net from "node:net";
|
||||
|
||||
import { UnauthorizedError } from "../errors";
|
||||
import { ForbiddenRequestError } from "../errors";
|
||||
|
||||
export enum IPType {
|
||||
IPV4 = "ipv4",
|
||||
@ -126,7 +126,7 @@ export const checkIPAgainstBlocklist = ({ ipAddress, trustedIps }: { ipAddress:
|
||||
const check = blockList.check(ipAddress, type);
|
||||
|
||||
if (!check)
|
||||
throw new UnauthorizedError({
|
||||
message: "Failed to authenticate"
|
||||
throw new ForbiddenRequestError({
|
||||
message: "You are not allowed to access this resource from the current IP address"
|
||||
});
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user