Compare commits

..

2 Commits

Author SHA1 Message Date
e15c9e72c6 allow inetgrations list fetch via UA 2024-04-12 10:06:16 -04:00
cbb749e34a Update list-project-integrations.mdx 2024-04-11 23:52:25 -04:00
104 changed files with 99 additions and 5013 deletions

2
.gitignore vendored
View File

@ -59,8 +59,6 @@ yarn-error.log*
# Infisical init
.infisical.json
.infisicalignore
# Editor specific
.vscode/*

View File

@ -1,5 +1 @@
.github/resources/docker-compose.be-test.yml:generic-api-key:16
frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx:generic-api-key:206
frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:304
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx:generic-api-key:206
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:292

View File

@ -5,7 +5,6 @@ import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-se
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@ -26,7 +25,6 @@ import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
@ -91,8 +89,6 @@ declare module "fastify" {
orgRole: TOrgRoleServiceFactory;
superAdmin: TSuperAdminServiceFactory;
user: TUserServiceFactory;
group: TGroupServiceFactory;
groupProject: TGroupProjectServiceFactory;
apiKey: TApiKeyServiceFactory;
project: TProjectServiceFactory;
projectMembership: TProjectMembershipServiceFactory;

View File

@ -29,15 +29,6 @@ import {
TGitAppOrg,
TGitAppOrgInsert,
TGitAppOrgUpdate,
TGroupProjectMembershipRoles,
TGroupProjectMembershipRolesInsert,
TGroupProjectMembershipRolesUpdate,
TGroupProjectMemberships,
TGroupProjectMembershipsInsert,
TGroupProjectMembershipsUpdate,
TGroups,
TGroupsInsert,
TGroupsUpdate,
TIdentities,
TIdentitiesInsert,
TIdentitiesUpdate,
@ -197,9 +188,6 @@ import {
TUserEncryptionKeys,
TUserEncryptionKeysInsert,
TUserEncryptionKeysUpdate,
TUserGroupMembership,
TUserGroupMembershipInsert,
TUserGroupMembershipUpdate,
TUsers,
TUsersInsert,
TUsersUpdate,
@ -211,22 +199,6 @@ import {
declare module "knex/types/tables" {
interface Tables {
[TableName.Users]: Knex.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
[TableName.Groups]: Knex.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
[TableName.UserGroupMembership]: Knex.CompositeTableType<
TUserGroupMembership,
TUserGroupMembershipInsert,
TUserGroupMembershipUpdate
>;
[TableName.GroupProjectMembership]: Knex.CompositeTableType<
TGroupProjectMemberships,
TGroupProjectMembershipsInsert,
TGroupProjectMembershipsUpdate
>;
[TableName.GroupProjectMembershipRole]: Knex.CompositeTableType<
TGroupProjectMembershipRoles,
TGroupProjectMembershipRolesInsert,
TGroupProjectMembershipRolesUpdate
>;
[TableName.UserAliases]: Knex.CompositeTableType<TUserAliases, TUserAliasesInsert, TUserAliasesUpdate>;
[TableName.UserEncryptionKey]: Knex.CompositeTableType<
TUserEncryptionKeys,

View File

@ -1,82 +0,0 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.Groups))) {
await knex.schema.createTable(TableName.Groups, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("orgId").notNullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.string("name").notNullable();
t.string("slug").notNullable();
t.unique(["orgId", "slug"]);
t.string("role").notNullable();
t.uuid("roleId");
t.foreign("roleId").references("id").inTable(TableName.OrgRoles);
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.Groups);
if (!(await knex.schema.hasTable(TableName.UserGroupMembership))) {
await knex.schema.createTable(TableName.UserGroupMembership, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); // link to user and link to groups cascade on groups
t.uuid("userId").notNullable();
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
t.uuid("groupId").notNullable();
t.foreign("groupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.UserGroupMembership);
if (!(await knex.schema.hasTable(TableName.GroupProjectMembership))) {
await knex.schema.createTable(TableName.GroupProjectMembership, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("projectId").notNullable();
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.uuid("groupId").notNullable();
t.foreign("groupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.GroupProjectMembership);
if (!(await knex.schema.hasTable(TableName.GroupProjectMembershipRole))) {
await knex.schema.createTable(TableName.GroupProjectMembershipRole, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("role").notNullable();
t.uuid("projectMembershipId").notNullable();
t.foreign("projectMembershipId").references("id").inTable(TableName.GroupProjectMembership).onDelete("CASCADE");
// until role is changed/removed the role should not deleted
t.uuid("customRoleId");
t.foreign("customRoleId").references("id").inTable(TableName.ProjectRoles);
t.boolean("isTemporary").notNullable().defaultTo(false);
t.string("temporaryMode");
t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc
t.datetime("temporaryAccessStartTime");
t.datetime("temporaryAccessEndTime");
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.GroupProjectMembershipRole);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.GroupProjectMembershipRole);
await dropOnUpdateTrigger(knex, TableName.GroupProjectMembershipRole);
await knex.schema.dropTableIfExists(TableName.UserGroupMembership);
await dropOnUpdateTrigger(knex, TableName.UserGroupMembership);
await knex.schema.dropTableIfExists(TableName.GroupProjectMembership);
await dropOnUpdateTrigger(knex, TableName.GroupProjectMembership);
await knex.schema.dropTableIfExists(TableName.Groups);
await dropOnUpdateTrigger(knex, TableName.Groups);
}

View File

@ -1,31 +0,0 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const GroupProjectMembershipRolesSchema = z.object({
id: z.string().uuid(),
role: z.string(),
projectMembershipId: z.string().uuid(),
customRoleId: z.string().uuid().nullable().optional(),
isTemporary: z.boolean().default(false),
temporaryMode: z.string().nullable().optional(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TGroupProjectMembershipRoles = z.infer<typeof GroupProjectMembershipRolesSchema>;
export type TGroupProjectMembershipRolesInsert = Omit<
z.input<typeof GroupProjectMembershipRolesSchema>,
TImmutableDBKeys
>;
export type TGroupProjectMembershipRolesUpdate = Partial<
Omit<z.input<typeof GroupProjectMembershipRolesSchema>, TImmutableDBKeys>
>;

View File

@ -1,22 +0,0 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const GroupProjectMembershipsSchema = z.object({
id: z.string().uuid(),
projectId: z.string(),
groupId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TGroupProjectMemberships = z.infer<typeof GroupProjectMembershipsSchema>;
export type TGroupProjectMembershipsInsert = Omit<z.input<typeof GroupProjectMembershipsSchema>, TImmutableDBKeys>;
export type TGroupProjectMembershipsUpdate = Partial<
Omit<z.input<typeof GroupProjectMembershipsSchema>, TImmutableDBKeys>
>;

View File

@ -1,23 +0,0 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const GroupsSchema = z.object({
id: z.string().uuid(),
orgId: z.string().uuid(),
name: z.string(),
slug: z.string(),
role: z.string(),
roleId: z.string().uuid().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TGroups = z.infer<typeof GroupsSchema>;
export type TGroupsInsert = Omit<z.input<typeof GroupsSchema>, TImmutableDBKeys>;
export type TGroupsUpdate = Partial<Omit<z.input<typeof GroupsSchema>, TImmutableDBKeys>>;

View File

@ -7,9 +7,6 @@ export * from "./dynamic-secret-leases";
export * from "./dynamic-secrets";
export * from "./git-app-install-sessions";
export * from "./git-app-org";
export * from "./group-project-membership-roles";
export * from "./group-project-memberships";
export * from "./groups";
export * from "./identities";
export * from "./identity-access-tokens";
export * from "./identity-org-memberships";
@ -64,6 +61,5 @@ export * from "./trusted-ips";
export * from "./user-actions";
export * from "./user-aliases";
export * from "./user-encryption-keys";
export * from "./user-group-membership";
export * from "./users";
export * from "./webhooks";

View File

@ -2,10 +2,6 @@ import { z } from "zod";
export enum TableName {
Users = "users",
Groups = "groups",
GroupProjectMembership = "group_project_memberships",
GroupProjectMembershipRole = "group_project_membership_roles",
UserGroupMembership = "user_group_membership",
UserAliases = "user_aliases",
UserEncryptionKey = "user_encryption_keys",
AuthTokens = "auth_tokens",

View File

@ -1,20 +0,0 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const UserGroupMembershipSchema = z.object({
id: z.string().uuid(),
userId: z.string().uuid(),
groupId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TUserGroupMembership = z.infer<typeof UserGroupMembershipSchema>;
export type TUserGroupMembershipInsert = Omit<z.input<typeof UserGroupMembershipSchema>, TImmutableDBKeys>;
export type TUserGroupMembershipUpdate = Partial<Omit<z.input<typeof UserGroupMembershipSchema>, TImmutableDBKeys>>;

View File

@ -1,220 +0,0 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { GroupsSchema, OrgMembershipRole, UsersSchema } from "@app/db/schemas";
import { GROUPS } from "@app/lib/api-docs";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerGroupRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/",
method: "POST",
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
body: z.object({
name: z.string().trim().min(1).max(50).describe(GROUPS.CREATE.name),
slug: z
.string()
.min(5)
.max(36)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(GROUPS.CREATE.slug),
role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(GROUPS.CREATE.role)
}),
response: {
200: GroupsSchema
}
},
handler: async (req) => {
const group = await server.services.group.createGroup({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
return group;
}
});
server.route({
url: "/:currentSlug",
method: "PATCH",
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
currentSlug: z.string().trim().describe(GROUPS.UPDATE.currentSlug)
}),
body: z
.object({
name: z.string().trim().min(1).describe(GROUPS.UPDATE.name),
slug: z
.string()
.min(5)
.max(36)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.describe(GROUPS.UPDATE.slug),
role: z.string().trim().min(1).describe(GROUPS.UPDATE.role)
})
.partial(),
response: {
200: GroupsSchema
}
},
handler: async (req) => {
const group = await server.services.group.updateGroup({
currentSlug: req.params.currentSlug,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
return group;
}
});
server.route({
url: "/:slug",
method: "DELETE",
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
slug: z.string().trim().describe(GROUPS.DELETE.slug)
}),
response: {
200: GroupsSchema
}
},
handler: async (req) => {
const group = await server.services.group.deleteGroup({
groupSlug: req.params.slug,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return group;
}
});
server.route({
method: "GET",
url: "/:slug/users",
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
slug: z.string().trim().describe(GROUPS.LIST_USERS.slug)
}),
querystring: z.object({
offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset),
limit: z.coerce.number().min(1).max(100).default(10).describe(GROUPS.LIST_USERS.limit),
username: z.string().optional().describe(GROUPS.LIST_USERS.username)
}),
response: {
200: z.object({
users: UsersSchema.pick({
email: true,
username: true,
firstName: true,
lastName: true,
id: true
})
.merge(
z.object({
isPartOfGroup: z.boolean()
})
)
.array(),
totalCount: z.number()
})
}
},
handler: async (req) => {
const { users, totalCount } = await server.services.group.listGroupUsers({
groupSlug: req.params.slug,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query
});
return { users, totalCount };
}
});
server.route({
method: "POST",
url: "/:slug/users/:username",
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
slug: z.string().trim().describe(GROUPS.ADD_USER.slug),
username: z.string().trim().describe(GROUPS.ADD_USER.username)
}),
response: {
200: UsersSchema.pick({
email: true,
username: true,
firstName: true,
lastName: true,
id: true
})
}
},
handler: async (req) => {
const user = await server.services.group.addUserToGroup({
groupSlug: req.params.slug,
username: req.params.username,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return user;
}
});
server.route({
method: "DELETE",
url: "/:slug/users/:username",
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
slug: z.string().trim().describe(GROUPS.DELETE_USER.slug),
username: z.string().trim().describe(GROUPS.DELETE_USER.username)
}),
response: {
200: UsersSchema.pick({
email: true,
username: true,
firstName: true,
lastName: true,
id: true
})
}
},
handler: async (req) => {
const user = await server.services.group.removeUserFromGroup({
groupSlug: req.params.slug,
username: req.params.username,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return user;
}
});
};

View File

@ -1,6 +1,5 @@
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
import { registerGroupRouter } from "./group-router";
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
import { registerLdapRouter } from "./ldap-router";
import { registerLicenseRouter } from "./license-router";
@ -54,7 +53,6 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
await server.register(registerGroupRouter, { prefix: "/groups" });
await server.register(
async (privilegeRouter) => {
await privilegeRouter.register(registerUserAdditionalPrivilegeRouter, { prefix: "/users" });

View File

@ -171,7 +171,6 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
req.permission.authMethod,
req.permission.orgId
);
return { data: { permissions, membership } };
}
});

View File

@ -206,7 +206,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
schema: {
body: z.object({
schemas: z.array(z.string()),
userName: z.string().trim(),
userName: z.string().trim().email(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
@ -227,7 +227,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
userName: z.string().trim().email(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
@ -262,244 +262,25 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/Users/:userId",
method: "DELETE",
method: "PATCH",
schema: {
params: z.object({
userId: z.string().trim()
}),
response: {
200: z.object({})
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const user = await req.server.services.scim.deleteScimUser({
userId: req.params.userId,
orgId: req.permission.orgId
});
return user;
}
});
server.route({
url: "/Groups",
method: "POST",
schema: {
body: z.object({
schemas: z.array(z.string()),
displayName: z.string().trim(),
members: z.array(z.any()).length(0).optional() // okta-specific
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z.array(z.any()).length(0),
meta: z.object({
resourceType: z.string().trim()
})
})
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const group = await req.server.services.scim.createScimGroup({
displayName: req.body.displayName,
orgId: req.permission.orgId
});
return group;
}
});
server.route({
url: "/Groups",
method: "GET",
schema: {
querystring: z.object({
startIndex: z.coerce.number().default(1),
count: z.coerce.number().default(20),
filter: z.string().trim().optional()
}),
response: {
200: z.object({
Resources: z.array(
z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z.array(z.any()).length(0),
meta: z.object({
resourceType: z.string().trim()
})
})
),
itemsPerPage: z.number(),
schemas: z.array(z.string()),
startIndex: z.number(),
totalResults: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const groups = await req.server.services.scim.listScimGroups({
orgId: req.permission.orgId,
offset: req.query.startIndex,
limit: req.query.count
});
return groups;
}
});
server.route({
url: "/Groups/:groupId",
method: "GET",
schema: {
params: z.object({
groupId: z.string().trim()
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z.array(
z.object({
value: z.string(),
display: z.string()
})
),
meta: z.object({
resourceType: z.string().trim()
})
})
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const group = await req.server.services.scim.getScimGroup({
groupId: req.params.groupId,
orgId: req.permission.orgId
});
return group;
}
});
server.route({
url: "/Groups/:groupId",
method: "PUT",
schema: {
params: z.object({
groupId: z.string().trim()
}),
body: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z.array(z.any()).length(0)
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z.array(
z.object({
value: z.string(),
display: z.string()
})
),
meta: z.object({
resourceType: z.string().trim()
})
})
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const group = await req.server.services.scim.updateScimGroupNamePut({
groupId: req.params.groupId,
orgId: req.permission.orgId,
displayName: req.body.displayName
});
return group;
}
});
server.route({
url: "/Groups/:groupId",
method: "PATCH",
schema: {
params: z.object({
groupId: z.string().trim()
}),
body: z.object({
schemas: z.array(z.string()),
Operations: z.array(
z.union([
z.object({
op: z.literal("replace"),
value: z.object({
id: z.string().trim(),
displayName: z.string().trim()
})
}),
z.object({
op: z.literal("remove"),
path: z.string().trim()
}),
z.object({
op: z.literal("add"),
value: z.object({
value: z.string().trim(),
display: z.string().trim().optional()
})
})
])
)
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z.array(
z.object({
value: z.string(),
display: z.string()
})
),
meta: z.object({
resourceType: z.string().trim()
z.object({
op: z.string().trim(),
path: z.string().trim().optional(),
value: z.union([
z.object({
active: z.boolean()
}),
z.string().trim()
])
})
})
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
// console.log("PATCH /Groups/:groupId req.body: ", req.body);
// console.log("PATCH /Groups/:groupId req.body: ", req.body.Operations[0]);
const group = await req.server.services.scim.updateScimGroupNamePatch({
groupId: req.params.groupId,
orgId: req.permission.orgId,
operations: req.body.Operations
});
return group;
}
});
server.route({
url: "/Groups/:groupId",
method: "DELETE",
schema: {
params: z.object({
groupId: z.string().trim()
)
}),
response: {
200: z.object({})
@ -507,12 +288,12 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const group = await req.server.services.scim.deleteScimGroup({
groupId: req.params.groupId,
orgId: req.permission.orgId
const user = await req.server.services.scim.updateScimUser({
userId: req.params.userId,
orgId: req.permission.orgId,
operations: req.body.Operations
});
return group;
return user;
}
});

View File

@ -1,157 +0,0 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TGroups } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex";
export type TGroupDALFactory = ReturnType<typeof groupDALFactory>;
export const groupDALFactory = (db: TDbClient) => {
const groupOrm = ormify(db, TableName.Groups);
const findGroups = async (filter: TFindFilter<TGroups>, { offset, limit, sort, tx }: TFindOpt<TGroups> = {}) => {
try {
const query = (tx || db)(TableName.Groups)
// eslint-disable-next-line
.where(buildFindFilter(filter))
.select(selectAllTableCols(TableName.Groups));
if (limit) void query.limit(limit);
if (offset) void query.limit(offset);
if (sort) {
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
}
const res = await query;
return res;
} catch (err) {
throw new DatabaseError({ error: err, name: "Find groups" });
}
};
const findByOrgId = async (orgId: string, tx?: Knex) => {
try {
const docs = await (tx || db)(TableName.Groups)
.where(`${TableName.Groups}.orgId`, orgId)
.leftJoin(TableName.OrgRoles, `${TableName.Groups}.roleId`, `${TableName.OrgRoles}.id`)
.select(selectAllTableCols(TableName.Groups))
// cr stands for custom role
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
.select(db.ref("name").as("crName").withSchema(TableName.OrgRoles))
.select(db.ref("slug").as("crSlug").withSchema(TableName.OrgRoles))
.select(db.ref("description").as("crDescription").withSchema(TableName.OrgRoles))
.select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles));
return docs.map(({ crId, crDescription, crSlug, crPermission, crName, ...el }) => ({
...el,
customRole: el.roleId
? {
id: crId,
name: crName,
slug: crSlug,
permissions: crPermission,
description: crDescription
}
: undefined
}));
} catch (error) {
throw new DatabaseError({ error, name: "FindByOrgId" });
}
};
const countAllGroupMembers = async ({ orgId, groupId }: { orgId: string; groupId: string }) => {
try {
interface CountResult {
count: string;
}
const doc = await db<CountResult>(TableName.OrgMembership)
.where(`${TableName.OrgMembership}.orgId`, orgId)
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.UserGroupMembership, function () {
this.on(`${TableName.UserGroupMembership}.userId`, "=", `${TableName.Users}.id`).andOn(
`${TableName.UserGroupMembership}.groupId`,
"=",
db.raw("?", [groupId])
);
})
.where({ isGhost: false })
.count(`${TableName.Users}.id`)
.first();
return parseInt((doc?.count as string) || "0", 10);
} catch (err) {
throw new DatabaseError({ error: err, name: "Count all group members" });
}
};
// special query
const findAllGroupMembers = async ({
orgId,
groupId,
offset = 0,
limit,
username
}: {
orgId: string;
groupId: string;
offset?: number;
limit?: number;
username?: string;
}) => {
try {
let query = db(TableName.OrgMembership)
.where(`${TableName.OrgMembership}.orgId`, orgId)
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.UserGroupMembership, function () {
this.on(`${TableName.UserGroupMembership}.userId`, "=", `${TableName.Users}.id`).andOn(
`${TableName.UserGroupMembership}.groupId`,
"=",
db.raw("?", [groupId])
);
})
.select(
db.ref("id").withSchema(TableName.OrgMembership),
db.ref("groupId").withSchema(TableName.UserGroupMembership),
db.ref("email").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId")
)
.where({ isGhost: false })
.offset(offset);
if (limit) {
query = query.limit(limit);
}
if (username) {
query = query.andWhere(`${TableName.Users}.username`, "ilike", `%${username}%`);
}
const members = await query;
return members.map(
({ email, username: memberUsername, firstName, lastName, userId, groupId: memberGroupId }) => ({
id: userId,
email,
username: memberUsername,
firstName,
lastName,
isPartOfGroup: !!memberGroupId
})
);
} catch (error) {
throw new DatabaseError({ error, name: "Find all org members" });
}
};
return {
findGroups,
findByOrgId,
countAllGroupMembers,
findAllGroupMembers,
...groupOrm
};
};

View File

@ -1,474 +0,0 @@
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
import { OrgMembershipRole, SecretKeyEncoding, TOrgRoles } from "@app/db/schemas";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { decryptAsymmetric, encryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TGroupProjectDALFactory } from "../../../services/group-project/group-project-dal";
import { TOrgDALFactory } from "../../../services/org/org-dal";
import { TProjectDALFactory } from "../../../services/project/project-dal";
import { TProjectBotDALFactory } from "../../../services/project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "../../../services/project-key/project-key-dal";
import { TUserDALFactory } from "../../../services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { TGroupDALFactory } from "./group-dal";
import {
TAddUserToGroupDTO,
TCreateGroupDTO,
TDeleteGroupDTO,
TListGroupUsersDTO,
TRemoveUserFromGroupDTO,
TUpdateGroupDTO
} from "./group-types";
import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal";
type TGroupServiceFactoryDep = {
userDAL: Pick<TUserDALFactory, "findOne" | "findUserEncKeyByUsername">;
groupDAL: Pick<
TGroupDALFactory,
"create" | "findOne" | "update" | "delete" | "findAllGroupMembers" | "countAllGroupMembers"
>;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
orgDAL: Pick<TOrgDALFactory, "findMembership">;
userGroupMembershipDAL: Pick<
TUserGroupMembershipDALFactory,
"findOne" | "create" | "delete" | "filterProjectsByUserMembership"
>;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "create" | "delete" | "findLatestProjectKey">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TGroupServiceFactory = ReturnType<typeof groupServiceFactory>;
export const groupServiceFactory = ({
userDAL,
groupDAL,
groupProjectDAL,
orgDAL,
userGroupMembershipDAL,
projectDAL,
projectBotDAL,
projectKeyDAL,
permissionService,
licenseService
}: TGroupServiceFactoryDep) => {
const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => {
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Groups);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to create group due to plan restriction. Upgrade plan to create group."
});
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
role,
actorOrgId
);
const isCustomRole = Boolean(customRole);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasRequiredPriviledges) throw new BadRequestError({ message: "Failed to create a more privileged group" });
const group = await groupDAL.create({
name,
slug: slug || slugify(`${name}-${alphaNumericNanoId(4)}`),
orgId: actorOrgId,
role: isCustomRole ? OrgMembershipRole.Custom : role,
roleId: customRole?.id
});
return group;
};
const updateGroup = async ({
currentSlug,
name,
slug,
role,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TUpdateGroupDTO) => {
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to update group due to plan restrictio Upgrade plan to update group."
});
const group = await groupDAL.findOne({ orgId: actorOrgId, slug: currentSlug });
if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${currentSlug}` });
let customRole: TOrgRoles | undefined;
if (role) {
const { permission: rolePermission, role: customOrgRole } = await permissionService.getOrgPermissionByRole(
role,
group.orgId
);
const isCustomRole = Boolean(customOrgRole);
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasRequiredNewRolePermission)
throw new BadRequestError({ message: "Failed to create a more privileged group" });
if (isCustomRole) customRole = customOrgRole;
}
const [updatedGroup] = await groupDAL.update(
{
orgId: actorOrgId,
slug: currentSlug
},
{
name,
slug: slug ? slugify(slug) : undefined,
...(role
? {
role: customRole ? OrgMembershipRole.Custom : role,
roleId: customRole?.id ?? null
}
: {})
}
);
return updatedGroup;
};
const deleteGroup = async ({ groupSlug, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => {
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Groups);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to delete group due to plan restriction. Upgrade plan to delete group."
});
const [group] = await groupDAL.delete({
orgId: actorOrgId,
slug: groupSlug
});
return group;
};
const listGroupUsers = async ({
groupSlug,
offset,
limit,
username,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TListGroupUsersDTO) => {
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
const group = await groupDAL.findOne({
orgId: actorOrgId,
slug: groupSlug
});
if (!group)
throw new BadRequestError({
message: `Failed to find group with slug ${groupSlug}`
});
const users = await groupDAL.findAllGroupMembers({
orgId: group.orgId,
groupId: group.id,
offset,
limit,
username
});
const totalCount = await groupDAL.countAllGroupMembers({
orgId: group.orgId,
groupId: group.id
});
return { users, totalCount };
};
const addUserToGroup = async ({
groupSlug,
username,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TAddUserToGroupDTO) => {
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
// check if group with slug exists
const group = await groupDAL.findOne({
orgId: actorOrgId,
slug: groupSlug
});
if (!group)
throw new BadRequestError({
message: `Failed to find group with slug ${groupSlug}`
});
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
// check if user has broader or equal to privileges than group
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, groupRolePermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" });
// get user with username
const user = await userDAL.findUserEncKeyByUsername({
username
});
if (!user)
throw new BadRequestError({
message: `Failed to find user with username ${username}`
});
// check if user group membership already exists
const existingUserGroupMembership = await userGroupMembershipDAL.findOne({
groupId: group.id,
userId: user.userId
});
if (existingUserGroupMembership)
throw new BadRequestError({
message: `User ${username} is already part of the group ${groupSlug}`
});
// check if user is even part of the organization
const existingUserOrgMembership = await orgDAL.findMembership({
userId: user.userId,
orgId: actorOrgId
});
if (!existingUserOrgMembership)
throw new BadRequestError({
message: `User ${username} is not part of the organization`
});
await userGroupMembershipDAL.create({
userId: user.userId,
groupId: group.id
});
// check which projects the group is part of
const projectIds = (
await groupProjectDAL.find({
groupId: group.id
})
).map((gp) => gp.projectId);
const keys = await projectKeyDAL.find({
receiverId: user.userId,
$in: {
projectId: projectIds
}
});
const keysSet = new Set(keys.map((k) => k.projectId));
const projectsToAddKeyFor = projectIds.filter((p) => !keysSet.has(p));
for await (const projectId of projectsToAddKeyFor) {
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
if (!ghostUser) {
throw new BadRequestError({
message: "Failed to find sudo user"
});
}
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
if (!ghostUserLatestKey) {
throw new BadRequestError({
message: "Failed to find sudo user latest key"
});
}
const bot = await projectBotDAL.findOne({ projectId });
if (!bot) {
throw new BadRequestError({
message: "Failed to find bot"
});
}
const botPrivateKey = infisicalSymmetricDecrypt({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const plaintextProjectKey = decryptAsymmetric({
ciphertext: ghostUserLatestKey.encryptedKey,
nonce: ghostUserLatestKey.nonce,
publicKey: ghostUserLatestKey.sender.publicKey,
privateKey: botPrivateKey
});
const { ciphertext: encryptedKey, nonce } = encryptAsymmetric(plaintextProjectKey, user.publicKey, botPrivateKey);
await projectKeyDAL.create({
encryptedKey,
nonce,
senderId: ghostUser.id,
receiverId: user.userId,
projectId
});
}
return user;
};
const removeUserFromGroup = async ({
groupSlug,
username,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TRemoveUserFromGroupDTO) => {
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
// check if group with slug exists
const group = await groupDAL.findOne({
orgId: actorOrgId,
slug: groupSlug
});
if (!group)
throw new BadRequestError({
message: `Failed to find group with slug ${groupSlug}`
});
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
// check if user has broader or equal to privileges than group
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, groupRolePermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to delete user from more privileged group" });
const user = await userDAL.findOne({
username
});
if (!user)
throw new BadRequestError({
message: `Failed to find user with username ${username}`
});
// check if user group membership already exists
const existingUserGroupMembership = await userGroupMembershipDAL.findOne({
groupId: group.id,
userId: user.id
});
if (!existingUserGroupMembership)
throw new BadRequestError({
message: `User ${username} is not part of the group ${groupSlug}`
});
const projectIds = (
await groupProjectDAL.find({
groupId: group.id
})
).map((gp) => gp.projectId);
const t = await userGroupMembershipDAL.filterProjectsByUserMembership(user.id, group.id, projectIds);
const projectsToDeleteKeyFor = projectIds.filter((p) => !t.has(p));
if (projectsToDeleteKeyFor.length) {
await projectKeyDAL.delete({
receiverId: user.id,
$in: {
projectId: projectsToDeleteKeyFor
}
});
}
await userGroupMembershipDAL.delete({
groupId: group.id,
userId: user.id
});
return user;
};
return {
createGroup,
updateGroup,
deleteGroup,
listGroupUsers,
addUserToGroup,
removeUserFromGroup
};
};

View File

@ -1,37 +0,0 @@
import { TGenericPermission } from "@app/lib/types";
export type TCreateGroupDTO = {
name: string;
slug?: string;
role: string;
} & TGenericPermission;
export type TUpdateGroupDTO = {
currentSlug: string;
} & Partial<{
name: string;
slug: string;
role: string;
}> &
TGenericPermission;
export type TDeleteGroupDTO = {
groupSlug: string;
} & TGenericPermission;
export type TListGroupUsersDTO = {
groupSlug: string;
offset: number;
limit: number;
username?: string;
} & TGenericPermission;
export type TAddUserToGroupDTO = {
groupSlug: string;
username: string;
} & TGenericPermission;
export type TRemoveUserFromGroupDTO = {
groupSlug: string;
username: string;
} & TGenericPermission;

View File

@ -1,125 +0,0 @@
import { TDbClient } from "@app/db";
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
export type TUserGroupMembershipDALFactory = ReturnType<typeof userGroupMembershipDALFactory>;
export const userGroupMembershipDALFactory = (db: TDbClient) => {
const userGroupMembershipOrm = ormify(db, TableName.UserGroupMembership);
/**
* Returns a sub-set of projectIds fed into this function corresponding to projects where either:
* - The user is a direct member of the project.
* - The user is a member of a group that is a member of the project, excluding projects that they are part of
* through the group with id [groupId].
*/
const filterProjectsByUserMembership = async (userId: string, groupId: string, projectIds: string[]) => {
const userProjectMemberships: string[] = await db(TableName.ProjectMembership)
.where(`${TableName.ProjectMembership}.userId`, userId)
.whereIn(`${TableName.ProjectMembership}.projectId`, projectIds)
.pluck(`${TableName.ProjectMembership}.projectId`);
const userGroupMemberships: string[] = await db(TableName.UserGroupMembership)
.where(`${TableName.UserGroupMembership}.userId`, userId)
.whereNot(`${TableName.UserGroupMembership}.groupId`, groupId)
.join(
TableName.GroupProjectMembership,
`${TableName.UserGroupMembership}.groupId`,
`${TableName.GroupProjectMembership}.groupId`
)
.whereIn(`${TableName.GroupProjectMembership}.projectId`, projectIds)
.pluck(`${TableName.GroupProjectMembership}.projectId`);
return new Set(userProjectMemberships.concat(userGroupMemberships));
};
// special query
const findUserGroupMembershipsInProject = async (usernames: string[], projectId: string) => {
try {
const usernameDocs: string[] = await db(TableName.UserGroupMembership)
.join(
TableName.GroupProjectMembership,
`${TableName.UserGroupMembership}.groupId`,
`${TableName.GroupProjectMembership}.groupId`
)
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
.whereIn(`${TableName.Users}.username`, usernames) // TODO: pluck usernames
.pluck(`${TableName.Users}.id`);
return usernameDocs;
} catch (error) {
throw new DatabaseError({ error, name: "Find user group members in project" });
}
};
/**
* Return list of users that are part of the group with id [groupId]
* that have not yet been added individually to project with id [projectId].
*
* Note: Filters out users that are part of other groups in the project.
* @param groupId
* @param projectId
* @returns
*/
const findGroupMembersNotInProject = async (groupId: string, projectId: string) => {
try {
// get list of groups in the project with id [projectId]
// that that are not the group with id [groupId]
const groups: string[] = await db(TableName.GroupProjectMembership)
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
.whereNot(`${TableName.GroupProjectMembership}.groupId`, groupId)
.pluck(`${TableName.GroupProjectMembership}.groupId`);
// main query
const members = await db(TableName.UserGroupMembership)
.where(`${TableName.UserGroupMembership}.groupId`, groupId)
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.ProjectMembership, function () {
this.on(`${TableName.Users}.id`, "=", `${TableName.ProjectMembership}.userId`).andOn(
`${TableName.ProjectMembership}.projectId`,
"=",
db.raw("?", [projectId])
);
})
.whereNull(`${TableName.ProjectMembership}.userId`)
.leftJoin<TUserEncryptionKeys>(
TableName.UserEncryptionKey,
`${TableName.UserEncryptionKey}.userId`,
`${TableName.Users}.id`
)
.select(
db.ref("id").withSchema(TableName.UserGroupMembership),
db.ref("groupId").withSchema(TableName.UserGroupMembership),
db.ref("email").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
)
.where({ isGhost: false }) // MAKE SURE USER IS NOT A GHOST USER
.whereNotIn(`${TableName.UserGroupMembership}.userId`, function () {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.select(`${TableName.UserGroupMembership}.userId`)
.from(TableName.UserGroupMembership)
.whereIn(`${TableName.UserGroupMembership}.groupId`, groups);
});
return members.map(({ email, username, firstName, lastName, userId, publicKey, ...data }) => ({
...data,
user: { email, username, firstName, lastName, id: userId, publicKey }
}));
} catch (error) {
throw new DatabaseError({ error, name: "Find group members not in project" });
}
};
return {
...userGroupMembershipOrm,
filterProjectsByUserMembership,
findUserGroupMembershipsInProject,
findGroupMembersNotInProject
};
};

