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:
parent
c3d72a269e
commit
403f5eff81
@ -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.
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user