[Billing for self host] End dummy enterprise key validity (#19560)
<img width="1504" height="755" alt="Screenshot 2026-04-10 at 16 40 07" src="https://github.com/user-attachments/assets/68a12e40-a077-48df-9e18-885493520a32" /> Re-using hasValidEnterpriseKey to avoid breaking changes. This will be entirely removed in the next versions.
This commit is contained in:
+6
-6
@@ -286,6 +286,7 @@ describe('EnterprisePlanService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// hasValidEnterpriseKey now means "has any ENTERPRISE_KEY configured"
|
||||
describe('hasValidEnterpriseKey', () => {
|
||||
it('should return true when signed enterprise key is valid', async () => {
|
||||
await setupValidState();
|
||||
@@ -293,13 +294,12 @@ describe('EnterprisePlanService', () => {
|
||||
expect(service.hasValidEnterpriseKey()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true with legacy unsigned key as fallback', async () => {
|
||||
it('should return true with unsigned legacy key', async () => {
|
||||
setupEnterpriseKey('some-legacy-key');
|
||||
mockCryptoVerify.mockReturnValue(false);
|
||||
appTokenFindOneMock.mockResolvedValue(null);
|
||||
await service.onModuleInit();
|
||||
|
||||
expect(service.hasValidSignedEnterpriseKey()).toBe(false);
|
||||
expect(service.hasValidEnterpriseKey()).toBe(true);
|
||||
});
|
||||
|
||||
@@ -318,13 +318,13 @@ describe('EnterprisePlanService', () => {
|
||||
expect(service.isValid()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true with legacy key as fallback', async () => {
|
||||
it('should return false with unsigned legacy key', async () => {
|
||||
setupEnterpriseKey('some-legacy-key');
|
||||
mockCryptoVerify.mockReturnValue(false);
|
||||
appTokenFindOneMock.mockResolvedValue(null);
|
||||
await service.onModuleInit();
|
||||
|
||||
expect(service.isValid()).toBe(true);
|
||||
expect(service.isValid()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when no key or token exists', async () => {
|
||||
@@ -399,7 +399,7 @@ describe('EnterprisePlanService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return legacy license info when only legacy key exists', async () => {
|
||||
it('should return invalid license info when only unsigned legacy key exists', async () => {
|
||||
setupEnterpriseKey('some-legacy-key');
|
||||
mockCryptoVerify.mockReturnValue(false);
|
||||
appTokenFindOneMock.mockResolvedValue(null);
|
||||
@@ -407,7 +407,7 @@ describe('EnterprisePlanService', () => {
|
||||
const licenseInfo = await service.getLicenseInfo();
|
||||
|
||||
expect(licenseInfo).toEqual({
|
||||
isValid: true,
|
||||
isValid: false,
|
||||
licensee: null,
|
||||
expiresAt: null,
|
||||
subscriptionId: null,
|
||||
|
||||
+12
-35
@@ -143,40 +143,22 @@ export class EnterprisePlanService implements OnModuleInit {
|
||||
}
|
||||
|
||||
hasValidEnterpriseKey(): boolean {
|
||||
if (this.hasValidSignedEnterpriseKey()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.checkLegacyKey();
|
||||
return this.hasValidSignedEnterpriseKey() || this.checkLegacyKey();
|
||||
}
|
||||
|
||||
isValid(): boolean {
|
||||
if (this.hasValidEnterpriseValidityToken()) {
|
||||
return true;
|
||||
}
|
||||
return this.hasValidEnterpriseValidityToken();
|
||||
}
|
||||
|
||||
return this.checkLegacyKey(); // temporary
|
||||
private checkLegacyKey(): boolean {
|
||||
// temporary
|
||||
return isDefined(this.twentyConfigService.get('ENTERPRISE_KEY'));
|
||||
}
|
||||
|
||||
isValidEnterpriseKeyFormat(key: string): boolean {
|
||||
return this.verifyJwt<EnterpriseKeyPayload>(key) !== null;
|
||||
}
|
||||
|
||||
private checkLegacyKey(): boolean {
|
||||
const enterpriseKey = this.twentyConfigService.get('ENTERPRISE_KEY');
|
||||
|
||||
if (!isDefined(enterpriseKey)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.logger.warn(
|
||||
'Unsigned enterprise keys are deprecated and will stop working ' +
|
||||
'in a future version. Please obtain a signed key from twenty.com.',
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async getLicenseInfo(): Promise<EnterpriseLicenseInfo> {
|
||||
this.refreshKeyPayload();
|
||||
await this.loadValidityToken();
|
||||
@@ -192,15 +174,6 @@ export class EnterprisePlanService implements OnModuleInit {
|
||||
};
|
||||
}
|
||||
|
||||
if (this.checkLegacyKey()) {
|
||||
return {
|
||||
isValid: true,
|
||||
licensee: null,
|
||||
expiresAt: null,
|
||||
subscriptionId: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: false,
|
||||
licensee: null,
|
||||
@@ -477,11 +450,15 @@ export class EnterprisePlanService implements OnModuleInit {
|
||||
}
|
||||
}
|
||||
|
||||
// In development, try both keys so production keys work when testing locally
|
||||
// In development and Jest integration tests, try both keys so production keys
|
||||
// work locally
|
||||
private getPublicKeysToTry(): string[] {
|
||||
const nodeEnv = this.twentyConfigService.get('NODE_ENV');
|
||||
|
||||
if (nodeEnv === NodeEnvironment.DEVELOPMENT) {
|
||||
if (
|
||||
nodeEnv === NodeEnvironment.DEVELOPMENT ||
|
||||
nodeEnv === NodeEnvironment.TEST
|
||||
) {
|
||||
return [ENTERPRISE_JWT_PUBLIC_KEY, ENTERPRISE_JWT_DEV_PUBLIC_KEY];
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ import { UpsertRowLevelPermissionPredicatesInput } from 'src/engine/metadata-mod
|
||||
import { RowLevelPermissionPredicateGroupDTO } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate-group.dto';
|
||||
import { RowLevelPermissionPredicateDTO } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate.dto';
|
||||
import { UpsertRowLevelPermissionPredicatesResultDTO } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/upsert-row-level-permission-predicates-result.dto';
|
||||
import { RowLevelPermissionPredicateGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/row-level-permission-predicate/filters/row-level-permission-predicate-graphql-api-exception.filter';
|
||||
import { RowLevelPermissionPredicateGroupService } from 'src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate-group.service';
|
||||
import { RowLevelPermissionPredicateService } from 'src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate.service';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
@@ -77,6 +78,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
|
||||
SettingsPermissionGuard(PermissionFlagType.ROLES),
|
||||
)
|
||||
@UseFilters(
|
||||
RowLevelPermissionPredicateGraphqlApiExceptionFilter,
|
||||
PermissionsGraphqlApiExceptionFilter,
|
||||
PreventNestToAutoLogGraphqlErrorsFilter,
|
||||
)
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { Catch, type ExceptionFilter } from '@nestjs/common';
|
||||
|
||||
import { RowLevelPermissionPredicateException } from 'src/engine/metadata-modules/row-level-permission-predicate/exceptions/row-level-permission-predicate.exception';
|
||||
import { rowLevelPermissionPredicateGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/row-level-permission-predicate/utils/row-level-permission-predicate-graphql-api-exception-handler.util';
|
||||
|
||||
@Catch(RowLevelPermissionPredicateException)
|
||||
export class RowLevelPermissionPredicateGraphqlApiExceptionFilter
|
||||
implements ExceptionFilter
|
||||
{
|
||||
catch(exception: RowLevelPermissionPredicateException) {
|
||||
return rowLevelPermissionPredicateGraphqlApiExceptionHandler(exception);
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
ForbiddenError,
|
||||
InternalServerError,
|
||||
NotFoundError,
|
||||
UserInputError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import {
|
||||
RowLevelPermissionPredicateException,
|
||||
RowLevelPermissionPredicateExceptionCode,
|
||||
} from 'src/engine/metadata-modules/row-level-permission-predicate/exceptions/row-level-permission-predicate.exception';
|
||||
import { rowLevelPermissionPredicateGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/row-level-permission-predicate/utils/row-level-permission-predicate-graphql-api-exception-handler.util';
|
||||
|
||||
describe('rowLevelPermissionPredicateGraphqlApiExceptionHandler', () => {
|
||||
it('should throw ForbiddenError for ROW_LEVEL_PERMISSION_FEATURE_DISABLED', () => {
|
||||
const exception = new RowLevelPermissionPredicateException(
|
||||
'Row level permission predicate feature is disabled.',
|
||||
RowLevelPermissionPredicateExceptionCode.ROW_LEVEL_PERMISSION_FEATURE_DISABLED,
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
rowLevelPermissionPredicateGraphqlApiExceptionHandler(exception),
|
||||
).toThrow(ForbiddenError);
|
||||
});
|
||||
|
||||
it('should throw NotFoundError for ROW_LEVEL_PERMISSION_PREDICATE_NOT_FOUND', () => {
|
||||
const exception = new RowLevelPermissionPredicateException(
|
||||
'Predicate not found',
|
||||
RowLevelPermissionPredicateExceptionCode.ROW_LEVEL_PERMISSION_PREDICATE_NOT_FOUND,
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
rowLevelPermissionPredicateGraphqlApiExceptionHandler(exception),
|
||||
).toThrow(NotFoundError);
|
||||
});
|
||||
|
||||
it('should throw UserInputError for INVALID_ROW_LEVEL_PERMISSION_PREDICATE_DATA', () => {
|
||||
const exception = new RowLevelPermissionPredicateException(
|
||||
'Invalid data',
|
||||
RowLevelPermissionPredicateExceptionCode.INVALID_ROW_LEVEL_PERMISSION_PREDICATE_DATA,
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
rowLevelPermissionPredicateGraphqlApiExceptionHandler(exception),
|
||||
).toThrow(UserInputError);
|
||||
});
|
||||
|
||||
it('should throw InternalServerError for INTERNAL_SERVER_ERROR', () => {
|
||||
const exception = new RowLevelPermissionPredicateException(
|
||||
'Unexpected',
|
||||
RowLevelPermissionPredicateExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
rowLevelPermissionPredicateGraphqlApiExceptionHandler(exception),
|
||||
).toThrow(InternalServerError);
|
||||
});
|
||||
});
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
ForbiddenError,
|
||||
InternalServerError,
|
||||
NotFoundError,
|
||||
UserInputError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import {
|
||||
type RowLevelPermissionPredicateException,
|
||||
RowLevelPermissionPredicateExceptionCode,
|
||||
} from 'src/engine/metadata-modules/row-level-permission-predicate/exceptions/row-level-permission-predicate.exception';
|
||||
|
||||
export const rowLevelPermissionPredicateGraphqlApiExceptionHandler = (
|
||||
error: RowLevelPermissionPredicateException,
|
||||
) => {
|
||||
switch (error.code) {
|
||||
case RowLevelPermissionPredicateExceptionCode.ROW_LEVEL_PERMISSION_FEATURE_DISABLED:
|
||||
case RowLevelPermissionPredicateExceptionCode.UNAUTHORIZED_ROLE_MODIFICATION:
|
||||
case RowLevelPermissionPredicateExceptionCode.UNAUTHORIZED_OBJECT_MODIFICATION:
|
||||
throw new ForbiddenError(error);
|
||||
case RowLevelPermissionPredicateExceptionCode.INVALID_ROW_LEVEL_PERMISSION_PREDICATE_DATA:
|
||||
throw new UserInputError(error);
|
||||
case RowLevelPermissionPredicateExceptionCode.ROW_LEVEL_PERMISSION_PREDICATE_NOT_FOUND:
|
||||
case RowLevelPermissionPredicateExceptionCode.FIELD_METADATA_NOT_FOUND:
|
||||
case RowLevelPermissionPredicateExceptionCode.OBJECT_METADATA_NOT_FOUND:
|
||||
case RowLevelPermissionPredicateExceptionCode.ROLE_NOT_FOUND:
|
||||
throw new NotFoundError(error);
|
||||
case RowLevelPermissionPredicateExceptionCode.INTERNAL_SERVER_ERROR:
|
||||
throw new InternalServerError(error);
|
||||
default: {
|
||||
return assertUnreachable(error.code);
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user