View File

@ -12,6 +12,7 @@ import {
infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { TOrgPermission } from "@app/lib/types";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
@ -23,7 +24,7 @@ import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { TLdapConfigDALFactory } from "./ldap-config-dal";
import { TCreateLdapCfgDTO, TGetLdapCfgDTO, TLdapLoginDTO, TUpdateLdapCfgDTO } from "./ldap-config-types";
import { TCreateLdapCfgDTO, TLdapLoginDTO, TUpdateLdapCfgDTO } from "./ldap-config-types";
type TLdapConfigServiceFactoryDep = {
ldapConfigDAL: TLdapConfigDALFactory;
@ -281,7 +282,7 @@ export const ldapConfigServiceFactory = ({
orgId,
actorAuthMethod,
actorOrgId
}: TGetLdapCfgDTO) => {
}: TOrgPermission) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap);
return getLdapCfg({

View File

@ -1,7 +1,6 @@
import { TOrgPermission } from "@app/lib/types";
export type TCreateLdapCfgDTO = {
orgId: string;
isActive: boolean;
url: string;
bindDN: string;
@ -10,9 +9,7 @@ export type TCreateLdapCfgDTO = {
caCert: string;
} & TOrgPermission;
export type TUpdateLdapCfgDTO = {
orgId: string;
} & Partial<{
export type TUpdateLdapCfgDTO = Partial<{
isActive: boolean;
url: string;
bindDN: string;
@ -22,10 +19,6 @@ export type TUpdateLdapCfgDTO = {
}> &
TOrgPermission;
export type TGetLdapCfgDTO = {
orgId: string;
} & TOrgPermission;
export type TLdapLoginDTO = {
externalId: string;
username: string;

View File

@ -20,7 +20,6 @@ export const getDefaultOnPremFeatures = () => {
samlSSO: false,
scim: false,
ldap: false,
groups: false,
status: null,
trial_end: null,
has_used_trial: true,

View File

@ -27,7 +27,6 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
samlSSO: false,
scim: false,
ldap: false,
groups: false,
status: null,
trial_end: null,
has_used_trial: true,

View File

@ -43,7 +43,6 @@ export type TFeatureSet = {
samlSSO: false;
scim: false;
ldap: false;
groups: false;
status: null;
trial_end: null;
has_used_trial: true;

View File

@ -18,7 +18,6 @@ export enum OrgPermissionSubjects {
Sso = "sso",
Scim = "scim",
Ldap = "ldap",
Groups = "groups",
Billing = "billing",
SecretScanning = "secret-scanning",
Identity = "identity"
@ -34,7 +33,6 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
| [OrgPermissionActions, OrgPermissionSubjects.Scim]
| [OrgPermissionActions, OrgPermissionSubjects.Ldap]
| [OrgPermissionActions, OrgPermissionSubjects.Groups]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity];
@ -85,11 +83,6 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Ldap);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Groups);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Groups);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
@ -112,7 +105,6 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);

View File

@ -45,43 +45,6 @@ export const permissionDALFactory = (db: TDbClient) => {
const getProjectPermission = async (userId: string, projectId: string) => {
try {
const groups: string[] = await db(TableName.GroupProjectMembership)
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
.pluck(`${TableName.GroupProjectMembership}.groupId`);
const groupDocs = await db(TableName.UserGroupMembership)
.where(`${TableName.UserGroupMembership}.userId`, userId)
.whereIn(`${TableName.UserGroupMembership}.groupId`, groups)
.join(
TableName.GroupProjectMembership,
`${TableName.GroupProjectMembership}.groupId`,
`${TableName.UserGroupMembership}.groupId`
)
.join(
TableName.GroupProjectMembershipRole,
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
`${TableName.GroupProjectMembership}.id`
)
.leftJoin(
TableName.ProjectRoles,
`${TableName.GroupProjectMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id`
)
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
.select(selectAllTableCols(TableName.GroupProjectMembershipRole))
.select(
db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"),
// TODO(roll-forward-migration): remove this field when we drop this in next migration after a week
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("membershipCreatedAt"),
db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("membershipUpdatedAt"),
db.ref("projectId").withSchema(TableName.GroupProjectMembership),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("orgId").withSchema(TableName.Project),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug")
)
.select("permissions");
const docs = await db(TableName.ProjectMembership)
.join(
TableName.ProjectUserMembershipRole,
@ -106,9 +69,9 @@ export const permissionDALFactory = (db: TDbClient) => {
.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("projectId").withSchema(TableName.ProjectMembership),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("orgId").withSchema(TableName.Project),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
@ -130,12 +93,19 @@ export const permissionDALFactory = (db: TDbClient) => {
const permission = sqlNestRelationships({
data: docs,
key: "projectId",
parentMapper: ({ orgId, orgAuthEnforced, membershipId, membershipCreatedAt, membershipUpdatedAt, role }) => ({
key: "membershipId",
parentMapper: ({
orgId,
orgAuthEnforced,
membershipId,
membershipCreatedAt,
membershipUpdatedAt,
oldRoleField
}) => ({
orgId,
orgAuthEnforced,
userId,
role,
role: oldRoleField,
id: membershipId,
projectId,
createdAt: membershipCreatedAt,
@ -175,66 +145,19 @@ export const permissionDALFactory = (db: TDbClient) => {
]
});
const groupPermission = groupDocs.length
? sqlNestRelationships({
data: groupDocs,
key: "projectId",
parentMapper: ({
orgId,
orgAuthEnforced,
membershipId,
membershipCreatedAt,
membershipUpdatedAt,
role
}) => ({
orgId,
orgAuthEnforced,
userId,
role,
id: membershipId,
projectId,
createdAt: membershipCreatedAt,
updatedAt: membershipUpdatedAt
}),
childrenMapper: [
{
key: "id",
label: "roles" as const,
mapper: (data) =>
ProjectUserMembershipRolesSchema.extend({
permissions: z.unknown(),
customRoleSlug: z.string().optional().nullable()
}).parse(data)
}
]
})
: [];
if (!permission?.[0] && !groupPermission[0]) return undefined;
if (!permission?.[0]) return undefined;
// when introducting cron mode change it here
const activeRoles =
permission?.[0]?.roles?.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
) ?? [];
const activeGroupRoles =
groupPermission?.[0]?.roles?.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
) ?? [];
const activeRoles = permission?.[0]?.roles?.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
);
const activeAdditionalPrivileges = permission?.[0]?.additionalPrivileges?.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
);
return {
...(permission[0] || groupPermission[0]),
roles: [...activeRoles, ...activeGroupRoles],
additionalPrivileges: activeAdditionalPrivileges
};
return { ...permission[0], roles: activeRoles, additionalPrivileges: activeAdditionalPrivileges };
} catch (error) {
throw new DatabaseError({ error, name: "GetProjectPermission" });
}

View File

@ -12,7 +12,6 @@ export enum ProjectPermissionActions {
export enum ProjectPermissionSub {
Role = "role",
Member = "member",
Groups = "groups",
Settings = "settings",
Integrations = "integrations",
Webhooks = "webhooks",
@ -42,7 +41,6 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.Role]
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
| [ProjectPermissionActions, ProjectPermissionSub.Member]
| [ProjectPermissionActions, ProjectPermissionSub.Groups]
| [ProjectPermissionActions, ProjectPermissionSub.Integrations]
| [ProjectPermissionActions, ProjectPermissionSub.Webhooks]
| [ProjectPermissionActions, ProjectPermissionSub.AuditLogs]
@ -84,11 +82,6 @@ const buildAdminPermissionRules = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Role);
@ -164,8 +157,6 @@ const buildMemberPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
@ -218,7 +209,6 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);

View File

@ -1,4 +1,4 @@
import { TListScimGroups, TListScimUsers, TScimGroup, TScimUser } from "./scim-types";
import { TListScimUsers, TScimUser } from "./scim-types";
export const buildScimUserList = ({
scimUsers,
@ -62,47 +62,3 @@ export const buildScimUser = ({
return scimUser;
};
export const buildScimGroupList = ({
scimGroups,
offset,
limit
}: {
scimGroups: TScimGroup[];
offset: number;
limit: number;
}): TListScimGroups => {
return {
Resources: scimGroups,
itemsPerPage: limit,
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
startIndex: offset,
totalResults: scimGroups.length
};
};
export const buildScimGroup = ({
groupId,
name,
members
}: {
groupId: string;
name: string;
members: {
value: string;
display: string;
}[];
}): TScimGroup => {
const scimGroup = {
schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"],
id: groupId,
displayName: name,
members,
meta: {
resourceType: "Group",
location: null
}
};
return scimGroup;
};

View File

@ -1,13 +1,10 @@
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
import jwt from "jsonwebtoken";
import { OrgMembershipRole, OrgMembershipStatus, TableName, TGroups } from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { OrgMembershipRole, OrgMembershipStatus, TableName } from "@app/db/schemas";
import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TOrgPermission } from "@app/lib/types";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TOrgDALFactory } from "@app/services/org/org-dal";
@ -20,23 +17,16 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList } from "./scim-fns";
import { buildScimUser, buildScimUserList } from "./scim-fns";
import {
TCreateScimGroupDTO,
TCreateScimTokenDTO,
TCreateScimUserDTO,
TDeleteScimGroupDTO,
TDeleteScimTokenDTO,
TDeleteScimUserDTO,
TGetScimGroupDTO,
TGetScimUserDTO,
TListScimGroupsDTO,
TListScimUsers,
TListScimUsersDTO,
TReplaceScimUserDTO,
TScimTokenJwtPayload,
TUpdateScimGroupNamePatchDTO,
TUpdateScimGroupNamePutDTO,
TUpdateScimUserDTO
} from "./scim-types";
@ -49,7 +39,6 @@ type TScimServiceFactoryDep = {
>;
projectDAL: Pick<TProjectDALFactory, "find">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete">;
groupDAL: Pick<TGroupDALFactory, "create" | "findOne" | "findAllGroupMembers" | "update" | "delete" | "findGroups">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
smtpService: TSmtpService;
@ -64,7 +53,6 @@ export const scimServiceFactory = ({
orgDAL,
projectDAL,
projectMembershipDAL,
groupDAL,
permissionService,
smtpService
}: TScimServiceFactoryDep) => {
@ -435,221 +423,6 @@ export const scimServiceFactory = ({
});
};
const deleteScimUser = async ({ userId, orgId }: TDeleteScimUserDTO) => {
const [membership] = await orgDAL
.findMembership({
userId,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
})
.catch(() => {
throw new ScimRequestError({
detail: "User not found",
status: 404
});
});
if (!membership)
throw new ScimRequestError({
detail: "User not found",
status: 404
});
if (!membership.scimEnabled) {
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
status: 403
});
}
await deleteOrgMembership({
orgMembershipId: membership.id,
orgId: membership.orgId,
orgDAL,
projectDAL,
projectMembershipDAL
});
return {}; // intentionally return empty object upon success
};
const listScimGroups = async ({ orgId, offset, limit }: TListScimGroupsDTO) => {
const org = await orgDAL.findById(orgId);
if (!org.scimEnabled)
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
status: 403
});
const groups = await groupDAL.findGroups({
orgId
});
const scimGroups = groups.map((group) =>
buildScimGroup({
groupId: group.id,
name: group.name,
members: []
})
);
return buildScimGroupList({
scimGroups,
offset,
limit
});
};
const createScimGroup = async ({ displayName, orgId }: TCreateScimGroupDTO) => {
const org = await orgDAL.findById(orgId);
if (!org.scimEnabled)
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
status: 403
});
const group = await groupDAL.create({
name: displayName,
slug: slugify(`${displayName}-${alphaNumericNanoId(4)}`),
orgId,
role: OrgMembershipRole.NoAccess
});
return buildScimGroup({
groupId: group.id,
name: group.name,
members: []
});
};
const getScimGroup = async ({ groupId, orgId }: TGetScimGroupDTO) => {
const group = await groupDAL.findOne({
id: groupId,
orgId
});
if (!group) {
throw new ScimRequestError({
detail: "Group Not Found",
status: 404
});
}
const users = await groupDAL.findAllGroupMembers({
orgId: group.orgId,
groupId: group.id
});
return buildScimGroup({
groupId: group.id,
name: group.name,
members: users
.filter((user) => user.isPartOfGroup)
.map((user) => ({
value: user.id,
display: `${user.firstName} ${user.lastName}`
}))
});
};
const updateScimGroupNamePut = async ({ groupId, orgId, displayName }: TUpdateScimGroupNamePutDTO) => {
const [group] = await groupDAL.update(
{
id: groupId,
orgId
},
{
name: displayName
}
);
if (!group) {
throw new ScimRequestError({
detail: "Group Not Found",
status: 404
});
}
return buildScimGroup({
groupId: group.id,
name: group.name,
members: []
});
};
// TODO: add support for add/remove op
const updateScimGroupNamePatch = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => {
const org = await orgDAL.findById(orgId);
if (!org.scimEnabled)
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
status: 403
});
let group: TGroups | undefined;
for await (const operation of operations) {
switch (operation.op) {
case "replace": {
await groupDAL.update(
{
id: groupId,
orgId
},
{
name: operation.value.displayName
}
);
break;
}
case "add": {
// TODO
break;
}
case "remove": {
// TODO
break;
}
default: {
throw new ScimRequestError({
detail: "Invalid Operation",
status: 400
});
}
}
}
if (!group) {
throw new ScimRequestError({
detail: "Group Not Found",
status: 404
});
}
return buildScimGroup({
groupId: group.id,
name: group.name,
members: []
});
};
const deleteScimGroup = async ({ groupId, orgId }: TDeleteScimGroupDTO) => {
const [group] = await groupDAL.delete({
id: groupId,
orgId
});
if (!group) {
throw new ScimRequestError({
detail: "Group Not Found",
status: 404
});
}
return {}; // intentionally return empty object upon success
};
const fnValidateScimToken = async (token: TScimTokenJwtPayload) => {
const scimToken = await scimDAL.findById(token.scimTokenId);
if (!scimToken) throw new UnauthorizedError();
@ -682,13 +455,6 @@ export const scimServiceFactory = ({
createScimUser,
updateScimUser,
replaceScimUser,
deleteScimUser,
listScimGroups,
createScimGroup,
getScimGroup,
deleteScimGroup,
updateScimGroupNamePut,
updateScimGroupNamePatch,
fnValidateScimToken
};
};

View File

@ -59,73 +59,6 @@ export type TReplaceScimUserDTO = {
orgId: string;
};
export type TDeleteScimUserDTO = {
userId: string;
orgId: string;
};
export type TListScimGroupsDTO = {
offset: number;
limit: number;
orgId: string;
};
export type TListScimGroups = {
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"];
totalResults: number;
Resources: TScimGroup[];
itemsPerPage: number;
startIndex: number;
};
export type TCreateScimGroupDTO = {
displayName: string;
orgId: string;
};
export type TGetScimGroupDTO = {
groupId: string;
orgId: string;
};
export type TUpdateScimGroupNamePutDTO = {
groupId: string;
orgId: string;
displayName: string;
};
export type TUpdateScimGroupNamePatchDTO = {
groupId: string;
orgId: string;
operations: (TRemoveOp | TReplaceOp | TAddOp)[];
};
type TReplaceOp = {
op: "replace";
value: {
id: string;
displayName: string;
};
};
type TRemoveOp = {
op: "remove";
path: string;
};
type TAddOp = {
op: "add";
value: {
value: string;
display?: string;
};
};
export type TDeleteScimGroupDTO = {
groupId: string;
orgId: string;
};
export type TScimTokenJwtPayload = {
scimTokenId: string;
authTokenType: string;
@ -153,17 +86,3 @@ export type TScimUser = {
location: null;
};
};
export type TScimGroup = {
schemas: string[];
id: string;
displayName: string;
members: {
value: string;
display: string;
}[];
meta: {
resourceType: string;
location: null;
};
};

View File

@ -1,34 +1,3 @@
export const GROUPS = {
CREATE: {
name: "The name of the group to create.",
slug: "The slug of the group to create.",
role: "The role of the group to create."
},
UPDATE: {
currentSlug: "The current slug of the group to update.",
name: "The new name of the group to update to.",
slug: "The new slug of the group to update to.",
role: "The new role of the group to update to."
},
DELETE: {
slug: "The slug of the group to delete"
},
LIST_USERS: {
slug: "The slug of the group to list users for",
offset: "The offset to start from. If you enter 10, it will start from the 10th user.",
limit: "The number of users to return.",
username: "The username to search for."
},
ADD_USER: {
slug: "The slug of the group to add the user to.",
username: "The username of the user to add to the group."
},
DELETE_USER: {
slug: "The slug of the group to remove the user from.",
username: "The username of the user to remove from the group."
}
} as const;
export const IDENTITIES = {
CREATE: {
name: "The name of the identity to create.",
@ -110,9 +79,6 @@ export const ORGANIZATIONS = {
},
GET_PROJECTS: {
organizationId: "The ID of the organization to get projects from."
},
LIST_GROUPS: {
organizationId: "The ID of the organization to list groups for."
}
} as const;
@ -176,23 +142,6 @@ export const PROJECTS = {
ROLLBACK_TO_SNAPSHOT: {
secretSnapshotId: "The ID of the snapshot to rollback to."
},
ADD_GROUP_TO_PROJECT: {
projectSlug: "The slug of the project to add the group to.",
groupSlug: "The slug of the group to add to the project.",
role: "The role for the group to assume in the project."
},
UPDATE_GROUP_IN_PROJECT: {
projectSlug: "The slug of the project to update the group in.",
groupSlug: "The slug of the group to update in the project.",
roles: "A list of roles to update the group to."
},
REMOVE_GROUP_FROM_PROJECT: {
projectSlug: "The slug of the project to delete the group from.",
groupSlug: "The slug of the group to delete from the project."
},
LIST_GROUPS_IN_PROJECT: {
projectSlug: "The slug of the project to list groups for."
},
LIST_INTEGRATION: {
workspaceId: "The ID of the project to list integrations for."
},

View File

@ -1,17 +1,5 @@
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
export type TGenericPermission = {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string | undefined;
};
/**
* TODO(dangtony98): ideally move service fns to use TGenericPermission
* because TOrgPermission [orgId] is not as relevant anymore with the
* introduction of organizationIds bound to all user tokens
*/
export type TOrgPermission = {
actor: ActorType;
actorId: string;
@ -28,15 +16,6 @@ export type TProjectPermission = {
actorOrgId: string;
};
// same as TProjectPermission but with projectSlug requirement instead of projectId
export type TProjectSlugPermission = {
actor: ActorType;
actorId: string;
projectSlug: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
};
export type RequiredKeys<T> = {
[K in keyof T]-?: undefined extends T[K] ? never : K;
}[keyof T];

View File

@ -11,9 +11,6 @@ import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/pro
import { dynamicSecretLeaseDALFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal";
import { dynamicSecretLeaseQueueServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue";
import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
import { groupDALFactory } from "@app/ee/services/group/group-dal";
import { groupServiceFactory } from "@app/ee/services/group/group-service";
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal";
import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal";
@ -61,9 +58,6 @@ import { authPaswordServiceFactory } from "@app/services/auth/auth-password-serv
import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service";
import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal";
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
import { identityDALFactory } from "@app/services/identity/identity-dal";
import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal";
import { identityServiceFactory } from "@app/services/identity/identity-service";
@ -213,10 +207,6 @@ export const registerRoutes = async (
const gitAppInstallSessionDAL = gitAppInstallSessionDALFactory(db);
const gitAppOrgDAL = gitAppDALFactory(db);
const groupDAL = groupDALFactory(db);
const groupProjectDAL = groupProjectDALFactory(db);
const groupProjectMembershipRoleDAL = groupProjectMembershipRoleDALFactory(db);
const userGroupMembershipDAL = userGroupMembershipDALFactory(db);
const secretScanningDAL = secretScanningDALFactory(db);
const licenseDAL = licenseDALFactory(db);
const dynamicSecretDAL = dynamicSecretDALFactory(db);
@ -259,29 +249,6 @@ export const registerRoutes = async (
samlConfigDAL,
licenseService
});
const groupService = groupServiceFactory({
userDAL,
groupDAL,
groupProjectDAL,
orgDAL,
userGroupMembershipDAL,
projectDAL,
projectBotDAL,
projectKeyDAL,
permissionService,
licenseService
});
const groupProjectService = groupProjectServiceFactory({
groupDAL,
groupProjectDAL,
groupProjectMembershipRoleDAL,
userGroupMembershipDAL,
projectDAL,
projectKeyDAL,
projectBotDAL,
projectRoleDAL,
permissionService
});
const scimService = scimServiceFactory({
licenseService,
scimDAL,
@ -289,7 +256,6 @@ export const registerRoutes = async (
orgDAL,
projectDAL,
projectMembershipDAL,
groupDAL,
permissionService,
smtpService
});
@ -336,7 +302,6 @@ export const registerRoutes = async (
projectKeyDAL,
smtpService,
userDAL,
groupDAL,
orgBotDAL
});
const signupService = authSignupServiceFactory({
@ -382,7 +347,6 @@ export const registerRoutes = async (
projectBotDAL,
orgDAL,
userDAL,
userGroupMembershipDAL,
smtpService,
projectKeyDAL,
projectRoleDAL,
@ -662,8 +626,6 @@ export const registerRoutes = async (
password: passwordService,
signup: signupService,
user: userService,
group: groupService,
groupProject: groupProjectService,
permission: permissionService,
org: orgService,
orgRole: orgRoleService,

View File

@ -1,14 +1,6 @@
import { z } from "zod";
import {
GroupsSchema,
IncidentContactsSchema,
OrganizationsSchema,
OrgMembershipsSchema,
OrgRolesSchema,
UsersSchema
} from "@app/db/schemas";
import { ORGANIZATIONS } from "@app/lib/api-docs";
import { IncidentContactsSchema, OrganizationsSchema, OrgMembershipsSchema, UsersSchema } from "@app/db/schemas";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@ -226,41 +218,4 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
return { incidentContactsOrg };
}
});
server.route({
method: "GET",
url: "/:organizationId/groups",
schema: {
params: z.object({
organizationId: z.string().trim().describe(ORGANIZATIONS.LIST_GROUPS.organizationId)
}),
response: {
200: z.object({
groups: GroupsSchema.merge(
z.object({
customRole: OrgRolesSchema.pick({
id: true,
name: true,
slug: true,
permissions: true,
description: true
}).optional()
})
).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const groups = await server.services.org.getOrgGroups({
actor: req.permission.type,
actorId: req.permission.id,
orgId: req.params.organizationId,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return { groups };
}
});
};

View File

@ -349,7 +349,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const integrations = await server.services.integration.listIntegrationByProject({
actorId: req.permission.id,

View File

@ -1,201 +0,0 @@
import ms from "ms";
import { z } from "zod";
import {
GroupProjectMembershipsSchema,
GroupsSchema,
ProjectMembershipRole,
ProjectUserMembershipRolesSchema
} from "@app/db/schemas";
import { PROJECTS } from "@app/lib/api-docs";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
export const registerGroupProjectRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:projectSlug/groups/:groupSlug",
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Add group to project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectSlug: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.projectSlug),
groupSlug: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.groupSlug)
}),
body: z.object({
role: z
.string()
.trim()
.min(1)
.default(ProjectMembershipRole.NoAccess)
.describe(PROJECTS.ADD_GROUP_TO_PROJECT.role)
}),
response: {
200: z.object({
groupMembership: GroupProjectMembershipsSchema
})
}
},
handler: async (req) => {
const groupMembership = await server.services.groupProject.addGroupToProject({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
groupSlug: req.params.groupSlug,
projectSlug: req.params.projectSlug,
role: req.body.role
});
return { groupMembership };
}
});
server.route({
method: "PATCH",
url: "/:projectSlug/groups/:groupSlug",
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Update group in project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectSlug: z.string().trim().describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.projectSlug),
groupSlug: z.string().trim().describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.groupSlug)
}),
body: z.object({
roles: z
.array(
z.union([
z.object({
role: z.string(),
isTemporary: z.literal(false).default(false)
}),
z.object({
role: z.string(),
isTemporary: z.literal(true),
temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode),
temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"),
temporaryAccessStartTime: z.string().datetime()
})
])
)
.min(1)
.describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.roles)
}),
response: {
200: z.object({
roles: ProjectUserMembershipRolesSchema.array()
})
}
},
handler: async (req) => {
const roles = await server.services.groupProject.updateGroupInProject({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
groupSlug: req.params.groupSlug,
projectSlug: req.params.projectSlug,
roles: req.body.roles
});
return { roles };
}
});
server.route({
method: "DELETE",
url: "/:projectSlug/groups/:groupSlug",
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Remove group from project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectSlug: z.string().trim().describe(PROJECTS.REMOVE_GROUP_FROM_PROJECT.projectSlug),
groupSlug: z.string().trim().describe(PROJECTS.REMOVE_GROUP_FROM_PROJECT.groupSlug)
}),
response: {
200: z.object({
groupMembership: GroupProjectMembershipsSchema
})
}
},
handler: async (req) => {
const groupMembership = await server.services.groupProject.removeGroupFromProject({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
groupSlug: req.params.groupSlug,
projectSlug: req.params.projectSlug
});
return { groupMembership };
}
});
server.route({
method: "GET",
url: "/:projectSlug/groups",
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Return list of groups in project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectSlug: z.string().trim().describe(PROJECTS.LIST_GROUPS_IN_PROJECT.projectSlug)
}),
response: {
200: z.object({
groupMemberships: z
.object({
id: z.string(),
groupId: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
),
group: GroupsSchema.pick({ name: true, id: true, slug: true })
})
.array()
})
}
},
handler: async (req) => {
const groupMemberships = await server.services.groupProject.listGroupsInProject({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectSlug: req.params.projectSlug
});
return { groupMemberships };
}
});
};

