garm/auth/auth.go

190 lines
5.4 KiB
Go
Raw Permalink Normal View History

2022-05-05 13:25:50 +00:00
// Copyright 2022 Cloudbase Solutions SRL
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
2022-04-28 16:13:20 +00:00
package auth
import (
"context"
"errors"
"fmt"
2022-04-28 16:13:20 +00:00
"time"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/nbutton23/zxcvbn-go"
"golang.org/x/crypto/bcrypt"
runnerErrors "github.com/cloudbase/garm-provider-common/errors"
"github.com/cloudbase/garm-provider-common/util"
"github.com/cloudbase/garm/config"
"github.com/cloudbase/garm/database/common"
"github.com/cloudbase/garm/params"
2022-04-28 16:13:20 +00:00
)
func NewAuthenticator(cfg config.JWTAuth, store common.Store) *Authenticator {
return &Authenticator{
cfg: cfg,
store: store,
}
}
type Authenticator struct {
store common.Store
cfg config.JWTAuth
}
func (a *Authenticator) IsInitialized() bool {
return a.store.HasAdminUser(context.Background())
2022-04-28 16:13:20 +00:00
}
func (a *Authenticator) GetJWTToken(ctx context.Context) (string, error) {
tokenID, err := util.GetRandomString(16)
if err != nil {
return "", fmt.Errorf("error generating random string: %w", err)
2022-04-28 16:13:20 +00:00
}
expireToken := time.Now().Add(a.cfg.TimeToLive.Duration())
expires := &jwt.NumericDate{
Time: expireToken,
}
generation := PasswordGeneration(ctx)
2022-04-28 16:13:20 +00:00
claims := JWTClaims{
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: expires,
// nolint:golangci-lint,godox
2022-04-28 16:13:20 +00:00
// TODO: make this configurable
Issuer: "garm",
2022-04-28 16:13:20 +00:00
},
UserID: UserID(ctx),
TokenID: tokenID,
IsAdmin: IsAdmin(ctx),
FullName: FullName(ctx),
Generation: generation,
2022-04-28 16:13:20 +00:00
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(a.cfg.Secret))
if err != nil {
return "", fmt.Errorf("error fetching token string: %w", err)
}
return tokenString, nil
}
// GetJWTMetricsToken returns a JWT token that can be used to read metrics.
// This token is not tied to a user, no user is stored in the db.
func (a *Authenticator) GetJWTMetricsToken(ctx context.Context) (string, error) {
2023-01-26 14:02:53 +01:00
if !IsAdmin(ctx) {
return "", runnerErrors.ErrUnauthorized
}
tokenID, err := util.GetRandomString(16)
if err != nil {
return "", fmt.Errorf("error generating random string: %w", err)
}
// nolint:golangci-lint,godox
// TODO: currently this is the same TTL as the normal Token
// maybe we should make this configurable
// it's usually pretty nasty if the monitoring fails because the token expired
expireToken := time.Now().Add(a.cfg.TimeToLive.Duration())
expires := &jwt.NumericDate{
Time: expireToken,
}
claims := JWTClaims{
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: expires,
// nolint:golangci-lint,godox
// TODO: make this configurable
Issuer: "garm",
},
TokenID: tokenID,
IsAdmin: false,
ReadMetrics: true,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(a.cfg.Secret))
if err != nil {
return "", fmt.Errorf("error fetching token string: %w", err)
2022-04-28 16:13:20 +00:00
}
return tokenString, nil
}
func (a *Authenticator) InitController(ctx context.Context, param params.NewUserParams) (params.User, error) {
_, err := a.store.ControllerInfo()
if err != nil {
if !errors.Is(err, runnerErrors.ErrNotFound) {
return params.User{}, fmt.Errorf("error initializing controller: %w", err)
2022-04-28 16:13:20 +00:00
}
}
if a.store.HasAdminUser(ctx) {
return params.User{}, runnerErrors.ErrNotFound
}
if param.Email == "" || param.Username == "" {
return params.User{}, runnerErrors.NewBadRequestError("missing username or email")
}
if !util.IsValidEmail(param.Email) {
return params.User{}, runnerErrors.NewBadRequestError("invalid email address")
}
// username is varchar(64)
if len(param.Username) > 64 || !util.IsAlphanumeric(param.Username) {
return params.User{}, runnerErrors.NewBadRequestError("invalid username")
}
param.IsAdmin = true
param.Enabled = true
passwordStenght := zxcvbn.PasswordStrength(param.Password, nil)
if passwordStenght.Score < 4 {
return params.User{}, runnerErrors.NewBadRequestError("password is too weak")
}
hashed, err := util.PaswsordToBcrypt(param.Password)
if err != nil {
return params.User{}, fmt.Errorf("error creating user: %w", err)
2022-04-28 16:13:20 +00:00
}
param.Password = hashed
return a.store.CreateUser(ctx, param)
}
func (a *Authenticator) AuthenticateUser(ctx context.Context, info params.PasswordLoginParams) (context.Context, error) {
if info.Username == "" || info.Password == "" {
2022-04-28 16:13:20 +00:00
return ctx, runnerErrors.ErrUnauthorized
}
user, err := a.store.GetUser(ctx, info.Username)
if err != nil {
2022-05-03 12:40:59 +00:00
if errors.Is(err, runnerErrors.ErrNotFound) {
2022-04-28 16:13:20 +00:00
return ctx, runnerErrors.ErrUnauthorized
}
return ctx, fmt.Errorf("error authenticating: %w", err)
2022-04-28 16:13:20 +00:00
}
2022-04-28 16:13:20 +00:00
if !user.Enabled {
return ctx, runnerErrors.ErrUnauthorized
}
if user.Password == "" {
return ctx, runnerErrors.ErrUnauthorized
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(info.Password)); err != nil {
return ctx, runnerErrors.ErrUnauthorized
}
return PopulateContext(ctx, user, nil), nil
2022-04-28 16:13:20 +00:00
}