321 lines
8.7 KiB
Go
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
|
|
}
|