View File

@ -50,7 +50,6 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId,
orgId: req.params.orgId
});
return { identityMemberships };
}
});

View File

@ -1,4 +1,3 @@
import { registerGroupProjectRouter } from "./group-project-router";
import { registerIdentityOrgRouter } from "./identity-org-router";
import { registerIdentityProjectRouter } from "./identity-project-router";
import { registerMfaRouter } from "./mfa-router";
@ -23,7 +22,6 @@ export const registerV2Routes = async (server: FastifyZodProvider) => {
async (projectServer) => {
await projectServer.register(registerProjectRouter);
await projectServer.register(registerIdentityProjectRouter);
await projectServer.register(registerGroupProjectRouter);
await projectServer.register(registerProjectMembershipRouter);
},
{ prefix: "/workspace" }

View File

@ -1,99 +0,0 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, sqlNestRelationships } from "@app/lib/knex";
export type TGroupProjectDALFactory = ReturnType<typeof groupProjectDALFactory>;
export const groupProjectDALFactory = (db: TDbClient) => {
const groupProjectOrm = ormify(db, TableName.GroupProjectMembership);
const findByProjectId = async (projectId: string, tx?: Knex) => {
try {
const docs = await (tx || db)(TableName.GroupProjectMembership)
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
.join(TableName.Groups, `${TableName.GroupProjectMembership}.groupId`, `${TableName.Groups}.id`)
.join(
TableName.GroupProjectMembershipRole,
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
`${TableName.GroupProjectMembership}.id`
)
.leftJoin(
TableName.ProjectRoles,
`${TableName.GroupProjectMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id`
)
.select(
db.ref("id").withSchema(TableName.GroupProjectMembership),
db.ref("createdAt").withSchema(TableName.GroupProjectMembership),
db.ref("updatedAt").withSchema(TableName.GroupProjectMembership),
db.ref("id").as("groupId").withSchema(TableName.Groups),
db.ref("name").as("groupName").withSchema(TableName.Groups),
db.ref("slug").as("groupSlug").withSchema(TableName.Groups),
db.ref("id").withSchema(TableName.GroupProjectMembership),
db.ref("role").withSchema(TableName.GroupProjectMembershipRole),
db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("membershipRoleId"),
db.ref("customRoleId").withSchema(TableName.GroupProjectMembershipRole),
db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
db.ref("temporaryMode").withSchema(TableName.GroupProjectMembershipRole),
db.ref("isTemporary").withSchema(TableName.GroupProjectMembershipRole),
db.ref("temporaryRange").withSchema(TableName.GroupProjectMembershipRole),
db.ref("temporaryAccessStartTime").withSchema(TableName.GroupProjectMembershipRole),
db.ref("temporaryAccessEndTime").withSchema(TableName.GroupProjectMembershipRole)
);
const members = sqlNestRelationships({
data: docs,
parentMapper: ({ groupId, groupName, groupSlug, id, createdAt, updatedAt }) => ({
id,
groupId,
createdAt,
updatedAt,
group: {
id: groupId,
name: groupName,
slug: groupSlug
}
}),
key: "id",
childrenMapper: [
{
label: "roles" as const,
key: "membershipRoleId",
mapper: ({
role,
customRoleId,
customRoleName,
customRoleSlug,
membershipRoleId,
temporaryRange,
temporaryMode,
temporaryAccessEndTime,
temporaryAccessStartTime,
isTemporary
}) => ({
id: membershipRoleId,
role,
customRoleId,
customRoleName,
customRoleSlug,
temporaryRange,
temporaryMode,
temporaryAccessEndTime,
temporaryAccessStartTime,
isTemporary
})
}
]
});
return members;
} catch (error) {
throw new DatabaseError({ error, name: "FindByProjectId" });
}
};
return { ...groupProjectOrm, findByProjectId };
};

