Skip to content

Commit 9bd1f8f

Browse files
Merge pull request #54 from Dyallab/feature/sso
Feature/sso
2 parents 8626e8b + 128cfaa commit 9bd1f8f

26 files changed

Lines changed: 1088 additions & 43 deletions

File tree

.env.example

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,13 @@ AI_VALIDATION_PROVIDER=cloudflare
7272
# OPTIONAL: Telemetry (anonymous usage stats, enabled by default)
7373
# ============================================================================
7474
# HENKAIPAN_TELEMETRY_ENABLED=false # Set to false to opt out
75+
# ============================================================================
76+
# OPTIONAL: SSO / OIDC (single sign-on)
77+
# ============================================================================
78+
# SSO_ENABLED=false # Set to true to enable SSO login
79+
# SSO_ISSUER_URL= # OIDC issuer URL (e.g. https://keycloak/auth/realms/myrealm)
80+
# SSO_CLIENT_ID= # OIDC client ID registered with your IdP
81+
# SSO_CLIENT_SECRET= # OIDC client secret
82+
# SSO_REDIRECT_URI=/api/auth/sso/callback # Redirect URI (path or full URL)
83+
# SSO_GROUP_CLAIM=groups # OIDC claim name for group membership
84+
# SSO_ADMIN_GROUP= # Group claim value that maps to admin role

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
- **Queue** (`internal/queue/`): Asynq client + server. Job types: `scan:run` (3 retries, 30min timeout), `agent:validate` (5 retries), `webhook:send`, `email:send`, `snippet:enrich`, `digest:send`, `report:send`.
4141
- **SSE bridge** (`internal/events/redis_bridge.go`): worker publishes events to Redis pub/sub; API subscribes and relays to SSE clients.
4242
- **AI providers**: OpenRouter / Cloudflare / Ollama. Per-task selection via `AI_{REMEDIATION,SUMMARY,VALIDATION}_PROVIDER`. If unconfigured, handlers silently not registered — check worker logs.
43+
- **SSO/OIDC**: `internal/sso/provider.go` wraps `coreos/go-oidc`. Env-var config (`SSO_ENABLED`, `SSO_ISSUER_URL`, `SSO_CLIENT_ID`, `SSO_CLIENT_SECRET`, `SSO_ADMIN_GROUP`, `SSO_GROUP_CLAIM`). Claims come **only** from the signed ID token (no UserInfo fallback) — IdP must ship `email`/`groups` in the ID token. Group claim maps to role via `ResolveRole`; role is re-synced on every SSO login. Setup guide: `docs/sso-authelia.md`.
4344
- **Scanner packs**: `sast`, `sca`, `secrets`, `iac`, `containers` — resolved in `internal/scanner/registry.go`. Scanner binaries bundled in worker Docker image.
4445

