feat: Add authentication and personal user endpoint (#29)

* feat: Add authentication and personal user endpoint

This contribution adds a lot of scaffolding for the database fake
and testability of coderd.

A new endpoint "/user" is added to return the currently authenticated
user to the requester.

* Use TestMain to catch leak instead

* Add userpassword package

* Add WIP

* Add user auth

* Fix test

* Add comments

* Fix login response

* Fix order

* Fix generated code

* Update httpapi/httpapi.go

Co-authored-by: Bryan <bryan@coder.com>

Co-authored-by: Bryan <bryan@coder.com>
This commit is contained in:
Kyle Carberry
2022-01-20 13:46:51 +00:00
committed by GitHub
co-authored by Bryan
parent 36b7b20e2a
commit 6a919aea79
39 changed files with 2232 additions and 61 deletions
+111
View File
@@ -0,0 +1,111 @@
package databasefake
import (
"context"
"database/sql"
"github.com/coder/coder/database"
)
// New returns an in-memory fake of the database.
func New() database.Store {
return &fakeQuerier{
apiKeys: make([]database.APIKey, 0),
users: make([]database.User, 0),
}
}
// fakeQuerier replicates database functionality to enable quick testing.
type fakeQuerier struct {
apiKeys []database.APIKey
users []database.User
}
// InTx doesn't rollback data properly for in-memory yet.
func (q *fakeQuerier) InTx(ctx context.Context, fn func(database.Store) error) error {
return fn(q)
}
func (q *fakeQuerier) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
for _, apiKey := range q.apiKeys {
if apiKey.ID == id {
return apiKey, nil
}
}
return database.APIKey{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetUserByEmailOrUsername(ctx context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) {
for _, user := range q.users {
if user.Email == arg.Email || user.Username == arg.Username {
return user, nil
}
}
return database.User{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetUserByID(ctx context.Context, id string) (database.User, error) {
for _, user := range q.users {
if user.ID == id {
return user, nil
}
}
return database.User{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetUserCount(ctx context.Context) (int64, error) {
return int64(len(q.users)), nil
}
func (q *fakeQuerier) InsertAPIKey(ctx context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) {
key := database.APIKey{
ID: arg.ID,
HashedSecret: arg.HashedSecret,
UserID: arg.UserID,
Application: arg.Application,
Name: arg.Name,
LastUsed: arg.LastUsed,
ExpiresAt: arg.ExpiresAt,
CreatedAt: arg.CreatedAt,
UpdatedAt: arg.UpdatedAt,
LoginType: arg.LoginType,
OIDCAccessToken: arg.OIDCAccessToken,
OIDCRefreshToken: arg.OIDCRefreshToken,
OIDCIDToken: arg.OIDCIDToken,
OIDCExpiry: arg.OIDCExpiry,
DevurlToken: arg.DevurlToken,
}
q.apiKeys = append(q.apiKeys, key)
return key, nil
}
func (q *fakeQuerier) InsertUser(ctx context.Context, arg database.InsertUserParams) (database.User, error) {
user := database.User{
ID: arg.ID,
Email: arg.Email,
Name: arg.Name,
LoginType: arg.LoginType,
HashedPassword: arg.HashedPassword,
CreatedAt: arg.CreatedAt,
UpdatedAt: arg.UpdatedAt,
Username: arg.Username,
}
q.users = append(q.users, user)
return user, nil
}
func (q *fakeQuerier) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateAPIKeyByIDParams) error {
for index, apiKey := range q.apiKeys {
if apiKey.ID != arg.ID {
continue
}
apiKey.LastUsed = arg.LastUsed
apiKey.ExpiresAt = arg.ExpiresAt
apiKey.OIDCAccessToken = arg.OIDCAccessToken
apiKey.OIDCRefreshToken = arg.OIDCRefreshToken
apiKey.OIDCExpiry = arg.OIDCExpiry
q.apiKeys[index] = apiKey
return nil
}
return sql.ErrNoRows
}
-19
View File
@@ -1,19 +0,0 @@
package database
import "context"
// NewInMemory returns an in-memory store of the database.
func NewInMemory() Store {
return &memoryQuerier{}
}
type memoryQuerier struct{}
// InTx doesn't rollback data properly for in-memory yet.
func (q *memoryQuerier) InTx(ctx context.Context, fn func(Store) error) error {
return fn(q)
}
func (q *memoryQuerier) ExampleQuery(ctx context.Context) error {
return nil
}
+6 -6
View File
@@ -13,7 +13,7 @@ type LoginType string
const (
LoginTypeBuiltIn LoginType = "built-in"
LoginTypeSaml LoginType = "saml"
LoginTypeOidc LoginType = "oidc"
LoginTypeOIDC LoginType = "oidc"
)
func (e *LoginType) Scan(src interface{}) error {
@@ -48,7 +48,7 @@ func (e *UserStatus) Scan(src interface{}) error {
return nil
}
type ApiKey struct {
type APIKey struct {
ID string `db:"id" json:"id"`
HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"`
UserID string `db:"user_id" json:"user_id"`
@@ -59,10 +59,10 @@ type ApiKey struct {
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
LoginType LoginType `db:"login_type" json:"login_type"`
OidcAccessToken string `db:"oidc_access_token" json:"oidc_access_token"`
OidcRefreshToken string `db:"oidc_refresh_token" json:"oidc_refresh_token"`
OidcIDToken string `db:"oidc_id_token" json:"oidc_id_token"`
OidcExpiry time.Time `db:"oidc_expiry" json:"oidc_expiry"`
OIDCAccessToken string `db:"oidc_access_token" json:"oidc_access_token"`
OIDCRefreshToken string `db:"oidc_refresh_token" json:"oidc_refresh_token"`
OIDCIDToken string `db:"oidc_id_token" json:"oidc_id_token"`
OIDCExpiry time.Time `db:"oidc_expiry" json:"oidc_expiry"`
DevurlToken bool `db:"devurl_token" json:"devurl_token"`
}
+7 -1
View File
@@ -7,7 +7,13 @@ import (
)
type querier interface {
ExampleQuery(ctx context.Context) error
GetAPIKeyByID(ctx context.Context, id string) (APIKey, error)
GetUserByEmailOrUsername(ctx context.Context, arg GetUserByEmailOrUsernameParams) (User, error)
GetUserByID(ctx context.Context, id string) (User, error)
GetUserCount(ctx context.Context) (int64, error)
InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error)
InsertUser(ctx context.Context, arg InsertUserParams) (User, error)
UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error
}
var _ querier = (*sqlQuerier)(nil)
+107 -2
View File
@@ -1,2 +1,107 @@
-- name: ExampleQuery :exec
SELECT 'example query';
-- Database queries are generated using sqlc. See:
-- https://docs.sqlc.dev/en/latest/tutorials/getting-started-postgresql.html
--
-- Run "make gen" to generate models and query functions.
;
-- name: GetAPIKeyByID :one
SELECT
*
FROM
api_keys
WHERE
id = $1
LIMIT
1;
-- name: GetUserByID :one
SELECT
*
FROM
users
WHERE
id = $1
LIMIT
1;
-- name: GetUserByEmailOrUsername :one
SELECT
*
FROM
users
WHERE
username = $1
OR email = $2
LIMIT
1;
-- name: GetUserCount :one
SELECT
COUNT(*)
FROM
users;
-- name: InsertAPIKey :one
INSERT INTO
api_keys (
id,
hashed_secret,
user_id,
application,
name,
last_used,
expires_at,
created_at,
updated_at,
login_type,
oidc_access_token,
oidc_refresh_token,
oidc_id_token,
oidc_expiry,
devurl_token
)
VALUES
(
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$11,
$12,
$13,
$14,
$15
) RETURNING *;
-- name: InsertUser :one
INSERT INTO
users (
id,
email,
name,
login_type,
hashed_password,
created_at,
updated_at,
username
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *;
-- name: UpdateAPIKeyByID :exec
UPDATE
api_keys
SET
last_used = $2,
expires_at = $3,
oidc_access_token = $4,
oidc_refresh_token = $5,
oidc_expiry = $6
WHERE
id = $1;
+321 -4
View File
@@ -5,13 +5,330 @@ package database
import (
"context"
"time"
"github.com/lib/pq"
)
const exampleQuery = `-- name: ExampleQuery :exec
SELECT 'example query'
const getAPIKeyByID = `-- name: GetAPIKeyByID :one
SELECT
id, hashed_secret, user_id, application, name, last_used, expires_at, created_at, updated_at, login_type, oidc_access_token, oidc_refresh_token, oidc_id_token, oidc_expiry, devurl_token
FROM
api_keys
WHERE
id = $1
LIMIT
1
`
func (q *sqlQuerier) ExampleQuery(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, exampleQuery)
func (q *sqlQuerier) GetAPIKeyByID(ctx context.Context, id string) (APIKey, error) {
row := q.db.QueryRowContext(ctx, getAPIKeyByID, id)
var i APIKey
err := row.Scan(
&i.ID,
&i.HashedSecret,
&i.UserID,
&i.Application,
&i.Name,
&i.LastUsed,
&i.ExpiresAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.LoginType,
&i.OIDCAccessToken,
&i.OIDCRefreshToken,
&i.OIDCIDToken,
&i.OIDCExpiry,
&i.DevurlToken,
)
return i, err
}
const getUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one
SELECT
id, email, name, revoked, login_type, hashed_password, created_at, updated_at, temporary_password, avatar_hash, ssh_key_regenerated_at, username, dotfiles_git_uri, roles, status, relatime, gpg_key_regenerated_at, _decomissioned, shell
FROM
users
WHERE
username = $1
OR email = $2
LIMIT
1
`
type GetUserByEmailOrUsernameParams struct {
Username string `db:"username" json:"username"`
Email string `db:"email" json:"email"`
}
func (q *sqlQuerier) GetUserByEmailOrUsername(ctx context.Context, arg GetUserByEmailOrUsernameParams) (User, error) {
row := q.db.QueryRowContext(ctx, getUserByEmailOrUsername, arg.Username, arg.Email)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Name,
&i.Revoked,
&i.LoginType,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.TemporaryPassword,
&i.AvatarHash,
&i.SshKeyRegeneratedAt,
&i.Username,
&i.DotfilesGitUri,
pq.Array(&i.Roles),
&i.Status,
&i.Relatime,
&i.GpgKeyRegeneratedAt,
&i.Decomissioned,
&i.Shell,
)
return i, err
}
const getUserByID = `-- name: GetUserByID :one
SELECT
id, email, name, revoked, login_type, hashed_password, created_at, updated_at, temporary_password, avatar_hash, ssh_key_regenerated_at, username, dotfiles_git_uri, roles, status, relatime, gpg_key_regenerated_at, _decomissioned, shell
FROM
users
WHERE
id = $1
LIMIT
1
`
func (q *sqlQuerier) GetUserByID(ctx context.Context, id string) (User, error) {
row := q.db.QueryRowContext(ctx, getUserByID, id)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Name,
&i.Revoked,
&i.LoginType,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.TemporaryPassword,
&i.AvatarHash,
&i.SshKeyRegeneratedAt,
&i.Username,
&i.DotfilesGitUri,
pq.Array(&i.Roles),
&i.Status,
&i.Relatime,
&i.GpgKeyRegeneratedAt,
&i.Decomissioned,
&i.Shell,
)
return i, err
}
const getUserCount = `-- name: GetUserCount :one
SELECT
COUNT(*)
FROM
users
`
func (q *sqlQuerier) GetUserCount(ctx context.Context) (int64, error) {
row := q.db.QueryRowContext(ctx, getUserCount)
var count int64
err := row.Scan(&count)
return count, err
}
const insertAPIKey = `-- name: InsertAPIKey :one
INSERT INTO
api_keys (
id,
hashed_secret,
user_id,
application,
name,
last_used,
expires_at,
created_at,
updated_at,
login_type,
oidc_access_token,
oidc_refresh_token,
oidc_id_token,
oidc_expiry,
devurl_token
)
VALUES
(
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$11,
$12,
$13,
$14,
$15
) RETURNING id, hashed_secret, user_id, application, name, last_used, expires_at, created_at, updated_at, login_type, oidc_access_token, oidc_refresh_token, oidc_id_token, oidc_expiry, devurl_token
`
type InsertAPIKeyParams struct {
ID string `db:"id" json:"id"`
HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"`
UserID string `db:"user_id" json:"user_id"`
Application bool `db:"application" json:"application"`
Name string `db:"name" json:"name"`
LastUsed time.Time `db:"last_used" json:"last_used"`
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
LoginType LoginType `db:"login_type" json:"login_type"`
OIDCAccessToken string `db:"oidc_access_token" json:"oidc_access_token"`
OIDCRefreshToken string `db:"oidc_refresh_token" json:"oidc_refresh_token"`
OIDCIDToken string `db:"oidc_id_token" json:"oidc_id_token"`
OIDCExpiry time.Time `db:"oidc_expiry" json:"oidc_expiry"`
DevurlToken bool `db:"devurl_token" json:"devurl_token"`
}
func (q *sqlQuerier) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error) {
row := q.db.QueryRowContext(ctx, insertAPIKey,
arg.ID,
arg.HashedSecret,
arg.UserID,
arg.Application,
arg.Name,
arg.LastUsed,
arg.ExpiresAt,
arg.CreatedAt,
arg.UpdatedAt,
arg.LoginType,
arg.OIDCAccessToken,
arg.OIDCRefreshToken,
arg.OIDCIDToken,
arg.OIDCExpiry,
arg.DevurlToken,
)
var i APIKey
err := row.Scan(
&i.ID,
&i.HashedSecret,
&i.UserID,
&i.Application,
&i.Name,
&i.LastUsed,
&i.ExpiresAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.LoginType,
&i.OIDCAccessToken,
&i.OIDCRefreshToken,
&i.OIDCIDToken,
&i.OIDCExpiry,
&i.DevurlToken,
)
return i, err
}
const insertUser = `-- name: InsertUser :one
INSERT INTO
users (
id,
email,
name,
login_type,
hashed_password,
created_at,
updated_at,
username
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, email, name, revoked, login_type, hashed_password, created_at, updated_at, temporary_password, avatar_hash, ssh_key_regenerated_at, username, dotfiles_git_uri, roles, status, relatime, gpg_key_regenerated_at, _decomissioned, shell
`
type InsertUserParams struct {
ID string `db:"id" json:"id"`
Email string `db:"email" json:"email"`
Name string `db:"name" json:"name"`
LoginType LoginType `db:"login_type" json:"login_type"`
HashedPassword []byte `db:"hashed_password" json:"hashed_password"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Username string `db:"username" json:"username"`
}
func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User, error) {
row := q.db.QueryRowContext(ctx, insertUser,
arg.ID,
arg.Email,
arg.Name,
arg.LoginType,
arg.HashedPassword,
arg.CreatedAt,
arg.UpdatedAt,
arg.Username,
)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Name,
&i.Revoked,
&i.LoginType,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.TemporaryPassword,
&i.AvatarHash,
&i.SshKeyRegeneratedAt,
&i.Username,
&i.DotfilesGitUri,
pq.Array(&i.Roles),
&i.Status,
&i.Relatime,
&i.GpgKeyRegeneratedAt,
&i.Decomissioned,
&i.Shell,
)
return i, err
}
const updateAPIKeyByID = `-- name: UpdateAPIKeyByID :exec
UPDATE
api_keys
SET
last_used = $2,
expires_at = $3,
oidc_access_token = $4,
oidc_refresh_token = $5,
oidc_expiry = $6
WHERE
id = $1
`
type UpdateAPIKeyByIDParams struct {
ID string `db:"id" json:"id"`
LastUsed time.Time `db:"last_used" json:"last_used"`
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
OIDCAccessToken string `db:"oidc_access_token" json:"oidc_access_token"`
OIDCRefreshToken string `db:"oidc_refresh_token" json:"oidc_refresh_token"`
OIDCExpiry time.Time `db:"oidc_expiry" json:"oidc_expiry"`
}
func (q *sqlQuerier) UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error {
_, err := q.db.ExecContext(ctx, updateAPIKeyByID,
arg.ID,
arg.LastUsed,
arg.ExpiresAt,
arg.OIDCAccessToken,
arg.OIDCRefreshToken,
arg.OIDCExpiry,
)
return err
}
+6
View File
@@ -19,4 +19,10 @@ overrides:
- db_type: citext
go_type: string
rename:
api_key: APIKey
login_type_oidc: LoginTypeOIDC
oidc_access_token: OIDCAccessToken
oidc_expiry: OIDCExpiry
oidc_id_token: OIDCIDToken
oidc_refresh_token: OIDCRefreshToken
userstatus: UserStatus
+8
View File
@@ -0,0 +1,8 @@
package database
import "time"
// Now returns a standardized timezone used for database resources.
func Now() time.Time {
return time.Now().UTC()
}