diff --git a/cli/organizationmembers.go b/cli/organizationmembers.go index 99b0700c4688d..bbd4d8519e1d1 100644 --- a/cli/organizationmembers.go +++ b/cli/organizationmembers.go @@ -137,7 +137,7 @@ func (r *RootCmd) assignOrganizationRoles(orgContext *OrganizationContext) *serp func (r *RootCmd) listOrganizationMembers(orgContext *OrganizationContext) *serpent.Command { formatter := cliui.NewOutputFormatter( - cliui.TableFormat([]codersdk.OrganizationMemberWithName{}, []string{"username", "organization_roles"}), + cliui.TableFormat([]codersdk.OrganizationMemberWithUserData{}, []string{"username", "organization_roles"}), cliui.JSONFormat(), ) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 0aecfdea8ff5e..4e3b374186554 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -2340,7 +2340,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/codersdk.OrganizationMemberWithName" + "$ref": "#/definitions/codersdk.OrganizationMemberWithUserData" } } } @@ -2467,9 +2467,6 @@ const docTemplate = `{ "CoderSessionToken": [] } ], - "produces": [ - "application/json" - ], "tags": [ "Members" ], @@ -2492,11 +2489,8 @@ const docTemplate = `{ } ], "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.OrganizationMember" - } + "204": { + "description": "No Content" } } } @@ -10388,13 +10382,25 @@ const docTemplate = `{ } } }, - "codersdk.OrganizationMemberWithName": { + "codersdk.OrganizationMemberWithUserData": { "type": "object", "properties": { + "avatar_url": { + "type": "string" + }, "created_at": { "type": "string", "format": "date-time" }, + "global_roles": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.SlimRole" + } + }, + "name": { + "type": "string" + }, "organization_id": { "type": "string", "format": "uuid" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 6d5ac99848dbf..e4500397821ba 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -2044,7 +2044,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/codersdk.OrganizationMemberWithName" + "$ref": "#/definitions/codersdk.OrganizationMemberWithUserData" } } } @@ -2159,7 +2159,6 @@ "CoderSessionToken": [] } ], - "produces": ["application/json"], "tags": ["Members"], "summary": "Remove organization member", "operationId": "remove-organization-member", @@ -2180,11 +2179,8 @@ } ], "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.OrganizationMember" - } + "204": { + "description": "No Content" } } } @@ -9344,13 +9340,25 @@ } } }, - "codersdk.OrganizationMemberWithName": { + "codersdk.OrganizationMemberWithUserData": { "type": "object", "properties": { + "avatar_url": { + "type": "string" + }, "created_at": { "type": "string", "format": "date-time" }, + "global_roles": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.SlimRole" + } + }, + "name": { + "type": "string" + }, "organization_id": { "type": "string", "format": "uuid" diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index 7dc35bca22bda..3c39423e2d917 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -166,25 +166,7 @@ func User(user database.User, organizationIDs []uuid.UUID) codersdk.User { convertedUser := codersdk.User{ ReducedUser: ReducedUser(user), OrganizationIDs: organizationIDs, - Roles: make([]codersdk.SlimRole, 0, len(user.RBACRoles)), - } - - for _, roleName := range user.RBACRoles { - // TODO: Currently the api only returns site wide roles. - // Should it return organization roles? - rbacRole, err := rbac.RoleByName(rbac.RoleIdentifier{ - Name: roleName, - OrganizationID: uuid.Nil, - }) - if err == nil { - convertedUser.Roles = append(convertedUser.Roles, SlimRole(rbacRole)) - } else { - // TODO: Fix this for custom roles to display the actual display_name - // Requires plumbing either a cached role value, or the db. - convertedUser.Roles = append(convertedUser.Roles, codersdk.SlimRole{ - Name: roleName, - }) - } + Roles: SlimRolesFromNames(user.RBACRoles), } return convertedUser @@ -537,6 +519,27 @@ func SlimRole(role rbac.Role) codersdk.SlimRole { } } +func SlimRolesFromNames(names []string) []codersdk.SlimRole { + convertedRoles := make([]codersdk.SlimRole, 0, len(names)) + + for _, name := range names { + convertedRoles = append(convertedRoles, SlimRoleFromName(name)) + } + + return convertedRoles +} + +func SlimRoleFromName(name string) codersdk.SlimRole { + rbacRole, err := rbac.RoleByName(rbac.RoleIdentifier{Name: name}) + var convertedRole codersdk.SlimRole + if err == nil { + convertedRole = SlimRole(rbacRole) + } else { + convertedRole = codersdk.SlimRole{Name: name} + } + return convertedRole +} + func RBACRole(role rbac.Role) codersdk.Role { slim := SlimRole(role) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index ee439996e34dd..acd4c9341c4ef 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -4283,7 +4283,7 @@ func (q *sqlQuerier) InsertOrganizationMember(ctx context.Context, arg InsertOrg const organizationMembers = `-- name: OrganizationMembers :many SELECT organization_members.user_id, organization_members.organization_id, organization_members.created_at, organization_members.updated_at, organization_members.roles, - users.username + users.username, users.avatar_url, users.name, users.rbac_roles as "global_roles" FROM organization_members INNER JOIN @@ -4311,6 +4311,9 @@ type OrganizationMembersParams struct { type OrganizationMembersRow struct { OrganizationMember OrganizationMember `db:"organization_member" json:"organization_member"` Username string `db:"username" json:"username"` + AvatarURL string `db:"avatar_url" json:"avatar_url"` + Name string `db:"name" json:"name"` + GlobalRoles pq.StringArray `db:"global_roles" json:"global_roles"` } // Arguments are optional with uuid.Nil to ignore. @@ -4333,6 +4336,9 @@ func (q *sqlQuerier) OrganizationMembers(ctx context.Context, arg OrganizationMe &i.OrganizationMember.UpdatedAt, pq.Array(&i.OrganizationMember.Roles), &i.Username, + &i.AvatarURL, + &i.Name, + &i.GlobalRoles, ); err != nil { return nil, err } diff --git a/coderd/database/queries/organizationmembers.sql b/coderd/database/queries/organizationmembers.sql index 4722973d38589..8cf6a804e2682 100644 --- a/coderd/database/queries/organizationmembers.sql +++ b/coderd/database/queries/organizationmembers.sql @@ -5,7 +5,7 @@ -- - Use both to get a specific org member row SELECT sqlc.embed(organization_members), - users.username + users.username, users.avatar_url, users.name, users.rbac_roles as "global_roles" FROM organization_members INNER JOIN diff --git a/coderd/members.go b/coderd/members.go index f505645ec4d50..e27f5f8840733 100644 --- a/coderd/members.go +++ b/coderd/members.go @@ -83,15 +83,15 @@ func (api *API) postOrganizationMember(rw http.ResponseWriter, r *http.Request) // @Summary Remove organization member // @ID remove-organization-member // @Security CoderSessionToken -// @Produce json // @Tags Members // @Param organization path string true "Organization ID" // @Param user path string true "User ID, name, or me" -// @Success 200 {object} codersdk.OrganizationMember +// @Success 204 // @Router /organizations/{organization}/members/{user} [delete] func (api *API) deleteOrganizationMember(rw http.ResponseWriter, r *http.Request) { var ( ctx = r.Context() + apiKey = httpmw.APIKey(r) organization = httpmw.OrganizationParam(r) member = httpmw.OrganizationMemberParam(r) auditor = api.Auditor.Load() @@ -106,6 +106,11 @@ func (api *API) deleteOrganizationMember(rw http.ResponseWriter, r *http.Request aReq.Old = member.OrganizationMember.Auditable(member.Username) defer commitAudit() + if member.UserID == apiKey.UserID { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{Message: "cannot remove self from an organization"}) + return + } + err := api.Database.DeleteOrganizationMember(ctx, database.DeleteOrganizationMemberParams{ OrganizationID: organization.ID, UserID: member.UserID, @@ -120,7 +125,7 @@ func (api *API) deleteOrganizationMember(rw http.ResponseWriter, r *http.Request } aReq.New = database.AuditableOrganizationMember{} - httpapi.Write(ctx, rw, http.StatusOK, "organization member removed") + rw.WriteHeader(http.StatusNoContent) } // @Summary List organization members @@ -129,7 +134,7 @@ func (api *API) deleteOrganizationMember(rw http.ResponseWriter, r *http.Request // @Produce json // @Tags Members // @Param organization path string true "Organization ID" -// @Success 200 {object} []codersdk.OrganizationMemberWithName +// @Success 200 {object} []codersdk.OrganizationMemberWithUserData // @Router /organizations/{organization}/members [get] func (api *API) listMembers(rw http.ResponseWriter, r *http.Request) { var ( @@ -150,7 +155,7 @@ func (api *API) listMembers(rw http.ResponseWriter, r *http.Request) { return } - resp, err := convertOrganizationMemberRows(ctx, api.Database, members) + resp, err := convertOrganizationMembersWithUserData(ctx, api.Database, members) if err != nil { httpapi.InternalServerError(rw, err) return @@ -294,7 +299,7 @@ func convertOrganizationMembers(ctx context.Context, db database.Store, mems []d return converted, nil } -func convertOrganizationMemberRows(ctx context.Context, db database.Store, rows []database.OrganizationMembersRow) ([]codersdk.OrganizationMemberWithName, error) { +func convertOrganizationMembersWithUserData(ctx context.Context, db database.Store, rows []database.OrganizationMembersRow) ([]codersdk.OrganizationMemberWithUserData, error) { members := make([]database.OrganizationMember, 0) for _, row := range rows { members = append(members, row.OrganizationMember) @@ -308,10 +313,13 @@ func convertOrganizationMemberRows(ctx context.Context, db database.Store, rows return nil, xerrors.Errorf("conversion failed, mismatch slice lengths") } - converted := make([]codersdk.OrganizationMemberWithName, 0) + converted := make([]codersdk.OrganizationMemberWithUserData, 0) for i := range convertedMembers { - converted = append(converted, codersdk.OrganizationMemberWithName{ + converted = append(converted, codersdk.OrganizationMemberWithUserData{ Username: rows[i].Username, + AvatarURL: rows[i].AvatarURL, + Name: rows[i].Name, + GlobalRoles: db2sdk.SlimRolesFromNames(rows[i].GlobalRoles), OrganizationMember: convertedMembers[i], }) } diff --git a/coderd/members_test.go b/coderd/members_test.go index 3db296ef6009a..3066e15a8f783 100644 --- a/coderd/members_test.go +++ b/coderd/members_test.go @@ -185,6 +185,6 @@ func TestRemoveMember(t *testing.T) { }) } -func onlyIDs(u codersdk.OrganizationMemberWithName) uuid.UUID { +func onlyIDs(u codersdk.OrganizationMemberWithUserData) uuid.UUID { return u.UserID } diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 4cb59482d15e7..0841bdba8554f 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -66,8 +66,11 @@ type OrganizationMember struct { Roles []SlimRole `table:"organization_roles" json:"roles"` } -type OrganizationMemberWithName struct { - Username string `table:"username,default_sort" json:"username"` +type OrganizationMemberWithUserData struct { + Username string `table:"username,default_sort" json:"username"` + Name string `table:"name" json:"name"` + AvatarURL string `json:"avatar_url"` + GlobalRoles []SlimRole `json:"global_roles"` OrganizationMember `table:"m,recursive_inline"` } diff --git a/codersdk/users.go b/codersdk/users.go index e56c9cc90d1c7..16b6a5df6489d 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -402,14 +402,14 @@ func (c *Client) DeleteOrganizationMember(ctx context.Context, organizationID uu return err } defer res.Body.Close() - if res.StatusCode != http.StatusOK { + if res.StatusCode != http.StatusNoContent { return ReadBodyAsError(res) } return nil } // OrganizationMembers lists all members in an organization -func (c *Client) OrganizationMembers(ctx context.Context, organizationID uuid.UUID) ([]OrganizationMemberWithName, error) { +func (c *Client) OrganizationMembers(ctx context.Context, organizationID uuid.UUID) ([]OrganizationMemberWithUserData, error) { res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/members/", organizationID), nil) if err != nil { return nil, err @@ -418,7 +418,7 @@ func (c *Client) OrganizationMembers(ctx context.Context, organizationID uuid.UU if res.StatusCode != http.StatusOK { return nil, ReadBodyAsError(res) } - var members []OrganizationMemberWithName + var members []OrganizationMemberWithUserData return members, json.NewDecoder(res.Body).Decode(&members) } diff --git a/docs/api/members.md b/docs/api/members.md index 1a9beae285157..7cb23306a4742 100644 --- a/docs/api/members.md +++ b/docs/api/members.md @@ -26,7 +26,16 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members ```json [ { + "avatar_url": "string", "created_at": "2019-08-24T14:15:22Z", + "global_roles": [ + { + "display_name": "string", + "name": "string", + "organization_id": "string" + } + ], + "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "roles": [ { @@ -44,9 +53,9 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members ### Responses -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------- | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.OrganizationMemberWithName](schemas.md#codersdkorganizationmemberwithname) | +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.OrganizationMemberWithUserData](schemas.md#codersdkorganizationmemberwithuserdata) |