WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
347aa57
feat: adds identity group table
gupta-piyush19 Dec 2, 2025
22cea00
feat: implemented removeIdentityFromGroup service and endpoint
gupta-piyush19 Dec 2, 2025
6a8316d
feat: implemented addIdentitiesToGroup service and endpoint
gupta-piyush19 Dec 2, 2025
5d2a34c
fix: identity org check
gupta-piyush19 Dec 2, 2025
f9b7791
Merge branch 'main' of https://github.com/Infisical/infisical into fe…
gupta-piyush19 Dec 3, 2025
5fb0b59
feat: implemented listGroupIdentities endpoint and service
gupta-piyush19 Dec 3, 2025
eb1e890
feat: adds frontend hooks
gupta-piyush19 Dec 3, 2025
1c25327
fix: identity group permission extension
gupta-piyush19 Dec 3, 2025
5e0f1d8
fix: identity-org-dal, services and added add group members modal
gupta-piyush19 Dec 3, 2025
76d7f26
feat: group members table
gupta-piyush19 Dec 4, 2025
2b91afd
fix: machine identity tables and UI
gupta-piyush19 Dec 5, 2025
eb6c976
Merge branch 'main' of https://github.com/Infisical/infisical into fe…
gupta-piyush19 Dec 7, 2025
37546a7
docs: added machine identity group docs and API endpoints ref
gupta-piyush19 Dec 7, 2025
24efd8b
Merge branch 'main' of https://github.com/Infisical/infisical into fe…
gupta-piyush19 Dec 8, 2025
d41ece8
feat: review changes
gupta-piyush19 Dec 8, 2025
771c981
Merge branch 'main' of https://github.com/Infisical/infisical into fe…
gupta-piyush19 Dec 9, 2025
d1b3b69
fix: review changes
gupta-piyush19 Dec 9, 2025
7f1636f
Merge branch 'main' of https://github.com/Infisical/infisical into fe…
gupta-piyush19 Dec 9, 2025
0aff45d
fix: service nesting
gupta-piyush19 Dec 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions backend/src/@types/knex.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ import {
TIdentityGcpAuths,
TIdentityGcpAuthsInsert,
TIdentityGcpAuthsUpdate,
TIdentityGroupMembership,
TIdentityGroupMembershipInsert,
TIdentityGroupMembershipUpdate,
TIdentityJwtAuths,
TIdentityJwtAuthsInsert,
TIdentityJwtAuthsUpdate,
Expand Down Expand Up @@ -857,6 +860,11 @@ declare module "knex/types/tables" {
TUserGroupMembershipInsert,
TUserGroupMembershipUpdate
>;
[TableName.IdentityGroupMembership]: KnexOriginal.CompositeTableType<
TIdentityGroupMembership,
TIdentityGroupMembershipInsert,
TIdentityGroupMembershipUpdate
>;
[TableName.GroupProjectMembership]: KnexOriginal.CompositeTableType<
TGroupProjectMemberships,
TGroupProjectMembershipsInsert,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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.IdentityGroupMembership))) {
await knex.schema.createTable(TableName.IdentityGroupMembership, (t) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we discussed to roll out a generic membership and then migrate user to it later

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially proposed a separate membership. Then we’ll merge them later so the generic membership doesn’t look incomplete until we merge. I think merging it in another PR while deprecating the existing ones makes more sense. What do you suggest? Should I change to the generic membership from this PR itself?

t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("identityId").notNullable();
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
t.uuid("groupId").notNullable();
t.foreign("groupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
t.timestamps(true, true, true);

t.unique(["identityId", "groupId"]);
});
}

await createOnUpdateTrigger(knex, TableName.IdentityGroupMembership);
}

export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.IdentityGroupMembership)) {
await knex.schema.dropTable(TableName.IdentityGroupMembership);
await dropOnUpdateTrigger(knex, TableName.IdentityGroupMembership);
}
}
22 changes: 22 additions & 0 deletions backend/src/db/schemas/identity-group-membership.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.

import { z } from "zod";

import { TImmutableDBKeys } from "./models";

export const IdentityGroupMembershipSchema = z.object({
id: z.string().uuid(),
identityId: z.string().uuid(),
groupId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});

