-
Notifications
You must be signed in to change notification settings - Fork 935
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
auth: Add entitlements to LXD resources if the current identity is fine-grained #14476
auth: Add entitlements to LXD resources if the current identity is fine-grained #14476
Conversation
bb48b39
to
fb5a139
Compare
} | ||
|
||
return nil | ||
} |
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.
I'm very confused by this. There are a few things to address here.
- We can't bypass calls to OpenFGA by accessing the cache directly. We don't know that the cache contains all the necessary information. We will need to call the OpenFGA server itself to check the entitlements on a per-entity basis. The cache should be read from/written to only from in OpenFGA datastore implementation itself.
- This is leaking a lot of OpenFGA specific logic from the
lxd/db/openfga
andlxd/auth/drivers
packages. We should try our utmost to keep that logic where it is. - Using reflection should be a last resort IMO. If we absolutely require it then we need to have an awful lot of comments to explain why and what it's doing.
For 1, we still need to call the OpenFGA server. I suggest calling CheckPermission
for each entitlement in the auth.EntitlementsByEntityType
map value for the entity type. This will be inefficient when listing entitlements for a list of resources (e.g. GET /1.0/instances?recursion=1&with-entitlements=true
) so I suggest in this calling auth.GetPermissionChecker
with each entitlement in the auth.EntitlementsByEntityType
map.
Alternatively, I suggest adding two methods to the auth.Authorizer
interface to perform the enrichment. One for a single entity which will call Check
on the OpenFGA server, and one for a list of entities that will call ListObjects
. The implementation for the TLS authorizer can return a 501 not implemented. The authorizer itself can return a bad request error if the caller is not fine-grained.
For 3, I suggest creating an interface with method AddEntitlements([]auth.Entitlement)
and implement it for the API types in question (all API types represented by the OpenFGA model).
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. I agree with you on the three points. I'll come up with a new approach.
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.
@markylaing I updated the approach. What do you think now?
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.
The approach looks good :) It might be possible to make the new Authorizer functions more efficient by calling ListObjects
and Check
on the OpenFGA server directly. We can revisit that at a later date though, for now re-using CheckPermission
and GetPermissionChecker
is reasonable.
e011a8c
to
826ef24
Compare
7609e3c
to
fe6efde
Compare
fe6efde
to
eabf4ad
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.
Looking good overall. Just a few tweaks and we'll be nearly there I think.
lxd/auth/types.go
Outdated
AddEntitlements(ctx context.Context, entityType entity.Type, entityURL *api.URL, entity entity.EntityWithEntitlements) error | ||
|
||
// AddEntitlementsToEntities attaches entitlements to multiple entities. | ||
AddEntitlementsToEntities(ctx context.Context, entityType entity.Type, entityURLs []*api.URL, entities []entity.EntityWithEntitlements) error |
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.
I'm not sure on these function signatures. Especially in the second case where the caller must prepare a list of URLs to correspond by index to the list of entities.
It seems odd to me that we're passing in e.g. an api.Instance
, along with it's URL and it's type, both of which can be derived from the fact that it is an api.Instance
.
Do you think it's worth adding two more methods to EntityWithEntitlements
e.g.:
type EntityWithEntitlements interface {
AddEntitlement(auth.Entitlement)
URL() string
EntityType() entity.Type
}
Then these function signatures can become:
AddEntityEntitlements(ctx context.Context, entity entity.EntityWithEntitlements)
AddEntitiesEntitlements(ctx context.Context, entities entity.EntityWithEntitlements)
Let me know what you think.
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.
I like the the idea but I'd prefer to call the URL() string
method EntityURL() string
instead to be more explicit. For example, the URL scheme used for an LXD instance can be defined like:
url := api.NewURL().Path(version.APIVersion, instancePath, instance.Name).Project(instance.Project)
and if instance.Project
is default
then no query param will be added whereas an OpenFGA entity URL will systematically have the project
query param set even if its default
.
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.
@markylaing Actually, I realize this will be a bit more complicated than that:
There is an existing logic for URL generation in the shared/entity
package, but I won't be able to use that from the shared/api
package (circular dependency issues) where my implementations of EntityWithEntitlements
are defined...
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.
@markylaing can't we just move the url.go
file from shared/entity
into the shared/api
package? We'd keep the type.go
in shared/entity
. This way we can reuse the existing <Entity>URL
functions for the API LXD entity that would implement the EntityWithEntitlements
interface. I think this would make the circular dep issues go away..
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 seems difficult. Instead could we do something like:
type EntityWithEntitlements interface {
AddEntitlement(entitlement string)
EntityType() Type
}
type EntityWithEntitlementsAndURL struct {
Entity EntityWithEntitlements
EntityURL *api.URL
}
...
// In the interface
AddEntityEntitlements(ctx context.Context, *entity entity.EntityWithEntitlementsAndURL)
AddEntitiesEntitlements(ctx context.Context, entities []*entity.EntityWithEntitlementsAndURL)
This way, we can set the EntityURL in the lxd
package when calling the AddEntityEntitlements
/ AddEntitiesEntitlements
function without having any issues. What do you think?
Here is an example of calling this interface for enriching an api.Instance
:
err = d.authorizer.AddEntitlements(
r.Context(),
&entity.EntityWithEntitlementsAndURL{
Entity: state.(*api.Instance),
EntityURL: entity.InstanceURL(c.Project().Name, c.Name()),
},
)
if err != nil {
return response.SmartError(err)
}
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.
Can we just define the interface somewhere else? In fact, it is only used internally, so perhaps it is better placed in lxd/auth
? Would that help with the circular dependencies?
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.
I think leave it as it is for now. These are finer details that we can work out before merging. For now it might be worth getting good test coverage and waiting on reviews of the specification and PR from Tom. Then we can discuss together :)
cba996a
to
8e68504
Compare
832a775
to
8cc1739
Compare
62b6d60
to
cd016d8
Compare
4ccdcf7
to
ff2b6f1
Compare
Adds `is_fine_grained` field to `GET /1.0/auth/identities/current` to indicate if the current identity interacting with the LXD API is fine-grained (i.e, associated permissions are managed via group membership) and allow LXD resources to be returned with an `entitlements` field if the current identity is fine-grained and if the GET request to fetch the LXD resources has the `with-entitlements=true` query parameter. Signed-off-by: Gabriel Mougard <gabriel.mougard@canonical.com>
…nsion Signed-off-by: Gabriel Mougard <gabriel.mougard@canonical.com>
…rt permissions Signed-off-by: Gabriel Mougard <gabriel.mougard@canonical.com>
Signed-off-by: Gabriel Mougard <gabriel.mougard@canonical.com>
…parsing. If `(Type).URL`, was called on non-project specific entities with a project parameter it was being set on the URL. For example: ``` TypeServer.URL("default", "") ``` Returned the URL: `/1.0?project=default`. This isn't what we want. Additionally, the project query parameter was not being persisted on `ParseURL` for operations or warnings. Additional unit tests have been added to account for these cases. Signed-off-by: Mark Laing <mark.laing@canonical.com>
…ities/current` endpoint This is needed to let know the client if the currently used identity is fine-grained or not. Signed-off-by: Gabriel Mougard <gabriel.mougard@canonical.com>
This interface should be implement for each LXD resource that can have entitlements attached to it. Signed-off-by: Gabriel Mougard <gabriel.mougard@canonical.com>
… LXD entities Signed-off-by: Gabriel Mougard <gabriel.mougard@canonical.com>
…e `Authorizer` interface These methods enrich the entity/entities with their respective entitlements through calls to the OpenFGA datastore. These functions should be called at the end of a LXD API handler so that the OpenFGA per-request cache can be hit. Signed-off-by: Gabriel Mougard <gabriel.mougard@canonical.com>
Signed-off-by: Gabriel Mougard <gabriel.mougard@canonical.com>
Signed-off-by: Gabriel Mougard <gabriel.mougard@canonical.com>
Signed-off-by: Gabriel Mougard <gabriel.mougard@canonical.com>
Signed-off-by: Gabriel Mougard <gabriel.mougard@canonical.com>
Signed-off-by: Gabriel Mougard <gabriel.mougard@canonical.com>
Signed-off-by: Gabriel Mougard <gabriel.mougard@canonical.com>
Signed-off-by: Gabriel Mougard <gabriel.mougard@canonical.com>
ff2b6f1
to
9ea7340
Compare
@gabrielmougard closing for now until you can work on this in the new year. |
This PR decrease the latency of API calls that are going through the OpenFGA fine-grained authorizer. Thanks to a per-request cache mechanism, we avoid calling the database when the cache key is present. This work has been benchmarked and has been used in the following PR: #14476 [openfga_benchmark.pdf](https://github.com/user-attachments/files/17960500/openfga_benchmark.pdf) The benchmarking script can be found at: https://paste.ubuntu.com/p/WCwsk6gSSK/
JIRA ticket: https://warthogs.atlassian.net/browse/LXD-2208
Specification link: https://docs.google.com/document/d/1GxWV5J57MLrjGEY5RDG7eS86J99A8RMmjKTpn0mEhgY/edit?tab=t.0
How to test
The CURL output should give you something like: