-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(server): client version check (#9205)
Co-authored-by: forehalo <forehalo@gmail.com>
- Loading branch information
1 parent
4fee2a9
commit fa86f71
Showing
17 changed files
with
369 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
import { Controller, Get } from '@nestjs/common'; | ||
import test from 'ava'; | ||
import Sinon from 'sinon'; | ||
|
||
import { AppModule } from '../app.module'; | ||
import { Runtime, UseNamedGuard } from '../base'; | ||
import { Public } from '../core/auth/guard'; | ||
import { VersionService } from '../core/version/service'; | ||
import { createTestingApp, TestingApp } from './utils'; | ||
|
||
@Public() | ||
@Controller('/guarded') | ||
class GuardedController { | ||
@UseNamedGuard('version') | ||
@Get('/test') | ||
test() { | ||
return 'test'; | ||
} | ||
} | ||
|
||
let app: TestingApp; | ||
let runtime: Sinon.SinonStubbedInstance<Runtime>; | ||
let version: VersionService; | ||
|
||
function checkVersion(enabled = true) { | ||
runtime.fetch.withArgs('client/versionControl.enabled').resolves(enabled); | ||
|
||
runtime.fetch | ||
.withArgs('client/versionControl.requiredVersion') | ||
.resolves('>=0.20.0'); | ||
} | ||
|
||
test.before(async () => { | ||
app = await createTestingApp({ | ||
imports: [AppModule], | ||
controllers: [GuardedController], | ||
tapModule: m => { | ||
m.overrideProvider(Runtime).useValue(Sinon.createStubInstance(Runtime)); | ||
}, | ||
}); | ||
|
||
runtime = app.get(Runtime); | ||
version = app.get(VersionService, { strict: false }); | ||
}); | ||
|
||
test.beforeEach(async () => { | ||
Sinon.reset(); | ||
|
||
checkVersion(true); | ||
}); | ||
|
||
test.after.always(async () => { | ||
await app.close(); | ||
}); | ||
|
||
test('should passthrough if version check is not enabled', async t => { | ||
checkVersion(false); | ||
|
||
const spy = Sinon.spy(version, 'checkVersion'); | ||
|
||
let res = await app.GET('/guarded/test'); | ||
|
||
t.is(res.status, 200); | ||
|
||
res = await app.GET('/guarded/test').set('x-affine-version', '0.20.0'); | ||
|
||
t.is(res.status, 200); | ||
|
||
res = await app.GET('/guarded/test').set('x-affine-version', 'invalid'); | ||
|
||
t.is(res.status, 200); | ||
t.true(spy.notCalled); | ||
spy.restore(); | ||
}); | ||
|
||
test('should passthrough is version range is invalid', async t => { | ||
runtime.fetch | ||
.withArgs('client/versionControl.requiredVersion') | ||
.resolves('invalid'); | ||
|
||
let res = await app.GET('/guarded/test').set('x-affine-version', 'invalid'); | ||
|
||
t.is(res.status, 200); | ||
}); | ||
|
||
test('should pass if client version is allowed', async t => { | ||
let res = await app.GET('/guarded/test').set('x-affine-version', '0.20.0'); | ||
|
||
t.is(res.status, 200); | ||
|
||
res = await app.GET('/guarded/test').set('x-affine-version', '0.21.0'); | ||
|
||
t.is(res.status, 200); | ||
|
||
runtime.fetch | ||
.withArgs('client/versionControl.requiredVersion') | ||
.resolves('>=0.19.0'); | ||
|
||
res = await app.GET('/guarded/test').set('x-affine-version', '0.19.0'); | ||
|
||
t.is(res.status, 200); | ||
}); | ||
|
||
test('should fail if client version is not set or invalid', async t => { | ||
let res = await app.GET('/guarded/test'); | ||
|
||
t.is(res.status, 403); | ||
t.is( | ||
res.body.message, | ||
'Unsupported client with version [unset_or_invalid], required version is [>=0.20.0].' | ||
); | ||
|
||
res = await app.GET('/guarded/test').set('x-affine-version', 'invalid'); | ||
|
||
t.is(res.status, 403); | ||
t.is( | ||
res.body.message, | ||
'Unsupported client with version [invalid], required version is [>=0.20.0].' | ||
); | ||
}); | ||
|
||
test('should tell upgrade if client version is lower than allowed', async t => { | ||
runtime.fetch | ||
.withArgs('client/versionControl.requiredVersion') | ||
.resolves('>=0.21.0 <=0.22.0'); | ||
|
||
let res = await app.GET('/guarded/test').set('x-affine-version', '0.20.0'); | ||
|
||
t.is(res.status, 403); | ||
t.is( | ||
res.body.message, | ||
'Unsupported client with version [0.20.0], required version is [>=0.21.0 <=0.22.0].' | ||
); | ||
}); | ||
|
||
test('should tell downgrade if client version is higher than allowed', async t => { | ||
runtime.fetch | ||
.withArgs('client/versionControl.requiredVersion') | ||
.resolves('>=0.20.0 <=0.22.0'); | ||
|
||
let res = await app.GET('/guarded/test').set('x-affine-version', '0.23.0'); | ||
|
||
t.is(res.status, 403); | ||
t.is( | ||
res.body.message, | ||
'Unsupported client with version [0.23.0], required version is [>=0.20.0 <=0.22.0].' | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { defineRuntimeConfig, ModuleConfig } from '../../base/config'; | ||
|
||
export interface VersionConfig { | ||
versionControl: { | ||
enabled: boolean; | ||
requiredVersion: string; | ||
}; | ||
} | ||
|
||
declare module '../../base/config' { | ||
interface AppConfig { | ||
client: ModuleConfig<never, VersionConfig>; | ||
} | ||
} | ||
|
||
declare module '../../base/guard' { | ||
interface RegisterGuardName { | ||
version: 'version'; | ||
} | ||
} | ||
|
||
defineRuntimeConfig('client', { | ||
'versionControl.enabled': { | ||
desc: 'Whether check version of client before accessing the server.', | ||
default: false, | ||
}, | ||
'versionControl.requiredVersion': { | ||
desc: "Allowed version range of the app that allowed to access the server. Requires 'client/versionControl.enabled' to be true to take effect.", | ||
default: '>=0.20.0', | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import type { | ||
CanActivate, | ||
ExecutionContext, | ||
OnModuleInit, | ||
} from '@nestjs/common'; | ||
import { Injectable } from '@nestjs/common'; | ||
|
||
import { | ||
getRequestResponseFromContext, | ||
GuardProvider, | ||
Runtime, | ||
} from '../../base'; | ||
import { VersionService } from './service'; | ||
|
||
@Injectable() | ||
export class VersionGuardProvider | ||
extends GuardProvider | ||
implements CanActivate, OnModuleInit | ||
{ | ||
name = 'version' as const; | ||
|
||
constructor( | ||
private readonly runtime: Runtime, | ||
private readonly version: VersionService | ||
) { | ||
super(); | ||
} | ||
|
||
async canActivate(context: ExecutionContext) { | ||
if (!(await this.runtime.fetch('client/versionControl.enabled'))) { | ||
return true; | ||
} | ||
|
||
const { req } = getRequestResponseFromContext(context); | ||
|
||
const version = req.headers['x-affine-version'] as string | undefined; | ||
|
||
return this.version.checkVersion(version); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import './config'; | ||
|
||
import { Module } from '@nestjs/common'; | ||
|
||
import { VersionGuardProvider } from './guard'; | ||
import { VersionService } from './service'; | ||
|
||
@Module({ | ||
providers: [VersionService, VersionGuardProvider], | ||
}) | ||
export class VersionModule {} | ||
|
||
export type { VersionConfig } from './config'; |
Oops, something went wrong.