2020-02-07 16:36:28 +00:00
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
// Package admin implements administrative endpoints for satellite.
package admin
import (
"context"
2020-02-07 17:24:58 +00:00
"crypto/subtle"
2021-10-01 13:26:21 +01:00
"embed"
2020-04-16 16:50:22 +01:00
"errors"
2021-10-01 13:26:21 +01:00
"io/fs"
2020-02-07 16:36:28 +00:00
"net"
"net/http"
2020-09-03 22:12:26 +01:00
"time"
2020-02-07 16:36:28 +00:00
"github.com/gorilla/mux"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
2020-02-07 17:24:58 +00:00
2020-04-16 16:50:22 +01:00
"storj.io/common/errs2"
2020-02-07 17:24:58 +00:00
"storj.io/storj/satellite/accounting"
"storj.io/storj/satellite/console"
2020-05-18 18:36:09 +01:00
"storj.io/storj/satellite/metainfo"
2020-07-06 22:31:40 +01:00
"storj.io/storj/satellite/payments"
2020-05-19 11:36:13 +01:00
"storj.io/storj/satellite/payments/stripecoinpayments"
2020-02-07 16:36:28 +00:00
)
2021-10-01 13:26:21 +01:00
//go:embed ui/public
var ui embed . FS
2020-02-07 16:36:28 +00:00
// Config defines configuration for debug server.
type Config struct {
2021-10-01 13:26:21 +01:00
Address string ` help:"admin peer http listening address" releaseDefault:"" devDefault:"" `
StaticDir string ` help:"an alternate directory path which contains the static assets to serve. When empty, it uses the embedded assets" releaseDefault:"" devDefault:"" `
2020-02-07 17:24:58 +00:00
AuthorizationToken string ` internal:"true" `
}
// DB is databases needed for the admin server.
type DB interface {
// ProjectAccounting returns database for storing information about project data use
ProjectAccounting ( ) accounting . ProjectAccounting
// Console returns database for satellite console
Console ( ) console . DB
2020-05-19 11:36:13 +01:00
// StripeCoinPayments returns database for satellite stripe coin payments
StripeCoinPayments ( ) stripecoinpayments . DB
2020-05-18 18:36:09 +01:00
// Buckets returns database for satellite buckets
Buckets ( ) metainfo . BucketsDB
2020-02-07 16:36:28 +00:00
}
2020-08-13 13:40:05 +01:00
// Server provides endpoints for administrative tasks.
2020-02-07 16:36:28 +00:00
type Server struct {
log * zap . Logger
listener net . Listener
server http . Server
2020-02-07 17:24:58 +00:00
2020-07-06 22:31:40 +01:00
db DB
2020-07-16 12:58:40 +01:00
payments payments . Accounts
2020-09-03 22:12:26 +01:00
nowFn func ( ) time . Time
2020-02-07 16:36:28 +00:00
}
2020-08-13 13:40:05 +01:00
// NewServer returns a new administration Server.
2020-07-16 12:58:40 +01:00
func NewServer ( log * zap . Logger , listener net . Listener , db DB , accounts payments . Accounts , config Config ) * Server {
2020-02-07 17:24:58 +00:00
server := & Server {
2020-07-16 12:58:40 +01:00
log : log ,
2020-07-06 22:31:40 +01:00
listener : listener ,
db : db ,
2020-07-16 12:58:40 +01:00
payments : accounts ,
2020-09-03 22:12:26 +01:00
nowFn : time . Now ,
2020-02-07 17:24:58 +00:00
}
2020-02-07 16:36:28 +00:00
2021-10-01 13:26:21 +01:00
root := mux . NewRouter ( )
2020-02-07 17:24:58 +00:00
2021-10-01 13:26:21 +01:00
api := root . PathPrefix ( "/api/" ) . Subrouter ( )
api . Use ( allowedAuthorization ( config . AuthorizationToken ) )
2020-02-07 17:24:58 +00:00
2021-10-01 13:26:21 +01:00
// When adding new options, also update README.md
api . HandleFunc ( "/users" , server . addUser ) . Methods ( "POST" )
api . HandleFunc ( "/users/{useremail}" , server . updateUser ) . Methods ( "PUT" )
api . HandleFunc ( "/users/{useremail}" , server . userInfo ) . Methods ( "GET" )
api . HandleFunc ( "/users/{useremail}" , server . deleteUser ) . Methods ( "DELETE" )
api . HandleFunc ( "/projects" , server . addProject ) . Methods ( "POST" )
api . HandleFunc ( "/projects/{project}/usage" , server . checkProjectUsage ) . Methods ( "GET" )
api . HandleFunc ( "/projects/{project}/limit" , server . getProjectLimit ) . Methods ( "GET" )
api . HandleFunc ( "/projects/{project}/limit" , server . putProjectLimit ) . Methods ( "PUT" , "POST" )
api . HandleFunc ( "/projects/{project}" , server . getProject ) . Methods ( "GET" )
api . HandleFunc ( "/projects/{project}" , server . renameProject ) . Methods ( "PUT" )
api . HandleFunc ( "/projects/{project}" , server . deleteProject ) . Methods ( "DELETE" )
api . HandleFunc ( "/projects/{project}/apikeys" , server . listAPIKeys ) . Methods ( "GET" )
api . HandleFunc ( "/projects/{project}/apikeys" , server . addAPIKey ) . Methods ( "POST" )
api . HandleFunc ( "/projects/{project}/apikeys/{name}" , server . deleteAPIKeyByName ) . Methods ( "DELETE" )
api . HandleFunc ( "/apikeys/{apikey}" , server . deleteAPIKey ) . Methods ( "DELETE" )
// This handler must be the last one because it uses the root as prefix,
// otherwise will try to serve all the handlers set after this one.
if config . StaticDir == "" {
uiAssets , err := fs . Sub ( ui , "ui/public" )
if err != nil {
log . Error ( "invalid embbeded static assets directory, the Admin UI is not enabled" )
} else {
root . PathPrefix ( "/" ) . Handler ( http . FileServer ( http . FS ( uiAssets ) ) ) . Methods ( "GET" )
}
} else {
root . PathPrefix ( "/" ) . Handler ( http . FileServer ( http . Dir ( config . StaticDir ) ) ) . Methods ( "GET" )
2020-02-07 17:24:58 +00:00
}
2021-10-01 13:26:21 +01:00
server . server . Handler = root
return server
2020-02-07 17:24:58 +00:00
}
2020-08-13 13:40:05 +01:00
// Run starts the admin endpoint.
2020-02-07 16:36:28 +00:00
func ( server * Server ) Run ( ctx context . Context ) error {
if server . listener == nil {
return nil
}
ctx , cancel := context . WithCancel ( ctx )
var group errgroup . Group
group . Go ( func ( ) error {
<- ctx . Done ( )
return Error . Wrap ( server . server . Shutdown ( context . Background ( ) ) )
} )
group . Go ( func ( ) error {
defer cancel ( )
2020-04-16 16:50:22 +01:00
err := server . server . Serve ( server . listener )
if errs2 . IsCanceled ( err ) || errors . Is ( err , http . ErrServerClosed ) {
err = nil
}
return Error . Wrap ( err )
2020-02-07 16:36:28 +00:00
} )
return group . Wait ( )
}
2020-09-03 22:12:26 +01:00
// SetNow allows tests to have the server act as if the current time is whatever they want.
func ( server * Server ) SetNow ( nowFn func ( ) time . Time ) {
server . nowFn = nowFn
}
2020-02-07 16:36:28 +00:00
// Close closes server and underlying listener.
func ( server * Server ) Close ( ) error {
return Error . Wrap ( server . server . Close ( ) )
}
2021-10-01 13:26:21 +01:00
func allowedAuthorization ( token string ) func ( next http . Handler ) http . Handler {
return func ( next http . Handler ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
if token == "" {
sendJSONError ( w , "Authorization not enabled." ,
"" , http . StatusForbidden )
return
}
equality := subtle . ConstantTimeCompare (
[ ] byte ( r . Header . Get ( "Authorization" ) ) ,
[ ] byte ( token ) ,
)
if equality != 1 {
sendJSONError ( w , "Forbidden" ,
"" , http . StatusForbidden )
return
}
r . Header . Set ( "Cache-Control" , "must-revalidate" )
next . ServeHTTP ( w , r )
} )
}
}