From 77509930d75eb2d130bfd229c639f793eed11fa3 Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:13:27 +0200 Subject: [PATCH 1/7] Wired in auth and removed gauth --- go.mod | 2 - go.sum | 4 -- internal/controller/authentication.go | 63 +++++++++++++++++ internal/controller/authentication_test.go | 79 ++++++++++++++++++++++ internal/controller/controller.go | 32 +++------ pkg/api/auth/auth_middleware.go | 27 +++++--- pkg/api/auth/policy.go | 65 ++++++++++++++++++ pkg/api/auth/policy_test.go | 79 ++++++++++++++++++++++ pkg/api/permissions.go | 45 ++++++++++++ pkg/api/permissions_test.go | 64 ++++++++++++++++++ pkg/auth/anonymous.go | 28 ++++++++ pkg/auth/anonymous_test.go | 29 ++++++++ 12 files changed, 480 insertions(+), 37 deletions(-) create mode 100644 internal/controller/authentication.go create mode 100644 internal/controller/authentication_test.go create mode 100644 pkg/api/auth/policy.go create mode 100644 pkg/api/auth/policy_test.go create mode 100644 pkg/api/permissions.go create mode 100644 pkg/api/permissions_test.go create mode 100644 pkg/auth/anonymous.go create mode 100644 pkg/auth/anonymous_test.go diff --git a/go.mod b/go.mod index e1c2e497..88452735 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module soarca go 1.25.0 require ( - github.com/COSSAS/gauth v1.0.3 github.com/caarlos0/env/v11 v11.4.1 github.com/coreos/go-oidc/v3 v3.11.0 github.com/eclipse/paho.mqtt.golang v1.5.1 @@ -53,7 +52,6 @@ require ( github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/sessions v1.4.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect diff --git a/go.sum b/go.sum index 0971f31f..d733a7b5 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw= github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= -github.com/COSSAS/gauth v1.0.3 h1:Darc5FPOGSEWUE7wRDrqx0w6KZheYjnksHpyCAw6HvM= -github.com/COSSAS/gauth v1.0.3/go.mod h1:JMZoZ7yVFOBJy5y1haZEFoR12zqugDtGnFYJft/uUwY= github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6 h1:w0E0fgc1YafGEh5cROhlROMWXiNoZqApk2PDN0M1+Ns= github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= @@ -77,8 +75,6 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW 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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= diff --git a/internal/controller/authentication.go b/internal/controller/authentication.go new file mode 100644 index 00000000..215f1f3d --- /dev/null +++ b/internal/controller/authentication.go @@ -0,0 +1,63 @@ +package controller + +import ( + "soarca/internal/config" + "soarca/pkg/auth" + "soarca/pkg/auth/providers/apikey" + "soarca/pkg/auth/providers/entra" + "soarca/pkg/auth/providers/oidc" +) + +// newAuthenticator builds the authenticator for every provider that +// authConfig enables. config.Load has already checked that each enabled +// provider is complete and that at least one is enabled. +func newAuthenticator(authConfig config.Auth) (auth.IAuthenticator, error) { + if !authConfig.Enabled { + log.Warning("authentication is disabled: every API request is accepted with full permissions") + return auth.Anonymous{}, nil + } + + var providers []auth.IIdentityProvider + if authConfig.OidcEnabled() { + provider, err := oidc.New(oidc.Config{ + IssuerUrl: authConfig.Issuer, + Audiences: []string{authConfig.ClientId}, + Roles: oidc.DefaultRoles(authConfig.AdminGroup), + SkipTlsVerify: authConfig.SkipTlsVerify, + }) + if err != nil { + return nil, err + } + providers = append(providers, provider) + log.Info("accepting OIDC tokens from ", authConfig.Issuer) + } + + if authConfig.EntraEnabled() { + entraConfig := authConfig.Entra + provider, err := entra.New(entra.Config{ + TenantIds: entraConfig.TenantIds, + ApiClientId: entraConfig.ApiClientId, + ApiAppIdUri: entraConfig.ApiAppIdUri, + AllowedClientIds: entraConfig.AllowedClientIds, + RequiredUserScope: entraConfig.RequiredUserScope, + AcceptV1Tokens: entraConfig.AcceptV1Tokens, + Authority: entraConfig.Authority, + }) + if err != nil { + return nil, err + } + providers = append(providers, provider) + log.Info("accepting Entra ID tokens from tenants ", entraConfig.TenantIds) + } + + if authConfig.ApiKeysFile != "" { + store, err := apikey.LoadFile(authConfig.ApiKeysFile) + if err != nil { + return nil, err + } + providers = append(providers, apikey.New(store)) + log.Info("accepting ", len(store), " API keys from ", authConfig.ApiKeysFile) + } + + return auth.New(providers) +} diff --git a/internal/controller/authentication_test.go b/internal/controller/authentication_test.go new file mode 100644 index 00000000..5ff81f48 --- /dev/null +++ b/internal/controller/authentication_test.go @@ -0,0 +1,79 @@ +package controller + +import ( + "context" + "os" + "path/filepath" + "testing" + + "soarca/internal/config" + "soarca/pkg/auth" + "soarca/pkg/auth/providers/apikey" + "soarca/pkg/auth/roles" + + "github.com/go-playground/assert/v2" +) + +func writeKeys(t *testing.T, content string) string { + path := filepath.Join(t.TempDir(), "api-keys.json") + assert.Equal(t, os.WriteFile(path, []byte(content), 0o600), nil) + return path +} + +func TestDisabledAuthIsAnonymous(t *testing.T) { + authenticator, err := newAuthenticator(config.Auth{Enabled: false, ApiKeysFile: "/does/not/exist"}) + assert.Equal(t, err, nil) + + principal, err := authenticator.Authenticate(context.Background(), "") + assert.Equal(t, err, nil) + assert.Equal(t, principal.Provider, auth.AnonymousName) + assert.Equal(t, principal.Has(roles.Admin), true) +} + +func TestApiKeysFromFile(t *testing.T) { + plaintext, hash, err := apikey.Generate() + assert.Equal(t, err, nil) + path := writeKeys(t, `[{"name":"webhook","sha256":"`+hash+`","permissions":["playbook:execute"],"expires":"2999-01-01T00:00:00Z"}]`) + + authenticator, err := newAuthenticator(config.Auth{Enabled: true, ApiKeysFile: path}) + assert.Equal(t, err, nil) + + principal, err := authenticator.Authenticate(context.Background(), plaintext) + assert.Equal(t, err, nil) + assert.Equal(t, principal.Provider, apikey.Name) + assert.Equal(t, principal.Has(roles.PlaybookExecute), true) + assert.Equal(t, principal.Has(roles.PlaybookWrite), false) +} + +func TestInvalidApiKeysFileStopsStartup(t *testing.T) { + _, err := newAuthenticator(config.Auth{Enabled: true, ApiKeysFile: "/does/not/exist"}) + assert.NotEqual(t, err, nil) + + _, err = newAuthenticator(config.Auth{Enabled: true, ApiKeysFile: writeKeys(t, `[{"name":"no-expiry","sha256":"x"}]`)}) + assert.NotEqual(t, err, nil) +} + +func TestEveryProviderTogether(t *testing.T) { + _, hash, _ := apikey.Generate() + authConfig := config.Auth{ + Enabled: true, + Issuer: "https://authentik.example.com/application/o/soarca/", + ClientId: "soarca", + AdminGroup: "soarca_admin", + Entra: config.Entra{ + TenantIds: []string{"11111111-1111-1111-1111-111111111111"}, + ApiClientId: "aaaaaaaa-0000-0000-0000-000000000001", + RequiredUserScope: "access_as_user", + Authority: "https://login.microsoftonline.com", + }, + ApiKeysFile: writeKeys(t, `[{"name":"webhook","sha256":"`+hash+`","permissions":["playbook:read"],"expires":"2999-01-01T00:00:00Z"}]`), + } + + authenticator, err := newAuthenticator(authConfig) + assert.Equal(t, err, nil) + + // Neither identity provider is contacted at startup, and a credential + // no provider handles is rejected without reaching out either. + _, err = authenticator.Authenticate(context.Background(), "") + assert.NotEqual(t, err, nil) +} diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 3e2d9e53..e3ca0a79 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -46,12 +46,12 @@ import ( downstreamReporter "soarca/pkg/reporting/reporter/downstream_reporter" - "github.com/COSSAS/gauth" "github.com/gin-gonic/gin" mongo "soarca/internal/database/mongodb" playbookrepository "soarca/internal/database/playbook" routes "soarca/pkg/api" + apiAuth "soarca/pkg/api/auth" ) var log *logger.Log @@ -277,9 +277,10 @@ func initializeCore(app *gin.Engine, configuration *config.Config) error { return configuration.Dynamic().AllowedOrigins }) - err := intializeAuthenticationMiddleware(app, configuration.Static.Auth) + // Before any route is registered, so it covers all of them. + err := initializeAuthentication(app, configuration.Static.Auth) if err != nil { - log.Error("Failed to setup Authentication middleware") + log.Error("Failed to set up authentication: ", err) return err } err = mainController.setupDatabase() @@ -374,24 +375,13 @@ func initializeIntegrationTheHiveReporting(theHive config.TheHive) (downstreamRe return theHiveReporter, nil } -func intializeAuthenticationMiddleware(app *gin.Engine, authConfig config.Auth) error { - if authConfig.Enabled { - // Same settings gauth.DefaultConfig() builds, but from SOARCA's config - // instead of gauth reading the environment itself. - auth, err := gauth.New(&gauth.Config{ - Mode: gauth.ModeVerify, - IssuerUri: authConfig.Issuer, - ClientID: authConfig.ClientId, - Provider: gauth.Authentik, - SkipTLSValidation: authConfig.SkipTlsVerify, - }) - if err != nil { - log.Error("Failed to initialize authenticator:", err) - return err - } - app.Use(auth.LoadAuthContext()) - app.Use(auth.Middleware([]string{"soarca_admin"})) - +// initializeAuthentication puts every route registered after it behind the +// authenticator and the route permissions in routes.Permissions. +func initializeAuthentication(app *gin.Engine, authConfig config.Auth) error { + authenticator, err := newAuthenticator(authConfig) + if err != nil { + return err } + app.Use(apiAuth.Protect(authenticator, routes.Permissions)) return nil } diff --git a/pkg/api/auth/auth_middleware.go b/pkg/api/auth/auth_middleware.go index d39176d9..71e7552c 100644 --- a/pkg/api/auth/auth_middleware.go +++ b/pkg/api/auth/auth_middleware.go @@ -28,20 +28,27 @@ const principalKey = "soarca.auth.principal" // caller for PrincipalFromContext. It responds 401 when that fails. func Middleware(authenticator auth.IAuthenticator) gin.HandlerFunc { return func(ginContext *gin.Context) { - credential := bearerCredential(ginContext.GetHeader("Authorization")) - principal, err := authenticator.Authenticate(ginContext.Request.Context(), credential) - if err != nil { - log.Debug(fmt.Sprintf("authentication failed for %s: %s", ginContext.FullPath(), err)) - ginContext.Header("WWW-Authenticate", `Bearer realm="soarca"`) - apiError.SendErrorResponse(ginContext, http.StatusUnauthorized, "unauthenticated", originalCall(ginContext), "") - ginContext.Abort() - return + if _, ok := authenticate(ginContext, authenticator); ok { + ginContext.Next() } - ginContext.Set(principalKey, principal) - ginContext.Next() } } +// authenticate stores the caller on the context, or responds 401 and aborts. +func authenticate(ginContext *gin.Context, authenticator auth.IAuthenticator) (*auth.Principal, bool) { + credential := bearerCredential(ginContext.GetHeader("Authorization")) + principal, err := authenticator.Authenticate(ginContext.Request.Context(), credential) + if err != nil { + log.Debug(fmt.Sprintf("authentication failed for %s: %s", originalCall(ginContext), err)) + ginContext.Header("WWW-Authenticate", `Bearer realm="soarca"`) + apiError.SendErrorResponse(ginContext, http.StatusUnauthorized, "unauthenticated", originalCall(ginContext), "") + ginContext.Abort() + return nil, false + } + ginContext.Set(principalKey, principal) + return principal, true +} + // RequireAll lets a request through only if the caller holds every permission. func RequireAll(permissions ...roles.Permission) gin.HandlerFunc { return func(ginContext *gin.Context) { diff --git a/pkg/api/auth/policy.go b/pkg/api/auth/policy.go new file mode 100644 index 00000000..5749cb5f --- /dev/null +++ b/pkg/api/auth/policy.go @@ -0,0 +1,65 @@ +package auth + +import ( + "soarca/pkg/auth" + "soarca/pkg/auth/roles" + + "github.com/gin-gonic/gin" +) + +// Rule says who may call one route. +type Rule struct { + // Public routes need no credential at all. + Public bool + // Permission the caller must hold. Ignored for public routes. + Permission roles.Permission +} + +// Policy maps routes to rules. Keys are the method and the route pattern as +// registered with gin, e.g. "GET /playbook/:id". +type Policy map[string]Rule + +// RouteKey returns the Policy key for a method and a route pattern. +func RouteKey(method string, fullPath string) string { + return method + " " + fullPath +} + +// Protect authenticates and authorizes every request against policy. Install +// it with app.Use before registering routes, so it covers all of them: +// +// - a public route passes without a credential; +// - any other route needs a valid credential (401 otherwise) and the +// permission its rule names (403 otherwise); +// - a registered route missing from policy needs roles.Admin, so a new +// route is closed until someone decides who may call it; +// - a path that matches no route still needs a valid credential, so +// unauthenticated callers can't probe which routes exist. +func Protect(authenticator auth.IAuthenticator, policy Policy) gin.HandlerFunc { + return func(ginContext *gin.Context) { + fullPath := ginContext.FullPath() + rule, known := policy[RouteKey(ginContext.Request.Method, fullPath)] + if known && rule.Public { + ginContext.Next() + return + } + + principal, ok := authenticate(ginContext, authenticator) + if !ok { + return + } + + if fullPath != "" { + required := roles.Admin + if known { + required = rule.Permission + } else { + log.Warning("no permission rule for ", RouteKey(ginContext.Request.Method, fullPath), ", requiring ", roles.Admin) + } + if !principal.Has(required) { + deny(ginContext, principal, string(required)) + return + } + } + ginContext.Next() + } +} diff --git a/pkg/api/auth/policy_test.go b/pkg/api/auth/policy_test.go new file mode 100644 index 00000000..68d73335 --- /dev/null +++ b/pkg/api/auth/policy_test.go @@ -0,0 +1,79 @@ +package auth + +import ( + "net/http" + "testing" + + "soarca/pkg/auth" + "soarca/pkg/auth/roles" + + "github.com/gin-gonic/gin" + "github.com/go-playground/assert/v2" +) + +var admin = &auth.Principal{ + Subjects: []string{"authentik:user:admin"}, + Permissions: map[roles.Permission]bool{roles.Admin: true, roles.PlaybookRead: true}, +} + +var testPolicy = Policy{ + RouteKey(http.MethodGet, "/status/ping"): {Public: true}, + RouteKey(http.MethodGet, "/playbook/:id"): {Permission: roles.PlaybookRead}, + RouteKey(http.MethodPost, "/trigger/playbook"): {Permission: roles.PlaybookExecute}, +} + +func newProtectedRouter(authenticator auth.IAuthenticator) *gin.Engine { + gin.SetMode(gin.TestMode) + router := gin.New() + router.Use(Protect(authenticator, testPolicy)) + ok := func(ginContext *gin.Context) { + ginContext.String(http.StatusOK, PrincipalFromContext(ginContext).Self()) + } + router.GET("/status/ping", func(ginContext *gin.Context) { ginContext.String(http.StatusOK, "pong") }) + router.GET("/playbook/:id", ok) + router.POST("/trigger/playbook", ok) + router.GET("/unlisted", ok) + return router +} + +func TestPublicRouteNeedsNoCredential(t *testing.T) { + router := newProtectedRouter(authenticatorFor(nil)) + + recorder := serve(router, http.MethodGet, "/status/ping", "") + assert.Equal(t, recorder.Code, http.StatusOK) + assert.Equal(t, recorder.Body.String(), "pong") +} + +func TestProtectedRouteNeedsCredentialAndPermission(t *testing.T) { + router := newProtectedRouter(authenticatorFor(map[string]*auth.Principal{"executor": executor, "reader": reader})) + + assert.Equal(t, serve(router, http.MethodGet, "/playbook/abc", "").Code, http.StatusUnauthorized) + assert.Equal(t, serve(router, http.MethodGet, "/playbook/abc", "Bearer forged").Code, http.StatusUnauthorized) + assert.Equal(t, serve(router, http.MethodPost, "/trigger/playbook", "Bearer reader").Code, http.StatusForbidden) + + recorder := serve(router, http.MethodGet, "/playbook/abc", "Bearer reader") + assert.Equal(t, recorder.Code, http.StatusOK) + assert.Equal(t, recorder.Body.String(), "authentik:user:reader") + assert.Equal(t, serve(router, http.MethodPost, "/trigger/playbook", "Bearer executor").Code, http.StatusOK) +} + +func TestRouteWithoutRuleNeedsAdmin(t *testing.T) { + router := newProtectedRouter(authenticatorFor(map[string]*auth.Principal{"executor": executor, "admin": admin})) + + assert.Equal(t, serve(router, http.MethodGet, "/unlisted", "Bearer executor").Code, http.StatusForbidden) + assert.Equal(t, serve(router, http.MethodGet, "/unlisted", "Bearer admin").Code, http.StatusOK) +} + +func TestUnknownPathNeedsCredentialBefore404(t *testing.T) { + router := newProtectedRouter(authenticatorFor(map[string]*auth.Principal{"reader": reader})) + + assert.Equal(t, serve(router, http.MethodGet, "/does-not-exist", "").Code, http.StatusUnauthorized) + assert.Equal(t, serve(router, http.MethodGet, "/does-not-exist", "Bearer reader").Code, http.StatusNotFound) +} + +func TestRuleIsPerMethod(t *testing.T) { + router := newProtectedRouter(authenticatorFor(nil)) + router.POST("/status/ping", func(ginContext *gin.Context) { ginContext.Status(http.StatusOK) }) + + assert.Equal(t, serve(router, http.MethodPost, "/status/ping", "").Code, http.StatusUnauthorized) +} diff --git a/pkg/api/permissions.go b/pkg/api/permissions.go new file mode 100644 index 00000000..7ddef692 --- /dev/null +++ b/pkg/api/permissions.go @@ -0,0 +1,45 @@ +package api + +import ( + "net/http" + + apiAuth "soarca/pkg/api/auth" + "soarca/pkg/auth/roles" +) + +// Permissions says who may call each route. Keep it in step with the route +// table in docs/content/en/docs/installation-configuration/authentication. +// A route missing from it needs roles.Admin. +var Permissions = apiAuth.Policy{ + // Lets anything check that SOARCA is up. + route(http.MethodGet, "/status/ping"): {Public: true}, + // The Swagger UI and spec. Calls made from the UI need a token, which + // the UI's Authorize button adds. + route(http.MethodGet, "/swagger/*any"): {Public: true}, + + route(http.MethodGet, "/status/"): {Permission: roles.PlaybookRead}, + + route(http.MethodGet, "/playbook/"): {Permission: roles.PlaybookRead}, + route(http.MethodGet, "/playbook/meta/"): {Permission: roles.PlaybookRead}, + route(http.MethodGet, "/playbook/:id"): {Permission: roles.PlaybookRead}, + route(http.MethodPost, "/playbook/"): {Permission: roles.PlaybookWrite}, + route(http.MethodPut, "/playbook/:id"): {Permission: roles.PlaybookWrite}, + route(http.MethodDelete, "/playbook/:id"): {Permission: roles.PlaybookWrite}, + route(http.MethodGet, "/reporter/"): {Permission: roles.PlaybookRead}, + route(http.MethodGet, "/reporter/:id"): {Permission: roles.PlaybookRead}, + route(http.MethodPost, "/trigger/playbook"): {Permission: roles.PlaybookExecute}, + route(http.MethodPost, "/trigger/playbook/:id"): {Permission: roles.PlaybookExecute}, + + route(http.MethodGet, "/manual/"): {Permission: roles.PlaybookExecute}, + route(http.MethodGet, "/manual/:exec_id/:step_id"): {Permission: roles.PlaybookExecute}, + route(http.MethodPost, "/manual/continue"): {Permission: roles.PlaybookExecute}, + + route(http.MethodGet, "/keymanagement/"): {Permission: roles.Admin}, + route(http.MethodPut, "/keymanagement/:keyname"): {Permission: roles.Admin}, + route(http.MethodPatch, "/keymanagement/:keyname"): {Permission: roles.Admin}, + route(http.MethodDelete, "/keymanagement/:keyname"): {Permission: roles.Admin}, +} + +func route(method string, path string) string { + return apiAuth.RouteKey(method, path) +} diff --git a/pkg/api/permissions_test.go b/pkg/api/permissions_test.go new file mode 100644 index 00000000..b3287bd8 --- /dev/null +++ b/pkg/api/permissions_test.go @@ -0,0 +1,64 @@ +package api + +import ( + "testing" + + apiAuth "soarca/pkg/api/auth" + mock_database_controller "soarca/test/unittest/mocks/mock_controller/database" + mock_playbook "soarca/test/unittest/mocks/mock_playbook_database" + + "github.com/gin-gonic/gin" +) + +// allRoutes registers every route the way the controller does. +func allRoutes(t *testing.T) gin.RoutesInfo { + gin.SetMode(gin.TestMode) + app := gin.New() + database := &mock_database_controller.Mock_Controller{} + database.On("GetDatabaseInstance").Return(&mock_playbook.MockPlaybook{}) + + if err := Api(app, nil, database); err != nil { + t.Fatal(err) + } + if err := Database(app, database); err != nil { + t.Fatal(err) + } + if err := Reporter(app, nil); err != nil { + t.Fatal(err) + } + KeyManagement(app, nil) + Manual(app, nil) + Swagger(app) + return app.Routes() +} + +func TestEveryRouteHasAPermission(t *testing.T) { + for _, info := range allRoutes(t) { + if _, ok := Permissions[apiAuth.RouteKey(info.Method, info.Path)]; !ok { + t.Errorf("%s %s has no entry in Permissions", info.Method, info.Path) + } + } +} + +func TestEveryPermissionHasARoute(t *testing.T) { + registered := map[string]bool{} + for _, info := range allRoutes(t) { + registered[apiAuth.RouteKey(info.Method, info.Path)] = true + } + for key := range Permissions { + if !registered[key] { + t.Errorf("Permissions lists %s, which isn't a route", key) + } + } +} + +func TestOnlyPingAndSwaggerArePublic(t *testing.T) { + for key, rule := range Permissions { + if rule.Public && key != "GET /status/ping" && key != "GET /swagger/*any" { + t.Errorf("%s is public", key) + } + if !rule.Public && rule.Permission == "" { + t.Errorf("%s needs a permission or Public", key) + } + } +} diff --git a/pkg/auth/anonymous.go b/pkg/auth/anonymous.go new file mode 100644 index 00000000..95afe065 --- /dev/null +++ b/pkg/auth/anonymous.go @@ -0,0 +1,28 @@ +package auth + +import ( + "context" + + "soarca/pkg/auth/roles" +) + +// AnonymousName labels the principal Anonymous returns. +const AnonymousName = "anonymous" + +// Anonymous accepts every request, with or without a credential, as a caller +// that holds every permission. SOARCA uses it when authentication is +// disabled, so handlers can always read the caller from the request. +type Anonymous struct{} + +func (anonymous Anonymous) Authenticate(ctx context.Context, credential string) (*Principal, error) { + permissions := map[roles.Permission]bool{} + for _, permission := range roles.All() { + permissions[permission] = true + } + return &Principal{ + Provider: AnonymousName, + Subject: AnonymousName, + Permissions: permissions, + Subjects: []string{AnonymousName}, + }, nil +} diff --git a/pkg/auth/anonymous_test.go b/pkg/auth/anonymous_test.go new file mode 100644 index 00000000..9f2e9fd0 --- /dev/null +++ b/pkg/auth/anonymous_test.go @@ -0,0 +1,29 @@ +package auth_test + +import ( + "context" + "testing" + + "soarca/pkg/auth" + "soarca/pkg/auth/roles" + + "github.com/go-playground/assert/v2" +) + +func TestAnonymousHoldsEveryPermission(t *testing.T) { + principal, err := auth.Anonymous{}.Authenticate(context.Background(), "") + assert.Equal(t, err, nil) + assert.Equal(t, principal.Provider, auth.AnonymousName) + assert.Equal(t, principal.Self(), auth.AnonymousName) + for _, permission := range roles.All() { + assert.Equal(t, principal.Has(permission), true) + } +} + +func TestAnonymousReturnsAFreshPrincipal(t *testing.T) { + first, _ := auth.Anonymous{}.Authenticate(context.Background(), "") + first.Permissions[roles.Admin] = false + + second, _ := auth.Anonymous{}.Authenticate(context.Background(), "") + assert.Equal(t, second.Has(roles.Admin), true) +} From c9417d30bbe66332bf22dda97150e3ccef6ec79c Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:14:36 +0200 Subject: [PATCH 2/7] Updated swagger comments so it works correctly --- cmd/soarca/main.go | 5 +++++ pkg/api/keymanagement/keymanagement_api.go | 12 ++++++++++++ pkg/api/manual/manual_api.go | 9 +++++++++ pkg/api/playbook/playbook_api.go | 18 ++++++++++++++++++ pkg/api/reporter/reporter_api.go | 6 ++++++ pkg/api/status/status_api.go | 3 +++ pkg/api/trigger/trigger_api.go | 6 ++++++ 7 files changed, 59 insertions(+) diff --git a/cmd/soarca/main.go b/cmd/soarca/main.go index e7735192..d7acd312 100644 --- a/cmd/soarca/main.go +++ b/cmd/soarca/main.go @@ -38,6 +38,11 @@ const banner = ` // @title SOARCA API // @version 1.0.0 +// +// @securityDefinitions.apikey BearerAuth +// @in header +// @name Authorization +// @description "Bearer " followed by an access token or API key. Not needed when AUTH_ENABLED is false. func main() { fmt.Print(banner) log.Info("Version: ", Version) diff --git a/pkg/api/keymanagement/keymanagement_api.go b/pkg/api/keymanagement/keymanagement_api.go index 18d24212..2bfd10c2 100644 --- a/pkg/api/keymanagement/keymanagement_api.go +++ b/pkg/api/keymanagement/keymanagement_api.go @@ -40,6 +40,9 @@ func NewKeyManagementHandler(manager *keymanagement.KeyManagement) *KeyManagemen // @Produce json // @success 200 {array} string // @failure 400 {object} api.Error +// @failure 401 {object} api.Error +// @failure 403 {object} api.Error +// @Security BearerAuth // @Router /keymanagement/ [GET] func (handler *KeyManagementHandler) GetKeys(context *gin.Context) { keyInfo, err := handler.Manager.ListAllNames() @@ -63,6 +66,9 @@ func (handler *KeyManagementHandler) GetKeys(context *gin.Context) { // @Produce json // @success 200 {json} Empty // @failure 400 {object} api.Error +// @failure 401 {object} api.Error +// @failure 403 {object} api.Error +// @Security BearerAuth // @Router /keymanagement/{keyname} [PUT] func (handler *KeyManagementHandler) AddKey(context *gin.Context) { keyname := context.Param("keyname") @@ -99,6 +105,9 @@ func (handler *KeyManagementHandler) AddKey(context *gin.Context) { // @Produce json // @success 200 {json} Empty // @failure 400 {object} api.Error +// @failure 401 {object} api.Error +// @failure 403 {object} api.Error +// @Security BearerAuth // @Router /keymanagement/{keyname} [PATCH] func (handler *KeyManagementHandler) UpdateKey(context *gin.Context) { keyname := context.Param("keyname") @@ -133,6 +142,9 @@ func (handler *KeyManagementHandler) UpdateKey(context *gin.Context) { // @Produce json // @success 200 {json} Empty // @failure 400 {object} api.Error +// @failure 401 {object} api.Error +// @failure 403 {object} api.Error +// @Security BearerAuth // @Router /keymanagement/{keyname} [DELETE] func (handler *KeyManagementHandler) RevokeKey(context *gin.Context) { keyname := context.Param("keyname") diff --git a/pkg/api/manual/manual_api.go b/pkg/api/manual/manual_api.go index ff4bb918..bc023012 100644 --- a/pkg/api/manual/manual_api.go +++ b/pkg/api/manual/manual_api.go @@ -60,6 +60,9 @@ func NewManualHandler(interaction interaction.IInteractionStorage) *ManualHandle // @Produce json // @Success 200 {object} []api.InteractionCommandData // @failure 400 {object} []api.InteractionCommandData +// @failure 401 {object} api.Error +// @failure 403 {object} api.Error +// @Security BearerAuth // @Router /manual/ [GET] func (manualHandler *ManualHandler) GetPendingCommands(g *gin.Context) { commands, err := manualHandler.interactionCapability.GetPendingCommands() @@ -92,6 +95,9 @@ func (manualHandler *ManualHandler) GetPendingCommands(g *gin.Context) { // @Param step_id path string true "step ID" // @Success 200 {object} api.InteractionCommandData // @failure 400 {object} api.Error +// @failure 401 {object} api.Error +// @failure 403 {object} api.Error +// @Security BearerAuth // @Router /manual/{exec_id}/{step_id} [GET] func (manualHandler *ManualHandler) GetPendingCommand(g *gin.Context) { execution_id := g.Param("exec_id") @@ -137,6 +143,9 @@ func (manualHandler *ManualHandler) GetPendingCommand(g *gin.Context) { // @Param data body api.ManualOutArgsUpdatePayload true "playbook" // @Success 200 {object} api.Execution // @failure 400 {object} api.Error +// @failure 401 {object} api.Error +// @failure 403 {object} api.Error +// @Security BearerAuth // @Router /manual/continue [POST] func (manualHandler *ManualHandler) PostContinue(g *gin.Context) { diff --git a/pkg/api/playbook/playbook_api.go b/pkg/api/playbook/playbook_api.go index e8b108ef..1d9668e8 100644 --- a/pkg/api/playbook/playbook_api.go +++ b/pkg/api/playbook/playbook_api.go @@ -40,6 +40,9 @@ func NewPlaybookHandler(controller database.IController) *playbookHandler { // @Produce json // @success 200 {array} cacao.Playbook // @failure 400 {object} api.Error +// @failure 401 {object} api.Error +// @failure 403 {object} api.Error +// @Security BearerAuth // @Router /playbook/ [GET] func (handler *playbookHandler) GetAllPlaybooks(g *gin.Context) { log.Trace("Trying to obtain all playbook IDs") @@ -64,6 +67,9 @@ func (handler *playbookHandler) GetAllPlaybooks(g *gin.Context) { // @Produce json // @success 200 {array} api.PlaybookMeta // @failure 400 {object} api.Error +// @failure 401 {object} api.Error +// @failure 403 {object} api.Error +// @Security BearerAuth // @Router /playbook/meta [GET] func (handler *playbookHandler) GetAllPlaybookMetas(g *gin.Context) { log.Trace("Trying to obtain all playbook IDs") @@ -89,6 +95,9 @@ func (handler *playbookHandler) GetAllPlaybookMetas(g *gin.Context) { // @Param data body cacao.Playbook true "playbook" // @Success 200 {object} cacao.Playbook // @failure 400 {object} api.Error +// @failure 401 {object} api.Error +// @failure 403 {object} api.Error +// @Security BearerAuth // @Router /playbook/ [POST] func (handler *playbookHandler) SubmitPlaybook(g *gin.Context) { jsonData, err := io.ReadAll(g.Request.Body) @@ -121,6 +130,9 @@ func (handler *playbookHandler) SubmitPlaybook(g *gin.Context) { // @Param id path string true "playbook ID" // @Success 200 {object} cacao.Playbook // @failure 400 {object} api.Error +// @failure 401 {object} api.Error +// @failure 403 {object} api.Error +// @Security BearerAuth // @Router /playbook/{id} [GET] func (handler *playbookHandler) GetPlaybookByID(g *gin.Context) { id := g.Param("id") @@ -147,6 +159,9 @@ func (handler *playbookHandler) GetPlaybookByID(g *gin.Context) { // @Param data body cacao.Playbook true "playbook" // @Success 200 {object} cacao.Playbook // @failure 400 {object} api.Error +// @failure 401 {object} api.Error +// @failure 403 {object} api.Error +// @Security BearerAuth // @Router /playbook/{id} [PUT] func (handler *playbookHandler) UpdatePlaybookByID(g *gin.Context) { id := g.Param("id") @@ -178,6 +193,9 @@ func (handler *playbookHandler) UpdatePlaybookByID(g *gin.Context) { // @Param id path string true "playbook ID" // @Success 200 // @failure 400 {object} api.Error +// @failure 401 {object} api.Error +// @failure 403 {object} api.Error +// @Security BearerAuth // @Router /playbook/{id} [DELETE] func (handler *playbookHandler) DeleteByPlaybookID(g *gin.Context) { id := g.Param("id") diff --git a/pkg/api/reporter/reporter_api.go b/pkg/api/reporter/reporter_api.go index 682f123e..da8c6779 100644 --- a/pkg/api/reporter/reporter_api.go +++ b/pkg/api/reporter/reporter_api.go @@ -40,6 +40,9 @@ func NewReportHandler(informer informer.IExecutionInformer) *reportHandler { // @Produce json // @success 200 {array} api.PlaybookExecutionReport // @failure 400 {object} api.Error +// @failure 401 {object} api.Error +// @failure 403 {object} api.Error +// @Security BearerAuth // @Router /reporter [GET] func (reportHandler *reportHandler) GetExecutions(g *gin.Context) { executions, err := reportHandler.informer.GetExecutions() @@ -75,6 +78,9 @@ func (reportHandler *reportHandler) GetExecutions(g *gin.Context) { // @Param id path string true "execution identifier" // @success 200 {object} api.PlaybookExecutionReport // @failure 400 {object} api.Error +// @failure 401 {object} api.Error +// @failure 403 {object} api.Error +// @Security BearerAuth // @Router /reporter/{id} [GET] func (handler *reportHandler) GetExecutionReport(g *gin.Context) { id := g.Param("id") diff --git a/pkg/api/status/status_api.go b/pkg/api/status/status_api.go index 622ac654..a9cee3e1 100644 --- a/pkg/api/status/status_api.go +++ b/pkg/api/status/status_api.go @@ -44,6 +44,9 @@ func GetPong(g *gin.Context) { // @Produce json // @success 200 {object} api.Status // @failure 400 {object} api.Error +// @failure 401 {object} api.Error +// @failure 403 {object} api.Error +// @Security BearerAuth // @Router /status [GET] func GetApi(g *gin.Context) { status.Uptime.Milliseconds = uint64(time.Since(status.Uptime.Since).Milliseconds()) diff --git a/pkg/api/trigger/trigger_api.go b/pkg/api/trigger/trigger_api.go index f5d9ca5c..265e8b31 100644 --- a/pkg/api/trigger/trigger_api.go +++ b/pkg/api/trigger/trigger_api.go @@ -60,6 +60,9 @@ func NewTriggerHandler(controller decomposer_controller.IController, database da // @Param data body cacao.Variables true "playbook" // @Success 200 {object} api.Execution // @failure 400 {object} api.Error +// @failure 401 {object} api.Error +// @failure 403 {object} api.Error +// @Security BearerAuth // @Router /trigger/playbook/{id} [POST] func (handler *TriggerHandler) ExecuteById(context *gin.Context) { log.Trace("received execute by ID") @@ -101,6 +104,9 @@ func (handler *TriggerHandler) ExecuteById(context *gin.Context) { // @Param playbook body cacao.Playbook true "execute playbook by payload" // @Success 200 {object} api.Execution // @failure 400 {object} api.Error +// @failure 401 {object} api.Error +// @failure 403 {object} api.Error +// @Security BearerAuth // @Router /trigger/playbook [POST] func (handler *TriggerHandler) Execute(context *gin.Context) { log.Trace("received execute with body") From 253cccd261ec24286bcd49e94744c4b6c06a4169 Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:14:43 +0200 Subject: [PATCH 3/7] Updated config --- internal/config/config.go | 5 +-- internal/config/config_test.go | 81 ++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 4ba91638..93b6b531 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -69,14 +69,13 @@ type TheHive struct { } // Auth configures who may call the API. With AUTH_ENABLED=true, at least one -// identity provider must be configured. The OIDC_* settings are the ones -// gauth used. +// identity provider must be configured. type Auth struct { Enabled bool `env:"AUTH_ENABLED" envDefault:"false"` Issuer string `env:"OIDC_ISSUER"` ClientId string `env:"OIDC_CLIENT_ID"` SkipTlsVerify bool `env:"OIDC_SKIP_TLS_VERIFY" envDefault:"false"` - // Members of AUTH_GROUP get every permission, as they did with gauth. + // Members of AUTH_GROUP get every permission. AdminGroup string `env:"AUTH_GROUP" envDefault:"soarca_admin"` Entra Entra // JSON file with hashed static API keys. Empty disables API keys. diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 9cd1772f..9919b140 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -2,10 +2,14 @@ package config import ( "encoding/json" + "os" + "reflect" + "regexp" "strings" "testing" "github.com/go-playground/assert/v2" + "github.com/joho/godotenv" ) // patch mirrors what an API handler does with a request body. @@ -203,3 +207,80 @@ func TestLoadIgnoresAuthSettingsWhenAuthDisabled(t *testing.T) { _, err := Load() assert.Equal(t, err, nil) } + +// envSettings returns every setting SOARCA reads, keyed by its env tag. +func envSettings() map[string]reflect.StructTag { + settings := map[string]reflect.StructTag{} + var collect func(typ reflect.Type) + collect = func(typ reflect.Type) { + for index := 0; index < typ.NumField(); index++ { + field := typ.Field(index) + if field.Type.Kind() == reflect.Struct { + collect(field.Type) + } else if name := field.Tag.Get("env"); name != "" { + settings[name] = field.Tag + } + } + } + collect(reflect.TypeOf(Static{})) + collect(reflect.TypeOf(Dynamic{})) + return settings +} + +// envExample returns every variable in .env.example, commented out or not, +// with its value unquoted. +func envExample(t *testing.T) map[string]string { + content, err := os.ReadFile("../../.env.example") + assert.Equal(t, err, nil) + listed := map[string]string{} + for _, match := range regexp.MustCompile(`(?m)^#?\s*([A-Z][A-Z0-9_]+):\s*(.*)$`).FindAllStringSubmatch(string(content), -1) { + listed[match[1]] = strings.Trim(strings.TrimSpace(match[2]), `"`) + } + return listed +} + +// TestEnvExampleListsEverySetting keeps .env.example in step with the +// structs above: every env tag must appear in it, commented out or not, +// and it must not list variables SOARCA doesn't read. +func TestEnvExampleListsEverySetting(t *testing.T) { + listed := envExample(t) + known := envSettings() + + for name := range known { + if _, ok := listed[name]; !ok { + t.Errorf("%s is read by SOARCA but missing from .env.example", name) + } + } + for name := range listed { + if _, ok := known[name]; !ok { + t.Errorf("%s is in .env.example but SOARCA doesn't read it", name) + } + } +} + +// TestEnvExampleShowsDefaults makes sure .env.example doesn't suggest a +// default that differs from the code. Settings without a default may hold +// any example value. +func TestEnvExampleShowsDefaults(t *testing.T) { + listed := envExample(t) + for name, tag := range envSettings() { + fallback, hasDefault := tag.Lookup("envDefault") + value, ok := listed[name] + if hasDefault && ok && value != fallback { + t.Errorf("%s is %q in .env.example but defaults to %q", name, value, fallback) + } + } +} + +// TestEnvExampleLoads checks that the example, as shipped, is valid. +func TestEnvExampleLoads(t *testing.T) { + values, err := godotenv.Read("../../.env.example") + assert.Equal(t, err, nil) + for name, value := range values { + t.Setenv(name, value) + } + + config, err := Load() + assert.Equal(t, err, nil) + assert.Equal(t, config.Static.Auth.Issuer, "https://authentikuri:9443/application/o/soarca/") +} From a5acd8040779480e67cbeda2bfeb77d6901140ac Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:14:58 +0200 Subject: [PATCH 4/7] Fixed comment with gauth reference --- pkg/auth/providers/oidc/roles.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/auth/providers/oidc/roles.go b/pkg/auth/providers/oidc/roles.go index 52d33225..55daa8b7 100644 --- a/pkg/auth/providers/oidc/roles.go +++ b/pkg/auth/providers/oidc/roles.go @@ -2,7 +2,7 @@ package oidc import "soarca/pkg/auth/roles" -// DefaultAdminGroup is the group that gauth-based SOARCA required for all +// DefaultAdminGroup is the group that older SOARCA versions required for all // access. It keeps full access. const DefaultAdminGroup = "soarca_admin" From 3f9174fac2c2c43701cfa6c19707c3248b42801a Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:16:12 +0200 Subject: [PATCH 5/7] Fixed apikeys could not have soarca:admin permissions --- internal/controller/authentication_test.go | 14 ++++++++++++++ pkg/auth/principal.go | 5 +++-- pkg/auth/principal_test.go | 8 ++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/internal/controller/authentication_test.go b/internal/controller/authentication_test.go index 5ff81f48..dc9fce32 100644 --- a/internal/controller/authentication_test.go +++ b/internal/controller/authentication_test.go @@ -77,3 +77,17 @@ func TestEveryProviderTogether(t *testing.T) { _, err = authenticator.Authenticate(context.Background(), "") assert.NotEqual(t, err, nil) } + +func TestAdminApiKeyReachesEveryPermission(t *testing.T) { + plaintext, hash, _ := apikey.Generate() + path := writeKeys(t, `[{"name":"admin","sha256":"`+hash+`","permissions":["soarca:admin"],"expires":"2999-01-01T00:00:00Z"}]`) + + authenticator, err := newAuthenticator(config.Auth{Enabled: true, ApiKeysFile: path}) + assert.Equal(t, err, nil) + + principal, err := authenticator.Authenticate(context.Background(), plaintext) + assert.Equal(t, err, nil) + for _, permission := range roles.All() { + assert.Equal(t, principal.Has(permission), true) + } +} diff --git a/pkg/auth/principal.go b/pkg/auth/principal.go index 109d11fb..f48209c1 100644 --- a/pkg/auth/principal.go +++ b/pkg/auth/principal.go @@ -39,9 +39,10 @@ type Principal struct { GroupsIncomplete bool `json:"groups_incomplete,omitempty"` } -// Has reports whether the principal holds permission. +// Has reports whether the principal holds permission. roles.Admin implies +// every permission, whichever provider granted it. func (principal *Principal) Has(permission roles.Permission) bool { - return principal != nil && principal.Permissions[permission] + return principal != nil && (principal.Permissions[permission] || principal.Permissions[roles.Admin]) } // Self returns the subject that identifies the caller itself. diff --git a/pkg/auth/principal_test.go b/pkg/auth/principal_test.go index a5a2eaef..f4032ca2 100644 --- a/pkg/auth/principal_test.go +++ b/pkg/auth/principal_test.go @@ -15,6 +15,14 @@ func TestHas(t *testing.T) { assert.Equal(t, principal.Has(roles.PlaybookExecute), false) } +func TestAdminHasEveryPermission(t *testing.T) { + principal := &Principal{Permissions: map[roles.Permission]bool{roles.Admin: true}} + + for _, permission := range roles.All() { + assert.Equal(t, principal.Has(permission), true) + } +} + func TestNilPrincipalHasNothing(t *testing.T) { var principal *Principal From ce8a6498c8a23083bd9b8120125f51460cf15dad Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:23:56 +0200 Subject: [PATCH 6/7] Fixed env.example --- .env.example | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index d0157ba4..8b4a6431 100644 --- a/.env.example +++ b/.env.example @@ -5,15 +5,15 @@ ### Server PORT: 8080 ENABLE_TLS: false -CERT_FILE: "/certs/server.crt" -CERT_KEY_FILE: "/certs/server.key" +CERT_FILE: "./certs/server.crt" +CERT_KEY_FILE: "./certs/server.key" GIN_MODE: "release" # Comma-separated, or "*" for any origin. SOARCA_ALLOWED_ORIGINS: "*" ### Execution MAX_EXECUTIONS: 10 -MAX_REPORTERS: 5 +MAX_REPORTERS: 10 # Empty uses the CACAO v2 schema embedded in SOARCA. VALIDATION_SCHEMA_URL: "" HTTP_SKIP_CERT_VALIDATION: false @@ -26,7 +26,7 @@ DB_PASSWORD: "rootpassword" ### Logging LOG_GLOBAL_LEVEL: "info" -LOG_MODE: "development" +LOG_MODE: "production" LOG_FILE_PATH: "" LOG_FORMAT: "json" @@ -69,4 +69,4 @@ AUTH_GROUP: "soarca_admin" ### SSH key management ENABLE_SSH_KMS: false -SSH_KMS_DIR: "deployments/docker/testing/ssh-kms-test/ssh-keystore/" \ No newline at end of file +SSH_KMS_DIR: "deployments/docker/testing/ssh-kms-test/ssh-keystore/" From d85733f1584bca35ed2908e3cc43b4256934dd5b Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:29:41 +0200 Subject: [PATCH 7/7] Updated naming of AUTH_GROUP to AUTH_ADMIN_GROUP for clarity --- .env.example | 2 +- internal/config/config.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 8b4a6431..a56f0e0d 100644 --- a/.env.example +++ b/.env.example @@ -53,7 +53,7 @@ OIDC_ISSUER: "https://authentikuri:9443/application/o/soarca/" OIDC_CLIENT_ID: "some client ID" OIDC_SKIP_TLS_VERIFY: false # Members of this group get every permission. -AUTH_GROUP: "soarca_admin" +AUTH_ADMIN_GROUP: "soarca_admin" # Microsoft Entra ID # AUTH_ENTRA_TENANT_IDS: "," diff --git a/internal/config/config.go b/internal/config/config.go index 93b6b531..f61c1409 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -75,8 +75,8 @@ type Auth struct { Issuer string `env:"OIDC_ISSUER"` ClientId string `env:"OIDC_CLIENT_ID"` SkipTlsVerify bool `env:"OIDC_SKIP_TLS_VERIFY" envDefault:"false"` - // Members of AUTH_GROUP get every permission. - AdminGroup string `env:"AUTH_GROUP" envDefault:"soarca_admin"` + // Members of AUTH_ADMIN_GROUP get every permission. + AdminGroup string `env:"AUTH_ADMIN_GROUP" envDefault:"soarca_admin"` Entra Entra // JSON file with hashed static API keys. Empty disables API keys. ApiKeysFile string `env:"AUTH_API_KEYS_FILE"`