Skip to content

Commit

Permalink
Add create/list/delete functions for API keys (#161)
Browse files Browse the repository at this point in the history
Add create/list/delete functions for API keys
  • Loading branch information
K-Phoen authored Jan 17, 2022
1 parent 8ebcd92 commit 2f080fd
Show file tree
Hide file tree
Showing 2 changed files with 229 additions and 0 deletions.
124 changes: 124 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,51 @@ var ErrDashboardNotFound = errors.New("dashboard not found")
// ErrDatasourceNotFound is returned when the given datasource can not be found.
var ErrDatasourceNotFound = errors.New("datasource not found")

// ErrAPIKeyNotFound is returned when the given API key can not be found.
var ErrAPIKeyNotFound = errors.New("API key not found")

// ErrAlertChannelNotFound is returned when the given alert notification
// channel can not be found.
var ErrAlertChannelNotFound = errors.New("alert channel not found")

// APIKeyRole represents a role given to an API key.
type APIKeyRole uint8

const (
AdminRole APIKeyRole = iota
EditorRole
ViewerRole
)

func (role APIKeyRole) MarshalJSON() ([]byte, error) {
var s string
switch role {
case ViewerRole:
s = "Viewer"
case EditorRole:
s = "Editor"
case AdminRole:
s = "Admin"
default:
s = "None"
}

return json.Marshal(s)
}

// CreateAPIKeyRequest represents a request made to the API key creation endpoint.
type CreateAPIKeyRequest struct {
Name string `json:"name"`
Role APIKeyRole `json:"role"`
SecondsToLive int `json:"secondsToLive"`
}

// APIKey represents an API key.
type APIKey struct {
ID uint `json:"id"`
Name string `json:"name"`
}

// Dashboard represents a Grafana dashboard.
type Dashboard struct {
ID uint `json:"id"`
Expand Down Expand Up @@ -95,6 +136,89 @@ func (client *Client) modifyRequest(request *http.Request) {
}
}

// CreateAPIKey creates a new API key.
func (client *Client) CreateAPIKey(ctx context.Context, request CreateAPIKeyRequest) (string, error) {
buf, err := json.Marshal(request)
if err != nil {
return "", err
}

resp, err := client.sendJSON(ctx, http.MethodPost, "/api/auth/keys", buf)
if err != nil {
return "", err
}

defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
return "", client.httpError(resp)
}

var response struct {
Key string `json:"key"`
}
if err := decodeJSON(resp.Body, &response); err != nil {
return "", err
}

return response.Key, nil
}

// DeleteAPIKeyByName deletes an API key given its name.
func (client *Client) DeleteAPIKeyByName(ctx context.Context, name string) error {
apiKeys, err := client.APIKeys(ctx)
if err != nil {
return err
}

keyToDelete, ok := apiKeys[name]
if !ok {
return ErrAPIKeyNotFound
}

resp, err := client.delete(ctx, fmt.Sprintf("/api/auth/keys/%d", keyToDelete.ID))
if err != nil {
return err
}

defer func() { _ = resp.Body.Close() }()

if resp.StatusCode == http.StatusNotFound {
return ErrAPIKeyNotFound
}
if resp.StatusCode != http.StatusOK {
return client.httpError(resp)
}

return nil
}

// APIKeys lists active API keys.
func (client *Client) APIKeys(ctx context.Context) (map[string]APIKey, error) {
resp, err := client.get(ctx, "/api/auth/keys")
if err != nil {
return nil, err
}

defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
return nil, client.httpError(resp)
}

var keys []APIKey
if err := decodeJSON(resp.Body, &keys); err != nil {
return nil, err
}

keysMap := make(map[string]APIKey, len(keys))
for _, key := range keys {
keysMap[key.Name] = key
}

return keysMap, nil
}

// FindOrCreateFolder returns the folder by its name or creates it if it doesn't exist.
func (client *Client) FindOrCreateFolder(ctx context.Context, name string) (*Folder, error) {
folder, err := client.GetFolderByTitle(ctx, name)
Expand Down
105 changes: 105 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -534,3 +534,108 @@ func TestGetDatasourceUIDByNameReturnsASpecificErrorIfDatasourceIsNotFound(t *te
req.Equal(ErrDatasourceNotFound, err)
req.Empty(uid)
}

func TestCreateAPIKey(t *testing.T) {
req := require.New(t)

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, `{"name":"mykey","key":"eyJrIjoiWHZiSWd3NzdCYUZnNUtibE9obUpESmE3bzJYNDRIc0UiLCJuIjoibXlrZXkiLCJpZCI6MX1=","id":1}`)
}))
defer ts.Close()

client := NewClient(http.DefaultClient, ts.URL)

token, err := client.CreateAPIKey(context.TODO(), CreateAPIKeyRequest{
Name: "mykey",
Role: AdminRole,
})

req.NoError(err)
req.Equal("eyJrIjoiWHZiSWd3NzdCYUZnNUtibE9obUpESmE3bzJYNDRIc0UiLCJuIjoibXlrZXkiLCJpZCI6MX1=", token)
}

func TestDeleteDeleteAPIKeyByName(t *testing.T) {
req := require.New(t)
deleted := false

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// deletion call
if strings.HasPrefix(r.URL.Path, "/api/auth/keys/") {
deleted = true
req.Equal(http.MethodDelete, r.Method)
req.Equal("/api/auth/keys/2", r.URL.Path)

w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, `{}`)
return
}

// API keys list call
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, `[{"id": 2, "name": "foo"}]`)
}))
defer ts.Close()

client := NewClient(http.DefaultClient, ts.URL)

err := client.DeleteAPIKeyByName(context.TODO(), "foo")

req.NoError(err)
req.True(deleted)
}

func TestDeleteAPIKeyByNameReturnsKnownErrorIfDatasourceDoesNotExist(t *testing.T) {
req := require.New(t)

deleted := false
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// deletion call
if strings.HasPrefix(r.URL.Path, "/api/auth/keys/") {
deleted = true
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, `{}`)
return
}

// API keys list call
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, `[{"id": 2, "name": "foo"}]`)
}))
defer ts.Close()

client := NewClient(http.DefaultClient, ts.URL)

err := client.DeleteAPIKeyByName(context.TODO(), "unknown")

req.Error(err)
req.Equal(ErrAPIKeyNotFound, err)
req.False(deleted)
}

func TestDeleteAPIKeyByNameForwardsErrorOnFailure(t *testing.T) {
req := require.New(t)

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// deletion call
if strings.HasPrefix(r.URL.Path, "/api/auth/keys/") {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintln(w, `{
"message": "something when wrong"
}`)
return
}

// API keys list call
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, `[{"id": 2, "name": "foo"}]`)
}))
defer ts.Close()

client := NewClient(http.DefaultClient, ts.URL)

err := client.DeleteAPIKeyByName(context.TODO(), "foo")

req.Error(err)
req.Contains(err.Error(), "something when wrong")
}

0 comments on commit 2f080fd

Please sign in to comment.