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)
|
* [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)
|
* [DELETE /api/projects/{project-id}/buckets/{bucket-name}/geofence](#delete-apiprojectsproject-idbucketsbucket-namegeofence)
|
||||||
* [APIKey Management](#apikey-management)
|
* [APIKey Management](#apikey-management)
|
||||||
|
* [GET /api/apikeys/{apikey}](#get-apiapikeysapikey)
|
||||||
* [DELETE /api/apikeys/{apikey}](#delete-apiapikeysapikey)
|
* [DELETE /api/apikeys/{apikey}](#delete-apiapikeysapikey)
|
||||||
|
|
||||||
<!-- tocstop -->
|
<!-- tocstop -->
|
||||||
@ -402,6 +403,31 @@ Removes the geofencing configuration for the specified bucket. The bucket MUST b
|
|||||||
|
|
||||||
### APIKey Management
|
### 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}
|
#### DELETE /api/apikeys/{apikey}
|
||||||
|
|
||||||
Deletes the given apikey.
|
Deletes the given apikey.
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
@ -108,6 +109,93 @@ func (server *Server) addAPIKey(w http.ResponseWriter, r *http.Request) {
|
|||||||
sendJSONData(w, http.StatusOK, data)
|
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) {
|
func (server *Server) deleteAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -251,3 +252,78 @@ func TestApiKeysList(t *testing.T) {
|
|||||||
assertGet(ctx, t, link, "[]", authToken)
|
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.createGeofenceForBucket).Methods("POST")
|
||||||
fullAccessAPI.HandleFunc("/projects/{project}/buckets/{bucket}/geofence", server.deleteGeofenceForBucket).Methods("DELETE")
|
fullAccessAPI.HandleFunc("/projects/{project}/buckets/{bucket}/geofence", server.deleteGeofenceForBucket).Methods("DELETE")
|
||||||
fullAccessAPI.HandleFunc("/projects/{project}/usage", server.checkProjectUsage).Methods("GET")
|
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("/apikeys/{apikey}", server.deleteAPIKey).Methods("DELETE")
|
||||||
fullAccessAPI.HandleFunc("/restkeys/{useremail}", server.addRESTKey).Methods("POST")
|
fullAccessAPI.HandleFunc("/restkeys/{useremail}", server.addRESTKey).Methods("POST")
|
||||||
fullAccessAPI.HandleFunc("/restkeys/{apikey}/revoke", server.revokeRESTKey).Methods("PUT")
|
fullAccessAPI.HandleFunc("/restkeys/{apikey}/revoke", server.revokeRESTKey).Methods("PUT")
|
||||||
|
@ -21,6 +21,14 @@ export interface API {
|
|||||||
export class Admin {
|
export class Admin {
|
||||||
readonly operations = {
|
readonly operations = {
|
||||||
APIKeys: [
|
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',
|
name: 'delete key',
|
||||||
desc: 'Delete an API key',
|
desc: 'Delete an API key',
|
||||||
|
Loading…
Reference in New Issue
Block a user