Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
1 change: 1 addition & 0 deletions cli/organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func (r *RootCmd) organizations() *serpent.Command {
r.createOrganization(),
r.organizationMembers(orgContext),
r.organizationRoles(orgContext),
r.organizationSettings(orgContext),
},
}

Expand Down
159 changes: 159 additions & 0 deletions cli/organizationsettings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package cli

import (
"bytes"
"encoding/json"
"fmt"
"io"
"strings"

"golang.org/x/xerrors"

"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)

func (r *RootCmd) organizationSettings(orgContext *OrganizationContext) *serpent.Command {
cmd := &serpent.Command{
Use: "settings",
Short: "Manage organization settings.",
Aliases: []string{"setting"},
Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv)
},
Hidden: true,
Children: []*serpent.Command{
r.printOrganizationSetting(orgContext),
r.updateOrganizationSetting(orgContext),
},
}
return cmd
}

func (r *RootCmd) updateOrganizationSetting(orgContext *OrganizationContext) *serpent.Command {
client := new(codersdk.Client)
cmd := &serpent.Command{
Use: "set <groupsync | rolesync>",
Short: "Update specified organization setting.",
Long: FormatExamples(
Example{
Description: "Update group sync settings.",
Command: "coder organization settings set groupsync < input.json",
},
),
Options: []serpent.Option{},
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
r.InitClient(client),
),
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
org, err := orgContext.Selected(inv, client)
if err != nil {
return err
}

// Read in the json
inputData, err := io.ReadAll(inv.Stdin)
if err != nil {
return xerrors.Errorf("reading stdin: %w", err)
}

var setting any
switch strings.ToLower(inv.Args[0]) {
case "groupsync", "group-sync":
var req codersdk.GroupSyncSettings
err = json.Unmarshal(inputData, &req)
if err != nil {
return xerrors.Errorf("unmarshalling group sync settings: %w", err)
}
setting, err = client.PatchGroupIDPSyncSettings(ctx, org.ID.String(), req)
case "rolesync", "role-sync":
var req codersdk.RoleSyncSettings
err = json.Unmarshal(inputData, &req)
if err != nil {
return xerrors.Errorf("unmarshalling role sync settings: %w", err)
}
setting, err = client.PatchRoleIDPSyncSettings(ctx, org.ID.String(), req)
default:
_, _ = fmt.Fprintln(inv.Stderr, "Valid organization settings are: 'groupsync', 'rolesync'")
return fmt.Errorf("unknown organization setting %s", inv.Args[0])
}

if err != nil {
return fmt.Errorf("failed to get organization setting %s: %w", inv.Args[0], err)
}

settingJSON, err := json.Marshal(setting)
if err != nil {
return fmt.Errorf("failed to marshal organization setting %s: %w", inv.Args[0], err)
}

var dst bytes.Buffer
err = json.Indent(&dst, settingJSON, "", "\t")
if err != nil {
return fmt.Errorf("failed to indent organization setting as json %s: %w", inv.Args[0], err)
}

_, err = fmt.Fprintln(inv.Stdout, dst.String())
return err
},
}
return cmd
}

func (r *RootCmd) printOrganizationSetting(orgContext *OrganizationContext) *serpent.Command {
client := new(codersdk.Client)
cmd := &serpent.Command{
Use: "show <groupsync | rolesync>",
Short: "Outputs specified organization setting.",
Long: FormatExamples(
Example{
Description: "Output group sync settings.",
Command: "coder organization settings show groupsync",
},
),
Options: []serpent.Option{},
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
r.InitClient(client),
),
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
org, err := orgContext.Selected(inv, client)
if err != nil {
return err
}

var setting any
switch strings.ToLower(inv.Args[0]) {
case "groupsync", "group-sync":
setting, err = client.GroupIDPSyncSettings(ctx, org.ID.String())
case "rolesync", "role-sync":
setting, err = client.RoleIDPSyncSettings(ctx, org.ID.String())
default:
_, _ = fmt.Fprintln(inv.Stderr, "Valid organization settings are: 'groupsync', 'rolesync'")
return fmt.Errorf("unknown organization setting %s", inv.Args[0])
}

if err != nil {
return fmt.Errorf("failed to get organization setting %s: %w", inv.Args[0], err)
}

settingJSON, err := json.Marshal(setting)
if err != nil {
return fmt.Errorf("failed to marshal organization setting %s: %w", inv.Args[0], err)
}

var dst bytes.Buffer
err = json.Indent(&dst, settingJSON, "", "\t")
if err != nil {
return fmt.Errorf("failed to indent organization setting as json %s: %w", inv.Args[0], err)
}

_, err = fmt.Fprintln(inv.Stdout, dst.String())
return err
},
}
return cmd
}
6 changes: 5 additions & 1 deletion cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"golang.org/x/mod/semver"
"golang.org/x/xerrors"

"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/pretty"

"github.com/coder/coder/v2/buildinfo"
Expand Down Expand Up @@ -657,7 +658,10 @@ func (o *OrganizationContext) Selected(inv *serpent.Invocation, client *codersdk
}

