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

auth: Add entitlements to LXD resources if the current identity is fine-grained #14476

Closed

Conversation

gabrielmougard
Copy link
Contributor

@gabrielmougard gabrielmougard commented Nov 16, 2024

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

# Add TLS certificate to LXD (this can be done through LXD UI. Download the .pfx file)
# Create an auth group
lxc auth group create test-group

# Add an new identity to this group ("lxd-ui" is my TLS certificate identifier created by LXD UI)
lxc auth identity group add tls/lxd-ui test-group-1

# Create resources and add permissions to them
lxc auth group permission add test-group-1 instance c1 can_view project=default

# Create a .pfx certificate (with password "1234") and associate it to an auth group and give it some permissions. You can call the API like the following to perform fine-grained calls:
curl --insecure --cert-type P12 --cert lxd-ui-fine-grained-tls.pfx:1234 "https://127.0.0.1:8443/1.0/instances/c1?recursion=1&with-entitlements=true" | jq '.'

The CURL output should give you something like:

{
  "type": "sync",
  "status": "Success",
  "status_code": 200,
  "operation": "",
  "error_code": 0,
  "error": "",
  "metadata": {
    "entitlements": [
      "can_view"
    ],
    "name": "c1",
    "description": "this is a test ",
    "status": "Running",
    "status_code": 103,
    "created_at": "2024-11-12T09:58:54.960586253Z",
    "last_used_at": "2024-11-16T12:15:13.572661467Z",
    "location": "none",
    "type": "container",
    ...
  }
}

@github-actions github-actions bot added Documentation Documentation needs updating API Changes to the REST API labels Nov 16, 2024
@gabrielmougard gabrielmougard force-pushed the feat/auth-dry-run-check branch 3 times, most recently from bb48b39 to fb5a139 Compare November 18, 2024 14:28
@gabrielmougard gabrielmougard marked this pull request as ready for review November 18, 2024 14:43
}

return nil
}
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 very confused by this. There are a few things to address here.

  1. 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.
  2. This is leaking a lot of OpenFGA specific logic from the lxd/db/openfga and lxd/auth/drivers packages. We should try our utmost to keep that logic where it is.
  3. 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).

Copy link
Contributor Author

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

doc/api-extensions.md Outdated Show resolved Hide resolved
doc/api-extensions.md Outdated Show resolved Hide resolved
doc/rest-api.yaml Outdated Show resolved Hide resolved
@gabrielmougard gabrielmougard force-pushed the feat/auth-dry-run-check branch 5 times, most recently from e011a8c to 826ef24 Compare November 19, 2024 15:58
@gabrielmougard gabrielmougard force-pushed the feat/auth-dry-run-check branch from 7609e3c to fe6efde Compare November 19, 2024 17:06
@gabrielmougard gabrielmougard force-pushed the feat/auth-dry-run-check branch from fe6efde to eabf4ad Compare November 19, 2024 17:40
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.

Looking good overall. Just a few tweaks and we'll be nearly there I think.

lxd/db/openfga/openfga.go Outdated Show resolved Hide resolved
lxd/db/openfga/openfga.go Outdated Show resolved Hide resolved
lxd/db/openfga/openfga.go Outdated Show resolved Hide resolved
lxd/db/openfga/openfga.go Outdated Show resolved Hide resolved
lxd/db/openfga/openfga.go Outdated Show resolved Hide resolved
shared/api/auth.go Outdated Show resolved Hide resolved
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
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 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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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...

Copy link
Contributor Author

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..

Copy link
Contributor Author

@gabrielmougard gabrielmougard Nov 20, 2024

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)
}

Copy link
Contributor

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?

Copy link
Contributor

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 :)

lxd/instance_get.go Outdated Show resolved Hide resolved
lxd/instance_get.go Show resolved Hide resolved
lxd/api_project.go Show resolved Hide resolved
@gabrielmougard gabrielmougard force-pushed the feat/auth-dry-run-check branch 5 times, most recently from cba996a to 8e68504 Compare November 20, 2024 17:07
@gabrielmougard gabrielmougard force-pushed the feat/auth-dry-run-check branch 2 times, most recently from 832a775 to 8cc1739 Compare November 25, 2024 16:36
@gabrielmougard gabrielmougard force-pushed the feat/auth-dry-run-check branch 6 times, most recently from 62b6d60 to cd016d8 Compare November 27, 2024 09:34
@gabrielmougard gabrielmougard force-pushed the feat/auth-dry-run-check branch 2 times, most recently from 4ccdcf7 to ff2b6f1 Compare November 28, 2024 09:35
gabrielmougard and others added 5 commits November 29, 2024 15:28
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>
@gabrielmougard gabrielmougard force-pushed the feat/auth-dry-run-check branch from ff2b6f1 to 9ea7340 Compare November 29, 2024 14:45
@tomponline
Copy link
Member

@gabrielmougard closing for now until you can work on this in the new year.

@tomponline tomponline closed this Dec 9, 2024
tomponline added a commit that referenced this pull request Dec 11, 2024
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/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
API Changes to the REST API Documentation Documentation needs updating
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants