Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lxd: Entitlement enrichment for remaining API entities #14880

Merged
merged 15 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
5c313b4
shared/api/auth: Add 'omitempty' annotation for the `access_entitleme…
gabrielmougard Jan 29, 2025
692abe4
shared/api: Add support for entitlements for remaining API types that…
gabrielmougard Jan 31, 2025
adf42a0
doc/rest-api: Refresh swagger YAML
gabrielmougard Jan 31, 2025
261691e
lxd/server: Add entitlement enrichment for api.Server
gabrielmougard Jan 31, 2025
05606c4
lxd/image_alias: Add entitlement enrichment for api.ImageAliasEntry
gabrielmougard Jan 31, 2025
4d46149
lxd/instance_backup: Add entitlement enrichment for api.InstanceBackup
gabrielmougard Jan 31, 2025
3f1f08b
lxd/instance_snapshot: Add entitlement enrichment for api.InstanceSna…
gabrielmougard Jan 31, 2025
b196d07
lxd/storage_volumes_backup: Add entitlement enrichment for api.Storag…
gabrielmougard Jan 31, 2025
dd61875
lxd/storage_volumes_snapshot: Add entitlement enrichment for api.Stor…
gabrielmougard Jan 31, 2025
e28d731
lxd/auth_groups: Return a slice of pointer to api.AuthGroup for in-pl…
gabrielmougard Jan 29, 2025
5d0010f
lxd/certificates: Return a slice of pointer to api.Certificate for in…
gabrielmougard Jan 29, 2025
8009599
lxd/identities: Return a slice of pointer to api.Identity for in-plac…
gabrielmougard Jan 29, 2025
d43af8c
lxd/network: Return a slice of pointer to api.Network for in-place en…
gabrielmougard Jan 29, 2025
cfdfae3
lxd/storage-pools: Return a slice of pointer to api.StoragePool for i…
gabrielmougard Jan 29, 2025
ce7b04e
test/auth: remove check for `access_entitlements: []` since we added …
gabrielmougard Jan 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions doc/rest-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1106,6 +1106,15 @@ definitions:
ImageAliasesEntry:
description: ImageAliasesEntry represents a LXD image alias
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
description:
description: Alias description
example: Our preferred Ubuntu image
Expand Down Expand Up @@ -1156,6 +1165,15 @@ definitions:
ImageAliasesPost:
description: ImageAliasesPost represents a new LXD image alias
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
description:
description: Alias description
example: Our preferred Ubuntu image
Expand Down Expand Up @@ -1795,6 +1813,15 @@ 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
Expand Down Expand Up @@ -2264,6 +2291,15 @@ 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
Expand Down Expand Up @@ -5856,6 +5892,15 @@ definitions:
Server:
description: Server represents a LXD server
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
api_extensions:
description: List of supported API extensions
example:
Expand Down Expand Up @@ -6432,6 +6477,15 @@ 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"
Expand Down Expand Up @@ -6687,6 +6741,15 @@ 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
Expand Down
14 changes: 13 additions & 1 deletion lxd/api_1.0.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,11 @@ func api10Get(d *Daemon, r *http.Request) response.Response {
return response.SyncResponseETag(true, srv, nil)
}

withEntitlements, err := extractEntitlementsFromQuery(r, entity.TypeServer, false)
if err != nil {
return response.SmartError(err)
}

// If a target was specified, forward the request to the relevant node.
resp := forwardedResponseIfTargetIsRemote(s, r)
if resp != nil {
Expand Down Expand Up @@ -396,7 +401,7 @@ func api10Get(d *Daemon, r *http.Request) response.Response {

env.StorageSupportedDrivers = supportedStorageDrivers

fullSrv := api.Server{ServerUntrusted: srv}
fullSrv := &api.Server{ServerUntrusted: srv}
fullSrv.Environment = env
requestor := request.CreateRequestor(r)
fullSrv.AuthUserName = requestor.Username
Expand All @@ -413,6 +418,13 @@ func api10Get(d *Daemon, r *http.Request) response.Response {
}
}

if len(withEntitlements) > 0 {
err = reportEntitlements(r.Context(), s.Authorizer, s.IdentityCache, entity.TypeServer, withEntitlements, map[*api.URL]auth.EntitlementReporter{entity.ServerURL(): fullSrv})
if err != nil {
return response.SmartError(err)
}
}

return response.SyncResponseETag(true, fullSrv, fullSrv.Config)
}

Expand Down
10 changes: 6 additions & 4 deletions lxd/auth_groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,10 @@ func getAuthGroups(d *Daemon, r *http.Request) response.Response {
for _, permission := range authGroupPermissions {
authGroupPermissionsByGroupID[permission.GroupID] = append(authGroupPermissionsByGroupID[permission.GroupID], permission)
}

apiGroups := make([]api.AuthGroup, 0, len(groups))
// We need to allocate a slice of pointer to api.AuthGroup because
// these records will be modified in place by the reportEntitlements function.
// We'll then return a slice of api.AuthGroup as an API response.
apiGroups := make([]*api.AuthGroup, 0, len(groups))
urlToGroup := make(map[*api.URL]auth.EntitlementReporter, len(groups))
for _, group := range groups {
var apiPermissions []api.Permission
Expand Down Expand Up @@ -278,7 +280,7 @@ func getAuthGroups(d *Daemon, r *http.Request) response.Response {
}
}

group := api.AuthGroup{
group := &api.AuthGroup{
Name: group.Name,
Description: group.Description,
Permissions: apiPermissions,
Expand All @@ -287,7 +289,7 @@ func getAuthGroups(d *Daemon, r *http.Request) response.Response {
}

apiGroups = append(apiGroups, group)
urlToGroup[entity.AuthGroupURL(group.Name)] = &group
urlToGroup[entity.AuthGroupURL(group.Name)] = group
}

if len(withEntitlements) > 0 {
Expand Down
6 changes: 3 additions & 3 deletions lxd/certificates.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ func certificatesGet(d *Daemon, r *http.Request) response.Response {
}

if recursion {
var certResponses []api.Certificate
var certResponses []*api.Certificate
var baseCerts []dbCluster.Certificate
urlToCertificate := make(map[*api.URL]auth.EntitlementReporter)
var err error
Expand All @@ -159,7 +159,7 @@ func certificatesGet(d *Daemon, r *http.Request) response.Response {
return err
}

certResponses = make([]api.Certificate, 0, len(baseCerts))
certResponses = make([]*api.Certificate, 0, len(baseCerts))
for _, baseCert := range baseCerts {
if !userHasPermission(entity.CertificateURL(baseCert.Fingerprint)) {
continue
Expand All @@ -170,7 +170,7 @@ func certificatesGet(d *Daemon, r *http.Request) response.Response {
return err
}

certResponses = append(certResponses, *apiCert)
certResponses = append(certResponses, apiCert)
urlToCertificate[entity.CertificateURL(apiCert.Fingerprint)] = apiCert
}

Expand Down
6 changes: 3 additions & 3 deletions lxd/identities.go
Original file line number Diff line number Diff line change
Expand Up @@ -894,7 +894,7 @@ func getIdentities(authenticationMethod string) func(d *Daemon, r *http.Request)
}
}

apiIdentities := make([]api.Identity, 0, len(identities))
apiIdentities := make([]*api.Identity, 0, len(identities))
urlToIdentity := make(map[*api.URL]auth.EntitlementReporter, len(identities))
for _, id := range identities {
var certificate string
Expand All @@ -907,7 +907,7 @@ func getIdentities(authenticationMethod string) func(d *Daemon, r *http.Request)
certificate = metadata.Certificate
}

identity := api.Identity{
identity := &api.Identity{
AuthenticationMethod: string(id.AuthMethod),
Type: string(id.Type),
Identifier: id.Identifier,
Expand All @@ -917,7 +917,7 @@ func getIdentities(authenticationMethod string) func(d *Daemon, r *http.Request)
}

apiIdentities = append(apiIdentities, identity)
urlToIdentity[entity.IdentityURL(string(id.AuthMethod), id.Identifier)] = &identity
urlToIdentity[entity.IdentityURL(string(id.AuthMethod), id.Identifier)] = identity
}

if len(withEntitlements) > 0 {
Expand Down
37 changes: 32 additions & 5 deletions lxd/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -3628,9 +3628,14 @@ func imageAliasesGet(d *Daemon, r *http.Request) response.Response {

s := d.State()

withEntitlements, err := extractEntitlementsFromQuery(r, entity.TypeImageAlias, true)
if err != nil {
return response.SmartError(err)
}

projectName := request.ProjectParam(r)
var effectiveProjectName string
err := s.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 {
var err error
effectiveProjectName, err = projectutils.ImageProject(ctx, tx.Tx(), projectName)
return err
Expand All @@ -3646,15 +3651,16 @@ func imageAliasesGet(d *Daemon, r *http.Request) response.Response {
}

var responseStr []string
var responseMap []api.ImageAliasesEntry
var responseMap []*api.ImageAliasesEntry
urlToImageAlias := make(map[*api.URL]auth.EntitlementReporter)
err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
names, err := tx.GetImageAliases(ctx, projectName)
if err != nil {
return err
}

if recursion {
responseMap = make([]api.ImageAliasesEntry, 0, len(names))
responseMap = make([]*api.ImageAliasesEntry, 0, len(names))
} else {
responseStr = make([]string, 0, len(names))
}
Expand All @@ -3672,7 +3678,8 @@ func imageAliasesGet(d *Daemon, r *http.Request) response.Response {
continue
}

responseMap = append(responseMap, alias)
responseMap = append(responseMap, &alias)
urlToImageAlias[entity.ImageAliasURL(projectName, name)] = &alias
}
}

Expand All @@ -3686,6 +3693,13 @@ func imageAliasesGet(d *Daemon, r *http.Request) response.Response {
return response.SyncResponse(true, responseStr)
}

if len(withEntitlements) > 0 {
err = reportEntitlements(r.Context(), s.Authorizer, s.IdentityCache, entity.TypeImageAlias, withEntitlements, urlToImageAlias)
if err != nil {
return response.SmartError(err)
}
}

return response.SyncResponse(true, responseMap)
}

Expand Down Expand Up @@ -3779,6 +3793,12 @@ func imageAliasGet(d *Daemon, r *http.Request) response.Response {
}

s := d.State()

withEntitlements, err := extractEntitlementsFromQuery(r, entity.TypeImageAlias, false)
if err != nil {
return response.SmartError(err)
}

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)
Expand Down Expand Up @@ -3810,7 +3830,14 @@ func imageAliasGet(d *Daemon, r *http.Request) response.Response {
return response.NotFound(nil)
}

return response.SyncResponseETag(true, alias, alias)
if len(withEntitlements) > 0 {
err = reportEntitlements(r.Context(), s.Authorizer, s.IdentityCache, entity.TypeImageAlias, withEntitlements, map[*api.URL]auth.EntitlementReporter{entity.ImageAliasURL(projectName, name): &alias})
if err != nil {
return response.SmartError(err)
}
}

return response.SyncResponseETag(true, &alias, alias)
}

// swagger:operation DELETE /1.0/images/aliases/{name} images image_alias_delete
Expand Down
30 changes: 28 additions & 2 deletions lxd/instance_backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ 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"))
}
Expand Down Expand Up @@ -163,7 +168,7 @@ 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)
Expand All @@ -187,13 +192,21 @@ 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
}
}

if !recursion {
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)
}

Expand Down Expand Up @@ -427,6 +440,11 @@ 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"))
}
Expand All @@ -452,7 +470,15 @@ func instanceBackupGet(d *Daemon, r *http.Request) response.Response {
return response.SmartError(err)
}

return response.SyncResponse(true, backup.Render())
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)
}

// swagger:operation POST /1.0/instances/{name}/backups/{backup} instances instance_backup_post
Expand Down
Loading
Loading