satellite/console: delete api key by name and project id endpoint
WHAT: new endpoint to be able to delete apiKey/accessGrant by name and project id WHY: it will be called to delete special pregenerated access grant which will be used to generate gateway credentials for file browser component or bucket management Change-Id: I7467ebaab27a7da33efd062536c6da41e6ed4c30
This commit is contained in:
parent
f19ef4afe5
commit
3e37d1e71c
88
satellite/console/consoleweb/consoleapi/apikeys.go
Normal file
88
satellite/console/consoleweb/consoleapi/apikeys.go
Normal file
@ -0,0 +1,88 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package consoleapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/zeebo/errs"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/common/uuid"
|
||||
"storj.io/storj/satellite/console"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrAPIKeysAPI - console api keys api error type.
|
||||
ErrAPIKeysAPI = errs.Class("console api keys api error")
|
||||
)
|
||||
|
||||
// APIKeys is an api controller that exposes all api keys related functionality.
|
||||
type APIKeys struct {
|
||||
log *zap.Logger
|
||||
service *console.Service
|
||||
}
|
||||
|
||||
// NewAPIKeys is a constructor for api api keys controller.
|
||||
func NewAPIKeys(log *zap.Logger, service *console.Service) *APIKeys {
|
||||
return &APIKeys{
|
||||
log: log,
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteByNameAndProjectID deletes specific api key by it's name and project ID.
|
||||
func (keys *APIKeys) DeleteByNameAndProjectID(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var err error
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
name := r.URL.Query().Get("name")
|
||||
projectIDString := r.URL.Query().Get("projectID")
|
||||
|
||||
if name == "" {
|
||||
keys.serveJSONError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
projectID, err := uuid.FromString(projectIDString)
|
||||
if err != nil {
|
||||
keys.serveJSONError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = keys.service.DeleteAPIKeyByNameAndProjectID(ctx, name, projectID)
|
||||
if err != nil {
|
||||
if console.ErrUnauthorized.Has(err) {
|
||||
keys.serveJSONError(w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
keys.serveJSONError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// serveJSONError writes JSON error to response output stream.
|
||||
func (keys *APIKeys) serveJSONError(w http.ResponseWriter, status int, err error) {
|
||||
if status == http.StatusInternalServerError {
|
||||
keys.log.Error("returning internal server error to client", zap.Int("code", status), zap.Error(err))
|
||||
} else {
|
||||
keys.log.Debug("returning error to client", zap.Int("code", status), zap.Error(err))
|
||||
}
|
||||
|
||||
w.WriteHeader(status)
|
||||
|
||||
var response struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
response.Error = err.Error()
|
||||
|
||||
err = json.NewEncoder(w).Encode(response)
|
||||
if err != nil {
|
||||
keys.log.Error("failed to write json error response", zap.Error(ErrAPIKeysAPI.Wrap(err)))
|
||||
}
|
||||
}
|
92
satellite/console/consoleweb/consoleapi/apikeys_test.go
Normal file
92
satellite/console/consoleweb/consoleapi/apikeys_test.go
Normal file
@ -0,0 +1,92 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package consoleapi_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/common/macaroon"
|
||||
"storj.io/common/testcontext"
|
||||
"storj.io/storj/private/testplanet"
|
||||
"storj.io/storj/satellite"
|
||||
"storj.io/storj/satellite/console"
|
||||
)
|
||||
|
||||
func Test_DeleteAPIKeyByNameAndProjectID(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.Console.OpenRegistrationEnabled = true
|
||||
config.Console.RateLimit.Burst = 10
|
||||
},
|
||||
},
|
||||
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||
sat := planet.Satellites[0]
|
||||
|
||||
newUser := console.CreateUser{
|
||||
FullName: "test_name",
|
||||
ShortName: "",
|
||||
Email: "apikeytest@test.test",
|
||||
}
|
||||
|
||||
user, err := sat.AddUser(ctx, newUser, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
project, err := sat.AddProject(ctx, user.ID, "apikeytest")
|
||||
require.NoError(t, err)
|
||||
|
||||
secret, err := macaroon.NewSecret()
|
||||
require.NoError(t, err)
|
||||
|
||||
key, err := macaroon.NewAPIKey(secret)
|
||||
require.NoError(t, err)
|
||||
|
||||
apikey := console.APIKeyInfo{
|
||||
Name: "test",
|
||||
ProjectID: project.ID,
|
||||
Secret: secret,
|
||||
}
|
||||
|
||||
created, err := sat.DB.Console().APIKeys().Create(ctx, key.Head(), apikey)
|
||||
require.NoError(t, err)
|
||||
|
||||
// we are using full name as a password
|
||||
token, err := sat.API.Console.Service.Token(ctx, user.Email, user.FullName)
|
||||
require.NoError(t, err)
|
||||
|
||||
client := http.Client{}
|
||||
|
||||
req, err := http.NewRequest("DELETE", "http://"+planet.Satellites[0].API.Console.Listener.Addr().String()+"/api/v0/api-keys/delete-by-name?name="+apikey.Name+"&projectID="+project.ID.String(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
expire := time.Now().AddDate(0, 0, 1)
|
||||
cookie := http.Cookie{
|
||||
Name: "_tokenKey",
|
||||
Path: "/",
|
||||
Value: token,
|
||||
Expires: expire,
|
||||
}
|
||||
|
||||
req.AddCookie(&cookie)
|
||||
|
||||
result, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, result.StatusCode)
|
||||
|
||||
keyAfterDelete, err := sat.DB.Console().APIKeys().Get(ctx, created.ID)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, keyAfterDelete)
|
||||
|
||||
defer func() {
|
||||
err = result.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
})
|
||||
}
|
@ -202,6 +202,11 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, mail
|
||||
bucketsRouter.Use(server.withAuth)
|
||||
bucketsRouter.HandleFunc("/bucket-names", bucketsController.AllBucketNames).Methods(http.MethodGet)
|
||||
|
||||
apiKeysController := consoleapi.NewAPIKeys(logger, service)
|
||||
apiKeysRouter := router.PathPrefix("/api/v0/api-keys").Subrouter()
|
||||
apiKeysRouter.Use(server.withAuth)
|
||||
apiKeysRouter.HandleFunc("/delete-by-name", apiKeysController.DeleteByNameAndProjectID).Methods(http.MethodDelete)
|
||||
|
||||
if server.config.StaticDir != "" {
|
||||
router.HandleFunc("/activation/", server.accountActivationHandler)
|
||||
router.HandleFunc("/password-recovery/", server.passwordRecoveryHandler)
|
||||
|
@ -1325,6 +1325,33 @@ func (s *Service) DeleteAPIKeys(ctx context.Context, ids []uuid.UUID) (err error
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
// DeleteAPIKeyByNameAndProjectID deletes api key by name and project ID.
|
||||
func (s *Service) DeleteAPIKeyByNameAndProjectID(ctx context.Context, name string, projectID uuid.UUID) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
auth, err := s.getAuthAndAuditLog(ctx, "delete api key by name and project ID", zap.String("apiKeyName", name), zap.String("projectID", projectID.String()))
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
_, err = s.isProjectMember(ctx, auth.User.ID, projectID)
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
key, err := s.store.APIKeys().GetByNameAndProjectID(ctx, name, projectID)
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
err = s.store.APIKeys().Delete(ctx, key.ID)
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAPIKeys returns paged api key list for given Project.
|
||||
func (s *Service) GetAPIKeys(ctx context.Context, projectID uuid.UUID, cursor APIKeyCursor) (page *APIKeyPage, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"storj.io/common/macaroon"
|
||||
"storj.io/common/storj"
|
||||
"storj.io/common/testcontext"
|
||||
"storj.io/common/testrand"
|
||||
@ -176,5 +177,37 @@ func TestService(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
require.Nil(t, bucketsForUnauthorizedUser)
|
||||
})
|
||||
|
||||
t.Run("TestDeleteAPIKeyByNameAndProjectID", func(t *testing.T) {
|
||||
secret, err := macaroon.NewSecret()
|
||||
require.NoError(t, err)
|
||||
|
||||
key, err := macaroon.NewAPIKey(secret)
|
||||
require.NoError(t, err)
|
||||
|
||||
apikey := console.APIKeyInfo{
|
||||
Name: "test",
|
||||
ProjectID: up2Pro1.ID,
|
||||
Secret: secret,
|
||||
}
|
||||
|
||||
createdKey, err := sat.DB.Console().APIKeys().Create(ctx, key.Head(), apikey)
|
||||
require.NoError(t, err)
|
||||
|
||||
info, err := sat.DB.Console().APIKeys().Get(ctx, createdKey.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, info)
|
||||
|
||||
// Deleting someone else api keys should not work
|
||||
err = service.DeleteAPIKeyByNameAndProjectID(authCtx1, apikey.Name, up2Pro1.ID)
|
||||
require.Error(t, err)
|
||||
|
||||
err = service.DeleteAPIKeyByNameAndProjectID(authCtx2, apikey.Name, up2Pro1.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
info, err = sat.DB.Console().APIKeys().Get(ctx, createdKey.ID)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, info)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import { BaseGql } from '@/api/baseGql';
|
||||
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
|
||||
import {
|
||||
AccessGrant,
|
||||
AccessGrantCursor,
|
||||
@ -18,6 +19,7 @@ import { MetaUtils } from '@/utils/meta';
|
||||
*/
|
||||
export class AccessGrantsApiGql extends BaseGql implements AccessGrantsApi {
|
||||
private readonly client: HttpClient = new HttpClient();
|
||||
private readonly ROOT_PATH: string = '/api/v0/api-keys';
|
||||
|
||||
/**
|
||||
* Fetch access grants.
|
||||
@ -128,6 +130,28 @@ export class AccessGrantsApiGql extends BaseGql implements AccessGrantsApi {
|
||||
return response.data.deleteAPIKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to delete access grant access grant by name and project ID.
|
||||
*
|
||||
* @param name - name of the access grant that will be deleted
|
||||
* @param projectID - id of the project where access grant was created
|
||||
* @throws Error
|
||||
*/
|
||||
public async deleteByNameAndProjectID(name: string, projectID: string): Promise<void> {
|
||||
const path = `${this.ROOT_PATH}/delete-by-name?name=${name}&projectID=${projectID}`;
|
||||
const response = await this.client.delete(path);
|
||||
|
||||
if (response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
throw new ErrorUnauthorized();
|
||||
}
|
||||
|
||||
throw new Error('can not delete access grant');
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to get gateway credentials using access grant.
|
||||
*
|
||||
|
@ -73,7 +73,7 @@ export class BucketsApiGql extends BaseGql implements BucketsApi {
|
||||
throw new ErrorUnauthorized();
|
||||
}
|
||||
|
||||
throw new Error('Can not get account balance');
|
||||
throw new Error('Can not get bucket names');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
Loading…
Reference in New Issue
Block a user