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.Use(server.withAuth)
|
||||||
bucketsRouter.HandleFunc("/bucket-names", bucketsController.AllBucketNames).Methods(http.MethodGet)
|
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 != "" {
|
if server.config.StaticDir != "" {
|
||||||
router.HandleFunc("/activation/", server.accountActivationHandler)
|
router.HandleFunc("/activation/", server.accountActivationHandler)
|
||||||
router.HandleFunc("/password-recovery/", server.passwordRecoveryHandler)
|
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)
|
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.
|
// 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) {
|
func (s *Service) GetAPIKeys(ctx context.Context, projectID uuid.UUID, cursor APIKeyCursor) (page *APIKeyPage, err error) {
|
||||||
defer mon.Task()(&ctx)(&err)
|
defer mon.Task()(&ctx)(&err)
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"storj.io/common/macaroon"
|
||||||
"storj.io/common/storj"
|
"storj.io/common/storj"
|
||||||
"storj.io/common/testcontext"
|
"storj.io/common/testcontext"
|
||||||
"storj.io/common/testrand"
|
"storj.io/common/testrand"
|
||||||
@ -176,5 +177,37 @@ func TestService(t *testing.T) {
|
|||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Nil(t, bucketsForUnauthorizedUser)
|
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.
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
import { BaseGql } from '@/api/baseGql';
|
import { BaseGql } from '@/api/baseGql';
|
||||||
|
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
|
||||||
import {
|
import {
|
||||||
AccessGrant,
|
AccessGrant,
|
||||||
AccessGrantCursor,
|
AccessGrantCursor,
|
||||||
@ -18,6 +19,7 @@ import { MetaUtils } from '@/utils/meta';
|
|||||||
*/
|
*/
|
||||||
export class AccessGrantsApiGql extends BaseGql implements AccessGrantsApi {
|
export class AccessGrantsApiGql extends BaseGql implements AccessGrantsApi {
|
||||||
private readonly client: HttpClient = new HttpClient();
|
private readonly client: HttpClient = new HttpClient();
|
||||||
|
private readonly ROOT_PATH: string = '/api/v0/api-keys';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch access grants.
|
* Fetch access grants.
|
||||||
@ -128,6 +130,28 @@ export class AccessGrantsApiGql extends BaseGql implements AccessGrantsApi {
|
|||||||
return response.data.deleteAPIKeys;
|
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.
|
* Used to get gateway credentials using access grant.
|
||||||
*
|
*
|
||||||
|
@ -73,7 +73,7 @@ export class BucketsApiGql extends BaseGql implements BucketsApi {
|
|||||||
throw new ErrorUnauthorized();
|
throw new ErrorUnauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('Can not get account balance');
|
throw new Error('Can not get bucket names');
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
Loading…
Reference in New Issue
Block a user