export type TIdentityGroupMembership = z.infer<typeof IdentityGroupMembershipSchema>;
export type TIdentityGroupMembershipInsert = Omit<z.input<typeof IdentityGroupMembershipSchema>, TImmutableDBKeys>;
export type TIdentityGroupMembershipUpdate = Partial<
Omit<z.input<typeof IdentityGroupMembershipSchema>, TImmutableDBKeys>
>;
1 change: 1 addition & 0 deletions backend/src/db/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export * from "./identity-alicloud-auths";
export * from "./identity-aws-auths";
export * from "./identity-azure-auths";
export * from "./identity-gcp-auths";
export * from "./identity-group-membership";
export * from "./identity-jwt-auths";
export * from "./identity-kubernetes-auths";
export * from "./identity-metadata";
Expand Down
1 change: 1 addition & 0 deletions backend/src/db/schemas/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export enum TableName {
GroupProjectMembershipRole = "group_project_membership_roles",
ExternalGroupOrgRoleMapping = "external_group_org_role_mappings",
UserGroupMembership = "user_group_membership",
IdentityGroupMembership = "identity_group_membership",
UserAliases = "user_aliases",
UserEncryptionKey = "user_encryption_keys",
AuthTokens = "auth_tokens",
Expand Down
219 changes: 200 additions & 19 deletions backend/src/ee/routes/v1/group-router.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { z } from "zod";

import { GroupsSchema, OrgMembershipRole, ProjectsSchema, UsersSchema } from "@app/db/schemas";
import { GroupsSchema, IdentitiesSchema, OrgMembershipRole, ProjectsSchema, UsersSchema } from "@app/db/schemas";
import {
EFilterReturnedProjects,
EFilterReturnedUsers,
EGroupProjectsOrderBy
FilterMemberType,
FilterReturnedIdentities,
FilterReturnedProjects,
FilterReturnedUsers,
GroupMembersOrderBy,
GroupProjectsOrderBy
} from "@app/ee/services/group/group-types";
import { ApiDocsTags, GROUPS } from "@app/lib/api-docs";
import { OrderByDirection } from "@app/lib/types";
Expand All @@ -13,6 +16,11 @@ import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";

const GroupIdentityResponseSchema = IdentitiesSchema.pick({
id: true,
name: true
});

export const registerGroupRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/",
Expand Down Expand Up @@ -191,7 +199,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
limit: z.coerce.number().min(1).max(100).default(10).describe(GROUPS.LIST_USERS.limit),
username: z.string().trim().optional().describe(GROUPS.LIST_USERS.username),
search: z.string().trim().optional().describe(GROUPS.LIST_USERS.search),
filter: z.nativeEnum(EFilterReturnedUsers).optional().describe(GROUPS.LIST_USERS.filterUsers)
filter: z.nativeEnum(FilterReturnedUsers).optional().describe(GROUPS.LIST_USERS.filterUsers)
}),
response: {
200: z.object({
Expand All @@ -202,12 +210,10 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
lastName: true,
id: true
})
.merge(
z.object({
isPartOfGroup: z.boolean(),
joinedGroupAt: z.date().nullable()
})
)
.extend({
isPartOfGroup: z.boolean(),
joinedGroupAt: z.date().nullable()
})
.array(),
totalCount: z.number()
})
Expand All @@ -227,6 +233,119 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
}
});

server.route({
method: "GET",
url: "/:id/identities",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.Groups],
params: z.object({
id: z.string().trim().describe(GROUPS.LIST_IDENTITIES.id)
}),
querystring: z.object({
offset: z.coerce.number().min(0).default(0).describe(GROUPS.LIST_IDENTITIES.offset),
limit: z.coerce.number().min(1).max(100).default(10).describe(GROUPS.LIST_IDENTITIES.limit),
search: z.string().trim().optional().describe(GROUPS.LIST_IDENTITIES.search),
filter: z.nativeEnum(FilterReturnedIdentities).optional().describe(GROUPS.LIST_IDENTITIES.filterIdentities)
}),
response: {
200: z.object({
identities: GroupIdentityResponseSchema.extend({
isPartOfGroup: z.boolean(),
joinedGroupAt: z.date().nullable()
}).array(),
totalCount: z.number()
})
}
},
handler: async (req) => {
const { identities, totalCount } = await server.services.group.listGroupIdentities({
id: req.params.id,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query
});

return { identities, totalCount };
}
});