View File

@ -1,10 +0,0 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TGroupProjectMembershipRoleDALFactory = ReturnType<typeof groupProjectMembershipRoleDALFactory>;
export const groupProjectMembershipRoleDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.GroupProjectMembershipRole);
return orm;
};

View File

@ -1,338 +0,0 @@
import { ForbiddenError } from "@casl/ability";
import ms from "ms";
import { ProjectMembershipRole, SecretKeyEncoding } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { TGroupDALFactory } from "../../ee/services/group/group-dal";
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
import { TGroupProjectDALFactory } from "./group-project-dal";
import { TGroupProjectMembershipRoleDALFactory } from "./group-project-membership-role-dal";
import {
TCreateProjectGroupDTO,
TDeleteProjectGroupDTO,
TListProjectGroupDTO,
TUpdateProjectGroupDTO
} from "./group-project-types";
type TGroupProjectServiceFactoryDep = {
groupProjectDAL: Pick<TGroupProjectDALFactory, "findOne" | "transaction" | "create" | "delete" | "findByProjectId">;
groupProjectMembershipRoleDAL: Pick<
TGroupProjectMembershipRoleDALFactory,
"create" | "transaction" | "insertMany" | "delete"
>;
userGroupMembershipDAL: TUserGroupMembershipDALFactory;
projectDAL: Pick<TProjectDALFactory, "findOne" | "findProjectGhostUser">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany" | "transaction">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
projectBotDAL: TProjectBotDALFactory;
groupDAL: Pick<TGroupDALFactory, "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissionByRole">;
};
export type TGroupProjectServiceFactory = ReturnType<typeof groupProjectServiceFactory>;
export const groupProjectServiceFactory = ({
groupDAL,
groupProjectDAL,
groupProjectMembershipRoleDAL,
userGroupMembershipDAL,
projectDAL,
projectKeyDAL,
projectBotDAL,
projectRoleDAL,
permissionService
}: TGroupProjectServiceFactoryDep) => {
const addGroupToProject = async ({
groupSlug,
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectSlug,
role
}: TCreateProjectGroupDTO) => {
const project = await projectDAL.findOne({
slug: projectSlug
});
if (!project) throw new BadRequestError({ message: `Failed to find project with slug ${projectSlug}` });
if (project.version < 2) throw new BadRequestError({ message: `Failed to add group to E2EE project` });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Groups);
const group = await groupDAL.findOne({ orgId: actorOrgId, slug: groupSlug });
if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
const existingGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
if (existingGroup)
throw new BadRequestError({
message: `Group with slug ${groupSlug} already exists in project with id ${project.id}`
});
const { permission: rolePermission, role: customRole } = await permissionService.getProjectPermissionByRole(
role,
project.id
);
const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPrivilege)
throw new ForbiddenRequestError({
message: "Failed to add group to project with more privileged role"
});
const isCustomRole = Boolean(customRole);
const projectGroup = await groupProjectDAL.transaction(async (tx) => {
const groupProjectMembership = await groupProjectDAL.create(
{
groupId: group.id,
projectId: project.id
},
tx
);
await groupProjectMembershipRoleDAL.create(
{
projectMembershipId: groupProjectMembership.id,
role: isCustomRole ? ProjectMembershipRole.Custom : role,
customRoleId: customRole?.id
},
tx
);
return groupProjectMembership;
});
// share project key with users in group that have not
// individually been added to the project and that are not part of
// other groups that are in the project
const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, project.id);
if (groupMembers.length) {
const ghostUser = await projectDAL.findProjectGhostUser(project.id);
if (!ghostUser) {
throw new BadRequestError({
message: "Failed to find sudo user"
});
}
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, project.id);
if (!ghostUserLatestKey) {
throw new BadRequestError({
message: "Failed to find sudo user latest key"
});
}
const bot = await projectBotDAL.findOne({ projectId: project.id });
if (!bot) {
throw new BadRequestError({
message: "Failed to find bot"
});
}
const botPrivateKey = infisicalSymmetricDecrypt({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const plaintextProjectKey = decryptAsymmetric({
ciphertext: ghostUserLatestKey.encryptedKey,
nonce: ghostUserLatestKey.nonce,
publicKey: ghostUserLatestKey.sender.publicKey,
privateKey: botPrivateKey
});
const projectKeyData = groupMembers.map(({ user: { publicKey, id } }) => {
const { ciphertext: encryptedKey, nonce } = encryptAsymmetric(plaintextProjectKey, publicKey, botPrivateKey);
return {
encryptedKey,
nonce,
senderId: ghostUser.id,
receiverId: id,
projectId: project.id
};
});
await projectKeyDAL.insertMany(projectKeyData);
}
return projectGroup;
};
const updateGroupInProject = async ({
projectSlug,
groupSlug,
roles,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TUpdateProjectGroupDTO) => {
const project = await projectDAL.findOne({
slug: projectSlug
});
if (!project) throw new BadRequestError({ message: `Failed to find project with slug ${projectSlug}` });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Groups);
const group = await groupDAL.findOne({ orgId: actorOrgId, slug: groupSlug });
if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
const projectGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
if (!projectGroup) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
// validate custom roles input
const customInputRoles = roles.filter(
({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
);
const hasCustomRole = Boolean(customInputRoles.length);
const customRoles = hasCustomRole
? await projectRoleDAL.find({
projectId: project.id,
$in: { slug: customInputRoles.map(({ role }) => role) }
})
: [];
if (customRoles.length !== customInputRoles.length) throw new BadRequestError({ message: "Custom role not found" });
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
const sanitizedProjectMembershipRoles = roles.map((inputRole) => {
const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
if (!inputRole.isTemporary) {
return {
projectMembershipId: projectGroup.id,
role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role,
customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null
};
}
// check cron or relative here later for now its just relative
const relativeTimeInMs = ms(inputRole.temporaryRange);
return {
projectMembershipId: projectGroup.id,
role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role,
customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null,
isTemporary: true,
temporaryMode: ProjectUserMembershipTemporaryMode.Relative,
temporaryRange: inputRole.temporaryRange,
temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs)
};
});
const updatedRoles = await groupProjectMembershipRoleDAL.transaction(async (tx) => {
await groupProjectMembershipRoleDAL.delete({ projectMembershipId: projectGroup.id }, tx);
return groupProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
});
return updatedRoles;
};
const removeGroupFromProject = async ({
projectSlug,
groupSlug,
actorId,
actor,
actorOrgId,
actorAuthMethod
}: TDeleteProjectGroupDTO) => {
const project = await projectDAL.findOne({
slug: projectSlug
});
if (!project) throw new BadRequestError({ message: `Failed to find project with slug ${projectSlug}` });
const group = await groupDAL.findOne({ orgId: actorOrgId, slug: groupSlug });
if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
const groupProjectMembership = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
if (!groupProjectMembership) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Groups);
const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, project.id);
if (groupMembers.length) {
await projectKeyDAL.delete({
projectId: project.id,
$in: {
receiverId: groupMembers.map(({ user: { id } }) => id)
}
});
}
const [deletedGroup] = await groupProjectDAL.delete({ groupId: group.id, projectId: project.id });
return deletedGroup;
};
const listGroupsInProject = async ({
projectSlug,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TListProjectGroupDTO) => {
const project = await projectDAL.findOne({
slug: projectSlug
});
if (!project) throw new BadRequestError({ message: `Failed to find project with slug ${projectSlug}` });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
const groupMemberships = await groupProjectDAL.findByProjectId(project.id);
return groupMemberships;
};
return {
addGroupToProject,
updateGroupInProject,
removeGroupFromProject,
listGroupsInProject
};
};

