The following pull request was submitted through Github. It can be accessed and reviewed at: https://github.com/lxc/lxd/pull/5540
This e-mail was sent by the LXC bot, direct replies will not reach the author unless they happen to be subscribed to this list. === Description (from pull-request) ===
From f60d98d52b937db5f7119f09b8b32639cc3a61b7 Mon Sep 17 00:00:00 2001 From: Thomas Hipp <[email protected]> Date: Thu, 28 Feb 2019 09:33:00 +0100 Subject: [PATCH 1/7] shared/api: Add RBAC API extension Signed-off-by: Thomas Hipp <[email protected]> --- shared/version/api.go | 1 + 1 file changed, 1 insertion(+) diff --git a/shared/version/api.go b/shared/version/api.go index f01dc66b22..0b9f77f26f 100644 --- a/shared/version/api.go +++ b/shared/version/api.go @@ -139,6 +139,7 @@ var APIExtensions = []string{ "snapshot_expiry", "container_backup_override_pool", "snapshot_expiry_creation", + "rbac", } // APIExtensionsCount returns the number of available API extensions. From 66266ff7089ef39b2be7439784bac03f8afd5d07 Mon Sep 17 00:00:00 2001 From: Thomas Hipp <[email protected]> Date: Thu, 28 Feb 2019 09:56:53 +0100 Subject: [PATCH 2/7] doc: Add RBAC Signed-off-by: Thomas Hipp <[email protected]> --- doc/api-extensions.md | 10 ++++++++++ doc/server.md | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/doc/api-extensions.md b/doc/api-extensions.md index 613dbf287f..189d713301 100644 --- a/doc/api-extensions.md +++ b/doc/api-extensions.md @@ -693,3 +693,13 @@ This adds the following new endpoint (see [RESTful API](rest-api.md) for details ## snapshot\_expiry\_creation Adds `expires\_at` to container creation, allowing for override of a snapshot's expiry at creation time. + +## rbac +Adds support for RBAC (role based access control). This introduces new config keys: + + * rbac.api.url + * rbac.api.key + * rbac.api.expiry + * rbac.agent.username + * rbac.agent.private_key + * rbac.agent.public_key \ No newline at end of file diff --git a/doc/server.md b/doc/server.md index f2b7a18ce0..e5eb55f1dd 100644 --- a/doc/server.md +++ b/doc/server.md @@ -35,6 +35,12 @@ images.remote\_cache\_expiry | integer | 10 | - maas.api.key | string | - | maas\_network | API key to manage MAAS maas.api.url | string | - | maas\_network | URL of the MAAS server maas.machine | string | hostname | maas\_network | Name of this LXD host in MAAS +rbac.agent.public_key | string | | rbac | The Candid agent public key as provided during RBAC registration +rbac.agent.private_key | string | | rbac | The Candid agent private key as provided during RBAC registration +rbac.api.key | string | | rbac | The RBAC public key +rbac.api.url | string | | rbac | The root URL of the RBAC server +rbac.api.username | string | | rbac | The Candid agent username as provided during RBAC registration +rbac.api.expiry | integer | | rbac | TBD Those keys can be set using the lxc tool with: From bc205b4eb9aac4277c788779e28da3c3ac58e261 Mon Sep 17 00:00:00 2001 From: Thomas Hipp <[email protected]> Date: Thu, 28 Feb 2019 09:28:14 +0100 Subject: [PATCH 3/7] lxd: Add RBAC config options Signed-off-by: Thomas Hipp <[email protected]> --- lxd/cluster/config.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/lxd/cluster/config.go b/lxd/cluster/config.go index 3b32f89d00..d2e773ca4c 100644 --- a/lxd/cluster/config.go +++ b/lxd/cluster/config.go @@ -132,6 +132,36 @@ func (c *Config) ImagesMinimalReplica() int64 { return c.m.GetInt64("cluster.images_minimal_replica") } +// RBACAgentPublicKey returns Candid agent public key as provided during RBAC registration +func (c *Config) RBACAgentPublicKey() string { + return c.m.GetString("rbac.agent.public_key") +} + +// RBACAgentPrivateKey returns Candid agent private key as provided during RBAC registration +func (c *Config) RBACAgentPrivateKey() string { + return c.m.GetString("rbac.agent.private_key") +} + +// RBACAPIURL returns the root URL of the RBAC server +func (c *Config) RBACAPIURL() string { + return c.m.GetString("rbac.api.url") +} + +// RBACAPIUsername returns the Candid agent username as provided during RBAC registration +func (c *Config) RBACAPIUsername() string { + return c.m.GetString("rbac.api.username") +} + +// RBACAPIKey returns the RBAC public key +func (c *Config) RBACAPIKey() string { + return c.m.GetString("rbac.api.key") +} + +// RBACAPIExpiry returns the RBAC expiry +func (c *Config) RBACAPIExpiry() int64 { + return c.m.GetInt64("rbac.api.expiry") +} + // Dump current configuration keys and their values. Keys with values matching // their defaults are omitted. func (c *Config) Dump() map[string]interface{} { @@ -242,6 +272,12 @@ var ConfigSchema = config.Schema{ "images.remote_cache_expiry": {Type: config.Int64, Default: "10"}, "maas.api.key": {}, "maas.api.url": {}, + "rbac.agent.private_key": {}, + "rbac.agent.public_key": {}, + "rbac.api.url": {}, + "rbac.api.username": {}, + "rbac.api.key": {}, + "rbac.api.expiry": {Type: config.Int64, Default: "60"}, // Keys deprecated since the implementation of the storage api. "storage.lvm_fstype": {Setter: deprecatedStorage, Default: "ext4"}, From 75f0913e1d754dc8981b283f890bfb10fde15755 Mon Sep 17 00:00:00 2001 From: Thomas Hipp <[email protected]> Date: Fri, 1 Mar 2019 11:49:39 +0100 Subject: [PATCH 4/7] lxd/rbac: Add initial code Signed-off-by: Thomas Hipp <[email protected]> --- lxd/rbac/server.go | 243 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 lxd/rbac/server.go diff --git a/lxd/rbac/server.go b/lxd/rbac/server.go new file mode 100644 index 0000000000..489d7405f7 --- /dev/null +++ b/lxd/rbac/server.go @@ -0,0 +1,243 @@ +package rbac + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "sync" + "time" + + "github.com/juju/persistent-cookiejar" + "gopkg.in/macaroon-bakery.v2/bakery" + "gopkg.in/macaroon-bakery.v2/httpbakery" + "gopkg.in/macaroon-bakery.v2/httpbakery/agent" + + "github.com/lxc/lxd/shared" + "github.com/lxc/lxd/shared/logger" +) + +// UnexpectedSyncIDError is returned when the RBAC server received an unexpected sync ID. +type UnexpectedSyncIDError struct{} + +// Error implements error.Error. +func (e *UnexpectedSyncIDError) Error() string { + return "Unexpected sync ID" +} + +type rbacResource struct { + Identifier string `json:"identifier"` + Name string `json:"name"` +} + +type rbacResourcePost struct { + LastSyncID *string `json:"last-sync-id"` + Updates []rbacResource `json:"updates"` +} + +type rbacResourcePostResponse struct { + SyncID string `json:"sync-id"` +} + +// Server represents an RBAC server. +type Server struct { + url string + publicKey string + username string + agentPrivateKey bakery.PrivateKey + agentPublicKey bakery.PublicKey + + lastSyncID string + client *httpbakery.Client + + resources map[string]string // Maps name to identifier + permissions map[string]map[string][]string + + permissionsLock *sync.Mutex +} + +// NewServer returns a new RBAC server instance. +func NewServer(URL string, publicKey string, username string, agentPrivateKey string, + agentPublicKey string) (*Server, error) { + r := Server{ + url: URL, + publicKey: publicKey, + username: username, + lastSyncID: "", + resources: make(map[string]string), + permissions: make(map[string]map[string][]string), + permissionsLock: &sync.Mutex{}, + } + + r.agentPrivateKey.UnmarshalText([]byte(agentPrivateKey)) + r.agentPublicKey.UnmarshalText([]byte(agentPublicKey)) + + r.client = httpbakery.NewClient() + + authInfo := agent.AuthInfo{ + Key: &bakery.KeyPair{ + Private: r.agentPrivateKey, + Public: r.agentPublicKey, + }, + Agents: []agent.Agent{ + { + URL: r.url, + Username: r.username, + }, + }, + } + + err := agent.SetUpAuth(r.client, &authInfo) + if err != nil { + return nil, err + } + + jar, err := cookiejar.New(&cookiejar.Options{ + Filename: cookiejar.DefaultCookieFile(), + }) + if err != nil { + return nil, err + } + + r.client.Client.Jar = jar + + go r.flushCache() + + return &r, nil +} + +// Sync updates resources information for the project resource. +func (r *Server) Sync(force bool) error { + // Get all resources + u := fmt.Sprintf("%s/api/service/v1/resources/project", r.url) + + resp, err := r.client.Get(u) + if err != nil { + return err + } + defer resp.Body.Close() + + var resources []rbacResource + + err = json.NewDecoder(resp.Body).Decode(&resources) + if err != nil { + return err + } + + for _, res := range resources { + if res.Name != "" { + r.resources[res.Name] = res.Identifier + } + } + + // Sync + var resourcePost rbacResourcePost + + if force || r.lastSyncID == "" { + resourcePost.LastSyncID = nil + } else { + resourcePost.LastSyncID = &r.lastSyncID + } + + resourcePost.Updates = resources[1:] + + body, err := json.Marshal(&resourcePost) + if err != nil { + return err + } + + req, err := http.NewRequest("POST", u, bytes.NewReader(body)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + + resp, err = r.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode == 409 { + return &UnexpectedSyncIDError{} + } + + var postRespose rbacResourcePostResponse + + err = json.NewDecoder(resp.Body).Decode(&postRespose) + if err != nil { + return err + } + + r.lastSyncID = postRespose.SyncID + + logger.Infof("Received RBAC sync ID: %s", r.lastSyncID) + + return nil +} + +// HasPermission returns whether or not the user has the permission to perform a certain task. +func (r *Server) HasPermission(username, project, permission string) bool { + r.permissionsLock.Lock() + defer r.permissionsLock.Unlock() + + // Check whether the permissions are cached + _, cached := r.permissions[username] + + if !cached { + r.syncPermissions(username) + } + + return shared.StringInSlice(permission, r.permissions[username][r.resources[project]]) +} + +func (r *Server) flushCache() { + for { + <-time.After(time.Minute) + + r.permissionsLock.Lock() + + if len(r.permissions) == 0 { + r.permissionsLock.Unlock() + continue + } + + logger.Info("Flushing RBAC permissions cache") + + for k, v := range r.permissions { + for k := range v { + delete(v, k) + } + + delete(r.permissions, k) + } + + logger.Info("Flushed RBAC permissions cache") + + r.permissionsLock.Unlock() + } +} + +func (r *Server) syncPermissions(username string) error { + u := fmt.Sprintf("%s/api/service/v1/resources/project/permissions-for-user?u=%s", + r.url, username) + + resp, err := r.client.Get(u) + if err != nil { + return err + } + defer resp.Body.Close() + + var permissions map[string][]string + + err = json.NewDecoder(resp.Body).Decode(&permissions) + if err != nil { + return err + } + + // No need to acquire the lock since the caller (HasPermission) already has it. + r.permissions[username] = permissions + + return nil +} From 524ce5b3bcbc3db7320761ead55e3a32aba575b9 Mon Sep 17 00:00:00 2001 From: Thomas Hipp <[email protected]> Date: Fri, 1 Mar 2019 10:25:54 +0100 Subject: [PATCH 5/7] lxd: Initialize RBAC Signed-off-by: Thomas Hipp <[email protected]> --- lxd/daemon.go | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/lxd/daemon.go b/lxd/daemon.go index 9059174b97..5cf830dff2 100644 --- a/lxd/daemon.go +++ b/lxd/daemon.go @@ -31,6 +31,7 @@ import ( "github.com/lxc/lxd/lxd/endpoints" "github.com/lxc/lxd/lxd/maas" "github.com/lxc/lxd/lxd/node" + "github.com/lxc/lxd/lxd/rbac" "github.com/lxc/lxd/lxd/state" "github.com/lxc/lxd/lxd/sys" "github.com/lxc/lxd/lxd/task" @@ -49,6 +50,7 @@ type Daemon struct { os *sys.OS db *db.Node maas *maas.Controller + rbac *rbac.Server cluster *db.Cluster setupChan chan struct{} // Closed when basic Daemon setup is completed readyChan chan struct{} // Closed when LXD is fully ready @@ -725,6 +727,11 @@ func (d *Daemon) init() error { maasAPIURL := "" maasAPIKey := "" maasMachine := "" + rbacAPIURL := "" + rbacAPIKey := "" + rbacAgentPrivateKey := "" + rbacAgentPublicKey := "" + rbacAPIUsername := "" err = d.db.Transaction(func(tx *db.NodeTx) error { config, err := node.ConfigLoad(tx) @@ -754,6 +761,12 @@ func (d *Daemon) init() error { candidExpiry = config.CandidExpiry() candidDomains = config.CandidDomains() maasAPIURL, maasAPIKey = config.MAASController() + rbacAPIURL = config.RBACAPIURL() + rbacAPIKey = config.RBACAPIKey() + rbacAgentPrivateKey = config.RBACAgentPrivateKey() + rbacAgentPublicKey = config.RBACAgentPublicKey() + rbacAPIUsername = config.RBACAPIUsername() + return nil }) if err != nil { @@ -765,6 +778,11 @@ func (d *Daemon) init() error { return err } + err = d.setupRBACServer(rbacAPIURL, rbacAPIKey, rbacAPIUsername, rbacAgentPrivateKey, rbacAgentPublicKey) + if err != nil { + return err + } + if !d.os.MockMode { // Start the scheduler go deviceEventListener(d.State()) @@ -1088,6 +1106,49 @@ func (d *Daemon) setupExternalAuthentication(authEndpoint string, authPubkey str return nil } +// Setup RBAC +func (d *Daemon) setupRBACServer(rbacURL, rbacKey, rbacUsername, rbacAgentPrivateKey, + rbacAgentPublicKey string) error { + d.rbac = nil + + if rbacURL == "" || rbacUsername == "" || rbacAgentPrivateKey == "" || rbacAgentPublicKey == "" { + return nil + } + + // Get a new server struct + server, err := rbac.NewServer(rbacURL, rbacKey, rbacUsername, rbacAgentPrivateKey, rbacAgentPublicKey) + if err != nil { + return err + } + + // Perform full sync + err = server.Sync(true) + if err != nil { + return err + } + + d.rbac = server + + return nil +} + +func (d *Daemon) userHasPermission(r *http.Request, project string, permission string) bool { + if d.externalAuth == nil || d.rbac == nil { + return true + } + + valid, userID, err := d.Authenticate(r) + if err != nil { + return false + } + + if valid && userID == "" { + return true + } + + return d.rbac.HasPermission(userID, project, permission) +} + // Setup MAAS func (d *Daemon) setupMAASController(server string, key string, machine string) error { var err error From 8776c6478517f1f34c75e8ae934e1dc4091ad115 Mon Sep 17 00:00:00 2001 From: Thomas Hipp <[email protected]> Date: Fri, 1 Mar 2019 16:11:35 +0100 Subject: [PATCH 6/7] lxd: Add RBAC permission checks Signed-off-by: Thomas Hipp <[email protected]> --- lxd/container_backup.go | 30 ++++++++++++++++++++++++++ lxd/container_delete.go | 5 +++++ lxd/container_exec.go | 5 +++++ lxd/container_file.go | 5 +++++ lxd/container_get.go | 6 ++++++ lxd/container_patch.go | 4 ++++ lxd/container_put.go | 10 +++++++++ lxd/container_snapshot.go | 26 +++++++++++++++++++++++ lxd/containers_get.go | 7 +++++++ lxd/containers_post.go | 4 ++++ lxd/images.go | 44 +++++++++++++++++++++++++++++++++++++++ 11 files changed, 146 insertions(+) diff --git a/lxd/container_backup.go b/lxd/container_backup.go index e50b4d2792..a9f20fa02b 100644 --- a/lxd/container_backup.go +++ b/lxd/container_backup.go @@ -19,6 +19,11 @@ import ( func containerBackupsGet(d *Daemon, r *http.Request) Response { project := projectParam(r) + + if !d.userHasPermission(r, project, "view") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + cname := mux.Vars(r)["name"] // Handle requests targeted to a container on a different node @@ -65,6 +70,11 @@ func containerBackupsGet(d *Daemon, r *http.Request) Response { func containerBackupsPost(d *Daemon, r *http.Request) Response { project := projectParam(r) + + if !d.userHasPermission(r, project, "operate-containers") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + name := mux.Vars(r)["name"] // Handle requests targeted to a container on a different node @@ -177,6 +187,11 @@ func containerBackupsPost(d *Daemon, r *http.Request) Response { func containerBackupGet(d *Daemon, r *http.Request) Response { project := projectParam(r) + + if !d.userHasPermission(r, project, "view") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + name := mux.Vars(r)["name"] backupName := mux.Vars(r)["backupName"] @@ -200,6 +215,11 @@ func containerBackupGet(d *Daemon, r *http.Request) Response { func containerBackupPost(d *Daemon, r *http.Request) Response { project := projectParam(r) + + if !d.userHasPermission(r, project, "operate-containers") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + name := mux.Vars(r)["name"] backupName := mux.Vars(r)["backupName"] @@ -254,6 +274,11 @@ func containerBackupPost(d *Daemon, r *http.Request) Response { func containerBackupDelete(d *Daemon, r *http.Request) Response { project := projectParam(r) + + if !d.userHasPermission(r, project, "operate-containers") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + name := mux.Vars(r)["name"] backupName := mux.Vars(r)["backupName"] @@ -295,6 +320,11 @@ func containerBackupDelete(d *Daemon, r *http.Request) Response { func containerBackupExportGet(d *Daemon, r *http.Request) Response { project := projectParam(r) + + if !d.userHasPermission(r, project, "operate-containers") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + name := mux.Vars(r)["name"] backupName := mux.Vars(r)["backupName"] diff --git a/lxd/container_delete.go b/lxd/container_delete.go index 6e6d653928..782f7ba9cd 100644 --- a/lxd/container_delete.go +++ b/lxd/container_delete.go @@ -10,6 +10,11 @@ import ( func containerDelete(d *Daemon, r *http.Request) Response { project := projectParam(r) + + if !d.userHasPermission(r, project, "manage-containers") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + name := mux.Vars(r)["name"] // Handle requests targeted to a container on a different node diff --git a/lxd/container_exec.go b/lxd/container_exec.go index 71c694a06d..d0e9b72740 100644 --- a/lxd/container_exec.go +++ b/lxd/container_exec.go @@ -338,6 +338,11 @@ func (s *execWs) Do(op *operation) error { func containerExecPost(d *Daemon, r *http.Request) Response { project := projectParam(r) + + if !d.userHasPermission(r, project, "operate-containers") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + name := mux.Vars(r)["name"] post := api.ContainerExecPost{} diff --git a/lxd/container_file.go b/lxd/container_file.go index e9358c091a..826fdda6c8 100644 --- a/lxd/container_file.go +++ b/lxd/container_file.go @@ -15,6 +15,11 @@ import ( func containerFileHandler(d *Daemon, r *http.Request) Response { project := projectParam(r) + + if !d.userHasPermission(r, project, "operate-containers") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + name := mux.Vars(r)["name"] response, err := ForwardedResponseIfContainerIsRemote(d, r, project, name) diff --git a/lxd/container_get.go b/lxd/container_get.go index 565699364d..937d9e163d 100644 --- a/lxd/container_get.go +++ b/lxd/container_get.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "net/http" "github.com/gorilla/mux" @@ -8,6 +9,11 @@ import ( func containerGet(d *Daemon, r *http.Request) Response { project := projectParam(r) + + if !d.userHasPermission(r, project, "view") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + name := mux.Vars(r)["name"] // Handle requests targeted to a container on a different node diff --git a/lxd/container_patch.go b/lxd/container_patch.go index cb3a28e12c..bb92c319a4 100644 --- a/lxd/container_patch.go +++ b/lxd/container_patch.go @@ -19,6 +19,10 @@ import ( func containerPatch(d *Daemon, r *http.Request) Response { project := projectParam(r) + if !d.userHasPermission(r, project, "manage-containers") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + // Get the container name := mux.Vars(r)["name"] diff --git a/lxd/container_put.go b/lxd/container_put.go index 90a12b7d5e..fd2954cef7 100644 --- a/lxd/container_put.go +++ b/lxd/container_put.go @@ -60,6 +60,11 @@ func containerPut(d *Daemon, r *http.Request) Response { var opType db.OperationType if configRaw.Restore == "" { // Update container configuration + + if !d.userHasPermission(r, project, "manage-containers") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + do = func(op *operation) error { args := db.ContainerArgs{ Architecture: architecture, @@ -83,6 +88,11 @@ func containerPut(d *Daemon, r *http.Request) Response { opType = db.OperationSnapshotUpdate } else { // Snapshot Restore + + if !d.userHasPermission(r, project, "operate-containers") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + do = func(op *operation) error { return containerSnapRestore(d.State(), project, name, configRaw.Restore, configRaw.Stateful) } diff --git a/lxd/container_snapshot.go b/lxd/container_snapshot.go index 0d3fde4f76..8ec70708f4 100644 --- a/lxd/container_snapshot.go +++ b/lxd/container_snapshot.go @@ -21,6 +21,11 @@ import ( func containerSnapshotsGet(d *Daemon, r *http.Request) Response { project := projectParam(r) + + if !d.userHasPermission(r, project, "view") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + cname := mux.Vars(r)["name"] // Handle requests targeted to a container on a different node @@ -76,6 +81,11 @@ func containerSnapshotsGet(d *Daemon, r *http.Request) Response { func containerSnapshotsPost(d *Daemon, r *http.Request) Response { project := projectParam(r) + + if !d.userHasPermission(r, project, "operate-containers") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + name := mux.Vars(r)["name"] // Handle requests targeted to a container on a different node @@ -198,12 +208,28 @@ func snapshotHandler(d *Daemon, r *http.Request) Response { switch r.Method { case "GET": + if !d.userHasPermission(r, project, "view") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + return snapshotGet(sc, snapshotName) case "POST": + if !d.userHasPermission(r, project, "operate-containers") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + return snapshotPost(d, r, sc, containerName) case "DELETE": + if !d.userHasPermission(r, project, "operate-containers") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + return snapshotDelete(sc, snapshotName) case "PUT": + if !d.userHasPermission(r, project, "operate-containers") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + return snapshotPut(d, r, sc, snapshotName) default: return NotFound(fmt.Errorf("Method '%s' not found", r.Method)) diff --git a/lxd/containers_get.go b/lxd/containers_get.go index d93190be33..3ca888de78 100644 --- a/lxd/containers_get.go +++ b/lxd/containers_get.go @@ -19,6 +19,13 @@ import ( ) func containersGet(d *Daemon, r *http.Request) Response { + // Parse the project field + project := projectParam(r) + + if !d.userHasPermission(r, project, "view") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + for i := 0; i < 100; i++ { result, err := doContainersGet(d, r) if err == nil { diff --git a/lxd/containers_post.go b/lxd/containers_post.go index 606f40913a..1103c0f070 100644 --- a/lxd/containers_post.go +++ b/lxd/containers_post.go @@ -666,6 +666,10 @@ func containersPost(d *Daemon, r *http.Request) Response { project := projectParam(r) logger.Debugf("Responding to container create") + if !d.userHasPermission(r, project, "manage-containers") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + // If we're getting binary content, process separately if r.Header.Get("Content-Type") == "application/octet-stream" { return createFromBackup(d, project, r.Body, r.Header.Get("X-LXD-pool")) diff --git a/lxd/images.go b/lxd/images.go index bdd29c9583..4786335c7e 100644 --- a/lxd/images.go +++ b/lxd/images.go @@ -657,6 +657,10 @@ func imageCreateInPool(d *Daemon, info *api.Image, storagePool string) error { func imagesPost(d *Daemon, r *http.Request) Response { project := projectParam(r) + if !d.userHasPermission(r, project, "manage-containers") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + var err error // create a directory under which we keep everything while building @@ -881,6 +885,11 @@ func doImagesGet(d *Daemon, recursion bool, project string, public bool) (interf func imagesGet(d *Daemon, r *http.Request) Response { project := projectParam(r) + + if !d.userHasPermission(r, project, "view") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + public := d.checkTrustedClient(r) != nil result, err := doImagesGet(d, util.IsRecursionRequest(r), project, public) @@ -1308,6 +1317,11 @@ func doDeleteImageFromPool(state *state.State, fingerprint string, storagePool s func imageDelete(d *Daemon, r *http.Request) Response { project := projectParam(r) + + if !d.userHasPermission(r, project, "manage-containers") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + fingerprint := mux.Vars(r)["fingerprint"] deleteFromAllPools := func() error { @@ -1483,6 +1497,11 @@ func imageValidSecret(fingerprint string, secret string) bool { func imageGet(d *Daemon, r *http.Request) Response { project := projectParam(r) + + if !d.userHasPermission(r, project, "view") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + fingerprint := mux.Vars(r)["fingerprint"] public := d.checkTrustedClient(r) != nil secret := r.FormValue("secret") @@ -1503,6 +1522,11 @@ func imageGet(d *Daemon, r *http.Request) Response { func imagePut(d *Daemon, r *http.Request) Response { // Get current value project := projectParam(r) + + if !d.userHasPermission(r, project, "manage-containers") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + fingerprint := mux.Vars(r)["fingerprint"] id, info, err := d.cluster.ImageGet(project, fingerprint, false, false) if err != nil { @@ -1532,6 +1556,11 @@ func imagePut(d *Daemon, r *http.Request) Response { func imagePatch(d *Daemon, r *http.Request) Response { // Get current value project := projectParam(r) + + if !d.userHasPermission(r, project, "manage-containers") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + fingerprint := mux.Vars(r)["fingerprint"] id, info, err := d.cluster.ImageGet(project, fingerprint, false, false) if err != nil { @@ -1809,6 +1838,11 @@ func aliasPost(d *Daemon, r *http.Request) Response { func imageExport(d *Daemon, r *http.Request) Response { project := projectParam(r) + + if !d.userHasPermission(r, project, "operate-containers") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + fingerprint := mux.Vars(r)["fingerprint"] public := d.checkTrustedClient(r) != nil @@ -1893,6 +1927,11 @@ func imageExport(d *Daemon, r *http.Request) Response { func imageSecret(d *Daemon, r *http.Request) Response { project := projectParam(r) + + if !d.userHasPermission(r, project, "operate-containers") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + fingerprint := mux.Vars(r)["fingerprint"] _, imgInfo, err := d.cluster.ImageGet(project, fingerprint, false, false) if err != nil { @@ -1977,6 +2016,11 @@ func imageImportFromNode(imagesDir string, client lxd.ContainerServer, fingerpri func imageRefresh(d *Daemon, r *http.Request) Response { project := projectParam(r) + + if !d.userHasPermission(r, project, "operate-containers") { + return Forbidden(fmt.Errorf("Insufficient permissions")) + } + fingerprint := mux.Vars(r)["fingerprint"] imageId, imageInfo, err := d.cluster.ImageGet(project, fingerprint, false, false) if err != nil { From c93c81e8b3aca5416b373aab3621d6a04b3f5543 Mon Sep 17 00:00:00 2001 From: Thomas Hipp <[email protected]> Date: Fri, 1 Mar 2019 23:21:30 +0100 Subject: [PATCH 7/7] lxd: Get username from context for RBAC Signed-off-by: Thomas Hipp <[email protected]> --- lxd/daemon.go | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/lxd/daemon.go b/lxd/daemon.go index 5cf830dff2..e6f28963aa 100644 --- a/lxd/daemon.go +++ b/lxd/daemon.go @@ -1133,20 +1133,11 @@ func (d *Daemon) setupRBACServer(rbacURL, rbacKey, rbacUsername, rbacAgentPrivat } func (d *Daemon) userHasPermission(r *http.Request, project string, permission string) bool { - if d.externalAuth == nil || d.rbac == nil { + if d.externalAuth == nil || d.rbac == nil || r.RemoteAddr == "@" { return true } - valid, userID, err := d.Authenticate(r) - if err != nil { - return false - } - - if valid && userID == "" { - return true - } - - return d.rbac.HasPermission(userID, project, permission) + return d.rbac.HasPermission(r.Context().Value("username").(string), project, permission) } // Setup MAAS
_______________________________________________ lxc-devel mailing list [email protected] http://lists.linuxcontainers.org/listinfo/lxc-devel
