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

* chore: allow CreateUser to accept multiple organizations

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

* Handle sdk deprecation better by maintaining cli functions
This commit is contained in:
Steven Masley
2024-08-23 21:23:51 +00:00
committed by GitHub
parent af125c3795
commit c8eacc6df7
28 changed files with 597 additions and 367 deletions
+66 -63
View File
@@ -186,13 +186,13 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
}
//nolint:gocritic // needed to create first user
user, organizationID, err := api.CreateUser(dbauthz.AsSystemRestricted(ctx), api.Database, CreateUserRequest{
CreateUserRequest: codersdk.CreateUserRequest{
Email: createUser.Email,
Username: createUser.Username,
Name: createUser.Name,
Password: createUser.Password,
OrganizationID: defaultOrg.ID,
user, err := api.CreateUser(dbauthz.AsSystemRestricted(ctx), api.Database, CreateUserRequest{
CreateUserRequestWithOrgs: codersdk.CreateUserRequestWithOrgs{
Email: createUser.Email,
Username: createUser.Username,
Name: createUser.Name,
Password: createUser.Password,
OrganizationIDs: []uuid.UUID{defaultOrg.ID},
},
LoginType: database.LoginTypePassword,
})
@@ -240,7 +240,7 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusCreated, codersdk.CreateFirstUserResponse{
UserID: user.ID,
OrganizationID: organizationID,
OrganizationID: defaultOrg.ID,
})
}
@@ -342,7 +342,7 @@ func (api *API) GetUsers(rw http.ResponseWriter, r *http.Request) ([]database.Us
// @Accept json
// @Produce json
// @Tags Users
// @Param request body codersdk.CreateUserRequest true "Create user request"
// @Param request body codersdk.CreateUserRequestWithOrgs true "Create user request"
// @Success 201 {object} codersdk.User
// @Router /users [post]
func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
@@ -356,15 +356,11 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
})
defer commitAudit()
var req codersdk.CreateUserRequest
var req codersdk.CreateUserRequestWithOrgs
if !httpapi.Read(ctx, rw, r, &req) {
return
}
if req.UserLoginType == "" && req.DisableLogin {
// Handle the deprecated field
req.UserLoginType = codersdk.LoginTypeNone
}
if req.UserLoginType == "" {
// Default to password auth
req.UserLoginType = codersdk.LoginTypePassword
@@ -386,6 +382,20 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
return
}
if len(req.OrganizationIDs) == 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "No organization specified to place the user as a member of. It is required to specify at least one organization id to place the user in.",
Detail: "required at least 1 value for the array 'organization_ids'",
Validations: []codersdk.ValidationError{
{
Field: "organization_ids",
Detail: "Missing values, this cannot be empty",
},
},
})
return
}
// TODO: @emyrk Authorize the organization create if the createUser will do that.
_, err := api.Database.GetUserByEmailOrUsername(ctx, database.GetUserByEmailOrUsernameParams{
@@ -406,44 +416,33 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
return
}
if req.OrganizationID != uuid.Nil {
// If an organization was provided, make sure it exists.
_, err := api.Database.GetOrganizationByID(ctx, req.OrganizationID)
if err != nil {
if httpapi.Is404Error(err) {
// If an organization was provided, make sure it exists.
for i, orgID := range req.OrganizationIDs {
var orgErr error
if orgID != uuid.Nil {
_, orgErr = api.Database.GetOrganizationByID(ctx, orgID)
} else {
var defaultOrg database.Organization
defaultOrg, orgErr = api.Database.GetDefaultOrganization(ctx)
if orgErr == nil {
// converts uuid.Nil --> default org.ID
req.OrganizationIDs[i] = defaultOrg.ID
}
}
if orgErr != nil {
if httpapi.Is404Error(orgErr) {
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
Message: fmt.Sprintf("Organization does not exist with the provided id %q.", req.OrganizationID),
Message: fmt.Sprintf("Organization does not exist with the provided id %q.", orgID),
})
return
}
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching organization.",
Detail: err.Error(),
Detail: orgErr.Error(),
})
return
}
} else {
// If no organization is provided, add the user to the default
defaultOrg, err := api.Database.GetDefaultOrganization(ctx)
if err != nil {
if httpapi.Is404Error(err) {
httpapi.Write(ctx, rw, http.StatusNotFound,
codersdk.Response{
Message: "Resource not found or you do not have access to this resource",
Detail: "Organization not found",
},
)
return
}
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching orgs.",
Detail: err.Error(),
})
return
}
req.OrganizationID = defaultOrg.ID
}
var loginType database.LoginType
@@ -480,9 +479,9 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
return
}
user, _, err := api.CreateUser(ctx, api.Database, CreateUserRequest{
CreateUserRequest: req,
LoginType: loginType,
user, err := api.CreateUser(ctx, api.Database, CreateUserRequest{
CreateUserRequestWithOrgs: req,
LoginType: loginType,
})
if dbauthz.IsNotAuthorizedError(err) {
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
@@ -505,7 +504,7 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
Users: []telemetry.User{telemetry.ConvertUser(user)},
})
httpapi.Write(ctx, rw, http.StatusCreated, db2sdk.User(user, []uuid.UUID{req.OrganizationID}))
httpapi.Write(ctx, rw, http.StatusCreated, db2sdk.User(user, req.OrganizationIDs))
}
// @Summary Delete user
@@ -1280,23 +1279,23 @@ func (api *API) organizationByUserAndName(rw http.ResponseWriter, r *http.Reques
}
type CreateUserRequest struct {
codersdk.CreateUserRequest
codersdk.CreateUserRequestWithOrgs
LoginType database.LoginType
SkipNotifications bool
}
func (api *API) CreateUser(ctx context.Context, store database.Store, req CreateUserRequest) (database.User, uuid.UUID, error) {
func (api *API) CreateUser(ctx context.Context, store database.Store, req CreateUserRequest) (database.User, error) {
// Ensure the username is valid. It's the caller's responsibility to ensure
// the username is valid and unique.
if usernameValid := httpapi.NameValid(req.Username); usernameValid != nil {
return database.User{}, uuid.Nil, xerrors.Errorf("invalid username %q: %w", req.Username, usernameValid)
return database.User{}, xerrors.Errorf("invalid username %q: %w", req.Username, usernameValid)
}
var user database.User
err := store.InTx(func(tx database.Store) error {
orgRoles := make([]string, 0)
// Organization is required to know where to allocate the user.
if req.OrganizationID == uuid.Nil {
if len(req.OrganizationIDs) == 0 {
return xerrors.Errorf("organization ID must be provided")
}
@@ -1341,26 +1340,30 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
if err != nil {
return xerrors.Errorf("insert user gitsshkey: %w", err)
}
_, err = tx.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{
OrganizationID: req.OrganizationID,
UserID: user.ID,
CreatedAt: dbtime.Now(),
UpdatedAt: dbtime.Now(),
// By default give them membership to the organization.
Roles: orgRoles,
})
if err != nil {
return xerrors.Errorf("create organization member: %w", err)
for _, orgID := range req.OrganizationIDs {
_, err = tx.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{
OrganizationID: orgID,
UserID: user.ID,
CreatedAt: dbtime.Now(),
UpdatedAt: dbtime.Now(),
// By default give them membership to the organization.
Roles: orgRoles,
})
if err != nil {
return xerrors.Errorf("create organization member for %q: %w", orgID.String(), err)
}
}
return nil
}, nil)
if err != nil || req.SkipNotifications {
return user, req.OrganizationID, err
return user, err
}
userAdmins, err := findUserAdmins(ctx, store)
if err != nil {
return user, req.OrganizationID, xerrors.Errorf("find user admins: %w", err)
return user, xerrors.Errorf("find user admins: %w", err)
}
for _, u := range userAdmins {
@@ -1373,7 +1376,7 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
api.Logger.Warn(ctx, "unable to notify about created user", slog.F("created_user", user.Username), slog.Error(err))
}
}
return user, req.OrganizationID, err
return user, err
}
// findUserAdmins fetches all users with user admin permission including owners.