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 5 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
56 changes: 45 additions & 11 deletions cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,10 @@ func server() *cobra.Command {
pprofAddress string
cacheDir string
dev bool
devUserEmail string
devUserPassword string
devFirstEmail string
devFirstPassword string
devMemberEmail string
devMemberPassword string
postgresURL string
// provisionerDaemonCount is a uint8 to ensure a number > 0.
provisionerDaemonCount uint8
Expand Down Expand Up @@ -330,18 +332,30 @@ func server() *cobra.Command {
config := createConfig(cmd)

if dev {
if devUserPassword == "" {
devUserPassword, err = cryptorand.String(10)
if devFirstPassword == "" {
devFirstPassword, err = cryptorand.String(10)
if err != nil {
return xerrors.Errorf("generate random admin password for dev: %w", err)
}
}
err = createFirstUser(cmd, client, config, devUserEmail, devUserPassword)
if devMemberPassword == "" {
devMemberPassword, err = cryptorand.String(10)
if err != nil {
return xerrors.Errorf("generate random member password for dev: %w", err)
}
}

// Create first user with 1 additional user as a member of the same org.
err = createFirstUser(cmd, client, config, devFirstEmail, devFirstPassword, extraUsers{
Username: "member",
Email: devMemberEmail,
Password: devMemberPassword,
})
if err != nil {
return xerrors.Errorf("create first user: %w", err)
}
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "email: %s\n", devUserEmail)
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "password: %s\n", devUserPassword)
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "email: %s\n", devFirstEmail)
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "password: %s\n", devFirstPassword)
_, _ = fmt.Fprintln(cmd.ErrOrStderr())

_, _ = fmt.Fprintf(cmd.ErrOrStderr(), cliui.Styles.Wrap.Render(`Started in dev mode. All data is in-memory! `+cliui.Styles.Bold.Render("Do not use in production")+`. Press `+
Expand Down Expand Up @@ -474,8 +488,10 @@ func server() *cobra.Command {
// systemd uses the CACHE_DIRECTORY environment variable!
cliflag.StringVarP(root.Flags(), &cacheDir, "cache-dir", "", "CACHE_DIRECTORY", filepath.Join(os.TempDir(), "coder-cache"), "Specifies a directory to cache binaries for provision operations.")
cliflag.BoolVarP(root.Flags(), &dev, "dev", "", "CODER_DEV_MODE", false, "Serve Coder in dev mode for tinkering")
cliflag.StringVarP(root.Flags(), &devUserEmail, "dev-admin-email", "", "CODER_DEV_ADMIN_EMAIL", "admin@coder.com", "Specifies the admin email to be used in dev mode (--dev)")
cliflag.StringVarP(root.Flags(), &devUserPassword, "dev-admin-password", "", "CODER_DEV_ADMIN_PASSWORD", "", "Specifies the admin password to be used in dev mode (--dev) instead of a randomly generated one")
cliflag.StringVarP(root.Flags(), &devFirstEmail, "dev-admin-email", "", "CODER_DEV_ADMIN_EMAIL", "admin@coder.com", "Specifies the admin email to be used in dev mode (--dev)")
cliflag.StringVarP(root.Flags(), &devFirstPassword, "dev-admin-password", "", "CODER_DEV_ADMIN_PASSWORD", "", "Specifies the admin password to be used in dev mode (--dev) instead of a randomly generated one")
cliflag.StringVarP(root.Flags(), &devMemberEmail, "dev-member-email", "", "CODER_DEV_MEMBER_EMAIL", "member@coder.com", "Specifies the member email to be used in dev mode (--dev)")
cliflag.StringVarP(root.Flags(), &devMemberPassword, "dev-member-password", "", "CODER_DEV_MEMBER_PASSWORD", "", "Specifies the member password to be used in dev mode (--dev) instead of a randomly generated one")
cliflag.StringVarP(root.Flags(), &postgresURL, "postgres-url", "", "CODER_PG_CONNECTION_URL", "", "URL of a PostgreSQL database to connect to")
cliflag.Uint8VarP(root.Flags(), &provisionerDaemonCount, "provisioner-daemons", "", "CODER_PROVISIONER_DAEMONS", 3, "The amount of provisioner daemons to create on start.")
cliflag.StringVarP(root.Flags(), &oauth2GithubClientID, "oauth2-github-client-id", "", "CODER_OAUTH2_GITHUB_CLIENT_ID", "",
Expand Down Expand Up @@ -518,14 +534,20 @@ func server() *cobra.Command {
return root
}

func createFirstUser(cmd *cobra.Command, client *codersdk.Client, cfg config.Root, email, password string) error {
type extraUsers struct {
Username string
Email string
Password string
}

func createFirstUser(cmd *cobra.Command, client *codersdk.Client, cfg config.Root, email, password string, users ...extraUsers) error {
if email == "" {
return xerrors.New("email is empty")
}
if password == "" {
return xerrors.New("password is empty")
}
_, err := client.CreateFirstUser(cmd.Context(), codersdk.CreateFirstUserRequest{
first, err := client.CreateFirstUser(cmd.Context(), codersdk.CreateFirstUserRequest{
Email: email,
Username: "developer",
Password: password,
Expand All @@ -551,6 +573,18 @@ func createFirstUser(cmd *cobra.Command, client *codersdk.Client, cfg config.Roo
if err != nil {
return xerrors.Errorf("write session token: %w", err)
}

for _, user := range users {
_, err := client.CreateUser(cmd.Context(), codersdk.CreateUserRequest{
Email: user.Email,
Username: user.Username,
Password: user.Password,
OrganizationID: first.OrganizationID,
})
if err != nil {
return xerrors.Errorf("create extra user %q: %w", user.Email, err)
}
}
return nil
}

Expand Down
13 changes: 1 addition & 12 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,15 +133,13 @@ func New(options *Options) *API {
r.Route("/provisionerdaemons", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
authRolesMiddleware,
)
r.Get("/", api.provisionerDaemons)
})
r.Route("/organizations/{organization}", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
httpmw.ExtractOrganizationParam(options.Database),
authRolesMiddleware,
)
r.Get("/", api.organization)
r.Post("/templateversions", api.postTemplateVersionsByOrganization)
Expand Down Expand Up @@ -174,7 +169,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 @@ -184,7 +179,6 @@ func New(options *Options) *API {
r.Route("/templates/{template}", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
authRolesMiddleware,
httpmw.ExtractTemplateParam(options.Database),
)

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

Expand All @@ -225,7 +218,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 Down Expand Up @@ -288,7 +280,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 @@ -297,7 +288,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 @@ -323,7 +313,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
1 change: 1 addition & 0 deletions coderd/database/databasefake/databasefake.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,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
18 changes: 13 additions & 5 deletions coderd/database/queries.sql.go

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

4 changes: 3 additions & 1 deletion coderd/database/queries/users.sql
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,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