chore!: allow CreateUser to accept multiple organizations (#14383)

* chore: allow CreateUser to accept multiple organizations

In a multi-org deployment, it makes more sense to allow for multiple
org memberships to be assigned at create. The legacy param will still
be honored.

* Handle sdk deprecation better by maintaining cli functions
This commit is contained in:
Steven Masley
2024-08-23 21:23:51 +00:00
committed by GitHub
parent af125c3795
commit c8eacc6df7
28 changed files with 597 additions and 367 deletions
+63 -1
View File
@@ -113,6 +113,11 @@ type CreateFirstUserResponse struct {
OrganizationID uuid.UUID `json:"organization_id" format:"uuid"`
}
// CreateUserRequest
// Deprecated: Use CreateUserRequestWithOrgs instead. This will be removed.
// TODO: When removing, we should rename CreateUserRequestWithOrgs -> CreateUserRequest
// Then alias CreateUserRequestWithOrgs to CreateUserRequest.
// @typescript-ignore CreateUserRequest
type CreateUserRequest struct {
Email string `json:"email" validate:"required,email" format:"email"`
Username string `json:"username" validate:"required,username"`
@@ -127,6 +132,45 @@ type CreateUserRequest struct {
OrganizationID uuid.UUID `json:"organization_id" validate:"" format:"uuid"`
}
type CreateUserRequestWithOrgs struct {
Email string `json:"email" validate:"required,email" format:"email"`
Username string `json:"username" validate:"required,username"`
Name string `json:"name" validate:"user_real_name"`
Password string `json:"password"`
// UserLoginType defaults to LoginTypePassword.
UserLoginType LoginType `json:"login_type"`
// OrganizationIDs is a list of organization IDs that the user should be a member of.
OrganizationIDs []uuid.UUID `json:"organization_ids" validate:"" format:"uuid"`
}
// UnmarshalJSON implements the unmarshal for the legacy param "organization_id".
// To accommodate multiple organizations, the field has been switched to a slice.
// The previous field will just be appended to the slice.
// Note in the previous behavior, omitting the field would result in the
// default org being applied, but that is no longer the case.
// TODO: Remove this method in it's entirety after some period of time.
// This will be released in v1.16.0, and is associated with the multiple orgs
// feature.
func (r *CreateUserRequestWithOrgs) UnmarshalJSON(data []byte) error {
// By using a type alias, we prevent an infinite recursion when unmarshalling.
// This allows us to use the default unmarshal behavior of the original type.
type AliasedReq CreateUserRequestWithOrgs
type DeprecatedCreateUserRequest struct {
AliasedReq
OrganizationID *uuid.UUID `json:"organization_id" format:"uuid"`
}
var dep DeprecatedCreateUserRequest
err := json.Unmarshal(data, &dep)
if err != nil {
return err
}
*r = CreateUserRequestWithOrgs(dep.AliasedReq)
if dep.OrganizationID != nil {
r.OrganizationIDs = append(r.OrganizationIDs, *dep.OrganizationID)
}
return nil
}
type UpdateUserProfileRequest struct {
Username string `json:"username" validate:"required,username"`
Name string `json:"name" validate:"user_real_name"`
@@ -288,8 +332,26 @@ func (c *Client) CreateFirstUser(ctx context.Context, req CreateFirstUserRequest
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
// CreateUser creates a new user.
// CreateUser
// Deprecated: Use CreateUserWithOrgs instead. This will be removed.
// TODO: When removing, we should rename CreateUserWithOrgs -> CreateUser
// with an alias of CreateUserWithOrgs.
func (c *Client) CreateUser(ctx context.Context, req CreateUserRequest) (User, error) {
if req.DisableLogin {
req.UserLoginType = LoginTypeNone
}
return c.CreateUserWithOrgs(ctx, CreateUserRequestWithOrgs{
Email: req.Email,
Username: req.Username,
Name: req.Name,
Password: req.Password,
UserLoginType: req.UserLoginType,
OrganizationIDs: []uuid.UUID{req.OrganizationID},
})
}
// CreateUserWithOrgs creates a new user.
func (c *Client) CreateUserWithOrgs(ctx context.Context, req CreateUserRequestWithOrgs) (User, error) {
res, err := c.Request(ctx, http.MethodPost, "/api/v2/users", req)
if err != nil {
return User{}, err
+149
View File
@@ -0,0 +1,149 @@
package codersdk_test
import (
"encoding/json"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/codersdk"
)
func TestDeprecatedCreateUserRequest(t *testing.T) {
t.Parallel()
t.Run("DefaultOrganization", func(t *testing.T) {
t.Parallel()
input := `
{
"email":"alice@coder.com",
"password":"hunter2",
"username":"alice",
"name":"alice",
"organization_id":"00000000-0000-0000-0000-000000000000",
"disable_login":false,
"login_type":"none"
}
`
var req codersdk.CreateUserRequestWithOrgs
err := json.Unmarshal([]byte(input), &req)
require.NoError(t, err)
require.Equal(t, req.Email, "alice@coder.com")
require.Equal(t, req.Password, "hunter2")
require.Equal(t, req.Username, "alice")
require.Equal(t, req.Name, "alice")
require.Equal(t, req.OrganizationIDs, []uuid.UUID{uuid.Nil})
require.Equal(t, req.UserLoginType, codersdk.LoginTypeNone)
})
t.Run("MultipleOrganizations", func(t *testing.T) {
t.Parallel()
input := `
{
"email":"alice@coder.com",
"password":"hunter2",
"username":"alice",
"name":"alice",
"organization_id":"00000000-0000-0000-0000-000000000000",
"organization_ids":["a618cb03-99fb-4380-adb6-aa801629a4cf","8309b0dc-44ea-435d-a9ff-72cb302835e4"],
"disable_login":false,
"login_type":"none"
}
`
var req codersdk.CreateUserRequestWithOrgs
err := json.Unmarshal([]byte(input), &req)
require.NoError(t, err)
require.Equal(t, req.Email, "alice@coder.com")
require.Equal(t, req.Password, "hunter2")
require.Equal(t, req.Username, "alice")
require.Equal(t, req.Name, "alice")
require.ElementsMatch(t, req.OrganizationIDs,
[]uuid.UUID{
uuid.Nil,
uuid.MustParse("a618cb03-99fb-4380-adb6-aa801629a4cf"),
uuid.MustParse("8309b0dc-44ea-435d-a9ff-72cb302835e4"),
})
require.Equal(t, req.UserLoginType, codersdk.LoginTypeNone)
})
t.Run("OmittedOrganizations", func(t *testing.T) {
t.Parallel()
input := `
{
"email":"alice@coder.com",
"password":"hunter2",
"username":"alice",
"name":"alice",
"disable_login":false,
"login_type":"none"
}
`
var req codersdk.CreateUserRequestWithOrgs
err := json.Unmarshal([]byte(input), &req)
require.NoError(t, err)
require.Empty(t, req.OrganizationIDs)
})
}
func TestCreateUserRequestJSON(t *testing.T) {
t.Parallel()
marshalTest := func(t *testing.T, req codersdk.CreateUserRequestWithOrgs) {
t.Helper()
data, err := json.Marshal(req)
require.NoError(t, err)
var req2 codersdk.CreateUserRequestWithOrgs
err = json.Unmarshal(data, &req2)
require.NoError(t, err)
require.Equal(t, req, req2)
}
t.Run("MultipleOrganizations", func(t *testing.T) {
t.Parallel()
req := codersdk.CreateUserRequestWithOrgs{
Email: coderdtest.RandomName(t),
Username: coderdtest.RandomName(t),
Name: coderdtest.RandomName(t),
Password: "",
UserLoginType: codersdk.LoginTypePassword,
OrganizationIDs: []uuid.UUID{uuid.New(), uuid.New()},
}
marshalTest(t, req)
})
t.Run("SingleOrganization", func(t *testing.T) {
t.Parallel()
req := codersdk.CreateUserRequestWithOrgs{
Email: coderdtest.RandomName(t),
Username: coderdtest.RandomName(t),
Name: coderdtest.RandomName(t),
Password: "",
UserLoginType: codersdk.LoginTypePassword,
OrganizationIDs: []uuid.UUID{uuid.New()},
}
marshalTest(t, req)
})
t.Run("NoOrganization", func(t *testing.T) {
t.Parallel()
req := codersdk.CreateUserRequestWithOrgs{
Email: coderdtest.RandomName(t),
Username: coderdtest.RandomName(t),
Name: coderdtest.RandomName(t),
Password: "",
UserLoginType: codersdk.LoginTypePassword,
OrganizationIDs: []uuid.UUID{},
}
marshalTest(t, req)
})
}