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

Effective project handling #13886

Merged
merged 21 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d513418
lxd: Fix network forward deletion entitlement.
markylaing Aug 2, 2024
fd011a2
lxd: Handle effective projects for profiles.
markylaing Aug 7, 2024
a6b53d1
lxd: Handle effective projects for networks.
markylaing Aug 7, 2024
b621203
lxd: Handle effective projects for network zones.
markylaing Aug 7, 2024
6a099ca
lxd/project: Add project helper for getting image effective project.
markylaing Aug 7, 2024
ed4e3ad
lxd: Handle effective projects for images.
markylaing Aug 7, 2024
bcb7d49
lxd/project: Fix lint errors (revive: confusing-results).
markylaing Aug 7, 2024
ebaf86a
lxd/auth: Add comments to Authorizer interface.
markylaing Aug 7, 2024
27c7171
lxd: Always use request project name in authorizer checks.
markylaing Aug 7, 2024
30ee62a
lxd/auth/drivers: Remove effective project check from TLS authorizer.
markylaing Aug 7, 2024
3ce92c0
lxd/auth/drivers: Handle effective projects in the OpenFGA driver.
markylaing Aug 7, 2024
0850ee9
lxd/auth/drivers: Update comments on Authorizer method implementations.
markylaing Aug 7, 2024
a181704
test/includes: Add helper for setting up object storage pools.
markylaing Aug 8, 2024
3eecce3
test/suites: Use storage pool helper in bucket tests.
markylaing Aug 8, 2024
2f9af9f
test/suites: Improve coverage of TLS restrictions tests.
markylaing Aug 8, 2024
6ebeaa0
test/suites: Test project feature interaction with fine-grained auth.
markylaing Aug 8, 2024
651e0e6
doc/explanation: Add note about authorization and project isolation.
markylaing Aug 8, 2024
4d43905
lxd/auth/drivers: Fix linter errors (govet: printf).
markylaing Aug 22, 2024
ecde8f1
lxd: Fix linter errors (govet: printf).
markylaing Aug 22, 2024
081bcbc
lxd: Fix linter error (staticcheck: SA1032).
markylaing Aug 22, 2024
8674666
lxd: Add comment explaining behaviour of events websocket with effect…
markylaing Aug 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions doc/explanation/projects.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ To edit them, you must remove all instances first.
New features that are added in an upgrade are disabled for existing projects.
```

```{important}
In a multi-tenant environment, unless using {ref}`fine-grained-authorization`, all projects should have all features enabled.
Otherwise, clients with {ref}`restricted-tls-certs` are able to create, edit, and delete resources in the default project. This might affect other tenants.

For example, if project "foo" is created and `features.networks` is not set to true, then a restricted client certificate with access to "foo" can view, edit, and delete networks in the default project.

