Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/azdo_help_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,21 @@ Work with Azure DevOps security.

Manage security groups

#### `azdo security group create [ORGANIZATION|ORGANIZATION/PROJECT] [flags]`

Create a security group

```
--description string Description of the new security group.
--email string Create a security group using an existing AAD group's email address.
--groups strings A comma-separated list of group descriptors to add the new group to.
-q, --jq expression Filter JSON output using a jq expression
--json fields Output JSON with the specified fields
--name string Name of the new security group.
--origin-id string Create a security group using an existing AAD group's origin ID.
-t, --template string Format JSON output using a Go template; see "azdo help formatting"
````

#### `azdo security group list [ORGANIZATION[/PROJECT]] [flags]`

List security groups
Expand Down
1 change: 1 addition & 0 deletions docs/azdo_security_group.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## azdo security group
Manage security groups in Azure DevOps.
### Available commands
* [azdo security group create](./azdo_security_group_create.md)
* [azdo security group list](./azdo_security_group_list.md)

### See also
Expand Down
47 changes: 47 additions & 0 deletions docs/azdo_security_group_create.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
## azdo security group create
```
azdo security group create [ORGANIZATION|ORGANIZATION/PROJECT] [flags]
```
Create a security group in an Azure DevOps organization or project.

Security groups can be created by name, email, or origin ID. Exactly one of these must be specified.

### Options


* `--description` `string`

Description of the new security group.

* `--email` `string`

Create a security group using an existing AAD group's email address.

* `--groups` `strings`

A comma-separated list of group descriptors to add the new group to.

* `-q`, `--jq` `expression`

Filter JSON output using a jq expression

* `--json` `fields`

Output JSON with the specified fields

* `--name` `string`

Name of the new security group.

* `--origin-id` `string`

Create a security group using an existing AAD group's origin ID.

* `-t`, `--template` `string`

Format JSON output using a Go template; see "azdo help formatting"


### See also

* [azdo security group](./azdo_security_group.md)
10 changes: 3 additions & 7 deletions internal/cmd/graph/user/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,8 @@ func runCmd(ctx util.CmdContext, opts *usersListOptions) error {
}

// Build subject types
var subj *[]string
if len(opts.subjectTypes) > 0 {
s := opts.subjectTypes
subj = &s
} else {
subj = &[]string{"aad"}
if len(opts.subjectTypes) == 0 {
opts.subjectTypes = []string{"aad"}
}

// Pagination loop using continuation token header
Expand Down Expand Up @@ -203,7 +199,7 @@ func runCmd(ctx util.CmdContext, opts *usersListOptions) error {
cont = nil
for len(users) < opts.top {
res, err := client.ListUsers(ctx.Context(), graph.ListUsersArgs{
SubjectTypes: subj,
SubjectTypes: &opts.subjectTypes,
ContinuationToken: cont,
ScopeDescriptor: types.ToPtr(scopeDescriptor),
})
Expand Down
216 changes: 216 additions & 0 deletions internal/cmd/security/group/create/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package create

import (
"context"
"fmt"
"slices"
"strings"

"github.com/MakeNowJust/heredoc"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/core"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/graph"
"github.com/spf13/cobra"
"github.com/tmeckel/azdo-cli/internal/cmd/util"
"github.com/tmeckel/azdo-cli/internal/types"
)

type createOpts struct {
name string
description string
email string
originID string
groups []string
scope string
exporter util.Exporter
}

type groupCreateResult struct {
Descriptor *string `json:"descriptor,omitempty"`
PrincipalName *string `json:"principalName,omitempty"`
DisplayName *string `json:"displayName,omitempty"`
Description *string `json:"description,omitempty"`
MailAddress *string `json:"mailAddress,omitempty"`
Origin *string `json:"origin,omitempty"`
OriginID *string `json:"originId,omitempty"`
URL *string `json:"url,omitempty"`
Domain *string `json:"domain,omitempty"`
}

func NewCmd(ctx util.CmdContext) *cobra.Command {
opts := &createOpts{}

cmd := &cobra.Command{
Use: "create [ORGANIZATION|ORGANIZATION/PROJECT]",
Short: "Create a security group",
Long: heredoc.Doc(`
Create a security group in an Azure DevOps organization or project.

