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:
Ivan Fraixedes 2021-07-30 20:06:36 +02:00 committed by Ivan Fraixedes
parent 1f353f3231
commit 8d75a35e56
6 changed files with 274 additions and 20 deletions

View File

@ -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.

View File

@ -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)
}

View File

@ -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)
})
}

View File

@ -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))
}

View File

@ -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")

View 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
}