View File

@ -1,31 +0,0 @@
import { TProjectSlugPermission } from "@app/lib/types";
import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
export type TCreateProjectGroupDTO = {
groupSlug: string;
role: string;
} & TProjectSlugPermission;
export type TUpdateProjectGroupDTO = {
roles: (
| {
role: string;
isTemporary?: false;
}
| {
role: string;
isTemporary: true;
temporaryMode: ProjectUserMembershipTemporaryMode.Relative;
temporaryRange: string;
temporaryAccessStartTime: string;
}
)[];
groupSlug: string;
} & TProjectSlugPermission;
export type TDeleteProjectGroupDTO = {
groupSlug: string;
} & TProjectSlugPermission;
export type TListProjectGroupDTO = TProjectSlugPermission;

View File

@ -163,7 +163,7 @@ export const identityProjectServiceFactory = ({
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
const sanitizedProjectMembershipRoles = roles.map((inputRole) => {
const santiziedProjectMembershipRoles = roles.map((inputRole) => {
const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
if (!inputRole.isTemporary) {
return {
@ -189,7 +189,7 @@ export const identityProjectServiceFactory = ({
const updatedRoles = await identityProjectMembershipRoleDAL.transaction(async (tx) => {
await identityProjectMembershipRoleDAL.delete({ projectMembershipId: projectIdentity.id }, tx);
return identityProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
return identityProjectMembershipRoleDAL.insertMany(santiziedProjectMembershipRoles, tx);
});
return updatedRoles;
@ -246,8 +246,8 @@ export const identityProjectServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
const identityMemberships = await identityProjectDAL.findByProjectId(projectId);
return identityMemberships;
const identityMemberhips = await identityProjectDAL.findByProjectId(projectId);
return identityMemberhips;
};
return {

View File

@ -157,8 +157,8 @@ export const identityServiceFactory = ({
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
const identityMemberships = await identityOrgMembershipDAL.findByOrgId(orgId);
return identityMemberships;
const identityMemberhips = await identityOrgMembershipDAL.findByOrgId(orgId);
return identityMemberhips;
};
return {

View File

@ -6,7 +6,6 @@ import { Knex } from "knex";
import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas";
import { TProjects } from "@app/db/schemas/projects";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@ -35,7 +34,6 @@ import {
TDeleteOrgMembershipDTO,
TFindAllWorkspacesDTO,
TFindOrgMembersByEmailDTO,
TGetOrgGroupsDTO,
TInviteUserToOrgDTO,
TUpdateOrgDTO,
TUpdateOrgMembershipDTO,
@ -47,7 +45,6 @@ type TOrgServiceFactoryDep = {
orgBotDAL: TOrgBotDALFactory;
orgRoleDAL: TOrgRoleDALFactory;
userDAL: TUserDALFactory;
groupDAL: TGroupDALFactory;
projectDAL: TProjectDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findProjectMembershipsByUserId" | "delete">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
@ -67,7 +64,6 @@ export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
export const orgServiceFactory = ({
orgDAL,
userDAL,
groupDAL,
orgRoleDAL,
incidentContactDAL,
permissionService,
@ -117,13 +113,6 @@ export const orgServiceFactory = ({
return members;
};
const getOrgGroups = async ({ actor, actorId, orgId, actorAuthMethod, actorOrgId }: TGetOrgGroupsDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
const groups = await groupDAL.findByOrgId(orgId);
return groups;
};
const findOrgMembersByUsername = async ({
actor,
actorId,
@ -685,7 +674,6 @@ export const orgServiceFactory = ({
// incident contacts
findIncidentContacts,
createIncidentContact,
deleteIncidentContact,
getOrgGroups
deleteIncidentContact
};
};

View File

@ -53,5 +53,3 @@ export type TFindAllWorkspacesDTO = {
export type TUpdateOrgDTO = {
data: Partial<{ name: string; slug: string; authEnforced: boolean; scimEnabled: boolean }>;
} & TOrgPermission;
export type TGetOrgGroupsDTO = TOrgPermission;

View File

@ -17,7 +17,6 @@ import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
import { ActorType } from "../auth/auth-type";
import { TOrgDALFactory } from "../org/org-dal";
import { TProjectDALFactory } from "../project/project-dal";
@ -46,7 +45,6 @@ type TProjectMembershipServiceFactoryDep = {
projectMembershipDAL: TProjectMembershipDALFactory;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "find" | "delete">;
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">;
userGroupMembershipDAL: TUserGroupMembershipDALFactory;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction">;
@ -65,7 +63,6 @@ export const projectMembershipServiceFactory = ({
projectBotDAL,
orgDAL,
userDAL,
userGroupMembershipDAL,
projectDAL,
projectKeyDAL,
licenseService
@ -123,13 +120,6 @@ export const projectMembershipServiceFactory = ({
});
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
const userIdsToExcludeForProjectKeyAddition = new Set(
await userGroupMembershipDAL.findUserGroupMembershipsInProject(
orgMembers.map(({ username }) => username),
projectId
)
);
await projectMembershipDAL.transaction(async (tx) => {
const projectMemberships = await projectMembershipDAL.insertMany(
orgMembers.map(({ userId }) => ({
@ -145,15 +135,13 @@ export const projectMembershipServiceFactory = ({
);
const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId);
await projectKeyDAL.insertMany(
orgMembers
.filter(({ userId }) => !userIdsToExcludeForProjectKeyAddition.has(userId as string))
.map(({ userId, id }) => ({
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
senderId: actorId,
receiverId: userId as string,
projectId
})),
orgMembers.map(({ userId, id }) => ({
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
senderId: actorId,
receiverId: userId as string,
projectId
})),
tx
);
});
@ -259,10 +247,6 @@ export const projectMembershipServiceFactory = ({
const members: TProjectMemberships[] = [];
const userIdsToExcludeForProjectKeyAddition = new Set(
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId)
);
await projectMembershipDAL.transaction(async (tx) => {
const projectMemberships = await projectMembershipDAL.insertMany(
orgMembers.map(({ user }) => ({
@ -281,15 +265,13 @@ export const projectMembershipServiceFactory = ({
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
await projectKeyDAL.insertMany(
orgMembers
.filter(({ user }) => !userIdsToExcludeForProjectKeyAddition.has(user.id))
.map(({ user, id }) => ({
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
senderId: ghostUser.id,
receiverId: user.id,
projectId
})),
orgMembers.map(({ user, id }) => ({
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
senderId: ghostUser.id,
receiverId: user.id,
projectId
})),
tx
);
});
@ -362,7 +344,7 @@ export const projectMembershipServiceFactory = ({
if (customRoles.length !== customInputRoles.length) throw new BadRequestError({ message: "Custom role not found" });
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
const sanitizedProjectMembershipRoles = roles.map((inputRole) => {
const santiziedProjectMembershipRoles = roles.map((inputRole) => {
const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
if (!inputRole.isTemporary) {
return {
@ -388,7 +370,7 @@ export const projectMembershipServiceFactory = ({
const updatedRoles = await projectMembershipDAL.transaction(async (tx) => {
await projectUserMembershipRoleDAL.delete({ projectMembershipId: membershipId }, tx);
return projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
return projectUserMembershipRoleDAL.insertMany(santiziedProjectMembershipRoles, tx);
});
return updatedRoles;
@ -476,10 +458,6 @@ export const projectMembershipServiceFactory = ({
});
}
const userIdsToExcludeFromProjectKeyRemoval = new Set(
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId)
);
const memberships = await projectMembershipDAL.transaction(async (tx) => {
const deletedMemberships = await projectMembershipDAL.delete(
{
@ -491,15 +469,11 @@ export const projectMembershipServiceFactory = ({
tx
);
// delete project keys belonging to users that are not part of any other groups in the project
await projectKeyDAL.delete(
{
projectId,
$in: {
receiverId: projectMembers
.filter(({ user }) => !userIdsToExcludeFromProjectKeyRemoval.has(user.id))
.map(({ user }) => user.id)
.filter(Boolean)
receiverId: projectMembers.map(({ user }) => user.id).filter(Boolean)
}
},
tx

View File

@ -30,33 +30,8 @@ export const projectDALFactory = (db: TDbClient) => {
{ column: `${TableName.Environment}.position`, order: "asc" }
]);
const groups: string[] = await db(TableName.UserGroupMembership)
.where({ userId })
.select(selectAllTableCols(TableName.UserGroupMembership))
.pluck("groupId");
const groupWorkspaces = await db(TableName.GroupProjectMembership)
.whereIn("groupId", groups)
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
.whereNotIn(
`${TableName.Project}.id`,
workspaces.map(({ id }) => id)
)
.leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
.select(
selectAllTableCols(TableName.Project),
db.ref("id").withSchema(TableName.Project).as("_id"),
db.ref("id").withSchema(TableName.Environment).as("envId"),
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("name").withSchema(TableName.Environment).as("envName")
)
.orderBy([
{ column: `${TableName.Project}.name`, order: "asc" },
{ column: `${TableName.Environment}.position`, order: "asc" }
]);
const nestedWorkspaces = sqlNestRelationships({
data: workspaces.concat(groupWorkspaces),
data: workspaces,
key: "id",
parentMapper: ({ _id, ...el }) => ({ _id, ...ProjectsSchema.parse(el) }),
childrenMapper: [

View File

@ -1,4 +1,4 @@
---
title: "List project integrations "
title: "List Project Integrations"
openapi: "GET /api/v1/workspace/{workspaceId}/integrations"
---

View File

@ -1,67 +0,0 @@
---
title: "User Groups"
description: "Manage user groups in Infisical."
---
<Info>
User Groups is a paid feature.
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
then you should contact team@infisical.com to purchase an enterprise license to use it.
</Info>
## Concept
A (user) group is a collection of users that you can create in an Infisical organization to more efficiently manage permissions and access control for multiple users together. For example, you can have a group called `Developers` with the `Developer` role containing all the developers in your organization.
User groups have the following properties:
- If a group is added to a project under specific role(s), all users in the group will be provisioned access to the project with the role(s). Conversely, if a group is removed from a project, all users in the group will lose access to the project.
- If a user is added to a group, they will inherit the access control properties of the group including access to project(s) under the role(s) assigned to the group. Conversely, if a user is removed from a group, they will lose access to project(s) that the group has access to.
- If a user was previously added to a project under a role and is later added to a group that has access to the same project under a different role, then the user will now have access to the project under the composite permissions of the two roles. If the group is subsequently removed from the project, the user will not lose access to the project as they were previously added to the project separately.
- A user can be part of multiple groups. If a user is part of multiple groups, they will inherit the composite permissions of all the groups that they are part of.
## Workflow
In the following steps, we explore how to create and use user groups to provision user access to projects in Infisical.
<Steps>
<Step title="Creating a group">
To create a group, head to your Organization Settings > Access Control > Groups and press **Create group**.
![groups org](/images/platform/groups/groups-org.png)
When creating a group, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![groups org create](/images/platform/groups/groups-org-create.png)
Now input a few details for your new group. Heres some guidance for each field:
- Name (required): A friendly name for the group like `Engineering`.
- Slug (required): A unique identifier for the group like `engineering`.
- Role (required): A role from the Organization Roles tab for the group to assume. The organization role assigned will determine what organization level resources this group can have access to.
</Step>
<Step title="Adding users to the group">
Next, you'll want to assign users to the group. To do this, press on the users icon on the group and start assigning users to the group.
![groups org users](/images/platform/groups/groups-org-users.png)
In this example, we're assigning **Alan Turing** and **Ada Lovelace** to the group **Engineering**.
![groups org assign users](/images/platform/groups/groups-org-users-assign.png)
</Step>
<Step title="Adding the group to a project">
To enable the group to access project-level resources such as secrets within a specific project, you should add it to that project.
To do this, head over to the project you want to add the group to and go to Project Settings > Access Control > Groups and press **Add group**.
![groups project](/images/platform/groups/groups-project.png)
Next, select the group you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this group can have access to.
![groups project add](/images/platform/groups/groups-project-create.png)
That's it!
The users of the group now have access to the project under the role you assigned to the group.
</Step>
</Steps>

View File

@ -16,7 +16,7 @@ Prerequisites:
<Steps>
<Step title="Create a SCIM token in Infisical">
In Infisical, head to your Organization Settings > Authentication > SCIM Configuration and
press the **Enable SCIM provisioning** toggle to allow JumpCloud to provision/deprovision users and user groups for your organization.
press the **Enable SCIM provisioning** toggle to allow JumpCloud to provision/deprovision users for your organization.
![SCIM enable provisioning](/images/platform/scim/scim-enable-provisioning.png)
@ -49,7 +49,7 @@ Prerequisites:
![SCIM JumpCloud](/images/platform/scim/jumpcloud/scim-jumpcloud-test-connection.png)
Now JumpCloud can provision/deprovision users and user groups to/from your organization in Infisical.
Now JumpCloud can provision/deprovision users to/from your organization in Infisical.
</Step>
</Steps>

View File

@ -16,7 +16,7 @@ Prerequisites:
<Steps>
<Step title="Create a SCIM token in Infisical">
In Infisical, head to your Organization Settings > Authentication > SCIM Configuration and
press the **Enable SCIM provisioning** toggle to allow Okta to provision/deprovision users and user groups for your organization.
press the **Enable SCIM provisioning** toggle to allow Okta to provision/deprovision users for your organization.
![SCIM enable provisioning](/images/platform/scim/scim-enable-provisioning.png)
@ -38,7 +38,7 @@ Prerequisites:
- SCIM connector base URL: Input the **SCIM URL** from Step 1.
- Unique identifier field for users: Input `email`.
- Supported provisioning actions: Select **Push New Users**, **Push Profile Updates**, and **Push Groups**.
- Supported provisioning actions: Select **Push New Users** and **Push Profile Updates**.
- Authentication Mode: `HTTP Header`.
![SCIM Okta](/images/platform/scim/okta/scim-okta-config.png)
@ -55,7 +55,7 @@ Prerequisites:
![SCIM Okta](/images/platform/scim/okta/scim-okta-app-settings.png)
Now Okta can provision/deprovision users and user groups to/from your organization in Infisical.
Now Okta can provision/deprovision users to/from your organization in Infisical.
</Step>
</Steps>

View File

@ -10,7 +10,7 @@ description: "Learn how to provision users for Infisical via SCIM."
then you should contact sales@infisical.com to purchase an enterprise license to use it.
</Info>
You can configure your organization in Infisical to have users and user groups be provisioned/deprovisioned using [SCIM](https://scim.cloud/#Implementations2) via providers like Okta, Azure, JumpCloud, etc.
You can configure your organization in Infisical to have members be provisioned/deprovisioned using [SCIM](https://scim.cloud/#Implementations2) via providers like Okta, Azure, JumpCloud, etc.
- Provisioning: The SCIM provider pushes user information to Infisical. If the user exists in Infisical, Infisical sends an email invitation to add them to the relevant organization in Infisical; if not, Infisical initializes a new user and sends them an email invitation to finish setting up their account in the organization.
- Deprovisioning: The SCIM provider instructs Infisical to remove user(s) from an organization in Infisical.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 431 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 636 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 626 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 371 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 615 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 KiB

After

Width:  |  Height:  |  Size: 308 KiB

View File

@ -144,8 +144,7 @@
"documentation/platform/dynamic-secrets/overview",
"documentation/platform/dynamic-secrets/postgresql"
]
},
"documentation/platform/groups"
}
]
},
{

View File

@ -4,6 +4,7 @@
"requires": true,
"packages": {
"": {
"name": "frontend",
"dependencies": {
"@casl/ability": "^6.5.0",
"@casl/react": "^3.1.0",

View File

@ -16,7 +16,6 @@ export enum OrgPermissionSubjects {
Scim = "scim",
Sso = "sso",
Ldap = "ldap",
Groups = "groups",
Billing = "billing",
SecretScanning = "secret-scanning",
Identity = "identity"
@ -32,7 +31,6 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Scim]
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
| [OrgPermissionActions, OrgPermissionSubjects.Ldap]
| [OrgPermissionActions, OrgPermissionSubjects.Groups]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity];

View File

@ -10,7 +10,6 @@ export enum ProjectPermissionActions {
export enum ProjectPermissionSub {
Role = "role",
Member = "member",
Groups = "groups",
Settings = "settings",
Integrations = "integrations",
Webhooks = "webhooks",
@ -40,7 +39,6 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.Role]
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
| [ProjectPermissionActions, ProjectPermissionSub.Member]
| [ProjectPermissionActions, ProjectPermissionSub.Groups]
| [ProjectPermissionActions, ProjectPermissionSub.Integrations]
| [ProjectPermissionActions, ProjectPermissionSub.Webhooks]
| [ProjectPermissionActions, ProjectPermissionSub.AuditLogs]

View File

@ -1,9 +0,0 @@
export {
useAddUserToGroup,
useCreateGroup,
useDeleteGroup,
useRemoveUserFromGroup,
useUpdateGroup} from "./mutations";
export {
useListGroupUsers
} from "./queries";

View File

@ -1,130 +0,0 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { organizationKeys } from "../organization/queries";
import { groupKeys } from "./queries";
import { TGroup } from "./types";
export const useCreateGroup = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
name,
slug,
role
}: {
name: string;
slug: string;
organizationId: string;
role?: string;
}) => {
const {
data: group
} = await apiRequest.post<TGroup>("/api/v1/groups", {
name,
slug,
role
});
return group;
},
onSuccess: (_, { organizationId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgGroups(organizationId));
}
});
};
export const useUpdateGroup = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
currentSlug,
name,
slug,
role
}: {
currentSlug: string;
name?: string;
slug?: string;
role?: string;
}) => {
const {
data: group
} = await apiRequest.patch<TGroup>(`/api/v1/groups/${currentSlug}`, {
name,
slug,
role
});
return group;
},
onSuccess: ({ orgId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgGroups(orgId));
}
});
};
export const useDeleteGroup = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
slug
}: {
slug: string;
}) => {
const {
data: group
} = await apiRequest.delete<TGroup>(`/api/v1/groups/${slug}`);
return group;
},
onSuccess: ({ orgId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgGroups(orgId));
}
});
};
export const useAddUserToGroup = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
slug,
username
}: {
slug: string;
username: string;
}) => {
const {
data
} = await apiRequest.post<TGroup>(`/api/v1/groups/${slug}/users/${username}`);
return data;
},
onSuccess: (_, { slug }) => {
queryClient.invalidateQueries(groupKeys.forGroupUserMemberships(slug));
}
});
};
export const useRemoveUserFromGroup = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
slug,
username
}: {
slug: string;
username: string;
}) => {
const {
data
} = await apiRequest.delete<TGroup>(`/api/v1/groups/${slug}/users/${username}`);
return data;
},
onSuccess: (_, { slug }) => {
queryClient.invalidateQueries(groupKeys.forGroupUserMemberships(slug));
}
});
};

View File

@ -1,65 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
export const groupKeys = {
allGroupUserMemberships: () => ["group-user-memberships"] as const,
forGroupUserMemberships: (slug: string) => [...groupKeys.allGroupUserMemberships(), slug] as const,
specificGroupUserMemberships: ({
slug,
offset,
limit,
username
}: {
slug: string;
offset: number;
limit: number;
username: string;
}) => [...groupKeys.forGroupUserMemberships(slug), { offset, limit, username }] as const
};
type TUser = {
id: string;
email: string;
username: string;
firstName: string;
lastName: string;
isPartOfGroup: boolean;
};
export const useListGroupUsers = ({
groupSlug,
offset = 0,
limit = 10,
username
}: {
groupSlug: string;
offset: number;
limit: number;
username: string;
}) => {
return useQuery({
queryKey: groupKeys.specificGroupUserMemberships({
slug: groupSlug,
offset,
limit,
username
}),
enabled: Boolean(groupSlug),
queryFn: async () => {
const params = new URLSearchParams({
offset: String(offset),
limit: String(limit),
username
});
const { data } = await apiRequest.get<{ users: TUser[]; totalCount: number; }>(
`/api/v1/groups/${groupSlug}/users`, {
params
}
);
return data;
},
});
};

View File

@ -1,36 +0,0 @@
import { TOrgRole } from "../roles/types";
// TODO: rectify/standardize types
export type TGroupOrgMembership = TGroup & {
customRole?: TOrgRole;
}
export type TGroup = {
id: string;
name: string;
slug: string;
orgId: string;
createAt: string;
updatedAt: string;
role: string;
};
export type TGroupMembership = {
id: string;
group: TGroup;
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;
}[];
createdAt: string;
updatedAt: string;
};

View File

@ -5,7 +5,6 @@ export * from "./auth";
export * from "./bots";
export * from "./dynamicSecret";
export * from "./dynamicSecretLease";
export * from "./groups";
export * from "./identities";
export * from "./identityProjectAdditionalPrivilege";
export * from "./incidentContacts";

View File

@ -7,8 +7,7 @@ export {
useDeleteOrgPmtMethod,
useDeleteOrgTaxId,
useGetIdentityMembershipOrgs,
useGetOrganizationGroups,
useGetOrganizations,
useGetOrganizations,
useGetOrgBillingDetails,
useGetOrgInvoices,
useGetOrgLicenses,
@ -20,4 +19,4 @@ export {
useGetOrgTrialUrl,
useUpdateOrg,
useUpdateOrgBillingDetails
} from "./queries";
} from "./queries";

View File

@ -2,7 +2,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TGroupOrgMembership } from "../groups/types";
import { IdentityMembershipOrg } from "../identities/types";
import {
BillingDetails,
@ -29,8 +28,7 @@ export const organizationKeys = {
getOrgInvoices: (orgId: string) => [{ orgId }, "organization-invoices"] as const,
getOrgLicenses: (orgId: string) => [{ orgId }, "organization-licenses"] as const,
getOrgIdentityMemberships: (orgId: string) =>
[{ orgId }, "organization-identity-memberships"] as const,
getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const
[{ orgId }, "organization-identity-memberships"] as const
};
export const fetchOrganizations = async () => {
@ -406,19 +404,3 @@ export const useDeleteOrgById = () => {
}
});
};
export const useGetOrganizationGroups = (organizationId: string) => {
return useQuery({
queryKey: organizationKeys.getOrgGroups(organizationId),
enabled: Boolean(organizationId),
queryFn: async () => {
const {
data: { groups }
} = await apiRequest.get<{ groups: TGroupOrgMembership[] }>(
`/api/v1/organization/${organizationId}/groups`
);
return groups;
}
});
};

View File

@ -21,7 +21,6 @@ export type SubscriptionPlan = {
samlSSO: boolean;
scim: boolean;
ldap: boolean;
groups: boolean;
status:
| "incomplete"
| "incomplete_expired"

View File

@ -1,8 +1,3 @@
export {
useAddGroupToWorkspace,
useDeleteGroupFromWorkspace,
useUpdateGroupWorkspaceRole
} from "./mutations";
export {
useAddIdentityToWorkspace,
useCreateWorkspace,
@ -21,7 +16,6 @@ export {
useGetWorkspaceIntegrations,
useGetWorkspaceSecrets,
useGetWorkspaceUsers,
useListWorkspaceGroups,
useNameWorkspaceSecrets,
useRenameWorkspace,
useToggleAutoCapitalization,

View File

@ -1,64 +0,0 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "./queries";
import { TUpdateWorkspaceGroupRoleDTO } from "./types";
export const useAddGroupToWorkspace = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
groupSlug,
projectSlug,
role
}: {
groupSlug: string;
projectSlug: string;
role?: string;
}) => {
const {
data: { groupMembership }
} = await apiRequest.post(`/api/v2/workspace/${projectSlug}/groups/${groupSlug}`, {
role
});
return groupMembership;
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(projectSlug));
}
});
};
export const useUpdateGroupWorkspaceRole = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ groupSlug, projectSlug, roles }: TUpdateWorkspaceGroupRoleDTO) => {
const {
data: { groupMembership }
} = await apiRequest.patch(`/api/v2/workspace/${projectSlug}/groups/${groupSlug}`, {
roles
});
return groupMembership;
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(projectSlug));
}
});
};
export const useDeleteGroupFromWorkspace = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ groupSlug, projectSlug }: { groupSlug: string; projectSlug: string }) => {
const {
data: { groupMembership }
} = await apiRequest.delete(`/api/v2/workspace/${projectSlug}/groups/${groupSlug}`);
return groupMembership;
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(projectSlug));
}
});
};

View File

@ -2,7 +2,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TGroupMembership } from "../groups/types";
import { IdentityMembership } from "../identities/types";
import { IntegrationAuth } from "../integrationAuth/types";
import { TIntegration } from "../integrations/types";
@ -37,9 +36,7 @@ export const workspaceKeys = {
[{ workspaceId }, "workspace-audit-logs"] as const,
getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }, "workspace-users"] as const,
getWorkspaceIdentityMemberships: (workspaceId: string) =>
[{ workspaceId }, "workspace-identity-memberships"] as const,
getWorkspaceGroupMemberships: (workspaceId: string) =>
[{ workspaceId }, "workspace-groups"] as const
[{ workspaceId }, "workspace-identity-memberships"] as const
};
const fetchWorkspaceById = async (workspaceId: string) => {
@ -453,18 +450,3 @@ export const useGetWorkspaceIdentityMemberships = (workspaceId: string) => {
enabled: true
});
};
export const useListWorkspaceGroups = (projectSlug: string) => {
return useQuery({
queryKey: workspaceKeys.getWorkspaceGroupMemberships(projectSlug),
queryFn: async () => {
const {
data: { groupMemberships }
} = await apiRequest.get<{ groupMemberships: TGroupMembership[] }>(
`/api/v2/workspace/${projectSlug}/groups`
);
return groupMemberships;
},
enabled: true
});
};

View File

@ -111,21 +111,3 @@ export type TUpdateWorkspaceIdentityRoleDTO = {
}
)[];
};
export type TUpdateWorkspaceGroupRoleDTO = {
groupSlug: string;
projectSlug: string;
roles: (
| {
role: string;
isTemporary?: false;
}
| {
role: string;
isTemporary: true;
temporaryMode: ProjectUserMembershipTemporaryMode;
temporaryRange: string;
temporaryAccessStartTime: string;
}
)[];
};

View File

@ -3,16 +3,10 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import {
OrgGroupsTab,
OrgIdentityTab,
OrgMembersTab,
OrgRoleTabSection
} from "./components";
import { OrgIdentityTab, OrgMembersTab, OrgRoleTabSection } from "./components";
enum TabSections {
Member = "members",
Groups = "groups",
Roles = "roles",
Identities = "identities"
}
@ -26,7 +20,6 @@ export const MembersPage = withPermission(
<Tabs defaultValue={TabSections.Member}>
<TabList>
<Tab value={TabSections.Member}>People</Tab>
<Tab value={TabSections.Groups}>Groups</Tab>
<Tab value={TabSections.Identities}>
<div className="flex items-center">
<p>Machine Identities</p>
@ -37,9 +30,6 @@ export const MembersPage = withPermission(
<TabPanel value={TabSections.Member}>
<OrgMembersTab />
</TabPanel>
<TabPanel value={TabSections.Groups}>
<OrgGroupsTab />
</TabPanel>
<TabPanel value={TabSections.Identities}>
<OrgIdentityTab />
</TabPanel>

View File

@ -1,17 +0,0 @@
import { motion } from "framer-motion";
import { OrgGroupsSection } from "./components";
export const OrgGroupsTab = () => {
return (
<motion.div
key="panel-org-groups"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<OrgGroupsSection />
</motion.div>
);
}

View File

@ -1,169 +0,0 @@
import { useState } from "react";
import { faMagnifyingGlass,faUsers } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
EmptyState,
Input,
Modal,
ModalContent,
Pagination,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr} from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects
} from "@app/context";
import {
useAddUserToGroup,
useListGroupUsers,
useRemoveUserFromGroup} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
popUp: UsePopUpState<["groupMembers"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["groupMembers"]>, state?: boolean) => void;
};
export const OrgGroupMembersModal = ({
popUp,
handlePopUpToggle
}: Props) => {
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(10);
const [searchMemberFilter, setSearchMemberFilter] = useState("");
const popUpData = popUp?.groupMembers?.data as {
slug: string;
};
const { data, isLoading } = useListGroupUsers({
groupSlug: popUpData?.slug,
offset: (page - 1) * perPage,
limit: perPage,
username: searchMemberFilter
});
const { mutateAsync: assignMutateAsync } = useAddUserToGroup();
const { mutateAsync: unassignMutateAsync } = useRemoveUserFromGroup();
const handleAssignment = async (username: string, assign: boolean) => {
try {
if (!popUpData?.slug) return;
if (assign) {
await assignMutateAsync({
username,
slug: popUpData.slug
});
} else {
await unassignMutateAsync({
username,
slug: popUpData.slug
});
}
createNotification({
text: `Successfully ${assign ? "assigned" : "removed "} user ${assign ? "to" : "from"} group`,
type: "success"
});
} catch (err) {
createNotification({
text: `Failed to ${assign ? "assigned" : "remove"} user ${assign ? "to" : "from"} group`,
type: "error"
});
}
}
return (
<Modal
isOpen={popUp?.groupMembers?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("groupMembers", isOpen);
}}
>
<ModalContent title="Manage Group Members">
<Input
value={searchMemberFilter}
onChange={(e) => setSearchMemberFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search members..."
/>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th>User</Th>
<Th />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={2} innerKey="group-users" />}
{!isLoading && data?.users?.map(({
id,
firstName,
lastName,
username,
isPartOfGroup
}) => {
return (
<Tr className="items-center" key={`group-user-${id}`}>
<Td>
<p>{`${firstName} ${lastName}`}</p>
<p>{username}</p>
</Td>
<Td className="flex justify-end">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => {
return (
<Button
isLoading={isLoading}
isDisabled={!isAllowed}
colorSchema="primary"
variant="outline_bg"
type="submit"
onClick={() => handleAssignment(username, !isPartOfGroup)}
>
{isPartOfGroup ? "Unassign" : "Assign"}
</Button>
);
}}
</OrgPermissionCan>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isLoading && data?.totalCount !== undefined && (
<Pagination
count={data.totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
{!isLoading && !data?.users?.length && (
<EmptyState
title="No users found"
icon={faUsers}
/>
)}
</TableContainer>
</ModalContent>
</Modal>
);
}

View File

@ -1,188 +0,0 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useCreateGroup, useGetOrgRoles, useUpdateGroup } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const GroupFormSchema = z.object({
name: z.string().min(1, "Name cannot be empty").max(50, "Name must be 50 characters or fewer"),
slug: z.string().min(5, "Slug cannot be empty").max(36, "Slug must be 36 characters or fewer"),
role: z.string()
});
export type TGroupFormData = z.infer<typeof GroupFormSchema>;
type Props = {
popUp: UsePopUpState<["group"]>;
handlePopUpClose: (popUpName: keyof UsePopUpState<["group"]>) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["group"]>, state?: boolean) => void;
};
export const OrgGroupModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => {
const { currentOrg } = useOrganization();
const { data: roles } = useGetOrgRoles(currentOrg?.id || "");
const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateGroup();
const { mutateAsync: updateMutateAsync, isLoading: updateIsLoading } = useUpdateGroup();
const { control, handleSubmit, reset } = useForm<TGroupFormData>({
resolver: zodResolver(GroupFormSchema)
});
useEffect(() => {
const group = popUp?.group?.data as {
groupId: string;
name: string;
slug: string;
role: string;
customRole: {
name: string;
slug: string;
};
};
if (!roles?.length) return;
if (group) {
reset({
name: group.name,
slug: group.slug,
role: group?.customRole?.slug ?? group.role
});
} else {
reset({
name: "",
slug: "",
role: roles[0].slug
});
}
}, [popUp?.group?.data, roles]);
const onGroupModalSubmit = async ({ name, slug, role }: TGroupFormData) => {
try {
if (!currentOrg?.id) return;
const group = popUp?.group?.data as {
groupId: string;
name: string;
slug: string;
};
if (group) {
await updateMutateAsync({
currentSlug: group.slug,
name,
slug,
role: role || undefined
});
} else {
await createMutateAsync({
name,
slug,
organizationId: currentOrg.id,
role: role || undefined
});
}
handlePopUpToggle("group", false);
reset();
createNotification({
text: `Successfully ${popUp?.group?.data ? "updated" : "created"} group`,
type: "success"
});
} catch (err) {
createNotification({
text: `Failed to ${popUp?.group?.data ? "updated" : "created"} group`,
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.group?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("group", isOpen);
reset();
}}
>
<ModalContent title={`${popUp?.group?.data ? "Update" : "Create"} Group`}>
<form onSubmit={handleSubmit(onGroupModalSubmit)}>
<Controller
control={control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl label="Name" errorText={error?.message} isError={Boolean(error)}>
<Input {...field} placeholder="Engineering" />
</FormControl>
)}
/>
<Controller
control={control}
name="slug"
render={({ field, fieldState: { error } }) => (
<FormControl label="Slug" errorText={error?.message} isError={Boolean(error)}>
<Input {...field} placeholder="engineering" />
</FormControl>
)}
/>
<Controller
control={control}
name="role"
defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label={`${popUp?.group?.data ? "Update" : ""} Role`}
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{(roles || []).map(({ name, slug }) => (
<SelectItem value={slug} key={`org-group-role-${slug}`}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={createIsLoading || updateIsLoading}
>
{!popUp?.group?.data ? "Create" : "Update"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("group")}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};

View File

@ -1,120 +0,0 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
UpgradePlanModal
} from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useSubscription
} from "@app/context";
import { useDeleteGroup } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { OrgGroupMembersModal } from "./OrgGroupMembersModal";
import { OrgGroupModal } from "./OrgGroupModal";
import { OrgGroupsTable } from "./OrgGroupsTable";
export const OrgGroupsSection = () => {
const { subscription } = useSubscription();
const { mutateAsync: deleteMutateAsync } = useDeleteGroup();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"group",
"groupMembers",
"deleteGroup",
"upgradePlan"
] as const);
const handleAddGroupModal = () => {
if (!subscription?.groups) {
handlePopUpOpen("upgradePlan", {
description: "You can manage users more efficiently with groups if you upgrade your Infisical plan."
});
} else {
handlePopUpOpen("group");
}
}
const onDeleteGroupSubmit = async ({
name,
slug
}: {
name: string;
slug: string;
}) => {
try {
await deleteMutateAsync({
slug
});
createNotification({
text: `Successfully deleted the group named ${name}`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: `Failed to delete the group named ${name}`,
type: "error"
});
}
handlePopUpClose("deleteGroup");
}
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Groups</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handleAddGroupModal()}
isDisabled={!isAllowed}
>
Create Group
</Button>
)}
</OrgPermissionCan>
</div>
<OrgGroupsTable
handlePopUpOpen={handlePopUpOpen}
/>
<OrgGroupModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
<OrgGroupMembersModal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal
isOpen={popUp.deleteGroup.isOpen}
title={`Are you sure want to delete the group named ${
(popUp?.deleteGroup?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteGroup", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onDeleteGroupSubmit(
(popUp?.deleteGroup?.data as { name: string; slug: string })
)
}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
</div>
);
}

View File

@ -1,234 +0,0 @@
import { useState } from "react";
import { faMagnifyingGlass, faPencil, faUsers, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
EmptyState,
IconButton,
Input,
Select,
SelectItem,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization} from "@app/context";
import {
useGetOrganizationGroups,
useGetOrgRoles,
useUpdateGroup
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
["group", "deleteGroup", "groupMembers"]
>,
data?: {
groupId?: string;
name?: string;
slug?: string;
role?: string;
customRole?: {
name: string;
slug: string;
}
}
) => void;
};
export const OrgGroupsTable = ({
handlePopUpOpen
}: Props) => {
const [searchGroupsFilter, setSearchGroupsFilter] = useState("");
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { isLoading, data: groups } = useGetOrganizationGroups(orgId);
const { mutateAsync: updateMutateAsync } = useUpdateGroup();
const { data: roles } = useGetOrgRoles(orgId);
const handleChangeRole = async ({
currentSlug,
role
}: {
currentSlug: string;
role: string;
}) => {
try {
await updateMutateAsync({
currentSlug,
role
});
createNotification({
text: "Successfully updated group role",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to update group role",
type: "error"
});
}
}
return (
<div>
<Input
value={searchGroupsFilter}
onChange={(e) => setSearchGroupsFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search groups..."
/>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Slug</Th>
<Th>Role</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="org-groups" />}
{!isLoading && groups?.map(({ id, name, slug, role, customRole }) => {
return (
<Tr className="h-10" key={`org-group-${id}`}>
<Td>{name}</Td>
<Td>{slug}</Td>
<Td>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => {
return (
<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({
currentSlug: slug,
role: selectedRole
})
}
>
{(roles || []).map(({ slug: roleSlug, name: roleName }) => (
<SelectItem value={roleSlug} key={`role-option-${roleSlug}`}>
{roleName}
</SelectItem>
))}
</Select>
);
}}
</OrgPermissionCan>
</Td>
<Td>
<div className="flex items-center justify-end">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => (
<Tooltip content="Manage group members">
<IconButton
onClick={() => {
handlePopUpOpen("groupMembers", {
slug
});
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faUsers} />
</IconButton>
</Tooltip>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => (
<Tooltip content="Edit group">
<IconButton
onClick={async () => {
handlePopUpOpen("group", {
groupId: id,
name,
slug,
role,
customRole
});
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
</Tooltip>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => (
<Tooltip content="Delete group">
<IconButton
onClick={() => {
handlePopUpOpen("deleteGroup", {
slug,
name
});
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Tooltip>
)}
</OrgPermissionCan>
</div>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{groups?.length === 0 && (
<EmptyState title="No groups found" icon={faUsers} />
)}
</TableContainer>
</div>
);
}

View File

@ -1 +0,0 @@
export { OrgGroupsSection } from "./OrgGroupsSection";

View File

@ -1 +0,0 @@
export { OrgGroupsSection } from "./OrgGroupsSection";

View File

@ -1 +0,0 @@
export { OrgGroupsTab } from "./OrgGroupsTab";

View File

@ -151,7 +151,7 @@ export const IdentityModal = ({ popUp, /* handlePopUpOpen, */ handlePopUpToggle
const error = err as any;
const text =
error?.response?.data?.message ??
`Failed to ${popUp?.identity?.data ? "update" : "create"} identity`;
`Failed to ${popUp?.identity?.data ? "updated" : "created"} identity`;
createNotification({
text,

View File

@ -5,13 +5,13 @@ import { OrgMembersSection } from "./components";
export const OrgMembersTab = () => {
return (
<motion.div
key="panel-org-members"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
key="panel-service-token"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<OrgMembersSection />
<OrgMembersSection />
</motion.div>
);
}
};

View File

@ -264,7 +264,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
</TBody>
</Table>
{!isLoading && filterdUser?.length === 0 && (
<EmptyState title="No organization members found" icon={faUsers} />
<EmptyState title="No project members found" icon={faUsers} />
)}
</TableContainer>
</div>

View File

@ -7,7 +7,6 @@ import {
faMoneyBill,
faServer,
faSignIn,
faUser,
faUserCog,
faUsers
} from "@fortawesome/free-solid-svg-icons";
@ -36,16 +35,10 @@ type Props = {
const SIMPLE_PERMISSION_OPTIONS = [
{
title: "User management",
subtitle: "Invite, view and remove users from the organization",
icon: faUser,
formName: "member"
},
{
title: "Group management",
subtitle: "Invite, view and remove user groups from the organization",
title: "Members management",
subtitle: "Invite, view and remove members from the organization",
icon: faUsers,
formName: "groups"
formName: "member"
},
{
title: "Machine identity management",

View File

@ -28,7 +28,6 @@ export const formSchema = z.object({
})
.optional(),
member: generalPermissionSchema,
groups: generalPermissionSchema,
role: generalPermissionSchema,
settings: generalPermissionSchema,
"service-account": generalPermissionSchema,

View File

@ -1,4 +1,3 @@
export { OrgGroupsTab } from "./OrgGroupsTab";
export { OrgIdentityTab } from "./OrgIdentityTab";
export { OrgMembersTab } from "./OrgMembersTab";
export { OrgRoleTabSection } from "./OrgRoleTabSection";

View File

@ -2,28 +2,20 @@
import { motion } from "framer-motion";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import {
GroupsTab,
IdentityTab,
MemberListTab,
ProjectRoleListTab,
ServiceTokenTab
} from "./components";
import { IdentityTab, MemberListTab, ProjectRoleListTab, ServiceTokenTab } from "./components";
enum TabSections {
Member = "members",
Roles = "roles",
Groups = "groups",
Identities = "identities",
ServiceTokens = "service-tokens"
}
export const MembersPage = withProjectPermission(
() => {
const { currentWorkspace } = useWorkspace();
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
@ -31,9 +23,6 @@ export const MembersPage = withProjectPermission(
<Tabs defaultValue={TabSections.Member}>
<TabList>
<Tab value={TabSections.Member}>People</Tab>
{currentWorkspace?.version && currentWorkspace.version > 1 && (
<Tab value={TabSections.Groups}>Groups</Tab>
)}
<Tab value={TabSections.Identities}>
<div className="flex items-center">
<p>Machine Identities</p>
@ -45,19 +34,6 @@ export const MembersPage = withProjectPermission(
<TabPanel value={TabSections.Member}>
<MemberListTab />
</TabPanel>
{currentWorkspace?.version && currentWorkspace.version > 1 && (
<TabPanel value={TabSections.Groups}>
<motion.div
key="panel-groups"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<GroupsTab />
</motion.div>
</TabPanel>
)}
<TabPanel value={TabSections.Identities}>
<IdentityTab />
</TabPanel>

View File

@ -1,19 +0,0 @@
import { motion } from "framer-motion";
import {
GroupsSection
} from "./components";
export const GroupsTab = () => {
return (
<motion.div
key="panel-groups"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<GroupsSection />
</motion.div>
);
}

View File

@ -1,182 +0,0 @@
import { useMemo } from "react";
import { Controller, useForm } from "react-hook-form";
import Link from "next/link";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Modal,
ModalContent,
Select,
SelectItem} from "@app/components/v2";
import { useOrganization, useWorkspace } from "@app/context";
import {
useAddGroupToWorkspace,
useGetOrganizationGroups,
useGetProjectRoles,
useListWorkspaceGroups,
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z.object({
slug: z.string(),
role: z.string()
});
export type FormData = z.infer<typeof schema>;
type Props = {
popUp: UsePopUpState<["group"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["group"]>, state?: boolean) => void;
};
export const GroupModal = ({
popUp,
handlePopUpToggle
}: Props) => {
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const orgId = currentOrg?.id || "";
const workspaceId = currentWorkspace?.id || "";
const { data: groups } = useGetOrganizationGroups(orgId);
const { data: groupMemberships } = useListWorkspaceGroups(currentWorkspace?.slug || "");
const { data: roles } = useGetProjectRoles(workspaceId);
const { mutateAsync: addGroupToWorkspaceMutateAsync } = useAddGroupToWorkspace();
const filteredGroupMembershipOrgs = useMemo(() => {
const wsGroupIds = new Map();
groupMemberships?.forEach((groupMembership) => {
wsGroupIds.set(groupMembership.group.id, true);
});
return (groups || []).filter(({ id }) => !wsGroupIds.has(id));
}, [groups, groupMemberships]);
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema)
});
const onFormSubmit = async ({ slug, role }: FormData) => {
try {
await addGroupToWorkspaceMutateAsync({
projectSlug: currentWorkspace?.slug || "",
groupSlug: slug,
role: role || undefined
});
reset();
handlePopUpToggle("group", false);
createNotification({
text: "Successfully added group to project",
type: "success"
});
} catch (err) {
createNotification({
text: "Failed to add group to project",
type: "error"
});
}
}
return (
<Modal
isOpen={popUp?.group?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("group", isOpen);
reset();
}}
>
<ModalContent title="Add Group to Project">
{filteredGroupMembershipOrgs.length ? (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="slug"
defaultValue={filteredGroupMembershipOrgs?.[0]?.id}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Group" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{filteredGroupMembershipOrgs.map(({ name, slug, id }) => (
<SelectItem value={slug} key={`org-group-${id}`}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="role"
defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Role"
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{(roles || []).map(({ name, slug }) => (
<SelectItem value={slug} key={`st-role-${slug}`}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.group?.data ? "Update" : "Create"}
</Button>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</div>
</form>
) : (
<div className="flex flex-col space-y-4">
<div className="text-sm">
All groups in your organization have already been added to this project.
</div>
<Link href={`/org/${currentWorkspace?.orgId}/members`}>
<Button variant="outline_bg">Create a new group</Button>
</Link>
</div>
)}
</ModalContent>
</Modal>
);
}

View File

@ -1,457 +0,0 @@
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 { createNotification } from "@app/components/notifications";
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, useUpdateGroupWorkspaceRole } from "@app/hooks/api";
import { TGroupMembership } from "@app/hooks/api/groups/types";
import { ProjectMembershipRole } from "@app/hooks/api/roles/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;
groupSlug: string;
roles: TGroupMembership["roles"];
};
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
export const GroupRoles = ({
roles = [],
disableEdit = false,
groupSlug
}: TMemberRolesProp) => {
const { currentWorkspace } = useWorkspace();
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 updateGroupWorkspaceRole = useUpdateGroupWorkspaceRole();
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 updateGroupWorkspaceRole.mutateAsync({
projectSlug: currentWorkspace?.slug || "",
groupSlug,
roles: selectedRoles
});
createNotification({ text: "Successfully updated group role", type: "success" });
handlePopUpToggle("editRole");
setSearchRoles("");
} catch (err) {
createNotification({ text: "Failed to update group 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 });
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>
);
};

View File

@ -1,105 +0,0 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
UpgradePlanModal
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useSubscription,useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteGroupFromWorkspace } from "@app/hooks/api";
import { GroupModal } from "./GroupModal";
import { GroupTable } from "./GroupsTable";
export const GroupsSection = () => {
const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace();
const { mutateAsync: deleteMutateAsync } = useDeleteGroupFromWorkspace();
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"group",
"deleteGroup",
"upgradePlan"
] as const);
const handleAddGroupModal = () => {
if (!subscription?.groups) {
handlePopUpOpen("upgradePlan", {
description: "You can manage users more efficiently with groups if you upgrade your Infisical plan."
});
} else {
handlePopUpOpen("group");
}
}
const onRemoveGroupSubmit = async (groupSlug: string) => {
try {
await deleteMutateAsync({
groupSlug,
projectSlug: currentWorkspace?.slug || ""
});
createNotification({
text: "Successfully removed identity from project",
type: "success"
});
handlePopUpClose("deleteGroup");
} catch (err) {
console.error(err);
const error = err as any;
const text = error?.response?.data?.message ?? "Failed to remove group from project";
createNotification({
text,
type: "error"
});
}
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Groups</p>
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Groups}>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handleAddGroupModal()}
isDisabled={!isAllowed}
>
Add Group
</Button>
)}
</ProjectPermissionCan>
</div>
<GroupModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<GroupTable handlePopUpOpen={handlePopUpOpen} />
<DeleteActionModal
isOpen={popUp.deleteGroup.isOpen}
title={`Are you sure want to remove the group ${
(popUp?.deleteGroup?.data as { name: string })?.name || ""
} from the project?`}
onChange={(isOpen) => handlePopUpToggle("deleteGroup", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onRemoveGroupSubmit(
(popUp?.deleteGroup?.data as { slug: string })?.slug
)
}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
</div>
);
}

View File

@ -1,103 +0,0 @@
import { faServer, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
EmptyState,
IconButton,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useListWorkspaceGroups } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { GroupRoles } from "./GroupRoles";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteGroup", "group"]>,
data?: {
slug?: string;
name?: string;
}
) => void;
};
export const GroupTable = ({ handlePopUpOpen }: Props) => {
const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useListWorkspaceGroups(currentWorkspace?.slug || "");
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Role</Th>
<Th>Added on</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="project-groups" />}
{!isLoading &&
data &&
data.length > 0 &&
data.map(({ group: { id, name, slug }, roles, createdAt }) => {
return (
<Tr className="h-10" key={`st-v3-${id}`}>
<Td>{name}</Td>
<Td>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Groups}
>
{(isAllowed) => (
<GroupRoles roles={roles} disableEdit={!isAllowed} groupSlug={slug} />
)}
</ProjectPermissionCan>
</Td>
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
<Td className="flex justify-end">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Groups}
>
{(isAllowed) => (
<IconButton
onClick={() => {
handlePopUpOpen("deleteGroup", {
slug,
name
});
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
)}
</ProjectPermissionCan>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isLoading && data?.length === 0 && (
<EmptyState title="No groups have been added to this project" icon={faServer} />
)}
</TableContainer>
);
};

View File

@ -1 +0,0 @@
export { GroupsSection } from "./GroupsSection";

View File

@ -1 +0,0 @@
export { GroupsSection } from "./GroupsSection";

View File

@ -1 +0,0 @@
export { GroupsTab } from "./GroupsTab";

Some files were not shown because too many files have changed in this diff Show More