[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