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"
2023-08-31 14:22:08 +01:00
backofficeui "storj.io/storj/satellite/admin/back-office/ui"
2022-03-23 13:29:47 +00:00
adminui "storj.io/storj/satellite/admin/ui"
2023-06-13 16:58:24 +01:00
"storj.io/storj/satellite/attribution"
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"
2023-04-06 12:41:14 +01:00
"storj.io/storj/satellite/payments/stripe"
2020-02-07 16:36:28 +00:00
)
2023-04-12 16:06:36 +01:00
const (
// UnauthorizedThroughOauth - message for full accesses through Oauth.
UnauthorizedThroughOauth = "This operation is not authorized through oauth."
// UnauthorizedNotInGroup - message for when api user is not part of a required access group.
UnauthorizedNotInGroup = "User must be a member of one of these groups to conduct this operation: %s"
// AuthorizationNotEnabled - message for when authorization is disabled.
AuthorizationNotEnabled = "Authorization not enabled."
)
2020-02-07 16:36:28 +00:00
// Config defines configuration for debug server.
type Config struct {
2023-08-31 14:22:08 +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:"" `
StaticDirBackOffice string ` help:"an alternate directory path which contains the static assets for the currently in development back-office. When empty, it uses the embedded assets" releaseDefault:"" devDefault:"" `
AllowedOauthHost string ` help:"the oauth host allowed to bypass token authentication." `
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
2023-04-06 12:41:14 +01:00
StripeCoinPayments ( ) stripe . DB
2023-06-13 16:58:24 +01:00
// Buckets returns database for buckets metainfo.
Buckets ( ) buckets . DB
// Attribution returns database for value attribution.
Attribution ( ) attribution . 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.
2023-08-31 14:22:08 +01: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 ( )
2023-04-14 19:06:49 +01:00
fullAccessAPI . Use ( server . withAuth ( nil ) )
2023-02-21 21:47:45 +00:00
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" )
2023-06-13 16:58:24 +01:00
fullAccessAPI . HandleFunc ( "/users/{useremail}/useragent" , server . updateUsersUserAgent ) . Methods ( "PATCH" )
2023-07-27 13:54:48 +01:00
fullAccessAPI . HandleFunc ( "/users/{useremail}/geofence" , server . createGeofenceForAccount ) . Methods ( "PATCH" )
fullAccessAPI . HandleFunc ( "/users/{useremail}/geofence" , server . deleteGeofenceForAccount ) . Methods ( "DELETE" )
2023-02-21 21:47:45 +00:00
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" )
2023-06-13 16:58:24 +01:00
fullAccessAPI . HandleFunc ( "/projects/{project}/useragent" , server . updateProjectsUserAgent ) . Methods ( "PATCH" )
2023-07-25 16:26:35 +01:00
fullAccessAPI . HandleFunc ( "/projects/{project}/geofence" , server . createGeofenceForProject ) . Methods ( "POST" )
fullAccessAPI . HandleFunc ( "/projects/{project}/geofence" , server . deleteGeofenceForProject ) . Methods ( "DELETE" )
2023-05-25 01:20:18 +01:00
fullAccessAPI . HandleFunc ( "/apikeys/{apikey}" , server . getAPIKey ) . Methods ( "GET" )
2023-02-21 21:47:45 +00:00
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 ( )
2023-04-14 19:06:49 +01:00
limitUpdateAPI . Use ( server . withAuth ( [ ] string { config . Groups . LimitUpdate } ) )
2023-02-21 21:47:45 +00:00
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" )
2023-07-21 18:09:54 +01:00
limitUpdateAPI . HandleFunc ( "/users/{useremail}/warning" , server . unWarnUser ) . Methods ( "DELETE" )
2023-02-21 21:47:45 +00:00
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
2023-08-31 14:22:08 +01:00
// Temporary path until the new back-office is implemented and we can remove the current admin UI.
if config . StaticDirBackOffice == "" {
root . PathPrefix ( "/back-office" ) . Handler (
http . StripPrefix ( "/back-office/" , http . FileServer ( http . FS ( backofficeui . Assets ) ) ) ,
) . Methods ( "GET" )
} else {
root . PathPrefix ( "/back-office" ) . Handler (
http . StripPrefix ( "/back-office/" , http . FileServer ( http . Dir ( config . StaticDirBackOffice ) ) ) ,
) . Methods ( "GET" )
}
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-04-14 19:06:49 +01:00
// SetAllowedOauthHost allows tests to set which address to recognize as belonging to the OAuth proxy.
func ( server * Server ) SetAllowedOauthHost ( host string ) {
server . config . AllowedOauthHost = host
}
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.
2023-04-14 19:06:49 +01:00
func ( server * Server ) withAuth ( 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 ) {
2023-04-14 19:06:49 +01:00
if r . Host != server . config . AllowedOauthHost {
2022-11-10 13:19:42 +00:00
// not behind the proxy; use old authentication method.
2023-04-14 19:06:49 +01:00
if server . config . AuthorizationToken == "" {
2023-04-12 16:06:36 +01:00
sendJSONError ( w , AuthorizationNotEnabled , "" , http . StatusForbidden )
2022-11-10 13:19:42 +00:00
return
}
equality := subtle . ConstantTimeCompare (
[ ] byte ( r . Header . Get ( "Authorization" ) ) ,
2023-04-14 19:06:49 +01:00
[ ] byte ( server . config . AuthorizationToken ) ,
2022-11-10 13:19:42 +00:00
)
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 {
2023-04-12 16:06:36 +01:00
// Endpoint is a full access endpoint, and requires token auth.
sendJSONError ( w , "Forbidden" , UnauthorizedThroughOauth , http . StatusForbidden )
2023-02-21 21:47:45 +00:00
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 {
2023-04-12 16:06:36 +01:00
sendJSONError ( w , "Forbidden" , fmt . Sprintf ( UnauthorizedNotInGroup , allowedGroups ) , http . StatusForbidden )
2023-02-21 21:47:45 +00:00
return
}
2021-10-01 13:26:21 +01:00
}
2023-04-14 19:06:49 +01:00
server . log . Info (
2022-11-10 13:19:42 +00:00
"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 )
} )
}
}