Skip to content

[server] Provide token-based API access #1868

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

Merged
merged 3 commits into from
Sep 18, 2020
Merged

[server] Provide token-based API access #1868

merged 3 commits into from
Sep 18, 2020

Conversation

csweichel
Copy link
Contributor

@csweichel csweichel commented Sep 17, 2020

This PR introduces token-based access to Gitpod's JSON-RPC over Websocket API.
We will use this kind of access for moving functionality to supervisor and integrating IDE's other than Theia.

Token-based Authentication

We already have API tokens in Gitpod: the GitpodToken previously introduced for the GraphQL admin API. This PR re-uses those tokens and introduces a new token type: MACHINE_API_TOKEN == 1. These tokens can be used to authenticate against Gitpod when connecting to the new websocket API endpoint /api/v1. Sending an Authorization: Bearer <token> header will provide access to the API.

Tokens are always associated with a user and by default have the same rights as the user has - on the functions the token is scoped for (see below). To create a new token use the generateNewGitpodToken function. For now there's a bit of a chicken and egg problem: one needs to use the classic session-based authentication to create an API token in the first place. If we ever find that to be a problem (e.g. in combination with new IDE's/supervisor token integration), we could consider producing a token per workspace and making that available as OTS, or allowing the owner token to create specifically scoped tokens.

We are introducing the new /api/v1 API endpoint has different requirements from the old /api/gitpod one:

  • no sessions: because the machine API access is token authenticated we do not need or want sessions. The latter would just cost resources for no benefit.
  • hard authentication: if there's no token, or the token is invalid we want to end the websocket connection early. This is unlike the /api/gitpod endpoint where some operations can happen without user session (e.g. getBranding).
  • API versioning: at the moment the API exposed on /api/v1 isn't meant for public consumption, but sooner than later we'll have such a public API. It's a good idea to implement the versioning pattern now. Having a special endpoint for the dashboard (/api/gitpod) also gives us more control over which API version we want to use in the dashboard.

Scoping

Tokens are constrained along two dimensions: functions and resources. Functions refer to the functions of the Gitpod JSON-RPC API, and resources to various resources within Gitpod. Function scoping is enforced on the RPC interface, resource scoping is enforced throughout the server implementation.

Function Scopes

All tokens must explicitly list the functions they can use, as part of their scopes. E.g. a token with [] as scopes cannot access a single function of the API. Function scopes are written as function:<functionName>, e.g. function:getWorkspace. A token's scopes are defined when the token is created. Note: you can not exceed the scope of your own authentication when creating new tokens, e.g. if your current authentication/token only has function:generateNewGitpodToken and function:getWorkspace, you cannot create a new token with function:startWorkspace.

Resource Scopes

All tokens must explicitly list the resources they can use, as part of their scopes. E.g. a token with [] as scopes cannot access a single resource. Other than the scheme described below, there's a special resource:default scope which grants the same access to resources that the token's owner has. Mixing the default scope with other explicit resource scopes behaves just like mixing other resource scopes: they are additive in what they allow.

Resource scopes consist of three elements:

  • kind: determines the type of resource we're talking about. The list of kinds is defined in resource-access.ts, and includes workspace, workspaceInstance, gitpodToken, and snapshot.
  • subjectID: is the ID of the resource that we want to provide access to. What that ID actually is depends on the kind of resource and is defined here. For example, for a workspace it's the ID of that workspace, same goes for workspace instances, users and snapshots.
  • operations: denote the type of access to a resource and is one of create, update, get, delete.

We encode resource scopes like this: resource:<kind>::<subjectID>::<op1>,<op2>. There's a marshal and unmarshal function available.

Other access-limiting concepts in Gitpod

The resource and function guards are not the first - or only - means of controlling access in Gitpod. Below is a comparison to the existing methods:

  • Roles and Permissions: are per-user (not per token) based permissions that enable/disable entire functional branches of Gitpod. E.g. the admin permission grants access to the Admin dashboard. These permissions are orthogonal to function/resource scoping, in that they limit access beyond that function scoping does. E.g. if a token has the function:adminListWorkspaces scope, the function call will only succeed if the token's user has the ADMIN_WORKSPACES permission.
  • Owner checks: Gitpod has historically done a set of owner checks, e.g. when sharing the workspace we ensured that only the owner can do so. This strategy remains the default strategy. The previous owner checks however have been superseded by the resource guard calls. This way we don't keep the old checks around and can actually support tokens.
  • checkUser/checkUserBlocked: it's actually the GitpodServerImpl that checks if a user is present and if it isn't blocked. This part of the authentication process remains as it is. Using a token for authentication provides the token's owner to the server, hence all operations happen in the name of token owner, as does checkUser and checkUserBlocked. Thus, if a token owner gets blocked, so do all of their tokens.

How to test?

  1. Get yourself some token: the easiest way for now to do that is to manually add one in the database:
insert into d_b_gitpod_token (tokenHash, type, userId, scopes, created) VALUES ("<your-hash>", 1, "<your-gitpod-user-id>", "resource:default,function:getWorkspace,function:generateNewGitpodToken", "foo");

To get the token hash, run

node<<EOF
const crypto = require('crypto');
const hash = (token) => crypto.createHash('sha256').update(token, 'utf8').digest("hex");
console.log(hash("foobar"))
EOF

where foobar is your token.
2. connect to the API endpoint, e.g. using websocat. Notice the bearer header. Replace foobar with whatever your token is:

./websocat  ws://cw-resource-guard.staging.gitpod-dev.com/api/v1 -H "Origin:http://cw-resource-guard.staging.gitpod-dev.com" -H "Authorization:Bearer foobar" --jsonrpc -v
  1. Once websocat is running you can send JSON RPC messages like:
generateNewGitpodToken {"type":1, "scopes": ["resource:default", "function:getWorkspace"]}
getWorkspace "some-workspace-id"
startWorkspace

Caveats

  • this PR does nothing for token expiry and invalidation. Tokens, once created, live until they're manually deleted. We should introduce a time and resource-based expiry mechanism.

Thanks

to @AlexTugarev for the super helpful discussions, listening to my Node/TS rants and help wading through the JSON RPC stack.

Copy link
Member

@akosyakov akosyakov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it works for me besides some leftovers

@csweichel csweichel merged commit 9e52bbd into master Sep 18, 2020
@csweichel csweichel deleted the cw/resource-guard branch September 18, 2020 10:31
@laushinka laushinka mentioned this pull request Nov 11, 2022
32 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants