diff --git a/README.md b/README.md index 5c3a20f..133dc21 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ around this library to make that easier. ## Usage Create a client and use the public methods to access Proxmox resources. -### Basic usage with login credentials +### Basic usage with login with a username and password credential ```go package main @@ -37,15 +37,19 @@ import ( ) func main() { - client := proxmox.NewClient("https://localhost:8006/api2/json") - if err := client.Login("root@pam", "password"); err != nil { - panic(err) + credentials := proxmox.Credentials{ + Username: "root@pam", + Password: "12345", } + client := proxmox.NewClient("https://localhost:8006/api2/json", + proxmox.WithCredentials(&credentials), + ) + version, err := client.Version() if err != nil { panic(err) } - fmt.Println(version.Release) // 6.3 + fmt.Println(version.Release) // 7.4 } ``` @@ -70,7 +74,7 @@ func main() { secret := "somegeneratedapitokenguidefromtheproxmoxui" client := proxmox.NewClient("https://localhost:8006/api2/json", - proxmox.WithClient(&insecureHTTPClient), + proxmox.WithHTTPClient(&insecureHTTPClient), proxmox.WithAPIToken(tokenID, secret), ) diff --git a/access.go b/access.go index 6999f18..228ce00 100644 --- a/access.go +++ b/access.go @@ -2,8 +2,10 @@ package proxmox import ( "fmt" + "net/url" ) +// Deprecated: Use WithCredentials Option func (c *Client) Login(username, password string) error { _, err := c.Ticket(&Credentials{ Username: username, @@ -13,6 +15,7 @@ func (c *Client) Login(username, password string) error { return err } +// Deprecated: Use the WithAPIToken Option func (c *Client) APIToken(tokenID, secret string) { c.token = fmt.Sprintf("%s=%s", tokenID, secret) } @@ -20,3 +23,21 @@ func (c *Client) APIToken(tokenID, secret string) { func (c *Client) Ticket(credentials *Credentials) (*Session, error) { return c.session, c.Post("/access/ticket", credentials, &c.session) } + +// Permissions get permissions for the current user for the client which passes no params, use Permission +func (c *Client) Permissions(o *PermissionsOptions) (permissions Permissions, err error) { + u := url.URL{Path: "/access/permissions"} + + if o != nil { // params are optional + params := url.Values{} + if o.UserID != "" { + params.Add("userid", o.UserID) + } + if o.Path != "" { + params.Add("path", o.Path) + } + u.RawQuery = params.Encode() + } + + return permissions, c.Get(u.String(), &permissions) +} diff --git a/access_test.go b/access_test.go new file mode 100644 index 0000000..35b64cb --- /dev/null +++ b/access_test.go @@ -0,0 +1,59 @@ +package proxmox + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/luthermonson/go-proxmox/tests/mocks" +) + +func TestTicket(t *testing.T) { + mocks.On(mockConfig) + defer mocks.Off() + + // todo current mocks are hardcoded with test data, make configurable via mock config + client := mockClient(WithCredentials( + &Credentials{ + Username: "root@pam", + Password: "1234", + })) + + session, err := client.Ticket(client.credentials) + assert.Nil(t, err) + assert.Equal(t, "root@pam", session.Username) + assert.Equal(t, "pve-cluster", session.ClusterName) +} + +func TestPermissions(t *testing.T) { + mocks.On(mockConfig) + defer mocks.Off() + client := mockClient() + + perms, err := client.Permissions(nil) + assert.Nil(t, err) + assert.Equal(t, 8, len(perms)) + assert.Equal(t, 1, perms["/"]["Datastore.Allocate"]) + + // test path option + perms, err = client.Permissions(&PermissionsOptions{ + Path: "path", + }) + assert.Nil(t, err) + assert.Equal(t, 1, perms["path"]["permission"]) + + // test userid + perms, err = client.Permissions(&PermissionsOptions{ + UserID: "userid", + }) + assert.Nil(t, err) + assert.Equal(t, 2, perms["path"]["permission"]) + + // test both path and userid + perms, err = client.Permissions(&PermissionsOptions{ + UserID: "userid", + Path: "path", + }) + assert.Nil(t, err) + assert.Equal(t, 3, perms["path"]["permission"]) +} diff --git a/options.go b/options.go index 6c3917a..bc36c3c 100644 --- a/options.go +++ b/options.go @@ -7,18 +7,28 @@ import ( type Option func(*Client) +// Deprecated: Use WithHTTPClient func WithClient(client *http.Client) Option { + return WithHTTPClient(client) +} + +func WithHTTPClient(client *http.Client) Option { return func(c *Client) { c.httpClient = client } } +// Deprecated: Use WithCredential func WithLogins(username, password string) Option { + return WithCredentials(&Credentials{ + Username: username, + Password: password, + }) +} + +func WithCredentials(credentials *Credentials) Option { return func(c *Client) { - c.credentials = &Credentials{ - Username: username, - Password: password, - } + c.credentials = credentials } } @@ -29,11 +39,11 @@ func WithAPIToken(tokenID, secret string) Option { } // WithSession experimental -func WithSession(ticket, csrfPreventionToken string) Option { +func WithSession(ticket, CSRFPreventionToken string) Option { return func(c *Client) { c.session = &Session{ Ticket: ticket, - CsrfPreventionToken: csrfPreventionToken, + CSRFPreventionToken: CSRFPreventionToken, } } } diff --git a/options_test.go b/options_test.go index 0827763..866f756 100644 --- a/options_test.go +++ b/options_test.go @@ -19,6 +19,14 @@ func TestWithLogins(t *testing.T) { assert.Equal(t, client.credentials, &Credentials{Username: "root@pam", Password: "1234"}) } +func TestWithCredentials(t *testing.T) { + client := NewClient("", WithCredentials(&Credentials{ + Username: "root@pam", + Password: "1234", + })) + assert.Equal(t, client.credentials, &Credentials{Username: "root@pam", Password: "1234"}) +} + func TestWithAPIToken(t *testing.T) { client := NewClient("", WithAPIToken("root@pam!test", "1234")) assert.Equal(t, client.token, "root@pam!test=1234") @@ -26,7 +34,7 @@ func TestWithAPIToken(t *testing.T) { func TestWithSession(t *testing.T) { client := NewClient("", WithSession("ticket", "csrf")) - assert.Equal(t, client.session, &Session{Ticket: "ticket", CsrfPreventionToken: "csrf"}) + assert.Equal(t, client.session, &Session{Ticket: "ticket", CSRFPreventionToken: "csrf"}) } func TestWithUserAgent(t *testing.T) { diff --git a/proxmox.go b/proxmox.go index dd4a35d..89d6b18 100644 --- a/proxmox.go +++ b/proxmox.go @@ -238,7 +238,7 @@ func (c *Client) authHeaders(header *http.Header) { header.Add("Authorization", "PVEAPIToken="+c.token) } else if c.session != nil { header.Add("Cookie", "PVEAuthCookie="+c.session.Ticket) - header.Add("CSRFPreventionToken", c.session.CsrfPreventionToken) + header.Add("CSRFPreventionToken", c.session.CSRFPreventionToken) } } diff --git a/proxmox_test.go b/proxmox_test.go index 56e9dc8..da58fe9 100644 --- a/proxmox_test.go +++ b/proxmox_test.go @@ -23,6 +23,10 @@ func mockClient(options ...Option) *Client { return NewClient(mockConfig.URI, options...) } +func TestMakeTag(t *testing.T) { + assert.Equal(t, "go-proxmox+tagname", MakeTag("tagname")) +} + // options tested in options_test.go func TestNewClient(t *testing.T) { v := NewClient(TestURI) diff --git a/tests/integration/access_test.go b/tests/integration/access_test.go index 062d993..76ab1b7 100644 --- a/tests/integration/access_test.go +++ b/tests/integration/access_test.go @@ -1,32 +1 @@ package integration - -import ( - "testing" - - "github.com/luthermonson/go-proxmox" - "github.com/stretchr/testify/assert" -) - -func TestLogin(t *testing.T) { - client := ClientFromEnv() - _, err := client.Version() - assert.True(t, proxmox.IsNotAuthorized(err)) - - err = client.Login(td.username, td.password) - assert.Nil(t, err) - - version, err := client.Version() - assert.Nil(t, err) - assert.NotEmpty(t, version.Version) -} - -func TestAPIToken(t *testing.T) { - client := ClientFromEnv() - _, err := client.Version() - assert.True(t, proxmox.IsNotAuthorized(err)) - - client.APIToken(td.tokenID, td.secret) - version, err := client.Version() - assert.Nil(t, err) - assert.NotNil(t, version.Version) -} diff --git a/tests/mocks/pve7x/access.go b/tests/mocks/pve7x/access.go index 1c74205..14a6fd2 100644 --- a/tests/mocks/pve7x/access.go +++ b/tests/mocks/pve7x/access.go @@ -7,7 +7,7 @@ import ( func access() { gock.New(config.C.URI). - Get("/access"). + Get("^/access$"). Reply(200). JSON(` { @@ -41,13 +41,455 @@ func access() { } ] }`) + + // full access user with all paths + gock.New(config.C.URI). + Get("^/access/permissions$"). + Reply(200). + JSON(`{ + "data": { + "/pools": { + "VM.Audit": 1, + "VM.Config.CPU": 1, + "Datastore.Audit": 1, + "VM.Config.CDROM": 1, + "Group.Allocate": 1, + "SDN.Use": 1, + "VM.Config.HWType": 1, + "VM.Backup": 1, + "VM.Config.Disk": 1, + "Sys.Incoming": 1, + "VM.Config.Memory": 1, + "Sys.Audit": 1, + "VM.Monitor": 1, + "Datastore.AllocateTemplate": 1, + "Realm.AllocateUser": 1, + "VM.Console": 1, + "VM.Migrate": 1, + "VM.Snapshot": 1, + "Permissions.Modify": 1, + "VM.Config.Options": 1, + "VM.PowerMgmt": 1, + "Datastore.Allocate": 1, + "Sys.PowerMgmt": 1, + "User.Modify": 1, + "SDN.Allocate": 1, + "Datastore.AllocateSpace": 1, + "Realm.Allocate": 1, + "VM.Clone": 1, + "VM.Allocate": 1, + "Pool.Allocate": 1, + "Sys.Modify": 1, + "VM.Config.Cloudinit": 1, + "Sys.Syslog": 1, + "VM.Config.Network": 1, + "VM.Snapshot.Rollback": 1, + "Sys.Console": 1, + "SDN.Audit": 1, + "Pool.Audit": 1 + }, + "/storage": { + "VM.Audit": 1, + "VM.Config.CPU": 1, + "Datastore.Audit": 1, + "VM.Config.CDROM": 1, + "Group.Allocate": 1, + "SDN.Use": 1, + "VM.Config.HWType": 1, + "VM.Backup": 1, + "Sys.Incoming": 1, + "VM.Config.Memory": 1, + "VM.Config.Disk": 1, + "Sys.Audit": 1, + "VM.Monitor": 1, + "Datastore.AllocateTemplate": 1, + "Realm.AllocateUser": 1, + "VM.Console": 1, + "VM.Migrate": 1, + "VM.Snapshot": 1, + "Permissions.Modify": 1, + "VM.Config.Options": 1, + "VM.PowerMgmt": 1, + "Datastore.Allocate": 1, + "User.Modify": 1, + "Sys.PowerMgmt": 1, + "SDN.Allocate": 1, + "Datastore.AllocateSpace": 1, + "Realm.Allocate": 1, + "VM.Clone": 1, + "VM.Allocate": 1, + "Pool.Allocate": 1, + "Sys.Modify": 1, + "VM.Config.Cloudinit": 1, + "Sys.Syslog": 1, + "VM.Config.Network": 1, + "VM.Snapshot.Rollback": 1, + "Sys.Console": 1, + "SDN.Audit": 1, + "Pool.Audit": 1 + }, + "/access": { + "Pool.Audit": 1, + "SDN.Audit": 1, + "Sys.Console": 1, + "VM.Snapshot.Rollback": 1, + "VM.Config.Network": 1, + "Sys.Syslog": 1, + "VM.Config.Cloudinit": 1, + "Sys.Modify": 1, + "Pool.Allocate": 1, + "VM.Allocate": 1, + "VM.Clone": 1, + "Realm.Allocate": 1, + "Datastore.AllocateSpace": 1, + "SDN.Allocate": 1, + "Sys.PowerMgmt": 1, + "User.Modify": 1, + "Datastore.Allocate": 1, + "VM.PowerMgmt": 1, + "VM.Config.Options": 1, + "Permissions.Modify": 1, + "VM.Snapshot": 1, + "VM.Migrate": 1, + "VM.Console": 1, + "Realm.AllocateUser": 1, + "Datastore.AllocateTemplate": 1, + "VM.Monitor": 1, + "Sys.Audit": 1, + "VM.Config.Disk": 1, + "Sys.Incoming": 1, + "VM.Config.Memory": 1, + "VM.Config.HWType": 1, + "VM.Backup": 1, + "SDN.Use": 1, + "Group.Allocate": 1, + "VM.Config.CDROM": 1, + "Datastore.Audit": 1, + "VM.Audit": 1, + "VM.Config.CPU": 1 + }, + "/vms": { + "VM.Snapshot.Rollback": 1, + "VM.Config.Network": 1, + "Sys.Console": 1, + "SDN.Audit": 1, + "Pool.Audit": 1, + "VM.Config.Cloudinit": 1, + "Sys.Syslog": 1, + "VM.Allocate": 1, + "Pool.Allocate": 1, + "Sys.Modify": 1, + "Realm.Allocate": 1, + "VM.Clone": 1, + "SDN.Allocate": 1, + "Datastore.AllocateSpace": 1, + "Datastore.Allocate": 1, + "User.Modify": 1, + "Sys.PowerMgmt": 1, + "Permissions.Modify": 1, + "VM.Config.Options": 1, + "VM.PowerMgmt": 1, + "VM.Console": 1, + "VM.Migrate": 1, + "VM.Snapshot": 1, + "Realm.AllocateUser": 1, + "Datastore.AllocateTemplate": 1, + "Sys.Audit": 1, + "VM.Monitor": 1, + "VM.Config.HWType": 1, + "VM.Backup": 1, + "Sys.Incoming": 1, + "VM.Config.Memory": 1, + "VM.Config.Disk": 1, + "Group.Allocate": 1, + "SDN.Use": 1, + "Datastore.Audit": 1, + "VM.Config.CDROM": 1, + "VM.Config.CPU": 1, + "VM.Audit": 1 + }, + "/sdn": { + "VM.Console": 1, + "VM.Snapshot": 1, + "VM.Migrate": 1, + "Realm.AllocateUser": 1, + "Datastore.AllocateTemplate": 1, + "Sys.Audit": 1, + "VM.Monitor": 1, + "VM.Config.HWType": 1, + "VM.Backup": 1, + "Sys.Incoming": 1, + "VM.Config.Memory": 1, + "VM.Config.Disk": 1, + "SDN.Use": 1, + "Group.Allocate": 1, + "Datastore.Audit": 1, + "VM.Config.CDROM": 1, + "VM.Config.CPU": 1, + "VM.Audit": 1, + "VM.Snapshot.Rollback": 1, + "VM.Config.Network": 1, + "Pool.Audit": 1, + "SDN.Audit": 1, + "Sys.Console": 1, + "VM.Config.Cloudinit": 1, + "Sys.Syslog": 1, + "Pool.Allocate": 1, + "VM.Allocate": 1, + "Sys.Modify": 1, + "VM.Clone": 1, + "Realm.Allocate": 1, + "Datastore.AllocateSpace": 1, + "SDN.Allocate": 1, + "User.Modify": 1, + "Sys.PowerMgmt": 1, + "Datastore.Allocate": 1, + "VM.Config.Options": 1, + "Permissions.Modify": 1, + "VM.PowerMgmt": 1 + }, + "/nodes": { + "Datastore.Allocate": 1, + "User.Modify": 1, + "Sys.PowerMgmt": 1, + "Permissions.Modify": 1, + "VM.Config.Options": 1, + "VM.PowerMgmt": 1, + "Realm.Allocate": 1, + "VM.Clone": 1, + "SDN.Allocate": 1, + "Datastore.AllocateSpace": 1, + "VM.Allocate": 1, + "Pool.Allocate": 1, + "Sys.Modify": 1, + "VM.Snapshot.Rollback": 1, + "VM.Config.Network": 1, + "Sys.Console": 1, + "SDN.Audit": 1, + "Pool.Audit": 1, + "VM.Config.Cloudinit": 1, + "Sys.Syslog": 1, + "Datastore.Audit": 1, + "VM.Config.CDROM": 1, + "VM.Audit": 1, + "VM.Config.CPU": 1, + "VM.Config.HWType": 1, + "VM.Backup": 1, + "VM.Config.Memory": 1, + "Sys.Incoming": 1, + "VM.Config.Disk": 1, + "Group.Allocate": 1, + "SDN.Use": 1, + "Datastore.AllocateTemplate": 1, + "Realm.AllocateUser": 1, + "Sys.Audit": 1, + "VM.Monitor": 1, + "VM.Console": 1, + "VM.Migrate": 1, + "VM.Snapshot": 1 + }, + "/": { + "Realm.AllocateUser": 1, + "Datastore.AllocateTemplate": 1, + "Sys.Audit": 1, + "VM.Monitor": 1, + "VM.Console": 1, + "VM.Migrate": 1, + "VM.Snapshot": 1, + "Datastore.Audit": 1, + "VM.Config.CDROM": 1, + "VM.Audit": 1, + "VM.Config.CPU": 1, + "VM.Backup": 1, + "VM.Config.HWType": 1, + "VM.Config.Disk": 1, + "Sys.Incoming": 1, + "VM.Config.Memory": 1, + "Group.Allocate": 1, + "SDN.Use": 1, + "VM.Allocate": 1, + "Pool.Allocate": 1, + "Sys.Modify": 1, + "VM.Config.Network": 1, + "VM.Snapshot.Rollback": 1, + "SDN.Audit": 1, + "Sys.Console": 1, + "Pool.Audit": 1, + "VM.Config.Cloudinit": 1, + "Sys.Syslog": 1, + "Datastore.Allocate": 1, + "User.Modify": 1, + "Sys.PowerMgmt": 1, + "Permissions.Modify": 1, + "VM.Config.Options": 1, + "VM.PowerMgmt": 1, + "Realm.Allocate": 1, + "VM.Clone": 1, + "SDN.Allocate": 1, + "Datastore.AllocateSpace": 1 + }, + "/access/groups": { + "VM.Migrate": 1, + "VM.Snapshot": 1, + "VM.Console": 1, + "VM.Monitor": 1, + "Sys.Audit": 1, + "Realm.AllocateUser": 1, + "Datastore.AllocateTemplate": 1, + "Group.Allocate": 1, + "SDN.Use": 1, + "Sys.Incoming": 1, + "VM.Config.Disk": 1, + "VM.Config.Memory": 1, + "VM.Backup": 1, + "VM.Config.HWType": 1, + "VM.Config.CPU": 1, + "VM.Audit": 1, + "VM.Config.CDROM": 1, + "Datastore.Audit": 1, + "Sys.Syslog": 1, + "VM.Config.Cloudinit": 1, + "Sys.Console": 1, + "SDN.Audit": 1, + "Pool.Audit": 1, + "VM.Snapshot.Rollback": 1, + "VM.Config.Network": 1, + "Sys.Modify": 1, + "VM.Allocate": 1, + "Pool.Allocate": 1, + "SDN.Allocate": 1, + "Datastore.AllocateSpace": 1, + "Realm.Allocate": 1, + "VM.Clone": 1, + "VM.PowerMgmt": 1, + "Permissions.Modify": 1, + "VM.Config.Options": 1, + "Datastore.Allocate": 1, + "User.Modify": 1, + "Sys.PowerMgmt": 1 + } + } +}`) + + gock.New(config.C.URI). + Get("^/access/permissions$"). + MatchParams(map[string]string{ + "path": "path", + }). + Reply(200). + JSON(`{ + "data": { + "path": { + "permission": 1 + } + } +}`) + + // user with no access + gock.New(config.C.URI). + Get("^/access/permissions"). + MatchParams(map[string]string{ + "userid": "userid", + }). + Reply(200). + JSON(`{ + "data": { + "path": { + "permission": 2 + } + } +}`) + + // user with no access + gock.New(config.C.URI). + Get("^/access/permissions"). + MatchParams(map[string]string{ + "path": "path", + "userid": "userid", + }). + Reply(200). + JSON(`{ + "data": { + "path": { + "permission": 3 + } + } +}`) } func ticket() { gock.New(config.C.URI). - Get("/access/ticket"). + Get("^/access/ticket$"). Reply(200). JSON(`{"data": null}`) + + gock.New(config.C.URI). + Post("^/access/ticket$"). + Reply(200). + JSON(`{ + "data": { + "username": "root@pam", + "CSRFPreventionToken": "64E10CBA:YDNz71IKnE0sWsm1SbV1PGwz3hAyprvygQ7SBkxHVtE", + "cap": { + "sdn": { + "SDN.Audit": 1, + "SDN.Allocate": 1, + "SDN.Use": 1, + "Permissions.Modify": 1 + }, + "access": { + "Group.Allocate": 1, + "User.Modify": 1, + "Permissions.Modify": 1 + }, + "dc": { + "SDN.Allocate": 1, + "SDN.Audit": 1, + "SDN.Use": 1, + "Sys.Audit": 1 + }, + "nodes": { + "Sys.Modify": 1, + "Sys.Syslog": 1, + "Sys.Audit": 1, + "Sys.Console": 1, + "Permissions.Modify": 1, + "Sys.Incoming": 1, + "Sys.PowerMgmt": 1 + }, + "storage": { + "Datastore.Allocate": 1, + "Datastore.Audit": 1, + "Datastore.AllocateTemplate": 1, + "Datastore.AllocateSpace": 1, + "Permissions.Modify": 1 + }, + "vms": { + "VM.Config.CPU": 1, + "VM.Config.HWType": 1, + "VM.Clone": 1, + "VM.Allocate": 1, + "Permissions.Modify": 1, + "VM.Config.Options": 1, + "VM.Config.Memory": 1, + "VM.Audit": 1, + "VM.Monitor": 1, + "VM.Snapshot.Rollback": 1, + "VM.Config.Network": 1, + "VM.Config.Cloudinit": 1, + "VM.Backup": 1, + "VM.Migrate": 1, + "VM.Config.Disk": 1, + "VM.PowerMgmt": 1, + "VM.Config.CDROM": 1, + "VM.Console": 1, + "VM.Snapshot": 1 + } + }, + "clustername": "pve-cluster", + "ticket": "PVE:root@pam:64E10CBA::yTMqV7BmOXUCzb0ODceFH7F+Uy3gQTlp3sepUzIicpL2KeJ4finWjuZ9SBZg/iTz7tACDGvnX0biv6JMZvYBuqzWu0S3eF6xrLX4A3YLahhWaMJJ4Dw8hIquSO5AMQr3Ea3xdN5CcLIuW8hPOLHrPFzDC2MDk6e6VtJ9lWF5htz8nq6ge+kcwZBgB80ZABc+lIwtcB1UcJ8NY5EYGS9czcEXSse2xmG1j2F1+gMfoF+4O7wiCV0iHGabG+8n3oEBZUE89jhzjQoVCGCzVpmxYpag+5I4+W+POZm8DzQCdvPmynH9fAT6bSD8Vu+le8aHGigoKz81xNMsFxIjd1Zr2g==" + } +}`) } func user() { diff --git a/types.go b/types.go index 3af7b2f..2fc2fcf 100644 --- a/types.go +++ b/types.go @@ -19,17 +19,28 @@ var ( type Credentials struct { Username string `json:"username"` Password string `json:"password"` - Otp string `json:"otp,omitempty"` + Otp string `json:"otp,omitempty"` // One-time password for Two-factor authentication. Path string `json:"path,omitempty"` Privs string `json:"privs,omitempty"` Realm string `json:"realm,omitempty"` } +type Permission map[string]int +type Permissions map[string]Permission + +type PermissionsOptions struct { + Path string // path to limit the return e.g. / or /nodes + UserID string // username e.g. root@pam or token +} + type Session struct { Username string `json:"username"` - CsrfPreventionToken string `json:"CSRFPreventionToken,omitempty"` - ClusterName string `json:"clustername,omitempty"` - Ticket string `json:"ticket,omitempty"` + CSRFPreventionToken string `json:"CSRFPreventionToken,omitempty"` + + // Cap is being returned but not documented in the API docs, likely will get rewritten later with better types + Cap map[string]map[string]int `json:"cap,omitempty"` + ClusterName string `json:"clustername,omitempty"` + Ticket string `json:"ticket,omitempty"` } type Version struct {