Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/server_createadminuser.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ func (r *RootCmd) newCreateAdminUserCommand() *serpent.Command {
UpdatedAt: dbtime.Now(),
RBACRoles: []string{rbac.RoleOwner().String()},
LoginType: database.LoginTypePassword,
Status: "",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we pass "" here? Should we just pass active?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally I would omit it but the linter doesn't like leaving it out. It doesn't really matter what this is created as, so "" just does the default behavior which is dormant.

})
if err != nil {
return xerrors.Errorf("insert user: %w", err)
Expand Down
8 changes: 8 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions coderd/coderdtest/coderdtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,9 @@ func createAnotherUserRetry(t testing.TB, client *codersdk.Client, organizationI
Name: RandomName(t),
Password: "SomeSecurePassword!",
OrganizationIDs: organizationIDs,
// Always create users as active in tests to ignore an extra audit log
// when logging in.
UserStatus: ptr.Ref(codersdk.UserStatusActive),
}
for _, m := range mutators {
m(&req)
Expand Down
1 change: 1 addition & 0 deletions coderd/database/dbgen/dbgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ func User(t testing.TB, db database.Store, orig database.User) database.User {
UpdatedAt: takeFirst(orig.UpdatedAt, dbtime.Now()),
RBACRoles: takeFirstSlice(orig.RBACRoles, []string{}),
LoginType: takeFirst(orig.LoginType, database.LoginTypePassword),
Status: string(takeFirst(orig.Status, database.UserStatusDormant)),
})
require.NoError(t, err, "insert user")

Expand Down
8 changes: 7 additions & 1 deletion coderd/database/dbmem/dbmem.go
Original file line number Diff line number Diff line change
Expand Up @@ -7709,6 +7709,11 @@ func (q *FakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam
}
}

status := database.UserStatusDormant
if arg.Status != "" {
status = database.UserStatus(arg.Status)
}

