@@ -10,6 +10,8 @@ import (
1010 "net"
1111 "net/http"
1212 "net/url"
13+ "os"
14+ "path/filepath"
1315 "reflect"
1416 "sort"
1517 "strconv"
@@ -19,20 +21,23 @@ import (
1921
2022 "github.com/strahe/synaps3/internal/config"
2123 "github.com/urfave/cli/v3"
24+ "golang.org/x/term"
2225)
2326
2427const (
25- defaultAdminTimeout = 10 * time .Second
26- adminSettingsWriteHeader = "X-SynapS3-Settings-Write"
27- adminSettingsWriteValue = "1"
28+ defaultAdminTimeout = 10 * time .Second
2829)
2930
3031type adminCommandOptions struct {
31- AdminURL string
32- ConfigPath string
33- ConfigSet bool
34- Timeout time.Duration
35- JSON bool
32+ AdminURL string
33+ ConfigPath string
34+ ConfigSet bool
35+ Timeout time.Duration
36+ JSON bool
37+ AdminUsername string
38+ AdminPassword string
39+ AuthDisabled bool
40+ ConfigSource string
3641}
3742
3843func adminCommand () * cli.Command {
@@ -452,17 +457,29 @@ func adminTaskCommand() *cli.Command {
452457
453458type adminAPIClient struct {
454459 baseURL string
460+ username string
461+ password string
455462 httpClient * http.Client
456463}
457464
458465func newAdminClientFromCommand (ctx context.Context , cmd * cli.Command ) (* adminAPIClient , adminCommandOptions , error ) {
459466 opts := adminOptionsFromCommand (cmd )
467+ if err := resolveAdminClientConfig (ctx , & opts ); err != nil {
468+ return nil , adminCommandOptions {}, err
469+ }
470+ password , err := resolveAdminPassword (cmd , opts )
471+ if err != nil {
472+ return nil , adminCommandOptions {}, err
473+ }
474+ opts .AdminPassword = password
460475 baseURL , err := resolveAdminBaseURL (ctx , opts )
461476 if err != nil {
462477 return nil , adminCommandOptions {}, err
463478 }
464479 return & adminAPIClient {
465- baseURL : baseURL ,
480+ baseURL : baseURL ,
481+ username : opts .AdminUsername ,
482+ password : opts .AdminPassword ,
466483 httpClient : & http.Client {
467484 Timeout : opts .Timeout ,
468485 },
@@ -476,12 +493,77 @@ func adminOptionsFromCommand(cmd *cli.Command) adminCommandOptions {
476493 timeout = defaultAdminTimeout
477494 }
478495 return adminCommandOptions {
479- AdminURL : cmd .String ("admin-url" ),
480- ConfigPath : root .String ("config" ),
481- ConfigSet : root .IsSet ("config" ),
482- Timeout : timeout ,
483- JSON : cmd .Bool ("json" ),
496+ AdminURL : cmd .String ("admin-url" ),
497+ ConfigPath : root .String ("config" ),
498+ ConfigSet : root .IsSet ("config" ),
499+ Timeout : timeout ,
500+ JSON : cmd .Bool ("json" ),
501+ AdminUsername : "admin" ,
502+ AdminPassword : os .Getenv ("SYNAPS3_ADMIN_PASSWORD" ),
503+ }
504+ }
505+
506+ func resolveAdminClientConfig (_ context.Context , opts * adminCommandOptions ) error {
507+ src , err := config .ResolveSource (opts .ConfigPath , opts .ConfigSet )
508+ if err != nil {
509+ if strings .TrimSpace (opts .AdminURL ) != "" && ! opts .ConfigSet {
510+ return nil
511+ }
512+ return err
513+ }
514+ opts .ConfigSource = src .Path
515+ cfg , err := config .LoadSource (src )
516+ if err != nil {
517+ if strings .TrimSpace (opts .AdminURL ) != "" && ! opts .ConfigSet {
518+ return nil
519+ }
520+ return fmt .Errorf ("loading config for admin client: %w" , err )
521+ }
522+ if username := strings .TrimSpace (cfg .Admin .Auth .Username ); username != "" {
523+ opts .AdminUsername = username
524+ }
525+ opts .AuthDisabled = ! cfg .Admin .Auth .Enabled
526+ return nil
527+ }
528+
529+ func resolveAdminPassword (cmd * cli.Command , opts adminCommandOptions ) (string , error ) {
530+ if opts .AdminPassword != "" || opts .AuthDisabled {
531+ return opts .AdminPassword , nil
484532 }
533+ if opts .ConfigSource != "" {
534+ password , ok , err := config .ReadAdminInitialPasswordFile (filepath .Dir (opts .ConfigSource ))
535+ if err != nil {
536+ return "" , err
537+ }
538+ if ok {
539+ return password , nil
540+ }
541+ }
542+ if ! adminPasswordPromptAvailable (cmd .Root ().ErrWriter ) {
543+ if strings .TrimSpace (opts .AdminURL ) != "" && ! opts .ConfigSet {
544+ return opts .AdminPassword , nil
545+ }
546+ return "" , errors .New ("admin password is required but terminal is not interactive; set SYNAPS3_ADMIN_PASSWORD or create admin-initial-password next to the config file" )
547+ }
548+ if _ , err := fmt .Fprint (cmd .Root ().ErrWriter , "Admin password: " ); err != nil {
549+ return "" , err
550+ }
551+ passwordBytes , err := term .ReadPassword (int (os .Stdin .Fd ()))
552+ if err != nil {
553+ return "" , fmt .Errorf ("reading admin password: %w" , err )
554+ }
555+ if _ , err := fmt .Fprintln (cmd .Root ().ErrWriter ); err != nil {
556+ return "" , err
557+ }
558+ return string (passwordBytes ), nil
559+ }
560+
561+ func adminPasswordPromptAvailable (errWriter io.Writer ) bool {
562+ errFile , ok := errWriter .(* os.File )
563+ if ! ok || errFile != os .Stderr {
564+ return false
565+ }
566+ return term .IsTerminal (int (os .Stdin .Fd ())) && term .IsTerminal (int (errFile .Fd ()))
485567}
486568
487569func resolveAdminBaseURL (_ context.Context , opts adminCommandOptions ) (string , error ) {
@@ -582,12 +664,10 @@ func (c *adminAPIClient) doJSON(ctx context.Context, method, path string, body a
582664 if err != nil {
583665 return err
584666 }
585- if hasBody || writeHeader {
667+ c .applyAuth (req )
668+ if hasBody {
586669 req .Header .Set ("Content-Type" , "application/json" )
587670 }
588- if writeHeader {
589- req .Header .Set (adminSettingsWriteHeader , adminSettingsWriteValue )
590- }
591671
592672 resp , err := c .httpClient .Do (req )
593673 if err != nil {
@@ -607,6 +687,17 @@ func (c *adminAPIClient) doJSON(ctx context.Context, method, path string, body a
607687 return nil
608688}
609689
690+ func (c * adminAPIClient ) applyAuth (req * http.Request ) {
691+ if c .password == "" {
692+ return
693+ }
694+ username := strings .TrimSpace (c .username )
695+ if username == "" {
696+ username = "admin"
697+ }
698+ req .SetBasicAuth (username , c .password )
699+ }
700+
610701func (c * adminAPIClient ) endpoint (path string ) string {
611702 if strings .HasPrefix (path , "/" ) {
612703 return c .baseURL + path
0 commit comments