satellite/console: get all bucket names endpoint and service method

WHAT:
new endpoint for fetching all bucket names

WHY:
used by new access grant flow

Change-Id: I356a3381359665fd2726120139b34b1e611fe3c4
This commit is contained in:
VitaliiShpital 2020-11-13 13:41:35 +02:00
parent db480e6e1b
commit 51a712f9e8
10 changed files with 320 additions and 1 deletions

View File

@ -744,5 +744,4 @@ func TestProjectUsage_BandwidthDownloadLimit(t *testing.T) {
_, err = planet.Uplinks[0].Download(ctx, planet.Satellites[0], "testbucket", "test/path1")
require.NoError(t, err)
})
}

View File

@ -0,0 +1,89 @@
// Copyright (C) 2020 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 (
// ErrBucketsAPI - console buckets api error type.
ErrBucketsAPI = errs.Class("console buckets api error")
)
// Buckets is an api controller that exposes all buckets related functionality.
type Buckets struct {
log *zap.Logger
service *console.Service
}
// NewBuckets is a constructor for api buckets controller.
func NewBuckets(log *zap.Logger, service *console.Service) *Buckets {
return &Buckets{
log: log,
service: service,
}
}
// AllBucketNames returns all bucket names for a specific project.
func (b *Buckets) AllBucketNames(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
projectIDString := r.URL.Query().Get("projectID")
projectID, err := uuid.FromString(projectIDString)
if err != nil {
b.serveJSONError(w, http.StatusInternalServerError, err)
return
}
bucketNames, err := b.service.GetAllBucketNames(ctx, projectID)
if err != nil {
if console.ErrUnauthorized.Has(err) {
b.serveJSONError(w, http.StatusUnauthorized, err)
return
}
b.serveJSONError(w, http.StatusInternalServerError, err)
return
}
err = json.NewEncoder(w).Encode(bucketNames)
if err != nil {
b.log.Error("failed to write json all bucket names response", zap.Error(ErrBucketsAPI.Wrap(err)))
}
}
// serveJSONError writes JSON error to response output stream.
func (b *Buckets) serveJSONError(w http.ResponseWriter, status int, err error) {
if status == http.StatusInternalServerError {
b.log.Error("returning error to client", zap.Int("code", status), zap.Error(err))
} else {
b.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 {
b.log.Error("failed to write json error response", zap.Error(ErrBucketsAPI.Wrap(err)))
}
}

View File

@ -0,0 +1,114 @@
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
package consoleapi_test
import (
"encoding/json"
"io/ioutil"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"storj.io/common/storj"
"storj.io/common/testcontext"
"storj.io/common/testrand"
"storj.io/storj/private/testplanet"
"storj.io/storj/satellite"
"storj.io/storj/satellite/console"
)
func Test_AllBucketNames(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]
project := planet.Uplinks[0].Projects[0]
service := sat.API.Console.Service
bucket1 := storj.Bucket{
ID: testrand.UUID(),
Name: "testBucket1",
ProjectID: project.ID,
}
bucket2 := storj.Bucket{
ID: testrand.UUID(),
Name: "testBucket2",
ProjectID: project.ID,
}
_, err := sat.DB.Buckets().CreateBucket(ctx, bucket1)
require.NoError(t, err)
_, err = sat.DB.Buckets().CreateBucket(ctx, bucket2)
require.NoError(t, err)
user := console.CreateUser{
FullName: "Jack",
ShortName: "",
Email: "bucketest@test.test",
Password: "123a123",
}
refUserID := ""
regToken, err := service.CreateRegToken(ctx, 1)
require.NoError(t, err)
createdUser, err := service.CreateUser(ctx, user, regToken.Secret, refUserID)
require.NoError(t, err)
activationToken, err := service.GenerateActivationToken(ctx, createdUser.ID, createdUser.Email)
require.NoError(t, err)
err = service.ActivateAccount(ctx, activationToken)
require.NoError(t, err)
token, err := service.Token(ctx, user.Email, user.Password)
require.NoError(t, err)
client := http.Client{}
req, err := http.NewRequest("GET", "http://"+planet.Satellites[0].API.Console.Listener.Addr().String()+"/api/v0/buckets/bucket-names?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)
body, err := ioutil.ReadAll(result.Body)
require.NoError(t, err)
var output []string
err = json.Unmarshal(body, &output)
require.NoError(t, err)
require.Equal(t, bucket1.Name, output[0])
require.Equal(t, bucket2.Name, output[1])
defer func() {
err = result.Body.Close()
require.NoError(t, err)
}()
})
}

