gogcli/internal/cmd/admin_groups.go
2026-05-05 08:48:59 +01:00

321 lines
8.7 KiB
Go

package cmd
import (
"context"
"fmt"
"os"
"strings"
admin "google.golang.org/api/admin/directory/v1"
"github.com/steipete/gogcli/internal/outfmt"
"github.com/steipete/gogcli/internal/ui"
)
// AdminGroupsCmd manages Workspace groups.
type AdminGroupsCmd struct {
List AdminGroupsListCmd `cmd:"" name:"list" aliases:"ls" help:"List groups in a domain"`
Members AdminGroupsMembersCmd `cmd:"" name:"members" help:"Manage group members"`
}
type AdminGroupsListCmd struct {
Domain string `name:"domain" help:"Domain to list groups from (e.g., example.com)"`
Max int64 `name:"max" aliases:"limit" help:"Max results" default:"100"`
Page string `name:"page" aliases:"cursor" help:"Page token"`
All bool `name:"all" aliases:"all-pages,allpages" help:"Fetch all pages"`
FailEmpty bool `name:"fail-empty" aliases:"non-empty,require-results" help:"Exit with code 3 if no results"`
}
func (c *AdminGroupsListCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
account, err := requireAdminAccount(flags)
if err != nil {
return err
}
domain := strings.TrimSpace(c.Domain)
if domain == "" {
return usage("domain required (e.g., --domain example.com)")
}
svc, err := newAdminDirectoryService(ctx, account)
if err != nil {
return wrapAdminDirectoryError(err, account)
}
fetch := func(pageToken string) ([]*admin.Group, string, error) {
call := svc.Groups.List().
Domain(domain).
MaxResults(c.Max).
Context(ctx)
if strings.TrimSpace(pageToken) != "" {
call = call.PageToken(pageToken)
}
resp, fetchErr := call.Do()
if fetchErr != nil {
return nil, "", wrapAdminDirectoryError(fetchErr, account)
}
return resp.Groups, resp.NextPageToken, nil
}
groups, nextPageToken, err := loadPagedItems(c.Page, c.All, fetch)
if err != nil {
return err
}
if outfmt.IsJSON(ctx) {
type item struct {
Email string `json:"email"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
DirectMembersCount int64 `json:"directMembersCount"`
}
items := make([]item, 0, len(groups))
for _, group := range groups {
if group == nil {
continue
}
items = append(items, item{
Email: group.Email,
Name: group.Name,
Description: group.Description,
DirectMembersCount: group.DirectMembersCount,
})
}
if err := outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
"groups": items,
"nextPageToken": nextPageToken,
}); err != nil {
return err
}
if len(items) == 0 {
return failEmptyExit(c.FailEmpty)
}
return nil
}
if len(groups) == 0 {
u.Err().Println("No groups found")
return failEmptyExit(c.FailEmpty)
}
w, flush := tableWriter(ctx)
defer flush()
fmt.Fprintln(w, "EMAIL\tNAME\tMEMBERS\tDESCRIPTION")
for _, group := range groups {
if group == nil {
continue
}
desc := group.Description
if len(desc) > 50 {
desc = desc[:47] + "..."
}
fmt.Fprintf(w, "%s\t%s\t%d\t%s\n",
sanitizeTab(group.Email),
sanitizeTab(group.Name),
group.DirectMembersCount,
sanitizeTab(desc),
)
}
printNextPageHint(u, nextPageToken)
return nil
}
type AdminGroupsMembersCmd struct {
List AdminGroupsMembersListCmd `cmd:"" name:"list" aliases:"ls" help:"List group members"`
Add AdminGroupsMembersAddCmd `cmd:"" name:"add" aliases:"invite" help:"Add a member to a group"`
Remove AdminGroupsMembersRemoveCmd `cmd:"" name:"remove" aliases:"rm,del,delete" help:"Remove a member from a group"`
}
type AdminGroupsMembersListCmd struct {
GroupEmail string `arg:"" name:"groupEmail" help:"Group email (e.g., engineering@example.com)"`
Max int64 `name:"max" aliases:"limit" help:"Max results" default:"100"`
Page string `name:"page" aliases:"cursor" help:"Page token"`
All bool `name:"all" aliases:"all-pages,allpages" help:"Fetch all pages"`
FailEmpty bool `name:"fail-empty" aliases:"non-empty,require-results" help:"Exit with code 3 if no results"`
}
func (c *AdminGroupsMembersListCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
account, err := requireAdminAccount(flags)
if err != nil {
return err
}
groupEmail := strings.TrimSpace(c.GroupEmail)
if groupEmail == "" {
return usage("group email required")
}
svc, err := newAdminDirectoryService(ctx, account)
if err != nil {
return wrapAdminDirectoryError(err, account)
}
fetch := func(pageToken string) ([]*admin.Member, string, error) {
call := svc.Members.List(groupEmail).
MaxResults(c.Max).
Context(ctx)
if strings.TrimSpace(pageToken) != "" {
call = call.PageToken(pageToken)
}
resp, fetchErr := call.Do()
if fetchErr != nil {
return nil, "", wrapAdminDirectoryError(fetchErr, account)
}
return resp.Members, resp.NextPageToken, nil
}
members, nextPageToken, err := loadPagedItems(c.Page, c.All, fetch)
if err != nil {
return err
}
if outfmt.IsJSON(ctx) {
type item struct {
Email string `json:"email"`
Role string `json:"role"`
Type string `json:"type"`
}
items := make([]item, 0, len(members))
for _, member := range members {
if member == nil {
continue
}
items = append(items, item{
Email: member.Email,
Role: member.Role,
Type: member.Type,
})
}
if err := outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
"members": items,
"nextPageToken": nextPageToken,
}); err != nil {
return err
}
if len(items) == 0 {
return failEmptyExit(c.FailEmpty)
}
return nil
}
if len(members) == 0 {
u.Err().Println("No members found")
return failEmptyExit(c.FailEmpty)
}
w, flush := tableWriter(ctx)
defer flush()
fmt.Fprintln(w, "EMAIL\tROLE\tTYPE")
for _, member := range members {
if member == nil {
continue
}
fmt.Fprintf(w, "%s\t%s\t%s\n",
sanitizeTab(member.Email),
sanitizeTab(member.Role),
sanitizeTab(member.Type),
)
}
printNextPageHint(u, nextPageToken)
return nil
}
type AdminGroupsMembersAddCmd struct {
GroupEmail string `arg:"" name:"groupEmail" help:"Group email"`
MemberEmail string `arg:"" name:"memberEmail" help:"Member email to add"`
Role string `name:"role" help:"Member role (MEMBER, MANAGER, OWNER)" default:"MEMBER"`
}
func (c *AdminGroupsMembersAddCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
account, err := requireAdminAccount(flags)
if err != nil {
return err
}
groupEmail := strings.TrimSpace(c.GroupEmail)
memberEmail := strings.TrimSpace(c.MemberEmail)
if groupEmail == "" || memberEmail == "" {
return usage("group email and member email required")
}
role := strings.ToUpper(c.Role)
if role != adminRoleMember && role != adminRoleManager && role != adminRoleOwner {
return usage("role must be MEMBER, MANAGER, or OWNER")
}
member := &admin.Member{
Email: memberEmail,
Role: role,
}
if dryRunErr := dryRunExit(ctx, flags, fmt.Sprintf("add %s to %s as %s", memberEmail, groupEmail, role), member); dryRunErr != nil {
return dryRunErr
}
svc, err := newAdminDirectoryService(ctx, account)
if err != nil {
return wrapAdminDirectoryError(err, account)
}
created, err := svc.Members.Insert(groupEmail, member).Context(ctx).Do()
if err != nil {
return wrapAdminDirectoryError(err, account)
}
if outfmt.IsJSON(ctx) {
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
"email": created.Email,
"role": created.Role,
})
}
u.Out().Printf("Added %s to %s as %s", created.Email, groupEmail, created.Role)
return nil
}
type AdminGroupsMembersRemoveCmd struct {
GroupEmail string `arg:"" name:"groupEmail" help:"Group email"`
MemberEmail string `arg:"" name:"memberEmail" help:"Member email to remove"`
}
func (c *AdminGroupsMembersRemoveCmd) Run(ctx context.Context, flags *RootFlags) error {
account, err := requireAdminAccount(flags)
if err != nil {
return err
}
groupEmail := strings.TrimSpace(c.GroupEmail)
memberEmail := strings.TrimSpace(c.MemberEmail)
if groupEmail == "" || memberEmail == "" {
return usage("group email and member email required")
}
if confirmErr := confirmDestructive(ctx, flags, fmt.Sprintf("remove %s from %s", memberEmail, groupEmail)); confirmErr != nil {
return confirmErr
}
svc, err := newAdminDirectoryService(ctx, account)
if err != nil {
return wrapAdminDirectoryError(err, account)
}
if err := svc.Members.Delete(groupEmail, memberEmail).Context(ctx).Do(); err != nil {
return wrapAdminDirectoryError(err, account)
}
if outfmt.IsJSON(ctx) {
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
"removed": true,
"email": memberEmail,
"group": groupEmail,
})
}
u := ui.FromContext(ctx)
u.Out().Printf("Removed %s from %s", memberEmail, groupEmail)
return nil
}