user := database.User{
ID: arg.ID,
Email: arg.Email,
Expand All @@ -7717,7 +7722,7 @@ func (q *FakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam
UpdatedAt: arg.UpdatedAt,
Username: arg.Username,
Name: arg.Name,
Status: database.UserStatusDormant,
Status: status,
RBACRoles: arg.RBACRoles,
LoginType: arg.LoginType,
}
Expand Down Expand Up @@ -8640,6 +8645,7 @@ func (q *FakeQuerier) UpdateInactiveUsersToDormant(_ context.Context, params dat
updated = append(updated, database.UpdateInactiveUsersToDormantRow{
ID: user.ID,
Email: user.Email,
Username: user.Username,
LastSeenAt: user.LastSeenAt,
})
}
Expand Down
21 changes: 17 additions & 4 deletions coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions coderd/database/queries/users.sql
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,15 @@ INSERT INTO
created_at,
updated_at,
rbac_roles,
login_type
login_type,
status
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *;
($1, $2, $3, $4, $5, $6, $7, $8, $9,
-- if the status passed in is empty, fallback to dormant, which is what
-- we were doing before.
COALESCE(NULLIF(@status::text, '')::user_status, 'dormant'::user_status)
) RETURNING *;
Comment on lines +74 to +78
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we handle this as a special case? Shouldn't we expect the status to be provided?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't really want to leak the previous default behavior out into the API code. I think it makes more sense to do the default behavior here, which is what was happening before when it wasn't being inserted.


-- name: UpdateUserProfile :one
UPDATE
Expand Down Expand Up @@ -286,7 +291,7 @@ SET
WHERE
last_seen_at < @last_seen_after :: timestamp
AND status = 'active'::user_status
RETURNING id, email, last_seen_at;
RETURNING id, email, username, last_seen_at;

-- AllUserIDs returns all UserIDs regardless of user status or deletion.
-- name: AllUserIDs :many
Expand Down
1 change: 1 addition & 0 deletions coderd/provisionerdserver/provisionerdserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1063,6 +1063,7 @@ func (s *server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*proto.
wriBytes, err := json.Marshal(buildResourceInfo)
if err != nil {
s.Logger.Error(ctx, "marshal workspace resource info for failed job", slog.Error(err))
wriBytes = []byte("{}")
}

bag := audit.BaggageFromContext(ctx)
Expand Down
23 changes: 23 additions & 0 deletions coderd/userauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,7 @@ func (api *API) loginRequest(ctx context.Context, rw http.ResponseWriter, req co
}

if user.Status == database.UserStatusDormant {
oldUser := user
//nolint:gocritic // System needs to update status of the user account (dormant -> active).
user, err = api.Database.UpdateUserStatus(dbauthz.AsSystemRestricted(ctx), database.UpdateUserStatusParams{
ID: user.ID,
Expand All @@ -579,6 +580,28 @@ func (api *API) loginRequest(ctx context.Context, rw http.ResponseWriter, req co
})
return user, rbac.Subject{}, false
}

af := map[string]string{
"automatic_actor": "coder",
"automatic_subsystem": "dormancy",
}

wriBytes, err := json.Marshal(af)
if err != nil {
api.Logger.Error(ctx, "marshal additional fields for dormancy audit", slog.Error(err))
wriBytes = []byte("{}")
}

audit.BackgroundAudit(ctx, &audit.BackgroundAuditParams[database.User]{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't audit if we fail to update the user status. Also why is this a background audit if it's part of an API request?

Copy link
Contributor Author

@coadler coadler Oct 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I don't really think we get anything valuable from generating an audit log if this fails. The main reason for failed audit logs is for logging users attempting to take an action for which they don't have permission, or similar. The status failing to update here doesn't really matter, it can only ever be a 500 error. Plus, it'll be captured by the main login audit anyways.

I think BackgroundAudit is poorly named, it's really just for anything not specifically tied to a request. I don't think it make sense to use a request audit here because the status could successfully be changed while the api request as a whole fails. Also, since dormancy is really something Coder just does in the background, it maintains consistency. The user isn't really intentionally modifying their status.

Audit: *api.Auditor.Load(),
Log: api.Logger,
UserID: user.ID,
Action: database.AuditActionWrite,
Old: oldUser,
New: user,
Status: http.StatusOK,
AdditionalFields: wriBytes,
})
}

subject, userStatus, err := httpmw.UserRBACSubject(ctx, api.Database, user.ID, rbac.ScopeAll)
Expand Down
11 changes: 10 additions & 1 deletion coderd/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/coder/coder/v2/coderd/searchquery"
"github.com/coder/coder/v2/coderd/telemetry"
"github.com/coder/coder/v2/coderd/userpassword"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/codersdk"
)
Expand Down Expand Up @@ -192,9 +193,12 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
Username: createUser.Username,
Name: createUser.Name,
Password: createUser.Password,
UserStatus: ptr.Ref(codersdk.UserStatusActive),
OrganizationIDs: []uuid.UUID{defaultOrg.ID},
},
LoginType: database.LoginTypePassword,
LoginType: database.LoginTypePassword,
// There's no reason to create the first user as dormant, since you have
// to login immediately anyways.
accountCreatorName: "coder",
})
if err != nil {
Expand Down Expand Up @@ -1343,6 +1347,10 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
err := store.InTx(func(tx database.Store) error {
orgRoles := make([]string, 0)

status := ""
if req.UserStatus != nil {
status = string(*req.UserStatus)
}
Comment on lines +1350 to +1353
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly when would we expect the status to be empty?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty just means use the default behavior, which is dormant. It would be a breaking change if this was required

params := database.InsertUserParams{
ID: uuid.New(),
Email: req.Email,
Expand All @@ -1354,6 +1362,7 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
// All new users are defaulted to members of the site.
RBACRoles: []string{},
LoginType: req.LoginType,
Status: status,
}
// If a user signs up with OAuth, they can have no password!
if req.Password != "" {
Expand Down
36 changes: 36 additions & 0 deletions coderd/users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/testutil"
Expand Down Expand Up @@ -695,6 +696,41 @@ func TestPostUsers(t *testing.T) {
})
require.NoError(t, err)

// User should default to dormant.
require.Equal(t, codersdk.UserStatusDormant, user.Status)

require.Len(t, auditor.AuditLogs(), numLogs)
require.Equal(t, database.AuditActionCreate, auditor.AuditLogs()[numLogs-1].Action)
require.Equal(t, database.AuditActionLogin, auditor.AuditLogs()[numLogs-2].Action)

require.Len(t, user.OrganizationIDs, 1)
assert.Equal(t, firstUser.OrganizationID, user.OrganizationIDs[0])
})

t.Run("CreateWithStatus", func(t *testing.T) {
t.Parallel()
auditor := audit.NewMock()
client := coderdtest.New(t, &coderdtest.Options{Auditor: auditor})
numLogs := len(auditor.AuditLogs())

firstUser := coderdtest.CreateFirstUser(t, client)
numLogs++ // add an audit log for user create
numLogs++ // add an audit log for login

ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()

user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
OrganizationIDs: []uuid.UUID{firstUser.OrganizationID},
Email: "another@user.org",
Username: "someone-else",
Password: "SomeSecurePassword!",
UserStatus: ptr.Ref(codersdk.UserStatusActive),
})
require.NoError(t, err)

require.Equal(t, codersdk.UserStatusActive, user.Status)

require.Len(t, auditor.AuditLogs(), numLogs)
require.Equal(t, database.AuditActionCreate, auditor.AuditLogs()[numLogs-1].Action)
require.Equal(t, database.AuditActionLogin, auditor.AuditLogs()[numLogs-2].Action)
Expand Down
2 changes: 2 additions & 0 deletions codersdk/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ type CreateUserRequestWithOrgs struct {
Password string `json:"password"`
// UserLoginType defaults to LoginTypePassword.
UserLoginType LoginType `json:"login_type"`
// UserStatus defaults to UserStatusDormant.
UserStatus *UserStatus `json:"user_status"`
// OrganizationIDs is a list of organization IDs that the user should be a member of.
OrganizationIDs []uuid.UUID `json:"organization_ids" validate:"" format:"uuid"`
}
Expand Down
18 changes: 10 additions & 8 deletions docs/reference/api/schemas.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/reference/api/users.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion enterprise/cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func (r *RootCmd) Server(_ func()) *serpent.Command {
DefaultQuietHoursSchedule: options.DeploymentValues.UserQuietHoursSchedule.DefaultSchedule.Value(),
ProvisionerDaemonPSK: options.DeploymentValues.Provisioner.DaemonPSK.Value(),

CheckInactiveUsersCancelFunc: dormancy.CheckInactiveUsers(ctx, options.Logger, options.Database),
CheckInactiveUsersCancelFunc: dormancy.CheckInactiveUsers(ctx, options.Logger, options.Database, options.Auditor),
}

if encKeys := options.DeploymentValues.ExternalTokenEncryptionKeys.Value(); len(encKeys) != 0 {
Expand Down
Loading
Loading