From a50d081356d46ba0824e6ac6e3c58bf35dd2f2c2 Mon Sep 17 00:00:00 2001 From: telyn Date: Mon, 3 Dec 2018 12:19:20 +0000 Subject: [PATCH 01/36] Add RevokeAPIKey and test --- lib/requests/brain/revoke_api_key.go | 22 ++++++++ lib/requests/brain/revoke_api_key_test.go | 64 +++++++++++++++++++++++ lib/testutil/request_test_spec.go | 5 +- 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 lib/requests/brain/revoke_api_key.go create mode 100644 lib/requests/brain/revoke_api_key_test.go diff --git a/lib/requests/brain/revoke_api_key.go b/lib/requests/brain/revoke_api_key.go new file mode 100644 index 00000000..676f3e36 --- /dev/null +++ b/lib/requests/brain/revoke_api_key.go @@ -0,0 +1,22 @@ +package brain + +import ( + "strconv" + + "github.com/BytemarkHosting/bytemark-client/lib" + "github.com/BytemarkHosting/bytemark-client/lib/brain" +) + +// RevokeAPIKey takes an API key id and revokes it. +func RevokeAPIKey(client lib.Client, id int) (err error) { + r, err := client.BuildRequest("PUT", lib.BrainEndpoint, "/api_keys/%s", strconv.Itoa(id)) + if err != nil { + return + } + + apiKey := brain.APIKey{ + ExpiresAt: "00:00:00", + } + r.MarshalAndRun(spec, &apiKey) + return +} diff --git a/lib/requests/brain/revoke_api_key_test.go b/lib/requests/brain/revoke_api_key_test.go new file mode 100644 index 00000000..4f989be6 --- /dev/null +++ b/lib/requests/brain/revoke_api_key_test.go @@ -0,0 +1,64 @@ +package brain + +import ( + "fmt" + "testing" + + "github.com/BytemarkHosting/bytemark-client/lib" + "github.com/BytemarkHosting/bytemark-client/lib/brain" + "github.com/BytemarkHosting/bytemark-client/lib/testutil" + "github.com/BytemarkHosting/bytemark-client/lib/testutil/assert" +) + +func TestRevokeAPIKey(t *testing.T) { + tests := []struct { + id int + requestExpected map[string]interface{} + statusCode int + shouldErr error + }{ + { + id: 9, + requestExpected: map[string]interface{}{ + "id": 9, + "expires_at": "00:00:00", + }, + statusCode: 200, + shouldErr: false, + }, { + id: 25, + requestExpected: map[string]interface{}{ + "id": 25, + "expires_at": "00:00:00", + }, + statusCode: 500, + shouldErr: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + rts := testutil.RequestTestSpec{ + Method: "PUT", + URL: fmt.Sprintf("/api_keys/%d", test.id), + Endpoint: lib.BrainEndpoint, + Response: brain.APIKey{ + ID: test.id, + Label: "jeff's cool api key for arctic exploration", + APIKey: "fake-api-key-whatever", + ExpiresAt: "2018-11-12T00:00:00Z", + }, + StatusCode: test.statusCode, + AssertRequest: assert.BodyUnmarshalEqual(test.requestExpected), + } + rts.Run(t, test.Name, true, func(client lib.Client) { + err := brainRequests.RevokeAPIKey(client, test.id) + if err != nil && !test.shouldErr { + t.Errorf("Unexpected error: %v", err) + } else if err == nil && test.shouldErr { + t.Error("Error expected but not returned") + } + }) + }) + } +} diff --git a/lib/testutil/request_test_spec.go b/lib/testutil/request_test_spec.go index c4c6bb7b..f389c641 100644 --- a/lib/testutil/request_test_spec.go +++ b/lib/testutil/request_test_spec.go @@ -21,7 +21,8 @@ type RequestTestFunc func(lib.Client) type RequestTestSpec struct { // MuxHandlers will be used if defined - this allows for the test to support // multiple endpoints, URLs, methods, etc. while still keeping as DRY as - // possible. Otherwise, set the Method, Endpoint, URL, ExpectedRequest, and Response fields. + // possible. Otherwise, set the Method, Endpoint, URL, AssertRequest, + // Response and StatusCode. MuxHandlers *MuxHandlers // Method is used to assert that the request was given the correct type @@ -35,6 +36,8 @@ type RequestTestSpec struct { // to use a raw string (i.e. if you don't want to use JSON) cast it to // encoding/json.RawMessage - this will be reproduced verbatim Response interface{} + // StatusCode is the status code that will be returned + StatusCode int // AssertRequest is an optional func which will get called to check the // request object further - for example to check the URL has particular // query string keys From 63bd8c4573724a909c5d47fd5aacea4263292b74 Mon Sep 17 00:00:00 2001 From: telyn Date: Thu, 29 Nov 2018 16:58:46 +0000 Subject: [PATCH 02/36] add CreateAPIKey and half-test it --- lib/brain/api_key.go | 9 ++++ lib/requests/brain/create_api_key.go | 35 ++++++++++++ lib/requests/brain/create_api_key_test.go | 65 +++++++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 lib/brain/api_key.go create mode 100644 lib/requests/brain/create_api_key.go create mode 100644 lib/requests/brain/create_api_key_test.go diff --git a/lib/brain/api_key.go b/lib/brain/api_key.go new file mode 100644 index 00000000..f896df0d --- /dev/null +++ b/lib/brain/api_key.go @@ -0,0 +1,9 @@ +package brain + +type APIKey struct { + ID int `json:"id,omitempty"` + UserID int `json:"user_id,omitempty"` + Name string `json:"id,omitempty"` + APIKey string `json:"id,omitempty"` + ExpiresAt string `json:"id,omitempty"` +} diff --git a/lib/requests/brain/create_api_key.go b/lib/requests/brain/create_api_key.go new file mode 100644 index 00000000..4e130415 --- /dev/null +++ b/lib/requests/brain/create_api_key.go @@ -0,0 +1,35 @@ +package brain + +import ( + "errors" + + "github.com/BytemarkHosting/bytemark-client/lib" + "github.com/BytemarkHosting/bytemark-client/lib/brain" +) + +// CreateAPIKey creates an API key for the given user, then returns it. +// Neither the ID nor APIKey field should be specified in the spec. +// username may be blank if the spec.UserID is set. +func CreateAPIKey(client lib.Client, username string, spec brain.APIKey) (apiKey brain.APIKey, err error) { + if spec.UserID != 0 && username != "" { + err = errors.New("only specify one of username and spec.UserID") + return + } + if spec.UserID == 0 && username == "" { + err = errors.New("one of user and spec.UserID must be specified") + return + } + if spec.UserID == 0 { + user, err := client.GetUser(username) + if err != nil { + return apiKey, err + } + spec.UserID = user.ID + } + r, err := client.BuildRequest("POST", lib.BrainEndpoint, "/api_keys") + if err != nil { + return + } + _, _, err = r.MarshalAndRun(spec, apiKey) + return +} diff --git a/lib/requests/brain/create_api_key_test.go b/lib/requests/brain/create_api_key_test.go new file mode 100644 index 00000000..8de0dec8 --- /dev/null +++ b/lib/requests/brain/create_api_key_test.go @@ -0,0 +1,65 @@ +package brain + +import ( + "encoding/json" + "net/http" + "testing" + + "github.com/BytemarkHosting/bytemark-client/lib/brain" + "github.com/BytemarkHosting/bytemark-client/lib/testutil" + "github.com/BytemarkHosting/bytemark-client/lib/testutil/assert" +) + +func TestCreateAPIKey(t *testing.T) { + tests := []struct { + name string + user string + spec brain.APIKey + // if the user ID needs to be retrieved from the api (when user == "" and spec.UserID == 0) + // then specify the user id to return here + userID int + expect map[string]interface{} + response brain.APIKey + responseErr error + + shouldErr bool + }{ + { + name: "neither user nor UserID set", + spec: brain.APIKey{ + ExpiresAt: "2018-03-03T03:03:03Z", + }, + shouldErr: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + rts := testutil.RequestTestSpec{ + MuxHandlers: &testutil.MuxHandlers{ + Brain: testutil.Mux{ + "/api_keys": func(wr http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.name, "POST", r.Method) + assert.BodyUnmarshalEqual(test.expect)(t, test.name, r) + bytes, err := json.Marshal(test.response) + if err != nil { + t.Fatal(err) + } + wr.Write(bytes) + }, + }, + }, + } + if test.userID != 0 { + rts.MuxHandlers.Brain["/users/"+test.user] = func(wr http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.name, "GET", r.Method) + bytes, err := json.Marshal(brain.User{ID: test.userID}) + if err != nil { + t.Fatal(err) + } + wr.Write(bytes) + } + } + + }) + } +} From 6494098b9250ca8af44cbc0460348850b71b0dda Mon Sep 17 00:00:00 2001 From: telyn Date: Fri, 30 Nov 2018 11:53:36 +0000 Subject: [PATCH 03/36] finish writing test framework for CreateAPIKey --- lib/brain/api_key.go | 6 +- lib/requests/brain/create_api_key.go | 2 +- lib/requests/brain/create_api_key_test.go | 71 ++++++++++++++++++++--- 3 files changed, 67 insertions(+), 12 deletions(-) diff --git a/lib/brain/api_key.go b/lib/brain/api_key.go index f896df0d..0243cf40 100644 --- a/lib/brain/api_key.go +++ b/lib/brain/api_key.go @@ -3,7 +3,7 @@ package brain type APIKey struct { ID int `json:"id,omitempty"` UserID int `json:"user_id,omitempty"` - Name string `json:"id,omitempty"` - APIKey string `json:"id,omitempty"` - ExpiresAt string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + APIKey string `json:"api_key,omitempty"` + ExpiresAt string `json:"expires_at,omitempty"` } diff --git a/lib/requests/brain/create_api_key.go b/lib/requests/brain/create_api_key.go index 4e130415..7534f588 100644 --- a/lib/requests/brain/create_api_key.go +++ b/lib/requests/brain/create_api_key.go @@ -30,6 +30,6 @@ func CreateAPIKey(client lib.Client, username string, spec brain.APIKey) (apiKey if err != nil { return } - _, _, err = r.MarshalAndRun(spec, apiKey) + _, _, err = r.MarshalAndRun(spec, &apiKey) return } diff --git a/lib/requests/brain/create_api_key_test.go b/lib/requests/brain/create_api_key_test.go index 8de0dec8..7c3ede9b 100644 --- a/lib/requests/brain/create_api_key_test.go +++ b/lib/requests/brain/create_api_key_test.go @@ -1,11 +1,13 @@ -package brain +package brain_test import ( "encoding/json" "net/http" "testing" + "github.com/BytemarkHosting/bytemark-client/lib" "github.com/BytemarkHosting/bytemark-client/lib/brain" + brainRequests "github.com/BytemarkHosting/bytemark-client/lib/requests/brain" "github.com/BytemarkHosting/bytemark-client/lib/testutil" "github.com/BytemarkHosting/bytemark-client/lib/testutil/assert" ) @@ -17,10 +19,10 @@ func TestCreateAPIKey(t *testing.T) { spec brain.APIKey // if the user ID needs to be retrieved from the api (when user == "" and spec.UserID == 0) // then specify the user id to return here - userID int - expect map[string]interface{} - response brain.APIKey - responseErr error + userID int + requestExpected map[string]interface{} + response brain.APIKey + responseErr error shouldErr bool }{ @@ -30,6 +32,47 @@ func TestCreateAPIKey(t *testing.T) { ExpiresAt: "2018-03-03T03:03:03Z", }, shouldErr: true, + }, { + name: "both user and UserID set", + user: "jeff", + spec: brain.APIKey{ + UserID: 4123.0, + ExpiresAt: "2018-03-03T03:03:03Z", + }, + shouldErr: true, + }, { + name: "user set", + user: "jeff", + spec: brain.APIKey{ + ExpiresAt: "2019-04-04T04:04:04Z", + }, + userID: 1111, + requestExpected: map[string]interface{}{ + "user_id": 1111.0, + "expires_at": "2019-04-04T04:04:04Z", + }, + response: brain.APIKey{ + ID: 9, + APIKey: "fake-api-key", + UserID: 1111, + ExpiresAt: "2019-04-04T04:04:04Z", + }, + }, { + name: "UserID set", + spec: brain.APIKey{ + UserID: 4123, + ExpiresAt: "2018-05-05T05:05:05Z", + }, + requestExpected: map[string]interface{}{ + "user_id": 4123.0, + "expires_at": "2018-05-05T05:05:05Z", + }, + response: brain.APIKey{ + ID: 12435, + APIKey: "this-key-be-fake", + UserID: 4123, + ExpiresAt: "2018-05-05T05:05:05Z", + }, }, } for _, test := range tests { @@ -38,8 +81,8 @@ func TestCreateAPIKey(t *testing.T) { MuxHandlers: &testutil.MuxHandlers{ Brain: testutil.Mux{ "/api_keys": func(wr http.ResponseWriter, r *http.Request) { - assert.Equal(t, test.name, "POST", r.Method) - assert.BodyUnmarshalEqual(test.expect)(t, test.name, r) + assert.Equal(t, "method", "POST", r.Method) + assert.BodyUnmarshalEqual(test.requestExpected)(t, "request", r) bytes, err := json.Marshal(test.response) if err != nil { t.Fatal(err) @@ -51,7 +94,7 @@ func TestCreateAPIKey(t *testing.T) { } if test.userID != 0 { rts.MuxHandlers.Brain["/users/"+test.user] = func(wr http.ResponseWriter, r *http.Request) { - assert.Equal(t, test.name, "GET", r.Method) + assert.Equal(t, "", "GET", r.Method) bytes, err := json.Marshal(brain.User{ID: test.userID}) if err != nil { t.Fatal(err) @@ -60,6 +103,18 @@ func TestCreateAPIKey(t *testing.T) { } } + rts.Run(t, "", true, func(client lib.Client) { + apikey, err := brainRequests.CreateAPIKey(client, test.user, test.spec) + if err != nil && !test.shouldErr { + t.Errorf("Unexpected error: %v", err) + } else if err == nil && test.shouldErr { + t.Error("Error expected but not returned") + } + + if !test.shouldErr { + assert.Equal(t, "response", test.response, apikey) + } + }) }) } } From 1f834e0a956e673a67e391c8f73c1f3d16d2cc43 Mon Sep 17 00:00:00 2001 From: telyn Date: Fri, 30 Nov 2018 12:16:08 +0000 Subject: [PATCH 04/36] add an error test --- lib/requests/brain/create_api_key_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/requests/brain/create_api_key_test.go b/lib/requests/brain/create_api_key_test.go index 7c3ede9b..118b32d2 100644 --- a/lib/requests/brain/create_api_key_test.go +++ b/lib/requests/brain/create_api_key_test.go @@ -2,6 +2,7 @@ package brain_test import ( "encoding/json" + "errors" "net/http" "testing" @@ -73,6 +74,18 @@ func TestCreateAPIKey(t *testing.T) { UserID: 4123, ExpiresAt: "2018-05-05T05:05:05Z", }, + }, { + name: "error!!", + spec: brain.APIKey{ + UserID: 4123, + ExpiresAt: "2018-05-05T05:05:05Z", + }, + requestExpected: map[string]interface{}{ + "user_id": 4123.0, + "expires_at": "2018-05-05T05:05:05Z", + }, + shouldErr: true, + responseErr: errors.New("the turboencabulator couldn't effectively prevent side fumbling"), }, } for _, test := range tests { @@ -83,6 +96,11 @@ func TestCreateAPIKey(t *testing.T) { "/api_keys": func(wr http.ResponseWriter, r *http.Request) { assert.Equal(t, "method", "POST", r.Method) assert.BodyUnmarshalEqual(test.requestExpected)(t, "request", r) + + if test.responseErr != nil { + wr.WriteHeader(500) + wr.Write([]byte(test.responseErr.Error())) + } bytes, err := json.Marshal(test.response) if err != nil { t.Fatal(err) From 823cafd7f95c1cea2bf226a6e379ab0f4cdeac69 Mon Sep 17 00:00:00 2001 From: telyn Date: Mon, 3 Dec 2018 12:32:40 +0000 Subject: [PATCH 05/36] Fix up RevokeAPIKey tests and finish implementing RequestTestSpec.StatusCode --- lib/requests/brain/revoke_api_key.go | 2 +- lib/requests/brain/revoke_api_key_test.go | 29 ++++++++++------------- lib/testutil/request_test_spec.go | 7 +++++- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/requests/brain/revoke_api_key.go b/lib/requests/brain/revoke_api_key.go index 676f3e36..6bd94d07 100644 --- a/lib/requests/brain/revoke_api_key.go +++ b/lib/requests/brain/revoke_api_key.go @@ -17,6 +17,6 @@ func RevokeAPIKey(client lib.Client, id int) (err error) { apiKey := brain.APIKey{ ExpiresAt: "00:00:00", } - r.MarshalAndRun(spec, &apiKey) + _, _, err = r.MarshalAndRun(apiKey, nil) return } diff --git a/lib/requests/brain/revoke_api_key_test.go b/lib/requests/brain/revoke_api_key_test.go index 4f989be6..4a0fec2a 100644 --- a/lib/requests/brain/revoke_api_key_test.go +++ b/lib/requests/brain/revoke_api_key_test.go @@ -1,34 +1,35 @@ -package brain +package brain_test import ( "fmt" "testing" "github.com/BytemarkHosting/bytemark-client/lib" - "github.com/BytemarkHosting/bytemark-client/lib/brain" + brainRequests "github.com/BytemarkHosting/bytemark-client/lib/requests/brain" "github.com/BytemarkHosting/bytemark-client/lib/testutil" "github.com/BytemarkHosting/bytemark-client/lib/testutil/assert" ) func TestRevokeAPIKey(t *testing.T) { tests := []struct { + name string id int requestExpected map[string]interface{} statusCode int - shouldErr error + shouldErr bool }{ { - id: 9, + name: "success", + id: 9, requestExpected: map[string]interface{}{ - "id": 9, "expires_at": "00:00:00", }, statusCode: 200, shouldErr: false, }, { - id: 25, + name: "error 500", + id: 25, requestExpected: map[string]interface{}{ - "id": 25, "expires_at": "00:00:00", }, statusCode: 500, @@ -39,19 +40,13 @@ func TestRevokeAPIKey(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { rts := testutil.RequestTestSpec{ - Method: "PUT", - URL: fmt.Sprintf("/api_keys/%d", test.id), - Endpoint: lib.BrainEndpoint, - Response: brain.APIKey{ - ID: test.id, - Label: "jeff's cool api key for arctic exploration", - APIKey: "fake-api-key-whatever", - ExpiresAt: "2018-11-12T00:00:00Z", - }, + Method: "PUT", + URL: fmt.Sprintf("/api_keys/%d", test.id), + Endpoint: lib.BrainEndpoint, StatusCode: test.statusCode, AssertRequest: assert.BodyUnmarshalEqual(test.requestExpected), } - rts.Run(t, test.Name, true, func(client lib.Client) { + rts.Run(t, test.name, true, func(client lib.Client) { err := brainRequests.RevokeAPIKey(client, test.id) if err != nil && !test.shouldErr { t.Errorf("Unexpected error: %v", err) diff --git a/lib/testutil/request_test_spec.go b/lib/testutil/request_test_spec.go index f389c641..7c3998d8 100644 --- a/lib/testutil/request_test_spec.go +++ b/lib/testutil/request_test_spec.go @@ -36,7 +36,9 @@ type RequestTestSpec struct { // to use a raw string (i.e. if you don't want to use JSON) cast it to // encoding/json.RawMessage - this will be reproduced verbatim Response interface{} - // StatusCode is the status code that will be returned + // StatusCode is the status code that will be returned. If unset, will + // default to whatever http.ResponseWriter.Write defaults to. + // Only used if MuxHandlers is nil StatusCode int // AssertRequest is an optional func which will get called to check the // request object further - for example to check the URL has particular @@ -62,6 +64,9 @@ func (rts *RequestTestSpec) handlerFunc(t *testing.T, testName string, auth bool if rts.AssertRequest != nil { rts.AssertRequest(t, testName, r) } + if rts.StatusCode != 0 { + wr.WriteHeader(rts.StatusCode) + } WriteJSON(t, wr, rts.Response) } } From 7bd2866a9322855e7b3ea82e828ffc63baca8bc7 Mon Sep 17 00:00:00 2001 From: telyn Date: Mon, 3 Dec 2018 12:54:24 +0000 Subject: [PATCH 06/36] Correct APIKey - has Label not Name --- lib/brain/api_key.go | 2 +- lib/requests/brain/create_api_key_test.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/brain/api_key.go b/lib/brain/api_key.go index 0243cf40..9b23a56d 100644 --- a/lib/brain/api_key.go +++ b/lib/brain/api_key.go @@ -3,7 +3,7 @@ package brain type APIKey struct { ID int `json:"id,omitempty"` UserID int `json:"user_id,omitempty"` - Name string `json:"name,omitempty"` + Label string `json:"label,omitempty"` APIKey string `json:"api_key,omitempty"` ExpiresAt string `json:"expires_at,omitempty"` } diff --git a/lib/requests/brain/create_api_key_test.go b/lib/requests/brain/create_api_key_test.go index 118b32d2..2f4a083e 100644 --- a/lib/requests/brain/create_api_key_test.go +++ b/lib/requests/brain/create_api_key_test.go @@ -63,15 +63,18 @@ func TestCreateAPIKey(t *testing.T) { spec: brain.APIKey{ UserID: 4123, ExpiresAt: "2018-05-05T05:05:05Z", + Label: "jeffs-cool-key-for-arctic-exploration", }, requestExpected: map[string]interface{}{ "user_id": 4123.0, "expires_at": "2018-05-05T05:05:05Z", + "label": "jeffs-cool-key-for-arctic-exploration", }, response: brain.APIKey{ ID: 12435, APIKey: "this-key-be-fake", UserID: 4123, + Label: "jeffs-cool-key-for-arctic-exploration", ExpiresAt: "2018-05-05T05:05:05Z", }, }, { From 2a2789ec11dc37579f145d0aa75fcad22a10c1cc Mon Sep 17 00:00:00 2001 From: telyn Date: Mon, 3 Dec 2018 14:19:34 +0000 Subject: [PATCH 07/36] add GetAPIKeys --- lib/brain/api_key.go | 21 +++++-- lib/requests/brain/get_api_keys.go | 18 ++++++ lib/requests/brain/get_api_keys_test.go | 75 +++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 lib/requests/brain/get_api_keys.go create mode 100644 lib/requests/brain/get_api_keys_test.go diff --git a/lib/brain/api_key.go b/lib/brain/api_key.go index 9b23a56d..5c80eefd 100644 --- a/lib/brain/api_key.go +++ b/lib/brain/api_key.go @@ -1,9 +1,22 @@ package brain +// APIKey represents an api_key in the brain. type APIKey struct { - ID int `json:"id,omitempty"` - UserID int `json:"user_id,omitempty"` - Label string `json:"label,omitempty"` - APIKey string `json:"api_key,omitempty"` + // ID must not be set during creation. + ID int `json:"id,omitempty"` + UserID int `json:"user_id,omitempty"` + // Label is a friendly display name for this API key + Label string `json:"label,omitempty"` + // API key is the actual key. To use it, it must be prepended with + // 'apikey.' in the HTTP Authorization header. For example, if the api key + // is xpq21, the HTTP headers should include `Authorization: Bearer apikey.xpq21` + APIKey string `json:"api_key,omitempty"` + // ExpiresAt should be a time or datetime in HH:MM:SS or + // YYYY-MM-DDTHH:MM:SS.msZ where T is a literal T, .ms are optional + // microseconds and Z is either literal Z (meaning UTC) or a timezone + // specified like -600 or +1200 ExpiresAt string `json:"expires_at,omitempty"` + // Privileges cannot be set at creation or update time, but are returned by + // the brain when view=overview. + Privileges Privileges `json:"privileges,omitempty"` } diff --git a/lib/requests/brain/get_api_keys.go b/lib/requests/brain/get_api_keys.go new file mode 100644 index 00000000..c3b45a28 --- /dev/null +++ b/lib/requests/brain/get_api_keys.go @@ -0,0 +1,18 @@ +package brain + +import ( + "github.com/BytemarkHosting/bytemark-client/lib" + "github.com/BytemarkHosting/bytemark-client/lib/brain" +) + +// GetAPIKeys gets all API keys that you can currently see. +// In general this means those for your user, but users with cluster_admin +// will be able to see all API keys on the cluster +func GetAPIKeys(client lib.Client) (apiKeys []brain.APIKey, err error) { + r, err := client.BuildRequest("GET", lib.BrainEndpoint, "/api_keys?view=overview") + if err != nil { + return + } + _, _, err = r.MarshalAndRun(nil, &apiKeys) + return +} diff --git a/lib/requests/brain/get_api_keys_test.go b/lib/requests/brain/get_api_keys_test.go new file mode 100644 index 00000000..b469b17d --- /dev/null +++ b/lib/requests/brain/get_api_keys_test.go @@ -0,0 +1,75 @@ +package brain_test + +import ( + "testing" + + "github.com/BytemarkHosting/bytemark-client/lib" + "github.com/BytemarkHosting/bytemark-client/lib/brain" + brainRequests "github.com/BytemarkHosting/bytemark-client/lib/requests/brain" + "github.com/BytemarkHosting/bytemark-client/lib/testutil" + "github.com/BytemarkHosting/bytemark-client/lib/testutil/assert" +) + +func TestGetAPIKeys(t *testing.T) { + tests := []struct { + name string + response []brain.APIKey + statusCode int + shouldErr bool + }{ + { + name: "empty array", + }, + { + name: "http 500", + statusCode: 500, + shouldErr: true, + }, + { + name: "some keys", + response: []brain.APIKey{ + { + ID: 6, + UserID: 2152, + Label: "gitlab-autoscaling", + APIKey: "extremelyrandomdatahereuhhhhh7", + ExpiresAt: "2019-03-21T00:24:45.0312Z", + Privileges: brain.Privileges{ + { + Username: "dr-gitlabotopis", + APIKeyID: 6, + GroupID: 2433, + Level: "group_admin", + }, + }, + }, { + ID: 912, + UserID: 2505, + Label: "kubernetes-cloud-controller-manager", + APIKey: "keykeykeykeykeynoonesgonnaguessthat", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + rts := testutil.RequestTestSpec{ + Method: "GET", + Endpoint: lib.BrainEndpoint, + URL: "/api_keys", + StatusCode: test.statusCode, + Response: test.response, + } + rts.Run(t, test.name, true, func(client lib.Client) { + keys, err := brainRequests.GetAPIKeys(client) + if err != nil && !test.shouldErr { + t.Errorf("Unexpected error: %v", err) + } else if err == nil && test.shouldErr { + t.Error("Error expected but not returned") + } + assert.Equal(t, "api keys", test.response, keys) + }) + }) + } +} From 5ed5f0fc6c1033015050b21d7711df004b3ce56b Mon Sep 17 00:00:00 2001 From: telyn Date: Tue, 4 Dec 2018 16:15:00 +0000 Subject: [PATCH 08/36] Add APIKey.PrettyPrint --- lib/brain/api_key.go | 47 ++++++++++++ lib/brain/api_key_test.go | 92 ++++++++++++++++++++++++ lib/output/prettyprint/template_funcs.go | 25 +++++++ 3 files changed, 164 insertions(+) create mode 100644 lib/brain/api_key_test.go diff --git a/lib/brain/api_key.go b/lib/brain/api_key.go index 5c80eefd..aedcb644 100644 --- a/lib/brain/api_key.go +++ b/lib/brain/api_key.go @@ -1,5 +1,13 @@ package brain +import ( + "io" + "time" + + "github.com/BytemarkHosting/bytemark-client/lib/output" + "github.com/BytemarkHosting/bytemark-client/lib/output/prettyprint" +) + // APIKey represents an api_key in the brain. type APIKey struct { // ID must not be set during creation. @@ -20,3 +28,42 @@ type APIKey struct { // the brain when view=overview. Privileges Privileges `json:"privileges,omitempty"` } + +func (key APIKey) DefaultFields(f output.Format) string { + switch f { + case output.List: + return "ID, Label, Expired, ExpiresAt, Privileges" + } + return "ID, UserID, Label, Expired, ExpiresAt, Privileges" +} + +func (key APIKey) Expired() bool { + if key.ExpiresAt == "" { + return false + } + // TODO(telyn): not keen on this ad-hoc iso8601 parsing + expiresAt, err := time.Parse("2006-01-02T15:04:05-0700", key.ExpiresAt) + if err != nil { + return false + } + return expiresAt.Before(time.Now()) +} + +func (key APIKey) PrettyPrint(wr io.Writer, detail prettyprint.DetailLevel) error { + accountTpl := ` +{{ define "apikey_sgl" }}{{ .Label }}{{ if .Expired }} (expired){{ end }}{{ end }} +{{ define "apikey_medium" }}{{ template "apikey_sgl" . }}{{ end }} +{{ define "apikey_full" }}{{ template "apikey_sgl" . }} + Expire{{ if .Expired }}d{{ else }}s{{ end }}: {{ if eq .ExpiresAt "" }}never{{ else }}{{ .ExpiresAt }}{{ end -}} +{{ if .APIKey}} + Key: apikey.{{ .APIKey }}{{ end -}} +{{ if len .Privileges | le 1 }} + + Privileges: +{{ prettysprint .Privileges "_medium" | prefixEachLine "* " | indent 4 -}} +{{ else }} +{{ end -}} +{{ end }} +` + return prettyprint.Run(wr, accountTpl, "apikey"+string(detail), key) +} diff --git a/lib/brain/api_key_test.go b/lib/brain/api_key_test.go new file mode 100644 index 00000000..9fb9d1fd --- /dev/null +++ b/lib/brain/api_key_test.go @@ -0,0 +1,92 @@ +package brain_test + +import ( + "bytes" + "testing" + + "github.com/BytemarkHosting/bytemark-client/lib/brain" + "github.com/BytemarkHosting/bytemark-client/lib/output/prettyprint" +) + +func TestAPIKeyPrettyPrint(t *testing.T) { + tests := []struct { + name string + level prettyprint.DetailLevel + in brain.APIKey + out string + }{ + { + name: "single line", + level: prettyprint.SingleLine, + in: brain.APIKey{ + Label: "jeff", + }, + out: "jeff", + }, { + name: "single line (expired)", + level: prettyprint.SingleLine, + in: brain.APIKey{ + Label: "jeff", + ExpiresAt: "2006-01-01T01:01:01.000-0000", + }, + out: "jeff (expired)", + }, { + name: "full no privs expired", + level: prettyprint.Full, + in: brain.APIKey{ + Label: "jeff", + ExpiresAt: "2006-01-01T01:01:01.0124-0000", + }, + out: `jeff (expired) + Expired: 2006-01-01T01:01:01.0124-0000 +`, + }, { + name: "full with privs", + level: prettyprint.Full, + in: brain.APIKey{ + Label: "jeff", + ExpiresAt: "3000-01-01T01:01:01-0000", + Privileges: brain.Privileges{ + { + Username: "jeffathan", + AccountID: 23, + Level: "account_admin", + APIKeyID: 4, + }, + }, + }, + out: `jeff + Expires: 3000-01-01T01:01:01-0000 + + Privileges: + * account_admin on account #23 for jeffathan +`, + }, { + name: "full with key", + level: prettyprint.Full, + in: brain.APIKey{ + Label: "jeff", + APIKey: "abcdefgh", + ExpiresAt: "3006-01-01T01:01:01-0000", + }, + out: `jeff + Expires: 3006-01-01T01:01:01-0000 + Key: apikey.abcdefgh +`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + buf := bytes.Buffer{} + err := test.in.PrettyPrint(&buf, test.level) + if err != nil { + t.Fatal(err) + } + str := buf.String() + if str != test.out { + t.Errorf("Output didn't match expected\nexpected: %q\n actual: %q", test.out, str) + } + }) + } + +} diff --git a/lib/output/prettyprint/template_funcs.go b/lib/output/prettyprint/template_funcs.go index 39651408..2c515f3a 100644 --- a/lib/output/prettyprint/template_funcs.go +++ b/lib/output/prettyprint/template_funcs.go @@ -12,6 +12,16 @@ type TemplateFragmentMapper interface { MapTemplateFragment(templateFrag string) (strs []string, err error) } +func prefixEachLine(prefix string, input string) string { + lines := strings.Split(input, "\n") + for i := range lines { + if lines[i] != "" { + lines[i] = string(prefix) + lines[i] + } + } + return strings.Join(lines, "\n") +} + var templateFuncMap = map[string]interface{}{ // capitalize the first letter of str "capitalize": func(str string) string { @@ -39,6 +49,17 @@ var templateFuncMap = map[string]interface{}{ } return fmt.Sprintf("%s%d%ciB", lt, size, gt) }, + // indent indents every line of the input string by n spaces. + // for example indent 1 " hi\nhello" would produce " hi\n hello" + "indent": func(amount int, input string) string { + spaces := make([]rune, amount) + for i := range spaces { + spaces[i] = ' ' + } + return prefixEachLine(string(spaces), input) + + }, + "prefixEachLine": prefixEachLine, // mibgib takes a size in megabytes and formats it with a unit in either MiB or GiB, if size >= 1024. "mibgib": func(size int) string { mg := 'M' @@ -74,7 +95,9 @@ var templateFuncMap = map[string]interface{}{ }, // join joins multiple strings together with a separator "join": strings.Join, + // joinWithSpecialLast joins multiple strings together with a separator, except the last two, which are seperated by a different seperator. e.g. joinWithSpecialLast ", " " and " []string{"hi","hello","welcome","good evening"} would produce "hi, hello, welcome and good evening" "joinWithSpecialLast": func(sep string, fin string, strs []string) string { + // special cases for when there are 0, 1 or 2 strings switch len(strs) { case 0: return "" @@ -83,7 +106,9 @@ var templateFuncMap = map[string]interface{}{ case 2: return strs[0] + fin + strs[1] } + // join with one seperator most := strings.Join(strs[0:len(strs)-1], sep) + // and add the last with the 'fin' seperator. return most + fin + strs[len(strs)-1] }, } From 92b48e62d3bcfc5881d08770d5f581087c2b6fee Mon Sep 17 00:00:00 2001 From: telyn Date: Tue, 4 Dec 2018 17:21:25 +0000 Subject: [PATCH 09/36] add first draft of create api key command --- cmd/bytemark/commands/add/api_key.go | 46 ++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 cmd/bytemark/commands/add/api_key.go diff --git a/cmd/bytemark/commands/add/api_key.go b/cmd/bytemark/commands/add/api_key.go new file mode 100644 index 00000000..4981dc76 --- /dev/null +++ b/cmd/bytemark/commands/add/api_key.go @@ -0,0 +1,46 @@ +package add + +import "github.com/urfave/cli" + +func init() { + Commands = append(Commands, cli.Command{ + Name: "api key", + Aliases: []string{"apikey"}, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "expires-at", + Usage: "Date the API key should expire. Leave unset for keys that never expire", + }, + cli.StringSliceFlag{ + Name: "group", + Usage: "Group to grant the API key administrative privilege over", + }, + cli.StringSliceFlag{ + Name: "server", + Usage: "Server to grant the API key administrative privilege over", + }, + cli.StringFlag{ + Name: "user", + Usage: "User the API key will be attached to. Defaults to the user you log in as", + }, + }, + Usage: "add an API key to your Bytemark Cloud Servers user", + UsageText: "add api key [--server ]... [--group ]... [--user <", + Description: `--expires-at may be set to any date format the Brain +accepts, but we generally recommend ISO8601 format. + +Servers and groups will be searched for on the default account for the user +you are logged in as. This may trip up cluster administrators, so +bytemark-client will refuse to create an API key whose access is not a subset +of the access the specified user normally has. To create such an API key you +can either add the necessary privileges to ensure that the API key privileges +are a subset, or create the API key without privileges and add them via the +grant command, which does not have this limitation. + +Note that the API key will only currently be able to access the Bytemark Cloud +Servers API - to manage servers and groups. + +Multiple --group and --server flags (and combinations thereof) can be supplied, +and the API key will be have privileges over each that is supplied.`, + }) +} From 1c0ed603825ce581ce5e0977c73adabfbc80fe29 Mon Sep 17 00:00:00 2001 From: telyn Date: Wed, 5 Dec 2018 15:07:17 +0000 Subject: [PATCH 10/36] continue work on add api key command --- cmd/bytemark/commands/add/api_key.go | 36 ++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/cmd/bytemark/commands/add/api_key.go b/cmd/bytemark/commands/add/api_key.go index 4981dc76..c2ffc20f 100644 --- a/cmd/bytemark/commands/add/api_key.go +++ b/cmd/bytemark/commands/add/api_key.go @@ -1,6 +1,11 @@ package add -import "github.com/urfave/cli" +import ( + "github.com/BytemarkHosting/bytemark-client/cmd/bytemark/app" + "github.com/BytemarkHosting/bytemark-client/cmd/bytemark/app/args" + "github.com/BytemarkHosting/bytemark-client/cmd/bytemark/app/with" + "github.com/urfave/cli" +) func init() { Commands = append(Commands, cli.Command{ @@ -15,6 +20,10 @@ func init() { Name: "group", Usage: "Group to grant the API key administrative privilege over", }, + cli.StringFlag{ + Name: "label", + Usage: "user-friendly label for the API key", + }, cli.StringSliceFlag{ Name: "server", Usage: "Server to grant the API key administrative privilege over", @@ -25,7 +34,7 @@ func init() { }, }, Usage: "add an API key to your Bytemark Cloud Servers user", - UsageText: "add api key [--server ]... [--group ]... [--user <", + UsageText: "add api key [--server ]... [--group ]... [--user ]