diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index d05ce34e2..d632be3df 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -13,6 +13,7 @@ import { conditionsMatcher } from "@app/lib/casl"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectRoleDALFactory } from "@app/services/project-role/project-role-dal"; import { TServiceTokenDALFactory } from "@app/services/service-token/service-token-dal"; @@ -33,6 +34,7 @@ type TPermissionServiceFactoryDep = { orgRoleDAL: Pick; projectRoleDAL: Pick; serviceTokenDAL: Pick; + projectDAL: Pick; permissionDAL: TPermissionDALFactory; }; @@ -42,7 +44,8 @@ export const permissionServiceFactory = ({ permissionDAL, orgRoleDAL, projectRoleDAL, - serviceTokenDAL + serviceTokenDAL, + projectDAL }: TPermissionServiceFactoryDep) => { const buildOrgPermission = (role: string, permission?: unknown) => { switch (role) { @@ -99,7 +102,7 @@ export const permissionServiceFactory = ({ /* * Get user permission in an organization - * */ + */ const getUserOrgPermission = async ( userId: string, orgId: string, @@ -112,8 +115,13 @@ export const permissionServiceFactory = ({ throw new BadRequestError({ name: "Custom permission not found" }); } - if (membership.orgId !== userOrgId) { - throw new UnauthorizedError({ name: "You are not a member of this organization" }); + // If the org ID is API_KEY, the request is being made with an API Key. + // Since we can't scope API keys to an organization, we'll need to do an arbitrary check to see if the user is a member of the organization. + + // Extra: This means that when users are using API keys to make requests, they can't use slug-based routes. + // Slug-based routes depend on the organization ID being present on the request, since project slugs aren't globally unique, and we need a way to filter by organization. + if (userOrgId !== "API_KEY" && membership.orgId !== userOrgId) { + throw new UnauthorizedError({ name: "You are not logged into this organization" }); } validateOrgSAML(authMethod, membership.orgAuthEnforced); @@ -179,8 +187,13 @@ export const permissionServiceFactory = ({ throw new BadRequestError({ name: "Custom permission not found" }); } - if (membership.orgId !== userOrgId) { - throw new UnauthorizedError({ name: "You are not a member of this organization" }); + // If the org ID is API_KEY, the request is being made with an API Key. + // Since we can't scope API keys to an organization, we'll need to do an arbitrary check to see if the user is a member of the organization. + + // Extra: This means that when users are using API keys to make requests, they can't use slug-based routes. + // Slug-based routes depend on the organization ID being present on the request, since project slugs aren't globally unique, and we need a way to filter by organization. + if (userOrgId !== "API_KEY" && membership.orgId !== userOrgId) { + throw new UnauthorizedError({ name: "You are not logged into this organization" }); } validateOrgSAML(authMethod, membership.orgAuthEnforced); @@ -195,7 +208,8 @@ export const permissionServiceFactory = ({ const getIdentityProjectPermission = async ( identityId: string, - projectId: string + projectId: string, + identityOrgId: string | undefined ): Promise> => { const identityProjectPermission = await permissionDAL.getProjectIdentityPermission(identityId, projectId); if (!identityProjectPermission) throw new UnauthorizedError({ name: "Identity not in project" }); @@ -208,6 +222,10 @@ export const permissionServiceFactory = ({ throw new BadRequestError({ name: "Custom permission not found" }); } + if (identityProjectPermission.orgId !== identityOrgId) { + throw new UnauthorizedError({ name: "You are not a member of this organization" }); + } + return { permission: buildProjectPermission(identityProjectPermission.roles), membership: identityProjectPermission, @@ -218,14 +236,32 @@ export const permissionServiceFactory = ({ }; }; - const getServiceTokenProjectPermission = async (serviceTokenId: string, projectId: string) => { + const getServiceTokenProjectPermission = async ( + serviceTokenId: string, + projectId: string, + actorOrgId: string | undefined + ) => { const serviceToken = await serviceTokenDAL.findById(serviceTokenId); if (!serviceToken) throw new BadRequestError({ message: "Service token not found" }); + const serviceTokenProject = await projectDAL.findById(serviceToken.projectId); + + if (!serviceTokenProject) throw new BadRequestError({ message: "Service token not linked to a project" }); + + if (serviceTokenProject.orgId !== actorOrgId) { + throw new UnauthorizedError({ message: "Service token not a part of this organization" }); + } + if (serviceToken.projectId !== projectId) throw new UnauthorizedError({ message: "Failed to find service authorization for given project" }); + + if (serviceTokenProject.orgId !== actorOrgId) + throw new UnauthorizedError({ + message: "Failed to find service authorization for given project" + }); + const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []); return { permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions), @@ -260,9 +296,9 @@ export const permissionServiceFactory = ({ case ActorType.USER: return getUserProjectPermission(id, projectId, actorAuthMethod, actorOrgId) as Promise>; case ActorType.SERVICE: - return getServiceTokenProjectPermission(id, projectId) as Promise>; + return getServiceTokenProjectPermission(id, projectId, actorOrgId) as Promise>; case ActorType.IDENTITY: - return getIdentityProjectPermission(id, projectId) as Promise>; + return getIdentityProjectPermission(id, projectId, actorOrgId) as Promise>; default: throw new UnauthorizedError({ message: "Permission not defined",