mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: add organization member api + cli (#13577)
This commit is contained in:
+1
-1
@@ -29,8 +29,8 @@ func (r *RootCmd) organizations() *serpent.Command {
|
||||
r.currentOrganization(),
|
||||
r.switchOrganization(),
|
||||
r.createOrganization(),
|
||||
r.organizationRoles(),
|
||||
r.organizationMembers(),
|
||||
r.organizationRoles(),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ func (r *RootCmd) organizationMembers() *serpent.Command {
|
||||
Children: []*serpent.Command{
|
||||
r.listOrganizationMembers(),
|
||||
r.assignOrganizationRoles(),
|
||||
r.addOrganizationMember(),
|
||||
},
|
||||
Handler: func(inv *serpent.Invocation) error {
|
||||
return inv.Command.HelpHandler(inv)
|
||||
@@ -28,6 +29,37 @@ func (r *RootCmd) organizationMembers() *serpent.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (r *RootCmd) addOrganizationMember() *serpent.Command {
|
||||
client := new(codersdk.Client)
|
||||
|
||||
cmd := &serpent.Command{
|
||||
Use: "add <username | user_id>",
|
||||
Short: "Add a new member to the current organization",
|
||||
Middleware: serpent.Chain(
|
||||
r.InitClient(client),
|
||||
serpent.RequireNArgs(1),
|
||||
),
|
||||
Handler: func(inv *serpent.Invocation) error {
|
||||
ctx := inv.Context()
|
||||
organization, err := CurrentOrganization(r, inv, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user := inv.Args[0]
|
||||
|
||||
_, err = client.PostOrganizationMember(ctx, organization.ID, user)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("could not add member to organization: %w", err)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(inv.Stdout, "Organization member added")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (r *RootCmd) assignOrganizationRoles() *serpent.Command {
|
||||
client := new(codersdk.Client)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"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/testutil"
|
||||
)
|
||||
|
||||
@@ -34,3 +35,40 @@ func TestListOrganizationMembers(t *testing.T) {
|
||||
require.Contains(t, buf.String(), owner.UserID.String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestAddOrganizationMembers(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)
|
||||
_, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
//nolint:gocritic // must be an owner, only owners can create orgs
|
||||
otherOrg, err := ownerClient.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
|
||||
Name: "Other",
|
||||
DisplayName: "",
|
||||
Description: "",
|
||||
Icon: "",
|
||||
})
|
||||
require.NoError(t, err, "create another organization")
|
||||
|
||||
inv, root := clitest.New(t, "organization", "members", "add", "--organization", otherOrg.ID.String(), user.Username)
|
||||
//nolint:gocritic // must be an owner
|
||||
clitest.SetupConfig(t, ownerClient, root)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
inv.Stdout = buf
|
||||
err = inv.WithContext(ctx).Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
//nolint:gocritic // must be an owner
|
||||
members, err := ownerClient.OrganizationMembers(ctx, otherOrg.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, members, 2)
|
||||
})
|
||||
}
|
||||
|
||||
Generated
+41
@@ -2356,6 +2356,47 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/organizations/{organization}/members/{user}": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Members"
|
||||
],
|
||||
"summary": "Add organization member",
|
||||
"operationId": "add-organization-member",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Organization ID",
|
||||
"name": "organization",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID, name, or me",
|
||||
"name": "user",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.OrganizationMember"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/organizations/{organization}/members/{user}/roles": {
|
||||
"put": {
|
||||
"security": [
|
||||
|
||||
Generated
+37
@@ -2062,6 +2062,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/organizations/{organization}/members/{user}": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Members"],
|
||||
"summary": "Add organization member",
|
||||
"operationId": "add-organization-member",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Organization ID",
|
||||
"name": "organization",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID, name, or me",
|
||||
"name": "user",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.OrganizationMember"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/organizations/{organization}/members/{user}/roles": {
|
||||
"put": {
|
||||
"security": [
|
||||
|
||||
+18
-5
@@ -845,11 +845,24 @@ func New(options *Options) *API {
|
||||
})
|
||||
|
||||
r.Route("/{user}", func(r chi.Router) {
|
||||
r.Use(
|
||||
httpmw.ExtractOrganizationMemberParam(options.Database),
|
||||
)
|
||||
r.Put("/roles", api.putMemberRoles)
|
||||
r.Post("/workspaces", api.postWorkspacesByOrganization)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(
|
||||
// Adding a member requires "read" permission
|
||||
// on the site user. So limited to owners and user-admins.
|
||||
// TODO: Allow org-admins to add users via some new permission? Or give them
|
||||
// read on site users.
|
||||
httpmw.ExtractUserParam(options.Database),
|
||||
)
|
||||
r.Post("/", api.postOrganizationMember)
|
||||
})
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(
|
||||
httpmw.ExtractOrganizationMemberParam(options.Database),
|
||||
)
|
||||
r.Put("/roles", api.putMemberRoles)
|
||||
r.Post("/workspaces", api.postWorkspacesByOrganization)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,12 +9,59 @@ import (
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"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"
|
||||
)
|
||||
|
||||
// @Summary Add organization member
|
||||
// @ID add-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
|
||||
// @Router /organizations/{organization}/members/{user} [post]
|
||||
func (api *API) postOrganizationMember(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
organization = httpmw.OrganizationParam(r)
|
||||
user = httpmw.UserParam(r)
|
||||
)
|
||||
|
||||
member, err := api.Database.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{
|
||||
OrganizationID: organization.ID,
|
||||
UserID: user.ID,
|
||||
CreatedAt: dbtime.Now(),
|
||||
UpdatedAt: dbtime.Now(),
|
||||
Roles: []string{},
|
||||
})
|
||||
if httpapi.Is404Error(err) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := convertOrganizationMembers(ctx, api.Database, []database.OrganizationMember{member})
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(resp) == 0 {
|
||||
httpapi.InternalServerError(rw, xerrors.Errorf("marshal member"))
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, resp[0])
|
||||
}
|
||||
|
||||
// @Summary List organization members
|
||||
// @ID list-organization-members
|
||||
// @Security CoderSessionToken
|
||||
|
||||
@@ -13,6 +13,65 @@ import (
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestAddMember(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
owner := coderdtest.New(t, nil)
|
||||
first := coderdtest.CreateFirstUser(t, owner)
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
org, err := owner.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
|
||||
Name: "other",
|
||||
DisplayName: "",
|
||||
Description: "",
|
||||
Icon: "",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Make a user not in the second organization
|
||||
_, user := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID)
|
||||
|
||||
members, err := owner.OrganizationMembers(ctx, org.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, members, 1) // Verify just the 1 member
|
||||
|
||||
// Add user to org
|
||||
_, err = owner.PostOrganizationMember(ctx, org.ID, user.Username)
|
||||
require.NoError(t, err)
|
||||
|
||||
members, err = owner.OrganizationMembers(ctx, org.ID)
|
||||
require.NoError(t, err)
|
||||
// Owner + new member
|
||||
require.Len(t, members, 2)
|
||||
require.ElementsMatch(t,
|
||||
[]uuid.UUID{first.UserID, user.ID},
|
||||
db2sdk.List(members, onlyIDs))
|
||||
})
|
||||
|
||||
t.Run("UserNotExists", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
owner := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, owner)
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
org, err := owner.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
|
||||
Name: "other",
|
||||
DisplayName: "",
|
||||
Description: "",
|
||||
Icon: "",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add user to org
|
||||
_, err = owner.PostOrganizationMember(ctx, org.ID, uuid.NewString())
|
||||
require.Error(t, err)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Contains(t, apiErr.Message, "must be an existing")
|
||||
})
|
||||
}
|
||||
|
||||
func TestListMembers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -379,6 +379,20 @@ func (c *Client) UpdateUserPassword(ctx context.Context, user string, req Update
|
||||
return nil
|
||||
}
|
||||
|
||||
// PostOrganizationMember adds a user to an organization
|
||||
func (c *Client) PostOrganizationMember(ctx context.Context, organizationID uuid.UUID, user string) (OrganizationMember, error) {
|
||||
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/organizations/%s/members/%s", organizationID, user), nil)
|
||||
if err != nil {
|
||||
return OrganizationMember{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return OrganizationMember{}, ReadBodyAsError(res)
|
||||
}
|
||||
var member OrganizationMember
|
||||
return member, json.NewDecoder(res.Body).Decode(&member)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
Generated
+48
@@ -315,6 +315,54 @@ Status Code **200**
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Add organization member
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/members/{user} \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`POST /organizations/{organization}/members/{user}`
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
| -------------- | ---- | ------ | -------- | -------------------- |
|
||||
| `organization` | path | string | true | Organization ID |
|
||||
| `user` | path | string | true | User ID, name, or me |
|
||||
|
||||
### 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"
|
||||
}
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------------------------- |
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.OrganizationMember](schemas.md#codersdkorganizationmember) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Assign role to organization member
|
||||
|
||||
### Code samples
|
||||
|
||||
Reference in New Issue
Block a user