Security groups can be created by name, email, or origin ID. Exactly one of these must be specified.
`),
Args: cobra.MaximumNArgs(1),
Aliases: []string{
"add",
"new",
"c",
},
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
opts.scope = args[0]
}
return runCreate(ctx, opts)
},
}

cmd.Flags().StringVar(&opts.name, "name", "", "Name of the new security group.")
cmd.Flags().StringVar(&opts.description, "description", "", "Description of the new security group.")
cmd.Flags().StringVar(&opts.email, "email", "", "Create a security group using an existing AAD group's email address.")
cmd.Flags().StringVar(&opts.originID, "origin-id", "", "Create a security group using an existing AAD group's origin ID.")
cmd.Flags().StringSliceVar(&opts.groups, "groups", nil, "A comma-separated list of group descriptors to add the new group to.")
util.AddJSONFlags(cmd, &opts.exporter, []string{"descriptor", "principalName", "displayName", "description", "mailAddress", "origin", "originId", "url", "domain"})

return cmd
}

func runCreate(ctx util.CmdContext, opts *createOpts) error {
ios, err := ctx.IOStreams()
if err != nil {
return err
}

ios.StartProgressIndicator()
defer ios.StopProgressIndicator()

cfg, err := ctx.Config()
if err != nil {
return err
}

var organization, project string

organization, err = cfg.Authentication().GetDefaultOrganization()
if err != nil {
return err
}

if opts.scope != "" {
parts := strings.Split(opts.scope, "/")
if len(parts) < 1 || len(parts) > 2 {
return util.FlagErrorf("invalid scope format: %s", opts.scope)
}
organization = parts[0]
if len(parts) == 2 {
organization = parts[0]
project = parts[1]
}
if !slices.Contains(cfg.Authentication().GetOrganizations(), organization) {
return util.FlagErrorf("organization %q not found", organization)
}
}

cf := ctx.ClientFactory()

graphClient, err := cf.Graph(ctx.Context(), organization)
if err != nil {
return err
}

var scopeDescriptor *string
if project != "" {
coreClient, err := cf.Core(ctx.Context(), organization)
if err != nil {
return err
}

prj, err := coreClient.GetProject(context.Background(), core.GetProjectArgs{
ProjectId: &project,
})
if err != nil {
return fmt.Errorf("failed to get project: %w", err)
}

descriptor, err := graphClient.GetDescriptor(ctx.Context(), graph.GetDescriptorArgs{
StorageKey: prj.Id,
})
if err != nil {
return fmt.Errorf("failed to get project descriptor: %w", err)
}

scopeDescriptor = descriptor.Value
}

var groupDescriptors *[]string
if len(opts.groups) > 0 {
groupDescriptors = &opts.groups
}

var createdGroup *graph.GraphGroup

switch {
case opts.name != "":
args := graph.CreateGroupVstsArgs{
CreationContext: &graph.GraphGroupVstsCreationContext{
DisplayName: &opts.name,
Description: &opts.description,
},
ScopeDescriptor: scopeDescriptor,
GroupDescriptors: groupDescriptors,
}
createdGroup, err = graphClient.CreateGroupVsts(context.Background(), args)
case opts.email != "":
args := graph.CreateGroupMailAddressArgs{
CreationContext: &graph.GraphGroupMailAddressCreationContext{
MailAddress: &opts.email,
},
ScopeDescriptor: scopeDescriptor,
GroupDescriptors: groupDescriptors,
}
createdGroup, err = graphClient.CreateGroupMailAddress(context.Background(), args)
case opts.originID != "":
args := graph.CreateGroupOriginIdArgs{
CreationContext: &graph.GraphGroupOriginIdCreationContext{
OriginId: &opts.originID,
},
ScopeDescriptor: scopeDescriptor,
GroupDescriptors: groupDescriptors,
}
createdGroup, err = graphClient.CreateGroupOriginId(context.Background(), args)
default:
return fmt.Errorf("exactly one of --name, --email, or --origin-id must be specified")
}

if err != nil {
return err
}

ios.StopProgressIndicator()

if opts.exporter != nil {
result := groupCreateResult{
Descriptor: createdGroup.Descriptor,
PrincipalName: createdGroup.PrincipalName,
DisplayName: createdGroup.DisplayName,
Description: createdGroup.Description,
MailAddress: createdGroup.MailAddress,
Origin: createdGroup.Origin,
OriginID: createdGroup.OriginId,
URL: createdGroup.Url,
Domain: createdGroup.Domain,
}
return opts.exporter.Write(ios, result)
}

tp, err := ctx.Printer("list")
if err != nil {
return err
}

tp.AddColumns("Descriptor", "PrincipalName", "DisplayName", "Description")
tp.EndRow()
tp.AddField(*createdGroup.Descriptor)
tp.AddField(*createdGroup.PrincipalName)
tp.AddField(types.GetValue(createdGroup.DisplayName, ""))
tp.AddField(types.GetValue(createdGroup.Description, ""))

tp.EndRow()
return tp.Render()
}
2 changes: 2 additions & 0 deletions internal/cmd/security/group/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package group

import (
"github.com/spf13/cobra"
"github.com/tmeckel/azdo-cli/internal/cmd/security/group/create"
"github.com/tmeckel/azdo-cli/internal/cmd/security/group/list"
"github.com/tmeckel/azdo-cli/internal/cmd/util"
)
Expand All @@ -17,6 +18,7 @@ func NewCmd(ctx util.CmdContext) *cobra.Command {
},
}

cmd.AddCommand(create.NewCmd(ctx))
cmd.AddCommand(list.NewCmd(ctx))

return cmd
Expand Down
11 changes: 2 additions & 9 deletions internal/cmd/security/group/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"go.uber.org/zap"

"github.com/MakeNowJust/heredoc"
"github.com/google/uuid"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/core"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/graph"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -151,14 +150,8 @@ func runCommand(ctx util.CmdContext, o *opts) error {
}
zap.L().Sugar().Debugf("Fetched project: %s", types.GetValue(project.Name, ""))

// Get project descriptor
projectUUID, err := uuid.Parse(project.Id.String())
if err != nil {
return fmt.Errorf("invalid project ID: %w", err)
}

descriptor, err := graphClient.GetDescriptor(ctx.Context(), graph.GetDescriptorArgs{
StorageKey: &projectUUID,
StorageKey: project.Id,
})
if err != nil {
return fmt.Errorf("failed to get project descriptor: %w", err)
Expand Down Expand Up @@ -234,7 +227,7 @@ func runCommand(ctx util.CmdContext, o *opts) error {
return err
}

tp.AddColumns("ID", "Name", "Description", "Principal Name")
tp.AddColumns("ID", "DisplayName", "Description", "Principal Name")
tp.EndRow()
for _, g := range allGroups {
tp.AddField(types.GetValue(g.Descriptor, ""))
Expand Down
Loading