diff --git a/doc/explanation/projects.md b/doc/explanation/projects.md index 65d291b4c362..75d448416b43 100644 --- a/doc/explanation/projects.md +++ b/doc/explanation/projects.md @@ -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 diff --git a/lxd/auth/drivers/openfga.go b/lxd/auth/drivers/openfga.go index 4002e83f25b4..1bfef2064955 100644 --- a/lxd/auth/drivers/openfga.go +++ b/lxd/auth/drivers/openfga.go @@ -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" @@ -112,7 +113,19 @@ 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) @@ -120,7 +133,7 @@ func (e *embeddedOpenFGA) CheckPermission(ctx context.Context, entityURL *api.UR // 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) @@ -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 { @@ -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) @@ -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}) diff --git a/lxd/auth/drivers/tls.go b/lxd/auth/drivers/tls.go index 93d74edfae3a..237b9d972de8 100644 --- a/lxd/auth/drivers/tls.go +++ b/lxd/auth/drivers/tls.go @@ -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" @@ -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) @@ -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) @@ -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 diff --git a/lxd/auth/types.go b/lxd/auth/types.go index ab6f2d80a033..8ededc2d83a3 100644 --- a/lxd/auth/types.go +++ b/lxd/auth/types.go @@ -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) } diff --git a/lxd/events.go b/lxd/events.go index f91d08b4faea..1a814b5fd8b0 100644 --- a/lxd/events.go +++ b/lxd/events.go @@ -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) diff --git a/lxd/images.go b/lxd/images.go index 341ae033a6d6..09528d6edaa8 100644 --- a/lxd/images.go +++ b/lxd/images.go @@ -66,46 +66,147 @@ var imagesCmd = APIEndpoint{ var imageCmd = APIEndpoint{ Path: "images/{fingerprint}", - Delete: APIEndpointAction{Handler: imageDelete, AccessHandler: allowPermission(entity.TypeImage, auth.EntitlementCanDelete, "fingerprint")}, + Delete: APIEndpointAction{Handler: imageDelete, AccessHandler: imageAccessHandler(auth.EntitlementCanDelete)}, Get: APIEndpointAction{Handler: imageGet, AllowUntrusted: true}, - Patch: APIEndpointAction{Handler: imagePatch, AccessHandler: allowPermission(entity.TypeImage, auth.EntitlementCanEdit, "fingerprint")}, - Put: APIEndpointAction{Handler: imagePut, AccessHandler: allowPermission(entity.TypeImage, auth.EntitlementCanEdit, "fingerprint")}, + Patch: APIEndpointAction{Handler: imagePatch, AccessHandler: imageAccessHandler(auth.EntitlementCanEdit)}, + Put: APIEndpointAction{Handler: imagePut, AccessHandler: imageAccessHandler(auth.EntitlementCanEdit)}, } var imageExportCmd = APIEndpoint{ Path: "images/{fingerprint}/export", Get: APIEndpointAction{Handler: imageExport, AllowUntrusted: true}, - Post: APIEndpointAction{Handler: imageExportPost, AccessHandler: allowPermission(entity.TypeImage, auth.EntitlementCanEdit, "fingerprint")}, + Post: APIEndpointAction{Handler: imageExportPost, AccessHandler: imageAccessHandler(auth.EntitlementCanEdit)}, } var imageSecretCmd = APIEndpoint{ Path: "images/{fingerprint}/secret", - Post: APIEndpointAction{Handler: imageSecret, AccessHandler: allowPermission(entity.TypeImage, auth.EntitlementCanEdit, "fingerprint")}, + Post: APIEndpointAction{Handler: imageSecret, AccessHandler: imageAccessHandler(auth.EntitlementCanEdit)}, } var imageRefreshCmd = APIEndpoint{ Path: "images/{fingerprint}/refresh", - Post: APIEndpointAction{Handler: imageRefresh, AccessHandler: allowPermission(entity.TypeImage, auth.EntitlementCanEdit, "fingerprint")}, + Post: APIEndpointAction{Handler: imageRefresh, AccessHandler: imageAccessHandler(auth.EntitlementCanEdit)}, } var imageAliasesCmd = APIEndpoint{ Path: "images/aliases", - Get: APIEndpointAction{Handler: imageAliasesGet, AccessHandler: allowAuthenticated}, + Get: APIEndpointAction{Handler: imageAliasesGet, AccessHandler: allowProjectResourceList}, Post: APIEndpointAction{Handler: imageAliasesPost, AccessHandler: allowPermission(entity.TypeProject, auth.EntitlementCanCreateImageAliases)}, } var imageAliasCmd = APIEndpoint{ Path: "images/aliases/{name:.*}", - Delete: APIEndpointAction{Handler: imageAliasDelete, AccessHandler: allowPermission(entity.TypeImageAlias, auth.EntitlementCanDelete, "name")}, + Delete: APIEndpointAction{Handler: imageAliasDelete, AccessHandler: imageAliasAccessHandler(auth.EntitlementCanDelete)}, Get: APIEndpointAction{Handler: imageAliasGet, AllowUntrusted: true}, - Patch: APIEndpointAction{Handler: imageAliasPatch, AccessHandler: allowPermission(entity.TypeImageAlias, auth.EntitlementCanEdit, "name")}, - Post: APIEndpointAction{Handler: imageAliasPost, AccessHandler: allowPermission(entity.TypeImageAlias, auth.EntitlementCanEdit, "name")}, - Put: APIEndpointAction{Handler: imageAliasPut, AccessHandler: allowPermission(entity.TypeImageAlias, auth.EntitlementCanEdit, "name")}, + Patch: APIEndpointAction{Handler: imageAliasPatch, AccessHandler: imageAliasAccessHandler(auth.EntitlementCanEdit)}, + Post: APIEndpointAction{Handler: imageAliasPost, AccessHandler: imageAliasAccessHandler(auth.EntitlementCanEdit)}, + Put: APIEndpointAction{Handler: imageAliasPut, AccessHandler: imageAliasAccessHandler(auth.EntitlementCanEdit)}, +} + +const ctxImageDetails request.CtxKey = "image-details" + +// imageDetails contains fields that are determined prior to the access check. This is set in the request context when +// addImageDetailsToRequestContext is called. +type imageDetails struct { + imageFingerprintPrefix string + imageID int + image api.Image +} + +// addImageDetailsToRequestContext sets request.CtxEffectiveProjectName (string) and ctxImageDetails (imageDetails) +// in the request context. +func addImageDetailsToRequestContext(s *state.State, r *http.Request) error { + imageFingerprintPrefix, err := url.PathUnescape(mux.Vars(r)["fingerprint"]) + if err != nil { + return err + } + + requestProjectName := request.ProjectParam(r) + effectiveProjectName := requestProjectName + var imageID int + var image *api.Image + err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { + effectiveProjectName, err = projectutils.ImageProject(ctx, tx.Tx(), requestProjectName) + if err != nil { + return err + } + + imageID, image, err = tx.GetImageByFingerprintPrefix(ctx, imageFingerprintPrefix, dbCluster.ImageFilter{Project: &requestProjectName}) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return fmt.Errorf("Failed to check project %q image feature: %w", requestProjectName, err) + } + + request.SetCtxValue(r, request.CtxEffectiveProjectName, effectiveProjectName) + request.SetCtxValue(r, ctxImageDetails, imageDetails{ + imageFingerprintPrefix: imageFingerprintPrefix, + imageID: imageID, + image: *image, + }) + + return nil +} + +func imageAccessHandler(entitlement auth.Entitlement) func(d *Daemon, r *http.Request) response.Response { + return func(d *Daemon, r *http.Request) response.Response { + s := d.State() + err := addImageDetailsToRequestContext(s, r) + if err != nil { + return response.SmartError(err) + } + + details, err := request.GetCtxValue[imageDetails](r.Context(), ctxImageDetails) + if err != nil { + return response.SmartError(err) + } + + err = s.Authorizer.CheckPermission(r.Context(), entity.ImageURL(request.ProjectParam(r), details.image.Fingerprint), entitlement) + if err != nil { + return response.SmartError(err) + } + + return response.EmptySyncResponse + } +} + +func imageAliasAccessHandler(entitlement auth.Entitlement) func(d *Daemon, r *http.Request) response.Response { + return func(d *Daemon, r *http.Request) response.Response { + imageAliasName, err := url.PathUnescape(mux.Vars(r)["name"]) + if err != nil { + return response.SmartError(err) + } + + requestProjectName := request.ProjectParam(r) + var effectiveProjectName string + s := d.State() + err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { + effectiveProjectName, err = projectutils.ImageProject(ctx, tx.Tx(), requestProjectName) + return err + }) + if err != nil && api.StatusErrorCheck(err, http.StatusNotFound) { + return response.NotFound(nil) + } else if err != nil { + return response.SmartError(err) + } + + request.SetCtxValue(r, request.CtxEffectiveProjectName, effectiveProjectName) + err = s.Authorizer.CheckPermission(r.Context(), entity.ImageAliasURL(requestProjectName, imageAliasName), entitlement) + if err != nil { + return response.SmartError(err) + } + + return response.EmptySyncResponse + } } /* @@ -1615,16 +1716,9 @@ func imagesGet(d *Daemon, r *http.Request) response.Response { s := d.State() var effectiveProjectName string err := s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - hasImages, err := dbCluster.ProjectHasImages(ctx, tx.Tx(), projectName) - if err != nil { - return err - } - - if !hasImages { - effectiveProjectName = api.ProjectDefaultName - } - - return nil + var err error + effectiveProjectName, err = projectutils.ImageProject(ctx, tx.Tx(), projectName) + return err }) if err != nil { return response.SmartError(err) @@ -2655,21 +2749,7 @@ func imageDelete(d *Daemon, r *http.Request) response.Response { projectName := request.ProjectParam(r) - fingerprint, err := url.PathUnescape(mux.Vars(r)["fingerprint"]) - if err != nil { - return response.SmartError(err) - } - - var imgID int - var imgInfo *api.Image - - err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { - // Use the fingerprint we received in a LIKE query and use the full - // fingerprint we receive from the database in all further queries. - imgID, imgInfo, err = tx.GetImage(ctx, fingerprint, dbCluster.ImageFilter{Project: &projectName}) - - return err - }) + details, err := request.GetCtxValue[imageDetails](r.Context(), ctxImageDetails) if err != nil { return response.SmartError(err) } @@ -2677,7 +2757,7 @@ func imageDelete(d *Daemon, r *http.Request) response.Response { do := func(op *operations.Operation) error { // Lock this operation to ensure that concurrent image operations don't conflict. // Other operations will wait for this one to finish. - unlock, err := imageOperationLock(imgInfo.Fingerprint) + unlock, err := imageOperationLock(details.image.Fingerprint) if err != nil { return err } @@ -2689,7 +2769,7 @@ func imageDelete(d *Daemon, r *http.Request) response.Response { err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { // Check image still exists and another request hasn't removed it since we resolved the image // fingerprint above. - exist, err = tx.ImageExists(ctx, projectName, imgInfo.Fingerprint) + exist, err = tx.ImageExists(ctx, projectName, details.image.Fingerprint) return err }) @@ -2709,13 +2789,13 @@ func imageDelete(d *Daemon, r *http.Request) response.Response { // referenced by other projects. In that case we don't want to // physically delete it just yet, but just to remove the // relevant database entry. - referenced, err = tx.ImageIsReferencedByOtherProjects(ctx, projectName, imgInfo.Fingerprint) + referenced, err = tx.ImageIsReferencedByOtherProjects(ctx, projectName, details.image.Fingerprint) if err != nil { return err } if referenced { - err = tx.DeleteImage(ctx, imgID) + err = tx.DeleteImage(ctx, details.imageID) if err != nil { return fmt.Errorf("Error deleting image info from the database: %w", err) } @@ -2738,7 +2818,7 @@ func imageDelete(d *Daemon, r *http.Request) response.Response { } err = notifier(func(client lxd.InstanceServer) error { - op, err := client.UseProject(projectName).DeleteImage(imgInfo.Fingerprint) + op, err := client.UseProject(projectName).DeleteImage(details.image.Fingerprint) if err != nil { return fmt.Errorf("Failed to request to delete image from peer node: %w", err) } @@ -2760,7 +2840,7 @@ func imageDelete(d *Daemon, r *http.Request) response.Response { err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { // Delete the pool volumes. - poolIDs, err = tx.GetPoolsWithImage(ctx, imgInfo.Fingerprint) + poolIDs, err = tx.GetPoolsWithImage(ctx, details.image.Fingerprint) if err != nil { return err } @@ -2779,14 +2859,14 @@ func imageDelete(d *Daemon, r *http.Request) response.Response { for _, poolName := range poolNames { pool, err := storagePools.LoadByName(s, poolName) if err != nil { - return fmt.Errorf("Error loading storage pool %q to delete image %q: %w", poolName, imgInfo.Fingerprint, err) + return fmt.Errorf("Error loading storage pool %q to delete image %q: %w", poolName, details.image.Fingerprint, err) } // Only perform the deletion of remote volumes on the server handling the request. if !isClusterNotification(r) || !pool.Driver().Info().Remote { - err = pool.DeleteImage(imgInfo.Fingerprint, op) + err = pool.DeleteImage(details.image.Fingerprint, op) if err != nil { - return fmt.Errorf("Error deleting image %q from storage pool %q: %w", imgInfo.Fingerprint, pool.Name(), err) + return fmt.Errorf("Error deleting image %q from storage pool %q: %w", details.image.Fingerprint, pool.Name(), err) } } } @@ -2794,7 +2874,7 @@ func imageDelete(d *Daemon, r *http.Request) response.Response { // Remove the database entry. if !isClusterNotification(r) { err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { - return tx.DeleteImage(ctx, imgID) + return tx.DeleteImage(ctx, details.imageID) }) if err != nil { return fmt.Errorf("Error deleting image info from the database: %w", err) @@ -2802,15 +2882,15 @@ func imageDelete(d *Daemon, r *http.Request) response.Response { } // Remove main image file from disk. - imageDeleteFromDisk(imgInfo.Fingerprint) + imageDeleteFromDisk(details.image.Fingerprint) - s.Events.SendLifecycle(projectName, lifecycle.ImageDeleted.Event(imgInfo.Fingerprint, projectName, op.Requestor(), nil)) + s.Events.SendLifecycle(projectName, lifecycle.ImageDeleted.Event(details.image.Fingerprint, projectName, op.Requestor(), nil)) return nil } resources := map[string][]api.URL{} - resources["images"] = []api.URL{*api.NewURL().Path(version.APIVersion, "images", imgInfo.Fingerprint)} + resources["images"] = []api.URL{*api.NewURL().Path(version.APIVersion, "images", details.image.Fingerprint)} op, err := operations.OperationCreate(s, projectName, operations.OperationClassTask, operationtype.ImageDelete, resources, nil, do, nil, nil, r) if err != nil { @@ -3001,7 +3081,13 @@ func imageGet(d *Daemon, r *http.Request) response.Response { // Get the image. We need to do this before the permission check because the URL in the permission check will not // work with partial fingerprints. var info *api.Image + effectiveProjectName := projectName err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { + effectiveProjectName, err = projectutils.ImageProject(ctx, tx.Tx(), projectName) + if err != nil { + return err + } + info, err = doImageGet(ctx, tx, projectName, fingerprint, publicOnly) if err != nil { return err @@ -3040,6 +3126,7 @@ func imageGet(d *Daemon, r *http.Request) response.Response { userCanViewImage = true } else { // Otherwise perform an access check with the full image fingerprint. + request.SetCtxValue(r, request.CtxEffectiveProjectName, effectiveProjectName) err = s.Authorizer.CheckPermission(r.Context(), entity.ImageURL(projectName, info.Fingerprint), auth.EntitlementCanView) if err != nil && !auth.IsDeniedError(err) { return response.SmartError(err) @@ -3097,25 +3184,13 @@ func imagePut(d *Daemon, r *http.Request) response.Response { // Get current value projectName := request.ProjectParam(r) - fingerprint, err := url.PathUnescape(mux.Vars(r)["fingerprint"]) - if err != nil { - return response.SmartError(err) - } - - var id int - var info *api.Image - - err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { - id, info, err = tx.GetImage(ctx, fingerprint, dbCluster.ImageFilter{Project: &projectName}) - - return err - }) + details, err := request.GetCtxValue[imageDetails](r.Context(), ctxImageDetails) if err != nil { return response.SmartError(err) } // Validate ETag - etag := []any{info.Public, info.AutoUpdate, info.Properties} + etag := []any{details.image.Public, details.image.AutoUpdate, details.image.Properties} err = util.EtagCheck(r, etag) if err != nil { return response.PreconditionFailed(err) @@ -3129,7 +3204,7 @@ func imagePut(d *Daemon, r *http.Request) response.Response { // Get ExpiresAt if !req.ExpiresAt.IsZero() { - info.ExpiresAt = req.ExpiresAt + details.image.ExpiresAt = req.ExpiresAt } // Get profile IDs @@ -3151,7 +3226,7 @@ func imagePut(d *Daemon, r *http.Request) response.Response { profileIDs[i] = profileID } - return tx.UpdateImage(ctx, id, info.Filename, info.Size, req.Public, req.AutoUpdate, info.Architecture, info.CreatedAt, info.ExpiresAt, req.Properties, projectName, profileIDs) + return tx.UpdateImage(ctx, details.imageID, details.image.Filename, details.image.Size, req.Public, req.AutoUpdate, details.image.Architecture, details.image.CreatedAt, details.image.ExpiresAt, req.Properties, projectName, profileIDs) }) if err != nil { if response.IsNotFoundError(err) { @@ -3162,7 +3237,7 @@ func imagePut(d *Daemon, r *http.Request) response.Response { } requestor := request.CreateRequestor(r) - s.Events.SendLifecycle(projectName, lifecycle.ImageUpdated.Event(info.Fingerprint, projectName, requestor, nil)) + s.Events.SendLifecycle(projectName, lifecycle.ImageUpdated.Event(details.image.Fingerprint, projectName, requestor, nil)) return response.EmptySyncResponse } @@ -3206,25 +3281,13 @@ func imagePatch(d *Daemon, r *http.Request) response.Response { // Get current value projectName := request.ProjectParam(r) - fingerprint, err := url.PathUnescape(mux.Vars(r)["fingerprint"]) - if err != nil { - return response.SmartError(err) - } - - var id int - var info *api.Image - - err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { - id, info, err = tx.GetImage(ctx, fingerprint, dbCluster.ImageFilter{Project: &projectName}) - - return err - }) + details, err := request.GetCtxValue[imageDetails](r.Context(), ctxImageDetails) if err != nil { return response.SmartError(err) } // Validate ETag - etag := []any{info.Public, info.AutoUpdate, info.Properties} + etag := []any{details.image.Public, details.image.AutoUpdate, details.image.Properties} err = util.EtagCheck(r, etag) if err != nil { return response.PreconditionFailed(err) @@ -3253,37 +3316,38 @@ func imagePatch(d *Daemon, r *http.Request) response.Response { // Get AutoUpdate autoUpdate, err := reqRaw.GetBool("auto_update") if err == nil { - info.AutoUpdate = autoUpdate + details.image.AutoUpdate = autoUpdate } // Get Public public, err := reqRaw.GetBool("public") if err == nil { - info.Public = public + details.image.Public = public } // Get Properties _, ok := reqRaw["properties"] if ok { properties := req.Properties - for k, v := range info.Properties { + for k, v := range details.image.Properties { _, ok := req.Properties[k] if !ok { properties[k] = v } } - info.Properties = properties + + details.image.Properties = properties } err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { - return tx.UpdateImage(ctx, id, info.Filename, info.Size, info.Public, info.AutoUpdate, info.Architecture, info.CreatedAt, info.ExpiresAt, info.Properties, "", nil) + return tx.UpdateImage(ctx, details.imageID, details.image.Filename, details.image.Size, details.image.Public, details.image.AutoUpdate, details.image.Architecture, details.image.CreatedAt, details.image.ExpiresAt, details.image.Properties, "", nil) }) if err != nil { return response.SmartError(err) } requestor := request.CreateRequestor(r) - s.Events.SendLifecycle(projectName, lifecycle.ImageUpdated.Event(info.Fingerprint, projectName, requestor, nil)) + s.Events.SendLifecycle(projectName, lifecycle.ImageUpdated.Event(details.image.Fingerprint, projectName, requestor, nil)) return response.EmptySyncResponse } @@ -3468,16 +3532,9 @@ func imageAliasesGet(d *Daemon, r *http.Request) response.Response { projectName := request.ProjectParam(r) var effectiveProjectName string err := s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - projectHasImages, err := dbCluster.ProjectHasImages(ctx, tx.Tx(), projectName) - if err != nil { - return err - } - - if !projectHasImages { - effectiveProjectName = api.ProjectDefaultName - } - - return nil + var err error + effectiveProjectName, err = projectutils.ImageProject(ctx, tx.Tx(), projectName) + return err }) if err != nil { return response.SmartError(err) @@ -3623,10 +3680,16 @@ func imageAliasGet(d *Daemon, r *http.Request) response.Response { } s := d.State() + var effectiveProjectName string + err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { + effectiveProjectName, err = projectutils.ImageProject(ctx, tx.Tx(), projectName) + return err + }) // Set `userCanViewImageAlias` to true only when the caller is authenticated and can view the alias. // We don't abort the request if this is false because the image alias may be for a public image. var userCanViewImageAlias bool + request.SetCtxValue(r, request.CtxEffectiveProjectName, effectiveProjectName) err = s.Authorizer.CheckPermission(r.Context(), entity.ImageAliasURL(projectName, name), auth.EntitlementCanView) if err != nil && !auth.IsDeniedError(err) { return response.SmartError(err) @@ -3635,7 +3698,7 @@ func imageAliasGet(d *Daemon, r *http.Request) response.Response { } var alias api.ImageAliasesEntry - err = d.State().DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { + err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { // If `userCanViewImageAlias` is false, the query will be restricted to public images only. _, alias, err = tx.GetImageAlias(ctx, projectName, name, userCanViewImageAlias) @@ -4054,7 +4117,13 @@ func imageExport(d *Daemon, r *http.Request) response.Response { // Get the image. We need to do this before the permission check because the URL in the permission check will not // work with partial fingerprints. var imgInfo *api.Image + effectiveProjectName := projectName err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { + effectiveProjectName, err = projectutils.ImageProject(ctx, tx.Tx(), projectName) + if err != nil { + return err + } + filter := dbCluster.ImageFilter{Project: &projectName} if publicOnly { filter.Public = &publicOnly @@ -4107,6 +4176,7 @@ func imageExport(d *Daemon, r *http.Request) response.Response { userCanViewImage = true } else { // Otherwise perform an access check with the full image fingerprint. + request.SetCtxValue(r, request.CtxEffectiveProjectName, effectiveProjectName) err = s.Authorizer.CheckPermission(r.Context(), entity.ImageURL(projectName, imgInfo.Fingerprint), auth.EntitlementCanView) if err != nil && !auth.IsDeniedError(err) { return response.SmartError(err) @@ -4227,17 +4297,7 @@ func imageExportPost(d *Daemon, r *http.Request) response.Response { s := d.State() projectName := request.ProjectParam(r) - fingerprint, err := url.PathUnescape(mux.Vars(r)["fingerprint"]) - if err != nil { - return response.SmartError(err) - } - - err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { - // Check if the image exists - _, _, err = tx.GetImage(ctx, fingerprint, dbCluster.ImageFilter{Project: &projectName}) - - return err - }) + details, err := request.GetCtxValue[imageDetails](r.Context(), ctxImageDetails) if err != nil { return response.SmartError(err) } @@ -4268,8 +4328,8 @@ func imageExportPost(d *Daemon, r *http.Request) response.Response { run := func(op *operations.Operation) error { createArgs := &lxd.ImageCreateArgs{} - imageMetaPath := shared.VarPath("images", fingerprint) - imageRootfsPath := shared.VarPath("images", fingerprint+".rootfs") + imageMetaPath := shared.VarPath("images", details.imageFingerprintPrefix) + imageRootfsPath := shared.VarPath("images", details.imageFingerprintPrefix+".rootfs") metaFile, err := os.Open(imageMetaPath) if err != nil { @@ -4296,7 +4356,7 @@ func imageExportPost(d *Daemon, r *http.Request) response.Response { image := api.ImagesPost{ Filename: createArgs.MetaName, Source: &api.ImagesPostSource{ - Fingerprint: fingerprint, + Fingerprint: details.imageFingerprintPrefix, Secret: req.Secret, Mode: "push", }, @@ -4335,7 +4395,7 @@ func imageExportPost(d *Daemon, r *http.Request) response.Response { return fmt.Errorf("Failed operation %q: %q", opWaitAPI.Status, opWaitAPI.Err) } - s.Events.SendLifecycle(projectName, lifecycle.ImageRetrieved.Event(fingerprint, projectName, op.Requestor(), logger.Ctx{"target": req.Target})) + s.Events.SendLifecycle(projectName, lifecycle.ImageRetrieved.Event(details.imageFingerprintPrefix, projectName, op.Requestor(), logger.Ctx{"target": req.Target})) return nil } @@ -4376,23 +4436,12 @@ func imageSecret(d *Daemon, r *http.Request) response.Response { s := d.State() projectName := request.ProjectParam(r) - fingerprint, err := url.PathUnescape(mux.Vars(r)["fingerprint"]) - if err != nil { - return response.SmartError(err) - } - - var imgInfo *api.Image - - err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { - _, imgInfo, err = tx.GetImage(ctx, fingerprint, dbCluster.ImageFilter{Project: &projectName}) - - return err - }) + details, err := request.GetCtxValue[imageDetails](r.Context(), ctxImageDetails) if err != nil { return response.SmartError(err) } - return createTokenResponse(s, r, projectName, imgInfo.Fingerprint, nil) + return createTokenResponse(s, r, projectName, details.image.Fingerprint, nil) } func imageImportFromNode(imagesDir string, client lxd.InstanceServer, fingerprint string) error { @@ -4495,19 +4544,7 @@ func imageRefresh(d *Daemon, r *http.Request) response.Response { s := d.State() projectName := request.ProjectParam(r) - fingerprint, err := url.PathUnescape(mux.Vars(r)["fingerprint"]) - if err != nil { - return response.SmartError(err) - } - - var imageID int - var imageInfo *api.Image - - err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { - imageID, imageInfo, err = tx.GetImage(ctx, fingerprint, dbCluster.ImageFilter{Project: &projectName}) - - return err - }) + details, err := request.GetCtxValue[imageDetails](r.Context(), ctxImageDetails) if err != nil { return response.SmartError(err) } @@ -4517,22 +4554,22 @@ func imageRefresh(d *Daemon, r *http.Request) response.Response { var nodes []string err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { - nodes, err = tx.GetNodesWithImageAndAutoUpdate(ctx, fingerprint, true) + nodes, err = tx.GetNodesWithImageAndAutoUpdate(ctx, details.imageFingerprintPrefix, true) return err }) if err != nil { - return fmt.Errorf("Error getting cluster members for refreshing image %q in project %q: %w", fingerprint, projectName, err) + return fmt.Errorf("Error getting cluster members for refreshing image %q in project %q: %w", details.imageFingerprintPrefix, projectName, err) } - newImage, err := autoUpdateImage(s.ShutdownCtx, s, op, imageID, imageInfo, projectName, true) + newImage, err := autoUpdateImage(s.ShutdownCtx, s, op, details.imageID, &details.image, projectName, true) if err != nil { - return fmt.Errorf("Failed to update image %q in project %q: %w", fingerprint, projectName, err) + return fmt.Errorf("Failed to update image %q in project %q: %w", details.imageFingerprintPrefix, projectName, err) } if newImage != nil { if len(nodes) > 1 { - err := distributeImage(s.ShutdownCtx, s, nodes, fingerprint, newImage) + err := distributeImage(s.ShutdownCtx, s, nodes, details.imageFingerprintPrefix, newImage) if err != nil { return fmt.Errorf("Failed to distribute new image %q: %w", newImage.Fingerprint, err) } @@ -4540,10 +4577,10 @@ func imageRefresh(d *Daemon, r *http.Request) response.Response { err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { // Remove the database entry for the image after distributing to cluster members. - return tx.DeleteImage(ctx, imageID) + return tx.DeleteImage(ctx, details.imageID) }) if err != nil { - logger.Error("Error deleting old image from database", logger.Ctx{"err": err, "fingerprint": fingerprint, "ID": imageID}) + logger.Error("Error deleting old image from database", logger.Ctx{"err": err, "fingerprint": details.imageFingerprintPrefix, "ID": details.imageID}) } } diff --git a/lxd/network_acls.go b/lxd/network_acls.go index 4edf4a37a542..aea941b34be5 100644 --- a/lxd/network_acls.go +++ b/lxd/network_acls.go @@ -146,7 +146,8 @@ var networkACLLogCmd = APIEndpoint{ func networkACLsGet(d *Daemon, r *http.Request) response.Response { s := d.State() - projectName, _, err := project.NetworkProject(s.DB.Cluster, request.ProjectParam(r)) + requestProjectName := request.ProjectParam(r) + effectiveProjectName, _, err := project.NetworkProject(s.DB.Cluster, requestProjectName) if err != nil { return response.SmartError(err) } @@ -159,7 +160,7 @@ func networkACLsGet(d *Daemon, r *http.Request) response.Response { var err error // Get list of Network ACLs. - aclNames, err = tx.GetNetworkACLs(ctx, projectName) + aclNames, err = tx.GetNetworkACLs(ctx, effectiveProjectName) return err }) @@ -167,7 +168,7 @@ func networkACLsGet(d *Daemon, r *http.Request) response.Response { return response.InternalError(err) } - request.SetCtxValue(r, request.CtxEffectiveProjectName, projectName) + request.SetCtxValue(r, request.CtxEffectiveProjectName, effectiveProjectName) userHasPermission, err := s.Authorizer.GetPermissionChecker(r.Context(), auth.EntitlementCanView, entity.TypeNetworkACL) if err != nil { return response.SmartError(err) @@ -176,14 +177,14 @@ func networkACLsGet(d *Daemon, r *http.Request) response.Response { resultString := []string{} resultMap := []api.NetworkACL{} for _, aclName := range aclNames { - if !userHasPermission(entity.NetworkACLURL(projectName, aclName)) { + if !userHasPermission(entity.NetworkACLURL(requestProjectName, aclName)) { continue } if !recursion { resultString = append(resultString, fmt.Sprintf("/%s/network-acls/%s", version.APIVersion, aclName)) } else { - netACL, err := acl.LoadByName(s, projectName, aclName) + netACL, err := acl.LoadByName(s, effectiveProjectName, aclName) if err != nil { continue } diff --git a/lxd/network_allocations.go b/lxd/network_allocations.go index e626d00f655a..909e58c6c384 100644 --- a/lxd/network_allocations.go +++ b/lxd/network_allocations.go @@ -76,18 +76,20 @@ var networkAllocationsCmd = APIEndpoint{ func networkAllocationsGet(d *Daemon, r *http.Request) response.Response { s := d.State() - projectName, _, err := project.NetworkProject(d.State().DB.Cluster, request.ProjectParam(r)) + requestProjectName := request.ProjectParam(r) + effectiveProjectName, _, err := project.NetworkProject(d.State().DB.Cluster, requestProjectName) if err != nil { return response.SmartError(err) } + request.SetCtxValue(r, request.CtxEffectiveProjectName, effectiveProjectName) allProjects := shared.IsTrue(request.QueryParam(r, "all-projects")) var projectNames []string err = d.db.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { // Figure out the projects to retrieve. if !allProjects { - projectNames = []string{projectName} + projectNames = []string{effectiveProjectName} } else { // Get all project names if no specific project requested. projectNames, err = dbCluster.GetProjectNames(ctx, tx.Tx()) @@ -126,6 +128,13 @@ func networkAllocationsGet(d *Daemon, r *http.Request) response.Response { // Then, get all the networks, their network forwards and their network load balancers. for _, projectName := range projectNames { + // The auth.PermissionChecker expects the url to contain the request project (not the effective project). + // So when getting networks in a single project, ensure we use the request project name. + authCheckProjectName := projectName + if !allProjects { + authCheckProjectName = requestProjectName + } + var networkNames []string err := d.db.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { @@ -141,7 +150,7 @@ func networkAllocationsGet(d *Daemon, r *http.Request) response.Response { // Get all the networks, their attached instances, their network forwards and their network load balancers. for _, networkName := range networkNames { - if !userHasPermission(entity.NetworkURL(projectName, networkName)) { + if !userHasPermission(entity.NetworkURL(authCheckProjectName, networkName)) { continue } @@ -167,7 +176,7 @@ func networkAllocationsGet(d *Daemon, r *http.Request) response.Response { } leases, err := n.Leases(projectName, clusterRequest.ClientTypeNormal) - if err != nil && !errors.Is(network.ErrNotImplemented, err) { + if err != nil && !errors.Is(err, network.ErrNotImplemented) { return response.SmartError(fmt.Errorf("Failed getting leases for network %q in project %q: %w", networkName, projectName, err)) } diff --git a/lxd/network_forwards.go b/lxd/network_forwards.go index 289bf1a06101..21851c4bb038 100644 --- a/lxd/network_forwards.go +++ b/lxd/network_forwards.go @@ -19,24 +19,23 @@ import ( "github.com/canonical/lxd/lxd/response" "github.com/canonical/lxd/lxd/util" "github.com/canonical/lxd/shared/api" - "github.com/canonical/lxd/shared/entity" "github.com/canonical/lxd/shared/version" ) var networkForwardsCmd = APIEndpoint{ Path: "networks/{networkName}/forwards", - Get: APIEndpointAction{Handler: networkForwardsGet, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanView, "networkName")}, - Post: APIEndpointAction{Handler: networkForwardsPost, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanEdit, "networkName")}, + Get: APIEndpointAction{Handler: networkForwardsGet, AccessHandler: networkAccessHandler(auth.EntitlementCanView)}, + Post: APIEndpointAction{Handler: networkForwardsPost, AccessHandler: networkAccessHandler(auth.EntitlementCanEdit)}, } var networkForwardCmd = APIEndpoint{ Path: "networks/{networkName}/forwards/{listenAddress}", - Delete: APIEndpointAction{Handler: networkForwardDelete, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanDelete, "networkName")}, - Get: APIEndpointAction{Handler: networkForwardGet, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanView, "networkName")}, - Put: APIEndpointAction{Handler: networkForwardPut, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanEdit, "networkName")}, - Patch: APIEndpointAction{Handler: networkForwardPut, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanEdit, "networkName")}, + Delete: APIEndpointAction{Handler: networkForwardDelete, AccessHandler: networkAccessHandler(auth.EntitlementCanEdit)}, + Get: APIEndpointAction{Handler: networkForwardGet, AccessHandler: networkAccessHandler(auth.EntitlementCanView)}, + Put: APIEndpointAction{Handler: networkForwardPut, AccessHandler: networkAccessHandler(auth.EntitlementCanEdit)}, + Patch: APIEndpointAction{Handler: networkForwardPut, AccessHandler: networkAccessHandler(auth.EntitlementCanEdit)}, } // API endpoints @@ -136,23 +135,23 @@ var networkForwardCmd = APIEndpoint{ func networkForwardsGet(d *Daemon, r *http.Request) response.Response { s := d.State() - projectName, reqProject, err := project.NetworkProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - networkName, err := url.PathUnescape(mux.Vars(r)["networkName"]) + details, err := request.GetCtxValue[networkDetails](r.Context(), ctxNetworkDetails) if err != nil { return response.SmartError(err) } - n, err := network.LoadByName(s, projectName, networkName) + n, err := network.LoadByName(s, effectiveProjectName, details.networkName) if err != nil { return response.SmartError(fmt.Errorf("Failed loading network: %w", err)) } // Check if project allows access to network. - if !project.NetworkAllowed(reqProject.Config, networkName, n.IsManaged()) { + if !project.NetworkAllowed(details.requestProject.Config, details.networkName, n.IsManaged()) { return response.SmartError(api.StatusErrorf(http.StatusNotFound, "Network not found")) } @@ -241,7 +240,12 @@ func networkForwardsPost(d *Daemon, r *http.Request) response.Response { return resp } - projectName, reqProject, err := project.NetworkProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) + if err != nil { + return response.SmartError(err) + } + + details, err := request.GetCtxValue[networkDetails](r.Context(), ctxNetworkDetails) if err != nil { return response.SmartError(err) } @@ -255,18 +259,13 @@ func networkForwardsPost(d *Daemon, r *http.Request) response.Response { req.Normalise() // So we handle the request in normalised/canonical form. - networkName, err := url.PathUnescape(mux.Vars(r)["networkName"]) - if err != nil { - return response.SmartError(err) - } - - n, err := network.LoadByName(s, projectName, networkName) + n, err := network.LoadByName(s, effectiveProjectName, details.networkName) if err != nil { return response.SmartError(fmt.Errorf("Failed loading network: %w", err)) } // Check if project allows access to network. - if !project.NetworkAllowed(reqProject.Config, networkName, n.IsManaged()) { + if !project.NetworkAllowed(details.requestProject.Config, details.networkName, n.IsManaged()) { return response.SmartError(api.StatusErrorf(http.StatusNotFound, "Network not found")) } @@ -282,7 +281,7 @@ func networkForwardsPost(d *Daemon, r *http.Request) response.Response { } lc := lifecycle.NetworkForwardCreated.Event(n, listenAddress.String(), request.CreateRequestor(r), nil) - s.Events.SendLifecycle(projectName, lc) + s.Events.SendLifecycle(effectiveProjectName, lc) return response.SyncResponseLocation(true, nil, lc.Source) } @@ -319,23 +318,23 @@ func networkForwardDelete(d *Daemon, r *http.Request) response.Response { return resp } - projectName, reqProject, err := project.NetworkProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - networkName, err := url.PathUnescape(mux.Vars(r)["networkName"]) + details, err := request.GetCtxValue[networkDetails](r.Context(), ctxNetworkDetails) if err != nil { return response.SmartError(err) } - n, err := network.LoadByName(s, projectName, networkName) + n, err := network.LoadByName(s, effectiveProjectName, details.networkName) if err != nil { return response.SmartError(fmt.Errorf("Failed loading network: %w", err)) } // Check if project allows access to network. - if !project.NetworkAllowed(reqProject.Config, networkName, n.IsManaged()) { + if !project.NetworkAllowed(details.requestProject.Config, details.networkName, n.IsManaged()) { return response.SmartError(api.StatusErrorf(http.StatusNotFound, "Network not found")) } @@ -355,7 +354,7 @@ func networkForwardDelete(d *Daemon, r *http.Request) response.Response { return response.SmartError(fmt.Errorf("Failed deleting forward: %w", err)) } - s.Events.SendLifecycle(projectName, lifecycle.NetworkForwardDeleted.Event(n, listenAddress, request.CreateRequestor(r), nil)) + s.Events.SendLifecycle(effectiveProjectName, lifecycle.NetworkForwardDeleted.Event(n, listenAddress, request.CreateRequestor(r), nil)) return response.EmptySyncResponse } @@ -408,23 +407,23 @@ func networkForwardGet(d *Daemon, r *http.Request) response.Response { return resp } - projectName, reqProject, err := project.NetworkProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - networkName, err := url.PathUnescape(mux.Vars(r)["networkName"]) + details, err := request.GetCtxValue[networkDetails](r.Context(), ctxNetworkDetails) if err != nil { return response.SmartError(err) } - n, err := network.LoadByName(s, projectName, networkName) + n, err := network.LoadByName(s, effectiveProjectName, details.networkName) if err != nil { return response.SmartError(fmt.Errorf("Failed loading network: %w", err)) } // Check if project allows access to network. - if !project.NetworkAllowed(reqProject.Config, networkName, n.IsManaged()) { + if !project.NetworkAllowed(details.requestProject.Config, details.networkName, n.IsManaged()) { return response.SmartError(api.StatusErrorf(http.StatusNotFound, "Network not found")) } @@ -531,23 +530,23 @@ func networkForwardPut(d *Daemon, r *http.Request) response.Response { return resp } - projectName, reqProject, err := project.NetworkProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - networkName, err := url.PathUnescape(mux.Vars(r)["networkName"]) + details, err := request.GetCtxValue[networkDetails](r.Context(), ctxNetworkDetails) if err != nil { return response.SmartError(err) } - n, err := network.LoadByName(s, projectName, networkName) + n, err := network.LoadByName(s, effectiveProjectName, details.networkName) if err != nil { return response.SmartError(fmt.Errorf("Failed loading network: %w", err)) } // Check if project allows access to network. - if !project.NetworkAllowed(reqProject.Config, networkName, n.IsManaged()) { + if !project.NetworkAllowed(details.requestProject.Config, details.networkName, n.IsManaged()) { return response.SmartError(api.StatusErrorf(http.StatusNotFound, "Network not found")) } @@ -607,7 +606,7 @@ func networkForwardPut(d *Daemon, r *http.Request) response.Response { return response.SmartError(fmt.Errorf("Failed updating forward: %w", err)) } - s.Events.SendLifecycle(projectName, lifecycle.NetworkForwardUpdated.Event(n, listenAddress, request.CreateRequestor(r), nil)) + s.Events.SendLifecycle(effectiveProjectName, lifecycle.NetworkForwardUpdated.Event(n, listenAddress, request.CreateRequestor(r), nil)) return response.EmptySyncResponse } diff --git a/lxd/network_load_balancers.go b/lxd/network_load_balancers.go index 89253cd96843..77f5c275a90e 100644 --- a/lxd/network_load_balancers.go +++ b/lxd/network_load_balancers.go @@ -19,24 +19,23 @@ import ( "github.com/canonical/lxd/lxd/response" "github.com/canonical/lxd/lxd/util" "github.com/canonical/lxd/shared/api" - "github.com/canonical/lxd/shared/entity" "github.com/canonical/lxd/shared/version" ) var networkLoadBalancersCmd = APIEndpoint{ Path: "networks/{networkName}/load-balancers", - Get: APIEndpointAction{Handler: networkLoadBalancersGet, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanView, "networkName")}, - Post: APIEndpointAction{Handler: networkLoadBalancersPost, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanEdit, "networkName")}, + Get: APIEndpointAction{Handler: networkLoadBalancersGet, AccessHandler: networkAccessHandler(auth.EntitlementCanView)}, + Post: APIEndpointAction{Handler: networkLoadBalancersPost, AccessHandler: networkAccessHandler(auth.EntitlementCanEdit)}, } var networkLoadBalancerCmd = APIEndpoint{ Path: "networks/{networkName}/load-balancers/{listenAddress}", - Delete: APIEndpointAction{Handler: networkLoadBalancerDelete, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanEdit, "networkName")}, - Get: APIEndpointAction{Handler: networkLoadBalancerGet, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanView, "networkName")}, - Put: APIEndpointAction{Handler: networkLoadBalancerPut, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanEdit, "networkName")}, - Patch: APIEndpointAction{Handler: networkLoadBalancerPut, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanEdit, "networkName")}, + Delete: APIEndpointAction{Handler: networkLoadBalancerDelete, AccessHandler: networkAccessHandler(auth.EntitlementCanEdit)}, + Get: APIEndpointAction{Handler: networkLoadBalancerGet, AccessHandler: networkAccessHandler(auth.EntitlementCanView)}, + Put: APIEndpointAction{Handler: networkLoadBalancerPut, AccessHandler: networkAccessHandler(auth.EntitlementCanEdit)}, + Patch: APIEndpointAction{Handler: networkLoadBalancerPut, AccessHandler: networkAccessHandler(auth.EntitlementCanEdit)}, } // API endpoints @@ -136,23 +135,23 @@ var networkLoadBalancerCmd = APIEndpoint{ func networkLoadBalancersGet(d *Daemon, r *http.Request) response.Response { s := d.State() - projectName, reqProject, err := project.NetworkProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - networkName, err := url.PathUnescape(mux.Vars(r)["networkName"]) + details, err := request.GetCtxValue[networkDetails](r.Context(), ctxNetworkDetails) if err != nil { return response.SmartError(err) } - n, err := network.LoadByName(s, projectName, networkName) + n, err := network.LoadByName(s, effectiveProjectName, details.networkName) if err != nil { return response.SmartError(fmt.Errorf("Failed loading network: %w", err)) } // Check if project allows access to network. - if !project.NetworkAllowed(reqProject.Config, networkName, n.IsManaged()) { + if !project.NetworkAllowed(details.requestProject.Config, details.networkName, n.IsManaged()) { return response.SmartError(api.StatusErrorf(http.StatusNotFound, "Network not found")) } @@ -242,7 +241,12 @@ func networkLoadBalancersPost(d *Daemon, r *http.Request) response.Response { return resp } - projectName, reqProject, err := project.NetworkProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) + if err != nil { + return response.SmartError(err) + } + + details, err := request.GetCtxValue[networkDetails](r.Context(), ctxNetworkDetails) if err != nil { return response.SmartError(err) } @@ -256,18 +260,13 @@ func networkLoadBalancersPost(d *Daemon, r *http.Request) response.Response { req.Normalise() // So we handle the request in normalised/canonical form. - networkName, err := url.PathUnescape(mux.Vars(r)["networkName"]) - if err != nil { - return response.SmartError(err) - } - - n, err := network.LoadByName(s, projectName, networkName) + n, err := network.LoadByName(s, effectiveProjectName, details.networkName) if err != nil { return response.SmartError(fmt.Errorf("Failed loading network: %w", err)) } // Check if project allows access to network. - if !project.NetworkAllowed(reqProject.Config, networkName, n.IsManaged()) { + if !project.NetworkAllowed(details.requestProject.Config, details.networkName, n.IsManaged()) { return response.SmartError(api.StatusErrorf(http.StatusNotFound, "Network not found")) } @@ -283,7 +282,7 @@ func networkLoadBalancersPost(d *Daemon, r *http.Request) response.Response { } lc := lifecycle.NetworkLoadBalancerCreated.Event(n, listenAddress.String(), request.CreateRequestor(r), nil) - s.Events.SendLifecycle(projectName, lc) + s.Events.SendLifecycle(effectiveProjectName, lc) return response.SyncResponseLocation(true, nil, lc.Source) } @@ -320,23 +319,23 @@ func networkLoadBalancerDelete(d *Daemon, r *http.Request) response.Response { return resp } - projectName, reqProject, err := project.NetworkProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - networkName, err := url.PathUnescape(mux.Vars(r)["networkName"]) + details, err := request.GetCtxValue[networkDetails](r.Context(), ctxNetworkDetails) if err != nil { return response.SmartError(err) } - n, err := network.LoadByName(s, projectName, networkName) + n, err := network.LoadByName(s, effectiveProjectName, details.networkName) if err != nil { return response.SmartError(fmt.Errorf("Failed loading network: %w", err)) } // Check if project allows access to network. - if !project.NetworkAllowed(reqProject.Config, networkName, n.IsManaged()) { + if !project.NetworkAllowed(details.requestProject.Config, details.networkName, n.IsManaged()) { return response.SmartError(api.StatusErrorf(http.StatusNotFound, "Network not found")) } @@ -356,7 +355,7 @@ func networkLoadBalancerDelete(d *Daemon, r *http.Request) response.Response { return response.SmartError(fmt.Errorf("Failed deleting load balancer: %w", err)) } - s.Events.SendLifecycle(projectName, lifecycle.NetworkLoadBalancerDeleted.Event(n, listenAddress, request.CreateRequestor(r), nil)) + s.Events.SendLifecycle(effectiveProjectName, lifecycle.NetworkLoadBalancerDeleted.Event(n, listenAddress, request.CreateRequestor(r), nil)) return response.EmptySyncResponse } @@ -409,23 +408,23 @@ func networkLoadBalancerGet(d *Daemon, r *http.Request) response.Response { return resp } - projectName, reqProject, err := project.NetworkProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - networkName, err := url.PathUnescape(mux.Vars(r)["networkName"]) + details, err := request.GetCtxValue[networkDetails](r.Context(), ctxNetworkDetails) if err != nil { return response.SmartError(err) } - n, err := network.LoadByName(s, projectName, networkName) + n, err := network.LoadByName(s, effectiveProjectName, details.networkName) if err != nil { return response.SmartError(fmt.Errorf("Failed loading network: %w", err)) } // Check if project allows access to network. - if !project.NetworkAllowed(reqProject.Config, networkName, n.IsManaged()) { + if !project.NetworkAllowed(details.requestProject.Config, details.networkName, n.IsManaged()) { return response.SmartError(api.StatusErrorf(http.StatusNotFound, "Network not found")) } @@ -532,23 +531,23 @@ func networkLoadBalancerPut(d *Daemon, r *http.Request) response.Response { return resp } - projectName, reqProject, err := project.NetworkProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - networkName, err := url.PathUnescape(mux.Vars(r)["networkName"]) + details, err := request.GetCtxValue[networkDetails](r.Context(), ctxNetworkDetails) if err != nil { return response.SmartError(err) } - n, err := network.LoadByName(s, projectName, networkName) + n, err := network.LoadByName(s, effectiveProjectName, details.networkName) if err != nil { return response.SmartError(fmt.Errorf("Failed loading network: %w", err)) } // Check if project allows access to network. - if !project.NetworkAllowed(reqProject.Config, networkName, n.IsManaged()) { + if !project.NetworkAllowed(details.requestProject.Config, details.networkName, n.IsManaged()) { return response.SmartError(api.StatusErrorf(http.StatusNotFound, "Network not found")) } @@ -614,7 +613,7 @@ func networkLoadBalancerPut(d *Daemon, r *http.Request) response.Response { return response.SmartError(fmt.Errorf("Failed updating load balancer: %w", err)) } - s.Events.SendLifecycle(projectName, lifecycle.NetworkLoadBalancerUpdated.Event(n, listenAddress, request.CreateRequestor(r), nil)) + s.Events.SendLifecycle(effectiveProjectName, lifecycle.NetworkLoadBalancerUpdated.Event(n, listenAddress, request.CreateRequestor(r), nil)) return response.EmptySyncResponse } diff --git a/lxd/network_peer.go b/lxd/network_peer.go index c86af162501f..52c4e5633664 100644 --- a/lxd/network_peer.go +++ b/lxd/network_peer.go @@ -18,24 +18,23 @@ import ( "github.com/canonical/lxd/lxd/response" "github.com/canonical/lxd/lxd/util" "github.com/canonical/lxd/shared/api" - "github.com/canonical/lxd/shared/entity" "github.com/canonical/lxd/shared/version" ) var networkPeersCmd = APIEndpoint{ Path: "networks/{networkName}/peers", - Get: APIEndpointAction{Handler: networkPeersGet, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanView, "networkName")}, - Post: APIEndpointAction{Handler: networkPeersPost, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanEdit, "networkName")}, + Get: APIEndpointAction{Handler: networkPeersGet, AccessHandler: networkAccessHandler(auth.EntitlementCanView)}, + Post: APIEndpointAction{Handler: networkPeersPost, AccessHandler: networkAccessHandler(auth.EntitlementCanEdit)}, } var networkPeerCmd = APIEndpoint{ Path: "networks/{networkName}/peers/{peerName}", - Delete: APIEndpointAction{Handler: networkPeerDelete, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanEdit, "networkName")}, - Get: APIEndpointAction{Handler: networkPeerGet, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanView, "networkName")}, - Put: APIEndpointAction{Handler: networkPeerPut, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanEdit, "networkName")}, - Patch: APIEndpointAction{Handler: networkPeerPut, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanEdit, "networkName")}, + Delete: APIEndpointAction{Handler: networkPeerDelete, AccessHandler: networkAccessHandler(auth.EntitlementCanEdit)}, + Get: APIEndpointAction{Handler: networkPeerGet, AccessHandler: networkAccessHandler(auth.EntitlementCanView)}, + Put: APIEndpointAction{Handler: networkPeerPut, AccessHandler: networkAccessHandler(auth.EntitlementCanEdit)}, + Patch: APIEndpointAction{Handler: networkPeerPut, AccessHandler: networkAccessHandler(auth.EntitlementCanEdit)}, } // API endpoints @@ -135,23 +134,23 @@ var networkPeerCmd = APIEndpoint{ func networkPeersGet(d *Daemon, r *http.Request) response.Response { s := d.State() - projectName, reqProject, err := project.NetworkProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - networkName, err := url.PathUnescape(mux.Vars(r)["networkName"]) + details, err := request.GetCtxValue[networkDetails](r.Context(), ctxNetworkDetails) if err != nil { return response.SmartError(err) } - n, err := network.LoadByName(s, projectName, networkName) + n, err := network.LoadByName(s, effectiveProjectName, details.networkName) if err != nil { return response.SmartError(fmt.Errorf("Failed loading network: %w", err)) } // Check if project allows access to network. - if !project.NetworkAllowed(reqProject.Config, networkName, n.IsManaged()) { + if !project.NetworkAllowed(details.requestProject.Config, details.networkName, n.IsManaged()) { return response.SmartError(api.StatusErrorf(http.StatusNotFound, "Network not found")) } @@ -242,7 +241,12 @@ func networkPeersPost(d *Daemon, r *http.Request) response.Response { return resp } - projectName, reqProject, err := project.NetworkProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) + if err != nil { + return response.SmartError(err) + } + + details, err := request.GetCtxValue[networkDetails](r.Context(), ctxNetworkDetails) if err != nil { return response.SmartError(err) } @@ -254,18 +258,13 @@ func networkPeersPost(d *Daemon, r *http.Request) response.Response { return response.BadRequest(err) } - networkName, err := url.PathUnescape(mux.Vars(r)["networkName"]) - if err != nil { - return response.SmartError(err) - } - - n, err := network.LoadByName(s, projectName, networkName) + n, err := network.LoadByName(s, effectiveProjectName, details.networkName) if err != nil { return response.SmartError(fmt.Errorf("Failed loading network: %w", err)) } // Check if project allows access to network. - if !project.NetworkAllowed(reqProject.Config, networkName, n.IsManaged()) { + if !project.NetworkAllowed(details.requestProject.Config, details.networkName, n.IsManaged()) { return response.SmartError(api.StatusErrorf(http.StatusNotFound, "Network not found")) } @@ -279,7 +278,7 @@ func networkPeersPost(d *Daemon, r *http.Request) response.Response { } lc := lifecycle.NetworkPeerCreated.Event(n, req.Name, request.CreateRequestor(r), nil) - s.Events.SendLifecycle(projectName, lc) + s.Events.SendLifecycle(effectiveProjectName, lc) return response.SyncResponseLocation(true, nil, lc.Source) } @@ -316,23 +315,23 @@ func networkPeerDelete(d *Daemon, r *http.Request) response.Response { return resp } - projectName, reqProject, err := project.NetworkProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - networkName, err := url.PathUnescape(mux.Vars(r)["networkName"]) + details, err := request.GetCtxValue[networkDetails](r.Context(), ctxNetworkDetails) if err != nil { return response.SmartError(err) } - n, err := network.LoadByName(s, projectName, networkName) + n, err := network.LoadByName(s, effectiveProjectName, details.networkName) if err != nil { return response.SmartError(fmt.Errorf("Failed loading network: %w", err)) } // Check if project allows access to network. - if !project.NetworkAllowed(reqProject.Config, networkName, n.IsManaged()) { + if !project.NetworkAllowed(details.requestProject.Config, details.networkName, n.IsManaged()) { return response.SmartError(api.StatusErrorf(http.StatusNotFound, "Network not found")) } @@ -350,7 +349,7 @@ func networkPeerDelete(d *Daemon, r *http.Request) response.Response { return response.SmartError(fmt.Errorf("Failed deleting peer: %w", err)) } - s.Events.SendLifecycle(projectName, lifecycle.NetworkPeerDeleted.Event(n, peerName, request.CreateRequestor(r), nil)) + s.Events.SendLifecycle(effectiveProjectName, lifecycle.NetworkPeerDeleted.Event(n, peerName, request.CreateRequestor(r), nil)) return response.EmptySyncResponse } @@ -403,23 +402,23 @@ func networkPeerGet(d *Daemon, r *http.Request) response.Response { return resp } - projectName, reqProject, err := project.NetworkProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - networkName, err := url.PathUnescape(mux.Vars(r)["networkName"]) + details, err := request.GetCtxValue[networkDetails](r.Context(), ctxNetworkDetails) if err != nil { return response.SmartError(err) } - n, err := network.LoadByName(s, projectName, networkName) + n, err := network.LoadByName(s, effectiveProjectName, details.networkName) if err != nil { return response.SmartError(fmt.Errorf("Failed loading network: %w", err)) } // Check if project allows access to network. - if !project.NetworkAllowed(reqProject.Config, networkName, n.IsManaged()) { + if !project.NetworkAllowed(details.requestProject.Config, details.networkName, n.IsManaged()) { return response.SmartError(api.StatusErrorf(http.StatusNotFound, "Network not found")) } @@ -526,23 +525,23 @@ func networkPeerPut(d *Daemon, r *http.Request) response.Response { return resp } - projectName, reqProject, err := project.NetworkProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - networkName, err := url.PathUnescape(mux.Vars(r)["networkName"]) + details, err := request.GetCtxValue[networkDetails](r.Context(), ctxNetworkDetails) if err != nil { return response.SmartError(err) } - n, err := network.LoadByName(s, projectName, networkName) + n, err := network.LoadByName(s, effectiveProjectName, details.networkName) if err != nil { return response.SmartError(fmt.Errorf("Failed loading network: %w", err)) } // Check if project allows access to network. - if !project.NetworkAllowed(reqProject.Config, networkName, n.IsManaged()) { + if !project.NetworkAllowed(details.requestProject.Config, details.networkName, n.IsManaged()) { return response.SmartError(api.StatusErrorf(http.StatusNotFound, "Network not found")) } @@ -567,7 +566,7 @@ func networkPeerPut(d *Daemon, r *http.Request) response.Response { return response.SmartError(fmt.Errorf("Failed updating peer: %w", err)) } - s.Events.SendLifecycle(projectName, lifecycle.NetworkPeerUpdated.Event(n, peerName, request.CreateRequestor(r), nil)) + s.Events.SendLifecycle(effectiveProjectName, lifecycle.NetworkPeerUpdated.Event(n, peerName, request.CreateRequestor(r), nil)) return response.EmptySyncResponse } diff --git a/lxd/network_zones.go b/lxd/network_zones.go index 3d098207d67a..c8921f067a69 100644 --- a/lxd/network_zones.go +++ b/lxd/network_zones.go @@ -17,6 +17,7 @@ import ( "github.com/canonical/lxd/lxd/project" "github.com/canonical/lxd/lxd/request" "github.com/canonical/lxd/lxd/response" + "github.com/canonical/lxd/lxd/state" "github.com/canonical/lxd/lxd/util" "github.com/canonical/lxd/shared/api" "github.com/canonical/lxd/shared/entity" @@ -33,10 +34,67 @@ var networkZonesCmd = APIEndpoint{ var networkZoneCmd = APIEndpoint{ Path: "network-zones/{zone}", - Delete: APIEndpointAction{Handler: networkZoneDelete, AccessHandler: allowPermission(entity.TypeNetworkZone, auth.EntitlementCanDelete, "zone")}, - Get: APIEndpointAction{Handler: networkZoneGet, AccessHandler: allowPermission(entity.TypeNetworkZone, auth.EntitlementCanView, "zone")}, - Put: APIEndpointAction{Handler: networkZonePut, AccessHandler: allowPermission(entity.TypeNetworkZone, auth.EntitlementCanEdit, "zone")}, - Patch: APIEndpointAction{Handler: networkZonePut, AccessHandler: allowPermission(entity.TypeNetworkZone, auth.EntitlementCanEdit, "zone")}, + Delete: APIEndpointAction{Handler: networkZoneDelete, AccessHandler: networkZoneAccessHandler(auth.EntitlementCanDelete)}, + Get: APIEndpointAction{Handler: networkZoneGet, AccessHandler: networkZoneAccessHandler(auth.EntitlementCanView)}, + Put: APIEndpointAction{Handler: networkZonePut, AccessHandler: networkZoneAccessHandler(auth.EntitlementCanEdit)}, + Patch: APIEndpointAction{Handler: networkZonePut, AccessHandler: networkZoneAccessHandler(auth.EntitlementCanEdit)}, +} + +// ctxNetworkZoneDetails should be used only for getting/setting networkZoneDetails in the request context. +const ctxNetworkZoneDetails request.CtxKey = "network-zone-details" + +// networkZoneDetails contains fields that are determined prior to the access check. This is set in the request context when +// addNetworkZoneDetailsToRequestContext is called. +type networkZoneDetails struct { + zoneName string + requestProject api.Project +} + +// addNetworkZoneDetailsToRequestContext sets request.CtxEffectiveProjectName (string) and ctxNetworkZoneDetails (networkZoneDetails) +// in the request context. +func addNetworkZoneDetailsToRequestContext(s *state.State, r *http.Request) error { + zoneName, err := url.PathUnescape(mux.Vars(r)["zone"]) + if err != nil { + return err + } + + requestProjectName := request.ProjectParam(r) + effectiveProjectName, requestProject, err := project.NetworkZoneProject(s.DB.Cluster, requestProjectName) + if err != nil { + return fmt.Errorf("Failed to check project %q network feature: %w", requestProjectName, err) + } + + request.SetCtxValue(r, request.CtxEffectiveProjectName, effectiveProjectName) + request.SetCtxValue(r, ctxNetworkZoneDetails, networkZoneDetails{ + zoneName: zoneName, + requestProject: *requestProject, + }) + + return nil +} + +// profileAccessHandler calls addNetworkZoneDetailsToRequestContext, then uses the details to perform an access check with +// the given auth.Entitlement. +func networkZoneAccessHandler(entitlement auth.Entitlement) func(d *Daemon, r *http.Request) response.Response { + return func(d *Daemon, r *http.Request) response.Response { + s := d.State() + err := addNetworkZoneDetailsToRequestContext(s, r) + if err != nil { + return response.SmartError(err) + } + + details, err := request.GetCtxValue[networkZoneDetails](r.Context(), ctxNetworkZoneDetails) + if err != nil { + return response.SmartError(err) + } + + err = s.Authorizer.CheckPermission(r.Context(), entity.NetworkZoneURL(details.requestProject.Name, details.zoneName), entitlement) + if err != nil { + return response.SmartError(err) + } + + return response.EmptySyncResponse + } } // API endpoints. @@ -136,7 +194,8 @@ var networkZoneCmd = APIEndpoint{ func networkZonesGet(d *Daemon, r *http.Request) response.Response { s := d.State() - projectName, _, err := project.NetworkZoneProject(s.DB.Cluster, request.ProjectParam(r)) + requestProjectName := request.ProjectParam(r) + effectiveProjectName, _, err := project.NetworkZoneProject(s.DB.Cluster, requestProjectName) if err != nil { return response.SmartError(err) } @@ -147,7 +206,7 @@ func networkZonesGet(d *Daemon, r *http.Request) response.Response { err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { // Get list of Network zones. - zoneNames, err = tx.GetNetworkZonesByProject(ctx, projectName) + zoneNames, err = tx.GetNetworkZonesByProject(ctx, effectiveProjectName) return err }) @@ -155,7 +214,7 @@ func networkZonesGet(d *Daemon, r *http.Request) response.Response { return response.InternalError(err) } - request.SetCtxValue(r, request.CtxEffectiveProjectName, projectName) + request.SetCtxValue(r, request.CtxEffectiveProjectName, effectiveProjectName) userHasPermission, err := s.Authorizer.GetPermissionChecker(r.Context(), auth.EntitlementCanView, entity.TypeNetworkZone) if err != nil { return response.InternalError(err) @@ -164,14 +223,14 @@ func networkZonesGet(d *Daemon, r *http.Request) response.Response { resultString := []string{} resultMap := []api.NetworkZone{} for _, zoneName := range zoneNames { - if !userHasPermission(entity.NetworkZoneURL(projectName, zoneName)) { + if !userHasPermission(entity.NetworkZoneURL(requestProjectName, zoneName)) { continue } if !recursion { resultString = append(resultString, api.NewURL().Path(version.APIVersion, "network-zones", zoneName).String()) } else { - netzone, err := zone.LoadByNameAndProject(s, projectName, zoneName) + netzone, err := zone.LoadByNameAndProject(s, effectiveProjectName, zoneName) if err != nil { continue } @@ -288,17 +347,17 @@ func networkZonesPost(d *Daemon, r *http.Request) response.Response { func networkZoneDelete(d *Daemon, r *http.Request) response.Response { s := d.State() - projectName, _, err := project.NetworkZoneProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - zoneName, err := url.PathUnescape(mux.Vars(r)["zone"]) + details, err := request.GetCtxValue[networkZoneDetails](r.Context(), ctxNetworkZoneDetails) if err != nil { return response.SmartError(err) } - netzone, err := zone.LoadByNameAndProject(s, projectName, zoneName) + netzone, err := zone.LoadByNameAndProject(s, effectiveProjectName, details.zoneName) if err != nil { return response.SmartError(err) } @@ -308,7 +367,7 @@ func networkZoneDelete(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } - s.Events.SendLifecycle(projectName, lifecycle.NetworkZoneDeleted.Event(netzone, request.CreateRequestor(r), nil)) + s.Events.SendLifecycle(effectiveProjectName, lifecycle.NetworkZoneDeleted.Event(netzone, request.CreateRequestor(r), nil)) return response.EmptySyncResponse } @@ -356,17 +415,17 @@ func networkZoneDelete(d *Daemon, r *http.Request) response.Response { func networkZoneGet(d *Daemon, r *http.Request) response.Response { s := d.State() - projectName, _, err := project.NetworkZoneProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - zoneName, err := url.PathUnescape(mux.Vars(r)["zone"]) + details, err := request.GetCtxValue[networkZoneDetails](r.Context(), ctxNetworkZoneDetails) if err != nil { return response.SmartError(err) } - netzone, err := zone.LoadByNameAndProject(s, projectName, zoneName) + netzone, err := zone.LoadByNameAndProject(s, effectiveProjectName, details.zoneName) if err != nil { return response.SmartError(err) } @@ -454,18 +513,18 @@ func networkZoneGet(d *Daemon, r *http.Request) response.Response { func networkZonePut(d *Daemon, r *http.Request) response.Response { s := d.State() - projectName, _, err := project.NetworkZoneProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - zoneName, err := url.PathUnescape(mux.Vars(r)["zone"]) + details, err := request.GetCtxValue[networkZoneDetails](r.Context(), ctxNetworkZoneDetails) if err != nil { return response.SmartError(err) } // Get the existing Network zone. - netzone, err := zone.LoadByNameAndProject(s, projectName, zoneName) + netzone, err := zone.LoadByNameAndProject(s, effectiveProjectName, details.zoneName) if err != nil { return response.SmartError(err) } @@ -502,7 +561,7 @@ func networkZonePut(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } - s.Events.SendLifecycle(projectName, lifecycle.NetworkZoneUpdated.Event(netzone, request.CreateRequestor(r), nil)) + s.Events.SendLifecycle(effectiveProjectName, lifecycle.NetworkZoneUpdated.Event(netzone, request.CreateRequestor(r), nil)) return response.EmptySyncResponse } diff --git a/lxd/network_zones_records.go b/lxd/network_zones_records.go index c98b60f8001e..6227f91d6d9d 100644 --- a/lxd/network_zones_records.go +++ b/lxd/network_zones_records.go @@ -11,29 +11,27 @@ import ( clusterRequest "github.com/canonical/lxd/lxd/cluster/request" "github.com/canonical/lxd/lxd/lifecycle" "github.com/canonical/lxd/lxd/network/zone" - "github.com/canonical/lxd/lxd/project" "github.com/canonical/lxd/lxd/request" "github.com/canonical/lxd/lxd/response" "github.com/canonical/lxd/lxd/util" "github.com/canonical/lxd/shared/api" - "github.com/canonical/lxd/shared/entity" "github.com/canonical/lxd/shared/version" ) var networkZoneRecordsCmd = APIEndpoint{ Path: "network-zones/{zone}/records", - Get: APIEndpointAction{Handler: networkZoneRecordsGet, AccessHandler: allowPermission(entity.TypeNetworkZone, auth.EntitlementCanView, "zone")}, - Post: APIEndpointAction{Handler: networkZoneRecordsPost, AccessHandler: allowPermission(entity.TypeNetworkZone, auth.EntitlementCanEdit, "zone")}, + Get: APIEndpointAction{Handler: networkZoneRecordsGet, AccessHandler: networkZoneAccessHandler(auth.EntitlementCanView)}, + Post: APIEndpointAction{Handler: networkZoneRecordsPost, AccessHandler: networkZoneAccessHandler(auth.EntitlementCanEdit)}, } var networkZoneRecordCmd = APIEndpoint{ Path: "network-zones/{zone}/records/{name}", - Delete: APIEndpointAction{Handler: networkZoneRecordDelete, AccessHandler: allowPermission(entity.TypeNetworkZone, auth.EntitlementCanEdit, "zone")}, - Get: APIEndpointAction{Handler: networkZoneRecordGet, AccessHandler: allowPermission(entity.TypeNetworkZone, auth.EntitlementCanView, "zone")}, - Put: APIEndpointAction{Handler: networkZoneRecordPut, AccessHandler: allowPermission(entity.TypeNetworkZone, auth.EntitlementCanEdit, "zone")}, - Patch: APIEndpointAction{Handler: networkZoneRecordPut, AccessHandler: allowPermission(entity.TypeNetworkZone, auth.EntitlementCanEdit, "zone")}, + Delete: APIEndpointAction{Handler: networkZoneRecordDelete, AccessHandler: networkZoneAccessHandler(auth.EntitlementCanEdit)}, + Get: APIEndpointAction{Handler: networkZoneRecordGet, AccessHandler: networkZoneAccessHandler(auth.EntitlementCanView)}, + Put: APIEndpointAction{Handler: networkZoneRecordPut, AccessHandler: networkZoneAccessHandler(auth.EntitlementCanEdit)}, + Patch: APIEndpointAction{Handler: networkZoneRecordPut, AccessHandler: networkZoneAccessHandler(auth.EntitlementCanEdit)}, } // API endpoints. @@ -133,20 +131,20 @@ var networkZoneRecordCmd = APIEndpoint{ func networkZoneRecordsGet(d *Daemon, r *http.Request) response.Response { s := d.State() - projectName, _, err := project.NetworkZoneProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - recursion := util.IsRecursionRequest(r) - - zoneName, err := url.PathUnescape(mux.Vars(r)["zone"]) + details, err := request.GetCtxValue[networkZoneDetails](r.Context(), ctxNetworkZoneDetails) if err != nil { return response.SmartError(err) } + recursion := util.IsRecursionRequest(r) + // Get the network zone. - netzone, err := zone.LoadByNameAndProject(s, projectName, zoneName) + netzone, err := zone.LoadByNameAndProject(s, effectiveProjectName, details.zoneName) if err != nil { return response.SmartError(err) } @@ -161,7 +159,7 @@ func networkZoneRecordsGet(d *Daemon, r *http.Request) response.Response { resultMap := []api.NetworkZoneRecord{} for _, record := range records { if !recursion { - resultString = append(resultString, api.NewURL().Path(version.APIVersion, "network-zones", zoneName, "records", record.Name).String()) + resultString = append(resultString, api.NewURL().Path(version.APIVersion, "network-zones", details.zoneName, "records", record.Name).String()) } else { resultMap = append(resultMap, record) } @@ -209,18 +207,18 @@ func networkZoneRecordsGet(d *Daemon, r *http.Request) response.Response { func networkZoneRecordsPost(d *Daemon, r *http.Request) response.Response { s := d.State() - projectName, _, err := project.NetworkZoneProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - zoneName, err := url.PathUnescape(mux.Vars(r)["zone"]) + details, err := request.GetCtxValue[networkZoneDetails](r.Context(), ctxNetworkZoneDetails) if err != nil { return response.SmartError(err) } // Get the network zone. - netzone, err := zone.LoadByNameAndProject(s, projectName, zoneName) + netzone, err := zone.LoadByNameAndProject(s, effectiveProjectName, details.zoneName) if err != nil { return response.SmartError(err) } @@ -239,7 +237,7 @@ func networkZoneRecordsPost(d *Daemon, r *http.Request) response.Response { } lc := lifecycle.NetworkZoneRecordCreated.Event(netzone, req.Name, request.CreateRequestor(r), nil) - s.Events.SendLifecycle(projectName, lc) + s.Events.SendLifecycle(effectiveProjectName, lc) return response.SyncResponseLocation(true, nil, lc.Source) } @@ -271,12 +269,12 @@ func networkZoneRecordsPost(d *Daemon, r *http.Request) response.Response { func networkZoneRecordDelete(d *Daemon, r *http.Request) response.Response { s := d.State() - projectName, _, err := project.NetworkZoneProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - zoneName, err := url.PathUnescape(mux.Vars(r)["zone"]) + details, err := request.GetCtxValue[networkZoneDetails](r.Context(), ctxNetworkZoneDetails) if err != nil { return response.SmartError(err) } @@ -287,7 +285,7 @@ func networkZoneRecordDelete(d *Daemon, r *http.Request) response.Response { } // Get the network zone. - netzone, err := zone.LoadByNameAndProject(s, projectName, zoneName) + netzone, err := zone.LoadByNameAndProject(s, effectiveProjectName, details.zoneName) if err != nil { return response.SmartError(err) } @@ -298,7 +296,7 @@ func networkZoneRecordDelete(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } - s.Events.SendLifecycle(projectName, lifecycle.NetworkZoneRecordDeleted.Event(netzone, recordName, request.CreateRequestor(r), nil)) + s.Events.SendLifecycle(effectiveProjectName, lifecycle.NetworkZoneRecordDeleted.Event(netzone, recordName, request.CreateRequestor(r), nil)) return response.EmptySyncResponse } @@ -346,12 +344,12 @@ func networkZoneRecordDelete(d *Daemon, r *http.Request) response.Response { func networkZoneRecordGet(d *Daemon, r *http.Request) response.Response { s := d.State() - projectName, _, err := project.NetworkZoneProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - zoneName, err := url.PathUnescape(mux.Vars(r)["zone"]) + details, err := request.GetCtxValue[networkZoneDetails](r.Context(), ctxNetworkZoneDetails) if err != nil { return response.SmartError(err) } @@ -362,7 +360,7 @@ func networkZoneRecordGet(d *Daemon, r *http.Request) response.Response { } // Get the network zone. - netzone, err := zone.LoadByNameAndProject(s, projectName, zoneName) + netzone, err := zone.LoadByNameAndProject(s, effectiveProjectName, details.zoneName) if err != nil { return response.SmartError(err) } @@ -448,12 +446,12 @@ func networkZoneRecordGet(d *Daemon, r *http.Request) response.Response { func networkZoneRecordPut(d *Daemon, r *http.Request) response.Response { s := d.State() - projectName, _, err := project.NetworkZoneProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - zoneName, err := url.PathUnescape(mux.Vars(r)["zone"]) + details, err := request.GetCtxValue[networkZoneDetails](r.Context(), ctxNetworkZoneDetails) if err != nil { return response.SmartError(err) } @@ -464,7 +462,7 @@ func networkZoneRecordPut(d *Daemon, r *http.Request) response.Response { } // Get the network zone. - netzone, err := zone.LoadByNameAndProject(s, projectName, zoneName) + netzone, err := zone.LoadByNameAndProject(s, effectiveProjectName, details.zoneName) if err != nil { return response.SmartError(err) } @@ -505,7 +503,7 @@ func networkZoneRecordPut(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } - s.Events.SendLifecycle(projectName, lifecycle.NetworkZoneRecordUpdated.Event(netzone, recordName, request.CreateRequestor(r), nil)) + s.Events.SendLifecycle(effectiveProjectName, lifecycle.NetworkZoneRecordUpdated.Event(netzone, recordName, request.CreateRequestor(r), nil)) return response.EmptySyncResponse } diff --git a/lxd/networks.go b/lxd/networks.go index 990dd2d6beee..7eac1ca082ae 100644 --- a/lxd/networks.go +++ b/lxd/networks.go @@ -54,23 +54,80 @@ var networksCmd = APIEndpoint{ var networkCmd = APIEndpoint{ Path: "networks/{networkName}", - Delete: APIEndpointAction{Handler: networkDelete, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanDelete, "networkName")}, - Get: APIEndpointAction{Handler: networkGet, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanView, "networkName")}, - Patch: APIEndpointAction{Handler: networkPatch, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanEdit, "networkName")}, - Post: APIEndpointAction{Handler: networkPost, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanEdit, "networkName")}, - Put: APIEndpointAction{Handler: networkPut, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanEdit, "networkName")}, + Delete: APIEndpointAction{Handler: networkDelete, AccessHandler: networkAccessHandler(auth.EntitlementCanDelete)}, + Get: APIEndpointAction{Handler: networkGet, AccessHandler: networkAccessHandler(auth.EntitlementCanView)}, + Patch: APIEndpointAction{Handler: networkPatch, AccessHandler: networkAccessHandler(auth.EntitlementCanEdit)}, + Post: APIEndpointAction{Handler: networkPost, AccessHandler: networkAccessHandler(auth.EntitlementCanEdit)}, + Put: APIEndpointAction{Handler: networkPut, AccessHandler: networkAccessHandler(auth.EntitlementCanEdit)}, } var networkLeasesCmd = APIEndpoint{ Path: "networks/{networkName}/leases", - Get: APIEndpointAction{Handler: networkLeasesGet, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanView, "networkName")}, + Get: APIEndpointAction{Handler: networkLeasesGet, AccessHandler: networkAccessHandler(auth.EntitlementCanView)}, } var networkStateCmd = APIEndpoint{ Path: "networks/{networkName}/state", - Get: APIEndpointAction{Handler: networkStateGet, AccessHandler: allowPermission(entity.TypeNetwork, auth.EntitlementCanView, "networkName")}, + Get: APIEndpointAction{Handler: networkStateGet, AccessHandler: networkAccessHandler(auth.EntitlementCanView)}, +} + +// ctxNetworkDetails should be used only for getting/setting networkDetails in the request context. +const ctxNetworkDetails request.CtxKey = "network-details" + +// networkDetails contains fields that are determined prior to the access check. This is set in the request context when +// addNetworkDetailsToRequestContext is called. +type networkDetails struct { + networkName string + requestProject api.Project +} + +// addNetworkDetailsToRequestContext sets request.CtxEffectiveProjectName (string) and ctxNetworkDetails (networkDetails) +// in the request context. +func addNetworkDetailsToRequestContext(s *state.State, r *http.Request) error { + networkName, err := url.PathUnescape(mux.Vars(r)["networkName"]) + if err != nil { + return err + } + + requestProjectName := request.ProjectParam(r) + effectiveProjectName, requestProject, err := project.NetworkProject(s.DB.Cluster, requestProjectName) + if err != nil { + return fmt.Errorf("Failed to check project %q network feature: %w", requestProjectName, err) + } + + request.SetCtxValue(r, request.CtxEffectiveProjectName, effectiveProjectName) + request.SetCtxValue(r, ctxNetworkDetails, networkDetails{ + networkName: networkName, + requestProject: *requestProject, + }) + + return nil +} + +// profileAccessHandler calls addProfileDetailsToRequestContext, then uses the details to perform an access check with +// the given auth.Entitlement. +func networkAccessHandler(entitlement auth.Entitlement) func(d *Daemon, r *http.Request) response.Response { + return func(d *Daemon, r *http.Request) response.Response { + s := d.State() + err := addNetworkDetailsToRequestContext(s, r) + if err != nil { + return response.SmartError(err) + } + + details, err := request.GetCtxValue[networkDetails](r.Context(), ctxNetworkDetails) + if err != nil { + return response.SmartError(err) + } + + err = s.Authorizer.CheckPermission(r.Context(), entity.NetworkURL(details.requestProject.Name, details.networkName), entitlement) + if err != nil { + return response.SmartError(err) + } + + return response.EmptySyncResponse + } } // API endpoints @@ -170,12 +227,13 @@ var networkStateCmd = APIEndpoint{ func networksGet(d *Daemon, r *http.Request) response.Response { s := d.State() - projectName, reqProject, err := project.NetworkProject(s.DB.Cluster, request.ProjectParam(r)) + requestProjectName := request.ProjectParam(r) + effectiveProjectName, reqProject, err := project.NetworkProject(s.DB.Cluster, requestProjectName) if err != nil { return response.SmartError(err) } - request.SetCtxValue(r, request.CtxEffectiveProjectName, projectName) + request.SetCtxValue(r, request.CtxEffectiveProjectName, effectiveProjectName) recursion := util.IsRecursionRequest(r) @@ -183,7 +241,7 @@ func networksGet(d *Daemon, r *http.Request) response.Response { err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { // Get list of managed networks (that may or may not have network interfaces on the host). - networkNames, err = tx.GetNetworks(ctx, projectName) + networkNames, err = tx.GetNetworks(ctx, effectiveProjectName) return err }) @@ -192,7 +250,7 @@ func networksGet(d *Daemon, r *http.Request) response.Response { } // Get list of actual network interfaces on the host as well if the effective project is Default. - if projectName == api.ProjectDefaultName { + if effectiveProjectName == api.ProjectDefaultName { ifaces, err := net.Interfaces() if err != nil { return response.InternalError(err) @@ -219,14 +277,14 @@ func networksGet(d *Daemon, r *http.Request) response.Response { resultString := []string{} resultMap := []api.Network{} for _, networkName := range networkNames { - if !userHasPermission(entity.NetworkURL(projectName, networkName)) { + if !userHasPermission(entity.NetworkURL(requestProjectName, networkName)) { continue } if !recursion { resultString = append(resultString, fmt.Sprintf("/%s/networks/%s", version.APIVersion, networkName)) } else { - net, err := doNetworkGet(s, r, s.ServerClustered, projectName, reqProject.Config, networkName) + net, err := doNetworkGet(s, r, s.ServerClustered, requestProjectName, reqProject.Config, networkName) if err != nil { continue } @@ -796,12 +854,7 @@ func networkGet(d *Daemon, r *http.Request) response.Response { return resp } - projectName, reqProject, err := project.NetworkProject(s.DB.Cluster, request.ProjectParam(r)) - if err != nil { - return response.SmartError(err) - } - - networkName, err := url.PathUnescape(mux.Vars(r)["networkName"]) + details, err := request.GetCtxValue[networkDetails](r.Context(), ctxNetworkDetails) if err != nil { return response.SmartError(err) } @@ -811,7 +864,7 @@ func networkGet(d *Daemon, r *http.Request) response.Response { allNodes = true } - n, err := doNetworkGet(s, r, allNodes, projectName, reqProject.Config, networkName) + n, err := doNetworkGet(s, r, allNodes, details.requestProject.Name, details.requestProject.Config, details.networkName) if err != nil { return response.SmartError(err) } @@ -824,20 +877,25 @@ func networkGet(d *Daemon, r *http.Request) response.Response { // doNetworkGet returns information about the specified network. // If the network being requested is a managed network and allNodes is true then node specific config is removed. // Otherwise if allNodes is false then the network's local status is returned. -func doNetworkGet(s *state.State, r *http.Request, allNodes bool, projectName string, reqProjectConfig map[string]string, networkName string) (api.Network, error) { +func doNetworkGet(s *state.State, r *http.Request, allNodes bool, requestProjectName string, reqProjectConfig map[string]string, networkName string) (api.Network, error) { + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) + if err != nil { + return api.Network{}, err + } + // Ignore veth pairs (for performance reasons). if strings.HasPrefix(networkName, "veth") { return api.Network{}, api.StatusErrorf(http.StatusNotFound, "Network not found") } // Get some information. - n, err := network.LoadByName(s, projectName, networkName) + n, err := network.LoadByName(s, effectiveProjectName, networkName) if err != nil && !api.StatusErrorCheck(err, http.StatusNotFound) { return api.Network{}, fmt.Errorf("Failed loading network: %w", err) } // Don't allow retrieving info about the local server interfaces when not using default project. - if projectName != api.ProjectDefaultName && n == nil { + if effectiveProjectName != api.ProjectDefaultName && n == nil { return api.Network{}, api.StatusErrorf(http.StatusNotFound, "Network not found") } @@ -865,7 +923,7 @@ func doNetworkGet(s *state.State, r *http.Request, allNodes bool, projectName st apiNet.Description = n.Description() apiNet.Type = n.Type() - err = s.Authorizer.CheckPermission(r.Context(), entity.NetworkURL(projectName, networkName), auth.EntitlementCanEdit) + err = s.Authorizer.CheckPermission(r.Context(), entity.NetworkURL(requestProjectName, networkName), auth.EntitlementCanEdit) if err != nil && !auth.IsDeniedError(err) { return api.Network{}, err } else if err == nil { @@ -906,7 +964,7 @@ func doNetworkGet(s *state.State, r *http.Request, allNodes bool, projectName st networkID = n.ID() } - usedBy, err := network.UsedBy(s, projectName, networkID, apiNet.Name, apiNet.Type, false) + usedBy, err := network.UsedBy(s, effectiveProjectName, networkID, apiNet.Name, apiNet.Type, false) if err != nil { return api.Network{}, err } @@ -954,24 +1012,24 @@ func doNetworkGet(s *state.State, r *http.Request, allNodes bool, projectName st func networkDelete(d *Daemon, r *http.Request) response.Response { s := d.State() - projectName, reqProject, err := project.NetworkProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - networkName, err := url.PathUnescape(mux.Vars(r)["networkName"]) + details, err := request.GetCtxValue[networkDetails](r.Context(), ctxNetworkDetails) if err != nil { return response.SmartError(err) } // Get the existing network. - n, err := network.LoadByName(s, projectName, networkName) + n, err := network.LoadByName(s, effectiveProjectName, details.networkName) if err != nil { return response.SmartError(fmt.Errorf("Failed loading network: %w", err)) } // Check if project allows access to network. - if !project.NetworkAllowed(reqProject.Config, networkName, n.IsManaged()) { + if !project.NetworkAllowed(details.requestProject.Config, details.networkName, n.IsManaged()) { return response.SmartError(api.StatusErrorf(http.StatusNotFound, "Network not found")) } @@ -1029,7 +1087,7 @@ func networkDelete(d *Daemon, r *http.Request) response.Response { } requestor := request.CreateRequestor(r) - s.Events.SendLifecycle(projectName, lifecycle.NetworkDeleted.Event(n, requestor, nil)) + s.Events.SendLifecycle(effectiveProjectName, lifecycle.NetworkDeleted.Event(n, requestor, nil)) return response.EmptySyncResponse } @@ -1080,12 +1138,12 @@ func networkPost(d *Daemon, r *http.Request) response.Response { return response.BadRequest(fmt.Errorf("Renaming clustered network not supported")) } - projectName, reqProject, err := project.NetworkProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - networkName, err := url.PathUnescape(mux.Vars(r)["networkName"]) + details, err := request.GetCtxValue[networkDetails](r.Context(), ctxNetworkDetails) if err != nil { return response.SmartError(err) } @@ -1099,13 +1157,13 @@ func networkPost(d *Daemon, r *http.Request) response.Response { } // Get the existing network. - n, err := network.LoadByName(s, projectName, networkName) + n, err := network.LoadByName(s, effectiveProjectName, details.networkName) if err != nil { return response.SmartError(fmt.Errorf("Failed loading network: %w", err)) } // Check if project allows access to network. - if !project.NetworkAllowed(reqProject.Config, networkName, n.IsManaged()) { + if !project.NetworkAllowed(details.requestProject.Config, details.networkName, n.IsManaged()) { return response.SmartError(api.StatusErrorf(http.StatusNotFound, "Network not found")) } @@ -1137,7 +1195,7 @@ func networkPost(d *Daemon, r *http.Request) response.Response { err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { // Check that the name isn't already in used by an existing managed network. - networks, err = tx.GetNetworks(ctx, projectName) + networks, err = tx.GetNetworks(ctx, effectiveProjectName) return err }) @@ -1156,8 +1214,8 @@ func networkPost(d *Daemon, r *http.Request) response.Response { } requestor := request.CreateRequestor(r) - lc := lifecycle.NetworkRenamed.Event(n, requestor, map[string]any{"old_name": networkName}) - s.Events.SendLifecycle(projectName, lc) + lc := lifecycle.NetworkRenamed.Event(n, requestor, map[string]any{"old_name": details.networkName}) + s.Events.SendLifecycle(effectiveProjectName, lc) return response.SyncResponseLocation(true, nil, lc.Source) } @@ -1210,24 +1268,24 @@ func networkPut(d *Daemon, r *http.Request) response.Response { return resp } - projectName, reqProject, err := project.NetworkProject(s.DB.Cluster, request.ProjectParam(r)) + effectiveProjectName, err := request.GetCtxValue[string](r.Context(), request.CtxEffectiveProjectName) if err != nil { return response.SmartError(err) } - networkName, err := url.PathUnescape(mux.Vars(r)["networkName"]) + details, err := request.GetCtxValue[networkDetails](r.Context(), ctxNetworkDetails) if err != nil { return response.SmartError(err) } // Get the existing network. - n, err := network.LoadByName(s, projectName, networkName) + n, err := network.LoadByName(s, effectiveProjectName, details.networkName) if err != nil { return response.SmartError(fmt.Errorf("Failed loading network: %w", err)) } // Check if project allows access to network. - if !project.NetworkAllowed(reqProject.Config, networkName, n.IsManaged()) { + if !project.NetworkAllowed(details.requestProject.Config, details.networkName, n.IsManaged()) { return response.SmartError(api.StatusErrorf(http.StatusNotFound, "Network not found")) } @@ -1287,10 +1345,10 @@ func networkPut(d *Daemon, r *http.Request) response.Response { clientType := clusterRequest.UserAgentClientType(r.Header.Get("User-Agent")) - response := doNetworkUpdate(projectName, n, req, targetNode, clientType, r.Method, s.ServerClustered) + response := doNetworkUpdate(effectiveProjectName, n, req, targetNode, clientType, r.Method, s.ServerClustered) requestor := request.CreateRequestor(r) - s.Events.SendLifecycle(projectName, lifecycle.NetworkUpdated.Event(n, requestor, nil)) + s.Events.SendLifecycle(effectiveProjectName, lifecycle.NetworkUpdated.Event(n, requestor, nil)) return response } diff --git a/lxd/profiles.go b/lxd/profiles.go index 3526076b9140..d2aaa587996e 100644 --- a/lxd/profiles.go +++ b/lxd/profiles.go @@ -25,6 +25,7 @@ import ( "github.com/canonical/lxd/lxd/project" "github.com/canonical/lxd/lxd/request" "github.com/canonical/lxd/lxd/response" + "github.com/canonical/lxd/lxd/state" "github.com/canonical/lxd/lxd/util" "github.com/canonical/lxd/shared" "github.com/canonical/lxd/shared/api" @@ -43,11 +44,68 @@ var profilesCmd = APIEndpoint{ var profileCmd = APIEndpoint{ Path: "profiles/{name}", - Delete: APIEndpointAction{Handler: profileDelete, AccessHandler: allowPermission(entity.TypeProfile, auth.EntitlementCanDelete, "name")}, - Get: APIEndpointAction{Handler: profileGet, AccessHandler: allowPermission(entity.TypeProfile, auth.EntitlementCanView, "name")}, - Patch: APIEndpointAction{Handler: profilePatch, AccessHandler: allowPermission(entity.TypeProfile, auth.EntitlementCanEdit, "name")}, - Post: APIEndpointAction{Handler: profilePost, AccessHandler: allowPermission(entity.TypeProfile, auth.EntitlementCanEdit, "name")}, - Put: APIEndpointAction{Handler: profilePut, AccessHandler: allowPermission(entity.TypeProfile, auth.EntitlementCanEdit, "name")}, + Delete: APIEndpointAction{Handler: profileDelete, AccessHandler: profileAccessHandler(auth.EntitlementCanDelete)}, + Get: APIEndpointAction{Handler: profileGet, AccessHandler: profileAccessHandler(auth.EntitlementCanView)}, + Patch: APIEndpointAction{Handler: profilePatch, AccessHandler: profileAccessHandler(auth.EntitlementCanEdit)}, + Post: APIEndpointAction{Handler: profilePost, AccessHandler: profileAccessHandler(auth.EntitlementCanEdit)}, + Put: APIEndpointAction{Handler: profilePut, AccessHandler: profileAccessHandler(auth.EntitlementCanEdit)}, +} + +// ctxProfileDetails should be used only for getting/setting profileDetails in the request context. +const ctxProfileDetails request.CtxKey = "profile-details" + +// profileDetails contains fields that are determined prior to the access check. This is set in the request context when +// addProfileDetailsToRequestContext is called. +type profileDetails struct { + profileName string + effectiveProject api.Project +} + +// addProfileDetailsToRequestContext sets request.CtxEffectiveProjectName (string) and ctxProfileDetails (profileDetails) +// in the request context. +func addProfileDetailsToRequestContext(s *state.State, r *http.Request) error { + profileName, err := url.PathUnescape(mux.Vars(r)["name"]) + if err != nil { + return err + } + + requestProjectName := request.ProjectParam(r) + effectiveProject, err := project.ProfileProject(s.DB.Cluster, requestProjectName) + if err != nil { + return fmt.Errorf("Failed to check project %q profile feature: %w", requestProjectName, err) + } + + request.SetCtxValue(r, request.CtxEffectiveProjectName, effectiveProject.Name) + request.SetCtxValue(r, ctxProfileDetails, profileDetails{ + profileName: profileName, + effectiveProject: *effectiveProject, + }) + + return nil +} + +// profileAccessHandler calls addProfileDetailsToRequestContext, then uses the details to perform an access check with +// the given auth.Entitlement. +func profileAccessHandler(entitlement auth.Entitlement) func(d *Daemon, r *http.Request) response.Response { + return func(d *Daemon, r *http.Request) response.Response { + s := d.State() + err := addProfileDetailsToRequestContext(s, r) + if err != nil { + return response.SmartError(err) + } + + details, err := request.GetCtxValue[profileDetails](r.Context(), ctxProfileDetails) + if err != nil { + return response.SmartError(err) + } + + err = s.Authorizer.CheckPermission(r.Context(), entity.ProfileURL(request.ProjectParam(r), details.profileName), entitlement) + if err != nil { + return response.SmartError(err) + } + + return response.EmptySyncResponse + } } // swagger:operation GET /1.0/profiles profiles profiles_get @@ -145,7 +203,8 @@ var profileCmd = APIEndpoint{ func profilesGet(d *Daemon, r *http.Request) response.Response { s := d.State() - p, err := project.ProfileProject(s.DB.Cluster, request.ProjectParam(r)) + requestProjectName := request.ProjectParam(r) + p, err := project.ProfileProject(s.DB.Cluster, requestProjectName) if err != nil { return response.SmartError(err) } @@ -173,7 +232,7 @@ func profilesGet(d *Daemon, r *http.Request) response.Response { if recursion { apiProfiles = make([]*api.Profile, 0, len(profiles)) for _, profile := range profiles { - if !userHasPermission(entity.ProfileURL(p.Name, profile.Name)) { + if !userHasPermission(entity.ProfileURL(requestProjectName, profile.Name)) { continue } @@ -192,7 +251,7 @@ func profilesGet(d *Daemon, r *http.Request) response.Response { } else { profileURLs = make([]string, 0, len(profiles)) for _, profile := range profiles { - profileURL := entity.ProfileURL(p.Name, profile.Name) + profileURL := entity.ProfileURL(requestProjectName, profile.Name) if userHasPermission(profileURL) { profileURLs = append(profileURLs, profileURL.String()) } @@ -391,12 +450,7 @@ func profilesPost(d *Daemon, r *http.Request) response.Response { func profileGet(d *Daemon, r *http.Request) response.Response { s := d.State() - p, err := project.ProfileProject(s.DB.Cluster, request.ProjectParam(r)) - if err != nil { - return response.SmartError(err) - } - - name, err := url.PathUnescape(mux.Vars(r)["name"]) + details, err := request.GetCtxValue[profileDetails](r.Context(), ctxProfileDetails) if err != nil { return response.SmartError(err) } @@ -404,7 +458,7 @@ func profileGet(d *Daemon, r *http.Request) response.Response { var resp *api.Profile err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { - profile, err := dbCluster.GetProfile(ctx, tx.Tx(), p.Name, name) + profile, err := dbCluster.GetProfile(ctx, tx.Tx(), details.effectiveProject.Name, details.profileName) if err != nil { return fmt.Errorf("Fetch profile: %w", err) } @@ -468,12 +522,7 @@ func profileGet(d *Daemon, r *http.Request) response.Response { func profilePut(d *Daemon, r *http.Request) response.Response { s := d.State() - p, err := project.ProfileProject(s.DB.Cluster, request.ProjectParam(r)) - if err != nil { - return response.SmartError(err) - } - - name, err := url.PathUnescape(mux.Vars(r)["name"]) + details, err := request.GetCtxValue[profileDetails](r.Context(), ctxProfileDetails) if err != nil { return response.SmartError(err) } @@ -487,7 +536,7 @@ func profilePut(d *Daemon, r *http.Request) response.Response { return response.BadRequest(err) } - err = doProfileUpdateCluster(s, p.Name, name, old) + err = doProfileUpdateCluster(s, details.effectiveProject.Name, details.profileName, old) return response.SmartError(err) } @@ -495,9 +544,9 @@ func profilePut(d *Daemon, r *http.Request) response.Response { var profile *api.Profile err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { - current, err := dbCluster.GetProfile(ctx, tx.Tx(), p.Name, name) + current, err := dbCluster.GetProfile(ctx, tx.Tx(), details.effectiveProject.Name, details.profileName) if err != nil { - return fmt.Errorf("Failed to retrieve profile %q: %w", name, err) + return fmt.Errorf("Failed to retrieve profile %q: %w", details.profileName, err) } profile, err = current.ToAPI(ctx, tx.Tx()) @@ -526,7 +575,7 @@ func profilePut(d *Daemon, r *http.Request) response.Response { return response.BadRequest(err) } - err = doProfileUpdate(s, *p, name, id, profile, req) + err = doProfileUpdate(s, details.effectiveProject, details.profileName, id, profile, req) if err == nil && !isClusterNotification(r) { // Notify all other nodes. If a node is down, it will be ignored. @@ -536,7 +585,7 @@ func profilePut(d *Daemon, r *http.Request) response.Response { } err = notifier(func(client lxd.InstanceServer) error { - return client.UseProject(p.Name).UpdateProfile(name, profile.Writable(), "") + return client.UseProject(details.effectiveProject.Name).UpdateProfile(details.profileName, profile.Writable(), "") }) if err != nil { return response.SmartError(err) @@ -544,7 +593,7 @@ func profilePut(d *Daemon, r *http.Request) response.Response { } requestor := request.CreateRequestor(r) - s.Events.SendLifecycle(p.Name, lifecycle.ProfileUpdated.Event(name, p.Name, requestor, nil)) + s.Events.SendLifecycle(details.effectiveProject.Name, lifecycle.ProfileUpdated.Event(details.profileName, details.effectiveProject.Name, requestor, nil)) return response.SmartError(err) } @@ -586,12 +635,7 @@ func profilePut(d *Daemon, r *http.Request) response.Response { func profilePatch(d *Daemon, r *http.Request) response.Response { s := d.State() - p, err := project.ProfileProject(s.DB.Cluster, request.ProjectParam(r)) - if err != nil { - return response.SmartError(err) - } - - name, err := url.PathUnescape(mux.Vars(r)["name"]) + details, err := request.GetCtxValue[profileDetails](r.Context(), ctxProfileDetails) if err != nil { return response.SmartError(err) } @@ -600,9 +644,9 @@ func profilePatch(d *Daemon, r *http.Request) response.Response { var profile *api.Profile err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { - current, err := dbCluster.GetProfile(ctx, tx.Tx(), p.Name, name) + current, err := dbCluster.GetProfile(ctx, tx.Tx(), details.effectiveProject.Name, details.profileName) if err != nil { - return fmt.Errorf("Failed to retrieve profile=%q: %w", name, err) + return fmt.Errorf("Failed to retrieve profile=%q: %w", details.profileName, err) } profile, err = current.ToAPI(ctx, tx.Tx()) @@ -676,9 +720,9 @@ func profilePatch(d *Daemon, r *http.Request) response.Response { } requestor := request.CreateRequestor(r) - s.Events.SendLifecycle(p.Name, lifecycle.ProfileUpdated.Event(name, p.Name, requestor, nil)) + s.Events.SendLifecycle(details.effectiveProject.Name, lifecycle.ProfileUpdated.Event(details.profileName, details.effectiveProject.Name, requestor, nil)) - return response.SmartError(doProfileUpdate(s, *p, name, id, profile, req)) + return response.SmartError(doProfileUpdate(s, details.effectiveProject, details.profileName, id, profile, req)) } // swagger:operation POST /1.0/profiles/{name} profiles profile_post @@ -716,17 +760,12 @@ func profilePatch(d *Daemon, r *http.Request) response.Response { func profilePost(d *Daemon, r *http.Request) response.Response { s := d.State() - p, err := project.ProfileProject(s.DB.Cluster, request.ProjectParam(r)) - if err != nil { - return response.SmartError(err) - } - - name, err := url.PathUnescape(mux.Vars(r)["name"]) + details, err := request.GetCtxValue[profileDetails](r.Context(), ctxProfileDetails) if err != nil { return response.SmartError(err) } - if name == "default" { + if details.profileName == "default" { return response.Forbidden(errors.New(`The "default" profile cannot be renamed`)) } @@ -751,20 +790,20 @@ func profilePost(d *Daemon, r *http.Request) response.Response { err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { // Check that the name isn't already in use. - _, err = dbCluster.GetProfile(ctx, tx.Tx(), p.Name, req.Name) + _, err = dbCluster.GetProfile(ctx, tx.Tx(), details.effectiveProject.Name, req.Name) if err == nil { return fmt.Errorf("Name %q already in use", req.Name) } - return dbCluster.RenameProfile(ctx, tx.Tx(), p.Name, name, req.Name) + return dbCluster.RenameProfile(ctx, tx.Tx(), details.effectiveProject.Name, details.profileName, req.Name) }) if err != nil { return response.SmartError(err) } requestor := request.CreateRequestor(r) - lc := lifecycle.ProfileRenamed.Event(req.Name, p.Name, requestor, logger.Ctx{"old_name": name}) - s.Events.SendLifecycle(p.Name, lc) + lc := lifecycle.ProfileRenamed.Event(req.Name, details.effectiveProject.Name, requestor, logger.Ctx{"old_name": details.profileName}) + s.Events.SendLifecycle(details.effectiveProject.Name, lc) return response.SyncResponseLocation(true, nil, lc.Source) } @@ -796,22 +835,17 @@ func profilePost(d *Daemon, r *http.Request) response.Response { func profileDelete(d *Daemon, r *http.Request) response.Response { s := d.State() - p, err := project.ProfileProject(s.DB.Cluster, request.ProjectParam(r)) - if err != nil { - return response.SmartError(err) - } - - name, err := url.PathUnescape(mux.Vars(r)["name"]) + details, err := request.GetCtxValue[profileDetails](r.Context(), ctxProfileDetails) if err != nil { return response.SmartError(err) } - if name == "default" { + if details.profileName == "default" { return response.Forbidden(errors.New(`The "default" profile cannot be deleted`)) } err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { - profile, err := dbCluster.GetProfile(ctx, tx.Tx(), p.Name, name) + profile, err := dbCluster.GetProfile(ctx, tx.Tx(), details.effectiveProject.Name, details.profileName) if err != nil { return err } @@ -825,14 +859,14 @@ func profileDelete(d *Daemon, r *http.Request) response.Response { return fmt.Errorf("Profile is currently in use") } - return dbCluster.DeleteProfile(ctx, tx.Tx(), p.Name, name) + return dbCluster.DeleteProfile(ctx, tx.Tx(), details.effectiveProject.Name, details.profileName) }) if err != nil { return response.SmartError(err) } requestor := request.CreateRequestor(r) - s.Events.SendLifecycle(p.Name, lifecycle.ProfileDeleted.Event(name, p.Name, requestor, nil)) + s.Events.SendLifecycle(details.effectiveProject.Name, lifecycle.ProfileDeleted.Event(details.profileName, details.effectiveProject.Name, requestor, nil)) return response.EmptySyncResponse } diff --git a/lxd/project/project.go b/lxd/project/project.go index 389e7c575f05..e9259f0e023b 100644 --- a/lxd/project/project.go +++ b/lxd/project/project.go @@ -2,6 +2,7 @@ package project import ( "context" + "database/sql" "fmt" "strings" @@ -37,7 +38,7 @@ func DNS(projectName string, instanceName string) string { // name is returned unmodified in the 2nd return value. This is suitable for passing back into Instance(). // Note: This should only be used with Instance names (because they cannot contain the project separator) and this // function relies on this rule as project names can contain the project separator. -func InstanceParts(projectInstanceName string) (string, string) { +func InstanceParts(projectInstanceName string) (projectName string, instanceName string) { i := strings.LastIndex(projectInstanceName, separator) if i < 0 { // This string is not project prefixed or is part of default project. @@ -56,7 +57,7 @@ func StorageVolume(projectName string, storageVolumeName string) string { // StorageVolumeParts takes a project prefixed storage volume name and returns the project and storage volume // name as separate variables. -func StorageVolumeParts(projectStorageVolumeName string) (string, string) { +func StorageVolumeParts(projectStorageVolumeName string) (projectName string, storageVolumeName string) { parts := strings.SplitN(projectStorageVolumeName, "_", 2) // If the given name doesn't contain any project, only return the volume name. @@ -311,3 +312,17 @@ func NetworkZoneProjectFromRecord(p *api.Project) string { return api.ProjectDefaultName } + +// ImageProject returns the effective project for images based on the value of `features.images` in the given project. +func ImageProject(ctx context.Context, tx *sql.Tx, requestProjectName string) (string, error) { + projectHasImages, err := cluster.ProjectHasImages(ctx, tx, requestProjectName) + if err != nil { + return "", err + } + + if !projectHasImages { + return api.ProjectDefaultName, nil + } + + return requestProjectName, nil +} diff --git a/lxd/storage_volumes.go b/lxd/storage_volumes.go index 4f947bbd0ee0..b81dcab20c0f 100644 --- a/lxd/storage_volumes.go +++ b/lxd/storage_volumes.go @@ -798,13 +798,23 @@ func storagePoolVolumesGet(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } + // The auth.PermissionChecker expects the url to contain the request project (not the effective project). + // So when getting networks in a single project, ensure we use the request project name. + authCheckProject := func(dbProject string) string { + if !allProjects { + return requestProjectName + } + + return dbProject + } + if util.IsRecursionRequest(r) { volumes := make([]*api.StorageVolume, 0, len(dbVolumes)) for _, dbVol := range dbVolumes { vol := &dbVol.StorageVolume volumeName, _, _ := api.GetParentAndSnapshotName(vol.Name) - if !userHasPermission(entity.StorageVolumeURL(vol.Project, vol.Location, dbVol.Pool, dbVol.Type, volumeName)) { + if !userHasPermission(entity.StorageVolumeURL(authCheckProject(vol.Project), vol.Location, dbVol.Pool, dbVol.Type, volumeName)) { continue } @@ -828,7 +838,7 @@ func storagePoolVolumesGet(d *Daemon, r *http.Request) response.Response { for _, dbVol := range dbVolumes { volumeName, _, _ := api.GetParentAndSnapshotName(dbVol.Name) - if !userHasPermission(entity.StorageVolumeURL(dbVol.Project, dbVol.Location, dbVol.Pool, dbVol.Type, volumeName)) { + if !userHasPermission(entity.StorageVolumeURL(authCheckProject(dbVol.Project), dbVol.Location, dbVol.Pool, dbVol.Type, volumeName)) { continue } @@ -2691,7 +2701,7 @@ func addStoragePoolVolumeDetailsToRequestContext(s *state.State, r *http.Request // Convert the volume type name to our internal integer representation. volumeType, err := storagePools.VolumeTypeNameToDBType(volumeTypeName) if err != nil { - return api.StatusErrorf(http.StatusBadRequest, err.Error()) + return api.StatusErrorf(http.StatusBadRequest, "Failed to get storage volume type: %w", err) } details.volumeType = volumeType diff --git a/test/includes/storage.sh b/test/includes/storage.sh index 9cb4fa98e5ab..9f3f001c8faa 100644 --- a/test/includes/storage.sh +++ b/test/includes/storage.sh @@ -130,3 +130,56 @@ umount_loops() { fi } +create_object_storage_pool() { + poolName="${1}" + lxd_backend=$(storage_backend "$LXD_DIR") + + # Pool cannot already exist. + if lxc storage show "${poolName}"; then + echo "Storage pool pool ${poolName} already exists" + exit 1 + fi + + # Check cephobject.radosgw.endpoint is required for cephobject pools. + if [ "${lxd_backend}" = "ceph" ]; then + lxc storage create "${poolName}" cephobject cephobject.radosgw.endpoint="${LXD_CEPH_CEPHOBJECT_RADOSGW}" + else + + # Create a loop device for dir pools as MinIO doesn't support running on tmpfs (which the test suite can do). + # This is because tmpfs does not support O_direct which MinIO requires. This landed in kernel 6.6 (https://kernelnewbies.org/Linux_6.6#TMPFS). + if [ "${lxd_backend}" = "dir" ]; then + mkdir -p "${TEST_DIR}/s3/${poolName}" + configure_loop_device loop_file_1 loop_device_1 + # shellcheck disable=SC2154 + mkfs.ext4 "${loop_device_1}" + mount "${loop_device_1}" "${TEST_DIR}/s3/${poolName}" + mkdir "${TEST_DIR}/s3/${poolName}/objects" + lxc storage create "${poolName}" dir source="${TEST_DIR}/s3/${poolName}/objects" + # shellcheck disable=SC2154 + echo "${loop_device_1}" > "${TEST_DIR}/s3/${poolName}/dev" + # shellcheck disable=SC2154 + echo "${loop_file_1}" > "${TEST_DIR}/s3/${poolName}/file" + else + lxc storage create "${poolName}" "${lxd_backend}" + fi + + buckets_addr="127.0.0.1:$(local_tcp_port)" + lxc config set core.storage_buckets_address "${buckets_addr}" + fi +} + +delete_object_storage_pool() { + poolName="${1}" + lxd_backend=$(storage_backend "$LXD_DIR") + + lxc storage delete "${poolName}" + if [ "$lxd_backend" = "dir" ]; then + loop_file="$(cat "${TEST_DIR}/s3/${poolName}/file")" + loop_device="$(cat "${TEST_DIR}/s3/${poolName}/dev")" + umount "${TEST_DIR}/s3/${poolName}" + rmdir "${TEST_DIR}/s3/${poolName}" + + # shellcheck disable=SC2154 + deconfigure_loop_device "${loop_file}" "${loop_device}" + fi +} \ No newline at end of file diff --git a/test/suites/auth.sh b/test/suites/auth.sh index f058e9a04c8d..c227c6de1a83 100644 --- a/test/suites/auth.sh +++ b/test/suites/auth.sh @@ -133,6 +133,9 @@ EOF # Perform access checks fine_grained_authorization + # Perform access check compatibility with project feature flags + auth_project_features + # Cleanup lxc auth group delete test-group lxc auth identity-provider-group delete test-idp-group @@ -191,7 +194,7 @@ fine_grained_authorization() { # Change permission to "user" for instance "user-foo" lxc auth group permission add test-group instance user-foo user project=default - # To exec into an instance, the test-group will also need `can_view_events` for the project. + # To exec into an instance, Members of test-group will also need `can_view_events` for the project. # This is because the client uses the events API to figure out when the operation is finished. # Ideally we would use operations for this instead or allow more fine-grained filtering on events. lxc auth group permission add test-group project default can_view_events @@ -444,3 +447,365 @@ user_is_instance_user() { # We can't edit the instance though ! lxc_remote config set "oidc:${instance_name}" user.fizz=buzz || false } + +auth_project_features() { + # test-group must have no permissions to start the test. + [ "$(lxc query /1.0/auth/groups/test-group | jq '.permissions | length')" -eq 0 ] + + # Create project blah + lxc project create blah + + # Validate view with no permissions + [ "$(lxc_remote project list oidc: --format csv | wc -l)" -eq 0 ] + + # Allow operator permissions on project blah + lxc auth group permission add test-group project blah operator + + # Confirm we can still view storage pools + [ "$(lxc_remote storage list oidc: --format csv | wc -l)" = 1 ] + + # Confirm we cannot view storage pool configuration + pool_name="$(lxc_remote storage list oidc: --format csv | cut -d, -f1)" + [ "$(lxc_remote storage get "oidc:${pool_name}" source)" = "" ] + + # Validate restricted view + ! lxc_remote project list oidc: --format csv | grep -w ^default || false + lxc_remote project list oidc: --format csv | grep -w ^blah + + # Validate that the restricted caller cannot edit or delete the project. + ! lxc_remote project set oidc:blah user.foo=bar || false + ! lxc_remote project delete oidc:blah || false + + # Validate restricted caller cannot create projects. + ! lxc_remote project create oidc:blah1 || false + + # Validate restricted caller cannot see resources in projects they do not have access to (the call will not fail, but + # the lists should be empty + [ "$(lxc_remote list oidc: --project default --format csv)" = "" ] + [ "$(lxc_remote profile list oidc: --project default --format csv)" = "" ] + [ "$(lxc_remote network list oidc: --project default --format csv)" = "" ] + [ "$(lxc_remote operation list oidc: --project default --format csv)" = "" ] + [ "$(lxc_remote network zone list oidc: --project default --format csv)" = "" ] + [ "$(lxc_remote storage volume list "oidc:${pool_name}" --project default --format csv)" = "" ] + [ "$(lxc_remote storage bucket list "oidc:${pool_name}" --project default --format csv)" = "" ] + + ### Validate images. + test_image_fingerprint="$(lxc image info testimage --project default | awk '/^Fingerprint/ {print $2}')" + + # We can always list images, but there are no public images in the default project now, so the list should be empty. + [ "$(lxc_remote image list oidc: --project default --format csv)" = "" ] + ! lxc_remote image show oidc:testimage --project default || false + + # Set the image to public and ensure we can view it. + lxc image show testimage --project default | sed -e "s/public: false/public: true/" | lxc image edit testimage --project default + [ "$(lxc_remote image list oidc: --project default --format csv | wc -l)" = 1 ] + lxc_remote image show oidc:testimage --project default + + # Check we can export the public image: + lxc image export oidc:testimage "${TEST_DIR}/" --project default + [ "${test_image_fingerprint}" = "$(sha256sum "${TEST_DIR}/${test_image_fingerprint}.tar.xz" | cut -d' ' -f1)" ] + + # While the image is public, copy it to the blah project and create an alias for it. + lxc_remote image copy oidc:testimage oidc: --project default --target-project blah + lxc_remote image alias create oidc:testimage "${test_image_fingerprint}" --project blah + + # Restore privacy on the test image in the default project. + lxc image show testimage --project default | sed -e "s/public: true/public: false/" | lxc image edit testimage --project default + + # Set up a profile in the blah project. Additionally ensures project operator can edit profiles. + lxc profile show default | lxc_remote profile edit oidc:default --project blah + + # Create an instance (using the test image copied from the default project while it was public). + lxc_remote init testimage oidc:blah-instance --project blah + + # Create a custom volume. + lxc_remote storage volume create "oidc:${pool_name}" blah-volume --project blah + + # There should now be two volume URLs, one instance, one image, and one profile URL in the used-by list. + [ "$(lxc_remote project list oidc: --format csv | cut -d, -f9)" = "5" ] + + # Delete resources in project blah so that we can modify project features. + lxc_remote delete oidc:blah-instance --project blah + lxc_remote storage volume delete "oidc:${pool_name}" blah-volume --project blah + lxc_remote image delete "oidc:${test_image_fingerprint}" --project blah + + # Ensure we can create and view resources that are not enabled for the project (e.g. their effective project is + # the default project). + + ### IMAGES (initial value is true for new projects) + + # Unset the images feature (the default is false). + lxc project unset blah features.images + + # The test image in the default project *not* should be visible by default via project blah. + ! lxc_remote image info "oidc:${test_image_fingerprint}" --project blah || false + ! lxc_remote image show "oidc:${test_image_fingerprint}" --project blah || false + test_image_fingerprint_short="$(echo "${test_image_fingerprint}" | cut -c1-12)" + ! lxc_remote image list oidc: --project blah | grep -F "${test_image_fingerprint_short}" || false + + # Make the images in the default project viewable to members of test-group + lxc auth group permission add test-group project default can_view_images + + # The test image in the default project should now be visible via project blah. + lxc_remote image info "oidc:${test_image_fingerprint}" --project blah + lxc_remote image show "oidc:${test_image_fingerprint}" --project blah + lxc_remote image list oidc: --project blah | grep -F "${test_image_fingerprint_short}" + + # Members of test-group can view it via project default. (This is true even though they do not have can_view on project default). + lxc_remote image info "oidc:${test_image_fingerprint}" --project default + lxc_remote image show "oidc:${test_image_fingerprint}" --project default + lxc_remote image list oidc: --project default | grep -F "${test_image_fingerprint_short}" + + # Members of test-group cannot edit the image. + ! lxc_remote image set-property "oidc:${test_image_fingerprint}" requirements.secureboot true --project blah || false + ! lxc_remote image unset-property "oidc:${test_image_fingerprint}" requirements.secureboot --project blah || false + + # Members of test-group cannot delete the image. + ! lxc_remote image delete "oidc:${test_image_fingerprint}" --project blah || false + + # Delete it anyway to test that we can import a new one. + lxc image delete "${test_image_fingerprint}" --project default + + # Members of test-group can create images. + lxc_remote image import "${TEST_DIR}/${test_image_fingerprint}.tar.xz" oidc: --project blah + lxc_remote image alias create oidc:testimage "${test_image_fingerprint}" --project blah + + # We can view the image we've created via project blah (whose effective project is default) because we've granted the + # group permission to view all images in the default project. + lxc_remote image show "oidc:${test_image_fingerprint}" --project blah + lxc_remote image show "oidc:${test_image_fingerprint}" --project default + + # Image clean up + lxc image delete "${test_image_fingerprint}" --project default + lxc auth group permission remove test-group project default can_view_images + rm "${TEST_DIR}/${test_image_fingerprint}.tar.xz" + + ### NETWORKS (initial value is false in new projects). + + # Create a network in the default project. + networkName="net$$" + lxc network create "${networkName}" --project default + + # The network we created in the default project is not visible in project blah. + ! lxc_remote network show "oidc:${networkName}" --project blah || false + ! lxc_remote network list oidc: --project blah | grep -F "${networkName}" || false + + # Make networks in the default project viewable to members of test-group + lxc auth group permission add test-group project default can_view_networks + + # The network we created in the default project is now visible in project blah. + lxc_remote network show "oidc:${networkName}" --project blah + lxc_remote network list oidc: --project blah | grep -F "${networkName}" + + # Members of test-group can view it via project default. + lxc_remote network show "oidc:${networkName}" --project default + lxc_remote network list oidc: --project default | grep -F "${networkName}" + + # Members of test-group cannot edit the network. + ! lxc_remote network set "oidc:${networkName}" user.foo=bar --project blah || false + + # Members of test-group cannot delete the network. + ! lxc_remote network delete "oidc:${networkName}" --project blah || false + + # Create a network in the blah project. + lxc_remote network create oidc:blah-network --project blah + + # The network is visible only because we have granted view access on networks in the default project. + lxc_remote network show oidc:blah-network --project blah + lxc_remote network list oidc: --project blah | grep blah-network + + # Members of test-group can view it via the default project. + lxc_remote network show oidc:blah-network --project default + + # Members of test-group cannot edit the network. + ! lxc_remote network set oidc:blah-network user.foo=bar --project blah || false + + # Members of test-group cannot delete the network. + ! lxc_remote network delete oidc:blah-network --project blah || false + + # Network clean up + lxc network delete "${networkName}" --project blah + lxc network delete blah-network --project blah + lxc auth group permission remove test-group project default can_view_networks + + ### NETWORK ZONES (initial value is false in new projects). + + # Create a network zone in the default project. + zoneName="zone$$" + lxc network zone create "${zoneName}" --project default + + # The network zone we created in the default project is *not* visible in project blah. + ! lxc_remote network zone show "oidc:${zoneName}" --project blah || false + ! lxc_remote network zone list oidc: --project blah | grep -F "${zoneName}" || false + + # Allow view access to network zones in the default project. + lxc auth group permission add test-group project default can_view_network_zones + + # Members of test-group can now view the network zone via the default project and via the blah project. + lxc_remote network zone show "oidc:${zoneName}" --project default + lxc_remote network zone list oidc: --project default | grep -F "${zoneName}" + lxc_remote network zone show "oidc:${zoneName}" --project blah + lxc_remote network zone list oidc: --project blah | grep -F "${zoneName}" + + # Members of test-group cannot edit the network zone. + ! lxc_remote network zone set "oidc:${zoneName}" user.foo=bar --project blah || false + + # Members of test-group can delete the network zone. + ! lxc_remote network zone delete "oidc:${zoneName}" --project blah || false + + # Create a network zone in the blah project. + lxc_remote network zone create oidc:blah-zone --project blah + + # Network zone is visible to members of test-group in project blah (because they can view network zones in the default project). + lxc_remote network zone show oidc:blah-zone --project blah + lxc_remote network zone list oidc: --project blah | grep blah-zone + lxc_remote network zone show oidc:blah-zone --project default + lxc_remote network zone list oidc: --project default | grep blah-zone + + # Members of test-group cannot delete the network zone. + ! lxc_remote network zone delete oidc:blah-zone --project blah || false + + # Network zone clean up + lxc network zone delete "${zoneName}" --project blah + lxc network zone delete blah-zone --project blah + lxc auth group permission remove test-group project default can_view_network_zones + + ### PROFILES (initial value is true for new projects) + + # Unset the profiles feature (the default is false). + lxc project unset blah features.profiles + + # Create a profile in the default project. + profileName="prof$$" + lxc profile create "${profileName}" --project default + + # The profile we created in the default project is not visible in project blah. + ! lxc_remote profile show "oidc:${profileName}" --project blah || false + ! lxc_remote profile list oidc: --project blah | grep -F "${profileName}" || false + + # Grant members of test-group permission to view profiles in the default project + lxc auth group permission add test-group project default can_view_profiles + + # The profile we just created is now visible via the default project and via the blah project + lxc_remote profile show "oidc:${profileName}" --project default + lxc_remote profile list oidc: --project default | grep -F "${profileName}" + lxc_remote profile show "oidc:${profileName}" --project blah + lxc_remote profile list oidc: --project blah | grep -F "${profileName}" + + # Members of test-group cannot edit the profile. + ! lxc_remote profile set "oidc:${profileName}" user.foo=bar --project blah || false + + # Members of test-group cannot delete the profile. + ! lxc_remote profile delete "oidc:${profileName}" --project blah || false + + # Create a profile in the blah project. + lxc_remote profile create oidc:blah-profile --project blah + + # Profile is visible to members of test-group in project blah and project default. + lxc_remote profile show oidc:blah-profile --project blah + lxc_remote profile list oidc: --project blah | grep blah-profile + lxc_remote profile show oidc:blah-profile --project default + lxc_remote profile list oidc: --project default | grep blah-profile + + # Members of test-group cannot delete the profile. + ! lxc_remote profile delete oidc:blah-profile --project blah || false + + # Profile clean up + lxc profile delete "${profileName}" --project blah + lxc profile delete blah-profile --project blah + lxc auth group permission remove test-group project default can_view_profiles + + ### STORAGE VOLUMES (initial value is true for new projects) + + # Unset the storage volumes feature (the default is false). + lxc project unset blah features.storage.volumes + + # Create a storage volume in the default project. + volName="vol$$" + lxc storage volume create "${pool_name}" "${volName}" --project default + + # The storage volume we created in the default project is not visible in project blah. + ! lxc_remote storage volume show "oidc:${pool_name}" "${volName}" --project blah || false + ! lxc_remote storage volume list "oidc:${pool_name}" --project blah | grep -F "${volName}" || false + + # Grant members of test-group permission to view storage volumes in project default + lxc auth group permission add test-group project default can_view_storage_volumes + + # Members of test-group can't view it via project default and project blah. + lxc_remote storage volume show "oidc:${pool_name}" "${volName}" --project default + lxc_remote storage volume list "oidc:${pool_name}" --project default | grep -F "${volName}" + lxc_remote storage volume show "oidc:${pool_name}" "${volName}" --project blah + lxc_remote storage volume list "oidc:${pool_name}" --project blah | grep -F "${volName}" + + # Members of test-group cannot edit the storage volume. + ! lxc_remote storage volume set "oidc:${pool_name}" "${volName}" user.foo=bar --project blah || false + + # Members of test-group cannot delete the storage volume. + ! lxc_remote storage volume delete "oidc:${pool_name}" "${volName}" --project blah || false + + # Create a storage volume in the blah project. + lxc_remote storage volume create "oidc:${pool_name}" blah-volume --project blah + + # Storage volume is visible to members of test-group in project blah (because they can view volumes in the default project). + lxc_remote storage volume show "oidc:${pool_name}" blah-volume --project blah + lxc_remote storage volume list "oidc:${pool_name}" --project blah | grep blah-volume + lxc_remote storage volume show "oidc:${pool_name}" blah-volume --project default + lxc_remote storage volume list "oidc:${pool_name}" --project default | grep blah-volume + + # Members of test-group cannot delete the storage volume. + ! lxc_remote storage volume delete "oidc:${pool_name}" blah-volume --project blah || false + + # Storage volume clean up + lxc storage volume delete "${pool_name}" "${volName}" + lxc storage volume delete "${pool_name}" blah-volume + lxc auth group permission remove test-group project default can_view_storage_volumes + + ### STORAGE BUCKETS (initial value is true for new projects) + + # Create a storage pool to use with object storage. + create_object_storage_pool s3 + + # Unset the storage buckets feature (the default is false). + lxc project unset blah features.storage.buckets + + # Create a storage bucket in the default project. + bucketName="bucket$$" + lxc storage bucket create s3 "${bucketName}" --project default + + # The storage bucket we created in the default project is not visible in project blah. + ! lxc_remote storage bucket show oidc:s3 "${bucketName}" --project blah || false + ! lxc_remote storage bucket list oidc:s3 --project blah | grep -F "${bucketName}" || false + + # Grant view permission on storage buckets in project default to members of test-group + lxc auth group permission add test-group project default can_view_storage_buckets + + # Members of test-group can now view the bucket via project default and project blah. + lxc_remote storage bucket show oidc:s3 "${bucketName}" --project default + lxc_remote storage bucket list oidc:s3 --project default | grep -F "${bucketName}" + lxc_remote storage bucket show oidc:s3 "${bucketName}" --project blah + lxc_remote storage bucket list oidc:s3 --project blah | grep -F "${bucketName}" + + # Members of test-group cannot edit the storage bucket. + ! lxc_remote storage bucket set oidc:s3 "${bucketName}" user.foo=bar --project blah || false + + # Members of test-group cannot delete the storage bucket. + ! lxc_remote storage bucket delete oidc:s3 "${bucketName}" --project blah || false + + # Create a storage bucket in the blah project. + lxc_remote storage bucket create oidc:s3 blah-bucket --project blah + + # Storage bucket is visible to members of test-group in project blah (because they can view buckets in the default project). + lxc_remote storage bucket show oidc:s3 blah-bucket --project blah + lxc_remote storage bucket list oidc:s3 --project blah | grep blah-bucket + + # Members of test-group cannot delete the storage bucket. + ! lxc_remote storage bucket delete oidc:s3 blah-bucket --project blah || false + + # Cleanup storage buckets + lxc storage bucket delete s3 blah-bucket --project blah + lxc storage bucket delete s3 "${bucketName}" --project blah + delete_object_storage_pool s3 + + # General clean up + lxc project delete blah +} \ No newline at end of file diff --git a/test/suites/storage_buckets.sh b/test/suites/storage_buckets.sh index 4c7a84057024..17faa4f8d194 100644 --- a/test/suites/storage_buckets.sh +++ b/test/suites/storage_buckets.sh @@ -42,33 +42,16 @@ test_storage_buckets() { return fi - poolName=$(lxc profile device get default root pool) + poolName="s3" bucketPrefix="lxd$$" + create_object_storage_pool "${poolName}" + # Check cephobject.radosgw.endpoint is required for cephobject pools. if [ "$lxd_backend" = "ceph" ]; then - ! lxc storage create s3 cephobject || false - lxc storage create s3 cephobject cephobject.radosgw.endpoint="${LXD_CEPH_CEPHOBJECT_RADOSGW}" - lxc storage show s3 - poolName="s3" s3Endpoint="${LXD_CEPH_CEPHOBJECT_RADOSGW}" else - # Create a loop device for dir pools as MinIO doesn't support running on tmpfs (which the test suite can do). - if [ "$lxd_backend" = "dir" ]; then - configure_loop_device loop_file_1 loop_device_1 - # shellcheck disable=SC2154 - mkfs.ext4 "${loop_device_1}" - mkdir "${TEST_DIR}/${bucketPrefix}" - mount "${loop_device_1}" "${TEST_DIR}/${bucketPrefix}" - losetup -d "${loop_device_1}" - mkdir "${TEST_DIR}/${bucketPrefix}/s3" - lxc storage create s3 dir source="${TEST_DIR}/${bucketPrefix}/s3" - poolName="s3" - fi - - buckets_addr="127.0.0.1:$(local_tcp_port)" - lxc config set core.storage_buckets_address "${buckets_addr}" - s3Endpoint="https://${buckets_addr}" + s3Endpoint="https://$(lxc config get core.storage_buckets_address)" fi # Check bucket name validation. @@ -192,15 +175,5 @@ EOF ! lxc storage bucket list "${poolName}" | grep -F "${bucketPrefix}.foo" || false ! lxc storage bucket show "${poolName}" "${bucketPrefix}.foo" || false - if [ "$lxd_backend" = "ceph" ] || [ "$lxd_backend" = "dir" ]; then - lxc storage delete "${poolName}" - fi - - if [ "$lxd_backend" = "dir" ]; then - umount "${TEST_DIR}/${bucketPrefix}" - rmdir "${TEST_DIR}/${bucketPrefix}" - - # shellcheck disable=SC2154 - deconfigure_loop_device "${loop_file_1}" "${loop_device_1}" - fi + delete_object_storage_pool "${poolName}" } diff --git a/test/suites/tls_restrictions.sh b/test/suites/tls_restrictions.sh index d898d864b078..461a496c7384 100644 --- a/test/suites/tls_restrictions.sh +++ b/test/suites/tls_restrictions.sh @@ -73,9 +73,8 @@ test_tls_restrictions() { lxc_remote image show localhost:testimage --project default # Check we can export the public image: - lxc image export localhost:testimage "${LXD_DIR}/" --project default - [ "${test_image_fingerprint}" = "$(sha256sum "${LXD_DIR}/${test_image_fingerprint}.tar.xz" | cut -d' ' -f1)" ] - rm "${LXD_DIR}/${test_image_fingerprint}.tar.xz" + lxc image export localhost:testimage "${TEST_DIR}/" --project default + [ "${test_image_fingerprint}" = "$(sha256sum "${TEST_DIR}/${test_image_fingerprint}.tar.xz" | cut -d' ' -f1)" ] # While the image is public, copy it to the blah project and create an alias for it. lxc_remote image copy localhost:testimage localhost: --project default --target-project blah @@ -96,43 +95,240 @@ test_tls_restrictions() { # There should now be two volume URLs, one instance, one image, and one profile URL in the used-by list. [ "$(lxc_remote project list localhost: --format csv | cut -d, -f9)" = "5" ] - # Delete resources in project blah so that we can modify project limits. + # Delete resources in project blah so that we can modify project features. lxc_remote delete localhost:blah-instance --project blah lxc_remote storage volume delete "localhost:${pool_name}" blah-volume --project blah - test_image_fingerprint="$(lxc_remote image list localhost: --format csv --columns f --project blah)" lxc_remote image delete "localhost:${test_image_fingerprint}" --project blah # Ensure we can create and view resources that are not enabled for the project (e.g. their effective project is # the default project). - # Networks are disabled when projects are created. + ### IMAGES (initial value is true for new projects) + + # Unset the images feature (the default is false). + lxc project unset blah features.images + + # The test image in the default project should be visible via project blah. + lxc_remote image info "localhost:${test_image_fingerprint}" --project blah + lxc_remote image show "localhost:${test_image_fingerprint}" --project blah + test_image_fingerprint_short="$(echo "${test_image_fingerprint}" | cut -c1-12)" + lxc_remote image list localhost: --project blah | grep -F "${test_image_fingerprint_short}" + + # The restricted client can't view it via project default. + ! lxc_remote image info "localhost:${test_image_fingerprint}" --project default || false + ! lxc_remote image show "localhost:${test_image_fingerprint}" --project default || false + ! lxc_remote image list localhost: --project default | grep -F "${test_image_fingerprint_short}" || false + + # The restricted client can edit the image. + lxc_remote image set-property "localhost:${test_image_fingerprint}" requirements.secureboot true --project blah + lxc_remote image unset-property "localhost:${test_image_fingerprint}" requirements.secureboot --project blah + + # The restricted client can delete the image. + lxc_remote image delete "localhost:${test_image_fingerprint}" --project blah + + # The restricted client can create images. + lxc_remote image import "${TEST_DIR}/${test_image_fingerprint}.tar.xz" localhost: --project blah + + # Clean up + lxc_remote image delete "localhost:${test_image_fingerprint}" --project blah + + + ### NETWORKS (initial value is false in new projects). + + # Create a network in the default project. + networkName="net$$" + lxc network create "${networkName}" --project default + + # The network we created in the default project is visible in project blah. + lxc_remote network show "localhost:${networkName}" --project blah + lxc_remote network list localhost: --project blah | grep -F "${networkName}" + + # The restricted client can't view it via project default. + ! lxc_remote network show "localhost:${networkName}" --project default || false + ! lxc_remote network list localhost: --project default | grep -F "${networkName}" || false + + # The restricted client can edit the network. + lxc_remote network set "localhost:${networkName}" user.foo=bar --project blah + + # The restricted client can delete the network. + lxc_remote network delete "localhost:${networkName}" --project blah + + # Create a network in the blah project. lxc_remote network create localhost:blah-network --project blah + + # Network is visible to restricted client in project blah. lxc_remote network show localhost:blah-network --project blah lxc_remote network list localhost: --project blah | grep blah-network - lxc_remote network rm localhost:blah-network --project blah - # Network zones are disabled when projects are created. + # The network is actually in the default project. + lxc network show blah-network --project default + + # The restricted client can't view it via the default project. + ! lxc_remote network show localhost:blah-network --project default || false + + # The restricted client can delete the network. + lxc_remote network delete localhost:blah-network --project blah + + + ### NETWORK ZONES (initial value is false in new projects). + + # Create a network zone in the default project. + zoneName="zone$$" + lxc network zone create "${zoneName}" --project default + + # The network zone we created in the default project is visible in project blah. + lxc_remote network zone show "localhost:${zoneName}" --project blah + lxc_remote network zone list localhost: --project blah | grep -F "${zoneName}" + + # The restricted client can't view it via project default. + ! lxc_remote network zone show "localhost:${zoneName}" --project default || false + ! lxc_remote network zone list localhost: --project default | grep -F "${zoneName}" || false + + # The restricted client can edit the network zone. + lxc_remote network zone set "localhost:${zoneName}" user.foo=bar --project blah + + # The restricted client can delete the network zone. + lxc_remote network zone delete "localhost:${zoneName}" --project blah + + # Create a network zone in the blah project. lxc_remote network zone create localhost:blah-zone --project blah + + # Network zone is visible to restricted client in project blah. lxc_remote network zone show localhost:blah-zone --project blah lxc_remote network zone list localhost: --project blah | grep blah-zone + + # The network zone is actually in the default project. + lxc network zone show blah-zone --project default + + # The restricted client can't view it via the default project. + ! lxc_remote network zone show localhost:blah-zone --project default || false + + # The restricted client can delete the network zone. lxc_remote network zone delete localhost:blah-zone --project blah + + ### PROFILES (initial value is true for new projects) + # Unset the profiles feature (the default is false). lxc project unset blah features.profiles + + # Create a profile in the default project. + profileName="prof$$" + lxc profile create "${profileName}" --project default + + # The profile we created in the default project is visible in project blah. + lxc_remote profile show "localhost:${profileName}" --project blah + lxc_remote profile list localhost: --project blah | grep -F "${profileName}" + + # The restricted client can't view it via project default. + ! lxc_remote profile show "localhost:${profileName}" --project default || false + ! lxc_remote profile list localhost: --project default | grep -F "${profileName}" || false + + # The restricted client can edit the profile. + lxc_remote profile set "localhost:${profileName}" user.foo=bar --project blah + + # The restricted client can delete the profile. + lxc_remote profile delete "localhost:${profileName}" --project blah + + # Create a profile in the blah project. lxc_remote profile create localhost:blah-profile --project blah + + # Profile is visible to restricted client in project blah. lxc_remote profile show localhost:blah-profile --project blah lxc_remote profile list localhost: --project blah | grep blah-profile + + # The profile is actually in the default project. + lxc profile show blah-profile --project default + + # The restricted client can't view it via the default project. + ! lxc_remote profile show localhost:blah-profile --project default || false + + # The restricted client can delete the profile. lxc_remote profile delete localhost:blah-profile --project blah + + ### STORAGE VOLUMES (initial value is true for new projects) + # Unset the storage volumes feature (the default is false). lxc project unset blah features.storage.volumes + + # Create a storage volume in the default project. + volName="vol$$" + lxc storage volume create "${pool_name}" "${volName}" --project default + + # The storage volume we created in the default project is visible in project blah. + lxc_remote storage volume show "localhost:${pool_name}" "${volName}" --project blah + lxc_remote storage volume list "localhost:${pool_name}" --project blah | grep -F "${volName}" + + # The restricted client can't view it via project default. + ! lxc_remote storage volume show "localhost:${pool_name}" "${volName}" --project default || false + ! lxc_remote storage volume list "localhost:${pool_name}" --project default | grep -F "${volName}" || false + + # The restricted client can edit the storage volume. + lxc_remote storage volume set "localhost:${pool_name}" "${volName}" user.foo=bar --project blah + + # The restricted client can delete the storage volume. + lxc_remote storage volume delete "localhost:${pool_name}" "${volName}" --project blah + + # Create a storage volume in the blah project. lxc_remote storage volume create "localhost:${pool_name}" blah-volume --project blah + + # Storage volume is visible to restricted client in project blah. lxc_remote storage volume show "localhost:${pool_name}" blah-volume --project blah - lxc_remote storage volume list "localhost:${pool_name}" --project blah lxc_remote storage volume list "localhost:${pool_name}" --project blah | grep blah-volume + + # The storage volume is actually in the default project. + lxc storage volume show "${pool_name}" blah-volume --project default + + # The restricted client can't view it via the default project. + ! lxc_remote storage volume show "localhost:${pool_name}" blah-volume --project default || false + + # The restricted client can delete the storage volume. lxc_remote storage volume delete "localhost:${pool_name}" blah-volume --project blah + ### STORAGE BUCKETS (initial value is true for new projects) + create_object_storage_pool s3 + + # Unset the storage buckets feature (the default is false). + lxc project unset blah features.storage.buckets + + # Create a storage bucket in the default project. + bucketName="bucket$$" + lxc storage bucket create s3 "${bucketName}" --project default + + # The storage bucket we created in the default project is visible in project blah. + lxc_remote storage bucket show localhost:s3 "${bucketName}" --project blah + lxc_remote storage bucket list localhost:s3 --project blah | grep -F "${bucketName}" + + # The restricted client can't view it via project default. + ! lxc_remote storage bucket show localhost:s3 "${bucketName}" --project default || false + ! lxc_remote storage bucket list localhost:s3 --project default | grep -F "${bucketName}" || false + + # The restricted client can edit the storage bucket. + lxc_remote storage bucket set localhost:s3 "${bucketName}" user.foo=bar --project blah + + # The restricted client can delete the storage bucket. + lxc_remote storage bucket delete localhost:s3 "${bucketName}" --project blah + + # Create a storage bucket in the blah project. + lxc_remote storage bucket create localhost:s3 blah-bucket --project blah + + # Storage bucket is visible to restricted client in project blah. + lxc_remote storage bucket show localhost:s3 blah-bucket --project blah + lxc_remote storage bucket list localhost:s3 --project blah | grep blah-bucket + + # The storage bucket is actually in the default project. + lxc storage bucket show s3 blah-bucket --project default + + # The restricted client can't view it via the default project. + ! lxc_remote storage bucket show localhost:s3 blah-bucket --project default || false + + # The restricted client can delete the storage bucket. + lxc_remote storage bucket delete localhost:s3 blah-bucket --project blah + # Cleanup + delete_object_storage_pool s3 + rm "${TEST_DIR}/${test_image_fingerprint}.tar.xz" lxc config trust show "${FINGERPRINT}" | sed -e "s/restricted: true/restricted: false/" | lxc config trust edit "${FINGERPRINT}" lxc project delete blah }