From bc390a6d05841c2decdd434793e823c17517e426 Mon Sep 17 00:00:00 2001 From: Alex Tugarev Date: Tue, 7 Nov 2023 12:21:55 +0000 Subject: [PATCH] [server] add Unauthenticated decorator for public-api --- components/server/src/api/server.ts | 31 ++++++++++++++----- .../server/src/api/unauthenticated.spec.ts | 28 +++++++++++++++++ components/server/src/api/unauthenticated.ts | 17 ++++++++++ 3 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 components/server/src/api/unauthenticated.spec.ts create mode 100644 components/server/src/api/unauthenticated.ts diff --git a/components/server/src/api/server.ts b/components/server/src/api/server.ts index 346e0f2bf1a5ec..334ad30b9e5808 100644 --- a/components/server/src/api/server.ts +++ b/components/server/src/api/server.ts @@ -41,6 +41,7 @@ import { APITeamsService as TeamsServiceAPI } from "./teams"; import { APIUserService as UserServiceAPI } from "./user"; import { WorkspaceServiceAPI } from "./workspace-service-api"; import { AuthProviderServiceAPI } from "./auth-provider-service-api"; +import { Unauthenticated } from "./unauthenticated"; decorate(injectable(), PublicAPIConverter); @@ -214,9 +215,21 @@ export class API { const apply = async (): Promise => { const subjectId = await self.verify(context); - await rateLimit(subjectId); - context.user = await self.ensureFgaMigration(subjectId); + const isAuthenticated = !!subjectId; + const requiresAuthentication = !Unauthenticated.get(target, prop); + if (!isAuthenticated && requiresAuthentication) { + throw new ConnectError("unauthenticated", Code.Unauthenticated); + } + + if (isAuthenticated) { + await rateLimit(subjectId); + context.user = await self.ensureFgaMigration(subjectId); + } + + // TODO(at) if unauthenticated, we still need to apply enforece a rate limit + + // actually call the RPC handler return Reflect.apply(target[prop as any], target, args); }; if (grpc_type === "unary" || grpc_type === "client_stream") { @@ -250,14 +263,16 @@ export class API { }; } - private async verify(context: HandlerContext): Promise { + private async verify(context: HandlerContext): Promise { const cookieHeader = (context.requestHeader.get("cookie") || "") as string; - const claims = await this.sessionHandler.verifyJWTCookie(cookieHeader); - const subjectId = claims?.sub; - if (!subjectId) { - throw new ConnectError("unauthenticated", Code.Unauthenticated); + try { + const claims = await this.sessionHandler.verifyJWTCookie(cookieHeader); + const subjectId = claims?.sub; + return subjectId; + } catch (error) { + log.warn("Failed to authenticate user with JWT Session", error); + return undefined; } - return subjectId; } private async ensureFgaMigration(userId: string): Promise { diff --git a/components/server/src/api/unauthenticated.spec.ts b/components/server/src/api/unauthenticated.spec.ts new file mode 100644 index 00000000000000..c97ba9c0ec6ec4 --- /dev/null +++ b/components/server/src/api/unauthenticated.spec.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2023 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +import * as chai from "chai"; +import { Unauthenticated } from "./unauthenticated"; + +const expect = chai.expect; + +class Foo { + @Unauthenticated() + async fooUnauthenticated() {} + + async foo() {} +} + +describe("Unauthenticated decorator", function () { + const foo = new Foo(); + + it("function is decorated", function () { + expect(Unauthenticated.get(foo, "fooUnauthenticated")).to.be.true; + }); + it("function is not decorated", function () { + expect(Unauthenticated.get(foo, "foo")).to.be.false; + }); +}); diff --git a/components/server/src/api/unauthenticated.ts b/components/server/src/api/unauthenticated.ts new file mode 100644 index 00000000000000..1d9721e80c99a4 --- /dev/null +++ b/components/server/src/api/unauthenticated.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2023 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +const UNAUTHENTICATED_METADATA_KEY = Symbol("Unauthenticated"); + +export function Unauthenticated() { + return Reflect.metadata(UNAUTHENTICATED_METADATA_KEY, true); +} + +export namespace Unauthenticated { + export function get(target: Object, properyKey: string | symbol): boolean { + return !!Reflect.getMetadata(UNAUTHENTICATED_METADATA_KEY, target, properyKey); + } +}