- Notifications
You must be signed in to change notification settings - Fork927
chore: implement cli list organization members#13555
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package cli | ||
import ( | ||
"fmt" | ||
"golang.org/x/xerrors" | ||
"github.com/coder/coder/v2/cli/cliui" | ||
"github.com/coder/coder/v2/codersdk" | ||
"github.com/coder/serpent" | ||
) | ||
func (r *RootCmd) organizationMembers() *serpent.Command { | ||
formatter := cliui.NewOutputFormatter( | ||
cliui.TableFormat([]codersdk.OrganizationMemberWithName{}, []string{"username", "organization_roles"}), | ||
cliui.JSONFormat(), | ||
) | ||
client := new(codersdk.Client) | ||
cmd := &serpent.Command{ | ||
Use: "members", | ||
Short: "List all organization members", | ||
Aliases: []string{"member"}, | ||
Middleware: serpent.Chain( | ||
serpent.RequireNArgs(0), | ||
r.InitClient(client), | ||
), | ||
Handler: func(inv *serpent.Invocation) error { | ||
ctx := inv.Context() | ||
organization, err := CurrentOrganization(r, inv, client) | ||
if err != nil { | ||
return err | ||
} | ||
res, err := client.OrganizationMembers(ctx, organization.ID) | ||
if err != nil { | ||
return xerrors.Errorf("fetch members: %w", err) | ||
} | ||
out, err := formatter.Format(inv.Context(), res) | ||
if err != nil { | ||
return err | ||
} | ||
_, err = fmt.Fprintln(inv.Stdout, out) | ||
return err | ||
}, | ||
} | ||
formatter.AttachOptions(&cmd.Options) | ||
return cmd | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package cli_test | ||
import ( | ||
"bytes" | ||
"testing" | ||
"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/testutil" | ||
) | ||
func TestListOrganizationMembers(t *testing.T) { | ||
t.Parallel() | ||
t.Run("OK", func(t *testing.T) { | ||
t.Parallel() | ||
ownerClient := coderdtest.New(t, &coderdtest.Options{}) | ||
owner := coderdtest.CreateFirstUser(t, ownerClient) | ||
client, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleUserAdmin()) | ||
ctx := testutil.Context(t, testutil.WaitMedium) | ||
inv, root := clitest.New(t, "organization", "members", "-c", "user_id,username,roles") | ||
clitest.SetupConfig(t, client, root) | ||
buf := new(bytes.Buffer) | ||
inv.Stdout = buf | ||
err := inv.WithContext(ctx).Run() | ||
require.NoError(t, err) | ||
require.Contains(t, buf.String(), user.Username) | ||
require.Contains(t, buf.String(), owner.UserID.String()) | ||
}) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,17 @@ | ||
package coderd | ||
import ( | ||
"context" | ||
"net/http" | ||
"github.com/google/uuid" | ||
"golang.org/x/xerrors" | ||
"github.com/coder/coder/v2/coderd/database" | ||
"github.com/coder/coder/v2/coderd/database/db2sdk" | ||
"github.com/coder/coder/v2/coderd/httpapi" | ||
"github.com/coder/coder/v2/coderd/httpmw" | ||
"github.com/coder/coder/v2/coderd/rbac" | ||
"github.com/coder/coder/v2/codersdk" | ||
) | ||
@@ -41,7 +42,13 @@ func (api *API) listMembers(rw http.ResponseWriter, r *http.Request) { | ||
return | ||
} | ||
resp, err := convertOrganizationMemberRows(ctx, api.Database, members) | ||
if err != nil { | ||
httpapi.InternalServerError(rw, err) | ||
return | ||
} | ||
httpapi.Write(ctx, rw, http.StatusOK, resp) | ||
} | ||
// @Summary Assign role to organization member | ||
@@ -87,30 +94,101 @@ func (api *API) putMemberRoles(rw http.ResponseWriter, r *http.Request) { | ||
return | ||
} | ||
resp, err := convertOrganizationMembers(ctx, api.Database, []database.OrganizationMember{updatedUser}) | ||
if err != nil { | ||
httpapi.InternalServerError(rw, err) | ||
return | ||
} | ||
if len(resp) != 1 { | ||
httpapi.InternalServerError(rw, xerrors.Errorf("failed to serialize member to response, update still succeeded")) | ||
return | ||
} | ||
httpapi.Write(ctx, rw, http.StatusOK, resp[0]) | ||
} | ||
// convertOrganizationMembers batches the role lookup to make only 1 sql call | ||
// We | ||
func convertOrganizationMembers(ctx context.Context, db database.Store, mems []database.OrganizationMember) ([]codersdk.OrganizationMember, error) { | ||
converted := make([]codersdk.OrganizationMember, 0, len(mems)) | ||
roleLookup := make([]database.NameOrganizationPair, 0) | ||
for _, m := range mems { | ||
converted = append(converted, codersdk.OrganizationMember{ | ||
UserID: m.UserID, | ||
OrganizationID: m.OrganizationID, | ||
CreatedAt: m.CreatedAt, | ||
UpdatedAt: m.UpdatedAt, | ||
Roles: db2sdk.List(m.Roles, func(r string) codersdk.SlimRole { | ||
// If it is a built-in role, no lookups are needed. | ||
rbacRole, err := rbac.RoleByName(rbac.RoleIdentifier{Name: r, OrganizationID: m.OrganizationID}) | ||
if err == nil { | ||
return db2sdk.SlimRole(rbacRole) | ||
} | ||
// We know the role name and the organization ID. We are missing the | ||
// display name. Append the lookup parameter, so we can get the display name | ||
roleLookup = append(roleLookup, database.NameOrganizationPair{ | ||
Name: r, | ||
OrganizationID: m.OrganizationID, | ||
}) | ||
return codersdk.SlimRole{ | ||
Name: r, | ||
DisplayName: "", | ||
OrganizationID: m.OrganizationID.String(), | ||
} | ||
}), | ||
}) | ||
} | ||
customRoles, err := db.CustomRoles(ctx, database.CustomRolesParams{ | ||
LookupRoles: roleLookup, | ||
ExcludeOrgRoles: false, | ||
OrganizationID: uuid.UUID{}, | ||
}) | ||
if err != nil { | ||
// We are missing the display names, but that is not absolutely required. So just | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Are we expecting an error here? I see wecan do without it, but I wonder if we should instead of just presenting the error? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. I'll return the error 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Just unfortunate we can block api calls on this kind of error. Would be nice if we could return some sort of "warning" instead. I'll return an error though as that is what we usually do for failed db calls. | ||
// return the converted and the names will be used instead of the display names. | ||
return converted, xerrors.Errorf("lookup custom roles: %w", err) | ||
} | ||
// Now map the customRoles back to the slimRoles for their display name. | ||
customRolesMap := make(map[string]database.CustomRole) | ||
for _, role := range customRoles { | ||
customRolesMap[role.RoleIdentifier().UniqueName()] = role | ||
} | ||
for i := range converted { | ||
for j, role := range converted[i].Roles { | ||
if cr, ok := customRolesMap[role.UniqueName()]; ok { | ||
converted[i].Roles[j].DisplayName = cr.DisplayName | ||
} | ||
} | ||
} | ||
return converted, nil | ||
} | ||
func convertOrganizationMemberRows(ctx context.Context, db database.Store, rows []database.OrganizationMembersRow) ([]codersdk.OrganizationMemberWithName, error) { | ||
members := make([]database.OrganizationMember, 0) | ||
for _, row := range rows { | ||
members = append(members, row.OrganizationMember) | ||
} | ||
convertedMembers, err := convertOrganizationMembers(ctx, db, members) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if len(convertedMembers) != len(rows) { | ||
return nil, xerrors.Errorf("conversion failed, mismatch slice lengths") | ||
} | ||
converted := make([]codersdk.OrganizationMemberWithName, 0) | ||
for i := range convertedMembers { | ||
converted = append(converted, codersdk.OrganizationMemberWithName{ | ||
Username: rows[i].Username, | ||
OrganizationMember: convertedMembers[i], | ||
}) | ||
} | ||
returnconverted, nil | ||
} |
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.