Fix merge conflicts
@@ -70,6 +70,7 @@
|
|||||||
"vitest": "^1.2.2"
|
"vitest": "^1.2.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-iam": "^3.525.0",
|
||||||
"@aws-sdk/client-secrets-manager": "^3.504.0",
|
"@aws-sdk/client-secrets-manager": "^3.504.0",
|
||||||
"@casl/ability": "^6.5.0",
|
"@casl/ability": "^6.5.0",
|
||||||
"@fastify/cookie": "^9.3.1",
|
"@fastify/cookie": "^9.3.1",
|
||||||
@@ -106,6 +107,7 @@
|
|||||||
"knex": "^3.0.1",
|
"knex": "^3.0.1",
|
||||||
"libsodium-wrappers": "^0.7.13",
|
"libsodium-wrappers": "^0.7.13",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"ms": "^2.1.3",
|
||||||
"mysql2": "^3.9.1",
|
"mysql2": "^3.9.1",
|
||||||
"nanoid": "^5.0.4",
|
"nanoid": "^5.0.4",
|
||||||
"nodemailer": "^6.9.9",
|
"nodemailer": "^6.9.9",
|
||||||
@@ -115,6 +117,7 @@
|
|||||||
"passport-google-oauth20": "^2.0.0",
|
"passport-google-oauth20": "^2.0.0",
|
||||||
"passport-ldapauth": "^3.0.1",
|
"passport-ldapauth": "^3.0.1",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
|
"pg-query-stream": "^4.5.3",
|
||||||
"picomatch": "^3.0.1",
|
"picomatch": "^3.0.1",
|
||||||
"pino": "^8.16.2",
|
"pino": "^8.16.2",
|
||||||
"posthog-node": "^3.6.2",
|
"posthog-node": "^3.6.2",
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ import {
|
|||||||
TIdentityOrgMemberships,
|
TIdentityOrgMemberships,
|
||||||
TIdentityOrgMembershipsInsert,
|
TIdentityOrgMembershipsInsert,
|
||||||
TIdentityOrgMembershipsUpdate,
|
TIdentityOrgMembershipsUpdate,
|
||||||
|
TIdentityProjectMembershipRole,
|
||||||
|
TIdentityProjectMembershipRoleInsert,
|
||||||
|
TIdentityProjectMembershipRoleUpdate,
|
||||||
TIdentityProjectMemberships,
|
TIdentityProjectMemberships,
|
||||||
TIdentityProjectMembershipsInsert,
|
TIdentityProjectMembershipsInsert,
|
||||||
TIdentityProjectMembershipsUpdate,
|
TIdentityProjectMembershipsUpdate,
|
||||||
@@ -83,6 +86,9 @@ import {
|
|||||||
TProjects,
|
TProjects,
|
||||||
TProjectsInsert,
|
TProjectsInsert,
|
||||||
TProjectsUpdate,
|
TProjectsUpdate,
|
||||||
|
TProjectUserMembershipRoles,
|
||||||
|
TProjectUserMembershipRolesInsert,
|
||||||
|
TProjectUserMembershipRolesUpdate,
|
||||||
TSamlConfigs,
|
TSamlConfigs,
|
||||||
TSamlConfigsInsert,
|
TSamlConfigsInsert,
|
||||||
TSamlConfigsUpdate,
|
TSamlConfigsUpdate,
|
||||||
@@ -221,6 +227,11 @@ declare module "knex/types/tables" {
|
|||||||
TProjectEnvironmentsUpdate
|
TProjectEnvironmentsUpdate
|
||||||
>;
|
>;
|
||||||
[TableName.ProjectBot]: Knex.CompositeTableType<TProjectBots, TProjectBotsInsert, TProjectBotsUpdate>;
|
[TableName.ProjectBot]: Knex.CompositeTableType<TProjectBots, TProjectBotsInsert, TProjectBotsUpdate>;
|
||||||
|
[TableName.ProjectUserMembershipRole]: Knex.CompositeTableType<
|
||||||
|
TProjectUserMembershipRoles,
|
||||||
|
TProjectUserMembershipRolesInsert,
|
||||||
|
TProjectUserMembershipRolesUpdate
|
||||||
|
>;
|
||||||
[TableName.ProjectRoles]: Knex.CompositeTableType<TProjectRoles, TProjectRolesInsert, TProjectRolesUpdate>;
|
[TableName.ProjectRoles]: Knex.CompositeTableType<TProjectRoles, TProjectRolesInsert, TProjectRolesUpdate>;
|
||||||
[TableName.ProjectKeys]: Knex.CompositeTableType<TProjectKeys, TProjectKeysInsert, TProjectKeysUpdate>;
|
[TableName.ProjectKeys]: Knex.CompositeTableType<TProjectKeys, TProjectKeysInsert, TProjectKeysUpdate>;
|
||||||
[TableName.Secret]: Knex.CompositeTableType<TSecrets, TSecretsInsert, TSecretsUpdate>;
|
[TableName.Secret]: Knex.CompositeTableType<TSecrets, TSecretsInsert, TSecretsUpdate>;
|
||||||
@@ -272,6 +283,11 @@ declare module "knex/types/tables" {
|
|||||||
TIdentityProjectMembershipsInsert,
|
TIdentityProjectMembershipsInsert,
|
||||||
TIdentityProjectMembershipsUpdate
|
TIdentityProjectMembershipsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.IdentityProjectMembershipRole]: Knex.CompositeTableType<
|
||||||
|
TIdentityProjectMembershipRole,
|
||||||
|
TIdentityProjectMembershipRoleInsert,
|
||||||
|
TIdentityProjectMembershipRoleUpdate
|
||||||
|
>;
|
||||||
[TableName.ScimToken]: Knex.CompositeTableType<TScimTokens, TScimTokensInsert, TScimTokensUpdate>;
|
[TableName.ScimToken]: Knex.CompositeTableType<TScimTokens, TScimTokensInsert, TScimTokensUpdate>;
|
||||||
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
|
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
|
||||||
TSecretApprovalPolicies,
|
TSecretApprovalPolicies,
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const doesTableExist = await knex.schema.hasTable(TableName.ProjectUserMembershipRole);
|
||||||
|
if (!doesTableExist) {
|
||||||
|
await knex.schema.createTable(TableName.ProjectUserMembershipRole, (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.ProjectMembership).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.ProjectUserMembershipRole);
|
||||||
|
|
||||||
|
const projectMemberships = await knex(TableName.ProjectMembership).select(
|
||||||
|
"id",
|
||||||
|
"role",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt",
|
||||||
|
knex.ref("roleId").withSchema(TableName.ProjectMembership).as("customRoleId")
|
||||||
|
);
|
||||||
|
if (projectMemberships.length)
|
||||||
|
await knex.batchInsert(
|
||||||
|
TableName.ProjectUserMembershipRole,
|
||||||
|
projectMemberships.map((data) => ({ ...data, projectMembershipId: data.id }))
|
||||||
|
);
|
||||||
|
// will be dropped later
|
||||||
|
// await knex.schema.alterTable(TableName.ProjectMembership, (t) => {
|
||||||
|
// t.dropColumn("roleId");
|
||||||
|
// t.dropColumn("role");
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.ProjectUserMembershipRole);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.ProjectUserMembershipRole);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const doesTableExist = await knex.schema.hasTable(TableName.IdentityProjectMembershipRole);
|
||||||
|
if (!doesTableExist) {
|
||||||
|
await knex.schema.createTable(TableName.IdentityProjectMembershipRole, (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.IdentityProjectMembership)
|
||||||
|
.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.IdentityProjectMembershipRole);
|
||||||
|
|
||||||
|
const identityMemberships = await knex(TableName.IdentityProjectMembership).select(
|
||||||
|
"id",
|
||||||
|
"role",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt",
|
||||||
|
knex.ref("roleId").withSchema(TableName.IdentityProjectMembership).as("customRoleId")
|
||||||
|
);
|
||||||
|
if (identityMemberships.length)
|
||||||
|
await knex.batchInsert(
|
||||||
|
TableName.IdentityProjectMembershipRole,
|
||||||
|
identityMemberships.map((data) => ({ ...data, projectMembershipId: data.id }))
|
||||||
|
);
|
||||||
|
// await knex.schema.alterTable(TableName.IdentityProjectMembership, (t) => {
|
||||||
|
// t.dropColumn("roleId");
|
||||||
|
// t.dropColumn("role");
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.IdentityProjectMembershipRole);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.IdentityProjectMembershipRole);
|
||||||
|
}
|
||||||
@@ -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 IdentityProjectMembershipRoleSchema = 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 TIdentityProjectMembershipRole = z.infer<typeof IdentityProjectMembershipRoleSchema>;
|
||||||
|
export type TIdentityProjectMembershipRoleInsert = Omit<
|
||||||
|
z.input<typeof IdentityProjectMembershipRoleSchema>,
|
||||||
|
TImmutableDBKeys
|
||||||
|
>;
|
||||||
|
export type TIdentityProjectMembershipRoleUpdate = Partial<
|
||||||
|
Omit<z.input<typeof IdentityProjectMembershipRoleSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
||||||
@@ -8,6 +8,7 @@ export * from "./git-app-org";
|
|||||||
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";
|
||||||
|
export * from "./identity-project-membership-role";
|
||||||
export * from "./identity-project-memberships";
|
export * from "./identity-project-memberships";
|
||||||
export * from "./identity-ua-client-secrets";
|
export * from "./identity-ua-client-secrets";
|
||||||
export * from "./identity-universal-auths";
|
export * from "./identity-universal-auths";
|
||||||
@@ -25,6 +26,7 @@ export * from "./project-environments";
|
|||||||
export * from "./project-keys";
|
export * from "./project-keys";
|
||||||
export * from "./project-memberships";
|
export * from "./project-memberships";
|
||||||
export * from "./project-roles";
|
export * from "./project-roles";
|
||||||
|
export * from "./project-user-membership-roles";
|
||||||
export * from "./projects";
|
export * from "./projects";
|
||||||
export * from "./saml-configs";
|
export * from "./saml-configs";
|
||||||
export * from "./scim-tokens";
|
export * from "./scim-tokens";
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export enum TableName {
|
|||||||
Environment = "project_environments",
|
Environment = "project_environments",
|
||||||
ProjectMembership = "project_memberships",
|
ProjectMembership = "project_memberships",
|
||||||
ProjectRoles = "project_roles",
|
ProjectRoles = "project_roles",
|
||||||
|
ProjectUserMembershipRole = "project_user_membership_roles",
|
||||||
ProjectKeys = "project_keys",
|
ProjectKeys = "project_keys",
|
||||||
Secret = "secrets",
|
Secret = "secrets",
|
||||||
SecretBlindIndex = "secret_blind_indexes",
|
SecretBlindIndex = "secret_blind_indexes",
|
||||||
@@ -41,6 +42,7 @@ export enum TableName {
|
|||||||
IdentityUaClientSecret = "identity_ua_client_secrets",
|
IdentityUaClientSecret = "identity_ua_client_secrets",
|
||||||
IdentityOrgMembership = "identity_org_memberships",
|
IdentityOrgMembership = "identity_org_memberships",
|
||||||
IdentityProjectMembership = "identity_project_memberships",
|
IdentityProjectMembership = "identity_project_memberships",
|
||||||
|
IdentityProjectMembershipRole = "identity_project_membership_role",
|
||||||
ScimToken = "scim_tokens",
|
ScimToken = "scim_tokens",
|
||||||
SecretApprovalPolicy = "secret_approval_policies",
|
SecretApprovalPolicy = "secret_approval_policies",
|
||||||
SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
|
SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
|
||||||
|
|||||||
@@ -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 ProjectUserMembershipRolesSchema = 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 TProjectUserMembershipRoles = z.infer<typeof ProjectUserMembershipRolesSchema>;
|
||||||
|
export type TProjectUserMembershipRolesInsert = Omit<
|
||||||
|
z.input<typeof ProjectUserMembershipRolesSchema>,
|
||||||
|
TImmutableDBKeys
|
||||||
|
>;
|
||||||
|
export type TProjectUserMembershipRolesUpdate = Partial<
|
||||||
|
Omit<z.input<typeof ProjectUserMembershipRolesSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
||||||
@@ -4,7 +4,7 @@ import { Knex } from "knex";
|
|||||||
|
|
||||||
import { encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
import { encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||||
|
|
||||||
import { OrgMembershipRole, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
|
import { ProjectMembershipRole, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
|
||||||
import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data";
|
import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data";
|
||||||
|
|
||||||
export const DEFAULT_PROJECT_ENVS = [
|
export const DEFAULT_PROJECT_ENVS = [
|
||||||
@@ -30,10 +30,16 @@ export async function seed(knex: Knex): Promise<void> {
|
|||||||
})
|
})
|
||||||
.returning("*");
|
.returning("*");
|
||||||
|
|
||||||
await knex(TableName.ProjectMembership).insert({
|
const projectMembership = await knex(TableName.ProjectMembership)
|
||||||
projectId: project.id,
|
.insert({
|
||||||
role: OrgMembershipRole.Admin,
|
projectId: project.id,
|
||||||
userId: seedData1.id
|
userId: seedData1.id,
|
||||||
|
role: ProjectMembershipRole.Admin
|
||||||
|
})
|
||||||
|
.returning("*");
|
||||||
|
await knex(TableName.ProjectUserMembershipRole).insert({
|
||||||
|
role: ProjectMembershipRole.Admin,
|
||||||
|
projectMembershipId: projectMembership[0].id
|
||||||
});
|
});
|
||||||
|
|
||||||
const user = await knex(TableName.UserEncryptionKey).where({ userId: seedData1.id }).first();
|
const user = await knex(TableName.UserEncryptionKey).where({ userId: seedData1.id }).first();
|
||||||
|
|||||||
@@ -75,9 +75,16 @@ export async function seed(knex: Knex): Promise<void> {
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await knex(TableName.IdentityProjectMembership).insert({
|
const identityProjectMembership = await knex(TableName.IdentityProjectMembership)
|
||||||
identityId: seedData1.machineIdentity.id,
|
.insert({
|
||||||
|
identityId: seedData1.machineIdentity.id,
|
||||||
|
projectId: seedData1.project.id,
|
||||||
|
role: ProjectMembershipRole.Admin
|
||||||
|
})
|
||||||
|
.returning("*");
|
||||||
|
|
||||||
|
await knex(TableName.IdentityProjectMembershipRole).insert({
|
||||||
role: ProjectMembershipRole.Admin,
|
role: ProjectMembershipRole.Admin,
|
||||||
projectId: seedData1.project.id
|
projectMembershipId: identityProjectMembership[0].id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import slugify from "@sindresorhus/slugify";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
|
import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
|
||||||
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";
|
||||||
|
|
||||||
@@ -13,7 +14,17 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
organizationId: z.string().trim()
|
organizationId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
slug: z.string().trim(),
|
slug: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.trim()
|
||||||
|
.refine(
|
||||||
|
(val) => Object.keys(OrgMembershipRole).includes(val),
|
||||||
|
"Please choose a different slug, the slug you have entered is reserved"
|
||||||
|
)
|
||||||
|
.refine((v) => slugify(v) === v, {
|
||||||
|
message: "Slug must be a valid"
|
||||||
|
}),
|
||||||
name: z.string().trim(),
|
name: z.string().trim(),
|
||||||
description: z.string().trim().optional(),
|
description: z.string().trim().optional(),
|
||||||
permissions: z.any().array()
|
permissions: z.any().array()
|
||||||
@@ -45,7 +56,17 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
roleId: z.string().trim()
|
roleId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
slug: z.string().trim().optional(),
|
slug: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.optional()
|
||||||
|
.refine(
|
||||||
|
(val) => typeof val === "undefined" || Object.keys(OrgMembershipRole).includes(val),
|
||||||
|
"Please choose a different slug, the slug you have entered is reserved."
|
||||||
|
)
|
||||||
|
.refine((val) => typeof val === "undefined" || slugify(val) === val, {
|
||||||
|
message: "Slug must be a valid"
|
||||||
|
}),
|
||||||
name: z.string().trim().optional(),
|
name: z.string().trim().optional(),
|
||||||
description: z.string().trim().optional(),
|
description: z.string().trim().optional(),
|
||||||
permissions: z.any().array()
|
permissions: z.any().array()
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName } from "@app/db/schemas";
|
import { IdentityProjectMembershipRoleSchema, ProjectUserMembershipRolesSchema, TableName } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { selectAllTableCols } from "@app/lib/knex";
|
import { selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TPermissionDALFactory = ReturnType<typeof permissionDALFactory>;
|
export type TPermissionDALFactory = ReturnType<typeof permissionDALFactory>;
|
||||||
|
|
||||||
@@ -43,21 +45,72 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const getProjectPermission = async (userId: string, projectId: string) => {
|
const getProjectPermission = async (userId: string, projectId: string) => {
|
||||||
try {
|
try {
|
||||||
const membership = await db(TableName.ProjectMembership)
|
const docs = await db(TableName.ProjectMembership)
|
||||||
.leftJoin(TableName.ProjectRoles, `${TableName.ProjectMembership}.roleId`, `${TableName.ProjectRoles}.id`)
|
.join(
|
||||||
|
TableName.ProjectUserMembershipRole,
|
||||||
|
`${TableName.ProjectUserMembershipRole}.projectMembershipId`,
|
||||||
|
`${TableName.ProjectMembership}.id`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ProjectRoles,
|
||||||
|
`${TableName.ProjectUserMembershipRole}.customRoleId`,
|
||||||
|
`${TableName.ProjectRoles}.id`
|
||||||
|
)
|
||||||
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
|
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||||
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
||||||
.where("userId", userId)
|
.where("userId", userId)
|
||||||
.where(`${TableName.ProjectMembership}.projectId`, projectId)
|
.where(`${TableName.ProjectMembership}.projectId`, projectId)
|
||||||
.select(selectAllTableCols(TableName.ProjectMembership))
|
.select(selectAllTableCols(TableName.ProjectUserMembershipRole))
|
||||||
.select(
|
.select(
|
||||||
|
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("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
|
||||||
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")
|
||||||
)
|
)
|
||||||
.select("permissions")
|
.select("permissions");
|
||||||
.first();
|
|
||||||
|
|
||||||
return membership;
|
const permission = sqlNestRelationships({
|
||||||
|
data: docs,
|
||||||
|
key: "membershipId",
|
||||||
|
parentMapper: ({
|
||||||
|
orgId,
|
||||||
|
orgAuthEnforced,
|
||||||
|
membershipId,
|
||||||
|
membershipCreatedAt,
|
||||||
|
membershipUpdatedAt,
|
||||||
|
oldRoleField
|
||||||
|
}) => ({
|
||||||
|
orgId,
|
||||||
|
orgAuthEnforced,
|
||||||
|
userId,
|
||||||
|
role: oldRoleField,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
// when introducting cron mode change it here
|
||||||
|
const activeRoles = permission?.[0]?.roles.filter(
|
||||||
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
|
);
|
||||||
|
return permission?.[0] ? { ...permission[0], roles: activeRoles } : undefined;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "GetProjectPermission" });
|
throw new DatabaseError({ error, name: "GetProjectPermission" });
|
||||||
}
|
}
|
||||||
@@ -65,18 +118,62 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const getProjectIdentityPermission = async (identityId: string, projectId: string) => {
|
const getProjectIdentityPermission = async (identityId: string, projectId: string) => {
|
||||||
try {
|
try {
|
||||||
const membership = await db(TableName.IdentityProjectMembership)
|
const docs = await db(TableName.IdentityProjectMembership)
|
||||||
|
.join(
|
||||||
|
TableName.IdentityProjectMembershipRole,
|
||||||
|
`${TableName.IdentityProjectMembershipRole}.projectMembershipId`,
|
||||||
|
`${TableName.IdentityProjectMembership}.id`
|
||||||
|
)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.ProjectRoles,
|
TableName.ProjectRoles,
|
||||||
`${TableName.IdentityProjectMembership}.roleId`,
|
`${TableName.IdentityProjectMembershipRole}.customRoleId`,
|
||||||
`${TableName.ProjectRoles}.id`
|
`${TableName.ProjectRoles}.id`
|
||||||
)
|
)
|
||||||
.where("identityId", identityId)
|
.where("identityId", identityId)
|
||||||
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
|
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
|
||||||
.select(selectAllTableCols(TableName.IdentityProjectMembership))
|
.select(selectAllTableCols(TableName.IdentityProjectMembershipRole))
|
||||||
.select("permissions")
|
.select(
|
||||||
.first();
|
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
|
||||||
return membership;
|
db.ref("role").withSchema(TableName.IdentityProjectMembership).as("oldRoleField"),
|
||||||
|
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
|
||||||
|
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
|
||||||
|
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug")
|
||||||
|
)
|
||||||
|
.select("permissions");
|
||||||
|
|
||||||
|
const permission = sqlNestRelationships({
|
||||||
|
data: docs,
|
||||||
|
key: "membershipId",
|
||||||
|
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, oldRoleField }) => ({
|
||||||
|
id: membershipId,
|
||||||
|
identityId,
|
||||||
|
projectId,
|
||||||
|
role: oldRoleField,
|
||||||
|
createdAt: membershipCreatedAt,
|
||||||
|
updatedAt: membershipUpdatedAt,
|
||||||
|
// just a prefilled value
|
||||||
|
orgAuthEnforced: false,
|
||||||
|
orgId: ""
|
||||||
|
}),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "id",
|
||||||
|
label: "roles" as const,
|
||||||
|
mapper: (data) =>
|
||||||
|
IdentityProjectMembershipRoleSchema.extend({
|
||||||
|
permissions: z.unknown(),
|
||||||
|
customRoleSlug: z.string().optional().nullable()
|
||||||
|
}).parse(data)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// when introducting cron mode change it here
|
||||||
|
const activeRoles = permission?.[0]?.roles.filter(
|
||||||
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
|
);
|
||||||
|
return permission?.[0] ? { ...permission[0], roles: activeRoles } : undefined;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "GetProjectIdentityPermission" });
|
throw new DatabaseError({ error, name: "GetProjectIdentityPermission" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok
|
|||||||
|
|
||||||
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
|
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
|
||||||
import { TPermissionDALFactory } from "./permission-dal";
|
import { TPermissionDALFactory } from "./permission-dal";
|
||||||
|
import { TBuildProjectPermissionDTO } from "./permission-types";
|
||||||
import {
|
import {
|
||||||
buildServiceTokenProjectPermission,
|
buildServiceTokenProjectPermission,
|
||||||
projectAdminPermissions,
|
projectAdminPermissions,
|
||||||
@@ -64,31 +65,35 @@ export const permissionServiceFactory = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildProjectPermission = (role: string, permission?: unknown) => {
|
const buildProjectPermission = (projectUserRoles: TBuildProjectPermissionDTO) => {
|
||||||
switch (role) {
|
const rules = projectUserRoles
|
||||||
case ProjectMembershipRole.Admin:
|
.map(({ role, permissions }) => {
|
||||||
return projectAdminPermissions;
|
switch (role) {
|
||||||
case ProjectMembershipRole.Member:
|
case ProjectMembershipRole.Admin:
|
||||||
return projectMemberPermissions;
|
return projectAdminPermissions;
|
||||||
case ProjectMembershipRole.Viewer:
|
case ProjectMembershipRole.Member:
|
||||||
return projectViewerPermission;
|
return projectMemberPermissions;
|
||||||
case ProjectMembershipRole.NoAccess:
|
case ProjectMembershipRole.Viewer:
|
||||||
return projectNoAccessPermissions;
|
return projectViewerPermission;
|
||||||
case ProjectMembershipRole.Custom:
|
case ProjectMembershipRole.NoAccess:
|
||||||
return createMongoAbility<ProjectPermissionSet>(
|
return projectNoAccessPermissions;
|
||||||
unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(
|
case ProjectMembershipRole.Custom: {
|
||||||
permission as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[]
|
return unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(
|
||||||
),
|
permissions as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[]
|
||||||
{
|
);
|
||||||
conditionsMatcher
|
|
||||||
}
|
}
|
||||||
);
|
default:
|
||||||
default:
|
throw new BadRequestError({
|
||||||
throw new BadRequestError({
|
name: "ProjectRoleInvalid",
|
||||||
name: "ProjectRoleInvalid",
|
message: "Project role not found"
|
||||||
message: "Project role not found"
|
});
|
||||||
});
|
}
|
||||||
}
|
})
|
||||||
|
.reduce((curr, prev) => prev.concat(curr), []);
|
||||||
|
|
||||||
|
return createMongoAbility<ProjectPermissionSet>(rules, {
|
||||||
|
conditionsMatcher
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -145,33 +150,56 @@ export const permissionServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// user permission for a project in an organization
|
// user permission for a project in an organization
|
||||||
const getUserProjectPermission = async (userId: string, projectId: string, userOrgId?: string) => {
|
const getUserProjectPermission = async (
|
||||||
const membership = await permissionDAL.getProjectPermission(userId, projectId);
|
userId: string,
|
||||||
if (!membership) throw new UnauthorizedError({ name: "User not in project" });
|
projectId: string,
|
||||||
if (membership.role === ProjectMembershipRole.Custom && !membership.permissions) {
|
userOrgId?: string
|
||||||
|
): Promise<TProjectPermissionRT<ActorType.USER>> => {
|
||||||
|
const userProjectPermission = await permissionDAL.getProjectPermission(userId, projectId);
|
||||||
|
if (!userProjectPermission) throw new UnauthorizedError({ name: "User not in project" });
|
||||||
|
|
||||||
|
if (
|
||||||
|
userProjectPermission.roles.some(({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions)
|
||||||
|
) {
|
||||||
throw new BadRequestError({ name: "Custom permission not found" });
|
throw new BadRequestError({ name: "Custom permission not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (membership.orgAuthEnforced && membership.orgId !== userOrgId) {
|
if (userProjectPermission.orgAuthEnforced && userProjectPermission.orgId !== userOrgId) {
|
||||||
throw new BadRequestError({ name: "Cannot access org-scoped resource" });
|
throw new BadRequestError({ name: "Cannot access org-scoped resource" });
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
permission: buildProjectPermission(membership.role, membership.permissions),
|
permission: buildProjectPermission(userProjectPermission.roles),
|
||||||
membership
|
membership: userProjectPermission,
|
||||||
|
hasRole: (role: string) =>
|
||||||
|
userProjectPermission.roles.findIndex(
|
||||||
|
({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug
|
||||||
|
) !== -1
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getIdentityProjectPermission = async (identityId: string, projectId: string) => {
|
const getIdentityProjectPermission = async (
|
||||||
const membership = await permissionDAL.getProjectIdentityPermission(identityId, projectId);
|
identityId: string,
|
||||||
if (!membership) throw new UnauthorizedError({ name: "Identity not in project" });
|
projectId: string
|
||||||
if (membership.role === ProjectMembershipRole.Custom && !membership.permissions) {
|
): Promise<TProjectPermissionRT<ActorType.IDENTITY>> => {
|
||||||
|
const identityProjectPermission = await permissionDAL.getProjectIdentityPermission(identityId, projectId);
|
||||||
|
if (!identityProjectPermission) throw new UnauthorizedError({ name: "Identity not in project" });
|
||||||
|
|
||||||
|
if (
|
||||||
|
identityProjectPermission.roles.some(
|
||||||
|
({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions
|
||||||
|
)
|
||||||
|
) {
|
||||||
throw new BadRequestError({ name: "Custom permission not found" });
|
throw new BadRequestError({ name: "Custom permission not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
permission: buildProjectPermission(membership.role, membership.permissions),
|
permission: buildProjectPermission(identityProjectPermission.roles),
|
||||||
membership
|
membership: identityProjectPermission,
|
||||||
|
hasRole: (role: string) =>
|
||||||
|
identityProjectPermission.roles.findIndex(
|
||||||
|
({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug
|
||||||
|
) !== -1
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -191,14 +219,19 @@ export const permissionServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
type TProjectPermissionRT<T extends ActorType> = T extends ActorType.SERVICE
|
type TProjectPermissionRT<T extends ActorType> = T extends ActorType.SERVICE
|
||||||
? { permission: MongoAbility<ProjectPermissionSet, MongoQuery>; membership: undefined }
|
? {
|
||||||
|
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
|
||||||
|
membership: undefined;
|
||||||
|
hasRole: (arg: string) => boolean;
|
||||||
|
} // service token doesn't have both membership and roles
|
||||||
: {
|
: {
|
||||||
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
|
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
|
||||||
membership: (T extends ActorType.USER ? TProjectMemberships : TIdentityProjectMemberships) & {
|
membership: (T extends ActorType.USER ? TProjectMemberships : TIdentityProjectMemberships) & {
|
||||||
orgAuthEnforced: boolean;
|
orgAuthEnforced: boolean | null | undefined;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
permissions?: unknown;
|
roles: Array<{ role: string }>;
|
||||||
};
|
};
|
||||||
|
hasRole: (role: string) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProjectPermission = async <T extends ActorType>(
|
const getProjectPermission = async <T extends ActorType>(
|
||||||
@@ -228,11 +261,13 @@ export const permissionServiceFactory = ({
|
|||||||
const projectRole = await projectRoleDAL.findOne({ slug: role, projectId });
|
const projectRole = await projectRoleDAL.findOne({ slug: role, projectId });
|
||||||
if (!projectRole) throw new BadRequestError({ message: "Role not found" });
|
if (!projectRole) throw new BadRequestError({ message: "Role not found" });
|
||||||
return {
|
return {
|
||||||
permission: buildProjectPermission(ProjectMembershipRole.Custom, projectRole.permissions),
|
permission: buildProjectPermission([
|
||||||
|
{ role: ProjectMembershipRole.Custom, permissions: projectRole.permissions }
|
||||||
|
]),
|
||||||
role: projectRole
|
role: projectRole
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { permission: buildProjectPermission(role, []) };
|
return { permission: buildProjectPermission([{ role, permissions: [] }]) };
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export type TBuildProjectPermissionDTO = {
|
||||||
|
permissions?: unknown;
|
||||||
|
role: string;
|
||||||
|
}[];
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback];
|
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback];
|
||||||
|
|
||||||
const buildAdminPermission = () => {
|
const buildAdminPermissionRules = () => {
|
||||||
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets);
|
can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets);
|
||||||
@@ -135,13 +135,13 @@ const buildAdminPermission = () => {
|
|||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
||||||
|
|
||||||
return build({ conditionsMatcher });
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const projectAdminPermissions = buildAdminPermission();
|
export const projectAdminPermissions = buildAdminPermissionRules();
|
||||||
|
|
||||||
const buildMemberPermission = () => {
|
const buildMemberPermissionRules = () => {
|
||||||
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets);
|
can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets);
|
||||||
@@ -196,13 +196,13 @@ const buildMemberPermission = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||||
|
|
||||||
return build({ conditionsMatcher });
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const projectMemberPermissions = buildMemberPermission();
|
export const projectMemberPermissions = buildMemberPermissionRules();
|
||||||
|
|
||||||
const buildViewerPermission = () => {
|
const buildViewerPermissionRules = () => {
|
||||||
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||||
@@ -220,14 +220,14 @@ const buildViewerPermission = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||||
|
|
||||||
return build({ conditionsMatcher });
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const projectViewerPermission = buildViewerPermission();
|
export const projectViewerPermission = buildViewerPermissionRules();
|
||||||
|
|
||||||
const buildNoAccessProjectPermission = () => {
|
const buildNoAccessProjectPermission = () => {
|
||||||
const { build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
const { rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||||
return build({ conditionsMatcher });
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buildServiceTokenProjectPermission = (
|
export const buildServiceTokenProjectPermission = (
|
||||||
|
|||||||
@@ -129,14 +129,14 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||||
|
|
||||||
const { policy } = secretApprovalRequest;
|
const { policy } = secretApprovalRequest;
|
||||||
const { membership } = await permissionService.getProjectPermission(
|
const { membership, hasRole } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
secretApprovalRequest.projectId,
|
secretApprovalRequest.projectId,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
membership.role !== ProjectMembershipRole.Admin &&
|
!hasRole(ProjectMembershipRole.Admin) &&
|
||||||
secretApprovalRequest.committerId !== membership.id &&
|
secretApprovalRequest.committerId !== membership.id &&
|
||||||
!policy.approvers.find((approverId) => approverId === membership.id)
|
!policy.approvers.find((approverId) => approverId === membership.id)
|
||||||
) {
|
) {
|
||||||
@@ -156,14 +156,14 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||||
|
|
||||||
const { policy } = secretApprovalRequest;
|
const { policy } = secretApprovalRequest;
|
||||||
const { membership } = await permissionService.getProjectPermission(
|
const { membership, hasRole } = await permissionService.getProjectPermission(
|
||||||
ActorType.USER,
|
ActorType.USER,
|
||||||
actorId,
|
actorId,
|
||||||
secretApprovalRequest.projectId,
|
secretApprovalRequest.projectId,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
membership.role !== ProjectMembershipRole.Admin &&
|
!hasRole(ProjectMembershipRole.Admin) &&
|
||||||
secretApprovalRequest.committerId !== membership.id &&
|
secretApprovalRequest.committerId !== membership.id &&
|
||||||
!policy.approvers.find((approverId) => approverId === membership.id)
|
!policy.approvers.find((approverId) => approverId === membership.id)
|
||||||
) {
|
) {
|
||||||
@@ -198,14 +198,14 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||||
|
|
||||||
const { policy } = secretApprovalRequest;
|
const { policy } = secretApprovalRequest;
|
||||||
const { membership } = await permissionService.getProjectPermission(
|
const { membership, hasRole } = await permissionService.getProjectPermission(
|
||||||
ActorType.USER,
|
ActorType.USER,
|
||||||
actorId,
|
actorId,
|
||||||
secretApprovalRequest.projectId,
|
secretApprovalRequest.projectId,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
membership.role !== ProjectMembershipRole.Admin &&
|
!hasRole(ProjectMembershipRole.Admin) &&
|
||||||
secretApprovalRequest.committerId !== membership.id &&
|
secretApprovalRequest.committerId !== membership.id &&
|
||||||
!policy.approvers.find((approverId) => approverId === membership.id)
|
!policy.approvers.find((approverId) => approverId === membership.id)
|
||||||
) {
|
) {
|
||||||
@@ -236,9 +236,14 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||||
|
|
||||||
const { policy, folderId, projectId } = secretApprovalRequest;
|
const { policy, folderId, projectId } = secretApprovalRequest;
|
||||||
const { membership } = await permissionService.getProjectPermission(ActorType.USER, actorId, projectId, actorOrgId);
|
const { membership, hasRole } = await permissionService.getProjectPermission(
|
||||||
|
ActorType.USER,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
membership.role !== ProjectMembershipRole.Admin &&
|
!hasRole(ProjectMembershipRole.Admin) &&
|
||||||
secretApprovalRequest.committerId !== membership.id &&
|
secretApprovalRequest.committerId !== membership.id &&
|
||||||
!policy.approvers.find((approverId) => approverId === membership.id)
|
!policy.approvers.find((approverId) => approverId === membership.id)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
import {
|
||||||
|
CreateAccessKeyCommand,
|
||||||
|
DeleteAccessKeyCommand,
|
||||||
|
GetAccessKeyLastUsedCommand,
|
||||||
|
IAMClient
|
||||||
|
} from "@aws-sdk/client-iam";
|
||||||
|
|
||||||
import { SecretKeyEncoding, SecretType } from "@app/db/schemas";
|
import { SecretKeyEncoding, SecretType } from "@app/db/schemas";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import {
|
import {
|
||||||
@@ -18,7 +25,12 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
|||||||
|
|
||||||
import { TSecretRotationDALFactory } from "../secret-rotation-dal";
|
import { TSecretRotationDALFactory } from "../secret-rotation-dal";
|
||||||
import { rotationTemplates } from "../templates";
|
import { rotationTemplates } from "../templates";
|
||||||
import { TDbProviderClients, TProviderFunctionTypes, TSecretRotationProviderTemplate } from "../templates/types";
|
import {
|
||||||
|
TAwsProviderSystems,
|
||||||
|
TDbProviderClients,
|
||||||
|
TProviderFunctionTypes,
|
||||||
|
TSecretRotationProviderTemplate
|
||||||
|
} from "../templates/types";
|
||||||
import {
|
import {
|
||||||
getDbSetQuery,
|
getDbSetQuery,
|
||||||
secretRotationDbFn,
|
secretRotationDbFn,
|
||||||
@@ -127,7 +139,10 @@ export const secretRotationQueueFactory = ({
|
|||||||
internal: {}
|
internal: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
// when its a database we keep cycling the variables accordingly
|
/* Rotation Function For Database
|
||||||
|
* A database like sql cannot have multiple password for a user
|
||||||
|
* thus we ask users to create two users with required permission and then we keep cycling between these two db users
|
||||||
|
*/
|
||||||
if (provider.template.type === TProviderFunctionTypes.DB) {
|
if (provider.template.type === TProviderFunctionTypes.DB) {
|
||||||
const lastCred = variables.creds.at(-1);
|
const lastCred = variables.creds.at(-1);
|
||||||
if (lastCred && variables.creds.length === 1) {
|
if (lastCred && variables.creds.length === 1) {
|
||||||
@@ -170,6 +185,65 @@ export const secretRotationQueueFactory = ({
|
|||||||
if (variables.creds.length === 2) variables.creds.pop();
|
if (variables.creds.length === 2) variables.creds.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Rotation Function For AWS Services
|
||||||
|
* Due to complexity in AWS Authorization hashing signature process we keep it as seperate entity instead of http template mode
|
||||||
|
* We first delete old key before creating a new one because aws iam has a quota limit of 2 keys
|
||||||
|
* */
|
||||||
|
if (provider.template.type === TProviderFunctionTypes.AWS) {
|
||||||
|
if (provider.template.client === TAwsProviderSystems.IAM) {
|
||||||
|
const client = new IAMClient({
|
||||||
|
region: newCredential.inputs.manager_user_aws_region as string,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: newCredential.inputs.manager_user_access_key as string,
|
||||||
|
secretAccessKey: newCredential.inputs.manager_user_secret_key as string
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const iamUserName = newCredential.inputs.iam_username as string;
|
||||||
|
|
||||||
|
if (variables.creds.length === 2) {
|
||||||
|
const deleteCycleCredential = variables.creds.pop();
|
||||||
|
if (deleteCycleCredential) {
|
||||||
|
const deletedIamAccessKey = await client.send(
|
||||||
|
new DeleteAccessKeyCommand({
|
||||||
|
UserName: iamUserName,
|
||||||
|
AccessKeyId: deleteCycleCredential.outputs.iam_user_access_key as string
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!deletedIamAccessKey?.$metadata?.httpStatusCode ||
|
||||||
|
deletedIamAccessKey?.$metadata?.httpStatusCode > 300
|
||||||
|
) {
|
||||||
|
throw new DisableRotationErrors({
|
||||||
|
message: "Failed to delete aws iam access key. Check managed iam user policy"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newIamAccessKey = await client.send(new CreateAccessKeyCommand({ UserName: iamUserName }));
|
||||||
|
if (!newIamAccessKey.AccessKey)
|
||||||
|
throw new DisableRotationErrors({ message: "Failed to create access key. Check managed iam user policy" });
|
||||||
|
|
||||||
|
// test
|
||||||
|
const testAccessKey = await client.send(
|
||||||
|
new GetAccessKeyLastUsedCommand({ AccessKeyId: newIamAccessKey.AccessKey.AccessKeyId })
|
||||||
|
);
|
||||||
|
if (testAccessKey?.UserName !== iamUserName)
|
||||||
|
throw new DisableRotationErrors({ message: "Failed to create access key. Check managed iam user policy" });
|
||||||
|
|
||||||
|
newCredential.outputs.iam_user_access_key = newIamAccessKey.AccessKey.AccessKeyId;
|
||||||
|
newCredential.outputs.iam_user_secret_key = newIamAccessKey.AccessKey.SecretAccessKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rotation function of HTTP infisical template
|
||||||
|
* This is a generic http based template system for rotation
|
||||||
|
* we use this for sendgrid and for custom secret rotation
|
||||||
|
* This will ensure user provided rotation is easier to make
|
||||||
|
* */
|
||||||
if (provider.template.type === TProviderFunctionTypes.HTTP) {
|
if (provider.template.type === TProviderFunctionTypes.HTTP) {
|
||||||
if (provider.template.functions.set?.pre) {
|
if (provider.template.functions.set?.pre) {
|
||||||
secretRotationPreSetFn(provider.template.functions.set.pre, newCredential);
|
secretRotationPreSetFn(provider.template.functions.set.pre, newCredential);
|
||||||
@@ -185,6 +259,9 @@ export const secretRotationQueueFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// insert the new variables to start
|
||||||
|
// encrypt the data - save it
|
||||||
variables.creds.unshift({
|
variables.creds.unshift({
|
||||||
outputs: newCredential.outputs,
|
outputs: newCredential.outputs,
|
||||||
internal: newCredential.internal
|
internal: newCredential.internal
|
||||||
@@ -200,6 +277,7 @@ export const secretRotationQueueFactory = ({
|
|||||||
key
|
key
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
|
// map the final values to output keys in the board
|
||||||
await secretRotationDAL.transaction(async (tx) => {
|
await secretRotationDAL.transaction(async (tx) => {
|
||||||
await secretRotationDAL.updateById(
|
await secretRotationDAL.updateById(
|
||||||
rotationId,
|
rotationId,
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { TAwsProviderSystems, TProviderFunctionTypes } from "./types";
|
||||||
|
|
||||||
|
export const AWS_IAM_TEMPLATE = {
|
||||||
|
type: TProviderFunctionTypes.AWS as const,
|
||||||
|
client: TAwsProviderSystems.IAM,
|
||||||
|
inputs: {
|
||||||
|
type: "object" as const,
|
||||||
|
properties: {
|
||||||
|
manager_user_access_key: { type: "string" as const },
|
||||||
|
manager_user_secret_key: { type: "string" as const },
|
||||||
|
manager_user_aws_region: { type: "string" as const },
|
||||||
|
iam_username: { type: "string" as const }
|
||||||
|
},
|
||||||
|
required: ["manager_user_access_key", "manager_user_secret_key", "manager_user_aws_region", "iam_username"],
|
||||||
|
additionalProperties: false
|
||||||
|
},
|
||||||
|
outputs: {
|
||||||
|
iam_user_access_key: { type: "string" },
|
||||||
|
iam_user_secret_key: { type: "string" }
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AWS_IAM_TEMPLATE } from "./aws-iam";
|
||||||
import { MYSQL_TEMPLATE } from "./mysql";
|
import { MYSQL_TEMPLATE } from "./mysql";
|
||||||
import { POSTGRES_TEMPLATE } from "./postgres";
|
import { POSTGRES_TEMPLATE } from "./postgres";
|
||||||
import { SENDGRID_TEMPLATE } from "./sendgrid";
|
import { SENDGRID_TEMPLATE } from "./sendgrid";
|
||||||
@@ -24,5 +25,12 @@ export const rotationTemplates: TSecretRotationProviderTemplate[] = [
|
|||||||
image: "mysql.png",
|
image: "mysql.png",
|
||||||
description: "Rotate MySQL@7/MariaDB user credentials",
|
description: "Rotate MySQL@7/MariaDB user credentials",
|
||||||
template: MYSQL_TEMPLATE
|
template: MYSQL_TEMPLATE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "aws-iam",
|
||||||
|
title: "AWS IAM",
|
||||||
|
image: "aws-iam.svg",
|
||||||
|
description: "Rotate AWS IAM User credentials",
|
||||||
|
template: AWS_IAM_TEMPLATE
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export enum TProviderFunctionTypes {
|
export enum TProviderFunctionTypes {
|
||||||
HTTP = "http",
|
HTTP = "http",
|
||||||
DB = "database"
|
DB = "database",
|
||||||
|
AWS = "aws"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TDbProviderClients {
|
export enum TDbProviderClients {
|
||||||
@@ -10,6 +11,10 @@ export enum TDbProviderClients {
|
|||||||
MySql = "mysql"
|
MySql = "mysql"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum TAwsProviderSystems {
|
||||||
|
IAM = "iam"
|
||||||
|
}
|
||||||
|
|
||||||
export enum TAssignOp {
|
export enum TAssignOp {
|
||||||
Direct = "direct",
|
Direct = "direct",
|
||||||
JmesPath = "jmesopath"
|
JmesPath = "jmesopath"
|
||||||
@@ -42,7 +47,7 @@ export type TSecretRotationProviderTemplate = {
|
|||||||
title: string;
|
title: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
template: THttpProviderTemplate | TDbProviderTemplate;
|
template: THttpProviderTemplate | TDbProviderTemplate | TAwsProviderTemplate;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type THttpProviderTemplate = {
|
export type THttpProviderTemplate = {
|
||||||
@@ -70,3 +75,14 @@ export type TDbProviderTemplate = {
|
|||||||
};
|
};
|
||||||
outputs: Record<string, unknown>;
|
outputs: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TAwsProviderTemplate = {
|
||||||
|
type: TProviderFunctionTypes.AWS;
|
||||||
|
client: TAwsProviderSystems;
|
||||||
|
inputs: {
|
||||||
|
type: "object";
|
||||||
|
properties: Record<string, { type: string; [x: string]: unknown; desc?: string }>;
|
||||||
|
required?: string[];
|
||||||
|
};
|
||||||
|
outputs: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import { identityServiceFactory } from "@app/services/identity/identity-service"
|
|||||||
import { identityAccessTokenDALFactory } from "@app/services/identity-access-token/identity-access-token-dal";
|
import { identityAccessTokenDALFactory } from "@app/services/identity-access-token/identity-access-token-dal";
|
||||||
import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
||||||
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||||
|
import { identityProjectMembershipRoleDALFactory } from "@app/services/identity-project/identity-project-membership-role-dal";
|
||||||
import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
||||||
import { identityUaClientSecretDALFactory } from "@app/services/identity-ua/identity-ua-client-secret-dal";
|
import { identityUaClientSecretDALFactory } from "@app/services/identity-ua/identity-ua-client-secret-dal";
|
||||||
import { identityUaDALFactory } from "@app/services/identity-ua/identity-ua-dal";
|
import { identityUaDALFactory } from "@app/services/identity-ua/identity-ua-dal";
|
||||||
@@ -78,6 +79,7 @@ import { projectKeyDALFactory } from "@app/services/project-key/project-key-dal"
|
|||||||
import { projectKeyServiceFactory } from "@app/services/project-key/project-key-service";
|
import { projectKeyServiceFactory } from "@app/services/project-key/project-key-service";
|
||||||
import { projectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
import { projectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||||
import { projectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
|
import { projectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
|
||||||
|
import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal";
|
||||||
import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal";
|
import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal";
|
||||||
import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
||||||
import { secretDALFactory } from "@app/services/secret/secret-dal";
|
import { secretDALFactory } from "@app/services/secret/secret-dal";
|
||||||
@@ -141,6 +143,7 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const projectDAL = projectDALFactory(db);
|
const projectDAL = projectDALFactory(db);
|
||||||
const projectMembershipDAL = projectMembershipDALFactory(db);
|
const projectMembershipDAL = projectMembershipDALFactory(db);
|
||||||
|
const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(db);
|
||||||
const projectRoleDAL = projectRoleDALFactory(db);
|
const projectRoleDAL = projectRoleDALFactory(db);
|
||||||
const projectEnvDAL = projectEnvDALFactory(db);
|
const projectEnvDAL = projectEnvDALFactory(db);
|
||||||
const projectKeyDAL = projectKeyDALFactory(db);
|
const projectKeyDAL = projectKeyDALFactory(db);
|
||||||
@@ -164,6 +167,7 @@ export const registerRoutes = async (
|
|||||||
const identityAccessTokenDAL = identityAccessTokenDALFactory(db);
|
const identityAccessTokenDAL = identityAccessTokenDALFactory(db);
|
||||||
const identityOrgMembershipDAL = identityOrgDALFactory(db);
|
const identityOrgMembershipDAL = identityOrgDALFactory(db);
|
||||||
const identityProjectDAL = identityProjectDALFactory(db);
|
const identityProjectDAL = identityProjectDALFactory(db);
|
||||||
|
const identityProjectMembershipRoleDAL = identityProjectMembershipRoleDALFactory(db);
|
||||||
|
|
||||||
const identityUaDAL = identityUaDALFactory(db);
|
const identityUaDAL = identityUaDALFactory(db);
|
||||||
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
|
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
|
||||||
@@ -321,6 +325,7 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const projectMembershipService = projectMembershipServiceFactory({
|
const projectMembershipService = projectMembershipServiceFactory({
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
|
projectUserMembershipRoleDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
@@ -352,7 +357,8 @@ export const registerRoutes = async (
|
|||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
secretApprovalRequestDAL,
|
secretApprovalRequestDAL,
|
||||||
secretApprovalSecretDAL: sarSecretDAL
|
secretApprovalSecretDAL: sarSecretDAL,
|
||||||
|
projectUserMembershipRoleDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectService = projectServiceFactory({
|
const projectService = projectServiceFactory({
|
||||||
@@ -369,8 +375,11 @@ export const registerRoutes = async (
|
|||||||
orgService,
|
orgService,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
licenseService
|
licenseService,
|
||||||
|
projectUserMembershipRoleDAL,
|
||||||
|
identityProjectMembershipRoleDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectEnvService = projectEnvServiceFactory({
|
const projectEnvService = projectEnvServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
@@ -521,7 +530,9 @@ export const registerRoutes = async (
|
|||||||
permissionService,
|
permissionService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
identityProjectDAL,
|
identityProjectDAL,
|
||||||
identityOrgMembershipDAL
|
identityOrgMembershipDAL,
|
||||||
|
identityProjectMembershipRoleDAL,
|
||||||
|
projectRoleDAL
|
||||||
});
|
});
|
||||||
const identityUaService = identityUaServiceFactory({
|
const identityUaService = identityUaServiceFactory({
|
||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
|
import ms from "ms";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
OrgMembershipsSchema,
|
OrgMembershipsSchema,
|
||||||
ProjectMembershipRole,
|
|
||||||
ProjectMembershipsSchema,
|
ProjectMembershipsSchema,
|
||||||
|
ProjectUserMembershipRolesSchema,
|
||||||
UserEncryptionKeysSchema,
|
UserEncryptionKeysSchema,
|
||||||
UsersSchema
|
UsersSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
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";
|
||||||
|
import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
|
||||||
|
|
||||||
export const registerProjectMembershipRouter = async (server: FastifyZodProvider) => {
|
export const registerProjectMembershipRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@@ -28,16 +30,31 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
memberships: ProjectMembershipsSchema.merge(
|
memberships: ProjectMembershipsSchema.omit({ role: true })
|
||||||
z.object({
|
.merge(
|
||||||
user: UsersSchema.pick({
|
z.object({
|
||||||
email: true,
|
user: UsersSchema.pick({
|
||||||
firstName: true,
|
email: true,
|
||||||
lastName: true,
|
firstName: true,
|
||||||
id: true
|
lastName: true,
|
||||||
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true }))
|
id: true
|
||||||
})
|
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
|
||||||
)
|
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()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
.omit({ createdAt: true, updatedAt: true })
|
.omit({ createdAt: true, updatedAt: true })
|
||||||
.array()
|
.array()
|
||||||
})
|
})
|
||||||
@@ -86,10 +103,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
|
|||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.workspaceId,
|
||||||
members: req.body.members.map((member) => ({
|
members: req.body.members
|
||||||
...member,
|
|
||||||
projectRole: ProjectMembershipRole.Member
|
|
||||||
}))
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
@@ -124,39 +138,56 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
|
|||||||
membershipId: z.string().trim()
|
membershipId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
role: z.string().trim()
|
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)
|
||||||
|
.refine((data) => data.some(({ isTemporary }) => !isTemporary), "At least long lived role is required")
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
membership: ProjectMembershipsSchema
|
roles: ProjectUserMembershipRolesSchema.array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const membership = await server.services.projectMembership.updateProjectMembership({
|
const roles = await server.services.projectMembership.updateProjectMembership({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.workspaceId,
|
||||||
membershipId: req.params.membershipId,
|
membershipId: req.params.membershipId,
|
||||||
role: req.body.role
|
roles: req.body.roles
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
// await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
// ...req.auditLogInfo,
|
||||||
projectId: req.params.workspaceId,
|
// projectId: req.params.workspaceId,
|
||||||
event: {
|
// event: {
|
||||||
type: EventType.UPDATE_USER_WORKSPACE_ROLE,
|
// type: EventType.UPDATE_USER_WORKSPACE_ROLE,
|
||||||
metadata: {
|
// metadata: {
|
||||||
userId: membership.userId,
|
// userId: membership.userId,
|
||||||
newRole: req.body.role,
|
// newRole: req.body.role,
|
||||||
oldRole: membership.role,
|
// oldRole: membership.role,
|
||||||
email: ""
|
// email: ""
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
return { membership };
|
return { roles };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -60,17 +60,32 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
users: ProjectMembershipsSchema.merge(
|
users: ProjectMembershipsSchema.omit({ role: true })
|
||||||
z.object({
|
.merge(
|
||||||
user: UsersSchema.pick({
|
z.object({
|
||||||
username: true,
|
user: UsersSchema.pick({
|
||||||
email: true,
|
username: true,
|
||||||
firstName: true,
|
email: true,
|
||||||
lastName: true,
|
firstName: true,
|
||||||
id: true
|
lastName: true,
|
||||||
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true }))
|
id: true
|
||||||
})
|
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
|
||||||
)
|
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()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
.omit({ createdAt: true, updatedAt: true })
|
.omit({ createdAt: true, updatedAt: true })
|
||||||
.array()
|
.array()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
|
import ms from "ms";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IdentitiesSchema,
|
IdentitiesSchema,
|
||||||
IdentityProjectMembershipsSchema,
|
IdentityProjectMembershipsSchema,
|
||||||
ProjectMembershipRole,
|
ProjectMembershipRole,
|
||||||
ProjectRolesSchema
|
ProjectUserMembershipRolesSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
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";
|
||||||
|
import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
|
||||||
|
|
||||||
export const registerIdentityProjectRouter = async (server: FastifyZodProvider) => {
|
export const registerIdentityProjectRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@@ -57,24 +59,40 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
|
|||||||
identityId: z.string().trim()
|
identityId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
role: z.string().trim().min(1).default(ProjectMembershipRole.NoAccess)
|
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)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
identityMembership: IdentityProjectMembershipsSchema
|
roles: ProjectUserMembershipRolesSchema.array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const identityMembership = await server.services.identityProject.updateProjectIdentity({
|
const roles = await server.services.identityProject.updateProjectIdentity({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
identityId: req.params.identityId,
|
identityId: req.params.identityId,
|
||||||
projectId: req.params.projectId,
|
projectId: req.params.projectId,
|
||||||
role: req.body.role
|
roles: req.body.roles
|
||||||
});
|
});
|
||||||
return { identityMembership };
|
return { roles };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -127,18 +145,29 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
identityMemberships: IdentityProjectMembershipsSchema.merge(
|
identityMemberships: z
|
||||||
z.object({
|
.object({
|
||||||
customRole: ProjectRolesSchema.pick({
|
id: z.string(),
|
||||||
id: true,
|
identityId: z.string(),
|
||||||
name: true,
|
createdAt: z.date(),
|
||||||
slug: true,
|
updatedAt: z.date(),
|
||||||
permissions: true,
|
roles: z.array(
|
||||||
description: true
|
z.object({
|
||||||
}).optional(),
|
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()
|
||||||
|
})
|
||||||
|
),
|
||||||
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
|
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
|
||||||
})
|
})
|
||||||
).array()
|
.array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Knex } from "knex";
|
|||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName } from "@app/db/schemas";
|
import { TableName } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
import { ormify, sqlNestRelationships } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TIdentityProjectDALFactory = ReturnType<typeof identityProjectDALFactory>;
|
export type TIdentityProjectDALFactory = ReturnType<typeof identityProjectDALFactory>;
|
||||||
|
|
||||||
@@ -15,52 +15,81 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
|||||||
const docs = await (tx || db)(TableName.IdentityProjectMembership)
|
const docs = await (tx || db)(TableName.IdentityProjectMembership)
|
||||||
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
|
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
|
||||||
.join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`)
|
.join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`)
|
||||||
|
.join(
|
||||||
|
TableName.IdentityProjectMembershipRole,
|
||||||
|
`${TableName.IdentityProjectMembershipRole}.projectMembershipId`,
|
||||||
|
`${TableName.IdentityProjectMembership}.id`
|
||||||
|
)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.ProjectRoles,
|
TableName.ProjectRoles,
|
||||||
`${TableName.IdentityProjectMembership}.roleId`,
|
`${TableName.IdentityProjectMembershipRole}.customRoleId`,
|
||||||
`${TableName.ProjectRoles}.id`
|
`${TableName.ProjectRoles}.id`
|
||||||
)
|
)
|
||||||
.select(selectAllTableCols(TableName.IdentityProjectMembership))
|
.select(
|
||||||
// cr stands for custom role
|
db.ref("id").withSchema(TableName.IdentityProjectMembership),
|
||||||
.select(db.ref("id").as("crId").withSchema(TableName.ProjectRoles))
|
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership),
|
||||||
.select(db.ref("name").as("crName").withSchema(TableName.ProjectRoles))
|
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership),
|
||||||
.select(db.ref("slug").as("crSlug").withSchema(TableName.ProjectRoles))
|
db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity),
|
||||||
.select(db.ref("description").as("crDescription").withSchema(TableName.ProjectRoles))
|
db.ref("id").as("identityId").withSchema(TableName.Identity),
|
||||||
.select(db.ref("permissions").as("crPermission").withSchema(TableName.ProjectRoles))
|
db.ref("name").as("identityName").withSchema(TableName.Identity),
|
||||||
.select(db.ref("permissions").as("crPermission").withSchema(TableName.ProjectRoles))
|
db.ref("id").withSchema(TableName.IdentityProjectMembership),
|
||||||
.select(db.ref("id").as("identityId").withSchema(TableName.Identity))
|
db.ref("role").withSchema(TableName.IdentityProjectMembershipRole),
|
||||||
.select(db.ref("name").as("identityName").withSchema(TableName.Identity))
|
db.ref("id").withSchema(TableName.IdentityProjectMembershipRole).as("membershipRoleId"),
|
||||||
.select(db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity));
|
db.ref("customRoleId").withSchema(TableName.IdentityProjectMembershipRole),
|
||||||
return docs.map(
|
db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"),
|
||||||
({
|
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
||||||
crId,
|
db.ref("temporaryMode").withSchema(TableName.IdentityProjectMembershipRole),
|
||||||
crDescription,
|
db.ref("isTemporary").withSchema(TableName.IdentityProjectMembershipRole),
|
||||||
crSlug,
|
db.ref("temporaryRange").withSchema(TableName.IdentityProjectMembershipRole),
|
||||||
crPermission,
|
db.ref("temporaryAccessStartTime").withSchema(TableName.IdentityProjectMembershipRole),
|
||||||
crName,
|
db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole)
|
||||||
identityId,
|
);
|
||||||
identityName,
|
|
||||||
identityAuthMethod,
|
const members = sqlNestRelationships({
|
||||||
...el
|
data: docs,
|
||||||
}) => ({
|
parentMapper: ({ identityId, identityName, identityAuthMethod, id, createdAt, updatedAt }) => ({
|
||||||
...el,
|
id,
|
||||||
identityId,
|
identityId,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
identity: {
|
identity: {
|
||||||
id: identityId,
|
id: identityId,
|
||||||
name: identityName,
|
name: identityName,
|
||||||
authMethod: identityAuthMethod
|
authMethod: identityAuthMethod
|
||||||
},
|
}
|
||||||
customRole: el.roleId
|
}),
|
||||||
? {
|
key: "id",
|
||||||
id: crId,
|
childrenMapper: [
|
||||||
name: crName,
|
{
|
||||||
slug: crSlug,
|
label: "roles" as const,
|
||||||
permissions: crPermission,
|
key: "membershipRoleId",
|
||||||
description: crDescription
|
mapper: ({
|
||||||
}
|
role,
|
||||||
: undefined
|
customRoleId,
|
||||||
})
|
customRoleName,
|
||||||
);
|
customRoleSlug,
|
||||||
|
membershipRoleId,
|
||||||
|
temporaryRange,
|
||||||
|
temporaryMode,
|
||||||
|
temporaryAccessEndTime,
|
||||||
|
temporaryAccessStartTime,
|
||||||
|
isTemporary
|
||||||
|
}) => ({
|
||||||
|
id: membershipRoleId,
|
||||||
|
role,
|
||||||
|
customRoleId,
|
||||||
|
customRoleName,
|
||||||
|
customRoleSlug,
|
||||||
|
temporaryRange,
|
||||||
|
temporaryMode,
|
||||||
|
temporaryAccessEndTime,
|
||||||
|
temporaryAccessStartTime,
|
||||||
|
isTemporary
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
return members;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "FindByProjectId" });
|
throw new DatabaseError({ error, name: "FindByProjectId" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TIdentityProjectMembershipRoleDALFactory = ReturnType<typeof identityProjectMembershipRoleDALFactory>;
|
||||||
|
|
||||||
|
export const identityProjectMembershipRoleDALFactory = (db: TDbClient) => {
|
||||||
|
const orm = ormify(db, TableName.IdentityProjectMembershipRole);
|
||||||
|
return orm;
|
||||||
|
};
|
||||||
@@ -1,15 +1,20 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import ms from "ms";
|
||||||
|
|
||||||
import { ProjectMembershipRole, TProjectRoles } from "@app/db/schemas";
|
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||||
|
import { groupBy } from "@app/lib/fn";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
|
import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
|
||||||
|
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||||
import { TIdentityProjectDALFactory } from "./identity-project-dal";
|
import { TIdentityProjectDALFactory } from "./identity-project-dal";
|
||||||
|
import { TIdentityProjectMembershipRoleDALFactory } from "./identity-project-membership-role-dal";
|
||||||
import {
|
import {
|
||||||
TCreateProjectIdentityDTO,
|
TCreateProjectIdentityDTO,
|
||||||
TDeleteProjectIdentityDTO,
|
TDeleteProjectIdentityDTO,
|
||||||
@@ -19,7 +24,12 @@ import {
|
|||||||
|
|
||||||
type TIdentityProjectServiceFactoryDep = {
|
type TIdentityProjectServiceFactoryDep = {
|
||||||
identityProjectDAL: TIdentityProjectDALFactory;
|
identityProjectDAL: TIdentityProjectDALFactory;
|
||||||
|
identityProjectMembershipRoleDAL: Pick<
|
||||||
|
TIdentityProjectMembershipRoleDALFactory,
|
||||||
|
"create" | "transaction" | "insertMany" | "delete"
|
||||||
|
>;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
|
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
||||||
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
|
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissionByRole">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissionByRole">;
|
||||||
};
|
};
|
||||||
@@ -30,7 +40,9 @@ export const identityProjectServiceFactory = ({
|
|||||||
identityProjectDAL,
|
identityProjectDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
projectDAL
|
identityProjectMembershipRoleDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectRoleDAL
|
||||||
}: TIdentityProjectServiceFactoryDep) => {
|
}: TIdentityProjectServiceFactoryDep) => {
|
||||||
const createProjectIdentity = async ({
|
const createProjectIdentity = async ({
|
||||||
identityId,
|
identityId,
|
||||||
@@ -70,11 +82,26 @@ export const identityProjectServiceFactory = ({
|
|||||||
});
|
});
|
||||||
const isCustomRole = Boolean(customRole);
|
const isCustomRole = Boolean(customRole);
|
||||||
|
|
||||||
const projectIdentity = await identityProjectDAL.create({
|
const projectIdentity = await identityProjectDAL.transaction(async (tx) => {
|
||||||
identityId,
|
const identityProjectMembership = await identityProjectDAL.create(
|
||||||
projectId: project.id,
|
{
|
||||||
role: isCustomRole ? ProjectMembershipRole.Custom : role,
|
identityId,
|
||||||
roleId: customRole?.id
|
projectId: project.id,
|
||||||
|
role: isCustomRole ? ProjectMembershipRole.Custom : role,
|
||||||
|
roleId: customRole?.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await identityProjectMembershipRoleDAL.create(
|
||||||
|
{
|
||||||
|
projectMembershipId: identityProjectMembership.id,
|
||||||
|
role: isCustomRole ? ProjectMembershipRole.Custom : role,
|
||||||
|
customRoleId: customRole?.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return identityProjectMembership;
|
||||||
});
|
});
|
||||||
return projectIdentity;
|
return projectIdentity;
|
||||||
};
|
};
|
||||||
@@ -82,7 +109,7 @@ export const identityProjectServiceFactory = ({
|
|||||||
const updateProjectIdentity = async ({
|
const updateProjectIdentity = async ({
|
||||||
projectId,
|
projectId,
|
||||||
identityId,
|
identityId,
|
||||||
role,
|
roles,
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
@@ -106,28 +133,51 @@ export const identityProjectServiceFactory = ({
|
|||||||
if (!hasRequiredPriviledges)
|
if (!hasRequiredPriviledges)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
|
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
|
||||||
|
|
||||||
let customRole: TProjectRoles | undefined;
|
// validate custom roles input
|
||||||
if (role) {
|
const customInputRoles = roles.filter(
|
||||||
const { permission: rolePermission, role: customOrgRole } = await permissionService.getProjectPermissionByRole(
|
({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
|
||||||
role,
|
|
||||||
projectIdentity.projectId
|
|
||||||
);
|
|
||||||
|
|
||||||
const isCustomRole = Boolean(customOrgRole);
|
|
||||||
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission);
|
|
||||||
if (!hasRequiredNewRolePermission)
|
|
||||||
throw new BadRequestError({ message: "Failed to create a more privileged identity" });
|
|
||||||
if (isCustomRole) customRole = customOrgRole;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [updatedProjectIdentity] = await identityProjectDAL.update(
|
|
||||||
{ projectId, identityId: projectIdentity.identityId },
|
|
||||||
{
|
|
||||||
role: customRole ? ProjectMembershipRole.Custom : role,
|
|
||||||
roleId: customRole ? customRole.id : null
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
return updatedProjectIdentity;
|
const hasCustomRole = Boolean(customInputRoles.length);
|
||||||
|
const customRoles = hasCustomRole
|
||||||
|
? await projectRoleDAL.find({
|
||||||
|
projectId,
|
||||||
|
$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 santiziedProjectMembershipRoles = roles.map((inputRole) => {
|
||||||
|
const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
|
||||||
|
if (!inputRole.isTemporary) {
|
||||||
|
return {
|
||||||
|
projectMembershipId: projectIdentity.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: projectIdentity.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 identityProjectMembershipRoleDAL.transaction(async (tx) => {
|
||||||
|
await identityProjectMembershipRoleDAL.delete({ projectMembershipId: projectIdentity.id }, tx);
|
||||||
|
return identityProjectMembershipRoleDAL.insertMany(santiziedProjectMembershipRoles, tx);
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedRoles;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteProjectIdentity = async ({
|
const deleteProjectIdentity = async ({
|
||||||
|
|||||||
@@ -1,12 +1,26 @@
|
|||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
|
||||||
|
|
||||||
export type TCreateProjectIdentityDTO = {
|
export type TCreateProjectIdentityDTO = {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
role: string;
|
role: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TUpdateProjectIdentityDTO = {
|
export type TUpdateProjectIdentityDTO = {
|
||||||
role: string;
|
roles: (
|
||||||
|
| {
|
||||||
|
role: string;
|
||||||
|
isTemporary?: false;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
role: string;
|
||||||
|
isTemporary: true;
|
||||||
|
temporaryMode: ProjectUserMembershipTemporaryMode.Relative;
|
||||||
|
temporaryRange: string;
|
||||||
|
temporaryAccessStartTime: string;
|
||||||
|
}
|
||||||
|
)[];
|
||||||
identityId: string;
|
identityId: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
|
|||||||
{ id: roleId, orgId },
|
{ id: roleId, orgId },
|
||||||
{ ...data, permissions: data.permissions ? JSON.stringify(data.permissions) : undefined }
|
{ ...data, permissions: data.permissions ? JSON.stringify(data.permissions) : undefined }
|
||||||
);
|
);
|
||||||
if (!updateRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
|
if (!updatedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
|
||||||
return updatedRole;
|
return updatedRole;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
|
|||||||
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId);
|
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Role);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Role);
|
||||||
const [deletedRole] = await orgRoleDAL.delete({ id: roleId, orgId });
|
const [deletedRole] = await orgRoleDAL.delete({ id: roleId, orgId });
|
||||||
if (!deleteRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
|
if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
|
||||||
|
|
||||||
return deletedRole;
|
return deletedRole;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
|
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TProjectMembershipDALFactory = ReturnType<typeof projectMembershipDALFactory>;
|
export type TProjectMembershipDALFactory = ReturnType<typeof projectMembershipDALFactory>;
|
||||||
|
|
||||||
@@ -11,32 +11,86 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
|||||||
// special query
|
// special query
|
||||||
const findAllProjectMembers = async (projectId: string) => {
|
const findAllProjectMembers = async (projectId: string) => {
|
||||||
try {
|
try {
|
||||||
const members = await db(TableName.ProjectMembership)
|
const docs = await db(TableName.ProjectMembership)
|
||||||
.where({ projectId })
|
.where({ [`${TableName.ProjectMembership}.projectId` as "projectId"]: projectId })
|
||||||
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||||
.join<TUserEncryptionKeys>(
|
.join<TUserEncryptionKeys>(
|
||||||
TableName.UserEncryptionKey,
|
TableName.UserEncryptionKey,
|
||||||
`${TableName.UserEncryptionKey}.userId`,
|
`${TableName.UserEncryptionKey}.userId`,
|
||||||
`${TableName.Users}.id`
|
`${TableName.Users}.id`
|
||||||
)
|
)
|
||||||
|
.join(
|
||||||
|
TableName.ProjectUserMembershipRole,
|
||||||
|
`${TableName.ProjectUserMembershipRole}.projectMembershipId`,
|
||||||
|
`${TableName.ProjectMembership}.id`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ProjectRoles,
|
||||||
|
`${TableName.ProjectUserMembershipRole}.customRoleId`,
|
||||||
|
`${TableName.ProjectRoles}.id`
|
||||||
|
)
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.ProjectMembership),
|
db.ref("id").withSchema(TableName.ProjectMembership),
|
||||||
db.ref("projectId").withSchema(TableName.ProjectMembership),
|
|
||||||
db.ref("role").withSchema(TableName.ProjectMembership),
|
|
||||||
db.ref("roleId").withSchema(TableName.ProjectMembership),
|
|
||||||
db.ref("isGhost").withSchema(TableName.Users),
|
db.ref("isGhost").withSchema(TableName.Users),
|
||||||
db.ref("username").withSchema(TableName.Users),
|
db.ref("username").withSchema(TableName.Users),
|
||||||
db.ref("email").withSchema(TableName.Users),
|
db.ref("email").withSchema(TableName.Users),
|
||||||
db.ref("publicKey").withSchema(TableName.UserEncryptionKey),
|
db.ref("publicKey").withSchema(TableName.UserEncryptionKey),
|
||||||
db.ref("firstName").withSchema(TableName.Users),
|
db.ref("firstName").withSchema(TableName.Users),
|
||||||
db.ref("lastName").withSchema(TableName.Users),
|
db.ref("lastName").withSchema(TableName.Users),
|
||||||
db.ref("id").withSchema(TableName.Users).as("userId")
|
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||||
|
db.ref("role").withSchema(TableName.ProjectUserMembershipRole),
|
||||||
|
db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("membershipRoleId"),
|
||||||
|
db.ref("customRoleId").withSchema(TableName.ProjectUserMembershipRole),
|
||||||
|
db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"),
|
||||||
|
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
||||||
|
db.ref("temporaryMode").withSchema(TableName.ProjectUserMembershipRole),
|
||||||
|
db.ref("isTemporary").withSchema(TableName.ProjectUserMembershipRole),
|
||||||
|
db.ref("temporaryRange").withSchema(TableName.ProjectUserMembershipRole),
|
||||||
|
db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole),
|
||||||
|
db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole)
|
||||||
)
|
)
|
||||||
.where({ isGhost: false });
|
.where({ isGhost: false });
|
||||||
return members.map(({ username, email, firstName, lastName, publicKey, isGhost, ...data }) => ({
|
|
||||||
...data,
|
const members = sqlNestRelationships({
|
||||||
user: { username, email, firstName, lastName, id: data.userId, publicKey, isGhost }
|
data: docs,
|
||||||
}));
|
parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, userId }) => ({
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
projectId,
|
||||||
|
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost }
|
||||||
|
}),
|
||||||
|
key: "id",
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
label: "roles" as const,
|
||||||
|
key: "membershipRoleId",
|
||||||
|
mapper: ({
|
||||||
|
role,
|
||||||
|
customRoleId,
|
||||||
|
customRoleName,
|
||||||
|
customRoleSlug,
|
||||||
|
membershipRoleId,
|
||||||
|
temporaryRange,
|
||||||
|
temporaryMode,
|
||||||
|
temporaryAccessEndTime,
|
||||||
|
temporaryAccessStartTime,
|
||||||
|
isTemporary
|
||||||
|
}) => ({
|
||||||
|
id: membershipRoleId,
|
||||||
|
role,
|
||||||
|
customRoleId,
|
||||||
|
customRoleName,
|
||||||
|
customRoleSlug,
|
||||||
|
temporaryRange,
|
||||||
|
temporaryMode,
|
||||||
|
temporaryAccessEndTime,
|
||||||
|
temporaryAccessStartTime,
|
||||||
|
isTemporary
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
return members;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find all project members" });
|
throw new DatabaseError({ error, name: "Find all project members" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import ms from "ms";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
OrgMembershipStatus,
|
|
||||||
ProjectMembershipRole,
|
ProjectMembershipRole,
|
||||||
ProjectVersion,
|
ProjectVersion,
|
||||||
SecretKeyEncoding,
|
SecretKeyEncoding,
|
||||||
TableName,
|
TableName,
|
||||||
TProjectMemberships,
|
TProjectMemberships
|
||||||
TUsers
|
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
@@ -29,22 +28,24 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
|||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TProjectMembershipDALFactory } from "./project-membership-dal";
|
import { TProjectMembershipDALFactory } from "./project-membership-dal";
|
||||||
import {
|
import {
|
||||||
|
ProjectUserMembershipTemporaryMode,
|
||||||
TAddUsersToWorkspaceDTO,
|
TAddUsersToWorkspaceDTO,
|
||||||
TAddUsersToWorkspaceNonE2EEDTO,
|
TAddUsersToWorkspaceNonE2EEDTO,
|
||||||
TDeleteProjectMembershipOldDTO,
|
TDeleteProjectMembershipOldDTO,
|
||||||
TDeleteProjectMembershipsDTO,
|
TDeleteProjectMembershipsDTO,
|
||||||
TGetProjectMembershipDTO,
|
TGetProjectMembershipDTO,
|
||||||
TInviteUserToProjectDTO,
|
|
||||||
TUpdateProjectMembershipDTO
|
TUpdateProjectMembershipDTO
|
||||||
} from "./project-membership-types";
|
} from "./project-membership-types";
|
||||||
|
import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal";
|
||||||
|
|
||||||
type TProjectMembershipServiceFactoryDep = {
|
type TProjectMembershipServiceFactoryDep = {
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
smtpService: TSmtpService;
|
smtpService: TSmtpService;
|
||||||
projectBotDAL: TProjectBotDALFactory;
|
projectBotDAL: TProjectBotDALFactory;
|
||||||
projectMembershipDAL: TProjectMembershipDALFactory;
|
projectMembershipDAL: TProjectMembershipDALFactory;
|
||||||
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "find" | "delete">;
|
||||||
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">;
|
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">;
|
||||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "findOne">;
|
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">;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
|
||||||
@@ -56,6 +57,7 @@ export type TProjectMembershipServiceFactory = ReturnType<typeof projectMembersh
|
|||||||
export const projectMembershipServiceFactory = ({
|
export const projectMembershipServiceFactory = ({
|
||||||
permissionService,
|
permissionService,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
|
projectUserMembershipRoleDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
@@ -72,82 +74,6 @@ export const projectMembershipServiceFactory = ({
|
|||||||
return projectMembershipDAL.findAllProjectMembers(projectId);
|
return projectMembershipDAL.findAllProjectMembers(projectId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const inviteUserToProject = async ({ actorId, actor, actorOrgId, projectId, emails }: TInviteUserToProjectDTO) => {
|
|
||||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
|
||||||
|
|
||||||
const invitees: TUsers[] = [];
|
|
||||||
|
|
||||||
const project = await projectDAL.findById(projectId);
|
|
||||||
const users = await userDAL.find({
|
|
||||||
$in: { email: emails }
|
|
||||||
});
|
|
||||||
|
|
||||||
await projectDAL.transaction(async (tx) => {
|
|
||||||
for (const invitee of users) {
|
|
||||||
if (!invitee.isAccepted)
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Failed to validate invitee",
|
|
||||||
name: "Invite user to project"
|
|
||||||
});
|
|
||||||
|
|
||||||
const inviteeMembership = await projectMembershipDAL.findOne(
|
|
||||||
{
|
|
||||||
userId: invitee.id,
|
|
||||||
projectId
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
if (inviteeMembership) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Existing member of project",
|
|
||||||
name: "Invite user to project"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const inviteeMembershipOrg = await orgDAL.findMembership({
|
|
||||||
userId: invitee.id,
|
|
||||||
orgId: project.orgId,
|
|
||||||
status: OrgMembershipStatus.Accepted
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!inviteeMembershipOrg) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Failed to validate invitee org membership",
|
|
||||||
name: "Invite user to project"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await projectMembershipDAL.create(
|
|
||||||
{
|
|
||||||
userId: invitee.id,
|
|
||||||
projectId,
|
|
||||||
role: ProjectMembershipRole.Member
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
invitees.push(invitee);
|
|
||||||
}
|
|
||||||
|
|
||||||
const appCfg = getConfig();
|
|
||||||
await smtpService.sendMail({
|
|
||||||
template: SmtpTemplates.WorkspaceInvite,
|
|
||||||
subjectLine: "Infisical project invitation",
|
|
||||||
recipients: invitees.filter((i) => i.email).map((i) => i.email as string),
|
|
||||||
substitutions: {
|
|
||||||
workspaceName: project.name,
|
|
||||||
callback_url: `${appCfg.SITE_URL}/login`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const latestKey = await projectKeyDAL.findLatestProjectKey(actorId, projectId);
|
|
||||||
|
|
||||||
return { invitees, latestKey };
|
|
||||||
};
|
|
||||||
|
|
||||||
const addUsersToProject = async ({
|
const addUsersToProject = async ({
|
||||||
projectId,
|
projectId,
|
||||||
actorId,
|
actorId,
|
||||||
@@ -176,17 +102,16 @@ 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" });
|
||||||
|
|
||||||
await projectMembershipDAL.transaction(async (tx) => {
|
await projectMembershipDAL.transaction(async (tx) => {
|
||||||
await projectMembershipDAL.insertMany(
|
const projectMemberships = await projectMembershipDAL.insertMany(
|
||||||
orgMembers.map(({ userId, id: membershipId }) => {
|
orgMembers.map(({ userId }) => ({
|
||||||
const role =
|
projectId,
|
||||||
members.find((i) => i.orgMembershipId === membershipId)?.projectRole || ProjectMembershipRole.Member;
|
userId: userId as string,
|
||||||
|
role: ProjectMembershipRole.Member
|
||||||
return {
|
})),
|
||||||
projectId,
|
tx
|
||||||
userId: userId as string,
|
);
|
||||||
role
|
await projectUserMembershipRoleDAL.insertMany(
|
||||||
};
|
projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: ProjectMembershipRole.Member })),
|
||||||
}),
|
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId);
|
const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId);
|
||||||
@@ -296,7 +221,7 @@ export const projectMembershipServiceFactory = ({
|
|||||||
const members: TProjectMemberships[] = [];
|
const members: TProjectMemberships[] = [];
|
||||||
|
|
||||||
await projectMembershipDAL.transaction(async (tx) => {
|
await projectMembershipDAL.transaction(async (tx) => {
|
||||||
const result = await projectMembershipDAL.insertMany(
|
const projectMemberships = await projectMembershipDAL.insertMany(
|
||||||
orgMembers.map(({ user }) => ({
|
orgMembers.map(({ user }) => ({
|
||||||
projectId,
|
projectId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -304,8 +229,12 @@ export const projectMembershipServiceFactory = ({
|
|||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
await projectUserMembershipRoleDAL.insertMany(
|
||||||
|
projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: ProjectMembershipRole.Member })),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
members.push(...result);
|
members.push(...projectMemberships);
|
||||||
|
|
||||||
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
|
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
|
||||||
await projectKeyDAL.insertMany(
|
await projectKeyDAL.insertMany(
|
||||||
@@ -346,43 +275,71 @@ export const projectMembershipServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
projectId,
|
projectId,
|
||||||
membershipId,
|
membershipId,
|
||||||
role
|
roles
|
||||||
}: TUpdateProjectMembershipDTO) => {
|
}: TUpdateProjectMembershipDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||||
|
|
||||||
const membershipUser = await userDAL.findUserByProjectMembershipId(membershipId);
|
const membershipUser = await userDAL.findUserByProjectMembershipId(membershipId);
|
||||||
|
if (membershipUser?.isGhost || membershipUser?.projectId !== projectId) {
|
||||||
if (membershipUser?.isGhost) {
|
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Unauthorized member update",
|
message: "Unauthorized member update",
|
||||||
name: "Update project membership"
|
name: "Update project membership"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole);
|
// validate custom roles input
|
||||||
if (isCustomRole) {
|
const customInputRoles = roles.filter(
|
||||||
const customRole = await projectRoleDAL.findOne({ slug: role, projectId });
|
({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
|
||||||
if (!customRole) throw new BadRequestError({ name: "Update project membership", message: "Role not found" });
|
);
|
||||||
const project = await projectDAL.findById(customRole.projectId);
|
const hasCustomRole = Boolean(customInputRoles.length);
|
||||||
const plan = await licenseService.getPlan(project.orgId);
|
if (hasCustomRole) {
|
||||||
|
const plan = await licenseService.getPlan(actorOrgId as string);
|
||||||
if (!plan?.rbac)
|
if (!plan?.rbac)
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
|
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
|
||||||
});
|
});
|
||||||
|
|
||||||
const [membership] = await projectMembershipDAL.update(
|
|
||||||
{ id: membershipId, projectId },
|
|
||||||
{
|
|
||||||
role: ProjectMembershipRole.Custom,
|
|
||||||
roleId: customRole.id
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return membership;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const [membership] = await projectMembershipDAL.update({ id: membershipId, projectId }, { role, roleId: null });
|
const customRoles = hasCustomRole
|
||||||
return membership;
|
? await projectRoleDAL.find({
|
||||||
|
projectId,
|
||||||
|
$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 santiziedProjectMembershipRoles = roles.map((inputRole) => {
|
||||||
|
const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
|
||||||
|
if (!inputRole.isTemporary) {
|
||||||
|
return {
|
||||||
|
projectMembershipId: membershipId,
|
||||||
|
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: membershipId,
|
||||||
|
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 projectMembershipDAL.transaction(async (tx) => {
|
||||||
|
await projectUserMembershipRoleDAL.delete({ projectMembershipId: membershipId }, tx);
|
||||||
|
return projectUserMembershipRoleDAL.insertMany(santiziedProjectMembershipRoles, tx);
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedRoles;
|
||||||
};
|
};
|
||||||
|
|
||||||
// This is old and should be removed later. Its not used anywhere, but it is exposed in our API. So to avoid breaking changes, we are keeping it for now.
|
// This is old and should be removed later. Its not used anywhere, but it is exposed in our API. So to avoid breaking changes, we are keeping it for now.
|
||||||
@@ -481,7 +438,6 @@ export const projectMembershipServiceFactory = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
getProjectMemberships,
|
getProjectMemberships,
|
||||||
inviteUserToProject,
|
|
||||||
updateProjectMembership,
|
updateProjectMembership,
|
||||||
addUsersToProjectNonE2EE,
|
addUsersToProjectNonE2EE,
|
||||||
deleteProjectMemberships,
|
deleteProjectMemberships,
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { ProjectMembershipRole } from "@app/db/schemas";
|
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
export type TGetProjectMembershipDTO = TProjectPermission;
|
export type TGetProjectMembershipDTO = TProjectPermission;
|
||||||
|
export enum ProjectUserMembershipTemporaryMode {
|
||||||
|
Relative = "relative"
|
||||||
|
}
|
||||||
|
|
||||||
export type TInviteUserToProjectDTO = {
|
export type TInviteUserToProjectDTO = {
|
||||||
emails: string[];
|
emails: string[];
|
||||||
@@ -9,7 +11,19 @@ export type TInviteUserToProjectDTO = {
|
|||||||
|
|
||||||
export type TUpdateProjectMembershipDTO = {
|
export type TUpdateProjectMembershipDTO = {
|
||||||
membershipId: string;
|
membershipId: string;
|
||||||
role: string;
|
roles: (
|
||||||
|
| {
|
||||||
|
role: string;
|
||||||
|
isTemporary?: false;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
role: string;
|
||||||
|
isTemporary: true;
|
||||||
|
temporaryMode: ProjectUserMembershipTemporaryMode.Relative;
|
||||||
|
temporaryRange: string;
|
||||||
|
temporaryAccessStartTime: string;
|
||||||
|
}
|
||||||
|
)[];
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TDeleteProjectMembershipOldDTO = {
|
export type TDeleteProjectMembershipOldDTO = {
|
||||||
@@ -27,7 +41,6 @@ export type TAddUsersToWorkspaceDTO = {
|
|||||||
orgMembershipId: string;
|
orgMembershipId: string;
|
||||||
workspaceEncryptedKey: string;
|
workspaceEncryptedKey: string;
|
||||||
workspaceEncryptedNonce: string;
|
workspaceEncryptedNonce: string;
|
||||||
projectRole: ProjectMembershipRole;
|
|
||||||
}[];
|
}[];
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TProjectUserMembershipRoleDALFactory = ReturnType<typeof projectUserMembershipRoleDALFactory>;
|
||||||
|
|
||||||
|
export const projectUserMembershipRoleDALFactory = (db: TDbClient) => {
|
||||||
|
const orm = ormify(db, TableName.ProjectUserMembershipRole);
|
||||||
|
return orm;
|
||||||
|
};
|
||||||
@@ -76,7 +76,7 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }:
|
|||||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Role);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Role);
|
||||||
const [deletedRole] = await projectRoleDAL.delete({ id: roleId, projectId });
|
const [deletedRole] = await projectRoleDAL.delete({ id: roleId, projectId });
|
||||||
if (!deleteRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
|
if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
|
||||||
|
|
||||||
return deletedRole;
|
return deletedRole;
|
||||||
};
|
};
|
||||||
@@ -92,7 +92,7 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }:
|
|||||||
name: "Admin",
|
name: "Admin",
|
||||||
slug: ProjectMembershipRole.Admin,
|
slug: ProjectMembershipRole.Admin,
|
||||||
description: "Complete administration access over the project",
|
description: "Complete administration access over the project",
|
||||||
permissions: packRules(projectAdminPermissions.rules),
|
permissions: packRules(projectAdminPermissions),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
},
|
},
|
||||||
@@ -102,7 +102,7 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }:
|
|||||||
name: "Developer",
|
name: "Developer",
|
||||||
slug: ProjectMembershipRole.Member,
|
slug: ProjectMembershipRole.Member,
|
||||||
description: "Non-administrative role in an project",
|
description: "Non-administrative role in an project",
|
||||||
permissions: packRules(projectMemberPermissions.rules),
|
permissions: packRules(projectMemberPermissions),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
},
|
},
|
||||||
@@ -112,7 +112,7 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }:
|
|||||||
name: "Viewer",
|
name: "Viewer",
|
||||||
slug: ProjectMembershipRole.Viewer,
|
slug: ProjectMembershipRole.Viewer,
|
||||||
description: "Non-administrative role in an project",
|
description: "Non-administrative role in an project",
|
||||||
permissions: packRules(projectViewerPermission.rules),
|
permissions: packRules(projectViewerPermission),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
},
|
},
|
||||||
@@ -122,7 +122,7 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }:
|
|||||||
name: "No Access",
|
name: "No Access",
|
||||||
slug: "no-access",
|
slug: "no-access",
|
||||||
description: "No access to any resources in the project",
|
description: "No access to any resources in the project",
|
||||||
permissions: packRules(projectNoAccessPermissions.rules),
|
permissions: packRules(projectNoAccessPermissions),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
|||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
|
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||||
import { TSecretDALFactory } from "../secret/secret-dal";
|
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||||
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
|
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
@@ -58,9 +59,9 @@ type TProjectQueueFactoryDep = {
|
|||||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne" | "delete" | "create">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne" | "delete" | "create">;
|
||||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create">;
|
||||||
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
||||||
integrationAuthDAL: TIntegrationAuthDALFactory;
|
integrationAuthDAL: TIntegrationAuthDALFactory;
|
||||||
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserId">;
|
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserId">;
|
||||||
|
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "find">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "find">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "transaction" | "updateById" | "setProjectUpgradeStatus" | "find">;
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "transaction" | "updateById" | "setProjectUpgradeStatus" | "find">;
|
||||||
orgDAL: Pick<TOrgDALFactory, "findMembership">;
|
orgDAL: Pick<TOrgDALFactory, "findMembership">;
|
||||||
@@ -81,7 +82,8 @@ export const projectQueueFactory = ({
|
|||||||
orgDAL,
|
orgDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
orgService,
|
orgService,
|
||||||
projectMembershipDAL
|
projectMembershipDAL,
|
||||||
|
projectUserMembershipRoleDAL
|
||||||
}: TProjectQueueFactoryDep) => {
|
}: TProjectQueueFactoryDep) => {
|
||||||
const upgradeProject = async (dto: TQueueJobTypes["upgrade-project-to-ghost"]["payload"]) => {
|
const upgradeProject = async (dto: TQueueJobTypes["upgrade-project-to-ghost"]["payload"]) => {
|
||||||
await queueService.queue(QueueName.UpgradeProjectToGhost, QueueJobs.UpgradeProjectToGhost, dto, {
|
await queueService.queue(QueueName.UpgradeProjectToGhost, QueueJobs.UpgradeProjectToGhost, dto, {
|
||||||
@@ -227,7 +229,7 @@ export const projectQueueFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Create a membership for the ghost user
|
// Create a membership for the ghost user
|
||||||
await projectMembershipDAL.create(
|
const projectMembership = await projectMembershipDAL.create(
|
||||||
{
|
{
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
userId: ghostUser.user.id,
|
userId: ghostUser.user.id,
|
||||||
@@ -235,6 +237,10 @@ export const projectQueueFactory = ({
|
|||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
await projectUserMembershipRoleDAL.create(
|
||||||
|
{ projectMembershipId: projectMembership.id, role: ProjectMembershipRole.Admin },
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
// If a bot already exists, delete it
|
// If a bot already exists, delete it
|
||||||
if (existingBot) {
|
if (existingBot) {
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ import { TProjectPermission } from "@app/lib/types";
|
|||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||||
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
||||||
|
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
||||||
import { TOrgServiceFactory } from "../org/org-service";
|
import { TOrgServiceFactory } from "../org/org-service";
|
||||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
|
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||||
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
|
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
|
||||||
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
@@ -50,9 +52,11 @@ type TProjectServiceFactoryDep = {
|
|||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany" | "find">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany" | "find">;
|
||||||
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
||||||
identityProjectDAL: TIdentityProjectDALFactory;
|
identityProjectDAL: TIdentityProjectDALFactory;
|
||||||
|
identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "create" | "findById" | "delete" | "findOne">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "create" | "findById" | "delete" | "findOne">;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
|
||||||
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
||||||
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
|
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
|
||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||||
@@ -75,7 +79,9 @@ export const projectServiceFactory = ({
|
|||||||
secretBlindIndexDAL,
|
secretBlindIndexDAL,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
licenseService
|
licenseService,
|
||||||
|
projectUserMembershipRoleDAL,
|
||||||
|
identityProjectMembershipRoleDAL
|
||||||
}: TProjectServiceFactoryDep) => {
|
}: TProjectServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
* Create workspace. Make user the admin
|
* Create workspace. Make user the admin
|
||||||
@@ -114,14 +120,18 @@ export const projectServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
// set ghost user as admin of project
|
// set ghost user as admin of project
|
||||||
await projectMembershipDAL.create(
|
const projectMembership = await projectMembershipDAL.create(
|
||||||
{
|
{
|
||||||
userId: ghostUser.user.id,
|
userId: ghostUser.user.id,
|
||||||
role: ProjectMembershipRole.Admin,
|
projectId: project.id,
|
||||||
projectId: project.id
|
role: ProjectMembershipRole.Admin
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
await projectUserMembershipRoleDAL.create(
|
||||||
|
{ projectMembershipId: projectMembership.id, role: ProjectMembershipRole.Admin },
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
// generate the blind index for project
|
// generate the blind index for project
|
||||||
await secretBlindIndexDAL.create(
|
await secretBlindIndexDAL.create(
|
||||||
@@ -213,7 +223,7 @@ export const projectServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create a membership for the user
|
// Create a membership for the user
|
||||||
await projectMembershipDAL.create(
|
const userProjectMembership = await projectMembershipDAL.create(
|
||||||
{
|
{
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -221,6 +231,10 @@ export const projectServiceFactory = ({
|
|||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
await projectUserMembershipRoleDAL.create(
|
||||||
|
{ projectMembershipId: userProjectMembership.id, role: projectAdmin.projectRole },
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
// Create a project key for the user
|
// Create a project key for the user
|
||||||
await projectKeyDAL.create(
|
await projectKeyDAL.create(
|
||||||
@@ -266,7 +280,7 @@ export const projectServiceFactory = ({
|
|||||||
});
|
});
|
||||||
const isCustomRole = Boolean(customRole);
|
const isCustomRole = Boolean(customRole);
|
||||||
|
|
||||||
await identityProjectDAL.create(
|
const identityProjectMembership = await identityProjectDAL.create(
|
||||||
{
|
{
|
||||||
identityId: actorId,
|
identityId: actorId,
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
@@ -275,6 +289,15 @@ export const projectServiceFactory = ({
|
|||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await identityProjectMembershipRoleDAL.create(
|
||||||
|
{
|
||||||
|
projectMembershipId: identityProjectMembership.id,
|
||||||
|
role: isCustomRole ? ProjectMembershipRole.Custom : ProjectMembershipRole.Admin,
|
||||||
|
customRoleId: customRole?.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -350,11 +373,11 @@ export const projectServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const upgradeProject = async ({ projectId, actor, actorId, userPrivateKey }: TUpgradeProjectDTO) => {
|
const upgradeProject = async ({ projectId, actor, actorId, userPrivateKey }: TUpgradeProjectDTO) => {
|
||||||
const { permission, membership } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
const { permission, hasRole } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
||||||
|
|
||||||
if (membership?.role !== ProjectMembershipRole.Admin) {
|
if (!hasRole(ProjectMembershipRole.Admin)) {
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "User must be admin"
|
message: "User must be admin"
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ export const secretBlindIndexServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getProjectSecrets = async ({ projectId, actorId, actor }: TGetProjectSecretsDTO) => {
|
const getProjectSecrets = async ({ projectId, actorId, actor }: TGetProjectSecretsDTO) => {
|
||||||
const { membership } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
const { hasRole } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
if (membership?.role !== ProjectMembershipRole.Admin) {
|
if (!hasRole(ProjectMembershipRole.Admin)) {
|
||||||
throw new UnauthorizedError({ message: "User must be admin" });
|
throw new UnauthorizedError({ message: "User must be admin" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,8 +53,8 @@ export const secretBlindIndexServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
secretsToUpdate
|
secretsToUpdate
|
||||||
}: TUpdateProjectSecretNameDTO) => {
|
}: TUpdateProjectSecretNameDTO) => {
|
||||||
const { membership } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
const { hasRole } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||||
if (membership?.role !== ProjectMembershipRole.Admin) {
|
if (!hasRole(ProjectMembershipRole.Admin)) {
|
||||||
throw new UnauthorizedError({ message: "User must be admin" });
|
throw new UnauthorizedError({ message: "User must be admin" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,11 @@ func RequireLogin() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsLoggedIn() bool {
|
||||||
|
configFile, _ := GetConfigFile()
|
||||||
|
return configFile.LoggedInUserEmail != ""
|
||||||
|
}
|
||||||
|
|
||||||
func RequireServiceToken() {
|
func RequireServiceToken() {
|
||||||
serviceToken := os.Getenv(INFISICAL_TOKEN_NAME)
|
serviceToken := os.Getenv(INFISICAL_TOKEN_NAME)
|
||||||
if serviceToken == "" {
|
if serviceToken == "" {
|
||||||
|
|||||||
@@ -427,6 +427,8 @@ func ExpandSecrets(secrets []models.SingleEnvironmentVariable, auth models.Expan
|
|||||||
refSecs, err = GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: env, InfisicalToken: auth.InfisicalToken, SecretsPath: secPath}, projectConfigPathDir)
|
refSecs, err = GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: env, InfisicalToken: auth.InfisicalToken, SecretsPath: secPath}, projectConfigPathDir)
|
||||||
} else if auth.UniversalAuthAccessToken != "" {
|
} else if auth.UniversalAuthAccessToken != "" {
|
||||||
refSecs, err = GetAllEnvironmentVariables((models.GetAllSecretsParameters{Environment: env, UniversalAuthAccessToken: auth.UniversalAuthAccessToken, SecretsPath: secPath, WorkspaceId: sec.WorkspaceId}), projectConfigPathDir)
|
refSecs, err = GetAllEnvironmentVariables((models.GetAllSecretsParameters{Environment: env, UniversalAuthAccessToken: auth.UniversalAuthAccessToken, SecretsPath: secPath, WorkspaceId: sec.WorkspaceId}), projectConfigPathDir)
|
||||||
|
} else if IsLoggedIn() {
|
||||||
|
refSecs, err = GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: env, SecretsPath: secPath}, projectConfigPathDir)
|
||||||
} else {
|
} else {
|
||||||
HandleError(errors.New("no authentication provided"), "Please provide authentication to fetch secrets")
|
HandleError(errors.New("no authentication provided"), "Please provide authentication to fetch secrets")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,3 +135,38 @@ Another option to point the CLI to your self hosted Infisical instance is to set
|
|||||||
infisical <any-command> --domain="https://your-self-hosted-infisical.com/api"
|
infisical <any-command> --domain="https://your-self-hosted-infisical.com/api"
|
||||||
```
|
```
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
|
## History
|
||||||
|
|
||||||
|
Your terminal keeps a history with the commands you run. When you create Infisical secrets directly from your terminal, they'll stay there for a while.
|
||||||
|
|
||||||
|
For security and privacy concerns, we recommend you to configure your terminal to ignore those specific Infisical commands.
|
||||||
|
|
||||||
|
<Accordion title="Ignore commands">
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Unix/Linux">
|
||||||
|
<Tip>
|
||||||
|
`$HOME/.profile` is pretty common but, you could place it under `$HOME/.profile.d/infisical.sh` or any profile file run at login
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat <<EOF >> $HOME/.profile && source $HOME/.profile
|
||||||
|
|
||||||
|
# Ignoring specific Infisical CLI commands
|
||||||
|
DEFAULT_HISTIGNORE=$HISTIGNORE
|
||||||
|
export HISTIGNORE="*infisical secrets set*:$DEFAULT_HISTIGNORE"
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Windows">
|
||||||
|
If you're on WSL, then you can use the Unix/Linux method.
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
Here's some [documentation](https://superuser.com/a/1658331) about how to clear the terminal history, in PowerShell and CMD
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</Accordion>
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
---
|
||||||
|
title: "AWS IAM User"
|
||||||
|
description: "Rotated access key id and secret key of AWS IAM Users"
|
||||||
|
---
|
||||||
|
|
||||||
|
Infisical's AWS IAM User secret rotation capability lets you update the **Access key** and **Secret access key** credentials of a target IAM user from within Infisical
|
||||||
|
at a specified interval or on-demand.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
The typical workflow for using the AWS IAM User rotation strategy consists of four steps:
|
||||||
|
|
||||||
|
1. Creating the target IAM user whose credentials you wish to rotate.
|
||||||
|
2. Creating the managing IAM user used by Infisical to rotate the credentials of the target IAM user.
|
||||||
|
3. Configuring the rotation strategy in Infisical with the credentials of the managing IAM user.
|
||||||
|
4. Pressing the **Rotate** button in the Infisical dashboard to trigger the rotation of the target IAM user's credentials. The strategy can also be configured to rotate the credentials automatically at a specified interval.
|
||||||
|
|
||||||
|
In the following steps, we explore the end-to-end workflow for setting up this strategy in Infisical.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Create the target IAM user">
|
||||||
|
To begin, create an IAM user whose credentials you wish to rotate. If you already have an IAM user,
|
||||||
|
then you can skip this step.
|
||||||
|
</Step>
|
||||||
|
<Step title="Create the managing IAM user">
|
||||||
|
Next, create another IAM user to be used by Infisical to rotate the credentials of the IAM user in the previous step.
|
||||||
|
|
||||||
|
2.1. In your AWS console, head to IAM > Access management > Users and press **Create user**.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
2.2. Next, give the user a username like **infisical-rotation-manager** and press **Next**.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
2.3. Next, in the **Set permissions** step, select **Attach policies directly** and then press **Create policy**.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
2.4. Next, in the **Policy editor**, paste the following JSON and press **Next**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Sid": "VisualEditor0",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"iam:DeleteAccessKey",
|
||||||
|
"iam:GetAccessKeyLastUsed",
|
||||||
|
"iam:CreateAccessKey"
|
||||||
|
],
|
||||||
|
"Resource": "*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
The IAM policy above uses the wildcard option in Resource: "*".
|
||||||
|
|
||||||
|
You may want to restrict the policy to a specific path, and make any adjustments as necessary, to control access for the managing user in production.
|
||||||
|
|
||||||
|
Read more about this [here](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/).
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
In the **Review and create** step, give the policy a name like **infisical-rotation-manager**, press **Create policy** to finish creating the policy.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
2.5. Back in the **Set permissions** step from step 2.3, refresh the policy list and search for the policy you just created from step 2.4.
|
||||||
|
|
||||||
|
Select the policy and press **Next**.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
In the **Review and create** step, press **Create user** to finish creating the IAM user.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
2.5. Having created the user, head to its Security credentials > Access keys and press **Create access key**.
|
||||||
|
|
||||||
|
Follow the subsequent steps to create the **access key** and **secret access key** credential pair for the user.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
At the end of the flow, copy the **Access key** and **Secret access key** to use when configuring the AWS IAM User rotation strategy back in Infisical next.
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Configure the AWS IAM User secret rotation strategy in Infisical">
|
||||||
|
3.1. Back in Infisical, head to the Project > Secrets > Environment and path where you want the rotated AWS IAM credentials to appear and create two placeholder secrets.
|
||||||
|
|
||||||
|
In this example, we'll create two secrets called `AWS_ACCESS_KEY` and `AWS_SECRET_ACCESS_KEY`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3.2. Next, in the **Secret Rotation** tab, press on the **AWS IAM** tile to configure the AWS IAM User rotation strategy.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3.3. Input the configuration details for the AWS IAM User rotation strategy obtained from steps 1 and 2:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Here's some guidance on each field:
|
||||||
|
|
||||||
|
- Manager User Access Key: The managing IAM user's access key from step 2.5.
|
||||||
|
- Manager User Secret Key: The managing IAM user's secret access key from step 2.5.
|
||||||
|
- Manager User AWS Region: The [AWS region](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html) for Infisical to make requests to such as `us-east-1`.
|
||||||
|
- IAM Username: The IAM username of the user from step 1.
|
||||||
|
|
||||||
|
Next, specify the output secret mappings configuration for the rotated AWS IAM credentials; this is the secrets whose values will be replaced with new credentials after each rotation.
|
||||||
|
Here, you can also specify a rotation interval for the credentials to be automatically rotated periodically.
|
||||||
|
|
||||||
|
In this example, we want to map the output of the rotated AWS IAM credentials to the secrets that we created in step 3.1 (i.e. `AWS_ACCESS_KEY` and `AWS_SECRET_ACCESS_KEY`).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Finally, press **Submit** to create the secret rotation strategy.
|
||||||
|
</Step>
|
||||||
|
<Step title="Rotate secrets in Infisical">
|
||||||
|
You should now see the AWS IAM User rotation strategy listed in the **Secret Rotation** tab.
|
||||||
|
|
||||||
|
To manually trigger a rotation, you can press the **Rotate** button on the strategy.
|
||||||
|
Once triggered, the secrets in step 3.1 should be updated with new rotated credential values.
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
**FAQ**
|
||||||
|
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion title="Why are my AWS IAM credentials not rotating?">
|
||||||
|
There are a few reasons for why this might happen:
|
||||||
|
|
||||||
|
- The strategy configuration is invalid (e.g. the managing IAM user's credentials are incorrect, the target IAM username is incorrect, etc.).
|
||||||
|
- The managing IAM user is insufficently permissioned to rotate the credentials of the target IAM user. For instance, you may have setup [paths](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) for the managing IAM user and the policy does not have the necessary permissions to rotate the credentials.
|
||||||
|
- The target IAM user already has 2 access keys configured in AWS; you should delete one of the access keys to allow for rotation.
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
|
After Width: | Height: | Size: 471 KiB |
|
After Width: | Height: | Size: 494 KiB |
|
After Width: | Height: | Size: 532 KiB |
|
After Width: | Height: | Size: 412 KiB |
|
After Width: | Height: | Size: 341 KiB |
|
After Width: | Height: | Size: 361 KiB |
|
After Width: | Height: | Size: 434 KiB |
|
After Width: | Height: | Size: 402 KiB |
|
After Width: | Height: | Size: 288 KiB |
|
After Width: | Height: | Size: 367 KiB |
|
After Width: | Height: | Size: 343 KiB |
|
After Width: | Height: | Size: 311 KiB |
|
After Width: | Height: | Size: 597 KiB |
|
After Width: | Height: | Size: 691 KiB |
@@ -134,7 +134,8 @@
|
|||||||
"documentation/platform/secret-rotation/overview",
|
"documentation/platform/secret-rotation/overview",
|
||||||
"documentation/platform/secret-rotation/sendgrid",
|
"documentation/platform/secret-rotation/sendgrid",
|
||||||
"documentation/platform/secret-rotation/postgres",
|
"documentation/platform/secret-rotation/postgres",
|
||||||
"documentation/platform/secret-rotation/mysql"
|
"documentation/platform/secret-rotation/mysql",
|
||||||
|
"documentation/platform/secret-rotation/aws-iam"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg width="1306" height="2500" viewBox="0 0 256 490" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M21 165.75l-21 6.856 21.75 2.519-.75-9.375M19.955 206.806L128 213.714l108.045-6.908L128 185.75 19.955 206.806M234.5 175.125l21.5-2.519-21.5-5.731v8.25" fill="#3C4929"/><path d="M157.387 352.929l56.606 13.396-56.756 17.116.15-30.512" fill="#B7CA9D"/><path d="M19.955 92.221V54.019L128 0l.482.405-.248 48.496-.234.102-.405 1.117-59.098 23.856-.542 84.037 31.452-5.29L128 147.002V490.03l-32.369-16.177v-45.771l-28.354-11.338V202.069l-47.322 4.737v-38.195L0 172.606v-72.408l19.955-7.977" fill="#4B612C"/><path d="M99.408 152.727l-32.131 6.424V73.28l32.131 10.018v69.429M183.925 27.959l52.106 26.06v38.202L256 100.198V172.6l-19.969-3.989v38.195l-25.441-2.538-21.881-2.199v42.939h47.336v39.284l-21.997 1.974v39.611l-53.692 10.672v45.77l53.57-15.899.122 40.38-53.692 21.282v45.771L128 490.03V147.002l28.572 5.71 30.583 4.038V73.966l-58.338-22.498-.817-2.465V0l55.925 27.959" fill="#759C3E"/><path d="M160.356 61.941L128 49.01 67.277 73.28l32.131 10.018 60.948-21.357" fill="#3C4929"/><path d="M67.277 73.28L128 49.01l12.775 5.104 19.581 7.827 28.353 11.353-1.515 1.541-28.876 8.991-1.74-.528L128 73.28 99.408 83.298 67.277 73.28" fill="#3C4929"/><path d="M156.578 83.298l32.131-10.004v85.864l-32.131-6.446V83.298" fill="#4B612C"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -56,18 +56,20 @@ export default function NavHeader({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row items-center pt-6">
|
<div className="flex flex-row items-center pt-6">
|
||||||
<div className="mr-2 flex h-5 w-5 items-center justify-center rounded-md bg-primary text-sm text-black">
|
<div className="mr-2 flex h-5 w-5 items-center justify-center rounded-md bg-primary text-sm text-black min-w-[1.25rem]">
|
||||||
{currentOrg?.name?.charAt(0)}
|
{currentOrg?.name?.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
<Link passHref legacyBehavior href={`/org/${currentOrg?.id}/overview`}>
|
<Link passHref legacyBehavior href={`/org/${currentOrg?.id}/overview`}>
|
||||||
<a className="pl-0.5 text-sm font-semibold text-primary/80 hover:text-primary">
|
<a className="truncate pl-0.5 text-sm font-semibold text-primary/80 hover:text-primary">
|
||||||
{currentOrg?.name}
|
{currentOrg?.name}
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
{isProjectRelated && (
|
{isProjectRelated && (
|
||||||
<>
|
<>
|
||||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-xs text-gray-400" />
|
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-xs text-gray-400" />
|
||||||
<div className="text-sm font-semibold text-bunker-300">{currentWorkspace?.name}</div>
|
<div className="truncate text-sm font-semibold text-bunker-300">
|
||||||
|
{currentWorkspace?.name}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{isOrganizationRelated && (
|
{isOrganizationRelated && (
|
||||||
|
|||||||
@@ -245,6 +245,7 @@ export default function UserInfoStep({
|
|||||||
placeholder="Infisical"
|
placeholder="Infisical"
|
||||||
onChange={(e) => setOrganizationName(e.target.value)}
|
onChange={(e) => setOrganizationName(e.target.value)}
|
||||||
value={organizationName}
|
value={organizationName}
|
||||||
|
maxLength={64}
|
||||||
isRequired
|
isRequired
|
||||||
className="h-12"
|
className="h-12"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const Popover = PopoverPrimitive.Root;
|
|||||||
|
|
||||||
export type PopoverContentProps = {
|
export type PopoverContentProps = {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
arrowClassName?: string;
|
||||||
hideCloseBtn?: boolean;
|
hideCloseBtn?: boolean;
|
||||||
} & PopoverPrimitive.PopoverContentProps;
|
} & PopoverPrimitive.PopoverContentProps;
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ export const PopoverContent = ({
|
|||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
hideCloseBtn,
|
hideCloseBtn,
|
||||||
|
arrowClassName,
|
||||||
...props
|
...props
|
||||||
}: PopoverContentProps) => (
|
}: PopoverContentProps) => (
|
||||||
<PopoverPrimitive.Portal>
|
<PopoverPrimitive.Portal>
|
||||||
@@ -48,7 +50,7 @@ export const PopoverContent = ({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</PopoverPrimitive.Close>
|
</PopoverPrimitive.Close>
|
||||||
)}
|
)}
|
||||||
<PopoverPrimitive.Arrow className="fill-inherit" />
|
<PopoverPrimitive.Arrow className={twMerge("fill-inherit", arrowClassName)} />
|
||||||
</PopoverPrimitive.Content>
|
</PopoverPrimitive.Content>
|
||||||
</PopoverPrimitive.Portal>
|
</PopoverPrimitive.Portal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TOrgRole, TProjectRole } from "../roles/types";
|
import { TOrgRole } from "../roles/types";
|
||||||
import { IdentityAuthMethod } from "./enums";
|
import { IdentityAuthMethod } from "./enums";
|
||||||
|
|
||||||
export type IdentityTrustedIp = {
|
export type IdentityTrustedIp = {
|
||||||
@@ -29,9 +29,18 @@ export type IdentityMembershipOrg = {
|
|||||||
export type IdentityMembership = {
|
export type IdentityMembership = {
|
||||||
id: string;
|
id: string;
|
||||||
identity: Identity;
|
identity: Identity;
|
||||||
organization: string;
|
roles: {
|
||||||
role: "admin" | "member" | "viewer" | "no-access" | "custom";
|
id: string;
|
||||||
customRole?: TProjectRole;
|
role: "owner" | "admin" | "member" | "no-access" | "custom";
|
||||||
|
customRoleId: string;
|
||||||
|
customRoleName: string;
|
||||||
|
customRoleSlug: string;
|
||||||
|
isTemporary: boolean;
|
||||||
|
temporaryMode: string | null;
|
||||||
|
temporaryRange: string | null;
|
||||||
|
temporaryAccessStartTime: string | null;
|
||||||
|
temporaryAccessEndTime: string | null;
|
||||||
|
}[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -64,7 +64,33 @@ export type TProjectMembership = {
|
|||||||
roleId: string;
|
roleId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TWorkspaceUser = OrgUser;
|
export type TWorkspaceUser = {
|
||||||
|
id: string;
|
||||||
|
user: {
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
id: string;
|
||||||
|
publicKey: string;
|
||||||
|
};
|
||||||
|
inviteEmail: string;
|
||||||
|
organization: string;
|
||||||
|
roles: {
|
||||||
|
id: string;
|
||||||
|
role: "owner" | "admin" | "member" | "no-access" | "custom";
|
||||||
|
customRoleId: string;
|
||||||
|
customRoleName: string;
|
||||||
|
customRoleSlug: string;
|
||||||
|
isTemporary: boolean;
|
||||||
|
temporaryMode: string | null;
|
||||||
|
temporaryRange: string | null;
|
||||||
|
temporaryAccessStartTime: string | null;
|
||||||
|
temporaryAccessEndTime: string | null;
|
||||||
|
}[];
|
||||||
|
status: "invited" | "accepted" | "verified" | "completed";
|
||||||
|
deniedPermissions: any[];
|
||||||
|
};
|
||||||
|
|
||||||
export type AddUserToWsDTOE2EE = {
|
export type AddUserToWsDTOE2EE = {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import {
|
|||||||
RenameWorkspaceDTO,
|
RenameWorkspaceDTO,
|
||||||
TGetUpgradeProjectStatusDTO,
|
TGetUpgradeProjectStatusDTO,
|
||||||
ToggleAutoCapitalizationDTO,
|
ToggleAutoCapitalizationDTO,
|
||||||
|
TUpdateWorkspaceIdentityRoleDTO,
|
||||||
|
TUpdateWorkspaceUserRoleDTO,
|
||||||
UpdateEnvironmentDTO,
|
UpdateEnvironmentDTO,
|
||||||
Workspace
|
Workspace
|
||||||
} from "./types";
|
} from "./types";
|
||||||
@@ -340,27 +342,19 @@ export const useDeleteUserFromWorkspace = () => {
|
|||||||
export const useUpdateUserWorkspaceRole = () => {
|
export const useUpdateUserWorkspaceRole = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({
|
mutationFn: async ({ membershipId, roles, workspaceId }: TUpdateWorkspaceUserRoleDTO) => {
|
||||||
membershipId,
|
|
||||||
role,
|
|
||||||
workspaceId
|
|
||||||
}: {
|
|
||||||
membershipId: string;
|
|
||||||
role: string;
|
|
||||||
workspaceId: string;
|
|
||||||
}) => {
|
|
||||||
const {
|
const {
|
||||||
data: { membership }
|
data: { membership }
|
||||||
} = await apiRequest.patch<{ membership: { projectId: string } }>(
|
} = await apiRequest.patch<{ membership: { projectId: string } }>(
|
||||||
`/api/v1/workspace/${workspaceId}/memberships/${membershipId}`,
|
`/api/v1/workspace/${workspaceId}/memberships/${membershipId}`,
|
||||||
{
|
{
|
||||||
role
|
roles
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return membership;
|
return membership;
|
||||||
},
|
},
|
||||||
onSuccess: (res) => {
|
onSuccess: (_, { workspaceId }) => {
|
||||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(res.projectId));
|
queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(workspaceId));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -400,18 +394,14 @@ export const useUpdateIdentityWorkspaceRole = () => {
|
|||||||
mutationFn: async ({
|
mutationFn: async ({
|
||||||
identityId,
|
identityId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
role
|
roles
|
||||||
}: {
|
}:TUpdateWorkspaceIdentityRoleDTO)=> {
|
||||||
identityId: string;
|
|
||||||
workspaceId: string;
|
|
||||||
role?: string;
|
|
||||||
}) => {
|
|
||||||
const {
|
const {
|
||||||
data: { identityMembership }
|
data: { identityMembership }
|
||||||
} = await apiRequest.patch(
|
} = await apiRequest.patch(
|
||||||
`/api/v2/workspace/${workspaceId}/identity-memberships/${identityId}`,
|
`/api/v2/workspace/${workspaceId}/identity-memberships/${identityId}`,
|
||||||
{
|
{
|
||||||
role
|
roles
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ export enum ProjectVersion {
|
|||||||
V2 = 2
|
V2 = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProjectUserMembershipTemporaryMode {
|
||||||
|
Relative = "relative"
|
||||||
|
}
|
||||||
|
|
||||||
export type Workspace = {
|
export type Workspace = {
|
||||||
__v: number;
|
__v: number;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -72,3 +76,39 @@ export type UpdateEnvironmentDTO = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type DeleteEnvironmentDTO = { workspaceId: string; id: string };
|
export type DeleteEnvironmentDTO = { workspaceId: string; id: string };
|
||||||
|
|
||||||
|
export type TUpdateWorkspaceUserRoleDTO = {
|
||||||
|
membershipId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
roles: (
|
||||||
|
| {
|
||||||
|
role: string;
|
||||||
|
isTemporary?: false;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
role: string;
|
||||||
|
isTemporary: true;
|
||||||
|
temporaryMode: ProjectUserMembershipTemporaryMode;
|
||||||
|
temporaryRange: string;
|
||||||
|
temporaryAccessStartTime: string;
|
||||||
|
}
|
||||||
|
)[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateWorkspaceIdentityRoleDTO = {
|
||||||
|
identityId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
roles: (
|
||||||
|
| {
|
||||||
|
role: string;
|
||||||
|
isTemporary?: false;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
role: string;
|
||||||
|
isTemporary: true;
|
||||||
|
temporaryMode: ProjectUserMembershipTemporaryMode;
|
||||||
|
temporaryRange: string;
|
||||||
|
temporaryAccessStartTime: string;
|
||||||
|
}
|
||||||
|
)[];
|
||||||
|
};
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
@@ -99,7 +100,7 @@ const supportOptions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const formSchema = yup.object({
|
const formSchema = yup.object({
|
||||||
name: yup.string().required().label("Project Name").trim(),
|
name: yup.string().required().label("Project Name").trim().max(64, "Too long, maximum length is 64 characters"),
|
||||||
addMembers: yup.bool().required().label("Add Members")
|
addMembers: yup.bool().required().label("Add Members")
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -273,13 +274,16 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild className="data-[state=open]:bg-mineshaft-600">
|
<DropdownMenuTrigger
|
||||||
|
asChild
|
||||||
|
className="max-w-[160px] data-[state=open]:bg-mineshaft-600"
|
||||||
|
>
|
||||||
<div className="mr-auto flex items-center rounded-md py-1.5 pl-1.5 pr-2 hover:bg-mineshaft-600">
|
<div className="mr-auto flex items-center rounded-md py-1.5 pl-1.5 pr-2 hover:bg-mineshaft-600">
|
||||||
<div className="flex h-5 w-5 items-center justify-center rounded-md bg-primary text-sm">
|
<div className="flex h-5 w-5 min-w-[20px] items-center justify-center rounded-md bg-primary text-sm">
|
||||||
{currentOrg?.name.charAt(0)}
|
{currentOrg?.name.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="overflow-hidden text-ellipsis pl-2 text-sm text-mineshaft-100"
|
className="overflow-hidden truncate text-ellipsis pl-2 text-sm text-mineshaft-100"
|
||||||
style={{ maxWidth: "140px" }}
|
style={{ maxWidth: "140px" }}
|
||||||
>
|
>
|
||||||
{currentOrg?.name}
|
{currentOrg?.name}
|
||||||
@@ -323,7 +327,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full max-w-[150px] items-center justify-between truncate">
|
||||||
{org.name}
|
{org.name}
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -422,7 +426,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
<Select
|
<Select
|
||||||
defaultValue={currentWorkspace?.id}
|
defaultValue={currentWorkspace?.id}
|
||||||
value={currentWorkspace?.id}
|
value={currentWorkspace?.id}
|
||||||
className="w-full truncate bg-mineshaft-600 py-2.5 font-medium"
|
className="w-full [&>*:first-child]:truncate bg-mineshaft-600 py-2.5 font-medium"
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
router.push(`/project/${value}/secrets/overview`);
|
router.push(`/project/${value}/secrets/overview`);
|
||||||
localStorage.setItem("projectData.id", value);
|
localStorage.setItem("projectData.id", value);
|
||||||
@@ -437,7 +441,10 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
<SelectItem
|
<SelectItem
|
||||||
key={`ws-layout-list-${id}`}
|
key={`ws-layout-list-${id}`}
|
||||||
value={id}
|
value={id}
|
||||||
className={`${currentWorkspace?.id === id && "bg-mineshaft-600"}`}
|
className={twMerge(
|
||||||
|
currentWorkspace?.id === id && "bg-mineshaft-600",
|
||||||
|
"truncate"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Sorts an array of items into groups. The return value is a map where the keys are
|
||||||
|
* the group ids the given getGroupId function produced and the value is an array of
|
||||||
|
* each item in that group.
|
||||||
|
*/
|
||||||
|
export const groupBy = <T, Key extends string | number | symbol>(
|
||||||
|
array: readonly T[],
|
||||||
|
getGroupId: (item: T) => Key
|
||||||
|
): Record<Key, T[]> =>
|
||||||
|
array.reduce((acc, item) => {
|
||||||
|
const groupId = getGroupId(item);
|
||||||
|
if (!acc[groupId]) acc[groupId] = [];
|
||||||
|
acc[groupId].push(item);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<Key, T[]>);
|
||||||
@@ -453,7 +453,7 @@ const LearningItemSquare = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formSchema = yup.object({
|
const formSchema = yup.object({
|
||||||
name: yup.string().required().label("Project Name").trim(),
|
name: yup.string().required().label("Project Name").trim().max(64, "Too long, maximum length is 64 characters"),
|
||||||
addMembers: yup.bool().required().label("Add Members")
|
addMembers: yup.bool().required().label("Add Members")
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -633,7 +633,7 @@ const OrganizationPage = withPermission(
|
|||||||
key={workspace.id}
|
key={workspace.id}
|
||||||
className="min-w-72 flex h-40 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
|
className="min-w-72 flex h-40 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
|
||||||
>
|
>
|
||||||
<div className="mt-0 text-lg text-mineshaft-100">{workspace.name}</div>
|
<div className="mt-0 truncate text-lg text-mineshaft-100">{workspace.name}</div>
|
||||||
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
|
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
|
||||||
{workspace.environments?.length || 0} environments
|
{workspace.environments?.length || 0} environments
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,12 +5,7 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
|||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||||
import { withProjectPermission } from "@app/hoc";
|
import { withProjectPermission } from "@app/hoc";
|
||||||
|
|
||||||
import {
|
import { IdentityTab, MemberListTab, ProjectRoleListTab, ServiceTokenTab } from "./components";
|
||||||
IdentityTab,
|
|
||||||
MemberListTab,
|
|
||||||
ProjectRoleListTab,
|
|
||||||
ServiceTokenTab
|
|
||||||
} from "./components";
|
|
||||||
|
|
||||||
enum TabSections {
|
enum TabSections {
|
||||||
Member = "members",
|
Member = "members",
|
||||||
@@ -23,19 +18,14 @@ export const MembersPage = withProjectPermission(
|
|||||||
() => {
|
() => {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||||
<div className="mb-6 w-full py-6 px-6 max-w-7xl mx-auto">
|
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
|
||||||
<p className="mr-4 mb-4 text-3xl font-semibold text-white">
|
<p className="mr-4 mb-4 text-3xl font-semibold text-white">Project Access Control</p>
|
||||||
Project Access Control
|
|
||||||
</p>
|
|
||||||
<Tabs defaultValue={TabSections.Member}>
|
<Tabs defaultValue={TabSections.Member}>
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab value={TabSections.Member}>People</Tab>
|
<Tab value={TabSections.Member}>People</Tab>
|
||||||
<Tab value={TabSections.Identities}>
|
<Tab value={TabSections.Identities}>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<p>Machine Identities</p>
|
<p>Machine Identities</p>
|
||||||
<div className="ml-2 rounded-md text-yellow text-sm inline-block bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] opacity-80 hover:opacity-100 cursor-default">
|
|
||||||
New
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab value={TabSections.ServiceTokens}>Service Tokens</Tab>
|
<Tab value={TabSections.ServiceTokens}>Service Tokens</Tab>
|
||||||
|
|||||||
@@ -0,0 +1,459 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faCheck, faClock, faEdit, faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
FormControl,
|
||||||
|
HoverCard,
|
||||||
|
HoverCardContent,
|
||||||
|
HoverCardTrigger,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
Spinner,
|
||||||
|
Tag,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { useGetProjectRoles, useUpdateIdentityWorkspaceRole } from "@app/hooks/api";
|
||||||
|
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||||
|
import { TWorkspaceUser } from "@app/hooks/api/types";
|
||||||
|
import { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/workspace/types";
|
||||||
|
import { groupBy } from "@app/lib/fn/array";
|
||||||
|
|
||||||
|
const temporaryRoleFormSchema = z.object({
|
||||||
|
temporaryRange: z.string().min(1, "Required")
|
||||||
|
});
|
||||||
|
|
||||||
|
type TTemporaryRoleFormSchema = z.infer<typeof temporaryRoleFormSchema>;
|
||||||
|
|
||||||
|
type TTemporaryRoleFormProps = {
|
||||||
|
temporaryConfig?: {
|
||||||
|
isTemporary?: boolean;
|
||||||
|
temporaryAccessEndTime?: string | null;
|
||||||
|
temporaryAccessStartTime?: string | null;
|
||||||
|
temporaryRange?: string | null;
|
||||||
|
};
|
||||||
|
onSetTemporary: (data: { temporaryRange: string; temporaryAccessStartTime?: string }) => void;
|
||||||
|
onRemoveTemporary: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const IdentityTemporaryRoleForm = ({
|
||||||
|
temporaryConfig: defaultValues = {},
|
||||||
|
onSetTemporary,
|
||||||
|
onRemoveTemporary
|
||||||
|
}: TTemporaryRoleFormProps) => {
|
||||||
|
const { popUp, handlePopUpToggle } = usePopUp(["setTempRole"] as const);
|
||||||
|
const { control, handleSubmit } = useForm<TTemporaryRoleFormSchema>({
|
||||||
|
resolver: zodResolver(temporaryRoleFormSchema),
|
||||||
|
values: {
|
||||||
|
temporaryRange: defaultValues.temporaryRange || "1h"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const isTemporaryFieldValue = defaultValues.isTemporary;
|
||||||
|
const isExpired =
|
||||||
|
isTemporaryFieldValue && new Date() > new Date(defaultValues.temporaryAccessEndTime || "");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
open={popUp.setTempRole.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("setTempRole", isOpen);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<IconButton ariaLabel="role-temp" variant="plain" size="md">
|
||||||
|
<Tooltip content={isExpired ? "Access Expired" : "Grant Temporary Access"}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faClock}
|
||||||
|
className={twMerge(
|
||||||
|
isTemporaryFieldValue && "text-primary",
|
||||||
|
isExpired && "text-red-600"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
arrowClassName="fill-gray-600"
|
||||||
|
side="right"
|
||||||
|
sideOffset={12}
|
||||||
|
hideCloseBtn
|
||||||
|
className="border border-gray-600 pt-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
<div className="border-b border-b-gray-700 pb-2 text-sm text-mineshaft-300">
|
||||||
|
Set Role Temporarily
|
||||||
|
</div>
|
||||||
|
{isExpired && <Tag colorSchema="red">Expired</Tag>}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="temporaryRange"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Validity"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
helperText={
|
||||||
|
<span>
|
||||||
|
1m, 2h, 3d.{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/vercel/ms?tab=readme-ov-file#examples"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary-700"
|
||||||
|
>
|
||||||
|
More
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{isTemporaryFieldValue && (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
type="submit"
|
||||||
|
onClick={() => {
|
||||||
|
handleSubmit(({ temporaryRange }) => {
|
||||||
|
onSetTemporary({
|
||||||
|
temporaryRange,
|
||||||
|
temporaryAccessStartTime: new Date().toISOString()
|
||||||
|
});
|
||||||
|
handlePopUpToggle("setTempRole");
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Restart
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{!isTemporaryFieldValue ? (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
type="submit"
|
||||||
|
onClick={() =>
|
||||||
|
handleSubmit(({ temporaryRange }) => {
|
||||||
|
onSetTemporary({
|
||||||
|
temporaryRange,
|
||||||
|
temporaryAccessStartTime:
|
||||||
|
defaultValues.temporaryAccessStartTime || new Date().toISOString()
|
||||||
|
});
|
||||||
|
handlePopUpToggle("setTempRole");
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Grant access
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="outline_bg"
|
||||||
|
colorSchema="danger"
|
||||||
|
onClick={() => {
|
||||||
|
onRemoveTemporary();
|
||||||
|
handlePopUpToggle("setTempRole");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Revoke Access
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formSchema = z.record(
|
||||||
|
z.object({
|
||||||
|
isChecked: z.boolean().optional(),
|
||||||
|
temporaryAccess: z.union([
|
||||||
|
z.object({
|
||||||
|
isTemporary: z.literal(true),
|
||||||
|
temporaryRange: z.string().min(1),
|
||||||
|
temporaryAccessStartTime: z.string().datetime(),
|
||||||
|
temporaryAccessEndTime: z.string().datetime().nullable().optional()
|
||||||
|
}),
|
||||||
|
z.boolean()
|
||||||
|
])
|
||||||
|
})
|
||||||
|
);
|
||||||
|
type TForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export type TMemberRolesProp = {
|
||||||
|
disableEdit?: boolean;
|
||||||
|
identityId: string;
|
||||||
|
roles: TWorkspaceUser["roles"];
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
|
||||||
|
|
||||||
|
export const IdentityRoles = ({
|
||||||
|
roles = [],
|
||||||
|
disableEdit = false,
|
||||||
|
identityId
|
||||||
|
}: TMemberRolesProp) => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const { popUp, handlePopUpToggle } = usePopUp(["editRole"] as const);
|
||||||
|
const [searchRoles, setSearchRoles] = useState("");
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
reset,
|
||||||
|
setValue,
|
||||||
|
formState: { isSubmitting, isDirty }
|
||||||
|
} = useForm<TForm>({
|
||||||
|
resolver: zodResolver(formSchema)
|
||||||
|
});
|
||||||
|
|
||||||
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
|
|
||||||
|
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId);
|
||||||
|
const userRolesGroupBySlug = groupBy(roles, ({ customRoleSlug, role }) => customRoleSlug || role);
|
||||||
|
|
||||||
|
const updateIdentityWorkspaceRole = useUpdateIdentityWorkspaceRole();
|
||||||
|
|
||||||
|
const handleRoleUpdate = async (data: TForm) => {
|
||||||
|
const selectedRoles = Object.keys(data)
|
||||||
|
.filter((el) => Boolean(data[el].isChecked))
|
||||||
|
.map((el) => {
|
||||||
|
const isTemporary = Boolean(data[el].temporaryAccess);
|
||||||
|
if (!isTemporary) {
|
||||||
|
return { role: el, isTemporary: false as const };
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempCfg = data[el].temporaryAccess as {
|
||||||
|
temporaryRange: string;
|
||||||
|
temporaryAccessStartTime: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
role: el,
|
||||||
|
isTemporary: true as const,
|
||||||
|
temporaryMode: ProjectUserMembershipTemporaryMode.Relative,
|
||||||
|
temporaryRange: tempCfg.temporaryRange,
|
||||||
|
temporaryAccessStartTime: tempCfg.temporaryAccessStartTime
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateIdentityWorkspaceRole.mutateAsync({
|
||||||
|
workspaceId,
|
||||||
|
identityId,
|
||||||
|
roles: selectedRoles
|
||||||
|
});
|
||||||
|
createNotification({ text: "Successfully updated identity role", type: "success" });
|
||||||
|
handlePopUpToggle("editRole");
|
||||||
|
setSearchRoles("");
|
||||||
|
} catch (err) {
|
||||||
|
createNotification({ text: "Failed to update identity role", type: "error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatRoleName = (role: string, customRoleName?: string) => {
|
||||||
|
if (role === ProjectMembershipRole.Custom) return customRoleName;
|
||||||
|
if (role === ProjectMembershipRole.Member) return "Developer";
|
||||||
|
return role;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{roles
|
||||||
|
.slice(0, MAX_ROLES_TO_BE_SHOWN_IN_TABLE)
|
||||||
|
.map(({ role, customRoleName, id, isTemporary, temporaryAccessEndTime }) => {
|
||||||
|
const isExpired = new Date() > new Date(temporaryAccessEndTime || ("" as string));
|
||||||
|
return (
|
||||||
|
<Tag key={id} className="capitalize">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div>{formatRoleName(role, customRoleName)}</div>
|
||||||
|
{isTemporary && (
|
||||||
|
<div>
|
||||||
|
<Tooltip content={isExpired ? "Expired Temporary Access" : "Temporary Access"}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faClock}
|
||||||
|
className={twMerge(isExpired && "text-red-600")}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{roles.length > MAX_ROLES_TO_BE_SHOWN_IN_TABLE && (
|
||||||
|
<HoverCard>
|
||||||
|
<HoverCardTrigger>
|
||||||
|
<Tag>+{roles.length - MAX_ROLES_TO_BE_SHOWN_IN_TABLE}</Tag>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<HoverCardContent className="border border-gray-700 bg-mineshaft-800 p-4">
|
||||||
|
{roles
|
||||||
|
.slice(MAX_ROLES_TO_BE_SHOWN_IN_TABLE)
|
||||||
|
.map(({ role, customRoleName, id, isTemporary, temporaryAccessEndTime }) => {
|
||||||
|
const isExpired = new Date() > new Date(temporaryAccessEndTime || ("" as string));
|
||||||
|
return (
|
||||||
|
<Tag key={id} className="capitalize">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div>{formatRoleName(role, customRoleName)}</div>
|
||||||
|
{isTemporary && (
|
||||||
|
<div>
|
||||||
|
<Tooltip
|
||||||
|
content={isExpired ? "Expired Temporary Access" : "Temporary Access"}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faClock}
|
||||||
|
className={twMerge(
|
||||||
|
new Date() > new Date(temporaryAccessEndTime as string) &&
|
||||||
|
"text-red-600"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
})}{" "}
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCard>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<Popover
|
||||||
|
open={popUp.editRole.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("editRole", isOpen);
|
||||||
|
reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!disableEdit && (
|
||||||
|
<PopoverTrigger>
|
||||||
|
<IconButton size="sm" variant="plain" ariaLabel="update">
|
||||||
|
<FontAwesomeIcon icon={faEdit} />
|
||||||
|
</IconButton>
|
||||||
|
</PopoverTrigger>
|
||||||
|
)}
|
||||||
|
<PopoverContent hideCloseBtn className="pt-4">
|
||||||
|
{isRolesLoading ? (
|
||||||
|
<div className="flex h-8 w-full items-center justify-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleSubmit(handleRoleUpdate)} id="role-update-form">
|
||||||
|
<div className="thin-scrollbar max-h-80 space-y-4 overflow-y-auto">
|
||||||
|
{projectRoles
|
||||||
|
?.filter(
|
||||||
|
({ name, slug }) =>
|
||||||
|
name.toLowerCase().includes(searchRoles.toLowerCase()) ||
|
||||||
|
slug.toLowerCase().includes(searchRoles.toLowerCase())
|
||||||
|
)
|
||||||
|
?.map(({ id, name, slug }) => {
|
||||||
|
const userProjectRoleDetails = userRolesGroupBySlug?.[slug]?.[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={id} className="flex items-center space-x-4">
|
||||||
|
<div className="flex-grow">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue={Boolean(userProjectRoleDetails?.id)}
|
||||||
|
name={`${slug}.isChecked`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Checkbox
|
||||||
|
id={slug}
|
||||||
|
isChecked={field.value}
|
||||||
|
onCheckedChange={(isChecked) => {
|
||||||
|
field.onChange(isChecked);
|
||||||
|
setValue(`${slug}.temporaryAccess`, false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Checkbox>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`${slug}.temporaryAccess`}
|
||||||
|
defaultValue={
|
||||||
|
userProjectRoleDetails?.isTemporary
|
||||||
|
? {
|
||||||
|
isTemporary: true,
|
||||||
|
temporaryAccessStartTime:
|
||||||
|
userProjectRoleDetails.temporaryAccessStartTime as string,
|
||||||
|
temporaryRange:
|
||||||
|
userProjectRoleDetails.temporaryRange as string,
|
||||||
|
temporaryAccessEndTime:
|
||||||
|
userProjectRoleDetails.temporaryAccessEndTime
|
||||||
|
}
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
render={({ field }) => (
|
||||||
|
<IdentityTemporaryRoleForm
|
||||||
|
temporaryConfig={
|
||||||
|
typeof field.value === "boolean"
|
||||||
|
? { isTemporary: field.value }
|
||||||
|
: field.value
|
||||||
|
}
|
||||||
|
onSetTemporary={(data) => {
|
||||||
|
setValue(`${slug}.isChecked`, true, { shouldDirty: true });
|
||||||
|
console.log(data);
|
||||||
|
field.onChange({ isTemporary: true, ...data });
|
||||||
|
}}
|
||||||
|
onRemoveTemporary={() => {
|
||||||
|
setValue(`${slug}.isChecked`, false, { shouldDirty: true });
|
||||||
|
field.onChange(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 flex items-center space-x-2 border-t border-t-gray-700 pt-3">
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
className="w-full p-1.5 pl-8"
|
||||||
|
size="xs"
|
||||||
|
value={searchRoles}
|
||||||
|
onChange={(el) => setSearchRoles(el.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faSearch} />}
|
||||||
|
placeholder="Search roles.."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
type="submit"
|
||||||
|
form="role-update-form"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faCheck} />}
|
||||||
|
isDisabled={!isDirty || isSubmitting}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,13 +2,10 @@ import { faServer, faXmark } from "@fortawesome/free-solid-svg-icons";
|
|||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
EmptyState,
|
EmptyState,
|
||||||
IconButton,
|
IconButton,
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -19,13 +16,11 @@ import {
|
|||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
import {
|
import { useGetWorkspaceIdentityMemberships } from "@app/hooks/api";
|
||||||
useGetProjectRoles,
|
|
||||||
useGetWorkspaceIdentityMemberships,
|
|
||||||
useUpdateIdentityWorkspaceRole
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { IdentityRoles } from "./IdentityRoles";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (
|
handlePopUpOpen: (
|
||||||
popUpName: keyof UsePopUpState<["deleteIdentity", "identity"]>,
|
popUpName: keyof UsePopUpState<["deleteIdentity", "identity"]>,
|
||||||
@@ -37,39 +32,9 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||||
const { createNotification } = useNotificationContext();
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
|
||||||
const { data, isLoading } = useGetWorkspaceIdentityMemberships(currentWorkspace?.id || "");
|
const { data, isLoading } = useGetWorkspaceIdentityMemberships(currentWorkspace?.id || "");
|
||||||
|
|
||||||
const { data: roles } = useGetProjectRoles(workspaceId);
|
|
||||||
|
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityWorkspaceRole();
|
|
||||||
|
|
||||||
const handleChangeRole = async ({ identityId, role }: { identityId: string; role: string }) => {
|
|
||||||
try {
|
|
||||||
await updateMutateAsync({
|
|
||||||
identityId,
|
|
||||||
workspaceId,
|
|
||||||
role
|
|
||||||
});
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text: "Successfully updated identity role",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
const error = err as any;
|
|
||||||
const text = error?.response?.data?.message ?? "Failed to update identity role";
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text,
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
@@ -86,7 +51,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
data &&
|
data &&
|
||||||
data.length > 0 &&
|
data.length > 0 &&
|
||||||
data.map(({ identity: { id, name }, role, customRole, createdAt }) => {
|
data.map(({ identity: { id, name }, roles, createdAt }) => {
|
||||||
return (
|
return (
|
||||||
<Tr className="h-10" key={`st-v3-${id}`}>
|
<Tr className="h-10" key={`st-v3-${id}`}>
|
||||||
<Td>{name}</Td>
|
<Td>{name}</Td>
|
||||||
@@ -95,28 +60,9 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={ProjectPermissionSub.Identity}
|
a={ProjectPermissionSub.Identity}
|
||||||
>
|
>
|
||||||
{(isAllowed) => {
|
{(isAllowed) => (
|
||||||
return (
|
<IdentityRoles roles={roles} disableEdit={!isAllowed} identityId={id} />
|
||||||
<Select
|
)}
|
||||||
value={role === "custom" ? (customRole?.slug as string) : role}
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
className="w-40 bg-mineshaft-600"
|
|
||||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
|
||||||
onValueChange={(selectedRole) =>
|
|
||||||
handleChangeRole({
|
|
||||||
identityId: id,
|
|
||||||
role: selectedRole
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{(roles || []).map(({ slug, name: roleName }) => (
|
|
||||||
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
|
||||||
{roleName}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -34,7 +34,6 @@ import {
|
|||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useOrganization,
|
useOrganization,
|
||||||
useSubscription,
|
|
||||||
useUser,
|
useUser,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
@@ -44,14 +43,13 @@ import {
|
|||||||
useAddUserToWsNonE2EE,
|
useAddUserToWsNonE2EE,
|
||||||
useDeleteUserFromWorkspace,
|
useDeleteUserFromWorkspace,
|
||||||
useGetOrgUsers,
|
useGetOrgUsers,
|
||||||
useGetProjectRoles,
|
|
||||||
useGetUserWsKey,
|
useGetUserWsKey,
|
||||||
useGetWorkspaceUsers,
|
useGetWorkspaceUsers
|
||||||
useUpdateUserWorkspaceRole
|
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
|
||||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
|
import { MemberRoles } from "./MemberRoles";
|
||||||
|
|
||||||
const addMemberFormSchema = z.object({
|
const addMemberFormSchema = z.object({
|
||||||
orgMembershipId: z.string().trim()
|
orgMembershipId: z.string().trim()
|
||||||
});
|
});
|
||||||
@@ -60,7 +58,6 @@ type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
|
|||||||
|
|
||||||
export const MemberListTab = () => {
|
export const MemberListTab = () => {
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
const { subscription } = useSubscription();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
@@ -71,8 +68,6 @@ export const MemberListTab = () => {
|
|||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
|
|
||||||
const { data: roles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId);
|
|
||||||
|
|
||||||
const { data: wsKey } = useGetUserWsKey(workspaceId);
|
const { data: wsKey } = useGetUserWsKey(workspaceId);
|
||||||
const { data: members, isLoading: isMembersLoading } = useGetWorkspaceUsers(workspaceId);
|
const { data: members, isLoading: isMembersLoading } = useGetWorkspaceUsers(workspaceId);
|
||||||
const { data: orgUsers } = useGetOrgUsers(orgId);
|
const { data: orgUsers } = useGetOrgUsers(orgId);
|
||||||
@@ -95,7 +90,6 @@ export const MemberListTab = () => {
|
|||||||
const { mutateAsync: addUserToWorkspace } = useAddUserToWsE2EE();
|
const { mutateAsync: addUserToWorkspace } = useAddUserToWsE2EE();
|
||||||
const { mutateAsync: addUserToWorkspaceNonE2EE } = useAddUserToWsNonE2EE();
|
const { mutateAsync: addUserToWorkspaceNonE2EE } = useAddUserToWsNonE2EE();
|
||||||
const { mutateAsync: removeUserFromWorkspace } = useDeleteUserFromWorkspace();
|
const { mutateAsync: removeUserFromWorkspace } = useDeleteUserFromWorkspace();
|
||||||
const { mutateAsync: updateUserWorkspaceRole } = useUpdateUserWorkspaceRole();
|
|
||||||
|
|
||||||
const onAddMember = async ({ orgMembershipId }: TAddMemberForm) => {
|
const onAddMember = async ({ orgMembershipId }: TAddMemberForm) => {
|
||||||
if (!currentWorkspace) return;
|
if (!currentWorkspace) return;
|
||||||
@@ -167,47 +161,6 @@ export const MemberListTab = () => {
|
|||||||
handlePopUpClose("removeMember");
|
handlePopUpClose("removeMember");
|
||||||
};
|
};
|
||||||
|
|
||||||
const isIamOwner = useMemo(
|
|
||||||
() => members?.find(({ user: u }) => userId === u?.id)?.role === "owner",
|
|
||||||
[userId, members]
|
|
||||||
);
|
|
||||||
|
|
||||||
const findRoleFromId = useCallback(
|
|
||||||
(roleId: string) => {
|
|
||||||
return (roles || []).find(({ id }) => id === roleId);
|
|
||||||
},
|
|
||||||
[roles]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onRoleChange = async (membershipId: string, role: string) => {
|
|
||||||
if (!currentOrg?.id) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const isCustomRole = !Object.values(ProjectMembershipRole).includes(
|
|
||||||
role as ProjectMembershipRole
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isCustomRole && subscription && !subscription?.rbac) {
|
|
||||||
handlePopUpOpen("upgradePlan", {
|
|
||||||
description: "You can assign custom roles to members if you upgrade your Infisical plan."
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateUserWorkspaceRole({ membershipId, role, workspaceId });
|
|
||||||
createNotification({
|
|
||||||
text: "Successfully updated user role",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to update user role",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterdUsers = useMemo(
|
const filterdUsers = useMemo(
|
||||||
() =>
|
() =>
|
||||||
members?.filter(
|
members?.filter(
|
||||||
@@ -231,8 +184,6 @@ export const MemberListTab = () => {
|
|||||||
);
|
);
|
||||||
}, [orgUsers, members]);
|
}, [orgUsers, members]);
|
||||||
|
|
||||||
const isLoading = isMembersLoading || isRolesLoading;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
@@ -269,75 +220,61 @@ export const MemberListTab = () => {
|
|||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isLoading && <TableSkeleton columns={4} innerKey="project-members" />}
|
{isMembersLoading && <TableSkeleton columns={4} innerKey="project-members" />}
|
||||||
{!isLoading &&
|
{!isMembersLoading &&
|
||||||
filterdUsers?.map(
|
filterdUsers?.map(({ user: u, inviteEmail, id: membershipId, roles }) => {
|
||||||
({ user: u, id: membershipId, roleId, role }) => {
|
const name = u ? `${u.firstName} ${u.lastName}` : "-";
|
||||||
const name = u ? `${u.firstName} ${u.lastName}` : "-";
|
const email = u?.email || inviteEmail;
|
||||||
const username = u?.username ?? "-";
|
|
||||||
return (
|
return (
|
||||||
<Tr key={`membership-${membershipId}`} className="w-full">
|
<Tr key={`membership-${membershipId}`} className="w-full">
|
||||||
<Td>{name}</Td>
|
<Td>{name}</Td>
|
||||||
<Td>{username}</Td>
|
<Td>{email}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={ProjectPermissionSub.Member}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<MemberRoles
|
||||||
|
roles={roles}
|
||||||
|
disableEdit={u.id === user?.id || !isAllowed}
|
||||||
|
onOpenUpgradeModal={(description) =>
|
||||||
|
handlePopUpOpen("upgradePlan", { description })
|
||||||
|
}
|
||||||
|
membershipId={membershipId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
{userId !== u?.id && (
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Delete}
|
||||||
a={ProjectPermissionSub.Member}
|
a={ProjectPermissionSub.Member}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Select
|
<IconButton
|
||||||
value={role === "custom" ? findRoleFromId(roleId)?.slug : role}
|
size="lg"
|
||||||
isDisabled={userId === u?.id || !isAllowed}
|
colorSchema="danger"
|
||||||
className="w-40 bg-mineshaft-600"
|
variant="plain"
|
||||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
ariaLabel="update"
|
||||||
onValueChange={(selectedRole) =>
|
className="ml-4"
|
||||||
onRoleChange(membershipId, selectedRole)
|
isDisabled={userId === u?.id || !isAllowed}
|
||||||
}
|
onClick={() => handlePopUpOpen("removeMember", { email: u.email })}
|
||||||
>
|
>
|
||||||
{(roles || [])
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
.filter(({ slug }) =>
|
</IconButton>
|
||||||
slug === "owner" ? isIamOwner || role === "owner" : true
|
|
||||||
)
|
|
||||||
.map(({ slug, name: roleName }) => (
|
|
||||||
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
|
||||||
{roleName}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</Td>
|
)}
|
||||||
<Td>
|
</Td>
|
||||||
{userId !== u?.id && (
|
</Tr>
|
||||||
<ProjectPermissionCan
|
);
|
||||||
I={ProjectPermissionActions.Delete}
|
})}
|
||||||
a={ProjectPermissionSub.Member}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<IconButton
|
|
||||||
size="lg"
|
|
||||||
colorSchema="danger"
|
|
||||||
variant="plain"
|
|
||||||
ariaLabel="update"
|
|
||||||
className="ml-4"
|
|
||||||
isDisabled={userId === u?.id || !isAllowed}
|
|
||||||
onClick={() =>
|
|
||||||
handlePopUpOpen("removeMember", { username: u.username })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
)}
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
{!isLoading && filterdUsers?.length === 0 && (
|
{!isMembersLoading && filterdUsers?.length === 0 && (
|
||||||
<EmptyState title="No project members found" icon={faUsers} />
|
<EmptyState title="No project members found" icon={faUsers} />
|
||||||
)}
|
)}
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
|||||||
@@ -0,0 +1,473 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faCheck, faClock, faEdit, faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
FormControl,
|
||||||
|
HoverCard,
|
||||||
|
HoverCardContent,
|
||||||
|
HoverCardTrigger,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
Spinner,
|
||||||
|
Tag,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useSubscription, useWorkspace } from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { useGetProjectRoles, useUpdateUserWorkspaceRole } from "@app/hooks/api";
|
||||||
|
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||||
|
import { TWorkspaceUser } from "@app/hooks/api/types";
|
||||||
|
import { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/workspace/types";
|
||||||
|
import { groupBy } from "@app/lib/fn/array";
|
||||||
|
|
||||||
|
const temporaryRoleFormSchema = z.object({
|
||||||
|
temporaryRange: z.string().min(1, "Required")
|
||||||
|
});
|
||||||
|
|
||||||
|
type TTemporaryRoleFormSchema = z.infer<typeof temporaryRoleFormSchema>;
|
||||||
|
|
||||||
|
type TTemporaryRoleFormProps = {
|
||||||
|
temporaryConfig?: {
|
||||||
|
isTemporary?: boolean;
|
||||||
|
temporaryAccessEndTime?: string | null;
|
||||||
|
temporaryAccessStartTime?: string | null;
|
||||||
|
temporaryRange?: string | null;
|
||||||
|
};
|
||||||
|
onSetTemporary: (data: { temporaryRange: string; temporaryAccessStartTime?: string }) => void;
|
||||||
|
onRemoveTemporary: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TemporaryRoleForm = ({
|
||||||
|
temporaryConfig: defaultValues = {},
|
||||||
|
onSetTemporary,
|
||||||
|
onRemoveTemporary
|
||||||
|
}: TTemporaryRoleFormProps) => {
|
||||||
|
const { popUp, handlePopUpToggle } = usePopUp(["setTempRole"] as const);
|
||||||
|
const { control, handleSubmit } = useForm<TTemporaryRoleFormSchema>({
|
||||||
|
resolver: zodResolver(temporaryRoleFormSchema),
|
||||||
|
values: {
|
||||||
|
temporaryRange: defaultValues.temporaryRange || "1h"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const isTemporaryFieldValue = defaultValues.isTemporary;
|
||||||
|
const isExpired =
|
||||||
|
isTemporaryFieldValue && new Date() > new Date(defaultValues.temporaryAccessEndTime || "");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
open={popUp.setTempRole.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("setTempRole", isOpen);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<IconButton ariaLabel="role-temp" variant="plain" size="md">
|
||||||
|
<Tooltip content={isExpired ? "Timed access expired" : "Grant timed access"}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faClock}
|
||||||
|
className={twMerge(
|
||||||
|
isTemporaryFieldValue && "text-primary",
|
||||||
|
isExpired && "text-red-600"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
arrowClassName="fill-gray-600"
|
||||||
|
side="right"
|
||||||
|
sideOffset={12}
|
||||||
|
hideCloseBtn
|
||||||
|
className="border border-gray-600 pt-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
<div className="border-b border-b-gray-700 pb-2 text-sm text-mineshaft-300">
|
||||||
|
Configure timed access
|
||||||
|
</div>
|
||||||
|
{isExpired && <Tag colorSchema="red">Expired</Tag>}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="temporaryRange"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Validity"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
helperText={
|
||||||
|
<span>
|
||||||
|
1m, 2h, 3d.{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/vercel/ms?tab=readme-ov-file#examples"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary-700"
|
||||||
|
>
|
||||||
|
More
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{isTemporaryFieldValue && (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
type="submit"
|
||||||
|
onClick={() => {
|
||||||
|
handleSubmit(({ temporaryRange }) => {
|
||||||
|
onSetTemporary({
|
||||||
|
temporaryRange,
|
||||||
|
temporaryAccessStartTime: new Date().toISOString()
|
||||||
|
});
|
||||||
|
handlePopUpToggle("setTempRole");
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Restart
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{!isTemporaryFieldValue ? (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
type="submit"
|
||||||
|
onClick={() =>
|
||||||
|
handleSubmit(({ temporaryRange }) => {
|
||||||
|
onSetTemporary({
|
||||||
|
temporaryRange,
|
||||||
|
temporaryAccessStartTime:
|
||||||
|
defaultValues.temporaryAccessStartTime || new Date().toISOString()
|
||||||
|
});
|
||||||
|
handlePopUpToggle("setTempRole");
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Grant access
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="outline_bg"
|
||||||
|
colorSchema="danger"
|
||||||
|
onClick={() => {
|
||||||
|
onRemoveTemporary();
|
||||||
|
handlePopUpToggle("setTempRole");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Revoke Access
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formSchema = z.record(
|
||||||
|
z.object({
|
||||||
|
isChecked: z.boolean().optional(),
|
||||||
|
temporaryAccess: z.union([
|
||||||
|
z.object({
|
||||||
|
isTemporary: z.literal(true),
|
||||||
|
temporaryRange: z.string().min(1),
|
||||||
|
temporaryAccessStartTime: z.string().datetime(),
|
||||||
|
temporaryAccessEndTime: z.string().datetime().nullable().optional()
|
||||||
|
}),
|
||||||
|
z.boolean()
|
||||||
|
])
|
||||||
|
})
|
||||||
|
);
|
||||||
|
type TForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export type TMemberRolesProp = {
|
||||||
|
disableEdit?: boolean;
|
||||||
|
membershipId: string;
|
||||||
|
onOpenUpgradeModal: (description: string) => void;
|
||||||
|
roles: TWorkspaceUser["roles"];
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
|
||||||
|
|
||||||
|
export const MemberRoles = ({
|
||||||
|
roles = [],
|
||||||
|
disableEdit = false,
|
||||||
|
membershipId,
|
||||||
|
onOpenUpgradeModal
|
||||||
|
}: TMemberRolesProp) => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const { popUp, handlePopUpToggle } = usePopUp(["editRole"] as const);
|
||||||
|
const [searchRoles, setSearchRoles] = useState("");
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
reset,
|
||||||
|
setValue,
|
||||||
|
formState: { isSubmitting, isDirty }
|
||||||
|
} = useForm<TForm>({
|
||||||
|
resolver: zodResolver(formSchema)
|
||||||
|
});
|
||||||
|
|
||||||
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
|
|
||||||
|
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId);
|
||||||
|
const userRolesGroupBySlug = groupBy(roles, ({ customRoleSlug, role }) => customRoleSlug || role);
|
||||||
|
|
||||||
|
const updateMembershipRole = useUpdateUserWorkspaceRole();
|
||||||
|
|
||||||
|
const handleRoleUpdate = async (data: TForm) => {
|
||||||
|
const selectedRoles = Object.keys(data)
|
||||||
|
.filter((el) => Boolean(data[el].isChecked))
|
||||||
|
.map((el) => {
|
||||||
|
const isTemporary = Boolean(data[el].temporaryAccess);
|
||||||
|
if (!isTemporary) {
|
||||||
|
return { role: el, isTemporary: false as const };
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempCfg = data[el].temporaryAccess as {
|
||||||
|
temporaryRange: string;
|
||||||
|
temporaryAccessStartTime: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
role: el,
|
||||||
|
isTemporary: true as const,
|
||||||
|
temporaryMode: ProjectUserMembershipTemporaryMode.Relative,
|
||||||
|
temporaryRange: tempCfg.temporaryRange,
|
||||||
|
temporaryAccessStartTime: tempCfg.temporaryAccessStartTime
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasCustomRoleSelected = selectedRoles.some(
|
||||||
|
(el) => !Object.values(ProjectMembershipRole).includes(el.role as ProjectMembershipRole)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasCustomRoleSelected && subscription && !subscription?.rbac) {
|
||||||
|
onOpenUpgradeModal(
|
||||||
|
"You can assign custom roles to members if you upgrade your Infisical plan."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateMembershipRole.mutateAsync({
|
||||||
|
workspaceId,
|
||||||
|
membershipId,
|
||||||
|
roles: selectedRoles
|
||||||
|
});
|
||||||
|
createNotification({ text: "Successfully updated role", type: "success" });
|
||||||
|
handlePopUpToggle("editRole");
|
||||||
|
setSearchRoles("");
|
||||||
|
} catch (err) {
|
||||||
|
createNotification({ text: "Failed to update role", type: "error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatRoleName = (role: string, customRoleName?: string) => {
|
||||||
|
if (role === ProjectMembershipRole.Custom) return customRoleName;
|
||||||
|
if (role === ProjectMembershipRole.Member) return "Developer";
|
||||||
|
return role;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{roles
|
||||||
|
.slice(0, MAX_ROLES_TO_BE_SHOWN_IN_TABLE)
|
||||||
|
.map(({ role, customRoleName, id, isTemporary, temporaryAccessEndTime }) => {
|
||||||
|
const isExpired = new Date() > new Date(temporaryAccessEndTime || ("" as string));
|
||||||
|
return (
|
||||||
|
<Tag key={id}>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div>{formatRoleName(role, customRoleName)}</div>
|
||||||
|
{isTemporary && (
|
||||||
|
<div>
|
||||||
|
<Tooltip content={isExpired ? "Timed role expired" : "Timed role access"}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faClock}
|
||||||
|
className={twMerge(isExpired && "text-red-600")}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{roles.length > MAX_ROLES_TO_BE_SHOWN_IN_TABLE && (
|
||||||
|
<HoverCard>
|
||||||
|
<HoverCardTrigger>
|
||||||
|
<Tag>+{roles.length - MAX_ROLES_TO_BE_SHOWN_IN_TABLE}</Tag>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<HoverCardContent className="border border-gray-700 bg-mineshaft-800 p-4">
|
||||||
|
{roles
|
||||||
|
.slice(MAX_ROLES_TO_BE_SHOWN_IN_TABLE)
|
||||||
|
.map(({ role, customRoleName, id, isTemporary, temporaryAccessEndTime }) => {
|
||||||
|
const isExpired = new Date() > new Date(temporaryAccessEndTime || ("" as string));
|
||||||
|
return (
|
||||||
|
<Tag key={id} className="capitalize">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div>{formatRoleName(role, customRoleName)}</div>
|
||||||
|
{isTemporary && (
|
||||||
|
<div>
|
||||||
|
<Tooltip
|
||||||
|
content={isExpired ? "Access expired" : "Temporary access"}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faClock}
|
||||||
|
className={twMerge(
|
||||||
|
new Date() > new Date(temporaryAccessEndTime as string) &&
|
||||||
|
"text-red-600"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
})}{" "}
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCard>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<Popover
|
||||||
|
open={popUp.editRole.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("editRole", isOpen);
|
||||||
|
reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!disableEdit && (
|
||||||
|
<PopoverTrigger>
|
||||||
|
<IconButton size="sm" variant="plain" ariaLabel="update">
|
||||||
|
<FontAwesomeIcon icon={faEdit} />
|
||||||
|
</IconButton>
|
||||||
|
</PopoverTrigger>
|
||||||
|
)}
|
||||||
|
<PopoverContent hideCloseBtn className="pt-4">
|
||||||
|
{isRolesLoading ? (
|
||||||
|
<div className="flex h-8 w-full items-center justify-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleSubmit(handleRoleUpdate)} id="role-update-form">
|
||||||
|
<div className="thin-scrollbar max-h-80 space-y-4 overflow-y-auto">
|
||||||
|
{projectRoles
|
||||||
|
?.filter(
|
||||||
|
({ name, slug }) =>
|
||||||
|
name.toLowerCase().includes(searchRoles.toLowerCase()) ||
|
||||||
|
slug.toLowerCase().includes(searchRoles.toLowerCase())
|
||||||
|
)
|
||||||
|
?.map(({ id, name, slug }) => {
|
||||||
|
const userProjectRoleDetails = userRolesGroupBySlug?.[slug]?.[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={id} className="flex items-center space-x-4">
|
||||||
|
<div className="flex-grow">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue={Boolean(userProjectRoleDetails?.id)}
|
||||||
|
name={`${slug}.isChecked`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Checkbox
|
||||||
|
id={slug}
|
||||||
|
isChecked={field.value}
|
||||||
|
onCheckedChange={(isChecked) => {
|
||||||
|
field.onChange(isChecked);
|
||||||
|
setValue(`${slug}.temporaryAccess`, false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Checkbox>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`${slug}.temporaryAccess`}
|
||||||
|
defaultValue={
|
||||||
|
userProjectRoleDetails?.isTemporary
|
||||||
|
? {
|
||||||
|
isTemporary: true,
|
||||||
|
temporaryAccessStartTime:
|
||||||
|
userProjectRoleDetails.temporaryAccessStartTime as string,
|
||||||
|
temporaryRange:
|
||||||
|
userProjectRoleDetails.temporaryRange as string,
|
||||||
|
temporaryAccessEndTime:
|
||||||
|
userProjectRoleDetails.temporaryAccessEndTime
|
||||||
|
}
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TemporaryRoleForm
|
||||||
|
temporaryConfig={
|
||||||
|
typeof field.value === "boolean"
|
||||||
|
? { isTemporary: field.value }
|
||||||
|
: field.value
|
||||||
|
}
|
||||||
|
onSetTemporary={(data) => {
|
||||||
|
setValue(`${slug}.isChecked`, true, { shouldDirty: true });
|
||||||
|
console.log(data);
|
||||||
|
field.onChange({ isTemporary: true, ...data });
|
||||||
|
}}
|
||||||
|
onRemoveTemporary={() => {
|
||||||
|
setValue(`${slug}.isChecked`, false, { shouldDirty: true });
|
||||||
|
field.onChange(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 flex items-center space-x-2 border-t border-t-gray-700 pt-3">
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
className="w-full p-1.5 pl-8"
|
||||||
|
size="xs"
|
||||||
|
value={searchRoles}
|
||||||
|
onChange={(el) => setSearchRoles(el.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faSearch} />}
|
||||||
|
placeholder="Search roles.."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
type="submit"
|
||||||
|
form="role-update-form"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faCheck} />}
|
||||||
|
isDisabled={!isDirty || isSubmitting}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
import { faCheckCircle, faCircle } from "@fortawesome/free-regular-svg-icons";
|
||||||
import {
|
import {
|
||||||
faAngleDown,
|
faAngleDown,
|
||||||
faArrowDown,
|
faArrowDown,
|
||||||
@@ -18,7 +18,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import NavHeader from "@app/components/navigation/NavHeader";
|
import NavHeader from "@app/components/navigation/NavHeader";
|
||||||
import { PermissionDeniedBanner, ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -108,7 +108,11 @@ export const SecretOverviewPage = () => {
|
|||||||
}, [isWorkspaceLoading, workspaceId, router.isReady]);
|
}, [isWorkspaceLoading, workspaceId, router.isReady]);
|
||||||
|
|
||||||
const userAvailableEnvs = currentWorkspace?.environments || [];
|
const userAvailableEnvs = currentWorkspace?.environments || [];
|
||||||
const [visibleEnvs, setVisisbleEnvs] = useState(userAvailableEnvs);
|
const [visibleEnvs, setVisibleEnvs] = useState(userAvailableEnvs);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setVisibleEnvs(userAvailableEnvs);
|
||||||
|
}, [userAvailableEnvs]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: secrets,
|
data: secrets,
|
||||||
@@ -208,9 +212,9 @@ export const SecretOverviewPage = () => {
|
|||||||
|
|
||||||
const handleEnvSelect = (envId: string) => {
|
const handleEnvSelect = (envId: string) => {
|
||||||
if (visibleEnvs.map((env) => env.id).includes(envId)) {
|
if (visibleEnvs.map((env) => env.id).includes(envId)) {
|
||||||
setVisisbleEnvs(visibleEnvs.filter((env) => env.id !== envId));
|
setVisibleEnvs(visibleEnvs.filter((env) => env.id !== envId));
|
||||||
} else {
|
} else {
|
||||||
setVisisbleEnvs(visibleEnvs.concat(userAvailableEnvs.filter((env) => env.id === envId)));
|
setVisibleEnvs(visibleEnvs.concat(userAvailableEnvs.filter((env) => env.id === envId)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -390,41 +394,44 @@ export const SecretOverviewPage = () => {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
|
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
|
||||||
<div className="flex flex-row items-center justify-center space-x-2">
|
<div className="flex flex-row items-center justify-center space-x-2">
|
||||||
<DropdownMenu>
|
{userAvailableEnvs.length > 0 && (
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenu>
|
||||||
<IconButton
|
<DropdownMenuTrigger asChild>
|
||||||
ariaLabel="Environments"
|
<IconButton
|
||||||
variant="plain"
|
ariaLabel="Environments"
|
||||||
size="sm"
|
variant="plain"
|
||||||
className="mr-2 flex w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 hover:border-primary/60 hover:bg-primary/10"
|
size="sm"
|
||||||
>
|
className="flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 hover:border-primary/60 hover:bg-primary/10"
|
||||||
<Tooltip content="Choose visible environments" className="mb-2">
|
>
|
||||||
<FontAwesomeIcon icon={faList} />
|
<Tooltip content="Choose visible environments" className="mb-2">
|
||||||
</Tooltip>
|
<FontAwesomeIcon icon={faList} />
|
||||||
</IconButton>
|
</Tooltip>
|
||||||
</DropdownMenuTrigger>
|
</IconButton>
|
||||||
<DropdownMenuContent align="end">
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuLabel>Choose visible environments</DropdownMenuLabel>
|
<DropdownMenuContent align="end">
|
||||||
{userAvailableEnvs.map((avaiableEnv) => {
|
<DropdownMenuLabel>Choose visible environments</DropdownMenuLabel>
|
||||||
const { id: envId, name } = avaiableEnv;
|
{userAvailableEnvs.map((availableEnv) => {
|
||||||
|
const { id: envId, name } = availableEnv;
|
||||||
|
|
||||||
const isEnvSelected = visibleEnvs.map((env) => env.id).includes(envId);
|
const isEnvSelected = visibleEnvs.map((env) => env.id).includes(envId);
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => handleEnvSelect(envId)}
|
onClick={() => handleEnvSelect(envId)}
|
||||||
key={envId}
|
key={envId}
|
||||||
icon={
|
icon={
|
||||||
isEnvSelected && (
|
isEnvSelected ? (
|
||||||
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
|
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
|
||||||
)
|
) : (
|
||||||
}
|
<FontAwesomeIcon className="text-mineshaft-400" icon={faCircle} />
|
||||||
iconPos="left"
|
)
|
||||||
>
|
}
|
||||||
<div className="flex items-center">{name}</div>
|
iconPos="left"
|
||||||
</DropdownMenuItem>
|
>
|
||||||
);
|
<div className="flex items-center">{name}</div>
|
||||||
})}
|
</DropdownMenuItem>
|
||||||
{/* <DropdownMenuItem className="px-1.5" asChild>
|
);
|
||||||
|
})}
|
||||||
|
{/* <DropdownMenuItem className="px-1.5" asChild>
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@@ -436,8 +443,9 @@ export const SecretOverviewPage = () => {
|
|||||||
Create an environment
|
Create an environment
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuItem> */}
|
</DropdownMenuItem> */}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
<div className="w-80">
|
<div className="w-80">
|
||||||
<Input
|
<Input
|
||||||
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||||
@@ -447,62 +455,64 @@ export const SecretOverviewPage = () => {
|
|||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{userAvailableEnvs.length > 0 && (
|
||||||
<ProjectPermissionCan
|
<div>
|
||||||
I={ProjectPermissionActions.Create}
|
<ProjectPermissionCan
|
||||||
a={subject(ProjectPermissionSub.Secrets, { secretPath })}
|
I={ProjectPermissionActions.Create}
|
||||||
>
|
a={subject(ProjectPermissionSub.Secrets, { secretPath })}
|
||||||
{(isAllowed) => (
|
>
|
||||||
<Button
|
{(isAllowed) => (
|
||||||
variant="outline_bg"
|
<Button
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
variant="outline_bg"
|
||||||
onClick={() => handlePopUpOpen("addSecretsInAllEnvs")}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
className="h-10 rounded-r-none"
|
onClick={() => handlePopUpOpen("addSecretsInAllEnvs")}
|
||||||
isDisabled={!isAllowed}
|
className="h-10 rounded-r-none"
|
||||||
>
|
isDisabled={!isAllowed}
|
||||||
Add Secret
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
<DropdownMenu
|
|
||||||
open={popUp.misc.isOpen}
|
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("misc", isOpen)}
|
|
||||||
>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="add-folder-or-import"
|
|
||||||
variant="outline_bg"
|
|
||||||
className="rounded-l-none bg-mineshaft-600 p-3"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faAngleDown} />
|
|
||||||
</IconButton>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<div className="flex flex-col space-y-1 p-1.5">
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Create}
|
|
||||||
a={subject(ProjectPermissionSub.Secrets, { secretPath })}
|
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
Add Secret
|
||||||
<Button
|
</Button>
|
||||||
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
|
)}
|
||||||
onClick={() => {
|
</ProjectPermissionCan>
|
||||||
handlePopUpOpen("addFolder");
|
<DropdownMenu
|
||||||
handlePopUpClose("misc");
|
open={popUp.misc.isOpen}
|
||||||
}}
|
onOpenChange={(isOpen) => handlePopUpToggle("misc", isOpen)}
|
||||||
isDisabled={!isAllowed}
|
>
|
||||||
variant="outline_bg"
|
<DropdownMenuTrigger asChild>
|
||||||
className="h-10"
|
<IconButton
|
||||||
isFullWidth
|
ariaLabel="add-folder-or-import"
|
||||||
>
|
variant="outline_bg"
|
||||||
Add Folder
|
className="rounded-l-none bg-mineshaft-600 p-3"
|
||||||
</Button>
|
>
|
||||||
)}
|
<FontAwesomeIcon icon={faAngleDown} />
|
||||||
</ProjectPermissionCan>
|
</IconButton>
|
||||||
</div>
|
</DropdownMenuTrigger>
|
||||||
</DropdownMenuContent>
|
<DropdownMenuContent align="end">
|
||||||
</DropdownMenu>
|
<div className="flex flex-col space-y-1 p-1.5">
|
||||||
</div>
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Create}
|
||||||
|
a={subject(ProjectPermissionSub.Secrets, { secretPath })}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("addFolder");
|
||||||
|
handlePopUpClose("misc");
|
||||||
|
}}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
variant="outline_bg"
|
||||||
|
className="h-10"
|
||||||
|
isFullWidth
|
||||||
|
>
|
||||||
|
Add Folder
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -565,14 +575,25 @@ export const SecretOverviewPage = () => {
|
|||||||
className="bg-mineshaft-700"
|
className="bg-mineshaft-700"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isTableEmpty && !isTableLoading && (
|
{userAvailableEnvs.length > 0 && visibleEnvs.length === 0 && (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td colSpan={visibleEnvs.length + 1}>
|
<Td colSpan={visibleEnvs.length + 1}>
|
||||||
<EmptyState title="Let's add some secrets" icon={faFolderBlank} iconSize="3x">
|
<EmptyState title="You have no visible environments" iconSize="3x" />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
{userAvailableEnvs.length === 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={visibleEnvs.length + 1}>
|
||||||
|
<EmptyState
|
||||||
|
title="You have no environments, start by adding some"
|
||||||
|
iconSize="3x"
|
||||||
|
>
|
||||||
<Link
|
<Link
|
||||||
href={{
|
href={{
|
||||||
pathname: "/project/[id]/secrets/[env]",
|
pathname: "/project/[id]/settings",
|
||||||
query: { id: workspaceId, env: visibleEnvs?.[0]?.slug }
|
query: { id: workspaceId },
|
||||||
|
hash: "environments"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
@@ -581,13 +602,38 @@ export const SecretOverviewPage = () => {
|
|||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
Go to {visibleEnvs?.[0]?.name}
|
Add environments
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</EmptyState>
|
</EmptyState>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
|
{isTableEmpty && !isTableLoading && visibleEnvs.length > 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={visibleEnvs.length + 1}>
|
||||||
|
<EmptyState
|
||||||
|
title={
|
||||||
|
searchFilter
|
||||||
|
? "No secret found for your search, add one now"
|
||||||
|
: "Let's add some secrets"
|
||||||
|
}
|
||||||
|
icon={faFolderBlank}
|
||||||
|
iconSize="3x"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className="mt-4"
|
||||||
|
variant="outline_bg"
|
||||||
|
colorSchema="primary"
|
||||||
|
size="md"
|
||||||
|
onClick={() => handlePopUpOpen("addSecretsInAllEnvs")}
|
||||||
|
>
|
||||||
|
Add Secrets
|
||||||
|
</Button>
|
||||||
|
</EmptyState>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
{!isTableLoading &&
|
{!isTableLoading &&
|
||||||
filteredFolderNames.map((folderName, index) => (
|
filteredFolderNames.map((folderName, index) => (
|
||||||
<SecretOverviewFolderRow
|
<SecretOverviewFolderRow
|
||||||
@@ -599,22 +645,19 @@ export const SecretOverviewPage = () => {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{!isTableLoading &&
|
{!isTableLoading &&
|
||||||
(visibleEnvs?.length > 0 ? (
|
visibleEnvs?.length > 0 &&
|
||||||
filteredSecretNames.map((key, index) => (
|
filteredSecretNames.map((key, index) => (
|
||||||
<SecretOverviewTableRow
|
<SecretOverviewTableRow
|
||||||
secretPath={secretPath}
|
secretPath={secretPath}
|
||||||
onSecretCreate={handleSecretCreate}
|
onSecretCreate={handleSecretCreate}
|
||||||
onSecretDelete={handleSecretDelete}
|
onSecretDelete={handleSecretDelete}
|
||||||
onSecretUpdate={handleSecretUpdate}
|
onSecretUpdate={handleSecretUpdate}
|
||||||
key={`overview-${key}-${index + 1}`}
|
key={`overview-${key}-${index + 1}`}
|
||||||
environments={visibleEnvs}
|
environments={visibleEnvs}
|
||||||
secretKey={key}
|
secretKey={key}
|
||||||
getSecretByKey={getSecretByKey}
|
getSecretByKey={getSecretByKey}
|
||||||
expandableColWidth={expandableTableWidth}
|
expandableColWidth={expandableTableWidth}
|
||||||
/>
|
/>
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<PermissionDeniedBanner />
|
|
||||||
))}
|
))}
|
||||||
</TBody>
|
</TBody>
|
||||||
<TFoot>
|
<TFoot>
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ export const CreateSecretForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormLabel label="Environments" className="mb-2" />
|
<FormLabel label="Environments" className="mb-2" />
|
||||||
<div className="thin-scrollbar grid max-h-64 grid-cols-3 gap-4 overflow-auto ">
|
<div className="thin-scrollbar grid max-h-64 grid-cols-3 gap-4 overflow-auto py-2">
|
||||||
{environments.map((env) => {
|
{environments.map((env) => {
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
@@ -183,13 +183,23 @@ export const CreateSecretForm = ({
|
|||||||
isChecked={field.value}
|
isChecked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
id={`secret-input-${env.slug}`}
|
id={`secret-input-${env.slug}`}
|
||||||
|
className="!justify-start"
|
||||||
>
|
>
|
||||||
{env.name}
|
<span className="flex w-full flex-row items-center justify-start whitespace-pre-wrap">
|
||||||
{getSecretByKey(env.slug, newSecretKey) && (
|
<span title={env.name} className="truncate">
|
||||||
<Tooltip content="Secret exists. Will be overwritten">
|
{env.name}
|
||||||
<FontAwesomeIcon icon={faWarning} className="ml-1 text-yellow-400" />
|
</span>
|
||||||
</Tooltip>
|
<span>
|
||||||
)}
|
{getSecretByKey(env.slug, newSecretKey) && (
|
||||||
|
<Tooltip
|
||||||
|
className="max-w-[150px]"
|
||||||
|
content="Secret already exists, and it will be overwritten"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faWarning} className="ml-1 text-yellow-400" />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -148,8 +148,13 @@ export const SecretOverviewTableRow = ({
|
|||||||
key={`secret-expanded-${slug}-${secretKey}`}
|
key={`secret-expanded-${slug}-${secretKey}`}
|
||||||
className="hover:bg-mineshaft-700"
|
className="hover:bg-mineshaft-700"
|
||||||
>
|
>
|
||||||
<td className="flex" style={{ padding: "0.25rem 1rem" }}>
|
<td
|
||||||
<div className="flex h-8 items-center">{name}</div>
|
className="flex h-full items-center"
|
||||||
|
style={{ padding: "0.25rem 1rem" }}
|
||||||
|
>
|
||||||
|
<div title={name} className="flex h-8 w-[8rem] items-center ">
|
||||||
|
<span className="truncate">{name}</span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="col-span-2 h-8 w-full">
|
<td className="col-span-2 h-8 w-full">
|
||||||
<SecretEditRow
|
<SecretEditRow
|
||||||
|
|||||||
@@ -351,7 +351,7 @@ export const SecretRotationPage = withProjectPermission(
|
|||||||
{!isRotationProviderLoading &&
|
{!isRotationProviderLoading &&
|
||||||
secretRotationProviders?.providers.map((provider) => (
|
secretRotationProviders?.providers.map((provider) => (
|
||||||
<div
|
<div
|
||||||
className="group relative flex h-32 cursor-pointer flex-row items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4 hover:border-primary/40 hover:bg-primary/10"
|
className="group relative flex h-32 cursor-pointer flex-row items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4 hover:border-primary/40 hover:bg-primary/10"
|
||||||
key={`infisical-rotation-provider-${provider.name}`}
|
key={`infisical-rotation-provider-${provider.name}`}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="button"
|
role="button"
|
||||||
@@ -362,8 +362,8 @@ export const SecretRotationPage = withProjectPermission(
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={`/images/secretRotation/${provider.image}`}
|
src={`/images/secretRotation/${provider.image}`}
|
||||||
height={70}
|
className="max-h-16"
|
||||||
width={70}
|
style={{ maxWidth: "6rem" }}
|
||||||
alt="rotation provider logo"
|
alt="rotation provider logo"
|
||||||
/>
|
/>
|
||||||
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
|
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@a
|
|||||||
import { useUpdateOrg } from "@app/hooks/api";
|
import { useUpdateOrg } from "@app/hooks/api";
|
||||||
|
|
||||||
const formSchema = yup.object({
|
const formSchema = yup.object({
|
||||||
name: yup.string().required().label("Organization Name"),
|
name: yup.string().required().label("Organization Name").max(64, "Too long, maximum length is 64 characters"),
|
||||||
slug: yup
|
slug: yup
|
||||||
.string()
|
.string()
|
||||||
.matches(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens")
|
.matches(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens")
|
||||||
|
|||||||
@@ -63,7 +63,10 @@ export const EnvironmentSection = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div
|
||||||
|
id="environments"
|
||||||
|
className="mb-6 scroll-m-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||||
|
>
|
||||||
<div className="mb-8 flex justify-between">
|
<div className="mb-8 flex justify-between">
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">Environments</p>
|
<p className="text-xl font-semibold text-mineshaft-100">Environments</p>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { useRenameWorkspace } from "@app/hooks/api";
|
|||||||
import { CopyButton } from "./CopyButton";
|
import { CopyButton } from "./CopyButton";
|
||||||
|
|
||||||
const formSchema = yup.object({
|
const formSchema = yup.object({
|
||||||
name: yup.string().required().label("Project Name")
|
name: yup.string().required().label("Project Name").max(64, "Too long, maximum length is 64 characters"),
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormData = yup.InferType<typeof formSchema>;
|
type FormData = yup.InferType<typeof formSchema>;
|
||||||
@@ -78,18 +78,26 @@ export const ProjectNameChangeSection = () => {
|
|||||||
</CopyButton>
|
</CopyButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-md">
|
<div className="max-w-md">
|
||||||
<Controller
|
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Workspace}>
|
||||||
defaultValue=""
|
{(isAllowed) => (
|
||||||
render={({ field, fieldState: { error } }) => (
|
<Controller
|
||||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
defaultValue=""
|
||||||
<Input placeholder="Project name" {...field} className="bg-mineshaft-800" />
|
render={({ field, fieldState: { error } }) => (
|
||||||
</FormControl>
|
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Input
|
||||||
|
placeholder="Project name"
|
||||||
|
{...field}
|
||||||
|
className="bg-mineshaft-800"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
control={control}
|
||||||
|
name="name"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
control={control}
|
</ProjectPermissionCan>
|
||||||
name="name"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Workspace}>
|
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Workspace}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ export const UserInfoSSOStep = ({
|
|||||||
onChange={(e) => setOrganizationName(e.target.value)}
|
onChange={(e) => setOrganizationName(e.target.value)}
|
||||||
isRequired
|
isRequired
|
||||||
className="h-12"
|
className="h-12"
|
||||||
|
maxLength={64}
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
{organizationNameError && (
|
{organizationNameError && (
|
||||||
|
|||||||