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"
2020-04-16 16:50:22 +01:00
"errors"
2022-11-10 13:19:42 +00:00
"fmt"
2020-02-07 16:36:28 +00:00
"net"
"net/http"
2023-02-21 21:47:45 +00:00
"strings"
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"
2022-03-23 13:29:47 +00:00
adminui "storj.io/storj/satellite/admin/ui"
2021-11-12 20:47:41 +00:00
"storj.io/storj/satellite/buckets"
2020-02-07 17:24:58 +00:00
"storj.io/storj/satellite/console"
2022-05-27 21:13:01 +01:00
"storj.io/storj/satellite/console/consoleweb"
2022-04-12 17:59:07 +01:00
"storj.io/storj/satellite/console/restkeys"
2022-01-19 18:25:31 +00:00
"storj.io/storj/satellite/oidc"
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
)
// Config defines configuration for debug server.
type Config struct {
2022-11-10 13:19:42 +00: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:"" `
AllowedOauthHost string ` help:"the oauth host allowed to bypass token authentication." `
2023-02-21 22:03:46 +00:00
Groups Groups
2020-02-07 17:24:58 +00:00
AuthorizationToken string ` internal:"true" `
}
2023-02-21 22:03:46 +00:00
// Groups defines permission groups.
type Groups struct {
LimitUpdate string ` help:"the group which is only allowed to update user and project limits and freeze and unfreeze accounts." `
}
2020-02-07 17:24:58 +00:00
// 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
2022-01-19 18:25:31 +00:00
// OIDC returns the database for OIDC and OAuth information.
OIDC ( ) oidc . DB
2020-05-19 11:36:13 +01:00
// StripeCoinPayments returns database for satellite stripe coin payments
StripeCoinPayments ( ) stripecoinpayments . DB
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
2022-12-16 15:17:27 +00:00
db DB
payments payments . Accounts
buckets * buckets . Service
restKeys * restkeys . Service
freezeAccounts * console . AccountFreezeService
2020-09-03 22:12:26 +01:00
nowFn func ( ) time . Time
2021-11-01 15:27:32 +00:00
2022-05-27 21:13:01 +01:00
console consoleweb . Config
config Config
2020-02-07 16:36:28 +00:00
}
2020-08-13 13:40:05 +01:00
// NewServer returns a new administration Server.
2022-12-16 15:17:27 +00:00
func NewServer ( log * zap . Logger , listener net . Listener , db DB , buckets * buckets . Service , restKeys * restkeys . Service , freezeAccounts * console . AccountFreezeService , accounts payments . Accounts , console consoleweb . Config , 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 ,
2022-12-16 15:17:27 +00:00
db : db ,
payments : accounts ,
buckets : buckets ,
restKeys : restKeys ,
freezeAccounts : freezeAccounts ,
2020-09-03 22:12:26 +01:00
nowFn : time . Now ,
2021-11-01 15:27:32 +00:00
2022-05-27 21:13:01 +01:00
console : console ,
config : config ,
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 ( )
2020-02-07 17:24:58 +00:00
2021-10-01 13:26:21 +01:00
// When adding new options, also update README.md
2023-02-21 21:47:45 +00:00
// prod owners only
fullAccessAPI := api . NewRoute ( ) . Subrouter ( )
fullAccessAPI . Use ( withAuth ( log , config , nil ) )
fullAccessAPI . HandleFunc ( "/users" , server . addUser ) . Methods ( "POST" )
fullAccessAPI . HandleFunc ( "/users/{useremail}" , server . updateUser ) . Methods ( "PUT" )
fullAccessAPI . HandleFunc ( "/users/{useremail}" , server . deleteUser ) . Methods ( "DELETE" )
fullAccessAPI . HandleFunc ( "/users/{useremail}/mfa" , server . disableUserMFA ) . Methods ( "DELETE" )
fullAccessAPI . HandleFunc ( "/oauth/clients" , server . createOAuthClient ) . Methods ( "POST" )
fullAccessAPI . HandleFunc ( "/oauth/clients/{id}" , server . updateOAuthClient ) . Methods ( "PUT" )
fullAccessAPI . HandleFunc ( "/oauth/clients/{id}" , server . deleteOAuthClient ) . Methods ( "DELETE" )
fullAccessAPI . HandleFunc ( "/projects" , server . addProject ) . Methods ( "POST" )
fullAccessAPI . HandleFunc ( "/projects/{project}" , server . renameProject ) . Methods ( "PUT" )
fullAccessAPI . HandleFunc ( "/projects/{project}" , server . deleteProject ) . Methods ( "DELETE" )
fullAccessAPI . HandleFunc ( "/projects/{project}" , server . getProject ) . Methods ( "GET" )
fullAccessAPI . HandleFunc ( "/projects/{project}/apikeys" , server . addAPIKey ) . Methods ( "POST" )
fullAccessAPI . HandleFunc ( "/projects/{project}/apikeys" , server . listAPIKeys ) . Methods ( "GET" )
fullAccessAPI . HandleFunc ( "/projects/{project}/apikeys/{name}" , server . deleteAPIKeyByName ) . Methods ( "DELETE" )
fullAccessAPI . HandleFunc ( "/projects/{project}/buckets/{bucket}" , server . getBucketInfo ) . Methods ( "GET" )
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 . deleteAPIKey ) . Methods ( "DELETE" )
fullAccessAPI . HandleFunc ( "/restkeys/{useremail}" , server . addRESTKey ) . Methods ( "POST" )
fullAccessAPI . HandleFunc ( "/restkeys/{apikey}/revoke" , server . revokeRESTKey ) . Methods ( "PUT" )
// limit update access required
limitUpdateAPI := api . NewRoute ( ) . Subrouter ( )
limitUpdateAPI . Use ( withAuth ( log , config , [ ] string { config . Groups . LimitUpdate } ) )
limitUpdateAPI . HandleFunc ( "/users/{useremail}" , server . userInfo ) . Methods ( "GET" )
limitUpdateAPI . HandleFunc ( "/users/{useremail}/limits" , server . userLimits ) . Methods ( "GET" )
limitUpdateAPI . HandleFunc ( "/users/{useremail}/limits" , server . updateLimits ) . Methods ( "PUT" )
limitUpdateAPI . HandleFunc ( "/users/{useremail}/freeze" , server . freezeUser ) . Methods ( "PUT" )
limitUpdateAPI . HandleFunc ( "/users/{useremail}/freeze" , server . unfreezeUser ) . Methods ( "DELETE" )
limitUpdateAPI . HandleFunc ( "/projects/{project}/limit" , server . getProjectLimit ) . Methods ( "GET" )
limitUpdateAPI . HandleFunc ( "/projects/{project}/limit" , server . putProjectLimit ) . Methods ( "PUT" , "POST" )
2021-10-01 13:26:21 +01:00
// 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 == "" {
2022-03-23 13:29:47 +00:00
root . PathPrefix ( "/" ) . Handler ( http . FileServer ( http . FS ( adminui . Assets ) ) ) . Methods ( "GET" )
2021-10-01 13:26:21 +01:00
} 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
2023-02-21 21:47:45 +00:00
// withAuth checks if the requester is authorized to perform an operation. If the request did not come from the oauth proxy, verify the auth token.
// Otherwise, check that the user has the required permissions to conduct the operation. `allowedGroups` is a list of groups that are authorized.
// If it is nil, then the api method is not accessible from the oauth proxy.
func withAuth ( log * zap . Logger , config Config , allowedGroups [ ] string ) func ( next http . Handler ) http . Handler {
2021-10-01 13:26:21 +01:00
return func ( next http . Handler ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
2022-11-10 13:19:42 +00:00
if r . Host != config . AllowedOauthHost {
// not behind the proxy; use old authentication method.
if config . AuthorizationToken == "" {
sendJSONError ( w , "Authorization not enabled." ,
"" , http . StatusForbidden )
return
}
equality := subtle . ConstantTimeCompare (
[ ] byte ( r . Header . Get ( "Authorization" ) ) ,
[ ] byte ( config . AuthorizationToken ) ,
)
if equality != 1 {
sendJSONError ( w , "Forbidden" ,
"" , http . StatusForbidden )
return
}
2023-02-21 21:47:45 +00:00
} else {
// request made from oauth proxy. Check user groups against allowedGroups.
if allowedGroups == nil {
sendJSONError ( w , "Forbidden" ,
"This operation is not authorized through oauth. Please contact a production owner to proceed." , http . StatusForbidden )
return
}
var allowed bool
userGroupsString := r . Header . Get ( "X-Forwarded-Groups" )
userGroups := strings . Split ( userGroupsString , "," )
for _ , userGroup := range userGroups {
if userGroup == "" {
continue
}
for _ , permGroup := range allowedGroups {
if userGroup == permGroup {
allowed = true
break
}
}
if allowed {
break
}
}
if ! allowed {
sendJSONError ( w , "Forbidden" ,
fmt . Sprintf ( "User must be a member of one of these groups to conduct this operation: %s" , allowedGroups ) , http . StatusForbidden )
return
}
2021-10-01 13:26:21 +01:00
}
2022-11-10 13:19:42 +00:00
log . Info (
"admin action" ,
zap . String ( "host" , r . Host ) ,
zap . String ( "user" , r . Header . Get ( "X-Forwarded-Email" ) ) ,
zap . String ( "action" , fmt . Sprintf ( "%s-%s" , r . Method , r . RequestURI ) ) ,
zap . String ( "queries" , r . URL . Query ( ) . Encode ( ) ) ,
2021-10-01 13:26:21 +01:00
)
r . Header . Set ( "Cache-Control" , "must-revalidate" )
next . ServeHTTP ( w , r )
} )
}
}