mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57:29 -08:00
docs(core): Document access checks (#10929)
This commit is contained in:
parent
08ba9a36a4
commit
73daabbd0e
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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`.",
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
@ -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' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue