Skip to content
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

auth/basic: add createBasicAuth and updateBasicAuth for managing Basic credentials via GraphQL #3062

Merged
merged 6 commits into from
Jun 2, 2023
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
2 changes: 1 addition & 1 deletion app/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ Migration: %s (#%d)
fmt.Fprintln(os.Stderr)
}

pw, err := basicStore.NewHashedPassword(pass)
pw, err := basicStore.NewHashedPassword(ctx, pass)
if err != nil {
return errors.Wrap(err, "hash password")
}
Expand Down
131 changes: 128 additions & 3 deletions auth/basic/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import (
"database/sql"
"sync"

"github.com/target/goalert/config"
"github.com/target/goalert/permission"
"github.com/target/goalert/util"
"github.com/target/goalert/validation"
"github.com/target/goalert/validation/validate"

"github.com/pkg/errors"
Expand All @@ -18,6 +20,8 @@ import (
type Store struct {
insert *sql.Stmt
getByUsername *sql.Stmt
getByID *sql.Stmt
update *sql.Stmt

mx sync.Mutex
}
Expand All @@ -31,6 +35,8 @@ func NewStore(ctx context.Context, db *sql.DB) (*Store, error) {
return &Store{
insert: p.P("INSERT INTO auth_basic_users (user_id, username, password_hash) VALUES ($1, $2, $3)"),
getByUsername: p.P("SELECT user_id, password_hash FROM auth_basic_users WHERE username = $1"),
getByID: p.P("SELECT password_hash FROM auth_basic_users WHERE user_id = $1"),
update: p.P("UPDATE auth_basic_users SET password_hash = $2 WHERE user_id = $1"),
}, p.Err
}

Expand All @@ -46,9 +52,40 @@ type hashed []byte
func (h hashed) Hash() string { return string(h) }
func (h hashed) _private() {}

// ValidatedPassword represents a validated password for a UserID.
type ValidatedPassword interface {
UserID() string

_private() // prevent external implementations
}

type validated string

func (v validated) UserID() string { return string(v) }
func (v validated) _private() {}

// ValidateBasicAuth returns an access denied error for non-admins when basic auth is disabled in configs.
func ValidateBasicAuth(ctx context.Context) error {
if permission.Admin(ctx) {
return nil
}

cfg := config.FromContext(ctx)
if cfg.Auth.DisableBasic {
return permission.NewAccessDenied("Basic auth is disabled by administrator.")
}

return nil
}

// NewHashedPassword will hash the given password and return a Password object.
func (b *Store) NewHashedPassword(password string) (HashedPassword, error) {
err := validate.Text("Password", password, 8, 200)
func (b *Store) NewHashedPassword(ctx context.Context, password string) (HashedPassword, error) {
err := ValidateBasicAuth(ctx)
if err != nil {
return nil, err
}

err = validate.Text("Password", password, 8, 200)
if err != nil {
return nil, err
}
Expand All @@ -67,7 +104,12 @@ func (b *Store) NewHashedPassword(password string) (HashedPassword, error) {
// An error is returned if the username is not unique or the userID is invalid.
// Must have same user or admin role.
func (b *Store) CreateTx(ctx context.Context, tx *sql.Tx, userID, username string, password HashedPassword) error {
err := permission.LimitCheckAny(ctx, permission.System, permission.Admin, permission.MatchUser(userID))
err := ValidateBasicAuth(ctx)
if err != nil {
return err
}

err = permission.LimitCheckAny(ctx, permission.System, permission.Admin, permission.MatchUser(userID))
if err != nil {
return err
}
Expand All @@ -84,6 +126,47 @@ func (b *Store) CreateTx(ctx context.Context, tx *sql.Tx, userID, username strin
return err
}

// UpdateTx updates a user's password. oldPass is required if the current context is not an admin.
func (b *Store) UpdateTx(ctx context.Context, tx *sql.Tx, userID string, oldPass ValidatedPassword, newPass HashedPassword) error {
err := ValidateBasicAuth(ctx)
if err != nil {
return err
}

err = permission.LimitCheckAny(ctx, permission.Admin, permission.MatchUser(userID))
if err != nil {
return err
}

err = validate.UUID("UserID", userID)
if err != nil {
return err
}

if oldPass != nil && oldPass.UserID() != userID {
return validation.NewFieldError("OldPassword", "Password does not match User")
}
if (!permission.Admin(ctx) || permission.UserID(ctx) == userID) && oldPass == nil {
return validation.NewFieldError("OldPassword", "Previous password required")
}

res, err := tx.StmtContext(ctx, b.update).ExecContext(ctx, userID, newPass.Hash())
if err != nil {
return err
}

count, err := res.RowsAffected()
if err != nil {
return err
}

if count == 0 {
return validation.NewFieldError("UserID", "does not have basic auth configured")
}

return nil
}

// Validate should return a userID if the username and password match.
func (b *Store) Validate(ctx context.Context, username, password string) (string, error) {
err := validate.Many(
Expand Down Expand Up @@ -115,3 +198,45 @@ func (b *Store) Validate(ctx context.Context, username, password string) (string

return userID, nil
}

// ValidatePassword will validate the password of the currently authenticated user.
func (b *Store) ValidatePassword(ctx context.Context, password string) (ValidatedPassword, error) {
err := ValidateBasicAuth(ctx)
if err != nil {
return nil, err
}

err = permission.LimitCheckAny(ctx, permission.User)
if err != nil {
return nil, err
}

userID := permission.UserID(ctx)

err = validate.Many(
validate.UUID("UserID", userID),
validate.Text("OldPassword", password, 8, 200),
)
if err != nil {
return nil, err
}

var hash string
err = b.getByID.QueryRowContext(ctx, userID).Scan(&hash)
if errors.Is(err, sql.ErrNoRows) {
return nil, errors.New("unknown userID")
}
if err != nil {
return nil, errors.WithMessage(err, "user lookup failure")
}

b.mx.Lock()
defer b.mx.Unlock()

err = bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
if err != nil {
return nil, validation.NewFieldError("OldPassword", "invalid password")
}

return validated(userID), nil
}
Loading