server.route({
method: "GET",
url: "/:id/members",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.Groups],
params: z.object({
id: z.string().trim().describe(GROUPS.LIST_MEMBERS.id)
}),
querystring: z.object({
offset: z.coerce.number().min(0).default(0).describe(GROUPS.LIST_MEMBERS.offset),
limit: z.coerce.number().min(1).max(100).default(10).describe(GROUPS.LIST_MEMBERS.limit),
search: z.string().trim().optional().describe(GROUPS.LIST_MEMBERS.search),
orderBy: z
.nativeEnum(GroupMembersOrderBy)
.default(GroupMembersOrderBy.Name)
.optional()
.describe(GROUPS.LIST_MEMBERS.orderBy),
orderDirection: z.nativeEnum(OrderByDirection).optional().describe(GROUPS.LIST_MEMBERS.orderDirection),
memberTypeFilter: z
.union([z.nativeEnum(FilterMemberType), z.array(z.nativeEnum(FilterMemberType))])
.optional()
.describe(GROUPS.LIST_MEMBERS.memberTypeFilter)
.transform((val) => {
if (!val) return undefined;
return Array.isArray(val) ? val : [val];
})
}),
response: {
200: z.object({
members: z
.union([
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should only returns users - returning multiple format of api response is not allowed, due to parsing issue. We should keep it seperate endpoint for both list operation

UsersSchema.pick({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An API should not return multiple responses. It should be always one. I think we can split this into two endpoints

email: true,
username: true,
firstName: true,
lastName: true,
id: true
}).extend({
joinedGroupAt: z.date().nullable(),
memberType: z.literal("user")
}),
GroupIdentityResponseSchema.extend({
joinedGroupAt: z.date().nullable(),
memberType: z.literal("identity")
})
])
.array(),
totalCount: z.number()
})
}
},
handler: async (req) => {
const { members, totalCount } = await server.services.group.listGroupMembers({
id: req.params.id,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query
});

return { members, totalCount };
}
});

server.route({
method: "GET",
url: "/:id/projects",
Expand All @@ -244,10 +363,10 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
offset: z.coerce.number().min(0).default(0).describe(GROUPS.LIST_PROJECTS.offset),
limit: z.coerce.number().min(1).max(100).default(10).describe(GROUPS.LIST_PROJECTS.limit),
search: z.string().trim().optional().describe(GROUPS.LIST_PROJECTS.search),
filter: z.nativeEnum(EFilterReturnedProjects).optional().describe(GROUPS.LIST_PROJECTS.filterProjects),
filter: z.nativeEnum(FilterReturnedProjects).optional().describe(GROUPS.LIST_PROJECTS.filterProjects),
orderBy: z
.nativeEnum(EGroupProjectsOrderBy)
.default(EGroupProjectsOrderBy.Name)
.nativeEnum(GroupProjectsOrderBy)
.default(GroupProjectsOrderBy.Name)
.describe(GROUPS.LIST_PROJECTS.orderBy),
orderDirection: z
.nativeEnum(OrderByDirection)
Expand All @@ -263,11 +382,9 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
description: true,
type: true
})
.merge(
z.object({
joinedGroupAt: z.date().nullable()
})
)
.extend({
joinedGroupAt: z.date().nullable()
})
.array(),
totalCount: z.number()
})
Expand Down Expand Up @@ -325,6 +442,38 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
}
});

server.route({
method: "POST",
url: "/:id/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.Groups],
params: z.object({
id: z.string().trim().describe(GROUPS.ADD_IDENTITY.id),
identityId: z.string().trim().describe(GROUPS.ADD_IDENTITY.identityId)
}),
response: {
200: GroupIdentityResponseSchema
}
},
handler: async (req) => {
const identity = await server.services.group.addIdentityToGroup({
id: req.params.id,
identityId: req.params.identityId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});

return identity;
}
});

server.route({
method: "DELETE",
url: "/:id/users/:username",
Expand Down Expand Up @@ -362,4 +511,36 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
return user;
}
});

server.route({
method: "DELETE",
url: "/:id/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.Groups],
params: z.object({
id: z.string().trim().describe(GROUPS.DELETE_IDENTITY.id),
identityId: z.string().trim().describe(GROUPS.DELETE_IDENTITY.identityId)
}),
response: {
200: GroupIdentityResponseSchema
}
},
handler: async (req) => {
const identity = await server.services.group.removeIdentityFromGroup({
id: req.params.id,
identityId: req.params.identityId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});

return identity;
}
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
TAccessApprovalRequestReviewerDALFactory,
"create" | "find" | "findOne" | "transaction" | "delete"
>;
groupDAL: Pick<TGroupDALFactory, "findAllGroupPossibleMembers">;
groupDAL: Pick<TGroupDALFactory, "findAllGroupPossibleUsers">;
smtpService: Pick<TSmtpService, "sendMail">;
userDAL: Pick<
TUserDALFactory,
Expand Down Expand Up @@ -182,7 +182,7 @@ export const accessApprovalRequestServiceFactory = ({
await Promise.all(
approverGroupIds.map((groupApproverId) =>
groupDAL
.findAllGroupPossibleMembers({
.findAllGroupPossibleUsers({
orgId: actorOrgId,
groupId: groupApproverId
})
Expand Down
Loading
Loading