-
Notifications
You must be signed in to change notification settings - Fork 1.3k
[server] Perform authorization checks for Orgs against spicedb #16207
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
Conversation
dc7717e
to
ff08aae
Compare
started the job as gitpod-build-mp-spicedb-server-checks.6 because the annotations in the pull request description changed |
/hold for dependency on #16197 |
export const ReadOrganizationMetadata = check("user", "organization_metadata_read", "organization"); | ||
export const WriteOrganizationMetadata = check("user", "organization_metadata_write", "organization"); | ||
|
||
export const ReadOrganizationMembers = check("user", "organization_members_read", "organization"); | ||
export const WriteOrganizationMembers = check("user", "organization_members_write", "organization"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are declarative permission checks. The idea is to be able to collate all of these in a single place, and be able to trace their usage.
protected async guardOrganizationOperationWithCentralizedPerms( | ||
orgId: string, | ||
op: OrganizationOperation, | ||
): Promise<CheckResult> { | ||
const user = this.checkUser(); | ||
|
||
const members = await this.teamDB.findMembersByTeam(team.id); | ||
await this.guardAccess({ kind: "team", subject: team, members }, op); | ||
return { team, members }; | ||
switch (op) { | ||
case "org_metadata_read": | ||
return await this.authorizer.check(ReadOrganizationMetadata(user.id, orgId)); | ||
case "org_metadata_write": | ||
return await this.authorizer.check(WriteOrganizationMetadata(user.id, orgId)); | ||
|
||
case "org_members_read": | ||
return await this.authorizer.check(ReadOrganizationMembers(user.id, orgId)); | ||
case "org_members_write": | ||
return await this.authorizer.check(WriteOrganizationMembers(user.id, orgId)); | ||
|
||
default: | ||
return NotPermitted; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This acts as an adapter between the "fine grained ops" defined as strings, and the actual checks. Once we have fully migrated, this adapter won't exist, but it makes the existing code a bit easier to evolve, and reason about.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we going to pass the check constants instead fo the strings? Would be great to move this into the authorization folder as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eventually will go away, once we migrate but for now, they are useful as a way of adapting the behaviour.
@@ -2037,7 +2046,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { | |||
protected async guardTeamOperation( | |||
teamId: string, | |||
op: ResourceAccessOp, | |||
fineGrainedOps: OrganizationOperation[], | |||
fineGrainedOp: OrganizationOperation, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Originally I thought we'd have a list of these, but I've since changed my mind. We should only reference a single operation, which in turn may require multiple different perms (like Inviting team members will require organizaiton_metadata_read
and organization_members_write
)
ff08aae
to
c0fe7d6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really like where this is going!
@@ -2037,7 +2052,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { | |||
protected async guardTeamOperation( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be great to move this logic out of this class into it's own one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, eventually. For now, I'm trying to incorporate it to limit the amount of changes. The nice part of the PermissionChecker
interface is that we could effectively implement the current behaviour (with db) such that it conforms to the PermissionChecker interface, and then split on the two different implementations behind a feature flag.
const team = await this.teamDB.findTeamById(teamId); | ||
if (!team) { | ||
// We return Permission Denied because we don't want to leak the existence, or not of the Organization. | ||
throw new ResponseError(ErrorCodes.PERMISSION_DENIED, `No access to Organization ID: ${teamId}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't seem to be a change from this PR, but I think it is more common to return a not-found error code in such cases (including no permissions but resource exists).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's not a change from this PR (here, it's just thunked).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks commented there
|
||
export const PermissionChecker = Symbol("PermissionChecker"); | ||
|
||
export interface PermissionChecker { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the purpose of introducing an interface for this? Also the naming pattern is a little unusal, as we'd uzse the same name for bot interface and implmentations but just add a word about the implementation to it. E.g. PermissionCheckerSpiceDBImpl
but then again I'm not sure we need an interface at all, especially if the signature contains types from authzed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the guard
layer of APIs should only use a PermissionChecker
. Once that check passes, and we create a resource, we'll want to use a PermissionGranter
. They can both be implemented by the same class (Authorizer
), but the type signatures should be constrained. The goal is to use composition, rather than exposing everything. This makes it easier to actually write stub implementations for tests (if we had any..).
The type does in fact rely on the spicedb
type. Not ideal, but at this stage I wanted to reduce re-definition of types. If we needed to abstract, it would be relatively easy to swap the argument type for a custom one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you, makes sense 👍
protected async guardOrganizationOperationWithCentralizedPerms( | ||
orgId: string, | ||
op: OrganizationOperation, | ||
): Promise<CheckResult> { | ||
const user = this.checkUser(); | ||
|
||
const members = await this.teamDB.findMembersByTeam(team.id); | ||
await this.guardAccess({ kind: "team", subject: team, members }, op); | ||
return { team, members }; | ||
switch (op) { | ||
case "org_metadata_read": | ||
return await this.authorizer.check(ReadOrganizationMetadata(user.id, orgId)); | ||
case "org_metadata_write": | ||
return await this.authorizer.check(WriteOrganizationMetadata(user.id, orgId)); | ||
|
||
case "org_members_read": | ||
return await this.authorizer.check(ReadOrganizationMembers(user.id, orgId)); | ||
case "org_members_write": | ||
return await this.authorizer.check(WriteOrganizationMembers(user.id, orgId)); | ||
|
||
default: | ||
return NotPermitted; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we going to pass the check constants instead fo the strings? Would be great to move this into the authorization folder as well.
/werft run 👍 started the job as gitpod-build-mp-spicedb-server-checks.8 |
c0fe7d6
to
ceb0c9b
Compare
/werft run with-clean-slate-deployment=true 👍 started the job as gitpod-build-mp-spicedb-server-checks.10 |
/unhold |
Description
Behind an experiment, performs authorization checks for Organizaitons against SpiceDB, and reports the outcome. Whether the outcome matches, or not, we always return the result of the existing authorization check.
Effectively, this change allows us to being checking perms against spicedb, without affecting the system, only collecting data.
We do not yet create any relationships against spicedb. As a result, all of these checks will return "NO_PERMISSION" initially. That's desired. We want to have the checks in place, and incrementally start adding these relationships, either from a batch job, or when resources are actually created. This will be done in subsequent PR(s).
Related Issue(s)
How to test
Release Notes
Documentation
Build Options:
Experimental feature to run the build with GitHub Actions (and not in Werft).
leeway-target=components:all
Run Leeway with
--dont-test
Preview Environment Options:
If enabled this will build
install/preview
If enabled this will create the environment on GCE infra
Valid options are
all
,workspace
,webapp
,ide
,jetbrains
,vscode
,ssh