satellite/admin: Add new endpoint get list of API Keys
Add a new endpoint to get the list of API keys associated with a project. Change-Id: I9b5ed42e226786c03d853bbc8538344b40ee634f
This commit is contained in:
parent
1f353f3231
commit
8d75a35e56
@ -21,6 +21,7 @@ Requires setting `Authorization` header for requests.
|
||||
* [GET /api/project/{project-id}](#get-apiprojectproject-id)
|
||||
* [PUT /api/project/{project-id}](#put-apiprojectproject-id)
|
||||
* [DELETE /api/project/{project-id}](#delete-apiprojectproject-id)
|
||||
* [GET /api/project/{project}/apikeys](#get-apiprojectprojectapikeys)
|
||||
* [POST /api/project/{project}/apikey](#post-apiprojectprojectapikey)
|
||||
* [DELETE /api/project/{project}/apikey/{name}](#delete-apiprojectprojectapikeyname)
|
||||
* [GET /api/project/{project-id}/usage](#get-apiprojectproject-idusage)
|
||||
@ -223,6 +224,31 @@ Updates project name or description.
|
||||
|
||||
Deletes the project.
|
||||
|
||||
### GET /api/project/{project}/apikeys
|
||||
|
||||
Get the list of the API keys of a specific project.
|
||||
|
||||
A successful response body:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "b6988bd2-8d21-4bee-91ac-a3445bf38180",
|
||||
"ownerId": "ca7aa0fb-442a-4d4e-aa36-a49abddae837",
|
||||
"name": "mine",
|
||||
"partnerID": "a9d3b7ee-17da-4848-bb0e-1f64cf45af18",
|
||||
"createdAt": "2020-05-19T00:34:13.265761+02:00"
|
||||
},
|
||||
{
|
||||
"id": "f9f887c1-b178-4eb8-b669-14379c5a97ca",
|
||||
"ownerId": "3eb45ae9-822a-470e-a51a-9144dedda63e",
|
||||
"name": "family",
|
||||
"partnerID": "",
|
||||
"createdAt": "2020-02-20T15:34:24.265761+02:00"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### POST /api/project/{project}/apikey
|
||||
|
||||
Adds an apikey for specific project.
|
||||
|
@ -181,3 +181,61 @@ func (server *Server) deleteAPIKeyByName(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (server *Server) listAPIKeys(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
vars := mux.Vars(r)
|
||||
projectUUIDString, ok := vars["project"]
|
||||
if !ok {
|
||||
httpJSONError(w, "project-uuid missing",
|
||||
"", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
projectUUID, err := uuid.FromString(projectUUIDString)
|
||||
if err != nil {
|
||||
httpJSONError(w, "invalid project-uuid",
|
||||
err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
const apiKeysPerPage = 50 // This is the current maximum API Keys per page.
|
||||
var apiKeys []console.APIKeyInfo
|
||||
for i := uint(1); true; i++ {
|
||||
page, err := server.db.Console().APIKeys().GetPagedByProjectID(
|
||||
ctx, projectUUID, console.APIKeyCursor{
|
||||
Limit: apiKeysPerPage,
|
||||
Page: i,
|
||||
Order: console.KeyName,
|
||||
OrderDirection: console.Ascending,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
httpJSONError(w, "failed retrieving a cursor page of API Keys list",
|
||||
err.Error(), http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
apiKeys = append(apiKeys, page.APIKeys...)
|
||||
if len(page.APIKeys) < apiKeysPerPage {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var data []byte
|
||||
if len(apiKeys) == 0 {
|
||||
data = []byte("[]")
|
||||
} else {
|
||||
data, err = json.Marshal(apiKeys)
|
||||
if err != nil {
|
||||
httpJSONError(w, "json encoding failed",
|
||||
err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
@ -11,11 +11,13 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/common/macaroon"
|
||||
"storj.io/common/testcontext"
|
||||
"storj.io/common/uuid"
|
||||
"storj.io/storj/private/testplanet"
|
||||
"storj.io/storj/satellite"
|
||||
"storj.io/storj/satellite/console"
|
||||
@ -144,3 +146,119 @@ func TestDeleteApiKeyByName(t *testing.T) {
|
||||
require.Len(t, keys.APIKeys, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListAPIKeys(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1,
|
||||
StorageNodeCount: 0,
|
||||
UplinkCount: 1,
|
||||
Reconfigure: testplanet.Reconfigure{
|
||||
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
||||
config.Admin.Address = "127.0.0.1:0"
|
||||
},
|
||||
},
|
||||
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||
var (
|
||||
sat = planet.Satellites[0]
|
||||
authToken = planet.Satellites[0].Config.Console.AuthToken
|
||||
address = sat.Admin.Admin.Listener.Addr()
|
||||
)
|
||||
|
||||
project, err := sat.DB.Console().Projects().Get(ctx, planet.Uplinks[0].Projects[0].ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
linkAPIKey := "http://" + address.String() + "/api/project/" + project.ID.String() + "/apikey"
|
||||
linkAPIKeys := "http://" + address.String() + "/api/project/" + project.ID.String() + "/apikeys"
|
||||
|
||||
{ // Delete initial API Keys to run this test
|
||||
|
||||
page, err := sat.DB.Console().APIKeys().GetPagedByProjectID(
|
||||
ctx, project.ID, console.APIKeyCursor{
|
||||
Limit: 50, Page: 1, Order: console.KeyName, OrderDirection: console.Ascending,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Ensure that we are getting all the initial keys with one single page.
|
||||
require.Len(t, page.APIKeys, int(page.TotalCount))
|
||||
|
||||
for _, ak := range page.APIKeys {
|
||||
require.NoError(t, sat.DB.Console().APIKeys().Delete(ctx, ak.ID))
|
||||
}
|
||||
}
|
||||
|
||||
// Check get initial list of API keys.
|
||||
assertGet(ctx, t, linkAPIKeys, "[]", authToken)
|
||||
|
||||
{ // Create 2 new API Key.
|
||||
body := assertReq(ctx, t, linkAPIKey, http.MethodPost, `{"name": "first"}`, http.StatusOK, "", authToken)
|
||||
apiKey := struct {
|
||||
Apikey string `json:"apikey"`
|
||||
}{}
|
||||
require.NoError(t, json.Unmarshal(body, &apiKey))
|
||||
require.NotEmpty(t, apiKey.Apikey)
|
||||
|
||||
body = assertReq(ctx, t, linkAPIKey, http.MethodPost, `{"name": "second"}`, http.StatusOK, "", authToken)
|
||||
require.NoError(t, json.Unmarshal(body, &apiKey))
|
||||
require.NotEmpty(t, apiKey.Apikey)
|
||||
|
||||
// TODO: figure out how to create an API Key associated to a partner.
|
||||
// sat.DB.Console().APIKeys.Update only allows to update the API Key name
|
||||
}
|
||||
|
||||
// Check get list of API keys.
|
||||
body := assertReq(ctx, t, linkAPIKeys, http.MethodGet, "", http.StatusOK, "", authToken)
|
||||
|
||||
var apiKeys []struct {
|
||||
ID string `json:"id"`
|
||||
ProjectID string `json:"projectId"`
|
||||
Name string `json:"name"`
|
||||
PartnerID string `json:"partnerID"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(body, &apiKeys))
|
||||
require.Len(t, apiKeys, 2)
|
||||
{ // Assert API keys info.
|
||||
a := apiKeys[0]
|
||||
assert.NotEmpty(t, a.ID, "API key ID")
|
||||
assert.Equal(t, "first", a.Name, "API key name")
|
||||
assert.Equal(t, project.ID.String(), a.ProjectID, "API key project ID")
|
||||
assert.Equal(t, uuid.UUID{}.String(), a.PartnerID, "API key partner ID")
|
||||
assert.NotEmpty(t, a.CreatedAt, "API key created at")
|
||||
|
||||
a = apiKeys[1]
|
||||
assert.NotEmpty(t, a.ID, "API key ID")
|
||||
assert.Equal(t, "second", a.Name, "API key name")
|
||||
assert.Equal(t, project.ID.String(), a.ProjectID, "API key project ID")
|
||||
assert.Equal(t, uuid.UUID{}.String(), a.PartnerID, "API key partner ID")
|
||||
assert.NotEmpty(t, a.CreatedAt, "API key created at")
|
||||
}
|
||||
|
||||
{ // Delete one API key to check that the endpoint just returns one.
|
||||
id, err := uuid.FromString(apiKeys[1].ID)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, sat.DB.Console().APIKeys().Delete(ctx, id))
|
||||
}
|
||||
|
||||
body = assertReq(ctx, t, linkAPIKeys, http.MethodGet, "", http.StatusOK, "", authToken)
|
||||
require.NoError(t, json.Unmarshal(body, &apiKeys))
|
||||
require.Len(t, apiKeys, 1)
|
||||
{ // Assert API keys info.
|
||||
a := apiKeys[0]
|
||||
assert.NotEmpty(t, a.ID, "API key ID")
|
||||
assert.Equal(t, "first", a.Name, "API key name")
|
||||
assert.Equal(t, project.ID.String(), a.ProjectID, "API key project ID")
|
||||
assert.Equal(t, uuid.UUID{}.String(), a.PartnerID, "API key partner ID")
|
||||
assert.NotEmpty(t, a.CreatedAt, "API key created at")
|
||||
}
|
||||
|
||||
{ // Delete the one API key that last to check that the endpoint just returns none.
|
||||
id, err := uuid.FromString(apiKeys[0].ID)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, sat.DB.Console().APIKeys().Delete(ctx, id))
|
||||
}
|
||||
|
||||
// Check get initial list of API keys.
|
||||
assertGet(ctx, t, linkAPIKeys, "[]", authToken)
|
||||
})
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
package admin_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@ -567,22 +566,3 @@ func TestDeleteProjectWithUsagePreviousMonth(t *testing.T) {
|
||||
require.Equal(t, http.StatusConflict, response.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func assertGet(ctx context.Context, t *testing.T, link string, expected string, authToken string) {
|
||||
t.Helper()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, link, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
req.Header.Set("Authorization", authToken)
|
||||
|
||||
response, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
data, err := ioutil.ReadAll(response.Body)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, response.Body.Close())
|
||||
|
||||
require.Equal(t, http.StatusOK, response.StatusCode, string(data))
|
||||
require.Equal(t, expected, string(data))
|
||||
}
|
||||
|
@ -91,6 +91,7 @@ func NewServer(log *zap.Logger, listener net.Listener, db DB, accounts payments.
|
||||
server.mux.HandleFunc("/api/project/{project}", server.getProject).Methods("GET")
|
||||
server.mux.HandleFunc("/api/project/{project}", server.renameProject).Methods("PUT")
|
||||
server.mux.HandleFunc("/api/project/{project}", server.deleteProject).Methods("DELETE")
|
||||
server.mux.HandleFunc("/api/project/{project}/apikeys", server.listAPIKeys).Methods("GET")
|
||||
server.mux.HandleFunc("/api/project/{project}/apikey", server.addAPIKey).Methods("POST")
|
||||
server.mux.HandleFunc("/api/project/{project}/apikey/{name}", server.deleteAPIKeyByName).Methods("DELETE")
|
||||
server.mux.HandleFunc("/api/apikey/{apikey}", server.deleteAPIKey).Methods("DELETE")
|
||||
|
71
satellite/admin/testutils_test.go
Normal file
71
satellite/admin/testutils_test.go
Normal file
@ -0,0 +1,71 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package admin_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"storj.io/common/testcontext"
|
||||
)
|
||||
|
||||
func assertGet(ctx context.Context, t *testing.T, link string, expected string, authToken string) {
|
||||
t.Helper()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, link, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
req.Header.Set("Authorization", authToken)
|
||||
|
||||
response, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
data, err := ioutil.ReadAll(response.Body)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, response.Body.Close())
|
||||
|
||||
require.Equal(t, http.StatusOK, response.StatusCode, string(data))
|
||||
require.Equal(t, expected, string(data))
|
||||
}
|
||||
|
||||
// assertReq asserts the request and it's OK it returns the response body.
|
||||
func assertReq(
|
||||
ctx *testcontext.Context, t *testing.T, link string, method string, body string,
|
||||
expectedStatus int, expectedBody string, authToken string,
|
||||
) []byte {
|
||||
t.Helper()
|
||||
|
||||
var (
|
||||
req *http.Request
|
||||
err error
|
||||
)
|
||||
if body == "" {
|
||||
req, err = http.NewRequestWithContext(ctx, method, link, nil)
|
||||
} else {
|
||||
req, err = http.NewRequestWithContext(ctx, method, link, strings.NewReader(body))
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
req.Header.Set("Authorization", authToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
res, err := http.DefaultClient.Do(req) //nolint:bodyclose
|
||||
require.NoError(t, err)
|
||||
defer ctx.Check(res.Body.Close)
|
||||
|
||||
require.Equal(t, expectedStatus, res.StatusCode, "response status code")
|
||||
|
||||
resBody, err := ioutil.ReadAll(res.Body)
|
||||
if expectedBody != "" {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedBody, string(resBody), "response body")
|
||||
}
|
||||
|
||||
return resBody
|
||||
}
|
Loading…
Reference in New Issue
Block a user