Skip to content

Commit

Permalink
Effective project handling (#13886)
Browse files Browse the repository at this point in the history
Implements the following:
1. Always sets the effective project when querying entities that have
associated `features.*` project config. This includes:
i. Adding network, network zone, image, and profile specific access
handlers.
ii. For each entity type above, setting details in the request context
to avoid repeated calculation (same pattern as for storage buckets and
volumes).
2. Always uses the request project in calls to
`(Authorizer).CheckPermission` and in calls to the
`auth.PermissionChecker` returned by
`(Authorizer).GetPermissionChecker`.
3. In the TLS driver, we remove effective project handling (we always
expect calls to use the request project, this is what we check in their
allowed project list).
4. In the OpenFGA driver, overwrite the request project with the
effective project in calls to the embedded OpenFGA server. But do not
"punch through" to the default project like with the TLS driver, as
these permissions can be managed by an administrator.
5. Increased test coverage for project features with TLS authorization.
6. Adds tests for handling of project features with fine-grained
authorization.

Closes #13863
  • Loading branch information
tomponline authored Aug 27, 2024
2 parents b485c60 + 8674666 commit e4715fe
Show file tree
Hide file tree
Showing 21 changed files with 1,325 additions and 466 deletions.
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

0 comments on commit e4715fe

Please sign in to comment.