Code sync, security fixes

This commit is contained in:
Andy Dixon
2026-08-11 10:40:10 +01:00
parent 4e00deedf8
commit 53dbc28383
141 changed files with 36194 additions and 0 deletions
+191
View File
@@ -0,0 +1,191 @@
package mongo
import (
"context"
"fmt"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo/options"
"github.com/andydixon/relata/internal/domain"
)
// APITokenRepo stores API credentials.
//
// Every method here works in hashes. There is no query by prefix, no partial
// match and no method that returns a secret, because the secret is not stored:
// a database read, a backup or a dump yields nothing that can authenticate.
type APITokenRepo struct{ base }
var _ domain.APITokenRepository = (*APITokenRepo)(nil)
// Create inserts a token record.
func (r *APITokenRepo) Create(ctx context.Context, t *domain.APIToken) error {
ctx, cancel := r.ctx(ctx)
defer cancel()
started := time.Now()
if t.WorkspaceID == "" {
return domain.ErrWorkspaceMiss
}
if t.TokenHash == "" {
return fmt.Errorf("%w: a token record must carry a hash", domain.ErrInvalidInput)
}
_, err := r.coll.InsertOne(ctx, t)
r.observe("create", started, err)
return translate(err)
}
// GetByHash finds a token by the SHA-256 of its secret.
//
// The lookup is on the whole hash against a unique index. There is no
// comparison in application code for an attacker to time, and no way to probe
// for a prefix.
func (r *APITokenRepo) GetByHash(ctx context.Context, hash string) (*domain.APIToken, error) {
ctx, cancel := r.ctx(ctx)
defer cancel()
started := time.Now()
if hash == "" {
return nil, domain.ErrNotFound
}
var t domain.APIToken
err := r.coll.FindOne(ctx, bson.D{{Key: "token_hash", Value: hash}}).Decode(&t)
r.observe("get_by_hash", started, err)
if err != nil {
return nil, translate(err)
}
return &t, nil
}
// ListForUser returns a person's tokens, newest first.
//
// Revoked and expired tokens are included: the point of the list is to show
// what exists and what happened to it, and hiding a revocation would make the
// page a worse audit surface than the log.
func (r *APITokenRepo) ListForUser(ctx context.Context, workspaceID, userID string) ([]*domain.APIToken, error) {
ctx, cancel := r.ctx(ctx)
defer cancel()
started := time.Now()
f, err := wsFilter(workspaceID)
if err != nil {
return nil, err
}
if userID == "" {
return nil, fmt.Errorf("%w: user id must not be empty", domain.ErrInvalidInput)
}
f = append(f, bson.E{Key: "user_id", Value: userID})
opts := options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}}).SetLimit(500)
cur, err := r.coll.Find(ctx, f, opts)
if err != nil {
r.observe("list_for_user", started, err)
return nil, translate(err)
}
defer cur.Close(ctx)
var out []*domain.APIToken
for cur.Next(ctx) {
var t domain.APIToken
if err := cur.Decode(&t); err != nil {
r.observe("list_for_user", started, err)
return nil, err
}
out = append(out, &t)
}
r.observe("list_for_user", started, cur.Err())
return out, cur.Err()
}
// Get fetches one token within a workspace.
func (r *APITokenRepo) Get(ctx context.Context, workspaceID, id string) (*domain.APIToken, error) {
ctx, cancel := r.ctx(ctx)
defer cancel()
started := time.Now()
f, err := wsFilter(workspaceID)
if err != nil {
return nil, err
}
f = append(f, bson.E{Key: "_id", Value: id})
var t domain.APIToken
err = r.coll.FindOne(ctx, f).Decode(&t)
r.observe("get", started, err)
if err != nil {
return nil, translate(err)
}
return &t, nil
}
// Revoke marks a token unusable.
//
// The record is kept rather than deleted, so the token's existence and its end
// stay visible. A revoked token is refused on the next request; there is no
// cached grant to wait out.
func (r *APITokenRepo) Revoke(ctx context.Context, workspaceID, id, revokedBy string, at time.Time) error {
ctx, cancel := r.ctx(ctx)
defer cancel()
started := time.Now()
f, err := wsFilter(workspaceID)
if err != nil {
return err
}
f = append(f, bson.E{Key: "_id", Value: id})
update := bson.D{{Key: "$set", Value: bson.D{
{Key: "revoked_at", Value: at},
{Key: "revoked_by", Value: revokedBy},
}}}
res, err := r.coll.UpdateOne(ctx, f, update)
r.observe("revoke", started, err)
if err != nil {
return translate(err)
}
if res.MatchedCount == 0 {
return domain.ErrNotFound
}
return nil
}
// TouchLastUsed records that a token authenticated a request.
//
// Callers throttle this; the repository does not, because it has no view of how
// often it is being asked.
func (r *APITokenRepo) TouchLastUsed(ctx context.Context, id string, at time.Time) error {
ctx, cancel := r.ctx(ctx)
defer cancel()
started := time.Now()
_, err := r.coll.UpdateOne(ctx,
bson.D{{Key: "_id", Value: id}},
bson.D{{Key: "$set", Value: bson.D{{Key: "last_used_at", Value: at}}}})
r.observe("touch_last_used", started, err)
return translate(err)
}
// DeleteForUser removes every token belonging to a user.
func (r *APITokenRepo) DeleteForUser(ctx context.Context, workspaceID, userID string) (int64, error) {
ctx, cancel := r.ctx(ctx)
defer cancel()
started := time.Now()
f, err := wsFilter(workspaceID)
if err != nil {
return 0, err
}
if userID == "" {
return 0, fmt.Errorf("%w: user id must not be empty", domain.ErrInvalidInput)
}
f = append(f, bson.E{Key: "user_id", Value: userID})
res, err := r.coll.DeleteMany(ctx, f)
r.observe("delete_for_user", started, err)
if err != nil {
return 0, translate(err)
}
return res.DeletedCount, nil
}