Skip to content

fix: Suspended users cannot authenticate #1849

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
May 31, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 2 additions & 13 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,6 @@ func New(options *Options) *API {
apiKeyMiddleware := httpmw.ExtractAPIKey(options.Database, &httpmw.OAuth2Configs{
Github: options.GithubOAuth2Config,
})
// TODO: @emyrk we should just move this into 'ExtractAPIKey'.
authRolesMiddleware := httpmw.ExtractUserRoles(options.Database)

r.Use(
func(next http.Handler) http.Handler {
Expand Down Expand Up @@ -125,7 +123,6 @@ func New(options *Options) *API {
r.Route("/files", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
authRolesMiddleware,
// This number is arbitrary, but reading/writing
// file content is expensive so it should be small.
httpmw.RateLimitPerMinute(12),
Expand All @@ -136,14 +133,12 @@ func New(options *Options) *API {
r.Route("/provisionerdaemons", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
authRolesMiddleware,
)
r.Get("/", api.provisionerDaemons)
})
r.Route("/organizations", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
authRolesMiddleware,
)
r.Post("/", api.postOrganizations)
r.Route("/{organization}", func(r chi.Router) {
Expand Down Expand Up @@ -179,7 +174,7 @@ func New(options *Options) *API {
})
})
r.Route("/parameters/{scope}/{id}", func(r chi.Router) {
r.Use(apiKeyMiddleware, authRolesMiddleware)
r.Use(apiKeyMiddleware)
r.Post("/", api.postParameter)
r.Get("/", api.parameters)
r.Route("/{name}", func(r chi.Router) {
Expand All @@ -189,7 +184,6 @@ func New(options *Options) *API {
r.Route("/templates/{template}", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
authRolesMiddleware,
httpmw.ExtractTemplateParam(options.Database),
)

Expand All @@ -204,7 +198,6 @@ func New(options *Options) *API {
r.Route("/templateversions/{templateversion}", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
authRolesMiddleware,
httpmw.ExtractTemplateVersionParam(options.Database),
)

Expand All @@ -229,7 +222,6 @@ func New(options *Options) *API {
r.Group(func(r chi.Router) {
r.Use(
apiKeyMiddleware,
authRolesMiddleware,
)
r.Post("/", api.postUser)
r.Get("/", api.users)
Expand All @@ -244,7 +236,7 @@ func New(options *Options) *API {
r.Put("/profile", api.putUserProfile)
r.Route("/status", func(r chi.Router) {
r.Put("/suspend", api.putUserStatus(database.UserStatusSuspended))
r.Put("/active", api.putUserStatus(database.UserStatusActive))
r.Put("/activate", api.putUserStatus(database.UserStatusActive))
})
r.Route("/password", func(r chi.Router) {
r.Put("/", api.putUserPassword)
Expand Down Expand Up @@ -292,7 +284,6 @@ func New(options *Options) *API {
r.Route("/workspaceresources/{workspaceresource}", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
authRolesMiddleware,
httpmw.ExtractWorkspaceResourceParam(options.Database),
httpmw.ExtractWorkspaceParam(options.Database),
)
Expand All @@ -301,7 +292,6 @@ func New(options *Options) *API {
r.Route("/workspaces", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
authRolesMiddleware,
)
r.Get("/", api.workspaces)
r.Route("/{workspace}", func(r chi.Router) {
Expand All @@ -327,7 +317,6 @@ func New(options *Options) *API {
r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
authRolesMiddleware,
httpmw.ExtractWorkspaceBuildParam(options.Database),
httpmw.ExtractWorkspaceParam(options.Database),
)
Expand Down
9 changes: 6 additions & 3 deletions coderd/database/databasefake/databasefake.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,11 +231,13 @@ func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams
users = tmp
}

if params.Status != "" {
if len(params.Status) > 0 {
usersFilteredByStatus := make([]database.User, 0, len(users))
for i, user := range users {
if params.Status == string(user.Status) {
usersFilteredByStatus = append(usersFilteredByStatus, users[i])
for _, status := range params.Status {
if user.Status == status {
usersFilteredByStatus = append(usersFilteredByStatus, users[i])
}
}
}
users = usersFilteredByStatus
Expand Down Expand Up @@ -302,6 +304,7 @@ func (q *fakeQuerier) GetAllUserRoles(_ context.Context, userID uuid.UUID) (data
return database.GetAllUserRolesRow{
ID: userID,
Username: user.Username,
Status: user.Status,
Roles: roles,
}, nil
}
Expand Down
40 changes: 25 additions & 15 deletions coderd/database/queries.sql.go

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

14 changes: 9 additions & 5 deletions coderd/database/queries/users.sql
Original file line number Diff line number Diff line change
Expand Up @@ -101,17 +101,19 @@ WHERE
WHEN @search :: text != '' THEN (
email LIKE concat('%', @search, '%')
OR username LIKE concat('%', @search, '%')
)
)
ELSE true
END
-- Filter by status
AND CASE
-- @status needs to be a text because it can be empty, If it was
-- user_status enum, it would not.
WHEN @status :: text != '' THEN (
status = @status :: user_status
WHEN cardinality(@status :: user_status[]) > 0 THEN (
status = ANY(@status :: user_status[])
)
ELSE true
ELSE
-- Only show active by default
status = 'active'
END
-- End of filters
ORDER BY
Expand All @@ -135,7 +137,9 @@ WHERE
-- name: GetAllUserRoles :one
SELECT
-- username is returned just to help for logging purposes
id, username, array_cat(users.rbac_roles, organization_members.roles) :: text[] AS roles
-- status is used to enforce 'suspended' users, as all roles are ignored
-- when suspended.
id, username, status, array_cat(users.rbac_roles, organization_members.roles) :: text[] AS roles
FROM
users
LEFT JOIN organization_members
Expand Down
22 changes: 21 additions & 1 deletion coderd/httpmw/apikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,27 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs) func(http.Handler) h
}
}

ctx := context.WithValue(r.Context(), apiKeyContextKey{}, key)
// If the key is valid, we also fetch the user roles and status.
// The roles are used for RBAC authorize checks, and the status
// is to block 'suspended' users from accessing the platform.
roles, err := db.GetAllUserRoles(r.Context(), key.UserID)
if err != nil {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: "roles not found",
})
return
}

if roles.Status != database.UserStatusActive {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: fmt.Sprintf("user is not active (status = %q), contact an admin to reactivate your account", roles.Status),
})
return
}

ctx := r.Context()
ctx = context.WithValue(ctx, apiKeyContextKey{}, key)
ctx = context.WithValue(ctx, userRolesKey{}, roles)
next.ServeHTTP(rw, r.WithContext(ctx))
})
}
Expand Down
Loading