From 5dcea00ba26b9cb939c34ec0a6424be7f032b60d Mon Sep 17 00:00:00 2001 From: Gabriel Mougard Date: Fri, 14 Feb 2025 12:04:11 +0100 Subject: [PATCH 1/6] shared/api: instance and volume should not be enriched with entitlements Signed-off-by: Gabriel Mougard --- shared/api/instance_backup.go | 2 -- shared/api/instance_snapshot.go | 2 -- shared/api/storage_pool_volume_backup.go | 2 -- shared/api/storage_pool_volume_snapshot.go | 2 -- 4 files changed, 8 deletions(-) diff --git a/shared/api/instance_backup.go b/shared/api/instance_backup.go index 83d5da7705d8..ff59e7c719c0 100644 --- a/shared/api/instance_backup.go +++ b/shared/api/instance_backup.go @@ -45,8 +45,6 @@ type InstanceBackupsPost struct { // // API extension: instances. type InstanceBackup struct { - WithEntitlements `yaml:",inline"` - // Backup name // Example: backup0 Name string `json:"name" yaml:"name"` diff --git a/shared/api/instance_snapshot.go b/shared/api/instance_snapshot.go index a069f17a7cc0..33ff909482a5 100644 --- a/shared/api/instance_snapshot.go +++ b/shared/api/instance_snapshot.go @@ -64,8 +64,6 @@ type InstanceSnapshotPut struct { // // API extension: instances. type InstanceSnapshot struct { - WithEntitlements `yaml:",inline"` - // Architecture name // Example: x86_64 Architecture string `json:"architecture" yaml:"architecture"` diff --git a/shared/api/storage_pool_volume_backup.go b/shared/api/storage_pool_volume_backup.go index a9e9504385af..b38e468ec27a 100644 --- a/shared/api/storage_pool_volume_backup.go +++ b/shared/api/storage_pool_volume_backup.go @@ -10,8 +10,6 @@ import ( // // API extension: custom_volume_backup. type StoragePoolVolumeBackup struct { - WithEntitlements `yaml:",inline"` - // Backup name // Example: backup0 Name string `json:"name" yaml:"name"` diff --git a/shared/api/storage_pool_volume_snapshot.go b/shared/api/storage_pool_volume_snapshot.go index 1d5954361ab7..e6cf809ddad4 100644 --- a/shared/api/storage_pool_volume_snapshot.go +++ b/shared/api/storage_pool_volume_snapshot.go @@ -53,8 +53,6 @@ type StorageVolumeSnapshotPost struct { // // API extension: storage_api_volume_snapshots. type StorageVolumeSnapshot struct { - WithEntitlements `yaml:",inline"` - // Snapshot name // Example: snap0 Name string `json:"name" yaml:"name"` From bde6f60f5f7940fb3d878addb8ab0ebba4dcd9a9 Mon Sep 17 00:00:00 2001 From: Gabriel Mougard Date: Fri, 14 Feb 2025 12:05:01 +0100 Subject: [PATCH 2/6] lxd: disable enrichment with entitlements for instance, volume entities Signed-off-by: Gabriel Mougard --- lxd/instance_backup.go | 29 +----------------------- lxd/instance_snapshot.go | 26 --------------------- lxd/storage_volumes_backup.go | 29 +----------------------- lxd/storage_volumes_snapshot.go | 40 ++++++--------------------------- 4 files changed, 9 insertions(+), 115 deletions(-) diff --git a/lxd/instance_backup.go b/lxd/instance_backup.go index dcdcbbf577ad..d2cf3cb7133f 100644 --- a/lxd/instance_backup.go +++ b/lxd/instance_backup.go @@ -136,11 +136,6 @@ func instanceBackupsGet(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } - withEntitlements, err := extractEntitlementsFromQuery(r, entity.TypeInstanceBackup, true) - if err != nil { - return response.SmartError(err) - } - if shared.IsSnapshot(cname) { return response.BadRequest(fmt.Errorf("Invalid instance name")) } @@ -169,7 +164,6 @@ func instanceBackupsGet(d *Daemon, r *http.Request) response.Response { resultString := []string{} resultMap := []*api.InstanceBackup{} - urlToBackup := make(map[*api.URL]auth.EntitlementReporter, len(backups)) canView, err := s.Authorizer.GetPermissionChecker(r.Context(), auth.EntitlementCanView, entity.TypeInstanceBackup) if err != nil { return response.SmartError(err) @@ -193,7 +187,6 @@ func instanceBackupsGet(d *Daemon, r *http.Request) response.Response { } else { render := backup.Render() resultMap = append(resultMap, render) - urlToBackup[entity.InstanceBackupURL(projectName, c.Name(), backupName)] = render } } @@ -201,13 +194,6 @@ func instanceBackupsGet(d *Daemon, r *http.Request) response.Response { return response.SyncResponse(true, resultString) } - if len(withEntitlements) > 0 { - err = reportEntitlements(r.Context(), s.Authorizer, s.IdentityCache, entity.TypeInstanceBackup, withEntitlements, urlToBackup) - if err != nil { - return response.SmartError(err) - } - } - return response.SyncResponse(true, resultMap) } @@ -442,11 +428,6 @@ func instanceBackupGet(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } - withEntitlements, err := extractEntitlementsFromQuery(r, entity.TypeInstanceBackup, false) - if err != nil { - return response.SmartError(err) - } - if shared.IsSnapshot(name) { return response.BadRequest(fmt.Errorf("Invalid instance name")) } @@ -472,15 +453,7 @@ func instanceBackupGet(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } - renderedBackup := backup.Render() - if len(withEntitlements) > 0 { - err = reportEntitlements(r.Context(), s.Authorizer, s.IdentityCache, entity.TypeInstanceBackup, withEntitlements, map[*api.URL]auth.EntitlementReporter{entity.InstanceBackupURL(projectName, name, backupName): renderedBackup}) - if err != nil { - return response.SmartError(err) - } - } - - return response.SyncResponse(true, renderedBackup) + return response.SyncResponse(true, backup.Render()) } // swagger:operation POST /1.0/instances/{name}/backups/{backup} instances instance_backup_post diff --git a/lxd/instance_snapshot.go b/lxd/instance_snapshot.go index c6c6af1900f0..2579983f077e 100644 --- a/lxd/instance_snapshot.go +++ b/lxd/instance_snapshot.go @@ -157,15 +157,9 @@ func instanceSnapshotsGet(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } - withEntitlements, err := extractEntitlementsFromQuery(r, entity.TypeInstanceSnapshot, true) - if err != nil { - return response.SmartError(err) - } - recursion := util.IsRecursionRequest(r) resultString := []string{} resultMap := []*api.InstanceSnapshot{} - urlToSnaps := make(map[*api.URL]auth.EntitlementReporter) if !recursion { var snaps []string @@ -225,7 +219,6 @@ func instanceSnapshotsGet(d *Daemon, r *http.Request) response.Response { } resultMap = append(resultMap, renderedSnap) - urlToSnaps[entity.InstanceSnapshotURL(projectName, cname, snapName)] = renderedSnap } } @@ -233,13 +226,6 @@ func instanceSnapshotsGet(d *Daemon, r *http.Request) response.Response { return response.SyncResponse(true, resultString) } - if len(withEntitlements) > 0 { - err = reportEntitlements(r.Context(), s.Authorizer, s.IdentityCache, entity.TypeInstanceSnapshot, withEntitlements, urlToSnaps) - if err != nil { - return response.SmartError(err) - } - } - return response.SyncResponse(true, resultMap) } @@ -629,11 +615,6 @@ func snapshotPut(s *state.State, r *http.Request, snapInst instance.Instance) re // "500": // $ref: "#/responses/InternalServerError" func snapshotGet(s *state.State, r *http.Request, snapInst instance.Instance) response.Response { - withEntitlements, err := extractEntitlementsFromQuery(r, entity.TypeInstanceSnapshot, false) - if err != nil { - return response.SmartError(err) - } - render, _, err := snapInst.Render(storagePools.RenderSnapshotUsage(s, snapInst)) if err != nil { return response.SmartError(err) @@ -644,13 +625,6 @@ func snapshotGet(s *state.State, r *http.Request, snapInst instance.Instance) re return response.InternalError(fmt.Errorf("Render didn't return a snapshot")) } - if len(withEntitlements) > 0 { - err = reportEntitlements(r.Context(), s.Authorizer, s.IdentityCache, entity.TypeInstanceSnapshot, withEntitlements, map[*api.URL]auth.EntitlementReporter{entity.InstanceSnapshotURL(snapInst.Project().Name, snapInst.Name(), renderedSnap.Name): renderedSnap}) - if err != nil { - return response.SmartError(err) - } - } - etag := []any{snapInst.ExpiryDate()} return response.SyncResponseETag(true, renderedSnap, etag) } diff --git a/lxd/storage_volumes_backup.go b/lxd/storage_volumes_backup.go index 355575fe771d..dc3b9df130df 100644 --- a/lxd/storage_volumes_backup.go +++ b/lxd/storage_volumes_backup.go @@ -174,11 +174,6 @@ func storagePoolVolumeTypeCustomBackupsGet(d *Daemon, r *http.Request) response. return response.SmartError(err) } - withEntitlements, err := extractEntitlementsFromQuery(r, entity.TypeStorageVolumeBackup, true) - if err != nil { - return response.SmartError(err) - } - // Check that the storage volume type is valid. if details.volumeType != cluster.StoragePoolVolumeTypeCustom { return response.BadRequest(fmt.Errorf("Invalid storage volume type %q", details.volumeTypeName)) @@ -210,7 +205,6 @@ func storagePoolVolumeTypeCustomBackupsGet(d *Daemon, r *http.Request) response. resultString := []string{} resultMap := []*api.StoragePoolVolumeBackup{} - urlToBackup := make(map[*api.URL]auth.EntitlementReporter) canView, err := s.Authorizer.GetPermissionChecker(r.Context(), auth.EntitlementCanView, entity.TypeStorageVolumeBackup) if err != nil { @@ -234,7 +228,6 @@ func storagePoolVolumeTypeCustomBackupsGet(d *Daemon, r *http.Request) response. } else { render := backup.Render() resultMap = append(resultMap, render) - urlToBackup[entity.StorageVolumeBackupURL(request.ProjectParam(r), details.location, details.pool.Name(), details.volumeTypeName, details.volumeName, backupName)] = render } } @@ -242,13 +235,6 @@ func storagePoolVolumeTypeCustomBackupsGet(d *Daemon, r *http.Request) response. return response.SyncResponse(true, resultString) } - if len(withEntitlements) > 0 { - err = reportEntitlements(r.Context(), s.Authorizer, s.IdentityCache, entity.TypeStorageVolumeBackup, withEntitlements, urlToBackup) - if err != nil { - return response.SmartError(err) - } - } - return response.SyncResponse(true, resultMap) } @@ -493,11 +479,6 @@ func storagePoolVolumeTypeCustomBackupGet(d *Daemon, r *http.Request) response.R return response.SmartError(err) } - withEntitlements, err := extractEntitlementsFromQuery(r, entity.TypeStorageVolumeBackup, false) - if err != nil { - return response.SmartError(err) - } - // Get backup name. backupName, err := url.PathUnescape(mux.Vars(r)["backupName"]) if err != nil { @@ -531,15 +512,7 @@ func storagePoolVolumeTypeCustomBackupGet(d *Daemon, r *http.Request) response.R return response.SmartError(err) } - renderedBackup := backup.Render() - if len(withEntitlements) > 0 { - err = reportEntitlements(r.Context(), s.Authorizer, s.IdentityCache, entity.TypeStorageVolumeBackup, withEntitlements, map[*api.URL]auth.EntitlementReporter{entity.StorageVolumeBackupURL(request.ProjectParam(r), details.location, details.pool.Name(), details.volumeTypeName, details.volumeName, backupName): renderedBackup}) - if err != nil { - return response.SmartError(err) - } - } - - return response.SyncResponse(true, renderedBackup) + return response.SyncResponse(true, backup.Render()) } // swagger:operation POST /1.0/storage-pools/{poolName}/volumes/{type}/{volumeName}/backups/{backupName} storage storage_pool_volumes_type_backup_post diff --git a/lxd/storage_volumes_snapshot.go b/lxd/storage_volumes_snapshot.go index 3ebe64230710..9a67f7925ff2 100644 --- a/lxd/storage_volumes_snapshot.go +++ b/lxd/storage_volumes_snapshot.go @@ -356,11 +356,6 @@ func storagePoolVolumeSnapshotsTypeGet(d *Daemon, r *http.Request) response.Resp return response.SmartError(err) } - withEntitlements, err := extractEntitlementsFromQuery(r, entity.TypeStorageVolumeSnapshot, true) - if err != nil { - return response.SmartError(err) - } - recursion := util.IsRecursionRequest(r) // Check that the storage volume type is valid. @@ -398,7 +393,6 @@ func storagePoolVolumeSnapshotsTypeGet(d *Daemon, r *http.Request) response.Resp // Prepare the response. resultString := []string{} resultMap := []*api.StorageVolumeSnapshot{} - urlToSnapshot := make(map[*api.URL]auth.EntitlementReporter) for _, volume := range volumes { _, snapshotName, _ := api.GetParentAndSnapshotName(volume.Name) @@ -425,19 +419,18 @@ func storagePoolVolumeSnapshotsTypeGet(d *Daemon, r *http.Request) response.Resp vol.UsedBy = project.FilterUsedBy(s.Authorizer, r, volumeUsedBy) - tmp := &api.StorageVolumeSnapshot{} - tmp.Config = vol.Config - tmp.Description = vol.Description - tmp.Name = vol.Name - tmp.CreatedAt = vol.CreatedAt + snap := &api.StorageVolumeSnapshot{} + snap.Config = vol.Config + snap.Description = vol.Description + snap.Name = vol.Name + snap.CreatedAt = vol.CreatedAt expiryDate := volume.ExpiryDate if expiryDate.Unix() > 0 { - tmp.ExpiresAt = &expiryDate + snap.ExpiresAt = &expiryDate } - resultMap = append(resultMap, tmp) - urlToSnapshot[entity.StorageVolumeSnapshotURL(request.ProjectParam(r), details.location, details.pool.Name(), details.volumeTypeName, details.volumeName, snapshotName)] = tmp + resultMap = append(resultMap, snap) } } @@ -445,13 +438,6 @@ func storagePoolVolumeSnapshotsTypeGet(d *Daemon, r *http.Request) response.Resp return response.SyncResponse(true, resultString) } - if len(withEntitlements) > 0 { - err = reportEntitlements(r.Context(), s.Authorizer, s.IdentityCache, entity.TypeStorageVolumeSnapshot, withEntitlements, urlToSnapshot) - if err != nil { - return response.SmartError(err) - } - } - return response.SyncResponse(true, resultMap) } @@ -622,11 +608,6 @@ func storagePoolVolumeSnapshotTypeGet(d *Daemon, r *http.Request) response.Respo return response.SmartError(err) } - withEntitlements, err := extractEntitlementsFromQuery(r, entity.TypeStorageVolumeSnapshot, false) - if err != nil { - return response.SmartError(err) - } - // Get the name of the storage volume. snapshotName, err := url.PathUnescape(mux.Vars(r)["snapshotName"]) if err != nil { @@ -679,13 +660,6 @@ func storagePoolVolumeSnapshotTypeGet(d *Daemon, r *http.Request) response.Respo snapshot.ContentType = dbVolume.ContentType snapshot.CreatedAt = dbVolume.CreatedAt - if len(withEntitlements) > 0 { - err = reportEntitlements(r.Context(), s.Authorizer, s.IdentityCache, entity.TypeStorageVolumeSnapshot, withEntitlements, map[*api.URL]auth.EntitlementReporter{entity.StorageVolumeSnapshotURL(request.ProjectParam(r), details.location, details.pool.Name(), details.volumeTypeName, details.volumeName, snapshotName): snapshot}) - if err != nil { - return response.SmartError(err) - } - } - etag := []any{snapshot.Description, expiry} return response.SyncResponseETag(true, snapshot, etag) } From 9ee50a8cf04fc9ae8209ee91bd041b9fe31848dc Mon Sep 17 00:00:00 2001 From: Gabriel Mougard Date: Fri, 14 Feb 2025 18:05:09 +0100 Subject: [PATCH 3/6] lxd: `resultMap` for `network zones` and `network acls` should be a slice of pointers Signed-off-by: Gabriel Mougard --- lxd/network_acls.go | 4 ++-- lxd/network_zones.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lxd/network_acls.go b/lxd/network_acls.go index deec5bf84ffa..4a5c2ff6d226 100644 --- a/lxd/network_acls.go +++ b/lxd/network_acls.go @@ -182,7 +182,7 @@ func networkACLsGet(d *Daemon, r *http.Request) response.Response { } resultString := []string{} - resultMap := []api.NetworkACL{} + resultMap := []*api.NetworkACL{} urlToNetworkACL := make(map[*api.URL]auth.EntitlementReporter) for _, aclName := range aclNames { if !userHasPermission(entity.NetworkACLURL(requestProjectName, aclName)) { @@ -201,7 +201,7 @@ func networkACLsGet(d *Daemon, r *http.Request) response.Response { netACLInfo.UsedBy, _ = netACL.UsedBy() // Ignore errors in UsedBy, will return nil. netACLInfo.UsedBy = project.FilterUsedBy(s.Authorizer, r, netACLInfo.UsedBy) - resultMap = append(resultMap, *netACLInfo) + resultMap = append(resultMap, netACLInfo) urlToNetworkACL[entity.NetworkACLURL(requestProjectName, aclName)] = netACLInfo } } diff --git a/lxd/network_zones.go b/lxd/network_zones.go index 88ad5ec74e4c..450610d2f299 100644 --- a/lxd/network_zones.go +++ b/lxd/network_zones.go @@ -269,7 +269,7 @@ func networkZonesGet(d *Daemon, r *http.Request) response.Response { } resultString := []string{} - resultMap := []api.NetworkZone{} + resultMap := []*api.NetworkZone{} urlToNetworkZone := make(map[*api.URL]auth.EntitlementReporter) for zoneName, projectName := range zoneNamesMap { // Check permission for each network zone against the requested project. @@ -296,7 +296,7 @@ func networkZonesGet(d *Daemon, r *http.Request) response.Response { netzoneInfo.UsedBy = project.FilterUsedBy(s.Authorizer, r, netzoneInfo.UsedBy) netzoneInfo.Project = projectName - resultMap = append(resultMap, *netzoneInfo) + resultMap = append(resultMap, netzoneInfo) urlToNetworkZone[entity.NetworkZoneURL(projectName, zoneName)] = netzoneInfo } } From 69127595b5ba43de64b9243ec4f379821b67ff81 Mon Sep 17 00:00:00 2001 From: Gabriel Mougard Date: Fri, 14 Feb 2025 12:18:33 +0100 Subject: [PATCH 4/6] lxd: fix warning `SA1019: req.ContainerOnly is deprecated: Use InstanceOnly.` Signed-off-by: Gabriel Mougard --- lxd/instance_backup.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lxd/instance_backup.go b/lxd/instance_backup.go index d2cf3cb7133f..d46fc3d95ad1 100644 --- a/lxd/instance_backup.go +++ b/lxd/instance_backup.go @@ -335,7 +335,8 @@ func instanceBackupsPost(d *Daemon, r *http.Request) response.Response { } fullName := name + shared.SnapshotDelimiter + backupName - instanceOnly := req.InstanceOnly || req.ContainerOnly + // We keep the req.ContainerOnly for backward compatibility. + instanceOnly := req.InstanceOnly || req.ContainerOnly //nolint:staticcheck,unused backup := func(op *operations.Operation) error { args := db.InstanceBackup{ From 5981ad9ae5c7a5f57b2f9c2289f62d9cf7b729ff Mon Sep 17 00:00:00 2001 From: Gabriel Mougard Date: Fri, 14 Feb 2025 11:58:30 +0100 Subject: [PATCH 5/6] doc/rest-api: Refresh swagger YAML Signed-off-by: Gabriel Mougard --- doc/rest-api.yaml | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/doc/rest-api.yaml b/doc/rest-api.yaml index 7efd86d897c2..0b22950462f0 100644 --- a/doc/rest-api.yaml +++ b/doc/rest-api.yaml @@ -1803,15 +1803,6 @@ definitions: x-go-package: github.com/canonical/lxd/shared/api InstanceBackup: properties: - access_entitlements: - description: AccessEntitlements represents the entitlements that are granted to the requesting user on the attached entity. - example: - - can_view - - can_edit - items: - type: string - type: array - x-go-name: AccessEntitlements container_only: description: Whether to ignore snapshots (deprecated, use instance_only) example: false @@ -2281,15 +2272,6 @@ definitions: x-go-package: github.com/canonical/lxd/shared/api InstanceSnapshot: properties: - access_entitlements: - description: AccessEntitlements represents the entitlements that are granted to the requesting user on the attached entity. - example: - - can_view - - can_edit - items: - type: string - type: array - x-go-name: AccessEntitlements architecture: description: Architecture name example: x86_64 @@ -6482,15 +6464,6 @@ definitions: StoragePoolVolumeBackup: description: StoragePoolVolumeBackup represents a LXD volume backup properties: - access_entitlements: - description: AccessEntitlements represents the entitlements that are granted to the requesting user on the attached entity. - example: - - can_view - - can_edit - items: - type: string - type: array - x-go-name: AccessEntitlements created_at: description: When the backup was created example: "2021-03-23T16:38:37.753398689-04:00" @@ -6746,15 +6719,6 @@ definitions: StorageVolumeSnapshot: description: StorageVolumeSnapshot represents a LXD storage volume snapshot properties: - access_entitlements: - description: AccessEntitlements represents the entitlements that are granted to the requesting user on the attached entity. - example: - - can_view - - can_edit - items: - type: string - type: array - x-go-name: AccessEntitlements config: additionalProperties: type: string From b0a63b97c95dfdabca2c713786d32caee1c3fab1 Mon Sep 17 00:00:00 2001 From: Gabriel Mougard Date: Thu, 13 Feb 2025 11:00:10 +0100 Subject: [PATCH 6/6] test/auth: More tests for entity enrichment with entitlements Signed-off-by: Gabriel Mougard --- test/suites/auth.sh | 312 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 284 insertions(+), 28 deletions(-) diff --git a/test/suites/auth.sh b/test/suites/auth.sh index 7f6f551a0f0c..85d52581a4b4 100644 --- a/test/suites/auth.sh +++ b/test/suites/auth.sh @@ -288,35 +288,8 @@ fine_grained: true" lxc config trust remove "${lxdconf6_fingerprint_short}" lxc auth identity group add oidc/test-user@example.com test-group - # Create a new test project, add some entitlements on it and check that these are reflected in the 'access_entitlements' field returned from the API. - lxc project create test-project - lxc auth group permission add test-group project test-project can_view - lxc auth group permission add test-group project test-project can_edit - lxc auth group permission add test-group project test-project can_delete - # Check the created project entitlements given a list of candidate entitlements (some are wrong: `can_create_instances` and `can_create_networks`. These should not be returned). - [ "$(lxc_remote query "oidc:/1.0/projects/test-project?recursion=1&with-access-entitlements=can_view,can_edit,can_delete,can_create_instances,can_create_networks" | jq -r '.access_entitlements | sort | @csv')" = '"can_delete","can_edit","can_view"' ] - lxc project delete test-project - - # Repeat the same test for other entity types. - # Instance - ensure_import_testimage - lxc init testimage test-foo - lxc auth group permission add test-group instance test-foo can_view project=default - lxc auth group permission add test-group instance test-foo can_edit project=default - lxc auth group permission add test-group instance test-foo can_delete project=default - [ "$(lxc_remote query "oidc:/1.0/instances/test-foo?project=default&recursion=1&with-access-entitlements=can_view,can_edit,can_delete,can_exec" | jq -r '.access_entitlements | sort | @csv')" = '"can_delete","can_edit","can_view"' ] - lxc delete test-foo -f - - # Storage volume - # Storage volume entitlements test - pool_name="$(lxc storage list -f csv | cut -d, -f1)" - lxc storage volume create "${pool_name}" test-volume - lxc auth group permission add test-group storage_volume test-volume can_view project=default pool="${pool_name}" type=custom - lxc auth group permission add test-group storage_volume test-volume can_edit project=default pool="${pool_name}" type=custom - lxc auth group permission add test-group storage_volume test-volume can_delete project=default pool="${pool_name}" type=custom - [ "$(lxc_remote query "oidc:/1.0/storage-pools/${pool_name}/volumes/custom/test-volume?project=default&recursion=1&with-access-entitlements=can_view,can_edit,can_delete,can_manage_backups,can_manage_snapshots" | jq -r '.access_entitlements | sort | @csv')" = '"can_delete","can_edit","can_view"' ] - lxc storage volume delete "${pool_name}" test-volume + entities_enrichment_with_entitlements # Cleanup lxc auth group delete test-group @@ -1103,4 +1076,287 @@ auth_project_features() { # General clean up lxc project delete blah +} + +entities_enrichment_with_entitlements() { + ensure_import_testimage + + # Create a new test project, add some entitlements on it and check that these are reflected in the 'access_entitlements' field returned from the API. + lxc project create test-project + lxc auth group permission add test-group project test-project can_view + lxc auth group permission add test-group project test-project can_edit + lxc auth group permission add test-group project test-project can_delete + + # Check the created project entitlements given a list of candidate entitlements (some are wrong: `can_create_instances` and `can_create_networks`. These should not be returned). + [ "$(lxc_remote query "oidc:/1.0/projects/test-project?recursion=1&with-access-entitlements=can_view,can_edit,can_delete,can_create_instances,can_create_networks" | jq -r '.access_entitlements | sort | @csv')" = '"can_delete","can_edit","can_view"' ] + lxc project delete test-project + + # Repeat the same test for other entity types. + # Instance + ensure_import_testimage + lxc init testimage test-foo + lxc auth group permission add test-group instance test-foo can_view project=default + lxc auth group permission add test-group instance test-foo can_edit project=default + lxc auth group permission add test-group instance test-foo can_delete project=default + [ "$(lxc_remote query "oidc:/1.0/instances/test-foo?project=default&recursion=1&with-access-entitlements=can_view,can_edit,can_delete,can_exec" | jq -r '.access_entitlements | sort | @csv')" = '"can_delete","can_edit","can_view"' ] + lxc delete test-foo -f + + # Storage pool + lxc auth group permission add test-group storage_pool "${pool_name}" can_edit + lxc auth group permission add test-group storage_pool "${pool_name}" can_delete + [ "$(lxc_remote query "oidc:/1.0/storage-pools/${pool_name}?with-access-entitlements=can_edit,can_delete" | jq -r '.access_entitlements | sort | @csv')" = '"can_delete","can_edit"' ] + all_storage_pools=$(lxc_remote query "oidc:/1.0/storage-pools?recursion=1&with-access-entitlements=can_edit,can_delete") + if ! jq -e -r --arg pool_name "$pool_name" '.[] | select(.name == $pool_name) | .access_entitlements | sort | @csv == "\"can_delete\",\"can_edit\""' <<< "$all_storage_pools"; then + echo "Second check failed (or pool not found)!" >&2 + false + fi + + # Storage volume + pool_name="$(lxc storage list -f csv | cut -d, -f1)" + lxc storage volume create "${pool_name}" test-volume + lxc auth group permission add test-group storage_volume test-volume can_view project=default pool="${pool_name}" type=custom + lxc auth group permission add test-group storage_volume test-volume can_edit project=default pool="${pool_name}" type=custom + lxc auth group permission add test-group storage_volume test-volume can_delete project=default pool="${pool_name}" type=custom + [ "$(lxc_remote query "oidc:/1.0/storage-pools/${pool_name}/volumes/custom/test-volume?project=default&recursion=1&with-access-entitlements=can_view,can_edit,can_delete,can_manage_backups,can_manage_snapshots" | jq -r '.access_entitlements | sort | @csv')" = '"can_delete","can_edit","can_view"' ] + + lxc storage volume delete "${pool_name}" test-volume + + # Auth group + lxc auth group create test-group2 + lxc auth group create test-group3 + lxc auth group permission add test-group group test-group2 can_view + lxc auth group permission add test-group group test-group3 can_view + lxc auth group permission add test-group group test-group3 can_edit + [ "$(lxc_remote query "oidc:/1.0/auth/groups/test-group2?with-access-entitlements=can_view,can_edit" | jq -r '.access_entitlements | sort | @csv')" = '"can_view"' ] + [ "$(lxc_remote query "oidc:/1.0/auth/groups/test-group3?with-access-entitlements=can_view,can_edit" | jq -r '.access_entitlements | sort | @csv')" = '"can_edit","can_view"' ] + all_groups=$(lxc_remote query "oidc:/1.0/auth/groups?recursion=1&with-access-entitlements=can_view,can_edit") + if ! echo "$all_groups" | jq -e -r ' + all( + .[] | select(.name == "test-group2" or .name == "test-group3" or .name == "test-group"); + if .name == "test-group" then + .access_entitlements | sort | @csv == "\"can_view\"" + elif .name == "test-group2" then + .access_entitlements | sort | @csv == "\"can_view\"" + elif .name == "test-group3" then + .access_entitlements | sort | @csv == "\"can_edit\",\"can_view\"" + else + false + end + ) + ' ; then + echo "Failed to find expected entitlements for auth groups" + false + fi + + lxc auth group delete test-group2 + lxc auth group delete test-group3 + + # Certificate + openssl req -x509 -newkey rsa:4096 -keyout test1.key -out test1.crt -days 365 -nodes -subj "/CN=lxd-client-test1" + openssl req -x509 -newkey rsa:4096 -keyout test2.key -out test2.crt -days 365 -nodes -subj "/CN=lxd-client-test2" + chmod 400 test1.key + chmod 400 test2.key + lxc config trust add test1.crt + lxc config trust add test2.crt + test1Fingerprint="" + test2Fingerprint="" + all_certs=$(lxc config trust list --format json) + test1Fingerprint=$(echo "$all_certs" | jq -r '.[] | select(.name == "test1.crt") | .fingerprint') + test2Fingerprint=$(echo "$all_certs" | jq -r '.[] | select(.name == "test2.crt") | .fingerprint') + + lxc auth group permission add test-group certificate "${test1Fingerprint}" can_view + lxc auth group permission add test-group certificate "${test2Fingerprint}" can_view + lxc auth group permission add test-group certificate "${test2Fingerprint}" can_edit + [ "$(lxc_remote query "oidc:/1.0/certificates/${test1Fingerprint}?with-access-entitlements=can_view,can_edit" | jq -r '.access_entitlements | sort | @csv')" = '"can_view"' ] + [ "$(lxc_remote query "oidc:/1.0/certificates/${test2Fingerprint}?with-access-entitlements=can_view,can_edit" | jq -r '.access_entitlements | sort | @csv')" = '"can_edit","can_view"' ] + all_certs=$(lxc_remote query "oidc:/1.0/certificates?recursion=1&with-access-entitlements=can_view,can_edit") + if ! echo "$all_certs" | jq -e -r --arg f1 "$test1Fingerprint" --arg f2 "$test2Fingerprint" ' + all( + .[] | select(.fingerprint == $f1 or .fingerprint == $f2); + if .fingerprint == $f1 then + .access_entitlements | sort | @csv == "\"can_view\"" + elif .fingerprint == $f2 then + .access_entitlements | sort | @csv == "\"can_edit\",\"can_view\"" + else + false + end + ) + ' ; then + echo "Failed to find expected entitlements for certificates" + false + fi + + rm test1.crt test1.key test2.crt test2.key + lxc config trust remove "${test1Fingerprint}" + lxc config trust remove "${test2Fingerprint}" + + # Identity provider group + lxc auth identity-provider-group create test-idp-group2 + lxc auth identity-provider-group create test-idp-group3 + lxc auth group permission add test-group identity_provider_group test-idp-group2 can_view + lxc auth group permission add test-group identity_provider_group test-idp-group3 can_view + lxc auth group permission add test-group identity_provider_group test-idp-group3 can_edit + lxc auth group permission add test-group identity_provider_group test-idp-group3 can_delete + [ "$(lxc_remote query "oidc:/1.0/auth/identity-provider-groups/test-idp-group2?with-access-entitlements=can_view,can_edit,can_delete" | jq -r '.access_entitlements | sort | @csv')" = '"can_view"' ] + [ "$(lxc_remote query "oidc:/1.0/auth/identity-provider-groups/test-idp-group3?with-access-entitlements=can_view,can_edit,can_delete" | jq -r '.access_entitlements | sort | @csv')" = '"can_delete","can_edit","can_view"' ] + all_idp_groups=$(lxc_remote query "oidc:/1.0/auth/identity-provider-groups?recursion=1&with-access-entitlements=can_view,can_edit,can_delete") + if ! echo "$all_idp_groups" | jq -e -r ' + all( + .[] | select(.name == "test-idp-group2" or .name == "test-idp-group3"); + if .name == "test-idp-group2" then + .access_entitlements | sort | @csv == "\"can_view\"" + elif (.name == "test-idp-group3") then + .access_entitlements | sort | @csv == "\"can_delete\",\"can_edit\",\"can_view\"" + else + false + end + ) + ' ; then + echo "Failed to find expected entitlements for identity provider groups" + false + fi + + lxc auth identity-provider-group delete test-idp-group2 + lxc auth identity-provider-group delete test-idp-group3 + + # Image + lxc init images:alpine/3.21 c1 + lxc delete c1 -f + imgFingerprint=$(lxc image list --format json | jq -r '.[] | select(.update_source.alias == "alpine/3.21") | .fingerprint') + lxc auth group permission add test-group image "${imgFingerprint}" can_view project=default + lxc auth group permission add test-group image "${imgFingerprint}" can_edit project=default + lxc auth group permission add test-group image "${imgFingerprint}" can_delete project=default + [ "$(lxc_remote query "oidc:/1.0/images/${imgFingerprint}?project=default&with-access-entitlements=can_view,can_edit,can_delete" | jq -r '.access_entitlements | sort | @csv')" = '"can_delete","can_edit","can_view"' ] + all_imgs=$(lxc_remote query "oidc:/1.0/images?project=default&recursion=1&with-access-entitlements=can_view,can_edit,can_delete") + selected_image=$(jq -r --arg fingerprint "$imgFingerprint" '.[] | select(.fingerprint == $fingerprint and (.access_entitlements | sort | @csv == "\"can_delete\",\"can_edit\",\"can_view\""))' <<< "$all_imgs") + if [ -z "$selected_image" ]; then + echo "Failed to find expected entitlements for images" + false + fi + + lxc image delete "${imgFingerprint}" + + # Profile + lxc profile create test-profile1 + lxc profile create test-profile2 + lxc auth group permission add test-group profile test-profile1 can_view project=default + lxc auth group permission add test-group profile test-profile2 can_view project=default + lxc auth group permission add test-group profile test-profile2 can_edit project=default + lxc auth group permission add test-group profile test-profile2 can_delete project=default + [ "$(lxc_remote query "oidc:/1.0/profiles/test-profile1?project=default&with-access-entitlements=can_view,can_edit,can_delete" | jq -r '.access_entitlements | sort | @csv')" = '"can_view"' ] + [ "$(lxc_remote query "oidc:/1.0/profiles/test-profile2?project=default&with-access-entitlements=can_view,can_edit,can_delete" | jq -r '.access_entitlements | sort | @csv')" = '"can_delete","can_edit","can_view"' ] + all_profiles=$(lxc_remote query "oidc:/1.0/profiles?project=default&recursion=1&with-access-entitlements=can_view,can_edit,can_delete") + if ! jq -e -r ' + all( + .[] | select(.name == "test-profile1" or .name == "test-profile2"); + if .name == "test-profile1" then + .access_entitlements | sort | @csv == "\"can_view\"" + elif (.name == "test-profile2") then + .access_entitlements | sort | @csv == "\"can_delete\",\"can_edit\",\"can_view\"" + else + false + end + ) + ' <<< "$all_profiles"; then + echo "Failed to find expected entitlements for profiles" + false + fi + + lxc profile delete test-profile1 + lxc profile delete test-profile2 + + # Network + lxc network create test-network1 + lxc network create test-network2 + lxc auth group permission add test-group network test-network1 can_view project=default + lxc auth group permission add test-group network test-network2 can_view project=default + lxc auth group permission add test-group network test-network2 can_edit project=default + lxc auth group permission add test-group network test-network2 can_delete project=default + [ "$(lxc_remote query "oidc:/1.0/networks/test-network1?project=default&with-access-entitlements=can_view,can_edit,can_delete" | jq -r '.access_entitlements | sort | @csv')" = '"can_view"' ] + [ "$(lxc_remote query "oidc:/1.0/networks/test-network2?project=default&with-access-entitlements=can_view,can_edit,can_delete" | jq -r '.access_entitlements | sort | @csv')" = '"can_delete","can_edit","can_view"' ] + all_networks=$(lxc_remote query "oidc:/1.0/networks?project=default&recursion=1&with-access-entitlements=can_view,can_edit,can_delete") + if ! jq -e -r ' + all( + .[] | select(.name == "test-network1" or .name == "test-network2"); + if .name == "test-network1" then + .access_entitlements | sort | @csv == "\"can_view\"" + elif (.name == "test-network2") then + .access_entitlements | sort | @csv == "\"can_delete\",\"can_edit\",\"can_view\"" + else + false + end + ) + ' <<< "$all_networks"; then + echo "Failed to find expected entitlements for networks" + false + fi + + lxc network delete test-network1 + lxc network delete test-network2 + + # Network ACL + lxc network acl create acl1 + lxc network acl create acl2 + lxc auth group permission add test-group network_acl acl1 can_view project=default + lxc auth group permission add test-group network_acl acl2 can_view project=default + lxc auth group permission add test-group network_acl acl2 can_edit project=default + lxc auth group permission add test-group network_acl acl2 can_delete project=default + [ "$(lxc_remote query "oidc:/1.0/network-acls/acl1?project=default&with-access-entitlements=can_view,can_edit,can_delete" | jq -r '.access_entitlements | sort | @csv')" = '"can_view"' ] + [ "$(lxc_remote query "oidc:/1.0/network-acls/acl2?project=default&with-access-entitlements=can_view,can_edit,can_delete" | jq -r '.access_entitlements | sort | @csv')" = '"can_delete","can_edit","can_view"' ] + all_network_acls=$(lxc_remote query "oidc:/1.0/network-acls?project=default&recursion=1&with-access-entitlements=can_view,can_edit,can_delete") + if ! jq -e -r ' + all( + .[] | select(.name == "acl1" or .name == "acl2"); + if .name == "acl1" then + .access_entitlements | sort | @csv == "\"can_view\"" + elif (.name == "acl2") then + .access_entitlements | sort | @csv == "\"can_delete\",\"can_edit\",\"can_view\"" + else + false + end + ) + ' <<< "$all_network_acls"; then + echo "Failed to find expected entitlements for network ACLs" + false + fi + + lxc network acl delete acl1 + lxc network acl delete acl2 + + # Network zone + + lxc network zone create zone1 + lxc network zone create zone2 + + lxc auth group permission add test-group network_zone zone1 can_view project=default + lxc auth group permission add test-group network_zone zone2 can_view project=default + lxc auth group permission add test-group network_zone zone2 can_edit project=default + lxc auth group permission add test-group network_zone zone2 can_delete project=default + [ "$(lxc_remote query "oidc:/1.0/network-zones/zone1?project=default&with-access-entitlements=can_view,can_edit,can_delete" | jq -r '.access_entitlements | sort | @csv')" = '"can_view"' ] + [ "$(lxc_remote query "oidc:/1.0/network-zones/zone2?project=default&with-access-entitlements=can_view,can_edit,can_delete" | jq -r '.access_entitlements | sort | @csv')" = '"can_delete","can_edit","can_view"' ] + all_network_zones=$(lxc_remote query "oidc:/1.0/network-zones?project=default&recursion=1&with-access-entitlements=can_view,can_edit,can_delete") + if ! jq -e -r ' + all( + .[] | select(.name == "zone1" or .name == "zone2"); + if .name == "zone1" then + .access_entitlements | sort | @csv == "\"can_view\"" + elif (.name == "zone2") then + .access_entitlements | sort | @csv == "\"can_delete\",\"can_edit\",\"can_view\"" + else + false + end + ) + ' <<< "$all_network_zones"; then + echo "Failed to find expected entitlements for network zones" + false + fi + + lxc network zone delete zone1 + lxc network zone delete zone2 + + # Server + lxc auth group permission add test-group server admin + lxc auth group permission add test-group server viewer + lxc auth group permission add test-group server project_manager + [ "$(lxc_remote query "oidc:/1.0?with-access-entitlements=admin,viewer,project_manager" | jq -r '.access_entitlements | sort | @csv')" = '"admin","project_manager","viewer"' ] } \ No newline at end of file