docs(core): Document access checks (#10929)

This commit is contained in:
Iván Ovejero 2024-09-24 11:02:39 +02:00 committed by GitHub
parent 08ba9a36a4
commit 73daabbd0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 38 additions and 49 deletions

View file

@ -34,7 +34,7 @@ import { ExternalHooks } from '@/external-hooks';
import { validateEntity } from '@/generic-helpers';
import type { ICredentialsDb } from '@/interfaces';
import { Logger } from '@/logger';
import { userHasScope } from '@/permissions/check-access';
import { userHasScopes } from '@/permissions/check-access';
import type { CredentialRequest, ListQuery } from '@/requests';
import { CredentialsTester } from '@/services/credentials-tester.service';
import { OwnershipService } from '@/services/ownership.service';
@ -598,7 +598,7 @@ export class CredentialsService {
// could actually be testing the credential before saving it, so this should cover
// the cases we need it for.
if (
!(await userHasScope(user, ['credential:update'], false, { credentialId: credential.id }))
!(await userHasScopes(user, ['credential:update'], false, { credentialId: credential.id }))
) {
mergedCredentials.data = decryptedData;
}

View file

@ -11,7 +11,7 @@ import { inProduction, RESPONSE_ERROR_MESSAGES } from '@/constants';
import { UnauthenticatedError } from '@/errors/response-errors/unauthenticated.error';
import type { BooleanLicenseFeature } from '@/interfaces';
import { License } from '@/license';
import { userHasScope } from '@/permissions/check-access';
import { userHasScopes } from '@/permissions/check-access';
import type { AuthenticatedRequest } from '@/requests';
import { send } from '@/response-helper'; // TODO: move `ResponseHelper.send` to this file
@ -151,7 +151,7 @@ export class ControllerRegistry {
const { scope, globalOnly } = accessScope;
if (!(await userHasScope(req.user, [scope], globalOnly, req.params))) {
if (!(await userHasScopes(req.user, [scope], globalOnly, req.params))) {
return res.status(403).json({
status: 'error',
message: RESPONSE_ERROR_MESSAGES.MISSING_SCOPE,

View file

@ -10,7 +10,15 @@ import { SharedCredentialsRepository } from '@/databases/repositories/shared-cre
import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository';
import { RoleService } from '@/services/role.service';
export const userHasScope = async (
/**
* Check if a user has the required scopes. The check can be:
*
* - only for scopes in the user's global role, or
* - for scopes in the user's global role, else for scopes in the resource roles
* of projects including the user and the resource, else for scopes in the
* project roles in those projects.
*/
export async function userHasScopes(
user: User,
scopes: Scope[],
globalOnly: boolean,
@ -18,15 +26,14 @@ export const userHasScope = async (
credentialId,
workflowId,
projectId,
}: { credentialId?: string; workflowId?: string; projectId?: string },
): Promise<boolean> => {
// Short circuit here since a global role will always have access
if (user.hasGlobalScope(scopes, { mode: 'allOf' })) {
return true;
} else if (globalOnly) {
// The above check already failed so the user doesn't have access
return false;
}
}: { credentialId?: string; workflowId?: string; projectId?: string } /* only one */,
): Promise<boolean> {
if (user.hasGlobalScope(scopes, { mode: 'allOf' })) return true;
if (globalOnly) return false;
// Find which project roles are defined to contain the required scopes.
// Then find projects having this user and having those project roles.
const roleService = Container.get(RoleService);
const projectRoles = roleService.rolesWithScope('project', scopes);
@ -42,47 +49,29 @@ export const userHasScope = async (
})
).map((p) => p.id);
// Find which resource roles are defined to contain the required scopes.
// Then find at least one of the above qualifying projects having one of
// those resource roles over the resource being checked.
if (credentialId) {
const exists = await Container.get(SharedCredentialsRepository).find({
where: {
projectId: In(userProjectIds),
credentialsId: credentialId,
role: In(roleService.rolesWithScope('credential', scopes)),
},
return await Container.get(SharedCredentialsRepository).existsBy({
credentialsId: credentialId,
projectId: In(userProjectIds),
role: In(roleService.rolesWithScope('credential', scopes)),
});
if (!exists.length) {
return false;
}
return true;
}
if (workflowId) {
const exists = await Container.get(SharedWorkflowRepository).find({
where: {
projectId: In(userProjectIds),
workflowId,
role: In(roleService.rolesWithScope('workflow', scopes)),
},
return await Container.get(SharedWorkflowRepository).existsBy({
workflowId,
projectId: In(userProjectIds),
role: In(roleService.rolesWithScope('workflow', scopes)),
});
if (!exists.length) {
return false;
}
return true;
}
if (projectId) {
if (!userProjectIds.includes(projectId)) {
return false;
}
return true;
}
if (projectId) return userProjectIds.includes(projectId);
throw new ApplicationError(
"@ProjectScope decorator was used but does not have a credentialId, workflowId, or projectId in it's URL parameters. This is likely an implementation error. If you're a developer, please check you're URL is correct or that this should be using @GlobalScope.",
"`@ProjectScope` decorator was used but does not have a `credentialId`, `workflowId`, or `projectId` in its URL parameters. This is likely an implementation error. If you're a developer, please check your URL is correct or that this should be using `@GlobalScope`.",
);
};
}

View file

@ -6,7 +6,7 @@ import { Container } from 'typedi';
import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error';
import type { BooleanLicenseFeature } from '@/interfaces';
import { License } from '@/license';
import { userHasScope } from '@/permissions/check-access';
import { userHasScopes } from '@/permissions/check-access';
import type { AuthenticatedRequest } from '@/requests';
import type { PaginatedRequest } from '../../../types';
@ -34,7 +34,7 @@ const buildScopeMiddleware = (
params.credentialId = req.params.id;
}
}
if (!(await userHasScope(req.user, scopes, globalOnly, params))) {
if (!(await userHasScopes(req.user, scopes, globalOnly, params))) {
return res.status(403).json({ message: 'Forbidden' });
}