satellite/admin: add /api/apikeys/{apikey} GET endpoint

This allows scripted automation to get more details of the
API key such as project ID, and paid tier status.

Updates https://github.com/storj/gateway-mt/issues/321

Change-Id: I8a835752d4fd67382aca804b8c93e63de6c9a846
This commit is contained in:
Sean Harvey 2023-05-25 12:20:18 +12:00
parent c3d72a269e
commit 403f5eff81
No known key found for this signature in database
GPG Key ID: D917C00695250311
5 changed files with 199 additions and 0 deletions

View File

@ -48,6 +48,7 @@ Requires setting `Authorization` header for requests.
* [POST /api/projects/{project-id}/buckets/{bucket-name}/geofence?region={value}](#post-apiprojectsproject-idbucketsbucket-namegeofenceregionvalue)
* [DELETE /api/projects/{project-id}/buckets/{bucket-name}/geofence](#delete-apiprojectsproject-idbucketsbucket-namegeofence)
* [APIKey Management](#apikey-management)
* [GET /api/apikeys/{apikey}](#get-apiapikeysapikey)
* [DELETE /api/apikeys/{apikey}](#delete-apiapikeysapikey)
<!-- tocstop -->
@ -402,6 +403,31 @@ Removes the geofencing configuration for the specified bucket. The bucket MUST b
### APIKey Management
#### GET /api/apikeys/{apikey}
Gets information on the given apikey.
A successful response body:
```json
{
"api_key": {
"id": "12345678-1234-1234-1234-123456789abc",
"name": "my key",
"createdAt": "2020-05-19T00:34:13.265761+02:00"
},
"project": {
"id": "12345678-1234-1234-1234-123456789abc",
"name": "My Project",
},
"owner": {
"id": "12345678-1234-1234-1234-123456789abc",
"email": "bob@example.test",
"paidTier": true
}
}
```
#### DELETE /api/apikeys/{apikey}
Deletes the given apikey.

View File

@ -9,6 +9,7 @@ import (
"errors"
"io"
"net/http"
"time"
"github.com/gorilla/mux"
@ -108,6 +109,93 @@ func (server *Server) addAPIKey(w http.ResponseWriter, r *http.Request) {
sendJSONData(w, http.StatusOK, data)
}
func (server *Server) getAPIKey(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
apikeyString, ok := vars["apikey"]
if !ok {
sendJSONError(w, "apikey missing",
"", http.StatusBadRequest)
return
}
apikey, err := macaroon.ParseAPIKey(apikeyString)
if err != nil {
sendJSONError(w, "invalid apikey format",
err.Error(), http.StatusBadRequest)
return
}
apiKeyInfo, err := server.db.Console().APIKeys().GetByHead(ctx, apikey.Head())
if errors.Is(err, sql.ErrNoRows) {
sendJSONError(w, "API key does not exist",
"", http.StatusNotFound)
return
}
if err != nil {
sendJSONError(w, "could not get apikey id",
err.Error(), http.StatusInternalServerError)
return
}
project, err := server.db.Console().Projects().Get(ctx, apiKeyInfo.ProjectID)
if err != nil {
sendJSONError(w, "unable to fetch project details",
err.Error(), http.StatusInternalServerError)
return
}
user, err := server.db.Console().Users().Get(ctx, project.OwnerID)
if err != nil {
sendJSONError(w, "unable to fetch user details",
err.Error(), http.StatusInternalServerError)
return
}
type apiKeyData struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"createdAt"`
}
type projectData struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
}
type ownerData struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
PaidTier bool `json:"paidTier"`
}
data, err := json.Marshal(struct {
APIKey apiKeyData `json:"api_key"`
Project projectData `json:"project"`
Owner ownerData `json:"owner"`
}{
APIKey: apiKeyData{
ID: apiKeyInfo.ID,
Name: apiKeyInfo.Name,
CreatedAt: apiKeyInfo.CreatedAt.UTC(),
},
Project: projectData{
ID: project.ID,
Name: project.Name,
},
Owner: ownerData{
ID: user.ID,
Email: user.Email,
},
})
if err != nil {
sendJSONError(w, "json encoding failed",
err.Error(), http.StatusInternalServerError)
return
}
sendJSONData(w, http.StatusOK, data)
}
func (server *Server) deleteAPIKey(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

View File

@ -10,6 +10,7 @@ import (
"net/http"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -251,3 +252,78 @@ func TestApiKeysList(t *testing.T) {
assertGet(ctx, t, link, "[]", authToken)
})
}
func TestAPIKeyManagementGet(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1,
StorageNodeCount: 0,
UplinkCount: 1,
Reconfigure: testplanet.Reconfigure{
Satellite: func(_ *zap.Logger, _ int, config *satellite.Config) {
config.Admin.Address = "127.0.0.1:0"
},
},
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
address := planet.Satellites[0].Admin.Admin.Listener.Addr()
apikey := planet.Uplinks[0].APIKey[planet.Satellites[0].ID()]
link := fmt.Sprintf("http://"+address.String()+"/api/apikeys/%s", apikey.Serialize())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, link, nil)
require.NoError(t, err)
req.Header.Set("Authorization", planet.Satellites[0].Config.Console.AuthToken)
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose
require.NoError(t, err)
defer ctx.Check(resp.Body.Close)
require.Equal(t, http.StatusOK, resp.StatusCode)
type apiKeyData struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"createdAt"`
}
type projectData struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
}
type ownerData struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
PaidTier bool `json:"paidTier"`
}
type response struct {
APIKey apiKeyData `json:"api_key"`
Project projectData `json:"project"`
Owner ownerData `json:"owner"`
}
var apiResp response
require.NoError(t, json.NewDecoder(resp.Body).Decode(&apiResp))
apiKeyInfo, err := planet.Satellites[0].DB.Console().APIKeys().GetByHead(ctx, apikey.Head())
require.NoError(t, err)
project, err := planet.Satellites[0].DB.Console().Projects().Get(ctx, apiKeyInfo.ProjectID)
require.NoError(t, err)
owner, err := planet.Satellites[0].DB.Console().Users().Get(ctx, project.OwnerID)
require.NoError(t, err)
require.Equal(t, response{
APIKey: apiKeyData{
ID: apiKeyInfo.ID,
Name: apiKeyInfo.Name,
CreatedAt: apiKeyInfo.CreatedAt.UTC(),
},
Project: projectData{
ID: project.ID,
Name: project.Name,
},
Owner: ownerData{
ID: owner.ID,
Email: owner.Email,
PaidTier: owner.PaidTier,
},
}, apiResp)
})
}

View File

@ -131,6 +131,7 @@ func NewServer(log *zap.Logger, listener net.Listener, db DB, buckets *buckets.S
fullAccessAPI.HandleFunc("/projects/{project}/buckets/{bucket}/geofence", server.createGeofenceForBucket).Methods("POST")
fullAccessAPI.HandleFunc("/projects/{project}/buckets/{bucket}/geofence", server.deleteGeofenceForBucket).Methods("DELETE")
fullAccessAPI.HandleFunc("/projects/{project}/usage", server.checkProjectUsage).Methods("GET")
fullAccessAPI.HandleFunc("/apikeys/{apikey}", server.getAPIKey).Methods("GET")
fullAccessAPI.HandleFunc("/apikeys/{apikey}", server.deleteAPIKey).Methods("DELETE")
fullAccessAPI.HandleFunc("/restkeys/{useremail}", server.addRESTKey).Methods("POST")
fullAccessAPI.HandleFunc("/restkeys/{apikey}/revoke", server.revokeRESTKey).Methods("PUT")

View File

@ -21,6 +21,14 @@ export interface API {
export class Admin {
readonly operations = {
APIKeys: [
{
name: 'get',
desc: 'Get information on the specific API key',
params: [['API key', new InputText('text', true)]],
func: async (apiKey: string): Promise<Record<string, unknown>> => {
return this.fetch('GET', `apikeys/${apiKey}`);
}
},
{
name: 'delete key',
desc: 'Delete an API key',