diff --git a/internal/client/factory.go b/internal/client/factory.go index 1089788..2c28b0c 100644 --- a/internal/client/factory.go +++ b/internal/client/factory.go @@ -2,6 +2,7 @@ package client import ( "context" + "errors" "fmt" "net/http" "os" @@ -383,6 +384,26 @@ func (c *CustomConfigFlags) ensureOnboardingComplete( result, err := onboarding.CheckOrg(c.Context, apiHostname, tknSrc, userID, orgID, orgDisplayName) if err != nil { + // Surface auth failures as such instead of a misleading "not ready yet". + if userErr, ok := customerrors.IsUserError(err); ok { + return userErr + } + var statusErr *onboarding.HTTPStatusError + if errors.As(err, &statusErr) && statusErr.IsAuthFailure() { + return customerrors.WrapUserErrorWithHint( + "Your session is no longer valid.", + "Run 'datumctl login' to re-authenticate.", + err, + ) + } + var retrieveErr *oauth2.RetrieveError + if errors.As(err, &retrieveErr) { + return customerrors.WrapUserErrorWithHint( + "We couldn't refresh your session.", + "Run 'datumctl login' to re-authenticate.", + err, + ) + } return customerrors.WrapUserErrorWithHint( "We couldn't check whether this organization is ready yet.", "If you just finished setup in the portal, wait a moment and try again.", diff --git a/internal/onboarding/check.go b/internal/onboarding/check.go index 303df0d..d72fefd 100644 --- a/internal/onboarding/check.go +++ b/internal/onboarding/check.go @@ -267,6 +267,32 @@ func organizationRequestURL(apiHostname, userID, orgID string) string { ) } +// HTTPStatusError reports a non-200 response from the organization lookup. +type HTTPStatusError struct { + StatusCode int + Body string + OrgID string +} + +func (e *HTTPStatusError) Error() string { + if e.Body != "" { + return fmt.Sprintf("get organization %s: HTTP %d: %s", e.OrgID, e.StatusCode, e.Body) + } + return fmt.Sprintf("get organization %s: HTTP %d", e.OrgID, e.StatusCode) +} + +// IsAuthFailure reports whether the response means the caller's credentials +// were missing, expired or rejected, as opposed to a permission or server +// problem. Some gateways report this with a non-401 status and only say +// "Unauthenticated"/"Unauthorized" in the body. +func (e *HTTPStatusError) IsAuthFailure() bool { + if e.StatusCode == http.StatusUnauthorized { + return true + } + body := strings.ToLower(e.Body) + return strings.Contains(body, "unauthenticated") || strings.Contains(body, "unauthorized") +} + func fetchOrganization( ctx context.Context, apiHostname string, @@ -290,11 +316,11 @@ func fetchOrganization( if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) - msg := strings.TrimSpace(string(body)) - if msg != "" { - return resourcemanagerv1alpha1.Organization{}, fmt.Errorf("get organization %s: HTTP %d: %s", orgID, resp.StatusCode, msg) + return resourcemanagerv1alpha1.Organization{}, &HTTPStatusError{ + StatusCode: resp.StatusCode, + Body: strings.TrimSpace(string(body)), + OrgID: orgID, } - return resourcemanagerv1alpha1.Organization{}, fmt.Errorf("get organization %s: HTTP %d", orgID, resp.StatusCode) } var org resourcemanagerv1alpha1.Organization diff --git a/internal/onboarding/check_test.go b/internal/onboarding/check_test.go index caf26d2..0d9da08 100644 --- a/internal/onboarding/check_test.go +++ b/internal/onboarding/check_test.go @@ -182,3 +182,22 @@ func TestColumnLabel(t *testing.T) { t.Fatalf("ColumnLabel(OrgIncomplete) = %q", got) } } + +func TestHTTPStatusErrorIsAuthFailure(t *testing.T) { + tests := []struct { + name string + err HTTPStatusError + want bool + }{ + {"401", HTTPStatusError{StatusCode: 401}, true}, + {"403 unauthenticated body", HTTPStatusError{StatusCode: 403, Body: `{"reason":"Unauthenticated"}`}, true}, + {"500 unauthorized body", HTTPStatusError{StatusCode: 500, Body: "Unauthorized"}, true}, + {"403 forbidden", HTTPStatusError{StatusCode: 403, Body: "forbidden"}, false}, + {"500", HTTPStatusError{StatusCode: 500}, false}, + } + for _, tt := range tests { + if got := tt.err.IsAuthFailure(); got != tt.want { + t.Errorf("%s: got %v, want %v", tt.name, got, tt.want) + } + } +}