BREAKING CHANGE(vpn): replace tag-based VPN access control with source and target profiles

This commit is contained in:
2026-04-05 00:37:37 +00:00
parent 25365678e0
commit 1ddf83b28d
38 changed files with 1546 additions and 321 deletions

View File

@@ -1,13 +1,13 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { ReferenceResolver } from '../ts/config/classes.reference-resolver.js';
import type { ISecurityProfile, INetworkTarget, IRouteMetadata } from '../ts_interfaces/data/route-management.js';
import type { ISourceProfile, INetworkTarget, IRouteMetadata } from '../ts_interfaces/data/route-management.js';
import type { IRouteConfig } from '@push.rocks/smartproxy';
// ============================================================================
// Helpers: access private maps for direct unit testing without DB
// ============================================================================
function injectProfile(resolver: ReferenceResolver, profile: ISecurityProfile): void {
function injectProfile(resolver: ReferenceResolver, profile: ISourceProfile): void {
(resolver as any).profiles.set(profile.id, profile);
}
@@ -15,7 +15,7 @@ function injectTarget(resolver: ReferenceResolver, target: INetworkTarget): void
(resolver as any).targets.set(target.id, target);
}
function makeProfile(overrides: Partial<ISecurityProfile> = {}): ISecurityProfile {
function makeProfile(overrides: Partial<ISourceProfile> = {}): ISourceProfile {
return {
id: 'profile-1',
name: 'STANDARD',
@@ -72,14 +72,14 @@ tap.test('should list empty profiles and targets initially', async () => {
expect(resolver.listTargets().length).toEqual(0);
});
// ---- Security profile resolution ----
// ---- Source profile resolution ----
tap.test('should resolve security profile onto a route', async () => {
tap.test('should resolve source profile onto a route', async () => {
const profile = makeProfile();
injectProfile(resolver, profile);
const route = makeRoute();
const metadata: IRouteMetadata = { securityProfileRef: 'profile-1' };
const metadata: IRouteMetadata = { sourceProfileRef: 'profile-1' };
const result = resolver.resolveRoute(route, metadata);
@@ -87,7 +87,7 @@ tap.test('should resolve security profile onto a route', async () => {
expect(result.route.security!.ipAllowList).toContain('192.168.0.0/16');
expect(result.route.security!.ipAllowList).toContain('10.0.0.0/8');
expect(result.route.security!.maxConnections).toEqual(1000);
expect(result.metadata.securityProfileName).toEqual('STANDARD');
expect(result.metadata.sourceProfileName).toEqual('STANDARD');
expect(result.metadata.lastResolvedAt).toBeTruthy();
});
@@ -98,7 +98,7 @@ tap.test('should merge inline route security with profile security', async () =>
maxConnections: 5000,
},
});
const metadata: IRouteMetadata = { securityProfileRef: 'profile-1' };
const metadata: IRouteMetadata = { sourceProfileRef: 'profile-1' };
const result = resolver.resolveRoute(route, metadata);
@@ -117,7 +117,7 @@ tap.test('should deduplicate IP lists during merge', async () => {
ipAllowList: ['192.168.0.0/16', '127.0.0.1'],
},
});
const metadata: IRouteMetadata = { securityProfileRef: 'profile-1' };
const metadata: IRouteMetadata = { sourceProfileRef: 'profile-1' };
const result = resolver.resolveRoute(route, metadata);
@@ -128,13 +128,13 @@ tap.test('should deduplicate IP lists during merge', async () => {
tap.test('should handle missing profile gracefully', async () => {
const route = makeRoute();
const metadata: IRouteMetadata = { securityProfileRef: 'nonexistent-profile' };
const metadata: IRouteMetadata = { sourceProfileRef: 'nonexistent-profile' };
const result = resolver.resolveRoute(route, metadata);
// Route should be unchanged
expect(result.route.security).toBeUndefined();
expect(result.metadata.securityProfileName).toBeUndefined();
expect(result.metadata.sourceProfileName).toBeUndefined();
});
// ---- Profile inheritance ----
@@ -161,7 +161,7 @@ tap.test('should resolve profile inheritance (extendsProfiles)', async () => {
injectProfile(resolver, extendedProfile);
const route = makeRoute();
const metadata: IRouteMetadata = { securityProfileRef: 'extended-profile' };
const metadata: IRouteMetadata = { sourceProfileRef: 'extended-profile' };
const result = resolver.resolveRoute(route, metadata);
@@ -170,7 +170,7 @@ tap.test('should resolve profile inheritance (extendsProfiles)', async () => {
expect(result.route.security!.ipAllowList).toContain('160.79.104.0/21');
// maxConnections from base (extended doesn't override)
expect(result.route.security!.maxConnections).toEqual(500);
expect(result.metadata.securityProfileName).toEqual('EXTENDED');
expect(result.metadata.sourceProfileName).toEqual('EXTENDED');
});
tap.test('should detect circular profile inheritance', async () => {
@@ -190,7 +190,7 @@ tap.test('should detect circular profile inheritance', async () => {
injectProfile(resolver, profileB);
const route = makeRoute();
const metadata: IRouteMetadata = { securityProfileRef: 'circular-a' };
const metadata: IRouteMetadata = { sourceProfileRef: 'circular-a' };
// Should not infinite loop — resolves what it can
const result = resolver.resolveRoute(route, metadata);
@@ -232,7 +232,7 @@ tap.test('should handle missing target gracefully', async () => {
tap.test('should resolve both profile and target simultaneously', async () => {
const route = makeRoute();
const metadata: IRouteMetadata = {
securityProfileRef: 'profile-1',
sourceProfileRef: 'profile-1',
networkTargetRef: 'target-1',
};
@@ -247,7 +247,7 @@ tap.test('should resolve both profile and target simultaneously', async () => {
expect(result.route.action.targets![0].port).toEqual(443);
// Both names recorded
expect(result.metadata.securityProfileName).toEqual('STANDARD');
expect(result.metadata.sourceProfileName).toEqual('STANDARD');
expect(result.metadata.networkTargetName).toEqual('INFRA');
});
@@ -268,7 +268,7 @@ tap.test('should skip resolution when no metadata refs', async () => {
tap.test('should be idempotent — resolving twice gives same result', async () => {
const route = makeRoute();
const metadata: IRouteMetadata = {
securityProfileRef: 'profile-1',
sourceProfileRef: 'profile-1',
networkTargetRef: 'target-1',
};
@@ -288,7 +288,7 @@ tap.test('should find routes by profile ref (sync)', async () => {
id: 'route-a',
route: makeRoute({ name: 'route-a' }),
enabled: true,
metadata: { securityProfileRef: 'profile-1' },
metadata: { sourceProfileRef: 'profile-1' },
});
storedRoutes.set('route-b', {
id: 'route-b',
@@ -300,7 +300,7 @@ tap.test('should find routes by profile ref (sync)', async () => {
id: 'route-c',
route: makeRoute({ name: 'route-c' }),
enabled: true,
metadata: { securityProfileRef: 'profile-1', networkTargetRef: 'target-1' },
metadata: { sourceProfileRef: 'profile-1', networkTargetRef: 'target-1' },
});
const profileRefs = resolver.findRoutesByProfileRefSync('profile-1', storedRoutes);
@@ -320,7 +320,7 @@ tap.test('should get profile usage for a specific profile ID', async () => {
id: 'route-x',
route: makeRoute({ name: 'my-route' }),
enabled: true,
metadata: { securityProfileRef: 'profile-1' },
metadata: { sourceProfileRef: 'profile-1' },
});
const usage = resolver.getProfileUsageForId('profile-1', storedRoutes);