Skip to content
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

Add ReBAC authorization with OpenFGA #11905

Closed

Conversation

monstermunchkin
Copy link
Contributor

lxd/openfga/model.go Outdated Show resolved Hide resolved
@monstermunchkin monstermunchkin force-pushed the features/openfga branch 4 times, most recently from 939ccc4 to f03eda4 Compare July 3, 2023 09:48
Copy link
Contributor

@markylaing markylaing left a comment

Choose a reason for hiding this comment

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

Looks good. I've left a few comments on things to double check.

One thing I've noticed is that a few handlers have a userHasPermission(fmt.Sprintf("%s_%s", projectName, resourceName)). I think these might be better as first class functions in the openfga package since the handler shouldn't necessarily know about the naming conventions for FGA resource/relation names.

Makefile Outdated Show resolved Hide resolved
lxd/openfga/client.go Outdated Show resolved Hide resolved
@@ -268,6 +268,11 @@ func (c *Config) ClusterHealingThreshold() time.Duration {
return healingThreshold
}

// OpenFGA returns all OpenFGA settings need to interact with an OpenFGA server.
func (c *Config) OpenFGA() (string, string, string) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not usually a fan of named returns but here it might be useful to know the order of the returns since they are all of the same type.

lxd/openfga.go Outdated Show resolved Hide resolved
lxd/images.go Outdated Show resolved Hide resolved
lxd/network_acls.go Outdated Show resolved Hide resolved
lxd/network_zones.go Outdated Show resolved Hide resolved
@monstermunchkin monstermunchkin force-pushed the features/openfga branch 4 times, most recently from 47d4734 to 6f1ba9f Compare July 6, 2023 12:53
@tomponline
Copy link
Member

@roosterfish @MusicDin hey if you chaps have chance please can you take a look at this before I do a review. Thanks

Copy link
Contributor

@markylaing markylaing left a comment

Choose a reason for hiding this comment

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

I think this looks good, I just have a question about how we want to structure access handling. We already have access handlers like e.g. allowProjectPermission("containers", "view") which check RBAC permissions, so I wonder if we can perform the ReBAC check there instead of adding the ReBACAccessHandler to endpoint actions.

lxd/daemon.go Outdated
return resp
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Does this mean if a ReBACAccessHandler is defined for the endpoint action it is always called, regardless of whether or not we have an openfga client configured?

Also, if ReBAC is configured, do we still need to call the standard AccessHandler?

I wonder if a better pattern would be to remove the ReBACAccessHandler and perform the check inside the AccessHandler instead if the client is set. We could have utils for building access handlers e.g. access.NewHandler().Authenticated().AllowedReBACRelations("admin", "images_view").AllowedCandidRoles("admin", "images_view") or similar.

Copy link
Member

Choose a reason for hiding this comment

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

@monstermunchkin I too am a bit confused by the ramifications of this and am thinking the same as @markylaing above. It seems odd to have 2 access handlers defined for a single API route. Can we not check ReBAC inside the currently access handler?

@monstermunchkin
Copy link
Contributor Author

We already have access handlers like e.g. allowProjectPermission("containers", "view") which check RBAC permissions, so I wonder if we can perform the ReBAC check there instead of adding the ReBACAccessHandler to endpoint actions.

The problem is that RBAC isn't as fine-grained as ReBAC. For example, ReBAC has separate handlers for networks, network ACLs, and network zones, whereas RBAC only does networks which includes ACLs and zones. If we changed that in the current access handler, it would break things as RBAC doesn't know of network ACLs.

Also, for ReBAC, some permission handling is done inside of the actual handler. For example when listing instances, a user should only see the instances which they are allowed to view. RBAC would just show all instances.

So, for simplicity and backward compatibility I believe it's best to add ReBACAccessHandler.

@markylaing
Copy link
Contributor

The problem is that RBAC isn't as fine-grained as ReBAC. For example, ReBAC has separate handlers for networks, network ACLs, and network zones, whereas RBAC only does networks which includes ACLs and zones. If we changed that in the current access handler, it would break things as RBAC doesn't know of network ACLs.

I'm not suggesting we mangle permissions to make RBAC and ReBAC equivalent, I'm suggesting that we have a configurable AccessHandler that we can set both RBAC and ReBAC permissions against. You could then have something like access.NewHandler().RBAC("admin", "network-manager").ReBAC("admin", "network_acl#manager") (we have a similar pattern in LXD Cloud). Then if the ReBAC client is set, check against that, falling back to RBAC.

So, for simplicity and backward compatibility I believe it's best to add ReBACAccessHandler.