View File

@ -196,6 +196,11 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, mail
paymentsRouter.HandleFunc("/tokens/deposit", paymentController.TokenDeposit).Methods(http.MethodPost)
paymentsRouter.HandleFunc("/paywall-enabled/{userId}", paymentController.PaywallEnabled).Methods(http.MethodGet)
bucketsController := consoleapi.NewBuckets(logger, service)
bucketsRouter := router.PathPrefix("/api/v0/buckets").Subrouter()
bucketsRouter.Use(server.withAuth)
bucketsRouter.HandleFunc("/bucket-names", bucketsController.AllBucketNames).Methods(http.MethodGet)
if server.config.StaticDir != "" {
router.HandleFunc("/activation/", server.accountActivationHandler)
router.HandleFunc("/password-recovery/", server.passwordRecoveryHandler)

View File

@ -21,6 +21,7 @@ import (
"storj.io/common/macaroon"
"storj.io/common/memory"
"storj.io/common/storj"
"storj.io/common/uuid"
"storj.io/storj/satellite/accounting"
"storj.io/storj/satellite/console/consoleauth"
@ -1455,6 +1456,36 @@ func (s *Service) GetBucketTotals(ctx context.Context, projectID uuid.UUID, curs
return usage, nil
}
// GetAllBucketNames retrieves all bucket names of a specific project.
func (s *Service) GetAllBucketNames(ctx context.Context, projectID uuid.UUID) (_ []string, err error) {
defer mon.Task()(&ctx)(&err)
_, err = s.getAuthAndAuditLog(ctx, "get all bucket names", zap.String("projectID", projectID.String()))
if err != nil {
return nil, Error.Wrap(err)
}
listOptions := storj.BucketListOptions{
Direction: storj.Forward,
}
allowedBuckets := macaroon.AllowedBuckets{
All: true,
}
bucketsList, err := s.buckets.ListBuckets(ctx, projectID, listOptions, allowedBuckets)
if err != nil {
return nil, Error.Wrap(err)
}
var list []string
for _, bucket := range bucketsList.Items {
list = append(list, bucket.Name)
}
return list, nil
}
// GetBucketUsageRollups retrieves summed usage rollups for every bucket of particular project for a given period.
func (s *Service) GetBucketUsageRollups(ctx context.Context, projectID uuid.UUID, since, before time.Time) (_ []accounting.BucketUsageRollup, err error) {
defer mon.Task()(&ctx)(&err)

View File

@ -8,7 +8,9 @@ import (
"github.com/stretchr/testify/require"
"storj.io/common/storj"
"storj.io/common/testcontext"
"storj.io/common/testrand"
"storj.io/common/uuid"
"storj.io/storj/private/testplanet"
"storj.io/storj/satellite/console"
@ -144,5 +146,30 @@ func TestService(t *testing.T) {
err = service.ChangeEmail(authCtx2, newEmail)
require.Error(t, err)
})
t.Run("TestGetAllBucketNames", func(t *testing.T) {
bucket1 := storj.Bucket{
ID: testrand.UUID(),
Name: "testBucket1",
ProjectID: up2Pro1.ID,
}
bucket2 := storj.Bucket{
ID: testrand.UUID(),
Name: "testBucket2",
ProjectID: up2Pro1.ID,
}
_, err := sat.DB.Buckets().CreateBucket(authCtx1, bucket1)
require.NoError(t, err)
_, err = sat.DB.Buckets().CreateBucket(authCtx1, bucket2)
require.NoError(t, err)
bucketNames, err := service.GetAllBucketNames(authCtx1, up2Pro1.ID)
require.NoError(t, err)
require.Equal(t, bucket1.Name, bucketNames[0])
require.Equal(t, bucket2.Name, bucketNames[1])
})
})
}

View File

