fix(groups): improve cloud identity errors
This commit is contained in:
parent
f30aad0e9d
commit
40089bcbc7
@ -65,7 +65,7 @@ func (c *CalendarTeamCmd) Run(ctx context.Context, flags *RootFlags) error {
|
||||
// Get group members via Cloud Identity API
|
||||
cloudSvc, err := newCloudIdentityService(ctx, account)
|
||||
if err != nil {
|
||||
return wrapCloudIdentityError(err)
|
||||
return wrapCloudIdentityError(err, account)
|
||||
}
|
||||
|
||||
memberEmails, err := collectGroupMemberEmails(ctx, cloudSvc, groupEmail)
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
|
||||
"google.golang.org/api/cloudidentity/v1"
|
||||
|
||||
"github.com/steipete/gogcli/internal/errfmt"
|
||||
"github.com/steipete/gogcli/internal/googleapi"
|
||||
"github.com/steipete/gogcli/internal/outfmt"
|
||||
"github.com/steipete/gogcli/internal/ui"
|
||||
@ -41,7 +42,7 @@ func (c *GroupsListCmd) Run(ctx context.Context, flags *RootFlags) error {
|
||||
|
||||
svc, err := newCloudIdentityService(ctx, account)
|
||||
if err != nil {
|
||||
return wrapCloudIdentityError(err)
|
||||
return wrapCloudIdentityError(err, account)
|
||||
}
|
||||
|
||||
// Search for all groups the user belongs to
|
||||
@ -53,7 +54,7 @@ func (c *GroupsListCmd) Run(ctx context.Context, flags *RootFlags) error {
|
||||
Context(ctx).
|
||||
Do()
|
||||
if err != nil {
|
||||
return wrapCloudIdentityError(err)
|
||||
return wrapCloudIdentityError(err, account)
|
||||
}
|
||||
|
||||
if outfmt.IsJSON(ctx) {
|
||||
@ -102,15 +103,18 @@ func (c *GroupsListCmd) Run(ctx context.Context, flags *RootFlags) error {
|
||||
}
|
||||
|
||||
// wrapCloudIdentityError provides helpful error messages for common Cloud Identity API issues.
|
||||
func wrapCloudIdentityError(err error) error {
|
||||
func wrapCloudIdentityError(err error, account string) error {
|
||||
errStr := err.Error()
|
||||
if strings.Contains(errStr, "accessNotConfigured") ||
|
||||
strings.Contains(errStr, "Cloud Identity API has not been used") {
|
||||
return fmt.Errorf("cloud Identity API is not enabled; enable it at: https://console.developers.google.com/apis/api/cloudidentity.googleapis.com/overview (%w)", err)
|
||||
return errfmt.NewUserFacingError("Cloud Identity API is not enabled; enable it at: https://console.developers.google.com/apis/api/cloudidentity.googleapis.com/overview", err)
|
||||
}
|
||||
if strings.Contains(errStr, "insufficientPermissions") ||
|
||||
strings.Contains(errStr, "insufficient authentication scopes") {
|
||||
return fmt.Errorf("insufficient permissions for Cloud Identity API; re-authenticate with the cloud-identity.groups.readonly scope: gog auth add <account> --services groups\n\nOriginal error: %w", err)
|
||||
return errfmt.NewUserFacingError("Insufficient permissions for Cloud Identity API; re-authenticate with the cloud-identity.groups.readonly scope: gog auth add <account> --services groups", err)
|
||||
}
|
||||
if isConsumerAccount(account) && (strings.Contains(errStr, "invalid argument") || strings.Contains(errStr, "badRequest")) {
|
||||
return errfmt.NewUserFacingError("Cloud Identity groups require a Google Workspace/Cloud Identity account; consumer accounts (gmail.com/googlemail.com) are not supported.", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@ -147,7 +151,7 @@ func (c *GroupsMembersCmd) Run(ctx context.Context, flags *RootFlags) error {
|
||||
|
||||
svc, err := newCloudIdentityService(ctx, account)
|
||||
if err != nil {
|
||||
return wrapCloudIdentityError(err)
|
||||
return wrapCloudIdentityError(err, account)
|
||||
}
|
||||
|
||||
// First, look up the group by email to get its resource name
|
||||
@ -211,6 +215,11 @@ func (c *GroupsMembersCmd) Run(ctx context.Context, flags *RootFlags) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func isConsumerAccount(account string) bool {
|
||||
account = strings.ToLower(strings.TrimSpace(account))
|
||||
return strings.HasSuffix(account, "@gmail.com") || strings.HasSuffix(account, "@googlemail.com")
|
||||
}
|
||||
|
||||
// lookupGroupByEmail finds a group by its email address and returns its resource name.
|
||||
func lookupGroupByEmail(ctx context.Context, svc *cloudidentity.Service, email string) (string, error) {
|
||||
resp, err := svc.Groups.Lookup().
|
||||
|
||||
@ -9,22 +9,29 @@ import (
|
||||
)
|
||||
|
||||
func TestWrapCloudIdentityError(t *testing.T) {
|
||||
err := wrapCloudIdentityError(errors.New("accessNotConfigured: boom"))
|
||||
if !strings.Contains(err.Error(), "cloud Identity API is not enabled") {
|
||||
err := wrapCloudIdentityError(errors.New("accessNotConfigured: boom"), "user@company.com")
|
||||
if !strings.Contains(err.Error(), "Cloud Identity API is not enabled") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
err = wrapCloudIdentityError(errors.New("insufficientPermissions: nope"))
|
||||
if !strings.Contains(err.Error(), "insufficient permissions") {
|
||||
err = wrapCloudIdentityError(errors.New("insufficientPermissions: nope"), "user@company.com")
|
||||
if !strings.Contains(err.Error(), "Insufficient permissions") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
other := errors.New("other")
|
||||
if !errors.Is(wrapCloudIdentityError(other), other) {
|
||||
if !errors.Is(wrapCloudIdentityError(other, "user@company.com"), other) {
|
||||
t.Fatalf("expected passthrough error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapCloudIdentityError_ConsumerAccount(t *testing.T) {
|
||||
err := wrapCloudIdentityError(errors.New("badRequest: Request contains an invalid argument."), "person@gmail.com")
|
||||
if !strings.Contains(err.Error(), "consumer accounts") {
|
||||
t.Fatalf("unexpected consumer error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRelationType(t *testing.T) {
|
||||
if got := getRelationType("DIRECT"); got != "direct" {
|
||||
t.Fatalf("unexpected relation: %q", got)
|
||||
|
||||
@ -74,17 +74,17 @@ func TestGroupsList_NoGroups_Text(t *testing.T) {
|
||||
|
||||
func TestWrapCloudIdentityError_Messages(t *testing.T) {
|
||||
accessErr := errors.New("accessNotConfigured")
|
||||
if err := wrapCloudIdentityError(accessErr); err == nil || !strings.Contains(strings.ToLower(err.Error()), "cloud identity api is not enabled") {
|
||||
if err := wrapCloudIdentityError(accessErr, "user@company.com"); err == nil || !strings.Contains(err.Error(), "Cloud Identity API is not enabled") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
permErr := errors.New("insufficientPermissions")
|
||||
if err := wrapCloudIdentityError(permErr); err == nil || !strings.Contains(err.Error(), "insufficient permissions") {
|
||||
if err := wrapCloudIdentityError(permErr, "user@company.com"); err == nil || !strings.Contains(err.Error(), "Insufficient permissions") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
other := errors.New("other")
|
||||
if err := wrapCloudIdentityError(other); err == nil || err.Error() != "other" {
|
||||
if err := wrapCloudIdentityError(other, "user@company.com"); err == nil || err.Error() != "other" {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,6 +53,11 @@ func Format(err error) string {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
var userErr *UserFacingError
|
||||
if errors.As(err, &userErr) {
|
||||
return userErr.Message
|
||||
}
|
||||
|
||||
var gerr *ggoogleapi.Error
|
||||
if errors.As(err, &gerr) {
|
||||
reason := ""
|
||||
@ -70,6 +75,32 @@ func Format(err error) string {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
// UserFacingError forces a specific message, while preserving the underlying cause.
|
||||
type UserFacingError struct {
|
||||
Message string
|
||||
Cause error
|
||||
}
|
||||
|
||||
func (e *UserFacingError) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func (e *UserFacingError) Unwrap() error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
func NewUserFacingError(message string, cause error) error {
|
||||
return &UserFacingError{Message: message, Cause: cause}
|
||||
}
|
||||
|
||||
// formatParseError enhances Kong parse errors with helpful hints.
|
||||
func formatParseError(err *kong.ParseError) string {
|
||||
msg := err.Error()
|
||||
|
||||
@ -50,6 +50,15 @@ func TestFormat_KeyNotFound(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormat_UserFacingError(t *testing.T) {
|
||||
err := NewUserFacingError("friendly", errNope)
|
||||
got := Format(err)
|
||||
|
||||
if got != "friendly" {
|
||||
t.Fatalf("unexpected: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormat_GoogleAPIError(t *testing.T) {
|
||||
err := &ggoogleapi.Error{
|
||||
Code: 403,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user