Yeah I guess in practice my suggestion will be more difficult to implement and there is more risk of breaking existing access control, so I'm happy to keep the ReBACAccessHandler 👍

@tomponline
Copy link
Member

@monstermunchkin before I review this more thoroughly, it looks like there is no docs update for this PR?
Please can you add something to explain how to use it. @ru-fu any suggestions on what/where to add?

@ru-fu
Copy link
Contributor

ru-fu commented Jul 14, 2023

@monstermunchkin before I review this more thoroughly, it looks like there is no docs update for this PR? Please can you add something to explain how to use it. @ru-fu any suggestions on what/where to add?

https://documentation.ubuntu.com/lxd/en/latest/authentication/ will need some updates in the "OpenID Connect authentication" section and a new section similar to the RBAC section, describing what you can do and what roles there are.
https://documentation.ubuntu.com/lxd/en/latest/howto/projects_confine/ needs instructions for how to set this up. (Ideally a bit more detailed than the RBAC section - we kept that one short because we knew it's going away.)

@monstermunchkin monstermunchkin force-pushed the features/openfga branch 2 times, most recently from 17c9255 to b376e2a Compare July 14, 2023 14:01
func (c *Client) HasPermission(user string, relation Relation, object string) bool {
fullUser := UserObject(user)

logger.Debug("Checking ReBAC permission", logger.Ctx{"user": fullUser, "relation": relation, "object": object})
Copy link
Member

Choose a reason for hiding this comment

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

Can you create a new logger here using l := logger.AddContext( logger.Ctx{"user": fullUser, "relation": relation, "object": object}), then you can use l.Debug() and include all the context each time, including on line 86.


data, _, err := c.apiClient.OpenFgaApi.Check(context.Background()).Body(body).Execute()
if err != nil {
logger.Debug("Failed checking permissions", logger.Ctx{"err": err})
Copy link
Member

Choose a reason for hiding this comment

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

Please use l.Debug here

fullUser := fmt.Sprintf("user:%s", user)
body := openfga.NewListObjectsRequest(string(objectType), string(relation), fullUser)

objectResponse, _, err := c.apiClient.OpenFgaApi.ListObjects(context.Background()).Body(*body).Execute()
Copy link
Member

Choose a reason for hiding this comment

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

For new code we should be accepting context arguments not using context.Background.

return objects, nil
}

func (c *Client) AddTuple(user string, relation Relation, object string) error {
Copy link
Member

Choose a reason for hiding this comment

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

Missing doc block saying what it does.

},
AuthorizationModelId: openfga.PtrString(c.authModelID)}

_, _, err := c.apiClient.OpenFgaApi.Write(context.Background()).Body(body).Execute()
Copy link
Member

Choose a reason for hiding this comment

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

Needs to accept a context argument.

},
AuthorizationModelId: openfga.PtrString(c.authModelID)}

_, _, err := c.apiClient.OpenFgaApi.Write(context.Background()).Body(body).Execute()
Copy link
Member

Choose a reason for hiding this comment

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

This function need to accept a context argument.

}

// ListObjects returns the a list of objects the user is related to. It also returns whether the user is an admin.
func (c *Client) ListObjects(user string, relation Relation, objectType ObjectType) ([]string, error) {
Copy link
Member

Choose a reason for hiding this comment

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

It also returns whether the user is an admin.

How does it do that?

return nil
}

func (c *Client) DeleteTuple(user string, relation Relation, object string) error {
Copy link
Member

Choose a reason for hiding this comment

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

Missing doc block

@@ -0,0 +1,5 @@
package openfga

// Code generated by Makefile; DO NOT EDIT.
Copy link
Member

Choose a reason for hiding this comment

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

Could we change this to "Code generated by make update-openfga` so its clear what needs to be run to update it?

@@ -0,0 +1,61 @@
model
Copy link
Member

Choose a reason for hiding this comment

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

Can this file have comments explaining what it is and what it defines?

Copy link
Member

Choose a reason for hiding this comment

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

Also, in the future if we add new entity types to LXD, will this file need to be updated?
What are the ramifications if it isn't does the new resource become inaccessible to these users or does it "fail open"?

"fmt"
)

func GroupObject(groupName string) string {
Copy link
Member

Choose a reason for hiding this comment

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

Please can we change these functions to start with Object so they are grouped alphabetically by what type of info they return?

Copy link
Member

Choose a reason for hiding this comment

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

Like you've done with the Relation constants.

@tomponline
Copy link
Member

This can be rebased now please

Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
This adds the update-openfga target which generates a go source file
containing the JSON representation of the OpenFGA authorization model.

Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
@tomponline
Copy link
Member

@monstermunchkin @markylaing shall we close this in preference to #12252 ?

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.

4 participants