mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(security): gate credential-set invitation listing to admins and drop token (#5243)
GET /api/credential-sets/[id]/invite listed every invitation row — including the bearer token — to any org member, matching neither its sibling methods (POST/DELETE enforce admin/owner) nor the self-scoped /invitations endpoint. A non-privileged member could harvest a null-email invite token and self-join the credential set via POST /api/credential-sets/invite/[token]. - Add the admin/owner role gate to GET, matching POST/DELETE on the same route - Project explicit columns (drop token) so the secret is never returned to the management list; the creating admin still receives it via the create response
This commit is contained in:
@@ -68,8 +68,20 @@ export const GET = withRouteHandler(
|
||||
return NextResponse.json({ error: 'Credential set not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (result.role !== 'admin' && result.role !== 'owner') {
|
||||
return NextResponse.json({ error: 'Admin or owner permissions required' }, { status: 403 })
|
||||
}
|
||||
|
||||
const invitations = await db
|
||||
.select()
|
||||
.select({
|
||||
id: credentialSetInvitation.id,
|
||||
credentialSetId: credentialSetInvitation.credentialSetId,
|
||||
email: credentialSetInvitation.email,
|
||||
status: credentialSetInvitation.status,
|
||||
expiresAt: credentialSetInvitation.expiresAt,
|
||||
createdAt: credentialSetInvitation.createdAt,
|
||||
invitedBy: credentialSetInvitation.invitedBy,
|
||||
})
|
||||
.from(credentialSetInvitation)
|
||||
.where(eq(credentialSetInvitation.credentialSetId, id))
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
type CreateCredentialSetData,
|
||||
type CredentialSet,
|
||||
type CredentialSetInvitation,
|
||||
type CredentialSetInvitationDetail,
|
||||
type CredentialSetInvitationListItem,
|
||||
type CredentialSetMember,
|
||||
type CredentialSetMembership,
|
||||
cancelCredentialSetInvitationContract,
|
||||
@@ -34,7 +34,7 @@ export type {
|
||||
CreateCredentialSetData,
|
||||
CredentialSet,
|
||||
CredentialSetInvitation,
|
||||
CredentialSetInvitationDetail,
|
||||
CredentialSetInvitationListItem,
|
||||
CredentialSetMember,
|
||||
CredentialSetMembership,
|
||||
}
|
||||
@@ -251,7 +251,7 @@ export function useDeleteCredentialSet() {
|
||||
}
|
||||
|
||||
export function useCredentialSetInvitationsDetail(credentialSetId?: string) {
|
||||
return useQuery<CredentialSetInvitationDetail[]>({
|
||||
return useQuery<CredentialSetInvitationListItem[]>({
|
||||
queryKey: credentialSetKeys.detailInvitations(credentialSetId),
|
||||
queryFn: async ({ signal }) => {
|
||||
if (!credentialSetId) return []
|
||||
|
||||
@@ -63,6 +63,16 @@ export const credentialSetInvitationDetailSchema = z.object({
|
||||
invitedBy: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Management-list view of an invitation. Omits the bearer `token` — the secret
|
||||
* redeemed via the invite link — so listing a credential set's invitations never
|
||||
* broadcasts it. Only the creating admin receives the token (and its `inviteUrl`)
|
||||
* in the create response.
|
||||
*/
|
||||
export const credentialSetInvitationListItemSchema = credentialSetInvitationDetailSchema.omit({
|
||||
token: true,
|
||||
})
|
||||
|
||||
export const credentialSetInvitePreviewSchema = z.object({
|
||||
credentialSetName: z.string(),
|
||||
organizationName: z.string(),
|
||||
@@ -92,6 +102,7 @@ export type CredentialSetMembership = z.output<typeof credentialSetMembershipSch
|
||||
export type CredentialSetInvitation = z.output<typeof credentialSetInvitationSchema>
|
||||
export type CredentialSetMember = z.output<typeof credentialSetMemberSchema>
|
||||
export type CredentialSetInvitationDetail = z.output<typeof credentialSetInvitationDetailSchema>
|
||||
export type CredentialSetInvitationListItem = z.output<typeof credentialSetInvitationListItemSchema>
|
||||
export type CredentialSetInvitePreview = z.output<typeof credentialSetInvitePreviewSchema>
|
||||
|
||||
export const listCredentialSetsQuerySchema = z.object({
|
||||
@@ -245,7 +256,7 @@ export const listCredentialSetInvitationDetailsContract = defineRouteContract({
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
invitations: z.array(credentialSetInvitationDetailSchema).optional(),
|
||||
invitations: z.array(credentialSetInvitationListItemSchema).optional(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user