Skip to content

Commit 9d91849

Browse files
committed
[server] add interceptor to public api
1 parent e682235 commit 9d91849

File tree

11 files changed

+109
-52
lines changed

11 files changed

+109
-52
lines changed

components/proxy/conf/Caddyfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,9 +198,10 @@ https://{$GITPOD_DOMAIN} {
198198
@proxy_server_public_api path /public-api/gitpod.experimental.v1.HelloService*
199199
handle @proxy_server_public_api {
200200
uri strip_prefix /public-api
201+
# TODO(ak) verify that it only enabled for json content-type, not grpc
201202
import compression
202203

203-
reverse_proxy server.{$KUBE_NAMESPACE}.{$KUBE_DOMAIN}:3000 {
204+
reverse_proxy server.{$KUBE_NAMESPACE}.{$KUBE_DOMAIN}:3001 {
204205
import upstream_connection
205206
}
206207
}

components/server/src/api/dummy.ts

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { Code, ConnectError, HandlerContext, ServiceImpl } from "@bufbuild/connect";
7+
import { HandlerContext, ServiceImpl } from "@bufbuild/connect";
88
import { User } from "@gitpod/gitpod-protocol";
99
import { HelloService } from "@gitpod/public-api/lib/gitpod/experimental/v1/dummy_connectweb";
1010
import {
@@ -13,55 +13,33 @@ import {
1313
SayHelloRequest,
1414
SayHelloResponse,
1515
} from "@gitpod/public-api/lib/gitpod/experimental/v1/dummy_pb";
16-
import { inject, injectable } from "inversify";
17-
import { SessionHandler } from "../session-handler";
16+
import { injectable } from "inversify";
1817

1918
/**
2019
* TODO(ak):
21-
* - server-side observability
22-
* - client-side observability
23-
* - rate limitting
24-
* - logging context
25-
* - feature flags for unary and stream tests
26-
* - SLOs
27-
* - alerting
20+
* - client-side observability (add SLOs)
21+
* - client-side feature flags for unary and stream tests
2822
*/
2923
@injectable()
3024
export class APIHelloService implements ServiceImpl<typeof HelloService> {
31-
constructor(
32-
@inject(SessionHandler)
33-
private readonly sessionHandler: SessionHandler,
34-
) {}
35-
3625
async sayHello(req: SayHelloRequest, context: HandlerContext): Promise<SayHelloResponse> {
37-
const user = await this.authUser(context);
3826
const response = new SayHelloResponse();
39-
response.reply = "Hello " + this.getSubject(user);
27+
response.reply = "Hello " + this.getSubject(context);
4028
return response;
4129
}
4230
async *lotsOfReplies(req: LotsOfRepliesRequest, context: HandlerContext): AsyncGenerator<LotsOfRepliesResponse> {
43-
const user = await this.authUser(context);
4431
let count = req.previousCount || 0;
4532
while (true) {
4633
const response = new LotsOfRepliesResponse();
47-
response.reply = `Hello ${this.getSubject(user)} ${count}`;
34+
response.reply = `Hello ${this.getSubject(context)} ${count}`;
4835
response.count = count;
4936
yield response;
5037
count++;
5138
await new Promise((resolve) => setTimeout(resolve, 30000));
5239
}
5340
}
5441

55-
private getSubject(user: User): string {
56-
return User.getName(user) || "World";
57-
}
58-
59-
// TODO(ak) decorate
60-
private async authUser(context: HandlerContext) {
61-
const user = await this.sessionHandler.verify(context.requestHeader.get("cookie"));
62-
if (!user) {
63-
throw new ConnectError("unauthenticated", Code.Unauthenticated);
64-
}
65-
return user;
42+
private getSubject(context: HandlerContext): string {
43+
return User.getName(context.user) || "World";
6644
}
6745
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { User } from "@gitpod/gitpod-protocol";
8+
9+
declare module "@bufbuild/connect" {
10+
interface HandlerContext {
11+
user: User;
12+
}
13+
}

components/server/src/api/server.ts

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { ConnectRouter } from "@bufbuild/connect";
7+
import { Code, ConnectError, ConnectRouter, HandlerContext } from "@bufbuild/connect";
88
import { expressConnectMiddleware } from "@bufbuild/connect-express";
99
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
1010
import { HelloService } from "@gitpod/public-api/lib/gitpod/experimental/v1/dummy_connectweb";
@@ -16,6 +16,7 @@ import express from "express";
1616
import * as http from "http";
1717
import { inject, injectable, interfaces } from "inversify";
1818
import { AddressInfo } from "net";
19+
import { SessionHandler } from "../session-handler";
1920
import { APIHelloService } from "./dummy";
2021
import { APIStatsService } from "./stats";
2122
import { APITeamsService } from "./teams";
@@ -24,24 +25,36 @@ import { APIWorkspacesService } from "./workspaces";
2425

2526
@injectable()
2627
export class API {
27-
@inject(APIUserService) protected readonly apiUserService: APIUserService;
28-
@inject(APITeamsService) protected readonly apiTeamService: APITeamsService;
29-
@inject(APIWorkspacesService) protected readonly apiWorkspacesService: APIWorkspacesService;
30-
@inject(APIStatsService) protected readonly apiStatsService: APIStatsService;
28+
@inject(APIUserService) private readonly apiUserService: APIUserService;
29+
@inject(APITeamsService) private readonly apiTeamService: APITeamsService;
30+
@inject(APIWorkspacesService) private readonly apiWorkspacesService: APIWorkspacesService;
31+
@inject(APIStatsService) private readonly apiStatsService: APIStatsService;
3132
@inject(APIHelloService) private readonly apiHelloService: APIHelloService;
33+
@inject(SessionHandler) private readonly sessionHandler: SessionHandler;
3234

33-
public listen(): http.Server {
35+
listenPrivate(): http.Server {
3436
const app = express();
35-
this.register(app);
37+
this.registerPrivate(app);
3638

3739
const server = app.listen(9877, () => {
38-
log.info(`Connect API server listening on port: ${(server.address() as AddressInfo).port}`);
40+
log.info(`Connect Private API server listening on port: ${(server.address() as AddressInfo).port}`);
3941
});
4042

4143
return server;
4244
}
4345

44-
private register(app: express.Application) {
46+
listen(): http.Server {
47+
const app = express();
48+
this.register(app);
49+
50+
const server = app.listen(3001, () => {
51+
log.info(`Connect Public API server listening on port: ${(server.address() as AddressInfo).port}`);
52+
});
53+
54+
return server;
55+
}
56+
57+
private registerPrivate(app: express.Application) {
4558
app.use(
4659
expressConnectMiddleware({
4760
routes: (router: ConnectRouter) => {
@@ -54,16 +67,52 @@ export class API {
5467
);
5568
}
5669

57-
get apiRouter(): express.Router {
58-
const router = express.Router();
59-
router.use(
70+
private register(app: express.Application) {
71+
const self = this;
72+
const serviceInterceptor: ProxyHandler<any> = {
73+
get(target, prop, receiver) {
74+
const original = target[prop as any];
75+
if (typeof original !== "function") {
76+
return Reflect.get(target, prop, receiver);
77+
}
78+
return async (...args: any[]) => {
79+
const context = args[1] as HandlerContext;
80+
await self.intercept(context);
81+
return original.apply(target, args);
82+
};
83+
},
84+
};
85+
app.use(
6086
expressConnectMiddleware({
6187
routes: (router: ConnectRouter) => {
62-
router.service(HelloService, this.apiHelloService);
88+
for (const service of [this.apiHelloService]) {
89+
router.service(HelloService, new Proxy(service, serviceInterceptor));
90+
}
6391
},
6492
}),
6593
);
66-
return router;
94+
}
95+
96+
/**
97+
* intercept handles cross-cutting concerns for all calls:
98+
* - authentication
99+
* TODO(ak):
100+
* - server-side observability (SLOs)
101+
* - rate limitting
102+
* - logging context
103+
* - tracing
104+
*/
105+
private async intercept(context: HandlerContext): Promise<void> {
106+
const user = await this.verify(context);
107+
context.user = user;
108+
}
109+
110+
private async verify(context: HandlerContext) {
111+
const user = await this.sessionHandler.verify(context.requestHeader.get("cookie"));
112+
if (!user) {
113+
throw new ConnectError("unauthenticated", Code.Unauthenticated);
114+
}
115+
return user;
67116
}
68117

69118
static contribute(bind: interfaces.Bind): void {

components/server/src/api/teams.spec.db.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export class APITeamsServiceSpec {
4646
await this.dbConn.getRepository(DBTeam).delete({});
4747

4848
// Start an actual server for tests
49-
this.server = this.container.get<API>(API).listen();
49+
this.server = this.container.get<API>(API).listenPrivate();
5050

5151
// Construct a client to point against our server
5252
const address = this.server.address() as AddressInfo;

components/server/src/server.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ export class Server {
5656
protected iamSessionApp?: express.Application;
5757
protected iamSessionAppServer?: http.Server;
5858

59-
protected apiServer?: http.Server;
59+
protected publicApiServer?: http.Server;
60+
protected privateApiServer?: http.Server;
6061

6162
protected readonly eventEmitter = new EventEmitter();
6263
protected app?: express.Application;
@@ -317,9 +318,6 @@ export class Server {
317318

318319
log.info("Registered Bitbucket Server app at " + BitbucketServerApp.path);
319320
app.use(BitbucketServerApp.path, this.bitbucketServerApp.router);
320-
321-
// TODO(ak) move to own app on port 3001
322-
app.use(this.api.apiRouter);
323321
}
324322

325323
public async start(port: number) {
@@ -349,7 +347,8 @@ export class Server {
349347
});
350348
}
351349

352-
this.apiServer = this.api.listen();
350+
this.publicApiServer = this.api.listen();
351+
this.privateApiServer = this.api.listenPrivate();
353352

354353
this.debugApp.start();
355354
}
@@ -379,7 +378,8 @@ export class Server {
379378
race(this.stopServer(this.iamSessionAppServer), "stop iamsessionapp"),
380379
race(this.stopServer(this.monitoringHttpServer), "stop monitoringapp"),
381380
race(this.stopServer(this.httpServer), "stop httpserver"),
382-
race(this.stopServer(this.apiServer), "stop api server"),
381+
race(this.stopServer(this.privateApiServer), "stop private api server"),
382+
race(this.stopServer(this.publicApiServer), "stop public api server"),
383383
race((async () => this.disposables.dispose())(), "dispose disposables"),
384384
]);
385385

install/installer/pkg/common/constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const (
4141
ServerIAMSessionPort = 9876
4242
ServerInstallationAdminPort = 9000
4343
ServerGRPCAPIPort = 9877
44+
ServerPublicAPIPort = 3001
4445
SystemNodeCritical = "system-node-critical"
4546
PublicApiComponent = "public-api-server"
4647
UsageComponent = "usage"

install/installer/pkg/components/server/constants.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,7 @@ const (
3232

3333
GRPCAPIName = "grpc"
3434
GRPCAPIPort = common.ServerGRPCAPIPort
35+
36+
PublicAPIName = "publicApi"
37+
PublicAPIPort = common.ServerPublicAPIPort
3538
)

install/installer/pkg/components/server/deployment.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,9 @@ func deployment(ctx *common.RenderContext) ([]runtime.Object, error) {
382382
}, {
383383
Name: GRPCAPIName,
384384
ContainerPort: GRPCAPIPort,
385+
}, {
386+
Name: PublicAPIName,
387+
ContainerPort: PublicAPIPort,
385388
},
386389
},
387390
// todo(sje): do we need to cater for serverContainer.env from values.yaml?

install/installer/pkg/components/server/networkpolicy.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ func Networkpolicy(ctx *common.RenderContext, component string) ([]runtime.Objec
3636
Protocol: common.TCPProtocol,
3737
Port: &intstr.IntOrString{IntVal: ContainerPort},
3838
},
39+
{
40+
Protocol: common.TCPProtocol,
41+
Port: &intstr.IntOrString{IntVal: PublicAPIPort},
42+
},
3943
},
4044
From: []networkingv1.NetworkPolicyPeer{
4145
{

install/installer/pkg/components/server/objects.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ var Objects = common.CompositeRenderFunc(
5959
ContainerPort: GRPCAPIPort,
6060
ServicePort: GRPCAPIPort,
6161
},
62+
{
63+
Name: PublicAPIName,
64+
ContainerPort: PublicAPIPort,
65+
ServicePort: PublicAPIPort,
66+
},
6267
}),
6368
common.DefaultServiceAccount(Component),
6469
)

0 commit comments

Comments
 (0)