// No org selected, and we are more than 1? Return an error.
return codersdk.Organization{}, xerrors.Errorf("Must select an organization with --org=<org_name>.")
validOrgs := db2sdk.List(orgs, func(org codersdk.Organization) string {
return fmt.Sprintf("%q", org.Name)
})
return codersdk.Organization{}, xerrors.Errorf("Must select an organization with --org=<org_name>. Choose from: %s", strings.Join(validOrgs, ", "))
}

func splitNamedWorkspace(identifier string) (owner string, workspaceName string, err error) {
Expand Down
12 changes: 6 additions & 6 deletions cli/testdata/coder_organizations_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ USAGE:
Aliases: organization, org, orgs

SUBCOMMANDS:
create Create a new organization.
members Manage organization members
roles Manage organization roles.
show Show the organization. Using "selected" will show the selected
organization from the "--org" flag. Using "me" will show all
organizations you are a member of.
create Create a new organization.
members Manage organization members
roles Manage organization roles.
show Show the organization. Using "selected" will show the selected
organization from the "--org" flag. Using "me" will show all
organizations you are a member of.

OPTIONS:
-O, --org string, $CODER_ORGANIZATION
Expand Down
130 changes: 130 additions & 0 deletions enterprise/cli/organizationsettings_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package cli_test

import (
"bytes"
"encoding/json"
"regexp"
"testing"

"github.com/google/uuid"
"github.com/stretchr/testify/require"

"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
"github.com/coder/coder/v2/enterprise/coderd/license"
"github.com/coder/coder/v2/testutil"
)

func TestUpdateGroupSync(t *testing.T) {
t.Parallel()

t.Run("OK", func(t *testing.T) {
t.Parallel()

dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}

owner, first := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})

ctx := testutil.Context(t, testutil.WaitLong)
inv, root := clitest.New(t, "organization", "settings", "set", "groupsync")
//nolint:gocritic // Using the owner, testing the cli not perms
clitest.SetupConfig(t, owner, root)

expectedSettings := codersdk.GroupSyncSettings{
Field: "groups",
Mapping: map[string][]uuid.UUID{
"test": {first.OrganizationID},
},
RegexFilter: regexp.MustCompile("^foo"),
AutoCreateMissing: true,
LegacyNameMapping: nil,
}
expectedData, err := json.Marshal(expectedSettings)
require.NoError(t, err)

buf := new(bytes.Buffer)
inv.Stdout = buf
inv.Stdin = bytes.NewBuffer(expectedData)
err = inv.WithContext(ctx).Run()
require.NoError(t, err)
require.JSONEq(t, string(expectedData), buf.String())

// Now read it back
inv, root = clitest.New(t, "organization", "settings", "show", "groupsync")
//nolint:gocritic // Using the owner, testing the cli not perms
clitest.SetupConfig(t, owner, root)

buf = new(bytes.Buffer)
inv.Stdout = buf
err = inv.WithContext(ctx).Run()
require.NoError(t, err)
require.JSONEq(t, string(expectedData), buf.String())
})
}

func TestUpdateRoleSync(t *testing.T) {
t.Parallel()

t.Run("OK", func(t *testing.T) {
t.Parallel()

dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}

owner, _ := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})

ctx := testutil.Context(t, testutil.WaitLong)
inv, root := clitest.New(t, "organization", "settings", "set", "rolesync")
//nolint:gocritic // Using the owner, testing the cli not perms
clitest.SetupConfig(t, owner, root)

expectedSettings := codersdk.RoleSyncSettings{
Field: "roles",
Mapping: map[string][]string{
"test": {rbac.RoleOrgAdmin()},
},
}
expectedData, err := json.Marshal(expectedSettings)
require.NoError(t, err)

buf := new(bytes.Buffer)
inv.Stdout = buf
inv.Stdin = bytes.NewBuffer(expectedData)
err = inv.WithContext(ctx).Run()
require.NoError(t, err)
require.JSONEq(t, string(expectedData), buf.String())

// Now read it back
inv, root = clitest.New(t, "organization", "settings", "show", "rolesync")
//nolint:gocritic // Using the owner, testing the cli not perms
clitest.SetupConfig(t, owner, root)

buf = new(bytes.Buffer)
inv.Stdout = buf
err = inv.WithContext(ctx).Run()
require.NoError(t, err)
require.JSONEq(t, string(expectedData), buf.String())
})
}
15 changes: 15 additions & 0 deletions enterprise/coderd/idpsync.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/coder/coder/v2/coderd/idpsync"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/codersdk"
)

// @Summary Get group IdP Sync settings by organization
Expand Down Expand Up @@ -61,6 +62,20 @@ func (api *API) patchGroupIDPSyncSettings(rw http.ResponseWriter, r *http.Reques
return
}

if len(req.LegacyNameMapping) > 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Unexpected field 'legacy_group_name_mapping'. Field not allowed, set to null or remove it.",
Detail: "legacy_group_name_mapping is deprecated, use mapping instead",
Validations: []codersdk.ValidationError{
{
Field: "legacy_group_name_mapping",
Detail: "field is not allowed",
},
},
})
return
}

//nolint:gocritic // Requires system context to update runtime config
sysCtx := dbauthz.AsSystemRestricted(ctx)
err := api.IDPSync.UpdateGroupSettings(sysCtx, org.ID, api.Database, req)
Expand Down
Loading