[lxc-devel] [lxd/master] Support RBAC

monstermunchkin on Github lxc-bot at linuxcontainers.org
Sat Mar 2 09:25:31 UTC 2019


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 301 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20190302/d90df656/attachment.bin>
-------------- next part --------------
From f60d98d52b937db5f7119f09b8b32639cc3a61b7 Mon Sep 17 00:00:00 2001
From: Thomas Hipp <thomas.hipp at canonical.com>
Date: Thu, 28 Feb 2019 09:33:00 +0100
Subject: [PATCH 1/7] shared/api: Add RBAC API extension

Signed-off-by: Thomas Hipp <thomas.hipp at canonical.com>
---
 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 <thomas.hipp at canonical.com>
Date: Thu, 28 Feb 2019 09:56:53 +0100
Subject: [PATCH 2/7] doc: Add RBAC

Signed-off-by: Thomas Hipp <thomas.hipp at canonical.com>
---
 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 <thomas.hipp at canonical.com>
Date: Thu, 28 Feb 2019 09:28:14 +0100
Subject: [PATCH 3/7] lxd: Add RBAC config options

Signed-off-by: Thomas Hipp <thomas.hipp at canonical.com>
---
 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 <thomas.hipp at canonical.com>
Date: Fri, 1 Mar 2019 11:49:39 +0100
Subject: [PATCH 4/7] lxd/rbac: Add initial code

Signed-off-by: Thomas Hipp <thomas.hipp at canonical.com>
---
 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 <thomas.hipp at canonical.com>
Date: Fri, 1 Mar 2019 10:25:54 +0100
Subject: [PATCH 5/7] lxd: Initialize RBAC

Signed-off-by: Thomas Hipp <thomas.hipp at canonical.com>
---
 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 <thomas.hipp at canonical.com>
Date: Fri, 1 Mar 2019 16:11:35 +0100
Subject: [PATCH 6/7] lxd: Add RBAC permission checks

Signed-off-by: Thomas Hipp <thomas.hipp at canonical.com>
---
 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 <thomas.hipp at canonical.com>
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 <thomas.hipp at canonical.com>
---
 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


More information about the lxc-devel mailing list