Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,42 @@ jobs:

- name: Run tests
run: make test

proto:
name: Proto lint + breaking
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
# buf breaking needs main's history to diff against.
fetch-depth: 0

# buf breaking can only diff against main once main actually carries
# the proto module; before this PR series merges, main has no proto/
# and `buf breaking` fails with "had no .proto files".
- name: Check base branch has protos
id: base
run: |
if git ls-tree -d origin/main proto | grep -q proto; then
echo "has_proto=true" >> "$GITHUB_OUTPUT"
else
echo "has_proto=false" >> "$GITHUB_OUTPUT"
fi

- uses: bufbuild/buf-action@v1
with:
input: proto
lint: true
# `format: true` makes the action run `buf format -d --exit-code`,
# failing the job on any unformatted .proto. Catches drift before
# generated code can diverge.
format: true
# Only run breaking on PRs (push to main has nothing to diff
# against) and only once main carries the proto module.
breaking: ${{ github.event_name == 'pull_request' && steps.base.outputs.has_proto == 'true' }}
breaking_against: 'https://github.com/${{ github.repository }}.git#branch=main,subdir=proto'
# The action's PR comment needs permissions the default
# GITHUB_TOKEN of this job lacks ("Resource not accessible by
# integration"); skip it.
pr_comment: false
18 changes: 17 additions & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,26 @@ jobs:
- name: Run tests
run: make test

smoke:
name: Release smoke (GraphQL + REST + gRPC + MCP)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
# Black-box end-to-end pass over every public API surface against the
# freshly built binary (see internal/e2e/smoke_test.go).
- name: Run smoke tests
run: make smoke

build:
name: Build and push Docker image
runs-on: ubuntu-latest
needs: test
needs: [test, smoke]
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
Expand Down
32 changes: 32 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# golangci-lint v2 configuration.
# Run via `make lint-go` (or `golangci-lint run ./...`).
version: "2"

run:
# Build tags used by the test/storage matrix so all files type-check.
tests: true

linters:
# Default linter set (errcheck, govet, ineffassign, staticcheck, unused).
default: standard
settings:
staticcheck:
checks:
- all
# QF1008 wants embedded-field selectors collapsed (p.Config.X -> p.X);
# the codebase deliberately keeps the explicit p.Config.X form.
- -QF1008
exclusions:
# Generated protobuf/gateway/openapi code is owned by buf, not us.
generated: lax
paths:
- gen/
- ".*\\.pb\\.go$"
- ".*\\.pb\\.gw\\.go$"

formatters:
enable:
- gofmt
exclusions:
paths:
- gen/
76 changes: 76 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ dev:
test:
go clean --testcache && TEST_DBS="sqlite" $(GO_TEST_ALL)

# Release smoke tests: build the real binary and exercise every public API
# surface (GraphQL, REST, gRPC, MCP) end to end, including an authenticated
# FGA decision on each. Gated behind the `smoke` build tag so regular test
# runs skip them. CI runs this on every release.
.PHONY: smoke
smoke:
go test -tags smoke -count=1 -v -timeout 5m ./internal/e2e/

test-postgres: test-cleanup-postgres
docker run -d --name authorizer_postgres -p 5434:5432 -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=postgres postgres
sleep 3
Expand Down Expand Up @@ -174,3 +182,71 @@ generate-graphql:
generate-db-template:
cp -rf internal/storage/db/provider_template internal/storage/db/${dbname}
find internal/storage/db/${dbname} -type f -exec sed -i -e 's/provider_template/${dbname}/g' {} \;

# ----------------------------------------------------------------------------
# Protobuf (Phase 0+): public-API source of truth under ./proto.
# `buf` is installed on demand into $(GOBIN) if missing.
# ----------------------------------------------------------------------------
BUF ?= $(shell command -v buf 2>/dev/null)
BUF_VERSION ?= v1.47.2

.PHONY: proto-tools proto-lint proto-breaking proto-gen

proto-tools:
@if [ -z "$(BUF)" ]; then \
echo "Installing buf $(BUF_VERSION) via go install"; \
go install github.com/bufbuild/buf/cmd/buf@$(BUF_VERSION); \
fi

proto-lint: proto-tools
cd proto && buf lint