@ -2,13 +2,18 @@
// See LICENSE for copying information.
import { BaseGql } from '@/api/baseGql';
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
import { Bucket, BucketCursor, BucketPage, BucketsApi } from '@/types/buckets';
import { HttpClient } from '@/utils/httpClient';
/**
* BucketsApiGql is a graphql implementation of Buckets API.
* Exposes all bucket-related functionality.
*/
export class BucketsApiGql extends BaseGql implements BucketsApi {
private readonly client: HttpClient = new HttpClient();
private readonly ROOT_PATH: string = '/api/v0/buckets';
/**
* Fetch buckets.
*
@ -53,6 +58,27 @@ export class BucketsApiGql extends BaseGql implements BucketsApi {
return this.getBucketPage(response.data.project.bucketUsages);
}
/**
* Fetch all bucket names.
*
* @returns string[]
* @throws Error
*/
public async getAllBucketNames(projectId: string): Promise<string[]> {
const path = `${this.ROOT_PATH}/bucket-names?projectID=${projectId}`;
const response = await this.client.get(path);
if (!response.ok) {
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('Can not get account balance');
}
return await response.json();
}
/**
* Method for mapping buckets page from json to BucketPage type.
*

View File

@ -6,12 +6,14 @@ import { Bucket, BucketCursor, BucketPage, BucketsApi } from '@/types/buckets';
export const BUCKET_ACTIONS = {
FETCH: 'setBuckets',
FETCH_ALL_BUCKET_NAMES: 'getAllBucketNames',
SET_SEARCH: 'setBucketSearch',
CLEAR: 'clearBuckets',
};
export const BUCKET_MUTATIONS = {
SET: 'setBuckets',
SET_ALL_BUCKET_NAMES: 'setAllBucketNames',
SET_SEARCH: 'setBucketSearch',
SET_PAGE: 'setBucketPage',
CLEAR: 'clearBuckets',
@ -19,9 +21,11 @@ export const BUCKET_MUTATIONS = {
const {
FETCH,
FETCH_ALL_BUCKET_NAMES,
} = BUCKET_ACTIONS;
const {
SET,
SET_ALL_BUCKET_NAMES,
SET_PAGE,
SET_SEARCH,
CLEAR,
@ -30,6 +34,7 @@ const bucketPageLimit = 7;
const firstPage = 1;
export class BucketsState {
public allBucketNames: string[] = [];
public cursor: BucketCursor = { limit: bucketPageLimit, search: '', page: firstPage };
public page: BucketPage = { buckets: new Array<Bucket>(), currentPage: 1, pageCount: 1, offset: 0, limit: bucketPageLimit, search: '', totalCount: 0 };
}
@ -47,6 +52,9 @@ export function makeBucketsModule(api: BucketsApi): StoreModule<BucketsState> {
[SET](state: BucketsState, page: BucketPage) {
state.page = page;
},
[SET_ALL_BUCKET_NAMES](state: BucketsState, allBucketNames: string[]) {
state.allBucketNames = allBucketNames;
},
[SET_PAGE](state: BucketsState, page: number) {
state.cursor.page = page;
},
@ -54,6 +62,7 @@ export function makeBucketsModule(api: BucketsApi): StoreModule<BucketsState> {
state.cursor.search = search;
},
[CLEAR](state: BucketsState) {
state.allBucketNames = [];
state.cursor = new BucketCursor('', bucketPageLimit, firstPage);
state.page = new BucketPage([], '', bucketPageLimit, 0, 1, 1, 0);
},
@ -72,6 +81,13 @@ export function makeBucketsModule(api: BucketsApi): StoreModule<BucketsState> {
return result;
},
[FETCH_ALL_BUCKET_NAMES]: async function({commit, rootGetters}: any): Promise<string[]> {
const result: string[] = await api.getAllBucketNames(rootGetters.selectedProject.id);
commit(SET_ALL_BUCKET_NAMES, result);
return result;
},
[BUCKET_ACTIONS.SET_SEARCH]: function({commit}, search: string) {
commit(SET_SEARCH, search);
},

View File

@ -12,6 +12,14 @@ export interface BucketsApi {
* @throws Error
*/
get(projectId: string, before: Date, cursor: BucketCursor): Promise<BucketPage>;
/**
* Fetch all bucket names
*
* @returns string[]
* @throws Error
*/
getAllBucketNames(projectId: string): Promise<string[]>;
}
/**

View File

@ -10,4 +10,8 @@ export class BucketsMock implements BucketsApi {
get(projectId: string, before: Date, cursor: BucketCursor): Promise<BucketPage> {
return Promise.resolve(new BucketPage());
}
getAllBucketNames(projectId: string): Promise<string[]> {
return Promise.resolve(['test']);
}
}