feat(API): Implement users account quota guards (#6434)

* feat(cli): Implement users account quota guards

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Remove comment

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Address PR comments

- Getting `usersQuota` from `Settings` repo
- Revert `isUserManagementEnabled` helper
- Fix FE listing of users

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Refactor isWithinUserQuota getter and fix tests

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Revert testDb.ts changes

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Cleanup & improve types

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Fix duplicated method

* Fix failing test

* Remove `isUserManagementEnabled` completely

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Check for globalRole.name to determine if user is owner

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Fix unit tests

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Set isInstanceOwnerSetUp in specs

* Fix SettingsUserView UM

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* refactor: License typings suggestions for users quota guards (#6636)

refactor: License typings suggestions

* Update packages/cli/src/Ldap/helpers.ts

Co-authored-by: Iván Ovejero <ivov.src@gmail.com>

* Update packages/cli/test/integration/shared/utils.ts

Co-authored-by: Iván Ovejero <ivov.src@gmail.com>

* Address PR comments

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Use 403 for all user quota related errors

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

---------

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
This commit is contained in:
OlegIvaniv
2023-07-12 14:11:46 +02:00
committed by GitHub
parent 26046f6fe8
commit e5620ab1e4
33 changed files with 271 additions and 94 deletions

View File

@@ -61,6 +61,7 @@ import type {
WorkflowStatisticsRepository,
WorkflowTagMappingRepository,
} from '@db/repositories';
import type { LICENSE_FEATURES, LICENSE_QUOTAS } from './constants';
export interface IActivationError {
time: number;
@@ -716,6 +717,11 @@ export interface IExecutionTrackProperties extends ITelemetryTrackProperties {
// license
// ----------------------------------
type ValuesOf<T> = T[keyof T];
export type BooleanLicenseFeature = ValuesOf<typeof LICENSE_FEATURES>;
export type NumericLicenseFeature = ValuesOf<typeof LICENSE_QUOTAS>;
export interface ILicenseReadResponse {
usage: {
executions: {

View File

@@ -36,7 +36,9 @@ import { InternalServerError } from '../ResponseHelper';
/**
* Check whether the LDAP feature is disabled in the instance
*/
export const isLdapEnabled = () => Container.get(License).isLdapEnabled();
export const isLdapEnabled = () => {
return Container.get(License).isLdapEnabled();
};
/**
* Check whether the LDAP feature is enabled in the instance

View File

@@ -9,8 +9,16 @@ import {
LICENSE_QUOTAS,
N8N_VERSION,
SETTINGS_LICENSE_CERT_KEY,
UNLIMITED_LICENSE_QUOTA,
} from './constants';
import { Service } from 'typedi';
import type { BooleanLicenseFeature, NumericLicenseFeature } from './Interfaces';
type FeatureReturnType = Partial<
{
planName: string;
} & { [K in NumericLicenseFeature]: number } & { [K in BooleanLicenseFeature]: boolean }
>;
@Service()
export class License {
@@ -96,12 +104,8 @@ export class License {
await this.manager.renew();
}
isFeatureEnabled(feature: LICENSE_FEATURES): boolean {
if (!this.manager) {
return false;
}
return this.manager.hasFeatureEnabled(feature);
isFeatureEnabled(feature: BooleanLicenseFeature) {
return this.manager?.hasFeatureEnabled(feature) ?? false;
}
isSharingEnabled() {
@@ -140,15 +144,8 @@ export class License {
return this.manager?.getCurrentEntitlements() ?? [];
}
getFeatureValue(
feature: string,
requireValidCert?: boolean,
): undefined | boolean | number | string {
if (!this.manager) {
return undefined;
}
return this.manager.getFeatureValue(feature, requireValidCert);
getFeatureValue<T extends keyof FeatureReturnType>(feature: T): FeatureReturnType[T] {
return this.manager?.getFeatureValue(feature) as FeatureReturnType[T];
}
getManagementJwt(): string {
@@ -177,20 +174,20 @@ export class License {
}
// Helper functions for computed data
getTriggerLimit(): number {
return (this.getFeatureValue(LICENSE_QUOTAS.TRIGGER_LIMIT) ?? -1) as number;
getUsersLimit() {
return this.getFeatureValue(LICENSE_QUOTAS.USERS_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
}
getVariablesLimit(): number {
return (this.getFeatureValue(LICENSE_QUOTAS.VARIABLES_LIMIT) ?? -1) as number;
getTriggerLimit() {
return this.getFeatureValue(LICENSE_QUOTAS.TRIGGER_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
}
getUsersLimit(): number {
return this.getFeatureValue(LICENSE_QUOTAS.USERS_LIMIT) as number;
getVariablesLimit() {
return this.getFeatureValue(LICENSE_QUOTAS.VARIABLES_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
}
getPlanName(): string {
return (this.getFeatureValue('planName') ?? 'Community') as string;
return this.getFeatureValue('planName') ?? 'Community';
}
getInfo(): string {
@@ -200,4 +197,8 @@ export class License {
return this.manager.toString();
}
isWithinUsersLimit() {
return this.getUsersLimit() === UNLIMITED_LICENSE_QUOTA;
}
}

View File

@@ -1,18 +1,9 @@
import { Container } from 'typedi';
import { RoleRepository, UserRepository } from '@db/repositories';
import type { Role } from '@db/entities/Role';
import { UserRepository } from '@db/repositories';
import type { User } from '@db/entities/User';
import pick from 'lodash/pick';
import { validate as uuidValidate } from 'uuid';
export function isInstanceOwner(user: User): boolean {
return user.globalRole.name === 'owner';
}
export async function getWorkflowOwnerRole(): Promise<Role> {
return Container.get(RoleRepository).findWorkflowOwnerRoleOrFail();
}
export const getSelectableProperties = (table: 'user' | 'role'): string[] => {
return {
user: ['id', 'email', 'firstName', 'lastName', 'createdAt', 'updatedAt', 'isPending'],

View File

@@ -0,0 +1,7 @@
import { Container } from 'typedi';
import { RoleRepository } from '@db/repositories';
import type { Role } from '@db/entities/Role';
export async function getWorkflowOwnerRole(): Promise<Role> {
return Container.get(RoleRepository).findWorkflowOwnerRoleOrFail();
}

View File

@@ -11,7 +11,7 @@ import { addNodeIds, replaceInvalidCredentials } from '@/WorkflowHelpers';
import type { WorkflowRequest } from '../../../types';
import { authorize, validCursor } from '../../shared/middlewares/global.middleware';
import { encodeNextCursor } from '../../shared/services/pagination.service';
import { getWorkflowOwnerRole, isInstanceOwner } from '../users/users.service.ee';
import { getWorkflowOwnerRole } from '../users/users.service';
import {
getWorkflowById,
getSharedWorkflow,
@@ -101,7 +101,7 @@ export = {
...(active !== undefined && { active }),
};
if (isInstanceOwner(req.user)) {
if (req.user.isOwner) {
if (tags) {
const workflowIds = await getWorkflowIdsViaTags(parseTagNames(tags));
where.id = In(workflowIds);

View File

@@ -8,7 +8,6 @@ import * as Db from '@/Db';
import type { User } from '@db/entities/User';
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import { isInstanceOwner } from '../users/users.service.ee';
import type { Role } from '@db/entities/Role';
import config from '@/config';
import { START_NODES } from '@/constants';
@@ -32,7 +31,7 @@ export async function getSharedWorkflow(
): Promise<SharedWorkflow | null> {
return Db.collections.SharedWorkflow.findOne({
where: {
...(!isInstanceOwner(user) && { userId: user.id }),
...(!user.isOwner && { userId: user.id }),
...(workflowId && { workflowId }),
},
relations: [...insertIf(!config.getEnv('workflowTagsDisabled'), ['workflow.tags']), 'workflow'],
@@ -48,7 +47,7 @@ export async function getSharedWorkflows(
): Promise<SharedWorkflow[]> {
return Db.collections.SharedWorkflow.find({
where: {
...(!isInstanceOwner(user) && { userId: user.id }),
...(!user.isOwner && { userId: user.id }),
...(options.workflowIds && { workflowId: In(options.workflowIds) }),
},
...(options.relations && { relations: options.relations }),

View File

@@ -149,6 +149,7 @@ import { PostHogClient } from './posthog';
import { eventBus } from './eventbus';
import { Container } from 'typedi';
import { InternalHooks } from './InternalHooks';
import { License } from './License';
import {
getStatusUsingPreviousExecutionStatusMethod,
isAdvancedExecutionFiltersEnabled,
@@ -259,6 +260,7 @@ export class Server extends AbstractServer {
config.getEnv('personalization.enabled') && config.getEnv('diagnostics.enabled'),
defaultLocale: config.getEnv('defaultLocale'),
userManagement: {
quota: Container.get(License).getUsersLimit(),
showSetupOnFirstLoad: config.getEnv('userManagement.isInstanceOwnerSetUp') === false,
smtpSetup: isEmailSetUp(),
authenticationMethod: getCurrentAuthenticationMethod(),
@@ -407,6 +409,7 @@ export class Server extends AbstractServer {
getSettingsForFrontend(): IN8nUISettings {
// refresh user management status
Object.assign(this.frontendSettings.userManagement, {
quota: Container.get(License).getUsersLimit(),
authenticationMethod: getCurrentAuthenticationMethod(),
showSetupOnFirstLoad:
config.getEnv('userManagement.isInstanceOwnerSetUp') === false &&

View File

@@ -12,8 +12,8 @@ import { MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH } from '@db/entities/User';
import type { Role } from '@db/entities/Role';
import { RoleRepository } from '@db/repositories';
import config from '@/config';
import { getWebhookBaseUrl } from '@/WebhookHelpers';
import { License } from '@/License';
import { getWebhookBaseUrl } from '@/WebhookHelpers';
import type { PostHogClient } from '@/posthog';
export async function getWorkflowOwner(workflowId: string): Promise<User> {

View File

@@ -4,15 +4,18 @@ import jwt from 'jsonwebtoken';
import type { Response } from 'express';
import { createHash } from 'crypto';
import * as Db from '@/Db';
import { AUTH_COOKIE_NAME } from '@/constants';
import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from '@/constants';
import type { JwtPayload, JwtToken } from '@/Interfaces';
import type { User } from '@db/entities/User';
import config from '@/config';
import * as ResponseHelper from '@/ResponseHelper';
import { License } from '@/License';
import { Container } from 'typedi';
export function issueJWT(user: User): JwtToken {
const { id, email, password } = user;
const expiresIn = 7 * 86400000; // 7 days
const isWithinUsersLimit = Container.get(License).isWithinUsersLimit();
const payload: JwtPayload = {
id,
@@ -20,6 +23,13 @@ export function issueJWT(user: User): JwtToken {
password: password ?? null,
};
if (
config.getEnv('userManagement.isInstanceOwnerSetUp') &&
!user.isOwner &&
!isWithinUsersLimit
) {
throw new ResponseHelper.UnauthorizedError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED);
}
if (password) {
payload.password = createHash('sha256')
.update(password.slice(password.length / 2))

View File

@@ -47,6 +47,7 @@ export const RESPONSE_ERROR_MESSAGES = {
PACKAGE_DOES_NOT_CONTAIN_NODES: 'The specified package does not contain any nodes',
PACKAGE_LOADING_FAILED: 'The specified package could not be loaded',
DISK_IS_FULL: 'There appears to be insufficient disk space',
USERS_QUOTA_REACHED: 'Maximum number of users reached',
};
export const AUTH_COOKIE_NAME = 'n8n-auth';
@@ -68,21 +69,22 @@ export const WORKFLOW_REACTIVATE_MAX_TIMEOUT = 24 * 60 * 60 * 1000; // 1 day
export const SETTINGS_LICENSE_CERT_KEY = 'license.cert';
export const enum LICENSE_FEATURES {
SHARING = 'feat:sharing',
LDAP = 'feat:ldap',
SAML = 'feat:saml',
LOG_STREAMING = 'feat:logStreaming',
ADVANCED_EXECUTION_FILTERS = 'feat:advancedExecutionFilters',
VARIABLES = 'feat:variables',
SOURCE_CONTROL = 'feat:sourceControl',
API_DISABLED = 'feat:apiDisabled',
}
export const LICENSE_FEATURES = {
SHARING: 'feat:sharing',
LDAP: 'feat:ldap',
SAML: 'feat:saml',
LOG_STREAMING: 'feat:logStreaming',
ADVANCED_EXECUTION_FILTERS: 'feat:advancedExecutionFilters',
VARIABLES: 'feat:variables',
SOURCE_CONTROL: 'feat:sourceControl',
API_DISABLED: 'feat:apiDisabled',
} as const;
export const enum LICENSE_QUOTAS {
TRIGGER_LIMIT = 'quota:activeWorkflows',
VARIABLES_LIMIT = 'quota:maxVariables',
USERS_LIMIT = 'quota:users',
}
export const LICENSE_QUOTAS = {
TRIGGER_LIMIT: 'quota:activeWorkflows',
VARIABLES_LIMIT: 'quota:maxVariables',
USERS_LIMIT: 'quota:users',
} as const;
export const UNLIMITED_LICENSE_QUOTA = -1;
export const CREDENTIAL_BLANKING_VALUE = '__n8n_BLANK_VALUE_e5362baf-c777-4d57-a609-6eaf1f9e87f6';

View File

@@ -1,9 +1,14 @@
import validator from 'validator';
import { Authorized, Get, Post, RestController } from '@/decorators';
import { AuthError, BadRequestError, InternalServerError } from '@/ResponseHelper';
import {
AuthError,
BadRequestError,
InternalServerError,
UnauthorizedError,
} from '@/ResponseHelper';
import { sanitizeUser, withFeatureFlags } from '@/UserManagement/UserManagementHelper';
import { issueCookie, resolveJwt } from '@/auth/jwt';
import { AUTH_COOKIE_NAME } from '@/constants';
import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from '@/constants';
import { Request, Response } from 'express';
import type { ILogger } from 'n8n-workflow';
import type { User } from '@db/entities/User';
@@ -26,6 +31,7 @@ import {
import type { UserRepository } from '@db/repositories';
import { InternalHooks } from '../InternalHooks';
import Container from 'typedi';
import { License } from '@/License';
@RestController()
export class AuthController {
@@ -71,7 +77,6 @@ export class AuthController {
let user: User | undefined;
let usedAuthenticationMethod = getCurrentAuthenticationMethod();
if (isSamlCurrentAuthenticationMethod()) {
// attempt to fetch user data with the credentials, but don't log in yet
const preliminaryUser = await handleEmailLogin(email, password);
@@ -120,6 +125,7 @@ export class AuthController {
// If logged in, return user
try {
user = await resolveJwt(cookieContents);
return await withFeatureFlags(this.postHog, sanitizeUser(user));
} catch (error) {
res.clearCookie(AUTH_COOKIE_NAME);
@@ -155,6 +161,15 @@ export class AuthController {
@Get('/resolve-signup-token')
async resolveSignupToken(req: UserRequest.ResolveSignUp) {
const { inviterId, inviteeId } = req.query;
const isWithinUsersLimit = Container.get(License).isWithinUsersLimit();
if (!isWithinUsersLimit) {
this.logger.debug('Request to resolve signup token failed because of users quota reached', {
inviterId,
inviteeId,
});
throw new UnauthorizedError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED);
}
if (!inviterId || !inviteeId) {
this.logger.debug(

View File

@@ -11,6 +11,7 @@ import { License } from '@/License';
import { LICENSE_FEATURES, inE2ETests } from '@/constants';
import { NoAuthRequired, Patch, Post, RestController } from '@/decorators';
import type { UserSetupPayload } from '@/requests';
import type { BooleanLicenseFeature } from '@/Interfaces';
if (!inE2ETests) {
console.error('E2E endpoints only allowed during E2E tests');
@@ -51,7 +52,7 @@ type ResetRequest = Request<
@NoAuthRequired()
@RestController('/e2e')
export class E2EController {
private enabledFeatures: Record<LICENSE_FEATURES, boolean> = {
private enabledFeatures: Record<BooleanLicenseFeature, boolean> = {
[LICENSE_FEATURES.SHARING]: false,
[LICENSE_FEATURES.LDAP]: false,
[LICENSE_FEATURES.SAML]: false,
@@ -69,7 +70,7 @@ export class E2EController {
private userRepo: UserRepository,
private workflowRunner: ActiveWorkflowRunner,
) {
license.isFeatureEnabled = (feature: LICENSE_FEATURES) =>
license.isFeatureEnabled = (feature: BooleanLicenseFeature) =>
this.enabledFeatures[feature] ?? false;
}
@@ -84,14 +85,14 @@ export class E2EController {
}
@Patch('/feature')
setFeature(req: Request<{}, {}, { feature: LICENSE_FEATURES; enabled: boolean }>) {
setFeature(req: Request<{}, {}, { feature: BooleanLicenseFeature; enabled: boolean }>) {
const { enabled, feature } = req.body;
this.enabledFeatures[feature] = enabled;
}
private resetFeatures() {
for (const feature of Object.keys(this.enabledFeatures)) {
this.enabledFeatures[feature as LICENSE_FEATURES] = false;
this.enabledFeatures[feature as BooleanLicenseFeature] = false;
}
}

View File

@@ -23,8 +23,11 @@ import { PasswordResetRequest } from '@/requests';
import type { IDatabaseCollections, IExternalHooksClass, IInternalHooksClass } from '@/Interfaces';
import { issueCookie } from '@/auth/jwt';
import { isLdapEnabled } from '@/Ldap/helpers';
import { isSamlCurrentAuthenticationMethod } from '../sso/ssoHelpers';
import { UserService } from '../user/user.service';
import { isSamlCurrentAuthenticationMethod } from '@/sso/ssoHelpers';
import { UserService } from '@/user/user.service';
import { License } from '@/License';
import { Container } from 'typedi';
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
@RestController()
export class PasswordResetController {
@@ -103,6 +106,12 @@ export class PasswordResetController {
relations: ['authIdentities', 'globalRole'],
});
if (!user?.isOwner && !Container.get(License).isWithinUsersLimit()) {
this.logger.debug(
'Request to send password reset email failed because the user limit was reached',
);
throw new UnauthorizedError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED);
}
if (
isSamlCurrentAuthenticationMethod() &&
!(user?.globalRole.name === 'owner' || user?.settings?.allowSSOManualLogin === true)
@@ -116,7 +125,6 @@ export class PasswordResetController {
}
const ldapIdentity = user?.authIdentities?.find((i) => i.providerType === 'ldap');
if (!user?.password || (ldapIdentity && user.disabled)) {
this.logger.debug(
'Request to send password reset email failed because no user was found for the provided email',
@@ -182,12 +190,21 @@ export class PasswordResetController {
// Timestamp is saved in seconds
const currentTimestamp = Math.floor(Date.now() / 1000);
const user = await this.userRepository.findOneBy({
id,
resetPasswordToken,
resetPasswordTokenExpiration: MoreThanOrEqual(currentTimestamp),
const user = await this.userRepository.findOne({
where: {
id,
resetPasswordToken,
resetPasswordTokenExpiration: MoreThanOrEqual(currentTimestamp),
},
relations: ['globalRole'],
});
if (!user?.isOwner && !Container.get(License).isWithinUsersLimit()) {
this.logger.debug(
'Request to resolve password token failed because the user limit was reached',
{ userId: id },
);
throw new UnauthorizedError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED);
}
if (!user) {
this.logger.debug(
'Request to resolve password token failed because no user was found for the provided user ID and reset password token',

View File

@@ -17,7 +17,12 @@ import {
withFeatureFlags,
} from '@/UserManagement/UserManagementHelper';
import { issueCookie } from '@/auth/jwt';
import { BadRequestError, InternalServerError, NotFoundError } from '@/ResponseHelper';
import {
BadRequestError,
InternalServerError,
NotFoundError,
UnauthorizedError,
} from '@/ResponseHelper';
import { Response } from 'express';
import type { Config } from '@/config';
import { UserRequest, UserSettingsUpdatePayload } from '@/requests';
@@ -39,8 +44,11 @@ import type {
SharedWorkflowRepository,
UserRepository,
} from '@db/repositories';
import { UserService } from '../user/user.service';
import { UserService } from '@/user/user.service';
import { plainToInstance } from 'class-transformer';
import { License } from '@/License';
import { Container } from 'typedi';
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
@Authorized(['global', 'owner'])
@RestController('/users')
@@ -107,6 +115,8 @@ export class UsersController {
*/
@Post('/')
async sendEmailInvites(req: UserRequest.Invite) {
const isWithinUsersLimit = Container.get(License).isWithinUsersLimit();
if (isSamlLicensedAndEnabled()) {
this.logger.debug(
'SAML is enabled, so users are managed by the Identity Provider and cannot be added through invites',
@@ -116,6 +126,13 @@ export class UsersController {
);
}
if (!isWithinUsersLimit) {
this.logger.debug(
'Request to send email invite(s) to user(s) failed because the user limit quota has been reached',
);
throw new UnauthorizedError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED);
}
if (!this.config.getEnv('userManagement.isInstanceOwnerSetUp')) {
this.logger.debug(
'Request to send email invite(s) to user(s) failed because the owner account is not set up',
@@ -551,6 +568,14 @@ export class UsersController {
@Post('/:id/reinvite')
async reinviteUser(req: UserRequest.Reinvite) {
const { id: idToReinvite } = req.params;
const isWithinUsersLimit = Container.get(License).isWithinUsersLimit();
if (!isWithinUsersLimit) {
this.logger.debug(
'Request to send email invite(s) to user(s) failed because the user limit quota has been reached',
);
throw new UnauthorizedError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED);
}
if (!isEmailSetUp()) {
this.logger.error('Request to reinvite a user failed because email sending was not set up');

View File

@@ -113,4 +113,14 @@ export class User extends AbstractEntity implements IUser {
computeIsPending(): void {
this.isPending = this.password === null;
}
/**
* Whether the user is instance owner
*/
isOwner: boolean;
@AfterLoad()
computeIsOwner(): void {
this.isOwner = this.globalRole?.name === 'owner';
}
}

View File

@@ -9,7 +9,6 @@ import type { ILicensePostResponse, ILicenseReadResponse } from '@/Interfaces';
import { LicenseService } from './License.service';
import { License } from '@/License';
import type { AuthenticatedRequest, LicenseRequest } from '@/requests';
import { isInstanceOwner } from '@/PublicApi/v1/handlers/users/users.service.ee';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
@@ -34,7 +33,7 @@ licenseController.use((req, res, next) => {
*/
licenseController.use((req: AuthenticatedRequest, res, next) => {
if (OWNER_ROUTES.includes(req.path) && req.user) {
if (!isInstanceOwner(req.user)) {
if (!req.user.isOwner) {
LoggerProxy.info('Non-owner attempted to activate or renew a license', {
userId: req.user.id,
});

View File

@@ -77,9 +77,6 @@ export const setupPushHandler = (restEndpoint: string, app: Application) => {
}
return;
}
// Handle authentication
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const authCookie: string = req.cookies?.[AUTH_COOKIE_NAME] ?? '';

View File

@@ -52,7 +52,9 @@ export function setSamlLoginLabel(label: string): void {
config.set(SAML_LOGIN_LABEL, label);
}
export const isSamlLicensed = () => Container.get(License).isSamlEnabled();
export function isSamlLicensed(): boolean {
return Container.get(License).isSamlEnabled();
}
export function isSamlLicensedAndEnabled(): boolean {
return isSamlLoginEnabled() && isSamlLicensed() && isSamlCurrentAuthenticationMethod();