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 { validateEntity } from '@/generic-helpers';
import type { ICredentialsDb } from '@/interfaces'; import type { ICredentialsDb } from '@/interfaces';
import { Logger } from '@/logger'; import { Logger } from '@/logger';
import { userHasScope } from '@/permissions/check-access'; import { userHasScopes } from '@/permissions/check-access';
import type { CredentialRequest, ListQuery } from '@/requests'; import type { CredentialRequest, ListQuery } from '@/requests';
import { CredentialsTester } from '@/services/credentials-tester.service'; import { CredentialsTester } from '@/services/credentials-tester.service';
import { OwnershipService } from '@/services/ownership.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 // could actually be testing the credential before saving it, so this should cover
// the cases we need it for. // the cases we need it for.
if ( if (
!(await userHasScope(user, ['credential:update'], false, { credentialId: credential.id })) !(await userHasScopes(user, ['credential:update'], false, { credentialId: credential.id }))
) { ) {
mergedCredentials.data = decryptedData; 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 { UnauthenticatedError } from '@/errors/response-errors/unauthenticated.error';
import type { BooleanLicenseFeature } from '@/interfaces'; import type { BooleanLicenseFeature } from '@/interfaces';
import { License } from '@/license'; import { License } from '@/license';
import { userHasScope } from '@/permissions/check-access'; import { userHasScopes } from '@/permissions/check-access';
import type { AuthenticatedRequest } from '@/requests'; import type { AuthenticatedRequest } from '@/requests';
import { send } from '@/response-helper'; // TODO: move `ResponseHelper.send` to this file import { send } from '@/response-helper'; // TODO: move `ResponseHelper.send` to this file
@ -151,7 +151,7 @@ export class ControllerRegistry {
const { scope, globalOnly } = accessScope; 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({ return res.status(403).json({
status: 'error', status: 'error',
message: RESPONSE_ERROR_MESSAGES.MISSING_SCOPE, 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 { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository';
import { RoleService } from '@/services/role.service'; 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, user: User,
scopes: Scope[], scopes: Scope[],
globalOnly: boolean, globalOnly: boolean,
@ -18,15 +26,14 @@ export const userHasScope = async (
credentialId, credentialId,
workflowId, workflowId,
projectId, projectId,
}: { credentialId?: string; workflowId?: string; projectId?: string }, }: { credentialId?: string; workflowId?: string; projectId?: string } /* only one */,
): Promise<boolean> => { ): Promise<boolean> {
// Short circuit here since a global role will always have access if (user.hasGlobalScope(scopes, { mode: 'allOf' })) return true;
if (user.hasGlobalScope(scopes, { mode: 'allOf' })) {
return true; if (globalOnly) return false;
} else if (globalOnly) {
// The above check already failed so the user doesn't have access // Find which project roles are defined to contain the required scopes.
return false; // Then find projects having this user and having those project roles.
}
const roleService = Container.get(RoleService); const roleService = Container.get(RoleService);
const projectRoles = roleService.rolesWithScope('project', scopes); const projectRoles = roleService.rolesWithScope('project', scopes);
@ -42,47 +49,29 @@ export const userHasScope = async (
}) })
).map((p) => p.id); ).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) { if (credentialId) {
const exists = await Container.get(SharedCredentialsRepository).find({ return await Container.get(SharedCredentialsRepository).existsBy({
where: { credentialsId: credentialId,
projectId: In(userProjectIds), projectId: In(userProjectIds),
credentialsId: credentialId, role: In(roleService.rolesWithScope('credential', scopes)),
role: In(roleService.rolesWithScope('credential', scopes)),
},
}); });
if (!exists.length) {
return false;
}
return true;
} }
if (workflowId) { if (workflowId) {
const exists = await Container.get(SharedWorkflowRepository).find({ return await Container.get(SharedWorkflowRepository).existsBy({
where: { workflowId,
projectId: In(userProjectIds), projectId: In(userProjectIds),
workflowId, role: In(roleService.rolesWithScope('workflow', scopes)),
role: In(roleService.rolesWithScope('workflow', scopes)),
},
}); });
if (!exists.length) {
return false;
}
return true;
} }
if (projectId) { if (projectId) return userProjectIds.includes(projectId);
if (!userProjectIds.includes(projectId)) {
return false;
}
return true;
}
throw new ApplicationError( 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 { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error';
import type { BooleanLicenseFeature } from '@/interfaces'; import type { BooleanLicenseFeature } from '@/interfaces';
import { License } from '@/license'; import { License } from '@/license';
import { userHasScope } from '@/permissions/check-access'; import { userHasScopes } from '@/permissions/check-access';
import type { AuthenticatedRequest } from '@/requests'; import type { AuthenticatedRequest } from '@/requests';
import type { PaginatedRequest } from '../../../types'; import type { PaginatedRequest } from '../../../types';
@ -34,7 +34,7 @@ const buildScopeMiddleware = (
params.credentialId = req.params.id; 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' }); return res.status(403).json({ message: 'Forbidden' });
} }