Migrate db, auth to mcdsl; remove mcias client dependency

- db.Open: delegate to mcdsl/db.Open
- db.Migrate: convert to mcdsl/db.Migration format, delegate
- auth: type aliases for TokenInfo/Authenticator/Config from mcdsl,
  re-export error sentinels, Logout helper
- cmd/server: construct auth.Authenticator from Config (not mcias.Client)
- server/routes.go logout: use auth.Logout(authenticator, token)
- grpcserver/auth.go: same logout pattern, fix Login return type
  (time.Time not string)
- webserver: replace mcias.Client with mcdsl/auth for service token
  validation; resolveUser degrades to raw UUID (TODO: restore when
  mcias client library is properly tagged)
- Dockerfiles: bump to golang:1.25-alpine, remove gcc/musl-dev,
  add VERSION build arg
- Deploy: add docker-compose-rift.yml with localhost-only port mapping
- Remove git.wntrmute.dev/kyle/mcias/clients/go dependency entirely
- All tests pass, net -185 lines

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 18:42:43 -07:00
parent 5c5d7e184e
commit dd698ff6d8
15 changed files with 157 additions and 342 deletions

View File

@@ -1,137 +1,49 @@
// Package auth provides MCIAS authentication integration with token caching.
// Package auth provides MCIAS authentication integration, delegating to
// the mcdsl/auth package for token validation with caching.
package auth
import (
"crypto/sha256"
"encoding/hex"
"errors"
"log/slog"
"sync"
"time"
mcias "git.wntrmute.dev/kyle/mcias/clients/go"
mcdslauth "git.wntrmute.dev/kyle/mcdsl/auth"
)
// TokenInfo is an alias for the mcdsl auth.TokenInfo type.
type TokenInfo = mcdslauth.TokenInfo
// Authenticator is an alias for the mcdsl auth.Authenticator type.
type Authenticator = mcdslauth.Authenticator
// Config is an alias for the mcdsl auth.Config type.
type Config = mcdslauth.Config
// Errors re-exported from mcdsl/auth for compatibility.
var (
ErrInvalidCredentials = errors.New("auth: invalid credentials")
ErrInvalidToken = errors.New("auth: invalid token")
ErrInvalidCredentials = mcdslauth.ErrInvalidCredentials
ErrInvalidToken = mcdslauth.ErrInvalidToken
ErrForbidden = mcdslauth.ErrForbidden
ErrUnavailable = mcdslauth.ErrUnavailable
)
const tokenCacheTTL = 30 * time.Second
// TokenInfo holds validated token information.
type TokenInfo struct {
Username string
Roles []string
IsAdmin bool
// NewAuthenticator creates a new Authenticator backed by mcdsl/auth.
// This is a convenience wrapper matching the old constructor signature.
func NewAuthenticator(cfg mcdslauth.Config, logger *slog.Logger) (*Authenticator, error) {
return mcdslauth.New(cfg, logger)
}
// cachedClaims holds a cached token validation result.
type cachedClaims struct {
info *TokenInfo
expiresAt time.Time
// Logout revokes a token on the MCIAS server.
func Logout(authenticator *Authenticator, token string) error {
return authenticator.Logout(token)
}
// Authenticator provides MCIAS-backed authentication.
type Authenticator struct {
client *mcias.Client
logger *slog.Logger
cache map[string]*cachedClaims
mu sync.RWMutex
}
// NewAuthenticator creates a new authenticator with the given MCIAS client.
func NewAuthenticator(client *mcias.Client, logger *slog.Logger) *Authenticator {
return &Authenticator{
client: client,
logger: logger,
cache: make(map[string]*cachedClaims),
}
}
// Login authenticates a user via MCIAS and returns the token.
func (a *Authenticator) Login(username, password, totpCode string) (token string, expiresAt string, err error) {
a.logger.Debug("login attempt", "username", username)
tok, exp, err := a.client.Login(username, password, totpCode)
if err != nil {
var authErr *mcias.MciasAuthError
if errors.As(err, &authErr) {
a.logger.Debug("login failed: invalid credentials", "username", username)
return "", "", ErrInvalidCredentials
}
a.logger.Debug("login failed", "username", username, "error", err)
return "", "", err
}
a.logger.Debug("login succeeded", "username", username)
return tok, exp, nil
}
// ValidateToken validates a bearer token, using a short-lived cache.
func (a *Authenticator) ValidateToken(token string) (*TokenInfo, error) {
key := tokenHash(token)
// Check cache.
a.mu.RLock()
cached, ok := a.cache[key]
a.mu.RUnlock()
if ok && time.Now().Before(cached.expiresAt) {
a.logger.Debug("token validated from cache")
return cached.info, nil
}
a.logger.Debug("validating token with MCIAS")
// Validate with MCIAS.
claims, err := a.client.ValidateToken(token)
if err != nil {
a.logger.Debug("token validation failed", "error", err)
return nil, err
}
if !claims.Valid {
a.logger.Debug("token invalid per MCIAS")
return nil, ErrInvalidToken
}
info := &TokenInfo{
Username: claims.Sub,
Roles: claims.Roles,
IsAdmin: hasAdminRole(claims.Roles),
}
// Cache the result.
a.mu.Lock()
a.cache[key] = &cachedClaims{
info: info,
expiresAt: time.Now().Add(tokenCacheTTL),
}
a.mu.Unlock()
a.logger.Debug("token validated and cached", "username", info.Username, "is_admin", info.IsAdmin)
return info, nil
}
// Logout invalidates a token via MCIAS. The client must have the token set.
func (a *Authenticator) Logout(client *mcias.Client) error {
return client.Logout()
}
// ClearCache removes all cached token validations.
func (a *Authenticator) ClearCache() {
a.logger.Debug("clearing token cache")
a.mu.Lock()
a.cache = make(map[string]*cachedClaims)
a.mu.Unlock()
}
func tokenHash(token string) string {
h := sha256.Sum256([]byte(token))
return hex.EncodeToString(h[:])
}
func hasAdminRole(roles []string) bool {
for _, r := range roles {
if r == "admin" {
return true
}
}
return false
// ContextWithTokenInfo stores TokenInfo in a context.
var ContextWithTokenInfo = mcdslauth.ContextWithTokenInfo
// TokenInfoFromContext extracts TokenInfo from a context.
var TokenInfoFromContext = mcdslauth.TokenInfoFromContext
// IsInvalidCredentials checks if an error is ErrInvalidCredentials.
func IsInvalidCredentials(err error) bool {
return errors.Is(err, ErrInvalidCredentials)
}