WIPL tokens

This commit is contained in:
Tom 2023-06-06 18:26:53 +02:00
parent c09c729157
commit 161e08fe1f
25 changed files with 716 additions and 63 deletions

View file

@ -0,0 +1,90 @@
package tokens
import (
"errors"
"net/http"
"strings"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"golang.org/x/exp/slices"
)
const (
authHeader = "Authorization" // authorization
authBearer = "Bearer" + " " // Prefix for bearer
)
// TokenOf returns the token header found in the given request.
// If r is nil, or there is no token, returns nil.
// Error is only set if there is an error accessing the table that stores tokens.
func (tok *Tokens) TokenOf(r *http.Request) (*models.Token, error) {
if r == nil {
return nil, nil
}
// make sure that the authorization header exists and starts with the bearer
auth := r.Header.Get(authHeader)
if !strings.HasPrefix(auth, authBearer) {
return nil, nil
}
// get the token
id := strings.TrimSpace(strings.TrimPrefix(auth, authBearer))
if id == "" {
return nil, nil
}
table, err := tok.table(r.Context())
if err != nil {
return nil, err
}
// take a single object from the tokens
var tokenObj models.Token
res := table.Where(&models.Token{Token: id}).Find(&tokenObj)
if res.Error != nil {
return nil, errors.Join(ErrNoToken, res.Error)
}
if res.RowsAffected == 0 {
return nil, nil
}
// and return the token object
return &tokenObj, nil
}
var ErrNoToken = errors.New("no token")
// Check checks if there is a token in the given request and if this request has an appropriate token with the appropriate scope.
//
// If the token is found and has the requested token, returns true, nil.
// If there is a token found, but the specific scope is not set, returns false, nil.
// If there is no valid authentication token found, returns false and an error that wraps ErrNoToken.
// In other cases, other errors may be returned.
//
// Note that the scope may require an parameter to be validated.
// This validation should take place in the appropriate ScopeProvider; which should recursively invoke this method.
func (tok *Tokens) Check(r *http.Request, scope component.Scope) (bool, error) {
// get the token object from the request
tokenObj, err := tok.TokenOf(r)
if tokenObj == nil {
if err == nil {
return false, ErrNoToken
}
return false, errors.Join(ErrNoToken, err)
}
// TODO: Do we need this function?
// get the scopes
scopes := tokenObj.GetScopes()
if scopes == nil {
// all scopes (implicitly)
return true, nil
}
// else check if they are contained
return slices.Contains(scopes, string(scope)), nil
}

View file

@ -0,0 +1,148 @@
package tokens
import (
"context"
"crypto/rand"
"strings"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/sql"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/tkw1536/pkglib/password"
"github.com/tkw1536/pkglib/reflectx"
"gorm.io/gorm"
)
// Tokens implements Tokens
type Tokens struct {
component.Base
Dependencies struct {
SQL *sql.SQL
}
}
var (
_ component.UserDeleteHook = (*Tokens)(nil)
_ component.Table = (*Tokens)(nil)
)
func (tok *Tokens) TableInfo() component.TableInfo {
return component.TableInfo{
Name: models.TokensTable,
Model: reflectx.MakeType[models.Token](),
}
}
func (tok *Tokens) table(ctx context.Context) (*gorm.DB, error) {
return tok.Dependencies.SQL.QueryTable(ctx, tok)
}
func (tok *Tokens) OnUserDelete(ctx context.Context, user *models.User) error {
table, err := tok.table(ctx)
if err != nil {
return err
}
return table.Delete(&models.Token{}, &models.Token{User: user.User}).Error
}
// Tokens returns a list of tokens for the given user
func (tok *Tokens) Tokens(ctx context.Context, user string) ([]models.Token, error) {
// the empty user has no tokens
if user == "" {
return nil, nil
}
// get the table
table, err := tok.table(ctx)
if err != nil {
return nil, err
}
var tokens []models.Token
// make a query to find all keys (in the underlying model)
query := table.Find(&tokens, &models.Token{User: user})
if query.Error != nil {
return nil, query.Error
}
return tokens, nil
}
const (
tokenGroupLength = 8
tokenGroupCount = 8
tokenSeparator = "-"
tokenCharset password.Charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
)
// NewToken generates a new token
func NewToken() (string, error) {
// generate a new password
token, err := password.Generate(rand.Reader, tokenGroupCount*tokenGroupLength, tokenCharset)
if err != nil {
return "", err
}
// insert the token group separators
var result strings.Builder
result.Grow(len(token) + (tokenGroupCount-1)*len(tokenSeparator))
for i := 0; i < tokenGroupCount; i++ {
if i != 0 {
result.WriteString(tokenSeparator)
}
start := i * tokenGroupLength
result.WriteString(token[start : start+tokenGroupLength])
}
return result.String(), nil
}
// Add adds a new token, unless it already exists.
// The token is granted scopes with .SetScopes(scopes).
func (tok *Tokens) Add(ctx context.Context, user string, description string, scopes []string) (*models.Token, error) {
// create a new token and set the scopes
mk := models.Token{
User: user,
Description: description,
}
mk.SetScopes(scopes)
// generate a new random password
var err error
mk.Token, err = NewToken()
if err != nil {
return nil, err
}
// get the table
table, err := tok.table(ctx)
if err != nil {
return nil, err
}
// create the token instance
if err := table.Create(&mk).Error; err != nil {
return nil, err
}
// and return
return &mk, nil
}
// Remove removes a token with the given token from the user
func (tok *Tokens) Remove(ctx context.Context, user, token string) error {
// get the table
table, err := tok.table(ctx)
if err != nil {
return err
}
// and do the delete
return table.Where("user = ? AND token = ?", user, token).Delete(&models.Token{}).Error
}