Conversely, if a client's permissions are managed via {ref}`fine-grained-authorization`, resources may be inherited from the default project but access to those resources is not automatically granted.
```

(projects-confined)=
## Confined projects in a multi-user environment

Expand Down
42 changes: 38 additions & 4 deletions lxd/auth/drivers/openfga.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (

"github.com/canonical/lxd/lxd/auth"
"github.com/canonical/lxd/lxd/identity"
"github.com/canonical/lxd/lxd/request"
"github.com/canonical/lxd/shared"
"github.com/canonical/lxd/shared/api"
"github.com/canonical/lxd/shared/entity"
Expand Down Expand Up @@ -112,15 +113,27 @@ func (e *embeddedOpenFGA) load(ctx context.Context, identityCache *identity.Cach
}

// CheckPermission checks whether the user who sent the request has the given entitlement on the given entity using the
// embedded OpenFGA server.
// embedded OpenFGA server. A http.StatusNotFound error is returned when the entity does not exist, or when the entity
// exists but the caller does not have permission to view it. A http.StatusForbidden error is returned if the caller has
// permission to view the entity, but does not have the given entitlement.
//
// Note: Internally we call (openfgav1.OpenFGAServiceServer).Check to implement this. Since our implementation of
// storage.OpenFGADatastore pulls data directly from the database, we need to be careful about the handling of entities
// contained within projects that do not have features enabled. For example, if the given entity URL is for a network in
// project "foo", but project "foo" does not have `features.networks=true`, then we must not use project "foo" in our
// authorization check because this network does not exist in the database. We will always expect the given entity URL
// to contain the request project name, but we expect that request.CtxEffectiveProjectName will be set in the request
// context. The driver will rewrite the project name with the effective project name for the purpose of the authorization
// check, but will not automatically allow "punching through" to the effective (default) project. An administrator can
// allow specific permissions against those entities.
func (e *embeddedOpenFGA) CheckPermission(ctx context.Context, entityURL *api.URL, entitlement auth.Entitlement) error {
logCtx := logger.Ctx{"entity_url": entityURL.String(), "entitlement": entitlement}
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

// Untrusted requests are denied.
if !auth.IsTrusted(ctx) {
return api.StatusErrorf(http.StatusForbidden, http.StatusText(http.StatusForbidden))
return api.StatusErrorf(http.StatusForbidden, "%s", http.StatusText(http.StatusForbidden))
}

isRoot, err := auth.IsServerAdmin(ctx, e.identityCache)
Expand Down Expand Up @@ -175,6 +188,14 @@ func (e *embeddedOpenFGA) CheckPermission(ctx context.Context, entityURL *api.UR
return fmt.Errorf("Authorization driver failed to parse entity URL %q: %w", entityURL.String(), err)
}

// The project in the given URL may be for a project that does not have a feature enabled, in this case the auth check
// will fail because the resource doesn't actually exist in that project. To correct this, we use the effective project from
// the request context if present.
effectiveProject, _ := request.GetCtxValue[string](ctx, request.CtxEffectiveProjectName)
if effectiveProject != "" {
projectName = effectiveProject
}

// Construct the URL in a standardised form (adding the project parameter if it was not present).
entityURL, err = entityType.URL(projectName, location, pathArguments...)
if err != nil {
Expand Down Expand Up @@ -263,13 +284,18 @@ func (e *embeddedOpenFGA) CheckPermission(ctx context.Context, entityURL *api.UR
l.Info("Access denied", logger.Ctx{"http_code": responseCode})
}

return api.StatusErrorf(responseCode, http.StatusText(responseCode))
return api.StatusErrorf(responseCode, "%s", http.StatusText(responseCode))
}

return nil
}

// GetPermissionChecker returns a PermissionChecker using the embedded OpenFGA server.
// GetPermissionChecker returns an auth.PermissionChecker using the embedded OpenFGA server.
//
// Note: As with CheckPermission, we need to be careful about the usage of this function for entity types that may not
// be enabled within a project. For these cases request.CtxEffectiveProjectName must be set in the given context before
// this function is called. The returned auth.PermissionChecker will expect entity URLs to contain the request URL. These
// will be re-written to contain the effective project if set, so that they correspond to the list returned by OpenFGA.
func (e *embeddedOpenFGA) GetPermissionChecker(ctx context.Context, entitlement auth.Entitlement, entityType entity.Type) (auth.PermissionChecker, error) {
logCtx := logger.Ctx{"entity_type": entityType, "entitlement": entitlement}
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
Expand Down Expand Up @@ -404,6 +430,14 @@ func (e *embeddedOpenFGA) GetPermissionChecker(ctx context.Context, entitlement
return false
}

// The project in the given URL may be for a project that does not have a feature enabled, in this case the auth check
// will fail because the resource doesn't actually exist in that project. To correct this, we use the effective project from
// the request context if present.
effectiveProject, _ := request.GetCtxValue[string](ctx, request.CtxEffectiveProjectName)
if effectiveProject != "" {
projectName = effectiveProject
}

standardisedEntityURL, err := entityType.URL(projectName, location, pathArguments...)
if err != nil {
l.Error("Failed to standardise permission checker entity URL", logger.Ctx{"url": entityURL.String(), "err": err})
Expand Down
10 changes: 1 addition & 9 deletions lxd/auth/drivers/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (

"github.com/canonical/lxd/lxd/auth"
"github.com/canonical/lxd/lxd/identity"
"github.com/canonical/lxd/lxd/request"
"github.com/canonical/lxd/shared"
"github.com/canonical/lxd/shared/api"
"github.com/canonical/lxd/shared/entity"
Expand Down Expand Up @@ -44,7 +43,7 @@ func (t *tls) load(ctx context.Context, identityCache *identity.Cache, opts Opts
func (t *tls) CheckPermission(ctx context.Context, entityURL *api.URL, entitlement auth.Entitlement) error {
// Untrusted requests are denied.
if !auth.IsTrusted(ctx) {
return api.StatusErrorf(http.StatusForbidden, http.StatusText(http.StatusForbidden))
return api.StatusErrorf(http.StatusForbidden, "%s", http.StatusText(http.StatusForbidden))
}

isRoot, err := auth.IsServerAdmin(ctx, t.identities)
Expand Down Expand Up @@ -148,8 +147,6 @@ func (t *tls) GetPermissionChecker(ctx context.Context, entitlement auth.Entitle
return allowFunc(false), nil
}

effectiveProject, _ := request.GetCtxValue[string](ctx, request.CtxEffectiveProjectName)

// Filter objects by project.
return func(entityURL *api.URL) bool {
eType, project, _, _, err := entity.ParseURL(entityURL.URL)
Expand All @@ -164,11 +161,6 @@ func (t *tls) GetPermissionChecker(ctx context.Context, entitlement auth.Entitle
return false
}

// If an effective project has been set in the request context. We expect all entities to be in that project.
if effectiveProject != "" {
return project == effectiveProject
}

// Otherwise, check if the project is in the list of allowed projects for the entity.
return shared.ValueInSlice(project, id.Projects)
}, nil
Expand Down
11 changes: 11 additions & 0 deletions lxd/auth/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,20 @@ type PermissionChecker func(entityURL *api.URL) bool

// Authorizer is the primary external API for this package.
type Authorizer interface {
// Driver returns the driver name.
Driver() string

// CheckPermission checks if the caller has the given entitlement on the entity found at the given URL.
//
// Note: When a project does not have a feature enabled, the given URL should contain the request project, and the
// effective project for the entity should be set in the given context as request.CtxEffectiveProjectName.
CheckPermission(ctx context.Context, entityURL *api.URL, entitlement Entitlement) error

// GetPermissionChecker returns a PermissionChecker for a particular entity.Type.
//
// Note: As with CheckPermission, arguments to the returned PermissionChecker should contain the request project for
// the entity. The effective project for the entity must be set in the request context as request.CtxEffectiveProjectName
// *before* the call to GetPermissionChecker.
GetPermissionChecker(ctx context.Context, entitlement Entitlement, entityType entity.Type) (PermissionChecker, error)
}

Expand Down
8 changes: 8 additions & 0 deletions lxd/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ func eventsSocket(s *state.State, r *http.Request, w http.ResponseWriter) error
}
}

// Notes on authorization for events:
// - Checks are currently performed at the project level. Fine-grained auth uses `can_view_events` on the project,
// TLS auth checks if a restricted identity has access to the project against which the event is defined.
// - If project "foo" does not have a particular feature enabled, say 'features.networks', if a network is updated
// via project "foo", no events will be emitted in project "foo" relating to the network. They will only be emitted
// in project "default". In order to get all related events, TLS users must be granted access to the default project,
// fine-grained users can be granted `can_view_events` on the default project. Both must call the events API with
// `all-projects=true`.
var projectPermissionFunc auth.PermissionChecker
if projectName != "" {
err := s.Authorizer.CheckPermission(r.Context(), entity.ProjectURL(projectName), auth.EntitlementCanViewEvents)
Expand Down
Loading
Loading