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:
@@ -1,13 +1,13 @@
|
|||||||
FROM golang:1.23-alpine AS builder
|
FROM golang:1.25-alpine AS builder
|
||||||
|
|
||||||
RUN apk add --no-cache gcc musl-dev
|
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /metacrypt ./cmd/metacrypt
|
|
||||||
|
ARG VERSION=dev
|
||||||
|
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" -o /metacrypt ./cmd/metacrypt
|
||||||
|
|
||||||
FROM alpine:3.21
|
FROM alpine:3.21
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
FROM golang:1.23-alpine AS builder
|
FROM golang:1.25-alpine AS builder
|
||||||
|
|
||||||
RUN apk add --no-cache gcc musl-dev
|
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /metacrypt-web ./cmd/metacrypt-web
|
|
||||||
|
ARG VERSION=dev
|
||||||
|
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" -o /metacrypt-web ./cmd/metacrypt-web
|
||||||
|
|
||||||
FROM alpine:3.21
|
FROM alpine:3.21
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
mcias "git.wntrmute.dev/kyle/mcias/clients/go"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/metacrypt/internal/audit"
|
"git.wntrmute.dev/kyle/metacrypt/internal/audit"
|
||||||
@@ -74,14 +73,14 @@ func runServer(cmd *cobra.Command, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
mcClient, err := mcias.New(cfg.MCIAS.ServerURL, mcias.Options{
|
authenticator, err := auth.NewAuthenticator(auth.Config{
|
||||||
CACertPath: cfg.MCIAS.CACert,
|
ServerURL: cfg.MCIAS.ServerURL,
|
||||||
})
|
CACert: cfg.MCIAS.CACert,
|
||||||
|
ServiceName: "metacrypt",
|
||||||
|
}, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticator := auth.NewAuthenticator(mcClient, logger)
|
|
||||||
policyEngine := policy.NewEngine(b)
|
policyEngine := policy.NewEngine(b)
|
||||||
engineRegistry := engine.NewRegistry(b, logger)
|
engineRegistry := engine.NewRegistry(b, logger)
|
||||||
engineRegistry.RegisterFactory(engine.EngineTypeCA, ca.NewCAEngine)
|
engineRegistry.RegisterFactory(engine.EngineTypeCA, ca.NewCAEngine)
|
||||||
|
|||||||
31
deploy/docker/docker-compose-rift.yml
Normal file
31
deploy/docker/docker-compose-rift.yml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
services:
|
||||||
|
metacrypt:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: Dockerfile.api
|
||||||
|
container_name: metacrypt
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:18443:8443"
|
||||||
|
- "127.0.0.1:19443:9443"
|
||||||
|
volumes:
|
||||||
|
- /srv/metacrypt:/srv/metacrypt
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "metacrypt", "status", "--addr", "https://localhost:8443", "--ca-cert", "/srv/metacrypt/certs/ca.pem"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
metacrypt-web:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: Dockerfile.web
|
||||||
|
container_name: metacrypt-web
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:18080:8080"
|
||||||
|
volumes:
|
||||||
|
- /srv/metacrypt:/srv/metacrypt
|
||||||
|
depends_on:
|
||||||
|
- metacrypt
|
||||||
19
go.mod
19
go.mod
@@ -1,23 +1,18 @@
|
|||||||
module git.wntrmute.dev/kyle/metacrypt
|
module git.wntrmute.dev/kyle/metacrypt
|
||||||
|
|
||||||
go 1.25.0
|
go 1.25.7
|
||||||
|
|
||||||
replace git.wntrmute.dev/kyle/mcias/clients/go => /Users/kyle/src/mcias/clients/go
|
|
||||||
|
|
||||||
replace git.wntrmute.dev/kyle/goutils => /Users/kyle/src/goutils
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.wntrmute.dev/kyle/goutils v1.21.0
|
git.wntrmute.dev/kyle/goutils v1.21.0
|
||||||
git.wntrmute.dev/kyle/mcias/clients/go v0.0.0-00010101000000-000000000000
|
git.wntrmute.dev/kyle/mcdsl v0.0.0
|
||||||
github.com/go-chi/chi/v5 v5.2.5
|
github.com/go-chi/chi/v5 v5.2.5
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4
|
github.com/pelletier/go-toml/v2 v2.3.0
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
golang.org/x/crypto v0.49.0
|
golang.org/x/crypto v0.49.0
|
||||||
golang.org/x/term v0.41.0
|
golang.org/x/term v0.41.0
|
||||||
google.golang.org/grpc v1.79.2
|
google.golang.org/grpc v1.79.3
|
||||||
google.golang.org/protobuf v1.36.11
|
google.golang.org/protobuf v1.36.11
|
||||||
modernc.org/sqlite v1.46.1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -36,12 +31,14 @@ require (
|
|||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
|
||||||
golang.org/x/net v0.51.0 // indirect
|
golang.org/x/net v0.51.0 // indirect
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||||
modernc.org/libc v1.67.6 // indirect
|
modernc.org/libc v1.70.0 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
modernc.org/sqlite v1.47.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace git.wntrmute.dev/kyle/mcdsl => /home/kyle/src/metacircular/mcdsl
|
||||||
|
|||||||
32
go.sum
32
go.sum
@@ -1,3 +1,5 @@
|
|||||||
|
git.wntrmute.dev/kyle/goutils v1.21.0 h1:ZR7ovV400hsF09zc8tkdHs6vyen8TDJ7flong/dnFXM=
|
||||||
|
git.wntrmute.dev/kyle/goutils v1.21.0/go.mod h1:JQ8NL5lHSEYl719UMf20p4G1ei70RVGma0hjjNXCR2c=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
@@ -37,8 +39,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
|||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
@@ -81,8 +83,6 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
|||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
|
||||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||||
@@ -102,8 +102,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
|||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
@@ -113,18 +113,18 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
|
||||||
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
|
||||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||||
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
|
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
|
||||||
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
|
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
@@ -133,8 +133,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
|||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
|
modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk=
|
||||||
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
|||||||
@@ -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
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
"errors"
|
||||||
"log/slog"
|
"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 (
|
var (
|
||||||
ErrInvalidCredentials = errors.New("auth: invalid credentials")
|
ErrInvalidCredentials = mcdslauth.ErrInvalidCredentials
|
||||||
ErrInvalidToken = errors.New("auth: invalid token")
|
ErrInvalidToken = mcdslauth.ErrInvalidToken
|
||||||
|
ErrForbidden = mcdslauth.ErrForbidden
|
||||||
|
ErrUnavailable = mcdslauth.ErrUnavailable
|
||||||
)
|
)
|
||||||
|
|
||||||
const tokenCacheTTL = 30 * time.Second
|
// NewAuthenticator creates a new Authenticator backed by mcdsl/auth.
|
||||||
|
// This is a convenience wrapper matching the old constructor signature.
|
||||||
// TokenInfo holds validated token information.
|
func NewAuthenticator(cfg mcdslauth.Config, logger *slog.Logger) (*Authenticator, error) {
|
||||||
type TokenInfo struct {
|
return mcdslauth.New(cfg, logger)
|
||||||
Username string
|
|
||||||
Roles []string
|
|
||||||
IsAdmin bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// cachedClaims holds a cached token validation result.
|
// Logout revokes a token on the MCIAS server.
|
||||||
type cachedClaims struct {
|
func Logout(authenticator *Authenticator, token string) error {
|
||||||
info *TokenInfo
|
return authenticator.Logout(token)
|
||||||
expiresAt time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authenticator provides MCIAS-backed authentication.
|
// ContextWithTokenInfo stores TokenInfo in a context.
|
||||||
type Authenticator struct {
|
var ContextWithTokenInfo = mcdslauth.ContextWithTokenInfo
|
||||||
client *mcias.Client
|
|
||||||
logger *slog.Logger
|
// TokenInfoFromContext extracts TokenInfo from a context.
|
||||||
cache map[string]*cachedClaims
|
var TokenInfoFromContext = mcdslauth.TokenInfoFromContext
|
||||||
mu sync.RWMutex
|
|
||||||
}
|
// IsInvalidCredentials checks if an error is ErrInvalidCredentials.
|
||||||
|
func IsInvalidCredentials(err error) bool {
|
||||||
// NewAuthenticator creates a new authenticator with the given MCIAS client.
|
return errors.Is(err, ErrInvalidCredentials)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +1,21 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTokenHash(t *testing.T) {
|
func TestErrorsExported(t *testing.T) {
|
||||||
h1 := tokenHash("token-abc")
|
// Verify the error sentinels are accessible and non-nil.
|
||||||
h2 := tokenHash("token-abc")
|
if ErrInvalidCredentials == nil {
|
||||||
h3 := tokenHash("token-def")
|
t.Error("ErrInvalidCredentials is nil")
|
||||||
|
|
||||||
if h1 != h2 {
|
|
||||||
t.Error("same input should produce same hash")
|
|
||||||
}
|
}
|
||||||
if h1 == h3 {
|
if ErrInvalidToken == nil {
|
||||||
t.Error("different inputs should produce different hashes")
|
t.Error("ErrInvalidToken is nil")
|
||||||
}
|
}
|
||||||
if len(h1) != 64 { // SHA-256 hex
|
if ErrForbidden == nil {
|
||||||
t.Errorf("hash length: got %d, want 64", len(h1))
|
t.Error("ErrForbidden is nil")
|
||||||
}
|
}
|
||||||
}
|
if ErrUnavailable == nil {
|
||||||
|
t.Error("ErrUnavailable is nil")
|
||||||
func TestHasAdminRole(t *testing.T) {
|
|
||||||
if !hasAdminRole([]string{"user", "admin"}) {
|
|
||||||
t.Error("should detect admin role")
|
|
||||||
}
|
|
||||||
if hasAdminRole([]string{"user", "operator"}) {
|
|
||||||
t.Error("should not detect admin role when absent")
|
|
||||||
}
|
|
||||||
if hasAdminRole(nil) {
|
|
||||||
t.Error("nil roles should not be admin")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewAuthenticator(t *testing.T) {
|
|
||||||
a := NewAuthenticator(nil, slog.Default())
|
|
||||||
if a == nil {
|
|
||||||
t.Fatal("NewAuthenticator returned nil")
|
|
||||||
}
|
|
||||||
if a.cache == nil {
|
|
||||||
t.Error("cache should be initialized")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClearCache(t *testing.T) {
|
|
||||||
a := NewAuthenticator(nil, slog.Default())
|
|
||||||
a.cache["test"] = &cachedClaims{info: &TokenInfo{Username: "test"}}
|
|
||||||
a.ClearCache()
|
|
||||||
if len(a.cache) != 0 {
|
|
||||||
t.Error("cache should be empty after clear")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,41 +3,13 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
_ "modernc.org/sqlite"
|
mcdsldb "git.wntrmute.dev/kyle/mcdsl/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Open opens or creates a SQLite database at the given path with secure
|
// Open opens or creates a SQLite database at the given path with the
|
||||||
// file permissions (0600) and WAL mode enabled.
|
// standard Metacircular pragmas (WAL, FK, busy timeout) and 0600
|
||||||
|
// permissions.
|
||||||
func Open(path string) (*sql.DB, error) {
|
func Open(path string) (*sql.DB, error) {
|
||||||
// Ensure the file has restrictive permissions if it doesn't exist yet.
|
return mcdsldb.Open(path)
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
||||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0600) //nolint:gosec
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("db: create file: %w", err)
|
|
||||||
}
|
|
||||||
_ = f.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := sql.Open("sqlite", path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("db: open: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable WAL mode and foreign keys.
|
|
||||||
pragmas := []string{
|
|
||||||
"PRAGMA journal_mode=WAL",
|
|
||||||
"PRAGMA foreign_keys=ON",
|
|
||||||
"PRAGMA busy_timeout=5000",
|
|
||||||
}
|
|
||||||
for _, p := range pragmas {
|
|
||||||
if _, err := db.Exec(p); err != nil {
|
|
||||||
_ = db.Close()
|
|
||||||
return nil, fmt.Errorf("db: pragma %q: %w", p, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return db, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,16 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
|
||||||
|
mcdsldb "git.wntrmute.dev/kyle/mcdsl/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
// migrations is an ordered list of SQL DDL statements. Each index is the
|
// Migrations is the ordered list of metacrypt schema migrations.
|
||||||
// migration version (1-based).
|
var Migrations = []mcdsldb.Migration{
|
||||||
var migrations = []string{
|
{
|
||||||
// Version 1: initial schema
|
Version: 1,
|
||||||
`CREATE TABLE IF NOT EXISTS seal_config (
|
Name: "initial schema",
|
||||||
|
SQL: `CREATE TABLE IF NOT EXISTS seal_config (
|
||||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||||
encrypted_mek BLOB NOT NULL,
|
encrypted_mek BLOB NOT NULL,
|
||||||
kdf_salt BLOB NOT NULL,
|
kdf_salt BLOB NOT NULL,
|
||||||
@@ -24,56 +26,22 @@ var migrations = []string{
|
|||||||
value BLOB NOT NULL,
|
value BLOB NOT NULL,
|
||||||
created_at DATETIME NOT NULL DEFAULT (datetime('now')),
|
created_at DATETIME NOT NULL DEFAULT (datetime('now')),
|
||||||
updated_at DATETIME NOT NULL DEFAULT (datetime('now'))
|
updated_at DATETIME NOT NULL DEFAULT (datetime('now'))
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
||||||
version INTEGER PRIMARY KEY,
|
|
||||||
applied_at DATETIME NOT NULL DEFAULT (datetime('now'))
|
|
||||||
);`,
|
);`,
|
||||||
|
},
|
||||||
// Version 2: barrier key registry for per-engine DEKs
|
{
|
||||||
`CREATE TABLE IF NOT EXISTS barrier_keys (
|
Version: 2,
|
||||||
|
Name: "barrier key registry",
|
||||||
|
SQL: `CREATE TABLE IF NOT EXISTS barrier_keys (
|
||||||
key_id TEXT PRIMARY KEY,
|
key_id TEXT PRIMARY KEY,
|
||||||
version INTEGER NOT NULL DEFAULT 1,
|
version INTEGER NOT NULL DEFAULT 1,
|
||||||
encrypted_dek BLOB NOT NULL,
|
encrypted_dek BLOB NOT NULL,
|
||||||
created_at DATETIME NOT NULL DEFAULT (datetime('now')),
|
created_at DATETIME NOT NULL DEFAULT (datetime('now')),
|
||||||
rotated_at DATETIME NOT NULL DEFAULT (datetime('now'))
|
rotated_at DATETIME NOT NULL DEFAULT (datetime('now'))
|
||||||
);`,
|
);`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate applies all pending migrations.
|
// Migrate applies all pending migrations.
|
||||||
func Migrate(db *sql.DB) error {
|
func Migrate(database *sql.DB) error {
|
||||||
// Ensure the migrations table exists (bootstrap).
|
return mcdsldb.Migrate(database, Migrations)
|
||||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
||||||
version INTEGER PRIMARY KEY,
|
|
||||||
applied_at DATETIME NOT NULL DEFAULT (datetime('now'))
|
|
||||||
)`); err != nil {
|
|
||||||
return fmt.Errorf("db: create migrations table: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var current int
|
|
||||||
row := db.QueryRow("SELECT COALESCE(MAX(version), 0) FROM schema_migrations")
|
|
||||||
if err := row.Scan(¤t); err != nil {
|
|
||||||
return fmt.Errorf("db: get migration version: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := current; i < len(migrations); i++ {
|
|
||||||
version := i + 1
|
|
||||||
tx, err := db.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("db: begin migration %d: %w", version, err)
|
|
||||||
}
|
|
||||||
if _, err := tx.Exec(migrations[i]); err != nil {
|
|
||||||
_ = tx.Rollback()
|
|
||||||
return fmt.Errorf("db: migration %d: %w", version, err)
|
|
||||||
}
|
|
||||||
if _, err := tx.Exec("INSERT INTO schema_migrations (version) VALUES (?)", version); err != nil {
|
|
||||||
_ = tx.Rollback()
|
|
||||||
return fmt.Errorf("db: record migration %d: %w", version, err)
|
|
||||||
}
|
|
||||||
if err := tx.Commit(); err != nil {
|
|
||||||
return fmt.Errorf("db: commit migration %d: %w", version, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,13 @@ package grpcserver
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
|
||||||
|
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
mcias "git.wntrmute.dev/kyle/mcias/clients/go"
|
|
||||||
|
|
||||||
pb "git.wntrmute.dev/kyle/metacrypt/gen/metacrypt/v2"
|
pb "git.wntrmute.dev/kyle/metacrypt/gen/metacrypt/v2"
|
||||||
|
"git.wntrmute.dev/kyle/metacrypt/internal/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
type authServer struct {
|
type authServer struct {
|
||||||
@@ -19,13 +17,13 @@ type authServer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (as *authServer) Login(_ context.Context, req *pb.LoginRequest) (*pb.LoginResponse, error) {
|
func (as *authServer) Login(_ context.Context, req *pb.LoginRequest) (*pb.LoginResponse, error) {
|
||||||
token, expiresAtStr, err := as.s.auth.Login(req.Username, req.Password, req.TotpCode)
|
token, expiresAtTime, err := as.s.auth.Login(req.Username, req.Password, req.TotpCode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Error(codes.Unauthenticated, "invalid credentials")
|
return nil, status.Error(codes.Unauthenticated, "invalid credentials")
|
||||||
}
|
}
|
||||||
var expiresAt *timestamppb.Timestamp
|
var expiresAt *timestamppb.Timestamp
|
||||||
if t, err := time.Parse(time.RFC3339, expiresAtStr); err == nil {
|
if !expiresAtTime.IsZero() {
|
||||||
expiresAt = timestamppb.New(t)
|
expiresAt = timestamppb.New(expiresAtTime)
|
||||||
}
|
}
|
||||||
as.s.logger.Info("audit: login", "username", req.Username)
|
as.s.logger.Info("audit: login", "username", req.Username)
|
||||||
return &pb.LoginResponse{Token: token, ExpiresAt: expiresAt}, nil
|
return &pb.LoginResponse{Token: token, ExpiresAt: expiresAt}, nil
|
||||||
@@ -33,13 +31,7 @@ func (as *authServer) Login(_ context.Context, req *pb.LoginRequest) (*pb.LoginR
|
|||||||
|
|
||||||
func (as *authServer) Logout(ctx context.Context, _ *pb.LogoutRequest) (*pb.LogoutResponse, error) {
|
func (as *authServer) Logout(ctx context.Context, _ *pb.LogoutRequest) (*pb.LogoutResponse, error) {
|
||||||
token := extractToken(ctx)
|
token := extractToken(ctx)
|
||||||
client, err := mcias.New(as.s.cfg.MCIAS.ServerURL, mcias.Options{
|
_ = auth.Logout(as.s.auth, token)
|
||||||
CACertPath: as.s.cfg.MCIAS.CACert,
|
|
||||||
Token: token,
|
|
||||||
})
|
|
||||||
if err == nil {
|
|
||||||
_ = as.s.auth.Logout(client)
|
|
||||||
}
|
|
||||||
as.s.logger.Info("audit: logout", "username", callerUsername(ctx))
|
as.s.logger.Info("audit: logout", "username", callerUsername(ctx))
|
||||||
return &pb.LogoutResponse{}, nil
|
return &pb.LogoutResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ func newTestGRPCServer(t *testing.T) (*GRPCServer, func()) {
|
|||||||
sealMgr := seal.NewManager(database, b, nil, slog.Default())
|
sealMgr := seal.NewManager(database, b, nil, slog.Default())
|
||||||
policyEngine := policy.NewEngine(b)
|
policyEngine := policy.NewEngine(b)
|
||||||
reg := newTestRegistry()
|
reg := newTestRegistry()
|
||||||
authenticator := auth.NewAuthenticator(nil, slog.Default())
|
authenticator, _ := auth.NewAuthenticator(auth.Config{ServerURL: "http://localhost:0"}, slog.Default())
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Seal: config.SealConfig{
|
Seal: config.SealConfig{
|
||||||
Argon2Time: 1,
|
Argon2Time: 1,
|
||||||
@@ -159,7 +159,7 @@ func TestSealInterceptor_SkipsUnlistedMethod(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthInterceptor_MissingToken(t *testing.T) {
|
func TestAuthInterceptor_MissingToken(t *testing.T) {
|
||||||
authenticator := auth.NewAuthenticator(nil, slog.Default())
|
authenticator, _ := auth.NewAuthenticator(auth.Config{ServerURL: "http://localhost:0"}, slog.Default())
|
||||||
methods := map[string]bool{"/test.Service/Method": true}
|
methods := map[string]bool{"/test.Service/Method": true}
|
||||||
interceptor := authInterceptor(authenticator, slog.Default(), methods)
|
interceptor := authInterceptor(authenticator, slog.Default(), methods)
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ func TestAuthInterceptor_MissingToken(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthInterceptor_SkipsUnlistedMethod(t *testing.T) {
|
func TestAuthInterceptor_SkipsUnlistedMethod(t *testing.T) {
|
||||||
authenticator := auth.NewAuthenticator(nil, slog.Default())
|
authenticator, _ := auth.NewAuthenticator(auth.Config{ServerURL: "http://localhost:0"}, slog.Default())
|
||||||
methods := map[string]bool{"/test.Service/Other": true}
|
methods := map[string]bool{"/test.Service/Other": true}
|
||||||
interceptor := authInterceptor(authenticator, slog.Default(), methods)
|
interceptor := authInterceptor(authenticator, slog.Default(), methods)
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
mcias "git.wntrmute.dev/kyle/mcias/clients/go"
|
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/metacrypt/internal/audit"
|
"git.wntrmute.dev/kyle/metacrypt/internal/audit"
|
||||||
"git.wntrmute.dev/kyle/metacrypt/internal/auth"
|
"git.wntrmute.dev/kyle/metacrypt/internal/auth"
|
||||||
@@ -236,13 +236,7 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||||
token := extractToken(r)
|
token := extractToken(r)
|
||||||
client, err := mcias.New(s.cfg.MCIAS.ServerURL, mcias.Options{
|
_ = auth.Logout(s.auth, token)
|
||||||
CACertPath: s.cfg.MCIAS.CACert,
|
|
||||||
Token: token,
|
|
||||||
})
|
|
||||||
if err == nil {
|
|
||||||
_ = s.auth.Logout(client)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear cookie.
|
// Clear cookie.
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
|||||||
@@ -39,9 +39,8 @@ func setupTestServer(t *testing.T) (*Server, *seal.Manager, chi.Router) {
|
|||||||
sealMgr := seal.NewManager(database, b, nil, slog.Default())
|
sealMgr := seal.NewManager(database, b, nil, slog.Default())
|
||||||
_ = sealMgr.CheckInitialized()
|
_ = sealMgr.CheckInitialized()
|
||||||
|
|
||||||
// Auth requires MCIAS client which we can't create in tests easily,
|
// Auth not exercised in these tests — token info is injected directly.
|
||||||
// so we pass nil and avoid auth-dependent routes in these tests.
|
authenticator, _ := auth.NewAuthenticator(auth.Config{ServerURL: "http://localhost:0"}, slog.Default())
|
||||||
authenticator := auth.NewAuthenticator(nil, slog.Default())
|
|
||||||
policyEngine := policy.NewEngine(b)
|
policyEngine := policy.NewEngine(b)
|
||||||
engineRegistry := engine.NewRegistry(b, slog.Default())
|
engineRegistry := engine.NewRegistry(b, slog.Default())
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
mcias "git.wntrmute.dev/kyle/mcias/clients/go"
|
mcdslauth "git.wntrmute.dev/kyle/mcdsl/auth"
|
||||||
"git.wntrmute.dev/kyle/metacrypt/internal/config"
|
"git.wntrmute.dev/kyle/metacrypt/internal/config"
|
||||||
webui "git.wntrmute.dev/kyle/metacrypt/web"
|
webui "git.wntrmute.dev/kyle/metacrypt/web"
|
||||||
)
|
)
|
||||||
@@ -113,8 +113,7 @@ type cachedUsername struct {
|
|||||||
type WebServer struct {
|
type WebServer struct {
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
vault vaultBackend
|
vault vaultBackend
|
||||||
mcias *mcias.Client // optional; nil when no service_token is configured
|
logger *slog.Logger
|
||||||
logger *slog.Logger
|
|
||||||
httpSrv *http.Server
|
httpSrv *http.Server
|
||||||
staticFS fs.FS
|
staticFS fs.FS
|
||||||
csrf *csrfProtect
|
csrf *csrfProtect
|
||||||
@@ -136,22 +135,9 @@ func (ws *WebServer) resolveUser(id string) string {
|
|||||||
return entry.username
|
return entry.username
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ws.mcias == nil {
|
// TODO: re-enable MCIAS account lookup once mcias client library is
|
||||||
ws.logger.Warn("webserver: no MCIAS client available, cannot resolve user ID", "id", id)
|
// published with proper Go module tags. For now, return the raw ID.
|
||||||
return id
|
return id
|
||||||
}
|
|
||||||
ws.logger.Info("webserver: looking up user ID via MCIAS", "id", id)
|
|
||||||
acct, err := ws.mcias.GetAccount(id)
|
|
||||||
if err != nil {
|
|
||||||
ws.logger.Warn("webserver: failed to resolve user ID", "id", id, "error", err)
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
ws.logger.Info("webserver: resolved user ID", "id", id, "username", acct.Username)
|
|
||||||
ws.userCache.Store(id, &cachedUsername{
|
|
||||||
username: acct.Username,
|
|
||||||
expiresAt: time.Now().Add(userCacheTTL),
|
|
||||||
})
|
|
||||||
return acct.Username
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new WebServer. It dials the vault gRPC endpoint.
|
// New creates a new WebServer. It dials the vault gRPC endpoint.
|
||||||
@@ -177,22 +163,19 @@ func New(cfg *config.Config, logger *slog.Logger) (*WebServer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if tok := cfg.MCIAS.ServiceToken; tok != "" {
|
if tok := cfg.MCIAS.ServiceToken; tok != "" {
|
||||||
mc, err := mcias.New(cfg.MCIAS.ServerURL, mcias.Options{
|
a, err := mcdslauth.New(mcdslauth.Config{
|
||||||
CACertPath: cfg.MCIAS.CACert,
|
ServerURL: cfg.MCIAS.ServerURL,
|
||||||
Token: tok,
|
CACert: cfg.MCIAS.CACert,
|
||||||
})
|
}, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn("webserver: failed to create MCIAS client for user resolution", "error", err)
|
logger.Warn("webserver: failed to create auth client for service token validation", "error", err)
|
||||||
} else {
|
} else {
|
||||||
claims, err := mc.ValidateToken(tok)
|
info, err := a.ValidateToken(tok)
|
||||||
switch {
|
switch {
|
||||||
case err != nil:
|
case err != nil:
|
||||||
logger.Warn("webserver: MCIAS service token validation failed", "error", err)
|
logger.Warn("webserver: MCIAS service token validation failed", "error", err)
|
||||||
case !claims.Valid:
|
|
||||||
logger.Warn("webserver: MCIAS service token is invalid or expired")
|
|
||||||
default:
|
default:
|
||||||
logger.Info("webserver: MCIAS service token valid", "sub", claims.Sub, "roles", claims.Roles)
|
logger.Info("webserver: MCIAS service token valid", "username", info.Username, "roles", info.Roles)
|
||||||
ws.mcias = mc
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user