From d8e6672a75fea91f791387ec281237fe7a27199e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 21 Aug 2024 09:14:53 -0500 Subject: [PATCH 01/10] 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. --- coderd/userauth.go | 8 +-- coderd/users.go | 112 ++++++++++++++++++++------------------ codersdk/users.go | 30 +++++++++- codersdk/users_test.go | 92 +++++++++++++++++++++++++++++++ enterprise/coderd/scim.go | 2 +- 5 files changed, 185 insertions(+), 59 deletions(-) create mode 100644 codersdk/users_test.go diff --git a/coderd/userauth.go b/coderd/userauth.go index f876bf7686341..df1e553bfa753 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -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{ + user, err = api.CreateUser(dbauthz.AsSystemRestricted(ctx), tx, CreateUserRequest{ CreateUserRequest: codersdk.CreateUserRequest{ - Email: params.Email, - Username: params.Username, - OrganizationID: defaultOrganization.ID, + Email: params.Email, + Username: params.Username, + OrganizationIDs: []uuid.UUID{defaultOrganization.ID}, }, LoginType: params.LoginType, }) diff --git a/coderd/users.go b/coderd/users.go index 07ec053ca44a7..e66d8808139b7 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -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{ + user, 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, + 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, }) } @@ -386,6 +386,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: fmt.Sprintf("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: fmt.Sprintf("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 +420,34 @@ 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(), - }) - 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(), + Detail: orgErr.Error(), }) return } - req.OrganizationID = defaultOrg.ID } var loginType database.LoginType @@ -480,7 +484,7 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) { return } - user, _, err := api.CreateUser(ctx, api.Database, CreateUserRequest{ + user, err := api.CreateUser(ctx, api.Database, CreateUserRequest{ CreateUserRequest: req, LoginType: loginType, }) @@ -505,7 +509,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 @@ -1285,18 +1289,18 @@ type CreateUserRequest struct { 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 +1345,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 +1381,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. diff --git a/codersdk/users.go b/codersdk/users.go index a715194c11978..27ee93bc1e26b 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -123,8 +123,34 @@ type CreateUserRequest struct { // DisableLogin 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. - DisableLogin bool `json:"disable_login"` - OrganizationID uuid.UUID `json:"organization_id" validate:"" format:"uuid"` + DisableLogin bool `json:"disable_login"` + // 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. +func (r *CreateUserRequest) 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 CreateUserRequest + 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 = CreateUserRequest(dep.AliasedReq) + if dep.OrganizationID != nil { + r.OrganizationIDs = append(r.OrganizationIDs, *dep.OrganizationID) + } + return nil } type UpdateUserProfileRequest struct { diff --git a/codersdk/users_test.go b/codersdk/users_test.go new file mode 100644 index 0000000000000..04befed0e503d --- /dev/null +++ b/codersdk/users_test.go @@ -0,0 +1,92 @@ +package codersdk_test + +import ( + "encoding/json" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "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.CreateUserRequest + 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.CreateUserRequest + 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.CreateUserRequest + err := json.Unmarshal([]byte(input), &req) + require.NoError(t, err) + + require.Empty(t, req.OrganizationIDs) + }) +} diff --git a/enterprise/coderd/scim.go b/enterprise/coderd/scim.go index 9a803c51d9589..b02d3ca183a07 100644 --- a/enterprise/coderd/scim.go +++ b/enterprise/coderd/scim.go @@ -232,7 +232,7 @@ 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{ + dbUser, err = api.AGPL.CreateUser(dbauthz.AsSystemRestricted(ctx), api.Database, agpl.CreateUserRequest{ CreateUserRequest: codersdk.CreateUserRequest{ Username: sUser.UserName, Email: email, From ba45f5ee410b332be4ea22bebb0daac355905f91 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 21 Aug 2024 09:21:59 -0500 Subject: [PATCH 02/10] update comment --- codersdk/users.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/codersdk/users.go b/codersdk/users.go index 27ee93bc1e26b..e0363a24795a2 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -133,6 +133,9 @@ type CreateUserRequest struct { // 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 *CreateUserRequest) 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. From fa0409485cd2092b6e0f9d02540eefc4e9d0b8c1 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 21 Aug 2024 10:13:59 -0500 Subject: [PATCH 03/10] fixup tests to use slice of org ids --- cli/clitest/golden.go | 8 +- coderd/coderdtest/coderdtest.go | 23 ++-- coderd/userauth_test.go | 8 +- coderd/users_test.go | 188 ++++++++++++++++---------------- 4 files changed, 114 insertions(+), 113 deletions(-) diff --git a/cli/clitest/golden.go b/cli/clitest/golden.go index db0bbeb43874e..ce1b8d84f36cc 100644 --- a/cli/clitest/golden.go +++ b/cli/clitest/golden.go @@ -184,10 +184,10 @@ func prepareTestData(t *testing.T) (*codersdk.Client, map[string]string) { }) 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, + 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) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index afa2130212217..9cff87c456d96 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -646,11 +646,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:' 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...) + return createAnotherUserRetry(t, client, []uuid.UUID{organizationID}, 5, roles, mutators...) } // AuthzUserSubject does not include the user's groups. @@ -676,13 +676,13 @@ 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) { +func createAnotherUserRetry(t testing.TB, client *codersdk.Client, organizationIDs []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, + Email: namesgenerator.GetRandomName(10) + "@coder.com", + Username: RandomUsername(t), + Name: RandomName(t), + Password: "SomeSecurePassword!", + OrganizationIDs: organizationIDs, } for _, m := range mutators { m(&req) @@ -694,7 +694,7 @@ func createAnotherUserRetry(t testing.TB, client *codersdk.Client, organizationI 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) @@ -763,8 +763,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 diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go index 5519cfd599015..e6313a9a8d005 100644 --- a/coderd/userauth_test.go +++ b/coderd/userauth_test.go @@ -1471,10 +1471,10 @@ func TestUserLogout(t *testing.T) { password = "SomeSecurePassword123!" ) newUser, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ - Email: email, - Username: username, - Password: password, - OrganizationID: firstUser.OrganizationID, + Email: email, + Username: username, + Password: password, + OrganizationIDs: []uuid.UUID{firstUser.OrganizationID}, }) require.NoError(t, err) diff --git a/coderd/users_test.go b/coderd/users_test.go index 66eb2f8da1f94..0405f262758c0 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -221,10 +221,10 @@ 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, + Email: "test+user-@coder.com", + Username: "user", + Password: password, + OrganizationIDs: []uuid.UUID{first.OrganizationID}, }) require.NoError(t, err) @@ -318,10 +318,10 @@ func TestDeleteUser(t *testing.T) { 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, + Email: another.Email, + Username: another.Username, + Password: "SomeSecurePassword!", + OrganizationIDs: []uuid.UUID{user.OrganizationID}, }) require.NoError(t, err) err = client.DeleteUser(context.Background(), another.ID) @@ -495,10 +495,10 @@ func TestNotifyDeletedUser(t *testing.T) { defer cancel() user, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{ - OrganizationID: firstUser.OrganizationID, - Email: "another@user.org", - Username: "someone-else", - Password: "SomeSecurePassword!", + OrganizationIDs: []uuid.UUID{firstUser.OrganizationID}, + Email: "another@user.org", + Username: "someone-else", + Password: "SomeSecurePassword!", }) require.NoError(t, err) @@ -531,10 +531,10 @@ 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!", + OrganizationIDs: []uuid.UUID{firstUser.OrganizationID}, + Email: "another@user.org", + Username: "someone-else", + Password: "SomeSecurePassword!", }) require.NoError(t, err) @@ -640,10 +640,10 @@ 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(), + Email: me.Email, + Username: me.Username, + Password: "MySecurePassword!", + OrganizationIDs: []uuid.UUID{uuid.New()}, }) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) @@ -659,10 +659,10 @@ func TestPostUsers(t *testing.T) { defer cancel() _, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ - OrganizationID: uuid.New(), - Email: "another@user.org", - Username: "someone-else", - Password: "SomeSecurePassword!", + OrganizationIDs: []uuid.UUID{uuid.New()}, + Email: "another@user.org", + Username: "someone-else", + Password: "SomeSecurePassword!", }) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) @@ -683,10 +683,10 @@ func TestPostUsers(t *testing.T) { defer cancel() user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ - OrganizationID: firstUser.OrganizationID, - Email: "another@user.org", - Username: "someone-else", - Password: "SomeSecurePassword!", + OrganizationIDs: []uuid.UUID{firstUser.OrganizationID}, + Email: "another@user.org", + Username: "someone-else", + Password: "SomeSecurePassword!", }) require.NoError(t, err) @@ -736,11 +736,11 @@ func TestPostUsers(t *testing.T) { defer cancel() user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ - OrganizationID: first.OrganizationID, - Email: "another@user.org", - Username: "someone-else", - Password: "", - UserLoginType: codersdk.LoginTypeNone, + OrganizationIDs: []uuid.UUID{first.OrganizationID}, + Email: "another@user.org", + Username: "someone-else", + Password: "", + UserLoginType: codersdk.LoginTypeNone, }) require.NoError(t, err) @@ -768,11 +768,11 @@ func TestPostUsers(t *testing.T) { defer cancel() _, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ - OrganizationID: first.OrganizationID, - Email: email, - Username: "someone-else", - Password: "", - UserLoginType: codersdk.LoginTypeOIDC, + OrganizationIDs: []uuid.UUID{first.OrganizationID}, + Email: email, + Username: "someone-else", + Password: "", + UserLoginType: codersdk.LoginTypeOIDC, }) require.NoError(t, err) @@ -805,10 +805,10 @@ func TestNotifyCreatedUser(t *testing.T) { // when user, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{ - OrganizationID: firstUser.OrganizationID, - Email: "another@user.org", - Username: "someone-else", - Password: "SomeSecurePassword!", + OrganizationIDs: []uuid.UUID{firstUser.OrganizationID}, + Email: "another@user.org", + Username: "someone-else", + Password: "SomeSecurePassword!", }) require.NoError(t, err) @@ -834,10 +834,10 @@ func TestNotifyCreatedUser(t *testing.T) { defer cancel() userAdmin, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{ - OrganizationID: firstUser.OrganizationID, - Email: "user-admin@user.org", - Username: "mr-user-admin", - Password: "SomeSecurePassword!", + OrganizationIDs: []uuid.UUID{firstUser.OrganizationID}, + Email: "user-admin@user.org", + Username: "mr-user-admin", + Password: "SomeSecurePassword!", }) require.NoError(t, err) @@ -850,10 +850,10 @@ func TestNotifyCreatedUser(t *testing.T) { // when member, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{ - OrganizationID: firstUser.OrganizationID, - Email: "another@user.org", - Username: "someone-else", - Password: "SomeSecurePassword!", + OrganizationIDs: []uuid.UUID{firstUser.OrganizationID}, + Email: "another@user.org", + Username: "someone-else", + Password: "SomeSecurePassword!", }) require.NoError(t, err) @@ -909,10 +909,10 @@ func TestUpdateUserProfile(t *testing.T) { defer cancel() existentUser, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ - Email: "bruno@coder.com", - Username: "bruno", - Password: "SomeSecurePassword!", - OrganizationID: user.OrganizationID, + 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{ @@ -991,10 +991,10 @@ func TestUpdateUserProfile(t *testing.T) { defer cancel() _, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ - Email: "john@coder.com", - Username: "john", - Password: "SomeSecurePassword!", - OrganizationID: user.OrganizationID, + 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{ @@ -1033,10 +1033,10 @@ func TestUpdateUserPassword(t *testing.T) { defer cancel() member, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ - Email: "coder@coder.com", - Username: "coder", - Password: "SomeStrongPassword!", - OrganizationID: owner.OrganizationID, + 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{ @@ -1294,10 +1294,10 @@ func TestActivateDormantUser(t *testing.T) { 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, + Email: "coder@coder.com", + Username: "coder", + Password: "SomeStrongPassword!", + OrganizationIDs: []uuid.UUID{me.OrganizationID}, }) require.NoError(t, err) @@ -1601,10 +1601,10 @@ func TestGetUsers(t *testing.T) { defer cancel() client.CreateUser(ctx, codersdk.CreateUserRequest{ - Email: "alice@email.com", - Username: "alice", - Password: "MySecurePassword!", - OrganizationID: user.OrganizationID, + 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{}) @@ -1627,10 +1627,10 @@ func TestGetUsers(t *testing.T) { // Alice will be suspended alice, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ - Email: "alice@email.com", - Username: "alice", - Password: "MySecurePassword!", - OrganizationID: first.OrganizationID, + Email: "alice@email.com", + Username: "alice", + Password: "MySecurePassword!", + OrganizationIDs: []uuid.UUID{first.OrganizationID}, }) require.NoError(t, err) @@ -1639,10 +1639,10 @@ func TestGetUsers(t *testing.T) { // Tom will be active tom, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ - Email: "tom@email.com", - Username: "tom", - Password: "MySecurePassword!", - OrganizationID: first.OrganizationID, + Email: "tom@email.com", + Username: "tom", + Password: "MySecurePassword!", + OrganizationIDs: []uuid.UUID{first.OrganizationID}, }) require.NoError(t, err) @@ -1670,10 +1670,10 @@ func TestGetUsersPagination(t *testing.T) { require.NoError(t, err, "") _, err = client.CreateUser(ctx, codersdk.CreateUserRequest{ - Email: "alice@email.com", - Username: "alice", - Password: "MySecurePassword!", - OrganizationID: first.OrganizationID, + Email: "alice@email.com", + Username: "alice", + Password: "MySecurePassword!", + OrganizationIDs: []uuid.UUID{first.OrganizationID}, }) require.NoError(t, err) @@ -1751,10 +1751,10 @@ func TestWorkspacesByUser(t *testing.T) { defer cancel() newUser, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ - Email: "test@coder.com", - Username: "someone", - Password: "MySecurePassword!", - OrganizationID: user.OrganizationID, + Email: "test@coder.com", + Username: "someone", + Password: "MySecurePassword!", + OrganizationIDs: []uuid.UUID{user.OrganizationID}, }) require.NoError(t, err) auth, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{ @@ -1791,10 +1791,10 @@ func TestDormantUser(t *testing.T) { // Create a new user newUser, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ - Email: "test@coder.com", - Username: "someone", - Password: "MySecurePassword!", - OrganizationID: user.OrganizationID, + Email: "test@coder.com", + Username: "someone", + Password: "MySecurePassword!", + OrganizationIDs: []uuid.UUID{user.OrganizationID}, }) require.NoError(t, err) @@ -1842,10 +1842,10 @@ func TestSuspendedPagination(t *testing.T) { 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, + Email: email, + Username: username, + Password: "MySecurePassword!", + OrganizationIDs: []uuid.UUID{orgID}, }) require.NoError(t, err) users = append(users, user) From 0070ccabe7d70fa41533870a766986c4486a3fa9 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 21 Aug 2024 10:33:11 -0500 Subject: [PATCH 04/10] fixup tests --- cli/clitest/golden.go | 1 + cli/server_createadminuser.go | 10 +++---- cli/user_delete_test.go | 37 +++++++++++++------------ cli/usercreate.go | 13 +++++---- coderd/workspaceapps/apptest/apptest.go | 16 +++++------ enterprise/coderd/groups_test.go | 8 +++--- enterprise/coderd/scim.go | 6 ++-- enterprise/coderd/users_test.go | 26 ++++++++--------- scaletest/createworkspaces/run.go | 8 +++--- 9 files changed, 62 insertions(+), 63 deletions(-) diff --git a/cli/clitest/golden.go b/cli/clitest/golden.go index ce1b8d84f36cc..641d2e479d437 100644 --- a/cli/clitest/golden.go +++ b/cli/clitest/golden.go @@ -11,6 +11,7 @@ import ( "strings" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/cli/config" diff --git a/cli/server_createadminuser.go b/cli/server_createadminuser.go index 19326ba728ce6..6a980d4291bf1 100644 --- a/cli/server_createadminuser.go +++ b/cli/server_createadminuser.go @@ -84,11 +84,11 @@ 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(), + Username: "username", + Name: "Admin User", + Email: "email@coder.com", + Password: "ValidPa$$word123!", + OrganizationIDs: []uuid.UUID{uuid.New()}, } if username != "" { req.Username = username diff --git a/cli/user_delete_test.go b/cli/user_delete_test.go index 9ee546ca7a925..59a52e7f428e1 100644 --- a/cli/user_delete_test.go +++ b/cli/user_delete_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/cli/clitest" @@ -27,12 +28,12 @@ func TestUserDelete(t *testing.T) { 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, + Email: "colin5@coder.com", + Username: "coolin", + Password: pw, + UserLoginType: codersdk.LoginTypePassword, + OrganizationIDs: []uuid.UUID{owner.OrganizationID}, + DisableLogin: false, }) require.NoError(t, err) @@ -58,12 +59,12 @@ func TestUserDelete(t *testing.T) { 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, + Email: "colin5@coder.com", + Username: "coolin", + Password: pw, + UserLoginType: codersdk.LoginTypePassword, + OrganizationIDs: []uuid.UUID{owner.OrganizationID}, + DisableLogin: false, }) require.NoError(t, err) @@ -89,12 +90,12 @@ func TestUserDelete(t *testing.T) { 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, + Email: "colin5@coder.com", + Username: "coolin", + Password: pw, + UserLoginType: codersdk.LoginTypePassword, + OrganizationIDs: []uuid.UUID{owner.OrganizationID}, + DisableLogin: false, }) require.NoError(t, err) diff --git a/cli/usercreate.go b/cli/usercreate.go index 257bb1634f1d8..0fb642982cf82 100644 --- a/cli/usercreate.go +++ b/cli/usercreate.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/go-playground/validator/v10" + "github.com/google/uuid" "golang.org/x/xerrors" "github.com/coder/pretty" @@ -95,12 +96,12 @@ 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, + Email: email, + Username: username, + Name: name, + Password: password, + OrganizationIDs: []uuid.UUID{organization.ID}, + UserLoginType: userLoginType, }) if err != nil { return err diff --git a/coderd/workspaceapps/apptest/apptest.go b/coderd/workspaceapps/apptest/apptest.go index 3cd5e5a2f9935..33643f2299568 100644 --- a/coderd/workspaceapps/apptest/apptest.go +++ b/coderd/workspaceapps/apptest/apptest.go @@ -1207,10 +1207,10 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { // 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, + Email: "user@coder.com", + Username: "user", + Password: password, + OrganizationIDs: []uuid.UUID{appDetails.FirstUser.OrganizationID}, }) require.NoError(t, err) @@ -1259,10 +1259,10 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { }) 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, + Email: "no-template-access@coder.com", + Username: "no-template-access", + Password: password, + OrganizationIDs: []uuid.UUID{otherOrg.ID}, }) require.NoError(t, err) diff --git a/enterprise/coderd/groups_test.go b/enterprise/coderd/groups_test.go index 986b308b86fef..0c530114a98a9 100644 --- a/enterprise/coderd/groups_test.go +++ b/enterprise/coderd/groups_test.go @@ -758,10 +758,10 @@ func TestGroup(t *testing.T) { // 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, + Email: "coder@coder.com", + Username: "coder", + Password: "SomeStrongPassword!", + OrganizationIDs: []uuid.UUID{user.OrganizationID}, }) require.NoError(t, err) diff --git a/enterprise/coderd/scim.go b/enterprise/coderd/scim.go index b02d3ca183a07..b4eb64f1b7f8c 100644 --- a/enterprise/coderd/scim.go +++ b/enterprise/coderd/scim.go @@ -234,9 +234,9 @@ 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, + 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. diff --git a/enterprise/coderd/users_test.go b/enterprise/coderd/users_test.go index eb0f23b278d92..e975865dead51 100644 --- a/enterprise/coderd/users_test.go +++ b/enterprise/coderd/users_test.go @@ -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" @@ -510,10 +509,10 @@ func TestEnterprisePostUser(t *testing.T) { }) _, err := notInOrg.CreateUser(ctx, codersdk.CreateUserRequest{ - Email: "some@domain.com", - Username: "anotheruser", - Password: "SomeSecurePassword!", - OrganizationID: org.ID, + Email: "some@domain.com", + Username: "anotheruser", + Password: "SomeSecurePassword!", + OrganizationIDs: []uuid.UUID{org.ID}, }) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) @@ -544,10 +543,10 @@ 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, + 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,11 @@ 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.CreateUser(ctx, codersdk.CreateUserRequest{ Email: "another@user.org", Username: "someone-else", Password: "SomeSecurePassword!", }) - require.NoError(t, err) - - require.Len(t, user.OrganizationIDs, 1) - assert.Equal(t, firstUser.OrganizationID, user.OrganizationIDs[0]) + require.ErrorContains(t, err, "No organization specified") }) } diff --git a/scaletest/createworkspaces/run.go b/scaletest/createworkspaces/run.go index 6793475012194..5d67ec28858f7 100644 --- a/scaletest/createworkspaces/run.go +++ b/scaletest/createworkspaces/run.go @@ -73,10 +73,10 @@ 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, + 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) From 5d0d2baf71db24196ab2a2104a2ada7d0df60f70 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 21 Aug 2024 10:38:23 -0500 Subject: [PATCH 05/10] make gen --- coderd/apidoc/docs.go | 10 ++++-- coderd/apidoc/swagger.json | 10 ++++-- codersdk/users_test.go | 60 ++++++++++++++++++++++++++++++++++ docs/reference/api/schemas.md | 20 ++++++------ docs/reference/api/users.md | 2 +- site/src/api/typesGenerated.ts | 2 +- 6 files changed, 86 insertions(+), 18 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 58196165db9ce..8ecc25b14796e 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -9475,9 +9475,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" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 8e53c856296fc..20552b6fe31a2 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -8436,9 +8436,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" diff --git a/codersdk/users_test.go b/codersdk/users_test.go index 04befed0e503d..52dfd296f3c4e 100644 --- a/codersdk/users_test.go +++ b/codersdk/users_test.go @@ -7,6 +7,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/require" + "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/codersdk" ) @@ -90,3 +91,62 @@ func TestDeprecatedCreateUserRequest(t *testing.T) { require.Empty(t, req.OrganizationIDs) }) } + +func TestCreateUserRequestJSON(t *testing.T) { + t.Parallel() + + marshalTest := func(t *testing.T, req codersdk.CreateUserRequest) { + t.Helper() + data, err := json.Marshal(req) + require.NoError(t, err) + var req2 codersdk.CreateUserRequest + 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.CreateUserRequest{ + Email: coderdtest.RandomName(t), + Username: coderdtest.RandomName(t), + Name: coderdtest.RandomName(t), + Password: "", + UserLoginType: codersdk.LoginTypePassword, + DisableLogin: false, + OrganizationIDs: []uuid.UUID{uuid.New(), uuid.New()}, + } + marshalTest(t, req) + }) + + t.Run("SingleOrganization", func(t *testing.T) { + t.Parallel() + + req := codersdk.CreateUserRequest{ + Email: coderdtest.RandomName(t), + Username: coderdtest.RandomName(t), + Name: coderdtest.RandomName(t), + Password: "", + UserLoginType: codersdk.LoginTypePassword, + DisableLogin: false, + OrganizationIDs: []uuid.UUID{uuid.New()}, + } + marshalTest(t, req) + }) + + t.Run("NoOrganization", func(t *testing.T) { + t.Parallel() + + req := codersdk.CreateUserRequest{ + Email: coderdtest.RandomName(t), + Username: coderdtest.RandomName(t), + Name: coderdtest.RandomName(t), + Password: "", + UserLoginType: codersdk.LoginTypePassword, + DisableLogin: false, + OrganizationIDs: []uuid.UUID{}, + } + marshalTest(t, req) + }) +} diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 9f9188ced1761..6b62faee8382d 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -1292,7 +1292,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "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 +1300,15 @@ 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 | +| ------------------ | ---------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `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_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 diff --git a/docs/reference/api/users.md b/docs/reference/api/users.md index 2cca07030cfd1..1377a658842cb 100644 --- a/docs/reference/api/users.md +++ b/docs/reference/api/users.md @@ -85,7 +85,7 @@ curl -X POST http://coder-server:8080/api/v2/users \ "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" } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 1186f26ad90c8..670443e9897be 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -310,7 +310,7 @@ export interface CreateUserRequest { readonly password: string; readonly login_type: LoginType; readonly disable_login: boolean; - readonly organization_id: string; + readonly organization_ids: Readonly>; } // From codersdk/workspaces.go From 6dc62a5528b65a3b535065c27acf11ed634699a8 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 21 Aug 2024 10:39:12 -0500 Subject: [PATCH 06/10] fixup ui --- site/src/pages/CreateUserPage/CreateUserForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/CreateUserPage/CreateUserForm.tsx b/site/src/pages/CreateUserPage/CreateUserForm.tsx index 9d780f7355177..114fb81179882 100644 --- a/site/src/pages/CreateUserPage/CreateUserForm.tsx +++ b/site/src/pages/CreateUserPage/CreateUserForm.tsx @@ -93,7 +93,7 @@ export const CreateUserForm: FC< password: "", username: "", name: "", - organization_id: "00000000-0000-0000-0000-000000000000", + organization_ids: ["00000000-0000-0000-0000-000000000000"], disable_login: false, login_type: "", }, From cb6281313a1f3809ff453a87b56b266b321b43af Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 21 Aug 2024 10:52:58 -0500 Subject: [PATCH 07/10] linting and formatting --- coderd/users.go | 5 ++--- site/e2e/api.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/coderd/users.go b/coderd/users.go index e66d8808139b7..3a72e9ea76c38 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -388,8 +388,8 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) { if len(req.OrganizationIDs) == 0 { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: fmt.Sprintf("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: fmt.Sprintf("required at least 1 value for the array 'organization_ids'"), + 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", @@ -447,7 +447,6 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) { }) return } - } var loginType database.LoginType diff --git a/site/e2e/api.ts b/site/e2e/api.ts index 3f274a3c01910..4052510a00761 100644 --- a/site/e2e/api.ts +++ b/site/e2e/api.ts @@ -37,7 +37,7 @@ export const createUser = async (orgId: string) => { password: "s3cure&password!", login_type: "password", disable_login: false, - organization_id: orgId, + organization_ids: [orgId], }); return user; }; From a8fcd4274708c905993f396acafa3c81654e1b9c Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 21 Aug 2024 13:12:33 -0500 Subject: [PATCH 08/10] add unit test to add multiple users at once --- enterprise/coderd/users_test.go | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/enterprise/coderd/users_test.go b/enterprise/coderd/users_test.go index e975865dead51..b8c5b786bc5c5 100644 --- a/enterprise/coderd/users_test.go +++ b/enterprise/coderd/users_test.go @@ -584,4 +584,46 @@ func TestEnterprisePostUser(t *testing.T) { }) 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.CreateUser(ctx, codersdk.CreateUserRequest{ + Email: "another@user.org", + Username: "someone-else", + Password: "SomeSecurePassword!", + OrganizationIDs: []uuid.UUID{ + second.ID, + third.ID, + }, + }) + require.NoError(t, err) + + 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}) + }) } From e68f18d4fce1d4cfc35d4964ca603bf2f3075e56 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 22 Aug 2024 16:16:02 -0500 Subject: [PATCH 09/10] fixup test --- coderd/users_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/coderd/users_test.go b/coderd/users_test.go index 0405f262758c0..5cd4c08bc7962 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -416,10 +416,10 @@ 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!", + OrganizationIDs: []uuid.UUID{firstUser.OrganizationID}, + Email: "another@user.org", + Username: "someone-else", + Password: "SomeSecurePassword!", }) require.NoError(t, err) @@ -453,10 +453,10 @@ 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!", + OrganizationIDs: []uuid.UUID{firstUser.OrganizationID}, + Email: "another@user.org", + Username: "someone-else", + Password: "SomeSecurePassword!", }) require.NoError(t, err) From 4c93b3ba3caccffa30b2864fd3f4d7def086d879 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 23 Aug 2024 15:58:18 -0500 Subject: [PATCH 10/10] chore: safer SDK deprecation (#14425) * Handle sdk deprecation better by maintaining cli functions --- cli/clitest/golden.go | 2 +- cli/schedule_test.go | 2 +- cli/server_createadminuser.go | 2 +- cli/user_delete_test.go | 12 ++--- cli/usercreate.go | 2 +- coderd/apidoc/docs.go | 8 +-- coderd/apidoc/swagger.json | 8 +-- coderd/coderdtest/coderdtest.go | 10 ++-- coderd/insights_test.go | 2 +- coderd/userauth.go | 2 +- coderd/userauth_test.go | 20 +------ coderd/users.go | 16 +++--- coderd/users_test.go | 52 +++++++++---------- coderd/workspaceapps/apptest/apptest.go | 4 +- coderd/workspaces_test.go | 2 +- codersdk/users.go | 43 +++++++++++++-- codersdk/users_test.go | 19 +++---- docs/reference/api/schemas.md | 20 ++++--- docs/reference/api/users.md | 7 ++- enterprise/coderd/groups_test.go | 2 +- enterprise/coderd/scim.go | 2 +- enterprise/coderd/userauth_test.go | 4 +- enterprise/coderd/users_test.go | 8 +-- scaletest/createworkspaces/run.go | 2 +- site/e2e/api.ts | 1 - site/src/api/api.ts | 2 +- site/src/api/typesGenerated.ts | 3 +- .../pages/CreateUserPage/CreateUserForm.tsx | 9 ++-- 28 files changed, 129 insertions(+), 137 deletions(-) diff --git a/cli/clitest/golden.go b/cli/clitest/golden.go index 641d2e479d437..5e154e6087e6f 100644 --- a/cli/clitest/golden.go +++ b/cli/clitest/golden.go @@ -184,7 +184,7 @@ func prepareTestData(t *testing.T) (*codersdk.Client, map[string]string) { IncludeProvisionerDaemon: true, }) firstUser := coderdtest.CreateFirstUser(t, rootClient) - secondUser, err := rootClient.CreateUser(ctx, codersdk.CreateUserRequest{ + secondUser, err := rootClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "testuser2@coder.com", Username: "testuser2", Password: coderdtest.FirstUserParams.Password, diff --git a/cli/schedule_test.go b/cli/schedule_test.go index 9ed44de9e467f..11e0171417c04 100644 --- a/cli/schedule_test.go +++ b/cli/schedule_test.go @@ -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{ diff --git a/cli/server_createadminuser.go b/cli/server_createadminuser.go index 6a980d4291bf1..01eb56a83b7e8 100644 --- a/cli/server_createadminuser.go +++ b/cli/server_createadminuser.go @@ -83,7 +83,7 @@ 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{ + req := codersdk.CreateUserRequestWithOrgs{ Username: "username", Name: "Admin User", Email: "email@coder.com", diff --git a/cli/user_delete_test.go b/cli/user_delete_test.go index 59a52e7f428e1..e07d1e850e24d 100644 --- a/cli/user_delete_test.go +++ b/cli/user_delete_test.go @@ -27,13 +27,12 @@ func TestUserDelete(t *testing.T) { pw, err := cryptorand.String(16) require.NoError(t, err) - _, err = client.CreateUser(ctx, codersdk.CreateUserRequest{ + _, err = client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "colin5@coder.com", Username: "coolin", Password: pw, UserLoginType: codersdk.LoginTypePassword, OrganizationIDs: []uuid.UUID{owner.OrganizationID}, - DisableLogin: false, }) require.NoError(t, err) @@ -58,13 +57,12 @@ func TestUserDelete(t *testing.T) { pw, err := cryptorand.String(16) require.NoError(t, err) - user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "colin5@coder.com", Username: "coolin", Password: pw, UserLoginType: codersdk.LoginTypePassword, OrganizationIDs: []uuid.UUID{owner.OrganizationID}, - DisableLogin: false, }) require.NoError(t, err) @@ -89,13 +87,12 @@ func TestUserDelete(t *testing.T) { pw, err := cryptorand.String(16) require.NoError(t, err) - user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "colin5@coder.com", Username: "coolin", Password: pw, UserLoginType: codersdk.LoginTypePassword, OrganizationIDs: []uuid.UUID{owner.OrganizationID}, - DisableLogin: false, }) require.NoError(t, err) @@ -122,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) diff --git a/cli/usercreate.go b/cli/usercreate.go index 0fb642982cf82..78bb396916926 100644 --- a/cli/usercreate.go +++ b/cli/usercreate.go @@ -95,7 +95,7 @@ func (r *RootCmd) userCreate() *serpent.Command { } } - _, err = client.CreateUser(inv.Context(), codersdk.CreateUserRequest{ + _, err = client.CreateUserWithOrgs(inv.Context(), codersdk.CreateUserRequestWithOrgs{ Email: email, Username: username, Name: name, diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 8ecc25b14796e..d0527df61bed3 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -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" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 20552b6fe31a2..79fe02c6291d3 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -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" diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 9cff87c456d96..c38dd54d1d04d 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -649,7 +649,7 @@ func CreateAnotherUser(t testing.TB, client *codersdk.Client, organizationID uui 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) { +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...) } @@ -676,8 +676,8 @@ func AuthzUserSubject(user codersdk.User, orgID uuid.UUID) rbac.Subject { } } -func createAnotherUserRetry(t testing.TB, client *codersdk.Client, organizationIDs []uuid.UUID, retries int, roles []rbac.RoleIdentifier, mutators ...func(r *codersdk.CreateUserRequest)) (*codersdk.Client, codersdk.User) { - req := codersdk.CreateUserRequest{ +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), @@ -688,7 +688,7 @@ func createAnotherUserRetry(t testing.TB, client *codersdk.Client, organizationI 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) { @@ -700,7 +700,7 @@ func createAnotherUserRetry(t testing.TB, client *codersdk.Client, organizationI 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{ diff --git a/coderd/insights_test.go b/coderd/insights_test.go index 3c7c70e8c6743..40daed5d0ce02 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -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})) diff --git a/coderd/userauth.go b/coderd/userauth.go index df1e553bfa753..1a5488d2d6ded 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -1437,7 +1437,7 @@ 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{ + CreateUserRequestWithOrgs: codersdk.CreateUserRequestWithOrgs{ Email: params.Email, Username: params.Username, OrganizationIDs: []uuid.UUID{defaultOrganization.ID}, diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go index e6313a9a8d005..8603cfcfb439a 100644 --- a/coderd/userauth_test.go +++ b/coderd/userauth_test.go @@ -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,7 +1454,7 @@ func TestUserLogout(t *testing.T) { //nolint:gosec password = "SomeSecurePassword123!" ) - newUser, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + newUser, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: email, Username: username, Password: password, diff --git a/coderd/users.go b/coderd/users.go index 3a72e9ea76c38..3ae2d916e1de8 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -187,7 +187,7 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) { //nolint:gocritic // needed to create first user user, err := api.CreateUser(dbauthz.AsSystemRestricted(ctx), api.Database, CreateUserRequest{ - CreateUserRequest: codersdk.CreateUserRequest{ + CreateUserRequestWithOrgs: codersdk.CreateUserRequestWithOrgs{ Email: createUser.Email, Username: createUser.Username, Name: createUser.Name, @@ -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 @@ -484,8 +480,8 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) { } user, err := api.CreateUser(ctx, api.Database, CreateUserRequest{ - CreateUserRequest: req, - LoginType: loginType, + CreateUserRequestWithOrgs: req, + LoginType: loginType, }) if dbauthz.IsNotAuthorizedError(err) { httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ @@ -1283,7 +1279,7 @@ func (api *API) organizationByUserAndName(rw http.ResponseWriter, r *http.Reques } type CreateUserRequest struct { - codersdk.CreateUserRequest + codersdk.CreateUserRequestWithOrgs LoginType database.LoginType SkipNotifications bool } diff --git a/coderd/users_test.go b/coderd/users_test.go index 5cd4c08bc7962..622e2da54c3bc 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -220,7 +220,7 @@ func TestPostLogin(t *testing.T) { // With a user account. const password = "SomeSecurePassword!" - user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "test+user-@coder.com", Username: "user", Password: password, @@ -317,7 +317,7 @@ 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{ + another, err = client.CreateUserWithOrgs(context.Background(), codersdk.CreateUserRequestWithOrgs{ Email: another.Email, Username: another.Username, Password: "SomeSecurePassword!", @@ -415,7 +415,7 @@ func TestNotifyUserStatusChanged(t *testing.T) { _, userAdmin := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID, rbac.RoleUserAdmin()) - member, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{ + member, err := adminClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ OrganizationIDs: []uuid.UUID{firstUser.OrganizationID}, Email: "another@user.org", Username: "someone-else", @@ -452,7 +452,7 @@ func TestNotifyUserStatusChanged(t *testing.T) { _, userAdmin := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID, rbac.RoleUserAdmin()) - member, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{ + member, err := adminClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ OrganizationIDs: []uuid.UUID{firstUser.OrganizationID}, Email: "another@user.org", Username: "someone-else", @@ -494,7 +494,7 @@ func TestNotifyDeletedUser(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - user, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{ + user, err := adminClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ OrganizationIDs: []uuid.UUID{firstUser.OrganizationID}, Email: "another@user.org", Username: "someone-else", @@ -530,7 +530,7 @@ func TestNotifyDeletedUser(t *testing.T) { _, userAdmin := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID, rbac.RoleUserAdmin()) - member, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{ + member, err := adminClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ OrganizationIDs: []uuid.UUID{firstUser.OrganizationID}, Email: "another@user.org", Username: "someone-else", @@ -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,7 +639,7 @@ func TestPostUsers(t *testing.T) { me, err := client.User(ctx, codersdk.Me) require.NoError(t, err) - _, err = client.CreateUser(ctx, codersdk.CreateUserRequest{ + _, err = client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: me.Email, Username: me.Username, Password: "MySecurePassword!", @@ -658,7 +658,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{ OrganizationIDs: []uuid.UUID{uuid.New()}, Email: "another@user.org", Username: "someone-else", @@ -682,7 +682,7 @@ func TestPostUsers(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ OrganizationIDs: []uuid.UUID{firstUser.OrganizationID}, Email: "another@user.org", Username: "someone-else", @@ -735,7 +735,7 @@ func TestPostUsers(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ OrganizationIDs: []uuid.UUID{first.OrganizationID}, Email: "another@user.org", Username: "someone-else", @@ -767,7 +767,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{ OrganizationIDs: []uuid.UUID{first.OrganizationID}, Email: email, Username: "someone-else", @@ -804,7 +804,7 @@ func TestNotifyCreatedUser(t *testing.T) { defer cancel() // when - user, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{ + user, err := adminClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ OrganizationIDs: []uuid.UUID{firstUser.OrganizationID}, Email: "another@user.org", Username: "someone-else", @@ -833,7 +833,7 @@ func TestNotifyCreatedUser(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - userAdmin, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{ + userAdmin, err := adminClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ OrganizationIDs: []uuid.UUID{firstUser.OrganizationID}, Email: "user-admin@user.org", Username: "mr-user-admin", @@ -849,7 +849,7 @@ func TestNotifyCreatedUser(t *testing.T) { require.NoError(t, err) // when - member, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{ + member, err := adminClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ OrganizationIDs: []uuid.UUID{firstUser.OrganizationID}, Email: "another@user.org", Username: "someone-else", @@ -908,7 +908,7 @@ func TestUpdateUserProfile(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - existentUser, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + existentUser, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "bruno@coder.com", Username: "bruno", Password: "SomeSecurePassword!", @@ -990,7 +990,7 @@ func TestUpdateUserProfile(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{ Email: "john@coder.com", Username: "john", Password: "SomeSecurePassword!", @@ -1032,7 +1032,7 @@ func TestUpdateUserPassword(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - member, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + member, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "coder@coder.com", Username: "coder", Password: "SomeStrongPassword!", @@ -1293,7 +1293,7 @@ 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{ + anotherUser, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "coder@coder.com", Username: "coder", Password: "SomeStrongPassword!", @@ -1600,7 +1600,7 @@ func TestGetUsers(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - client.CreateUser(ctx, codersdk.CreateUserRequest{ + client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "alice@email.com", Username: "alice", Password: "MySecurePassword!", @@ -1626,7 +1626,7 @@ func TestGetUsers(t *testing.T) { active = append(active, firstUser) // Alice will be suspended - alice, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + alice, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "alice@email.com", Username: "alice", Password: "MySecurePassword!", @@ -1638,7 +1638,7 @@ func TestGetUsers(t *testing.T) { require.NoError(t, err) // Tom will be active - tom, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + tom, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "tom@email.com", Username: "tom", Password: "MySecurePassword!", @@ -1669,7 +1669,7 @@ func TestGetUsersPagination(t *testing.T) { _, err := client.User(ctx, first.UserID.String()) require.NoError(t, err, "") - _, err = client.CreateUser(ctx, codersdk.CreateUserRequest{ + _, err = client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "alice@email.com", Username: "alice", Password: "MySecurePassword!", @@ -1750,7 +1750,7 @@ func TestWorkspacesByUser(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - newUser, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + newUser, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "test@coder.com", Username: "someone", Password: "MySecurePassword!", @@ -1790,7 +1790,7 @@ func TestDormantUser(t *testing.T) { defer cancel() // Create a new user - newUser, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + newUser, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "test@coder.com", Username: "someone", Password: "MySecurePassword!", @@ -1841,7 +1841,7 @@ 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{ + user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: email, Username: username, Password: "MySecurePassword!", diff --git a/coderd/workspaceapps/apptest/apptest.go b/coderd/workspaceapps/apptest/apptest.go index 33643f2299568..14adf2d61d362 100644 --- a/coderd/workspaceapps/apptest/apptest.go +++ b/coderd/workspaceapps/apptest/apptest.go @@ -1206,7 +1206,7 @@ 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{ + user, err := ownerClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "user@coder.com", Username: "user", Password: password, @@ -1258,7 +1258,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { Name: "a-different-org", }) require.NoError(t, err) - userInOtherOrg, err := ownerClient.CreateUser(ctx, codersdk.CreateUserRequest{ + userInOtherOrg, err := ownerClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "no-template-access@coder.com", Username: "no-template-access", Password: password, diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index ec7c03dd53013..98f36c3b9a13e 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -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" }) diff --git a/codersdk/users.go b/codersdk/users.go index e0363a24795a2..e35803abeb15e 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -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"` @@ -123,7 +128,17 @@ type CreateUserRequest struct { // DisableLogin 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. - DisableLogin bool `json:"disable_login"` + DisableLogin bool `json:"disable_login"` + 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"` } @@ -136,10 +151,10 @@ type CreateUserRequest struct { // 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 *CreateUserRequest) UnmarshalJSON(data []byte) error { +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 CreateUserRequest + type AliasedReq CreateUserRequestWithOrgs type DeprecatedCreateUserRequest struct { AliasedReq OrganizationID *uuid.UUID `json:"organization_id" format:"uuid"` @@ -149,7 +164,7 @@ func (r *CreateUserRequest) UnmarshalJSON(data []byte) error { if err != nil { return err } - *r = CreateUserRequest(dep.AliasedReq) + *r = CreateUserRequestWithOrgs(dep.AliasedReq) if dep.OrganizationID != nil { r.OrganizationIDs = append(r.OrganizationIDs, *dep.OrganizationID) } @@ -317,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 diff --git a/codersdk/users_test.go b/codersdk/users_test.go index 52dfd296f3c4e..f1c691323bffd 100644 --- a/codersdk/users_test.go +++ b/codersdk/users_test.go @@ -28,7 +28,7 @@ func TestDeprecatedCreateUserRequest(t *testing.T) { "login_type":"none" } ` - var req codersdk.CreateUserRequest + var req codersdk.CreateUserRequestWithOrgs err := json.Unmarshal([]byte(input), &req) require.NoError(t, err) require.Equal(t, req.Email, "alice@coder.com") @@ -54,7 +54,7 @@ func TestDeprecatedCreateUserRequest(t *testing.T) { "login_type":"none" } ` - var req codersdk.CreateUserRequest + var req codersdk.CreateUserRequestWithOrgs err := json.Unmarshal([]byte(input), &req) require.NoError(t, err) require.Equal(t, req.Email, "alice@coder.com") @@ -84,7 +84,7 @@ func TestDeprecatedCreateUserRequest(t *testing.T) { "login_type":"none" } ` - var req codersdk.CreateUserRequest + var req codersdk.CreateUserRequestWithOrgs err := json.Unmarshal([]byte(input), &req) require.NoError(t, err) @@ -95,11 +95,11 @@ func TestDeprecatedCreateUserRequest(t *testing.T) { func TestCreateUserRequestJSON(t *testing.T) { t.Parallel() - marshalTest := func(t *testing.T, req codersdk.CreateUserRequest) { + marshalTest := func(t *testing.T, req codersdk.CreateUserRequestWithOrgs) { t.Helper() data, err := json.Marshal(req) require.NoError(t, err) - var req2 codersdk.CreateUserRequest + var req2 codersdk.CreateUserRequestWithOrgs err = json.Unmarshal(data, &req2) require.NoError(t, err) require.Equal(t, req, req2) @@ -108,13 +108,12 @@ func TestCreateUserRequestJSON(t *testing.T) { t.Run("MultipleOrganizations", func(t *testing.T) { t.Parallel() - req := codersdk.CreateUserRequest{ + req := codersdk.CreateUserRequestWithOrgs{ Email: coderdtest.RandomName(t), Username: coderdtest.RandomName(t), Name: coderdtest.RandomName(t), Password: "", UserLoginType: codersdk.LoginTypePassword, - DisableLogin: false, OrganizationIDs: []uuid.UUID{uuid.New(), uuid.New()}, } marshalTest(t, req) @@ -123,13 +122,12 @@ func TestCreateUserRequestJSON(t *testing.T) { t.Run("SingleOrganization", func(t *testing.T) { t.Parallel() - req := codersdk.CreateUserRequest{ + req := codersdk.CreateUserRequestWithOrgs{ Email: coderdtest.RandomName(t), Username: coderdtest.RandomName(t), Name: coderdtest.RandomName(t), Password: "", UserLoginType: codersdk.LoginTypePassword, - DisableLogin: false, OrganizationIDs: []uuid.UUID{uuid.New()}, } marshalTest(t, req) @@ -138,13 +136,12 @@ func TestCreateUserRequestJSON(t *testing.T) { t.Run("NoOrganization", func(t *testing.T) { t.Parallel() - req := codersdk.CreateUserRequest{ + req := codersdk.CreateUserRequestWithOrgs{ Email: coderdtest.RandomName(t), Username: coderdtest.RandomName(t), Name: coderdtest.RandomName(t), Password: "", UserLoginType: codersdk.LoginTypePassword, - DisableLogin: false, OrganizationIDs: []uuid.UUID{}, } marshalTest(t, req) diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 6b62faee8382d..e3a09df883001 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -1284,11 +1284,10 @@ 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", @@ -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_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 | | | +| 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 diff --git a/docs/reference/api/users.md b/docs/reference/api/users.md index 1377a658842cb..3979f5521b377 100644 --- a/docs/reference/api/users.md +++ b/docs/reference/api/users.md @@ -81,7 +81,6 @@ curl -X POST http://coder-server:8080/api/v2/users \ ```json { - "disable_login": true, "email": "user@example.com", "login_type": "", "name": "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 diff --git a/enterprise/coderd/groups_test.go b/enterprise/coderd/groups_test.go index 0c530114a98a9..1bd5bb8249b58 100644 --- a/enterprise/coderd/groups_test.go +++ b/enterprise/coderd/groups_test.go @@ -757,7 +757,7 @@ 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{ + anotherUser, err := userAdminClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "coder@coder.com", Username: "coder", Password: "SomeStrongPassword!", diff --git a/enterprise/coderd/scim.go b/enterprise/coderd/scim.go index b4eb64f1b7f8c..9ac9307ce2d12 100644 --- a/enterprise/coderd/scim.go +++ b/enterprise/coderd/scim.go @@ -233,7 +233,7 @@ 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{ + CreateUserRequestWithOrgs: codersdk.CreateUserRequestWithOrgs{ Username: sUser.UserName, Email: email, OrganizationIDs: []uuid.UUID{defaultOrganization.ID}, diff --git a/enterprise/coderd/userauth_test.go b/enterprise/coderd/userauth_test.go index 1f62235e5eb41..e5ca9c9180290 100644 --- a/enterprise/coderd/userauth_test.go +++ b/enterprise/coderd/userauth_test.go @@ -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 }) diff --git a/enterprise/coderd/users_test.go b/enterprise/coderd/users_test.go index b8c5b786bc5c5..54f2c8d0d3460 100644 --- a/enterprise/coderd/users_test.go +++ b/enterprise/coderd/users_test.go @@ -508,7 +508,7 @@ func TestEnterprisePostUser(t *testing.T) { request.Name = "another" }) - _, err := notInOrg.CreateUser(ctx, codersdk.CreateUserRequest{ + _, err := notInOrg.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "some@domain.com", Username: "anotheruser", Password: "SomeSecurePassword!", @@ -542,7 +542,7 @@ func TestEnterprisePostUser(t *testing.T) { org := coderdenttest.CreateOrganization(t, other, coderdenttest.CreateOrganizationOptions{}) - _, err := notInOrg.CreateUser(ctx, codersdk.CreateUserRequest{ + _, err := notInOrg.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "some@domain.com", Username: "anotheruser", Password: "SomeSecurePassword!", @@ -577,7 +577,7 @@ func TestEnterprisePostUser(t *testing.T) { // nolint:gocritic // intentional using the owner. // Manually making a user with the request instead of the coderdtest util - _, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + _, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "another@user.org", Username: "someone-else", Password: "SomeSecurePassword!", @@ -610,7 +610,7 @@ 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{ + user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "another@user.org", Username: "someone-else", Password: "SomeSecurePassword!", diff --git a/scaletest/createworkspaces/run.go b/scaletest/createworkspaces/run.go index 5d67ec28858f7..b31091f4984a1 100644 --- a/scaletest/createworkspaces/run.go +++ b/scaletest/createworkspaces/run.go @@ -72,7 +72,7 @@ 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{ + 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, diff --git a/site/e2e/api.ts b/site/e2e/api.ts index 4052510a00761..da5a57dee007d 100644 --- a/site/e2e/api.ts +++ b/site/e2e/api.ts @@ -36,7 +36,6 @@ export const createUser = async (orgId: string) => { name: name, password: "s3cure&password!", login_type: "password", - disable_login: false, organization_ids: [orgId], }); return user; diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 456607bf3d6cb..4c4abdc5b75c9 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1130,7 +1130,7 @@ class ApiMethods { }; createUser = async ( - user: TypesGen.CreateUserRequest, + user: TypesGen.CreateUserRequestWithOrgs, ): Promise => { const response = await this.axios.post( "/api/v2/users", diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 670443e9897be..5a27898ec3068 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -303,13 +303,12 @@ 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_ids: Readonly>; } diff --git a/site/src/pages/CreateUserPage/CreateUserForm.tsx b/site/src/pages/CreateUserPage/CreateUserForm.tsx index 114fb81179882..635c26387c00c 100644 --- a/site/src/pages/CreateUserPage/CreateUserForm.tsx +++ b/site/src/pages/CreateUserPage/CreateUserForm.tsx @@ -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 > = ({ onSubmit, onCancel, error, isLoading, authMethods }) => { - const form: FormikContextType = - useFormik({ + const form: FormikContextType = + useFormik({ initialValues: { email: "", password: "", username: "", name: "", organization_ids: ["00000000-0000-0000-0000-000000000000"], - disable_login: false, login_type: "", }, validationSchema, onSubmit, }); - const getFieldHelpers = getFormHelpers( + const getFieldHelpers = getFormHelpers( form, error, );