Compare commits
9 Commits
v1.7.0
...
clients/go
| Author | SHA1 | Date | |
|---|---|---|---|
| 41d01edfb4 | |||
| 9b521f3d99 | |||
| 115f23a3ea | |||
| 35e96444aa | |||
| db7cd73a6e | |||
| 39d9ffb79a | |||
| b0afe3b993 | |||
| 446b3df52d | |||
| 0b37fde155 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -20,7 +20,8 @@ mcias.toml
|
|||||||
*~
|
*~
|
||||||
go.work
|
go.work
|
||||||
go.work.sum
|
go.work.sum
|
||||||
dist/mcias_*.tar.gz
|
# dist/ is purely build output (tarballs); never commit it
|
||||||
|
dist/
|
||||||
man/man1/*.gz
|
man/man1/*.gz
|
||||||
|
|
||||||
# Client library build artifacts
|
# Client library build artifacts
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
CLAUDE.md
|
|
||||||
@@ -1377,7 +1377,7 @@ Error types exposed by every library:
|
|||||||
|
|
||||||
#### Go (`clients/go/`)
|
#### Go (`clients/go/`)
|
||||||
|
|
||||||
- Module: `git.wntrmute.dev/kyle/mcias/clients/go`
|
- Module: `git.wntrmute.dev/mc/mcias/clients/go`
|
||||||
- Package: `mciasgoclient`
|
- Package: `mciasgoclient`
|
||||||
- HTTP: `net/http` with custom `*tls.Config` for CA cert
|
- HTTP: `net/http` with custom `*tls.Config` for CA cert
|
||||||
- Token state: guarded by `sync.RWMutex`
|
- Token state: guarded by `sync.RWMutex`
|
||||||
|
|||||||
62
Dockerfile
62
Dockerfile
@@ -1,12 +1,16 @@
|
|||||||
# Dockerfile — MCIAS multi-stage container image
|
# Dockerfile — MCIAS multi-stage container image
|
||||||
#
|
#
|
||||||
# Stage 1 (builder): Compiles all four MCIAS binaries.
|
# Stage 1 (builder): Compiles all four MCIAS binaries.
|
||||||
# Stage 2 (runtime): Minimal Debian image containing only the binaries.
|
# Stage 2 (runtime): Minimal Alpine image containing only the binaries.
|
||||||
|
#
|
||||||
|
# modernc.org/sqlite is a pure-Go, CGo-free SQLite port. CGO_ENABLED=0
|
||||||
|
# produces fully static binaries with no C library dependencies, which
|
||||||
|
# deploy cleanly onto a minimal Alpine runtime image.
|
||||||
#
|
#
|
||||||
# The final image:
|
# The final image:
|
||||||
# - Runs as non-root uid 10001 (mcias)
|
# - Runs as non-root uid 10001 (mcias)
|
||||||
# - Exposes port 8443 (REST/TLS) and 9443 (gRPC/TLS)
|
# - Exposes port 8443 (REST/TLS) and 9443 (gRPC/TLS)
|
||||||
# - Declares VOLUME /data for the SQLite database
|
# - Declares VOLUME /srv/mcias for config, TLS, and database
|
||||||
# - Does NOT contain the Go toolchain, source code, or build cache
|
# - Does NOT contain the Go toolchain, source code, or build cache
|
||||||
#
|
#
|
||||||
# Build:
|
# Build:
|
||||||
@@ -15,8 +19,7 @@
|
|||||||
# Run:
|
# Run:
|
||||||
# docker run -d \
|
# docker run -d \
|
||||||
# --name mcias \
|
# --name mcias \
|
||||||
# -v /path/to/config:/etc/mcias:ro \
|
# -v /srv/mcias:/srv/mcias \
|
||||||
# -v mcias-data:/data \
|
|
||||||
# -e MCIAS_MASTER_PASSPHRASE=your-passphrase \
|
# -e MCIAS_MASTER_PASSPHRASE=your-passphrase \
|
||||||
# -p 8443:8443 \
|
# -p 8443:8443 \
|
||||||
# -p 9443:9443 \
|
# -p 9443:9443 \
|
||||||
@@ -25,7 +28,7 @@
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Stage 1 — builder
|
# Stage 1 — builder
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
FROM golang:1.26-bookworm AS builder
|
FROM golang:1.26-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
@@ -36,35 +39,29 @@ RUN go mod download
|
|||||||
# Copy source.
|
# Copy source.
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# CGO_ENABLED=1 is required by modernc.org/sqlite (pure-Go CGo-free SQLite).
|
# CGO_ENABLED=0: modernc.org/sqlite is pure Go; no C toolchain required.
|
||||||
# -trimpath removes local file system paths from the binary.
|
# -trimpath removes local file system paths from the binary.
|
||||||
# -ldflags="-s -w" strips the DWARF debug info and symbol table to reduce
|
# -ldflags="-s -w" strips the DWARF debug info and symbol table to reduce
|
||||||
# image size.
|
# image size.
|
||||||
RUN CGO_ENABLED=1 go build -trimpath -ldflags="-s -w" -o /out/mciassrv ./cmd/mciassrv && \
|
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/mciassrv ./cmd/mciassrv && \
|
||||||
CGO_ENABLED=1 go build -trimpath -ldflags="-s -w" -o /out/mciasctl ./cmd/mciasctl && \
|
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/mciasctl ./cmd/mciasctl && \
|
||||||
CGO_ENABLED=1 go build -trimpath -ldflags="-s -w" -o /out/mciasdb ./cmd/mciasdb && \
|
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/mciasdb ./cmd/mciasdb && \
|
||||||
CGO_ENABLED=1 go build -trimpath -ldflags="-s -w" -o /out/mciasgrpcctl ./cmd/mciasgrpcctl
|
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/mciasgrpcctl ./cmd/mciasgrpcctl
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Stage 2 — runtime
|
# Stage 2 — runtime
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
FROM debian:bookworm-slim
|
FROM alpine:3.21
|
||||||
|
|
||||||
# Install runtime dependencies.
|
|
||||||
# ca-certificates: required to validate external TLS certificates.
|
# ca-certificates: required to validate external TLS certificates.
|
||||||
# libc6: required by CGo-compiled binaries (sqlite).
|
RUN apk add --no-cache ca-certificates
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y --no-install-recommends \
|
|
||||||
ca-certificates \
|
|
||||||
libc6 && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Create a non-root user for the service.
|
# Create a non-root user for the service.
|
||||||
# uid/gid 10001 is chosen to be well above the range typically assigned to
|
# uid/gid 10001 is chosen to be well above the range typically assigned to
|
||||||
# system users (1–999) and human users (1000+), reducing the chance of
|
# system users (1–999) and human users (1000+), reducing the chance of
|
||||||
# collision with existing uids on the host when using host networking.
|
# collision with existing uids on the host when using host networking.
|
||||||
RUN groupadd --gid 10001 mcias && \
|
RUN addgroup -g 10001 mcias && \
|
||||||
useradd --uid 10001 --gid 10001 --no-create-home --shell /usr/sbin/nologin mcias
|
adduser -u 10001 -G mcias -H -s /sbin/nologin -D mcias
|
||||||
|
|
||||||
# Copy compiled binaries from the builder stage.
|
# Copy compiled binaries from the builder stage.
|
||||||
COPY --from=builder /out/mciassrv /usr/local/bin/mciassrv
|
COPY --from=builder /out/mciassrv /usr/local/bin/mciassrv
|
||||||
@@ -72,17 +69,15 @@ COPY --from=builder /out/mciasctl /usr/local/bin/mciasctl
|
|||||||
COPY --from=builder /out/mciasdb /usr/local/bin/mciasdb
|
COPY --from=builder /out/mciasdb /usr/local/bin/mciasdb
|
||||||
COPY --from=builder /out/mciasgrpcctl /usr/local/bin/mciasgrpcctl
|
COPY --from=builder /out/mciasgrpcctl /usr/local/bin/mciasgrpcctl
|
||||||
|
|
||||||
# Create the config and data directories.
|
# Create the data directory.
|
||||||
# /etc/mcias is mounted read-only by the operator with the config file,
|
# /srv/mcias is mounted from the host with config, TLS certs, and database.
|
||||||
# TLS cert, and TLS key.
|
RUN mkdir -p /srv/mcias/certs /srv/mcias/backups && \
|
||||||
# /data is the SQLite database mount point.
|
chown -R mcias:mcias /srv/mcias && \
|
||||||
RUN mkdir -p /etc/mcias /data && \
|
chmod 0750 /srv/mcias
|
||||||
chown mcias:mcias /data && \
|
|
||||||
chmod 0750 /data
|
|
||||||
|
|
||||||
# Declare /data as a volume so the operator must explicitly mount it.
|
# Declare /srv/mcias as a volume so the operator must explicitly mount it.
|
||||||
# The SQLite database must persist across container restarts.
|
# Contains the config file, TLS cert/key, and SQLite database.
|
||||||
VOLUME /data
|
VOLUME /srv/mcias
|
||||||
|
|
||||||
# REST/TLS port and gRPC/TLS port. These are documentation only; the actual
|
# REST/TLS port and gRPC/TLS port. These are documentation only; the actual
|
||||||
# ports are set in the config file. Override by mounting a different config.
|
# ports are set in the config file. Override by mounting a different config.
|
||||||
@@ -93,7 +88,8 @@ EXPOSE 9443
|
|||||||
USER mcias
|
USER mcias
|
||||||
|
|
||||||
# Default entry point and config path.
|
# Default entry point and config path.
|
||||||
# The operator mounts /etc/mcias/mcias.conf from the host or a volume.
|
# The operator mounts /srv/mcias from the host containing mcias.toml,
|
||||||
# See dist/mcias.conf.docker.example for a suitable template.
|
# TLS cert/key, and the SQLite database.
|
||||||
|
# See deploy/examples/mcias.conf.docker.example for a suitable template.
|
||||||
ENTRYPOINT ["mciassrv"]
|
ENTRYPOINT ["mciassrv"]
|
||||||
CMD ["-config", "/etc/mcias/mcias.conf"]
|
CMD ["-config", "/srv/mcias/mcias.toml"]
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ expose the same API surface:
|
|||||||
|
|
||||||
| Language | Location | Install |
|
| Language | Location | Install |
|
||||||
|----------|----------|---------|
|
|----------|----------|---------|
|
||||||
| Go | `clients/go/` | `go get git.wntrmute.dev/kyle/mcias/clients/go` |
|
| Go | `clients/go/` | `go get git.wntrmute.dev/mc/mcias/clients/go` |
|
||||||
| Python | `clients/python/` | `pip install ./clients/python` |
|
| Python | `clients/python/` | `pip install ./clients/python` |
|
||||||
| Rust | `clients/rust/` | `cargo add mcias-client` |
|
| Rust | `clients/rust/` | `cargo add mcias-client` |
|
||||||
| Common Lisp | `clients/lisp/` | ASDF `mcias-client` |
|
| Common Lisp | `clients/lisp/` | ASDF `mcias-client` |
|
||||||
@@ -389,7 +389,7 @@ expose the same API surface:
|
|||||||
### Go
|
### Go
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import mcias "git.wntrmute.dev/kyle/mcias/clients/go"
|
import mcias "git.wntrmute.dev/mc/mcias/clients/go"
|
||||||
|
|
||||||
c, err := mcias.New("https://auth.example.com:8443", "/etc/mcias/server.crt", "")
|
c, err := mcias.New("https://auth.example.com:8443", "/etc/mcias/server.crt", "")
|
||||||
if err != nil { ... }
|
if err != nil { ... }
|
||||||
|
|||||||
81
Makefile
81
Makefile
@@ -3,10 +3,14 @@
|
|||||||
# Usage:
|
# Usage:
|
||||||
# make build — compile all binaries to bin/
|
# make build — compile all binaries to bin/
|
||||||
# make test — run tests with race detector
|
# make test — run tests with race detector
|
||||||
|
# make vet — run go vet
|
||||||
# make lint — run golangci-lint
|
# make lint — run golangci-lint
|
||||||
|
# make all — vet → lint → test → build (CI pipeline)
|
||||||
# make generate — regenerate protobuf stubs (requires protoc)
|
# make generate — regenerate protobuf stubs (requires protoc)
|
||||||
|
# make proto-lint — lint proto files with buf
|
||||||
# make man — build compressed man pages
|
# make man — build compressed man pages
|
||||||
# make install — run dist/install.sh (requires root)
|
# make install — run deploy/scripts/install.sh (requires root)
|
||||||
|
# make devserver — build and run mciassrv against run/ config
|
||||||
# make clean — remove bin/ and generated artifacts
|
# make clean — remove bin/ and generated artifacts
|
||||||
# make dist — build release tarballs for linux/amd64 and linux/arm64
|
# make dist — build release tarballs for linux/amd64 and linux/arm64
|
||||||
# make docker — build Docker image tagged mcias:$(VERSION) and mcias:latest
|
# make docker — build Docker image tagged mcias:$(VERSION) and mcias:latest
|
||||||
@@ -15,7 +19,7 @@
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Variables
|
# Variables
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
MODULE := git.wntrmute.dev/kyle/mcias
|
MODULE := git.wntrmute.dev/mc/mcias
|
||||||
BINARIES := mciassrv mciasctl mciasdb mciasgrpcctl
|
BINARIES := mciassrv mciasctl mciasdb mciasgrpcctl
|
||||||
BIN_DIR := bin
|
BIN_DIR := bin
|
||||||
MAN_DIR := man/man1
|
MAN_DIR := man/man1
|
||||||
@@ -27,20 +31,25 @@ MAN_PAGES := $(MAN_DIR)/mciassrv.1 $(MAN_DIR)/mciasctl.1 \
|
|||||||
VERSION := $(shell git describe --tags --always 2>/dev/null || echo dev)
|
VERSION := $(shell git describe --tags --always 2>/dev/null || echo dev)
|
||||||
|
|
||||||
# Build flags: trim paths from binaries and strip DWARF/symbol table.
|
# Build flags: trim paths from binaries and strip DWARF/symbol table.
|
||||||
# CGO_ENABLED=1 is required for modernc.org/sqlite.
|
# modernc.org/sqlite is pure-Go and does not require CGo; CGO_ENABLED=0
|
||||||
|
# produces statically linked binaries that deploy cleanly to Alpine containers.
|
||||||
GO := go
|
GO := go
|
||||||
GOFLAGS := -trimpath
|
GOFLAGS := -trimpath
|
||||||
LDFLAGS := -s -w -X main.version=$(VERSION)
|
LDFLAGS := -s -w -X main.version=$(VERSION)
|
||||||
CGO := CGO_ENABLED=1
|
CGO := CGO_ENABLED=0
|
||||||
|
|
||||||
|
# The race detector requires CGo on some platforms, so tests continue to use
|
||||||
|
# CGO_ENABLED=1 while production builds are CGO_ENABLED=0.
|
||||||
|
CGO_TEST := CGO_ENABLED=1
|
||||||
|
|
||||||
# Platforms for cross-compiled dist tarballs.
|
# Platforms for cross-compiled dist tarballs.
|
||||||
DIST_PLATFORMS := linux/amd64 linux/arm64
|
DIST_PLATFORMS := linux/amd64 linux/arm64
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Default target
|
# Default target — CI pipeline: vet → lint → test → build
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
.PHONY: all
|
.PHONY: all
|
||||||
all: build
|
all: vet lint test build
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# build — compile all binaries to bin/
|
# build — compile all binaries to bin/
|
||||||
@@ -59,7 +68,14 @@ build:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test:
|
test:
|
||||||
$(CGO) $(GO) test -race ./...
|
$(CGO_TEST) $(GO) test -race ./...
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# vet — static analysis via go vet
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
.PHONY: vet
|
||||||
|
vet:
|
||||||
|
$(GO) vet ./...
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# lint — run golangci-lint
|
# lint — run golangci-lint
|
||||||
@@ -68,6 +84,15 @@ test:
|
|||||||
lint:
|
lint:
|
||||||
golangci-lint run ./...
|
golangci-lint run ./...
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# proto-lint — lint and check for breaking changes in proto definitions
|
||||||
|
# Requires: buf (https://buf.build/docs/installation)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
.PHONY: proto-lint
|
||||||
|
proto-lint:
|
||||||
|
buf lint
|
||||||
|
buf breaking --against '.git#branch=master,subdir=proto'
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# generate — regenerate protobuf stubs from proto/ definitions
|
# generate — regenerate protobuf stubs from proto/ definitions
|
||||||
# Requires: protoc, protoc-gen-go, protoc-gen-go-grpc
|
# Requires: protoc, protoc-gen-go, protoc-gen-go-grpc
|
||||||
@@ -76,6 +101,13 @@ lint:
|
|||||||
generate:
|
generate:
|
||||||
$(GO) generate ./...
|
$(GO) generate ./...
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# devserver — build and run mciassrv against the local run/ config
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
.PHONY: devserver
|
||||||
|
devserver: build
|
||||||
|
$(BIN_DIR)/mciassrv -config run/mcias.conf
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# man — build compressed man pages
|
# man — build compressed man pages
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -90,7 +122,7 @@ man: $(patsubst %.1,%.1.gz,$(MAN_PAGES))
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
.PHONY: install
|
.PHONY: install
|
||||||
install: build
|
install: build
|
||||||
sh dist/install.sh
|
sh deploy/scripts/install.sh
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# clean — remove build artifacts
|
# clean — remove build artifacts
|
||||||
@@ -98,6 +130,7 @@ install: build
|
|||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(BIN_DIR)
|
rm -rf $(BIN_DIR)
|
||||||
|
rm -rf dist/
|
||||||
rm -f $(patsubst %.1,%.1.gz,$(MAN_PAGES))
|
rm -f $(patsubst %.1,%.1.gz,$(MAN_PAGES))
|
||||||
-docker rmi mcias:$(VERSION) mcias:latest 2>/dev/null || true
|
-docker rmi mcias:$(VERSION) mcias:latest 2>/dev/null || true
|
||||||
|
|
||||||
@@ -106,7 +139,7 @@ clean:
|
|||||||
#
|
#
|
||||||
# Output files: dist/mcias_<version>_<os>_<arch>.tar.gz
|
# Output files: dist/mcias_<version>_<os>_<arch>.tar.gz
|
||||||
# Each tarball contains: mciassrv, mciasctl, mciasdb, mciasgrpcctl,
|
# Each tarball contains: mciassrv, mciasctl, mciasdb, mciasgrpcctl,
|
||||||
# man pages, and dist/ files.
|
# man pages, and deploy/ files.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
.PHONY: dist
|
.PHONY: dist
|
||||||
dist: man
|
dist: man
|
||||||
@@ -117,14 +150,12 @@ dist: man
|
|||||||
echo " DIST $$platform -> $$outdir.tar.gz"; \
|
echo " DIST $$platform -> $$outdir.tar.gz"; \
|
||||||
mkdir -p $$outdir/bin; \
|
mkdir -p $$outdir/bin; \
|
||||||
for bin in $(BINARIES); do \
|
for bin in $(BINARIES); do \
|
||||||
CGO_ENABLED=1 GOOS=$$os GOARCH=$$arch $(GO) build \
|
CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch $(GO) build \
|
||||||
$(GOFLAGS) -ldflags "$(LDFLAGS)" \
|
$(GOFLAGS) -ldflags "$(LDFLAGS)" \
|
||||||
-o $$outdir/bin/$$bin ./cmd/$$bin; \
|
-o $$outdir/bin/$$bin ./cmd/$$bin; \
|
||||||
done; \
|
done; \
|
||||||
cp -r man $$outdir/; \
|
cp -r man $$outdir/; \
|
||||||
cp dist/mcias.conf.example dist/mcias-dev.conf.example \
|
cp -r deploy $$outdir/; \
|
||||||
dist/mcias.env.example dist/mcias.service \
|
|
||||||
dist/install.sh $$outdir/; \
|
|
||||||
tar -czf $$outdir.tar.gz -C dist mcias_$$(echo $(VERSION) | tr -d 'v')_$${os}_$${arch}; \
|
tar -czf $$outdir.tar.gz -C dist mcias_$$(echo $(VERSION) | tr -d 'v')_$${os}_$${arch}; \
|
||||||
rm -rf $$outdir; \
|
rm -rf $$outdir; \
|
||||||
done
|
done
|
||||||
@@ -134,7 +165,7 @@ dist: man
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
.PHONY: docker
|
.PHONY: docker
|
||||||
docker:
|
docker:
|
||||||
docker build -t mcias:$(VERSION) -t mcias:latest .
|
docker build --force-rm -t mcias:$(VERSION) -t mcias:latest .
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# docker-clean — remove local mcias Docker images
|
# docker-clean — remove local mcias Docker images
|
||||||
@@ -154,13 +185,17 @@ install-local: build
|
|||||||
.PHONY: help
|
.PHONY: help
|
||||||
help:
|
help:
|
||||||
@echo "Available targets:"
|
@echo "Available targets:"
|
||||||
@echo " build Compile all binaries to bin/"
|
@echo " all vet → lint → test → build (CI pipeline)"
|
||||||
@echo " test Run tests with race detector"
|
@echo " build Compile all binaries to bin/"
|
||||||
@echo " lint Run golangci-lint"
|
@echo " test Run tests with race detector"
|
||||||
@echo " generate Regenerate protobuf stubs"
|
@echo " vet Run go vet"
|
||||||
@echo " man Build compressed man pages"
|
@echo " lint Run golangci-lint"
|
||||||
@echo " install Install to /usr/local/bin (requires root)"
|
@echo " proto-lint Lint proto files with buf"
|
||||||
@echo " clean Remove build artifacts"
|
@echo " generate Regenerate protobuf stubs"
|
||||||
@echo " dist Build release tarballs for Linux amd64/arm64"
|
@echo " devserver Build and run mciassrv against run/ config"
|
||||||
@echo " docker Build Docker image mcias:$(VERSION) and mcias:latest"
|
@echo " man Build compressed man pages"
|
||||||
|
@echo " install Install to /usr/local/bin (requires root)"
|
||||||
|
@echo " clean Remove build artifacts"
|
||||||
|
@echo " dist Build release tarballs for Linux amd64/arm64"
|
||||||
|
@echo " docker Build Docker image mcias:$(VERSION) and mcias:latest"
|
||||||
@echo " docker-clean Remove local mcias Docker images"
|
@echo " docker-clean Remove local mcias Docker images"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ features implemented beyond the original plan scope.
|
|||||||
|
|
||||||
### Step 0.1: Go module and dependency setup
|
### Step 0.1: Go module and dependency setup
|
||||||
**Acceptance criteria:**
|
**Acceptance criteria:**
|
||||||
- `go.mod` exists with module path `git.wntrmute.dev/kyle/mcias`
|
- `go.mod` exists with module path `git.wntrmute.dev/mc/mcias`
|
||||||
- Required dependencies declared: `modernc.org/sqlite` (CGo-free SQLite),
|
- Required dependencies declared: `modernc.org/sqlite` (CGo-free SQLite),
|
||||||
`golang.org/x/crypto` (Argon2, Ed25519 helpers), `github.com/golang-jwt/jwt/v5`,
|
`golang.org/x/crypto` (Argon2, Ed25519 helpers), `github.com/golang-jwt/jwt/v5`,
|
||||||
`github.com/pelletier/go-toml/v2`, `github.com/google/uuid`,
|
`github.com/pelletier/go-toml/v2`, `github.com/google/uuid`,
|
||||||
@@ -543,7 +543,7 @@ implementation notes.
|
|||||||
|
|
||||||
### Step 9.2: Go client library
|
### Step 9.2: Go client library
|
||||||
**Acceptance criteria:**
|
**Acceptance criteria:**
|
||||||
- `clients/go/` — Go module `git.wntrmute.dev/kyle/mcias/clients/go`
|
- `clients/go/` — Go module `git.wntrmute.dev/mc/mcias/clients/go`
|
||||||
- Package `mciasgoclient` exposes the canonical API surface from Step 9.1
|
- Package `mciasgoclient` exposes the canonical API surface from Step 9.1
|
||||||
- Uses `net/http` with `crypto/tls`; custom CA cert supported via `x509.CertPool`
|
- Uses `net/http` with `crypto/tls`; custom CA cert supported via `x509.CertPool`
|
||||||
- Token stored in-memory; `Client.Token()` accessor returns current token
|
- Token stored in-memory; `Client.Token()` accessor returns current token
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ See [ARCHITECTURE.md](ARCHITECTURE.md) for the technical design and
|
|||||||
**Prerequisites:** Go 1.26+, a C compiler (required by modernc.org/sqlite).
|
**Prerequisites:** Go 1.26+, a C compiler (required by modernc.org/sqlite).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://git.wntrmute.dev/kyle/mcias
|
git clone https://git.wntrmute.dev/mc/mcias
|
||||||
cd mcias
|
cd mcias
|
||||||
make build # produces bin/mciassrv, other binaries
|
make build # produces bin/mciassrv, other binaries
|
||||||
sudo make install
|
sudo make install
|
||||||
|
|||||||
90
RUNBOOK.md
90
RUNBOOK.md
@@ -322,6 +322,83 @@ mciasdb $CONF audit query --json
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## WebAuthn / Passkey Configuration
|
||||||
|
|
||||||
|
WebAuthn enables passwordless passkey login and hardware security key 2FA.
|
||||||
|
It is **disabled by default** — to enable it, add a `[webauthn]` section to
|
||||||
|
`mcias.toml` with the relying party ID and origin.
|
||||||
|
|
||||||
|
### Enable WebAuthn
|
||||||
|
|
||||||
|
Add to `/srv/mcias/mcias.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[webauthn]
|
||||||
|
rp_id = "auth.example.com"
|
||||||
|
rp_origin = "https://auth.example.com"
|
||||||
|
display_name = "MCIAS"
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`rp_id`** — The domain name (no scheme or port). Must match the domain
|
||||||
|
users see in their browser address bar.
|
||||||
|
- **`rp_origin`** — The full HTTPS origin. Include the port if non-standard
|
||||||
|
(e.g., `https://localhost:8443` for development).
|
||||||
|
- **`display_name`** — Shown to users during browser passkey prompts. Defaults
|
||||||
|
to "MCIAS" if omitted.
|
||||||
|
|
||||||
|
Restart the server after changing the config:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
systemctl restart mcias
|
||||||
|
```
|
||||||
|
|
||||||
|
Once enabled, the **Passkeys** section appears on the user's Profile page
|
||||||
|
(self-service enrollment) and on the admin Account Detail page (credential
|
||||||
|
management).
|
||||||
|
|
||||||
|
### Passkey enrollment
|
||||||
|
|
||||||
|
Passkey enrollment is self-service only. Users add passkeys from their
|
||||||
|
**Profile → Passkeys** section. Admins can view and remove passkeys from
|
||||||
|
the Account Detail page but cannot enroll on behalf of users (passkey
|
||||||
|
registration requires the authenticator device to be present).
|
||||||
|
|
||||||
|
### Disable WebAuthn
|
||||||
|
|
||||||
|
Remove or comment out the `[webauthn]` section and restart. Existing
|
||||||
|
credentials remain in the database but are unused. Passkey UI sections
|
||||||
|
will be hidden.
|
||||||
|
|
||||||
|
### Remove all passkeys for an account (break-glass)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mciasdb --config /srv/mcias/mcias.toml account reset-webauthn --id <UUID>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TOTP Two-Factor Authentication
|
||||||
|
|
||||||
|
TOTP enrollment is self-service via the **Profile → Two-Factor Authentication**
|
||||||
|
section. Users enter their current password to begin enrollment, scan the QR
|
||||||
|
code with an authenticator app, and confirm with a 6-digit code.
|
||||||
|
|
||||||
|
### Admin: Remove TOTP for an account
|
||||||
|
|
||||||
|
From the web UI: navigate to the account's detail page and click **Remove**
|
||||||
|
next to the TOTP status.
|
||||||
|
|
||||||
|
From the CLI:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mciasdb --config /srv/mcias/mcias.toml account reset-totp --id <UUID>
|
||||||
|
```
|
||||||
|
|
||||||
|
This clears the TOTP secret and disables the 2FA requirement. The user can
|
||||||
|
re-enroll from their Profile page.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Master Key Rotation
|
## Master Key Rotation
|
||||||
|
|
||||||
> This operation is not yet automated. Until a rotation command is
|
> This operation is not yet automated. Until a rotation command is
|
||||||
@@ -384,6 +461,19 @@ See `dist/mcias.conf.docker.example` for the full annotated Docker config.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## MCP Deployment
|
||||||
|
|
||||||
|
MCIAS is **not** managed by MCP and does not run on rift. Because MCIAS is the
|
||||||
|
authentication root for the entire platform — including MCP itself — running it
|
||||||
|
under MCP would create a circular dependency. Instead, MCIAS runs as a systemd
|
||||||
|
service on a separate VPS (`svc.metacircular.net`).
|
||||||
|
|
||||||
|
All deployment, upgrades, and operational tasks use systemd directly on the VPS.
|
||||||
|
See the [Installation](#installation), [Routine Operations](#routine-operations),
|
||||||
|
and [Upgrading](#upgrading) sections above for the relevant procedures.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Server fails to start: "open database"
|
### Server fails to start: "open database"
|
||||||
|
|||||||
14
buf.yaml
Normal file
14
buf.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
version: v2
|
||||||
|
modules:
|
||||||
|
- path: proto
|
||||||
|
lint:
|
||||||
|
use:
|
||||||
|
- STANDARD
|
||||||
|
except:
|
||||||
|
# PACKAGE_VERSION_SUFFIX requires package names to end in a version (e.g.
|
||||||
|
# mcias.v1). The current protos use mcias.v1 already so this is fine, but
|
||||||
|
# keeping the exception documents the intent explicitly.
|
||||||
|
- PACKAGE_VERSION_SUFFIX
|
||||||
|
breaking:
|
||||||
|
use:
|
||||||
|
- FILE
|
||||||
@@ -29,7 +29,7 @@ set_pg_creds(account_id, host, port, database, username, password) → void
|
|||||||
| `MciasConflictError` | 409 | Conflict (e.g. duplicate username) |
|
| `MciasConflictError` | 409 | Conflict (e.g. duplicate username) |
|
||||||
| `MciasServerError` | 5xx | Unexpected server error |
|
| `MciasServerError` | 5xx | Unexpected server error |
|
||||||
`testdata/` contains canonical JSON response fixtures shared across language tests.
|
`testdata/` contains canonical JSON response fixtures shared across language tests.
|
||||||
- `go/` — Go module `git.wntrmute.dev/kyle/mcias/clients/go`
|
- `go/` — Go module `git.wntrmute.dev/mc/mcias/clients/go`
|
||||||
- `rust/` — Rust crate `mcias-client`
|
- `rust/` — Rust crate `mcias-client`
|
||||||
- `lisp/` — ASDF system `mcias-client`
|
- `lisp/` — ASDF system `mcias-client`
|
||||||
- `python/` — Python package `mcias_client`
|
- `python/` — Python package `mcias_client`
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ Go client library for the [MCIAS](../../README.md) identity and access managemen
|
|||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go get git.wntrmute.dev/kyle/mcias/clients/go
|
go get git.wntrmute.dev/mc/mcias/clients/go
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "git.wntrmute.dev/kyle/mcias/clients/go/mcias"
|
import "git.wntrmute.dev/mc/mcias/clients/go/mcias"
|
||||||
|
|
||||||
// Connect to the MCIAS server.
|
// Connect to the MCIAS server.
|
||||||
client, err := mcias.New("https://auth.example.com", mcias.Options{})
|
client, err := mcias.New("https://auth.example.com", mcias.Options{})
|
||||||
|
|||||||
@@ -185,16 +185,28 @@ type Options struct {
|
|||||||
CACertPath string
|
CACertPath string
|
||||||
// Token is an optional pre-existing bearer token.
|
// Token is an optional pre-existing bearer token.
|
||||||
Token string
|
Token string
|
||||||
|
// ServiceName is the name of this service as registered in MCIAS. It is
|
||||||
|
// sent with every Login call so MCIAS can evaluate service-context policy
|
||||||
|
// rules (e.g. deny guest users from logging into this service).
|
||||||
|
// Populate from [mcias] service_name in the service's config file.
|
||||||
|
ServiceName string
|
||||||
|
// Tags are the service-level tags sent with every Login call. MCIAS
|
||||||
|
// evaluates auth:login policy against these tags, enabling rules such as
|
||||||
|
// "deny guest accounts from services tagged env:restricted".
|
||||||
|
// Populate from [mcias] tags in the service's config file.
|
||||||
|
Tags []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client is a thread-safe MCIAS REST API client.
|
// Client is a thread-safe MCIAS REST API client.
|
||||||
// Security: the bearer token is guarded by a sync.RWMutex; it is never
|
// Security: the bearer token is guarded by a sync.RWMutex; it is never
|
||||||
// written to logs or included in error messages in this library.
|
// written to logs or included in error messages in this library.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
baseURL string
|
baseURL string
|
||||||
http *http.Client
|
http *http.Client
|
||||||
mu sync.RWMutex
|
serviceName string
|
||||||
token string
|
tags []string
|
||||||
|
mu sync.RWMutex
|
||||||
|
token string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -224,9 +236,11 @@ func New(serverURL string, opts Options) (*Client, error) {
|
|||||||
}
|
}
|
||||||
transport := &http.Transport{TLSClientConfig: tlsCfg}
|
transport := &http.Transport{TLSClientConfig: tlsCfg}
|
||||||
c := &Client{
|
c := &Client{
|
||||||
baseURL: serverURL,
|
baseURL: serverURL,
|
||||||
http: &http.Client{Transport: transport},
|
http: &http.Client{Transport: transport},
|
||||||
token: opts.Token,
|
token: opts.Token,
|
||||||
|
serviceName: opts.ServiceName,
|
||||||
|
tags: opts.Tags,
|
||||||
}
|
}
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
@@ -343,16 +357,28 @@ func (c *Client) GetPublicKey() (*PublicKey, error) {
|
|||||||
// Login authenticates with username and password. On success the token is
|
// Login authenticates with username and password. On success the token is
|
||||||
// stored in the Client and returned along with the expiry timestamp.
|
// stored in the Client and returned along with the expiry timestamp.
|
||||||
// totpCode may be empty for accounts without TOTP.
|
// totpCode may be empty for accounts without TOTP.
|
||||||
|
//
|
||||||
|
// The client's ServiceName and Tags (from Options) are included in the
|
||||||
|
// request so MCIAS can evaluate service-context policy rules.
|
||||||
func (c *Client) Login(username, password, totpCode string) (token, expiresAt string, err error) {
|
func (c *Client) Login(username, password, totpCode string) (token, expiresAt string, err error) {
|
||||||
req := map[string]string{"username": username, "password": password}
|
body := map[string]interface{}{
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
}
|
||||||
if totpCode != "" {
|
if totpCode != "" {
|
||||||
req["totp_code"] = totpCode
|
body["totp_code"] = totpCode
|
||||||
|
}
|
||||||
|
if c.serviceName != "" {
|
||||||
|
body["service_name"] = c.serviceName
|
||||||
|
}
|
||||||
|
if len(c.tags) > 0 {
|
||||||
|
body["tags"] = c.tags
|
||||||
}
|
}
|
||||||
var resp struct {
|
var resp struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
ExpiresAt string `json:"expires_at"`
|
ExpiresAt string `json:"expires_at"`
|
||||||
}
|
}
|
||||||
if err := c.do(http.MethodPost, "/v1/auth/login", req, &resp); err != nil {
|
if err := c.do(http.MethodPost, "/v1/auth/login", body, &resp); err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
c.setToken(resp.Token)
|
c.setToken(resp.Token)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
mcias "git.wntrmute.dev/kyle/mcias/clients/go"
|
mcias "git.wntrmute.dev/mc/mcias/clients/go"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
module git.wntrmute.dev/kyle/mcias/clients/go
|
module git.wntrmute.dev/mc/mcias/clients/go
|
||||||
|
|
||||||
go 1.21
|
go 1.21
|
||||||
|
|||||||
@@ -20,9 +20,13 @@ class Client:
|
|||||||
ca_cert_path: str | None = None,
|
ca_cert_path: str | None = None,
|
||||||
token: str | None = None,
|
token: str | None = None,
|
||||||
timeout: float = 30.0,
|
timeout: float = 30.0,
|
||||||
|
service_name: str | None = None,
|
||||||
|
tags: list[str] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._base_url = server_url.rstrip("/")
|
self._base_url = server_url.rstrip("/")
|
||||||
self.token = token
|
self.token = token
|
||||||
|
self._service_name = service_name
|
||||||
|
self._tags = tags or []
|
||||||
ssl_context: ssl.SSLContext | bool
|
ssl_context: ssl.SSLContext | bool
|
||||||
if ca_cert_path is not None:
|
if ca_cert_path is not None:
|
||||||
ssl_context = ssl.create_default_context(cafile=ca_cert_path)
|
ssl_context = ssl.create_default_context(cafile=ca_cert_path)
|
||||||
@@ -115,6 +119,9 @@ class Client:
|
|||||||
) -> tuple[str, str]:
|
) -> tuple[str, str]:
|
||||||
"""POST /v1/auth/login — authenticate and obtain a JWT.
|
"""POST /v1/auth/login — authenticate and obtain a JWT.
|
||||||
Returns (token, expires_at). Stores the token on self.token.
|
Returns (token, expires_at). Stores the token on self.token.
|
||||||
|
|
||||||
|
The client's service_name and tags are included so MCIAS can evaluate
|
||||||
|
service-context policy rules (e.g. deny guests from restricted services).
|
||||||
"""
|
"""
|
||||||
payload: dict[str, Any] = {
|
payload: dict[str, Any] = {
|
||||||
"username": username,
|
"username": username,
|
||||||
@@ -122,6 +129,10 @@ class Client:
|
|||||||
}
|
}
|
||||||
if totp_code is not None:
|
if totp_code is not None:
|
||||||
payload["totp_code"] = totp_code
|
payload["totp_code"] = totp_code
|
||||||
|
if self._service_name is not None:
|
||||||
|
payload["service_name"] = self._service_name
|
||||||
|
if self._tags:
|
||||||
|
payload["tags"] = self._tags
|
||||||
data = self._request("POST", "/v1/auth/login", json=payload)
|
data = self._request("POST", "/v1/auth/login", json=payload)
|
||||||
assert data is not None
|
assert data is not None
|
||||||
token = str(data["token"])
|
token = str(data["token"])
|
||||||
|
|||||||
@@ -227,6 +227,10 @@ struct LoginRequest<'a> {
|
|||||||
password: &'a str,
|
password: &'a str,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
totp_code: Option<&'a str>,
|
totp_code: Option<&'a str>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
service_name: Option<&'a str>,
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
tags: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -268,6 +272,16 @@ pub struct ClientOptions {
|
|||||||
|
|
||||||
/// Optional pre-existing bearer token.
|
/// Optional pre-existing bearer token.
|
||||||
pub token: Option<String>,
|
pub token: Option<String>,
|
||||||
|
|
||||||
|
/// This service's name as registered in MCIAS. Sent with every login
|
||||||
|
/// request so MCIAS can evaluate service-context policy rules.
|
||||||
|
/// Populate from `[mcias] service_name` in the service config.
|
||||||
|
pub service_name: Option<String>,
|
||||||
|
|
||||||
|
/// Service-level tags sent with every login request. MCIAS evaluates
|
||||||
|
/// `auth:login` policy against these tags.
|
||||||
|
/// Populate from `[mcias] tags` in the service config.
|
||||||
|
pub tags: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Client ----
|
// ---- Client ----
|
||||||
@@ -280,6 +294,8 @@ pub struct ClientOptions {
|
|||||||
pub struct Client {
|
pub struct Client {
|
||||||
base_url: String,
|
base_url: String,
|
||||||
http: reqwest::Client,
|
http: reqwest::Client,
|
||||||
|
service_name: Option<String>,
|
||||||
|
tags: Vec<String>,
|
||||||
/// Bearer token storage. `Arc<RwLock<...>>` so clones share the token.
|
/// Bearer token storage. `Arc<RwLock<...>>` so clones share the token.
|
||||||
/// Security: the token is never logged or included in error messages.
|
/// Security: the token is never logged or included in error messages.
|
||||||
token: Arc<RwLock<Option<String>>>,
|
token: Arc<RwLock<Option<String>>>,
|
||||||
@@ -306,6 +322,8 @@ impl Client {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
base_url: base_url.trim_end_matches('/').to_owned(),
|
base_url: base_url.trim_end_matches('/').to_owned(),
|
||||||
http,
|
http,
|
||||||
|
service_name: opts.service_name,
|
||||||
|
tags: opts.tags,
|
||||||
token: Arc::new(RwLock::new(opts.token)),
|
token: Arc::new(RwLock::new(opts.token)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -336,6 +354,8 @@ impl Client {
|
|||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
totp_code,
|
totp_code,
|
||||||
|
service_name: self.service_name.as_deref(),
|
||||||
|
tags: self.tags.clone(),
|
||||||
};
|
};
|
||||||
let resp: TokenResponse = self.post("/v1/auth/login", &body).await?;
|
let resp: TokenResponse = self.post("/v1/auth/login", &body).await?;
|
||||||
*self.token.write().await = Some(resp.token.clone());
|
*self.token.write().await = Some(resp.token.clone());
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (t *tool) runAudit(args []string) {
|
func (t *tool) runAudit(args []string) {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
// pgcreds get --id UUID
|
// pgcreds get --id UUID
|
||||||
// pgcreds set --id UUID --host H --port P --db D --user U
|
// pgcreds set --id UUID --host H --port P --db D --user U
|
||||||
//
|
//
|
||||||
// rekey
|
// snapshot [--retain-days N]
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -49,9 +49,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/config"
|
"git.wntrmute.dev/mc/mcias/internal/config"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -68,6 +68,14 @@ func main() {
|
|||||||
command := args[0]
|
command := args[0]
|
||||||
subArgs := args[1:]
|
subArgs := args[1:]
|
||||||
|
|
||||||
|
// snapshot loads only the config (no master key needed — VACUUM INTO does
|
||||||
|
// not access encrypted columns) and must be handled before openDB, which
|
||||||
|
// requires the master key passphrase env var.
|
||||||
|
if command == "snapshot" {
|
||||||
|
runSnapshot(*configPath, subArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// schema subcommands manage migrations themselves and must not trigger
|
// schema subcommands manage migrations themselves and must not trigger
|
||||||
// auto-migration on open (a dirty database would prevent the tool from
|
// auto-migration on open (a dirty database would prevent the tool from
|
||||||
// opening at all, blocking recovery operations like "schema force").
|
// opening at all, blocking recovery operations like "schema force").
|
||||||
@@ -273,6 +281,11 @@ Commands:
|
|||||||
rekey Re-encrypt all secrets under a new master passphrase
|
rekey Re-encrypt all secrets under a new master passphrase
|
||||||
(prompts interactively; requires server to be stopped)
|
(prompts interactively; requires server to be stopped)
|
||||||
|
|
||||||
|
snapshot Write a timestamped VACUUM INTO backup to
|
||||||
|
<db-dir>/backups/; prune backups older than
|
||||||
|
--retain-days days (default 30, 0 = keep all).
|
||||||
|
Does not require the master key passphrase.
|
||||||
|
|
||||||
NOTE: mciasdb bypasses the mciassrv API and operates directly on the SQLite
|
NOTE: mciasdb bypasses the mciassrv API and operates directly on the SQLite
|
||||||
file. Use it only when the server is unavailable or for break-glass recovery.
|
file. Use it only when the server is unavailable or for break-glass recovery.
|
||||||
All write operations are recorded in the audit log.
|
All write operations are recorded in the audit log.
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// newTestTool creates a tool backed by an in-memory SQLite database with a
|
// newTestTool creates a tool backed by an in-memory SQLite database with a
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (t *tool) runPGCreds(args []string) {
|
func (t *tool) runPGCreds(args []string) {
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
// runRekey re-encrypts all secrets under a new passphrase-derived master key.
|
// runRekey re-encrypts all secrets under a new passphrase-derived master key.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (t *tool) runSchema(args []string) {
|
func (t *tool) runSchema(args []string) {
|
||||||
|
|||||||
44
cmd/mciasdb/snapshot.go
Normal file
44
cmd/mciasdb/snapshot.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/mc/mcias/internal/config"
|
||||||
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// runSnapshot handles the "snapshot" command.
|
||||||
|
//
|
||||||
|
// It opens the database read-only (no master key derivation needed — VACUUM
|
||||||
|
// INTO does not access encrypted columns) and writes a timestamped backup to
|
||||||
|
// /srv/mcias/backups/ (or the directory adjacent to the configured DB path).
|
||||||
|
// Backups older than --retain-days are pruned.
|
||||||
|
func runSnapshot(configPath string, args []string) {
|
||||||
|
fs := flag.NewFlagSet("snapshot", flag.ExitOnError)
|
||||||
|
retainDays := fs.Int("retain-days", 30, "prune backups older than this many days (0 = keep all)")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
fatalf("snapshot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := config.Load(configPath)
|
||||||
|
if err != nil {
|
||||||
|
fatalf("snapshot: load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
database, err := db.Open(cfg.Database.Path)
|
||||||
|
if err != nil {
|
||||||
|
fatalf("snapshot: open database: %v", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = database.Close() }()
|
||||||
|
|
||||||
|
// Place backups in a "backups" directory adjacent to the database file.
|
||||||
|
backupDir := filepath.Join(filepath.Dir(cfg.Database.Path), "backups")
|
||||||
|
|
||||||
|
dest, err := database.SnapshotDir(backupDir, *retainDays)
|
||||||
|
if err != nil {
|
||||||
|
fatalf("snapshot: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("snapshot written: %s\n", dest)
|
||||||
|
}
|
||||||
@@ -64,7 +64,7 @@ import (
|
|||||||
"google.golang.org/grpc/credentials"
|
"google.golang.org/grpc/credentials"
|
||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
|
|
||||||
mciasv1 "git.wntrmute.dev/kyle/mcias/gen/mcias/v1"
|
mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -31,12 +31,12 @@ import (
|
|||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials"
|
"google.golang.org/grpc/credentials"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/config"
|
"git.wntrmute.dev/mc/mcias/internal/config"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/grpcserver"
|
"git.wntrmute.dev/mc/mcias/internal/grpcserver"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/server"
|
"git.wntrmute.dev/mc/mcias/internal/server"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -41,3 +41,10 @@ threads = 4
|
|||||||
|
|
||||||
[master_key]
|
[master_key]
|
||||||
passphrase_env = "MCIAS_MASTER_PASSPHRASE"
|
passphrase_env = "MCIAS_MASTER_PASSPHRASE"
|
||||||
|
|
||||||
|
# WebAuthn — passkey authentication for local development.
|
||||||
|
# rp_origin includes the non-standard port since we're not behind a proxy.
|
||||||
|
[webauthn]
|
||||||
|
rp_id = "localhost"
|
||||||
|
rp_origin = "https://localhost:8443"
|
||||||
|
display_name = "MCIAS (dev)"
|
||||||
@@ -48,3 +48,14 @@ threads = 4
|
|||||||
# Set it with: docker run -e MCIAS_MASTER_PASSPHRASE=your-passphrase ...
|
# Set it with: docker run -e MCIAS_MASTER_PASSPHRASE=your-passphrase ...
|
||||||
# or with a Docker secret / Kubernetes secret.
|
# or with a Docker secret / Kubernetes secret.
|
||||||
passphrase_env = "MCIAS_MASTER_PASSPHRASE"
|
passphrase_env = "MCIAS_MASTER_PASSPHRASE"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# [webauthn] — FIDO2/WebAuthn passkey authentication (OPTIONAL)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Uncomment to enable passwordless passkey login. Set rp_id to your domain
|
||||||
|
# and rp_origin to the full HTTPS origin users access in their browser.
|
||||||
|
#
|
||||||
|
# [webauthn]
|
||||||
|
# rp_id = "auth.example.com"
|
||||||
|
# rp_origin = "https://auth.example.com"
|
||||||
|
# display_name = "MCIAS"
|
||||||
@@ -123,3 +123,24 @@ passphrase_env = "MCIAS_MASTER_PASSPHRASE"
|
|||||||
#
|
#
|
||||||
# Uncomment and comment out passphrase_env to switch modes.
|
# Uncomment and comment out passphrase_env to switch modes.
|
||||||
# keyfile = "/srv/mcias/master.key"
|
# keyfile = "/srv/mcias/master.key"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# [webauthn] — FIDO2/WebAuthn passkey authentication (OPTIONAL)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Enables passwordless passkey login and hardware security key 2FA.
|
||||||
|
# If this section is omitted or rp_id/rp_origin are empty, WebAuthn is
|
||||||
|
# disabled and passkey options will not appear in the UI.
|
||||||
|
#
|
||||||
|
# [webauthn]
|
||||||
|
#
|
||||||
|
# REQUIRED (if enabling). The Relying Party ID — typically the domain name
|
||||||
|
# (without port or scheme). Must match the domain users see in their browser.
|
||||||
|
# rp_id = "auth.example.com"
|
||||||
|
#
|
||||||
|
# REQUIRED (if enabling). The Relying Party Origin — the full origin URL
|
||||||
|
# including scheme. Must be HTTPS. Include the port if non-standard (not 443).
|
||||||
|
# rp_origin = "https://auth.example.com"
|
||||||
|
#
|
||||||
|
# OPTIONAL. Display name shown to users during passkey registration prompts.
|
||||||
|
# Default: "MCIAS".
|
||||||
|
# display_name = "MCIAS"
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# install.sh — MCIAS first-time and upgrade installer
|
# install.sh — MCIAS first-time and upgrade installer
|
||||||
#
|
#
|
||||||
# Usage: sh dist/install.sh
|
# Usage: sh deploy/scripts/install.sh
|
||||||
#
|
#
|
||||||
# This script must be run as root. It:
|
# This script must be run as root. It:
|
||||||
# 1. Creates the mcias system user and group (idempotent).
|
# 1. Creates the mcias system user and group (idempotent).
|
||||||
# 2. Copies binaries to /usr/local/bin/.
|
# 2. Copies binaries to /usr/local/bin/.
|
||||||
# 3. Creates /srv/mcias/ with correct permissions.
|
# 3. Creates /srv/mcias/ with correct permissions.
|
||||||
# 4. Installs the systemd service unit.
|
# 4. Installs the systemd service and backup units.
|
||||||
# 5. Prints post-install instructions.
|
# 5. Prints post-install instructions.
|
||||||
#
|
#
|
||||||
# The script does NOT start or enable the service automatically. Review the
|
# The script does NOT start or enable the service automatically. Review the
|
||||||
@@ -31,7 +31,8 @@ SYSTEMD_DIR="/etc/systemd/system"
|
|||||||
SERVICE_USER="mcias"
|
SERVICE_USER="mcias"
|
||||||
SERVICE_GROUP="mcias"
|
SERVICE_GROUP="mcias"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
|
DEPLOY_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
REPO_ROOT="$(dirname "$DEPLOY_DIR")"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Helpers
|
# Helpers
|
||||||
@@ -100,11 +101,7 @@ fi
|
|||||||
# Step 2: Install binaries.
|
# Step 2: Install binaries.
|
||||||
info "Installing binaries to $BIN_DIR"
|
info "Installing binaries to $BIN_DIR"
|
||||||
for bin in mciassrv mciasctl mciasdb mciasgrpcctl; do
|
for bin in mciassrv mciasctl mciasdb mciasgrpcctl; do
|
||||||
src="$REPO_ROOT/$bin"
|
src="$REPO_ROOT/bin/$bin"
|
||||||
if [ ! -f "$src" ]; then
|
|
||||||
# Try bin/ subdirectory (Makefile build output).
|
|
||||||
src="$REPO_ROOT/bin/$bin"
|
|
||||||
fi
|
|
||||||
if [ ! -f "$src" ]; then
|
if [ ! -f "$src" ]; then
|
||||||
warn "Binary not found: $bin — skipping. Run 'make build' first."
|
warn "Binary not found: $bin — skipping. Run 'make build' first."
|
||||||
continue
|
continue
|
||||||
@@ -113,30 +110,40 @@ for bin in mciassrv mciasctl mciasdb mciasgrpcctl; do
|
|||||||
install -m 0755 -o root -g root "$src" "$BIN_DIR/$bin"
|
install -m 0755 -o root -g root "$src" "$BIN_DIR/$bin"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Step 3: Create service directory.
|
# Step 3: Create service directory structure.
|
||||||
info "Creating $SRV_DIR"
|
info "Creating $SRV_DIR"
|
||||||
install -d -m 0750 -o "$SERVICE_USER" -g "$SERVICE_GROUP" "$SRV_DIR"
|
install -d -m 0750 -o "$SERVICE_USER" -g "$SERVICE_GROUP" "$SRV_DIR"
|
||||||
|
install -d -m 0750 -o "$SERVICE_USER" -g "$SERVICE_GROUP" "$SRV_DIR/certs"
|
||||||
|
install -d -m 0750 -o "$SERVICE_USER" -g "$SERVICE_GROUP" "$SRV_DIR/backups"
|
||||||
|
|
||||||
# Install example config files; never overwrite existing configs.
|
# Install example config files; never overwrite existing configs.
|
||||||
for f in mcias.conf.example mcias.env.example; do
|
for f in mcias.conf.example mcias.env.example; do
|
||||||
src="$SCRIPT_DIR/$f"
|
src="$DEPLOY_DIR/examples/$f"
|
||||||
dst="$SRV_DIR/$f"
|
dst="$SRV_DIR/$f"
|
||||||
if [ -f "$src" ]; then
|
if [ -f "$src" ]; then
|
||||||
install -m 0640 -o "$SERVICE_USER" -g "$SERVICE_GROUP" "$src" "$dst" 2>/dev/null || true
|
install -m 0640 -o "$SERVICE_USER" -g "$SERVICE_GROUP" "$src" "$dst" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Step 5: Install systemd service unit.
|
# Step 4: Install systemd units.
|
||||||
if [ -d "$SYSTEMD_DIR" ]; then
|
if [ -d "$SYSTEMD_DIR" ]; then
|
||||||
info "Installing systemd service unit to $SYSTEMD_DIR"
|
info "Installing systemd units to $SYSTEMD_DIR"
|
||||||
install -m 0644 -o root -g root "$SCRIPT_DIR/mcias.service" "$SYSTEMD_DIR/mcias.service"
|
for unit in mcias.service mcias-backup.service mcias-backup.timer; do
|
||||||
|
src="$DEPLOY_DIR/systemd/$unit"
|
||||||
|
if [ -f "$src" ]; then
|
||||||
|
install -m 0644 -o root -g root "$src" "$SYSTEMD_DIR/$unit"
|
||||||
|
info " Installed $unit"
|
||||||
|
fi
|
||||||
|
done
|
||||||
info "Reloading systemd daemon"
|
info "Reloading systemd daemon"
|
||||||
systemctl daemon-reload 2>/dev/null || warn "systemctl not available; reload manually."
|
systemctl daemon-reload 2>/dev/null || warn "systemctl not available; reload manually."
|
||||||
|
info "Enabling backup timer"
|
||||||
|
systemctl enable mcias-backup.timer 2>/dev/null || warn "Could not enable timer; enable manually with: systemctl enable mcias-backup.timer"
|
||||||
else
|
else
|
||||||
warn "systemd not found at $SYSTEMD_DIR; skipping service unit installation."
|
warn "systemd not found at $SYSTEMD_DIR; skipping service unit installation."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Step 6: Install man pages.
|
# Step 5: Install man pages.
|
||||||
if [ -d "$REPO_ROOT/man/man1" ]; then
|
if [ -d "$REPO_ROOT/man/man1" ]; then
|
||||||
install -d -m 0755 -o root -g root "$MAN_DIR"
|
install -d -m 0755 -o root -g root "$MAN_DIR"
|
||||||
info "Installing man pages to $MAN_DIR"
|
info "Installing man pages to $MAN_DIR"
|
||||||
@@ -170,12 +177,12 @@ Next steps:
|
|||||||
|
|
||||||
# Self-signed (development / personal use):
|
# Self-signed (development / personal use):
|
||||||
openssl req -x509 -newkey ed25519 -days 3650 \\
|
openssl req -x509 -newkey ed25519 -days 3650 \\
|
||||||
-keyout /srv/mcias/server.key \\
|
-keyout /srv/mcias/certs/server.key \\
|
||||||
-out /srv/mcias/server.crt \\
|
-out /srv/mcias/certs/server.crt \\
|
||||||
-subj "/CN=auth.example.com" \\
|
-subj "/CN=auth.example.com" \\
|
||||||
-nodes
|
-nodes
|
||||||
chmod 0640 /srv/mcias/server.key
|
chmod 0640 /srv/mcias/certs/server.key
|
||||||
chown mcias:mcias /srv/mcias/server.key /srv/mcias/server.crt
|
chown mcias:mcias /srv/mcias/certs/server.key /srv/mcias/certs/server.crt
|
||||||
|
|
||||||
2. Copy and edit the configuration file:
|
2. Copy and edit the configuration file:
|
||||||
|
|
||||||
@@ -200,6 +207,9 @@ Next steps:
|
|||||||
systemctl start mcias
|
systemctl start mcias
|
||||||
systemctl status mcias
|
systemctl status mcias
|
||||||
|
|
||||||
|
The backup timer was enabled automatically. Verify with:
|
||||||
|
systemctl status mcias-backup.timer
|
||||||
|
|
||||||
5. Create the first admin account using mciasdb (while the server is
|
5. Create the first admin account using mciasdb (while the server is
|
||||||
running, or before first start):
|
running, or before first start):
|
||||||
|
|
||||||
32
deploy/systemd/mcias-backup.service
Normal file
32
deploy/systemd/mcias-backup.service
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=MCIAS Database Backup
|
||||||
|
Documentation=man:mciasdb(1)
|
||||||
|
After=mcias.service
|
||||||
|
# Backup runs against the live database using VACUUM INTO, which is safe
|
||||||
|
# while mciassrv is running (WAL mode allows concurrent readers).
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
User=mcias
|
||||||
|
Group=mcias
|
||||||
|
|
||||||
|
EnvironmentFile=/srv/mcias/env
|
||||||
|
|
||||||
|
ExecStart=/usr/local/bin/mciasdb -config /srv/mcias/mcias.toml snapshot
|
||||||
|
|
||||||
|
# Filesystem restrictions (read-write to /srv/mcias for the backup output).
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ReadWritePaths=/srv/mcias
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateDevices=true
|
||||||
|
CapabilityBoundingSet=
|
||||||
|
RestrictSUIDSGID=true
|
||||||
|
RestrictNamespaces=true
|
||||||
|
RestrictRealtime=true
|
||||||
|
LockPersonality=true
|
||||||
|
MemoryDenyWriteExecute=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectControlGroups=true
|
||||||
15
deploy/systemd/mcias-backup.timer
Normal file
15
deploy/systemd/mcias-backup.timer
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Daily MCIAS Database Backup
|
||||||
|
Documentation=man:mciasdb(1)
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
# Run daily at 02:00 UTC with up to 5-minute random jitter to avoid
|
||||||
|
# thundering-herd on systems with many services.
|
||||||
|
OnCalendar=*-*-* 02:00:00 UTC
|
||||||
|
RandomizedDelaySec=5min
|
||||||
|
# Run immediately on boot if the last scheduled run was missed
|
||||||
|
# (e.g. host was offline at 02:00).
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
@@ -12,7 +12,7 @@ Group=mcias
|
|||||||
|
|
||||||
# Configuration and secrets.
|
# Configuration and secrets.
|
||||||
# /srv/mcias/env must contain MCIAS_MASTER_PASSPHRASE=<passphrase>
|
# /srv/mcias/env must contain MCIAS_MASTER_PASSPHRASE=<passphrase>
|
||||||
# See dist/mcias.env.example for the template.
|
# See deploy/examples/mcias.env.example for the template.
|
||||||
EnvironmentFile=/srv/mcias/env
|
EnvironmentFile=/srv/mcias/env
|
||||||
|
|
||||||
ExecStart=/usr/local/bin/mciassrv -config /srv/mcias/mcias.toml
|
ExecStart=/usr/local/bin/mciassrv -config /srv/mcias/mcias.toml
|
||||||
@@ -42,6 +42,7 @@ PrivateDevices=true
|
|||||||
ProtectKernelTunables=true
|
ProtectKernelTunables=true
|
||||||
ProtectKernelModules=true
|
ProtectKernelModules=true
|
||||||
ProtectControlGroups=true
|
ProtectControlGroups=true
|
||||||
|
RestrictSUIDSGID=true
|
||||||
RestrictNamespaces=true
|
RestrictNamespaces=true
|
||||||
RestrictRealtime=true
|
RestrictRealtime=true
|
||||||
LockPersonality=true
|
LockPersonality=true
|
||||||
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1774273680,
|
||||||
|
"narHash": "sha256-a++tZ1RQsDb1I0NHrFwdGuRlR5TORvCEUksM459wKUA=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "fdc7b8f7b30fdbedec91b71ed82f36e1637483ed",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
34
flake.nix
Normal file
34
flake.nix
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
description = "mcias - Metacircular Identity and Access Service";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{ self, nixpkgs }:
|
||||||
|
let
|
||||||
|
system = "x86_64-linux";
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
version = "1.7.0";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages.${system} = {
|
||||||
|
default = pkgs.buildGoModule {
|
||||||
|
pname = "mciasctl";
|
||||||
|
inherit version;
|
||||||
|
src = ./.;
|
||||||
|
vendorHash = null;
|
||||||
|
subPackages = [
|
||||||
|
"cmd/mciasctl"
|
||||||
|
"cmd/mciasgrpcctl"
|
||||||
|
];
|
||||||
|
ldflags = [
|
||||||
|
"-s"
|
||||||
|
"-w"
|
||||||
|
"-X main.version=${version}"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1080,7 +1080,7 @@ const file_mcias_v1_account_proto_rawDesc = "" +
|
|||||||
"\n" +
|
"\n" +
|
||||||
"GetPGCreds\x12\x1b.mcias.v1.GetPGCredsRequest\x1a\x1c.mcias.v1.GetPGCredsResponse\x12G\n" +
|
"GetPGCreds\x12\x1b.mcias.v1.GetPGCredsRequest\x1a\x1c.mcias.v1.GetPGCredsResponse\x12G\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"SetPGCreds\x12\x1b.mcias.v1.SetPGCredsRequest\x1a\x1c.mcias.v1.SetPGCredsResponseB2Z0git.wntrmute.dev/kyle/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
"SetPGCreds\x12\x1b.mcias.v1.SetPGCredsRequest\x1a\x1c.mcias.v1.SetPGCredsResponseB2Z0git.wntrmute.dev/mc/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
file_mcias_v1_account_proto_rawDescOnce sync.Once
|
file_mcias_v1_account_proto_rawDescOnce sync.Once
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ const file_mcias_v1_admin_proto_rawDesc = "" +
|
|||||||
"\x01x\x18\x05 \x01(\tR\x01x2\x9a\x01\n" +
|
"\x01x\x18\x05 \x01(\tR\x01x2\x9a\x01\n" +
|
||||||
"\fAdminService\x12;\n" +
|
"\fAdminService\x12;\n" +
|
||||||
"\x06Health\x12\x17.mcias.v1.HealthRequest\x1a\x18.mcias.v1.HealthResponse\x12M\n" +
|
"\x06Health\x12\x17.mcias.v1.HealthRequest\x1a\x18.mcias.v1.HealthResponse\x12M\n" +
|
||||||
"\fGetPublicKey\x12\x1d.mcias.v1.GetPublicKeyRequest\x1a\x1e.mcias.v1.GetPublicKeyResponseB2Z0git.wntrmute.dev/kyle/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
"\fGetPublicKey\x12\x1d.mcias.v1.GetPublicKeyRequest\x1a\x1e.mcias.v1.GetPublicKeyResponseB2Z0git.wntrmute.dev/mc/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
file_mcias_v1_admin_proto_rawDescOnce sync.Once
|
file_mcias_v1_admin_proto_rawDescOnce sync.Once
|
||||||
|
|||||||
@@ -919,7 +919,7 @@ const file_mcias_v1_auth_proto_rawDesc = "" +
|
|||||||
"\n" +
|
"\n" +
|
||||||
"RemoveTOTP\x12\x1b.mcias.v1.RemoveTOTPRequest\x1a\x1c.mcias.v1.RemoveTOTPResponse\x12n\n" +
|
"RemoveTOTP\x12\x1b.mcias.v1.RemoveTOTPRequest\x1a\x1c.mcias.v1.RemoveTOTPResponse\x12n\n" +
|
||||||
"\x17ListWebAuthnCredentials\x12(.mcias.v1.ListWebAuthnCredentialsRequest\x1a).mcias.v1.ListWebAuthnCredentialsResponse\x12q\n" +
|
"\x17ListWebAuthnCredentials\x12(.mcias.v1.ListWebAuthnCredentialsRequest\x1a).mcias.v1.ListWebAuthnCredentialsResponse\x12q\n" +
|
||||||
"\x18RemoveWebAuthnCredential\x12).mcias.v1.RemoveWebAuthnCredentialRequest\x1a*.mcias.v1.RemoveWebAuthnCredentialResponseB2Z0git.wntrmute.dev/kyle/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
"\x18RemoveWebAuthnCredential\x12).mcias.v1.RemoveWebAuthnCredentialRequest\x1a*.mcias.v1.RemoveWebAuthnCredentialResponseB2Z0git.wntrmute.dev/mc/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
file_mcias_v1_auth_proto_rawDescOnce sync.Once
|
file_mcias_v1_auth_proto_rawDescOnce sync.Once
|
||||||
|
|||||||
@@ -349,7 +349,7 @@ const file_mcias_v1_common_proto_rawDesc = "" +
|
|||||||
"\x04port\x18\x05 \x01(\x05R\x04port\"5\n" +
|
"\x04port\x18\x05 \x01(\x05R\x04port\"5\n" +
|
||||||
"\x05Error\x12\x18\n" +
|
"\x05Error\x12\x18\n" +
|
||||||
"\amessage\x18\x01 \x01(\tR\amessage\x12\x12\n" +
|
"\amessage\x18\x01 \x01(\tR\amessage\x12\x12\n" +
|
||||||
"\x04code\x18\x02 \x01(\tR\x04codeB2Z0git.wntrmute.dev/kyle/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
"\x04code\x18\x02 \x01(\tR\x04codeB2Z0git.wntrmute.dev/mc/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
file_mcias_v1_common_proto_rawDescOnce sync.Once
|
file_mcias_v1_common_proto_rawDescOnce sync.Once
|
||||||
|
|||||||
@@ -703,7 +703,7 @@ const file_mcias_v1_policy_proto_rawDesc = "" +
|
|||||||
"\x10CreatePolicyRule\x12!.mcias.v1.CreatePolicyRuleRequest\x1a\".mcias.v1.CreatePolicyRuleResponse\x12P\n" +
|
"\x10CreatePolicyRule\x12!.mcias.v1.CreatePolicyRuleRequest\x1a\".mcias.v1.CreatePolicyRuleResponse\x12P\n" +
|
||||||
"\rGetPolicyRule\x12\x1e.mcias.v1.GetPolicyRuleRequest\x1a\x1f.mcias.v1.GetPolicyRuleResponse\x12Y\n" +
|
"\rGetPolicyRule\x12\x1e.mcias.v1.GetPolicyRuleRequest\x1a\x1f.mcias.v1.GetPolicyRuleResponse\x12Y\n" +
|
||||||
"\x10UpdatePolicyRule\x12!.mcias.v1.UpdatePolicyRuleRequest\x1a\".mcias.v1.UpdatePolicyRuleResponse\x12Y\n" +
|
"\x10UpdatePolicyRule\x12!.mcias.v1.UpdatePolicyRuleRequest\x1a\".mcias.v1.UpdatePolicyRuleResponse\x12Y\n" +
|
||||||
"\x10DeletePolicyRule\x12!.mcias.v1.DeletePolicyRuleRequest\x1a\".mcias.v1.DeletePolicyRuleResponseB2Z0git.wntrmute.dev/kyle/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
"\x10DeletePolicyRule\x12!.mcias.v1.DeletePolicyRuleRequest\x1a\".mcias.v1.DeletePolicyRuleResponseB2Z0git.wntrmute.dev/mc/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
file_mcias_v1_policy_proto_rawDescOnce sync.Once
|
file_mcias_v1_policy_proto_rawDescOnce sync.Once
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ const file_mcias_v1_token_proto_rawDesc = "" +
|
|||||||
"\fTokenService\x12P\n" +
|
"\fTokenService\x12P\n" +
|
||||||
"\rValidateToken\x12\x1e.mcias.v1.ValidateTokenRequest\x1a\x1f.mcias.v1.ValidateTokenResponse\x12\\\n" +
|
"\rValidateToken\x12\x1e.mcias.v1.ValidateTokenRequest\x1a\x1f.mcias.v1.ValidateTokenResponse\x12\\\n" +
|
||||||
"\x11IssueServiceToken\x12\".mcias.v1.IssueServiceTokenRequest\x1a#.mcias.v1.IssueServiceTokenResponse\x12J\n" +
|
"\x11IssueServiceToken\x12\".mcias.v1.IssueServiceTokenRequest\x1a#.mcias.v1.IssueServiceTokenResponse\x12J\n" +
|
||||||
"\vRevokeToken\x12\x1c.mcias.v1.RevokeTokenRequest\x1a\x1d.mcias.v1.RevokeTokenResponseB2Z0git.wntrmute.dev/kyle/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
"\vRevokeToken\x12\x1c.mcias.v1.RevokeTokenRequest\x1a\x1d.mcias.v1.RevokeTokenResponseB2Z0git.wntrmute.dev/mc/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
file_mcias_v1_token_proto_rawDescOnce sync.Once
|
file_mcias_v1_token_proto_rawDescOnce sync.Once
|
||||||
|
|||||||
7
go.mod
7
go.mod
@@ -1,12 +1,14 @@
|
|||||||
module git.wntrmute.dev/kyle/mcias
|
module git.wntrmute.dev/mc/mcias
|
||||||
|
|
||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/go-webauthn/webauthn v0.16.1
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.1
|
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4
|
github.com/pelletier/go-toml/v2 v2.2.4
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
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.74.2
|
google.golang.org/grpc v1.74.2
|
||||||
@@ -18,14 +20,11 @@ require (
|
|||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/go-webauthn/webauthn v0.16.1 // indirect
|
|
||||||
github.com/go-webauthn/x v0.2.2 // indirect
|
github.com/go-webauthn/x v0.2.2 // indirect
|
||||||
github.com/google/go-tpm v0.9.8 // indirect
|
github.com/google/go-tpm v0.9.8 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
|
||||||
github.com/stretchr/testify v1.11.1 // indirect
|
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // 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
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -24,6 +24,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||||
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||||
|
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc=
|
||||||
|
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:EFYHy8/1y2KfgTAsx7Luu7NGhoxtuVHnNo8jE7FikKc=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
@@ -60,6 +62,8 @@ go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFw
|
|||||||
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||||
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
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 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import (
|
|||||||
|
|
||||||
"golang.org/x/crypto/argon2"
|
"golang.org/x/crypto/argon2"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrInvalidCredentials is returned for any authentication failure.
|
// ErrInvalidCredentials is returned for any authentication failure.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// openTestDB opens an in-memory SQLite database for testing.
|
// openTestDB opens an in-memory SQLite database for testing.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// openTestDB is defined in db_test.go in this package; reused here.
|
// openTestDB is defined in db_test.go in this package; reused here.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ListCredentialedAccountIDs returns the set of account IDs that already have
|
// ListCredentialedAccountIDs returns the set of account IDs that already have
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// policyRuleCols is the column list for all policy rule SELECT queries.
|
// policyRuleCols is the column list for all policy rule SELECT queries.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCreateAndGetPolicyRule(t *testing.T) {
|
func TestCreateAndGetPolicyRule(t *testing.T) {
|
||||||
|
|||||||
68
internal/db/snapshot.go
Normal file
68
internal/db/snapshot.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Snapshot creates a consistent backup of the database at destPath using
|
||||||
|
// SQLite's VACUUM INTO statement. VACUUM INTO acquires a read lock for the
|
||||||
|
// duration of the copy, which is safe while the server is running in WAL mode.
|
||||||
|
// The destination file is created by SQLite; the caller must ensure the parent
|
||||||
|
// directory exists.
|
||||||
|
func (db *DB) Snapshot(destPath string) error {
|
||||||
|
// VACUUM INTO is not supported on in-memory databases.
|
||||||
|
if strings.Contains(db.path, "mode=memory") {
|
||||||
|
return fmt.Errorf("db: snapshot not supported on in-memory databases")
|
||||||
|
}
|
||||||
|
if _, err := db.sql.Exec("VACUUM INTO ?", destPath); err != nil {
|
||||||
|
return fmt.Errorf("db: snapshot VACUUM INTO %q: %w", destPath, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SnapshotDir creates a timestamped backup in dir and prunes backups older
|
||||||
|
// than retainDays days. dir is created with mode 0750 if it does not exist.
|
||||||
|
// The backup filename format is mcias-20060102-150405.db.
|
||||||
|
func (db *DB) SnapshotDir(dir string, retainDays int) (string, error) {
|
||||||
|
if err := os.MkdirAll(dir, 0750); err != nil {
|
||||||
|
return "", fmt.Errorf("db: create backup dir %q: %w", dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := time.Now().UTC().Format("20060102-150405")
|
||||||
|
dest := filepath.Join(dir, fmt.Sprintf("mcias-%s.db", ts))
|
||||||
|
if err := db.Snapshot(dest); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prune backups older than retainDays.
|
||||||
|
if retainDays > 0 {
|
||||||
|
cutoff := time.Now().UTC().AddDate(0, 0, -retainDays)
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
// Non-fatal: the backup was written; log pruning failure separately.
|
||||||
|
return dest, fmt.Errorf("db: list backup dir for pruning: %w", err)
|
||||||
|
}
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() || !strings.HasSuffix(e.Name(), ".db") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Skip the file we just wrote.
|
||||||
|
if e.Name() == filepath.Base(dest) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
info, err := e.Info()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if info.ModTime().Before(cutoff) {
|
||||||
|
_ = os.Remove(filepath.Join(dir, e.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dest, nil
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ package db
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetAccountTags_Empty(t *testing.T) {
|
func TestGetAccountTags_Empty(t *testing.T) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateWebAuthnCredential inserts a new WebAuthn credential record.
|
// CreateWebAuthnCredential inserts a new WebAuthn credential record.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestWebAuthnCRUD(t *testing.T) {
|
func TestWebAuthnCRUD(t *testing.T) {
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import (
|
|||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
mciasv1 "git.wntrmute.dev/kyle/mcias/gen/mcias/v1"
|
mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/validate"
|
"git.wntrmute.dev/mc/mcias/internal/validate"
|
||||||
)
|
)
|
||||||
|
|
||||||
type accountServiceServer struct {
|
type accountServiceServer struct {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
mciasv1 "git.wntrmute.dev/kyle/mcias/gen/mcias/v1"
|
mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
type adminServiceServer struct {
|
type adminServiceServer struct {
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ import (
|
|||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
mciasv1 "git.wntrmute.dev/kyle/mcias/gen/mcias/v1"
|
mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/audit"
|
"git.wntrmute.dev/mc/mcias/internal/audit"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
type authServiceServer struct {
|
type authServiceServer struct {
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import (
|
|||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
mciasv1 "git.wntrmute.dev/kyle/mcias/gen/mcias/v1"
|
mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type credentialServiceServer struct {
|
type credentialServiceServer struct {
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ import (
|
|||||||
"google.golang.org/grpc/peer"
|
"google.golang.org/grpc/peer"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
mciasv1 "git.wntrmute.dev/kyle/mcias/gen/mcias/v1"
|
mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/config"
|
"git.wntrmute.dev/mc/mcias/internal/config"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
// contextKey is the unexported context key type for this package.
|
// contextKey is the unexported context key type for this package.
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ import (
|
|||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/grpc/test/bufconn"
|
"google.golang.org/grpc/test/bufconn"
|
||||||
|
|
||||||
mciasv1 "git.wntrmute.dev/kyle/mcias/gen/mcias/v1"
|
mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/config"
|
"git.wntrmute.dev/mc/mcias/internal/config"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ import (
|
|||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
mciasv1 "git.wntrmute.dev/kyle/mcias/gen/mcias/v1"
|
mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/policy"
|
"git.wntrmute.dev/mc/mcias/internal/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
type policyServiceServer struct {
|
type policyServiceServer struct {
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import (
|
|||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
mciasv1 "git.wntrmute.dev/kyle/mcias/gen/mcias/v1"
|
mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
type tokenServiceServer struct {
|
type tokenServiceServer struct {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import (
|
|||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
mciasv1 "git.wntrmute.dev/kyle/mcias/gen/mcias/v1"
|
mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ListWebAuthnCredentials returns metadata for an account's WebAuthn credentials.
|
// ListWebAuthnCredentials returns metadata for an account's WebAuthn credentials.
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/policy"
|
"git.wntrmute.dev/mc/mcias/internal/policy"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
// contextKey is the unexported type for context keys in this package, preventing
|
// contextKey is the unexported type for context keys in this package, preventing
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
func generateTestKey(t *testing.T) (ed25519.PublicKey, ed25519.PrivateKey) {
|
func generateTestKey(t *testing.T) (ed25519.PublicKey, ed25519.PrivateKey) {
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/middleware"
|
"git.wntrmute.dev/mc/mcias/internal/middleware"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/policy"
|
"git.wntrmute.dev/mc/mcias/internal/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ---- Tag endpoints ----
|
// ---- Tag endpoints ----
|
||||||
|
|||||||
@@ -23,13 +23,14 @@ import (
|
|||||||
"github.com/go-webauthn/webauthn/protocol"
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
libwebauthn "github.com/go-webauthn/webauthn/webauthn"
|
libwebauthn "github.com/go-webauthn/webauthn/webauthn"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/audit"
|
"git.wntrmute.dev/mc/mcias/internal/audit"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/middleware"
|
"git.wntrmute.dev/mc/mcias/internal/middleware"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/policy"
|
||||||
mciaswebauthn "git.wntrmute.dev/kyle/mcias/internal/webauthn"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
|
mciaswebauthn "git.wntrmute.dev/mc/mcias/internal/webauthn"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -618,13 +619,37 @@ func (s *Server) handleWebAuthnLoginFinish(w http.ResponseWriter, r *http.Reques
|
|||||||
// Login succeeded: clear lockout counter.
|
// Login succeeded: clear lockout counter.
|
||||||
_ = s.db.ClearLoginFailures(acct.ID)
|
_ = s.db.ClearLoginFailures(acct.ID)
|
||||||
|
|
||||||
// Issue JWT.
|
// Load roles for policy check and expiry decision.
|
||||||
roles, err := s.db.GetRoles(acct.ID)
|
roles, err := s.db.GetRoles(acct.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
middleware.WriteError(w, http.StatusInternalServerError, "internal error", "internal_error")
|
middleware.WriteError(w, http.StatusInternalServerError, "internal error", "internal_error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Policy check: evaluate auth:login rules.
|
||||||
|
// WebAuthn login has no service context (no service_name or tags in the
|
||||||
|
// request body), so per-service deny rules won't fire. Account-level deny
|
||||||
|
// rules (e.g. deny a specific role from all auth:login actions) apply.
|
||||||
|
// This mirrors the policy gate in handleLogin so both auth paths are consistent.
|
||||||
|
//
|
||||||
|
// Security: policy is checked after credential verification so that a
|
||||||
|
// policy-denied login returns 403 (not 401), distinguishing a policy
|
||||||
|
// restriction from a bad credential without leaking account existence.
|
||||||
|
if s.polEng != nil {
|
||||||
|
input := policy.PolicyInput{
|
||||||
|
Subject: acct.UUID,
|
||||||
|
AccountType: string(acct.AccountType),
|
||||||
|
Roles: roles,
|
||||||
|
Action: policy.ActionLogin,
|
||||||
|
Resource: policy.Resource{},
|
||||||
|
}
|
||||||
|
if effect, _ := s.polEng.Evaluate(input); effect == policy.Deny {
|
||||||
|
s.writeAudit(r, model.EventWebAuthnLoginFail, &acct.ID, nil, `{"reason":"policy_denied"}`)
|
||||||
|
middleware.WriteError(w, http.StatusForbidden, "access denied by policy", "policy_denied")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
expiry := s.cfg.DefaultExpiry()
|
expiry := s.cfg.DefaultExpiry()
|
||||||
for _, role := range roles {
|
for _, role := range roles {
|
||||||
if role == "admin" {
|
if role == "admin" {
|
||||||
|
|||||||
@@ -21,19 +21,19 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/audit"
|
"git.wntrmute.dev/mc/mcias/internal/audit"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/config"
|
"git.wntrmute.dev/mc/mcias/internal/config"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/middleware"
|
"git.wntrmute.dev/mc/mcias/internal/middleware"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/policy"
|
"git.wntrmute.dev/mc/mcias/internal/policy"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/ui"
|
"git.wntrmute.dev/mc/mcias/internal/ui"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/validate"
|
"git.wntrmute.dev/mc/mcias/internal/validate"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
"git.wntrmute.dev/kyle/mcias/web"
|
"git.wntrmute.dev/mc/mcias/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server holds the dependencies injected into all handlers.
|
// Server holds the dependencies injected into all handlers.
|
||||||
@@ -436,6 +436,12 @@ type loginRequest struct {
|
|||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
TOTPCode string `json:"totp_code,omitempty"`
|
TOTPCode string `json:"totp_code,omitempty"`
|
||||||
|
// ServiceName and Tags identify the calling service. MCIAS evaluates the
|
||||||
|
// auth:login policy with these as the resource context, enabling operators
|
||||||
|
// to restrict which roles/account-types may log into specific services.
|
||||||
|
// Clients populate these from their [mcias] config section.
|
||||||
|
ServiceName string `json:"service_name,omitempty"`
|
||||||
|
Tags []string `json:"tags,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// loginResponse is the response body for a successful login.
|
// loginResponse is the response body for a successful login.
|
||||||
@@ -546,13 +552,42 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Login succeeded: clear any outstanding failure counter.
|
// Login succeeded: clear any outstanding failure counter.
|
||||||
_ = s.db.ClearLoginFailures(acct.ID)
|
_ = s.db.ClearLoginFailures(acct.ID)
|
||||||
|
|
||||||
// Determine expiry.
|
// Load roles for expiry decision and policy check.
|
||||||
expiry := s.cfg.DefaultExpiry()
|
|
||||||
roles, err := s.db.GetRoles(acct.ID)
|
roles, err := s.db.GetRoles(acct.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
middleware.WriteError(w, http.StatusInternalServerError, "internal error", "internal_error")
|
middleware.WriteError(w, http.StatusInternalServerError, "internal error", "internal_error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Policy check: evaluate auth:login with the calling service's context.
|
||||||
|
// Operator rules can deny login based on role, account type, service name,
|
||||||
|
// or tags. The built-in default Allow for auth:login is overridden by any
|
||||||
|
// matching Deny rule (deny-wins semantics).
|
||||||
|
//
|
||||||
|
// Security: policy is checked after credential verification so that a
|
||||||
|
// policy-denied login returns 403 (not 401), distinguishing a service
|
||||||
|
// access restriction from a wrong password without leaking user existence.
|
||||||
|
{
|
||||||
|
input := policy.PolicyInput{
|
||||||
|
Subject: acct.UUID,
|
||||||
|
AccountType: string(acct.AccountType),
|
||||||
|
Roles: roles,
|
||||||
|
Action: policy.ActionLogin,
|
||||||
|
Resource: policy.Resource{
|
||||||
|
ServiceName: req.ServiceName,
|
||||||
|
Tags: req.Tags,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if effect, _ := s.polEng.Evaluate(input); effect == policy.Deny {
|
||||||
|
s.writeAudit(r, model.EventLoginFail, &acct.ID, nil,
|
||||||
|
audit.JSON("reason", "policy_deny", "service_name", req.ServiceName))
|
||||||
|
middleware.WriteError(w, http.StatusForbidden, "access denied by policy", "policy_denied")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine expiry.
|
||||||
|
expiry := s.cfg.DefaultExpiry()
|
||||||
for _, r := range roles {
|
for _, r := range roles {
|
||||||
if r == "admin" {
|
if r == "admin" {
|
||||||
expiry = s.cfg.AdminExpiry()
|
expiry = s.cfg.AdminExpiry()
|
||||||
@@ -669,11 +704,12 @@ type validateRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type validateResponse struct {
|
type validateResponse struct {
|
||||||
Subject string `json:"sub,omitempty"`
|
Subject string `json:"sub,omitempty"`
|
||||||
Username string `json:"username,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
ExpiresAt string `json:"expires_at,omitempty"`
|
AccountType string `json:"account_type,omitempty"`
|
||||||
Roles []string `json:"roles,omitempty"`
|
ExpiresAt string `json:"expires_at,omitempty"`
|
||||||
Valid bool `json:"valid"`
|
Roles []string `json:"roles,omitempty"`
|
||||||
|
Valid bool `json:"valid"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleTokenValidate(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleTokenValidate(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -718,6 +754,7 @@ func (s *Server) handleTokenValidate(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
if acct, err := s.db.GetAccountByUUID(claims.Subject); err == nil {
|
if acct, err := s.db.GetAccountByUUID(claims.Subject); err == nil {
|
||||||
resp.Username = acct.Username
|
resp.Username = acct.Username
|
||||||
|
resp.AccountType = string(acct.AccountType)
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK, resp)
|
writeJSON(w, http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/config"
|
"git.wntrmute.dev/mc/mcias/internal/config"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/policy"
|
"git.wntrmute.dev/mc/mcias/internal/policy"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
// generateTOTPCode computes a valid RFC 6238 TOTP code for the current time
|
// generateTOTPCode computes a valid RFC 6238 TOTP code for the current time
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ package server
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/audit"
|
"git.wntrmute.dev/mc/mcias/internal/audit"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/middleware"
|
"git.wntrmute.dev/mc/mcias/internal/middleware"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
// unsealRequest is the request body for POST /v1/vault/unseal.
|
// unsealRequest is the request body for POST /v1/vault/unseal.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHandleHealthSealed(t *testing.T) {
|
func TestHandleHealthSealed(t *testing.T) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package ui
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
// uiContextKey is the unexported type for UI context values, preventing
|
// uiContextKey is the unexported type for UI context values, preventing
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CSRFManager implements HMAC-signed Double-Submit Cookie CSRF protection.
|
// CSRFManager implements HMAC-signed Double-Submit Cookie CSRF protection.
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/validate"
|
"git.wntrmute.dev/mc/mcias/internal/validate"
|
||||||
)
|
)
|
||||||
|
|
||||||
// knownRoles lists the built-in roles shown as checkboxes in the roles editor.
|
// knownRoles lists the built-in roles shown as checkboxes in the roles editor.
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
const auditPageSize = 50
|
const auditPageSize = 50
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ package ui
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/audit"
|
"git.wntrmute.dev/mc/mcias/internal/audit"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/validate"
|
"git.wntrmute.dev/mc/mcias/internal/validate"
|
||||||
)
|
)
|
||||||
|
|
||||||
// handleLoginPage renders the login form.
|
// handleLoginPage renders the login form.
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package ui
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// handleDashboard renders the main dashboard page. Admin users see account
|
// handleDashboard renders the main dashboard page. Admin users see account
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/policy"
|
"git.wntrmute.dev/mc/mcias/internal/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ---- Policies page ----
|
// ---- Policies page ----
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ import (
|
|||||||
"encoding/base32"
|
"encoding/base32"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
qrcode "github.com/skip2/go-qrcode"
|
qrcode "github.com/skip2/go-qrcode"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/audit"
|
"git.wntrmute.dev/mc/mcias/internal/audit"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// handleTOTPEnrollStart processes the password re-auth step and generates
|
// handleTOTPEnrollStart processes the password re-auth step and generates
|
||||||
@@ -108,7 +109,7 @@ func (u *UIServer) handleTOTPEnrollStart(w http.ResponseWriter, r *http.Request)
|
|||||||
u.renderTOTPSection(w, r, ProfileData{TOTPError: "internal error"})
|
u.renderTOTPSection(w, r, ProfileData{TOTPError: "internal error"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
qrDataURI := "data:image/png;base64," + base64.StdEncoding.EncodeToString(png)
|
qrDataURI := template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(png)) //nolint:gosec // G203: trusted server-generated data URI
|
||||||
|
|
||||||
// Issue enrollment nonce for the confirm step.
|
// Issue enrollment nonce for the confirm step.
|
||||||
nonce, err := u.issueTOTPEnrollNonce(acct.ID)
|
nonce, err := u.issueTOTPEnrollNonce(acct.ID)
|
||||||
@@ -224,7 +225,7 @@ func (u *UIServer) reissueTOTPEnrollQR(w http.ResponseWriter, r *http.Request, a
|
|||||||
u.renderTOTPSection(w, r, ProfileData{TOTPError: "internal error"})
|
u.renderTOTPSection(w, r, ProfileData{TOTPError: "internal error"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
qrDataURI := "data:image/png;base64," + base64.StdEncoding.EncodeToString(png)
|
qrDataURI := template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(png)) //nolint:gosec // G203: trusted server-generated data URI
|
||||||
|
|
||||||
newNonce, nonceErr := u.issueTOTPEnrollNonce(acct.ID)
|
newNonce, nonceErr := u.issueTOTPEnrollNonce(acct.ID)
|
||||||
if nonceErr != nil {
|
if nonceErr != nil {
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ package ui
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/audit"
|
"git.wntrmute.dev/mc/mcias/internal/audit"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/middleware"
|
"git.wntrmute.dev/mc/mcias/internal/middleware"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UnsealData is the view model for the unseal page.
|
// UnsealData is the view model for the unseal page.
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ import (
|
|||||||
"github.com/go-webauthn/webauthn/protocol"
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
libwebauthn "github.com/go-webauthn/webauthn/webauthn"
|
libwebauthn "github.com/go-webauthn/webauthn/webauthn"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/audit"
|
"git.wntrmute.dev/mc/mcias/internal/audit"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
mciaswebauthn "git.wntrmute.dev/kyle/mcias/internal/webauthn"
|
mciaswebauthn "git.wntrmute.dev/mc/mcias/internal/webauthn"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
// validateSessionToken wraps token.ValidateToken for use by UI session middleware.
|
// validateSessionToken wraps token.ValidateToken for use by UI session middleware.
|
||||||
|
|||||||
@@ -27,13 +27,13 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/config"
|
"git.wntrmute.dev/mc/mcias/internal/config"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/middleware"
|
"git.wntrmute.dev/mc/mcias/internal/middleware"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
"git.wntrmute.dev/kyle/mcias/web"
|
"git.wntrmute.dev/mc/mcias/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -931,11 +931,11 @@ type ProfileData struct { //nolint:govet // fieldalignment: readability over ali
|
|||||||
WebAuthnEnabled bool
|
WebAuthnEnabled bool
|
||||||
// TOTP enrollment fields (populated only during enrollment flow).
|
// TOTP enrollment fields (populated only during enrollment flow).
|
||||||
TOTPEnabled bool
|
TOTPEnabled bool
|
||||||
TOTPSecret string // base32-encoded; shown once during enrollment
|
TOTPSecret string // base32-encoded; shown once during enrollment
|
||||||
TOTPQR string // data:image/png;base64,... QR code
|
TOTPQR template.URL // data:image/png;base64,... QR code; template.URL bypasses URL escaping
|
||||||
TOTPEnrollNonce string // single-use nonce for confirm step
|
TOTPEnrollNonce string // single-use nonce for confirm step
|
||||||
TOTPError string // enrollment-specific error message
|
TOTPError string // enrollment-specific error message
|
||||||
TOTPSuccess string // success flash after confirmation
|
TOTPSuccess string // success flash after confirmation
|
||||||
}
|
}
|
||||||
|
|
||||||
// PGCredsData is the view model for the "My PG Credentials" list page.
|
// PGCredsData is the view model for the "My PG Credentials" list page.
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/config"
|
"git.wntrmute.dev/mc/mcias/internal/config"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
const testIssuer = "https://auth.example.com"
|
const testIssuer = "https://auth.example.com"
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DeriveFromPassphrase derives the master encryption key from a passphrase
|
// DeriveFromPassphrase derives the master encryption key from a passphrase
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/go-webauthn/webauthn/webauthn"
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/config"
|
"git.wntrmute.dev/mc/mcias/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewWebAuthn creates a configured go-webauthn instance from MCIAS config.
|
// NewWebAuthn creates a configured go-webauthn instance from MCIAS config.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
libwebauthn "github.com/go-webauthn/webauthn/webauthn"
|
libwebauthn "github.com/go-webauthn/webauthn/webauthn"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/config"
|
"git.wntrmute.dev/mc/mcias/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewWebAuthn(t *testing.T) {
|
func TestNewWebAuthn(t *testing.T) {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"github.com/go-webauthn/webauthn/protocol"
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
"github.com/go-webauthn/webauthn/webauthn"
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DecryptCredential decrypts a stored WebAuthn credential's ID and public key
|
// DecryptCredential decrypts a stored WebAuthn credential's ID and public key
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"github.com/go-webauthn/webauthn/protocol"
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
libwebauthn "github.com/go-webauthn/webauthn/webauthn"
|
libwebauthn "github.com/go-webauthn/webauthn/webauthn"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testMasterKey(t *testing.T) []byte {
|
func testMasterKey(t *testing.T) []byte {
|
||||||
|
|||||||
32
openapi.yaml
32
openapi.yaml
@@ -567,6 +567,12 @@ paths:
|
|||||||
|
|
||||||
If the account has TOTP enrolled, `totp_code` is required.
|
If the account has TOTP enrolled, `totp_code` is required.
|
||||||
Omitting it returns HTTP 401 with code `totp_required`.
|
Omitting it returns HTTP 401 with code `totp_required`.
|
||||||
|
|
||||||
|
`service_name` and `tags` identify the calling service. MCIAS
|
||||||
|
evaluates `auth:login` policy against these values after credentials
|
||||||
|
are verified. A policy-denied login returns HTTP 403 (not 401) so
|
||||||
|
callers can distinguish a service access restriction from bad credentials.
|
||||||
|
Clients should populate these from their `[mcias]` config section.
|
||||||
operationId: login
|
operationId: login
|
||||||
tags: [Public]
|
tags: [Public]
|
||||||
requestBody:
|
requestBody:
|
||||||
@@ -587,6 +593,21 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
description: Current 6-digit TOTP code. Required if TOTP is enrolled.
|
description: Current 6-digit TOTP code. Required if TOTP is enrolled.
|
||||||
example: "123456"
|
example: "123456"
|
||||||
|
service_name:
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
Name of the calling service. Used by MCIAS to evaluate
|
||||||
|
auth:login policy rules that target specific services.
|
||||||
|
example: metatron
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
Tags describing the calling service (e.g. "env:restricted").
|
||||||
|
MCIAS evaluates auth:login policy rules with required_tags
|
||||||
|
against this list.
|
||||||
|
example: ["env:restricted"]
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Login successful. Returns JWT and expiry.
|
description: Login successful. Returns JWT and expiry.
|
||||||
@@ -607,6 +628,17 @@ paths:
|
|||||||
value: {error: invalid credentials, code: unauthorized}
|
value: {error: invalid credentials, code: unauthorized}
|
||||||
totp_required:
|
totp_required:
|
||||||
value: {error: TOTP code required, code: totp_required}
|
value: {error: TOTP code required, code: totp_required}
|
||||||
|
"403":
|
||||||
|
description: >
|
||||||
|
Login denied by policy. Credentials were valid but an operator
|
||||||
|
policy rule blocks this account from accessing the calling service.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Error"
|
||||||
|
examples:
|
||||||
|
policy_denied:
|
||||||
|
value: {error: access denied by policy, code: policy_denied}
|
||||||
"429":
|
"429":
|
||||||
$ref: "#/components/responses/RateLimited"
|
$ref: "#/components/responses/RateLimited"
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ syntax = "proto3";
|
|||||||
|
|
||||||
package mcias.v1;
|
package mcias.v1;
|
||||||
|
|
||||||
option go_package = "git.wntrmute.dev/kyle/mcias/gen/mcias/v1;mciasv1";
|
option go_package = "git.wntrmute.dev/mc/mcias/gen/mcias/v1;mciasv1";
|
||||||
|
|
||||||
import "mcias/v1/common.proto";
|
import "mcias/v1/common.proto";
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ syntax = "proto3";
|
|||||||
|
|
||||||
package mcias.v1;
|
package mcias.v1;
|
||||||
|
|
||||||
option go_package = "git.wntrmute.dev/kyle/mcias/gen/mcias/v1;mciasv1";
|
option go_package = "git.wntrmute.dev/mc/mcias/gen/mcias/v1;mciasv1";
|
||||||
|
|
||||||
// HealthRequest carries no parameters.
|
// HealthRequest carries no parameters.
|
||||||
message HealthRequest {}
|
message HealthRequest {}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ syntax = "proto3";
|
|||||||
|
|
||||||
package mcias.v1;
|
package mcias.v1;
|
||||||
|
|
||||||
option go_package = "git.wntrmute.dev/kyle/mcias/gen/mcias/v1;mciasv1";
|
option go_package = "git.wntrmute.dev/mc/mcias/gen/mcias/v1;mciasv1";
|
||||||
|
|
||||||
import "google/protobuf/timestamp.proto";
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ syntax = "proto3";
|
|||||||
|
|
||||||
package mcias.v1;
|
package mcias.v1;
|
||||||
|
|
||||||
option go_package = "git.wntrmute.dev/kyle/mcias/gen/mcias/v1;mciasv1";
|
option go_package = "git.wntrmute.dev/mc/mcias/gen/mcias/v1;mciasv1";
|
||||||
|
|
||||||
import "google/protobuf/timestamp.proto";
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ syntax = "proto3";
|
|||||||
|
|
||||||
package mcias.v1;
|
package mcias.v1;
|
||||||
|
|
||||||
option go_package = "git.wntrmute.dev/kyle/mcias/gen/mcias/v1;mciasv1";
|
option go_package = "git.wntrmute.dev/mc/mcias/gen/mcias/v1;mciasv1";
|
||||||
|
|
||||||
// PolicyRule is the wire representation of a policy rule record.
|
// PolicyRule is the wire representation of a policy rule record.
|
||||||
message PolicyRule {
|
message PolicyRule {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ syntax = "proto3";
|
|||||||
|
|
||||||
package mcias.v1;
|
package mcias.v1;
|
||||||
|
|
||||||
option go_package = "git.wntrmute.dev/kyle/mcias/gen/mcias/v1;mciasv1";
|
option go_package = "git.wntrmute.dev/mc/mcias/gen/mcias/v1;mciasv1";
|
||||||
|
|
||||||
import "google/protobuf/timestamp.proto";
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
|
|||||||
@@ -30,13 +30,13 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/config"
|
"git.wntrmute.dev/mc/mcias/internal/config"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/server"
|
"git.wntrmute.dev/mc/mcias/internal/server"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
const e2eIssuer = "https://auth.e2e.test"
|
const e2eIssuer = "https://auth.e2e.test"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user