diff --git a/lxd-agent/events.go b/lxd-agent/events.go index 3615f29c70fa..1936951b107b 100644 --- a/lxd-agent/events.go +++ b/lxd-agent/events.go @@ -72,7 +72,7 @@ func eventsSocket(d *Daemon, r *http.Request, w http.ResponseWriter) error { } // As we don't know which project we are in, subscribe to events from all projects. - listener, err := d.events.AddListener("", true, listenerConnection, strings.Split(typeStr, ","), nil, nil, nil) + listener, err := d.events.AddListener("", true, nil, listenerConnection, strings.Split(typeStr, ","), nil, nil, nil) if err != nil { return err } diff --git a/lxd/api_1.0.go b/lxd/api_1.0.go index a5d45ca5c5c4..afc89bae6ded 100644 --- a/lxd/api_1.0.go +++ b/lxd/api_1.0.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/canonical/lxd/client" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/auth/candid" "github.com/canonical/lxd/lxd/auth/oidc" "github.com/canonical/lxd/lxd/cluster" @@ -33,8 +34,8 @@ import ( var api10Cmd = APIEndpoint{ Get: APIEndpointAction{Handler: api10Get, AllowUntrusted: true}, - Patch: APIEndpointAction{Handler: api10Patch}, - Put: APIEndpointAction{Handler: api10Put}, + Patch: APIEndpointAction{Handler: api10Patch, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, + Put: APIEndpointAction{Handler: api10Put, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var api10 = []APIEndpoint{ @@ -235,6 +236,12 @@ func api10Get(d *Daemon, r *http.Request) response.Response { return response.SyncResponseETag(true, srv, nil) } + // If not authorized, return now. + err := s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectServer(), auth.EntitlementCanView) + 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 { @@ -375,11 +382,14 @@ func api10Get(d *Daemon, r *http.Request) response.Response { fullSrv.AuthUserName = requestor.Username fullSrv.AuthUserMethod = requestor.Protocol - if s.Authorizer.UserIsAdmin(r) { + err = s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectServer(), auth.EntitlementCanEdit) + if err == nil { fullSrv.Config, err = daemonConfigRender(s) if err != nil { return response.InternalError(err) } + } else if !api.StatusErrorCheck(err, http.StatusForbidden) { + return response.SmartError(err) } return response.SyncResponseETag(true, fullSrv, fullSrv.Config) diff --git a/lxd/api_cluster.go b/lxd/api_cluster.go index 4c402fa2a56e..69c21f8f6026 100644 --- a/lxd/api_cluster.go +++ b/lxd/api_cluster.go @@ -19,6 +19,7 @@ import ( "github.com/canonical/lxd/client" "github.com/canonical/lxd/lxd/acme" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/certificate" "github.com/canonical/lxd/lxd/cluster" clusterConfig "github.com/canonical/lxd/lxd/cluster/config" @@ -70,91 +71,91 @@ var targetGroupPrefix = "@" var clusterCmd = APIEndpoint{ Path: "cluster", - Get: APIEndpointAction{Handler: clusterGet, AccessHandler: allowAuthenticated}, - Put: APIEndpointAction{Handler: clusterPut}, + Get: APIEndpointAction{Handler: clusterGet, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanView)}, + Put: APIEndpointAction{Handler: clusterPut, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var clusterNodesCmd = APIEndpoint{ Path: "cluster/members", - Get: APIEndpointAction{Handler: clusterNodesGet, AccessHandler: allowAuthenticated}, - Post: APIEndpointAction{Handler: clusterNodesPost}, + Get: APIEndpointAction{Handler: clusterNodesGet, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanView)}, + Post: APIEndpointAction{Handler: clusterNodesPost, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var clusterNodeCmd = APIEndpoint{ Path: "cluster/members/{name}", - Delete: APIEndpointAction{Handler: clusterNodeDelete}, - Get: APIEndpointAction{Handler: clusterNodeGet, AccessHandler: allowAuthenticated}, - Patch: APIEndpointAction{Handler: clusterNodePatch}, - Put: APIEndpointAction{Handler: clusterNodePut}, - Post: APIEndpointAction{Handler: clusterNodePost}, + Delete: APIEndpointAction{Handler: clusterNodeDelete, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, + Get: APIEndpointAction{Handler: clusterNodeGet, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanView)}, + Patch: APIEndpointAction{Handler: clusterNodePatch, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, + Put: APIEndpointAction{Handler: clusterNodePut, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, + Post: APIEndpointAction{Handler: clusterNodePost, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var clusterNodeStateCmd = APIEndpoint{ Path: "cluster/members/{name}/state", - Get: APIEndpointAction{Handler: clusterNodeStateGet, AccessHandler: allowAuthenticated}, - Post: APIEndpointAction{Handler: clusterNodeStatePost}, + Get: APIEndpointAction{Handler: clusterNodeStateGet, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanView)}, + Post: APIEndpointAction{Handler: clusterNodeStatePost, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var clusterCertificateCmd = APIEndpoint{ Path: "cluster/certificate", - Put: APIEndpointAction{Handler: clusterCertificatePut}, + Put: APIEndpointAction{Handler: clusterCertificatePut, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var clusterGroupsCmd = APIEndpoint{ Path: "cluster/groups", - Get: APIEndpointAction{Handler: clusterGroupsGet, AccessHandler: allowAuthenticated}, - Post: APIEndpointAction{Handler: clusterGroupsPost}, + Get: APIEndpointAction{Handler: clusterGroupsGet, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanView)}, + Post: APIEndpointAction{Handler: clusterGroupsPost, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var clusterGroupCmd = APIEndpoint{ Path: "cluster/groups/{name}", - Get: APIEndpointAction{Handler: clusterGroupGet, AccessHandler: allowAuthenticated}, - Post: APIEndpointAction{Handler: clusterGroupPost}, - Put: APIEndpointAction{Handler: clusterGroupPut}, - Patch: APIEndpointAction{Handler: clusterGroupPatch}, - Delete: APIEndpointAction{Handler: clusterGroupDelete}, + Get: APIEndpointAction{Handler: clusterGroupGet, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanView)}, + Post: APIEndpointAction{Handler: clusterGroupPost, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, + Put: APIEndpointAction{Handler: clusterGroupPut, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, + Patch: APIEndpointAction{Handler: clusterGroupPatch, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, + Delete: APIEndpointAction{Handler: clusterGroupDelete, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var internalClusterAcceptCmd = APIEndpoint{ Path: "cluster/accept", - Post: APIEndpointAction{Handler: internalClusterPostAccept}, + Post: APIEndpointAction{Handler: internalClusterPostAccept, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var internalClusterRebalanceCmd = APIEndpoint{ Path: "cluster/rebalance", - Post: APIEndpointAction{Handler: internalClusterPostRebalance}, + Post: APIEndpointAction{Handler: internalClusterPostRebalance, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var internalClusterAssignCmd = APIEndpoint{ Path: "cluster/assign", - Post: APIEndpointAction{Handler: internalClusterPostAssign}, + Post: APIEndpointAction{Handler: internalClusterPostAssign, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var internalClusterHandoverCmd = APIEndpoint{ Path: "cluster/handover", - Post: APIEndpointAction{Handler: internalClusterPostHandover}, + Post: APIEndpointAction{Handler: internalClusterPostHandover, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var internalClusterRaftNodeCmd = APIEndpoint{ Path: "cluster/raft-node/{address}", - Delete: APIEndpointAction{Handler: internalClusterRaftNodeDelete}, + Delete: APIEndpointAction{Handler: internalClusterRaftNodeDelete, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var internalClusterHealCmd = APIEndpoint{ Path: "cluster/heal/{name}", - Post: APIEndpointAction{Handler: internalClusterHeal}, + Post: APIEndpointAction{Handler: internalClusterHeal, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } // swagger:operation GET /1.0/cluster cluster cluster_get diff --git a/lxd/api_internal.go b/lxd/api_internal.go index 563c5acffbf5..04e21d496aeb 100644 --- a/lxd/api_internal.go +++ b/lxd/api_internal.go @@ -19,6 +19,7 @@ import ( "github.com/gorilla/mux" "golang.org/x/sys/unix" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/backup" "github.com/canonical/lxd/lxd/db" "github.com/canonical/lxd/lxd/db/cluster" @@ -65,74 +66,74 @@ var apiInternal = []APIEndpoint{ var internalShutdownCmd = APIEndpoint{ Path: "shutdown", - Put: APIEndpointAction{Handler: internalShutdown}, + Put: APIEndpointAction{Handler: internalShutdown, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var internalReadyCmd = APIEndpoint{ Path: "ready", - Get: APIEndpointAction{Handler: internalWaitReady}, + Get: APIEndpointAction{Handler: internalWaitReady, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var internalContainerOnStartCmd = APIEndpoint{ Path: "containers/{instanceRef}/onstart", - Get: APIEndpointAction{Handler: internalContainerOnStart}, + Get: APIEndpointAction{Handler: internalContainerOnStart, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var internalContainerOnStopNSCmd = APIEndpoint{ Path: "containers/{instanceRef}/onstopns", - Get: APIEndpointAction{Handler: internalContainerOnStopNS}, + Get: APIEndpointAction{Handler: internalContainerOnStopNS, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var internalContainerOnStopCmd = APIEndpoint{ Path: "containers/{instanceRef}/onstop", - Get: APIEndpointAction{Handler: internalContainerOnStop}, + Get: APIEndpointAction{Handler: internalContainerOnStop, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var internalSQLCmd = APIEndpoint{ Path: "sql", - Get: APIEndpointAction{Handler: internalSQLGet}, - Post: APIEndpointAction{Handler: internalSQLPost}, + Get: APIEndpointAction{Handler: internalSQLGet, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, + Post: APIEndpointAction{Handler: internalSQLPost, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var internalGarbageCollectorCmd = APIEndpoint{ Path: "gc", - Get: APIEndpointAction{Handler: internalGC}, + Get: APIEndpointAction{Handler: internalGC, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var internalRAFTSnapshotCmd = APIEndpoint{ Path: "raft-snapshot", - Get: APIEndpointAction{Handler: internalRAFTSnapshot}, + Get: APIEndpointAction{Handler: internalRAFTSnapshot, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var internalImageRefreshCmd = APIEndpoint{ Path: "testing/image-refresh", - Get: APIEndpointAction{Handler: internalRefreshImage}, + Get: APIEndpointAction{Handler: internalRefreshImage, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var internalImageOptimizeCmd = APIEndpoint{ Path: "image-optimize", - Post: APIEndpointAction{Handler: internalOptimizeImage}, + Post: APIEndpointAction{Handler: internalOptimizeImage, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var internalWarningCreateCmd = APIEndpoint{ Path: "testing/warnings", - Post: APIEndpointAction{Handler: internalCreateWarning}, + Post: APIEndpointAction{Handler: internalCreateWarning, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var internalBGPStateCmd = APIEndpoint{ Path: "testing/bgp", - Get: APIEndpointAction{Handler: internalBGPState}, + Get: APIEndpointAction{Handler: internalBGPState, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } type internalImageOptimizePost struct { diff --git a/lxd/api_internal_recover.go b/lxd/api_internal_recover.go index 87ff4117a7e9..720550a38b22 100644 --- a/lxd/api_internal_recover.go +++ b/lxd/api_internal_recover.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/backup" backupConfig "github.com/canonical/lxd/lxd/backup/config" "github.com/canonical/lxd/lxd/cluster" @@ -31,13 +32,13 @@ import ( var internalRecoverValidateCmd = APIEndpoint{ Path: "recover/validate", - Post: APIEndpointAction{Handler: internalRecoverValidate}, + Post: APIEndpointAction{Handler: internalRecoverValidate, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var internalRecoverImportCmd = APIEndpoint{ Path: "recover/import", - Post: APIEndpointAction{Handler: internalRecoverImport}, + Post: APIEndpointAction{Handler: internalRecoverImport, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } // init recover adds API endpoints to handler slice. diff --git a/lxd/api_metrics.go b/lxd/api_metrics.go index c37bf5e2f573..1b78f9c79cc4 100644 --- a/lxd/api_metrics.go +++ b/lxd/api_metrics.go @@ -11,6 +11,7 @@ import ( "sync" "time" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/db" dbCluster "github.com/canonical/lxd/lxd/db/cluster" "github.com/canonical/lxd/lxd/instance" @@ -19,6 +20,7 @@ import ( "github.com/canonical/lxd/lxd/metrics" "github.com/canonical/lxd/lxd/request" "github.com/canonical/lxd/lxd/response" + "github.com/canonical/lxd/lxd/state" "github.com/canonical/lxd/shared" "github.com/canonical/lxd/shared/api" "github.com/canonical/lxd/shared/logger" @@ -41,13 +43,11 @@ var metricsCmd = APIEndpoint{ func allowMetrics(d *Daemon, r *http.Request) response.Response { s := d.State() - // Check if API is wide open. if !s.GlobalConfig.MetricsAuthentication() { return response.EmptySyncResponse } - // If not wide open, apply project access restrictions. - return allowProjectPermission("containers", "view")(d, r) + return allowPermission(auth.ObjectTypeServer, auth.EntitlementCanViewMetrics)(d, r) } // swagger:operation GET /1.0/metrics metrics metrics_get @@ -158,7 +158,7 @@ func metricsGet(d *Daemon, r *http.Request) response.Response { // If all valid, return immediately. if len(projectsToFetch) == 0 { - return response.SyncResponsePlain(true, compress, metricSet.String()) + return getFilteredMetrics(s, r, compress, metricSet) } cacheDuration := time.Duration(8) * time.Second @@ -183,7 +183,7 @@ func metricsGet(d *Daemon, r *http.Request) response.Response { // If all valid, return immediately. if len(projectsToFetch) == 0 { - return response.SyncResponsePlain(true, compress, metricSet.String()) + return getFilteredMetrics(s, r, compress, metricSet) } // Gather information about host interfaces once. @@ -286,6 +286,27 @@ func metricsGet(d *Daemon, r *http.Request) response.Response { metricsCacheLock.Unlock() + return getFilteredMetrics(s, r, compress, metricSet) +} + +func getFilteredMetrics(s *state.State, r *http.Request, compress bool, metricSet *metrics.MetricSet) response.Response { + if !s.GlobalConfig.MetricsAuthentication() { + return response.SyncResponsePlain(true, compress, metricSet.String()) + } + + // Get instances the user is allowed to view. + userHasPermission, err := s.Authorizer.GetPermissionChecker(r.Context(), r, auth.EntitlementCanView, auth.ObjectTypeInstance) + if err != nil && !api.StatusErrorCheck(err, http.StatusForbidden) { + return response.SmartError(err) + } else if err != nil { + // This is counterintuitive. We are unauthorized to get a permission checker for viewing instances because a metric type certificate + // can't view instances. However, in order to get to this point we must already have auth.EntitlementCanViewMetrics. So we can view + // the metrics but we can't do any filtering, so just return the metrics. + return response.SyncResponsePlain(true, compress, metricSet.String()) + } + + metricSet.FilterSamples(userHasPermission) + return response.SyncResponsePlain(true, compress, metricSet.String()) } diff --git a/lxd/api_project.go b/lxd/api_project.go index 986a252da2c5..05c0d8f594ae 100644 --- a/lxd/api_project.go +++ b/lxd/api_project.go @@ -13,6 +13,7 @@ import ( "github.com/gorilla/mux" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/db" "github.com/canonical/lxd/lxd/db/cluster" "github.com/canonical/lxd/lxd/db/operationtype" @@ -35,23 +36,23 @@ var projectsCmd = APIEndpoint{ Path: "projects", Get: APIEndpointAction{Handler: projectsGet, AccessHandler: allowAuthenticated}, - Post: APIEndpointAction{Handler: projectsPost}, + Post: APIEndpointAction{Handler: projectsPost, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanCreateProjects)}, } var projectCmd = APIEndpoint{ Path: "projects/{name}", - Delete: APIEndpointAction{Handler: projectDelete}, - Get: APIEndpointAction{Handler: projectGet, AccessHandler: allowAuthenticated}, - Patch: APIEndpointAction{Handler: projectPatch, AccessHandler: allowAuthenticated}, - Post: APIEndpointAction{Handler: projectPost}, - Put: APIEndpointAction{Handler: projectPut, AccessHandler: allowAuthenticated}, + Delete: APIEndpointAction{Handler: projectDelete, AccessHandler: allowPermission(auth.ObjectTypeProject, auth.EntitlementCanEdit, "name")}, + Get: APIEndpointAction{Handler: projectGet, AccessHandler: allowPermission(auth.ObjectTypeProject, auth.EntitlementCanView, "name")}, + Patch: APIEndpointAction{Handler: projectPatch, AccessHandler: allowPermission(auth.ObjectTypeProject, auth.EntitlementCanEdit, "name")}, + Post: APIEndpointAction{Handler: projectPost, AccessHandler: allowPermission(auth.ObjectTypeProject, auth.EntitlementCanEdit, "name")}, + Put: APIEndpointAction{Handler: projectPut, AccessHandler: allowPermission(auth.ObjectTypeProject, auth.EntitlementCanEdit, "name")}, } var projectStateCmd = APIEndpoint{ Path: "projects/{name}/state", - Get: APIEndpointAction{Handler: projectStateGet, AccessHandler: allowAuthenticated}, + Get: APIEndpointAction{Handler: projectStateGet, AccessHandler: allowPermission(auth.ObjectTypeProject, auth.EntitlementCanView, "name")}, } // swagger:operation GET /1.0/projects projects projects_get @@ -139,8 +140,13 @@ func projectsGet(d *Daemon, r *http.Request) response.Response { recursion := util.IsRecursionRequest(r) + userHasPermission, err := s.Authorizer.GetPermissionChecker(r.Context(), r, auth.EntitlementCanView, auth.ObjectTypeProject) + if err != nil { + return response.InternalError(err) + } + var result any - err := s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { + err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { projects, err := cluster.GetProjects(ctx, tx.Tx()) if err != nil { return err @@ -148,7 +154,7 @@ func projectsGet(d *Daemon, r *http.Request) response.Response { filtered := []api.Project{} for _, project := range projects { - if !s.Authorizer.UserHasPermission(r, project.Name, "view") { + if !userHasPermission(auth.ObjectProject(project.Name)) { continue } @@ -335,7 +341,7 @@ func projectsPost(d *Daemon, r *http.Request) response.Response { return response.SmartError(fmt.Errorf("Failed creating project %q: %w", project.Name, err)) } - err = s.Authorizer.AddProject(id, project.Name) + err = s.Authorizer.AddProject(r.Context(), id, project.Name) if err != nil { return response.SmartError(err) } @@ -405,11 +411,6 @@ func projectGet(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } - // Check user permissions - if !s.Authorizer.UserHasPermission(r, name, "view") { - return response.Forbidden(nil) - } - // Get the database entry var project *api.Project err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { @@ -475,11 +476,6 @@ func projectPut(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } - // Check user permissions - if !s.Authorizer.UserHasPermission(r, name, "manage-projects") { - return response.Forbidden(nil) - } - // Get the current data var project *api.Project err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { @@ -566,11 +562,6 @@ func projectPatch(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } - // Check user permissions - if !s.Authorizer.UserHasPermission(r, name, "manage-projects") { - return response.Forbidden(nil) - } - // Get the current data var project *api.Project err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { @@ -844,7 +835,7 @@ func projectPost(d *Daemon, r *http.Request) response.Response { return err } - err = s.Authorizer.RenameProject(id, req.Name) + err = s.Authorizer.RenameProject(r.Context(), id, name, req.Name) if err != nil { return err } @@ -922,7 +913,7 @@ func projectDelete(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } - err = s.Authorizer.DeleteProject(id) + err = s.Authorizer.DeleteProject(r.Context(), id, name) if err != nil { return response.SmartError(err) } @@ -975,11 +966,6 @@ func projectStateGet(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } - // Check user permissions. - if !s.Authorizer.UserHasPermission(r, name, "view") { - return response.Forbidden(nil) - } - // Setup the state struct. state := api.ProjectState{} diff --git a/lxd/auth/authorization.go b/lxd/auth/authorization.go index 4e1a5eb502da..34c1dcbe9dc6 100644 --- a/lxd/auth/authorization.go +++ b/lxd/auth/authorization.go @@ -1,21 +1,31 @@ package auth import ( + "context" "fmt" "net/http" + "github.com/canonical/lxd/lxd/certificate" "github.com/canonical/lxd/shared/logger" ) +const ( + // DriverTLS is the default TLS authorization driver. It is not compatible with OIDC or Candid authentication. + DriverTLS string = "tls" + + // DriverRBAC is role-based authorization. It is not compatible with TLS authentication. + DriverRBAC string = "rbac" +) + // ErrUnknownDriver is the "Unknown driver" error. var ErrUnknownDriver = fmt.Errorf("Unknown driver") var authorizers = map[string]func() authorizer{ - "tls": func() authorizer { return &tls{} }, - "rbac": func() authorizer { + DriverTLS: func() authorizer { return &tls{} }, + DriverRBAC: func() authorizer { return &rbac{ resources: map[string]string{}, - permissions: map[string]map[string][]string{}, + permissions: map[string]map[string][]Permission{}, } }, } @@ -23,40 +33,108 @@ var authorizers = map[string]func() authorizer{ type authorizer interface { Authorizer - init(name string, config map[string]any, logger logger.Logger, projectsGetFunc func() (map[int64]string, error)) - load() error + init(driverName string, logger logger.Logger) error + load(ctx context.Context, certificateCache *certificate.Cache, opts Opts) error } +// PermissionChecker is a type alias for a function that returns whether a user has required permissions on an object. +// It is returned by Authorizer.GetPermissionChecker. +type PermissionChecker func(object Object) bool + +// Authorizer is the primary external API for this package. type Authorizer interface { - AddProject(projectID int64, name string) error - DeleteProject(projectID int64) error - RenameProject(projectID int64, newName string) error + Driver() string + StopService(ctx context.Context) error + + CheckPermission(ctx context.Context, r *http.Request, object Object, entitlement Entitlement) error + GetPermissionChecker(ctx context.Context, r *http.Request, entitlement Entitlement, objectType ObjectType) (PermissionChecker, error) + + AddProject(ctx context.Context, projectID int64, projectName string) error + DeleteProject(ctx context.Context, projectID int64, projectName string) error + RenameProject(ctx context.Context, projectID int64, oldName string, newName string) error + + AddCertificate(ctx context.Context, fingerprint string) error + DeleteCertificate(ctx context.Context, fingerprint string) error + + AddStoragePool(ctx context.Context, storagePoolName string) error + DeleteStoragePool(ctx context.Context, storagePoolName string) error + + AddImage(ctx context.Context, projectName string, fingerprint string) error + DeleteImage(ctx context.Context, projectName string, fingerprint string) error + + AddImageAlias(ctx context.Context, projectName string, imageAliasName string) error + DeleteImageAlias(ctx context.Context, projectName string, imageAliasName string) error + RenameImageAlias(ctx context.Context, projectName string, oldAliasName string, newAliasName string) error + + AddInstance(ctx context.Context, projectName string, instanceName string) error + DeleteInstance(ctx context.Context, projectName string, instanceName string) error + RenameInstance(ctx context.Context, projectName string, oldInstanceName string, newInstanceName string) error + + AddNetwork(ctx context.Context, projectName string, networkName string) error + DeleteNetwork(ctx context.Context, projectName string, networkName string) error + RenameNetwork(ctx context.Context, projectName string, oldNetworkName string, newNetworkName string) error + + AddNetworkZone(ctx context.Context, projectName string, networkZoneName string) error + DeleteNetworkZone(ctx context.Context, projectName string, networkZoneName string) error - StopStatusCheck() + AddNetworkACL(ctx context.Context, projectName string, networkACLName string) error + DeleteNetworkACL(ctx context.Context, projectName string, networkACLName string) error + RenameNetworkACL(ctx context.Context, projectName string, oldNetworkACLName string, newNetworkACLName string) error - UserAccess(username string) (*UserAccess, error) - UserIsAdmin(r *http.Request) bool - UserHasPermission(r *http.Request, projectName string, permission string) bool + AddProfile(ctx context.Context, projectName string, profileName string) error + DeleteProfile(ctx context.Context, projectName string, profileName string) error + RenameProfile(ctx context.Context, projectName string, oldProfileName string, newProfileName string) error + + AddStoragePoolVolume(ctx context.Context, projectName string, storagePoolName string, storageVolumeType string, storageVolumeName string) error + DeleteStoragePoolVolume(ctx context.Context, projectName string, storagePoolName string, storageVolumeType string, storageVolumeName string) error + RenameStoragePoolVolume(ctx context.Context, projectName string, storagePoolName string, storageVolumeType string, oldStorageVolumeName string, newStorageVolumeName string) error + + AddStorageBucket(ctx context.Context, projectName string, storagePoolName string, storageBucketName string) error + DeleteStorageBucket(ctx context.Context, projectName string, storagePoolName string, storageBucketName string) error +} + +// Opts is used as part of the LoadAuthorizer function so that only the relevant configuration fields are passed into a +// particular driver. +type Opts struct { + config map[string]any + projectsGetFunc func(ctx context.Context) (map[int64]string, error) +} + +// WithConfig can be passed into LoadAuthorizer to pass in driver specific configuration. +func WithConfig(c map[string]any) func(*Opts) { + return func(o *Opts) { + o.config = c + } } -// UserAccess struct for permission checks. -type UserAccess struct { - Admin bool - Projects map[string][]string +// WithProjectsGetFunc should be passed into LoadAuthorizer when DriverRBAC is used. +func WithProjectsGetFunc(f func(ctx context.Context) (map[int64]string, error)) func(*Opts) { + return func(o *Opts) { + o.projectsGetFunc = f + } } -func LoadAuthorizer(name string, config map[string]any, logger logger.Logger, projectsGetFunc func() (map[int64]string, error)) (Authorizer, error) { - driverFunc, ok := authorizers[name] +// LoadAuthorizer instantiates, configures, and initialises an Authorizer. +func LoadAuthorizer(ctx context.Context, driver string, logger logger.Logger, certificateCache *certificate.Cache, options ...func(opts *Opts)) (Authorizer, error) { + opts := &Opts{} + for _, o := range options { + o(opts) + } + + driverFunc, ok := authorizers[driver] if !ok { return nil, ErrUnknownDriver } d := driverFunc() - d.init(name, config, logger, projectsGetFunc) + err := d.init(driver, logger) + if err != nil { + return nil, fmt.Errorf("Failed to initialize authorizer: %w", err) + } - err := d.load() + err = d.load(ctx, certificateCache, *opts) if err != nil { - return nil, err + return nil, fmt.Errorf("Failed to load authorizer: %w", err) } return d, nil diff --git a/lxd/auth/authorization_objects.go b/lxd/auth/authorization_objects.go new file mode 100644 index 000000000000..b545c8bf5396 --- /dev/null +++ b/lxd/auth/authorization_objects.go @@ -0,0 +1,293 @@ +package auth + +import ( + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/gorilla/mux" + + "github.com/canonical/lxd/shared/version" +) + +// Object is a string alias that represents an authorization object. These are formatted strings that +// uniquely identify an API resource, and can be constructed/deconstructed reliably. +// An Object is always of the form : where the identifier is a "/" delimited path containing elements that +// uniquely identify a resource. If the resource is defined at the project level, the first element of this path is always the project. +// Some example objects would be: +// - `instance:default/c1`: Instance object in project "default" and name "c1". +// - `storage_pool:local`: Storage pool object with name "local". +// - `storage_volume:default/local/custom/vol1`: Storage volume object in project "default", storage pool "local", type "custom", and name "vol1". +type Object string + +const ( + // objectTypeDelimiter is the string which separates the ObjectType from the remaining elements. Object types are + // statically defined and do not contain this character, so we can extract the object type from an object by splitting + // the string at this character. + objectTypeDelimiter = ":" + + // objectElementDelimiter is the string which separates the elements of an object that make it a uniquely identifiable + // resource. This was chosen because the character is not allowed in the majority of LXD resource names. Nevertheless + // it is still necessary to escape this character in order to reliably construct/deconstruct an Object. + objectElementDelimiter = "/" +) + +// String implements fmt.Stringer for Object. +func (o Object) String() string { + return string(o) +} + +// Type returns the ObjectType of the Object. +func (o Object) Type() ObjectType { + t, _, _ := strings.Cut(o.String(), objectTypeDelimiter) + return ObjectType(t) +} + +// Project returns the project of the Object if present. +func (o Object) Project() string { + project, _ := o.projectAndElements() + return project +} + +// Elements returns the elements that uniquely identify the authorization Object. +func (o Object) Elements() []string { + _, elements := o.projectAndElements() + return elements +} + +func (o Object) projectAndElements() (string, []string) { + validator := objectValidators[o.Type()] + _, identifier, _ := strings.Cut(o.String(), objectTypeDelimiter) + + var projectName string + escapedObjectComponents := strings.SplitN(identifier, objectElementDelimiter, -1) + components := make([]string, 0, len(escapedObjectComponents)) + for i, escapedComponent := range escapedObjectComponents { + if validator.requireProject && i == 0 { + projectName = unescape(escapedComponent) + continue + } + + components = append(components, unescape(escapedComponent)) + } + + return projectName, components +} + +func (o Object) validate() error { + objectType := o.Type() + v, ok := objectValidators[objectType] + if !ok { + return fmt.Errorf("Missing validator for object of type %q", objectType) + } + + projectName, identifierElements := o.projectAndElements() + if v.requireProject && projectName == "" { + return fmt.Errorf("Authorization objects of type %q require a project", objectType) + } + + if len(identifierElements) != v.nIdentifierElements { + return fmt.Errorf("Authorization objects of type %q require %d components to be uniquely identifiable", objectType, v.nIdentifierElements) + } + + return nil +} + +// objectValidator contains fields that can be used to determine if a string is a valid Object. +type objectValidator struct { + nIdentifierElements int + requireProject bool +} + +var objectValidators = map[ObjectType]objectValidator{ + ObjectTypeUser: {nIdentifierElements: 1, requireProject: false}, + ObjectTypeServer: {nIdentifierElements: 1, requireProject: false}, + ObjectTypeCertificate: {nIdentifierElements: 1, requireProject: false}, + ObjectTypeStoragePool: {nIdentifierElements: 1, requireProject: false}, + ObjectTypeProject: {nIdentifierElements: 0, requireProject: true}, + ObjectTypeImage: {nIdentifierElements: 1, requireProject: true}, + ObjectTypeImageAlias: {nIdentifierElements: 1, requireProject: true}, + ObjectTypeInstance: {nIdentifierElements: 1, requireProject: true}, + ObjectTypeNetwork: {nIdentifierElements: 1, requireProject: true}, + ObjectTypeNetworkACL: {nIdentifierElements: 1, requireProject: true}, + ObjectTypeNetworkZone: {nIdentifierElements: 1, requireProject: true}, + ObjectTypeProfile: {nIdentifierElements: 1, requireProject: true}, + ObjectTypeStorageBucket: {nIdentifierElements: 2, requireProject: true}, + ObjectTypeStorageVolume: {nIdentifierElements: 3, requireProject: true}, +} + +// NewObject returns an Object of the given type. The passed in arguments must be in the correct +// order (as found in the URL for the resource). This function will error if an invalid object type is +// given, or if the correct number of arguments is not passed in. +func NewObject(objectType ObjectType, projectName string, identifierElements ...string) (Object, error) { + v, ok := objectValidators[objectType] + if !ok { + return "", fmt.Errorf("Missing validator for object of type %q", objectType) + } + + if v.requireProject && projectName == "" { + return "", fmt.Errorf("Authorization objects of type %q require a project", objectType) + } + + if len(identifierElements) != v.nIdentifierElements { + return "", fmt.Errorf("Authorization objects of type %q require %d components to be uniquely identifiable", objectType, v.nIdentifierElements) + } + + builder := strings.Builder{} + builder.WriteString(string(objectType)) + builder.WriteString(objectTypeDelimiter) + if v.requireProject { + builder.WriteString(escape(projectName)) + if len(identifierElements) > 0 { + builder.WriteString(objectElementDelimiter) + } + } + + for i, c := range identifierElements { + builder.WriteString(escape(c)) + if i != len(identifierElements)-1 { + builder.WriteString(objectElementDelimiter) + } + } + + return Object(builder.String()), nil +} + +// ObjectFromRequest returns an object created from the request by evaluating the given mux vars. +// Mux vars must be provided in the order that they are found in the endpoint path. If the object +// requires a project name, this is taken from the project query parameter unless the URL begins +// with /1.0/projects. +func ObjectFromRequest(r *http.Request, objectType ObjectType, muxVars ...string) (Object, error) { + // Shortcut for server objects which don't require any arguments. + if objectType == ObjectTypeServer { + return ObjectServer(), nil + } + + muxValues := make([]string, 0, len(muxVars)) + vars := mux.Vars(r) + for _, muxVar := range muxVars { + muxValue, err := url.PathUnescape(vars[muxVar]) + if err != nil { + return "", fmt.Errorf("Failed to unescape mux var %q for object type %q: %w", muxVar, objectType, err) + } + + if muxValue == "" { + return "", fmt.Errorf("Mux var %q not found for object type %q", muxVar, objectType) + } + + muxValues = append(muxValues, muxValue) + } + + values, err := url.ParseQuery(r.URL.RawQuery) + if err != nil { + return "", err + } + + projectName := values.Get("project") + if projectName == "" { + projectName = "default" + } + + // If using projects API we want to pass in the mux var, not the query parameter. + if objectType == ObjectTypeProject && strings.HasPrefix(r.URL.Path, fmt.Sprintf("/%s/projects", version.APIVersion)) { + if len(muxValues) == 0 { + return "", fmt.Errorf("Missing project name path variable") + } + + return ObjectProject(muxValues[0]), nil + } + + return NewObject(objectType, projectName, muxValues...) +} + +// ObjectFromString parses a string into an Object. It returns an error if the string is not valid. +func ObjectFromString(objectstr string) (Object, error) { + o := Object(objectstr) + err := o.validate() + if err != nil { + return "", err + } + + return o, nil +} + +func ObjectUser(userName string) Object { + object, _ := NewObject(ObjectTypeUser, "", userName) + return object +} + +func ObjectServer() Object { + object, _ := NewObject(ObjectTypeServer, "", "lxd") + return object +} + +func ObjectCertificate(fingerprint string) Object { + object, _ := NewObject(ObjectTypeCertificate, "", fingerprint) + return object +} + +func ObjectStoragePool(storagePoolName string) Object { + object, _ := NewObject(ObjectTypeStoragePool, "", storagePoolName) + return object +} + +func ObjectProject(projectName string) Object { + object, _ := NewObject(ObjectTypeProject, projectName) + return object +} + +func ObjectImage(projectName string, imageFingerprint string) Object { + object, _ := NewObject(ObjectTypeImage, projectName, imageFingerprint) + return object +} + +func ObjectImageAlias(projectName string, aliasName string) Object { + object, _ := NewObject(ObjectTypeImageAlias, projectName, aliasName) + return object +} + +func ObjectInstance(projectName string, instanceName string) Object { + object, _ := NewObject(ObjectTypeInstance, projectName, instanceName) + return object +} + +func ObjectNetwork(projectName string, networkName string) Object { + object, _ := NewObject(ObjectTypeNetwork, projectName, networkName) + return object +} + +func ObjectNetworkACL(projectName string, networkACLName string) Object { + object, _ := NewObject(ObjectTypeNetworkACL, projectName, networkACLName) + return object +} + +func ObjectNetworkZone(projectName string, networkZoneName string) Object { + object, _ := NewObject(ObjectTypeNetworkZone, projectName, networkZoneName) + return object +} + +func ObjectProfile(projectName string, profileName string) Object { + object, _ := NewObject(ObjectTypeProfile, projectName, profileName) + return object +} + +func ObjectStorageBucket(projectName string, poolName string, bucketName string) Object { + object, _ := NewObject(ObjectTypeStorageBucket, projectName, poolName, bucketName) + return object +} + +func ObjectStorageVolume(projectName string, poolName string, volumeType string, volumeName string) Object { + object, _ := NewObject(ObjectTypeStorageVolume, projectName, poolName, volumeType, volumeName) + return object +} + +// escape escapes only the forward slash character as this is used as a delimiter. Everything else is allowed. +func escape(s string) string { + return strings.Replace(s, "/", "%2F", -1) +} + +// unescape replaces only the escaped forward slashes. +func unescape(s string) string { + return strings.Replace(s, "%2F", "/", -1) +} diff --git a/lxd/auth/authorization_objects_test.go b/lxd/auth/authorization_objects_test.go new file mode 100644 index 000000000000..7910b607968a --- /dev/null +++ b/lxd/auth/authorization_objects_test.go @@ -0,0 +1,189 @@ +package auth + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/canonical/lxd/shared" +) + +type objectSuite struct { + suite.Suite +} + +func TestObjectSuite(t *testing.T) { + suite.Run(t, new(objectSuite)) +} + +func (s *objectSuite) TestObjectCertificate() { + s.Assert().NotPanics(func() { + fingerprint := shared.TestingKeyPair().Fingerprint() + o := ObjectCertificate(fingerprint) + s.Equal(fmt.Sprintf("certificate:%s", fingerprint), string(o)) + }) +} + +func (s *objectSuite) TestObjectImage() { + s.Assert().NotPanics(func() { + fingerprint := shared.TestingKeyPair().Fingerprint() + o := ObjectImage("default", fingerprint) + s.Equal(fmt.Sprintf("image:default/%s", fingerprint), string(o)) + }) +} + +func (s *objectSuite) TestObjectImageAlias() { + s.Assert().NotPanics(func() { + o := ObjectImageAlias("default", "image_alias_name") + s.Equal("image_alias:default/image_alias_name", string(o)) + }) +} + +func (s *objectSuite) TestObjectInstance() { + s.Assert().NotPanics(func() { + o := ObjectInstance("default", "instance_name") + s.Equal("instance:default/instance_name", string(o)) + }) +} + +func (s *objectSuite) TestObjectNetwork() { + s.Assert().NotPanics(func() { + o := ObjectNetwork("default", "network_name") + s.Equal("network:default/network_name", string(o)) + }) +} + +func (s *objectSuite) TestObjectNetworkACL() { + s.Assert().NotPanics(func() { + o := ObjectNetworkACL("default", "network_acl_name") + s.Equal("network_acl:default/network_acl_name", string(o)) + }) +} + +func (s *objectSuite) TestObjectNetworkZone() { + s.Assert().NotPanics(func() { + o := ObjectNetworkZone("default", "network_zone_name") + s.Equal("network_zone:default/network_zone_name", string(o)) + }) +} + +func (s *objectSuite) TestObjectProfile() { + s.Assert().NotPanics(func() { + o := ObjectProfile("default", "profile_name") + s.Equal("profile:default/profile_name", string(o)) + }) +} + +func (s *objectSuite) TestObjectProject() { + s.Assert().NotPanics(func() { + o := ObjectProject("default") + s.Equal("project:default", string(o)) + }) +} + +func (s *objectSuite) TestObjectServer() { + s.Assert().NotPanics(func() { + o := ObjectServer() + s.Equal("server:lxd", string(o)) + }) +} + +func (s *objectSuite) TestObjectStorageBucket() { + s.Assert().NotPanics(func() { + o := ObjectStorageBucket("default", "pool_name", "storage_bucket_name") + s.Equal("storage_bucket:default/pool_name/storage_bucket_name", string(o)) + }) +} + +func (s *objectSuite) TestObjectStoragePool() { + s.Assert().NotPanics(func() { + o := ObjectStoragePool("pool_name") + s.Equal("storage_pool:pool_name", string(o)) + }) +} + +func (s *objectSuite) TestObjectStorageVolume() { + s.Assert().NotPanics(func() { + o := ObjectStorageVolume("default", "pool_name", "volume_type", "volume_name") + s.Equal("storage_volume:default/pool_name/volume_type/volume_name", string(o)) + }) +} + +func (s *objectSuite) TestObjectUser() { + s.Assert().NotPanics(func() { + o := ObjectUser("username") + s.Equal("user:username", string(o)) + }) +} + +func (s *objectSuite) TestObjectFromString() { + tests := []struct { + in string + out Object + err error + }{ + { + in: "server:lxd", + out: Object("server:lxd"), + }, + { + in: "certificate:weaowiejfoiawefpajewfpoawjfepojawef", + out: Object("certificate:weaowiejfoiawefpajewfpoawjfepojawef"), + }, + { + in: "storage_pool:local", + out: Object("storage_pool:local"), + }, + { + in: "project:default", + out: Object("project:default"), + }, + { + in: "profile:default/default", + out: Object("profile:default/default"), + }, + { + in: "image:default/eoaiwenfoaiwnefoianwef", + out: Object("image:default/eoaiwenfoaiwnefoianwef"), + }, + { + in: "image_alias:default/windows11", + out: Object("image_alias:default/windows11"), + }, + { + in: "network:default/lxdbr0", + out: Object("network:default/lxdbr0"), + }, + { + in: "network_acl:default/acl1", + out: Object("network_acl:default/acl1"), + }, + { + in: "network_zone:default/example.com", + out: Object("network_zone:default/example.com"), + }, + { + in: "storage_volume:default/local/custom/vol1", + out: Object("storage_volume:default/local/custom/vol1"), + }, + { + in: "storage_bucket:default/local/bucket1", + out: Object("storage_bucket:default/local/bucket1"), + }, + } + + for _, tt := range tests { + o, err := ObjectFromString(tt.in) + s.Equal(tt.err, err) + s.Equal(tt.out, o) + } +} + +// Objects shouldn't continuously path escape. +func (s *objectSuite) TestRemake() { + o := ObjectProject("contains/forward/slashes") + oSquared, err := ObjectFromString(o.String()) + s.Nil(err) + s.Equal(o.String(), oSquared.String()) +} diff --git a/lxd/auth/authorization_types.go b/lxd/auth/authorization_types.go new file mode 100644 index 000000000000..095f6c9fd290 --- /dev/null +++ b/lxd/auth/authorization_types.go @@ -0,0 +1,78 @@ +package auth + +// Entitlement is a type representation of a permission as it applies to a particular ObjectType. +type Entitlement string + +const ( + // Entitlements that apply to all resources. + EntitlementCanEdit Entitlement = "can_edit" + EntitlementCanView Entitlement = "can_view" + + // Server entitlements. + EntitlementCanCreateStoragePools Entitlement = "can_create_storage_pools" + EntitlementCanCreateProjects Entitlement = "can_create_projects" + EntitlementCanViewResources Entitlement = "can_view_resources" + EntitlementCanCreateCertificates Entitlement = "can_create_certificates" + EntitlementCanViewMetrics Entitlement = "can_view_metrics" + EntitlementCanOverrideClusterTargetRestriction Entitlement = "can_override_cluster_target_restriction" + EntitlementCanViewPrivilegedEvents Entitlement = "can_view_privileged_events" + + // Project entitlements. + EntitlementCanCreateImages Entitlement = "can_create_images" + EntitlementCanCreateImageAliases Entitlement = "can_create_image_aliases" + EntitlementCanCreateInstances Entitlement = "can_create_instances" + EntitlementCanCreateNetworks Entitlement = "can_create_networks" + EntitlementCanCreateNetworkACLs Entitlement = "can_create_network_acls" + EntitlementCanCreateNetworkZones Entitlement = "can_create_network_zones" + EntitlementCanCreateProfiles Entitlement = "can_create_profiles" + EntitlementCanCreateStorageVolumes Entitlement = "can_create_storage_volumes" + EntitlementCanCreateStorageBuckets Entitlement = "can_create_storage_buckets" + EntitlementCanViewOperations Entitlement = "can_view_operations" + EntitlementCanViewEvents Entitlement = "can_view_events" + + // Instance entitlements. + EntitlementCanUpdateState Entitlement = "can_update_state" + EntitlementCanConnectSFTP Entitlement = "can_connect_sftp" + EntitlementCanAccessFiles Entitlement = "can_access_files" + EntitlementCanAccessConsole Entitlement = "can_access_console" + EntitlementCanExec Entitlement = "can_exec" + + // Instance and storage volume entitlements. + EntitlementCanManageSnapshots Entitlement = "can_manage_snapshots" + EntitlementCanManageBackups Entitlement = "can_manage_backups" +) + +// ObjectType is a type of resource within LXD. +type ObjectType string + +const ( + ObjectTypeUser ObjectType = "user" + ObjectTypeServer ObjectType = "server" + ObjectTypeCertificate ObjectType = "certificate" + ObjectTypeStoragePool ObjectType = "storage_pool" + ObjectTypeProject ObjectType = "project" + ObjectTypeImage ObjectType = "image" + ObjectTypeImageAlias ObjectType = "image_alias" + ObjectTypeInstance ObjectType = "instance" + ObjectTypeNetwork ObjectType = "network" + ObjectTypeNetworkACL ObjectType = "network_acl" + ObjectTypeNetworkZone ObjectType = "network_zone" + ObjectTypeProfile ObjectType = "profile" + ObjectTypeStorageBucket ObjectType = "storage_bucket" + ObjectTypeStorageVolume ObjectType = "storage_volume" +) + +// Permission is a type representation of general permission levels in LXD. Used with TLS and RBAC drivers. +type Permission string + +const ( + PermissionAdmin Permission = "admin" + PermissionView Permission = "view" + PermissionManageProjects Permission = "manage-projects" + PermissionManageInstances Permission = "manage-containers" + PermissionManageImages Permission = "manage-images" + PermissionManageNetworks Permission = "manage-networks" + PermissionManageProfiles Permission = "manage-profiles" + PermissionManageStorageVolumes Permission = "manage-storage-volumes" + PermissionOperateInstances Permission = "operate-containers" +) diff --git a/lxd/auth/driver_common.go b/lxd/auth/driver_common.go index 197acf687e68..3d1f4861776c 100644 --- a/lxd/auth/driver_common.go +++ b/lxd/auth/driver_common.go @@ -1,19 +1,290 @@ package auth import ( + "context" + "fmt" + "net/http" + "net/url" + + "github.com/canonical/lxd/lxd/request" + "github.com/canonical/lxd/shared" "github.com/canonical/lxd/shared/logger" ) type commonAuthorizer struct { - name string - config map[string]any - logger logger.Logger - projectsGetFunc func() (map[int64]string, error) + driverName string + logger logger.Logger } -func (c *commonAuthorizer) init(name string, config map[string]any, l logger.Logger, projectsGetFunc func() (map[int64]string, error)) { - c.name = name - c.config = config +func (c *commonAuthorizer) init(driverName string, l logger.Logger) error { + if l == nil { + return fmt.Errorf("Cannot initialise authorizer: nil logger provided") + } + + l = l.AddContext(logger.Ctx{"driver": driverName}) + + c.driverName = driverName c.logger = l - c.projectsGetFunc = projectsGetFunc + return nil +} + +type requestDetails struct { + userName string + protocol string + forwardedUsername string + forwardedProtocol string + isAllProjectsRequest bool + projectName string +} + +func (r *requestDetails) isInternalOrUnix() bool { + if r.protocol == "unix" { + return true + } + + if r.protocol == "cluster" && (r.forwardedProtocol == "unix" || r.forwardedProtocol == "cluster" || r.forwardedProtocol == "") { + return true + } + + return false +} + +func (r *requestDetails) username() string { + if r.protocol == "cluster" && r.forwardedUsername != "" { + return r.forwardedUsername + } + + return r.userName +} + +func (r *requestDetails) authenticationProtocol() string { + if r.protocol == "cluster" { + return r.forwardedProtocol + } + + return r.protocol +} + +func (c *commonAuthorizer) requestDetails(r *http.Request) (*requestDetails, error) { + if r == nil { + return nil, fmt.Errorf("Cannot inspect nil request") + } else if r.URL == nil { + return nil, fmt.Errorf("Request URL is not set") + } + + val := r.Context().Value(request.CtxUsername) + if val == nil { + return nil, fmt.Errorf("Username not present in request context") + } + + username, ok := val.(string) + if !ok { + return nil, fmt.Errorf("Request context username has incorrect type") + } + + val = r.Context().Value(request.CtxProtocol) + if val == nil { + return nil, fmt.Errorf("Protocol not present in request context") + } + + protocol, ok := val.(string) + if !ok { + return nil, fmt.Errorf("Request context protocol has incorrect type") + } + + var forwardedUsername string + val = r.Context().Value(request.CtxForwardedUsername) + if val != nil { + forwardedUsername, ok = val.(string) + if !ok { + return nil, fmt.Errorf("Request context forwarded username has incorrect type") + } + } + + var forwardedProtocol string + val = r.Context().Value(request.CtxForwardedProtocol) + if val != nil { + forwardedProtocol, ok = val.(string) + if !ok { + return nil, fmt.Errorf("Request context forwarded username has incorrect type") + } + } + + values, err := url.ParseQuery(r.URL.RawQuery) + if err != nil { + return nil, fmt.Errorf("Failed to parse request query parameters: %w", err) + } + + return &requestDetails{ + userName: username, + protocol: protocol, + forwardedUsername: forwardedUsername, + forwardedProtocol: forwardedProtocol, + isAllProjectsRequest: shared.IsTrue(values.Get("all-projects")), + projectName: request.ProjectParam(r), + }, nil +} + +func (c *commonAuthorizer) Driver() string { + return c.driverName +} + +// StopService is a no-op. +func (c *commonAuthorizer) StopService(ctx context.Context) error { + return nil +} + +// AddProject is a no-op. +func (c *commonAuthorizer) AddProject(ctx context.Context, projectID int64, name string) error { + return nil +} + +// DeleteProject is a no-op. +func (c *commonAuthorizer) DeleteProject(ctx context.Context, projectID int64, name string) error { + return nil +} + +// RenameProject is a no-op. +func (c *commonAuthorizer) RenameProject(ctx context.Context, projectID int64, oldName string, newName string) error { + return nil +} + +// AddCertificate is a no-op. +func (c *commonAuthorizer) AddCertificate(ctx context.Context, fingerprint string) error { + return nil +} + +// DeleteCertificate is a no-op. +func (c *commonAuthorizer) DeleteCertificate(ctx context.Context, fingerprint string) error { + return nil +} + +// AddStoragePool is a no-op. +func (c *commonAuthorizer) AddStoragePool(ctx context.Context, storagePoolName string) error { + return nil +} + +// DeleteStoragePool is a no-op. +func (c *commonAuthorizer) DeleteStoragePool(ctx context.Context, storagePoolName string) error { + return nil +} + +// AddImage is a no-op. +func (c *commonAuthorizer) AddImage(ctx context.Context, projectName string, fingerprint string) error { + return nil +} + +// DeleteImage is a no-op. +func (c *commonAuthorizer) DeleteImage(ctx context.Context, projectName string, fingerprint string) error { + return nil +} + +// AddImageAlias is a no-op. +func (c *commonAuthorizer) AddImageAlias(ctx context.Context, projectName string, imageAliasName string) error { + return nil +} + +// DeleteImageAlias is a no-op. +func (c *commonAuthorizer) DeleteImageAlias(ctx context.Context, projectName string, imageAliasName string) error { + return nil +} + +// RenameImageAlias is a no-op. +func (c *commonAuthorizer) RenameImageAlias(ctx context.Context, projectName string, oldAliasName string, newAliasName string) error { + return nil +} + +// AddInstance is a no-op. +func (c *commonAuthorizer) AddInstance(ctx context.Context, projectName string, instanceName string) error { + return nil +} + +// DeleteInstance is a no-op. +func (c *commonAuthorizer) DeleteInstance(ctx context.Context, projectName string, instanceName string) error { + return nil +} + +// RenameInstance is a no-op. +func (c *commonAuthorizer) RenameInstance(ctx context.Context, projectName string, oldInstanceName string, newInstanceName string) error { + return nil +} + +// AddNetwork is a no-op. +func (c *commonAuthorizer) AddNetwork(ctx context.Context, projectName string, networkName string) error { + return nil +} + +// DeleteNetwork is a no-op. +func (c *commonAuthorizer) DeleteNetwork(ctx context.Context, projectName string, networkName string) error { + return nil +} + +// RenameNetwork is a no-op. +func (c *commonAuthorizer) RenameNetwork(ctx context.Context, projectName string, oldNetworkName string, newNetworkName string) error { + return nil +} + +// AddNetworkZone is a no-op. +func (c *commonAuthorizer) AddNetworkZone(ctx context.Context, projectName string, networkZoneName string) error { + return nil +} + +// DeleteNetworkZone is a no-op. +func (c *commonAuthorizer) DeleteNetworkZone(ctx context.Context, projectName string, networkZoneName string) error { + return nil +} + +// AddNetworkACL is a no-op. +func (c *commonAuthorizer) AddNetworkACL(ctx context.Context, projectName string, networkACLName string) error { + return nil +} + +// DeleteNetworkACL is a no-op. +func (c *commonAuthorizer) DeleteNetworkACL(ctx context.Context, projectName string, networkACLName string) error { + return nil +} + +// RenameNetworkACL is a no-op. +func (c *commonAuthorizer) RenameNetworkACL(ctx context.Context, projectName string, oldNetworkACLName string, newNetworkACLName string) error { + return nil +} + +// AddProfile is a no-op. +func (c *commonAuthorizer) AddProfile(ctx context.Context, projectName string, profileName string) error { + return nil +} + +// DeleteProfile is a no-op. +func (c *commonAuthorizer) DeleteProfile(ctx context.Context, projectName string, profileName string) error { + return nil +} + +// RenameProfile is a no-op. +func (c *commonAuthorizer) RenameProfile(ctx context.Context, projectName string, oldProfileName string, newProfileName string) error { + return nil +} + +// AddStoragePoolVolume is a no-op. +func (c *commonAuthorizer) AddStoragePoolVolume(ctx context.Context, projectName string, storagePoolName string, storageVolumeType string, storageVolumeName string) error { + return nil +} + +// DeleteStoragePoolVolume is a no-op. +func (c *commonAuthorizer) DeleteStoragePoolVolume(ctx context.Context, projectName string, storagePoolName string, storageVolumeType string, storageVolumeName string) error { + return nil +} + +// RenameStoragePoolVolume is a no-op. +func (c *commonAuthorizer) RenameStoragePoolVolume(ctx context.Context, projectName string, storagePoolName string, storageVolumeType string, oldStorageVolumeName string, newStorageVolumeName string) error { + return nil +} + +// AddStorageBucket is a no-op. +func (c *commonAuthorizer) AddStorageBucket(ctx context.Context, projectName string, storagePoolName string, storageBucketName string) error { + return nil +} + +// DeleteStorageBucket is a no-op. +func (c *commonAuthorizer) DeleteStorageBucket(ctx context.Context, projectName string, storagePoolName string, storageBucketName string) error { + return nil } diff --git a/lxd/auth/driver_rbac.go b/lxd/auth/driver_rbac.go index e8685638c115..8e6c4911c368 100644 --- a/lxd/auth/driver_rbac.go +++ b/lxd/auth/driver_rbac.go @@ -19,42 +19,25 @@ import ( "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" + "github.com/canonical/lxd/lxd/certificate" "github.com/canonical/lxd/shared" + "github.com/canonical/lxd/shared/api" "github.com/canonical/lxd/shared/logger" ) -type rbacResource struct { - Identifier string `json:"identifier"` - Name string `json:"name"` -} - -type rbacResourcePost struct { - LastSyncID *string `json:"last-sync-id"` - Updates []rbacResource `json:"updates,omitempty"` - Removals []string `json:"removals,omitempty"` -} - -type rbacResourcePostResponse struct { - SyncID string `json:"sync-id"` -} - -type rbacStatus struct { - LastChange string `json:"last-change"` -} - // Errors. -var errUnknownUser = fmt.Errorf("Unknown RBAC user") +var errUnknownUser = api.StatusErrorf(http.StatusForbidden, "Unknown RBAC user") // rbac represents an RBAC server. type rbac struct { commonAuthorizer - tls - + tls *tls apiURL string agentPrivateKey string agentPublicKey string agentAuthURL string agentUsername string + projectsGetFunc func(ctx context.Context) (map[int64]string, error) lastSyncID string client *httpbakery.Client @@ -64,21 +47,19 @@ type rbac struct { ctxCancel context.CancelFunc resources map[string]string // Maps name to identifier - resourcesLock sync.Mutex - - permissions map[string]map[string][]string + resourcesLock sync.RWMutex - permissionsLock *sync.Mutex + // Permission cache of username to map of project name to slice of Permission + permissions map[string]map[string][]Permission + permissionsLock sync.RWMutex } -func (r *rbac) load() error { - err := r.validateConfig() +func (r *rbac) load(ctx context.Context, certificateCache *certificate.Cache, opts Opts) error { + err := r.configure(opts) if err != nil { return err } - r.permissionsLock = &sync.Mutex{} - // Setup context r.ctx, r.ctxCancel = context.WithCancel(context.Background()) @@ -117,7 +98,7 @@ func (r *rbac) load() error { // Perform full sync when online go func() { for { - err = r.syncProjects() + err = r.syncProjects(r.ctx) if err != nil { time.Sleep(time.Minute) continue @@ -127,199 +108,251 @@ func (r *rbac) load() error { } }() + r.tls = &tls{} + err = r.tls.load(ctx, certificateCache, opts) + if err != nil { + return err + } + r.startStatusCheck() return nil } -func (r *rbac) validateConfig() error { - val, ok := r.config["rbac.agent.private_key"] +func (r *rbac) configure(opts Opts) error { + if opts.config == nil { + return fmt.Errorf("Missing RBAC configuration") + } + + val, ok := opts.config["rbac.agent.private_key"] if !ok { return fmt.Errorf("Missing rbac.agent.private_key") } r.agentPrivateKey = val.(string) - val, ok = r.config["rbac.agent.public_key"] + val, ok = opts.config["rbac.agent.public_key"] if !ok { return fmt.Errorf("Missing rbac.agent.public_key") } r.agentPublicKey = val.(string) - val, ok = r.config["rbac.agent.url"] + val, ok = opts.config["rbac.agent.url"] if !ok { return fmt.Errorf("Missing rbac.agent.url") } r.agentAuthURL = val.(string) - val, ok = r.config["rbac.agent.username"] + val, ok = opts.config["rbac.agent.username"] if !ok { return fmt.Errorf("Missing rbac.agent.username") } r.agentUsername = val.(string) - val, ok = r.config["rbac.api.url"] + val, ok = opts.config["rbac.api.url"] if !ok { return fmt.Errorf("Missing rbac.api.url") } r.apiURL = val.(string) + if opts.projectsGetFunc == nil { + return fmt.Errorf("Missing projects hook for RBAC driver") + } + + r.projectsGetFunc = opts.projectsGetFunc + return nil } -// startStatusCheck runs a status checking loop. -func (r *rbac) startStatusCheck() { - var status rbacStatus - - // Figure out the new URL. - u, err := url.Parse(r.apiURL) +// CheckPermission syncs the users permissions with the RBAC server, then maps the given Object and Entitlement to an RBAC permission +// and checks this against the users permissions. +func (r *rbac) CheckPermission(ctx context.Context, req *http.Request, object Object, entitlement Entitlement) error { + details, err := r.requestDetails(req) if err != nil { - logger.Errorf("Failed to parse RBAC url: %v", err) - return + return api.StatusErrorf(http.StatusForbidden, "Failed to extract request details: %v", err) } - u.Path = path.Join(u.Path, "/api/service/v1/changes") + if details.isInternalOrUnix() { + return nil + } - go func() { - for { - if r.ctx.Err() != nil { - return - } + // Use the TLS driver if the user authenticated with TLS. + if details.authenticationProtocol() == api.AuthenticationMethodTLS { + return r.tls.CheckPermission(ctx, req, object, entitlement) + } - if status.LastChange != "" { - values := url.Values{} - values.Set("last-change", status.LastChange) - u.RawQuery = values.Encode() - } + r.permissionsLock.Lock() + defer r.permissionsLock.Unlock() - req, err := http.NewRequestWithContext(r.ctx, "GET", u.String(), nil) - if err != nil { - if err == context.Canceled { - return - } + username := details.username() + permissions, ok := r.permissions[username] + if !ok { + err := r.syncPermissions(ctx, username) + if err != nil { + return fmt.Errorf("Failed to sync user permissions with RBAC server: %w", err) + } - logger.Errorf("Failed to prepare RBAC query: %v", err) - return - } + permissions, ok = r.permissions[username] + if !ok { + return errUnknownUser + } + } - resp, err := r.client.Do(req) - if err != nil { - if err == context.Canceled { - return - } + if shared.ValueInSlice(PermissionAdmin, permissions[""]) { + // Admin + return nil + } - // Handle server/load-balancer timeouts, errors aren't properly wrapped so checking string. - if strings.HasSuffix(err.Error(), "EOF") { - continue - } + if details.isAllProjectsRequest { + // Only admins can use the all-projects parameter. + return api.StatusErrorf(http.StatusForbidden, "User is not an administrator") + } - logger.Errorf("Failed to connect to RBAC, re-trying: %v", err) - time.Sleep(5 * time.Second) - continue - } + // Check server level object types + switch object.Type() { + case ObjectTypeServer: + if entitlement == EntitlementCanView || entitlement == EntitlementCanViewResources || entitlement == EntitlementCanViewMetrics { + return nil + } - if resp.StatusCode == 504 { - // 504 indicates the server timed out the background connection, just re-connect. - _ = resp.Body.Close() - continue - } + return api.StatusErrorf(http.StatusForbidden, "User is not an administrator") + case ObjectTypeStoragePool, ObjectTypeCertificate: + if entitlement == EntitlementCanView { + return nil + } - if resp.StatusCode != 200 { - // For other errors we assume a server restart and give it a few seconds. - _ = resp.Body.Close() - logger.Debugf("RBAC server disconnected, re-connecting. (code=%v)", resp.StatusCode) - time.Sleep(5 * time.Second) - continue - } + return api.StatusErrorf(http.StatusForbidden, "User is not an administrator") + } - err = json.NewDecoder(resp.Body).Decode(&status) - _ = resp.Body.Close() - if err != nil { - logger.Errorf("Failed to parse RBAC response, re-trying: %v", err) - time.Sleep(5 * time.Second) - continue - } + permission, err := r.relationToPermission(object, entitlement) + if err != nil { + return err + } - r.lastChange = status.LastChange - logger.Debugf("RBAC change detected, flushing cache") - r.flushCache() - } - }() -} + projectName := object.Project() + if !shared.ValueInSlice(permission, permissions[projectName]) { + return api.StatusErrorf(http.StatusForbidden, "User %q does not have permission %q on project %q", username, permission, projectName) + } -// StopStatusCheck stops the periodic status checker. -func (r *rbac) StopStatusCheck() { - r.ctxCancel() + return nil } -// syncProjects updates the list of projects in RBAC. -func (r *rbac) syncProjects() error { - if r.projectsGetFunc == nil { - return fmt.Errorf("ProjectsFunc isn't configured yet, cannot sync") +// GetPermissionChecker syncs the users permissions with the RBAC server, then in the returned PermissionChecker maps the +// given Object and Entitlement to an RBAC permission and checks this against the users permissions. +func (r *rbac) GetPermissionChecker(ctx context.Context, req *http.Request, entitlement Entitlement, objectType ObjectType) (PermissionChecker, error) { + allowFunc := func(b bool) func(Object) bool { + return func(Object) bool { + return b + } } - resources := []rbacResource{} - resourcesMap := map[string]string{} - - // Get all projects - projects, err := r.projectsGetFunc() + details, err := r.requestDetails(req) if err != nil { - return err + return nil, api.StatusErrorf(http.StatusForbidden, "Failed to extract request details: %v", err) } - // Convert to RBAC format - for id, name := range projects { - resources = append(resources, rbacResource{ - Name: name, - Identifier: strconv.FormatInt(id, 10), - }) + if details.isInternalOrUnix() { + return allowFunc(true), nil + } - resourcesMap[name] = strconv.FormatInt(id, 10) + // Use the TLS driver if the user authenticated with TLS. + if details.authenticationProtocol() == api.AuthenticationMethodTLS { + return r.tls.GetPermissionChecker(ctx, req, entitlement, objectType) } - // Update RBAC - err = r.postResources(resources, nil, true) - if err != nil { - return err + r.permissionsLock.Lock() + defer r.permissionsLock.Unlock() + + username := details.username() + permissions, ok := r.permissions[username] + if !ok { + err := r.syncPermissions(ctx, username) + if err != nil { + return nil, fmt.Errorf("Failed to sync user permissions with RBAC server: %w", err) + } + + permissions, ok = r.permissions[username] + if !ok { + return nil, errUnknownUser + } } - // Update project map - r.resourcesLock.Lock() - r.resources = resourcesMap - r.resourcesLock.Unlock() + if shared.ValueInSlice(PermissionAdmin, permissions[""]) { + // Admin user. Allow all. + return allowFunc(true), nil + } - return nil + if details.isAllProjectsRequest { + // Only admins can use the all-projects parameter. + return nil, api.StatusErrorf(http.StatusForbidden, "User is not an administrator") + } + + // Check server level object types + switch objectType { + case ObjectTypeServer: + if entitlement == EntitlementCanView || entitlement == EntitlementCanViewResources || entitlement == EntitlementCanViewMetrics { + return allowFunc(true), nil + } + + return nil, api.StatusErrorf(http.StatusForbidden, "User is not an administrator") + case ObjectTypeStoragePool, ObjectTypeCertificate: + if entitlement == EntitlementCanView { + return allowFunc(true), nil + } + + return nil, api.StatusErrorf(http.StatusForbidden, "User is not an administrator") + } + + // Error if user does not have access to the project (unless we're getting projects, where we want to filter the results). + _, ok = permissions[details.projectName] + if !ok && objectType != ObjectTypeProject { + return nil, api.StatusErrorf(http.StatusForbidden, "User does not have permissions for project %q", details.projectName) + } + + return func(object Object) bool { + // Acquire read lock on the permissions cache. + r.permissionsLock.RLock() + defer r.permissionsLock.RUnlock() + + permission, err := r.relationToPermission(object, entitlement) + if err != nil { + r.logger.Error("Could not convert object and entitlement to RBAC permission", logger.Ctx{"object": object, "entitlement": entitlement, "error": err}) + return false + } + + return shared.ValueInSlice(permission, permissions[object.Project()]) + }, nil } // AddProject adds a new project resource to RBAC. -func (r *rbac) AddProject(projectID int64, name string) error { +func (r *rbac) AddProject(ctx context.Context, projectID int64, projectName string) error { resource := rbacResource{ - Name: name, + Name: projectName, Identifier: strconv.FormatInt(projectID, 10), } // Update RBAC - err := r.postResources([]rbacResource{resource}, nil, false) + err := r.postResources(ctx, []rbacResource{resource}, nil, false) if err != nil { return err } // Update project map r.resourcesLock.Lock() - r.resources[name] = strconv.FormatInt(projectID, 10) + r.resources[projectName] = strconv.FormatInt(projectID, 10) r.resourcesLock.Unlock() return nil } // DeleteProject adds a new project resource to RBAC. -func (r *rbac) DeleteProject(projectID int64) error { +func (r *rbac) DeleteProject(ctx context.Context, projectID int64, _ string) error { // Update RBAC - err := r.postResources(nil, []string{strconv.FormatInt(projectID, 10)}, false) + err := r.postResources(ctx, nil, []string{strconv.FormatInt(projectID, 10)}, false) if err != nil { return err } @@ -338,54 +371,113 @@ func (r *rbac) DeleteProject(projectID int64) error { } // RenameProject renames an existing project resource in RBAC. -func (r *rbac) RenameProject(projectID int64, name string) error { - return r.AddProject(projectID, name) +func (r *rbac) RenameProject(ctx context.Context, projectID int64, oldName string, newName string) error { + return r.AddProject(ctx, projectID, newName) } -// UserAccess returns a UserAccess struct for the user. -func (r *rbac) UserAccess(username string) (*UserAccess, error) { - r.permissionsLock.Lock() - defer r.permissionsLock.Unlock() +// StopService stops the periodic status checker. +func (r *rbac) StopService(ctx context.Context) error { + r.ctxCancel() + return nil +} - // Check whether the permissions are cached. - _, cached := r.permissions[username] +type rbacResource struct { + Identifier string `json:"identifier"` + Name string `json:"name"` +} - if !cached { - _ = r.syncPermissions(username) - } +type rbacResourcePost struct { + LastSyncID *string `json:"last-sync-id"` + Updates []rbacResource `json:"updates,omitempty"` + Removals []string `json:"removals,omitempty"` +} - // Checked if the user exists. - permissions, ok := r.permissions[username] - if !ok { - return nil, errUnknownUser - } +type rbacResourcePostResponse struct { + SyncID string `json:"sync-id"` +} + +type rbacStatus struct { + LastChange string `json:"last-change"` +} - // Prepare the response. - access := UserAccess{ - Admin: shared.ValueInSlice("admin", permissions[""]), - Projects: map[string][]string{}, +// startStatusCheck runs a status checking loop. +func (r *rbac) startStatusCheck() { + var status rbacStatus + + // Figure out the new URL. + u, err := url.Parse(r.apiURL) + if err != nil { + logger.Errorf("Failed to parse RBAC url: %v", err) + return } - for k, v := range permissions { - // Skip the global permissions. - if k == "" { - continue - } + u.Path = path.Join(u.Path, "/api/service/v1/changes") - // Look for project name. - for projectName, resourceID := range r.resources { - if k != resourceID { + go func() { + for { + if r.ctx.Err() != nil { + return + } + + if status.LastChange != "" { + values := url.Values{} + values.Set("last-change", status.LastChange) + u.RawQuery = values.Encode() + } + + req, err := http.NewRequestWithContext(r.ctx, "GET", u.String(), nil) + if err != nil { + if err == context.Canceled { + return + } + + logger.Errorf("Failed to prepare RBAC query: %v", err) + return + } + + resp, err := r.client.Do(req) + if err != nil { + if err == context.Canceled { + return + } + + // Handle server/load-balancer timeouts, errors aren't properly wrapped so checking string. + if strings.HasSuffix(err.Error(), "EOF") { + continue + } + + logger.Errorf("Failed to connect to RBAC, re-trying: %v", err) + time.Sleep(5 * time.Second) continue } - access.Projects[projectName] = v - break - } + if resp.StatusCode == 504 { + // 504 indicates the server timed out the background connection, just re-connect. + _ = resp.Body.Close() + continue + } - // Ignore unknown projects. - } + if resp.StatusCode != 200 { + // For other errors we assume a server restart and give it a few seconds. + _ = resp.Body.Close() + logger.Debugf("RBAC server disconnected, re-connecting. (code=%v)", resp.StatusCode) + time.Sleep(5 * time.Second) + continue + } - return &access, nil + err = json.NewDecoder(resp.Body).Decode(&status) + _ = resp.Body.Close() + if err != nil { + logger.Errorf("Failed to parse RBAC response, re-trying: %v", err) + time.Sleep(5 * time.Second) + continue + } + + r.lastChange = status.LastChange + logger.Debugf("RBAC change detected, flushing cache") + r.flushCache() + } + }() } func (r *rbac) flushCache() { @@ -405,7 +497,7 @@ func (r *rbac) flushCache() { logger.Info("Flushed RBAC permissions cache") } -func (r *rbac) syncAdmin(username string) bool { +func (r *rbac) syncAdmin(ctx context.Context, username string) bool { u, err := url.Parse(r.apiURL) if err != nil { return false @@ -416,7 +508,7 @@ func (r *rbac) syncAdmin(username string) bool { u.RawQuery = values.Encode() u.Path = path.Join(u.Path, "/api/service/v1/resources/lxd/permissions-for-user") - req, err := http.NewRequest("GET", u.String(), nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) if err != nil { return false } @@ -438,7 +530,7 @@ func (r *rbac) syncAdmin(username string) bool { return shared.ValueInSlice("admin", permissions[""]) } -func (r *rbac) syncPermissions(username string) error { +func (r *rbac) syncPermissions(ctx context.Context, username string) error { u, err := url.Parse(r.apiURL) if err != nil { return err @@ -449,7 +541,7 @@ func (r *rbac) syncPermissions(username string) error { u.RawQuery = values.Encode() u.Path = path.Join(u.Path, "/api/service/v1/resources/project/permissions-for-user") - req, err := http.NewRequest("GET", u.String(), nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) if err != nil { return err } @@ -461,27 +553,89 @@ func (r *rbac) syncPermissions(username string) error { defer func() { _ = resp.Body.Close() }() - var permissions map[string][]string + var permissions map[string][]Permission err = json.NewDecoder(resp.Body).Decode(&permissions) if err != nil { return err } - if r.syncAdmin(username) { - permissions[""] = []string{"admin"} + if r.syncAdmin(ctx, username) { + permissions[""] = []Permission{PermissionAdmin} + } + + r.resourcesLock.Lock() + defer r.resourcesLock.Unlock() + + projectPermissions := make(map[string][]Permission) + for k, v := range permissions { + if k == "" { + projectPermissions[k] = v + continue + } + + // Look for project name. + for projectName, resourceID := range r.resources { + if k != resourceID { + continue + } + + projectPermissions[projectName] = v + break + } + + // Ignore unknown projects. } // No need to acquire the lock since the caller (HasPermission) already has it. - r.permissions[username] = permissions + r.permissions[username] = projectPermissions + + return nil +} + +// syncProjects updates the list of projects in RBAC. +func (r *rbac) syncProjects(ctx context.Context) error { + if r.projectsGetFunc == nil { + return fmt.Errorf("ProjectsFunc isn't configured yet, cannot sync") + } + + resources := []rbacResource{} + resourcesMap := map[string]string{} + + // Get all projects + projects, err := r.projectsGetFunc(ctx) + if err != nil { + return err + } + + // Convert to RBAC format + for id, name := range projects { + resources = append(resources, rbacResource{ + Name: name, + Identifier: strconv.FormatInt(id, 10), + }) + + resourcesMap[name] = strconv.FormatInt(id, 10) + } + + // Update RBAC + err = r.postResources(ctx, resources, nil, true) + if err != nil { + return err + } + + // Update project map + r.resourcesLock.Lock() + r.resources = resourcesMap + r.resourcesLock.Unlock() return nil } -func (r *rbac) postResources(updates []rbacResource, removals []string, force bool) error { +func (r *rbac) postResources(ctx context.Context, updates []rbacResource, removals []string, force bool) error { // Make sure that we have a baseline sync in place if !force && r.lastSyncID == "" { - return r.syncProjects() + return r.syncProjects(ctx) } // Generate the URL @@ -510,7 +664,7 @@ func (r *rbac) postResources(updates []rbacResource, removals []string, force bo } // Perform the request - req, err := http.NewRequest("POST", u.String(), bytes.NewReader(body)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewReader(body)) if err != nil { return err } @@ -527,7 +681,7 @@ func (r *rbac) postResources(updates []rbacResource, removals []string, force bo // Handle errors if resp.StatusCode == 409 { // Sync IDs don't match, force sync - return r.syncProjects() + return r.syncProjects(ctx) } else if resp.StatusCode != http.StatusOK { // Something went wrong return errors.New(resp.Status) @@ -544,3 +698,160 @@ func (r *rbac) postResources(updates []rbacResource, removals []string, force bo return nil } + +// relationToPermission is a mapping from fine-grained Object and Entitlement permissions to a less fine-grained RBAC Permission. +// This function will error if there is no mapping. This can be the case when an endpoint does not require any permissions, such +// as `GET /1.0/storage-pools`. These should be handled separately. +func (r *rbac) relationToPermission(object Object, entitlement Entitlement) (Permission, error) { + switch object.Type() { + case ObjectTypeServer: + switch entitlement { + case EntitlementCanEdit: + return PermissionAdmin, nil + case EntitlementCanCreateStoragePools: + return PermissionAdmin, nil + case EntitlementCanCreateProjects: + return PermissionAdmin, nil + case EntitlementCanCreateCertificates: + return PermissionAdmin, nil + case EntitlementCanOverrideClusterTargetRestriction: + return PermissionAdmin, nil + case EntitlementCanViewPrivilegedEvents: + return PermissionAdmin, nil + } + + case ObjectTypeCertificate: + switch entitlement { + case EntitlementCanEdit: + return PermissionAdmin, nil + } + + case ObjectTypeStoragePool: + switch entitlement { + case EntitlementCanEdit: + return PermissionAdmin, nil + } + + case ObjectTypeProject: + switch entitlement { + case EntitlementCanEdit: + return PermissionManageProjects, nil + case EntitlementCanView: + return PermissionView, nil + case EntitlementCanCreateInstances: + return PermissionManageInstances, nil + case EntitlementCanCreateImages: + return PermissionManageImages, nil + case EntitlementCanCreateImageAliases: + return PermissionManageImages, nil + case EntitlementCanCreateNetworks: + return PermissionManageNetworks, nil + case EntitlementCanCreateNetworkACLs: + return PermissionManageNetworks, nil + case EntitlementCanCreateNetworkZones: + return PermissionManageNetworks, nil + case EntitlementCanCreateProfiles: + return PermissionManageProfiles, nil + case EntitlementCanCreateStorageVolumes: + return PermissionManageStorageVolumes, nil + case EntitlementCanCreateStorageBuckets: + return PermissionManageStorageVolumes, nil + case EntitlementCanViewOperations: + return PermissionView, nil + case EntitlementCanViewEvents: + return PermissionView, nil + } + + case ObjectTypeImage: + switch entitlement { + case EntitlementCanEdit: + return PermissionManageImages, nil + case EntitlementCanView: + return PermissionView, nil + } + + case ObjectTypeImageAlias: + switch entitlement { + case EntitlementCanEdit: + return PermissionManageImages, nil + case EntitlementCanView: + return PermissionView, nil + } + + case ObjectTypeInstance: + switch entitlement { + case EntitlementCanEdit: + return PermissionManageInstances, nil + case EntitlementCanView: + return PermissionView, nil + case EntitlementCanUpdateState: + return PermissionOperateInstances, nil + case EntitlementCanManageBackups: + return PermissionOperateInstances, nil + case EntitlementCanManageSnapshots: + return PermissionOperateInstances, nil + case EntitlementCanConnectSFTP: + return PermissionOperateInstances, nil + case EntitlementCanAccessFiles: + return PermissionOperateInstances, nil + case EntitlementCanAccessConsole: + return PermissionOperateInstances, nil + case EntitlementCanExec: + return PermissionOperateInstances, nil + } + + case ObjectTypeNetwork: + switch entitlement { + case EntitlementCanEdit: + return PermissionManageNetworks, nil + case EntitlementCanView: + return PermissionView, nil + } + + case ObjectTypeNetworkACL: + switch entitlement { + case EntitlementCanEdit: + return PermissionManageNetworks, nil + case EntitlementCanView: + return PermissionView, nil + } + + case ObjectTypeNetworkZone: + switch entitlement { + case EntitlementCanEdit: + return PermissionManageNetworks, nil + case EntitlementCanView: + return PermissionView, nil + } + + case ObjectTypeProfile: + switch entitlement { + case EntitlementCanEdit: + return PermissionManageProfiles, nil + case EntitlementCanView: + return PermissionView, nil + } + + case ObjectTypeStorageBucket: + switch entitlement { + case EntitlementCanEdit: + return PermissionManageStorageVolumes, nil + case EntitlementCanView: + return PermissionView, nil + } + + case ObjectTypeStorageVolume: + switch entitlement { + case EntitlementCanEdit: + return PermissionManageStorageVolumes, nil + case EntitlementCanManageBackups: + return PermissionManageStorageVolumes, nil + case EntitlementCanManageSnapshots: + return PermissionManageStorageVolumes, nil + case EntitlementCanView: + return PermissionView, nil + } + } + + return "", fmt.Errorf("Could not map object %q and entitlement %q to an RBAC permission", object, entitlement) +} diff --git a/lxd/auth/driver_tls.go b/lxd/auth/driver_tls.go index 1c41b71ba7c7..6e1c2e7e837e 100644 --- a/lxd/auth/driver_tls.go +++ b/lxd/auth/driver_tls.go @@ -1,65 +1,176 @@ package auth import ( + "context" + "errors" "net/http" - "github.com/canonical/lxd/lxd/request" + "github.com/canonical/lxd/lxd/certificate" "github.com/canonical/lxd/shared" + "github.com/canonical/lxd/shared/api" + "github.com/canonical/lxd/shared/logger" ) type tls struct { commonAuthorizer + certificates *certificate.Cache } -func (a *tls) load() error { - return nil -} +func (t *tls) load(ctx context.Context, certificateCache *certificate.Cache, opts Opts) error { + if certificateCache == nil { + return errors.New("TLS authorization driver requires a certificate cache") + } -// AddProject is a no-op. It notifies the authorization service about new projects. -func (a *tls) AddProject(projectID int64, name string) error { + t.certificates = certificateCache return nil } -// DeleteProject is a no-op. It notifies the authorization service about deleted projects. -func (a *tls) DeleteProject(projectID int64) error { - return nil -} +// CheckPermission returns an error if the user does not have the given Entitlement on the given Object. +func (t *tls) CheckPermission(ctx context.Context, r *http.Request, object Object, entitlement Entitlement) error { + details, err := t.requestDetails(r) + if err != nil { + return api.StatusErrorf(http.StatusForbidden, "Failed to extract request details: %v", err) + } + + if details.isInternalOrUnix() { + return nil + } + + authenticationProtocol := details.authenticationProtocol() + if authenticationProtocol != api.AuthenticationMethodTLS { + t.logger.Warn("Authentication protocol is not compatible with authorization driver", logger.Ctx{"protocol": authenticationProtocol}) + // Return nil. If the server has been configured with an authentication method but no associated authorization driver, + // the default is to give these authenticated users admin privileges. + return nil + } + + certType, isNotRestricted, projectNames, err := t.certificateDetails(details.username()) + if err != nil { + return err + } + + if isNotRestricted || (certType == certificate.TypeMetrics && entitlement == EntitlementCanViewMetrics) { + return nil + } + + if details.isAllProjectsRequest { + // Only admins (users with non-restricted certs) can use the all-projects parameter. + return api.StatusErrorf(http.StatusForbidden, "Certificate is restricted") + } + + // Check server level object types + switch object.Type() { + case ObjectTypeServer: + if entitlement == EntitlementCanView || entitlement == EntitlementCanViewResources || entitlement == EntitlementCanViewMetrics { + return nil + } + + return api.StatusErrorf(http.StatusForbidden, "Certificate is restricted") + case ObjectTypeStoragePool, ObjectTypeCertificate: + if entitlement == EntitlementCanView { + return nil + } + + return api.StatusErrorf(http.StatusForbidden, "Certificate is restricted") + } + + // Check project level permissions against the certificates project list. + projectName := object.Project() + if !shared.ValueInSlice(projectName, projectNames) { + return api.StatusErrorf(http.StatusForbidden, "User does not have permission for project %q", projectName) + } -// RenameProject is a no-op. It notifies the authorization service that a project has been renamed. -func (a *tls) RenameProject(projectID int64, newName string) error { return nil } -// StopStatusCheck is a no-op. -func (a *tls) StopStatusCheck() { -} +// GetPermissionChecker returns a function that can be used to check whether a user has the required entitlement on an authorization object. +func (t *tls) GetPermissionChecker(ctx context.Context, r *http.Request, entitlement Entitlement, objectType ObjectType) (PermissionChecker, error) { + allowFunc := func(b bool) func(Object) bool { + return func(Object) bool { + return b + } + } -func (a *tls) UserAccess(username string) (*UserAccess, error) { - return &UserAccess{Admin: true}, nil -} + details, err := t.requestDetails(r) + if err != nil { + return nil, api.StatusErrorf(http.StatusForbidden, "Failed to extract request details: %v", err) + } + + if details.isInternalOrUnix() { + return allowFunc(true), nil + } + + authenticationProtocol := details.authenticationProtocol() + if authenticationProtocol != api.AuthenticationMethodTLS { + t.logger.Warn("Authentication protocol is not compatible with authorization driver", logger.Ctx{"protocol": authenticationProtocol}) + // Allow all. If the server has been configured with an authentication method but no associated authorization driver, + // the default is to give these authenticated users admin privileges. + return allowFunc(true), nil + } + + certType, isNotRestricted, projectNames, err := t.certificateDetails(details.username()) + if err != nil { + return nil, err + } + + if isNotRestricted || (certType == certificate.TypeMetrics && entitlement == EntitlementCanViewMetrics) { + return allowFunc(true), nil + } + + if details.isAllProjectsRequest { + // Only admins (users with non-restricted certs) can use the all-projects parameter. + return nil, api.StatusErrorf(http.StatusForbidden, "Certificate is restricted") + } -// UserIsAdmin checks whether the requestor is a global admin. -func (a *tls) UserIsAdmin(r *http.Request) bool { - val := r.Context().Value(request.CtxAccess) - if val == nil { - return false + // Check server level object types + switch objectType { + case ObjectTypeServer: + if entitlement == EntitlementCanView || entitlement == EntitlementCanViewResources || entitlement == EntitlementCanViewMetrics { + return allowFunc(true), nil + } + + return nil, api.StatusErrorf(http.StatusForbidden, "Certificate is restricted") + case ObjectTypeStoragePool, ObjectTypeCertificate: + if entitlement == EntitlementCanView { + return allowFunc(true), nil + } + + return nil, api.StatusErrorf(http.StatusForbidden, "Certificate is restricted") } - ua := val.(*UserAccess) - return ua.Admin + // Error if user does not have access to the project (unless we're getting projects, where we want to filter the results). + if !shared.ValueInSlice(details.projectName, projectNames) && objectType != ObjectTypeProject { + return nil, api.StatusErrorf(http.StatusForbidden, "User does not have permissions for project %q", details.projectName) + } + + // Filter objects by project. + return func(object Object) bool { + return shared.ValueInSlice(object.Project(), projectNames) + }, nil } -// UserHasPermission checks whether the requestor has a specific permission on a project. -func (a *tls) UserHasPermission(r *http.Request, projectName string, permission string) bool { - val := r.Context().Value(request.CtxAccess) - if val == nil { - return false +// certificateDetails returns the certificate type, a boolean indicating if the certificate is *not* restricted, a slice of +// project names for this certificate, or an error if the certificate could not be found. +func (t *tls) certificateDetails(fingerprint string) (certificate.Type, bool, []string, error) { + certs, projects := t.certificates.GetCertificatesAndProjects() + clientCerts := certs[certificate.TypeClient] + _, ok := clientCerts[fingerprint] + if ok { + projectNames, ok := projects[fingerprint] + if !ok { + // Certificate is not restricted. + return certificate.TypeClient, true, nil, nil + } + + return certificate.TypeClient, false, projectNames, nil } - ua := val.(*UserAccess) - if ua.Admin { - return true + // If not a client cert, could be a metrics cert. Only need to check one entitlement. + metricCerts := certs[certificate.TypeMetrics] + _, ok = metricCerts[fingerprint] + if ok { + return certificate.TypeMetrics, false, nil, nil } - return shared.ValueInSlice(permission, ua.Projects[projectName]) + return -1, false, nil, api.StatusErrorf(http.StatusForbidden, "Client certificate not found") } diff --git a/lxd/certificates.go b/lxd/certificates.go index 95079ab167bc..793ab00eb3d7 100644 --- a/lxd/certificates.go +++ b/lxd/certificates.go @@ -16,6 +16,7 @@ import ( "github.com/gorilla/mux" "github.com/canonical/lxd/client" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/certificate" "github.com/canonical/lxd/lxd/cluster" clusterConfig "github.com/canonical/lxd/lxd/cluster/config" @@ -45,8 +46,8 @@ var certificatesCmd = APIEndpoint{ var certificateCmd = APIEndpoint{ Path: "certificates/{fingerprint}", - Delete: APIEndpointAction{Handler: certificateDelete}, - Get: APIEndpointAction{Handler: certificateGet, AccessHandler: allowAuthenticated}, + Delete: APIEndpointAction{Handler: certificateDelete, AccessHandler: allowAuthenticated}, + Get: APIEndpointAction{Handler: certificateGet, AccessHandler: allowPermission(auth.ObjectTypeCertificate, auth.EntitlementCanView, "fingerprint")}, Patch: APIEndpointAction{Handler: certificatePatch, AccessHandler: allowAuthenticated}, Put: APIEndpointAction{Handler: certificatePut, AccessHandler: allowAuthenticated}, } @@ -133,6 +134,12 @@ var certificateCmd = APIEndpoint{ // $ref: "#/responses/InternalServerError" func certificatesGet(d *Daemon, r *http.Request) response.Response { recursion := util.IsRecursionRequest(r) + s := d.State() + + userHasPermission, err := s.Authorizer.GetPermissionChecker(r.Context(), r, auth.EntitlementCanView, auth.ObjectTypeCertificate) + if err != nil { + return response.SmartError(err) + } if recursion { var certResponses []api.Certificate @@ -146,6 +153,10 @@ func certificatesGet(d *Daemon, r *http.Request) response.Response { certResponses = make([]api.Certificate, 0, len(baseCerts)) for _, baseCert := range baseCerts { + if !userHasPermission(auth.ObjectCertificate(baseCert.Fingerprint)) { + continue + } + apiCert, err := baseCert.ToAPI(ctx, tx.Tx()) if err != nil { return err @@ -168,8 +179,13 @@ func certificatesGet(d *Daemon, r *http.Request) response.Response { trustedCertificates := d.getTrustedCertificates() for _, certs := range trustedCertificates { for _, cert := range certs { - fingerprint := fmt.Sprintf("/%s/certificates/%s", version.APIVersion, shared.CertFingerprint(&cert)) - body = append(body, fingerprint) + fingerprint := shared.CertFingerprint(&cert) + if !userHasPermission(auth.ObjectCertificate(fingerprint)) { + continue + } + + certificateURL := fmt.Sprintf("/%s/certificates/%s", version.APIVersion, fingerprint) + body = append(body, certificateURL) } } @@ -528,7 +544,15 @@ func certificatesPost(d *Daemon, r *http.Request) response.Response { } // Handle requests by non-admin users. - if !s.Authorizer.UserIsAdmin(r) { + var userCanCreateCertificates bool + err = s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectServer(), auth.EntitlementCanCreateCertificates) + if err == nil { + userCanCreateCertificates = true + } else if !api.StatusErrorCheck(err, http.StatusForbidden) { + return response.SmartError(err) + } + + if !trusted || !userCanCreateCertificates { // Non-admin cannot issue tokens. if req.Token { return response.Forbidden(nil) @@ -743,6 +767,12 @@ func certificatesPost(d *Daemon, r *http.Request) response.Response { if err != nil { return response.SmartError(err) } + + // Add the certificate resource to the authorizer. + err = s.Authorizer.AddCertificate(r.Context(), fingerprint) + if err != nil { + logger.Error("Failed to add certificate to authorizer", logger.Ctx{"fingerprint": fingerprint, "error": err}) + } } // Reload the cache. @@ -966,11 +996,19 @@ func doCertificateUpdate(d *Daemon, dbInfo api.Certificate, req api.CertificateP Type: reqDBType, } + var userCanEditCertificate bool + err = s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectCertificate(dbInfo.Fingerprint), auth.EntitlementCanEdit) + if err == nil { + userCanEditCertificate = true + } else if !api.StatusErrorCheck(err, http.StatusForbidden) { + return response.SmartError(err) + } + // Non-admins are able to change their own certificate but no other fields. // In order to prevent possible future security issues, the certificate information is // reset in case a non-admin user is performing the update. certProjects := req.Projects - if !s.Authorizer.UserIsAdmin(r) { + if !userCanEditCertificate { if r.TLS == nil { response.Forbidden(fmt.Errorf("Cannot update certificate information")) } @@ -1114,8 +1152,16 @@ func certificateDelete(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } + var userCanEditCertificate bool + err = s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectCertificate(fingerprint), auth.EntitlementCanEdit) + if err == nil { + userCanEditCertificate = true + } else if api.StatusErrorCheck(err, http.StatusForbidden) { + return response.SmartError(err) + } + // Non-admins are able to delete only their own certificate. - if !s.Authorizer.UserIsAdmin(r) { + if !userCanEditCertificate { if r.TLS == nil { response.Forbidden(fmt.Errorf("Cannot delete certificate")) } @@ -1166,6 +1212,12 @@ func certificateDelete(d *Daemon, r *http.Request) response.Response { if err != nil { return response.SmartError(err) } + + // Remove the certificate from the authorizer. + err = s.Authorizer.DeleteCertificate(r.Context(), certInfo.Fingerprint) + if err != nil { + logger.Error("Failed to remove certificate from authorizer", logger.Ctx{"fingerprint": certInfo.Fingerprint, "error": err}) + } } // Reload the cache. diff --git a/lxd/daemon.go b/lxd/daemon.go index 4f45b1336b87..a69765d7e199 100644 --- a/lxd/daemon.go +++ b/lxd/daemon.go @@ -232,30 +232,34 @@ type APIEndpointAction struct { AllowUntrusted bool } -// allowAuthenticated is an AccessHandler which allows all requests. -// This function doesn't do anything itself, except return the EmptySyncResponse that allows the request to -// proceed. However in order to access any API route you must be authenticated, unless the handler's AllowUntrusted -// property is set to true or you are an admin. +// allowAuthenticated is an AccessHandler which allows only authenticated requests. This should be used in conjunction +// with further access control within the handler (e.g. to filter resources the user is able to view/edit). func allowAuthenticated(d *Daemon, r *http.Request) response.Response { + err := d.checkTrustedClient(r) + if err != nil { + return response.SmartError(err) + } + return response.EmptySyncResponse } -// allowProjectPermission is a wrapper to check access against the project, its features and RBAC permission. -func allowProjectPermission(feature string, permission string) func(d *Daemon, r *http.Request) response.Response { +// allowPermission is a wrapper to check access against a given object, an object being an image, instance, network, etc. +// Mux vars should be passed in so that the object we are checking can be created. For example, a certificate object requires +// a fingerprint, the mux var for certificate fingerprints is "fingerprint", so that string should be passed in. +// Mux vars should always be passed in with the same order they appear in the API route. +func allowPermission(objectType auth.ObjectType, entitlement auth.Entitlement, muxVars ...string) func(d *Daemon, r *http.Request) response.Response { return func(d *Daemon, r *http.Request) response.Response { - s := d.State() - - // Shortcut for speed - if s.Authorizer.UserIsAdmin(r) { - return response.EmptySyncResponse + objectName, err := auth.ObjectFromRequest(r, objectType, muxVars...) + if err != nil { + return response.InternalError(fmt.Errorf("Failed to create authentication object: %w", err)) } - // Get the project - projectName := request.ProjectParam(r) + s := d.State() // Validate whether the user has the needed permission - if !s.Authorizer.UserHasPermission(r, projectName, permission) { - return response.Forbidden(nil) + err = s.Authorizer.CheckPermission(r.Context(), r, objectName, entitlement) + if err != nil { + return response.SmartError(err) } return response.EmptySyncResponse @@ -502,67 +506,9 @@ func (d *Daemon) createCmd(restAPI *mux.Router, version string, c APIEndpoint) { if trusted { logger.Debug("Handling API request", logCtx) - // Get user access data. - userAccess, err := func() (*auth.UserAccess, error) { - ua := &auth.UserAccess{} - ua.Admin = true - - // Internal cluster communications. - if protocol == "cluster" { - return ua, nil - } - - // Regular TLS clients. - if protocol == api.AuthenticationMethodTLS { - certProjects := d.clientCerts.GetProjects() - - // Check if we have restrictions on the key. - if certProjects != nil { - projects, ok := certProjects[username] - if ok { - ua.Admin = false - ua.Projects = map[string][]string{} - for _, projectName := range projects { - ua.Projects[projectName] = []string{ - "view", - "manage-containers", - "manage-images", - "manage-networks", - "manage-profiles", - "manage-storage-volumes", - "operate-containers", - } - } - } - } - - return ua, nil - } - - // If no external authentication configured, we're done now. - if d.candidVerifier == nil || r.RemoteAddr == "@" { - return ua, nil - } - - // Validate RBAC permissions. - ua, err = d.authorizer.UserAccess(username) - if err != nil { - return nil, err - } - - return ua, nil - }() - if err != nil { - logCtx["err"] = err - logger.Warn("Rejecting remote API request", logCtx) - _ = response.Forbidden(nil).Render(w) - return - } - // Add authentication/authorization context data. ctx := context.WithValue(r.Context(), request.CtxUsername, username) ctx = context.WithValue(ctx, request.CtxProtocol, protocol) - ctx = context.WithValue(ctx, request.CtxAccess, userAccess) // Add forwarded requestor data. if protocol == "cluster" { @@ -646,10 +592,7 @@ func (d *Daemon) createCmd(restAPI *mux.Router, version string, c APIEndpoint) { return resp } } else if !action.AllowUntrusted { - // Require admin privileges - if !d.authorizer.UserIsAdmin(r) { - return response.Forbidden(nil) - } + return response.Forbidden(nil) } return action.Handler(d, r) @@ -772,7 +715,7 @@ func (d *Daemon) init() error { var dbWarnings []dbCluster.Warning // Set default authorizer. - d.authorizer, err = auth.LoadAuthorizer("tls", nil, logger.Log, nil) + d.authorizer, err = auth.LoadAuthorizer(d.shutdownCtx, auth.DriverTLS, logger.Log, d.clientCerts) if err != nil { return err } @@ -1861,14 +1804,17 @@ func (d *Daemon) setupRBACServer(rbacURL string, rbacKey string, rbacExpiry int6 var err error if d.authorizer != nil { - d.authorizer.StopStatusCheck() + err := d.authorizer.StopService(d.shutdownCtx) + if err != nil { + logger.Error("Failed to stop authorizer service", logger.Ctx{"error": err}) + } } if rbacURL == "" || rbacAgentURL == "" || rbacAgentUsername == "" || rbacAgentPrivateKey == "" || rbacAgentPublicKey == "" { d.candidVerifier = nil // Reset to default authorizer. - d.authorizer, err = auth.LoadAuthorizer("tls", nil, logger.Log, nil) + d.authorizer, err = auth.LoadAuthorizer(d.shutdownCtx, auth.DriverTLS, logger.Log, d.clientCerts) if err != nil { return err } @@ -1879,9 +1825,9 @@ func (d *Daemon) setupRBACServer(rbacURL string, rbacKey string, rbacExpiry int6 revert := revert.New() defer revert.Fail() - projectsFunc := func() (map[int64]string, error) { + projectsFunc := func(ctx context.Context) (map[int64]string, error) { var result map[int64]string - err := d.State().DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { + err := d.State().DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error { var err error result, err = dbCluster.GetProjectIDsToNames(ctx, tx.Tx()) return err @@ -1905,18 +1851,21 @@ func (d *Daemon) setupRBACServer(rbacURL string, rbacKey string, rbacExpiry int6 d.candidVerifier = nil // Reset to default authorizer. - d.authorizer, _ = auth.LoadAuthorizer("tls", nil, logger.Log, nil) + d.authorizer, _ = auth.LoadAuthorizer(d.shutdownCtx, auth.DriverTLS, logger.Log, d.clientCerts) }) // Load RBAC authorizer - rbacAuthorizer, err := auth.LoadAuthorizer("rbac", config, logger.Log, projectsFunc) + rbacAuthorizer, err := auth.LoadAuthorizer(d.shutdownCtx, auth.DriverRBAC, logger.Log, d.clientCerts, auth.WithConfig(config), auth.WithProjectsGetFunc(projectsFunc)) if err != nil { return err } revert.Add(func() { // Stop status check in case candid fails. - rbacAuthorizer.StopStatusCheck() + err := rbacAuthorizer.StopService(d.shutdownCtx) + if err != nil { + logger.Error("Failed to stop authorizer service", logger.Ctx{"error": err}) + } }) // Enable candid authentication diff --git a/lxd/db/cluster/constants.go b/lxd/db/cluster/entities.go similarity index 59% rename from lxd/db/cluster/constants.go rename to lxd/db/cluster/entities.go index 4a0375a1e659..e6f682740550 100644 --- a/lxd/db/cluster/constants.go +++ b/lxd/db/cluster/entities.go @@ -1,6 +1,10 @@ package cluster import ( + "fmt" + "net/url" + "strings" + "github.com/canonical/lxd/shared/version" ) @@ -24,6 +28,7 @@ const ( TypeStorageVolumeSnapshot = 15 TypeWarning = 16 TypeClusterGroup = 17 + TypeStorageBucket = 18 ) // EntityNames associates an entity code to its name. @@ -44,6 +49,7 @@ var EntityNames = map[int]string{ TypeStorageVolume: "storage volume", TypeStorageVolumeBackup: "storage volume backup", TypeStorageVolumeSnapshot: "storage volume snapshot", + TypeStorageBucket: "storage bucket", TypeWarning: "warning", TypeClusterGroup: "cluster group", } @@ -69,6 +75,7 @@ var EntityURIs = map[int]string{ TypeStorageVolume: "/" + version.APIVersion + "/storage-pools/%s/volumes/%s/%s?project=%s", TypeStorageVolumeBackup: "/" + version.APIVersion + "/storage-pools/%s/volumes/%s/%s/backups/%s?project=%s", TypeStorageVolumeSnapshot: "/" + version.APIVersion + "/storage-pools/%s/volumes/%s/%s/snapshots/%s?project=%s", + TypeStorageBucket: "/" + version.APIVersion + "/storage-pools/%s/buckets/%s?project=%s", TypeWarning: "/" + version.APIVersion + "/warnings/%s", TypeClusterGroup: "/" + version.APIVersion + "/cluster/groups/%s", } @@ -78,3 +85,58 @@ func init() { EntityTypes[name] = code } } + +// URLToEntityType parses a raw URL string and returns the entity type, the project, and the path arguments. The +// returned project is set to "default" if it is not present (unless the entity type is TypeProject, in which case it is +// set to the value of the path parameter). An error is returned if the URL is not recognised. +func URLToEntityType(rawURL string) (int, string, []string, error) { + u, err := url.Parse(rawURL) + if err != nil { + return -1, "", nil, fmt.Errorf("Failed to parse url %q into an entity type: %w", rawURL, err) + } + + // We need to space separate the path because fmt.Sscanf uses this as a delimiter. + spaceSeparatedURLPath := strings.Replace(u.Path, "/", " / ", -1) + for entityType, entityURI := range EntityURIs { + entityPath, _, _ := strings.Cut(entityURI, "?") + + // Skip if we don't have the same number of slashes. + if strings.Count(entityPath, "/") != strings.Count(u.Path, "/") { + continue + } + + spaceSeparatedEntityPath := strings.Replace(entityPath, "/", " / ", -1) + + // Make an []any for the number of expected path arguments and set each value in the slice to a *string. + nPathArgs := strings.Count(spaceSeparatedEntityPath, "%s") + pathArgsAny := make([]any, 0, nPathArgs) + for i := 0; i < nPathArgs; i++ { + var pathComponentStr string + pathArgsAny = append(pathArgsAny, &pathComponentStr) + } + + // Scan the given URL into the entity URL. If we found all the expected path arguments and there + // are no errors we have a match. + nFound, err := fmt.Sscanf(spaceSeparatedURLPath, spaceSeparatedEntityPath, pathArgsAny...) + if nFound == nPathArgs && err == nil { + pathArgs := make([]string, 0, nPathArgs) + for _, pathArgAny := range pathArgsAny { + pathArgPtr := pathArgAny.(*string) + pathArgs = append(pathArgs, *pathArgPtr) + } + + projectName := u.Query().Get("project") + if projectName == "" { + projectName = "default" + } + + if entityType == TypeProject { + return TypeProject, pathArgs[0], pathArgs, nil + } + + return entityType, projectName, pathArgs, nil + } + } + + return -1, "", nil, fmt.Errorf("Unknown entity URL %q", u.String()) +} diff --git a/lxd/db/cluster/entities_test.go b/lxd/db/cluster/entities_test.go new file mode 100644 index 000000000000..db70ab87a350 --- /dev/null +++ b/lxd/db/cluster/entities_test.go @@ -0,0 +1,177 @@ +package cluster + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestURLToEntityType(t *testing.T) { + tests := []struct { + name string + rawURL string + expectedEntityType int + expectedProject string + expectedPathArgs []string + expectedErr error + }{ + { + name: "containers", + rawURL: "/1.0/containers/my-container?project=my-project", + expectedEntityType: TypeContainer, + expectedProject: "my-project", + expectedPathArgs: []string{"my-container"}, + expectedErr: nil, + }, + { + name: "images", + rawURL: "/1.0/images/fwirnoaiwnerfoiawnef", + expectedEntityType: TypeImage, + expectedProject: "default", + expectedPathArgs: []string{"fwirnoaiwnerfoiawnef"}, + expectedErr: nil, + }, + { + name: "profiles", + rawURL: "/1.0/profiles/my-profile?project=my-project", + expectedEntityType: TypeProfile, + expectedProject: "my-project", + expectedPathArgs: []string{"my-profile"}, + expectedErr: nil, + }, + { + name: "projects", + rawURL: "/1.0/projects/my-project", + expectedEntityType: TypeProject, + expectedProject: "my-project", + expectedPathArgs: []string{"my-project"}, + expectedErr: nil, + }, + { + name: "certificates", + rawURL: "/1.0/certificates/foawienfoawnefkanwelfknsfl", + expectedEntityType: TypeCertificate, + expectedProject: "default", + expectedPathArgs: []string{"foawienfoawnefkanwelfknsfl"}, + expectedErr: nil, + }, + { + name: "instances", + rawURL: "/1.0/instances/my-instance", + expectedEntityType: TypeInstance, + expectedProject: "default", + expectedPathArgs: []string{"my-instance"}, + expectedErr: nil, + }, + { + name: "instance backup", + rawURL: "/1.0/instances/my-instance/backups/my-backup?project=my-project", + expectedEntityType: TypeInstanceBackup, + expectedProject: "my-project", + expectedPathArgs: []string{"my-instance", "my-backup"}, + expectedErr: nil, + }, + { + name: "instance snapshot", + rawURL: "/1.0/instances/my-instance/snapshots/my-snapshot", + expectedEntityType: TypeInstanceSnapshot, + expectedProject: "default", + expectedPathArgs: []string{"my-instance", "my-snapshot"}, + expectedErr: nil, + }, + { + name: "networks", + rawURL: "/1.0/networks/my-network?project=my-project", + expectedEntityType: TypeNetwork, + expectedProject: "my-project", + expectedPathArgs: []string{"my-network"}, + expectedErr: nil, + }, + { + name: "network acls", + rawURL: "/1.0/network-acls/my-network-acl", + expectedEntityType: TypeNetworkACL, + expectedProject: "default", + expectedPathArgs: []string{"my-network-acl"}, + expectedErr: nil, + }, + { + name: "cluster members", + rawURL: "/1.0/cluster/members/node01", + expectedEntityType: TypeNode, + expectedProject: "default", + expectedPathArgs: []string{"node01"}, + expectedErr: nil, + }, + { + name: "operation", + rawURL: "/1.0/operations/3e75d1bf-30ed-45ce-9e02-267fa7338eb4", + expectedEntityType: TypeOperation, + expectedProject: "default", + expectedPathArgs: []string{"3e75d1bf-30ed-45ce-9e02-267fa7338eb4"}, + expectedErr: nil, + }, + { + name: "storage pools", + rawURL: "/1.0/storage-pools/my-storage-pool", + expectedEntityType: TypeStoragePool, + expectedProject: "default", + expectedPathArgs: []string{"my-storage-pool"}, + expectedErr: nil, + }, + { + name: "storage volumes", + rawURL: "/1.0/storage-pools/my-storage-pool/volumes/custom/my-storage-volume?project=my-project", + expectedEntityType: TypeStorageVolume, + expectedProject: "my-project", + expectedPathArgs: []string{"my-storage-pool", "custom", "my-storage-volume"}, + expectedErr: nil, + }, + { + name: "storage volume backups", + rawURL: "/1.0/storage-pools/my-storage-pool/volumes/custom/my-storage-volume/backups/my-backup?project=my-project", + expectedEntityType: TypeStorageVolumeBackup, + expectedProject: "my-project", + expectedPathArgs: []string{"my-storage-pool", "custom", "my-storage-volume", "my-backup"}, + expectedErr: nil, + }, + { + name: "storage volume snapshots", + rawURL: "/1.0/storage-pools/my-storage-pool/volumes/custom/my-storage-volume/snapshots/my-snapshot?project=my-project", + expectedEntityType: TypeStorageVolumeSnapshot, + expectedProject: "my-project", + expectedPathArgs: []string{"my-storage-pool", "custom", "my-storage-volume", "my-snapshot"}, + expectedErr: nil, + }, + { + name: "warnings", + rawURL: "/1.0/warnings/3e75d1bf-30ed-45ce-9e02-267fa7338eb4", + expectedEntityType: TypeWarning, + expectedProject: "default", + expectedPathArgs: []string{"3e75d1bf-30ed-45ce-9e02-267fa7338eb4"}, + expectedErr: nil, + }, + { + name: "cluster groups", + rawURL: "/1.0/cluster/groups/my-cluster-group", + expectedEntityType: TypeClusterGroup, + expectedProject: "default", + expectedPathArgs: []string{"my-cluster-group"}, + expectedErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actualEntityType, actualProject, actualPathArgs, actualErr := URLToEntityType(tt.rawURL) + + assert.Equal(t, tt.expectedEntityType, actualEntityType) + assert.Equal(t, tt.expectedProject, actualProject) + for i, pathArg := range actualPathArgs { + assert.Equal(t, tt.expectedPathArgs[i], pathArg) + } + + assert.Equal(t, tt.expectedErr, actualErr) + }) + } +} diff --git a/lxd/db/operationtype/operation_type.go b/lxd/db/operationtype/operation_type.go index 53b3943aa382..fddfec410c89 100644 --- a/lxd/db/operationtype/operation_type.go +++ b/lxd/db/operationtype/operation_type.go @@ -1,5 +1,9 @@ package operationtype +import ( + "github.com/canonical/lxd/lxd/auth" +) + // Type is a numeric code indentifying the type of an Operation. type Type int64 @@ -199,83 +203,83 @@ func (t Type) Description() string { } } -// Permission returns the needed RBAC permission to cancel the operation. -func (t Type) Permission() string { +// Permission returns the auth.ObjectType and auth.Entitlement required to cancel the operation. +func (t Type) Permission() (auth.ObjectType, auth.Entitlement) { switch t { case BackupCreate: - return "operate-containers" + return auth.ObjectTypeInstance, auth.EntitlementCanManageBackups case BackupRename: - return "operate-containers" + return auth.ObjectTypeInstance, auth.EntitlementCanManageBackups case BackupRestore: - return "operate-containers" + return auth.ObjectTypeInstance, auth.EntitlementCanManageBackups case BackupRemove: - return "operate-containers" + return auth.ObjectTypeInstance, auth.EntitlementCanManageBackups case ConsoleShow: - return "operate-containers" + return auth.ObjectTypeInstance, auth.EntitlementCanAccessConsole case InstanceFreeze: - return "operate-containers" + return auth.ObjectTypeInstance, auth.EntitlementCanUpdateState case InstanceUnfreeze: - return "operate-containers" + return auth.ObjectTypeInstance, auth.EntitlementCanUpdateState case InstanceStart: - return "operate-containers" + return auth.ObjectTypeInstance, auth.EntitlementCanUpdateState case InstanceStop: - return "operate-containers" + return auth.ObjectTypeInstance, auth.EntitlementCanUpdateState case InstanceRestart: - return "operate-containers" + return auth.ObjectTypeInstance, auth.EntitlementCanUpdateState case CommandExec: - return "operate-containers" + return auth.ObjectTypeInstance, auth.EntitlementCanExec case SnapshotCreate: - return "operate-containers" + return auth.ObjectTypeInstance, auth.EntitlementCanManageSnapshots case SnapshotRename: - return "operate-containers" + return auth.ObjectTypeInstance, auth.EntitlementCanManageSnapshots case SnapshotTransfer: - return "operate-containers" + return auth.ObjectTypeInstance, auth.EntitlementCanManageSnapshots case SnapshotUpdate: - return "operate-containers" + return auth.ObjectTypeInstance, auth.EntitlementCanManageSnapshots case SnapshotDelete: - return "operate-containers" + return auth.ObjectTypeInstance, auth.EntitlementCanManageSnapshots case InstanceCreate: - return "manage-containers" + return auth.ObjectTypeProject, auth.EntitlementCanCreateInstances case InstanceUpdate: - return "manage-containers" + return auth.ObjectTypeInstance, auth.EntitlementCanEdit case InstanceRename: - return "manage-containers" + return auth.ObjectTypeInstance, auth.EntitlementCanEdit case InstanceMigrate: - return "manage-containers" + return auth.ObjectTypeInstance, auth.EntitlementCanEdit case InstanceLiveMigrate: - return "manage-containers" + return auth.ObjectTypeInstance, auth.EntitlementCanEdit case InstanceDelete: - return "manage-containers" + return auth.ObjectTypeInstance, auth.EntitlementCanEdit case InstanceRebuild: - return "operate-containers" + return auth.ObjectTypeInstance, auth.EntitlementCanEdit case SnapshotRestore: - return "manage-containers" + return auth.ObjectTypeInstance, auth.EntitlementCanEdit case ImageDownload: - return "manage-images" + return auth.ObjectTypeImage, auth.EntitlementCanEdit case ImageDelete: - return "manage-images" + return auth.ObjectTypeImage, auth.EntitlementCanEdit case ImageToken: - return "manage-images" + return auth.ObjectTypeImage, auth.EntitlementCanEdit case ImageRefresh: - return "manage-images" + return auth.ObjectTypeImage, auth.EntitlementCanEdit case ImagesUpdate: - return "manage-images" + return auth.ObjectTypeImage, auth.EntitlementCanEdit case ImagesSynchronize: - return "manage-images" + return auth.ObjectTypeImage, auth.EntitlementCanEdit case CustomVolumeSnapshotsExpire: - return "operate-volumes" + return auth.ObjectTypeStorageVolume, auth.EntitlementCanEdit case CustomVolumeBackupCreate: - return "manage-storage-volumes" + return auth.ObjectTypeStorageVolume, auth.EntitlementCanManageBackups case CustomVolumeBackupRemove: - return "manage-storage-volumes" + return auth.ObjectTypeStorageVolume, auth.EntitlementCanManageBackups case CustomVolumeBackupRename: - return "manage-storage-volumes" + return auth.ObjectTypeStorageVolume, auth.EntitlementCanManageBackups case CustomVolumeBackupRestore: - return "manage-storage-volumes" + return auth.ObjectTypeStorageVolume, auth.EntitlementCanEdit } - return "" + return "", "" } diff --git a/lxd/events.go b/lxd/events.go index e0ceefa78946..26bc21d9d6f0 100644 --- a/lxd/events.go +++ b/lxd/events.go @@ -6,6 +6,7 @@ import ( "net/http" "strings" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/db" "github.com/canonical/lxd/lxd/db/cluster" "github.com/canonical/lxd/lxd/events" @@ -58,11 +59,27 @@ func eventsSocket(s *state.State, r *http.Request, w http.ResponseWriter) error } } + var projectPermissionFunc auth.PermissionChecker + if projectName != "" { + err := s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectProject(projectName), auth.EntitlementCanViewEvents) + if err != nil { + return err + } + } else if allProjects { + var err error + projectPermissionFunc, err = s.Authorizer.GetPermissionChecker(r.Context(), r, auth.EntitlementCanViewEvents, auth.ObjectTypeProject) + if err != nil { + return err + } + } + + canViewPrivilegedEvents := s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectServer(), auth.EntitlementCanViewPrivilegedEvents) == nil + types := strings.Split(r.FormValue("type"), ",") if len(types) == 1 && types[0] == "" { types = []string{} for _, entry := range eventTypes { - if !s.Authorizer.UserIsAdmin(r) && shared.ValueInSlice(entry, privilegedEventTypes) { + if !canViewPrivilegedEvents && shared.ValueInSlice(entry, privilegedEventTypes) { continue } @@ -77,7 +94,7 @@ func eventsSocket(s *state.State, r *http.Request, w http.ResponseWriter) error } } - if shared.ValueInSlice(api.EventTypeLogging, types) && !s.Authorizer.UserIsAdmin(r) { + if shared.ValueInSlice(api.EventTypeLogging, types) && !canViewPrivilegedEvents { return api.StatusErrorf(http.StatusForbidden, "Forbidden") } @@ -139,7 +156,7 @@ func eventsSocket(s *state.State, r *http.Request, w http.ResponseWriter) error listenerConnection := events.NewWebsocketListenerConnection(conn) - listener, err := s.Events.AddListener(projectName, allProjects, listenerConnection, types, excludeSources, recvFunc, excludeLocations) + listener, err := s.Events.AddListener(projectName, allProjects, projectPermissionFunc, listenerConnection, types, excludeSources, recvFunc, excludeLocations) if err != nil { l.Warn("Failed to add event listener", logger.Ctx{"err": err}) return nil diff --git a/lxd/events/events.go b/lxd/events/events.go index 93603a465fe0..06e68983f022 100644 --- a/lxd/events/events.go +++ b/lxd/events/events.go @@ -8,6 +8,7 @@ import ( "github.com/pborman/uuid" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/shared" "github.com/canonical/lxd/shared/api" "github.com/canonical/lxd/shared/cancel" @@ -65,11 +66,17 @@ func (s *Server) SetLocalLocation(location string) { } // AddListener creates and returns a new event listener. -func (s *Server) AddListener(projectName string, allProjects bool, connection EventListenerConnection, messageTypes []string, excludeSources []EventSource, recvFunc EventHandler, excludeLocations []string) (*Listener, error) { +func (s *Server) AddListener(projectName string, allProjects bool, projectPermissionFunc auth.PermissionChecker, connection EventListenerConnection, messageTypes []string, excludeSources []EventSource, recvFunc EventHandler, excludeLocations []string) (*Listener, error) { if allProjects && projectName != "" { return nil, fmt.Errorf("Cannot specify project name when listening for events on all projects") } + if projectPermissionFunc == nil { + projectPermissionFunc = func(auth.Object) bool { + return true + } + } + listener := &Listener{ listenerCommon: listenerCommon{ EventListenerConnection: connection, @@ -79,10 +86,11 @@ func (s *Server) AddListener(projectName string, allProjects bool, connection Ev recvFunc: recvFunc, }, - allProjects: allProjects, - projectName: projectName, - excludeSources: excludeSources, - excludeLocations: excludeLocations, + allProjects: allProjects, + projectName: projectName, + projectPermissionFunc: projectPermissionFunc, + excludeSources: excludeSources, + excludeLocations: excludeLocations, } s.lock.Lock() @@ -179,6 +187,11 @@ func (s *Server) broadcast(event api.Event, eventSource EventSource) error { continue } + // If the event is project specific, ensure we have permission to view it. + if event.Project != "" && !listener.projectPermissionFunc(auth.ObjectProject(event.Project)) { + continue + } + if sourceInSlice(eventSource, listener.excludeSources) { continue } @@ -228,8 +241,9 @@ func (s *Server) broadcast(event api.Event, eventSource EventSource) error { type Listener struct { listenerCommon - allProjects bool - projectName string - excludeSources []EventSource - excludeLocations []string + allProjects bool + projectName string + projectPermissionFunc auth.PermissionChecker + excludeSources []EventSource + excludeLocations []string } diff --git a/lxd/events/internalListener.go b/lxd/events/internalListener.go index 7d15c7647460..82101c3e2895 100644 --- a/lxd/events/internalListener.go +++ b/lxd/events/internalListener.go @@ -38,7 +38,7 @@ func (l *InternalListener) startListener() { aEnd, bEnd := memorypipe.NewPipePair(l.listenerCtx) listenerConnection := NewSimpleListenerConnection(aEnd) - l.listener, err = l.server.AddListener("", true, listenerConnection, []string{"lifecycle", "logging", "ovn"}, []EventSource{EventSourcePull}, nil, nil) + l.listener, err = l.server.AddListener("", true, nil, listenerConnection, []string{"lifecycle", "logging", "ovn"}, []EventSource{EventSourcePull}, nil, nil) if err != nil { return } diff --git a/lxd/images.go b/lxd/images.go index 6088c89f15dd..81b34dc9c9d6 100644 --- a/lxd/images.go +++ b/lxd/images.go @@ -28,6 +28,7 @@ import ( "gopkg.in/yaml.v2" "github.com/canonical/lxd/client" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/cluster" "github.com/canonical/lxd/lxd/db" dbCluster "github.com/canonical/lxd/lxd/db/cluster" @@ -63,46 +64,46 @@ var imagesCmd = APIEndpoint{ var imageCmd = APIEndpoint{ Path: "images/{fingerprint}", - Delete: APIEndpointAction{Handler: imageDelete, AccessHandler: allowProjectPermission("images", "manage-images")}, + Delete: APIEndpointAction{Handler: imageDelete, AccessHandler: allowPermission(auth.ObjectTypeImage, auth.EntitlementCanEdit, "fingerprint")}, Get: APIEndpointAction{Handler: imageGet, AllowUntrusted: true}, - Patch: APIEndpointAction{Handler: imagePatch, AccessHandler: allowProjectPermission("images", "manage-images")}, - Put: APIEndpointAction{Handler: imagePut, AccessHandler: allowProjectPermission("images", "manage-images")}, + Patch: APIEndpointAction{Handler: imagePatch, AccessHandler: allowPermission(auth.ObjectTypeImage, auth.EntitlementCanEdit, "fingerprint")}, + Put: APIEndpointAction{Handler: imagePut, AccessHandler: allowPermission(auth.ObjectTypeImage, auth.EntitlementCanEdit, "fingerprint")}, } var imageExportCmd = APIEndpoint{ Path: "images/{fingerprint}/export", Get: APIEndpointAction{Handler: imageExport, AllowUntrusted: true}, - Post: APIEndpointAction{Handler: imageExportPost, AccessHandler: allowProjectPermission("images", "manage-images")}, + Post: APIEndpointAction{Handler: imageExportPost, AccessHandler: allowPermission(auth.ObjectTypeImage, auth.EntitlementCanEdit, "fingerprint")}, } var imageSecretCmd = APIEndpoint{ Path: "images/{fingerprint}/secret", - Post: APIEndpointAction{Handler: imageSecret, AccessHandler: allowProjectPermission("images", "view")}, + Post: APIEndpointAction{Handler: imageSecret, AccessHandler: allowPermission(auth.ObjectTypeImage, auth.EntitlementCanEdit, "fingerprint")}, } var imageRefreshCmd = APIEndpoint{ Path: "images/{fingerprint}/refresh", - Post: APIEndpointAction{Handler: imageRefresh, AccessHandler: allowProjectPermission("images", "manage-images")}, + Post: APIEndpointAction{Handler: imageRefresh, AccessHandler: allowPermission(auth.ObjectTypeImage, auth.EntitlementCanEdit, "fingerprint")}, } var imageAliasesCmd = APIEndpoint{ Path: "images/aliases", - Get: APIEndpointAction{Handler: imageAliasesGet, AccessHandler: allowProjectPermission("images", "view")}, - Post: APIEndpointAction{Handler: imageAliasesPost, AccessHandler: allowProjectPermission("images", "manage-images")}, + Get: APIEndpointAction{Handler: imageAliasesGet, AccessHandler: allowAuthenticated}, + Post: APIEndpointAction{Handler: imageAliasesPost, AccessHandler: allowPermission(auth.ObjectTypeProject, auth.EntitlementCanCreateImageAliases)}, } var imageAliasCmd = APIEndpoint{ Path: "images/aliases/{name:.*}", - Delete: APIEndpointAction{Handler: imageAliasDelete, AccessHandler: allowProjectPermission("images", "manage-images")}, + Delete: APIEndpointAction{Handler: imageAliasDelete, AccessHandler: allowPermission(auth.ObjectTypeImageAlias, auth.EntitlementCanEdit, "name")}, Get: APIEndpointAction{Handler: imageAliasGet, AllowUntrusted: true}, - Patch: APIEndpointAction{Handler: imageAliasPatch, AccessHandler: allowProjectPermission("images", "manage-images")}, - Post: APIEndpointAction{Handler: imageAliasPost, AccessHandler: allowProjectPermission("images", "manage-images")}, - Put: APIEndpointAction{Handler: imageAliasPut, AccessHandler: allowProjectPermission("images", "manage-images")}, + Patch: APIEndpointAction{Handler: imageAliasPatch, AccessHandler: allowPermission(auth.ObjectTypeImageAlias, auth.EntitlementCanEdit, "name")}, + Post: APIEndpointAction{Handler: imageAliasPost, AccessHandler: allowPermission(auth.ObjectTypeImageAlias, auth.EntitlementCanEdit, "name")}, + Put: APIEndpointAction{Handler: imageAliasPut, AccessHandler: allowPermission(auth.ObjectTypeImageAlias, auth.EntitlementCanEdit, "name")}, } /* @@ -915,11 +916,20 @@ func imageCreateInPool(s *state.State, info *api.Image, storagePool string) erro func imagesPost(d *Daemon, r *http.Request) response.Response { s := d.State() - trusted := d.checkTrustedClient(r) == nil && allowProjectPermission("images", "manage-images")(d, r) == response.EmptySyncResponse + projectName := request.ProjectParam(r) + + var userCanCreateImages bool + err := s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectProject(projectName), auth.EntitlementCanCreateImages) + if err == nil { + userCanCreateImages = true + } else if !api.StatusErrorCheck(err, http.StatusForbidden) { + return response.SmartError(err) + } + + trusted := d.checkTrustedClient(r) == nil && userCanCreateImages secret := r.Header.Get("X-LXD-secret") fingerprint := r.Header.Get("X-LXD-fingerprint") - projectName := request.ProjectParam(r) var imageMetadata map[string]any @@ -1137,6 +1147,12 @@ func imagesPost(d *Daemon, r *http.Request) response.Response { return fmt.Errorf("Failed syncing image between nodes: %w", err) } + // Add the image to the authorizer. + err = s.Authorizer.AddImage(r.Context(), projectName, info.Fingerprint) + if err != nil { + logger.Error("Failed to add image to authorizer", logger.Ctx{"fingerprint": info.Fingerprint, "project": projectName, "error": err}) + } + s.Events.SendLifecycle(projectName, lifecycle.ImageCreated.Event(info.Fingerprint, projectName, op.Requestor(), logger.Ctx{"type": info.Type})) return nil @@ -1276,7 +1292,7 @@ func getImageMetadata(fname string) (*api.ImageMetadata, string, error) { return &result, imageType, nil } -func doImagesGet(ctx context.Context, tx *db.ClusterTx, recursion bool, projectName string, public bool, clauses *filter.ClauseSet) (any, error) { +func doImagesGet(ctx context.Context, tx *db.ClusterTx, recursion bool, projectName string, public bool, clauses *filter.ClauseSet, hasPermission auth.PermissionChecker) (any, error) { mustLoadObjects := recursion || clauses != nil fingerprints, err := tx.GetImagesFingerprints(ctx, projectName, public) @@ -1294,14 +1310,18 @@ func doImagesGet(ctx context.Context, tx *db.ClusterTx, recursion bool, projectN } for _, fingerprint := range fingerprints { + image, err := doImageGet(ctx, tx, projectName, fingerprint, public) + if err != nil { + continue + } + + if !image.Public && !hasPermission(auth.ObjectImage(projectName, fingerprint)) { + continue + } + if !mustLoadObjects { resultString = append(resultString, api.NewURL().Path(version.APIVersion, "images", fingerprint).String()) } else { - image, err := doImageGet(ctx, tx, projectName, fingerprint, public) - if err != nil { - continue - } - if clauses != nil { match, err := filter.Match(*image, *clauses) if err != nil { @@ -1536,7 +1556,15 @@ func doImagesGet(ctx context.Context, tx *db.ClusterTx, recursion bool, projectN func imagesGet(d *Daemon, r *http.Request) response.Response { projectName := request.ProjectParam(r) filterStr := r.FormValue("filter") - public := d.checkTrustedClient(r) != nil || allowProjectPermission("images", "view")(d, r) != response.EmptySyncResponse + + s := d.State() + + hasPermission, authorizationErr := s.Authorizer.GetPermissionChecker(r.Context(), r, auth.EntitlementCanView, auth.ObjectTypeImage) + if authorizationErr != nil && !api.StatusErrorCheck(authorizationErr, http.StatusForbidden) { + return response.SmartError(authorizationErr) + } + + public := d.checkTrustedClient(r) != nil || authorizationErr != nil clauses, err := filter.Parse(filterStr, filter.QueryOperatorSet()) if err != nil { @@ -1544,8 +1572,8 @@ func imagesGet(d *Daemon, r *http.Request) response.Response { } var result any - err = d.State().DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - result, err = doImagesGet(ctx, tx, util.IsRecursionRequest(r), projectName, public, clauses) + err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { + result, err = doImagesGet(ctx, tx, util.IsRecursionRequest(r), projectName, public, clauses, hasPermission) if err != nil { return err } @@ -2574,6 +2602,12 @@ func imageDelete(d *Daemon, r *http.Request) response.Response { // Remove main image file from disk. imageDeleteFromDisk(imgInfo.Fingerprint) + // Remove image from authorizer. + err = s.Authorizer.DeleteImage(r.Context(), projectName, imgInfo.Fingerprint) + if err != nil { + logger.Error("Failed to remove image from authorizer", logger.Ctx{"fingerprint": imgInfo.Fingerprint, "project": projectName, "error": err}) + } + s.Events.SendLifecycle(projectName, lifecycle.ImageDeleted.Event(imgInfo.Fingerprint, projectName, op.Requestor(), nil)) return nil @@ -2762,7 +2796,15 @@ func imageGet(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } - public := d.checkTrustedClient(r) != nil || allowProjectPermission("images", "view")(d, r) != response.EmptySyncResponse + var userCanViewImage bool + err = s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectImage(projectName, fingerprint), auth.EntitlementCanView) + if err == nil { + userCanViewImage = true + } else if !api.StatusErrorCheck(err, http.StatusForbidden) { + return response.SmartError(err) + } + + public := d.checkTrustedClient(r) != nil || !userCanViewImage secret := r.FormValue("secret") var info *api.Image @@ -3071,6 +3113,12 @@ func imageAliasesPost(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } + // Add the image alias to the authorizer. + err = s.Authorizer.AddImageAlias(r.Context(), projectName, req.Name) + if err != nil { + logger.Error("Failed to add image alias to authorizer", logger.Ctx{"name": req.Name, "project": projectName, "error": err}) + } + requestor := request.CreateRequestor(r) lc := lifecycle.ImageAliasCreated.Event(req.Name, projectName, requestor, logger.Ctx{"target": req.Target}) s.Events.SendLifecycle(projectName, lc) @@ -3174,10 +3222,15 @@ func imageAliasesGet(d *Daemon, r *http.Request) response.Response { projectName := request.ProjectParam(r) recursion := util.IsRecursionRequest(r) - var err error + s := d.State() + userHasPermission, err := s.Authorizer.GetPermissionChecker(r.Context(), r, auth.EntitlementCanView, auth.ObjectTypeImageAlias) + if err != nil { + return response.InternalError(fmt.Errorf("Failed to get a permission checker: %w", err)) + } + var responseStr []string var responseMap []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 { names, err := tx.GetImageAliases(ctx, projectName) if err != nil { return err @@ -3190,6 +3243,10 @@ func imageAliasesGet(d *Daemon, r *http.Request) response.Response { } for _, name := range names { + if !userHasPermission(auth.ObjectImageAlias(projectName, name)) { + continue + } + if !recursion { responseStr = append(responseStr, api.NewURL().Path(version.APIVersion, "images", "aliases", name).String()) } else { @@ -3304,7 +3361,17 @@ func imageAliasGet(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } - public := d.checkTrustedClient(r) != nil || allowProjectPermission("images", "view")(d, r) != response.EmptySyncResponse + s := d.State() + + var userCanViewImageAlias bool + err = s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectImageAlias(projectName, name), auth.EntitlementCanView) + if err == nil { + userCanViewImageAlias = true + } else if !api.StatusErrorCheck(err, http.StatusForbidden) { + return response.SmartError(err) + } + + public := d.checkTrustedClient(r) != nil || !userCanViewImageAlias var alias api.ImageAliasesEntry err = d.State().DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { @@ -3369,6 +3436,12 @@ func imageAliasDelete(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } + // Remove image alias from authorizer. + err = s.Authorizer.DeleteImageAlias(r.Context(), projectName, name) + if err != nil { + logger.Error("Failed to remove image alias from authorizer", logger.Ctx{"name": name, "project": projectName, "error": err}) + } + requestor := request.CreateRequestor(r) s.Events.SendLifecycle(projectName, lifecycle.ImageAliasDeleted.Event(name, projectName, requestor, nil)) @@ -3646,6 +3719,12 @@ func imageAliasPost(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } + // Rename image alias in authorizer. + err = s.Authorizer.RenameImageAlias(r.Context(), projectName, name, req.Name) + if err != nil { + logger.Error("Failed to rename image alias in authorizer", logger.Ctx{"old_name": name, "new_name": req.Name, "project": projectName}) + } + requestor := request.CreateRequestor(r) lc := lifecycle.ImageAliasRenamed.Event(req.Name, projectName, requestor, logger.Ctx{"old_name": name}) s.Events.SendLifecycle(projectName, lc) @@ -3716,7 +3795,15 @@ func imageExport(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } - public := d.checkTrustedClient(r) != nil || allowProjectPermission("images", "view")(d, r) != response.EmptySyncResponse + var userCanViewImage bool + err = s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectImage(projectName, fingerprint), auth.EntitlementCanView) + if err == nil { + userCanViewImage = true + } else if !api.StatusErrorCheck(err, http.StatusForbidden) { + return response.SmartError(err) + } + + public := d.checkTrustedClient(r) != nil || !userCanViewImage secret := r.FormValue("secret") var imgInfo *api.Image diff --git a/lxd/instance/drivers/driver_lxc.go b/lxd/instance/drivers/driver_lxc.go index 96d794c33d23..b72e49926e31 100644 --- a/lxd/instance/drivers/driver_lxc.go +++ b/lxd/instance/drivers/driver_lxc.go @@ -327,6 +327,12 @@ func lxcCreate(s *state.State, args db.InstanceArgs, p api.Project) (instance.In if d.isSnapshot { d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceSnapshotCreated.Event(d, nil)) } else { + // Add instance to authorizer. + err = d.state.Authorizer.AddInstance(d.state.ShutdownCtx, d.project.Name, d.Name()) + if err != nil { + logger.Error("Failed to add instance to authorizer", logger.Ctx{"instanceName": d.Name(), "projectName": d.project.Name, "error": err}) + } + d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceCreated.Event(d, map[string]any{ "type": api.InstanceTypeContainer, "storage-pool": d.storagePool.Name(), @@ -3826,6 +3832,11 @@ func (d *lxc) delete(force bool) error { if d.isSnapshot { d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceSnapshotDeleted.Event(d, nil)) } else { + err = d.state.Authorizer.DeleteInstance(d.state.ShutdownCtx, d.project.Name, d.Name()) + if err != nil { + logger.Error("Failed to remove instance from authorizer", logger.Ctx{"name": d.Name(), "project": d.project.Name, "error": err}) + } + d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceDeleted.Event(d, nil)) } @@ -4000,6 +4011,11 @@ func (d *lxc) Rename(newName string, applyTemplateTrigger bool) error { if d.isSnapshot { d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceSnapshotRenamed.Event(d, map[string]any{"old_name": oldName})) } else { + err = d.state.Authorizer.RenameInstance(d.state.ShutdownCtx, d.project.Name, oldName, newName) + if err != nil { + logger.Error("Failed to rename instance in authorizer", logger.Ctx{"old_name": oldName, "new_name": newName, "project": d.project.Name, "error": err}) + } + d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceRenamed.Event(d, map[string]any{"old_name": oldName})) } diff --git a/lxd/instance/drivers/driver_qemu.go b/lxd/instance/drivers/driver_qemu.go index 69258aca7ce8..772494779410 100644 --- a/lxd/instance/drivers/driver_qemu.go +++ b/lxd/instance/drivers/driver_qemu.go @@ -331,6 +331,11 @@ func qemuCreate(s *state.State, args db.InstanceArgs, p api.Project) (instance.I if d.isSnapshot { d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceSnapshotCreated.Event(d, nil)) } else { + err = d.state.Authorizer.AddInstance(d.state.ShutdownCtx, d.project.Name, d.Name()) + if err != nil { + logger.Error("Failed to add instance to authorizer", logger.Ctx{"name": d.Name(), "project": d.project.Name, "error": err}) + } + d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceCreated.Event(d, map[string]any{ "type": api.InstanceTypeVM, "storage-pool": d.storagePool.Name(), @@ -4886,6 +4891,11 @@ func (d *qemu) Rename(newName string, applyTemplateTrigger bool) error { if d.isSnapshot { d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceSnapshotRenamed.Event(d, map[string]any{"old_name": oldName})) } else { + err = d.state.Authorizer.RenameInstance(d.state.ShutdownCtx, d.project.Name, oldName, newName) + if err != nil { + logger.Error("Failed to rename instance in authorizer", logger.Ctx{"old_name": oldName, "new_name": newName, "project": d.project.Name, "error": err}) + } + d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceRenamed.Event(d, map[string]any{"old_name": oldName})) } @@ -5720,6 +5730,11 @@ func (d *qemu) delete(force bool) error { if d.isSnapshot { d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceSnapshotDeleted.Event(d, nil)) } else { + err = d.state.Authorizer.DeleteInstance(d.state.ShutdownCtx, d.project.Name, d.Name()) + if err != nil { + logger.Error("Failed to remove instance from authorizer", logger.Ctx{"name": d.Name(), "project": d.project.Name, "error": err}) + } + d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceDeleted.Event(d, nil)) } diff --git a/lxd/instance_logs.go b/lxd/instance_logs.go index 83fbbe8e9664..9bdc43f58552 100644 --- a/lxd/instance_logs.go +++ b/lxd/instance_logs.go @@ -10,6 +10,7 @@ import ( "github.com/gorilla/mux" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/instance" "github.com/canonical/lxd/lxd/lifecycle" "github.com/canonical/lxd/lxd/project" @@ -29,8 +30,8 @@ var instanceLogCmd = APIEndpoint{ {Name: "vmLog", Path: "virtual-machines/{name}/logs/{file}"}, }, - Delete: APIEndpointAction{Handler: instanceLogDelete, AccessHandler: allowProjectPermission("containers", "operate-containers")}, - Get: APIEndpointAction{Handler: instanceLogGet, AccessHandler: allowProjectPermission("containers", "view")}, + Delete: APIEndpointAction{Handler: instanceLogDelete, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanEdit, "name")}, + Get: APIEndpointAction{Handler: instanceLogGet, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanView, "name")}, } var instanceLogsCmd = APIEndpoint{ @@ -41,7 +42,7 @@ var instanceLogsCmd = APIEndpoint{ {Name: "vmLogs", Path: "virtual-machines/{name}/logs"}, }, - Get: APIEndpointAction{Handler: instanceLogsGet, AccessHandler: allowProjectPermission("containers", "view")}, + Get: APIEndpointAction{Handler: instanceLogsGet, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanView, "name")}, } var instanceExecOutputCmd = APIEndpoint{ @@ -52,8 +53,8 @@ var instanceExecOutputCmd = APIEndpoint{ {Name: "vmExecOutput", Path: "virtual-machines/{name}/logs/exec-output/{file}"}, }, - Delete: APIEndpointAction{Handler: instanceExecOutputDelete, AccessHandler: allowProjectPermission("containers", "operate-containers")}, - Get: APIEndpointAction{Handler: instanceExecOutputGet, AccessHandler: allowProjectPermission("containers", "view")}, + Delete: APIEndpointAction{Handler: instanceExecOutputDelete, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanExec, "name")}, + Get: APIEndpointAction{Handler: instanceExecOutputGet, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanExec, "name")}, } var instanceExecOutputsCmd = APIEndpoint{ @@ -64,7 +65,7 @@ var instanceExecOutputsCmd = APIEndpoint{ {Name: "vmExecOutputs", Path: "virtual-machines/{name}/logs/exec-output"}, }, - Get: APIEndpointAction{Handler: instanceExecOutputsGet, AccessHandler: allowProjectPermission("containers", "view")}, + Get: APIEndpointAction{Handler: instanceExecOutputsGet, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanExec, "name")}, } // swagger:operation GET /1.0/instances/{name}/logs instances instance_logs_get diff --git a/lxd/instance_post.go b/lxd/instance_post.go index 2caf07275463..315a8ec21209 100644 --- a/lxd/instance_post.go +++ b/lxd/instance_post.go @@ -11,6 +11,7 @@ import ( "github.com/gorilla/mux" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/cluster" "github.com/canonical/lxd/lxd/db" dbCluster "github.com/canonical/lxd/lxd/db/cluster" @@ -343,8 +344,9 @@ func instancePost(d *Daemon, r *http.Request) response.Response { // Server-side project migration. if req.Project != "" { // Check if user has access to target project - if !s.Authorizer.UserHasPermission(r, req.Project, "manage-containers") { - return response.Forbidden(nil) + err := s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectProject(req.Project), auth.EntitlementCanCreateInstances) + if err != nil { + return response.SmartError(err) } // Setup the instance move operation. diff --git a/lxd/instances.go b/lxd/instances.go index c912a7a4f823..f404d67d1603 100644 --- a/lxd/instances.go +++ b/lxd/instances.go @@ -11,6 +11,7 @@ import ( "sync" "time" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/db" "github.com/canonical/lxd/lxd/db/cluster" "github.com/canonical/lxd/lxd/db/warningtype" @@ -32,9 +33,9 @@ var instancesCmd = APIEndpoint{ {Name: "vms", Path: "virtual-machines"}, }, - Get: APIEndpointAction{Handler: instancesGet, AccessHandler: allowProjectPermission("containers", "view")}, - Post: APIEndpointAction{Handler: instancesPost, AccessHandler: allowProjectPermission("containers", "manage-containers")}, - Put: APIEndpointAction{Handler: instancesPut, AccessHandler: allowProjectPermission("containers", "operate-containers")}, + Get: APIEndpointAction{Handler: instancesGet, AccessHandler: allowAuthenticated}, + Post: APIEndpointAction{Handler: instancesPost, AccessHandler: allowPermission(auth.ObjectTypeProject, auth.EntitlementCanCreateInstances)}, + Put: APIEndpointAction{Handler: instancesPut, AccessHandler: allowAuthenticated}, } var instanceCmd = APIEndpoint{ @@ -45,11 +46,11 @@ var instanceCmd = APIEndpoint{ {Name: "vm", Path: "virtual-machines/{name}"}, }, - Get: APIEndpointAction{Handler: instanceGet, AccessHandler: allowProjectPermission("containers", "view")}, - Put: APIEndpointAction{Handler: instancePut, AccessHandler: allowProjectPermission("containers", "manage-containers")}, - Delete: APIEndpointAction{Handler: instanceDelete, AccessHandler: allowProjectPermission("containers", "manage-containers")}, - Post: APIEndpointAction{Handler: instancePost, AccessHandler: allowProjectPermission("containers", "manage-containers")}, - Patch: APIEndpointAction{Handler: instancePatch, AccessHandler: allowProjectPermission("containers", "manage-containers")}, + Get: APIEndpointAction{Handler: instanceGet, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanView, "name")}, + Put: APIEndpointAction{Handler: instancePut, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanEdit, "name")}, + Delete: APIEndpointAction{Handler: instanceDelete, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanEdit, "name")}, + Post: APIEndpointAction{Handler: instancePost, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanEdit, "name")}, + Patch: APIEndpointAction{Handler: instancePatch, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanEdit, "name")}, } var instanceRebuildCmd = APIEndpoint{ @@ -60,7 +61,7 @@ var instanceRebuildCmd = APIEndpoint{ {Name: "vmRebuild", Path: "virtual-machines/{name}/rebuild"}, }, - Post: APIEndpointAction{Handler: instanceRebuildPost, AccessHandler: allowProjectPermission("containers", "manage-containers")}, + Post: APIEndpointAction{Handler: instanceRebuildPost, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanEdit, "name")}, } var instanceStateCmd = APIEndpoint{ @@ -71,8 +72,8 @@ var instanceStateCmd = APIEndpoint{ {Name: "vmState", Path: "virtual-machines/{name}/state"}, }, - Get: APIEndpointAction{Handler: instanceState, AccessHandler: allowProjectPermission("containers", "view")}, - Put: APIEndpointAction{Handler: instanceStatePut, AccessHandler: allowProjectPermission("containers", "operate-containers")}, + Get: APIEndpointAction{Handler: instanceState, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanView, "name")}, + Put: APIEndpointAction{Handler: instanceStatePut, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanUpdateState, "name")}, } var instanceSFTPCmd = APIEndpoint{ @@ -83,7 +84,7 @@ var instanceSFTPCmd = APIEndpoint{ {Name: "vmFile", Path: "virtual-machines/{name}/sftp"}, }, - Get: APIEndpointAction{Handler: instanceSFTPHandler, AccessHandler: allowProjectPermission("containers", "operate-containers")}, + Get: APIEndpointAction{Handler: instanceSFTPHandler, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanConnectSFTP, "name")}, } var instanceFileCmd = APIEndpoint{ @@ -94,10 +95,10 @@ var instanceFileCmd = APIEndpoint{ {Name: "vmFile", Path: "virtual-machines/{name}/files"}, }, - Get: APIEndpointAction{Handler: instanceFileHandler, AccessHandler: allowProjectPermission("containers", "operate-containers")}, - Head: APIEndpointAction{Handler: instanceFileHandler, AccessHandler: allowProjectPermission("containers", "operate-containers")}, - Post: APIEndpointAction{Handler: instanceFileHandler, AccessHandler: allowProjectPermission("containers", "operate-containers")}, - Delete: APIEndpointAction{Handler: instanceFileHandler, AccessHandler: allowProjectPermission("containers", "operate-containers")}, + Get: APIEndpointAction{Handler: instanceFileHandler, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanAccessFiles, "name")}, + Head: APIEndpointAction{Handler: instanceFileHandler, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanAccessFiles, "name")}, + Post: APIEndpointAction{Handler: instanceFileHandler, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanAccessFiles, "name")}, + Delete: APIEndpointAction{Handler: instanceFileHandler, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanAccessFiles, "name")}, } var instanceSnapshotsCmd = APIEndpoint{ @@ -108,8 +109,8 @@ var instanceSnapshotsCmd = APIEndpoint{ {Name: "vmSnapshots", Path: "virtual-machines/{name}/snapshots"}, }, - Get: APIEndpointAction{Handler: instanceSnapshotsGet, AccessHandler: allowProjectPermission("containers", "view")}, - Post: APIEndpointAction{Handler: instanceSnapshotsPost, AccessHandler: allowProjectPermission("containers", "operate-containers")}, + Get: APIEndpointAction{Handler: instanceSnapshotsGet, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanView, "name")}, + Post: APIEndpointAction{Handler: instanceSnapshotsPost, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanManageSnapshots, "name")}, } var instanceSnapshotCmd = APIEndpoint{ @@ -120,11 +121,11 @@ var instanceSnapshotCmd = APIEndpoint{ {Name: "vmSnapshot", Path: "virtual-machines/{name}/snapshots/{snapshotName}"}, }, - Get: APIEndpointAction{Handler: instanceSnapshotHandler, AccessHandler: allowProjectPermission("containers", "operate-containers")}, - Post: APIEndpointAction{Handler: instanceSnapshotHandler, AccessHandler: allowProjectPermission("containers", "operate-containers")}, - Delete: APIEndpointAction{Handler: instanceSnapshotHandler, AccessHandler: allowProjectPermission("containers", "operate-containers")}, - Patch: APIEndpointAction{Handler: instanceSnapshotHandler, AccessHandler: allowProjectPermission("containers", "operate-containers")}, - Put: APIEndpointAction{Handler: instanceSnapshotHandler, AccessHandler: allowProjectPermission("containers", "operate-containers")}, + Get: APIEndpointAction{Handler: instanceSnapshotHandler, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanView, "name")}, + Post: APIEndpointAction{Handler: instanceSnapshotHandler, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanManageSnapshots, "name")}, + Delete: APIEndpointAction{Handler: instanceSnapshotHandler, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanManageSnapshots, "name")}, + Patch: APIEndpointAction{Handler: instanceSnapshotHandler, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanManageSnapshots, "name")}, + Put: APIEndpointAction{Handler: instanceSnapshotHandler, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanManageSnapshots, "name")}, } var instanceConsoleCmd = APIEndpoint{ @@ -135,9 +136,9 @@ var instanceConsoleCmd = APIEndpoint{ {Name: "vmConsole", Path: "virtual-machines/{name}/console"}, }, - Get: APIEndpointAction{Handler: instanceConsoleLogGet, AccessHandler: allowProjectPermission("containers", "view")}, - Post: APIEndpointAction{Handler: instanceConsolePost, AccessHandler: allowProjectPermission("containers", "operate-containers")}, - Delete: APIEndpointAction{Handler: instanceConsoleLogDelete, AccessHandler: allowProjectPermission("containers", "operate-containers")}, + Get: APIEndpointAction{Handler: instanceConsoleLogGet, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanView, "name")}, + Post: APIEndpointAction{Handler: instanceConsolePost, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanAccessConsole, "name")}, + Delete: APIEndpointAction{Handler: instanceConsoleLogDelete, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanEdit, "name")}, } var instanceExecCmd = APIEndpoint{ @@ -148,7 +149,7 @@ var instanceExecCmd = APIEndpoint{ {Name: "vmExec", Path: "virtual-machines/{name}/exec"}, }, - Post: APIEndpointAction{Handler: instanceExecPost, AccessHandler: allowProjectPermission("containers", "operate-containers")}, + Post: APIEndpointAction{Handler: instanceExecPost, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanExec, "name")}, } var instanceMetadataCmd = APIEndpoint{ @@ -159,9 +160,9 @@ var instanceMetadataCmd = APIEndpoint{ {Name: "vmMetadata", Path: "virtual-machines/{name}/metadata"}, }, - Get: APIEndpointAction{Handler: instanceMetadataGet, AccessHandler: allowProjectPermission("containers", "view")}, - Patch: APIEndpointAction{Handler: instanceMetadataPatch, AccessHandler: allowProjectPermission("containers", "manage-containers")}, - Put: APIEndpointAction{Handler: instanceMetadataPut, AccessHandler: allowProjectPermission("containers", "manage-containers")}, + Get: APIEndpointAction{Handler: instanceMetadataGet, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanView, "name")}, + Patch: APIEndpointAction{Handler: instanceMetadataPatch, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanEdit, "name")}, + Put: APIEndpointAction{Handler: instanceMetadataPut, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanEdit, "name")}, } var instanceMetadataTemplatesCmd = APIEndpoint{ @@ -172,9 +173,9 @@ var instanceMetadataTemplatesCmd = APIEndpoint{ {Name: "vmMetadataTemplates", Path: "virtual-machines/{name}/metadata/templates"}, }, - Get: APIEndpointAction{Handler: instanceMetadataTemplatesGet, AccessHandler: allowProjectPermission("containers", "view")}, - Post: APIEndpointAction{Handler: instanceMetadataTemplatesPost, AccessHandler: allowProjectPermission("containers", "manage-containers")}, - Delete: APIEndpointAction{Handler: instanceMetadataTemplatesDelete, AccessHandler: allowProjectPermission("containers", "manage-containers")}, + Get: APIEndpointAction{Handler: instanceMetadataTemplatesGet, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanView, "name")}, + Post: APIEndpointAction{Handler: instanceMetadataTemplatesPost, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanEdit, "name")}, + Delete: APIEndpointAction{Handler: instanceMetadataTemplatesDelete, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanEdit, "name")}, } var instanceBackupsCmd = APIEndpoint{ @@ -185,8 +186,8 @@ var instanceBackupsCmd = APIEndpoint{ {Name: "vmBackups", Path: "virtual-machines/{name}/backups"}, }, - Get: APIEndpointAction{Handler: instanceBackupsGet, AccessHandler: allowProjectPermission("containers", "view")}, - Post: APIEndpointAction{Handler: instanceBackupsPost, AccessHandler: allowProjectPermission("containers", "operate-containers")}, + Get: APIEndpointAction{Handler: instanceBackupsGet, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanView, "name")}, + Post: APIEndpointAction{Handler: instanceBackupsPost, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanManageBackups, "name")}, } var instanceBackupCmd = APIEndpoint{ @@ -197,9 +198,9 @@ var instanceBackupCmd = APIEndpoint{ {Name: "vmBackup", Path: "virtual-machines/{name}/backups/{backupName}"}, }, - Get: APIEndpointAction{Handler: instanceBackupGet, AccessHandler: allowProjectPermission("containers", "view")}, - Post: APIEndpointAction{Handler: instanceBackupPost, AccessHandler: allowProjectPermission("containers", "operate-containers")}, - Delete: APIEndpointAction{Handler: instanceBackupDelete, AccessHandler: allowProjectPermission("containers", "operate-containers")}, + Get: APIEndpointAction{Handler: instanceBackupGet, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanView, "name")}, + Post: APIEndpointAction{Handler: instanceBackupPost, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanManageBackups, "name")}, + Delete: APIEndpointAction{Handler: instanceBackupDelete, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanManageBackups, "name")}, } var instanceBackupExportCmd = APIEndpoint{ @@ -210,7 +211,7 @@ var instanceBackupExportCmd = APIEndpoint{ {Name: "vmBackupExport", Path: "virtual-machines/{name}/backups/{backupName}/export"}, }, - Get: APIEndpointAction{Handler: instanceBackupExportGet, AccessHandler: allowProjectPermission("containers", "view")}, + Get: APIEndpointAction{Handler: instanceBackupExportGet, AccessHandler: allowPermission(auth.ObjectTypeInstance, auth.EntitlementCanManageBackups, "name")}, } type instanceAutostartList []instance.Instance diff --git a/lxd/instances_get.go b/lxd/instances_get.go index c062f3219aac..486fc8502346 100644 --- a/lxd/instances_get.go +++ b/lxd/instances_get.go @@ -13,6 +13,7 @@ import ( "github.com/gorilla/mux" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/cluster" "github.com/canonical/lxd/lxd/db" dbCluster "github.com/canonical/lxd/lxd/db/cluster" @@ -286,10 +287,6 @@ func doInstancesGet(s *state.State, r *http.Request) (any, error) { } for _, project := range projects { - if !s.Authorizer.UserHasPermission(r, project.Name, "view") { - continue - } - filteredProjects = append(filteredProjects, project.Name) } } else { @@ -309,6 +306,26 @@ func doInstancesGet(s *state.State, r *http.Request) (any, error) { return nil, err } + userHasPermission, err := s.Authorizer.GetPermissionChecker(r.Context(), r, auth.EntitlementCanView, auth.ObjectTypeInstance) + if err != nil { + return nil, err + } + + // Removes instances the user doesn't have access to. + for address, instances := range memberAddressInstances { + var filteredInstances []db.Instance + + for _, inst := range instances { + if !userHasPermission(auth.ObjectInstance(inst.Project, inst.Name)) { + continue + } + + filteredInstances = append(filteredInstances, inst) + } + + memberAddressInstances[address] = filteredInstances + } + resultErrListAppend := func(inst db.Instance, err error) { instFull := &api.InstanceFull{ Instance: api.Instance{ diff --git a/lxd/instances_put.go b/lxd/instances_put.go index fcd83418f64d..856c4faca712 100644 --- a/lxd/instances_put.go +++ b/lxd/instances_put.go @@ -8,6 +8,7 @@ import ( "strings" "sync" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/cluster" "github.com/canonical/lxd/lxd/db" "github.com/canonical/lxd/lxd/instance" @@ -96,6 +97,11 @@ func instancesPut(d *Daemon, r *http.Request) response.Response { action := shared.InstanceAction(req.State.Action) + userHasPermission, err := s.Authorizer.GetPermissionChecker(r.Context(), r, auth.EntitlementCanUpdateState, auth.ObjectTypeInstance) + if err != nil { + return response.SmartError(err) + } + var names []string var instances []instance.Instance for _, inst := range c { @@ -103,6 +109,11 @@ func instancesPut(d *Daemon, r *http.Request) response.Response { continue } + // Only allow changing the state of instances the user has permission for. + if !userHasPermission(auth.ObjectInstance(inst.Project().Name, inst.Name())) { + continue + } + switch action { case shared.Freeze: if !inst.IsRunning() { diff --git a/lxd/metrics/metrics.go b/lxd/metrics/metrics.go index 0cd9cc404892..3ac047869073 100644 --- a/lxd/metrics/metrics.go +++ b/lxd/metrics/metrics.go @@ -5,6 +5,8 @@ import ( "sort" "strconv" "strings" + + "github.com/canonical/lxd/lxd/auth" ) // NewMetricSet returns a new MetricSet. @@ -20,6 +22,28 @@ func NewMetricSet(labels map[string]string) *MetricSet { return &out } +// FilterSamples filters the existing MetricSet using the given permission checker. Samples not containing "project" and +// "name" labels are skipped. +func (m *MetricSet) FilterSamples(permissionChecker func(object auth.Object) bool) { + for metricType, samples := range m.set { + allowedSamples := make([]Sample, 0, len(samples)) + for _, s := range samples { + projectName := s.Labels["project"] + instanceName := s.Labels["name"] + if projectName == "" || instanceName == "" { + continue + } + + hasPermission := permissionChecker(auth.ObjectInstance(projectName, instanceName)) + if hasPermission { + allowedSamples = append(allowedSamples, s) + } + } + + m.set[metricType] = allowedSamples + } +} + // AddSamples adds samples of the type metricType to the MetricSet. func (m *MetricSet) AddSamples(metricType MetricType, samples ...Sample) { for i := 0; i < len(samples); i++ { diff --git a/lxd/metrics/metrics_test.go b/lxd/metrics/metrics_test.go new file mode 100644 index 000000000000..1b597b21c302 --- /dev/null +++ b/lxd/metrics/metrics_test.go @@ -0,0 +1,40 @@ +package metrics + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/canonical/lxd/lxd/auth" +) + +func TestMetricSet_FilterSamples(t *testing.T) { + labels := map[string]string{"project": "default", "name": "jammy"} + newMetricSet := func() *MetricSet { + m := NewMetricSet(labels) + require.Equal(t, labels, m.labels) + m.AddSamples(CPUSecondsTotal, Sample{Value: 10}) + require.Equal(t, []Sample{{Value: 10, Labels: labels}}, m.set[CPUSecondsTotal]) + return m + } + + m := newMetricSet() + permissionChecker := func(object auth.Object) bool { + return object == auth.ObjectInstance("default", "jammy") + } + + m.FilterSamples(permissionChecker) + + // Should still contain the sample + require.Equal(t, []Sample{{Value: 10, Labels: labels}}, m.set[CPUSecondsTotal]) + + m = newMetricSet() + permissionChecker = func(object auth.Object) bool { + return object == auth.ObjectInstance("not-default", "not-jammy") + } + + m.FilterSamples(permissionChecker) + + // Should no longer contain the sample. + require.Equal(t, []Sample{}, m.set[CPUSecondsTotal]) +} diff --git a/lxd/network_acls.go b/lxd/network_acls.go index 347c82dcbd52..3e79cfd49cc6 100644 --- a/lxd/network_acls.go +++ b/lxd/network_acls.go @@ -10,6 +10,7 @@ import ( "github.com/gorilla/mux" + "github.com/canonical/lxd/lxd/auth" clusterRequest "github.com/canonical/lxd/lxd/cluster/request" "github.com/canonical/lxd/lxd/lifecycle" "github.com/canonical/lxd/lxd/network/acl" @@ -25,24 +26,24 @@ import ( var networkACLsCmd = APIEndpoint{ Path: "network-acls", - Get: APIEndpointAction{Handler: networkACLsGet, AccessHandler: allowProjectPermission("networks", "view")}, - Post: APIEndpointAction{Handler: networkACLsPost, AccessHandler: allowProjectPermission("networks", "manage-networks")}, + Get: APIEndpointAction{Handler: networkACLsGet, AccessHandler: allowAuthenticated}, + Post: APIEndpointAction{Handler: networkACLsPost, AccessHandler: allowPermission(auth.ObjectTypeProject, auth.EntitlementCanCreateNetworkACLs)}, } var networkACLCmd = APIEndpoint{ Path: "network-acls/{name}", - Delete: APIEndpointAction{Handler: networkACLDelete, AccessHandler: allowProjectPermission("networks", "manage-networks")}, - Get: APIEndpointAction{Handler: networkACLGet, AccessHandler: allowProjectPermission("networks", "view")}, - Put: APIEndpointAction{Handler: networkACLPut, AccessHandler: allowProjectPermission("networks", "manage-networks")}, - Patch: APIEndpointAction{Handler: networkACLPut, AccessHandler: allowProjectPermission("networks", "manage-networks")}, - Post: APIEndpointAction{Handler: networkACLPost, AccessHandler: allowProjectPermission("networks", "manage-networks")}, + Delete: APIEndpointAction{Handler: networkACLDelete, AccessHandler: allowPermission(auth.ObjectTypeNetworkACL, auth.EntitlementCanEdit, "name")}, + Get: APIEndpointAction{Handler: networkACLGet, AccessHandler: allowPermission(auth.ObjectTypeNetworkACL, auth.EntitlementCanView, "name")}, + Put: APIEndpointAction{Handler: networkACLPut, AccessHandler: allowPermission(auth.ObjectTypeNetworkACL, auth.EntitlementCanEdit, "name")}, + Patch: APIEndpointAction{Handler: networkACLPut, AccessHandler: allowPermission(auth.ObjectTypeNetworkACL, auth.EntitlementCanEdit, "name")}, + Post: APIEndpointAction{Handler: networkACLPost, AccessHandler: allowPermission(auth.ObjectTypeNetworkACL, auth.EntitlementCanEdit, "name")}, } var networkACLLogCmd = APIEndpoint{ Path: "network-acls/{name}/log", - Get: APIEndpointAction{Handler: networkACLLogGet, AccessHandler: allowProjectPermission("networks", "view")}, + Get: APIEndpointAction{Handler: networkACLLogGet, AccessHandler: allowPermission(auth.ObjectTypeNetworkACL, auth.EntitlementCanView, "name")}, } // API endpoints. @@ -155,9 +156,18 @@ func networkACLsGet(d *Daemon, r *http.Request) response.Response { return response.InternalError(err) } + userHasPermission, err := s.Authorizer.GetPermissionChecker(r.Context(), r, auth.EntitlementCanView, auth.ObjectTypeNetworkACL) + if err != nil { + return response.SmartError(err) + } + resultString := []string{} resultMap := []api.NetworkACL{} for _, aclName := range aclNames { + if !userHasPermission(auth.ObjectNetworkACL(projectName, aclName)) { + continue + } + if !recursion { resultString = append(resultString, fmt.Sprintf("/%s/network-acls/%s", version.APIVersion, aclName)) } else { @@ -243,6 +253,11 @@ func networkACLsPost(d *Daemon, r *http.Request) response.Response { return response.BadRequest(err) } + err = s.Authorizer.AddNetworkACL(r.Context(), projectName, req.Name) + if err != nil { + logger.Error("Failed to add network ACL to authorizer", logger.Ctx{"name": req.Name, "project": projectName, "error": err}) + } + lc := lifecycle.NetworkACLCreated.Event(netACL, request.CreateRequestor(r), nil) s.Events.SendLifecycle(projectName, lc) @@ -296,6 +311,11 @@ func networkACLDelete(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } + err = s.Authorizer.DeleteNetworkACL(r.Context(), projectName, aclName) + if err != nil { + logger.Error("Failed to remove network ACL from authorizer", logger.Ctx{"name": aclName, "project": projectName, "error": err}) + } + s.Events.SendLifecycle(projectName, lifecycle.NetworkACLDeleted.Event(netACL, request.CreateRequestor(r), nil)) return response.EmptySyncResponse @@ -557,6 +577,11 @@ func networkACLPost(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } + err = s.Authorizer.RenameNetworkACL(r.Context(), projectName, aclName, req.Name) + if err != nil { + logger.Error("Failed to rename network ACL in authorizer", logger.Ctx{"old_name": aclName, "new_name": req.Name, "project": projectName, "error": err}) + } + lc := lifecycle.NetworkACLRenamed.Event(netACL, request.CreateRequestor(r), logger.Ctx{"old_name": aclName}) s.Events.SendLifecycle(projectName, lc) diff --git a/lxd/network_allocations.go b/lxd/network_allocations.go index fad0644420e5..026662cf1553 100644 --- a/lxd/network_allocations.go +++ b/lxd/network_allocations.go @@ -7,6 +7,7 @@ import ( "net" "net/http" + "github.com/canonical/lxd/lxd/auth" clusterRequest "github.com/canonical/lxd/lxd/cluster/request" "github.com/canonical/lxd/lxd/db" dbCluster "github.com/canonical/lxd/lxd/db/cluster" @@ -22,7 +23,7 @@ import ( var networkAllocationsCmd = APIEndpoint{ Path: "network-allocations", - Get: APIEndpointAction{Handler: networkAllocationsGet, AccessHandler: allowProjectPermission("networks", "view")}, + Get: APIEndpointAction{Handler: networkAllocationsGet, AccessHandler: allowAuthenticated}, } // swagger:operation GET /1.0/network-allocations network-allocations network_allocations_get @@ -72,6 +73,8 @@ var networkAllocationsCmd = APIEndpoint{ // "500": // $ref: "#/responses/InternalServerError" func networkAllocationsGet(d *Daemon, r *http.Request) response.Response { + s := d.State() + projectName, _, err := project.NetworkProject(d.State().DB.Cluster, request.ProjectParam(r)) if err != nil { return response.SmartError(err) @@ -115,6 +118,11 @@ func networkAllocationsGet(d *Daemon, r *http.Request) response.Response { result := make([]api.NetworkAllocations, 0) + userHasPermission, err := s.Authorizer.GetPermissionChecker(r.Context(), r, auth.EntitlementCanView, auth.ObjectTypeNetwork) + if err != nil { + return response.SmartError(err) + } + // Then, get all the networks, their network forwards and their network load balancers. for _, projectName := range projectNames { networkNames, err := d.db.Cluster.GetNetworks(projectName) @@ -124,6 +132,10 @@ 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(auth.ObjectNetwork(projectName, networkName)) { + continue + } + n, err := network.LoadByName(d.State(), projectName, networkName) if err != nil { return response.SmartError(fmt.Errorf("Failed loading network %q in project %q: %w", networkName, projectName, err)) diff --git a/lxd/network_forwards.go b/lxd/network_forwards.go index c8086e9c2335..f4bab2a618a5 100644 --- a/lxd/network_forwards.go +++ b/lxd/network_forwards.go @@ -8,6 +8,7 @@ import ( "github.com/gorilla/mux" + "github.com/canonical/lxd/lxd/auth" clusterRequest "github.com/canonical/lxd/lxd/cluster/request" "github.com/canonical/lxd/lxd/lifecycle" "github.com/canonical/lxd/lxd/network" @@ -22,17 +23,17 @@ import ( var networkForwardsCmd = APIEndpoint{ Path: "networks/{networkName}/forwards", - Get: APIEndpointAction{Handler: networkForwardsGet, AccessHandler: allowProjectPermission("networks", "view")}, - Post: APIEndpointAction{Handler: networkForwardsPost, AccessHandler: allowProjectPermission("networks", "manage-networks")}, + Get: APIEndpointAction{Handler: networkForwardsGet, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanView, "networkName")}, + Post: APIEndpointAction{Handler: networkForwardsPost, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanEdit, "networkName")}, } var networkForwardCmd = APIEndpoint{ Path: "networks/{networkName}/forwards/{listenAddress}", - Delete: APIEndpointAction{Handler: networkForwardDelete, AccessHandler: allowProjectPermission("networks", "manage-networks")}, - Get: APIEndpointAction{Handler: networkForwardGet, AccessHandler: allowProjectPermission("networks", "view")}, - Put: APIEndpointAction{Handler: networkForwardPut, AccessHandler: allowProjectPermission("networks", "manage-networks")}, - Patch: APIEndpointAction{Handler: networkForwardPut, AccessHandler: allowProjectPermission("networks", "manage-networks")}, + Delete: APIEndpointAction{Handler: networkForwardDelete, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanEdit, "networkName")}, + Get: APIEndpointAction{Handler: networkForwardGet, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanView, "networkName")}, + Put: APIEndpointAction{Handler: networkForwardPut, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanEdit, "networkName")}, + Patch: APIEndpointAction{Handler: networkForwardPut, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanEdit, "networkName")}, } // API endpoints diff --git a/lxd/network_load_balancers.go b/lxd/network_load_balancers.go index b82c96773e46..7182eeef511e 100644 --- a/lxd/network_load_balancers.go +++ b/lxd/network_load_balancers.go @@ -8,6 +8,7 @@ import ( "github.com/gorilla/mux" + "github.com/canonical/lxd/lxd/auth" clusterRequest "github.com/canonical/lxd/lxd/cluster/request" "github.com/canonical/lxd/lxd/lifecycle" "github.com/canonical/lxd/lxd/network" @@ -22,17 +23,17 @@ import ( var networkLoadBalancersCmd = APIEndpoint{ Path: "networks/{networkName}/load-balancers", - Get: APIEndpointAction{Handler: networkLoadBalancersGet, AccessHandler: allowProjectPermission("networks", "view")}, - Post: APIEndpointAction{Handler: networkLoadBalancersPost, AccessHandler: allowProjectPermission("networks", "manage-networks")}, + Get: APIEndpointAction{Handler: networkLoadBalancersGet, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanView, "networkName")}, + Post: APIEndpointAction{Handler: networkLoadBalancersPost, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanEdit, "networkName")}, } var networkLoadBalancerCmd = APIEndpoint{ Path: "networks/{networkName}/load-balancers/{listenAddress}", - Delete: APIEndpointAction{Handler: networkLoadBalancerDelete, AccessHandler: allowProjectPermission("networks", "manage-networks")}, - Get: APIEndpointAction{Handler: networkLoadBalancerGet, AccessHandler: allowProjectPermission("networks", "view")}, - Put: APIEndpointAction{Handler: networkLoadBalancerPut, AccessHandler: allowProjectPermission("networks", "manage-networks")}, - Patch: APIEndpointAction{Handler: networkLoadBalancerPut, AccessHandler: allowProjectPermission("networks", "manage-networks")}, + Delete: APIEndpointAction{Handler: networkLoadBalancerDelete, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanEdit, "networkName")}, + Get: APIEndpointAction{Handler: networkLoadBalancerGet, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanView, "networkName")}, + Put: APIEndpointAction{Handler: networkLoadBalancerPut, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanEdit, "networkName")}, + Patch: APIEndpointAction{Handler: networkLoadBalancerPut, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanEdit, "networkName")}, } // API endpoints diff --git a/lxd/network_peer.go b/lxd/network_peer.go index 47432936b089..154225d4647a 100644 --- a/lxd/network_peer.go +++ b/lxd/network_peer.go @@ -8,6 +8,7 @@ import ( "github.com/gorilla/mux" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/lifecycle" "github.com/canonical/lxd/lxd/network" "github.com/canonical/lxd/lxd/project" @@ -21,17 +22,17 @@ import ( var networkPeersCmd = APIEndpoint{ Path: "networks/{networkName}/peers", - Get: APIEndpointAction{Handler: networkPeersGet, AccessHandler: allowProjectPermission("networks", "view")}, - Post: APIEndpointAction{Handler: networkPeersPost, AccessHandler: allowProjectPermission("networks", "manage-networks")}, + Get: APIEndpointAction{Handler: networkPeersGet, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanView, "networkName")}, + Post: APIEndpointAction{Handler: networkPeersPost, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanEdit, "networkName")}, } var networkPeerCmd = APIEndpoint{ Path: "networks/{networkName}/peers/{peerName}", - Delete: APIEndpointAction{Handler: networkPeerDelete, AccessHandler: allowProjectPermission("networks", "manage-networks")}, - Get: APIEndpointAction{Handler: networkPeerGet, AccessHandler: allowProjectPermission("networks", "view")}, - Put: APIEndpointAction{Handler: networkPeerPut, AccessHandler: allowProjectPermission("networks", "manage-networks")}, - Patch: APIEndpointAction{Handler: networkPeerPut, AccessHandler: allowProjectPermission("networks", "manage-networks")}, + Delete: APIEndpointAction{Handler: networkPeerDelete, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanEdit, "networkName")}, + Get: APIEndpointAction{Handler: networkPeerGet, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanView, "networkName")}, + Put: APIEndpointAction{Handler: networkPeerPut, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanEdit, "networkName")}, + Patch: APIEndpointAction{Handler: networkPeerPut, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanEdit, "networkName")}, } // API endpoints diff --git a/lxd/network_zones.go b/lxd/network_zones.go index 0a6281019e2c..22613e4bfb26 100644 --- a/lxd/network_zones.go +++ b/lxd/network_zones.go @@ -8,6 +8,7 @@ import ( "github.com/gorilla/mux" + "github.com/canonical/lxd/lxd/auth" clusterRequest "github.com/canonical/lxd/lxd/cluster/request" "github.com/canonical/lxd/lxd/lifecycle" "github.com/canonical/lxd/lxd/network/zone" @@ -16,23 +17,24 @@ import ( "github.com/canonical/lxd/lxd/response" "github.com/canonical/lxd/lxd/util" "github.com/canonical/lxd/shared/api" + "github.com/canonical/lxd/shared/logger" "github.com/canonical/lxd/shared/version" ) var networkZonesCmd = APIEndpoint{ Path: "network-zones", - Get: APIEndpointAction{Handler: networkZonesGet, AccessHandler: allowProjectPermission("networks", "view")}, - Post: APIEndpointAction{Handler: networkZonesPost, AccessHandler: allowProjectPermission("networks", "manage-networks")}, + Get: APIEndpointAction{Handler: networkZonesGet, AccessHandler: allowAuthenticated}, + Post: APIEndpointAction{Handler: networkZonesPost, AccessHandler: allowPermission(auth.ObjectTypeProject, auth.EntitlementCanCreateNetworkZones)}, } var networkZoneCmd = APIEndpoint{ Path: "network-zones/{zone}", - Delete: APIEndpointAction{Handler: networkZoneDelete, AccessHandler: allowProjectPermission("networks", "manage-networks")}, - Get: APIEndpointAction{Handler: networkZoneGet, AccessHandler: allowProjectPermission("networks", "view")}, - Put: APIEndpointAction{Handler: networkZonePut, AccessHandler: allowProjectPermission("networks", "manage-networks")}, - Patch: APIEndpointAction{Handler: networkZonePut, AccessHandler: allowProjectPermission("networks", "manage-networks")}, + Delete: APIEndpointAction{Handler: networkZoneDelete, AccessHandler: allowPermission(auth.ObjectTypeNetworkZone, auth.EntitlementCanEdit, "zone")}, + Get: APIEndpointAction{Handler: networkZoneGet, AccessHandler: allowPermission(auth.ObjectTypeNetworkZone, auth.EntitlementCanView, "zone")}, + Put: APIEndpointAction{Handler: networkZonePut, AccessHandler: allowPermission(auth.ObjectTypeNetworkZone, auth.EntitlementCanEdit, "zone")}, + Patch: APIEndpointAction{Handler: networkZonePut, AccessHandler: allowPermission(auth.ObjectTypeNetworkZone, auth.EntitlementCanEdit, "zone")}, } // API endpoints. @@ -145,9 +147,18 @@ func networkZonesGet(d *Daemon, r *http.Request) response.Response { return response.InternalError(err) } + userHasPermission, err := s.Authorizer.GetPermissionChecker(r.Context(), r, auth.EntitlementCanView, auth.ObjectTypeNetworkZone) + if err != nil { + return response.InternalError(err) + } + resultString := []string{} resultMap := []api.NetworkZone{} for _, zoneName := range zoneNames { + if !userHasPermission(auth.ObjectNetworkZone(projectName, zoneName)) { + continue + } + if !recursion { resultString = append(resultString, api.NewURL().Path(version.APIVersion, "network-zones", zoneName).String()) } else { @@ -234,6 +245,11 @@ func networkZonesPost(d *Daemon, r *http.Request) response.Response { return response.BadRequest(err) } + err = s.Authorizer.AddNetworkZone(r.Context(), projectName, req.Name) + if err != nil { + logger.Error("Failed to add network zone to authorizer", logger.Ctx{"name": req.Name, "project": projectName, "error": err}) + } + lc := lifecycle.NetworkZoneCreated.Event(netzone, request.CreateRequestor(r), nil) s.Events.SendLifecycle(projectName, lc) @@ -287,6 +303,11 @@ func networkZoneDelete(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } + err = s.Authorizer.DeleteNetworkZone(r.Context(), projectName, zoneName) + if err != nil { + logger.Error("Failed to remove network zone from authorizer", logger.Ctx{"name": zoneName, "project": projectName, "error": err}) + } + s.Events.SendLifecycle(projectName, lifecycle.NetworkZoneDeleted.Event(netzone, request.CreateRequestor(r), nil)) return response.EmptySyncResponse diff --git a/lxd/network_zones_records.go b/lxd/network_zones_records.go index 4fbed979cd5c..c3e45f6bc8d6 100644 --- a/lxd/network_zones_records.go +++ b/lxd/network_zones_records.go @@ -7,6 +7,7 @@ import ( "github.com/gorilla/mux" + "github.com/canonical/lxd/lxd/auth" clusterRequest "github.com/canonical/lxd/lxd/cluster/request" "github.com/canonical/lxd/lxd/lifecycle" "github.com/canonical/lxd/lxd/network/zone" @@ -21,17 +22,17 @@ import ( var networkZoneRecordsCmd = APIEndpoint{ Path: "network-zones/{zone}/records", - Get: APIEndpointAction{Handler: networkZoneRecordsGet, AccessHandler: allowProjectPermission("networks", "view")}, - Post: APIEndpointAction{Handler: networkZoneRecordsPost, AccessHandler: allowProjectPermission("networks", "manage-networks")}, + Get: APIEndpointAction{Handler: networkZoneRecordsGet, AccessHandler: allowPermission(auth.ObjectTypeNetworkZone, auth.EntitlementCanView, "zone")}, + Post: APIEndpointAction{Handler: networkZoneRecordsPost, AccessHandler: allowPermission(auth.ObjectTypeNetworkZone, auth.EntitlementCanEdit, "zone")}, } var networkZoneRecordCmd = APIEndpoint{ Path: "network-zones/{zone}/records/{name}", - Delete: APIEndpointAction{Handler: networkZoneRecordDelete, AccessHandler: allowProjectPermission("networks", "manage-networks")}, - Get: APIEndpointAction{Handler: networkZoneRecordGet, AccessHandler: allowProjectPermission("networks", "view")}, - Put: APIEndpointAction{Handler: networkZoneRecordPut, AccessHandler: allowProjectPermission("networks", "manage-networks")}, - Patch: APIEndpointAction{Handler: networkZoneRecordPut, AccessHandler: allowProjectPermission("networks", "manage-networks")}, + Delete: APIEndpointAction{Handler: networkZoneRecordDelete, AccessHandler: allowPermission(auth.ObjectTypeNetworkZone, auth.EntitlementCanEdit, "zone")}, + Get: APIEndpointAction{Handler: networkZoneRecordGet, AccessHandler: allowPermission(auth.ObjectTypeNetworkZone, auth.EntitlementCanView, "zone")}, + Put: APIEndpointAction{Handler: networkZoneRecordPut, AccessHandler: allowPermission(auth.ObjectTypeNetworkZone, auth.EntitlementCanEdit, "zone")}, + Patch: APIEndpointAction{Handler: networkZoneRecordPut, AccessHandler: allowPermission(auth.ObjectTypeNetworkZone, auth.EntitlementCanEdit, "zone")}, } // API endpoints. diff --git a/lxd/networks.go b/lxd/networks.go index b6e50399e839..70f15630df3c 100644 --- a/lxd/networks.go +++ b/lxd/networks.go @@ -16,6 +16,7 @@ import ( "github.com/gorilla/mux" "github.com/canonical/lxd/client" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/cluster" clusterRequest "github.com/canonical/lxd/lxd/cluster/request" "github.com/canonical/lxd/lxd/db" @@ -46,30 +47,30 @@ var networkCreateLock sync.Mutex var networksCmd = APIEndpoint{ Path: "networks", - Get: APIEndpointAction{Handler: networksGet, AccessHandler: allowProjectPermission("networks", "view")}, - Post: APIEndpointAction{Handler: networksPost, AccessHandler: allowProjectPermission("networks", "manage-networks")}, + Get: APIEndpointAction{Handler: networksGet, AccessHandler: allowAuthenticated}, + Post: APIEndpointAction{Handler: networksPost, AccessHandler: allowPermission(auth.ObjectTypeProject, auth.EntitlementCanCreateNetworks)}, } var networkCmd = APIEndpoint{ Path: "networks/{networkName}", - Delete: APIEndpointAction{Handler: networkDelete, AccessHandler: allowProjectPermission("networks", "manage-networks")}, - Get: APIEndpointAction{Handler: networkGet, AccessHandler: allowProjectPermission("networks", "view")}, - Patch: APIEndpointAction{Handler: networkPatch, AccessHandler: allowProjectPermission("networks", "manage-networks")}, - Post: APIEndpointAction{Handler: networkPost, AccessHandler: allowProjectPermission("networks", "manage-networks")}, - Put: APIEndpointAction{Handler: networkPut, AccessHandler: allowProjectPermission("networks", "manage-networks")}, + Delete: APIEndpointAction{Handler: networkDelete, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanEdit, "networkName")}, + Get: APIEndpointAction{Handler: networkGet, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanView, "networkName")}, + Patch: APIEndpointAction{Handler: networkPatch, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanEdit, "networkName")}, + Post: APIEndpointAction{Handler: networkPost, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanEdit, "networkName")}, + Put: APIEndpointAction{Handler: networkPut, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanEdit, "networkName")}, } var networkLeasesCmd = APIEndpoint{ Path: "networks/{networkName}/leases", - Get: APIEndpointAction{Handler: networkLeasesGet, AccessHandler: allowProjectPermission("networks", "view")}, + Get: APIEndpointAction{Handler: networkLeasesGet, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanView, "networkName")}, } var networkStateCmd = APIEndpoint{ Path: "networks/{networkName}/state", - Get: APIEndpointAction{Handler: networkStateGet, AccessHandler: allowProjectPermission("networks", "view")}, + Get: APIEndpointAction{Handler: networkStateGet, AccessHandler: allowPermission(auth.ObjectTypeNetwork, auth.EntitlementCanView, "networkName")}, } // API endpoints @@ -207,9 +208,18 @@ func networksGet(d *Daemon, r *http.Request) response.Response { } } + userHasPermission, err := s.Authorizer.GetPermissionChecker(r.Context(), r, auth.EntitlementCanView, auth.ObjectTypeNetwork) + if err != nil { + return response.InternalError(err) + } + resultString := []string{} resultMap := []api.Network{} for _, networkName := range networkNames { + if !userHasPermission(auth.ObjectNetwork(projectName, networkName)) { + continue + } + if !recursion { resultString = append(resultString, fmt.Sprintf("/%s/networks/%s", version.APIVersion, networkName)) } else { @@ -473,6 +483,11 @@ func networksPost(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } + err = s.Authorizer.AddNetwork(r.Context(), projectName, req.Name) + if err != nil { + logger.Error("Failed to add network to authorizer", logger.Ctx{"name": req.Name, "project": projectName, "error": err}) + } + requestor := request.CreateRequestor(r) s.Events.SendLifecycle(projectName, lifecycle.NetworkCreated.Event(n, requestor, nil)) @@ -835,9 +850,12 @@ func doNetworkGet(s *state.State, r *http.Request, allNodes bool, projectName st apiNet.Description = n.Description() apiNet.Type = n.Type() - if s.Authorizer.UserIsAdmin(r) || s.Authorizer.UserHasPermission(r, projectName, "manage-networks") { + err = s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectNetwork(projectName, networkName), auth.EntitlementCanEdit) + if err == nil { // Only allow admins to see network config as sensitive info can be stored there. apiNet.Config = n.Config() + } else if !api.StatusErrorCheck(err, http.StatusForbidden) { + return api.Network{}, err } // If no member is specified, we omit the node-specific fields. @@ -996,6 +1014,11 @@ func networkDelete(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } + err = s.Authorizer.DeleteNetwork(r.Context(), projectName, networkName) + if err != nil { + logger.Error("Failed to remove network from authorizer", logger.Ctx{"name": networkName, "project": projectName, "error": err}) + } + requestor := request.CreateRequestor(r) s.Events.SendLifecycle(projectName, lifecycle.NetworkDeleted.Event(n, requestor, nil)) @@ -1122,6 +1145,11 @@ func networkPost(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } + err = s.Authorizer.RenameNetwork(r.Context(), projectName, networkName, req.Name) + if err != nil { + logger.Error("Failed to rename network in authorizer", logger.Ctx{"old_name": networkName, "new_name": req.Name, "project": projectName, "error": err}) + } + requestor := request.CreateRequestor(r) lc := lifecycle.NetworkRenamed.Event(n, requestor, map[string]any{"old_name": networkName}) s.Events.SendLifecycle(projectName, lc) diff --git a/lxd/operations.go b/lxd/operations.go index 363b8063c8ec..86316d6bfee2 100644 --- a/lxd/operations.go +++ b/lxd/operations.go @@ -11,6 +11,7 @@ import ( "github.com/gorilla/mux" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/cluster" "github.com/canonical/lxd/lxd/db" dbCluster "github.com/canonical/lxd/lxd/db/cluster" @@ -251,13 +252,29 @@ func operationDelete(d *Daemon, r *http.Request) response.Response { op, err := operations.OperationGetInternal(id) if err == nil { projectName := op.Project() - if op.Permission() != "" { - if projectName == "" { - projectName = api.ProjectDefaultName - } + if projectName == "" { + projectName = api.ProjectDefaultName + } - if !s.Authorizer.UserHasPermission(r, projectName, op.Permission()) { - return response.Forbidden(nil) + objectType, entitlement := op.Permission() + if objectType != "" { + for _, v := range op.Resources() { + for _, u := range v { + _, _, pathArgs, err := dbCluster.URLToEntityType(u.String()) + if err != nil { + return response.InternalError(fmt.Errorf("Unable to parse operation resource URL: %w", err)) + } + + object, err := auth.NewObject(objectType, projectName, pathArgs...) + if err != nil { + return response.InternalError(fmt.Errorf("Unable to create authorization object for operation: %w", err)) + } + + err = s.Authorizer.CheckPermission(r.Context(), r, object, entitlement) + if err != nil { + return response.SmartError(err) + } + } } } @@ -480,6 +497,11 @@ func operationsGet(d *Daemon, r *http.Request) response.Response { projectName = api.ProjectDefaultName } + userHasPermission, err := s.Authorizer.GetPermissionChecker(r.Context(), r, auth.EntitlementCanViewOperations, auth.ObjectTypeProject) + if err != nil { + return response.InternalError(fmt.Errorf("Failed to get operation permission checker: %w", err)) + } + localOperationURLs := func() (shared.Jmap, error) { // Get all the operations. localOps := operations.Clone() @@ -492,6 +514,10 @@ func operationsGet(d *Daemon, r *http.Request) response.Response { continue } + if !userHasPermission(auth.ObjectProject(v.Project())) { + continue + } + status := strings.ToLower(v.Status().String()) _, ok := body[status] if !ok { @@ -516,6 +542,10 @@ func operationsGet(d *Daemon, r *http.Request) response.Response { continue } + if !userHasPermission(auth.ObjectProject(v.Project())) { + continue + } + status := strings.ToLower(v.Status().String()) _, ok := body[status] if !ok { @@ -557,7 +587,6 @@ func operationsGet(d *Daemon, r *http.Request) response.Response { // Start with local operations. var md shared.Jmap - var err error if recursion { md, err = localOperations() diff --git a/lxd/operations/operations.go b/lxd/operations/operations.go index a2b78d39c8f1..7000247903ff 100644 --- a/lxd/operations/operations.go +++ b/lxd/operations/operations.go @@ -9,6 +9,7 @@ import ( "github.com/pborman/uuid" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/db/operationtype" "github.com/canonical/lxd/lxd/events" "github.com/canonical/lxd/lxd/request" @@ -103,7 +104,8 @@ type Operation struct { readonly bool canceler *cancel.HTTPRequestCanceller description string - permission string + objectType auth.ObjectType + entitlement auth.Entitlement dbOpType operationtype.Type requestor *api.EventLifecycleRequestor logger logger.Logger @@ -136,7 +138,7 @@ func OperationCreate(s *state.State, projectName string, opClass OperationClass, op.projectName = projectName op.id = uuid.New() op.description = opType.Description() - op.permission = opType.Permission() + op.objectType, op.entitlement = opType.Permission() op.dbOpType = opType op.class = opClass op.createdAt = time.Now() @@ -659,9 +661,9 @@ func (op *Operation) SetCanceler(canceler *cancel.HTTPRequestCanceller) { op.canceler = canceler } -// Permission returns the operation permission. -func (op *Operation) Permission() string { - return op.permission +// Permission returns the operations auth.ObjectType and auth.Entitlement. +func (op *Operation) Permission() (auth.ObjectType, auth.Entitlement) { + return op.objectType, op.entitlement } // Project returns the operation project. diff --git a/lxd/profiles.go b/lxd/profiles.go index 2606df1a8488..98a1ea0856c0 100644 --- a/lxd/profiles.go +++ b/lxd/profiles.go @@ -14,6 +14,7 @@ import ( "github.com/gorilla/mux" "github.com/canonical/lxd/client" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/cluster" "github.com/canonical/lxd/lxd/db" dbCluster "github.com/canonical/lxd/lxd/db/cluster" @@ -34,18 +35,18 @@ import ( var profilesCmd = APIEndpoint{ Path: "profiles", - Get: APIEndpointAction{Handler: profilesGet, AccessHandler: allowProjectPermission("profiles", "view")}, - Post: APIEndpointAction{Handler: profilesPost, AccessHandler: allowProjectPermission("profiles", "manage-profiles")}, + Get: APIEndpointAction{Handler: profilesGet, AccessHandler: allowAuthenticated}, + Post: APIEndpointAction{Handler: profilesPost, AccessHandler: allowPermission(auth.ObjectTypeProject, auth.EntitlementCanCreateProfiles)}, } var profileCmd = APIEndpoint{ Path: "profiles/{name}", - Delete: APIEndpointAction{Handler: profileDelete, AccessHandler: allowProjectPermission("profiles", "manage-profiles")}, - Get: APIEndpointAction{Handler: profileGet, AccessHandler: allowProjectPermission("profiles", "view")}, - Patch: APIEndpointAction{Handler: profilePatch, AccessHandler: allowProjectPermission("profiles", "manage-profiles")}, - Post: APIEndpointAction{Handler: profilePost, AccessHandler: allowProjectPermission("profiles", "manage-profiles")}, - Put: APIEndpointAction{Handler: profilePut, AccessHandler: allowProjectPermission("profiles", "manage-profiles")}, + Delete: APIEndpointAction{Handler: profileDelete, AccessHandler: allowPermission(auth.ObjectTypeProfile, auth.EntitlementCanEdit, "name")}, + Get: APIEndpointAction{Handler: profileGet, AccessHandler: allowPermission(auth.ObjectTypeProfile, auth.EntitlementCanView, "name")}, + Patch: APIEndpointAction{Handler: profilePatch, AccessHandler: allowPermission(auth.ObjectTypeProfile, auth.EntitlementCanEdit, "name")}, + Post: APIEndpointAction{Handler: profilePost, AccessHandler: allowPermission(auth.ObjectTypeProfile, auth.EntitlementCanEdit, "name")}, + Put: APIEndpointAction{Handler: profilePut, AccessHandler: allowPermission(auth.ObjectTypeProfile, auth.EntitlementCanEdit, "name")}, } // swagger:operation GET /1.0/profiles profiles profiles_get @@ -150,6 +151,11 @@ func profilesGet(d *Daemon, r *http.Request) response.Response { recursion := util.IsRecursionRequest(r) + userHasPermission, err := s.Authorizer.GetPermissionChecker(r.Context(), r, auth.EntitlementCanView, auth.ObjectTypeProfile) + if err != nil { + return response.InternalError(err) + } + var result any err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { filter := dbCluster.ProfileFilter{ @@ -161,19 +167,24 @@ func profilesGet(d *Daemon, r *http.Request) response.Response { return err } - apiProfiles := make([]*api.Profile, len(profiles)) - for i, profile := range profiles { - apiProfiles[i], err = profile.ToAPI(ctx, tx.Tx()) + apiProfiles := make([]*api.Profile, 0, len(profiles)) + for _, profile := range profiles { + if !userHasPermission(auth.ObjectProfile(p.Name, profile.Name)) { + continue + } + + apiProfile, err := profile.ToAPI(ctx, tx.Tx()) if err != nil { return err } - apiProfiles[i].UsedBy, err = profileUsedBy(ctx, tx, profile) + apiProfile.UsedBy, err = profileUsedBy(ctx, tx, profile) if err != nil { return err } - apiProfiles[i].UsedBy = project.FilterUsedBy(s.Authorizer, r, apiProfiles[i].UsedBy) + apiProfile.UsedBy = project.FilterUsedBy(s.Authorizer, r, apiProfile.UsedBy) + apiProfiles = append(apiProfiles, apiProfile) } if recursion { @@ -321,6 +332,11 @@ func profilesPost(d *Daemon, r *http.Request) response.Response { return response.SmartError(fmt.Errorf("Error inserting %q into database: %w", req.Name, err)) } + err = s.Authorizer.AddProfile(r.Context(), p.Name, req.Name) + if err != nil { + logger.Error("Failed to add profile to authorizer", logger.Ctx{"name": req.Name, "project": p.Name, "error": err}) + } + requestor := request.CreateRequestor(r) lc := lifecycle.ProfileCreated.Event(req.Name, p.Name, requestor, nil) s.Events.SendLifecycle(p.Name, lc) @@ -742,6 +758,11 @@ func profilePost(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } + err = s.Authorizer.RenameProfile(r.Context(), p.Name, name, req.Name) + if err != nil { + logger.Error("Failed to rename profile in authorizer", logger.Ctx{"old_name": name, "new_name": req.Name, "project": p.Name, "error": 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) @@ -811,6 +832,11 @@ func profileDelete(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } + err = s.Authorizer.DeleteProfile(r.Context(), p.Name, name) + if err != nil { + logger.Error("Failed to remove profile from authorizer", logger.Ctx{"name": name, "project": p.Name, "error": err}) + } + requestor := request.CreateRequestor(r) s.Events.SendLifecycle(p.Name, lifecycle.ProfileDeleted.Event(name, p.Name, requestor, nil)) diff --git a/lxd/project/permissions.go b/lxd/project/permissions.go index 9b0c6998f08b..0dff87067036 100644 --- a/lxd/project/permissions.go +++ b/lxd/project/permissions.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/http" - "net/url" "path/filepath" "strconv" "strings" @@ -1419,30 +1418,36 @@ var aggregateLimitConfigValuePrinters = map[string]func(int64) string{ // FilterUsedBy filters a UsedBy list based on project access. func FilterUsedBy(authorizer auth.Authorizer, r *http.Request, entries []string) []string { - // Shortcut for admins and non-RBAC environments. - if authorizer.UserIsAdmin(r) { - return entries - } - // Filter the entries. usedBy := []string{} for _, entry := range entries { - projectName := api.ProjectDefaultName - - // Try to parse the query part of the URL. - u, err := url.Parse(entry) + entityType, projectName, pathArgs, err := cluster.URLToEntityType(entry) if err != nil { - // Skip URLs we can't parse. continue } - // Check if project= is specified in the URL. - val := u.Query().Get("project") - if val != "" { - projectName = val + var object auth.Object + switch entityType { + case cluster.TypeImage: + object = auth.ObjectImage(projectName, pathArgs[0]) + case cluster.TypeInstance: + object = auth.ObjectInstance(projectName, pathArgs[0]) + case cluster.TypeNetwork: + object = auth.ObjectNetwork(projectName, pathArgs[0]) + case cluster.TypeProfile: + object = auth.ObjectProfile(projectName, pathArgs[0]) + case cluster.TypeStoragePool: + object = auth.ObjectStoragePool(pathArgs[0]) + case cluster.TypeStorageVolume: + object = auth.ObjectStorageVolume(projectName, pathArgs[0], pathArgs[1], pathArgs[2]) + case cluster.TypeStorageBucket: + object = auth.ObjectStorageBucket(projectName, pathArgs[0], pathArgs[1]) + default: + continue } - if !authorizer.UserHasPermission(r, projectName, "view") { + err = authorizer.CheckPermission(r.Context(), r, object, auth.EntitlementCanView) + if err != nil { continue } @@ -1472,13 +1477,14 @@ func projectHasRestriction(project *api.Project, restrictionKey string, blockVal // CheckClusterTargetRestriction check if user is allowed to use cluster member targeting. func CheckClusterTargetRestriction(authorizer auth.Authorizer, r *http.Request, project *api.Project, targetFlag string) error { - // Allow server administrators to move instances around even when restricted (node evacuation, ...) - if authorizer.UserIsAdmin(r) { - return nil - } - if projectHasRestriction(project, "restricted.cluster.target", "block") && targetFlag != "" { - return fmt.Errorf("This project doesn't allow cluster member targeting") + // Allow server administrators to move instances around even when restricted (node evacuation, ...) + err := authorizer.CheckPermission(r.Context(), r, auth.ObjectServer(), auth.EntitlementCanOverrideClusterTargetRestriction) + if err != nil && api.StatusErrorCheck(err, http.StatusForbidden) { + return api.StatusErrorf(http.StatusForbidden, "This project doesn't allow cluster member targeting") + } else if err != nil { + return err + } } return nil diff --git a/lxd/project/permissions_test.go b/lxd/project/permissions_test.go index 3c1d0c6ea1d6..d83a3429f77e 100644 --- a/lxd/project/permissions_test.go +++ b/lxd/project/permissions_test.go @@ -2,6 +2,7 @@ package project_test import ( "context" + "crypto/x509" "net/http" "testing" @@ -9,11 +10,14 @@ import ( "github.com/stretchr/testify/require" "github.com/canonical/lxd/lxd/auth" + "github.com/canonical/lxd/lxd/certificate" "github.com/canonical/lxd/lxd/db" "github.com/canonical/lxd/lxd/db/cluster" "github.com/canonical/lxd/lxd/instance/instancetype" "github.com/canonical/lxd/lxd/project" + "github.com/canonical/lxd/lxd/request" "github.com/canonical/lxd/shared/api" + "github.com/canonical/lxd/shared/logger" ) // If there's no limit configured on the project, the check passes. @@ -172,13 +176,54 @@ func TestCheckClusterTargetRestriction_RestrictedTrue(t *testing.T) { require.NoError(t, err) req := &http.Request{} - authorizer, err := auth.LoadAuthorizer("tls", nil, nil, nil) + authorizer, err := auth.LoadAuthorizer(context.Background(), auth.DriverTLS, logger.Log, &certificate.Cache{}) require.NoError(t, err) err = project.CheckClusterTargetRestriction(authorizer, req, p, "n1") assert.EqualError(t, err, "This project doesn't allow cluster member targeting") } +// If a direct targeting is blocked but the user can override it, the check passes. +func TestCheckClusterTargetRestriction_RestrictedTrueWithOverride(t *testing.T) { + tx, cleanup := db.NewTestClusterTx(t) + defer cleanup() + + ctx := context.Background() + id, err := cluster.CreateProject(ctx, tx.Tx(), cluster.Project{Name: "p1"}) + require.NoError(t, err) + + err = cluster.CreateProjectConfig(ctx, tx.Tx(), id, map[string]string{"restricted": "true", "restricted.cluster.target": "block"}) + require.NoError(t, err) + + dbProject, err := cluster.GetProject(ctx, tx.Tx(), "p1") + require.NoError(t, err) + + p, err := dbProject.ToAPI(ctx, tx.Tx()) + require.NoError(t, err) + + req := &http.Request{ + URL: &api.NewURL().Path("1.0", "instances").WithQuery("target", "node01").URL, + } + + req = req.WithContext(context.WithValue(req.Context(), request.CtxProtocol, "tls")) + req = req.WithContext(context.WithValue(req.Context(), request.CtxUsername, "my-certificate-fingerprint")) + certificateCache := &certificate.Cache{} + + // Setting the certificate and not projects means the certificate is not restricted and therefore the user is an + // admin that can override the cluster targer restriction. + certificateCache.SetCertificates(map[certificate.Type]map[string]x509.Certificate{ + certificate.TypeClient: { + "my-certificate-fingerprint": x509.Certificate{}, + }, + }) + + authorizer, err := auth.LoadAuthorizer(context.Background(), auth.DriverTLS, logger.Log, certificateCache) + require.NoError(t, err) + + err = project.CheckClusterTargetRestriction(authorizer, req, p, "n1") + assert.Nil(t, err) +} + // If a direct targeting is allowed, the check passes. func TestCheckClusterTargetRestriction_RestrictedFalse(t *testing.T) { tx, cleanup := db.NewTestClusterTx(t) @@ -198,7 +243,7 @@ func TestCheckClusterTargetRestriction_RestrictedFalse(t *testing.T) { require.NoError(t, err) req := &http.Request{} - authorizer, err := auth.LoadAuthorizer("tls", nil, nil, nil) + authorizer, err := auth.LoadAuthorizer(context.Background(), auth.DriverTLS, logger.Log, &certificate.Cache{}) require.NoError(t, err) err = project.CheckClusterTargetRestriction(authorizer, req, p, "n1") diff --git a/lxd/resources.go b/lxd/resources.go index f7a91e312e2b..197b0c910eb8 100644 --- a/lxd/resources.go +++ b/lxd/resources.go @@ -6,6 +6,7 @@ import ( "github.com/gorilla/mux" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/resources" "github.com/canonical/lxd/lxd/response" storagePools "github.com/canonical/lxd/lxd/storage" @@ -15,13 +16,13 @@ import ( var api10ResourcesCmd = APIEndpoint{ Path: "resources", - Get: APIEndpointAction{Handler: api10ResourcesGet, AccessHandler: allowAuthenticated}, + Get: APIEndpointAction{Handler: api10ResourcesGet, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanViewResources)}, } var storagePoolResourcesCmd = APIEndpoint{ Path: "storage-pools/{name}/resources", - Get: APIEndpointAction{Handler: storagePoolResourcesGet, AccessHandler: allowAuthenticated}, + Get: APIEndpointAction{Handler: storagePoolResourcesGet, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanViewResources)}, } // swagger:operation GET /1.0/resources server resources_get diff --git a/lxd/storage/backend_lxd.go b/lxd/storage/backend_lxd.go index acfe197bb0e9..9a0e510b237a 100644 --- a/lxd/storage/backend_lxd.go +++ b/lxd/storage/backend_lxd.go @@ -4432,6 +4432,11 @@ func (b *lxdBackend) CreateCustomVolume(projectName string, volName string, desc eventCtx["location"] = b.state.ServerName } + err = b.state.Authorizer.AddStoragePoolVolume(b.state.ShutdownCtx, projectName, b.Name(), string(vol.Type()), volName) + if err != nil { + logger.Error("Failed to add storage volume to authorizer", logger.Ctx{"name": volName, "type": vol.Type(), "pool": b.Name(), "project": projectName, "error": err}) + } + b.state.Events.SendLifecycle(projectName, lifecycle.StorageVolumeCreated.Event(vol, string(vol.Type()), projectName, op, eventCtx)) revert.Success() @@ -4565,6 +4570,11 @@ func (b *lxdBackend) CreateCustomVolumeFromCopy(projectName string, srcProjectNa eventCtx["location"] = b.state.ServerName } + err = b.state.Authorizer.AddStoragePoolVolume(b.state.ShutdownCtx, projectName, b.Name(), string(vol.Type()), volName) + if err != nil { + logger.Error("Failed to add storage volume to authorizer", logger.Ctx{"name": volName, "type": vol.Type(), "pool": b.Name(), "project": projectName, "error": err}) + } + b.state.Events.SendLifecycle(projectName, lifecycle.StorageVolumeCreated.Event(vol, string(vol.Type()), projectName, op, eventCtx)) revert.Success() @@ -4963,6 +4973,11 @@ func (b *lxdBackend) CreateCustomVolumeFromMigration(projectName string, conn io eventCtx["location"] = b.state.ServerName } + err = b.state.Authorizer.AddStoragePoolVolume(b.state.ShutdownCtx, projectName, b.Name(), string(vol.Type()), args.Name) + if err != nil { + logger.Error("Failed to add storage volume to authorizer", logger.Ctx{"name": args.Name, "type": vol.Type(), "pool": b.Name(), "project": projectName, "error": err}) + } + b.state.Events.SendLifecycle(projectName, lifecycle.StorageVolumeCreated.Event(vol, string(vol.Type()), projectName, op, eventCtx)) revert.Success() @@ -5051,6 +5066,11 @@ func (b *lxdBackend) RenameCustomVolume(projectName string, volName string, newV return err } + err = b.state.Authorizer.RenameStoragePoolVolume(b.state.ShutdownCtx, projectName, b.Name(), string(vol.Type()), volName, newVolStorageName) + if err != nil { + logger.Error("Failed to rename storage volume in authorizer", logger.Ctx{"old_name": volName, "new_name": newVolStorageName, "type": vol.Type(), "pool": b.Name(), "project": projectName, "error": err}) + } + vol = b.GetVolume(drivers.VolumeTypeCustom, drivers.ContentType(volume.ContentType), newVolStorageName, nil) b.state.Events.SendLifecycle(projectName, lifecycle.StorageVolumeRenamed.Event(vol, string(vol.Type()), projectName, op, logger.Ctx{"old_name": volName})) @@ -5305,6 +5325,11 @@ func (b *lxdBackend) DeleteCustomVolume(projectName string, volName string, op * return err } + err = b.state.Authorizer.DeleteStoragePoolVolume(b.state.ShutdownCtx, projectName, b.Name(), string(vol.Type()), volName) + if err != nil { + logger.Error("Failed to remove storage volume from authorizer", logger.Ctx{"name": volName, "type": vol.Type(), "pool": b.Name(), "project": projectName, "error": err}) + } + b.state.Events.SendLifecycle(projectName, lifecycle.StorageVolumeDeleted.Event(vol, string(vol.Type()), projectName, op, nil)) return nil @@ -6727,6 +6752,11 @@ func (b *lxdBackend) CreateCustomVolumeFromISO(projectName string, volName strin eventCtx["location"] = b.state.ServerName } + err = b.state.Authorizer.AddStoragePoolVolume(b.state.ShutdownCtx, projectName, b.Name(), string(vol.Type()), volName) + if err != nil { + logger.Error("Failed to add storage volume to authorizer", logger.Ctx{"name": volName, "type": vol.Type(), "pool": b.Name(), "project": projectName, "error": err}) + } + b.state.Events.SendLifecycle(projectName, lifecycle.StorageVolumeCreated.Event(vol, string(vol.Type()), projectName, op, eventCtx)) revert.Success() @@ -6825,6 +6855,11 @@ func (b *lxdBackend) CreateCustomVolumeFromBackup(srcBackup backup.Info, srcData eventCtx["location"] = b.state.ServerName } + err = b.state.Authorizer.AddStoragePoolVolume(b.state.ShutdownCtx, srcBackup.Project, b.Name(), string(vol.Type()), srcBackup.Name) + if err != nil { + logger.Error("Failed to add storage volume to authorizer", logger.Ctx{"name": srcBackup.Name, "type": vol.Type(), "pool": b.Name(), "project": srcBackup.Project, "error": err}) + } + b.state.Events.SendLifecycle(srcBackup.Project, lifecycle.StorageVolumeCreated.Event(vol, string(vol.Type()), srcBackup.Project, op, eventCtx)) revert.Success() diff --git a/lxd/storage_buckets.go b/lxd/storage_buckets.go index f30020d4d6a9..d4cb88035647 100644 --- a/lxd/storage_buckets.go +++ b/lxd/storage_buckets.go @@ -10,6 +10,7 @@ import ( "github.com/gorilla/mux" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/db" "github.com/canonical/lxd/lxd/lifecycle" "github.com/canonical/lxd/lxd/project" @@ -19,38 +20,39 @@ import ( storagePools "github.com/canonical/lxd/lxd/storage" "github.com/canonical/lxd/lxd/util" "github.com/canonical/lxd/shared/api" + "github.com/canonical/lxd/shared/logger" "github.com/canonical/lxd/shared/version" ) var storagePoolBucketsCmd = APIEndpoint{ Path: "storage-pools/{poolName}/buckets", - Get: APIEndpointAction{Handler: storagePoolBucketsGet, AccessHandler: allowProjectPermission("storage-volumes", "view")}, - Post: APIEndpointAction{Handler: storagePoolBucketsPost, AccessHandler: allowProjectPermission("storage-volumes", "manage-storage-volumes")}, + Get: APIEndpointAction{Handler: storagePoolBucketsGet, AccessHandler: allowAuthenticated}, + Post: APIEndpointAction{Handler: storagePoolBucketsPost, AccessHandler: allowPermission(auth.ObjectTypeProject, auth.EntitlementCanCreateStorageBuckets)}, } var storagePoolBucketCmd = APIEndpoint{ Path: "storage-pools/{poolName}/buckets/{bucketName}", - Delete: APIEndpointAction{Handler: storagePoolBucketDelete, AccessHandler: allowProjectPermission("storage-volumes", "manage-storage-volumes")}, - Get: APIEndpointAction{Handler: storagePoolBucketGet, AccessHandler: allowProjectPermission("storage-volumes", "view")}, - Patch: APIEndpointAction{Handler: storagePoolBucketPut, AccessHandler: allowProjectPermission("storage-volumes", "manage-storage-volumes")}, - Put: APIEndpointAction{Handler: storagePoolBucketPut, AccessHandler: allowProjectPermission("storage-volumes", "manage-storage-volumes")}, + Delete: APIEndpointAction{Handler: storagePoolBucketDelete, AccessHandler: allowPermission(auth.ObjectTypeStorageBucket, auth.EntitlementCanEdit, "poolName", "bucketName")}, + Get: APIEndpointAction{Handler: storagePoolBucketGet, AccessHandler: allowPermission(auth.ObjectTypeStorageBucket, auth.EntitlementCanView, "poolName", "bucketName")}, + Patch: APIEndpointAction{Handler: storagePoolBucketPut, AccessHandler: allowPermission(auth.ObjectTypeStorageBucket, auth.EntitlementCanEdit, "poolName", "bucketName")}, + Put: APIEndpointAction{Handler: storagePoolBucketPut, AccessHandler: allowPermission(auth.ObjectTypeStorageBucket, auth.EntitlementCanEdit, "poolName", "bucketName")}, } var storagePoolBucketKeysCmd = APIEndpoint{ Path: "storage-pools/{poolName}/buckets/{bucketName}/keys", - Get: APIEndpointAction{Handler: storagePoolBucketKeysGet, AccessHandler: allowProjectPermission("storage-volumes", "view")}, - Post: APIEndpointAction{Handler: storagePoolBucketKeysPost, AccessHandler: allowProjectPermission("storage-volumes", "manage-storage-volumes")}, + Get: APIEndpointAction{Handler: storagePoolBucketKeysGet, AccessHandler: allowPermission(auth.ObjectTypeStorageBucket, auth.EntitlementCanView, "poolName", "bucketName")}, + Post: APIEndpointAction{Handler: storagePoolBucketKeysPost, AccessHandler: allowPermission(auth.ObjectTypeStorageBucket, auth.EntitlementCanEdit, "poolName", "bucketName")}, } var storagePoolBucketKeyCmd = APIEndpoint{ Path: "storage-pools/{poolName}/buckets/{bucketName}/keys/{keyName}", - Delete: APIEndpointAction{Handler: storagePoolBucketKeyDelete, AccessHandler: allowProjectPermission("storage-volumes", "manage-storage-volumes")}, - Get: APIEndpointAction{Handler: storagePoolBucketKeyGet, AccessHandler: allowProjectPermission("storage-volumes", "view")}, - Put: APIEndpointAction{Handler: storagePoolBucketKeyPut, AccessHandler: allowProjectPermission("storage-volumes", "manage-storage-volumes")}, + Delete: APIEndpointAction{Handler: storagePoolBucketKeyDelete, AccessHandler: allowPermission(auth.ObjectTypeStorageBucket, auth.EntitlementCanEdit, "poolName", "bucketName")}, + Get: APIEndpointAction{Handler: storagePoolBucketKeyGet, AccessHandler: allowPermission(auth.ObjectTypeStorageBucket, auth.EntitlementCanView, "poolName", "bucketName")}, + Put: APIEndpointAction{Handler: storagePoolBucketKeyPut, AccessHandler: allowPermission(auth.ObjectTypeStorageBucket, auth.EntitlementCanEdit, "poolName", "bucketName")}, } // API endpoints @@ -193,17 +195,32 @@ func storagePoolBucketsGet(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } + userHasPermission, err := s.Authorizer.GetPermissionChecker(r.Context(), r, auth.EntitlementCanView, auth.ObjectTypeStorageBucket) + if err != nil { + return response.SmartError(err) + } + + var filteredDBBuckets []*db.StorageBucket + + for _, bucket := range dbBuckets { + if !userHasPermission(auth.ObjectStorageBucket(requestProjectName, poolName, bucket.Name)) { + continue + } + + filteredDBBuckets = append(filteredDBBuckets, bucket) + } + // Sort by bucket name. - sort.SliceStable(dbBuckets, func(i, j int) bool { - bucketA := dbBuckets[i] - bucketB := dbBuckets[j] + sort.SliceStable(filteredDBBuckets, func(i, j int) bool { + bucketA := filteredDBBuckets[i] + bucketB := filteredDBBuckets[j] return bucketA.Name < bucketB.Name }) if util.IsRecursionRequest(r) { - buckets := make([]*api.StorageBucket, 0, len(dbBuckets)) - for _, dbBucket := range dbBuckets { + buckets := make([]*api.StorageBucket, 0, len(filteredDBBuckets)) + for _, dbBucket := range filteredDBBuckets { u := pool.GetBucketURL(dbBucket.Name) if u != nil { dbBucket.S3URL = u.String() @@ -215,8 +232,8 @@ func storagePoolBucketsGet(d *Daemon, r *http.Request) response.Response { return response.SyncResponse(true, buckets) } - urls := make([]string, 0, len(dbBuckets)) - for _, dbBucket := range dbBuckets { + urls := make([]string, 0, len(filteredDBBuckets)) + for _, dbBucket := range filteredDBBuckets { urls = append(urls, dbBucket.StorageBucket.URL(version.APIVersion, poolName, requestProjectName).String()) } @@ -401,6 +418,11 @@ func storagePoolBucketsPost(d *Daemon, r *http.Request) response.Response { return response.SmartError(fmt.Errorf("Failed creating storage bucket admin key: %w", err)) } + err = s.Authorizer.AddStorageBucket(r.Context(), bucketProjectName, poolName, req.Name) + if err != nil { + logger.Error("Failed to add storage bucket to authorizer", logger.Ctx{"name": req.Name, "pool": poolName, "project": bucketProjectName, "error": err}) + } + s.Events.SendLifecycle(bucketProjectName, lifecycle.StorageBucketCreated.Event(pool, bucketProjectName, req.Name, request.CreateRequestor(r), nil)) u := api.NewURL().Path(version.APIVersion, "storage-pools", pool.Name(), "buckets", req.Name) @@ -618,6 +640,11 @@ func storagePoolBucketDelete(d *Daemon, r *http.Request) response.Response { return response.SmartError(fmt.Errorf("Failed deleting storage bucket: %w", err)) } + err = s.Authorizer.DeleteStorageBucket(r.Context(), bucketProjectName, poolName, bucketName) + if err != nil { + logger.Error("Failed to add storage bucket to authorizer", logger.Ctx{"name": bucketName, "pool": poolName, "project": bucketProjectName, "error": err}) + } + s.Events.SendLifecycle(bucketProjectName, lifecycle.StorageBucketDeleted.Event(pool, bucketProjectName, bucketName, request.CreateRequestor(r), nil)) return response.EmptySyncResponse diff --git a/lxd/storage_pools.go b/lxd/storage_pools.go index 7e526bc929db..065c78bcbd8a 100644 --- a/lxd/storage_pools.go +++ b/lxd/storage_pools.go @@ -12,6 +12,7 @@ import ( "github.com/gorilla/mux" "github.com/canonical/lxd/client" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/cluster" clusterRequest "github.com/canonical/lxd/lxd/cluster/request" "github.com/canonical/lxd/lxd/db" @@ -35,16 +36,16 @@ var storagePoolsCmd = APIEndpoint{ Path: "storage-pools", Get: APIEndpointAction{Handler: storagePoolsGet, AccessHandler: allowAuthenticated}, - Post: APIEndpointAction{Handler: storagePoolsPost}, + Post: APIEndpointAction{Handler: storagePoolsPost, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanCreateStoragePools)}, } var storagePoolCmd = APIEndpoint{ Path: "storage-pools/{poolName}", - Delete: APIEndpointAction{Handler: storagePoolDelete}, - Get: APIEndpointAction{Handler: storagePoolGet, AccessHandler: allowAuthenticated}, - Patch: APIEndpointAction{Handler: storagePoolPatch}, - Put: APIEndpointAction{Handler: storagePoolPut}, + Delete: APIEndpointAction{Handler: storagePoolDelete, AccessHandler: allowPermission(auth.ObjectTypeStoragePool, auth.EntitlementCanEdit, "poolName")}, + Get: APIEndpointAction{Handler: storagePoolGet, AccessHandler: allowPermission(auth.ObjectTypeStoragePool, auth.EntitlementCanView, "poolName")}, + Patch: APIEndpointAction{Handler: storagePoolPatch, AccessHandler: allowPermission(auth.ObjectTypeStoragePool, auth.EntitlementCanEdit, "poolName")}, + Put: APIEndpointAction{Handler: storagePoolPut, AccessHandler: allowPermission(auth.ObjectTypeStoragePool, auth.EntitlementCanEdit, "poolName")}, } // swagger:operation GET /1.0/storage-pools storage storage_pools_get @@ -154,6 +155,11 @@ func storagePoolsGet(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } + hasEditPermission, err := s.Authorizer.GetPermissionChecker(r.Context(), r, auth.EntitlementCanEdit, auth.ObjectTypeStoragePool) + if err != nil { + return response.InternalError(err) + } + resultString := []string{} resultMap := []api.StoragePool{} for _, poolName := range poolNames { @@ -174,7 +180,7 @@ func storagePoolsGet(d *Daemon, r *http.Request) response.Response { poolAPI := pool.ToAPI() poolAPI.UsedBy = project.FilterUsedBy(s.Authorizer, r, poolUsedBy) - if !s.Authorizer.UserIsAdmin(r) { + if !hasEditPermission(auth.ObjectStoragePool(poolName)) { // Don't allow non-admins to see pool config as sensitive info can be stored there. poolAPI.Config = nil } @@ -358,6 +364,12 @@ func storagePoolsPost(d *Daemon, r *http.Request) response.Response { } } + // Add the storage pool to the authorizer. + err = s.Authorizer.AddStoragePool(r.Context(), req.Name) + if err != nil { + logger.Error("Failed to add storage pool to authorizer", logger.Ctx{"name": pool.Name, "error": err}) + } + s.Events.SendLifecycle(api.ProjectDefaultName, lc) return resp @@ -609,9 +621,12 @@ func storagePoolGet(d *Daemon, r *http.Request) response.Response { poolAPI := pool.ToAPI() poolAPI.UsedBy = project.FilterUsedBy(s.Authorizer, r, poolUsedBy) - if !s.Authorizer.UserIsAdmin(r) { + err = s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectStoragePool(poolName), auth.EntitlementCanEdit) + if err != nil && api.StatusErrorCheck(err, http.StatusForbidden) { // Don't allow non-admins to see pool config as sensitive info can be stored there. poolAPI.Config = nil + } else if err != nil { + return response.SmartError(err) } // If no member is specified and the daemon is clustered, we omit the node-specific fields. @@ -999,6 +1014,12 @@ func storagePoolDelete(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } + // Remove the storage pool from the authorizer. + err = s.Authorizer.DeleteStoragePool(r.Context(), pool.Name()) + if err != nil { + logger.Error("Failed to remove storage pool from authorizer", logger.Ctx{"name": pool.Name(), "error": err}) + } + requestor := request.CreateRequestor(r) s.Events.SendLifecycle(api.ProjectDefaultName, lifecycle.StoragePoolDeleted.Event(pool.Name(), requestor, nil)) diff --git a/lxd/storage_volumes.go b/lxd/storage_volumes.go index 93ac801b7820..865897c3bce8 100644 --- a/lxd/storage_volumes.go +++ b/lxd/storage_volumes.go @@ -20,6 +20,7 @@ import ( "github.com/gorilla/websocket" "github.com/canonical/lxd/lxd/archive" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/backup" lxdCluster "github.com/canonical/lxd/lxd/cluster" "github.com/canonical/lxd/lxd/db" @@ -44,25 +45,25 @@ import ( var storagePoolVolumesCmd = APIEndpoint{ Path: "storage-pools/{poolName}/volumes", - Get: APIEndpointAction{Handler: storagePoolVolumesGet, AccessHandler: allowProjectPermission("storage-volumes", "view")}, - Post: APIEndpointAction{Handler: storagePoolVolumesPost, AccessHandler: allowProjectPermission("storage-volumes", "manage-storage-volumes")}, + Get: APIEndpointAction{Handler: storagePoolVolumesGet, AccessHandler: allowAuthenticated}, + Post: APIEndpointAction{Handler: storagePoolVolumesPost, AccessHandler: allowPermission(auth.ObjectTypeProject, auth.EntitlementCanCreateStorageVolumes)}, } var storagePoolVolumesTypeCmd = APIEndpoint{ Path: "storage-pools/{poolName}/volumes/{type}", - Get: APIEndpointAction{Handler: storagePoolVolumesGet, AccessHandler: allowProjectPermission("storage-volumes", "view")}, - Post: APIEndpointAction{Handler: storagePoolVolumesTypePost, AccessHandler: allowProjectPermission("storage-volumes", "manage-storage-volumes")}, + Get: APIEndpointAction{Handler: storagePoolVolumesGet, AccessHandler: allowAuthenticated}, + Post: APIEndpointAction{Handler: storagePoolVolumesTypePost, AccessHandler: allowPermission(auth.ObjectTypeProject, auth.EntitlementCanCreateStorageVolumes)}, } var storagePoolVolumeTypeCmd = APIEndpoint{ Path: "storage-pools/{poolName}/volumes/{type}/{volumeName}", - Delete: APIEndpointAction{Handler: storagePoolVolumeDelete, AccessHandler: allowProjectPermission("storage-volumes", "manage-storage-volumes")}, - Get: APIEndpointAction{Handler: storagePoolVolumeGet, AccessHandler: allowProjectPermission("storage-volumes", "view")}, - Patch: APIEndpointAction{Handler: storagePoolVolumePatch, AccessHandler: allowProjectPermission("storage-volumes", "manage-storage-volumes")}, - Post: APIEndpointAction{Handler: storagePoolVolumePost, AccessHandler: allowProjectPermission("storage-volumes", "manage-storage-volumes")}, - Put: APIEndpointAction{Handler: storagePoolVolumePut, AccessHandler: allowProjectPermission("storage-volumes", "manage-storage-volumes")}, + Delete: APIEndpointAction{Handler: storagePoolVolumeDelete, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanEdit, "poolName", "type", "volumeName")}, + Get: APIEndpointAction{Handler: storagePoolVolumeGet, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanView, "poolName", "type", "volumeName")}, + Patch: APIEndpointAction{Handler: storagePoolVolumePatch, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanEdit, "poolName", "type", "volumeName")}, + Post: APIEndpointAction{Handler: storagePoolVolumePost, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanEdit, "poolName", "type", "volumeName")}, + Put: APIEndpointAction{Handler: storagePoolVolumePut, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanEdit, "poolName", "type", "volumeName")}, } // swagger:operation GET /1.0/storage-pools/{poolName}/volumes storage storage_pool_volumes_get @@ -432,11 +433,21 @@ func storagePoolVolumesGet(d *Daemon, r *http.Request) response.Response { return volA.Name < volB.Name }) + userHasPermission, err := s.Authorizer.GetPermissionChecker(r.Context(), r, auth.EntitlementCanView, auth.ObjectTypeStorageVolume) + if err != nil { + return response.SmartError(err) + } + 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(auth.ObjectStorageVolume(vol.Project, poolName, dbVol.Type, volumeName)) { + continue + } + volumeUsedBy, err := storagePoolVolumeUsedByGet(s, requestProjectName, poolName, dbVol) if err != nil { return response.InternalError(err) @@ -451,6 +462,12 @@ func storagePoolVolumesGet(d *Daemon, r *http.Request) response.Response { urls := make([]string, 0, len(dbVolumes)) for _, dbVol := range dbVolumes { + volumeName, _, _ := api.GetParentAndSnapshotName(dbVol.Name) + + if !userHasPermission(auth.ObjectStorageVolume(dbVol.Project, poolName, dbVol.Type, volumeName)) { + continue + } + urls = append(urls, dbVol.StorageVolume.URL(version.APIVersion, poolName).String()) } @@ -1042,8 +1059,9 @@ func storagePoolVolumePost(d *Daemon, r *http.Request) response.Response { } // Check if user has access to effective storage target project - if !s.Authorizer.UserHasPermission(r, targetProjectName, "manage-storage-volumes") { - return response.Forbidden(nil) + err := s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectProject(targetProjectName), auth.EntitlementCanCreateStorageVolumes) + if err != nil { + return response.SmartError(err) } } diff --git a/lxd/storage_volumes_backup.go b/lxd/storage_volumes_backup.go index a5ac2ac555ec..0ed2b0d5a248 100644 --- a/lxd/storage_volumes_backup.go +++ b/lxd/storage_volumes_backup.go @@ -11,6 +11,7 @@ import ( "github.com/gorilla/mux" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/backup" "github.com/canonical/lxd/lxd/db" "github.com/canonical/lxd/lxd/db/operationtype" @@ -30,22 +31,22 @@ import ( var storagePoolVolumeTypeCustomBackupsCmd = APIEndpoint{ Path: "storage-pools/{poolName}/volumes/{type}/{volumeName}/backups", - Get: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupsGet, AccessHandler: allowProjectPermission("storage-volumes", "view")}, - Post: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupsPost, AccessHandler: allowProjectPermission("storage-volumes", "manage-storage-volumes")}, + Get: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupsGet, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanView, "poolName", "type", "volumeName")}, + Post: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupsPost, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanManageBackups, "poolName", "type", "volumeName")}, } var storagePoolVolumeTypeCustomBackupCmd = APIEndpoint{ Path: "storage-pools/{poolName}/volumes/{type}/{volumeName}/backups/{backupName}", - Get: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupGet, AccessHandler: allowProjectPermission("storage-volumes", "view")}, - Post: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupPost, AccessHandler: allowProjectPermission("storage-volumes", "manage-storage-volumes")}, - Delete: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupDelete, AccessHandler: allowProjectPermission("storage-volumes", "manage-storage-volumes")}, + Get: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupGet, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanView, "poolName", "type", "volumeName")}, + Post: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupPost, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanManageBackups, "poolName", "type", "volumeName")}, + Delete: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupDelete, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanManageBackups, "poolName", "type", "volumeName")}, } var storagePoolVolumeTypeCustomBackupExportCmd = APIEndpoint{ Path: "storage-pools/{poolName}/volumes/{type}/{volumeName}/backups/{backupName}/export", - Get: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupExportGet, AccessHandler: allowProjectPermission("storage-volumes", "view")}, + Get: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupExportGet, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanView, "poolName", "type", "volumeName")}, } // swagger:operation GET /1.0/storage-pools/{poolName}/volumes/{type}/{volumeName}/backups storage storage_pool_volumes_type_backups_get diff --git a/lxd/storage_volumes_snapshot.go b/lxd/storage_volumes_snapshot.go index 31eb5c88bf2a..4a5e8ba32208 100644 --- a/lxd/storage_volumes_snapshot.go +++ b/lxd/storage_volumes_snapshot.go @@ -14,6 +14,7 @@ import ( "github.com/flosch/pongo2" "github.com/gorilla/mux" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/db" dbCluster "github.com/canonical/lxd/lxd/db/cluster" "github.com/canonical/lxd/lxd/db/operationtype" @@ -35,18 +36,18 @@ import ( var storagePoolVolumeSnapshotsTypeCmd = APIEndpoint{ Path: "storage-pools/{poolName}/volumes/{type}/{volumeName}/snapshots", - Get: APIEndpointAction{Handler: storagePoolVolumeSnapshotsTypeGet, AccessHandler: allowProjectPermission("storage-volumes", "view")}, - Post: APIEndpointAction{Handler: storagePoolVolumeSnapshotsTypePost, AccessHandler: allowProjectPermission("storage-volumes", "manage-storage-volumes")}, + Get: APIEndpointAction{Handler: storagePoolVolumeSnapshotsTypeGet, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanView, "poolName", "type", "volumeName")}, + Post: APIEndpointAction{Handler: storagePoolVolumeSnapshotsTypePost, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanManageSnapshots, "poolName", "type", "volumeName")}, } var storagePoolVolumeSnapshotTypeCmd = APIEndpoint{ Path: "storage-pools/{poolName}/volumes/{type}/{volumeName}/snapshots/{snapshotName}", - Delete: APIEndpointAction{Handler: storagePoolVolumeSnapshotTypeDelete, AccessHandler: allowProjectPermission("storage-volumes", "manage-storage-volumes")}, - Get: APIEndpointAction{Handler: storagePoolVolumeSnapshotTypeGet, AccessHandler: allowProjectPermission("storage-volumes", "view")}, - Post: APIEndpointAction{Handler: storagePoolVolumeSnapshotTypePost, AccessHandler: allowProjectPermission("storage-volumes", "manage-storage-volumes")}, - Patch: APIEndpointAction{Handler: storagePoolVolumeSnapshotTypePatch, AccessHandler: allowProjectPermission("storage-volumes", "manage-storage-volumes")}, - Put: APIEndpointAction{Handler: storagePoolVolumeSnapshotTypePut, AccessHandler: allowProjectPermission("storage-volumes", "manage-storage-volumes")}, + Delete: APIEndpointAction{Handler: storagePoolVolumeSnapshotTypeDelete, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanManageSnapshots, "poolName", "type", "volumeName")}, + Get: APIEndpointAction{Handler: storagePoolVolumeSnapshotTypeGet, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanView, "poolName", "type", "volumeName")}, + Post: APIEndpointAction{Handler: storagePoolVolumeSnapshotTypePost, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanManageSnapshots, "poolName", "type", "volumeName")}, + Patch: APIEndpointAction{Handler: storagePoolVolumeSnapshotTypePatch, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanManageSnapshots, "poolName", "type", "volumeName")}, + Put: APIEndpointAction{Handler: storagePoolVolumeSnapshotTypePut, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanManageSnapshots, "poolName", "type", "volumeName")}, } // swagger:operation POST /1.0/storage-pools/{poolName}/volumes/{type}/{volumeName}/snapshots storage storage_pool_volumes_type_snapshots_post diff --git a/lxd/storage_volumes_state.go b/lxd/storage_volumes_state.go index 732db3c58740..f466187ae656 100644 --- a/lxd/storage_volumes_state.go +++ b/lxd/storage_volumes_state.go @@ -7,6 +7,7 @@ import ( "github.com/gorilla/mux" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/db" "github.com/canonical/lxd/lxd/instance" "github.com/canonical/lxd/lxd/instance/instancetype" @@ -21,7 +22,7 @@ import ( var storagePoolVolumeTypeStateCmd = APIEndpoint{ Path: "storage-pools/{poolName}/volumes/{type}/{volumeName}/state", - Get: APIEndpointAction{Handler: storagePoolVolumeTypeStateGet, AccessHandler: allowProjectPermission("storage-volumes", "view")}, + Get: APIEndpointAction{Handler: storagePoolVolumeTypeStateGet, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanView, "poolName", "type", "volumeName")}, } // swagger:operation GET /1.0/storage-pools/{poolName}/volumes/{type}/{volumeName}/state storage storage_pool_volume_type_state_get diff --git a/lxd/warnings.go b/lxd/warnings.go index 3f61f33ff227..6372867b321a 100644 --- a/lxd/warnings.go +++ b/lxd/warnings.go @@ -12,6 +12,7 @@ import ( "github.com/gorilla/mux" + "github.com/canonical/lxd/lxd/auth" "github.com/canonical/lxd/lxd/db" "github.com/canonical/lxd/lxd/db/cluster" "github.com/canonical/lxd/lxd/db/operationtype" @@ -31,16 +32,16 @@ import ( var warningsCmd = APIEndpoint{ Path: "warnings", - Get: APIEndpointAction{Handler: warningsGet}, + Get: APIEndpointAction{Handler: warningsGet, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } var warningCmd = APIEndpoint{ Path: "warnings/{id}", - Get: APIEndpointAction{Handler: warningGet}, - Patch: APIEndpointAction{Handler: warningPatch}, - Put: APIEndpointAction{Handler: warningPut}, - Delete: APIEndpointAction{Handler: warningDelete}, + Get: APIEndpointAction{Handler: warningGet, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, + Patch: APIEndpointAction{Handler: warningPatch, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, + Put: APIEndpointAction{Handler: warningPut, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, + Delete: APIEndpointAction{Handler: warningDelete, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)}, } func filterWarnings(warnings []api.Warning, clauses *filter.ClauseSet) ([]api.Warning, error) {