mirror of
https://github.com/coder/coder.git
synced 2026-09-21 12:44:32 +08:00
chore: implement api layer for listing organization members (#13546)
This commit is contained in:
Generated
+67
@@ -2245,6 +2245,43 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/organizations/{organization}/members": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Members"
|
||||
],
|
||||
"summary": "List organization members",
|
||||
"operationId": "list-organization-members",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Organization ID",
|
||||
"name": "organization",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.OrganizationMemberWithName"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/organizations/{organization}/members/roles": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -10056,6 +10093,36 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.OrganizationMemberWithName": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"organization_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.SlimRole"
|
||||
}
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"user_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.PatchGroupRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Generated
+63
@@ -1963,6 +1963,39 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/organizations/{organization}/members": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Members"],
|
||||
"summary": "List organization members",
|
||||
"operationId": "list-organization-members",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Organization ID",
|
||||
"name": "organization",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.OrganizationMemberWithName"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/organizations/{organization}/members/roles": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -9045,6 +9078,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.OrganizationMemberWithName": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"organization_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.SlimRole"
|
||||
}
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"user_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.PatchGroupRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -837,6 +837,7 @@ func New(options *Options) *API {
|
||||
})
|
||||
})
|
||||
r.Route("/members", func(r chi.Router) {
|
||||
r.Get("/", api.listMembers)
|
||||
r.Route("/roles", func(r chi.Router) {
|
||||
r.Get("/", api.assignableOrgRoles)
|
||||
r.With(httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentCustomRoles)).
|
||||
|
||||
@@ -3,6 +3,8 @@ package coderd
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
|
||||
@@ -12,6 +14,36 @@ import (
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
// @Summary List organization members
|
||||
// @ID list-organization-members
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Members
|
||||
// @Param organization path string true "Organization ID"
|
||||
// @Success 200 {object} []codersdk.OrganizationMemberWithName
|
||||
// @Router /organizations/{organization}/members [get]
|
||||
func (api *API) listMembers(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
organization = httpmw.OrganizationParam(r)
|
||||
)
|
||||
|
||||
members, err := api.Database.OrganizationMembers(ctx, database.OrganizationMembersParams{
|
||||
OrganizationID: organization.ID,
|
||||
UserID: uuid.Nil,
|
||||
})
|
||||
if httpapi.Is404Error(err) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.List(members, convertOrganizationMemberRow))
|
||||
}
|
||||
|
||||
// @Summary Assign role to organization member
|
||||
// @ID assign-role-to-organization-member
|
||||
// @Security CoderSessionToken
|
||||
@@ -73,3 +105,12 @@ func convertOrganizationMember(mem database.OrganizationMember) codersdk.Organiz
|
||||
}
|
||||
return convertedMember
|
||||
}
|
||||
|
||||
func convertOrganizationMemberRow(row database.OrganizationMembersRow) codersdk.OrganizationMemberWithName {
|
||||
convertedMember := codersdk.OrganizationMemberWithName{
|
||||
Username: row.Username,
|
||||
OrganizationMember: convertOrganizationMember(row.OrganizationMember),
|
||||
}
|
||||
|
||||
return convertedMember
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package coderd_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestListMembers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
owner := coderdtest.New(t, nil)
|
||||
first := coderdtest.CreateFirstUser(t, owner)
|
||||
|
||||
client, user := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.ScopedRoleOrgAdmin(first.OrganizationID))
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
members, err := client.OrganizationMembers(ctx, first.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, members, 2)
|
||||
require.ElementsMatch(t,
|
||||
[]uuid.UUID{first.UserID, user.ID},
|
||||
db2sdk.List(members, onlyIDs))
|
||||
})
|
||||
|
||||
// Calling it from a user without the org access.
|
||||
t.Run("NotInOrg", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
owner := coderdtest.New(t, nil)
|
||||
first := coderdtest.CreateFirstUser(t, owner)
|
||||
|
||||
client, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.ScopedRoleOrgAdmin(first.OrganizationID))
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
org, err := owner.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
|
||||
Name: "test",
|
||||
DisplayName: "",
|
||||
Description: "",
|
||||
})
|
||||
require.NoError(t, err, "create organization")
|
||||
|
||||
// 404 error is expected instead of a 403/401 to not leak existence of
|
||||
// an organization.
|
||||
_, err = client.OrganizationMembers(ctx, org.ID)
|
||||
require.ErrorContains(t, err, "404")
|
||||
})
|
||||
}
|
||||
|
||||
func onlyIDs(u codersdk.OrganizationMemberWithName) uuid.UUID {
|
||||
return u.UserID
|
||||
}
|
||||
@@ -58,6 +58,11 @@ type OrganizationMember struct {
|
||||
Roles []SlimRole `db:"roles" json:"roles"`
|
||||
}
|
||||
|
||||
type OrganizationMemberWithName struct {
|
||||
Username string `table:"username,default_sort" json:"username"`
|
||||
OrganizationMember `table:"m,recursive_inline"`
|
||||
}
|
||||
|
||||
type CreateOrganizationRequest struct {
|
||||
Name string `json:"name" validate:"required,organization_name"`
|
||||
// DisplayName will default to the same value as `Name` if not provided.
|
||||
|
||||
@@ -379,6 +379,20 @@ func (c *Client) UpdateUserPassword(ctx context.Context, user string, req Update
|
||||
return nil
|
||||
}
|
||||
|
||||
// OrganizationMembers lists all members in an organization
|
||||
func (c *Client) OrganizationMembers(ctx context.Context, organizationID uuid.UUID) ([]OrganizationMemberWithName, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/members/", organizationID), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, ReadBodyAsError(res)
|
||||
}
|
||||
var members []OrganizationMemberWithName
|
||||
return members, json.NewDecoder(res.Body).Decode(&members)
|
||||
}
|
||||
|
||||
// UpdateUserRoles grants the userID the specified roles.
|
||||
// Include ALL roles the user has.
|
||||
func (c *Client) UpdateUserRoles(ctx context.Context, user string, req UpdateRoles) (User, error) {
|
||||
|
||||
Generated
+67
@@ -1,5 +1,72 @@
|
||||
# Members
|
||||
|
||||
## List organization members
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`GET /organizations/{organization}/members`
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
| -------------- | ---- | ------ | -------- | --------------- |
|
||||
| `organization` | path | string | true | Organization ID |
|
||||
|
||||
### Example responses
|
||||
|
||||
> 200 Response
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"created_at": "2019-08-24T14:15:22Z",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"roles": [
|
||||
{
|
||||
"display_name": "string",
|
||||
"name": "string",
|
||||
"organization_id": "string"
|
||||
}
|
||||
],
|
||||
"updated_at": "2019-08-24T14:15:22Z",
|
||||
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
|
||||
"username": "string"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 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) |
|
||||
|
||||
<h3 id="list-organization-members-responseschema">Response Schema</h3>
|
||||
|
||||
Status Code **200**
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| -------------------- | ----------------- | -------- | ------------ | ----------- |
|
||||
| `[array item]` | array | false | | |
|
||||
| `» created_at` | string(date-time) | false | | |
|
||||
| `» organization_id` | string(uuid) | false | | |
|
||||
| `» roles` | array | false | | |
|
||||
| `»» display_name` | string | false | | |
|
||||
| `»» name` | string | false | | |
|
||||
| `»» organization_id` | string | false | | |
|
||||
| `» updated_at` | string(date-time) | false | | |
|
||||
| `» user_id` | string(uuid) | false | | |
|
||||
| `» username` | string | false | | |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Get member roles by organization
|
||||
|
||||
### Code samples
|
||||
|
||||
Generated
+30
@@ -3265,6 +3265,36 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
| `updated_at` | string | false | | |
|
||||
| `user_id` | string | false | | |
|
||||
|
||||
## codersdk.OrganizationMemberWithName
|
||||
|
||||
```json
|
||||
{
|
||||
"created_at": "2019-08-24T14:15:22Z",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"roles": [
|
||||
{
|
||||
"display_name": "string",
|
||||
"name": "string",
|
||||
"organization_id": "string"
|
||||
}
|
||||
],
|
||||
"updated_at": "2019-08-24T14:15:22Z",
|
||||
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
|
||||
"username": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ----------------- | ----------------------------------------------- | -------- | ------------ | ----------- |
|
||||
| `created_at` | string | false | | |
|
||||
| `organization_id` | string | false | | |
|
||||
| `roles` | array of [codersdk.SlimRole](#codersdkslimrole) | false | | |
|
||||
| `updated_at` | string | false | | |
|
||||
| `user_id` | string | false | | |
|
||||
| `username` | string | false | | |
|
||||
|
||||
## codersdk.PatchGroupRequest
|
||||
|
||||
```json
|
||||
|
||||
Generated
+5
@@ -797,6 +797,11 @@ export interface OrganizationMember {
|
||||
readonly roles: readonly SlimRole[];
|
||||
}
|
||||
|
||||
// From codersdk/organizations.go
|
||||
export interface OrganizationMemberWithName extends OrganizationMember {
|
||||
readonly username: string;
|
||||
}
|
||||
|
||||
// From codersdk/pagination.go
|
||||
export interface Pagination {
|
||||
readonly after_id?: string;
|
||||
|
||||
Reference in New Issue
Block a user