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
+6 -5
View File
@@ -11,6 +11,7 @@ import (
"strings"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/config"
@@ -183,11 +184,11 @@ func prepareTestData(t *testing.T) (*codersdk.Client, map[string]string) {
IncludeProvisionerDaemon: true,
})
firstUser := coderdtest.CreateFirstUser(t, rootClient)
secondUser, err := rootClient.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "testuser2@coder.com",
Username: "testuser2",
Password: coderdtest.FirstUserParams.Password,
OrganizationID: firstUser.OrganizationID,
secondUser, err := rootClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: "testuser2@coder.com",
Username: "testuser2",
Password: coderdtest.FirstUserParams.Password,
OrganizationIDs: []uuid.UUID{firstUser.OrganizationID},
})
require.NoError(t, err)
version := coderdtest.CreateTemplateVersion(t, rootClient, firstUser.OrganizationID, nil)
+1 -1
View File
@@ -35,7 +35,7 @@ func setupTestSchedule(t *testing.T, sched *cron.Schedule) (ownerClient, memberC
ownerClient, db = coderdtest.NewWithDatabase(t, nil)
owner := coderdtest.CreateFirstUser(t, ownerClient)
memberClient, memberUser := coderdtest.CreateAnotherUserMutators(t, ownerClient, owner.OrganizationID, nil, func(r *codersdk.CreateUserRequest) {
memberClient, memberUser := coderdtest.CreateAnotherUserMutators(t, ownerClient, owner.OrganizationID, nil, func(r *codersdk.CreateUserRequestWithOrgs) {
r.Username = "testuser2" // ensure deterministic ordering
})
_ = dbfake.WorkspaceBuild(t, db, database.Workspace{
+6 -6
View File
@@ -83,12 +83,12 @@ func (r *RootCmd) newCreateAdminUserCommand() *serpent.Command {
validateInputs := func(username, email, password string) error {
// Use the validator tags so we match the API's validation.
req := codersdk.CreateUserRequest{
Username: "username",
Name: "Admin User",
Email: "email@coder.com",
Password: "ValidPa$$word123!",
OrganizationID: uuid.New(),
req := codersdk.CreateUserRequestWithOrgs{
Username: "username",
Name: "Admin User",
Email: "email@coder.com",
Password: "ValidPa$$word123!",
OrganizationIDs: []uuid.UUID{uuid.New()},
}
if username != "" {
req.Username = username
+20 -23
View File
@@ -4,6 +4,7 @@ import (
"context"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/clitest"
@@ -26,13 +27,12 @@ func TestUserDelete(t *testing.T) {
pw, err := cryptorand.String(16)
require.NoError(t, err)
_, err = client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "colin5@coder.com",
Username: "coolin",
Password: pw,
UserLoginType: codersdk.LoginTypePassword,
OrganizationID: owner.OrganizationID,
DisableLogin: false,
_, err = client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: "colin5@coder.com",
Username: "coolin",
Password: pw,
UserLoginType: codersdk.LoginTypePassword,
OrganizationIDs: []uuid.UUID{owner.OrganizationID},
})
require.NoError(t, err)
@@ -57,13 +57,12 @@ func TestUserDelete(t *testing.T) {
pw, err := cryptorand.String(16)
require.NoError(t, err)
user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "colin5@coder.com",
Username: "coolin",
Password: pw,
UserLoginType: codersdk.LoginTypePassword,
OrganizationID: owner.OrganizationID,
DisableLogin: false,
user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: "colin5@coder.com",
Username: "coolin",
Password: pw,
UserLoginType: codersdk.LoginTypePassword,
OrganizationIDs: []uuid.UUID{owner.OrganizationID},
})
require.NoError(t, err)
@@ -88,13 +87,12 @@ func TestUserDelete(t *testing.T) {
pw, err := cryptorand.String(16)
require.NoError(t, err)
user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "colin5@coder.com",
Username: "coolin",
Password: pw,
UserLoginType: codersdk.LoginTypePassword,
OrganizationID: owner.OrganizationID,
DisableLogin: false,
user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: "colin5@coder.com",
Username: "coolin",
Password: pw,
UserLoginType: codersdk.LoginTypePassword,
OrganizationIDs: []uuid.UUID{owner.OrganizationID},
})
require.NoError(t, err)
@@ -121,13 +119,12 @@ func TestUserDelete(t *testing.T) {
// pw, err := cryptorand.String(16)
// require.NoError(t, err)
// toDelete, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
// toDelete, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
// Email: "colin5@coder.com",
// Username: "coolin",
// Password: pw,
// UserLoginType: codersdk.LoginTypePassword,
// OrganizationID: aUser.OrganizationID,
// DisableLogin: false,
// })
// require.NoError(t, err)
+8 -7
View File
@@ -5,6 +5,7 @@ import (
"strings"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/pretty"
@@ -94,13 +95,13 @@ func (r *RootCmd) userCreate() *serpent.Command {
}
}
_, err = client.CreateUser(inv.Context(), codersdk.CreateUserRequest{
Email: email,
Username: username,
Name: name,
Password: password,
OrganizationID: organization.ID,
UserLoginType: userLoginType,
_, err = client.CreateUserWithOrgs(inv.Context(), codersdk.CreateUserRequestWithOrgs{
Email: email,
Username: username,
Name: name,
Password: password,
OrganizationIDs: []uuid.UUID{organization.ID},
UserLoginType: userLoginType,
})
if err != nil {
return err
+9 -9
View File
@@ -4875,7 +4875,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.CreateUserRequest"
"$ref": "#/definitions/codersdk.CreateUserRequestWithOrgs"
}
}
],
@@ -9449,17 +9449,13 @@ const docTemplate = `{
}
}
},
"codersdk.CreateUserRequest": {
"codersdk.CreateUserRequestWithOrgs": {
"type": "object",
"required": [
"email",
"username"
],
"properties": {
"disable_login": {
"description": "DisableLogin sets the user's login type to 'none'. This prevents the user\nfrom being able to use a password or any other authentication method to login.\nDeprecated: Set UserLoginType=LoginTypeDisabled instead.",
"type": "boolean"
},
"email": {
"type": "string",
"format": "email"
@@ -9475,9 +9471,13 @@ const docTemplate = `{
"name": {
"type": "string"
},
"organization_id": {
"type": "string",
"format": "uuid"
"organization_ids": {
"description": "OrganizationIDs is a list of organization IDs that the user should be a member of.",
"type": "array",
"items": {
"type": "string",
"format": "uuid"
}
},
"password": {
"type": "string"
+9 -9
View File
@@ -4305,7 +4305,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.CreateUserRequest"
"$ref": "#/definitions/codersdk.CreateUserRequestWithOrgs"
}
}
],
@@ -8413,14 +8413,10 @@
}
}
},
"codersdk.CreateUserRequest": {
"codersdk.CreateUserRequestWithOrgs": {
"type": "object",
"required": ["email", "username"],
"properties": {
"disable_login": {
"description": "DisableLogin sets the user's login type to 'none'. This prevents the user\nfrom being able to use a password or any other authentication method to login.\nDeprecated: Set UserLoginType=LoginTypeDisabled instead.",
"type": "boolean"
},
"email": {
"type": "string",
"format": "email"
@@ -8436,9 +8432,13 @@
"name": {
"type": "string"
},
"organization_id": {
"type": "string",
"format": "uuid"
"organization_ids": {
"description": "OrganizationIDs is a list of organization IDs that the user should be a member of.",
"type": "array",
"items": {
"type": "string",
"format": "uuid"
}
},
"password": {
"type": "string"
+16 -15
View File
@@ -648,11 +648,11 @@ func CreateFirstUser(t testing.TB, client *codersdk.Client) codersdk.CreateFirst
// CreateAnotherUser creates and authenticates a new user.
// Roles can include org scoped roles with 'roleName:<organization_id>'
func CreateAnotherUser(t testing.TB, client *codersdk.Client, organizationID uuid.UUID, roles ...rbac.RoleIdentifier) (*codersdk.Client, codersdk.User) {
return createAnotherUserRetry(t, client, organizationID, 5, roles)
return createAnotherUserRetry(t, client, []uuid.UUID{organizationID}, 5, roles)
}
func CreateAnotherUserMutators(t testing.TB, client *codersdk.Client, organizationID uuid.UUID, roles []rbac.RoleIdentifier, mutators ...func(r *codersdk.CreateUserRequest)) (*codersdk.Client, codersdk.User) {
return createAnotherUserRetry(t, client, organizationID, 5, roles, mutators...)
func CreateAnotherUserMutators(t testing.TB, client *codersdk.Client, organizationID uuid.UUID, roles []rbac.RoleIdentifier, mutators ...func(r *codersdk.CreateUserRequestWithOrgs)) (*codersdk.Client, codersdk.User) {
return createAnotherUserRetry(t, client, []uuid.UUID{organizationID}, 5, roles, mutators...)
}
// AuthzUserSubject does not include the user's groups.
@@ -678,31 +678,31 @@ func AuthzUserSubject(user codersdk.User, orgID uuid.UUID) rbac.Subject {
}
}
func createAnotherUserRetry(t testing.TB, client *codersdk.Client, organizationID uuid.UUID, retries int, roles []rbac.RoleIdentifier, mutators ...func(r *codersdk.CreateUserRequest)) (*codersdk.Client, codersdk.User) {
req := codersdk.CreateUserRequest{
Email: namesgenerator.GetRandomName(10) + "@coder.com",
Username: RandomUsername(t),
Name: RandomName(t),
Password: "SomeSecurePassword!",
OrganizationID: organizationID,
func createAnotherUserRetry(t testing.TB, client *codersdk.Client, organizationIDs []uuid.UUID, retries int, roles []rbac.RoleIdentifier, mutators ...func(r *codersdk.CreateUserRequestWithOrgs)) (*codersdk.Client, codersdk.User) {
req := codersdk.CreateUserRequestWithOrgs{
Email: namesgenerator.GetRandomName(10) + "@coder.com",
Username: RandomUsername(t),
Name: RandomName(t),
Password: "SomeSecurePassword!",
OrganizationIDs: organizationIDs,
}
for _, m := range mutators {
m(&req)
}
user, err := client.CreateUser(context.Background(), req)
user, err := client.CreateUserWithOrgs(context.Background(), req)
var apiError *codersdk.Error
// If the user already exists by username or email conflict, try again up to "retries" times.
if err != nil && retries >= 0 && xerrors.As(err, &apiError) {
if apiError.StatusCode() == http.StatusConflict {
retries--
return createAnotherUserRetry(t, client, organizationID, retries, roles)
return createAnotherUserRetry(t, client, organizationIDs, retries, roles)
}
}
require.NoError(t, err)
var sessionToken string
if req.DisableLogin || req.UserLoginType == codersdk.LoginTypeNone {
if req.UserLoginType == codersdk.LoginTypeNone {
// Cannot log in with a disabled login user. So make it an api key from
// the client making this user.
token, err := client.CreateToken(context.Background(), user.ID.String(), codersdk.CreateTokenRequest{
@@ -765,8 +765,9 @@ func createAnotherUserRetry(t testing.TB, client *codersdk.Client, organizationI
require.NoError(t, err, "update site roles")
// isMember keeps track of which orgs the user was added to as a member
isMember := map[uuid.UUID]bool{
organizationID: true,
isMember := make(map[uuid.UUID]bool)
for _, orgID := range organizationIDs {
isMember[orgID] = true
}
// Update org roles
+1 -1
View File
@@ -523,7 +523,7 @@ func TestTemplateInsights_Golden(t *testing.T) {
// Prepare all test users.
for _, user := range users {
user.client, user.sdk = coderdtest.CreateAnotherUserMutators(t, client, firstUser.OrganizationID, nil, func(r *codersdk.CreateUserRequest) {
user.client, user.sdk = coderdtest.CreateAnotherUserMutators(t, client, firstUser.OrganizationID, nil, func(r *codersdk.CreateUserRequestWithOrgs) {
r.Username = user.name
})
user.client.SetLogger(logger.Named("user").With(slog.Field{Name: "name", Value: user.name}))
+5 -5
View File
@@ -1436,11 +1436,11 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
}
//nolint:gocritic
user, _, err = api.CreateUser(dbauthz.AsSystemRestricted(ctx), tx, CreateUserRequest{
CreateUserRequest: codersdk.CreateUserRequest{
Email: params.Email,
Username: params.Username,
OrganizationID: defaultOrganization.ID,
user, err = api.CreateUser(dbauthz.AsSystemRestricted(ctx), tx, CreateUserRequest{
CreateUserRequestWithOrgs: codersdk.CreateUserRequestWithOrgs{
Email: params.Email,
Username: params.Username,
OrganizationIDs: []uuid.UUID{defaultOrganization.ID},
},
LoginType: params.LoginType,
})
+6 -22
View File
@@ -106,28 +106,12 @@ func TestUserLogin(t *testing.T) {
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
})
// Password auth should fail if the user is made without password login.
t.Run("DisableLoginDeprecatedField", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
anotherClient, anotherUser := coderdtest.CreateAnotherUserMutators(t, client, user.OrganizationID, nil, func(r *codersdk.CreateUserRequest) {
r.Password = ""
r.DisableLogin = true
})
_, err := anotherClient.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
Email: anotherUser.Email,
Password: "SomeSecurePassword!",
})
require.Error(t, err)
})
t.Run("LoginTypeNone", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
anotherClient, anotherUser := coderdtest.CreateAnotherUserMutators(t, client, user.OrganizationID, nil, func(r *codersdk.CreateUserRequest) {
anotherClient, anotherUser := coderdtest.CreateAnotherUserMutators(t, client, user.OrganizationID, nil, func(r *codersdk.CreateUserRequestWithOrgs) {
r.Password = ""
r.UserLoginType = codersdk.LoginTypeNone
})
@@ -1470,11 +1454,11 @@ func TestUserLogout(t *testing.T) {
//nolint:gosec
password = "SomeSecurePassword123!"
)
newUser, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: email,
Username: username,
Password: password,
OrganizationID: firstUser.OrganizationID,
newUser, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: email,
Username: username,
Password: password,
OrganizationIDs: []uuid.UUID{firstUser.OrganizationID},
})
require.NoError(t, err)
+66 -63
View File
@@ -186,13 +186,13 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
}
//nolint:gocritic // needed to create first user
user, organizationID, err := api.CreateUser(dbauthz.AsSystemRestricted(ctx), api.Database, CreateUserRequest{
CreateUserRequest: codersdk.CreateUserRequest{
Email: createUser.Email,
Username: createUser.Username,
Name: createUser.Name,
Password: createUser.Password,
OrganizationID: defaultOrg.ID,
user, err := api.CreateUser(dbauthz.AsSystemRestricted(ctx), api.Database, CreateUserRequest{
CreateUserRequestWithOrgs: codersdk.CreateUserRequestWithOrgs{
Email: createUser.Email,
Username: createUser.Username,
Name: createUser.Name,
Password: createUser.Password,
OrganizationIDs: []uuid.UUID{defaultOrg.ID},
},
LoginType: database.LoginTypePassword,
})
@@ -240,7 +240,7 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusCreated, codersdk.CreateFirstUserResponse{
UserID: user.ID,
OrganizationID: organizationID,
OrganizationID: defaultOrg.ID,
})
}
@@ -342,7 +342,7 @@ func (api *API) GetUsers(rw http.ResponseWriter, r *http.Request) ([]database.Us
// @Accept json
// @Produce json
// @Tags Users
// @Param request body codersdk.CreateUserRequest true "Create user request"
// @Param request body codersdk.CreateUserRequestWithOrgs true "Create user request"
// @Success 201 {object} codersdk.User
// @Router /users [post]
func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
@@ -356,15 +356,11 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
})
defer commitAudit()
var req codersdk.CreateUserRequest
var req codersdk.CreateUserRequestWithOrgs
if !httpapi.Read(ctx, rw, r, &req) {
return
}
if req.UserLoginType == "" && req.DisableLogin {
// Handle the deprecated field
req.UserLoginType = codersdk.LoginTypeNone
}
if req.UserLoginType == "" {
// Default to password auth
req.UserLoginType = codersdk.LoginTypePassword
@@ -386,6 +382,20 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
return
}
if len(req.OrganizationIDs) == 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "No organization specified to place the user as a member of. It is required to specify at least one organization id to place the user in.",
Detail: "required at least 1 value for the array 'organization_ids'",
Validations: []codersdk.ValidationError{
{
Field: "organization_ids",
Detail: "Missing values, this cannot be empty",
},
},
})
return
}
// TODO: @emyrk Authorize the organization create if the createUser will do that.
_, err := api.Database.GetUserByEmailOrUsername(ctx, database.GetUserByEmailOrUsernameParams{
@@ -406,44 +416,33 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
return
}
if req.OrganizationID != uuid.Nil {
// If an organization was provided, make sure it exists.
_, err := api.Database.GetOrganizationByID(ctx, req.OrganizationID)
if err != nil {
if httpapi.Is404Error(err) {
// If an organization was provided, make sure it exists.
for i, orgID := range req.OrganizationIDs {
var orgErr error
if orgID != uuid.Nil {
_, orgErr = api.Database.GetOrganizationByID(ctx, orgID)
} else {
var defaultOrg database.Organization
defaultOrg, orgErr = api.Database.GetDefaultOrganization(ctx)
if orgErr == nil {
// converts uuid.Nil --> default org.ID
req.OrganizationIDs[i] = defaultOrg.ID
}
}
if orgErr != nil {
if httpapi.Is404Error(orgErr) {
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
Message: fmt.Sprintf("Organization does not exist with the provided id %q.", req.OrganizationID),
Message: fmt.Sprintf("Organization does not exist with the provided id %q.", orgID),
})
return
}
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching organization.",
Detail: err.Error(),
Detail: orgErr.Error(),
})
return
}
} else {
// If no organization is provided, add the user to the default
defaultOrg, err := api.Database.GetDefaultOrganization(ctx)
if err != nil {
if httpapi.Is404Error(err) {
httpapi.Write(ctx, rw, http.StatusNotFound,
codersdk.Response{
Message: "Resource not found or you do not have access to this resource",
Detail: "Organization not found",
},
)
return
}
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching orgs.",
Detail: err.Error(),
})
return
}
req.OrganizationID = defaultOrg.ID
}
var loginType database.LoginType
@@ -480,9 +479,9 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
return
}
user, _, err := api.CreateUser(ctx, api.Database, CreateUserRequest{
CreateUserRequest: req,
LoginType: loginType,
user, err := api.CreateUser(ctx, api.Database, CreateUserRequest{
CreateUserRequestWithOrgs: req,
LoginType: loginType,
})
if dbauthz.IsNotAuthorizedError(err) {
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
@@ -505,7 +504,7 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
Users: []telemetry.User{telemetry.ConvertUser(user)},
})
httpapi.Write(ctx, rw, http.StatusCreated, db2sdk.User(user, []uuid.UUID{req.OrganizationID}))
httpapi.Write(ctx, rw, http.StatusCreated, db2sdk.User(user, req.OrganizationIDs))
}
// @Summary Delete user
@@ -1280,23 +1279,23 @@ func (api *API) organizationByUserAndName(rw http.ResponseWriter, r *http.Reques
}
type CreateUserRequest struct {
codersdk.CreateUserRequest
codersdk.CreateUserRequestWithOrgs
LoginType database.LoginType
SkipNotifications bool
}
func (api *API) CreateUser(ctx context.Context, store database.Store, req CreateUserRequest) (database.User, uuid.UUID, error) {
func (api *API) CreateUser(ctx context.Context, store database.Store, req CreateUserRequest) (database.User, error) {
// Ensure the username is valid. It's the caller's responsibility to ensure
// the username is valid and unique.
if usernameValid := httpapi.NameValid(req.Username); usernameValid != nil {
return database.User{}, uuid.Nil, xerrors.Errorf("invalid username %q: %w", req.Username, usernameValid)
return database.User{}, xerrors.Errorf("invalid username %q: %w", req.Username, usernameValid)
}
var user database.User
err := store.InTx(func(tx database.Store) error {
orgRoles := make([]string, 0)
// Organization is required to know where to allocate the user.
if req.OrganizationID == uuid.Nil {
if len(req.OrganizationIDs) == 0 {
return xerrors.Errorf("organization ID must be provided")
}
@@ -1341,26 +1340,30 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
if err != nil {
return xerrors.Errorf("insert user gitsshkey: %w", err)
}
_, err = tx.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{
OrganizationID: req.OrganizationID,
UserID: user.ID,
CreatedAt: dbtime.Now(),
UpdatedAt: dbtime.Now(),
// By default give them membership to the organization.
Roles: orgRoles,
})
if err != nil {
return xerrors.Errorf("create organization member: %w", err)
for _, orgID := range req.OrganizationIDs {
_, err = tx.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{
OrganizationID: orgID,
UserID: user.ID,
CreatedAt: dbtime.Now(),
UpdatedAt: dbtime.Now(),
// By default give them membership to the organization.
Roles: orgRoles,
})
if err != nil {
return xerrors.Errorf("create organization member for %q: %w", orgID.String(), err)
}
}
return nil
}, nil)
if err != nil || req.SkipNotifications {
return user, req.OrganizationID, err
return user, err
}
userAdmins, err := findUserAdmins(ctx, store)
if err != nil {
return user, req.OrganizationID, xerrors.Errorf("find user admins: %w", err)
return user, xerrors.Errorf("find user admins: %w", err)
}
for _, u := range userAdmins {
@@ -1373,7 +1376,7 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
api.Logger.Warn(ctx, "unable to notify about created user", slog.F("created_user", user.Username), slog.Error(err))
}
}
return user, req.OrganizationID, err
return user, err
}
// findUserAdmins fetches all users with user admin permission including owners.
+128 -128
View File
@@ -220,11 +220,11 @@ func TestPostLogin(t *testing.T) {
// With a user account.
const password = "SomeSecurePassword!"
user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "test+user-@coder.com",
Username: "user",
Password: password,
OrganizationID: first.OrganizationID,
user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: "test+user-@coder.com",
Username: "user",
Password: password,
OrganizationIDs: []uuid.UUID{first.OrganizationID},
})
require.NoError(t, err)
@@ -317,11 +317,11 @@ func TestDeleteUser(t *testing.T) {
err := client.DeleteUser(context.Background(), another.ID)
require.NoError(t, err)
// Attempt to create a user with the same email and username, and delete them again.
another, err = client.CreateUser(context.Background(), codersdk.CreateUserRequest{
Email: another.Email,
Username: another.Username,
Password: "SomeSecurePassword!",
OrganizationID: user.OrganizationID,
another, err = client.CreateUserWithOrgs(context.Background(), codersdk.CreateUserRequestWithOrgs{
Email: another.Email,
Username: another.Username,
Password: "SomeSecurePassword!",
OrganizationIDs: []uuid.UUID{user.OrganizationID},
})
require.NoError(t, err)
err = client.DeleteUser(context.Background(), another.ID)
@@ -415,11 +415,11 @@ func TestNotifyUserStatusChanged(t *testing.T) {
_, userAdmin := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID, rbac.RoleUserAdmin())
member, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{
OrganizationID: firstUser.OrganizationID,
Email: "another@user.org",
Username: "someone-else",
Password: "SomeSecurePassword!",
member, err := adminClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
OrganizationIDs: []uuid.UUID{firstUser.OrganizationID},
Email: "another@user.org",
Username: "someone-else",
Password: "SomeSecurePassword!",
})
require.NoError(t, err)
@@ -452,11 +452,11 @@ func TestNotifyUserStatusChanged(t *testing.T) {
_, userAdmin := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID, rbac.RoleUserAdmin())
member, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{
OrganizationID: firstUser.OrganizationID,
Email: "another@user.org",
Username: "someone-else",
Password: "SomeSecurePassword!",
member, err := adminClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
OrganizationIDs: []uuid.UUID{firstUser.OrganizationID},
Email: "another@user.org",
Username: "someone-else",
Password: "SomeSecurePassword!",
})
require.NoError(t, err)
@@ -494,11 +494,11 @@ func TestNotifyDeletedUser(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
user, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{
OrganizationID: firstUser.OrganizationID,
Email: "another@user.org",
Username: "someone-else",
Password: "SomeSecurePassword!",
user, err := adminClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
OrganizationIDs: []uuid.UUID{firstUser.OrganizationID},
Email: "another@user.org",
Username: "someone-else",
Password: "SomeSecurePassword!",
})
require.NoError(t, err)
@@ -530,11 +530,11 @@ func TestNotifyDeletedUser(t *testing.T) {
_, userAdmin := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID, rbac.RoleUserAdmin())
member, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{
OrganizationID: firstUser.OrganizationID,
Email: "another@user.org",
Username: "someone-else",
Password: "SomeSecurePassword!",
member, err := adminClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
OrganizationIDs: []uuid.UUID{firstUser.OrganizationID},
Email: "another@user.org",
Username: "someone-else",
Password: "SomeSecurePassword!",
})
require.NoError(t, err)
@@ -625,7 +625,7 @@ func TestPostUsers(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
_, err := client.CreateUser(ctx, codersdk.CreateUserRequest{})
_, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{})
require.Error(t, err)
})
@@ -639,11 +639,11 @@ func TestPostUsers(t *testing.T) {
me, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
_, err = client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: me.Email,
Username: me.Username,
Password: "MySecurePassword!",
OrganizationID: uuid.New(),
_, err = client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: me.Email,
Username: me.Username,
Password: "MySecurePassword!",
OrganizationIDs: []uuid.UUID{uuid.New()},
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
@@ -658,11 +658,11 @@ func TestPostUsers(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
_, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
OrganizationID: uuid.New(),
Email: "another@user.org",
Username: "someone-else",
Password: "SomeSecurePassword!",
_, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
OrganizationIDs: []uuid.UUID{uuid.New()},
Email: "another@user.org",
Username: "someone-else",
Password: "SomeSecurePassword!",
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
@@ -682,11 +682,11 @@ func TestPostUsers(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
OrganizationID: firstUser.OrganizationID,
Email: "another@user.org",
Username: "someone-else",
Password: "SomeSecurePassword!",
user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
OrganizationIDs: []uuid.UUID{firstUser.OrganizationID},
Email: "another@user.org",
Username: "someone-else",
Password: "SomeSecurePassword!",
})
require.NoError(t, err)
@@ -735,12 +735,12 @@ func TestPostUsers(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
OrganizationID: first.OrganizationID,
Email: "another@user.org",
Username: "someone-else",
Password: "",
UserLoginType: codersdk.LoginTypeNone,
user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
OrganizationIDs: []uuid.UUID{first.OrganizationID},
Email: "another@user.org",
Username: "someone-else",
Password: "",
UserLoginType: codersdk.LoginTypeNone,
})
require.NoError(t, err)
@@ -767,12 +767,12 @@ func TestPostUsers(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
_, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
OrganizationID: first.OrganizationID,
Email: email,
Username: "someone-else",
Password: "",
UserLoginType: codersdk.LoginTypeOIDC,
_, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
OrganizationIDs: []uuid.UUID{first.OrganizationID},
Email: email,
Username: "someone-else",
Password: "",
UserLoginType: codersdk.LoginTypeOIDC,
})
require.NoError(t, err)
@@ -804,11 +804,11 @@ func TestNotifyCreatedUser(t *testing.T) {
defer cancel()
// when
user, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{
OrganizationID: firstUser.OrganizationID,
Email: "another@user.org",
Username: "someone-else",
Password: "SomeSecurePassword!",
user, err := adminClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
OrganizationIDs: []uuid.UUID{firstUser.OrganizationID},
Email: "another@user.org",
Username: "someone-else",
Password: "SomeSecurePassword!",
})
require.NoError(t, err)
@@ -833,11 +833,11 @@ func TestNotifyCreatedUser(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
userAdmin, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{
OrganizationID: firstUser.OrganizationID,
Email: "user-admin@user.org",
Username: "mr-user-admin",
Password: "SomeSecurePassword!",
userAdmin, err := adminClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
OrganizationIDs: []uuid.UUID{firstUser.OrganizationID},
Email: "user-admin@user.org",
Username: "mr-user-admin",
Password: "SomeSecurePassword!",
})
require.NoError(t, err)
@@ -849,11 +849,11 @@ func TestNotifyCreatedUser(t *testing.T) {
require.NoError(t, err)
// when
member, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{
OrganizationID: firstUser.OrganizationID,
Email: "another@user.org",
Username: "someone-else",
Password: "SomeSecurePassword!",
member, err := adminClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
OrganizationIDs: []uuid.UUID{firstUser.OrganizationID},
Email: "another@user.org",
Username: "someone-else",
Password: "SomeSecurePassword!",
})
require.NoError(t, err)
@@ -908,11 +908,11 @@ func TestUpdateUserProfile(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
existentUser, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "bruno@coder.com",
Username: "bruno",
Password: "SomeSecurePassword!",
OrganizationID: user.OrganizationID,
existentUser, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: "bruno@coder.com",
Username: "bruno",
Password: "SomeSecurePassword!",
OrganizationIDs: []uuid.UUID{user.OrganizationID},
})
require.NoError(t, err)
_, err = client.UpdateUserProfile(ctx, codersdk.Me, codersdk.UpdateUserProfileRequest{
@@ -990,11 +990,11 @@ func TestUpdateUserProfile(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
_, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "john@coder.com",
Username: "john",
Password: "SomeSecurePassword!",
OrganizationID: user.OrganizationID,
_, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: "john@coder.com",
Username: "john",
Password: "SomeSecurePassword!",
OrganizationIDs: []uuid.UUID{user.OrganizationID},
})
require.NoError(t, err)
_, err = client.UpdateUserProfile(ctx, codersdk.Me, codersdk.UpdateUserProfileRequest{
@@ -1032,11 +1032,11 @@ func TestUpdateUserPassword(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
member, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "coder@coder.com",
Username: "coder",
Password: "SomeStrongPassword!",
OrganizationID: owner.OrganizationID,
member, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: "coder@coder.com",
Username: "coder",
Password: "SomeStrongPassword!",
OrganizationIDs: []uuid.UUID{owner.OrganizationID},
})
require.NoError(t, err, "create member")
err = client.UpdateUserPassword(ctx, member.ID.String(), codersdk.UpdateUserPasswordRequest{
@@ -1293,11 +1293,11 @@ func TestActivateDormantUser(t *testing.T) {
me := coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
anotherUser, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "coder@coder.com",
Username: "coder",
Password: "SomeStrongPassword!",
OrganizationID: me.OrganizationID,
anotherUser, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: "coder@coder.com",
Username: "coder",
Password: "SomeStrongPassword!",
OrganizationIDs: []uuid.UUID{me.OrganizationID},
})
require.NoError(t, err)
@@ -1600,11 +1600,11 @@ func TestGetUsers(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "alice@email.com",
Username: "alice",
Password: "MySecurePassword!",
OrganizationID: user.OrganizationID,
client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: "alice@email.com",
Username: "alice",
Password: "MySecurePassword!",
OrganizationIDs: []uuid.UUID{user.OrganizationID},
})
// No params is all users
res, err := client.Users(ctx, codersdk.UsersRequest{})
@@ -1626,11 +1626,11 @@ func TestGetUsers(t *testing.T) {
active = append(active, firstUser)
// Alice will be suspended
alice, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "alice@email.com",
Username: "alice",
Password: "MySecurePassword!",
OrganizationID: first.OrganizationID,
alice, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: "alice@email.com",
Username: "alice",
Password: "MySecurePassword!",
OrganizationIDs: []uuid.UUID{first.OrganizationID},
})
require.NoError(t, err)
@@ -1638,11 +1638,11 @@ func TestGetUsers(t *testing.T) {
require.NoError(t, err)
// Tom will be active
tom, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "tom@email.com",
Username: "tom",
Password: "MySecurePassword!",
OrganizationID: first.OrganizationID,
tom, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: "tom@email.com",
Username: "tom",
Password: "MySecurePassword!",
OrganizationIDs: []uuid.UUID{first.OrganizationID},
})
require.NoError(t, err)
@@ -1669,11 +1669,11 @@ func TestGetUsersPagination(t *testing.T) {
_, err := client.User(ctx, first.UserID.String())
require.NoError(t, err, "")
_, err = client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "alice@email.com",
Username: "alice",
Password: "MySecurePassword!",
OrganizationID: first.OrganizationID,
_, err = client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: "alice@email.com",
Username: "alice",
Password: "MySecurePassword!",
OrganizationIDs: []uuid.UUID{first.OrganizationID},
})
require.NoError(t, err)
@@ -1750,11 +1750,11 @@ func TestWorkspacesByUser(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
newUser, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "test@coder.com",
Username: "someone",
Password: "MySecurePassword!",
OrganizationID: user.OrganizationID,
newUser, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: "test@coder.com",
Username: "someone",
Password: "MySecurePassword!",
OrganizationIDs: []uuid.UUID{user.OrganizationID},
})
require.NoError(t, err)
auth, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
@@ -1790,11 +1790,11 @@ func TestDormantUser(t *testing.T) {
defer cancel()
// Create a new user
newUser, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "test@coder.com",
Username: "someone",
Password: "MySecurePassword!",
OrganizationID: user.OrganizationID,
newUser, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: "test@coder.com",
Username: "someone",
Password: "MySecurePassword!",
OrganizationIDs: []uuid.UUID{user.OrganizationID},
})
require.NoError(t, err)
@@ -1841,11 +1841,11 @@ func TestSuspendedPagination(t *testing.T) {
for i := 0; i < total; i++ {
email := fmt.Sprintf("%d@coder.com", i)
username := fmt.Sprintf("user%d", i)
user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: email,
Username: username,
Password: "MySecurePassword!",
OrganizationID: orgID,
user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: email,
Username: username,
Password: "MySecurePassword!",
OrganizationIDs: []uuid.UUID{orgID},
})
require.NoError(t, err)
users = append(users, user)
+10 -10
View File
@@ -1206,11 +1206,11 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
// Create a template-admin user in the same org. We don't use an owner
// since they have access to everything.
ownerClient = appDetails.SDKClient
user, err := ownerClient.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "user@coder.com",
Username: "user",
Password: password,
OrganizationID: appDetails.FirstUser.OrganizationID,
user, err := ownerClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: "user@coder.com",
Username: "user",
Password: password,
OrganizationIDs: []uuid.UUID{appDetails.FirstUser.OrganizationID},
})
require.NoError(t, err)
@@ -1258,11 +1258,11 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
Name: "a-different-org",
})
require.NoError(t, err)
userInOtherOrg, err := ownerClient.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "no-template-access@coder.com",
Username: "no-template-access",
Password: password,
OrganizationID: otherOrg.ID,
userInOtherOrg, err := ownerClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: "no-template-access@coder.com",
Username: "no-template-access",
Password: password,
OrganizationIDs: []uuid.UUID{otherOrg.ID},
})
require.NoError(t, err)
+1 -1
View File
@@ -451,7 +451,7 @@ func TestWorkspacesSortOrder(t *testing.T) {
client, db := coderdtest.NewWithDatabase(t, nil)
firstUser := coderdtest.CreateFirstUser(t, client)
secondUserClient, secondUser := coderdtest.CreateAnotherUserMutators(t, client, firstUser.OrganizationID, []rbac.RoleIdentifier{rbac.RoleOwner()}, func(r *codersdk.CreateUserRequest) {
secondUserClient, secondUser := coderdtest.CreateAnotherUserMutators(t, client, firstUser.OrganizationID, []rbac.RoleIdentifier{rbac.RoleOwner()}, func(r *codersdk.CreateUserRequestWithOrgs) {
r.Username = "zzz"
})
+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)
})
}
+10 -12
View File
@@ -1284,15 +1284,14 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `scope` | `all` |
| `scope` | `application_connect` |
## codersdk.CreateUserRequest
## codersdk.CreateUserRequestWithOrgs
```json
{
"disable_login": true,
"email": "user@example.com",
"login_type": "",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"password": "string",
"username": "string"
}
@@ -1300,15 +1299,14 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
### Properties
| Name | Type | Required | Restrictions | Description |
| ----------------- | ---------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `disable_login` | boolean | false | | Disable login sets the user's login type to 'none'. This prevents the user from being able to use a password or any other authentication method to login. Deprecated: Set UserLoginType=LoginTypeDisabled instead. |
| `email` | string | true | | |
| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | Login type defaults to LoginTypePassword. |
| `name` | string | false | | |
| `organization_id` | string | false | | |
| `password` | string | false | | |
| `username` | string | true | | |
| Name | Type | Required | Restrictions | Description |
| ------------------ | ---------------------------------------- | -------- | ------------ | ----------------------------------------------------------------------------------- |
| `email` | string | true | | |
| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | Login type defaults to LoginTypePassword. |
| `name` | string | false | | |
| `organization_ids` | array of string | false | | Organization ids is a list of organization IDs that the user should be a member of. |
| `password` | string | false | | |
| `username` | string | true | | |
## codersdk.CreateWorkspaceBuildRequest
+4 -5
View File
@@ -81,11 +81,10 @@ curl -X POST http://coder-server:8080/api/v2/users \
```json
{
"disable_login": true,
"email": "user@example.com",
"login_type": "",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"password": "string",
"username": "string"
}
@@ -93,9 +92,9 @@ curl -X POST http://coder-server:8080/api/v2/users \
### Parameters
| Name | In | Type | Required | Description |
| ------ | ---- | ------------------------------------------------------------------ | -------- | ------------------- |
| `body` | body | [codersdk.CreateUserRequest](schemas.md#codersdkcreateuserrequest) | true | Create user request |
| Name | In | Type | Required | Description |
| ------ | ---- | ---------------------------------------------------------------------------------- | -------- | ------------------- |
| `body` | body | [codersdk.CreateUserRequestWithOrgs](schemas.md#codersdkcreateuserrequestwithorgs) | true | Create user request |
### Example responses
+5 -5
View File
@@ -757,11 +757,11 @@ func TestGroup(t *testing.T) {
require.Contains(t, group.Members, user2.ReducedUser)
// cannot explicitly set a dormant user status so must create a new user
anotherUser, err := userAdminClient.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "coder@coder.com",
Username: "coder",
Password: "SomeStrongPassword!",
OrganizationID: user.OrganizationID,
anotherUser, err := userAdminClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: "coder@coder.com",
Username: "coder",
Password: "SomeStrongPassword!",
OrganizationIDs: []uuid.UUID{user.OrganizationID},
})
require.NoError(t, err)
+5 -5
View File
@@ -228,11 +228,11 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) {
}
//nolint:gocritic // needed for SCIM
dbUser, _, err = api.AGPL.CreateUser(dbauthz.AsSystemRestricted(ctx), api.Database, agpl.CreateUserRequest{
CreateUserRequest: codersdk.CreateUserRequest{
Username: sUser.UserName,
Email: email,
OrganizationID: defaultOrganization.ID,
dbUser, err = api.AGPL.CreateUser(dbauthz.AsSystemRestricted(ctx), api.Database, agpl.CreateUserRequest{
CreateUserRequestWithOrgs: codersdk.CreateUserRequestWithOrgs{
Username: sUser.UserName,
Email: email,
OrganizationIDs: []uuid.UUID{defaultOrganization.ID},
},
LoginType: database.LoginTypeOIDC,
// Do not send notifications to user admins as SCIM endpoint might be called sequentially to all users.
+2 -2
View File
@@ -717,7 +717,7 @@ func TestEnterpriseUserLogin(t *testing.T) {
Name: customRole.Name,
OrganizationID: owner.OrganizationID,
},
}, func(r *codersdk.CreateUserRequest) {
}, func(r *codersdk.CreateUserRequestWithOrgs) {
r.Password = "SomeSecurePassword!"
r.UserLoginType = codersdk.LoginTypePassword
})
@@ -752,7 +752,7 @@ func TestEnterpriseUserLogin(t *testing.T) {
},
})
anotherClient, anotherUser := coderdtest.CreateAnotherUserMutators(t, ownerClient, owner.OrganizationID, nil, func(r *codersdk.CreateUserRequest) {
anotherClient, anotherUser := coderdtest.CreateAnotherUserMutators(t, ownerClient, owner.OrganizationID, nil, func(r *codersdk.CreateUserRequestWithOrgs) {
r.Password = "SomeSecurePassword!"
r.UserLoginType = codersdk.LoginTypePassword
})
+53 -15
View File
@@ -7,7 +7,6 @@ import (
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/coderdtest"
@@ -509,11 +508,11 @@ func TestEnterprisePostUser(t *testing.T) {
request.Name = "another"
})
_, err := notInOrg.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "some@domain.com",
Username: "anotheruser",
Password: "SomeSecurePassword!",
OrganizationID: org.ID,
_, err := notInOrg.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: "some@domain.com",
Username: "anotheruser",
Password: "SomeSecurePassword!",
OrganizationIDs: []uuid.UUID{org.ID},
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
@@ -543,11 +542,11 @@ func TestEnterprisePostUser(t *testing.T) {
org := coderdenttest.CreateOrganization(t, other, coderdenttest.CreateOrganizationOptions{})
_, err := notInOrg.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "some@domain.com",
Username: "anotheruser",
Password: "SomeSecurePassword!",
OrganizationID: org.ID,
_, err := notInOrg.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: "some@domain.com",
Username: "anotheruser",
Password: "SomeSecurePassword!",
OrganizationIDs: []uuid.UUID{org.ID},
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
@@ -559,7 +558,7 @@ func TestEnterprisePostUser(t *testing.T) {
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
client, firstUser := coderdenttest.New(t, &coderdenttest.Options{
client, _ := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
@@ -578,14 +577,53 @@ func TestEnterprisePostUser(t *testing.T) {
// nolint:gocritic // intentional using the owner.
// Manually making a user with the request instead of the coderdtest util
user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
_, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: "another@user.org",
Username: "someone-else",
Password: "SomeSecurePassword!",
})
require.ErrorContains(t, err, "No organization specified")
})
t.Run("MultipleOrganizations", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
client, _ := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})
// Add an extra org to assign member into
second := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{})
third := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{})
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
// nolint:gocritic // intentional using the owner.
// Manually making a user with the request instead of the coderdtest util
user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: "another@user.org",
Username: "someone-else",
Password: "SomeSecurePassword!",
OrganizationIDs: []uuid.UUID{
second.ID,
third.ID,
},
})
require.NoError(t, err)
require.Len(t, user.OrganizationIDs, 1)
assert.Equal(t, firstUser.OrganizationID, user.OrganizationIDs[0])
memberedOrgs, err := client.OrganizationsByUser(ctx, user.ID.String())
require.NoError(t, err)
require.Len(t, memberedOrgs, 2)
require.ElementsMatch(t, []uuid.UUID{second.ID, third.ID}, []uuid.UUID{memberedOrgs[0].ID, memberedOrgs[1].ID})
})
}
+5 -5
View File
@@ -72,11 +72,11 @@ func (r *Runner) Run(ctx context.Context, id string, logs io.Writer) error {
_, _ = fmt.Fprintln(logs, "Creating user:")
user, err = r.client.CreateUser(ctx, codersdk.CreateUserRequest{
OrganizationID: r.cfg.User.OrganizationID,
Username: r.cfg.User.Username,
Email: r.cfg.User.Email,
Password: password,
user, err = r.client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
OrganizationIDs: []uuid.UUID{r.cfg.User.OrganizationID},
Username: r.cfg.User.Username,
Email: r.cfg.User.Email,
Password: password,
})
if err != nil {
return xerrors.Errorf("create user: %w", err)
+1 -2
View File
@@ -36,8 +36,7 @@ export const createUser = async (orgId: string) => {
name: name,
password: "s3cure&password!",
login_type: "password",
disable_login: false,
organization_id: orgId,
organization_ids: [orgId],
});
return user;
};
+1 -1
View File
@@ -1130,7 +1130,7 @@ class ApiMethods {
};
createUser = async (
user: TypesGen.CreateUserRequest,
user: TypesGen.CreateUserRequestWithOrgs,
): Promise<TypesGen.User> => {
const response = await this.axios.post<TypesGen.User>(
"/api/v2/users",
+2 -3
View File
@@ -303,14 +303,13 @@ export interface CreateTokenRequest {
}
// From codersdk/users.go
export interface CreateUserRequest {
export interface CreateUserRequestWithOrgs {
readonly email: string;
readonly username: string;
readonly name: string;
readonly password: string;
readonly login_type: LoginType;
readonly disable_login: boolean;
readonly organization_id: string;
readonly organization_ids: Readonly<Array<string>>;
}
// From codersdk/workspaces.go
@@ -61,7 +61,7 @@ export const authMethodLanguage = {
};
export interface CreateUserFormProps {
onSubmit: (user: TypesGen.CreateUserRequest) => void;
onSubmit: (user: TypesGen.CreateUserRequestWithOrgs) => void;
onCancel: () => void;
error?: unknown;
isLoading: boolean;
@@ -86,21 +86,20 @@ const validationSchema = Yup.object({
export const CreateUserForm: FC<
React.PropsWithChildren<CreateUserFormProps>
> = ({ onSubmit, onCancel, error, isLoading, authMethods }) => {
const form: FormikContextType<TypesGen.CreateUserRequest> =
useFormik<TypesGen.CreateUserRequest>({
const form: FormikContextType<TypesGen.CreateUserRequestWithOrgs> =
useFormik<TypesGen.CreateUserRequestWithOrgs>({
initialValues: {
email: "",
password: "",
username: "",
name: "",
organization_id: "00000000-0000-0000-0000-000000000000",
disable_login: false,
organization_ids: ["00000000-0000-0000-0000-000000000000"],
login_type: "",
},
validationSchema,
onSubmit,
});
const getFieldHelpers = getFormHelpers<TypesGen.CreateUserRequest>(
const getFieldHelpers = getFormHelpers<TypesGen.CreateUserRequestWithOrgs>(
form,
error,
);