@@ -2,8 +2,10 @@ package main
22
33import (
44 "embed"
5+ "encoding/base64"
56 "encoding/json"
67 "fmt"
8+ "io"
79 "io/fs"
810 "os"
911 "path/filepath"
@@ -169,9 +171,18 @@ func run() (exitCode int) {
169171 brandRootCommand ()
170172 brandDCIRootCommand ()
171173 registerStatusCommands (configDir )
172- registerAuthCommands ()
174+ registerAuthCommands (configDir )
173175 registerCustomerContextCommands (configDir )
174176 registerSkillCommands ()
177+ // Unhide the customer-context command for DoiT employees so it appears in help.
178+ if cachedTokenIsDoer () {
179+ for _ , c := range cli .Root .Commands () {
180+ if c .Use == "customer-context" {
181+ c .Hidden = false
182+ break
183+ }
184+ }
185+ }
175186 addOutputFlag ()
176187 hideGlobalFlags ()
177188 customizeDCIUsage ()
@@ -182,9 +193,12 @@ func run() (exitCode int) {
182193
183194 if err := cli .Run (); err != nil {
184195 fmt .Fprintf (os .Stderr , "%v\n " , err )
196+ maybeHintDoerContext (1 , configDir )
185197 return 1
186198 }
187- return cli .GetExitCode ()
199+ code := cli .GetExitCode ()
200+ maybeHintDoerContext (code , configDir )
201+ return code
188202}
189203
190204func rejectProfileFlags (args []string ) error {
@@ -779,7 +793,86 @@ func authSource() string {
779793 return "OAuth (DoiT Console)"
780794}
781795
782- func registerAuthCommands () {
796+ // maybeHintDoerContext prints a targeted hint when a @doit.com user hits a 403
797+ // without a customer context set — covering both interactive and CI/CD usage.
798+ func maybeHintDoerContext (exitCode int , configDir string ) {
799+ status := cli .GetLastStatus ()
800+ if exitCode == 0 || (status != 401 && status != 403 ) {
801+ return
802+ }
803+ if ! cachedTokenIsDoer () {
804+ return
805+ }
806+ if readCustomerContext (configDir ) != "" {
807+ return
808+ }
809+ if term .IsTerminal (int (os .Stderr .Fd ())) {
810+ fmt .Fprintln (os .Stderr , "" )
811+ fmt .Fprintf (os .Stderr , "\033 [1;33m!\033 [0m DoiT employees need a customer context for API calls.\n " )
812+ fmt .Fprintf (os .Stderr , " Interactive: \033 [1mdci customer-context set doit.com\033 [0m\n " )
813+ fmt .Fprintf (os .Stderr , " CI/scripts: \033 [1mexport DCI_CUSTOMER_CONTEXT=doit.com\033 [0m\n " )
814+ fmt .Fprintln (os .Stderr , "" )
815+ } else {
816+ fmt .Fprintln (os .Stderr , "" )
817+ fmt .Fprintln (os .Stderr , "! DoiT employees need a customer context for API calls." )
818+ fmt .Fprintln (os .Stderr , " Interactive: dci customer-context set doit.com" )
819+ fmt .Fprintln (os .Stderr , " CI/scripts: export DCI_CUSTOMER_CONTEXT=doit.com" )
820+ fmt .Fprintln (os .Stderr , "" )
821+ }
822+ }
823+
824+ // applyDoerContext auto-configures the customer context to "doit.com" for
825+ // @doit.com accounts that haven't set one yet. The validate endpoint requires
826+ // customerContext for DoiT employees; calling this after the OAuth token is
827+ // cached fixes the chicken-and-egg problem on first login. Returns true if the
828+ // context was written so the caller can clear a 403 error from validate.
829+ func applyDoerContext (configDir string ) bool {
830+ if ! cachedTokenIsDoer () {
831+ return false
832+ }
833+ if readCustomerContext (configDir ) != "" {
834+ return false // already configured, don't overwrite
835+ }
836+ err := os .WriteFile (customerContextPath (configDir ), []byte ("doit.com\n " ), 0o600 )
837+ if err != nil {
838+ return false
839+ }
840+ fmt .Fprintln (os .Stderr , "Detected DoiT account. Set default customer context to 'doit.com'." )
841+ fmt .Fprintln (os .Stderr , "To use a different context: dci customer-context set <CONTEXT>" )
842+ return true
843+ }
844+
845+ // cachedTokenIsDoer reports whether the cached OAuth JWT contains
846+ // DoitEmployee: true. This is more reliable than email-domain matching because
847+ // it is an explicit claim set by the DoiT auth server and is domain-independent.
848+ // Returns false if the cache is empty, the token is absent, or the JWT is malformed.
849+ func cachedTokenIsDoer () bool {
850+ if cli .Cache == nil {
851+ return false
852+ }
853+ token := cli .Cache .GetString ("dci:default.token" )
854+ if token == "" {
855+ return false
856+ }
857+ parts := strings .Split (token , "." )
858+ if len (parts ) != 3 {
859+ return false
860+ }
861+ // JWT payload is base64url-encoded without padding.
862+ b , err := base64 .RawURLEncoding .DecodeString (parts [1 ])
863+ if err != nil {
864+ return false
865+ }
866+ var claims struct {
867+ DoitEmployee bool `json:"DoitEmployee"`
868+ }
869+ if err := json .Unmarshal (b , & claims ); err != nil {
870+ return false
871+ }
872+ return claims .DoitEmployee
873+ }
874+
875+ func registerAuthCommands (configDir string ) {
783876 cli .Root .AddCommand (& cobra.Command {
784877 Use : "login" ,
785878 Aliases : []string {"auth" , "init" },
@@ -791,8 +884,26 @@ func registerAuthCommands() {
791884 return fmt .Errorf ("login is not needed when DCI_API_KEY is set" )
792885 }
793886 // Trigger the OAuth flow by calling a lightweight endpoint.
887+ // Suppress the validate response body — login only needs the OAuth
888+ // side effect (token cached), not the API output.
794889 os .Args = []string {os .Args [0 ], "dci" , "validate" }
795- if err := cli .Run (); err != nil {
890+ oldOut := cli .Stdout
891+ cli .Stdout = io .Discard
892+ err := cli .Run ()
893+ cli .Stdout = oldOut
894+
895+ // Auto-configure DoiT employees who have no customer context set.
896+ // The validate endpoint requires customerContext for @doit.com accounts,
897+ // causing a 403 on first login before any context is configured. The OAuth
898+ // token exchange succeeds (token is cached) even when validate returns 403,
899+ // so we can inspect the token here and fix the chicken-and-egg problem.
900+ if applyDoerContext (configDir ) {
901+ err = nil // the 403 was due to missing context; auth itself succeeded
902+ // Reset the HTTP status so GetExitCode() returns 0 for this process.
903+ viper .Set ("rsh-ignore-status-code" , true )
904+ }
905+
906+ if err != nil {
796907 return err
797908 }
798909 fmt .Fprintln (os .Stderr , "Authenticated successfully." )
0 commit comments