Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { createProductInfo } from './product_info';
const DOT_NET_TICKS_EPOCH_OFFSET = 621355968000000000n;
const DOT_NET_TICKS_PER_MS = 10000n;
const DEVEXTREME_HTML_JS_BIT = 1n << 54n;
const DEVEXTREME_ASPNET_BIT = 1n << 17n;
const DEVEXTREME_COMPLETE_BITS = DEVEXTREME_HTML_JS_BIT | DEVEXTREME_ASPNET_BIT;

function msToDotNetTicks(ms: number): string {
return (BigInt(ms) * DOT_NET_TICKS_PER_MS + DOT_NET_TICKS_EPOCH_OFFSET).toString();
Expand Down Expand Up @@ -83,4 +85,80 @@ describe('LCP key validation', () => {

expect(token.kind).toBe(TokenKind.verified);
});

it('does not accept AspNet-only product bit without compatibility mode', () => {
const { parseDevExpressProductKey, TokenKind } = loadParserWithBypassedSignatureCheck();
const payload = `meta;251,${DEVEXTREME_ASPNET_BIT};`;

const token = parseDevExpressProductKey(createLcpSource(payload));

expect(token.kind).toBe(TokenKind.corrupted);
if (token.kind === TokenKind.corrupted) {
expect(token.error).toBe('product-kind');
}
});

it('accepts AspNet-only product bit in compatibility mode', () => {
const { parseDevExpressProductKey, TokenKind } = loadParserWithBypassedSignatureCheck();
const payload = `meta;251,${DEVEXTREME_ASPNET_BIT};`;

const token = parseDevExpressProductKey(createLcpSource(payload), true);

expect(token.kind).toBe(TokenKind.verified);
if (token.kind === TokenKind.verified) {
expect(token.payload.maxVersionAllowed).toBe(251);
}
});

it.each([false, true])('accepts HtmlJs-only product bit (acceptAspNetEntitlement=%s)', (acceptAspNetEntitlement) => {
const { parseDevExpressProductKey, TokenKind } = loadParserWithBypassedSignatureCheck();
const payload = `meta;251,${DEVEXTREME_HTML_JS_BIT};`;

const token = parseDevExpressProductKey(createLcpSource(payload), acceptAspNetEntitlement);

expect(token.kind).toBe(TokenKind.verified);
if (token.kind === TokenKind.verified) {
expect(token.payload.maxVersionAllowed).toBe(251);
}
});

it.each([
{ acceptAspNetEntitlement: false, expectedVersion: 251 },
{ acceptAspNetEntitlement: true, expectedVersion: 252 },
])('selects the correct entitlement for Complete followed by legacy AspNet (acceptAspNetEntitlement=$acceptAspNetEntitlement)', ({
acceptAspNetEntitlement,
expectedVersion,
}) => {
const { parseDevExpressProductKey, TokenKind } = loadParserWithBypassedSignatureCheck();
const payload = [
'meta',
`251,${DEVEXTREME_COMPLETE_BITS}`,
`252,${DEVEXTREME_ASPNET_BIT}`,
'',
].join(';');

const token = parseDevExpressProductKey(createLcpSource(payload), acceptAspNetEntitlement);

expect(token.kind).toBe(TokenKind.verified);
if (token.kind === TokenKind.verified) {
expect(token.payload.maxVersionAllowed).toBe(expectedVersion);
}
});

it.each([false, true])('selects newer Complete after an older AspNet-only entitlement (acceptAspNetEntitlement=%s)', (acceptAspNetEntitlement) => {
const { parseDevExpressProductKey, TokenKind } = loadParserWithBypassedSignatureCheck();
const payload = [
'meta',
`251,${DEVEXTREME_ASPNET_BIT}`,
`252,${DEVEXTREME_COMPLETE_BITS}`,
'',
].join(';');

const token = parseDevExpressProductKey(createLcpSource(payload), acceptAspNetEntitlement);

expect(token.kind).toBe(TokenKind.verified);
if (token.kind === TokenKind.verified) {
expect(token.payload.maxVersionAllowed).toBe(252);
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ function productsFromString(encodedString: string): ParsedProducts {
}
}

export function parseDevExpressProductKey(productsLicenseSource: string): Token {
export function parseDevExpressProductKey(
productsLicenseSource: string,
acceptAspNetEntitlement = false,
): Token {
if (!isProductOnlyLicense(productsLicenseSource)) {
return GENERAL_ERROR;
}
Expand Down Expand Up @@ -93,7 +96,10 @@ export function parseDevExpressProductKey(productsLicenseSource: string): Token
return errorToken;
}

const maxVersionAllowed = findLatestDevExtremeVersion({ products });
const maxVersionAllowed = findLatestDevExtremeVersion(
{ products },
acceptAspNetEntitlement,
);

if (!maxVersionAllowed) {
return PRODUCT_KIND_ERROR;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,18 @@ export function getMaxExpiration(info: LicenseInfo): number {
return Math.max(...expirationDates);
}

export function findLatestDevExtremeVersion(info: LicenseInfo): number | undefined {
export function findLatestDevExtremeVersion(
info: LicenseInfo,
acceptAspNetEntitlement = false,
): number | undefined {
if (!isLicenseValid(info)) {
return undefined;
}

const productKinds = acceptAspNetEntitlement
? [ProductKind.DevExtremeHtmlJs, ProductKind.DevExtremeAspNet]
: [ProductKind.DevExtremeHtmlJs];
const sorted = [...info.products].sort((a, b) => b.version - a.version);

return sorted.find((p) => isProduct(p, ProductKind.DevExtremeHtmlJs))?.version;
return sorted.find((p) => isProduct(p, ...productKinds))?.version;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
} from '@jest/globals';
import config from '@js/core/config';
import errors from '@js/core/errors';
import { fullVersion } from '@js/core/version';

import { base } from '../../ui/overlay/z_index';
import {
Expand All @@ -11,12 +12,14 @@ import {
clearAssertedVersions,
} from '../../utils/version';
import { LICENSE_KEY_PLACEHOLDER } from './const';
import * as productKeyValidator from './lcp_key_validation/lcp_key_validator';
import {
parseLicenseKey,
setLicenseCheckSkipCondition,
validateLicense,
} from './license_validation';
import * as trialPanel from './trial_panel.client';
import { TokenKind } from './types';

jest.mock('./key', () => ({
PUBLIC_KEY: {
Expand Down Expand Up @@ -583,3 +586,120 @@ describe('assertedVersions integration', () => {
});
});
});

describe('AspNet entitlement gate', () => {
const LCP_LICENSE = 'LCPv1mock-license';

beforeEach(() => {
jest.spyOn(errors, 'log').mockImplementation(() => {});
jest.spyOn(console, 'warn').mockImplementation(() => {});
jest.spyOn(trialPanel, 'renderTrialPanel');
setLicenseCheckSkipCondition(false);
});

afterEach(() => {
jest.restoreAllMocks();
clearAssertedVersions();
});

it('accepts the AspNet entitlement when a DevExtreme.AspNet assertion exists', () => {
const parseSpy = jest.spyOn(productKeyValidator, 'parseDevExpressProductKey').mockReturnValue({
kind: TokenKind.verified,
payload: {
customerId: 'test',
maxVersionAllowed: 999,
format: 1,
},
});

assertDevExtremeVersion('DevExtreme.AspNet.Core', fullVersion);
validateLicense(LCP_LICENSE, fullVersion);

expect(parseSpy).toHaveBeenCalledWith(LCP_LICENSE, true);
});

it('does not accept the AspNet entitlement when there is no AspNet assertion', () => {
const parseSpy = jest.spyOn(productKeyValidator, 'parseDevExpressProductKey').mockReturnValue({
kind: TokenKind.verified,
payload: {
customerId: 'test',
maxVersionAllowed: 999,
format: 1,
},
});

assertDevExtremeVersion('DevExtreme.React', fullVersion);
validateLicense(LCP_LICENSE, fullVersion);

expect(parseSpy).toHaveBeenCalledWith(LCP_LICENSE, false);
});

it('does not accept the AspNet entitlement for an unrelated package with a matching prefix', () => {
const parseSpy = jest.spyOn(productKeyValidator, 'parseDevExpressProductKey').mockReturnValue({
kind: TokenKind.verified,
payload: {
customerId: 'test',
maxVersionAllowed: 999,
format: 1,
},
});

assertDevExtremeVersion('DevExtreme.AspNetFake', fullVersion);
validateLicense(LCP_LICENSE, fullVersion);

expect(parseSpy).toHaveBeenCalledWith(LCP_LICENSE, false);
});

it('keeps version mismatch warnings for other packages when accepting the AspNet entitlement', () => {
const parseSpy = jest.spyOn(productKeyValidator, 'parseDevExpressProductKey').mockReturnValue({
kind: TokenKind.verified,
payload: {
customerId: 'test',
maxVersionAllowed: 999,
format: 1,
},
});

assertDevExtremeVersion('DevExpress.SomeOtherProduct', '1.2.3');
assertDevExtremeVersion('DevExtreme.AspNet.Core', fullVersion);
validateLicense(LCP_LICENSE, fullVersion);

expect(errors.log).toHaveBeenCalledWith(
'W0023',
expect.stringContaining('DevExpress.SomeOtherProduct'),
);
expect(parseSpy).toHaveBeenCalledWith(LCP_LICENSE, true);
});

it('does not revalidate an early JavaScript component after a later AspNet assertion', () => {
const parseSpy = jest.spyOn(productKeyValidator, 'parseDevExpressProductKey').mockReturnValue({
kind: TokenKind.corrupted,
error: 'product-kind',
});

validateLicense(LCP_LICENSE, fullVersion);
assertDevExtremeVersion('DevExtreme.AspNet.Core', fullVersion);
validateLicense(LCP_LICENSE, fullVersion);

expect(parseSpy).toHaveBeenCalledTimes(1);
expect(parseSpy).toHaveBeenCalledWith(LCP_LICENSE, false);
});

it('does not revalidate an early JavaScript component whose license is valid without the AspNet entitlement', () => {
const parseSpy = jest.spyOn(productKeyValidator, 'parseDevExpressProductKey').mockReturnValue({
kind: TokenKind.verified,
payload: {
customerId: 'test',
maxVersionAllowed: 999,
format: 1,
},
});

validateLicense(LCP_LICENSE, fullVersion);
assertDevExtremeVersion('DevExtreme.AspNet.Core', fullVersion);
validateLicense(LCP_LICENSE, fullVersion);

expect(parseSpy).toHaveBeenCalledTimes(1);
expect(parseSpy).toHaveBeenCalledWith(LCP_LICENSE, false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { fullVersion } from '@js/core/version';
import type { Version } from '../../utils/version';
import {
assertedVersionsCompatible,
getAssertedVersions,
parseVersion,
} from '../../utils/version';
import {
Expand All @@ -24,14 +25,23 @@ import {
} from './types';

let validationPerformed = false;
const ASPNET_ASSERTION_PREFIX = 'devextreme.aspnet.';

export function parseLicenseKey(encodedKey: string | undefined): Token {
function hasAspNetVersionAssertion(): boolean {
return getAssertedVersions()
.some(({ packageName }) => packageName.toLowerCase().startsWith(ASPNET_ASSERTION_PREFIX));
}

export function parseLicenseKey(
encodedKey: string | undefined,
acceptAspNetEntitlement = false,
): Token {
Comment thread
ajivanyandev marked this conversation as resolved.
if (encodedKey === undefined) {
return GENERAL_ERROR;
}

if (isProductOnlyLicense(encodedKey)) {
return parseDevExpressProductKey(encodedKey);
return parseDevExpressProductKey(encodedKey, acceptAspNetEntitlement);
}

return GENERAL_ERROR;
Expand Down Expand Up @@ -77,7 +87,8 @@ function getLicenseCheckParams({
return { preview, error: 'W0021', warningType: 'old-devextreme-key' };
}

const license = parseLicenseKey(licenseKey);
const acceptAspNetEntitlement = hasAspNetVersionAssertion();
const license = parseLicenseKey(licenseKey, acceptAspNetEntitlement);

if (license.kind === TokenKind.corrupted) {
if (license.error === 'product-kind') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import type { Token } from './types';

// @ts-expect-error - only for internal usage
export function parseLicenseKey(encodedKey: string | undefined): Token {}
export function parseLicenseKey(
encodedKey: string | undefined,
acceptAspNetEntitlement?: boolean,
): Token {
return undefined as unknown as Token;
}

export function validateLicense(licenseKey: string, version?: string): void {}

// @ts-expect-error - only for internal usage
export function peekValidationPerformed(): boolean {}
export function peekValidationPerformed(): boolean {
return false;
}

export function setLicenseCheckSkipCondition(): void {}

Expand Down
Loading