diff --git a/satellite/admin/README.md b/satellite/admin/README.md index e3f950636..3be60af33 100644 --- a/satellite/admin/README.md +++ b/satellite/admin/README.md @@ -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) @@ -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. diff --git a/satellite/admin/apikeys.go b/satellite/admin/apikeys.go index 0185f2482..22b53dcea 100644 --- a/satellite/admin/apikeys.go +++ b/satellite/admin/apikeys.go @@ -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() diff --git a/satellite/admin/apikeys_test.go b/satellite/admin/apikeys_test.go index 94ba00210..62416ce25 100644 --- a/satellite/admin/apikeys_test.go +++ b/satellite/admin/apikeys_test.go @@ -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) + }) +} diff --git a/satellite/admin/server.go b/satellite/admin/server.go index 58748bf53..19000c810 100644 --- a/satellite/admin/server.go +++ b/satellite/admin/server.go @@ -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") diff --git a/satellite/admin/ui/src/lib/api.ts b/satellite/admin/ui/src/lib/api.ts index e0a0ed4f6..0b87b184b 100644 --- a/satellite/admin/ui/src/lib/api.ts +++ b/satellite/admin/ui/src/lib/api.ts @@ -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> => { + return this.fetch('GET', `apikeys/${apiKey}`); + } + }, { name: 'delete key', desc: 'Delete an API key',