4546
## Key quirks

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ See the [self-hosted repo](https://github.com/Dyallab/HenKaiPan-self-hosted) for
4646
6. **Knowledge** — remediation guides and AI-generated articles
4747
7. **Compliance** — SOC 2 / ISO 27001 / PCI-DSS frameworks, control mapping, TSV export
4848
8. **Settings** — integrations, security, policies, notifications, users, teams
49+
9. **SSO (OIDC)** — single sign-on via any OpenID Connect provider (Keycloak, Authelia, Google Workspace, etc.), with group-claim-based role mapping. See [`docs/sso-authelia.md`](docs/sso-authelia.md) for setup.
4950

5051
## Tech Stack
5152

@@ -287,6 +288,7 @@ Copy `.env.example` to `.env` and configure the required variables. With direnv,
287288
- **Server**: Port, Redis configuration
288289
- **Integrations**: GitHub, SMTP/email
289290
- **AI**: OpenRouter, Cloudflare Workers AI, and/or Ollama configuration
291+
- **SSO (OIDC)**: `SSO_ENABLED`, `SSO_ISSUER_URL`, `SSO_CLIENT_ID`, `SSO_CLIENT_SECRET`, and role-mapping vars. See [`docs/sso-authelia.md`](docs/sso-authelia.md).
290292

291293
If AI providers are not configured, AI remediation, validation, and summary features will be disabled.
292294

TODO.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,8 @@ Items that were open in the backlog but are already in production per [`HenKaiPa
210210

211211
### Enterprise Features
212212

213-
- [ ] SAML / OIDC SSO
213+
- [x] OIDC SSO (single sign-on via OpenID Connect) — feature-flagged, env-var config, group-claim role mapping. See `docs/sso-authelia.md` for the Authelia guide.
214+
- [ ] SAML SSO
214215
- [ ] Multi-tenant support (organizations)
215216
- [ ] **Advanced RBAC** (custom roles, granular permissions) — *partial: capability matrix v1.12.1, team-scoped access v1.29.0, datascope v1.30.2*
216217
- [ ] Audit log export + SIEM integration

cmd/api/main.go

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"net/http"
77
"net/http/pprof"
88
"os"
9+
"strings"
910
"time"
1011

1112
"aspm/internal/ai"
@@ -22,8 +23,10 @@ import (
2223
"aspm/internal/queue"
2324
"aspm/internal/repository"
2425
"aspm/internal/secrets"
26+
"aspm/internal/sso"
2527
"aspm/internal/telemetry"
2628

29+
"github.com/coreos/go-oidc/v3/oidc"
2730
"github.com/go-chi/chi/v5"
2831
"github.com/go-chi/chi/v5/middleware"
2932
"github.com/hibiken/asynq"
@@ -42,6 +45,35 @@ func main() {
4245
ai.SetCloudflareConfig(cfg.CfAccountID, cfg.CfAPIToken)
4346
}
4447

48+
// Initialize SSO/OIDC provider if enabled.
49+
var ssoProvider *sso.Provider
50+
if cfg.SSOEnabled && cfg.SSOIssuerURL != "" && cfg.SSOClientID != "" && cfg.SSOClientSecret != "" {
51+
redirectURI := cfg.SSORedirectURI
52+
if !strings.HasPrefix(redirectURI, "http") {
53+
// Relative path → construct full URL from frontend URL or request scheme.
54+
base := strings.TrimRight(cfg.FrontendURL, "/")
55+
if base == "" {
56+
base = "http://localhost:" + cfg.Port
57+
}
58+
redirectURI = base + redirectURI
59+
}
60+
var err error
61+
// OIDC discovery performs an HTTP request at startup; bound it so a
62+
// slow or unreachable IdP cannot hang the API process.
63+
oidcCtx := oidc.ClientContext(context.Background(), &http.Client{
64+
Timeout: 10 * time.Second,
65+
})
66+
ssoProvider, err = sso.NewProvider(oidcCtx, cfg.SSOIssuerURL, cfg.SSOClientID,
67+
cfg.SSOClientSecret, redirectURI, cfg.SSOGroupClaim, cfg.SSOAdminGroup)
68+
if err != nil {
69+
slog.Error("failed to initialize SSO provider", "err", err)
70+
} else {
71+
slog.Info("SSO provider initialized", "issuer", cfg.SSOIssuerURL)
72+
}
73+
} else if cfg.SSOEnabled {
74+
slog.Warn("SSO_ENABLED is true but SSO_ISSUER_URL/SSO_CLIENT_ID/SSO_CLIENT_SECRET not configured")
75+
}
76+
4577
pool := db.Connect(cfg.DatabaseURL)
4678
defer pool.Close()
4779

@@ -75,7 +107,8 @@ func main() {
75107
h := handlers.New(store, queueClient, cfg.FrontendURL, cfg.CookieSecure, cfg.CookieDomain, cfg.CookieSameSite,
76108
cfg.RemediationConfig.IsConfigured, cfg.SummaryConfig.IsConfigured, cfg.ValidationConfig.IsConfigured,
77109
cfg.EmailEnabled, cfg.WebhookSecret, findingCache,
78-
cfg.MaxProjects, cfg.MaxUsers, cfg.MaxAIScans)
110+
cfg.MaxProjects, cfg.MaxUsers, cfg.MaxAIScans,
111+
cfg.SSOEnabled && ssoProvider != nil, ssoProvider)
79112

80113
if cfg.TelemetryEnabled {
81114
go telemetry.NewClient(store, "https://telemetry.dyallab.com.ar/api/ping", handlers.Version, "self-hosted").Start(context.Background())
@@ -162,9 +195,12 @@ func main() {
162195
r.Get("/api/health", h.GetHealth)
163196
r.Get("/api/version", h.GetVersion)
164197
r.Get("/api/version/check", h.GetVersionCheck)
198+
r.Get("/api/config/status", h.GetConfigStatus)
165199

166200
r.Post("/api/auth/login", h.Login)
167201
r.Post("/api/auth/logout", h.Logout)
202+
r.Get("/api/auth/sso/login", h.SSOLogin)
203+
r.Get("/api/auth/sso/callback", h.SSOCallback)
168204

169205
// ── /api/v1/scans — External CI/CD endpoints (API key auth, no JWT) ────
170206
r.Route("/api/v1/scans", func(r chi.Router) {
@@ -300,9 +336,6 @@ func main() {
300336
// ── Free: Me ──
301337
r.Get("/api/me", h.GetMe)
302338

303-
// ── Free: Config Status ──
304-
r.Get("/api/config/status", h.GetConfigStatus)
305-
306339
// ── Tier limits ──
307340
r.With(auth.RequireRole("admin", "viewer")).Get("/api/limits", h.GetLimits)
308341

0 commit comments

Comments
 (0)