Skip to content

[WIP][fga] WorkspaceService.startWorkspace #18440

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 10 commits into from
Closed
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
6 changes: 5 additions & 1 deletion components/gitpod-db/src/traced-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ export class DBWithTracing<T> {
return async (...args: any[]) => {
// do not try and trace calls with an empty trace context - the callers intention most likely was to omit the trace
// so as to not spam the trace logs
if (!ctx.span) {
// Also, opentracing makes some assumptions about the Span object, so this might fail under some circumstances
function isEmptyObject(obj: object): boolean {
return Object.keys(obj).length === 0;
}
if (!ctx.span || isEmptyObject(ctx.span)) {
return await f.bind(_target)(...args);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ export class PrometheusClientCallMetrics implements IClientCallMetrics {
});
}

dispose(): void {
prometheusClient.register.removeSingleMetric("grpc_client_started_total");
prometheusClient.register.removeSingleMetric("grpc_client_msg_sent_total");
prometheusClient.register.removeSingleMetric("grpc_client_msg_received_total");
prometheusClient.register.removeSingleMetric("grpc_client_handled_total");
prometheusClient.register.removeSingleMetric("grpc_client_handling_seconds");
}

started(labels: IGrpcCallMetricsLabels): void {
this.startedCounter.inc({
grpc_service: labels.service,
Expand Down
2 changes: 2 additions & 0 deletions components/gitpod-protocol/src/util/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { log, LogContext } from "./logging";

export interface TraceContext {
span?: opentracing.Span;
// TODO(gpl) We are missing this method, but won't add right now because of different focus, and it's unclear how we want to use tracing going forward
isDebugIDContainerOnly?: () => boolean;
}
export type TraceContextWithSpan = TraceContext & {
span: opentracing.Span;
Expand Down
4 changes: 2 additions & 2 deletions components/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
"clean": "rimraf dist",
"clean:node": "rimraf node_modules",
"purge": "yarn clean && yarn clean:node && yarn run rimraf yarn.lock",
"test": "cleanup() { echo 'Cleanup started'; yarn stop-services; }; trap cleanup EXIT; yarn test:unit && yarn start-services && yarn test:db",
"test": "cleanup() { echo 'Cleanup started'; yarn stop-services; }; trap cleanup EXIT; yarn test:unit && yarn test:db",
"test:unit": "mocha --opts mocha.opts './**/*.spec.js' --exclude './node_modules/**'",
"test:db": ". $(leeway run components/gitpod-db:db-test-env) && mocha --opts mocha.opts './**/*.spec.db.js' --exclude './node_modules/**'",
"test:db": ". $(leeway run components/gitpod-db:db-test-env) && yarn start-services && mocha --opts mocha.opts './**/*.spec.db.js' --exclude './node_modules/**'",
"start-services": "yarn start-testdb && yarn start-redis && yarn start-spicedb",
"stop-services": "yarn stop-redis && yarn stop-spicedb",
"start-testdb": "if netstat -tuln | grep ':23306 '; then echo 'Mysql is already running.'; else leeway run components/gitpod-db:init-testdb; fi",
Expand Down
11 changes: 6 additions & 5 deletions components/server/src/api/user.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { suite, test } from "@testdeck/mocha";
import { APIUserService } from "./user";
import { Container } from "inversify";
import { testContainer } from "@gitpod/gitpod-db/lib";
import { WorkspaceStarter } from "../workspace/workspace-starter";
import { UserAuthentication } from "../user/user-authentication";
import { BlockUserRequest, BlockUserResponse } from "@gitpod/public-api/lib/gitpod/experimental/v1/user_pb";
import { User } from "@gitpod/gitpod-protocol";
Expand All @@ -18,22 +17,24 @@ import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing";
import { v4 as uuidv4 } from "uuid";
import { ConnectError, Code } from "@bufbuild/connect";
import * as chai from "chai";
import { WorkspaceService } from "../workspace/workspace-service";

const expect = chai.expect;

@suite()
export class APIUserServiceSpec {
private container: Container;
private workspaceStarterMock: WorkspaceStarter = {
private workspaceStarterMock: WorkspaceService = {
stopRunningWorkspacesForUser: async (
ctx: TraceContext,
userID: string,
userId: string,
userIdToStop: string,
reason: string,
policy?: StopWorkspacePolicy,
): Promise<Workspace[]> => {
return [];
},
} as WorkspaceStarter;
} as WorkspaceService;
private userServiceMock: UserAuthentication = {
blockUser: async (targetUserId: string, block: boolean): Promise<User> => {
return {
Expand All @@ -45,7 +46,7 @@ export class APIUserServiceSpec {
async before() {
this.container = testContainer.createChild();

this.container.bind(WorkspaceStarter).toConstantValue(this.workspaceStarterMock);
this.container.bind(WorkspaceService).toConstantValue(this.workspaceStarterMock);
this.container.bind(UserAuthentication).toConstantValue(this.userServiceMock);
this.container.bind(APIUserService).toSelf().inSingletonScope();
}
Expand Down
66 changes: 33 additions & 33 deletions components/server/src/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,11 @@ import {
} from "@gitpod/public-api/lib/gitpod/experimental/v1/user_pb";
import { WorkspaceStarter } from "../workspace/workspace-starter";
import { UserAuthentication } from "../user/user-authentication";
import { validate } from "uuid";
import { StopWorkspacePolicy } from "@gitpod/ws-manager/lib";
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
import { WorkspaceService } from "../workspace/workspace-service";

@injectable()
export class APIUserService implements ServiceImpl<typeof UserServiceInterface> {
@inject(WorkspaceStarter) protected readonly workspaceStarter: WorkspaceStarter;
@inject(WorkspaceStarter) protected readonly workspaceService: WorkspaceService;
@inject(UserAuthentication) protected readonly userService: UserAuthentication;

public async getAuthenticatedUser(req: GetAuthenticatedUserRequest): Promise<GetAuthenticatedUserResponse> {
Expand Down Expand Up @@ -59,39 +57,41 @@ export class APIUserService implements ServiceImpl<typeof UserServiceInterface>
}

public async blockUser(req: BlockUserRequest): Promise<BlockUserResponse> {
const { userId, reason } = req;
throw new ConnectError("unimplemented", Code.Unimplemented);
// TODO(gpl) Had to comment this out because of missing authentication info: Who is executing this?
// const { userId, reason } = req;

if (!userId) {
throw new ConnectError("userId is a required parameter", Code.InvalidArgument);
}
if (!validate(userId)) {
throw new ConnectError("userId must be a valid uuid", Code.InvalidArgument);
}
if (!reason) {
throw new ConnectError("reason is a required parameter", Code.InvalidArgument);
}
// if (!userId) {
// throw new ConnectError("userId is a required parameter", Code.InvalidArgument);
// }
// if (!validate(userId)) {
// throw new ConnectError("userId must be a valid uuid", Code.InvalidArgument);
// }
// if (!reason) {
// throw new ConnectError("reason is a required parameter", Code.InvalidArgument);
// }

// TODO: Once connect-node supports middlewares, lift the tracing into the middleware.
const trace = {};
await this.userService.blockUser(userId, true);
log.info(`Blocked user ${userId}.`, {
userId,
reason,
});
// // TODO: Once connect-node supports middlewares, lift the tracing into the middleware.
// const trace = {};
// await this.userService.blockUser(userId, true);
// log.info(`Blocked user ${userId}.`, {
// userId,
// reason,
// });

const stoppedWorkspaces = await this.workspaceStarter.stopRunningWorkspacesForUser(
trace,
userId,
reason,
StopWorkspacePolicy.IMMEDIATELY,
);
// const stoppedWorkspaces = await this.workspaceService.stopRunningWorkspacesForUser(
// trace,
// userId,
// reason,
// StopWorkspacePolicy.IMMEDIATELY,
// );

log.info(`Stopped ${stoppedWorkspaces.length} workspaces in response to BlockUser.`, {
userId,
reason,
workspaceIds: stoppedWorkspaces.map((w) => w.id),
});
// log.info(`Stopped ${stoppedWorkspaces.length} workspaces in response to BlockUser.`, {
// userId,
// reason,
// workspaceIds: stoppedWorkspaces.map((w) => w.id),
// });

return new BlockUserResponse();
// return new BlockUserResponse();
}
}
72 changes: 51 additions & 21 deletions components/server/src/authorization/authorizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
Relation,
ResourceType,
UserPermission,
WorkspacePermission,
rel,
} from "./definitions";
import { SpiceDBAuthorizer } from "./spicedb-authorizer";
Expand Down Expand Up @@ -101,11 +102,11 @@ export class Authorizer {
);
}

async hasPermissionOnUser(userId: string, permission: UserPermission, userResourceId: string): Promise<boolean> {
async hasPermissionOnUser(userId: string, permission: UserPermission, resourceUserId: string): Promise<boolean> {
const req = v1.CheckPermissionRequest.create({
subject: subject("user", userId),
permission,
resource: object("user", userResourceId),
resource: object("user", resourceUserId),
consistency,
});

Expand All @@ -126,6 +127,35 @@ export class Authorizer {
);
}

async hasPermissionOnWorkspace(
userId: string,
permission: WorkspacePermission,
workspaceId: string,
): Promise<boolean> {
const req = v1.CheckPermissionRequest.create({
subject: subject("user", userId),
permission,
resource: object("workspace", workspaceId),
consistency,
});

return this.authorizer.check(req, { userId });
}

async checkPermissionOnWorkspace(userId: string, permission: WorkspacePermission, workspaceId: string) {
if (await this.hasPermissionOnWorkspace(userId, permission, workspaceId)) {
return;
}
if ("read_info" === permission || !(await this.hasPermissionOnWorkspace(userId, "read_info", workspaceId))) {
throw new ApplicationError(ErrorCodes.NOT_FOUND, `Workspace ${workspaceId} not found.`);
}

throw new ApplicationError(
ErrorCodes.PERMISSION_DENIED,
`You do not have ${permission} on workspace ${workspaceId}`,
);
}

// write operations below

public async removeAllRelationships(type: ResourceType, id: string) {
Expand Down Expand Up @@ -158,7 +188,7 @@ export class Authorizer {

async addUser(userId: string, owningOrgId?: string) {
await this.authorizer.writeRelationships(
set(rel.user(userId).self.user(userId)), //
set(rel.user(userId).self.user(userId)),
set(
owningOrgId
? rel.user(userId).organization.organization(owningOrgId)
Expand Down Expand Up @@ -186,15 +216,11 @@ export class Authorizer {
}

async addProjectToOrg(orgID: string, projectID: string): Promise<void> {
await this.authorizer.writeRelationships(
set(rel.project(projectID).org.organization(orgID)), //
);
await this.authorizer.writeRelationships(set(rel.project(projectID).org.organization(orgID)));
}

async removeProjectFromOrg(orgID: string, projectID: string): Promise<void> {
await this.authorizer.writeRelationships(
remove(rel.project(projectID).org.organization(orgID)), //
);
await this.authorizer.writeRelationships(remove(rel.project(projectID).org.organization(orgID)));
}

async addOrganization(org: Organization, members: TeamMemberInfo[], projects: Project[]): Promise<void> {
Expand All @@ -206,32 +232,36 @@ export class Authorizer {
await this.addProjectToOrg(org.id, project.id);
}

await this.authorizer.writeRelationships(
set(rel.organization(org.id).installation.installation), //
);
await this.authorizer.writeRelationships(set(rel.organization(org.id).installation.installation));
}

async addInstallationMemberRole(userID: string) {
await this.authorizer.writeRelationships(
set(rel.installation.member.user(userID)), //
);
await this.authorizer.writeRelationships(set(rel.installation.member.user(userID)));
}

async removeInstallationMemberRole(userID: string) {
await this.authorizer.writeRelationships(
remove(rel.installation.member.user(userID)), //
);
await this.authorizer.writeRelationships(remove(rel.installation.member.user(userID)));
}

async addInstallationAdminRole(userID: string) {
await this.authorizer.writeRelationships(set(rel.installation.admin.user(userID)));
}

async removeInstallationAdminRole(userID: string) {
await this.authorizer.writeRelationships(remove(rel.installation.admin.user(userID)));
}

async createWorkspaceInOrg(orgID: string, userID: string, workspaceID: string): Promise<void> {
await this.authorizer.writeRelationships(
set(rel.installation.admin.user(userID)), //
set(rel.workspace(workspaceID).org.organization(orgID)),
set(rel.workspace(workspaceID).owner.user(userID)),
);
}

async removeInstallationAdminRole(userID: string) {
async deleteWorkspaceFromOrg(orgID: string, userID: string, workspaceID: string): Promise<void> {
await this.authorizer.writeRelationships(
remove(rel.installation.admin.user(userID)), //
remove(rel.workspace(workspaceID).org.organization(orgID)),
remove(rel.workspace(workspaceID).owner.user(userID)),
);
}

Expand Down
Loading