# Compare the working tree's proto against origin/main; fails on breaking changes.
# Override BUF_BREAKING_AGAINST for local runs (e.g. "main" or a SHA).
BUF_BREAKING_AGAINST ?= .git#branch=origin/main,subdir=proto
proto-breaking: proto-tools
cd proto && buf breaking --against '../$(BUF_BREAKING_AGAINST)'

proto-gen: proto-tools
cd proto && buf dep update && buf generate

# ----------------------------------------------------------------------------
# Formatting & linting (Go + TypeScript). `make fmt` before committing,
# `make lint` in CI. golangci-lint is installed on demand if missing.
# ----------------------------------------------------------------------------
GOLANGCI_LINT ?= $(shell command -v golangci-lint 2>/dev/null)
GOLANGCI_LINT_VERSION ?= v2.11.4

.PHONY: fmt fmt-go fmt-ts lint lint-go lint-ts lint-tools

# Format everything.
fmt: fmt-go fmt-ts

# gofmt -s over all hand-written Go sources (generated protobuf output under
# gen/ is excluded — it is owned by buf).
fmt-go:
@gofmt -s -w $(shell find . -type f -name '*.go' -not -path './gen/*')

# Prettier over both web apps via their configured format scripts.
fmt-ts:
cd web/app && npm run format
cd web/dashboard && npm run format

# Lint everything.
lint: lint-go lint-ts

lint-tools:
@if [ -z "$(GOLANGCI_LINT)" ]; then \
echo "Installing golangci-lint $(GOLANGCI_LINT_VERSION)"; \
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION); \
fi

# golangci-lint over the module. Generated code under gen/ is excluded via
# .golangci.yml.
lint-go: lint-tools
golangci-lint run ./...

# Prettier in --check mode: fails (non-zero) if any web source is unformatted.
lint-ts:
cd web/app && npx prettier --check 'src/**/*.(ts|tsx|js|jsx)'
cd web/dashboard && npx prettier --check 'src/**/*.(ts|tsx|js|jsx)'
55 changes: 55 additions & 0 deletions cmd/fga_engine.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package cmd

import (
"strings"

"github.com/rs/zerolog"

"github.com/authorizerdev/authorizer/internal/authorization/engine"
fgaengine "github.com/authorizerdev/authorizer/internal/authorization/engine/openfga"
"github.com/authorizerdev/authorizer/internal/config"
)

// initAuthzEngine initializes the embedded OpenFGA authorization engine from
// the --fga-store / --fga-store-url config, shared by the server (root) and
// the MCP subcommand. OpenFGA migrations run on boot for SQL stores
// (idempotent); the in-memory store needs none.
//
// Engine-init failure is deliberately NON-fatal: FGA is an optional
// subsystem, so a failure here (e.g. the DB user lacks DDL rights for the
// OpenFGA tables) logs loudly and returns a nil engine — fga_* and the
// permission APIs fail closed while core authentication keeps serving.
//
// The returned cleanup func is always non-nil and safe to defer; it closes
// the engine when one was created.
func initAuthzEngine(cfg *config.Config, log *zerolog.Logger) (engine.AuthorizationEngine, func()) {
cleanup := func() {}
fgaStore, fgaStoreURL, fgaEnabled := cfg.FGAStoreConfig()
if !fgaEnabled {
return nil, cleanup
}
runMigrations := !strings.EqualFold(fgaStore, fgaengine.StoreMemory)
fgaEngine, err := fgaengine.New(
&fgaengine.Config{
Store: fgaStore,
StoreURL: fgaStoreURL,
StoreName: cfg.OrganizationName,
RunMigrations: runMigrations,
},
&fgaengine.Dependencies{Log: log},
)
if err != nil {
log.Error().Err(err).
Str("fga_store", fgaStore).
Msg("failed to initialize OpenFGA authorization engine; fine-grained authorization is DISABLED (fail-closed) — core auth continues")
return nil, cleanup
}
if closer, ok := fgaEngine.(interface{ Close() }); ok {
cleanup = closer.Close
}
log.Info().
Str("fga_store", fgaStore).
Bool("reused_main_db", strings.TrimSpace(cfg.FGAStore) == "").
Msg("OpenFGA authorization engine initialized (embedded)")
return fgaEngine, cleanup
}
Loading
Loading