-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
fix: enforce admin permissions for services (Backend) #3418
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: canary
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -44,10 +44,13 @@ export const canPerformCreationService = async ( | |||||||||||||||||||||||||||||||||||||||||
| projectId: string, | ||||||||||||||||||||||||||||||||||||||||||
| organizationId: string, | ||||||||||||||||||||||||||||||||||||||||||
| ) => { | ||||||||||||||||||||||||||||||||||||||||||
| const { accessedProjects, canCreateServices } = await findMemberById( | ||||||||||||||||||||||||||||||||||||||||||
| userId, | ||||||||||||||||||||||||||||||||||||||||||
| organizationId, | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| const member = await findMemberById(userId, organizationId); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (member.role === "owner" || member.role === "admin") { | ||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const { accessedProjects, canCreateServices } = member; | ||||||||||||||||||||||||||||||||||||||||||
| const haveAccessToProject = accessedProjects.includes(projectId); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (canCreateServices && haveAccessToProject) { | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -62,7 +65,13 @@ export const canPerformAccessService = async ( | |||||||||||||||||||||||||||||||||||||||||
| serviceId: string, | ||||||||||||||||||||||||||||||||||||||||||
| organizationId: string, | ||||||||||||||||||||||||||||||||||||||||||
| ) => { | ||||||||||||||||||||||||||||||||||||||||||
| const { accessedServices } = await findMemberById(userId, organizationId); | ||||||||||||||||||||||||||||||||||||||||||
| const member = await findMemberById(userId, organizationId); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (member.role === "owner" || member.role === "admin") { | ||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const { accessedServices } = member; | ||||||||||||||||||||||||||||||||||||||||||
| const haveAccessToService = accessedServices.includes(serviceId); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (haveAccessToService) { | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -77,10 +86,13 @@ export const canPeformDeleteService = async ( | |||||||||||||||||||||||||||||||||||||||||
| serviceId: string, | ||||||||||||||||||||||||||||||||||||||||||
| organizationId: string, | ||||||||||||||||||||||||||||||||||||||||||
| ) => { | ||||||||||||||||||||||||||||||||||||||||||
| const { accessedServices, canDeleteServices } = await findMemberById( | ||||||||||||||||||||||||||||||||||||||||||
| userId, | ||||||||||||||||||||||||||||||||||||||||||
| organizationId, | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| const member = await findMemberById(userId, organizationId); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (member.role === "owner" || member.role === "admin") { | ||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const { accessedServices, canDeleteServices } = member; | ||||||||||||||||||||||||||||||||||||||||||
| const haveAccessToService = accessedServices.includes(serviceId); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (canDeleteServices && haveAccessToService) { | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -94,7 +106,13 @@ export const canPerformCreationProject = async ( | |||||||||||||||||||||||||||||||||||||||||
| userId: string, | ||||||||||||||||||||||||||||||||||||||||||
| organizationId: string, | ||||||||||||||||||||||||||||||||||||||||||
| ) => { | ||||||||||||||||||||||||||||||||||||||||||
| const { canCreateProjects } = await findMemberById(userId, organizationId); | ||||||||||||||||||||||||||||||||||||||||||
| const member = await findMemberById(userId, organizationId); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (member.role === "owner" || member.role === "admin") { | ||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const { canCreateProjects } = member; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (canCreateProjects) { | ||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -107,7 +125,13 @@ export const canPerformDeleteProject = async ( | |||||||||||||||||||||||||||||||||||||||||
| userId: string, | ||||||||||||||||||||||||||||||||||||||||||
| organizationId: string, | ||||||||||||||||||||||||||||||||||||||||||
| ) => { | ||||||||||||||||||||||||||||||||||||||||||
| const { canDeleteProjects } = await findMemberById(userId, organizationId); | ||||||||||||||||||||||||||||||||||||||||||
| const member = await findMemberById(userId, organizationId); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (member.role === "owner" || member.role === "admin") { | ||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const { canDeleteProjects } = member; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (canDeleteProjects) { | ||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -121,7 +145,13 @@ export const canPerformAccessProject = async ( | |||||||||||||||||||||||||||||||||||||||||
| projectId: string, | ||||||||||||||||||||||||||||||||||||||||||
| organizationId: string, | ||||||||||||||||||||||||||||||||||||||||||
| ) => { | ||||||||||||||||||||||||||||||||||||||||||
| const { accessedProjects } = await findMemberById(userId, organizationId); | ||||||||||||||||||||||||||||||||||||||||||
| const member = await findMemberById(userId, organizationId); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (member.role === "owner" || member.role === "admin") { | ||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const { accessedProjects } = member; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const haveAccessToProject = accessedProjects.includes(projectId); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -135,10 +165,13 @@ export const canAccessToTraefikFiles = async ( | |||||||||||||||||||||||||||||||||||||||||
| userId: string, | ||||||||||||||||||||||||||||||||||||||||||
| organizationId: string, | ||||||||||||||||||||||||||||||||||||||||||
| ) => { | ||||||||||||||||||||||||||||||||||||||||||
| const { canAccessToTraefikFiles } = await findMemberById( | ||||||||||||||||||||||||||||||||||||||||||
| userId, | ||||||||||||||||||||||||||||||||||||||||||
| organizationId, | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| const member = await findMemberById(userId, organizationId); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (member.role === "owner" || member.role === "admin") { | ||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const { canAccessToTraefikFiles } = member; | ||||||||||||||||||||||||||||||||||||||||||
| return canAccessToTraefikFiles; | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -182,6 +215,83 @@ export const checkServiceAccess = async ( | |||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| export const checkEnvironmentAccess = async ( | ||||||||||||||||||||||||||||||||||||||||||
| userId: string, | ||||||||||||||||||||||||||||||||||||||||||
| environmentId: string, | ||||||||||||||||||||||||||||||||||||||||||
| organizationId: string, | ||||||||||||||||||||||||||||||||||||||||||
| action = "access" as const, | ||||||||||||||||||||||||||||||||||||||||||
| ) => { | ||||||||||||||||||||||||||||||||||||||||||
| let hasPermission = false; | ||||||||||||||||||||||||||||||||||||||||||
| switch (action) { | ||||||||||||||||||||||||||||||||||||||||||
| case "access": | ||||||||||||||||||||||||||||||||||||||||||
| hasPermission = await canPerformAccessEnvironment( | ||||||||||||||||||||||||||||||||||||||||||
| userId, | ||||||||||||||||||||||||||||||||||||||||||
| environmentId, | ||||||||||||||||||||||||||||||||||||||||||
| organizationId, | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||
| hasPermission = false; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| if (!hasPermission) { | ||||||||||||||||||||||||||||||||||||||||||
| throw new TRPCError({ | ||||||||||||||||||||||||||||||||||||||||||
| code: "UNAUTHORIZED", | ||||||||||||||||||||||||||||||||||||||||||
| message: "Permission denied", | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| export const canPerformAccessEnvironment = async ( | ||||||||||||||||||||||||||||||||||||||||||
| userId: string, | ||||||||||||||||||||||||||||||||||||||||||
| environmentId: string, | ||||||||||||||||||||||||||||||||||||||||||
| organizationId: string, | ||||||||||||||||||||||||||||||||||||||||||
| ) => { | ||||||||||||||||||||||||||||||||||||||||||
| const { accessedEnvironments } = await findMemberById(userId, organizationId); | ||||||||||||||||||||||||||||||||||||||||||
| const haveAccessToEnvironment = accessedEnvironments.includes(environmentId); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (haveAccessToEnvironment) { | ||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| export const checkEnvironmentDeletionPermission = async ( | ||||||||||||||||||||||||||||||||||||||||||
| userId: string, | ||||||||||||||||||||||||||||||||||||||||||
| projectId: string, | ||||||||||||||||||||||||||||||||||||||||||
| organizationId: string, | ||||||||||||||||||||||||||||||||||||||||||
| ) => { | ||||||||||||||||||||||||||||||||||||||||||
| const member = await findMemberById(userId, organizationId); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (!member) { | ||||||||||||||||||||||||||||||||||||||||||
| throw new TRPCError({ | ||||||||||||||||||||||||||||||||||||||||||
| code: "UNAUTHORIZED", | ||||||||||||||||||||||||||||||||||||||||||
| message: "User not found in organization", | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
| if (!member) { | |
| throw new TRPCError({ | |
| code: "UNAUTHORIZED", | |
| message: "User not found in organization", | |
| }); | |
| } |
Outdated
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The null check for member is redundant because findMemberById already throws a TRPCError with code "UNAUTHORIZED" if the member is not found (line 381-386). This check will never be reached since the function would have already thrown an exception.
| // Get user's member record | |
| const member = await findMemberById(userId, organizationId); | |
| if (!member) { | |
| throw new TRPCError({ | |
| code: "UNAUTHORIZED", | |
| message: "User not found in organization", | |
| }); | |
| } | |
| // Get user's member record (throws if not found) | |
| const member = (await findMemberById(userId, organizationId))!; |
Outdated
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error handling in this function uses throw new Error instead of TRPCError which is the pattern used throughout the rest of this file for validation and permission errors. This inconsistency could lead to errors being handled differently by the TRPC framework and may not provide proper error codes to API consumers.
| throw new Error("Email is required and cannot be empty"); | |
| } | |
| // Basic email format validation | |
| const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; | |
| if (!emailRegex.test(userData.email)) { | |
| throw new Error("Please enter a valid email address"); | |
| throw new TRPCError({ | |
| code: "BAD_REQUEST", | |
| message: "Email is required and cannot be empty", | |
| }); | |
| } | |
| // Basic email format validation | |
| const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; | |
| if (!emailRegex.test(userData.email)) { | |
| throw new TRPCError({ | |
| code: "BAD_REQUEST", | |
| message: "Please enter a valid email address", | |
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
canPerformAccessEnvironmentfunction is missing the admin/owner role check that has been added to all other similar permission functions. Admins and owners should have access to all environments without needing to be in theaccessedEnvironmentslist. This inconsistency means admins/owners may be incorrectly denied access to environments.