mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-24 20:43:19 +00:00
Compare commits
181 Commits
rate-limit
...
daniel/rem
Author | SHA1 | Date | |
---|---|---|---|
|
03848b30a2 | ||
|
5537b00a26 | ||
|
d71d59e399 | ||
|
8f8553760a | ||
|
708c2af979 | ||
|
4b463c6fde | ||
|
e6823c520e | ||
|
e973a62753 | ||
|
94fa294455 | ||
|
be63e538d7 | ||
|
02e423f52c | ||
|
3cb226908b | ||
|
ba37b1c083 | ||
|
d23b39abba | ||
|
de92ba157a | ||
|
dadea751e3 | ||
|
0ff0357a7c | ||
|
85f257b4db | ||
|
18d7a14e3f | ||
|
ff4d932631 | ||
|
519f0025c0 | ||
|
d8d6d7dc1b | ||
|
a975fbd8a4 | ||
|
3a6ec3717b | ||
|
a4a961996b | ||
|
5b4777c1a5 | ||
|
2f526850d6 | ||
|
4f5d31d06f | ||
|
a8264b17e4 | ||
|
cb66733e6d | ||
|
40a0691ccb | ||
|
6410d51033 | ||
|
bc30ba9ad1 | ||
|
a0259712df | ||
|
1132d07dea | ||
|
1f0b1964b9 | ||
|
690e72b44c | ||
|
e2967f5e61 | ||
|
97afc4ff51 | ||
|
c47a91715f | ||
|
fbc7b34786 | ||
|
9e6641c058 | ||
|
d035403af1 | ||
|
1af0d958dd | ||
|
66a51658d7 | ||
|
28dc3a4b1c | ||
|
b27cadb651 | ||
|
3dca82ad2f | ||
|
1c90df9dd4 | ||
|
e15c9e72c6 | ||
|
71575b1d2e | ||
|
51f164c399 | ||
|
702cd0d403 | ||
|
75267987fc | ||
|
d734a3f6f4 | ||
|
cbb749e34a | ||
|
4535c1069a | ||
|
747acfe070 | ||
|
fa1b236f26 | ||
|
c98ef0eca8 | ||
|
9f23106c6c | ||
|
1e7744b498 | ||
|
44c736facd | ||
|
51928ddb47 | ||
|
c7cded4af6 | ||
|
8b56e20b42 | ||
|
39c2c37cc0 | ||
|
3131ae7dae | ||
|
5315a67d74 | ||
|
79de7f9f5b | ||
|
71ffed026d | ||
|
ee98b15e2b | ||
|
945d81ad4b | ||
|
ff8354605c | ||
|
09b63eee90 | ||
|
d175256bb4 | ||
|
ee0c79d018 | ||
|
d5d7564550 | ||
|
0db682c5f0 | ||
|
a01a995585 | ||
|
2ac785493a | ||
|
85489a81ff | ||
|
7116c85f2c | ||
|
31e4da0dd3 | ||
|
f255d891ae | ||
|
4774469244 | ||
|
e143a31e79 | ||
|
0baea4c5fd | ||
|
f6cc20b08b | ||
|
90e125454e | ||
|
fbdf3dc9ce | ||
|
f333c905d9 | ||
|
71e60df39a | ||
|
8b4d050d05 | ||
|
3b4bb591a3 | ||
|
54f1a4416b | ||
|
47e3f1b510 | ||
|
5810b76027 | ||
|
246e6c64d1 | ||
|
4e836c5dca | ||
|
63a289c3be | ||
|
0a52bbd55d | ||
|
593bdf74b8 | ||
|
1f3742e619 | ||
|
d6e5ac2133 | ||
|
fea48518a3 | ||
|
dde24d4c71 | ||
|
94d509eb01 | ||
|
8f1e662688 | ||
|
dcbbb67f03 | ||
|
055fd34c33 | ||
|
dc0d3b860e | ||
|
c0fb3c905e | ||
|
18b0766d96 | ||
|
b423696630 | ||
|
bf60489fde | ||
|
85ea6d2585 | ||
|
a97737ab90 | ||
|
3793858f0a | ||
|
66c48fbff8 | ||
|
b6b040375b | ||
|
9ad5e082e2 | ||
|
f1805811aa | ||
|
b135258cce | ||
|
a651de53d1 | ||
|
7d0a535f46 | ||
|
c4e3dd84e3 | ||
|
9193f13970 | ||
|
016f22c295 | ||
|
4d7182c9b1 | ||
|
6ea7b04efa | ||
|
3981d61853 | ||
|
3d391b4e2d | ||
|
4123177133 | ||
|
4d61188d0f | ||
|
fa33f35fcd | ||
|
13629223fb | ||
|
74fefa9879 | ||
|
ff2c8d017f | ||
|
ba1f8f4564 | ||
|
e26df005c2 | ||
|
aca9b47f82 | ||
|
a16ce8899b | ||
|
b61511d100 | ||
|
f8ea421a0e | ||
|
a945bdfc4c | ||
|
f7b8345da4 | ||
|
f6d7ec52c2 | ||
|
3f6999b2e3 | ||
|
9128461409 | ||
|
893235c40f | ||
|
e0f655ae30 | ||
|
93aeca3a38 | ||
|
1edebdf8a5 | ||
|
b3a9661755 | ||
|
175ce865aa | ||
|
51f220ba2c | ||
|
51819e57d1 | ||
|
e1d9f779b2 | ||
|
40bb9668fe | ||
|
abd62867eb | ||
|
179573a269 | ||
|
457edef5fe | ||
|
f0b84d5bc9 | ||
|
9fddcea3db | ||
|
0c2e566184 | ||
|
38adc83f2b | ||
|
f2e5f1bb10 | ||
|
9460eafd91 | ||
|
8afecac7d8 | ||
|
bf13b81c0f | ||
|
c753a91958 | ||
|
695a4a34b5 | ||
|
372f71f2b0 | ||
|
0da6262ead | ||
|
8ffbaa2f6c | ||
|
796d5e3540 | ||
|
686b88fc97 | ||
|
2a134b9dc2 | ||
|
d8d63ecaec | ||
|
efc186ae6c |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -59,6 +59,8 @@ yarn-error.log*
|
|||||||
# Infisical init
|
# Infisical init
|
||||||
.infisical.json
|
.infisical.json
|
||||||
|
|
||||||
|
.infisicalignore
|
||||||
|
|
||||||
# Editor specific
|
# Editor specific
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|
||||||
|
@@ -1 +1,5 @@
|
|||||||
.github/resources/docker-compose.be-test.yml:generic-api-key:16
|
.github/resources/docker-compose.be-test.yml:generic-api-key:16
|
||||||
|
frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx:generic-api-key:206
|
||||||
|
frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:304
|
||||||
|
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx:generic-api-key:206
|
||||||
|
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:292
|
@@ -1,6 +1,7 @@
|
|||||||
ARG POSTHOG_HOST=https://app.posthog.com
|
ARG POSTHOG_HOST=https://app.posthog.com
|
||||||
ARG POSTHOG_API_KEY=posthog-api-key
|
ARG POSTHOG_API_KEY=posthog-api-key
|
||||||
ARG INTERCOM_ID=intercom-id
|
ARG INTERCOM_ID=intercom-id
|
||||||
|
ARG SAML_ORG_SLUG=saml-org-slug-default
|
||||||
|
|
||||||
FROM node:20-alpine AS base
|
FROM node:20-alpine AS base
|
||||||
|
|
||||||
@@ -35,6 +36,8 @@ ARG INTERCOM_ID
|
|||||||
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
|
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
|
||||||
ARG INFISICAL_PLATFORM_VERSION
|
ARG INFISICAL_PLATFORM_VERSION
|
||||||
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
||||||
|
ARG SAML_ORG_SLUG
|
||||||
|
ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
@@ -100,6 +103,9 @@ ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
|
|||||||
ARG INTERCOM_ID=intercom-id
|
ARG INTERCOM_ID=intercom-id
|
||||||
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
|
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
|
||||||
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
|
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
|
||||||
|
ARG SAML_ORG_SLUG
|
||||||
|
ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG \
|
||||||
|
BAKED_NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG
|
||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
|
|
||||||
|
@@ -23,16 +23,17 @@ module.exports = {
|
|||||||
root: true,
|
root: true,
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ["./e2e-test/**/*"],
|
files: ["./e2e-test/**/*", "./src/db/migrations/**/*"],
|
||||||
rules: {
|
rules: {
|
||||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||||
"@typescript-eslint/no-unsafe-argument": "off",
|
"@typescript-eslint/no-unsafe-argument": "off",
|
||||||
"@typescript-eslint/no-unsafe-return": "off",
|
"@typescript-eslint/no-unsafe-return": "off",
|
||||||
"@typescript-eslint/no-unsafe-call": "off",
|
"@typescript-eslint/no-unsafe-call": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
rules: {
|
rules: {
|
||||||
"@typescript-eslint/no-empty-function": "off",
|
"@typescript-eslint/no-empty-function": "off",
|
||||||
"@typescript-eslint/no-unsafe-enum-comparison": "off",
|
"@typescript-eslint/no-unsafe-enum-comparison": "off",
|
||||||
|
@@ -46,7 +46,7 @@ const deleteSecretImport = async (id: string) => {
|
|||||||
|
|
||||||
describe("Secret Import Router", async () => {
|
describe("Secret Import Router", async () => {
|
||||||
test.each([
|
test.each([
|
||||||
{ importEnv: "dev", importPath: "/" }, // one in root
|
{ importEnv: "prod", importPath: "/" }, // one in root
|
||||||
{ importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones
|
{ importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones
|
||||||
])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => {
|
])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => {
|
||||||
// check for default environments
|
// check for default environments
|
||||||
@@ -66,7 +66,7 @@ describe("Secret Import Router", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Get secret imports", async () => {
|
test("Get secret imports", async () => {
|
||||||
const createdImport1 = await createSecretImport("/", "dev");
|
const createdImport1 = await createSecretImport("/", "prod");
|
||||||
const createdImport2 = await createSecretImport("/", "staging");
|
const createdImport2 = await createSecretImport("/", "staging");
|
||||||
const res = await testServer.inject({
|
const res = await testServer.inject({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -103,10 +103,10 @@ describe("Secret Import Router", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Update secret import position", async () => {
|
test("Update secret import position", async () => {
|
||||||
const devImportDetails = { path: "/", envSlug: "dev" };
|
const prodImportDetails = { path: "/", envSlug: "prod" };
|
||||||
const stagingImportDetails = { path: "/", envSlug: "staging" };
|
const stagingImportDetails = { path: "/", envSlug: "staging" };
|
||||||
|
|
||||||
const createdImport1 = await createSecretImport(devImportDetails.path, devImportDetails.envSlug);
|
const createdImport1 = await createSecretImport(prodImportDetails.path, prodImportDetails.envSlug);
|
||||||
const createdImport2 = await createSecretImport(stagingImportDetails.path, stagingImportDetails.envSlug);
|
const createdImport2 = await createSecretImport(stagingImportDetails.path, stagingImportDetails.envSlug);
|
||||||
|
|
||||||
const updateImportRes = await testServer.inject({
|
const updateImportRes = await testServer.inject({
|
||||||
@@ -136,7 +136,7 @@ describe("Secret Import Router", async () => {
|
|||||||
position: 2,
|
position: 2,
|
||||||
importEnv: expect.objectContaining({
|
importEnv: expect.objectContaining({
|
||||||
name: expect.any(String),
|
name: expect.any(String),
|
||||||
slug: expect.stringMatching(devImportDetails.envSlug),
|
slug: expect.stringMatching(prodImportDetails.envSlug),
|
||||||
id: expect.any(String)
|
id: expect.any(String)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -166,7 +166,7 @@ describe("Secret Import Router", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Delete secret import position", async () => {
|
test("Delete secret import position", async () => {
|
||||||
const createdImport1 = await createSecretImport("/", "dev");
|
const createdImport1 = await createSecretImport("/", "prod");
|
||||||
const createdImport2 = await createSecretImport("/", "staging");
|
const createdImport2 = await createSecretImport("/", "staging");
|
||||||
const deletedImport = await deleteSecretImport(createdImport1.id);
|
const deletedImport = await deleteSecretImport(createdImport1.id);
|
||||||
// check for default environments
|
// check for default environments
|
||||||
|
@@ -108,7 +108,7 @@
|
|||||||
"libsodium-wrappers": "^0.7.13",
|
"libsodium-wrappers": "^0.7.13",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"ms": "^2.1.3",
|
"ms": "^2.1.3",
|
||||||
"mysql2": "^3.9.1",
|
"mysql2": "^3.9.4",
|
||||||
"nanoid": "^5.0.4",
|
"nanoid": "^5.0.4",
|
||||||
"nodemailer": "^6.9.9",
|
"nodemailer": "^6.9.9",
|
||||||
"ora": "^7.0.1",
|
"ora": "^7.0.1",
|
||||||
|
@@ -7,10 +7,10 @@ const prompt = promptSync({ sigint: true });
|
|||||||
|
|
||||||
const migrationName = prompt("Enter name for migration: ");
|
const migrationName = prompt("Enter name for migration: ");
|
||||||
|
|
||||||
|
// Remove spaces from migration name and replace with hyphens
|
||||||
|
const formattedMigrationName = migrationName.replace(/\s+/g, "-");
|
||||||
|
|
||||||
execSync(
|
execSync(
|
||||||
`npx knex migrate:make --knexfile ${path.join(
|
`npx knex migrate:make --knexfile ${path.join(__dirname, "../src/db/knexfile.ts")} -x ts ${formattedMigrationName}`,
|
||||||
__dirname,
|
|
||||||
"../src/db/knexfile.ts"
|
|
||||||
)} -x ts ${migrationName}`,
|
|
||||||
{ stdio: "inherit" }
|
{ stdio: "inherit" }
|
||||||
);
|
);
|
||||||
|
4
backend/src/@types/fastify.d.ts
vendored
4
backend/src/@types/fastify.d.ts
vendored
@@ -5,6 +5,7 @@ import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-se
|
|||||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
||||||
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
||||||
|
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
|
||||||
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
||||||
import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
|
import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
@@ -25,6 +26,7 @@ import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
|
|||||||
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
||||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
|
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||||
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
||||||
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
||||||
@@ -89,6 +91,8 @@ declare module "fastify" {
|
|||||||
orgRole: TOrgRoleServiceFactory;
|
orgRole: TOrgRoleServiceFactory;
|
||||||
superAdmin: TSuperAdminServiceFactory;
|
superAdmin: TSuperAdminServiceFactory;
|
||||||
user: TUserServiceFactory;
|
user: TUserServiceFactory;
|
||||||
|
group: TGroupServiceFactory;
|
||||||
|
groupProject: TGroupProjectServiceFactory;
|
||||||
apiKey: TApiKeyServiceFactory;
|
apiKey: TApiKeyServiceFactory;
|
||||||
project: TProjectServiceFactory;
|
project: TProjectServiceFactory;
|
||||||
projectMembership: TProjectMembershipServiceFactory;
|
projectMembership: TProjectMembershipServiceFactory;
|
||||||
|
28
backend/src/@types/knex.d.ts
vendored
28
backend/src/@types/knex.d.ts
vendored
@@ -29,6 +29,15 @@ import {
|
|||||||
TGitAppOrg,
|
TGitAppOrg,
|
||||||
TGitAppOrgInsert,
|
TGitAppOrgInsert,
|
||||||
TGitAppOrgUpdate,
|
TGitAppOrgUpdate,
|
||||||
|
TGroupProjectMembershipRoles,
|
||||||
|
TGroupProjectMembershipRolesInsert,
|
||||||
|
TGroupProjectMembershipRolesUpdate,
|
||||||
|
TGroupProjectMemberships,
|
||||||
|
TGroupProjectMembershipsInsert,
|
||||||
|
TGroupProjectMembershipsUpdate,
|
||||||
|
TGroups,
|
||||||
|
TGroupsInsert,
|
||||||
|
TGroupsUpdate,
|
||||||
TIdentities,
|
TIdentities,
|
||||||
TIdentitiesInsert,
|
TIdentitiesInsert,
|
||||||
TIdentitiesUpdate,
|
TIdentitiesUpdate,
|
||||||
@@ -188,6 +197,9 @@ import {
|
|||||||
TUserEncryptionKeys,
|
TUserEncryptionKeys,
|
||||||
TUserEncryptionKeysInsert,
|
TUserEncryptionKeysInsert,
|
||||||
TUserEncryptionKeysUpdate,
|
TUserEncryptionKeysUpdate,
|
||||||
|
TUserGroupMembership,
|
||||||
|
TUserGroupMembershipInsert,
|
||||||
|
TUserGroupMembershipUpdate,
|
||||||
TUsers,
|
TUsers,
|
||||||
TUsersInsert,
|
TUsersInsert,
|
||||||
TUsersUpdate,
|
TUsersUpdate,
|
||||||
@@ -199,6 +211,22 @@ import {
|
|||||||
declare module "knex/types/tables" {
|
declare module "knex/types/tables" {
|
||||||
interface Tables {
|
interface Tables {
|
||||||
[TableName.Users]: Knex.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
|
[TableName.Users]: Knex.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
|
||||||
|
[TableName.Groups]: Knex.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
|
||||||
|
[TableName.UserGroupMembership]: Knex.CompositeTableType<
|
||||||
|
TUserGroupMembership,
|
||||||
|
TUserGroupMembershipInsert,
|
||||||
|
TUserGroupMembershipUpdate
|
||||||
|
>;
|
||||||
|
[TableName.GroupProjectMembership]: Knex.CompositeTableType<
|
||||||
|
TGroupProjectMemberships,
|
||||||
|
TGroupProjectMembershipsInsert,
|
||||||
|
TGroupProjectMembershipsUpdate
|
||||||
|
>;
|
||||||
|
[TableName.GroupProjectMembershipRole]: Knex.CompositeTableType<
|
||||||
|
TGroupProjectMembershipRoles,
|
||||||
|
TGroupProjectMembershipRolesInsert,
|
||||||
|
TGroupProjectMembershipRolesUpdate
|
||||||
|
>;
|
||||||
[TableName.UserAliases]: Knex.CompositeTableType<TUserAliases, TUserAliasesInsert, TUserAliasesUpdate>;
|
[TableName.UserAliases]: Knex.CompositeTableType<TUserAliases, TUserAliasesInsert, TUserAliasesUpdate>;
|
||||||
[TableName.UserEncryptionKey]: Knex.CompositeTableType<
|
[TableName.UserEncryptionKey]: Knex.CompositeTableType<
|
||||||
TUserEncryptionKeys,
|
TUserEncryptionKeys,
|
||||||
|
@@ -0,0 +1,111 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TableName, TOrgMemberships } from "../schemas";
|
||||||
|
|
||||||
|
const validateOrgMembership = (membershipToValidate: TOrgMemberships, firstMembership: TOrgMemberships) => {
|
||||||
|
const firstOrgId = firstMembership.orgId;
|
||||||
|
const firstUserId = firstMembership.userId;
|
||||||
|
|
||||||
|
if (membershipToValidate.id === firstMembership.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (membershipToValidate.inviteEmail !== firstMembership.inviteEmail) {
|
||||||
|
throw new Error(`Invite emails are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`);
|
||||||
|
}
|
||||||
|
if (membershipToValidate.orgId !== firstMembership.orgId) {
|
||||||
|
throw new Error(`OrgIds are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`);
|
||||||
|
}
|
||||||
|
if (membershipToValidate.role !== firstMembership.role) {
|
||||||
|
throw new Error(`Roles are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`);
|
||||||
|
}
|
||||||
|
if (membershipToValidate.roleId !== firstMembership.roleId) {
|
||||||
|
throw new Error(`RoleIds are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`);
|
||||||
|
}
|
||||||
|
if (membershipToValidate.status !== firstMembership.status) {
|
||||||
|
throw new Error(`Statuses are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`);
|
||||||
|
}
|
||||||
|
if (membershipToValidate.userId !== firstMembership.userId) {
|
||||||
|
throw new Error(`UserIds are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const RowSchema = z.object({
|
||||||
|
userId: z.string(),
|
||||||
|
orgId: z.string(),
|
||||||
|
cnt: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Transactional find and delete duplicate rows
|
||||||
|
await knex.transaction(async (tx) => {
|
||||||
|
const duplicateRows = await tx(TableName.OrgMembership)
|
||||||
|
.select("userId", "orgId") // Select the userId and orgId so we can group by them
|
||||||
|
.count("* as cnt") // Count the number of rows for each userId and orgId, so we can make sure there are more than 1 row (a duplicate)
|
||||||
|
.groupBy("userId", "orgId")
|
||||||
|
.havingRaw("count(*) > ?", [1]); // Using havingRaw for direct SQL expressions
|
||||||
|
|
||||||
|
// Parse the rows to ensure they are in the correct format, and for type safety
|
||||||
|
const parsedRows = RowSchema.array().parse(duplicateRows);
|
||||||
|
|
||||||
|
// For each of the duplicate rows, loop through and find the actual memberships to delete
|
||||||
|
for (const row of parsedRows) {
|
||||||
|
const count = Number(row.cnt);
|
||||||
|
|
||||||
|
// An extra check to ensure that the count is actually a number, and the number is greater than 2
|
||||||
|
if (typeof count !== "number" || count < 2) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all the organization memberships that have the same userId and orgId
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const rowsToDelete = await tx(TableName.OrgMembership).where({
|
||||||
|
userId: row.userId,
|
||||||
|
orgId: row.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure that all the rows have exactly the same value, except id, createdAt, updatedAt
|
||||||
|
for (const rowToDelete of rowsToDelete) {
|
||||||
|
validateOrgMembership(rowToDelete, rowsToDelete[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the row with the latest createdAt, which we will keep
|
||||||
|
|
||||||
|
let lowestCreatedAt: number | null = null;
|
||||||
|
let latestCreatedRow: TOrgMemberships | null = null;
|
||||||
|
|
||||||
|
for (const rowToDelete of rowsToDelete) {
|
||||||
|
if (lowestCreatedAt === null || rowToDelete.createdAt.getTime() < lowestCreatedAt) {
|
||||||
|
lowestCreatedAt = rowToDelete.createdAt.getTime();
|
||||||
|
latestCreatedRow = rowToDelete;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!latestCreatedRow) {
|
||||||
|
throw new Error("Failed to find last created membership");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out the latest row from the rows to delete
|
||||||
|
const membershipIdsToDelete = rowsToDelete.map((r) => r.id).filter((id) => id !== latestCreatedRow!.id);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const numberOfRowsDeleted = await tx(TableName.OrgMembership).whereIn("id", membershipIdsToDelete).delete();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(
|
||||||
|
`Deleted ${numberOfRowsDeleted} duplicate organization memberships for ${row.userId} and ${row.orgId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.OrgMembership, (table) => {
|
||||||
|
table.unique(["userId", "orgId"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.alterTable(TableName.OrgMembership, (table) => {
|
||||||
|
table.dropUnique(["userId", "orgId"]);
|
||||||
|
});
|
||||||
|
}
|
82
backend/src/db/migrations/20240412174842_group.ts
Normal file
82
backend/src/db/migrations/20240412174842_group.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.Groups))) {
|
||||||
|
await knex.schema.createTable(TableName.Groups, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.uuid("orgId").notNullable();
|
||||||
|
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
|
t.string("name").notNullable();
|
||||||
|
t.string("slug").notNullable();
|
||||||
|
t.unique(["orgId", "slug"]);
|
||||||
|
t.string("role").notNullable();
|
||||||
|
t.uuid("roleId");
|
||||||
|
t.foreign("roleId").references("id").inTable(TableName.OrgRoles);
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.Groups);
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.UserGroupMembership))) {
|
||||||
|
await knex.schema.createTable(TableName.UserGroupMembership, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); // link to user and link to groups cascade on groups
|
||||||
|
t.uuid("userId").notNullable();
|
||||||
|
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||||
|
t.uuid("groupId").notNullable();
|
||||||
|
t.foreign("groupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.UserGroupMembership);
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.GroupProjectMembership))) {
|
||||||
|
await knex.schema.createTable(TableName.GroupProjectMembership, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("projectId").notNullable();
|
||||||
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
t.uuid("groupId").notNullable();
|
||||||
|
t.foreign("groupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await createOnUpdateTrigger(knex, TableName.GroupProjectMembership);
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.GroupProjectMembershipRole))) {
|
||||||
|
await knex.schema.createTable(TableName.GroupProjectMembershipRole, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("role").notNullable();
|
||||||
|
t.uuid("projectMembershipId").notNullable();
|
||||||
|
t.foreign("projectMembershipId").references("id").inTable(TableName.GroupProjectMembership).onDelete("CASCADE");
|
||||||
|
// until role is changed/removed the role should not deleted
|
||||||
|
t.uuid("customRoleId");
|
||||||
|
t.foreign("customRoleId").references("id").inTable(TableName.ProjectRoles);
|
||||||
|
t.boolean("isTemporary").notNullable().defaultTo(false);
|
||||||
|
t.string("temporaryMode");
|
||||||
|
t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc
|
||||||
|
t.datetime("temporaryAccessStartTime");
|
||||||
|
t.datetime("temporaryAccessEndTime");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.GroupProjectMembershipRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.GroupProjectMembershipRole);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.GroupProjectMembershipRole);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.UserGroupMembership);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.UserGroupMembership);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.GroupProjectMembership);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.GroupProjectMembership);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.Groups);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.Groups);
|
||||||
|
}
|
@@ -0,0 +1,47 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { ProjectMembershipRole, TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const doesProjectRoleFieldExist = await knex.schema.hasColumn(TableName.ProjectMembership, "role");
|
||||||
|
const doesProjectRoleIdFieldExist = await knex.schema.hasColumn(TableName.ProjectMembership, "roleId");
|
||||||
|
await knex.schema.alterTable(TableName.ProjectMembership, (t) => {
|
||||||
|
if (doesProjectRoleFieldExist) t.dropColumn("roleId");
|
||||||
|
if (doesProjectRoleIdFieldExist) t.dropColumn("role");
|
||||||
|
});
|
||||||
|
|
||||||
|
const doesIdentityProjectRoleFieldExist = await knex.schema.hasColumn(TableName.IdentityProjectMembership, "role");
|
||||||
|
const doesIdentityProjectRoleIdFieldExist = await knex.schema.hasColumn(
|
||||||
|
TableName.IdentityProjectMembership,
|
||||||
|
"roleId"
|
||||||
|
);
|
||||||
|
await knex.schema.alterTable(TableName.IdentityProjectMembership, (t) => {
|
||||||
|
if (doesIdentityProjectRoleFieldExist) t.dropColumn("roleId");
|
||||||
|
if (doesIdentityProjectRoleIdFieldExist) t.dropColumn("role");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const doesProjectRoleFieldExist = await knex.schema.hasColumn(TableName.ProjectMembership, "role");
|
||||||
|
const doesProjectRoleIdFieldExist = await knex.schema.hasColumn(TableName.ProjectMembership, "roleId");
|
||||||
|
await knex.schema.alterTable(TableName.ProjectMembership, (t) => {
|
||||||
|
if (!doesProjectRoleFieldExist) t.string("role").defaultTo(ProjectMembershipRole.Member);
|
||||||
|
if (!doesProjectRoleIdFieldExist) {
|
||||||
|
t.uuid("roleId");
|
||||||
|
t.foreign("roleId").references("id").inTable(TableName.ProjectRoles);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const doesIdentityProjectRoleFieldExist = await knex.schema.hasColumn(TableName.IdentityProjectMembership, "role");
|
||||||
|
const doesIdentityProjectRoleIdFieldExist = await knex.schema.hasColumn(
|
||||||
|
TableName.IdentityProjectMembership,
|
||||||
|
"roleId"
|
||||||
|
);
|
||||||
|
await knex.schema.alterTable(TableName.IdentityProjectMembership, (t) => {
|
||||||
|
if (!doesIdentityProjectRoleFieldExist) t.string("role").defaultTo(ProjectMembershipRole.Member);
|
||||||
|
if (!doesIdentityProjectRoleIdFieldExist) {
|
||||||
|
t.uuid("roleId");
|
||||||
|
t.foreign("roleId").references("id").inTable(TableName.ProjectRoles);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
31
backend/src/db/schemas/group-project-membership-roles.ts
Normal file
31
backend/src/db/schemas/group-project-membership-roles.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// 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 GroupProjectMembershipRolesSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
role: z.string(),
|
||||||
|
projectMembershipId: z.string().uuid(),
|
||||||
|
customRoleId: z.string().uuid().nullable().optional(),
|
||||||
|
isTemporary: z.boolean().default(false),
|
||||||
|
temporaryMode: z.string().nullable().optional(),
|
||||||
|
temporaryRange: z.string().nullable().optional(),
|
||||||
|
temporaryAccessStartTime: z.date().nullable().optional(),
|
||||||
|
temporaryAccessEndTime: z.date().nullable().optional(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TGroupProjectMembershipRoles = z.infer<typeof GroupProjectMembershipRolesSchema>;
|
||||||
|
export type TGroupProjectMembershipRolesInsert = Omit<
|
||||||
|
z.input<typeof GroupProjectMembershipRolesSchema>,
|
||||||
|
TImmutableDBKeys
|
||||||
|
>;
|
||||||
|
export type TGroupProjectMembershipRolesUpdate = Partial<
|
||||||
|
Omit<z.input<typeof GroupProjectMembershipRolesSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
22
backend/src/db/schemas/group-project-memberships.ts
Normal file
22
backend/src/db/schemas/group-project-memberships.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// 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 GroupProjectMembershipsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
projectId: z.string(),
|
||||||
|
groupId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TGroupProjectMemberships = z.infer<typeof GroupProjectMembershipsSchema>;
|
||||||
|
export type TGroupProjectMembershipsInsert = Omit<z.input<typeof GroupProjectMembershipsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TGroupProjectMembershipsUpdate = Partial<
|
||||||
|
Omit<z.input<typeof GroupProjectMembershipsSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
23
backend/src/db/schemas/groups.ts
Normal file
23
backend/src/db/schemas/groups.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 GroupsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
orgId: z.string().uuid(),
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
role: z.string(),
|
||||||
|
roleId: z.string().uuid().nullable().optional(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TGroups = z.infer<typeof GroupsSchema>;
|
||||||
|
export type TGroupsInsert = Omit<z.input<typeof GroupsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TGroupsUpdate = Partial<Omit<z.input<typeof GroupsSchema>, TImmutableDBKeys>>;
|
@@ -9,8 +9,6 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
|
|
||||||
export const IdentityProjectMembershipsSchema = z.object({
|
export const IdentityProjectMembershipsSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
role: z.string(),
|
|
||||||
roleId: z.string().uuid().nullable().optional(),
|
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
identityId: z.string().uuid(),
|
identityId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
|
@@ -7,6 +7,9 @@ export * from "./dynamic-secret-leases";
|
|||||||
export * from "./dynamic-secrets";
|
export * from "./dynamic-secrets";
|
||||||
export * from "./git-app-install-sessions";
|
export * from "./git-app-install-sessions";
|
||||||
export * from "./git-app-org";
|
export * from "./git-app-org";
|
||||||
|
export * from "./group-project-membership-roles";
|
||||||
|
export * from "./group-project-memberships";
|
||||||
|
export * from "./groups";
|
||||||
export * from "./identities";
|
export * from "./identities";
|
||||||
export * from "./identity-access-tokens";
|
export * from "./identity-access-tokens";
|
||||||
export * from "./identity-org-memberships";
|
export * from "./identity-org-memberships";
|
||||||
@@ -61,5 +64,6 @@ export * from "./trusted-ips";
|
|||||||
export * from "./user-actions";
|
export * from "./user-actions";
|
||||||
export * from "./user-aliases";
|
export * from "./user-aliases";
|
||||||
export * from "./user-encryption-keys";
|
export * from "./user-encryption-keys";
|
||||||
|
export * from "./user-group-membership";
|
||||||
export * from "./users";
|
export * from "./users";
|
||||||
export * from "./webhooks";
|
export * from "./webhooks";
|
||||||
|
@@ -2,6 +2,10 @@ import { z } from "zod";
|
|||||||
|
|
||||||
export enum TableName {
|
export enum TableName {
|
||||||
Users = "users",
|
Users = "users",
|
||||||
|
Groups = "groups",
|
||||||
|
GroupProjectMembership = "group_project_memberships",
|
||||||
|
GroupProjectMembershipRole = "group_project_membership_roles",
|
||||||
|
UserGroupMembership = "user_group_membership",
|
||||||
UserAliases = "user_aliases",
|
UserAliases = "user_aliases",
|
||||||
UserEncryptionKey = "user_encryption_keys",
|
UserEncryptionKey = "user_encryption_keys",
|
||||||
AuthTokens = "auth_tokens",
|
AuthTokens = "auth_tokens",
|
||||||
|
@@ -9,12 +9,10 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
|
|
||||||
export const ProjectMembershipsSchema = z.object({
|
export const ProjectMembershipsSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
role: z.string(),
|
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
userId: z.string().uuid(),
|
userId: z.string().uuid(),
|
||||||
projectId: z.string(),
|
projectId: z.string()
|
||||||
roleId: z.string().uuid().nullable().optional()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TProjectMemberships = z.infer<typeof ProjectMembershipsSchema>;
|
export type TProjectMemberships = z.infer<typeof ProjectMembershipsSchema>;
|
||||||
|
20
backend/src/db/schemas/user-group-membership.ts
Normal file
20
backend/src/db/schemas/user-group-membership.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// 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 UserGroupMembershipSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
userId: z.string().uuid(),
|
||||||
|
groupId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TUserGroupMembership = z.infer<typeof UserGroupMembershipSchema>;
|
||||||
|
export type TUserGroupMembershipInsert = Omit<z.input<typeof UserGroupMembershipSchema>, TImmutableDBKeys>;
|
||||||
|
export type TUserGroupMembershipUpdate = Partial<Omit<z.input<typeof UserGroupMembershipSchema>, TImmutableDBKeys>>;
|
@@ -33,8 +33,7 @@ export async function seed(knex: Knex): Promise<void> {
|
|||||||
const projectMembership = await knex(TableName.ProjectMembership)
|
const projectMembership = await knex(TableName.ProjectMembership)
|
||||||
.insert({
|
.insert({
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
userId: seedData1.id,
|
userId: seedData1.id
|
||||||
role: ProjectMembershipRole.Admin
|
|
||||||
})
|
})
|
||||||
.returning("*");
|
.returning("*");
|
||||||
await knex(TableName.ProjectUserMembershipRole).insert({
|
await knex(TableName.ProjectUserMembershipRole).insert({
|
||||||
|
@@ -78,8 +78,7 @@ export async function seed(knex: Knex): Promise<void> {
|
|||||||
const identityProjectMembership = await knex(TableName.IdentityProjectMembership)
|
const identityProjectMembership = await knex(TableName.IdentityProjectMembership)
|
||||||
.insert({
|
.insert({
|
||||||
identityId: seedData1.machineIdentity.id,
|
identityId: seedData1.machineIdentity.id,
|
||||||
projectId: seedData1.project.id,
|
projectId: seedData1.project.id
|
||||||
role: ProjectMembershipRole.Admin
|
|
||||||
})
|
})
|
||||||
.returning("*");
|
.returning("*");
|
||||||
|
|
||||||
|
220
backend/src/ee/routes/v1/group-router.ts
Normal file
220
backend/src/ee/routes/v1/group-router.ts
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { GroupsSchema, OrgMembershipRole, UsersSchema } from "@app/db/schemas";
|
||||||
|
import { GROUPS } from "@app/lib/api-docs";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
url: "/",
|
||||||
|
method: "POST",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
name: z.string().trim().min(1).max(50).describe(GROUPS.CREATE.name),
|
||||||
|
slug: z
|
||||||
|
.string()
|
||||||
|
.min(5)
|
||||||
|
.max(36)
|
||||||
|
.refine((v) => slugify(v) === v, {
|
||||||
|
message: "Slug must be a valid slug"
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.describe(GROUPS.CREATE.slug),
|
||||||
|
role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(GROUPS.CREATE.role)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: GroupsSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const group = await server.services.group.createGroup({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:currentSlug",
|
||||||
|
method: "PATCH",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
currentSlug: z.string().trim().describe(GROUPS.UPDATE.currentSlug)
|
||||||
|
}),
|
||||||
|
body: z
|
||||||
|
.object({
|
||||||
|
name: z.string().trim().min(1).describe(GROUPS.UPDATE.name),
|
||||||
|
slug: z
|
||||||
|
.string()
|
||||||
|
.min(5)
|
||||||
|
.max(36)
|
||||||
|
.refine((v) => slugify(v) === v, {
|
||||||
|
message: "Slug must be a valid slug"
|
||||||
|
})
|
||||||
|
.describe(GROUPS.UPDATE.slug),
|
||||||
|
role: z.string().trim().min(1).describe(GROUPS.UPDATE.role)
|
||||||
|
})
|
||||||
|
.partial(),
|
||||||
|
response: {
|
||||||
|
200: GroupsSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const group = await server.services.group.updateGroup({
|
||||||
|
currentSlug: req.params.currentSlug,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:slug",
|
||||||
|
method: "DELETE",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
slug: z.string().trim().describe(GROUPS.DELETE.slug)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: GroupsSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const group = await server.services.group.deleteGroup({
|
||||||
|
groupSlug: req.params.slug,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:slug/users",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
slug: z.string().trim().describe(GROUPS.LIST_USERS.slug)
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset),
|
||||||
|
limit: z.coerce.number().min(1).max(100).default(10).describe(GROUPS.LIST_USERS.limit),
|
||||||
|
username: z.string().optional().describe(GROUPS.LIST_USERS.username)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
users: UsersSchema.pick({
|
||||||
|
email: true,
|
||||||
|
username: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
id: true
|
||||||
|
})
|
||||||
|
.merge(
|
||||||
|
z.object({
|
||||||
|
isPartOfGroup: z.boolean()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.array(),
|
||||||
|
totalCount: z.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { users, totalCount } = await server.services.group.listGroupUsers({
|
||||||
|
groupSlug: req.params.slug,
|
||||||
|
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]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
slug: z.string().trim().describe(GROUPS.ADD_USER.slug),
|
||||||
|
username: z.string().trim().describe(GROUPS.ADD_USER.username)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: UsersSchema.pick({
|
||||||
|
email: true,
|
||||||
|
username: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
id: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const user = await server.services.group.addUserToGroup({
|
||||||
|
groupSlug: req.params.slug,
|
||||||
|
username: req.params.username,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:slug/users/:username",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
slug: z.string().trim().describe(GROUPS.DELETE_USER.slug),
|
||||||
|
username: z.string().trim().describe(GROUPS.DELETE_USER.username)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: UsersSchema.pick({
|
||||||
|
email: true,
|
||||||
|
username: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
id: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const user = await server.services.group.removeUserFromGroup({
|
||||||
|
groupSlug: req.params.slug,
|
||||||
|
username: req.params.username,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -1,5 +1,6 @@
|
|||||||
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
||||||
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
||||||
|
import { registerGroupRouter } from "./group-router";
|
||||||
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
|
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
|
||||||
import { registerLdapRouter } from "./ldap-router";
|
import { registerLdapRouter } from "./ldap-router";
|
||||||
import { registerLicenseRouter } from "./license-router";
|
import { registerLicenseRouter } from "./license-router";
|
||||||
@@ -53,6 +54,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
|
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
|
||||||
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
|
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
|
||||||
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
|
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
|
||||||
|
await server.register(registerGroupRouter, { prefix: "/groups" });
|
||||||
await server.register(
|
await server.register(
|
||||||
async (privilegeRouter) => {
|
async (privilegeRouter) => {
|
||||||
await privilegeRouter.register(registerUserAdditionalPrivilegeRouter, { prefix: "/users" });
|
await privilegeRouter.register(registerUserAdditionalPrivilegeRouter, { prefix: "/users" });
|
||||||
|
@@ -171,6 +171,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
req.permission.authMethod,
|
req.permission.authMethod,
|
||||||
req.permission.orgId
|
req.permission.orgId
|
||||||
);
|
);
|
||||||
|
|
||||||
return { data: { permissions, membership } };
|
return { data: { permissions, membership } };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -3,7 +3,7 @@ import { z } from "zod";
|
|||||||
import { AuditLogsSchema, SecretSnapshotsSchema } from "@app/db/schemas";
|
import { AuditLogsSchema, SecretSnapshotsSchema } from "@app/db/schemas";
|
||||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs";
|
import { AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs";
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
@@ -19,7 +19,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: "Return project secret snapshots ids",
|
description: "Return project secret snapshots ids",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
apiKeyAuth: [],
|
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -97,8 +96,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: "Return audit logs",
|
description: "Return audit logs",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
@@ -145,6 +143,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.workspaceId,
|
||||||
...req.query,
|
...req.query,
|
||||||
|
startDate: req.query.endDate || getLastMidnightDateISO(),
|
||||||
auditLogActor: req.query.actor,
|
auditLogActor: req.query.actor,
|
||||||
actor: req.permission.type
|
actor: req.permission.type
|
||||||
});
|
});
|
||||||
|
@@ -206,7 +206,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
schemas: z.array(z.string()),
|
schemas: z.array(z.string()),
|
||||||
userName: z.string().trim().email(),
|
userName: z.string().trim(),
|
||||||
name: z.object({
|
name: z.object({
|
||||||
familyName: z.string().trim(),
|
familyName: z.string().trim(),
|
||||||
givenName: z.string().trim()
|
givenName: z.string().trim()
|
||||||
@@ -227,7 +227,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
|||||||
200: z.object({
|
200: z.object({
|
||||||
schemas: z.array(z.string()),
|
schemas: z.array(z.string()),
|
||||||
id: z.string().trim(),
|
id: z.string().trim(),
|
||||||
userName: z.string().trim().email(),
|
userName: z.string().trim(),
|
||||||
name: z.object({
|
name: z.object({
|
||||||
familyName: z.string().trim(),
|
familyName: z.string().trim(),
|
||||||
givenName: z.string().trim()
|
givenName: z.string().trim()
|
||||||
@@ -262,38 +262,257 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
url: "/Users/:userId",
|
url: "/Users/:userId",
|
||||||
method: "PATCH",
|
method: "DELETE",
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
userId: z.string().trim()
|
userId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
|
||||||
schemas: z.array(z.string()),
|
|
||||||
Operations: z.array(
|
|
||||||
z.object({
|
|
||||||
op: z.string().trim(),
|
|
||||||
path: z.string().trim().optional(),
|
|
||||||
value: z.union([
|
|
||||||
z.object({
|
|
||||||
active: z.boolean()
|
|
||||||
}),
|
|
||||||
z.string().trim()
|
|
||||||
])
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
response: {
|
response: {
|
||||||
200: z.object({})
|
200: z.object({})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const user = await req.server.services.scim.updateScimUser({
|
const user = await req.server.services.scim.deleteScimUser({
|
||||||
userId: req.params.userId,
|
userId: req.params.userId,
|
||||||
|
orgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/Groups",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
schemas: z.array(z.string()),
|
||||||
|
displayName: z.string().trim(),
|
||||||
|
members: z.array(z.any()).length(0).optional() // okta-specific
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
schemas: z.array(z.string()),
|
||||||
|
id: z.string().trim(),
|
||||||
|
displayName: z.string().trim(),
|
||||||
|
members: z.array(z.any()).length(0),
|
||||||
|
meta: z.object({
|
||||||
|
resourceType: z.string().trim()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const group = await req.server.services.scim.createScimGroup({
|
||||||
|
displayName: req.body.displayName,
|
||||||
|
orgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/Groups",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
startIndex: z.coerce.number().default(1),
|
||||||
|
count: z.coerce.number().default(20),
|
||||||
|
filter: z.string().trim().optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
Resources: z.array(
|
||||||
|
z.object({
|
||||||
|
schemas: z.array(z.string()),
|
||||||
|
id: z.string().trim(),
|
||||||
|
displayName: z.string().trim(),
|
||||||
|
members: z.array(z.any()).length(0),
|
||||||
|
meta: z.object({
|
||||||
|
resourceType: z.string().trim()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
),
|
||||||
|
itemsPerPage: z.number(),
|
||||||
|
schemas: z.array(z.string()),
|
||||||
|
startIndex: z.number(),
|
||||||
|
totalResults: z.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const groups = await req.server.services.scim.listScimGroups({
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
offset: req.query.startIndex,
|
||||||
|
limit: req.query.count
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/Groups/:groupId",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
groupId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
schemas: z.array(z.string()),
|
||||||
|
id: z.string().trim(),
|
||||||
|
displayName: z.string().trim(),
|
||||||
|
members: z.array(
|
||||||
|
z.object({
|
||||||
|
value: z.string(),
|
||||||
|
display: z.string()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
meta: z.object({
|
||||||
|
resourceType: z.string().trim()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const group = await req.server.services.scim.getScimGroup({
|
||||||
|
groupId: req.params.groupId,
|
||||||
|
orgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/Groups/:groupId",
|
||||||
|
method: "PUT",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
groupId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
schemas: z.array(z.string()),
|
||||||
|
id: z.string().trim(),
|
||||||
|
displayName: z.string().trim(),
|
||||||
|
members: z.array(z.any()).length(0)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
schemas: z.array(z.string()),
|
||||||
|
id: z.string().trim(),
|
||||||
|
displayName: z.string().trim(),
|
||||||
|
members: z.array(
|
||||||
|
z.object({
|
||||||
|
value: z.string(),
|
||||||
|
display: z.string()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
meta: z.object({
|
||||||
|
resourceType: z.string().trim()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const group = await req.server.services.scim.updateScimGroupNamePut({
|
||||||
|
groupId: req.params.groupId,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
displayName: req.body.displayName
|
||||||
|
});
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/Groups/:groupId",
|
||||||
|
method: "PATCH",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
groupId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
schemas: z.array(z.string()),
|
||||||
|
Operations: z.array(
|
||||||
|
z.union([
|
||||||
|
z.object({
|
||||||
|
op: z.literal("replace"),
|
||||||
|
value: z.object({
|
||||||
|
id: z.string().trim(),
|
||||||
|
displayName: z.string().trim()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
op: z.literal("remove"),
|
||||||
|
path: z.string().trim()
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
op: z.literal("add"),
|
||||||
|
value: z.object({
|
||||||
|
value: z.string().trim(),
|
||||||
|
display: z.string().trim().optional()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
schemas: z.array(z.string()),
|
||||||
|
id: z.string().trim(),
|
||||||
|
displayName: z.string().trim(),
|
||||||
|
members: z.array(
|
||||||
|
z.object({
|
||||||
|
value: z.string(),
|
||||||
|
display: z.string()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
meta: z.object({
|
||||||
|
resourceType: z.string().trim()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
// console.log("PATCH /Groups/:groupId req.body: ", req.body);
|
||||||
|
// console.log("PATCH /Groups/:groupId req.body: ", req.body.Operations[0]);
|
||||||
|
const group = await req.server.services.scim.updateScimGroupNamePatch({
|
||||||
|
groupId: req.params.groupId,
|
||||||
orgId: req.permission.orgId,
|
orgId: req.permission.orgId,
|
||||||
operations: req.body.Operations
|
operations: req.body.Operations
|
||||||
});
|
});
|
||||||
return user;
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/Groups/:groupId",
|
||||||
|
method: "DELETE",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
groupId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const group = await req.server.services.scim.deleteScimGroup({
|
||||||
|
groupId: req.params.groupId,
|
||||||
|
orgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return group;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -69,7 +69,6 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: "Roll back project secrets to those captured in a secret snapshot version.",
|
description: "Roll back project secrets to those captured in a secret snapshot version.",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
apiKeyAuth: [],
|
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@@ -249,7 +249,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
|
|
||||||
if ((revokeResponse as { error?: Error })?.error) {
|
if ((revokeResponse as { error?: Error })?.error) {
|
||||||
const { error } = revokeResponse as { error?: Error };
|
const { error } = revokeResponse as { error?: Error };
|
||||||
logger.error("Failed to revoke lease", { error: error?.message });
|
logger.error(error?.message, "Failed to revoke lease");
|
||||||
const deletedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
|
const deletedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
|
||||||
status: DynamicSecretLeaseStatus.FailedDeletion,
|
status: DynamicSecretLeaseStatus.FailedDeletion,
|
||||||
statusDetails: error?.message?.slice(0, 255)
|
statusDetails: error?.message?.slice(0, 255)
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export enum SqlProviders {
|
export enum SqlProviders {
|
||||||
Postgres = "postgres"
|
Postgres = "postgres",
|
||||||
|
MySQL = "mysql2"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DynamicSecretSqlDBSchema = z.object({
|
export const DynamicSecretSqlDBSchema = z.object({
|
||||||
@@ -13,7 +14,7 @@ export const DynamicSecretSqlDBSchema = z.object({
|
|||||||
password: z.string(),
|
password: z.string(),
|
||||||
creationStatement: z.string(),
|
creationStatement: z.string(),
|
||||||
revocationStatement: z.string(),
|
revocationStatement: z.string(),
|
||||||
renewStatement: z.string(),
|
renewStatement: z.string().optional(),
|
||||||
ca: z.string().optional()
|
ca: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -48,10 +48,10 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
host: providerInputs.host,
|
host: providerInputs.host,
|
||||||
user: providerInputs.username,
|
user: providerInputs.username,
|
||||||
password: providerInputs.password,
|
password: providerInputs.password,
|
||||||
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
|
|
||||||
ssl,
|
ssl,
|
||||||
pool: { min: 0, max: 1 }
|
pool: { min: 0, max: 1 }
|
||||||
}
|
},
|
||||||
|
acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
});
|
});
|
||||||
return db;
|
return db;
|
||||||
};
|
};
|
||||||
@@ -73,15 +73,25 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const username = alphaNumericNanoId(32);
|
const username = alphaNumericNanoId(32);
|
||||||
const password = generatePassword();
|
const password = generatePassword();
|
||||||
|
const { database } = providerInputs;
|
||||||
const expiration = new Date(expireAt).toISOString();
|
const expiration = new Date(expireAt).toISOString();
|
||||||
|
|
||||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
expiration
|
expiration,
|
||||||
|
database
|
||||||
});
|
});
|
||||||
|
|
||||||
await db.raw(creationStatement.toString());
|
await db.transaction(async (tx) =>
|
||||||
|
Promise.all(
|
||||||
|
creationStatement
|
||||||
|
.toString()
|
||||||
|
.split(";")
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((query) => tx.raw(query))
|
||||||
|
)
|
||||||
|
);
|
||||||
await db.destroy();
|
await db.destroy();
|
||||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||||
};
|
};
|
||||||
@@ -91,9 +101,18 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
const db = await getClient(providerInputs);
|
const db = await getClient(providerInputs);
|
||||||
|
|
||||||
const username = entityId;
|
const username = entityId;
|
||||||
|
const { database } = providerInputs;
|
||||||
|
|
||||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
|
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
|
||||||
await db.raw(revokeStatement);
|
await db.transaction(async (tx) =>
|
||||||
|
Promise.all(
|
||||||
|
revokeStatement
|
||||||
|
.toString()
|
||||||
|
.split(";")
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((query) => tx.raw(query))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
await db.destroy();
|
await db.destroy();
|
||||||
return { entityId: username };
|
return { entityId: username };
|
||||||
@@ -105,9 +124,19 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const username = entityId;
|
const username = entityId;
|
||||||
const expiration = new Date(expireAt).toISOString();
|
const expiration = new Date(expireAt).toISOString();
|
||||||
|
const { database } = providerInputs;
|
||||||
|
|
||||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration });
|
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration, database });
|
||||||
await db.raw(renewStatement);
|
if (renewStatement)
|
||||||
|
await db.transaction(async (tx) =>
|
||||||
|
Promise.all(
|
||||||
|
renewStatement
|
||||||
|
.toString()
|
||||||
|
.split(";")
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((query) => tx.raw(query))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
await db.destroy();
|
await db.destroy();
|
||||||
return { entityId: username };
|
return { entityId: username };
|
||||||
|
157
backend/src/ee/services/group/group-dal.ts
Normal file
157
backend/src/ee/services/group/group-dal.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName, TGroups } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TGroupDALFactory = ReturnType<typeof groupDALFactory>;
|
||||||
|
|
||||||
|
export const groupDALFactory = (db: TDbClient) => {
|
||||||
|
const groupOrm = ormify(db, TableName.Groups);
|
||||||
|
|
||||||
|
const findGroups = async (filter: TFindFilter<TGroups>, { offset, limit, sort, tx }: TFindOpt<TGroups> = {}) => {
|
||||||
|
try {
|
||||||
|
const query = (tx || db)(TableName.Groups)
|
||||||
|
// eslint-disable-next-line
|
||||||
|
.where(buildFindFilter(filter))
|
||||||
|
.select(selectAllTableCols(TableName.Groups));
|
||||||
|
|
||||||
|
if (limit) void query.limit(limit);
|
||||||
|
if (offset) void query.limit(offset);
|
||||||
|
if (sort) {
|
||||||
|
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await query;
|
||||||
|
return res;
|
||||||
|
} catch (err) {
|
||||||
|
throw new DatabaseError({ error: err, name: "Find groups" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findByOrgId = async (orgId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const docs = await (tx || db)(TableName.Groups)
|
||||||
|
.where(`${TableName.Groups}.orgId`, orgId)
|
||||||
|
.leftJoin(TableName.OrgRoles, `${TableName.Groups}.roleId`, `${TableName.OrgRoles}.id`)
|
||||||
|
.select(selectAllTableCols(TableName.Groups))
|
||||||
|
// cr stands for custom role
|
||||||
|
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
|
||||||
|
.select(db.ref("name").as("crName").withSchema(TableName.OrgRoles))
|
||||||
|
.select(db.ref("slug").as("crSlug").withSchema(TableName.OrgRoles))
|
||||||
|
.select(db.ref("description").as("crDescription").withSchema(TableName.OrgRoles))
|
||||||
|
.select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles));
|
||||||
|
return docs.map(({ crId, crDescription, crSlug, crPermission, crName, ...el }) => ({
|
||||||
|
...el,
|
||||||
|
customRole: el.roleId
|
||||||
|
? {
|
||||||
|
id: crId,
|
||||||
|
name: crName,
|
||||||
|
slug: crSlug,
|
||||||
|
permissions: crPermission,
|
||||||
|
description: crDescription
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindByOrgId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const countAllGroupMembers = async ({ orgId, groupId }: { orgId: string; groupId: string }) => {
|
||||||
|
try {
|
||||||
|
interface CountResult {
|
||||||
|
count: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = await db<CountResult>(TableName.OrgMembership)
|
||||||
|
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
||||||
|
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.leftJoin(TableName.UserGroupMembership, function () {
|
||||||
|
this.on(`${TableName.UserGroupMembership}.userId`, "=", `${TableName.Users}.id`).andOn(
|
||||||
|
`${TableName.UserGroupMembership}.groupId`,
|
||||||
|
"=",
|
||||||
|
db.raw("?", [groupId])
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.where({ isGhost: false })
|
||||||
|
.count(`${TableName.Users}.id`)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return parseInt((doc?.count as string) || "0", 10);
|
||||||
|
} catch (err) {
|
||||||
|
throw new DatabaseError({ error: err, name: "Count all group members" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// special query
|
||||||
|
const findAllGroupMembers = async ({
|
||||||
|
orgId,
|
||||||
|
groupId,
|
||||||
|
offset = 0,
|
||||||
|
limit,
|
||||||
|
username
|
||||||
|
}: {
|
||||||
|
orgId: string;
|
||||||
|
groupId: string;
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
username?: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
let query = db(TableName.OrgMembership)
|
||||||
|
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
||||||
|
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.leftJoin(TableName.UserGroupMembership, function () {
|
||||||
|
this.on(`${TableName.UserGroupMembership}.userId`, "=", `${TableName.Users}.id`).andOn(
|
||||||
|
`${TableName.UserGroupMembership}.groupId`,
|
||||||
|
"=",
|
||||||
|
db.raw("?", [groupId])
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("groupId").withSchema(TableName.UserGroupMembership),
|
||||||
|
db.ref("email").withSchema(TableName.Users),
|
||||||
|
db.ref("username").withSchema(TableName.Users),
|
||||||
|
db.ref("firstName").withSchema(TableName.Users),
|
||||||
|
db.ref("lastName").withSchema(TableName.Users),
|
||||||
|
db.ref("id").withSchema(TableName.Users).as("userId")
|
||||||
|
)
|
||||||
|
.where({ isGhost: false })
|
||||||
|
.offset(offset);
|
||||||
|
|
||||||
|
if (limit) {
|
||||||
|
query = query.limit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (username) {
|
||||||
|
query = query.andWhere(`${TableName.Users}.username`, "ilike", `%${username}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const members = await query;
|
||||||
|
|
||||||
|
return members.map(
|
||||||
|
({ email, username: memberUsername, firstName, lastName, userId, groupId: memberGroupId }) => ({
|
||||||
|
id: userId,
|
||||||
|
email,
|
||||||
|
username: memberUsername,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
isPartOfGroup: !!memberGroupId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find all org members" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
findGroups,
|
||||||
|
findByOrgId,
|
||||||
|
countAllGroupMembers,
|
||||||
|
findAllGroupMembers,
|
||||||
|
...groupOrm
|
||||||
|
};
|
||||||
|
};
|
474
backend/src/ee/services/group/group-service.ts
Normal file
474
backend/src/ee/services/group/group-service.ts
Normal file
@@ -0,0 +1,474 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
|
||||||
|
import { OrgMembershipRole, SecretKeyEncoding, TOrgRoles } from "@app/db/schemas";
|
||||||
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
|
import { decryptAsymmetric, encryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
|
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||||
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
|
import { TGroupProjectDALFactory } from "../../../services/group-project/group-project-dal";
|
||||||
|
import { TOrgDALFactory } from "../../../services/org/org-dal";
|
||||||
|
import { TProjectDALFactory } from "../../../services/project/project-dal";
|
||||||
|
import { TProjectBotDALFactory } from "../../../services/project-bot/project-bot-dal";
|
||||||
|
import { TProjectKeyDALFactory } from "../../../services/project-key/project-key-dal";
|
||||||
|
import { TUserDALFactory } from "../../../services/user/user-dal";
|
||||||
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
|
import { TGroupDALFactory } from "./group-dal";
|
||||||
|
import {
|
||||||
|
TAddUserToGroupDTO,
|
||||||
|
TCreateGroupDTO,
|
||||||
|
TDeleteGroupDTO,
|
||||||
|
TListGroupUsersDTO,
|
||||||
|
TRemoveUserFromGroupDTO,
|
||||||
|
TUpdateGroupDTO
|
||||||
|
} from "./group-types";
|
||||||
|
import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal";
|
||||||
|
|
||||||
|
type TGroupServiceFactoryDep = {
|
||||||
|
userDAL: Pick<TUserDALFactory, "findOne" | "findUserEncKeyByUsername">;
|
||||||
|
groupDAL: Pick<
|
||||||
|
TGroupDALFactory,
|
||||||
|
"create" | "findOne" | "update" | "delete" | "findAllGroupMembers" | "countAllGroupMembers"
|
||||||
|
>;
|
||||||
|
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||||
|
orgDAL: Pick<TOrgDALFactory, "findMembership">;
|
||||||
|
userGroupMembershipDAL: Pick<
|
||||||
|
TUserGroupMembershipDALFactory,
|
||||||
|
"findOne" | "create" | "delete" | "filterProjectsByUserMembership"
|
||||||
|
>;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
||||||
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||||
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "create" | "delete" | "findLatestProjectKey">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGroupServiceFactory = ReturnType<typeof groupServiceFactory>;
|
||||||
|
|
||||||
|
export const groupServiceFactory = ({
|
||||||
|
userDAL,
|
||||||
|
groupDAL,
|
||||||
|
groupProjectDAL,
|
||||||
|
orgDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
permissionService,
|
||||||
|
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" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Groups);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
|
if (!plan.groups)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to create group due to plan restriction. Upgrade plan to create group."
|
||||||
|
});
|
||||||
|
|
||||||
|
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
|
||||||
|
role,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
const isCustomRole = Boolean(customRole);
|
||||||
|
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
|
if (!hasRequiredPriviledges) throw new BadRequestError({ message: "Failed to create a more privileged group" });
|
||||||
|
|
||||||
|
const group = await groupDAL.create({
|
||||||
|
name,
|
||||||
|
slug: slug || slugify(`${name}-${alphaNumericNanoId(4)}`),
|
||||||
|
orgId: actorOrgId,
|
||||||
|
role: isCustomRole ? OrgMembershipRole.Custom : role,
|
||||||
|
roleId: customRole?.id
|
||||||
|
});
|
||||||
|
|
||||||
|
return group;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateGroup = async ({
|
||||||
|
currentSlug,
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
role,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TUpdateGroupDTO) => {
|
||||||
|
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
|
if (!plan.groups)
|
||||||
|
throw new BadRequestError({
|
||||||
|
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}` });
|
||||||
|
|
||||||
|
let customRole: TOrgRoles | undefined;
|
||||||
|
if (role) {
|
||||||
|
const { permission: rolePermission, role: customOrgRole } = await permissionService.getOrgPermissionByRole(
|
||||||
|
role,
|
||||||
|
group.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
const isCustomRole = Boolean(customOrgRole);
|
||||||
|
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
|
if (!hasRequiredNewRolePermission)
|
||||||
|
throw new BadRequestError({ message: "Failed to create a more privileged group" });
|
||||||
|
if (isCustomRole) customRole = customOrgRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [updatedGroup] = await groupDAL.update(
|
||||||
|
{
|
||||||
|
orgId: actorOrgId,
|
||||||
|
slug: currentSlug
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
slug: slug ? slugify(slug) : undefined,
|
||||||
|
...(role
|
||||||
|
? {
|
||||||
|
role: customRole ? OrgMembershipRole.Custom : role,
|
||||||
|
roleId: customRole?.id ?? null
|
||||||
|
}
|
||||||
|
: {})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return updatedGroup;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteGroup = async ({ groupSlug, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => {
|
||||||
|
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Groups);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
|
|
||||||
|
if (!plan.groups)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to delete group due to plan restriction. Upgrade plan to delete group."
|
||||||
|
});
|
||||||
|
|
||||||
|
const [group] = await groupDAL.delete({
|
||||||
|
orgId: actorOrgId,
|
||||||
|
slug: groupSlug
|
||||||
|
});
|
||||||
|
|
||||||
|
return group;
|
||||||
|
};
|
||||||
|
|
||||||
|
const listGroupUsers = async ({
|
||||||
|
groupSlug,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
username,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TListGroupUsersDTO) => {
|
||||||
|
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
||||||
|
|
||||||
|
const group = await groupDAL.findOne({
|
||||||
|
orgId: actorOrgId,
|
||||||
|
slug: groupSlug
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!group)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to find group with slug ${groupSlug}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const users = await groupDAL.findAllGroupMembers({
|
||||||
|
orgId: group.orgId,
|
||||||
|
groupId: group.id,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
username
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalCount = await groupDAL.countAllGroupMembers({
|
||||||
|
orgId: group.orgId,
|
||||||
|
groupId: group.id
|
||||||
|
});
|
||||||
|
|
||||||
|
return { users, totalCount };
|
||||||
|
};
|
||||||
|
|
||||||
|
const addUserToGroup = async ({
|
||||||
|
groupSlug,
|
||||||
|
username,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TAddUserToGroupDTO) => {
|
||||||
|
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
|
||||||
|
|
||||||
|
// check if group with slug exists
|
||||||
|
const group = await groupDAL.findOne({
|
||||||
|
orgId: actorOrgId,
|
||||||
|
slug: groupSlug
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!group)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to find group with slug ${groupSlug}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||||
|
|
||||||
|
// check if user has broader or equal to privileges than group
|
||||||
|
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, groupRolePermission);
|
||||||
|
if (!hasRequiredPriviledges)
|
||||||
|
throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" });
|
||||||
|
|
||||||
|
// get user with username
|
||||||
|
const user = await userDAL.findUserEncKeyByUsername({
|
||||||
|
username
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to find user with username ${username}`
|
||||||
|
});
|
||||||
|
|
||||||
|
// check if user group membership already exists
|
||||||
|
const existingUserGroupMembership = await userGroupMembershipDAL.findOne({
|
||||||
|
groupId: group.id,
|
||||||
|
userId: user.userId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingUserGroupMembership)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `User ${username} is already part of the group ${groupSlug}`
|
||||||
|
});
|
||||||
|
|
||||||
|
// check if user is even part of the organization
|
||||||
|
const existingUserOrgMembership = await orgDAL.findMembership({
|
||||||
|
userId: user.userId,
|
||||||
|
orgId: actorOrgId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingUserOrgMembership)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `User ${username} is not part of the organization`
|
||||||
|
});
|
||||||
|
|
||||||
|
await userGroupMembershipDAL.create({
|
||||||
|
userId: user.userId,
|
||||||
|
groupId: group.id
|
||||||
|
});
|
||||||
|
|
||||||
|
// check which projects the group is part of
|
||||||
|
const projectIds = (
|
||||||
|
await groupProjectDAL.find({
|
||||||
|
groupId: group.id
|
||||||
|
})
|
||||||
|
).map((gp) => gp.projectId);
|
||||||
|
|
||||||
|
const keys = await projectKeyDAL.find({
|
||||||
|
receiverId: user.userId,
|
||||||
|
$in: {
|
||||||
|
projectId: projectIds
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const keysSet = new Set(keys.map((k) => k.projectId));
|
||||||
|
const projectsToAddKeyFor = projectIds.filter((p) => !keysSet.has(p));
|
||||||
|
|
||||||
|
for await (const projectId of projectsToAddKeyFor) {
|
||||||
|
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
|
||||||
|
|
||||||
|
if (!ghostUser) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find sudo user"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
|
||||||
|
|
||||||
|
if (!ghostUserLatestKey) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find sudo user latest key"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const bot = await projectBotDAL.findOne({ projectId });
|
||||||
|
|
||||||
|
if (!bot) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find bot"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const botPrivateKey = infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
|
||||||
|
iv: bot.iv,
|
||||||
|
tag: bot.tag,
|
||||||
|
ciphertext: bot.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const plaintextProjectKey = decryptAsymmetric({
|
||||||
|
ciphertext: ghostUserLatestKey.encryptedKey,
|
||||||
|
nonce: ghostUserLatestKey.nonce,
|
||||||
|
publicKey: ghostUserLatestKey.sender.publicKey,
|
||||||
|
privateKey: botPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ciphertext: encryptedKey, nonce } = encryptAsymmetric(plaintextProjectKey, user.publicKey, botPrivateKey);
|
||||||
|
|
||||||
|
await projectKeyDAL.create({
|
||||||
|
encryptedKey,
|
||||||
|
nonce,
|
||||||
|
senderId: ghostUser.id,
|
||||||
|
receiverId: user.userId,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeUserFromGroup = async ({
|
||||||
|
groupSlug,
|
||||||
|
username,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TRemoveUserFromGroupDTO) => {
|
||||||
|
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
|
||||||
|
|
||||||
|
// check if group with slug exists
|
||||||
|
const group = await groupDAL.findOne({
|
||||||
|
orgId: actorOrgId,
|
||||||
|
slug: groupSlug
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!group)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to find group with slug ${groupSlug}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||||
|
|
||||||
|
// check if user has broader or equal to privileges than group
|
||||||
|
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, groupRolePermission);
|
||||||
|
if (!hasRequiredPriviledges)
|
||||||
|
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}`
|
||||||
|
});
|
||||||
|
|
||||||
|
// check if user group membership already exists
|
||||||
|
const existingUserGroupMembership = await userGroupMembershipDAL.findOne({
|
||||||
|
groupId: group.id,
|
||||||
|
userId: user.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingUserGroupMembership)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `User ${username} is not part of the group ${groupSlug}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectIds = (
|
||||||
|
await groupProjectDAL.find({
|
||||||
|
groupId: group.id
|
||||||
|
})
|
||||||
|
).map((gp) => gp.projectId);
|
||||||
|
|
||||||
|
const t = await userGroupMembershipDAL.filterProjectsByUserMembership(user.id, group.id, projectIds);
|
||||||
|
|
||||||
|
const projectsToDeleteKeyFor = projectIds.filter((p) => !t.has(p));
|
||||||
|
|
||||||
|
if (projectsToDeleteKeyFor.length) {
|
||||||
|
await projectKeyDAL.delete({
|
||||||
|
receiverId: user.id,
|
||||||
|
$in: {
|
||||||
|
projectId: projectsToDeleteKeyFor
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await userGroupMembershipDAL.delete({
|
||||||
|
groupId: group.id,
|
||||||
|
userId: user.id
|
||||||
|
});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createGroup,
|
||||||
|
updateGroup,
|
||||||
|
deleteGroup,
|
||||||
|
listGroupUsers,
|
||||||
|
addUserToGroup,
|
||||||
|
removeUserFromGroup
|
||||||
|
};
|
||||||
|
};
|
37
backend/src/ee/services/group/group-types.ts
Normal file
37
backend/src/ee/services/group/group-types.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { TGenericPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TCreateGroupDTO = {
|
||||||
|
name: string;
|
||||||
|
slug?: string;
|
||||||
|
role: string;
|
||||||
|
} & TGenericPermission;
|
||||||
|
|
||||||
|
export type TUpdateGroupDTO = {
|
||||||
|
currentSlug: string;
|
||||||
|
} & Partial<{
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
role: string;
|
||||||
|
}> &
|
||||||
|
TGenericPermission;
|
||||||
|
|
||||||
|
export type TDeleteGroupDTO = {
|
||||||
|
groupSlug: string;
|
||||||
|
} & TGenericPermission;
|
||||||
|
|
||||||
|
export type TListGroupUsersDTO = {
|
||||||
|
groupSlug: string;
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
username?: string;
|
||||||
|
} & TGenericPermission;
|
||||||
|
|
||||||
|
export type TAddUserToGroupDTO = {
|
||||||
|
groupSlug: string;
|
||||||
|
username: string;
|
||||||
|
} & TGenericPermission;
|
||||||
|
|
||||||
|
export type TRemoveUserFromGroupDTO = {
|
||||||
|
groupSlug: string;
|
||||||
|
username: string;
|
||||||
|
} & TGenericPermission;
|
125
backend/src/ee/services/group/user-group-membership-dal.ts
Normal file
125
backend/src/ee/services/group/user-group-membership-dal.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TUserGroupMembershipDALFactory = ReturnType<typeof userGroupMembershipDALFactory>;
|
||||||
|
|
||||||
|
export const userGroupMembershipDALFactory = (db: TDbClient) => {
|
||||||
|
const userGroupMembershipOrm = ormify(db, TableName.UserGroupMembership);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a sub-set of projectIds fed into this function corresponding to projects where either:
|
||||||
|
* - The user is a direct member of the project.
|
||||||
|
* - The user is a member of a group that is a member of the project, excluding projects that they are part of
|
||||||
|
* through the group with id [groupId].
|
||||||
|
*/
|
||||||
|
const filterProjectsByUserMembership = async (userId: string, groupId: string, projectIds: string[]) => {
|
||||||
|
const userProjectMemberships: string[] = await db(TableName.ProjectMembership)
|
||||||
|
.where(`${TableName.ProjectMembership}.userId`, userId)
|
||||||
|
.whereIn(`${TableName.ProjectMembership}.projectId`, projectIds)
|
||||||
|
.pluck(`${TableName.ProjectMembership}.projectId`);
|
||||||
|
|
||||||
|
const userGroupMemberships: string[] = await db(TableName.UserGroupMembership)
|
||||||
|
.where(`${TableName.UserGroupMembership}.userId`, userId)
|
||||||
|
.whereNot(`${TableName.UserGroupMembership}.groupId`, groupId)
|
||||||
|
.join(
|
||||||
|
TableName.GroupProjectMembership,
|
||||||
|
`${TableName.UserGroupMembership}.groupId`,
|
||||||
|
`${TableName.GroupProjectMembership}.groupId`
|
||||||
|
)
|
||||||
|
.whereIn(`${TableName.GroupProjectMembership}.projectId`, projectIds)
|
||||||
|
.pluck(`${TableName.GroupProjectMembership}.projectId`);
|
||||||
|
|
||||||
|
return new Set(userProjectMemberships.concat(userGroupMemberships));
|
||||||
|
};
|
||||||
|
|
||||||
|
// special query
|
||||||
|
const findUserGroupMembershipsInProject = async (usernames: string[], projectId: string) => {
|
||||||
|
try {
|
||||||
|
const usernameDocs: string[] = await db(TableName.UserGroupMembership)
|
||||||
|
.join(
|
||||||
|
TableName.GroupProjectMembership,
|
||||||
|
`${TableName.UserGroupMembership}.groupId`,
|
||||||
|
`${TableName.GroupProjectMembership}.groupId`
|
||||||
|
)
|
||||||
|
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
|
||||||
|
.whereIn(`${TableName.Users}.username`, usernames) // TODO: pluck usernames
|
||||||
|
.pluck(`${TableName.Users}.id`);
|
||||||
|
|
||||||
|
return usernameDocs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find user group members in project" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of users that are part of the group with id [groupId]
|
||||||
|
* that have not yet been added individually to project with id [projectId].
|
||||||
|
*
|
||||||
|
* Note: Filters out users that are part of other groups in the project.
|
||||||
|
* @param groupId
|
||||||
|
* @param projectId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const findGroupMembersNotInProject = async (groupId: string, projectId: string) => {
|
||||||
|
try {
|
||||||
|
// get list of groups in the project with id [projectId]
|
||||||
|
// that that are not the group with id [groupId]
|
||||||
|
const groups: string[] = await db(TableName.GroupProjectMembership)
|
||||||
|
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
|
||||||
|
.whereNot(`${TableName.GroupProjectMembership}.groupId`, groupId)
|
||||||
|
.pluck(`${TableName.GroupProjectMembership}.groupId`);
|
||||||
|
|
||||||
|
// main query
|
||||||
|
const members = await db(TableName.UserGroupMembership)
|
||||||
|
.where(`${TableName.UserGroupMembership}.groupId`, groupId)
|
||||||
|
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.leftJoin(TableName.ProjectMembership, function () {
|
||||||
|
this.on(`${TableName.Users}.id`, "=", `${TableName.ProjectMembership}.userId`).andOn(
|
||||||
|
`${TableName.ProjectMembership}.projectId`,
|
||||||
|
"=",
|
||||||
|
db.raw("?", [projectId])
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.whereNull(`${TableName.ProjectMembership}.userId`)
|
||||||
|
.leftJoin<TUserEncryptionKeys>(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
`${TableName.UserEncryptionKey}.userId`,
|
||||||
|
`${TableName.Users}.id`
|
||||||
|
)
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.UserGroupMembership),
|
||||||
|
db.ref("groupId").withSchema(TableName.UserGroupMembership),
|
||||||
|
db.ref("email").withSchema(TableName.Users),
|
||||||
|
db.ref("username").withSchema(TableName.Users),
|
||||||
|
db.ref("firstName").withSchema(TableName.Users),
|
||||||
|
db.ref("lastName").withSchema(TableName.Users),
|
||||||
|
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||||
|
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
||||||
|
)
|
||||||
|
.where({ isGhost: false }) // MAKE SURE USER IS NOT A GHOST USER
|
||||||
|
.whereNotIn(`${TableName.UserGroupMembership}.userId`, function () {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
this.select(`${TableName.UserGroupMembership}.userId`)
|
||||||
|
.from(TableName.UserGroupMembership)
|
||||||
|
.whereIn(`${TableName.UserGroupMembership}.groupId`, groups);
|
||||||
|
});
|
||||||
|
|
||||||
|
return members.map(({ email, username, firstName, lastName, userId, publicKey, ...data }) => ({
|
||||||
|
...data,
|
||||||
|
user: { email, username, firstName, lastName, id: userId, publicKey }
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find group members not in project" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...userGroupMembershipOrm,
|
||||||
|
filterProjectsByUserMembership,
|
||||||
|
findUserGroupMembershipsInProject,
|
||||||
|
findGroupMembersNotInProject
|
||||||
|
};
|
||||||
|
};
|
@@ -12,7 +12,6 @@ import {
|
|||||||
infisicalSymmetricEncypt
|
infisicalSymmetricEncypt
|
||||||
} from "@app/lib/crypto/encryption";
|
} from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { TOrgPermission } from "@app/lib/types";
|
|
||||||
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
@@ -24,7 +23,7 @@ import { TLicenseServiceFactory } from "../license/license-service";
|
|||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
import { TLdapConfigDALFactory } from "./ldap-config-dal";
|
import { TLdapConfigDALFactory } from "./ldap-config-dal";
|
||||||
import { TCreateLdapCfgDTO, TLdapLoginDTO, TUpdateLdapCfgDTO } from "./ldap-config-types";
|
import { TCreateLdapCfgDTO, TGetLdapCfgDTO, TLdapLoginDTO, TUpdateLdapCfgDTO } from "./ldap-config-types";
|
||||||
|
|
||||||
type TLdapConfigServiceFactoryDep = {
|
type TLdapConfigServiceFactoryDep = {
|
||||||
ldapConfigDAL: TLdapConfigDALFactory;
|
ldapConfigDAL: TLdapConfigDALFactory;
|
||||||
@@ -282,7 +281,7 @@ export const ldapConfigServiceFactory = ({
|
|||||||
orgId,
|
orgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
}: TOrgPermission) => {
|
}: TGetLdapCfgDTO) => {
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap);
|
||||||
return getLdapCfg({
|
return getLdapCfg({
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { TOrgPermission } from "@app/lib/types";
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
|
||||||
export type TCreateLdapCfgDTO = {
|
export type TCreateLdapCfgDTO = {
|
||||||
|
orgId: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
url: string;
|
url: string;
|
||||||
bindDN: string;
|
bindDN: string;
|
||||||
@@ -9,7 +10,9 @@ export type TCreateLdapCfgDTO = {
|
|||||||
caCert: string;
|
caCert: string;
|
||||||
} & TOrgPermission;
|
} & TOrgPermission;
|
||||||
|
|
||||||
export type TUpdateLdapCfgDTO = Partial<{
|
export type TUpdateLdapCfgDTO = {
|
||||||
|
orgId: string;
|
||||||
|
} & Partial<{
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
url: string;
|
url: string;
|
||||||
bindDN: string;
|
bindDN: string;
|
||||||
@@ -19,6 +22,10 @@ export type TUpdateLdapCfgDTO = Partial<{
|
|||||||
}> &
|
}> &
|
||||||
TOrgPermission;
|
TOrgPermission;
|
||||||
|
|
||||||
|
export type TGetLdapCfgDTO = {
|
||||||
|
orgId: string;
|
||||||
|
} & TOrgPermission;
|
||||||
|
|
||||||
export type TLdapLoginDTO = {
|
export type TLdapLoginDTO = {
|
||||||
externalId: string;
|
externalId: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
@@ -20,6 +20,7 @@ export const getDefaultOnPremFeatures = () => {
|
|||||||
samlSSO: false,
|
samlSSO: false,
|
||||||
scim: false,
|
scim: false,
|
||||||
ldap: false,
|
ldap: false,
|
||||||
|
groups: false,
|
||||||
status: null,
|
status: null,
|
||||||
trial_end: null,
|
trial_end: null,
|
||||||
has_used_trial: true,
|
has_used_trial: true,
|
||||||
|
@@ -27,6 +27,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
samlSSO: false,
|
samlSSO: false,
|
||||||
scim: false,
|
scim: false,
|
||||||
ldap: false,
|
ldap: false,
|
||||||
|
groups: false,
|
||||||
status: null,
|
status: null,
|
||||||
trial_end: null,
|
trial_end: null,
|
||||||
has_used_trial: true,
|
has_used_trial: true,
|
||||||
|
@@ -43,6 +43,7 @@ export type TFeatureSet = {
|
|||||||
samlSSO: false;
|
samlSSO: false;
|
||||||
scim: false;
|
scim: false;
|
||||||
ldap: false;
|
ldap: false;
|
||||||
|
groups: false;
|
||||||
status: null;
|
status: null;
|
||||||
trial_end: null;
|
trial_end: null;
|
||||||
has_used_trial: true;
|
has_used_trial: true;
|
||||||
|
@@ -18,6 +18,7 @@ export enum OrgPermissionSubjects {
|
|||||||
Sso = "sso",
|
Sso = "sso",
|
||||||
Scim = "scim",
|
Scim = "scim",
|
||||||
Ldap = "ldap",
|
Ldap = "ldap",
|
||||||
|
Groups = "groups",
|
||||||
Billing = "billing",
|
Billing = "billing",
|
||||||
SecretScanning = "secret-scanning",
|
SecretScanning = "secret-scanning",
|
||||||
Identity = "identity"
|
Identity = "identity"
|
||||||
@@ -33,6 +34,7 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
|
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Scim]
|
| [OrgPermissionActions, OrgPermissionSubjects.Scim]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Ldap]
|
| [OrgPermissionActions, OrgPermissionSubjects.Ldap]
|
||||||
|
| [OrgPermissionActions, OrgPermissionSubjects.Groups]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity];
|
| [OrgPermissionActions, OrgPermissionSubjects.Identity];
|
||||||
@@ -83,6 +85,11 @@ const buildAdminPermission = () => {
|
|||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap);
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Ldap);
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Ldap);
|
||||||
|
|
||||||
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
||||||
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.Groups);
|
||||||
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
|
||||||
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Groups);
|
||||||
|
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
|
||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
|
||||||
@@ -105,6 +112,7 @@ const buildMemberPermission = () => {
|
|||||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
||||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||||
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||||
|
@@ -45,6 +45,42 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const getProjectPermission = async (userId: string, projectId: string) => {
|
const getProjectPermission = async (userId: string, projectId: string) => {
|
||||||
try {
|
try {
|
||||||
|
const groups: string[] = await db(TableName.GroupProjectMembership)
|
||||||
|
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
|
||||||
|
.pluck(`${TableName.GroupProjectMembership}.groupId`);
|
||||||
|
|
||||||
|
const groupDocs = await db(TableName.UserGroupMembership)
|
||||||
|
.where(`${TableName.UserGroupMembership}.userId`, userId)
|
||||||
|
.whereIn(`${TableName.UserGroupMembership}.groupId`, groups)
|
||||||
|
.join(
|
||||||
|
TableName.GroupProjectMembership,
|
||||||
|
`${TableName.GroupProjectMembership}.groupId`,
|
||||||
|
`${TableName.UserGroupMembership}.groupId`
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
TableName.GroupProjectMembershipRole,
|
||||||
|
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
|
||||||
|
`${TableName.GroupProjectMembership}.id`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ProjectRoles,
|
||||||
|
`${TableName.GroupProjectMembershipRole}.customRoleId`,
|
||||||
|
`${TableName.ProjectRoles}.id`
|
||||||
|
)
|
||||||
|
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||||
|
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
||||||
|
.select(selectAllTableCols(TableName.GroupProjectMembershipRole))
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"),
|
||||||
|
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("membershipCreatedAt"),
|
||||||
|
db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("membershipUpdatedAt"),
|
||||||
|
db.ref("projectId").withSchema(TableName.GroupProjectMembership),
|
||||||
|
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||||
|
db.ref("orgId").withSchema(TableName.Project),
|
||||||
|
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug")
|
||||||
|
)
|
||||||
|
.select("permissions");
|
||||||
|
|
||||||
const docs = await db(TableName.ProjectMembership)
|
const docs = await db(TableName.ProjectMembership)
|
||||||
.join(
|
.join(
|
||||||
TableName.ProjectUserMembershipRole,
|
TableName.ProjectUserMembershipRole,
|
||||||
@@ -68,10 +104,9 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
.select(selectAllTableCols(TableName.ProjectUserMembershipRole))
|
.select(selectAllTableCols(TableName.ProjectUserMembershipRole))
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"),
|
db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"),
|
||||||
// TODO(roll-forward-migration): remove this field when we drop this in next migration after a week
|
|
||||||
db.ref("role").withSchema(TableName.ProjectMembership).as("oldRoleField"),
|
|
||||||
db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"),
|
db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"),
|
||||||
db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
|
db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
|
||||||
|
db.ref("projectId").withSchema(TableName.ProjectMembership),
|
||||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||||
db.ref("orgId").withSchema(TableName.Project),
|
db.ref("orgId").withSchema(TableName.Project),
|
||||||
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
||||||
@@ -93,19 +128,11 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const permission = sqlNestRelationships({
|
const permission = sqlNestRelationships({
|
||||||
data: docs,
|
data: docs,
|
||||||
key: "membershipId",
|
key: "projectId",
|
||||||
parentMapper: ({
|
parentMapper: ({ orgId, orgAuthEnforced, membershipId, membershipCreatedAt, membershipUpdatedAt }) => ({
|
||||||
orgId,
|
|
||||||
orgAuthEnforced,
|
|
||||||
membershipId,
|
|
||||||
membershipCreatedAt,
|
|
||||||
membershipUpdatedAt,
|
|
||||||
oldRoleField
|
|
||||||
}) => ({
|
|
||||||
orgId,
|
orgId,
|
||||||
orgAuthEnforced,
|
orgAuthEnforced,
|
||||||
userId,
|
userId,
|
||||||
role: oldRoleField,
|
|
||||||
id: membershipId,
|
id: membershipId,
|
||||||
projectId,
|
projectId,
|
||||||
createdAt: membershipCreatedAt,
|
createdAt: membershipCreatedAt,
|
||||||
@@ -145,19 +172,58 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!permission?.[0]) return undefined;
|
const groupPermission = groupDocs.length
|
||||||
|
? sqlNestRelationships({
|
||||||
|
data: groupDocs,
|
||||||
|
key: "projectId",
|
||||||
|
parentMapper: ({ orgId, orgAuthEnforced, membershipId, membershipCreatedAt, membershipUpdatedAt }) => ({
|
||||||
|
orgId,
|
||||||
|
orgAuthEnforced,
|
||||||
|
userId,
|
||||||
|
id: membershipId,
|
||||||
|
projectId,
|
||||||
|
createdAt: membershipCreatedAt,
|
||||||
|
updatedAt: membershipUpdatedAt
|
||||||
|
}),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "id",
|
||||||
|
label: "roles" as const,
|
||||||
|
mapper: (data) =>
|
||||||
|
ProjectUserMembershipRolesSchema.extend({
|
||||||
|
permissions: z.unknown(),
|
||||||
|
customRoleSlug: z.string().optional().nullable()
|
||||||
|
}).parse(data)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (!permission?.[0] && !groupPermission[0]) return undefined;
|
||||||
|
|
||||||
// when introducting cron mode change it here
|
// when introducting cron mode change it here
|
||||||
const activeRoles = permission?.[0]?.roles?.filter(
|
const activeRoles =
|
||||||
({ isTemporary, temporaryAccessEndTime }) =>
|
permission?.[0]?.roles?.filter(
|
||||||
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
);
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
const activeGroupRoles =
|
||||||
|
groupPermission?.[0]?.roles?.filter(
|
||||||
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
const activeAdditionalPrivileges = permission?.[0]?.additionalPrivileges?.filter(
|
const activeAdditionalPrivileges = permission?.[0]?.additionalPrivileges?.filter(
|
||||||
({ isTemporary, temporaryAccessEndTime }) =>
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
);
|
);
|
||||||
|
|
||||||
return { ...permission[0], roles: activeRoles, additionalPrivileges: activeAdditionalPrivileges };
|
return {
|
||||||
|
...(permission[0] || groupPermission[0]),
|
||||||
|
roles: [...activeRoles, ...activeGroupRoles],
|
||||||
|
additionalPrivileges: activeAdditionalPrivileges
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "GetProjectPermission" });
|
throw new DatabaseError({ error, name: "GetProjectPermission" });
|
||||||
}
|
}
|
||||||
@@ -193,7 +259,6 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
|
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
|
||||||
db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project
|
db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project
|
||||||
db.ref("role").withSchema(TableName.IdentityProjectMembership).as("oldRoleField"),
|
|
||||||
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
|
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
|
||||||
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
|
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
|
||||||
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
||||||
@@ -222,11 +287,10 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
const permission = sqlNestRelationships({
|
const permission = sqlNestRelationships({
|
||||||
data: docs,
|
data: docs,
|
||||||
key: "membershipId",
|
key: "membershipId",
|
||||||
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, oldRoleField, orgId }) => ({
|
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId }) => ({
|
||||||
id: membershipId,
|
id: membershipId,
|
||||||
identityId,
|
identityId,
|
||||||
projectId,
|
projectId,
|
||||||
role: oldRoleField,
|
|
||||||
createdAt: membershipCreatedAt,
|
createdAt: membershipCreatedAt,
|
||||||
updatedAt: membershipUpdatedAt,
|
updatedAt: membershipUpdatedAt,
|
||||||
orgId,
|
orgId,
|
||||||
|
@@ -12,6 +12,7 @@ export enum ProjectPermissionActions {
|
|||||||
export enum ProjectPermissionSub {
|
export enum ProjectPermissionSub {
|
||||||
Role = "role",
|
Role = "role",
|
||||||
Member = "member",
|
Member = "member",
|
||||||
|
Groups = "groups",
|
||||||
Settings = "settings",
|
Settings = "settings",
|
||||||
Integrations = "integrations",
|
Integrations = "integrations",
|
||||||
Webhooks = "webhooks",
|
Webhooks = "webhooks",
|
||||||
@@ -41,6 +42,7 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Member]
|
| [ProjectPermissionActions, ProjectPermissionSub.Member]
|
||||||
|
| [ProjectPermissionActions, ProjectPermissionSub.Groups]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Integrations]
|
| [ProjectPermissionActions, ProjectPermissionSub.Integrations]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Webhooks]
|
| [ProjectPermissionActions, ProjectPermissionSub.Webhooks]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.AuditLogs]
|
| [ProjectPermissionActions, ProjectPermissionSub.AuditLogs]
|
||||||
@@ -82,6 +84,11 @@ const buildAdminPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
|
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
|
||||||
|
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
|
||||||
|
can(ProjectPermissionActions.Create, ProjectPermissionSub.Groups);
|
||||||
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Groups);
|
||||||
|
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Groups);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Role);
|
can(ProjectPermissionActions.Create, ProjectPermissionSub.Role);
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Role);
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Role);
|
||||||
@@ -157,6 +164,8 @@ const buildMemberPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
can(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
||||||
|
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
|
can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
|
||||||
@@ -209,6 +218,7 @@ const buildViewerPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { TListScimUsers, TScimUser } from "./scim-types";
|
import { TListScimGroups, TListScimUsers, TScimGroup, TScimUser } from "./scim-types";
|
||||||
|
|
||||||
export const buildScimUserList = ({
|
export const buildScimUserList = ({
|
||||||
scimUsers,
|
scimUsers,
|
||||||
@@ -62,3 +62,47 @@ export const buildScimUser = ({
|
|||||||
|
|
||||||
return scimUser;
|
return scimUser;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const buildScimGroupList = ({
|
||||||
|
scimGroups,
|
||||||
|
offset,
|
||||||
|
limit
|
||||||
|
}: {
|
||||||
|
scimGroups: TScimGroup[];
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
}): TListScimGroups => {
|
||||||
|
return {
|
||||||
|
Resources: scimGroups,
|
||||||
|
itemsPerPage: limit,
|
||||||
|
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
||||||
|
startIndex: offset,
|
||||||
|
totalResults: scimGroups.length
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildScimGroup = ({
|
||||||
|
groupId,
|
||||||
|
name,
|
||||||
|
members
|
||||||
|
}: {
|
||||||
|
groupId: string;
|
||||||
|
name: string;
|
||||||
|
members: {
|
||||||
|
value: string;
|
||||||
|
display: string;
|
||||||
|
}[];
|
||||||
|
}): TScimGroup => {
|
||||||
|
const scimGroup = {
|
||||||
|
schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"],
|
||||||
|
id: groupId,
|
||||||
|
displayName: name,
|
||||||
|
members,
|
||||||
|
meta: {
|
||||||
|
resourceType: "Group",
|
||||||
|
location: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return scimGroup;
|
||||||
|
};
|
||||||
|
@@ -1,10 +1,13 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import slugify from "@sindresorhus/slugify";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
import { OrgMembershipRole, OrgMembershipStatus, TableName } from "@app/db/schemas";
|
import { OrgMembershipRole, OrgMembershipStatus, TableName, TGroups } from "@app/db/schemas";
|
||||||
|
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||||
import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
|
import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { TOrgPermission } from "@app/lib/types";
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
@@ -17,16 +20,23 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
|
|||||||
import { TLicenseServiceFactory } from "../license/license-service";
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
import { buildScimUser, buildScimUserList } from "./scim-fns";
|
import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList } from "./scim-fns";
|
||||||
import {
|
import {
|
||||||
|
TCreateScimGroupDTO,
|
||||||
TCreateScimTokenDTO,
|
TCreateScimTokenDTO,
|
||||||
TCreateScimUserDTO,
|
TCreateScimUserDTO,
|
||||||
|
TDeleteScimGroupDTO,
|
||||||
TDeleteScimTokenDTO,
|
TDeleteScimTokenDTO,
|
||||||
|
TDeleteScimUserDTO,
|
||||||
|
TGetScimGroupDTO,
|
||||||
TGetScimUserDTO,
|
TGetScimUserDTO,
|
||||||
|
TListScimGroupsDTO,
|
||||||
TListScimUsers,
|
TListScimUsers,
|
||||||
TListScimUsersDTO,
|
TListScimUsersDTO,
|
||||||
TReplaceScimUserDTO,
|
TReplaceScimUserDTO,
|
||||||
TScimTokenJwtPayload,
|
TScimTokenJwtPayload,
|
||||||
|
TUpdateScimGroupNamePatchDTO,
|
||||||
|
TUpdateScimGroupNamePutDTO,
|
||||||
TUpdateScimUserDTO
|
TUpdateScimUserDTO
|
||||||
} from "./scim-types";
|
} from "./scim-types";
|
||||||
|
|
||||||
@@ -39,6 +49,7 @@ type TScimServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
projectDAL: Pick<TProjectDALFactory, "find">;
|
projectDAL: Pick<TProjectDALFactory, "find">;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete">;
|
||||||
|
groupDAL: Pick<TGroupDALFactory, "create" | "findOne" | "findAllGroupMembers" | "update" | "delete" | "findGroups">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
smtpService: TSmtpService;
|
smtpService: TSmtpService;
|
||||||
@@ -53,6 +64,7 @@ export const scimServiceFactory = ({
|
|||||||
orgDAL,
|
orgDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
|
groupDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
smtpService
|
smtpService
|
||||||
}: TScimServiceFactoryDep) => {
|
}: TScimServiceFactoryDep) => {
|
||||||
@@ -423,6 +435,221 @@ export const scimServiceFactory = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteScimUser = async ({ userId, orgId }: TDeleteScimUserDTO) => {
|
||||||
|
const [membership] = await orgDAL
|
||||||
|
.findMembership({
|
||||||
|
userId,
|
||||||
|
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "User not found",
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "User not found",
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership.scimEnabled) {
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "SCIM is disabled for the organization",
|
||||||
|
status: 403
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteOrgMembership({
|
||||||
|
orgMembershipId: membership.id,
|
||||||
|
orgId: membership.orgId,
|
||||||
|
orgDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectMembershipDAL
|
||||||
|
});
|
||||||
|
|
||||||
|
return {}; // intentionally return empty object upon success
|
||||||
|
};
|
||||||
|
|
||||||
|
const listScimGroups = async ({ orgId, offset, limit }: TListScimGroupsDTO) => {
|
||||||
|
const org = await orgDAL.findById(orgId);
|
||||||
|
|
||||||
|
if (!org.scimEnabled)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "SCIM is disabled for the organization",
|
||||||
|
status: 403
|
||||||
|
});
|
||||||
|
|
||||||
|
const groups = await groupDAL.findGroups({
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
const scimGroups = groups.map((group) =>
|
||||||
|
buildScimGroup({
|
||||||
|
groupId: group.id,
|
||||||
|
name: group.name,
|
||||||
|
members: []
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return buildScimGroupList({
|
||||||
|
scimGroups,
|
||||||
|
offset,
|
||||||
|
limit
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createScimGroup = async ({ displayName, orgId }: TCreateScimGroupDTO) => {
|
||||||
|
const org = await orgDAL.findById(orgId);
|
||||||
|
|
||||||
|
if (!org.scimEnabled)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "SCIM is disabled for the organization",
|
||||||
|
status: 403
|
||||||
|
});
|
||||||
|
|
||||||
|
const group = await groupDAL.create({
|
||||||
|
name: displayName,
|
||||||
|
slug: slugify(`${displayName}-${alphaNumericNanoId(4)}`),
|
||||||
|
orgId,
|
||||||
|
role: OrgMembershipRole.NoAccess
|
||||||
|
});
|
||||||
|
|
||||||
|
return buildScimGroup({
|
||||||
|
groupId: group.id,
|
||||||
|
name: group.name,
|
||||||
|
members: []
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScimGroup = async ({ groupId, orgId }: TGetScimGroupDTO) => {
|
||||||
|
const group = await groupDAL.findOne({
|
||||||
|
id: groupId,
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "Group Not Found",
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = await groupDAL.findAllGroupMembers({
|
||||||
|
orgId: group.orgId,
|
||||||
|
groupId: group.id
|
||||||
|
});
|
||||||
|
|
||||||
|
return buildScimGroup({
|
||||||
|
groupId: group.id,
|
||||||
|
name: group.name,
|
||||||
|
members: users
|
||||||
|
.filter((user) => user.isPartOfGroup)
|
||||||
|
.map((user) => ({
|
||||||
|
value: user.id,
|
||||||
|
display: `${user.firstName} ${user.lastName}`
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateScimGroupNamePut = async ({ groupId, orgId, displayName }: TUpdateScimGroupNamePutDTO) => {
|
||||||
|
const [group] = await groupDAL.update(
|
||||||
|
{
|
||||||
|
id: groupId,
|
||||||
|
orgId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: displayName
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "Group Not Found",
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildScimGroup({
|
||||||
|
groupId: group.id,
|
||||||
|
name: group.name,
|
||||||
|
members: []
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: add support for add/remove op
|
||||||
|
const updateScimGroupNamePatch = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => {
|
||||||
|
const org = await orgDAL.findById(orgId);
|
||||||
|
|
||||||
|
if (!org.scimEnabled)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "SCIM is disabled for the organization",
|
||||||
|
status: 403
|
||||||
|
});
|
||||||
|
|
||||||
|
let group: TGroups | undefined;
|
||||||
|
for await (const operation of operations) {
|
||||||
|
switch (operation.op) {
|
||||||
|
case "replace": {
|
||||||
|
await groupDAL.update(
|
||||||
|
{
|
||||||
|
id: groupId,
|
||||||
|
orgId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: operation.value.displayName
|
||||||
|
}
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "add": {
|
||||||
|
// TODO
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "remove": {
|
||||||
|
// TODO
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "Invalid Operation",
|
||||||
|
status: 400
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "Group Not Found",
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildScimGroup({
|
||||||
|
groupId: group.id,
|
||||||
|
name: group.name,
|
||||||
|
members: []
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteScimGroup = async ({ groupId, orgId }: TDeleteScimGroupDTO) => {
|
||||||
|
const [group] = await groupDAL.delete({
|
||||||
|
id: groupId,
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "Group Not Found",
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}; // intentionally return empty object upon success
|
||||||
|
};
|
||||||
|
|
||||||
const fnValidateScimToken = async (token: TScimTokenJwtPayload) => {
|
const fnValidateScimToken = async (token: TScimTokenJwtPayload) => {
|
||||||
const scimToken = await scimDAL.findById(token.scimTokenId);
|
const scimToken = await scimDAL.findById(token.scimTokenId);
|
||||||
if (!scimToken) throw new UnauthorizedError();
|
if (!scimToken) throw new UnauthorizedError();
|
||||||
@@ -455,6 +682,13 @@ export const scimServiceFactory = ({
|
|||||||
createScimUser,
|
createScimUser,
|
||||||
updateScimUser,
|
updateScimUser,
|
||||||
replaceScimUser,
|
replaceScimUser,
|
||||||
|
deleteScimUser,
|
||||||
|
listScimGroups,
|
||||||
|
createScimGroup,
|
||||||
|
getScimGroup,
|
||||||
|
deleteScimGroup,
|
||||||
|
updateScimGroupNamePut,
|
||||||
|
updateScimGroupNamePatch,
|
||||||
fnValidateScimToken
|
fnValidateScimToken
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -59,6 +59,73 @@ export type TReplaceScimUserDTO = {
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TDeleteScimUserDTO = {
|
||||||
|
userId: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TListScimGroupsDTO = {
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TListScimGroups = {
|
||||||
|
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"];
|
||||||
|
totalResults: number;
|
||||||
|
Resources: TScimGroup[];
|
||||||
|
itemsPerPage: number;
|
||||||
|
startIndex: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateScimGroupDTO = {
|
||||||
|
displayName: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetScimGroupDTO = {
|
||||||
|
groupId: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateScimGroupNamePutDTO = {
|
||||||
|
groupId: string;
|
||||||
|
orgId: string;
|
||||||
|
displayName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateScimGroupNamePatchDTO = {
|
||||||
|
groupId: string;
|
||||||
|
orgId: string;
|
||||||
|
operations: (TRemoveOp | TReplaceOp | TAddOp)[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type TReplaceOp = {
|
||||||
|
op: "replace";
|
||||||
|
value: {
|
||||||
|
id: string;
|
||||||
|
displayName: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type TRemoveOp = {
|
||||||
|
op: "remove";
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TAddOp = {
|
||||||
|
op: "add";
|
||||||
|
value: {
|
||||||
|
value: string;
|
||||||
|
display?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteScimGroupDTO = {
|
||||||
|
groupId: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TScimTokenJwtPayload = {
|
export type TScimTokenJwtPayload = {
|
||||||
scimTokenId: string;
|
scimTokenId: string;
|
||||||
authTokenType: string;
|
authTokenType: string;
|
||||||
@@ -86,3 +153,17 @@ export type TScimUser = {
|
|||||||
location: null;
|
location: null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TScimGroup = {
|
||||||
|
schemas: string[];
|
||||||
|
id: string;
|
||||||
|
displayName: string;
|
||||||
|
members: {
|
||||||
|
value: string;
|
||||||
|
display: string;
|
||||||
|
}[];
|
||||||
|
meta: {
|
||||||
|
resourceType: string;
|
||||||
|
location: null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@@ -1,3 +1,34 @@
|
|||||||
|
export const GROUPS = {
|
||||||
|
CREATE: {
|
||||||
|
name: "The name of the group to create.",
|
||||||
|
slug: "The slug of the group to create.",
|
||||||
|
role: "The role of the group to create."
|
||||||
|
},
|
||||||
|
UPDATE: {
|
||||||
|
currentSlug: "The current slug 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: {
|
||||||
|
slug: "The slug of the group to delete"
|
||||||
|
},
|
||||||
|
LIST_USERS: {
|
||||||
|
slug: "The slug 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.",
|
||||||
|
username: "The username of the user to add to the group."
|
||||||
|
},
|
||||||
|
DELETE_USER: {
|
||||||
|
slug: "The slug of the group to remove the user from.",
|
||||||
|
username: "The username of the user to remove from the group."
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const IDENTITIES = {
|
export const IDENTITIES = {
|
||||||
CREATE: {
|
CREATE: {
|
||||||
name: "The name of the identity to create.",
|
name: "The name of the identity to create.",
|
||||||
@@ -79,6 +110,9 @@ export const ORGANIZATIONS = {
|
|||||||
},
|
},
|
||||||
GET_PROJECTS: {
|
GET_PROJECTS: {
|
||||||
organizationId: "The ID of the organization to get projects from."
|
organizationId: "The ID of the organization to get projects from."
|
||||||
|
},
|
||||||
|
LIST_GROUPS: {
|
||||||
|
organizationId: "The ID of the organization to list groups for."
|
||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -141,6 +175,29 @@ export const PROJECTS = {
|
|||||||
},
|
},
|
||||||
ROLLBACK_TO_SNAPSHOT: {
|
ROLLBACK_TO_SNAPSHOT: {
|
||||||
secretSnapshotId: "The ID of the snapshot to rollback to."
|
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.",
|
||||||
|
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.",
|
||||||
|
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."
|
||||||
|
},
|
||||||
|
LIST_GROUPS_IN_PROJECT: {
|
||||||
|
projectSlug: "The slug of the project to list groups for."
|
||||||
|
},
|
||||||
|
LIST_INTEGRATION: {
|
||||||
|
workspaceId: "The ID of the project to list integrations for."
|
||||||
|
},
|
||||||
|
LIST_INTEGRATION_AUTHORIZATION: {
|
||||||
|
workspaceId: "The ID of the project to list integration auths for."
|
||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -215,7 +272,8 @@ export const SECRETS = {
|
|||||||
|
|
||||||
export const RAW_SECRETS = {
|
export const RAW_SECRETS = {
|
||||||
LIST: {
|
LIST: {
|
||||||
recursive: "Whether or not to fetch all secrets from the specified base path, and all of its subdirectories.",
|
recursive:
|
||||||
|
"Whether or not to fetch all secrets from the specified base path, and all of its subdirectories. Note, the max depth is 20 deep.",
|
||||||
workspaceId: "The ID of the project to list secrets from.",
|
workspaceId: "The ID of the project to list secrets from.",
|
||||||
workspaceSlug: "The slug of the project to list secrets from. This parameter is only usable by machine identities.",
|
workspaceSlug: "The slug of the project to list secrets from. This parameter is only usable by machine identities.",
|
||||||
environment: "The slug of the environment to list secrets from.",
|
environment: "The slug of the environment to list secrets from.",
|
||||||
@@ -502,11 +560,8 @@ export const INTEGRATION_AUTH = {
|
|||||||
url: "",
|
url: "",
|
||||||
namespace: "",
|
namespace: "",
|
||||||
refreshToken: "The refresh token for integration authorization."
|
refreshToken: "The refresh token for integration authorization."
|
||||||
},
|
|
||||||
LIST_AUTHORIZATION: {
|
|
||||||
workspaceId: "The ID of the project to list integration auths for."
|
|
||||||
}
|
}
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
export const INTEGRATION = {
|
export const INTEGRATION = {
|
||||||
CREATE: {
|
CREATE: {
|
||||||
|
@@ -0,0 +1,2 @@
|
|||||||
|
export const getLastMidnightDateISO = (last = 1) =>
|
||||||
|
`${new Date(new Date().setDate(new Date().getDate() - last)).toISOString().slice(0, 10)}T00:00:00Z`;
|
||||||
|
@@ -2,5 +2,6 @@
|
|||||||
// Full credits goes to https://github.com/rayapps to those functions
|
// Full credits goes to https://github.com/rayapps to those functions
|
||||||
// Code taken to keep in in house and to adjust somethings for our needs
|
// Code taken to keep in in house and to adjust somethings for our needs
|
||||||
export * from "./array";
|
export * from "./array";
|
||||||
|
export * from "./dates";
|
||||||
export * from "./object";
|
export * from "./object";
|
||||||
export * from "./string";
|
export * from "./string";
|
||||||
|
@@ -1,5 +1,17 @@
|
|||||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export type TGenericPermission = {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
actorOrgId: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO(dangtony98): ideally move service fns to use TGenericPermission
|
||||||
|
* because TOrgPermission [orgId] is not as relevant anymore with the
|
||||||
|
* introduction of organizationIds bound to all user tokens
|
||||||
|
*/
|
||||||
export type TOrgPermission = {
|
export type TOrgPermission = {
|
||||||
actor: ActorType;
|
actor: ActorType;
|
||||||
actorId: string;
|
actorId: string;
|
||||||
@@ -16,6 +28,15 @@ export type TProjectPermission = {
|
|||||||
actorOrgId: string;
|
actorOrgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// same as TProjectPermission but with projectSlug requirement instead of projectId
|
||||||
|
export type TProjectSlugPermission = {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
projectSlug: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
actorOrgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type RequiredKeys<T> = {
|
export type RequiredKeys<T> = {
|
||||||
[K in keyof T]-?: undefined extends T[K] ? never : K;
|
[K in keyof T]-?: undefined extends T[K] ? never : K;
|
||||||
}[keyof T];
|
}[keyof T];
|
||||||
|
@@ -61,11 +61,11 @@ export type TQueueJobTypes = {
|
|||||||
};
|
};
|
||||||
[QueueName.SecretWebhook]: {
|
[QueueName.SecretWebhook]: {
|
||||||
name: QueueJobs.SecWebhook;
|
name: QueueJobs.SecWebhook;
|
||||||
payload: { projectId: string; environment: string; secretPath: string };
|
payload: { projectId: string; environment: string; secretPath: string; depth?: number };
|
||||||
};
|
};
|
||||||
[QueueName.IntegrationSync]: {
|
[QueueName.IntegrationSync]: {
|
||||||
name: QueueJobs.IntegrationSync;
|
name: QueueJobs.IntegrationSync;
|
||||||
payload: { projectId: string; environment: string; secretPath: string };
|
payload: { projectId: string; environment: string; secretPath: string; depth?: number };
|
||||||
};
|
};
|
||||||
[QueueName.SecretFullRepoScan]: {
|
[QueueName.SecretFullRepoScan]: {
|
||||||
name: QueueJobs.SecretScan;
|
name: QueueJobs.SecretScan;
|
||||||
|
@@ -30,12 +30,6 @@ export const fastifySwagger = fp(async (fastify) => {
|
|||||||
scheme: "bearer",
|
scheme: "bearer",
|
||||||
bearerFormat: "JWT",
|
bearerFormat: "JWT",
|
||||||
description: "An access token in Infisical"
|
description: "An access token in Infisical"
|
||||||
},
|
|
||||||
apiKeyAuth: {
|
|
||||||
type: "apiKey",
|
|
||||||
in: "header",
|
|
||||||
name: "X-API-Key",
|
|
||||||
description: "An API Key in Infisical"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,6 +11,9 @@ import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/pro
|
|||||||
import { dynamicSecretLeaseDALFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal";
|
import { dynamicSecretLeaseDALFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal";
|
||||||
import { dynamicSecretLeaseQueueServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue";
|
import { dynamicSecretLeaseQueueServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue";
|
||||||
import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
||||||
|
import { groupDALFactory } from "@app/ee/services/group/group-dal";
|
||||||
|
import { groupServiceFactory } from "@app/ee/services/group/group-service";
|
||||||
|
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||||
import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal";
|
import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal";
|
||||||
import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
||||||
import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal";
|
import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal";
|
||||||
@@ -58,6 +61,9 @@ import { authPaswordServiceFactory } from "@app/services/auth/auth-password-serv
|
|||||||
import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service";
|
import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service";
|
||||||
import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal";
|
import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal";
|
||||||
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
|
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
|
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
||||||
|
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||||
import { identityDALFactory } from "@app/services/identity/identity-dal";
|
import { identityDALFactory } from "@app/services/identity/identity-dal";
|
||||||
import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal";
|
import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal";
|
||||||
import { identityServiceFactory } from "@app/services/identity/identity-service";
|
import { identityServiceFactory } from "@app/services/identity/identity-service";
|
||||||
@@ -207,6 +213,10 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const gitAppInstallSessionDAL = gitAppInstallSessionDALFactory(db);
|
const gitAppInstallSessionDAL = gitAppInstallSessionDALFactory(db);
|
||||||
const gitAppOrgDAL = gitAppDALFactory(db);
|
const gitAppOrgDAL = gitAppDALFactory(db);
|
||||||
|
const groupDAL = groupDALFactory(db);
|
||||||
|
const groupProjectDAL = groupProjectDALFactory(db);
|
||||||
|
const groupProjectMembershipRoleDAL = groupProjectMembershipRoleDALFactory(db);
|
||||||
|
const userGroupMembershipDAL = userGroupMembershipDALFactory(db);
|
||||||
const secretScanningDAL = secretScanningDALFactory(db);
|
const secretScanningDAL = secretScanningDALFactory(db);
|
||||||
const licenseDAL = licenseDALFactory(db);
|
const licenseDAL = licenseDALFactory(db);
|
||||||
const dynamicSecretDAL = dynamicSecretDALFactory(db);
|
const dynamicSecretDAL = dynamicSecretDALFactory(db);
|
||||||
@@ -249,6 +259,29 @@ export const registerRoutes = async (
|
|||||||
samlConfigDAL,
|
samlConfigDAL,
|
||||||
licenseService
|
licenseService
|
||||||
});
|
});
|
||||||
|
const groupService = groupServiceFactory({
|
||||||
|
userDAL,
|
||||||
|
groupDAL,
|
||||||
|
groupProjectDAL,
|
||||||
|
orgDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
permissionService,
|
||||||
|
licenseService
|
||||||
|
});
|
||||||
|
const groupProjectService = groupProjectServiceFactory({
|
||||||
|
groupDAL,
|
||||||
|
groupProjectDAL,
|
||||||
|
groupProjectMembershipRoleDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
projectRoleDAL,
|
||||||
|
permissionService
|
||||||
|
});
|
||||||
const scimService = scimServiceFactory({
|
const scimService = scimServiceFactory({
|
||||||
licenseService,
|
licenseService,
|
||||||
scimDAL,
|
scimDAL,
|
||||||
@@ -256,6 +289,7 @@ export const registerRoutes = async (
|
|||||||
orgDAL,
|
orgDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
|
groupDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
smtpService
|
smtpService
|
||||||
});
|
});
|
||||||
@@ -302,6 +336,7 @@ export const registerRoutes = async (
|
|||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
userDAL,
|
userDAL,
|
||||||
|
groupDAL,
|
||||||
orgBotDAL
|
orgBotDAL
|
||||||
});
|
});
|
||||||
const signupService = authSignupServiceFactory({
|
const signupService = authSignupServiceFactory({
|
||||||
@@ -347,6 +382,7 @@ export const registerRoutes = async (
|
|||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
@@ -411,7 +447,12 @@ export const registerRoutes = async (
|
|||||||
folderDAL
|
folderDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectRoleService = projectRoleServiceFactory({ permissionService, projectRoleDAL });
|
const projectRoleService = projectRoleServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
projectRoleDAL,
|
||||||
|
projectUserMembershipRoleDAL,
|
||||||
|
identityProjectMembershipRoleDAL
|
||||||
|
});
|
||||||
|
|
||||||
const snapshotService = secretSnapshotServiceFactory({
|
const snapshotService = secretSnapshotServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
@@ -440,14 +481,6 @@ export const registerRoutes = async (
|
|||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
snapshotService
|
snapshotService
|
||||||
});
|
});
|
||||||
const secretImportService = secretImportServiceFactory({
|
|
||||||
projectEnvDAL,
|
|
||||||
folderDAL,
|
|
||||||
permissionService,
|
|
||||||
secretImportDAL,
|
|
||||||
projectDAL,
|
|
||||||
secretDAL
|
|
||||||
});
|
|
||||||
const integrationAuthService = integrationAuthServiceFactory({
|
const integrationAuthService = integrationAuthServiceFactory({
|
||||||
integrationAuthDAL,
|
integrationAuthDAL,
|
||||||
integrationDAL,
|
integrationDAL,
|
||||||
@@ -475,6 +508,15 @@ export const registerRoutes = async (
|
|||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL
|
secretVersionTagDAL
|
||||||
});
|
});
|
||||||
|
const secretImportService = secretImportServiceFactory({
|
||||||
|
projectEnvDAL,
|
||||||
|
folderDAL,
|
||||||
|
permissionService,
|
||||||
|
secretImportDAL,
|
||||||
|
projectDAL,
|
||||||
|
secretDAL,
|
||||||
|
secretQueueService
|
||||||
|
});
|
||||||
const secretBlindIndexService = secretBlindIndexServiceFactory({
|
const secretBlindIndexService = secretBlindIndexServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
@@ -620,6 +662,8 @@ export const registerRoutes = async (
|
|||||||
password: passwordService,
|
password: passwordService,
|
||||||
signup: signupService,
|
signup: signupService,
|
||||||
user: userService,
|
user: userService,
|
||||||
|
group: groupService,
|
||||||
|
groupProject: groupProjectService,
|
||||||
permission: permissionService,
|
permission: permissionService,
|
||||||
org: orgService,
|
org: orgService,
|
||||||
orgRole: orgRoleService,
|
orgRole: orgRoleService,
|
||||||
|
@@ -1,6 +1,14 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { IncidentContactsSchema, OrganizationsSchema, OrgMembershipsSchema, UsersSchema } from "@app/db/schemas";
|
import {
|
||||||
|
GroupsSchema,
|
||||||
|
IncidentContactsSchema,
|
||||||
|
OrganizationsSchema,
|
||||||
|
OrgMembershipsSchema,
|
||||||
|
OrgRolesSchema,
|
||||||
|
UsersSchema
|
||||||
|
} from "@app/db/schemas";
|
||||||
|
import { ORGANIZATIONS } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
@@ -218,4 +226,41 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
return { incidentContactsOrg };
|
return { incidentContactsOrg };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:organizationId/groups",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
organizationId: z.string().trim().describe(ORGANIZATIONS.LIST_GROUPS.organizationId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
groups: GroupsSchema.merge(
|
||||||
|
z.object({
|
||||||
|
customRole: OrgRolesSchema.pick({
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
slug: true,
|
||||||
|
permissions: true,
|
||||||
|
description: true
|
||||||
|
}).optional()
|
||||||
|
})
|
||||||
|
).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const groups = await server.services.org.getOrgGroups({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
orgId: req.params.organizationId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { groups };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@@ -18,8 +18,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: "Create environment",
|
description: "Create environment",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
@@ -77,8 +76,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: "Update environment",
|
description: "Update environment",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
@@ -144,8 +142,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: "Delete environment",
|
description: "Delete environment",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
|
@@ -26,8 +26,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
|
|||||||
description: "Return project user memberships",
|
description: "Return project user memberships",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
@@ -35,31 +34,28 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
memberships: ProjectMembershipsSchema.omit({ role: true })
|
memberships: ProjectMembershipsSchema.extend({
|
||||||
.merge(
|
user: UsersSchema.pick({
|
||||||
|
email: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
id: true
|
||||||
|
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
|
||||||
|
roles: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
user: UsersSchema.pick({
|
id: z.string(),
|
||||||
email: true,
|
role: z.string(),
|
||||||
firstName: true,
|
customRoleId: z.string().optional().nullable(),
|
||||||
lastName: true,
|
customRoleName: z.string().optional().nullable(),
|
||||||
id: true
|
customRoleSlug: z.string().optional().nullable(),
|
||||||
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
|
isTemporary: z.boolean(),
|
||||||
roles: z.array(
|
temporaryMode: z.string().optional().nullable(),
|
||||||
z.object({
|
temporaryRange: z.string().nullable().optional(),
|
||||||
id: z.string(),
|
temporaryAccessStartTime: z.date().nullable().optional(),
|
||||||
role: z.string(),
|
temporaryAccessEndTime: z.date().nullable().optional()
|
||||||
customRoleId: z.string().optional().nullable(),
|
|
||||||
customRoleName: z.string().optional().nullable(),
|
|
||||||
customRoleSlug: z.string().optional().nullable(),
|
|
||||||
isTemporary: z.boolean(),
|
|
||||||
temporaryMode: z.string().optional().nullable(),
|
|
||||||
temporaryRange: z.string().nullable().optional(),
|
|
||||||
temporaryAccessStartTime: z.date().nullable().optional(),
|
|
||||||
temporaryAccessEndTime: z.date().nullable().optional()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
})
|
||||||
.omit({ createdAt: true, updatedAt: true })
|
.omit({ createdAt: true, updatedAt: true })
|
||||||
.array()
|
.array()
|
||||||
})
|
})
|
||||||
@@ -142,8 +138,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
|
|||||||
description: "Update project user membership",
|
description: "Update project user membership",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
@@ -216,8 +211,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
|
|||||||
description: "Delete project user membership",
|
description: "Delete project user membership",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
|
@@ -7,7 +7,7 @@ import {
|
|||||||
UserEncryptionKeysSchema,
|
UserEncryptionKeysSchema,
|
||||||
UsersSchema
|
UsersSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { INTEGRATION_AUTH, PROJECTS } from "@app/lib/api-docs";
|
import { PROJECTS } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
@@ -70,32 +70,29 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
users: ProjectMembershipsSchema.omit({ role: true })
|
users: ProjectMembershipsSchema.extend({
|
||||||
.merge(
|
user: UsersSchema.pick({
|
||||||
|
email: true,
|
||||||
|
username: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
id: true
|
||||||
|
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
|
||||||
|
roles: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
user: UsersSchema.pick({
|
id: z.string(),
|
||||||
username: true,
|
role: z.string(),
|
||||||
email: true,
|
customRoleId: z.string().optional().nullable(),
|
||||||
firstName: true,
|
customRoleName: z.string().optional().nullable(),
|
||||||
lastName: true,
|
customRoleSlug: z.string().optional().nullable(),
|
||||||
id: true
|
isTemporary: z.boolean(),
|
||||||
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
|
temporaryMode: z.string().optional().nullable(),
|
||||||
roles: z.array(
|
temporaryRange: z.string().nullable().optional(),
|
||||||
z.object({
|
temporaryAccessStartTime: z.date().nullable().optional(),
|
||||||
id: z.string(),
|
temporaryAccessEndTime: z.date().nullable().optional()
|
||||||
role: z.string(),
|
|
||||||
customRoleId: z.string().optional().nullable(),
|
|
||||||
customRoleName: z.string().optional().nullable(),
|
|
||||||
customRoleSlug: z.string().optional().nullable(),
|
|
||||||
isTemporary: z.boolean(),
|
|
||||||
temporaryMode: z.string().optional().nullable(),
|
|
||||||
temporaryRange: z.string().nullable().optional(),
|
|
||||||
temporaryAccessStartTime: z.date().nullable().optional(),
|
|
||||||
temporaryAccessEndTime: z.date().nullable().optional()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
})
|
||||||
.omit({ createdAt: true, updatedAt: true })
|
.omit({ createdAt: true, updatedAt: true })
|
||||||
.array()
|
.array()
|
||||||
})
|
})
|
||||||
@@ -326,8 +323,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "List integrations for a project.",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
workspaceId: z.string().trim().describe(PROJECTS.LIST_INTEGRATION.workspaceId)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -343,7 +346,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const integrations = await server.services.integration.listIntegrationByProject({
|
const integrations = await server.services.integration.listIntegrationByProject({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
@@ -370,7 +373,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim().describe(INTEGRATION_AUTH.LIST_AUTHORIZATION.workspaceId)
|
workspaceId: z.string().trim().describe(PROJECTS.LIST_INTEGRATION_AUTHORIZATION.workspaceId)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
@@ -19,8 +19,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
description: "Create folders",
|
description: "Create folders",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
body: z.object({
|
body: z.object({
|
||||||
@@ -76,8 +75,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
description: "Update folder",
|
description: "Update folder",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
@@ -140,8 +138,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
description: "Delete a folder",
|
description: "Delete a folder",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
@@ -200,8 +197,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
description: "Get folders",
|
description: "Get folders",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
|
@@ -19,8 +19,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
|
|||||||
description: "Create secret imports",
|
description: "Create secret imports",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
body: z.object({
|
body: z.object({
|
||||||
@@ -84,8 +83,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
|
|||||||
description: "Update secret imports",
|
description: "Update secret imports",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
@@ -159,8 +157,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
|
|||||||
description: "Delete secret imports",
|
description: "Delete secret imports",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
@@ -223,8 +220,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
|
|||||||
description: "Get secret imports",
|
description: "Get secret imports",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
|
201
backend/src/server/routes/v2/group-project-router.ts
Normal file
201
backend/src/server/routes/v2/group-project-router.ts
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import ms from "ms";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
GroupProjectMembershipsSchema,
|
||||||
|
GroupsSchema,
|
||||||
|
ProjectMembershipRole,
|
||||||
|
ProjectUserMembershipRolesSchema
|
||||||
|
} from "@app/db/schemas";
|
||||||
|
import { PROJECTS } from "@app/lib/api-docs";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
|
||||||
|
|
||||||
|
export const registerGroupProjectRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:projectSlug/groups/:groupSlug",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Add group to project",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
projectSlug: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.projectSlug),
|
||||||
|
groupSlug: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.groupSlug)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
role: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.default(ProjectMembershipRole.NoAccess)
|
||||||
|
.describe(PROJECTS.ADD_GROUP_TO_PROJECT.role)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
groupMembership: GroupProjectMembershipsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const groupMembership = await server.services.groupProject.addGroupToProject({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
groupSlug: req.params.groupSlug,
|
||||||
|
projectSlug: req.params.projectSlug,
|
||||||
|
role: req.body.role
|
||||||
|
});
|
||||||
|
return { groupMembership };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:projectSlug/groups/:groupSlug",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Update group in project",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
projectSlug: z.string().trim().describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.projectSlug),
|
||||||
|
groupSlug: z.string().trim().describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.groupSlug)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
roles: z
|
||||||
|
.array(
|
||||||
|
z.union([
|
||||||
|
z.object({
|
||||||
|
role: z.string(),
|
||||||
|
isTemporary: z.literal(false).default(false)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
role: z.string(),
|
||||||
|
isTemporary: z.literal(true),
|
||||||
|
temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode),
|
||||||
|
temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"),
|
||||||
|
temporaryAccessStartTime: z.string().datetime()
|
||||||
|
})
|
||||||
|
])
|
||||||
|
)
|
||||||
|
.min(1)
|
||||||
|
.describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.roles)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
roles: ProjectUserMembershipRolesSchema.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const roles = await server.services.groupProject.updateGroupInProject({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
groupSlug: req.params.groupSlug,
|
||||||
|
projectSlug: req.params.projectSlug,
|
||||||
|
roles: req.body.roles
|
||||||
|
});
|
||||||
|
return { roles };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:projectSlug/groups/:groupSlug",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Remove group from project",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
projectSlug: z.string().trim().describe(PROJECTS.REMOVE_GROUP_FROM_PROJECT.projectSlug),
|
||||||
|
groupSlug: z.string().trim().describe(PROJECTS.REMOVE_GROUP_FROM_PROJECT.groupSlug)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
groupMembership: GroupProjectMembershipsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const groupMembership = await server.services.groupProject.removeGroupFromProject({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
groupSlug: req.params.groupSlug,
|
||||||
|
projectSlug: req.params.projectSlug
|
||||||
|
});
|
||||||
|
return { groupMembership };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:projectSlug/groups",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Return list of groups in project",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
projectSlug: z.string().trim().describe(PROJECTS.LIST_GROUPS_IN_PROJECT.projectSlug)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
groupMemberships: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
groupId: z.string(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
roles: z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
role: z.string(),
|
||||||
|
customRoleId: z.string().optional().nullable(),
|
||||||
|
customRoleName: z.string().optional().nullable(),
|
||||||
|
customRoleSlug: z.string().optional().nullable(),
|
||||||
|
isTemporary: z.boolean(),
|
||||||
|
temporaryMode: z.string().optional().nullable(),
|
||||||
|
temporaryRange: z.string().nullable().optional(),
|
||||||
|
temporaryAccessStartTime: z.date().nullable().optional(),
|
||||||
|
temporaryAccessEndTime: z.date().nullable().optional()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
group: GroupsSchema.pick({ name: true, id: true, slug: true })
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const groupMemberships = await server.services.groupProject.listGroupsInProject({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectSlug: req.params.projectSlug
|
||||||
|
});
|
||||||
|
return { groupMemberships };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -18,8 +18,7 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: "Return organization identity memberships",
|
description: "Return organization identity memberships",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
@@ -50,6 +49,7 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
orgId: req.params.orgId
|
orgId: req.params.orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
return { identityMemberships };
|
return { identityMemberships };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { registerGroupProjectRouter } from "./group-project-router";
|
||||||
import { registerIdentityOrgRouter } from "./identity-org-router";
|
import { registerIdentityOrgRouter } from "./identity-org-router";
|
||||||
import { registerIdentityProjectRouter } from "./identity-project-router";
|
import { registerIdentityProjectRouter } from "./identity-project-router";
|
||||||
import { registerMfaRouter } from "./mfa-router";
|
import { registerMfaRouter } from "./mfa-router";
|
||||||
@@ -22,6 +23,7 @@ export const registerV2Routes = async (server: FastifyZodProvider) => {
|
|||||||
async (projectServer) => {
|
async (projectServer) => {
|
||||||
await projectServer.register(registerProjectRouter);
|
await projectServer.register(registerProjectRouter);
|
||||||
await projectServer.register(registerIdentityProjectRouter);
|
await projectServer.register(registerIdentityProjectRouter);
|
||||||
|
await projectServer.register(registerGroupProjectRouter);
|
||||||
await projectServer.register(registerProjectMembershipRouter);
|
await projectServer.register(registerProjectMembershipRouter);
|
||||||
},
|
},
|
||||||
{ prefix: "/workspace" }
|
{ prefix: "/workspace" }
|
||||||
|
@@ -17,8 +17,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: "Return organization user memberships",
|
description: "Return organization user memberships",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
@@ -66,8 +65,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: "Return projects in organization that user is part of",
|
description: "Return projects in organization that user is part of",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
@@ -115,8 +113,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: "Update organization user memberships",
|
description: "Update organization user memberships",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
@@ -158,8 +155,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: "Delete organization user memberships",
|
description: "Delete organization user memberships",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
|
@@ -36,11 +36,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
description: "Return encrypted project key",
|
description: "Return encrypted project key",
|
||||||
security: [
|
|
||||||
{
|
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim().describe(PROJECTS.GET_KEY.workspaceId)
|
workspaceId: z.string().trim().describe(PROJECTS.GET_KEY.workspaceId)
|
||||||
}),
|
}),
|
||||||
|
@@ -85,11 +85,6 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
description: "Return organizations that current user is part of",
|
description: "Return organizations that current user is part of",
|
||||||
security: [
|
|
||||||
{
|
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
organizations: OrganizationsSchema.array()
|
organizations: OrganizationsSchema.array()
|
||||||
@@ -217,11 +212,6 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
description: "Retrieve the current user on the request",
|
description: "Retrieve the current user on the request",
|
||||||
security: [
|
|
||||||
{
|
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
user: UsersSchema.merge(UserEncryptionKeysSchema.omit({ verifier: true }))
|
user: UsersSchema.merge(UserEncryptionKeysSchema.omit({ verifier: true }))
|
||||||
|
@@ -158,8 +158,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: "List secrets",
|
description: "List secrets",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
@@ -280,8 +279,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: "Get a secret by name",
|
description: "Get a secret by name",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
@@ -375,8 +373,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: "Create secret",
|
description: "Create secret",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
@@ -464,8 +461,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: "Update secret",
|
description: "Update secret",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
@@ -550,8 +546,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: "Delete secret",
|
description: "Delete secret",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: [],
|
bearerAuth: []
|
||||||
apiKeyAuth: []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
|
@@ -153,7 +153,7 @@ export const authLoginServiceFactory = ({
|
|||||||
username: email
|
username: email
|
||||||
});
|
});
|
||||||
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
|
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
|
||||||
throw new Error("Failed to find user");
|
throw new Error("Failed to find user");
|
||||||
}
|
}
|
||||||
if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) {
|
if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) {
|
||||||
validateProviderAuthToken(providerAuthToken as string, email);
|
validateProviderAuthToken(providerAuthToken as string, email);
|
||||||
|
@@ -192,7 +192,7 @@ export const authPaswordServiceFactory = ({
|
|||||||
}: TCreateBackupPrivateKeyDTO) => {
|
}: TCreateBackupPrivateKeyDTO) => {
|
||||||
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
|
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
|
||||||
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
|
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
|
||||||
throw new Error("Failed to find user");
|
throw new Error("Failed to find user");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userEnc.clientPublicKey || !userEnc.serverPrivateKey) throw new Error("failed to create backup key");
|
if (!userEnc.clientPublicKey || !userEnc.serverPrivateKey) throw new Error("failed to create backup key");
|
||||||
@@ -239,7 +239,7 @@ export const authPaswordServiceFactory = ({
|
|||||||
const getBackupPrivateKeyOfUser = async (userId: string) => {
|
const getBackupPrivateKeyOfUser = async (userId: string) => {
|
||||||
const user = await userDAL.findUserEncKeyByUserId(userId);
|
const user = await userDAL.findUserEncKeyByUserId(userId);
|
||||||
if (!user || (user && !user.isAccepted)) {
|
if (!user || (user && !user.isAccepted)) {
|
||||||
throw new Error("Failed to find user");
|
throw new Error("Failed to find user");
|
||||||
}
|
}
|
||||||
const backupKey = await authDAL.getBackupPrivateKeyByUserId(userId);
|
const backupKey = await authDAL.getBackupPrivateKeyByUserId(userId);
|
||||||
if (!backupKey) throw new Error("Failed to find user backup key");
|
if (!backupKey) throw new Error("Failed to find user backup key");
|
||||||
|
99
backend/src/services/group-project/group-project-dal.ts
Normal file
99
backend/src/services/group-project/group-project-dal.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify, sqlNestRelationships } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TGroupProjectDALFactory = ReturnType<typeof groupProjectDALFactory>;
|
||||||
|
|
||||||
|
export const groupProjectDALFactory = (db: TDbClient) => {
|
||||||
|
const groupProjectOrm = ormify(db, TableName.GroupProjectMembership);
|
||||||
|
|
||||||
|
const findByProjectId = async (projectId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const docs = await (tx || db)(TableName.GroupProjectMembership)
|
||||||
|
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
|
||||||
|
.join(TableName.Groups, `${TableName.GroupProjectMembership}.groupId`, `${TableName.Groups}.id`)
|
||||||
|
.join(
|
||||||
|
TableName.GroupProjectMembershipRole,
|
||||||
|
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
|
||||||
|
`${TableName.GroupProjectMembership}.id`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ProjectRoles,
|
||||||
|
`${TableName.GroupProjectMembershipRole}.customRoleId`,
|
||||||
|
`${TableName.ProjectRoles}.id`
|
||||||
|
)
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.GroupProjectMembership),
|
||||||
|
db.ref("createdAt").withSchema(TableName.GroupProjectMembership),
|
||||||
|
db.ref("updatedAt").withSchema(TableName.GroupProjectMembership),
|
||||||
|
db.ref("id").as("groupId").withSchema(TableName.Groups),
|
||||||
|
db.ref("name").as("groupName").withSchema(TableName.Groups),
|
||||||
|
db.ref("slug").as("groupSlug").withSchema(TableName.Groups),
|
||||||
|
db.ref("id").withSchema(TableName.GroupProjectMembership),
|
||||||
|
db.ref("role").withSchema(TableName.GroupProjectMembershipRole),
|
||||||
|
db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("membershipRoleId"),
|
||||||
|
db.ref("customRoleId").withSchema(TableName.GroupProjectMembershipRole),
|
||||||
|
db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"),
|
||||||
|
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
||||||
|
db.ref("temporaryMode").withSchema(TableName.GroupProjectMembershipRole),
|
||||||
|
db.ref("isTemporary").withSchema(TableName.GroupProjectMembershipRole),
|
||||||
|
db.ref("temporaryRange").withSchema(TableName.GroupProjectMembershipRole),
|
||||||
|
db.ref("temporaryAccessStartTime").withSchema(TableName.GroupProjectMembershipRole),
|
||||||
|
db.ref("temporaryAccessEndTime").withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
);
|
||||||
|
|
||||||
|
const members = sqlNestRelationships({
|
||||||
|
data: docs,
|
||||||
|
parentMapper: ({ groupId, groupName, groupSlug, id, createdAt, updatedAt }) => ({
|
||||||
|
id,
|
||||||
|
groupId,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
group: {
|
||||||
|
id: groupId,
|
||||||
|
name: groupName,
|
||||||
|
slug: groupSlug
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
key: "id",
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
label: "roles" as const,
|
||||||
|
key: "membershipRoleId",
|
||||||
|
mapper: ({
|
||||||
|
role,
|
||||||
|
customRoleId,
|
||||||
|
customRoleName,
|
||||||
|
customRoleSlug,
|
||||||
|
membershipRoleId,
|
||||||
|
temporaryRange,
|
||||||
|
temporaryMode,
|
||||||
|
temporaryAccessEndTime,
|
||||||
|
temporaryAccessStartTime,
|
||||||
|
isTemporary
|
||||||
|
}) => ({
|
||||||
|
id: membershipRoleId,
|
||||||
|
role,
|
||||||
|
customRoleId,
|
||||||
|
customRoleName,
|
||||||
|
customRoleSlug,
|
||||||
|
temporaryRange,
|
||||||
|
temporaryMode,
|
||||||
|
temporaryAccessEndTime,
|
||||||
|
temporaryAccessStartTime,
|
||||||
|
isTemporary
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
return members;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindByProjectId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...groupProjectOrm, findByProjectId };
|
||||||
|
};
|
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TGroupProjectMembershipRoleDALFactory = ReturnType<typeof groupProjectMembershipRoleDALFactory>;
|
||||||
|
|
||||||
|
export const groupProjectMembershipRoleDALFactory = (db: TDbClient) => {
|
||||||
|
const orm = ormify(db, TableName.GroupProjectMembershipRole);
|
||||||
|
return orm;
|
||||||
|
};
|
338
backend/src/services/group-project/group-project-service.ts
Normal file
338
backend/src/services/group-project/group-project-service.ts
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import ms from "ms";
|
||||||
|
|
||||||
|
import { ProjectMembershipRole, SecretKeyEncoding } from "@app/db/schemas";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
|
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
||||||
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
|
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||||
|
import { groupBy } from "@app/lib/fn";
|
||||||
|
|
||||||
|
import { TGroupDALFactory } from "../../ee/services/group/group-dal";
|
||||||
|
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
|
||||||
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
|
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||||
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
|
import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
|
||||||
|
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||||
|
import { TGroupProjectDALFactory } from "./group-project-dal";
|
||||||
|
import { TGroupProjectMembershipRoleDALFactory } from "./group-project-membership-role-dal";
|
||||||
|
import {
|
||||||
|
TCreateProjectGroupDTO,
|
||||||
|
TDeleteProjectGroupDTO,
|
||||||
|
TListProjectGroupDTO,
|
||||||
|
TUpdateProjectGroupDTO
|
||||||
|
} from "./group-project-types";
|
||||||
|
|
||||||
|
type TGroupProjectServiceFactoryDep = {
|
||||||
|
groupProjectDAL: Pick<TGroupProjectDALFactory, "findOne" | "transaction" | "create" | "delete" | "findByProjectId">;
|
||||||
|
groupProjectMembershipRoleDAL: Pick<
|
||||||
|
TGroupProjectMembershipRoleDALFactory,
|
||||||
|
"create" | "transaction" | "insertMany" | "delete"
|
||||||
|
>;
|
||||||
|
userGroupMembershipDAL: TUserGroupMembershipDALFactory;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "findProjectGhostUser">;
|
||||||
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany" | "transaction">;
|
||||||
|
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
||||||
|
projectBotDAL: TProjectBotDALFactory;
|
||||||
|
groupDAL: Pick<TGroupDALFactory, "findOne">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissionByRole">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGroupProjectServiceFactory = ReturnType<typeof groupProjectServiceFactory>;
|
||||||
|
|
||||||
|
export const groupProjectServiceFactory = ({
|
||||||
|
groupDAL,
|
||||||
|
groupProjectDAL,
|
||||||
|
groupProjectMembershipRoleDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
projectRoleDAL,
|
||||||
|
permissionService
|
||||||
|
}: TGroupProjectServiceFactoryDep) => {
|
||||||
|
const addGroupToProject = async ({
|
||||||
|
groupSlug,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectSlug,
|
||||||
|
role
|
||||||
|
}: TCreateProjectGroupDTO) => {
|
||||||
|
const project = await projectDAL.findOne({
|
||||||
|
slug: projectSlug
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!project) throw new BadRequestError({ message: `Failed to find project with slug ${projectSlug}` });
|
||||||
|
if (project.version < 2) throw new BadRequestError({ message: `Failed to add group to E2EE project` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
project.id,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Groups);
|
||||||
|
|
||||||
|
const group = await groupDAL.findOne({ orgId: actorOrgId, slug: groupSlug });
|
||||||
|
if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
|
||||||
|
|
||||||
|
const existingGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
|
||||||
|
if (existingGroup)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Group with slug ${groupSlug} already exists in project with id ${project.id}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const { permission: rolePermission, role: customRole } = await permissionService.getProjectPermissionByRole(
|
||||||
|
role,
|
||||||
|
project.id
|
||||||
|
);
|
||||||
|
const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
|
if (!hasPrivilege)
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Failed to add group to project with more privileged role"
|
||||||
|
});
|
||||||
|
const isCustomRole = Boolean(customRole);
|
||||||
|
|
||||||
|
const projectGroup = await groupProjectDAL.transaction(async (tx) => {
|
||||||
|
const groupProjectMembership = await groupProjectDAL.create(
|
||||||
|
{
|
||||||
|
groupId: group.id,
|
||||||
|
projectId: project.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await groupProjectMembershipRoleDAL.create(
|
||||||
|
{
|
||||||
|
projectMembershipId: groupProjectMembership.id,
|
||||||
|
role: isCustomRole ? ProjectMembershipRole.Custom : role,
|
||||||
|
customRoleId: customRole?.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return groupProjectMembership;
|
||||||
|
});
|
||||||
|
|
||||||
|
// share project key with users in group that have not
|
||||||
|
// individually been added to the project and that are not part of
|
||||||
|
// other groups that are in the project
|
||||||
|
const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, project.id);
|
||||||
|
|
||||||
|
if (groupMembers.length) {
|
||||||
|
const ghostUser = await projectDAL.findProjectGhostUser(project.id);
|
||||||
|
|
||||||
|
if (!ghostUser) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find sudo user"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, project.id);
|
||||||
|
|
||||||
|
if (!ghostUserLatestKey) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find sudo user latest key"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const bot = await projectBotDAL.findOne({ projectId: project.id });
|
||||||
|
|
||||||
|
if (!bot) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find bot"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const botPrivateKey = infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
|
||||||
|
iv: bot.iv,
|
||||||
|
tag: bot.tag,
|
||||||
|
ciphertext: bot.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const plaintextProjectKey = decryptAsymmetric({
|
||||||
|
ciphertext: ghostUserLatestKey.encryptedKey,
|
||||||
|
nonce: ghostUserLatestKey.nonce,
|
||||||
|
publicKey: ghostUserLatestKey.sender.publicKey,
|
||||||
|
privateKey: botPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectKeyData = groupMembers.map(({ user: { publicKey, id } }) => {
|
||||||
|
const { ciphertext: encryptedKey, nonce } = encryptAsymmetric(plaintextProjectKey, publicKey, botPrivateKey);
|
||||||
|
|
||||||
|
return {
|
||||||
|
encryptedKey,
|
||||||
|
nonce,
|
||||||
|
senderId: ghostUser.id,
|
||||||
|
receiverId: id,
|
||||||
|
projectId: project.id
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await projectKeyDAL.insertMany(projectKeyData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectGroup;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateGroupInProject = async ({
|
||||||
|
projectSlug,
|
||||||
|
groupSlug,
|
||||||
|
roles,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TUpdateProjectGroupDTO) => {
|
||||||
|
const project = await projectDAL.findOne({
|
||||||
|
slug: projectSlug
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!project) throw new BadRequestError({ message: `Failed to find project with slug ${projectSlug}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
project.id,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Groups);
|
||||||
|
|
||||||
|
const group = await groupDAL.findOne({ orgId: actorOrgId, slug: groupSlug });
|
||||||
|
if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
|
||||||
|
|
||||||
|
const projectGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
|
||||||
|
if (!projectGroup) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
|
||||||
|
|
||||||
|
// validate custom roles input
|
||||||
|
const customInputRoles = roles.filter(
|
||||||
|
({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
|
||||||
|
);
|
||||||
|
const hasCustomRole = Boolean(customInputRoles.length);
|
||||||
|
const customRoles = hasCustomRole
|
||||||
|
? await projectRoleDAL.find({
|
||||||
|
projectId: project.id,
|
||||||
|
$in: { slug: customInputRoles.map(({ role }) => role) }
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
if (customRoles.length !== customInputRoles.length) throw new BadRequestError({ message: "Custom role not found" });
|
||||||
|
|
||||||
|
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
|
||||||
|
|
||||||
|
const sanitizedProjectMembershipRoles = roles.map((inputRole) => {
|
||||||
|
const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
|
||||||
|
if (!inputRole.isTemporary) {
|
||||||
|
return {
|
||||||
|
projectMembershipId: projectGroup.id,
|
||||||
|
role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role,
|
||||||
|
customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// check cron or relative here later for now its just relative
|
||||||
|
const relativeTimeInMs = ms(inputRole.temporaryRange);
|
||||||
|
return {
|
||||||
|
projectMembershipId: projectGroup.id,
|
||||||
|
role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role,
|
||||||
|
customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null,
|
||||||
|
isTemporary: true,
|
||||||
|
temporaryMode: ProjectUserMembershipTemporaryMode.Relative,
|
||||||
|
temporaryRange: inputRole.temporaryRange,
|
||||||
|
temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime),
|
||||||
|
temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedRoles = await groupProjectMembershipRoleDAL.transaction(async (tx) => {
|
||||||
|
await groupProjectMembershipRoleDAL.delete({ projectMembershipId: projectGroup.id }, tx);
|
||||||
|
return groupProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedRoles;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeGroupFromProject = async ({
|
||||||
|
projectSlug,
|
||||||
|
groupSlug,
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod
|
||||||
|
}: TDeleteProjectGroupDTO) => {
|
||||||
|
const project = await projectDAL.findOne({
|
||||||
|
slug: projectSlug
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!project) throw new BadRequestError({ message: `Failed to find project with slug ${projectSlug}` });
|
||||||
|
|
||||||
|
const group = await groupDAL.findOne({ orgId: actorOrgId, slug: groupSlug });
|
||||||
|
if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
|
||||||
|
|
||||||
|
const groupProjectMembership = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
|
||||||
|
if (!groupProjectMembership) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
project.id,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Groups);
|
||||||
|
|
||||||
|
const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, project.id);
|
||||||
|
|
||||||
|
if (groupMembers.length) {
|
||||||
|
await projectKeyDAL.delete({
|
||||||
|
projectId: project.id,
|
||||||
|
$in: {
|
||||||
|
receiverId: groupMembers.map(({ user: { id } }) => id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const [deletedGroup] = await groupProjectDAL.delete({ groupId: group.id, projectId: project.id });
|
||||||
|
|
||||||
|
return deletedGroup;
|
||||||
|
};
|
||||||
|
|
||||||
|
const listGroupsInProject = async ({
|
||||||
|
projectSlug,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TListProjectGroupDTO) => {
|
||||||
|
const project = await projectDAL.findOne({
|
||||||
|
slug: projectSlug
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!project) throw new BadRequestError({ message: `Failed to find project with slug ${projectSlug}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
project.id,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
|
||||||
|
|
||||||
|
const groupMemberships = await groupProjectDAL.findByProjectId(project.id);
|
||||||
|
return groupMemberships;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
addGroupToProject,
|
||||||
|
updateGroupInProject,
|
||||||
|
removeGroupFromProject,
|
||||||
|
listGroupsInProject
|
||||||
|
};
|
||||||
|
};
|
31
backend/src/services/group-project/group-project-types.ts
Normal file
31
backend/src/services/group-project/group-project-types.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { TProjectSlugPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
|
||||||
|
|
||||||
|
export type TCreateProjectGroupDTO = {
|
||||||
|
groupSlug: string;
|
||||||
|
role: string;
|
||||||
|
} & TProjectSlugPermission;
|
||||||
|
|
||||||
|
export type TUpdateProjectGroupDTO = {
|
||||||
|
roles: (
|
||||||
|
| {
|
||||||
|
role: string;
|
||||||
|
isTemporary?: false;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
role: string;
|
||||||
|
isTemporary: true;
|
||||||
|
temporaryMode: ProjectUserMembershipTemporaryMode.Relative;
|
||||||
|
temporaryRange: string;
|
||||||
|
temporaryAccessStartTime: string;
|
||||||
|
}
|
||||||
|
)[];
|
||||||
|
groupSlug: string;
|
||||||
|
} & TProjectSlugPermission;
|
||||||
|
|
||||||
|
export type TDeleteProjectGroupDTO = {
|
||||||
|
groupSlug: string;
|
||||||
|
} & TProjectSlugPermission;
|
||||||
|
|
||||||
|
export type TListProjectGroupDTO = TProjectSlugPermission;
|
@@ -93,9 +93,7 @@ export const identityProjectServiceFactory = ({
|
|||||||
const identityProjectMembership = await identityProjectDAL.create(
|
const identityProjectMembership = await identityProjectDAL.create(
|
||||||
{
|
{
|
||||||
identityId,
|
identityId,
|
||||||
projectId: project.id,
|
projectId: project.id
|
||||||
role: isCustomRole ? ProjectMembershipRole.Custom : role,
|
|
||||||
roleId: customRole?.id
|
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -163,7 +161,7 @@ export const identityProjectServiceFactory = ({
|
|||||||
|
|
||||||
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
|
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
|
||||||
|
|
||||||
const santiziedProjectMembershipRoles = roles.map((inputRole) => {
|
const sanitizedProjectMembershipRoles = roles.map((inputRole) => {
|
||||||
const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
|
const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
|
||||||
if (!inputRole.isTemporary) {
|
if (!inputRole.isTemporary) {
|
||||||
return {
|
return {
|
||||||
@@ -189,7 +187,7 @@ export const identityProjectServiceFactory = ({
|
|||||||
|
|
||||||
const updatedRoles = await identityProjectMembershipRoleDAL.transaction(async (tx) => {
|
const updatedRoles = await identityProjectMembershipRoleDAL.transaction(async (tx) => {
|
||||||
await identityProjectMembershipRoleDAL.delete({ projectMembershipId: projectIdentity.id }, tx);
|
await identityProjectMembershipRoleDAL.delete({ projectMembershipId: projectIdentity.id }, tx);
|
||||||
return identityProjectMembershipRoleDAL.insertMany(santiziedProjectMembershipRoles, tx);
|
return identityProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
|
||||||
});
|
});
|
||||||
|
|
||||||
return updatedRoles;
|
return updatedRoles;
|
||||||
@@ -246,8 +244,8 @@ export const identityProjectServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
|
||||||
|
|
||||||
const identityMemberhips = await identityProjectDAL.findByProjectId(projectId);
|
const identityMemberships = await identityProjectDAL.findByProjectId(projectId);
|
||||||
return identityMemberhips;
|
return identityMemberships;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@@ -157,8 +157,8 @@ export const identityServiceFactory = ({
|
|||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||||
|
|
||||||
const identityMemberhips = await identityOrgMembershipDAL.findByOrgId(orgId);
|
const identityMemberships = await identityOrgMembershipDAL.findByOrgId(orgId);
|
||||||
return identityMemberhips;
|
return identityMemberships;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@@ -146,7 +146,27 @@ export const integrationServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
|
||||||
|
|
||||||
const deletedIntegration = await integrationDAL.deleteById(id);
|
const deletedIntegration = await integrationDAL.transaction(async (tx) => {
|
||||||
|
// delete integration
|
||||||
|
const deletedIntegrationResult = await integrationDAL.deleteById(id, tx);
|
||||||
|
|
||||||
|
// check if there are other integrations that share the same integration auth
|
||||||
|
const integrations = await integrationDAL.find(
|
||||||
|
{
|
||||||
|
integrationAuthId: integration.integrationAuthId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (integrations.length === 0) {
|
||||||
|
// no other integration shares the same integration auth
|
||||||
|
// -> delete the integration auth
|
||||||
|
await integrationAuthDAL.deleteById(integration.integrationAuthId, tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return deletedIntegrationResult;
|
||||||
|
});
|
||||||
|
|
||||||
return { ...integration, ...deletedIntegration };
|
return { ...integration, ...deletedIntegration };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -6,6 +6,7 @@ import { Knex } from "knex";
|
|||||||
|
|
||||||
import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas";
|
import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas";
|
||||||
import { TProjects } from "@app/db/schemas/projects";
|
import { TProjects } from "@app/db/schemas/projects";
|
||||||
|
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
@@ -34,6 +35,7 @@ import {
|
|||||||
TDeleteOrgMembershipDTO,
|
TDeleteOrgMembershipDTO,
|
||||||
TFindAllWorkspacesDTO,
|
TFindAllWorkspacesDTO,
|
||||||
TFindOrgMembersByEmailDTO,
|
TFindOrgMembersByEmailDTO,
|
||||||
|
TGetOrgGroupsDTO,
|
||||||
TInviteUserToOrgDTO,
|
TInviteUserToOrgDTO,
|
||||||
TUpdateOrgDTO,
|
TUpdateOrgDTO,
|
||||||
TUpdateOrgMembershipDTO,
|
TUpdateOrgMembershipDTO,
|
||||||
@@ -45,6 +47,7 @@ type TOrgServiceFactoryDep = {
|
|||||||
orgBotDAL: TOrgBotDALFactory;
|
orgBotDAL: TOrgBotDALFactory;
|
||||||
orgRoleDAL: TOrgRoleDALFactory;
|
orgRoleDAL: TOrgRoleDALFactory;
|
||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
|
groupDAL: TGroupDALFactory;
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findProjectMembershipsByUserId" | "delete">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findProjectMembershipsByUserId" | "delete">;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
|
||||||
@@ -64,6 +67,7 @@ export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
|
|||||||
export const orgServiceFactory = ({
|
export const orgServiceFactory = ({
|
||||||
orgDAL,
|
orgDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
|
groupDAL,
|
||||||
orgRoleDAL,
|
orgRoleDAL,
|
||||||
incidentContactDAL,
|
incidentContactDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
@@ -113,6 +117,13 @@ export const orgServiceFactory = ({
|
|||||||
return members;
|
return members;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getOrgGroups = async ({ actor, actorId, orgId, actorAuthMethod, actorOrgId }: TGetOrgGroupsDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
||||||
|
const groups = await groupDAL.findByOrgId(orgId);
|
||||||
|
return groups;
|
||||||
|
};
|
||||||
|
|
||||||
const findOrgMembersByUsername = async ({
|
const findOrgMembersByUsername = async ({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@@ -674,6 +685,7 @@ export const orgServiceFactory = ({
|
|||||||
// incident contacts
|
// incident contacts
|
||||||
findIncidentContacts,
|
findIncidentContacts,
|
||||||
createIncidentContact,
|
createIncidentContact,
|
||||||
deleteIncidentContact
|
deleteIncidentContact,
|
||||||
|
getOrgGroups
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -53,3 +53,5 @@ export type TFindAllWorkspacesDTO = {
|
|||||||
export type TUpdateOrgDTO = {
|
export type TUpdateOrgDTO = {
|
||||||
data: Partial<{ name: string; slug: string; authEnforced: boolean; scimEnabled: boolean }>;
|
data: Partial<{ name: string; slug: string; authEnforced: boolean; scimEnabled: boolean }>;
|
||||||
} & TOrgPermission;
|
} & TOrgPermission;
|
||||||
|
|
||||||
|
export type TGetOrgGroupsDTO = TOrgPermission;
|
||||||
|
@@ -17,6 +17,7 @@ import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
|||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
|
|
||||||
|
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
@@ -45,6 +46,7 @@ type TProjectMembershipServiceFactoryDep = {
|
|||||||
projectMembershipDAL: TProjectMembershipDALFactory;
|
projectMembershipDAL: TProjectMembershipDALFactory;
|
||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "find" | "delete">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "find" | "delete">;
|
||||||
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">;
|
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">;
|
||||||
|
userGroupMembershipDAL: TUserGroupMembershipDALFactory;
|
||||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
||||||
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
|
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction">;
|
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction">;
|
||||||
@@ -63,6 +65,7 @@ export const projectMembershipServiceFactory = ({
|
|||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
licenseService
|
licenseService
|
||||||
@@ -120,6 +123,13 @@ export const projectMembershipServiceFactory = ({
|
|||||||
});
|
});
|
||||||
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
|
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
|
||||||
|
|
||||||
|
const userIdsToExcludeForProjectKeyAddition = new Set(
|
||||||
|
await userGroupMembershipDAL.findUserGroupMembershipsInProject(
|
||||||
|
orgMembers.map(({ username }) => username),
|
||||||
|
projectId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
await projectMembershipDAL.transaction(async (tx) => {
|
await projectMembershipDAL.transaction(async (tx) => {
|
||||||
const projectMemberships = await projectMembershipDAL.insertMany(
|
const projectMemberships = await projectMembershipDAL.insertMany(
|
||||||
orgMembers.map(({ userId }) => ({
|
orgMembers.map(({ userId }) => ({
|
||||||
@@ -135,13 +145,15 @@ export const projectMembershipServiceFactory = ({
|
|||||||
);
|
);
|
||||||
const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId);
|
const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId);
|
||||||
await projectKeyDAL.insertMany(
|
await projectKeyDAL.insertMany(
|
||||||
orgMembers.map(({ userId, id }) => ({
|
orgMembers
|
||||||
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
|
.filter(({ userId }) => !userIdsToExcludeForProjectKeyAddition.has(userId as string))
|
||||||
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
|
.map(({ userId, id }) => ({
|
||||||
senderId: actorId,
|
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
|
||||||
receiverId: userId as string,
|
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
|
||||||
projectId
|
senderId: actorId,
|
||||||
})),
|
receiverId: userId as string,
|
||||||
|
projectId
|
||||||
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -247,6 +259,10 @@ export const projectMembershipServiceFactory = ({
|
|||||||
|
|
||||||
const members: TProjectMemberships[] = [];
|
const members: TProjectMemberships[] = [];
|
||||||
|
|
||||||
|
const userIdsToExcludeForProjectKeyAddition = new Set(
|
||||||
|
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId)
|
||||||
|
);
|
||||||
|
|
||||||
await projectMembershipDAL.transaction(async (tx) => {
|
await projectMembershipDAL.transaction(async (tx) => {
|
||||||
const projectMemberships = await projectMembershipDAL.insertMany(
|
const projectMemberships = await projectMembershipDAL.insertMany(
|
||||||
orgMembers.map(({ user }) => ({
|
orgMembers.map(({ user }) => ({
|
||||||
@@ -265,13 +281,15 @@ export const projectMembershipServiceFactory = ({
|
|||||||
|
|
||||||
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
|
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
|
||||||
await projectKeyDAL.insertMany(
|
await projectKeyDAL.insertMany(
|
||||||
orgMembers.map(({ user, id }) => ({
|
orgMembers
|
||||||
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
|
.filter(({ user }) => !userIdsToExcludeForProjectKeyAddition.has(user.id))
|
||||||
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
|
.map(({ user, id }) => ({
|
||||||
senderId: ghostUser.id,
|
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
|
||||||
receiverId: user.id,
|
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
|
||||||
projectId
|
senderId: ghostUser.id,
|
||||||
})),
|
receiverId: user.id,
|
||||||
|
projectId
|
||||||
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -344,7 +362,7 @@ export const projectMembershipServiceFactory = ({
|
|||||||
if (customRoles.length !== customInputRoles.length) throw new BadRequestError({ message: "Custom role not found" });
|
if (customRoles.length !== customInputRoles.length) throw new BadRequestError({ message: "Custom role not found" });
|
||||||
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
|
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
|
||||||
|
|
||||||
const santiziedProjectMembershipRoles = roles.map((inputRole) => {
|
const sanitizedProjectMembershipRoles = roles.map((inputRole) => {
|
||||||
const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
|
const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
|
||||||
if (!inputRole.isTemporary) {
|
if (!inputRole.isTemporary) {
|
||||||
return {
|
return {
|
||||||
@@ -370,7 +388,7 @@ export const projectMembershipServiceFactory = ({
|
|||||||
|
|
||||||
const updatedRoles = await projectMembershipDAL.transaction(async (tx) => {
|
const updatedRoles = await projectMembershipDAL.transaction(async (tx) => {
|
||||||
await projectUserMembershipRoleDAL.delete({ projectMembershipId: membershipId }, tx);
|
await projectUserMembershipRoleDAL.delete({ projectMembershipId: membershipId }, tx);
|
||||||
return projectUserMembershipRoleDAL.insertMany(santiziedProjectMembershipRoles, tx);
|
return projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
|
||||||
});
|
});
|
||||||
|
|
||||||
return updatedRoles;
|
return updatedRoles;
|
||||||
@@ -458,6 +476,10 @@ export const projectMembershipServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userIdsToExcludeFromProjectKeyRemoval = new Set(
|
||||||
|
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId)
|
||||||
|
);
|
||||||
|
|
||||||
const memberships = await projectMembershipDAL.transaction(async (tx) => {
|
const memberships = await projectMembershipDAL.transaction(async (tx) => {
|
||||||
const deletedMemberships = await projectMembershipDAL.delete(
|
const deletedMemberships = await projectMembershipDAL.delete(
|
||||||
{
|
{
|
||||||
@@ -469,11 +491,15 @@ export const projectMembershipServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// delete project keys belonging to users that are not part of any other groups in the project
|
||||||
await projectKeyDAL.delete(
|
await projectKeyDAL.delete(
|
||||||
{
|
{
|
||||||
projectId,
|
projectId,
|
||||||
$in: {
|
$in: {
|
||||||
receiverId: projectMembers.map(({ user }) => user.id).filter(Boolean)
|
receiverId: projectMembers
|
||||||
|
.filter(({ user }) => !userIdsToExcludeFromProjectKeyRemoval.has(user.id))
|
||||||
|
.map(({ user }) => user.id)
|
||||||
|
.filter(Boolean)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
|
@@ -14,16 +14,25 @@ import {
|
|||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
|
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
||||||
|
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||||
import { TProjectRoleDALFactory } from "./project-role-dal";
|
import { TProjectRoleDALFactory } from "./project-role-dal";
|
||||||
|
|
||||||
type TProjectRoleServiceFactoryDep = {
|
type TProjectRoleServiceFactoryDep = {
|
||||||
projectRoleDAL: TProjectRoleDALFactory;
|
projectRoleDAL: TProjectRoleDALFactory;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
|
||||||
|
identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory;
|
||||||
|
projectUserMembershipRoleDAL: TProjectUserMembershipRoleDALFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TProjectRoleServiceFactory = ReturnType<typeof projectRoleServiceFactory>;
|
export type TProjectRoleServiceFactory = ReturnType<typeof projectRoleServiceFactory>;
|
||||||
|
|
||||||
export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }: TProjectRoleServiceFactoryDep) => {
|
export const projectRoleServiceFactory = ({
|
||||||
|
projectRoleDAL,
|
||||||
|
permissionService,
|
||||||
|
identityProjectMembershipRoleDAL,
|
||||||
|
projectUserMembershipRoleDAL
|
||||||
|
}: TProjectRoleServiceFactoryDep) => {
|
||||||
const createRole = async (
|
const createRole = async (
|
||||||
actor: ActorType,
|
actor: ActorType,
|
||||||
actorId: string,
|
actorId: string,
|
||||||
@@ -96,8 +105,25 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }:
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Role);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Role);
|
||||||
|
|
||||||
|
const identityRole = await identityProjectMembershipRoleDAL.findOne({ customRoleId: roleId });
|
||||||
|
const projectUserRole = await projectUserMembershipRoleDAL.findOne({ customRoleId: roleId });
|
||||||
|
|
||||||
|
if (identityRole) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The role is assigned to one or more identities. Make sure to unassign them before deleting the role.",
|
||||||
|
name: "Delete role"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (projectUserRole) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The role is assigned to one or more users. Make sure to unassign them before deleting the role.",
|
||||||
|
name: "Delete role"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const [deletedRole] = await projectRoleDAL.delete({ id: roleId, projectId });
|
const [deletedRole] = await projectRoleDAL.delete({ id: roleId, projectId });
|
||||||
if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
|
if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Delete role" });
|
||||||
|
|
||||||
return deletedRole;
|
return deletedRole;
|
||||||
};
|
};
|
||||||
|
@@ -30,8 +30,33 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
{ column: `${TableName.Environment}.position`, order: "asc" }
|
{ column: `${TableName.Environment}.position`, order: "asc" }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const groups: string[] = await db(TableName.UserGroupMembership)
|
||||||
|
.where({ userId })
|
||||||
|
.select(selectAllTableCols(TableName.UserGroupMembership))
|
||||||
|
.pluck("groupId");
|
||||||
|
|
||||||
|
const groupWorkspaces = await db(TableName.GroupProjectMembership)
|
||||||
|
.whereIn("groupId", groups)
|
||||||
|
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||||
|
.whereNotIn(
|
||||||
|
`${TableName.Project}.id`,
|
||||||
|
workspaces.map(({ id }) => id)
|
||||||
|
)
|
||||||
|
.leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
|
||||||
|
.select(
|
||||||
|
selectAllTableCols(TableName.Project),
|
||||||
|
db.ref("id").withSchema(TableName.Project).as("_id"),
|
||||||
|
db.ref("id").withSchema(TableName.Environment).as("envId"),
|
||||||
|
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||||
|
db.ref("name").withSchema(TableName.Environment).as("envName")
|
||||||
|
)
|
||||||
|
.orderBy([
|
||||||
|
{ column: `${TableName.Project}.name`, order: "asc" },
|
||||||
|
{ column: `${TableName.Environment}.position`, order: "asc" }
|
||||||
|
]);
|
||||||
|
|
||||||
const nestedWorkspaces = sqlNestRelationships({
|
const nestedWorkspaces = sqlNestRelationships({
|
||||||
data: workspaces,
|
data: workspaces.concat(groupWorkspaces),
|
||||||
key: "id",
|
key: "id",
|
||||||
parentMapper: ({ _id, ...el }) => ({ _id, ...ProjectsSchema.parse(el) }),
|
parentMapper: ({ _id, ...el }) => ({ _id, ...ProjectsSchema.parse(el) }),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
@@ -126,13 +151,11 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const findProjectById = async (id: string) => {
|
const findProjectById = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
const workspaces = await db(TableName.ProjectMembership)
|
const workspaces = await db(TableName.Project)
|
||||||
.where(`${TableName.Project}.id`, id)
|
.where(`${TableName.Project}.id`, id)
|
||||||
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
|
.leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
|
||||||
.join(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
|
|
||||||
.select(
|
.select(
|
||||||
selectAllTableCols(TableName.Project),
|
selectAllTableCols(TableName.Project),
|
||||||
db.ref("id").withSchema(TableName.Project).as("_id"),
|
|
||||||
db.ref("id").withSchema(TableName.Environment).as("envId"),
|
db.ref("id").withSchema(TableName.Environment).as("envId"),
|
||||||
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||||
db.ref("name").withSchema(TableName.Environment).as("envName")
|
db.ref("name").withSchema(TableName.Environment).as("envName")
|
||||||
@@ -141,10 +164,11 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
{ column: `${TableName.Project}.name`, order: "asc" },
|
{ column: `${TableName.Project}.name`, order: "asc" },
|
||||||
{ column: `${TableName.Environment}.position`, order: "asc" }
|
{ column: `${TableName.Environment}.position`, order: "asc" }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const project = sqlNestRelationships({
|
const project = sqlNestRelationships({
|
||||||
data: workspaces,
|
data: workspaces,
|
||||||
key: "id",
|
key: "id",
|
||||||
parentMapper: ({ _id, ...el }) => ({ _id, ...ProjectsSchema.parse(el) }),
|
parentMapper: ({ ...el }) => ({ _id: el.id, ...ProjectsSchema.parse(el) }),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
key: "envId",
|
key: "envId",
|
||||||
@@ -174,14 +198,12 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
throw new BadRequestError({ message: "Organization ID is required when querying with slugs" });
|
throw new BadRequestError({ message: "Organization ID is required when querying with slugs" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const projects = await db(TableName.ProjectMembership)
|
const projects = await db(TableName.Project)
|
||||||
.where(`${TableName.Project}.slug`, slug)
|
.where(`${TableName.Project}.slug`, slug)
|
||||||
.where(`${TableName.Project}.orgId`, orgId)
|
.where(`${TableName.Project}.orgId`, orgId)
|
||||||
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
|
.leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
|
||||||
.join(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
|
|
||||||
.select(
|
.select(
|
||||||
selectAllTableCols(TableName.Project),
|
selectAllTableCols(TableName.Project),
|
||||||
db.ref("id").withSchema(TableName.Project).as("_id"),
|
|
||||||
db.ref("id").withSchema(TableName.Environment).as("envId"),
|
db.ref("id").withSchema(TableName.Environment).as("envId"),
|
||||||
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||||
db.ref("name").withSchema(TableName.Environment).as("envName")
|
db.ref("name").withSchema(TableName.Environment).as("envName")
|
||||||
@@ -194,7 +216,7 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
const project = sqlNestRelationships({
|
const project = sqlNestRelationships({
|
||||||
data: projects,
|
data: projects,
|
||||||
key: "id",
|
key: "id",
|
||||||
parentMapper: ({ _id, ...el }) => ({ _id, ...ProjectsSchema.parse(el) }),
|
parentMapper: ({ ...el }) => ({ _id: el.id, ...ProjectsSchema.parse(el) }),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
key: "envId",
|
key: "envId",
|
||||||
|
@@ -232,8 +232,7 @@ export const projectQueueFactory = ({
|
|||||||
const projectMembership = await projectMembershipDAL.create(
|
const projectMembership = await projectMembershipDAL.create(
|
||||||
{
|
{
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
userId: ghostUser.user.id,
|
userId: ghostUser.user.id
|
||||||
role: ProjectMembershipRole.Admin
|
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
@@ -141,8 +141,7 @@ export const projectServiceFactory = ({
|
|||||||
const projectMembership = await projectMembershipDAL.create(
|
const projectMembership = await projectMembershipDAL.create(
|
||||||
{
|
{
|
||||||
userId: ghostUser.user.id,
|
userId: ghostUser.user.id,
|
||||||
projectId: project.id,
|
projectId: project.id
|
||||||
role: ProjectMembershipRole.Admin
|
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -244,8 +243,7 @@ export const projectServiceFactory = ({
|
|||||||
const userProjectMembership = await projectMembershipDAL.create(
|
const userProjectMembership = await projectMembershipDAL.create(
|
||||||
{
|
{
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
userId: user.id,
|
userId: user.id
|
||||||
role: projectAdmin.projectRole
|
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -302,9 +300,7 @@ export const projectServiceFactory = ({
|
|||||||
const identityProjectMembership = await identityProjectDAL.create(
|
const identityProjectMembership = await identityProjectDAL.create(
|
||||||
{
|
{
|
||||||
identityId: actorId,
|
identityId: actorId,
|
||||||
projectId: project.id,
|
projectId: project.id
|
||||||
role: isCustomRole ? ProjectMembershipRole.Custom : ProjectMembershipRole.Admin,
|
|
||||||
roleId: customRole?.id
|
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
@@ -170,7 +170,8 @@ const sqlFindSecretPathByFolderId = (db: Knex, projectId: string, folderIds: str
|
|||||||
// if the given folder id is root folder id then intial path is set as / instead of /root
|
// if the given folder id is root folder id then intial path is set as / instead of /root
|
||||||
// if not root folder the path here will be /<folder name>
|
// if not root folder the path here will be /<folder name>
|
||||||
path: db.raw(`CONCAT('/', (CASE WHEN "parentId" is NULL THEN '' ELSE ${TableName.SecretFolder}.name END))`),
|
path: db.raw(`CONCAT('/', (CASE WHEN "parentId" is NULL THEN '' ELSE ${TableName.SecretFolder}.name END))`),
|
||||||
child: db.raw("NULL::uuid")
|
child: db.raw("NULL::uuid"),
|
||||||
|
environmentSlug: `${TableName.Environment}.slug`
|
||||||
})
|
})
|
||||||
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||||
.where({ projectId })
|
.where({ projectId })
|
||||||
@@ -190,14 +191,15 @@ const sqlFindSecretPathByFolderId = (db: Knex, projectId: string, folderIds: str
|
|||||||
ELSE CONCAT('/', secret_folders.name)
|
ELSE CONCAT('/', secret_folders.name)
|
||||||
END, parent.path )`
|
END, parent.path )`
|
||||||
),
|
),
|
||||||
child: db.raw("COALESCE(parent.child, parent.id)")
|
child: db.raw("COALESCE(parent.child, parent.id)"),
|
||||||
|
environmentSlug: "parent.environmentSlug"
|
||||||
})
|
})
|
||||||
.from(TableName.SecretFolder)
|
.from(TableName.SecretFolder)
|
||||||
.join("parent", "parent.parentId", `${TableName.SecretFolder}.id`)
|
.join("parent", "parent.parentId", `${TableName.SecretFolder}.id`)
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.select("*")
|
.select("*")
|
||||||
.from<TSecretFolders & { child: string | null; path: string }>("parent");
|
.from<TSecretFolders & { child: string | null; path: string; environmentSlug: string }>("parent");
|
||||||
|
|
||||||
export type TSecretFolderDALFactory = ReturnType<typeof secretFolderDALFactory>;
|
export type TSecretFolderDALFactory = ReturnType<typeof secretFolderDALFactory>;
|
||||||
// never change this. If u do write a migration for it
|
// never change this. If u do write a migration for it
|
||||||
@@ -257,10 +259,12 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
|||||||
const findSecretPathByFolderIds = async (projectId: string, folderIds: string[], tx?: Knex) => {
|
const findSecretPathByFolderIds = async (projectId: string, folderIds: string[], tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const folders = await sqlFindSecretPathByFolderId(tx || db, projectId, folderIds);
|
const folders = await sqlFindSecretPathByFolderId(tx || db, projectId, folderIds);
|
||||||
|
|
||||||
const rootFolders = groupBy(
|
const rootFolders = groupBy(
|
||||||
folders.filter(({ parentId }) => parentId === null),
|
folders.filter(({ parentId }) => parentId === null),
|
||||||
(i) => i.child || i.id // root condition then child and parent will null
|
(i) => i.child || i.id // root condition then child and parent will null
|
||||||
);
|
);
|
||||||
|
|
||||||
return folderIds.map((folderId) => rootFolders[folderId]?.[0]);
|
return folderIds.map((folderId) => rootFolders[folderId]?.[0]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find by secret path" });
|
throw new DatabaseError({ error, name: "Find by secret path" });
|
||||||
|
@@ -49,7 +49,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const find = async (filter: Partial<TSecretImports>, tx?: Knex) => {
|
const find = async (filter: Partial<TSecretImports & { projectId: string }>, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const docs = await (tx || db)(TableName.SecretImport)
|
const docs = await (tx || db)(TableName.SecretImport)
|
||||||
.where(filter)
|
.where(filter)
|
||||||
|
@@ -7,6 +7,7 @@ import { BadRequestError } from "@app/lib/errors";
|
|||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TSecretDALFactory } from "../secret/secret-dal";
|
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||||
|
import { TSecretQueueFactory } from "../secret/secret-queue";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
import { TSecretImportDALFactory } from "./secret-import-dal";
|
import { TSecretImportDALFactory } from "./secret-import-dal";
|
||||||
import { fnSecretsFromImports } from "./secret-import-fns";
|
import { fnSecretsFromImports } from "./secret-import-fns";
|
||||||
@@ -25,6 +26,7 @@ type TSecretImportServiceFactoryDep = {
|
|||||||
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
|
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
|
||||||
projectEnvDAL: TProjectEnvDALFactory;
|
projectEnvDAL: TProjectEnvDALFactory;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets">;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ERR_SEC_IMP_NOT_FOUND = new BadRequestError({ message: "Secret import not found" });
|
const ERR_SEC_IMP_NOT_FOUND = new BadRequestError({ message: "Secret import not found" });
|
||||||
@@ -37,7 +39,8 @@ export const secretImportServiceFactory = ({
|
|||||||
permissionService,
|
permissionService,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
secretDAL
|
secretDAL,
|
||||||
|
secretQueueService
|
||||||
}: TSecretImportServiceFactoryDep) => {
|
}: TSecretImportServiceFactoryDep) => {
|
||||||
const createImport = async ({
|
const createImport = async ({
|
||||||
environment,
|
environment,
|
||||||
@@ -77,10 +80,19 @@ export const secretImportServiceFactory = ({
|
|||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create import" });
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create import" });
|
||||||
|
|
||||||
// TODO(akhilmhdh-pg): updated permission check add here
|
|
||||||
const [importEnv] = await projectEnvDAL.findBySlugs(projectId, [data.environment]);
|
const [importEnv] = await projectEnvDAL.findBySlugs(projectId, [data.environment]);
|
||||||
if (!importEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" });
|
if (!importEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" });
|
||||||
|
|
||||||
|
const sourceFolder = await folderDAL.findBySecretPath(projectId, data.environment, data.path);
|
||||||
|
if (sourceFolder) {
|
||||||
|
const existingImport = await secretImportDAL.findOne({
|
||||||
|
folderId: sourceFolder.id,
|
||||||
|
importEnv: folder.environment.id,
|
||||||
|
importPath: path
|
||||||
|
});
|
||||||
|
if (existingImport) throw new BadRequestError({ message: "Cyclic import not allowed" });
|
||||||
|
}
|
||||||
|
|
||||||
const secImport = await secretImportDAL.transaction(async (tx) => {
|
const secImport = await secretImportDAL.transaction(async (tx) => {
|
||||||
const lastPos = await secretImportDAL.findLastImportPosition(folder.id, tx);
|
const lastPos = await secretImportDAL.findLastImportPosition(folder.id, tx);
|
||||||
return secretImportDAL.create(
|
return secretImportDAL.create(
|
||||||
@@ -94,6 +106,12 @@ export const secretImportServiceFactory = ({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await secretQueueService.syncSecrets({
|
||||||
|
secretPath: secImport.importPath,
|
||||||
|
projectId,
|
||||||
|
environment: importEnv.slug
|
||||||
|
});
|
||||||
|
|
||||||
return { ...secImport, importEnv };
|
return { ...secImport, importEnv };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -131,6 +149,20 @@ export const secretImportServiceFactory = ({
|
|||||||
: await projectEnvDAL.findById(secImpDoc.importEnv);
|
: await projectEnvDAL.findById(secImpDoc.importEnv);
|
||||||
if (!importedEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" });
|
if (!importedEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" });
|
||||||
|
|
||||||
|
const sourceFolder = await folderDAL.findBySecretPath(
|
||||||
|
projectId,
|
||||||
|
importedEnv.slug,
|
||||||
|
data.path || secImpDoc.importPath
|
||||||
|
);
|
||||||
|
if (sourceFolder) {
|
||||||
|
const existingImport = await secretImportDAL.findOne({
|
||||||
|
folderId: sourceFolder.id,
|
||||||
|
importEnv: folder.environment.id,
|
||||||
|
importPath: path
|
||||||
|
});
|
||||||
|
if (existingImport) throw new BadRequestError({ message: "Cyclic import not allowed" });
|
||||||
|
}
|
||||||
|
|
||||||
const updatedSecImport = await secretImportDAL.transaction(async (tx) => {
|
const updatedSecImport = await secretImportDAL.transaction(async (tx) => {
|
||||||
const secImp = await secretImportDAL.findOne({ folderId: folder.id, id });
|
const secImp = await secretImportDAL.findOne({ folderId: folder.id, id });
|
||||||
if (!secImp) throw ERR_SEC_IMP_NOT_FOUND;
|
if (!secImp) throw ERR_SEC_IMP_NOT_FOUND;
|
||||||
@@ -185,6 +217,13 @@ export const secretImportServiceFactory = ({
|
|||||||
if (!importEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" });
|
if (!importEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" });
|
||||||
return { ...doc, importEnv };
|
return { ...doc, importEnv };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await secretQueueService.syncSecrets({
|
||||||
|
secretPath: path,
|
||||||
|
projectId,
|
||||||
|
environment
|
||||||
|
});
|
||||||
|
|
||||||
return secImport;
|
return secImport;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -21,6 +21,7 @@ import {
|
|||||||
} from "@app/lib/crypto";
|
} from "@app/lib/crypto";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { groupBy, unique } from "@app/lib/fn";
|
import { groupBy, unique } from "@app/lib/fn";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
import { getBotKeyFnFactory } from "../project-bot/project-bot-fns";
|
import { getBotKeyFnFactory } from "../project-bot/project-bot-fns";
|
||||||
@@ -92,7 +93,8 @@ const buildHierarchy = (folders: TSecretFolders[]): FolderMap => {
|
|||||||
const generatePaths = (
|
const generatePaths = (
|
||||||
map: FolderMap,
|
map: FolderMap,
|
||||||
parentId: string = "null",
|
parentId: string = "null",
|
||||||
basePath: string = ""
|
basePath: string = "",
|
||||||
|
currentDepth: number = 0
|
||||||
): { path: string; folderId: string }[] => {
|
): { path: string; folderId: string }[] => {
|
||||||
const children = map[parentId || "null"] || [];
|
const children = map[parentId || "null"] || [];
|
||||||
let paths: { path: string; folderId: string }[] = [];
|
let paths: { path: string; folderId: string }[] = [];
|
||||||
@@ -105,13 +107,20 @@ const generatePaths = (
|
|||||||
// eslint-disable-next-line no-nested-ternary
|
// eslint-disable-next-line no-nested-ternary
|
||||||
const currPath = basePath === "" ? (isRootFolder ? "/" : `/${child.name}`) : `${basePath}/${child.name}`;
|
const currPath = basePath === "" ? (isRootFolder ? "/" : `/${child.name}`) : `${basePath}/${child.name}`;
|
||||||
|
|
||||||
|
// Add the current path
|
||||||
paths.push({
|
paths.push({
|
||||||
path: currPath,
|
path: currPath,
|
||||||
folderId: child.id
|
folderId: child.id
|
||||||
}); // Add the current path
|
});
|
||||||
|
|
||||||
// Recursively generate paths for children, passing down the formatted pathh
|
// We make sure that the recursion depth doesn't exceed 20.
|
||||||
const childPaths = generatePaths(map, child.id, currPath);
|
// We do this to create "circuit break", basically to ensure that we can't encounter any potential memory leaks.
|
||||||
|
if (currentDepth >= 20) {
|
||||||
|
logger.info(`generatePaths: Recursion depth exceeded 20, breaking out of recursion [map=${JSON.stringify(map)}]`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Recursively generate paths for children, passing down the formatted path
|
||||||
|
const childPaths = generatePaths(map, child.id, currPath, currentDepth + 1);
|
||||||
paths = paths.concat(
|
paths = paths.concat(
|
||||||
childPaths.map((p) => ({
|
childPaths.map((p) => ({
|
||||||
path: p.path,
|
path: p.path,
|
||||||
|
@@ -3,7 +3,7 @@ import { getConfig } from "@app/lib/config/env";
|
|||||||
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||||
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
|
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { isSamePath } from "@app/lib/fn";
|
import { groupBy, isSamePath, unique } from "@app/lib/fn";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||||
@@ -23,7 +23,6 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
|||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
||||||
import { fnSecretsFromImports } from "../secret-import/secret-import-fns";
|
|
||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
import { TWebhookDALFactory } from "../webhook/webhook-dal";
|
import { TWebhookDALFactory } from "../webhook/webhook-dal";
|
||||||
import { fnTriggerWebhook } from "../webhook/webhook-fns";
|
import { fnTriggerWebhook } from "../webhook/webhook-fns";
|
||||||
@@ -32,7 +31,6 @@ import { interpolateSecrets } from "./secret-fns";
|
|||||||
import { TCreateSecretReminderDTO, THandleReminderDTO, TRemoveSecretReminderDTO } from "./secret-types";
|
import { TCreateSecretReminderDTO, THandleReminderDTO, TRemoveSecretReminderDTO } from "./secret-types";
|
||||||
|
|
||||||
export type TSecretQueueFactory = ReturnType<typeof secretQueueFactory>;
|
export type TSecretQueueFactory = ReturnType<typeof secretQueueFactory>;
|
||||||
|
|
||||||
type TSecretQueueFactoryDep = {
|
type TSecretQueueFactoryDep = {
|
||||||
queueService: TQueueServiceFactory;
|
queueService: TQueueServiceFactory;
|
||||||
integrationDAL: Pick<TIntegrationDALFactory, "findByProjectIdV2" | "updateById">;
|
integrationDAL: Pick<TIntegrationDALFactory, "findByProjectIdV2" | "updateById">;
|
||||||
@@ -60,6 +58,8 @@ export type TGetSecrets = {
|
|||||||
environment: string;
|
environment: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MAX_SYNC_SECRET_DEPTH = 5;
|
||||||
|
|
||||||
export const secretQueueFactory = ({
|
export const secretQueueFactory = ({
|
||||||
queueService,
|
queueService,
|
||||||
integrationDAL,
|
integrationDAL,
|
||||||
@@ -117,7 +117,10 @@ export const secretQueueFactory = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncSecrets = async (dto: TGetSecrets) => {
|
const syncSecrets = async (dto: TGetSecrets & { depth?: number }) => {
|
||||||
|
logger.info(
|
||||||
|
`syncSecrets: syncing project secrets where [projectId=${dto.projectId}] [environment=${dto.environment}] [path=${dto.secretPath}]`
|
||||||
|
);
|
||||||
await queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, dto, {
|
await queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, dto, {
|
||||||
jobId: `secret-webhook-${dto.environment}-${dto.projectId}-${dto.secretPath}`,
|
jobId: `secret-webhook-${dto.environment}-${dto.projectId}-${dto.secretPath}`,
|
||||||
removeOnFail: { count: 5 },
|
removeOnFail: { count: 5 },
|
||||||
@@ -227,62 +230,42 @@ export const secretQueueFactory = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getIntegrationSecrets = async (dto: TGetSecrets & { folderId: string }, key: string) => {
|
type Content = Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the secrets in a given [folderId] including secrets from
|
||||||
|
* nested imported folders recursively.
|
||||||
|
*/
|
||||||
|
const getIntegrationSecrets = async (dto: {
|
||||||
|
projectId: string;
|
||||||
|
environment: string;
|
||||||
|
folderId: string;
|
||||||
|
key: string;
|
||||||
|
depth: number;
|
||||||
|
}) => {
|
||||||
|
let content: Content = {};
|
||||||
|
if (dto.depth > MAX_SYNC_SECRET_DEPTH) {
|
||||||
|
logger.info(
|
||||||
|
`getIntegrationSecrets: secret depth exceeded for [projectId=${dto.projectId}] [folderId=${dto.folderId}] [depth=${dto.depth}]`
|
||||||
|
);
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// process secrets in current folder
|
||||||
const secrets = await secretDAL.findByFolderId(dto.folderId);
|
const secrets = await secretDAL.findByFolderId(dto.folderId);
|
||||||
|
|
||||||
// get imported secrets
|
|
||||||
const secretImport = await secretImportDAL.find({ folderId: dto.folderId });
|
|
||||||
const importedSecrets = await fnSecretsFromImports({
|
|
||||||
allowedImports: secretImport,
|
|
||||||
secretDAL,
|
|
||||||
folderDAL
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!secrets.length && !importedSecrets.length) return {};
|
|
||||||
|
|
||||||
const content: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }> = {};
|
|
||||||
|
|
||||||
importedSecrets.forEach(({ secrets: secs }) => {
|
|
||||||
secs.forEach((secret) => {
|
|
||||||
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
|
||||||
ciphertext: secret.secretKeyCiphertext,
|
|
||||||
iv: secret.secretKeyIV,
|
|
||||||
tag: secret.secretKeyTag,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
const secretValue = decryptSymmetric128BitHexKeyUTF8({
|
|
||||||
ciphertext: secret.secretValueCiphertext,
|
|
||||||
iv: secret.secretValueIV,
|
|
||||||
tag: secret.secretValueTag,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
content[secretKey] = { value: secretValue };
|
|
||||||
content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
|
|
||||||
|
|
||||||
if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
|
|
||||||
const commentValue = decryptSymmetric128BitHexKeyUTF8({
|
|
||||||
ciphertext: secret.secretCommentCiphertext,
|
|
||||||
iv: secret.secretCommentIV,
|
|
||||||
tag: secret.secretCommentTag,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
content[secretKey].comment = commentValue;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext: secret.secretKeyCiphertext,
|
ciphertext: secret.secretKeyCiphertext,
|
||||||
iv: secret.secretKeyIV,
|
iv: secret.secretKeyIV,
|
||||||
tag: secret.secretKeyTag,
|
tag: secret.secretKeyTag,
|
||||||
key
|
key: dto.key
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretValue = decryptSymmetric128BitHexKeyUTF8({
|
const secretValue = decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext: secret.secretValueCiphertext,
|
ciphertext: secret.secretValueCiphertext,
|
||||||
iv: secret.secretValueIV,
|
iv: secret.secretValueIV,
|
||||||
tag: secret.secretValueTag,
|
tag: secret.secretValueTag,
|
||||||
key
|
key: dto.key
|
||||||
});
|
});
|
||||||
|
|
||||||
content[secretKey] = { value: secretValue };
|
content[secretKey] = { value: secretValue };
|
||||||
@@ -292,38 +275,111 @@ export const secretQueueFactory = ({
|
|||||||
ciphertext: secret.secretCommentCiphertext,
|
ciphertext: secret.secretCommentCiphertext,
|
||||||
iv: secret.secretCommentIV,
|
iv: secret.secretCommentIV,
|
||||||
tag: secret.secretCommentTag,
|
tag: secret.secretCommentTag,
|
||||||
key
|
key: dto.key
|
||||||
});
|
});
|
||||||
content[secretKey].comment = commentValue;
|
content[secretKey].comment = commentValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
|
content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
|
||||||
});
|
});
|
||||||
|
|
||||||
const expandSecrets = interpolateSecrets({
|
const expandSecrets = interpolateSecrets({
|
||||||
projectId: dto.projectId,
|
projectId: dto.projectId,
|
||||||
secretEncKey: key,
|
secretEncKey: dto.key,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
secretDAL
|
secretDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
await expandSecrets(content);
|
await expandSecrets(content);
|
||||||
|
|
||||||
|
// check if current folder has any imports from other folders
|
||||||
|
const secretImport = await secretImportDAL.find({ folderId: dto.folderId });
|
||||||
|
|
||||||
|
// if no imports then return secrets in the current folder
|
||||||
|
if (!secretImport) return content;
|
||||||
|
|
||||||
|
const importedFolders = await folderDAL.findByManySecretPath(
|
||||||
|
secretImport.map(({ importEnv, importPath }) => ({
|
||||||
|
envId: importEnv.id,
|
||||||
|
secretPath: importPath
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
for await (const folder of importedFolders) {
|
||||||
|
if (folder) {
|
||||||
|
// get secrets contained in each imported folder by recursively calling
|
||||||
|
// this function against the imported folder
|
||||||
|
const importedSecrets = await getIntegrationSecrets({
|
||||||
|
environment: dto.environment,
|
||||||
|
projectId: dto.projectId,
|
||||||
|
folderId: folder.id,
|
||||||
|
key: dto.key,
|
||||||
|
depth: dto.depth + 1
|
||||||
|
});
|
||||||
|
|
||||||
|
// add the imported secrets to the current folder secrets
|
||||||
|
content = { ...content, ...importedSecrets };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
};
|
};
|
||||||
|
|
||||||
queueService.start(QueueName.IntegrationSync, async (job) => {
|
queueService.start(QueueName.IntegrationSync, async (job) => {
|
||||||
const { environment, projectId, secretPath } = job.data;
|
const { environment, projectId, secretPath, depth = 1 } = job.data;
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
if (!folder) {
|
if (!folder) {
|
||||||
logger.error("Secret path not found");
|
logger.error(new Error("Secret path not found"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const integrations = await integrationDAL.findByProjectIdV2(projectId, environment);
|
// start syncing all linked imports also
|
||||||
|
if (depth < MAX_SYNC_SECRET_DEPTH) {
|
||||||
|
// find all imports made with the given environment and secret path
|
||||||
|
const linkSourceDto = {
|
||||||
|
projectId,
|
||||||
|
importEnv: folder.environment.id,
|
||||||
|
importPath: secretPath
|
||||||
|
};
|
||||||
|
const imports = await secretImportDAL.find(linkSourceDto);
|
||||||
|
|
||||||
|
if (imports.length) {
|
||||||
|
// keep calling sync secret for all the imports made
|
||||||
|
const importedFolderIds = unique(imports, (i) => i.folderId).map(({ folderId }) => folderId);
|
||||||
|
const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds);
|
||||||
|
const foldersGroupedById = groupBy(importedFolders, (i) => i.child || i.id);
|
||||||
|
await Promise.all(
|
||||||
|
imports
|
||||||
|
.filter(({ folderId }) => Boolean(foldersGroupedById[folderId][0].path))
|
||||||
|
.map(({ folderId }) => {
|
||||||
|
const syncDto = {
|
||||||
|
depth: depth + 1,
|
||||||
|
projectId,
|
||||||
|
secretPath: foldersGroupedById[folderId][0].path,
|
||||||
|
environment: foldersGroupedById[folderId][0].environmentSlug
|
||||||
|
};
|
||||||
|
logger.info(
|
||||||
|
`getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
||||||
|
);
|
||||||
|
return syncSecrets(syncDto);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info(`getIntegrationSecrets: Secret depth exceeded for [projectId=${projectId}] [folderId=${folder.id}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); // note: returns array of integrations + integration auths in this environment
|
||||||
const toBeSyncedIntegrations = integrations.filter(
|
const toBeSyncedIntegrations = integrations.filter(
|
||||||
|
// note: sync only the integrations sourced from secretPath
|
||||||
({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath)
|
({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!integrations.length) return;
|
if (!integrations.length) return;
|
||||||
logger.info("Secret integration sync started", job.data, job.id);
|
logger.info(
|
||||||
|
`getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]`
|
||||||
|
);
|
||||||
for (const integration of toBeSyncedIntegrations) {
|
for (const integration of toBeSyncedIntegrations) {
|
||||||
const integrationAuth = {
|
const integrationAuth = {
|
||||||
...integration.integrationAuth,
|
...integration.integrationAuth,
|
||||||
@@ -334,7 +390,13 @@ export const secretQueueFactory = ({
|
|||||||
|
|
||||||
const botKey = await projectBotService.getBotKey(projectId);
|
const botKey = await projectBotService.getBotKey(projectId);
|
||||||
const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken(integrationAuth, botKey);
|
const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken(integrationAuth, botKey);
|
||||||
const secrets = await getIntegrationSecrets({ environment, projectId, secretPath, folderId: folder.id }, botKey);
|
const secrets = await getIntegrationSecrets({
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
folderId: folder.id,
|
||||||
|
key: botKey,
|
||||||
|
depth: 1
|
||||||
|
});
|
||||||
const suffixedSecrets: typeof secrets = {};
|
const suffixedSecrets: typeof secrets = {};
|
||||||
const metadata = integration.metadata as Record<string, string>;
|
const metadata = integration.metadata as Record<string, string>;
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
@@ -362,7 +424,7 @@ export const secretQueueFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Secret integration sync ended", job.id);
|
logger.info("Secret integration sync ended: %s", job.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
queueService.start(QueueName.SecretReminder, async ({ data }) => {
|
queueService.start(QueueName.SecretReminder, async ({ data }) => {
|
||||||
@@ -403,7 +465,7 @@ export const secretQueueFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
queueService.listen(QueueName.IntegrationSync, "failed", (job, err) => {
|
queueService.listen(QueueName.IntegrationSync, "failed", (job, err) => {
|
||||||
logger.error("Failed to sync integration", job?.data, err);
|
logger.error(err, "Failed to sync integration %s", job?.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
queueService.start(QueueName.SecretWebhook, async (job) => {
|
queueService.start(QueueName.SecretWebhook, async (job) => {
|
||||||
@@ -411,7 +473,8 @@ export const secretQueueFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
syncSecrets,
|
// depth is internal only field thus no need to make it available outside
|
||||||
|
syncSecrets: (dto: TGetSecrets) => syncSecrets(dto),
|
||||||
syncIntegrations,
|
syncIntegrations,
|
||||||
addSecretReminder,
|
addSecretReminder,
|
||||||
removeSecretReminder,
|
removeSecretReminder,
|
||||||
|
@@ -528,6 +528,7 @@ type GetRawSecretsV3Request struct {
|
|||||||
WorkspaceId string `json:"workspaceId"`
|
WorkspaceId string `json:"workspaceId"`
|
||||||
SecretPath string `json:"secretPath"`
|
SecretPath string `json:"secretPath"`
|
||||||
IncludeImport bool `json:"include_imports"`
|
IncludeImport bool `json:"include_imports"`
|
||||||
|
Recursive bool `json:"recursive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetRawSecretsV3Response struct {
|
type GetRawSecretsV3Response struct {
|
||||||
|
@@ -479,7 +479,7 @@ func (tm *AgentManager) GetToken() string {
|
|||||||
|
|
||||||
// Fetches a new access token using client credentials
|
// Fetches a new access token using client credentials
|
||||||
func (tm *AgentManager) FetchNewAccessToken() error {
|
func (tm *AgentManager) FetchNewAccessToken() error {
|
||||||
clientID := os.Getenv("INFISICAL_UNIVERSAL_AUTH_CLIENT_ID")
|
clientID := os.Getenv(util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME)
|
||||||
if clientID == "" {
|
if clientID == "" {
|
||||||
clientIDAsByte, err := ReadFile(tm.clientIdPath)
|
clientIDAsByte, err := ReadFile(tm.clientIdPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -509,7 +509,7 @@ func (tm *AgentManager) FetchNewAccessToken() error {
|
|||||||
// save as cache in memory
|
// save as cache in memory
|
||||||
tm.cachedClientSecret = clientSecret
|
tm.cachedClientSecret = clientSecret
|
||||||
|
|
||||||
err, loginResponse := universalAuthLogin(clientID, clientSecret)
|
loginResponse, err := util.UniversalAuthLogin(clientID, clientSecret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -725,20 +725,6 @@ func (tm *AgentManager) MonitorSecretChanges(secretTemplate Template, templateId
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func universalAuthLogin(clientId string, clientSecret string) (error, api.UniversalAuthLoginResponse) {
|
|
||||||
httpClient := resty.New()
|
|
||||||
httpClient.SetRetryCount(10000).
|
|
||||||
SetRetryMaxWaitTime(20 * time.Second).
|
|
||||||
SetRetryWaitTime(5 * time.Second)
|
|
||||||
|
|
||||||
tokenResponse, err := api.CallUniversalAuthLogin(httpClient, api.UniversalAuthLoginRequest{ClientId: clientId, ClientSecret: clientSecret})
|
|
||||||
if err != nil {
|
|
||||||
return err, api.UniversalAuthLoginResponse{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, tokenResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
// runCmd represents the run command
|
// runCmd represents the run command
|
||||||
var agentCmd = &cobra.Command{
|
var agentCmd = &cobra.Command{
|
||||||
Example: `
|
Example: `
|
||||||
|
@@ -44,6 +44,11 @@ var exportCmd = &cobra.Command{
|
|||||||
util.HandleError(err)
|
util.HandleError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
includeImports, err := cmd.Flags().GetBool("include-imports")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
projectId, err := cmd.Flags().GetString("projectId")
|
projectId, err := cmd.Flags().GetString("projectId")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err)
|
util.HandleError(err)
|
||||||
@@ -59,8 +64,7 @@ var exportCmd = &cobra.Command{
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
infisicalToken, err := util.GetInfisicalServiceToken(cmd)
|
token, err := util.GetInfisicalToken(cmd)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
@@ -75,7 +79,21 @@ var exportCmd = &cobra.Command{
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, WorkspaceId: projectId, SecretsPath: secretsPath}, "")
|
request := models.GetAllSecretsParameters{
|
||||||
|
Environment: environmentName,
|
||||||
|
TagSlugs: tagSlugs,
|
||||||
|
WorkspaceId: projectId,
|
||||||
|
SecretsPath: secretsPath,
|
||||||
|
IncludeImport: includeImports,
|
||||||
|
}
|
||||||
|
|
||||||
|
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
|
||||||
|
request.InfisicalToken = token.Token
|
||||||
|
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
|
||||||
|
request.UniversalAuthAccessToken = token.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, err := util.GetAllEnvironmentVariables(request, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to fetch secrets")
|
util.HandleError(err, "Unable to fetch secrets")
|
||||||
}
|
}
|
||||||
@@ -88,9 +106,16 @@ var exportCmd = &cobra.Command{
|
|||||||
|
|
||||||
var output string
|
var output string
|
||||||
if shouldExpandSecrets {
|
if shouldExpandSecrets {
|
||||||
secrets = util.ExpandSecrets(secrets, models.ExpandSecretsAuthentication{
|
|
||||||
InfisicalToken: infisicalToken,
|
authParams := models.ExpandSecretsAuthentication{}
|
||||||
}, "")
|
|
||||||
|
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
|
||||||
|
authParams.InfisicalToken = token.Token
|
||||||
|
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
|
||||||
|
authParams.UniversalAuthAccessToken = token.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets = util.ExpandSecrets(secrets, authParams, "")
|
||||||
}
|
}
|
||||||
secrets = util.FilterSecretsByTag(secrets, tagSlugs)
|
secrets = util.FilterSecretsByTag(secrets, tagSlugs)
|
||||||
output, err = formatEnvs(secrets, format)
|
output, err = formatEnvs(secrets, format)
|
||||||
@@ -110,6 +135,7 @@ func init() {
|
|||||||
exportCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
|
exportCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
|
||||||
exportCmd.Flags().StringP("format", "f", "dotenv", "Set the format of the output file (dotenv, json, csv)")
|
exportCmd.Flags().StringP("format", "f", "dotenv", "Set the format of the output file (dotenv, json, csv)")
|
||||||
exportCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
|
exportCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
|
||||||
|
exportCmd.Flags().Bool("include-imports", true, "Imported linked secrets")
|
||||||
exportCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
|
exportCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
|
||||||
exportCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs")
|
exportCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs")
|
||||||
exportCmd.Flags().String("projectId", "", "manually set the projectId to fetch secrets from")
|
exportCmd.Flags().String("projectId", "", "manually set the projectId to fetch secrets from")
|
||||||
|
@@ -36,18 +36,33 @@ var getCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
infisicalToken, err := util.GetInfisicalServiceToken(cmd)
|
projectId, err := cmd.Flags().GetString("projectId")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
token, err := util.GetInfisicalToken(cmd)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
foldersPath, err := cmd.Flags().GetString("path")
|
foldersPath, err := cmd.Flags().GetString("path")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
folders, err := util.GetAllFolders(models.GetAllFoldersParameters{Environment: environmentName, InfisicalToken: infisicalToken, FoldersPath: foldersPath})
|
request := models.GetAllFoldersParameters{
|
||||||
|
Environment: environmentName,
|
||||||
|
WorkspaceId: projectId,
|
||||||
|
FoldersPath: foldersPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
|
||||||
|
request.InfisicalToken = token.Token
|
||||||
|
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
|
||||||
|
request.UniversalAuthAccessToken = token.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
folders, err := util.GetAllFolders(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to get folders")
|
util.HandleError(err, "Unable to get folders")
|
||||||
}
|
}
|
||||||
|
@@ -55,95 +55,157 @@ var loginCmd = &cobra.Command{
|
|||||||
Short: "Login into your Infisical account",
|
Short: "Login into your Infisical account",
|
||||||
DisableFlagsInUseLine: true,
|
DisableFlagsInUseLine: true,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
|
|
||||||
// if the key can't be found or there is an error getting current credentials from key ring, allow them to override
|
loginMethod, err := cmd.Flags().GetString("method")
|
||||||
if err != nil && (strings.Contains(err.Error(), "we couldn't find your logged in details")) {
|
if err != nil {
|
||||||
log.Debug().Err(err)
|
util.HandleError(err)
|
||||||
} else if err != nil {
|
}
|
||||||
|
plainOutput, err := cmd.Flags().GetBool("plain")
|
||||||
|
if err != nil {
|
||||||
util.HandleError(err)
|
util.HandleError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentLoggedInUserDetails.IsUserLoggedIn && !currentLoggedInUserDetails.LoginExpired && len(currentLoggedInUserDetails.UserCredentials.PrivateKey) != 0 {
|
if loginMethod != "user" && loginMethod != "universal-auth" {
|
||||||
shouldOverride, err := userLoginMenu(currentLoggedInUserDetails.UserCredentials.Email)
|
util.PrintErrorMessageAndExit("Invalid login method. Please use either 'user' or 'universal-auth'")
|
||||||
if err != nil {
|
}
|
||||||
|
|
||||||
|
if loginMethod == "user" {
|
||||||
|
|
||||||
|
currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
|
||||||
|
// if the key can't be found or there is an error getting current credentials from key ring, allow them to override
|
||||||
|
if err != nil && (strings.Contains(err.Error(), "we couldn't find your logged in details")) {
|
||||||
|
log.Debug().Err(err)
|
||||||
|
} else if err != nil {
|
||||||
util.HandleError(err)
|
util.HandleError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !shouldOverride {
|
if currentLoggedInUserDetails.IsUserLoggedIn && !currentLoggedInUserDetails.LoginExpired && len(currentLoggedInUserDetails.UserCredentials.PrivateKey) != 0 {
|
||||||
return
|
shouldOverride, err := userLoginMenu(currentLoggedInUserDetails.UserCredentials.Email)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !shouldOverride {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
//override domain
|
||||||
//override domain
|
domainQuery := true
|
||||||
domainQuery := true
|
if config.INFISICAL_URL_MANUAL_OVERRIDE != "" && config.INFISICAL_URL_MANUAL_OVERRIDE != util.INFISICAL_DEFAULT_API_URL {
|
||||||
if config.INFISICAL_URL_MANUAL_OVERRIDE != "" && config.INFISICAL_URL_MANUAL_OVERRIDE != util.INFISICAL_DEFAULT_API_URL {
|
overrideDomain, err := DomainOverridePrompt()
|
||||||
overrideDomain, err := DomainOverridePrompt()
|
if err != nil {
|
||||||
if err != nil {
|
util.HandleError(err)
|
||||||
util.HandleError(err)
|
}
|
||||||
|
|
||||||
|
//if not override set INFISICAL_URL to exported var
|
||||||
|
//set domainQuery to false
|
||||||
|
if !overrideDomain {
|
||||||
|
domainQuery = false
|
||||||
|
config.INFISICAL_URL = config.INFISICAL_URL_MANUAL_OVERRIDE
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//if not override set INFISICAL_URL to exported var
|
//prompt user to select domain between Infisical cloud and self hosting
|
||||||
//set domainQuery to false
|
if domainQuery {
|
||||||
if !overrideDomain {
|
err = askForDomain()
|
||||||
domainQuery = false
|
if err != nil {
|
||||||
config.INFISICAL_URL = config.INFISICAL_URL_MANUAL_OVERRIDE
|
util.HandleError(err, "Unable to parse domain url")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
var userCredentialsToBeStored models.UserCredentials
|
||||||
|
|
||||||
}
|
interactiveLogin := false
|
||||||
|
if cmd.Flags().Changed("interactive") {
|
||||||
//prompt user to select domain between Infisical cloud and self hosting
|
interactiveLogin = true
|
||||||
if domainQuery {
|
|
||||||
err = askForDomain()
|
|
||||||
if err != nil {
|
|
||||||
util.HandleError(err, "Unable to parse domain url")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var userCredentialsToBeStored models.UserCredentials
|
|
||||||
|
|
||||||
interactiveLogin := false
|
|
||||||
if cmd.Flags().Changed("interactive") {
|
|
||||||
interactiveLogin = true
|
|
||||||
cliDefaultLogin(&userCredentialsToBeStored)
|
|
||||||
}
|
|
||||||
|
|
||||||
//call browser login function
|
|
||||||
if !interactiveLogin {
|
|
||||||
fmt.Println("Logging in via browser... To login via interactive mode run [infisical login -i]")
|
|
||||||
userCredentialsToBeStored, err = browserCliLogin()
|
|
||||||
if err != nil {
|
|
||||||
//default to cli login on error
|
|
||||||
cliDefaultLogin(&userCredentialsToBeStored)
|
cliDefaultLogin(&userCredentialsToBeStored)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//call browser login function
|
||||||
|
if !interactiveLogin {
|
||||||
|
fmt.Println("Logging in via browser... To login via interactive mode run [infisical login -i]")
|
||||||
|
userCredentialsToBeStored, err = browserCliLogin()
|
||||||
|
if err != nil {
|
||||||
|
//default to cli login on error
|
||||||
|
cliDefaultLogin(&userCredentialsToBeStored)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = util.StoreUserCredsInKeyRing(&userCredentialsToBeStored)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msgf("Unable to store your credentials in system vault [%s]")
|
||||||
|
log.Error().Msgf("\nTo trouble shoot further, read https://infisical.com/docs/cli/faq")
|
||||||
|
log.Debug().Err(err)
|
||||||
|
//return here
|
||||||
|
util.HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = util.WriteInitalConfig(&userCredentialsToBeStored)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to write write to Infisical Config file. Please try again")
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear backed up secrets from prev account
|
||||||
|
util.DeleteBackupSecrets()
|
||||||
|
|
||||||
|
whilte := color.New(color.FgGreen)
|
||||||
|
boldWhite := whilte.Add(color.Bold)
|
||||||
|
time.Sleep(time.Second * 1)
|
||||||
|
boldWhite.Printf(">>>> Welcome to Infisical!")
|
||||||
|
boldWhite.Printf(" You are now logged in as %v <<<< \n", userCredentialsToBeStored.Email)
|
||||||
|
|
||||||
|
plainBold := color.New(color.Bold)
|
||||||
|
|
||||||
|
plainBold.Println("\nQuick links")
|
||||||
|
fmt.Println("- Learn to inject secrets into your application at https://infisical.com/docs/cli/usage")
|
||||||
|
fmt.Println("- Stuck? Join our slack for quick support https://infisical.com/slack")
|
||||||
|
Telemetry.CaptureEvent("cli-command:login", posthog.NewProperties().Set("infisical-backend", config.INFISICAL_URL).Set("version", util.CLI_VERSION))
|
||||||
|
} else if loginMethod == "universal-auth" {
|
||||||
|
|
||||||
|
clientId, err := cmd.Flags().GetString("client-id")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientSecret, err := cmd.Flags().GetString("client-secret")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if clientId == "" {
|
||||||
|
clientId = os.Getenv(util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME)
|
||||||
|
if clientId == "" {
|
||||||
|
util.PrintErrorMessageAndExit("Please provide client-id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if clientSecret == "" {
|
||||||
|
clientSecret = os.Getenv(util.INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME)
|
||||||
|
if clientSecret == "" {
|
||||||
|
util.PrintErrorMessageAndExit("Please provide client-secret")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := util.UniversalAuthLogin(clientId, clientSecret)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if plainOutput {
|
||||||
|
fmt.Println(res.AccessToken)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
boldGreen := color.New(color.FgGreen).Add(color.Bold)
|
||||||
|
boldPlain := color.New(color.Bold)
|
||||||
|
time.Sleep(time.Second * 1)
|
||||||
|
boldGreen.Printf(">>>> Successfully authenticated with Universal Auth!\n\n")
|
||||||
|
boldPlain.Printf("Universal Auth Access Token:\n%v", res.AccessToken)
|
||||||
|
|
||||||
|
plainBold := color.New(color.Bold)
|
||||||
|
plainBold.Println("\n\nYou can use this access token to authenticate through other commands in the CLI.")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = util.StoreUserCredsInKeyRing(&userCredentialsToBeStored)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Msgf("Unable to store your credentials in system vault [%s]")
|
|
||||||
log.Error().Msgf("\nTo trouble shoot further, read https://infisical.com/docs/cli/faq")
|
|
||||||
log.Debug().Err(err)
|
|
||||||
//return here
|
|
||||||
util.HandleError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = util.WriteInitalConfig(&userCredentialsToBeStored)
|
|
||||||
if err != nil {
|
|
||||||
util.HandleError(err, "Unable to write write to Infisical Config file. Please try again")
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear backed up secrets from prev account
|
|
||||||
util.DeleteBackupSecrets()
|
|
||||||
|
|
||||||
whilte := color.New(color.FgGreen)
|
|
||||||
boldWhite := whilte.Add(color.Bold)
|
|
||||||
time.Sleep(time.Second * 1)
|
|
||||||
boldWhite.Printf(">>>> Welcome to Infisical!")
|
|
||||||
boldWhite.Printf(" You are now logged in as %v <<<< \n", userCredentialsToBeStored.Email)
|
|
||||||
|
|
||||||
plainBold := color.New(color.Bold)
|
|
||||||
|
|
||||||
plainBold.Println("\nQuick links")
|
|
||||||
fmt.Println("- Learn to inject secrets into your application at https://infisical.com/docs/cli/usage")
|
|
||||||
fmt.Println("- Stuck? Join our slack for quick support https://infisical.com/slack")
|
|
||||||
Telemetry.CaptureEvent("cli-command:login", posthog.NewProperties().Set("infisical-backend", config.INFISICAL_URL).Set("version", util.CLI_VERSION))
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,6 +375,10 @@ func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials) {
|
|||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(loginCmd)
|
rootCmd.AddCommand(loginCmd)
|
||||||
loginCmd.Flags().BoolP("interactive", "i", false, "login via the command line")
|
loginCmd.Flags().BoolP("interactive", "i", false, "login via the command line")
|
||||||
|
loginCmd.Flags().String("method", "user", "login method [user, universal-auth]")
|
||||||
|
loginCmd.Flags().String("client-id", "", "client id for universal auth")
|
||||||
|
loginCmd.Flags().Bool("plain", false, "only output the token without any formatting")
|
||||||
|
loginCmd.Flags().String("client-secret", "", "client secret for universal auth")
|
||||||
}
|
}
|
||||||
|
|
||||||
func DomainOverridePrompt() (bool, error) {
|
func DomainOverridePrompt() (bool, error) {
|
||||||
|
@@ -40,8 +40,14 @@ func init() {
|
|||||||
rootCmd.PersistentFlags().StringP("log-level", "l", "info", "log level (trace, debug, info, warn, error, fatal)")
|
rootCmd.PersistentFlags().StringP("log-level", "l", "info", "log level (trace, debug, info, warn, error, fatal)")
|
||||||
rootCmd.PersistentFlags().Bool("telemetry", true, "Infisical collects non-sensitive telemetry data to enhance features and improve user experience. Participation is voluntary")
|
rootCmd.PersistentFlags().Bool("telemetry", true, "Infisical collects non-sensitive telemetry data to enhance features and improve user experience. Participation is voluntary")
|
||||||
rootCmd.PersistentFlags().StringVar(&config.INFISICAL_URL, "domain", util.INFISICAL_DEFAULT_API_URL, "Point the CLI to your own backend [can also set via environment variable name: INFISICAL_API_URL]")
|
rootCmd.PersistentFlags().StringVar(&config.INFISICAL_URL, "domain", util.INFISICAL_DEFAULT_API_URL, "Point the CLI to your own backend [can also set via environment variable name: INFISICAL_API_URL]")
|
||||||
|
rootCmd.PersistentFlags().Bool("silent", false, "Disable output of tip/info messages. Useful when running in scripts or CI/CD pipelines.")
|
||||||
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
||||||
if !util.IsRunningInDocker() {
|
silent, err := cmd.Flags().GetBool("silent")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !util.IsRunningInDocker() && !silent {
|
||||||
util.CheckForUpdate()
|
util.CheckForUpdate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -62,8 +62,7 @@ var runCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
infisicalToken, err := util.GetInfisicalServiceToken(cmd)
|
token, err := util.GetInfisicalToken(cmd)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
@@ -73,6 +72,11 @@ var runCmd = &cobra.Command{
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
projectId, err := cmd.Flags().GetString("projectId")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
secretOverriding, err := cmd.Flags().GetBool("secret-overriding")
|
secretOverriding, err := cmd.Flags().GetBool("secret-overriding")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
@@ -103,7 +107,22 @@ var runCmd = &cobra.Command{
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports, Recursive: recursive}, projectConfigDir)
|
request := models.GetAllSecretsParameters{
|
||||||
|
Environment: environmentName,
|
||||||
|
WorkspaceId: projectId,
|
||||||
|
TagSlugs: tagSlugs,
|
||||||
|
SecretsPath: secretsPath,
|
||||||
|
IncludeImport: includeImports,
|
||||||
|
Recursive: recursive,
|
||||||
|
}
|
||||||
|
|
||||||
|
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
|
||||||
|
request.InfisicalToken = token.Token
|
||||||
|
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
|
||||||
|
request.UniversalAuthAccessToken = token.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, err := util.GetAllEnvironmentVariables(request, projectConfigDir)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")
|
util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")
|
||||||
@@ -116,9 +135,16 @@ var runCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if shouldExpandSecrets {
|
if shouldExpandSecrets {
|
||||||
secrets = util.ExpandSecrets(secrets, models.ExpandSecretsAuthentication{
|
|
||||||
InfisicalToken: infisicalToken,
|
authParams := models.ExpandSecretsAuthentication{}
|
||||||
}, projectConfigDir)
|
|
||||||
|
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
|
||||||
|
authParams.InfisicalToken = token.Token
|
||||||
|
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
|
||||||
|
authParams.UniversalAuthAccessToken = token.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets = util.ExpandSecrets(secrets, authParams, projectConfigDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
secretsByKey := getSecretsByKeys(secrets)
|
secretsByKey := getSecretsByKeys(secrets)
|
||||||
@@ -149,7 +175,15 @@ var runCmd = &cobra.Command{
|
|||||||
|
|
||||||
log.Debug().Msgf("injecting the following environment variables into shell: %v", env)
|
log.Debug().Msgf("injecting the following environment variables into shell: %v", env)
|
||||||
|
|
||||||
Telemetry.CaptureEvent("cli-command:run", posthog.NewProperties().Set("secretsCount", len(secrets)).Set("environment", environmentName).Set("isUsingServiceToken", infisicalToken != "").Set("single-command", strings.Join(args, " ")).Set("multi-command", cmd.Flag("command").Value.String()).Set("version", util.CLI_VERSION))
|
Telemetry.CaptureEvent("cli-command:run",
|
||||||
|
posthog.NewProperties().
|
||||||
|
Set("secretsCount", len(secrets)).
|
||||||
|
Set("environment", environmentName).
|
||||||
|
Set("isUsingServiceToken", token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER).
|
||||||
|
Set("isUsingUniversalAuthToken", token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER).
|
||||||
|
Set("single-command", strings.Join(args, " ")).
|
||||||
|
Set("multi-command", cmd.Flag("command").Value.String()).
|
||||||
|
Set("version", util.CLI_VERSION))
|
||||||
|
|
||||||
if cmd.Flags().Changed("command") {
|
if cmd.Flags().Changed("command") {
|
||||||
command := cmd.Flag("command").Value.String()
|
command := cmd.Flag("command").Value.String()
|
||||||
@@ -204,6 +238,7 @@ func filterReservedEnvVars(env map[string]models.SingleEnvironmentVariable) {
|
|||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(runCmd)
|
rootCmd.AddCommand(runCmd)
|
||||||
runCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
|
runCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
|
||||||
|
runCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity")
|
||||||
runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from")
|
runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from")
|
||||||
runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
|
runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
|
||||||
runCmd.Flags().Bool("include-imports", true, "Import linked secrets ")
|
runCmd.Flags().Bool("include-imports", true, "Import linked secrets ")
|
||||||
|
@@ -38,12 +38,12 @@ var secretsCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
infisicalToken, err := util.GetInfisicalServiceToken(cmd)
|
token, err := util.GetInfisicalToken(cmd)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
projectId, err := cmd.Flags().GetString("projectId")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,22 @@ var secretsCmd = &cobra.Command{
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports, Recursive: recursive}, "")
|
request := models.GetAllSecretsParameters{
|
||||||
|
Environment: environmentName,
|
||||||
|
WorkspaceId: projectId,
|
||||||
|
TagSlugs: tagSlugs,
|
||||||
|
SecretsPath: secretsPath,
|
||||||
|
IncludeImport: includeImports,
|
||||||
|
Recursive: recursive,
|
||||||
|
}
|
||||||
|
|
||||||
|
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
|
||||||
|
request.InfisicalToken = token.Token
|
||||||
|
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
|
||||||
|
request.UniversalAuthAccessToken = token.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, err := util.GetAllEnvironmentVariables(request, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err)
|
util.HandleError(err)
|
||||||
}
|
}
|
||||||
@@ -90,9 +105,15 @@ var secretsCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if shouldExpandSecrets {
|
if shouldExpandSecrets {
|
||||||
secrets = util.ExpandSecrets(secrets, models.ExpandSecretsAuthentication{
|
|
||||||
InfisicalToken: infisicalToken,
|
authParams := models.ExpandSecretsAuthentication{}
|
||||||
}, "")
|
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
|
||||||
|
authParams.InfisicalToken = token.Token
|
||||||
|
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
|
||||||
|
authParams.UniversalAuthAccessToken = token.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets = util.ExpandSecrets(secrets, authParams, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
visualize.PrintAllSecretDetails(secrets)
|
visualize.PrintAllSecretDetails(secrets)
|
||||||
@@ -402,8 +423,12 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
infisicalToken, err := util.GetInfisicalServiceToken(cmd)
|
token, err := util.GetInfisicalToken(cmd)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldExpand, err := cmd.Flags().GetBool("expand")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
@@ -413,6 +438,11 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
projectId, err := cmd.Flags().GetString("projectId")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
secretsPath, err := cmd.Flags().GetString("path")
|
secretsPath, err := cmd.Flags().GetString("path")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to parse path flag")
|
util.HandleError(err, "Unable to parse path flag")
|
||||||
@@ -428,11 +458,37 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Unable to parse path flag")
|
util.HandleError(err, "Unable to parse path flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: true, Recursive: recursive}, "")
|
request := models.GetAllSecretsParameters{
|
||||||
|
Environment: environmentName,
|
||||||
|
WorkspaceId: projectId,
|
||||||
|
TagSlugs: tagSlugs,
|
||||||
|
SecretsPath: secretsPath,
|
||||||
|
IncludeImport: true,
|
||||||
|
Recursive: recursive,
|
||||||
|
}
|
||||||
|
|
||||||
|
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
|
||||||
|
request.InfisicalToken = token.Token
|
||||||
|
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
|
||||||
|
request.UniversalAuthAccessToken = token.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, err := util.GetAllEnvironmentVariables(request, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "To fetch all secrets")
|
util.HandleError(err, "To fetch all secrets")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if shouldExpand {
|
||||||
|
authParams := models.ExpandSecretsAuthentication{}
|
||||||
|
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
|
||||||
|
authParams.InfisicalToken = token.Token
|
||||||
|
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
|
||||||
|
authParams.UniversalAuthAccessToken = token.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets = util.ExpandSecrets(secrets, authParams, "")
|
||||||
|
}
|
||||||
|
|
||||||
requestedSecrets := []models.SingleEnvironmentVariable{}
|
requestedSecrets := []models.SingleEnvironmentVariable{}
|
||||||
|
|
||||||
secretsMap := getSecretsByKeys(secrets)
|
secretsMap := getSecretsByKeys(secrets)
|
||||||
@@ -475,8 +531,12 @@ func generateExampleEnv(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
infisicalToken, err := util.GetInfisicalServiceToken(cmd)
|
token, err := util.GetInfisicalToken(cmd)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
projectId, err := cmd.Flags().GetString("projectId")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
@@ -486,7 +546,21 @@ func generateExampleEnv(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: true}, "")
|
request := models.GetAllSecretsParameters{
|
||||||
|
Environment: environmentName,
|
||||||
|
WorkspaceId: projectId,
|
||||||
|
TagSlugs: tagSlugs,
|
||||||
|
SecretsPath: secretsPath,
|
||||||
|
IncludeImport: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
|
||||||
|
request.InfisicalToken = token.Token
|
||||||
|
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
|
||||||
|
request.UniversalAuthAccessToken = token.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, err := util.GetAllEnvironmentVariables(request, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "To fetch all secrets")
|
util.HandleError(err, "To fetch all secrets")
|
||||||
}
|
}
|
||||||
@@ -686,19 +760,23 @@ func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]mod
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
secretsGenerateExampleEnvCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
|
secretsGenerateExampleEnvCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
|
||||||
|
secretsGenerateExampleEnvCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity")
|
||||||
secretsGenerateExampleEnvCmd.Flags().String("path", "/", "Fetch secrets from within a folder path")
|
secretsGenerateExampleEnvCmd.Flags().String("path", "/", "Fetch secrets from within a folder path")
|
||||||
secretsCmd.AddCommand(secretsGenerateExampleEnvCmd)
|
secretsCmd.AddCommand(secretsGenerateExampleEnvCmd)
|
||||||
|
|
||||||
secretsGetCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
|
secretsGetCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
|
||||||
secretsCmd.AddCommand(secretsGetCmd)
|
secretsGetCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity")
|
||||||
secretsGetCmd.Flags().String("path", "/", "get secrets within a folder path")
|
secretsGetCmd.Flags().String("path", "/", "get secrets within a folder path")
|
||||||
|
secretsGetCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
|
||||||
secretsGetCmd.Flags().Bool("raw-value", false, "Returns only the value of secret, only works with one secret")
|
secretsGetCmd.Flags().Bool("raw-value", false, "Returns only the value of secret, only works with one secret")
|
||||||
secretsGetCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders")
|
secretsGetCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders")
|
||||||
|
secretsCmd.AddCommand(secretsGetCmd)
|
||||||
|
|
||||||
secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
|
secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
|
||||||
secretsCmd.AddCommand(secretsSetCmd)
|
secretsCmd.AddCommand(secretsSetCmd)
|
||||||
secretsSetCmd.Flags().String("path", "/", "set secrets within a folder path")
|
secretsSetCmd.Flags().String("path", "/", "set secrets within a folder path")
|
||||||
|
|
||||||
|
// Only supports logged in users (JWT auth)
|
||||||
secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
||||||
util.RequireLogin()
|
util.RequireLogin()
|
||||||
util.RequireLocalWorkspaceFile()
|
util.RequireLocalWorkspaceFile()
|
||||||
@@ -707,6 +785,8 @@ func init() {
|
|||||||
secretsDeleteCmd.Flags().String("type", "personal", "the type of secret to delete: personal or shared (default: personal)")
|
secretsDeleteCmd.Flags().String("type", "personal", "the type of secret to delete: personal or shared (default: personal)")
|
||||||
secretsDeleteCmd.Flags().String("path", "/", "get secrets within a folder path")
|
secretsDeleteCmd.Flags().String("path", "/", "get secrets within a folder path")
|
||||||
secretsCmd.AddCommand(secretsDeleteCmd)
|
secretsCmd.AddCommand(secretsDeleteCmd)
|
||||||
|
|
||||||
|
// Only supports logged in users (JWT auth)
|
||||||
secretsDeleteCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
secretsDeleteCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
||||||
util.RequireLogin()
|
util.RequireLogin()
|
||||||
util.RequireLocalWorkspaceFile()
|
util.RequireLocalWorkspaceFile()
|
||||||
@@ -718,6 +798,7 @@ func init() {
|
|||||||
// Add getCmd, createCmd and deleteCmd flags here
|
// Add getCmd, createCmd and deleteCmd flags here
|
||||||
getCmd.Flags().StringP("path", "p", "/", "The path from where folders should be fetched from")
|
getCmd.Flags().StringP("path", "p", "/", "The path from where folders should be fetched from")
|
||||||
getCmd.Flags().String("token", "", "Fetch folders using the infisical token")
|
getCmd.Flags().String("token", "", "Fetch folders using the infisical token")
|
||||||
|
getCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity")
|
||||||
folderCmd.AddCommand(getCmd)
|
folderCmd.AddCommand(getCmd)
|
||||||
|
|
||||||
// Add createCmd flags here
|
// Add createCmd flags here
|
||||||
@@ -735,6 +816,7 @@ func init() {
|
|||||||
// ** End of folders sub command
|
// ** End of folders sub command
|
||||||
|
|
||||||
secretsCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
|
secretsCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
|
||||||
|
secretsCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity")
|
||||||
secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
|
secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
|
||||||
secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
|
secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
|
||||||
secretsCmd.Flags().Bool("include-imports", true, "Imported linked secrets ")
|
secretsCmd.Flags().Bool("include-imports", true, "Imported linked secrets ")
|
||||||
|
63
cli/packages/cmd/token.go
Normal file
63
cli/packages/cmd/token.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) 2023 Infisical Inc.
|
||||||
|
*/
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Infisical/infisical-merge/packages/util"
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tokenCmd = &cobra.Command{
|
||||||
|
Use: "token",
|
||||||
|
Short: "Manage your access tokens",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Example: "infisical token",
|
||||||
|
Args: cobra.ExactArgs(0),
|
||||||
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
util.RequireLogin()
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenRenewCmd = &cobra.Command{
|
||||||
|
Use: "renew [token]",
|
||||||
|
Short: "Used to renew your universal auth access token",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Example: "infisical token renew <access-token>",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
// args[0] will be the <INSERT_TOKEN> from your command call
|
||||||
|
token := args[0]
|
||||||
|
|
||||||
|
if strings.HasPrefix(token, "st.") {
|
||||||
|
util.PrintErrorMessageAndExit("You are trying to renew a service token. You can only renew universal auth access tokens.")
|
||||||
|
}
|
||||||
|
|
||||||
|
renewedAccessToken, err := util.RenewUniversalAuthAccessToken(token)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to renew token")
|
||||||
|
}
|
||||||
|
|
||||||
|
boldGreen := color.New(color.FgGreen).Add(color.Bold)
|
||||||
|
time.Sleep(time.Second * 1)
|
||||||
|
boldGreen.Printf(">>>> Successfully renewed token!\n\n")
|
||||||
|
boldGreen.Printf("Renewed Access Token:\n%v", renewedAccessToken)
|
||||||
|
|
||||||
|
plainBold := color.New(color.Bold)
|
||||||
|
plainBold.Println("\n\nYou can use the new access token to authenticate through other commands in the CLI.")
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
tokenCmd.AddCommand(tokenRenewCmd)
|
||||||
|
|
||||||
|
rootCmd.AddCommand(tokenCmd)
|
||||||
|
}
|
@@ -59,6 +59,11 @@ type DynamicSecretLease struct {
|
|||||||
Data map[string]interface{} `json:"data"`
|
Data map[string]interface{} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TokenDetails struct {
|
||||||
|
Type string
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
|
||||||
type SingleFolder struct {
|
type SingleFolder struct {
|
||||||
ID string `json:"_id"`
|
ID string `json:"_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -97,10 +102,11 @@ type GetAllSecretsParameters struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GetAllFoldersParameters struct {
|
type GetAllFoldersParameters struct {
|
||||||
WorkspaceId string
|
WorkspaceId string
|
||||||
Environment string
|
Environment string
|
||||||
FoldersPath string
|
FoldersPath string
|
||||||
InfisicalToken string
|
InfisicalToken string
|
||||||
|
UniversalAuthAccessToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateFolderParameters struct {
|
type CreateFolderParameters struct {
|
||||||
@@ -123,3 +129,8 @@ type ExpandSecretsAuthentication struct {
|
|||||||
InfisicalToken string
|
InfisicalToken string
|
||||||
UniversalAuthAccessToken string
|
UniversalAuthAccessToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MachineIdentityCredentials struct {
|
||||||
|
ClientId string
|
||||||
|
ClientSecret string
|
||||||
|
}
|
||||||
|
@@ -1,17 +1,23 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CONFIG_FILE_NAME = "infisical-config.json"
|
CONFIG_FILE_NAME = "infisical-config.json"
|
||||||
CONFIG_FOLDER_NAME = ".infisical"
|
CONFIG_FOLDER_NAME = ".infisical"
|
||||||
INFISICAL_DEFAULT_API_URL = "https://app.infisical.com/api"
|
INFISICAL_DEFAULT_API_URL = "https://app.infisical.com/api"
|
||||||
INFISICAL_DEFAULT_URL = "https://app.infisical.com"
|
INFISICAL_DEFAULT_URL = "https://app.infisical.com"
|
||||||
INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json"
|
INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json"
|
||||||
INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN"
|
INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN"
|
||||||
SECRET_TYPE_PERSONAL = "personal"
|
INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME = "INFISICAL_UNIVERSAL_AUTH_CLIENT_ID"
|
||||||
SECRET_TYPE_SHARED = "shared"
|
INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME = "INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET"
|
||||||
KEYRING_SERVICE_NAME = "infisical"
|
INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN"
|
||||||
PERSONAL_SECRET_TYPE_NAME = "personal"
|
SECRET_TYPE_PERSONAL = "personal"
|
||||||
SHARED_SECRET_TYPE_NAME = "shared"
|
SECRET_TYPE_SHARED = "shared"
|
||||||
|
KEYRING_SERVICE_NAME = "infisical"
|
||||||
|
PERSONAL_SECRET_TYPE_NAME = "personal"
|
||||||
|
SHARED_SECRET_TYPE_NAME = "shared"
|
||||||
|
|
||||||
|
SERVICE_TOKEN_IDENTIFIER = "service-token"
|
||||||
|
UNIVERSAL_AUTH_TOKEN_IDENTIFIER = "universal-auth-token"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@@ -19,7 +19,7 @@ func GetAllFolders(params models.GetAllFoldersParameters) ([]models.SingleFolder
|
|||||||
|
|
||||||
var foldersToReturn []models.SingleFolder
|
var foldersToReturn []models.SingleFolder
|
||||||
var folderErr error
|
var folderErr error
|
||||||
if params.InfisicalToken == "" {
|
if params.InfisicalToken == "" && params.UniversalAuthAccessToken == "" {
|
||||||
|
|
||||||
log.Debug().Msg("GetAllFolders: Trying to fetch folders using logged in details")
|
log.Debug().Msg("GetAllFolders: Trying to fetch folders using logged in details")
|
||||||
|
|
||||||
@@ -44,11 +44,24 @@ func GetAllFolders(params models.GetAllFoldersParameters) ([]models.SingleFolder
|
|||||||
folders, err := GetFoldersViaJTW(loggedInUserDetails.UserCredentials.JTWToken, workspaceFile.WorkspaceId, params.Environment, params.FoldersPath)
|
folders, err := GetFoldersViaJTW(loggedInUserDetails.UserCredentials.JTWToken, workspaceFile.WorkspaceId, params.Environment, params.FoldersPath)
|
||||||
folderErr = err
|
folderErr = err
|
||||||
foldersToReturn = folders
|
foldersToReturn = folders
|
||||||
} else {
|
} else if params.InfisicalToken != "" {
|
||||||
|
log.Debug().Msg("GetAllFolders: Trying to fetch folders using service token")
|
||||||
|
|
||||||
// get folders via service token
|
// get folders via service token
|
||||||
folders, err := GetFoldersViaServiceToken(params.InfisicalToken, params.WorkspaceId, params.Environment, params.FoldersPath)
|
folders, err := GetFoldersViaServiceToken(params.InfisicalToken, params.WorkspaceId, params.Environment, params.FoldersPath)
|
||||||
folderErr = err
|
folderErr = err
|
||||||
foldersToReturn = folders
|
foldersToReturn = folders
|
||||||
|
} else if params.UniversalAuthAccessToken != "" {
|
||||||
|
log.Debug().Msg("GetAllFolders: Trying to fetch folders using universal auth")
|
||||||
|
|
||||||
|
if params.WorkspaceId == "" {
|
||||||
|
PrintErrorMessageAndExit("Project ID is required when using machine identity")
|
||||||
|
}
|
||||||
|
|
||||||
|
// get folders via machine identity
|
||||||
|
folders, err := GetFoldersViaMachineIdentity(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.FoldersPath)
|
||||||
|
folderErr = err
|
||||||
|
foldersToReturn = folders
|
||||||
}
|
}
|
||||||
return foldersToReturn, folderErr
|
return foldersToReturn, folderErr
|
||||||
}
|
}
|
||||||
@@ -132,6 +145,34 @@ func GetFoldersViaServiceToken(fullServiceToken string, workspaceId string, envi
|
|||||||
return folders, nil
|
return folders, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetFoldersViaMachineIdentity(accessToken string, workspaceId string, envSlug string, foldersPath string) ([]models.SingleFolder, error) {
|
||||||
|
httpClient := resty.New()
|
||||||
|
httpClient.SetAuthToken(accessToken).
|
||||||
|
SetHeader("Accept", "application/json")
|
||||||
|
|
||||||
|
getFoldersRequest := api.GetFoldersV1Request{
|
||||||
|
WorkspaceId: workspaceId,
|
||||||
|
Environment: envSlug,
|
||||||
|
FoldersPath: foldersPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
apiResponse, err := api.CallGetFoldersV1(httpClient, getFoldersRequest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var folders []models.SingleFolder
|
||||||
|
|
||||||
|
for _, folder := range apiResponse.Folders {
|
||||||
|
folders = append(folders, models.SingleFolder{
|
||||||
|
Name: folder.Name,
|
||||||
|
ID: folder.ID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return folders, nil
|
||||||
|
}
|
||||||
|
|
||||||
// CreateFolder creates a folder in Infisical
|
// CreateFolder creates a folder in Infisical
|
||||||
func CreateFolder(params models.CreateFolderParameters) (models.SingleFolder, error) {
|
func CreateFolder(params models.CreateFolderParameters) (models.SingleFolder, error) {
|
||||||
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
|
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
|
||||||
|
@@ -9,8 +9,11 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Infisical/infisical-merge/packages/api"
|
||||||
"github.com/Infisical/infisical-merge/packages/models"
|
"github.com/Infisical/infisical-merge/packages/models"
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -64,18 +67,70 @@ func IsSecretTypeValid(s string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetInfisicalServiceToken(cmd *cobra.Command) (serviceToken string, err error) {
|
func GetInfisicalToken(cmd *cobra.Command) (token *models.TokenDetails, err error) {
|
||||||
infisicalToken, err := cmd.Flags().GetString("token")
|
infisicalToken, err := cmd.Flags().GetString("token")
|
||||||
|
|
||||||
if infisicalToken == "" {
|
if err != nil {
|
||||||
infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if infisicalToken == "" { // If no flag is passed, we first check for the universal auth access token env variable.
|
||||||
|
infisicalToken = os.Getenv(INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME)
|
||||||
|
|
||||||
|
if infisicalToken == "" { // If it's still empty after the first env check, we check for the service token env variable.
|
||||||
|
infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if infisicalToken == "" { // If it's empty, we return nothing at all.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(infisicalToken, "st.") {
|
||||||
|
return &models.TokenDetails{
|
||||||
|
Type: SERVICE_TOKEN_IDENTIFIER,
|
||||||
|
Token: infisicalToken,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.TokenDetails{
|
||||||
|
Type: UNIVERSAL_AUTH_TOKEN_IDENTIFIER,
|
||||||
|
Token: infisicalToken,
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func UniversalAuthLogin(clientId string, clientSecret string) (api.UniversalAuthLoginResponse, error) {
|
||||||
|
httpClient := resty.New()
|
||||||
|
httpClient.SetRetryCount(10000).
|
||||||
|
SetRetryMaxWaitTime(20 * time.Second).
|
||||||
|
SetRetryWaitTime(5 * time.Second)
|
||||||
|
|
||||||
|
tokenResponse, err := api.CallUniversalAuthLogin(httpClient, api.UniversalAuthLoginRequest{ClientId: clientId, ClientSecret: clientSecret})
|
||||||
|
if err != nil {
|
||||||
|
return api.UniversalAuthLoginResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenewUniversalAuthAccessToken(accessToken string) (string, error) {
|
||||||
|
|
||||||
|
httpClient := resty.New()
|
||||||
|
httpClient.SetRetryCount(10000).
|
||||||
|
SetRetryMaxWaitTime(20 * time.Second).
|
||||||
|
SetRetryWaitTime(5 * time.Second)
|
||||||
|
|
||||||
|
request := api.UniversalAuthRefreshRequest{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenResponse, err := api.CallUniversalAuthRefreshAccessToken(httpClient, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return infisicalToken, nil
|
return tokenResponse.AccessToken, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if the passed in email already exists in the users slice
|
// Checks if the passed in email already exists in the users slice
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user