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
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,13 @@ AI_VALIDATION_PROVIDER=cloudflare
# OPTIONAL: Telemetry (anonymous usage stats, enabled by default)
# ============================================================================
# HENKAIPAN_TELEMETRY_ENABLED=false # Set to false to opt out
# ============================================================================
# OPTIONAL: SSO / OIDC (single sign-on)
# ============================================================================
# SSO_ENABLED=false # Set to true to enable SSO login
# SSO_ISSUER_URL= # OIDC issuer URL (e.g. https://keycloak/auth/realms/myrealm)
# SSO_CLIENT_ID= # OIDC client ID registered with your IdP
# SSO_CLIENT_SECRET= # OIDC client secret
# SSO_REDIRECT_URI=/api/auth/sso/callback # Redirect URI (path or full URL)
# SSO_GROUP_CLAIM=groups # OIDC claim name for group membership
# SSO_ADMIN_GROUP= # Group claim value that maps to admin role
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
- **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`.
- **SSE bridge** (`internal/events/redis_bridge.go`): worker publishes events to Redis pub/sub; API subscribes and relays to SSE clients.
- **AI providers**: OpenRouter / Cloudflare / Ollama. Per-task selection via `AI_{REMEDIATION,SUMMARY,VALIDATION}_PROVIDER`. If unconfigured, handlers silently not registered — check worker logs.
- **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`.
- **Scanner packs**: `sast`, `sca`, `secrets`, `iac`, `containers` — resolved in `internal/scanner/registry.go`. Scanner binaries bundled in worker Docker image.

## Key quirks
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ See the [self-hosted repo](https://github.com/Dyallab/HenKaiPan-self-hosted) for
6. **Knowledge** — remediation guides and AI-generated articles
7. **Compliance** — SOC 2 / ISO 27001 / PCI-DSS frameworks, control mapping, TSV export
8. **Settings** — integrations, security, policies, notifications, users, teams
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.

## Tech Stack

Expand Down Expand Up @@ -287,6 +288,7 @@ Copy `.env.example` to `.env` and configure the required variables. With direnv,
- **Server**: Port, Redis configuration
- **Integrations**: GitHub, SMTP/email
- **AI**: OpenRouter, Cloudflare Workers AI, and/or Ollama configuration
- **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).

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

Expand Down
3 changes: 2 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,8 @@ Items that were open in the backlog but are already in production per [`HenKaiPa

### Enterprise Features

- [ ] SAML / OIDC SSO
- [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.
- [ ] SAML SSO
- [ ] Multi-tenant support (organizations)
- [ ] **Advanced RBAC** (custom roles, granular permissions) — *partial: capability matrix v1.12.1, team-scoped access v1.29.0, datascope v1.30.2*
- [ ] Audit log export + SIEM integration
Expand Down
41 changes: 37 additions & 4 deletions cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/http"
"net/http/pprof"
"os"
"strings"
"time"

"aspm/internal/ai"
Expand All @@ -22,8 +23,10 @@ import (
"aspm/internal/queue"
"aspm/internal/repository"
"aspm/internal/secrets"
"aspm/internal/sso"
"aspm/internal/telemetry"

"github.com/coreos/go-oidc/v3/oidc"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/hibiken/asynq"
Expand All @@ -42,6 +45,35 @@ func main() {
ai.SetCloudflareConfig(cfg.CfAccountID, cfg.CfAPIToken)
}

// Initialize SSO/OIDC provider if enabled.
var ssoProvider *sso.Provider
if cfg.SSOEnabled && cfg.SSOIssuerURL != "" && cfg.SSOClientID != "" && cfg.SSOClientSecret != "" {
redirectURI := cfg.SSORedirectURI
if !strings.HasPrefix(redirectURI, "http") {
// Relative path → construct full URL from frontend URL or request scheme.
base := strings.TrimRight(cfg.FrontendURL, "/")
if base == "" {
base = "http://localhost:" + cfg.Port
}
redirectURI = base + redirectURI
}
var err error
// OIDC discovery performs an HTTP request at startup; bound it so a
// slow or unreachable IdP cannot hang the API process.
oidcCtx := oidc.ClientContext(context.Background(), &http.Client{
Timeout: 10 * time.Second,
})
ssoProvider, err = sso.NewProvider(oidcCtx, cfg.SSOIssuerURL, cfg.SSOClientID,
cfg.SSOClientSecret, redirectURI, cfg.SSOGroupClaim, cfg.SSOAdminGroup)
if err != nil {
slog.Error("failed to initialize SSO provider", "err", err)
} else {
slog.Info("SSO provider initialized", "issuer", cfg.SSOIssuerURL)
}
} else if cfg.SSOEnabled {
slog.Warn("SSO_ENABLED is true but SSO_ISSUER_URL/SSO_CLIENT_ID/SSO_CLIENT_SECRET not configured")
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
pool := db.Connect(cfg.DatabaseURL)
defer pool.Close()

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

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

r.Post("/api/auth/login", h.Login)
r.Post("/api/auth/logout", h.Logout)
r.Get("/api/auth/sso/login", h.SSOLogin)
r.Get("/api/auth/sso/callback", h.SSOCallback)

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

// ── Free: Config Status ──
r.Get("/api/config/status", h.GetConfigStatus)

// ── Tier limits ──
r.With(auth.RequireRole("admin", "viewer")).Get("/api/limits", h.GetLimits)

Expand Down
Loading