From 1ad862d86c5a6685f37c884aa2d358772bcb8905 Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 4 Nov 2025 17:37:40 +0200 Subject: [PATCH] wip --- internal/bootstrap/app_bootstrap.go | 9 ++- internal/controller/proxy_controller_test.go | 2 +- internal/service/access_controls_service.go | 70 +++-------------- internal/service/docker_service.go | 2 +- internal/utils/app_utils.go | 55 ++++++++------ internal/utils/app_utils_test.go | 44 ++++++++++- internal/utils/decoders/decoders.go | 76 ------------------- internal/utils/decoders/env_decoder.go | 14 ++-- internal/utils/decoders/env_decoder_test.go | 14 ++-- internal/utils/decoders/flags_decoder.go | 36 +++++---- internal/utils/decoders/flags_decoder_test.go | 14 ++-- internal/utils/decoders/label_decoder.go | 12 ++- internal/utils/decoders/label_decoder_test.go | 37 ++++----- 13 files changed, 155 insertions(+), 230 deletions(-) delete mode 100644 internal/utils/decoders/decoders.go diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index fdbd382..dad0ae2 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -62,6 +62,13 @@ func (app *BootstrapApp) Setup() error { return err } + // Get access controls + acls, err := utils.GetACLS(os.Environ(), os.Args) + + if err != nil { + return err + } + // Get cookie domain cookieDomain, err := utils.GetCookieDomain(app.config.AppURL) @@ -139,7 +146,7 @@ func (app *BootstrapApp) Setup() error { // Create services dockerService := service.NewDockerService() - aclsService := service.NewAccessControlsService(dockerService) + aclsService := service.NewAccessControlsService(dockerService, acls) authService := service.NewAuthService(authConfig, dockerService, ldapService, database) oauthBrokerService := service.NewOAuthBrokerService(oauthProviders) diff --git a/internal/controller/proxy_controller_test.go b/internal/controller/proxy_controller_test.go index e7e27cf..6a9e444 100644 --- a/internal/controller/proxy_controller_test.go +++ b/internal/controller/proxy_controller_test.go @@ -40,7 +40,7 @@ func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.En assert.NilError(t, dockerService.Init()) // Access controls - accessControlsService := service.NewAccessControlsService(dockerService) + accessControlsService := service.NewAccessControlsService(dockerService, config.Apps{}) assert.NilError(t, accessControlsService.Init()) diff --git a/internal/service/access_controls_service.go b/internal/service/access_controls_service.go index cde27e5..398b759 100644 --- a/internal/service/access_controls_service.go +++ b/internal/service/access_controls_service.go @@ -1,82 +1,34 @@ package service import ( - "os" "strings" "tinyauth/internal/config" - "tinyauth/internal/utils/decoders" "github.com/rs/zerolog/log" ) type AccessControlsService struct { - docker *DockerService - envACLs config.Apps + docker *DockerService + nonDocker config.Apps } -func NewAccessControlsService(docker *DockerService) *AccessControlsService { +func NewAccessControlsService(docker *DockerService, nonDocker config.Apps) *AccessControlsService { return &AccessControlsService{ - docker: docker, + docker: docker, + nonDocker: nonDocker, } } func (acls *AccessControlsService) Init() error { - acls.envACLs = config.Apps{} - env := os.Environ() - appEnvVars := []string{} - - for _, e := range env { - if strings.HasPrefix(e, "TINYAUTH_APPS_") { - appEnvVars = append(appEnvVars, e) - } - } - - err := acls.loadEnvACLs(appEnvVars) - - if err != nil { - return err - } - return nil } -func (acls *AccessControlsService) loadEnvACLs(appEnvVars []string) error { - if len(appEnvVars) == 0 { +func (acls *AccessControlsService) lookupNonDockerACLs(appDomain string) *config.App { + if len(acls.nonDocker.Apps) == 0 { return nil } - envAcls := map[string]string{} - - for _, e := range appEnvVars { - parts := strings.SplitN(e, "=", 2) - if len(parts) != 2 { - continue - } - - // Normalize key, this should use the same normalization logic as in utils/decoders/decoders.go - key := parts[0] - key = strings.ToLower(key) - key = strings.ReplaceAll(key, "_", ".") - value := parts[1] - envAcls[key] = value - } - - apps, err := decoders.DecodeLabels(envAcls) - - if err != nil { - return err - } - - acls.envACLs = apps - return nil -} - -func (acls *AccessControlsService) lookupEnvACLs(appDomain string) *config.App { - if len(acls.envACLs.Apps) == 0 { - return nil - } - - for appName, appACLs := range acls.envACLs.Apps { + for appName, appACLs := range acls.nonDocker.Apps { if appACLs.Config.Domain == appDomain { return &appACLs } @@ -90,11 +42,11 @@ func (acls *AccessControlsService) lookupEnvACLs(appDomain string) *config.App { } func (acls *AccessControlsService) GetAccessControls(appDomain string) (config.App, error) { - // First check environment variables - envACLs := acls.lookupEnvACLs(appDomain) + // First check non-docker apps + envACLs := acls.lookupNonDockerACLs(appDomain) if envACLs != nil { - log.Debug().Str("domain", appDomain).Msg("Found matching access controls in environment variables") + log.Debug().Str("domain", appDomain).Msg("Found matching access controls in environment variables or flags") return *envACLs, nil } diff --git a/internal/service/docker_service.go b/internal/service/docker_service.go index b0f977d..9ef19f5 100644 --- a/internal/service/docker_service.go +++ b/internal/service/docker_service.go @@ -82,7 +82,7 @@ func (docker *DockerService) GetLabels(appDomain string) (config.App, error) { return config.App{}, err } - labels, err := decoders.DecodeLabels(inspect.Config.Labels) + labels, err := decoders.DecodeLabels[config.Apps](inspect.Config.Labels) if err != nil { return config.App{}, err } diff --git a/internal/utils/app_utils.go b/internal/utils/app_utils.go index 76044c9..15b9bdc 100644 --- a/internal/utils/app_utils.go +++ b/internal/utils/app_utils.go @@ -134,20 +134,11 @@ func GetLogLevel(level string) zerolog.Level { } } -func GetOAuthProvidersConfig(env []string, args []string, appUrl string) (map[string]config.OAuthServiceConfig, error) { +func GetOAuthProvidersConfig(environ []string, args []string, appUrl string) (map[string]config.OAuthServiceConfig, error) { providers := make(map[string]config.OAuthServiceConfig) // Get from environment variables - envMap := make(map[string]string) - - for _, e := range env { - pair := strings.SplitN(e, "=", 2) - if len(pair) == 2 { - envMap[pair[0]] = pair[1] - } - } - - envProviders, err := decoders.DecodeEnv[config.Providers, config.OAuthServiceConfig](envMap, "providers") + envProviders, err := decoders.DecodeEnv[config.Providers](environ) if err != nil { return nil, err @@ -155,25 +146,14 @@ func GetOAuthProvidersConfig(env []string, args []string, appUrl string) (map[st maps.Copy(providers, envProviders.Providers) - // Get from flags - flagsMap := make(map[string]string) - - for _, arg := range args[1:] { - if strings.HasPrefix(arg, "--") { - pair := strings.SplitN(arg[2:], "=", 2) - if len(pair) == 2 { - flagsMap[pair[0]] = pair[1] - } - } - } - - flagProviders, err := decoders.DecodeFlags[config.Providers, config.OAuthServiceConfig](flagsMap, "providers") + // Get from args + argProviders, err := decoders.DecodeFlags[config.Providers](args) if err != nil { return nil, err } - maps.Copy(providers, flagProviders.Providers) + maps.Copy(providers, argProviders.Providers) // For every provider get correct secret from file if set for name, provider := range providers { @@ -208,3 +188,28 @@ func GetOAuthProvidersConfig(env []string, args []string, appUrl string) (map[st // Return combined providers return providers, nil } + +func GetACLS(environ []string, args []string) (config.Apps, error) { + acls := config.Apps{} + acls.Apps = make(map[string]config.App) + + // Get from environment variables + envACLs, err := decoders.DecodeEnv[config.Apps](environ) + + if err != nil { + return config.Apps{}, err + } + + maps.Copy(acls.Apps, envACLs.Apps) + + // Get from args + argACLs, err := decoders.DecodeFlags[config.Apps](args) + + if err != nil { + return config.Apps{}, err + } + + maps.Copy(acls.Apps, argACLs.Apps) + + return acls, nil +} diff --git a/internal/utils/app_utils_test.go b/internal/utils/app_utils_test.go index e6cdaeb..cfa1e73 100644 --- a/internal/utils/app_utils_test.go +++ b/internal/utils/app_utils_test.go @@ -238,8 +238,8 @@ func TestIsRedirectSafeMultiLevel(t *testing.T) { } func TestGetOAuthProvidersConfig(t *testing.T) { - env := []string{"PROVIDERS_CLIENT1_CLIENT_ID=client1-id", "PROVIDERS_CLIENT1_CLIENT_SECRET=client1-secret"} - args := []string{"/tinyauth/tinyauth", "--providers-client2-client-id=client2-id", "--providers-client2-client-secret=client2-secret"} + env := []string{"PROVIDERS_CLIENT1_CLIENTID=client1-id", "PROVIDERS_CLIENT1_CLIENTSECRET=client1-secret"} + args := []string{"/tinyauth/tinyauth", "--providers-client2-clientid=client2-id", "--providers-client2-clientsecret=client2-secret"} expected := map[string]config.OAuthServiceConfig{ "client1": { @@ -278,7 +278,7 @@ func TestGetOAuthProvidersConfig(t *testing.T) { assert.NilError(t, err) defer os.Remove("/tmp/tinyauth_test_file") - env = []string{"PROVIDERS_CLIENT1_CLIENT_ID=client1-id", "PROVIDERS_CLIENT1_CLIENT_SECRET_FILE=/tmp/tinyauth_test_file"} + env = []string{"PROVIDERS_CLIENT1_CLIENTID=client1-id", "PROVIDERS_CLIENT1_CLIENTSECRETFILE=/tmp/tinyauth_test_file"} args = []string{"/tinyauth/tinyauth"} expected = map[string]config.OAuthServiceConfig{ "client1": { @@ -293,7 +293,7 @@ func TestGetOAuthProvidersConfig(t *testing.T) { assert.DeepEqual(t, expected, result) // Case with google provider and no redirect URL - env = []string{"PROVIDERS_GOOGLE_CLIENT_ID=google-id", "PROVIDERS_GOOGLE_CLIENT_SECRET=google-secret"} + env = []string{"PROVIDERS_GOOGLE_CLIENTID=google-id", "PROVIDERS_GOOGLE_CLIENTSECRET=google-secret"} args = []string{"/tinyauth/tinyauth"} expected = map[string]config.OAuthServiceConfig{ "google": { @@ -308,3 +308,39 @@ func TestGetOAuthProvidersConfig(t *testing.T) { assert.NilError(t, err) assert.DeepEqual(t, expected, result) } + +func TestGetACLS(t *testing.T) { + // Setup + env := []string{"TINYAUTH_APPS_APP1_CONFIG_DOMAIN=app1.com", "TINYAUTH_APPS_APP2_CONFIG_DOMAIN=app2.com"} + args := []string{"--apps-app3-config-domain=app3.com", "--apps-app4-config-domain=app4.com"} + + expected := config.Apps{ + Apps: map[string]config.App{ + "app1": { + Config: config.AppConfig{ + Domain: "app1.com", + }, + }, + "app2": { + Config: config.AppConfig{ + Domain: "app2.com", + }, + }, + "app3": { + Config: config.AppConfig{ + Domain: "app3.com", + }, + }, + "app4": { + Config: config.AppConfig{ + Domain: "app4.com", + }, + }, + }, + } + + // Test + result, err := utils.GetACLS(env, args) + assert.NilError(t, err) + assert.DeepEqual(t, expected, result) +} diff --git a/internal/utils/decoders/decoders.go b/internal/utils/decoders/decoders.go deleted file mode 100644 index 28b72fb..0000000 --- a/internal/utils/decoders/decoders.go +++ /dev/null @@ -1,76 +0,0 @@ -package decoders - -import ( - "reflect" - "strings" - - "github.com/stoewer/go-strcase" -) - -func normalizeKeys[T any](input map[string]string, root string, sep string) map[string]string { - knownKeys := getKnownKeys[T]() - normalized := make(map[string]string) - - for k, v := range input { - parts := []string{"tinyauth"} - - key := strings.ToLower(k) - key = strings.ReplaceAll(key, sep, "-") - - suffix := "" - - for _, known := range knownKeys { - if strings.HasSuffix(key, known) { - suffix = known - break - } - } - - if suffix == "" { - continue - } - - parts = append(parts, root) - - id := strings.TrimPrefix(key, root+"-") - id = strings.TrimSuffix(id, "-"+suffix) - - if id == "" { - continue - } - - parts = append(parts, id) - parts = append(parts, suffix) - - final := "" - - for i, part := range parts { - if i > 0 { - final += "." - } - final += strcase.LowerCamelCase(part) - } - - normalized[final] = v - } - - return normalized -} - -func getKnownKeys[T any]() []string { - var keys []string - var t T - - v := reflect.ValueOf(t) - typeOfT := v.Type() - - for field := range typeOfT.NumField() { - if typeOfT.Field(field).Tag.Get("field") != "" { - keys = append(keys, typeOfT.Field(field).Tag.Get("field")) - continue - } - keys = append(keys, strcase.KebabCase(typeOfT.Field(field).Name)) - } - - return keys -} diff --git a/internal/utils/decoders/env_decoder.go b/internal/utils/decoders/env_decoder.go index 532ec64..c5518a4 100644 --- a/internal/utils/decoders/env_decoder.go +++ b/internal/utils/decoders/env_decoder.go @@ -1,19 +1,17 @@ package decoders import ( - "github.com/traefik/paerser/parser" + "github.com/traefik/paerser/env" ) -func DecodeEnv[T any, C any](env map[string]string, subName string) (T, error) { - var result T +func DecodeEnv[T any](environ []string) (T, error) { + var target T - normalized := normalizeKeys[C](env, subName, "_") - - err := parser.Decode(normalized, &result, "tinyauth", "tinyauth."+subName) + err := env.Decode(environ, "TINYAUTH_", &target) if err != nil { - return result, err + return target, err } - return result, nil + return target, nil } diff --git a/internal/utils/decoders/env_decoder_test.go b/internal/utils/decoders/env_decoder_test.go index da679f0..0d90073 100644 --- a/internal/utils/decoders/env_decoder_test.go +++ b/internal/utils/decoders/env_decoder_test.go @@ -10,11 +10,11 @@ import ( func TestDecodeEnv(t *testing.T) { // Setup - env := map[string]string{ - "PROVIDERS_GOOGLE_CLIENT_ID": "google-client-id", - "PROVIDERS_GOOGLE_CLIENT_SECRET": "google-client-secret", - "PROVIDERS_MY_GITHUB_CLIENT_ID": "github-client-id", - "PROVIDERS_MY_GITHUB_CLIENT_SECRET": "github-client-secret", + env := []string{ + "TINYAUTH_PROVIDERS_GOOGLE_CLIENTID=google-client-id", + "TINYAUTH_PROVIDERS_GOOGLE_CLIENTSECRET=google-client-secret", + "TINYAUTH_PROVIDERS_GITHUB_CLIENTID=github-client-id", + "TINYAUTH_PROVIDERS_GITHUB_CLIENTSECRET=github-client-secret", } expected := config.Providers{ @@ -23,7 +23,7 @@ func TestDecodeEnv(t *testing.T) { ClientID: "google-client-id", ClientSecret: "google-client-secret", }, - "myGithub": { + "github": { ClientID: "github-client-id", ClientSecret: "github-client-secret", }, @@ -31,7 +31,7 @@ func TestDecodeEnv(t *testing.T) { } // Execute - result, err := decoders.DecodeEnv[config.Providers, config.OAuthServiceConfig](env, "providers") + result, err := decoders.DecodeEnv[config.Providers](env) assert.NilError(t, err) assert.DeepEqual(t, result, expected) } diff --git a/internal/utils/decoders/flags_decoder.go b/internal/utils/decoders/flags_decoder.go index 0aae234..ec35729 100644 --- a/internal/utils/decoders/flags_decoder.go +++ b/internal/utils/decoders/flags_decoder.go @@ -3,28 +3,32 @@ package decoders import ( "strings" - "github.com/traefik/paerser/parser" + "github.com/traefik/paerser/flag" ) -func DecodeFlags[T any, C any](flags map[string]string, subName string) (T, error) { - var result T +func DecodeFlags[T any](args []string) (T, error) { + var target T + var formatted = []string{} - filtered := filterFlags(flags) - normalized := normalizeKeys[C](filtered, subName, "_") + for _, arg := range args { + argFmt := strings.TrimPrefix(arg, "--") + argParts := strings.SplitN(argFmt, "=", 2) - err := parser.Decode(normalized, &result, "tinyauth", "tinyauth."+subName) + if len(argParts) != 2 { + continue + } + + key := argParts[0] + value := argParts[1] + + formatted = append(formatted, "--"+strings.ReplaceAll(key, "-", ".")+"="+value) + } + + err := flag.Decode(formatted, &target) if err != nil { - return result, err + return target, err } - return result, nil -} - -func filterFlags(flags map[string]string) map[string]string { - filtered := make(map[string]string) - for k, v := range flags { - filtered[strings.TrimPrefix(k, "--")] = v - } - return filtered + return target, nil } diff --git a/internal/utils/decoders/flags_decoder_test.go b/internal/utils/decoders/flags_decoder_test.go index 935dea0..7150129 100644 --- a/internal/utils/decoders/flags_decoder_test.go +++ b/internal/utils/decoders/flags_decoder_test.go @@ -10,11 +10,11 @@ import ( func TestDecodeFlags(t *testing.T) { // Setup - flags := map[string]string{ - "--providers-google-client-id": "google-client-id", - "--providers-google-client-secret": "google-client-secret", - "--providers-my-github-client-id": "github-client-id", - "--providers-my-github-client-secret": "github-client-secret", + args := []string{ + "--providers-google-clientid=google-client-id", + "--providers-google-clientsecret=google-client-secret", + "--providers-github-clientid=github-client-id", + "--providers-github-clientsecret=github-client-secret", } expected := config.Providers{ @@ -23,7 +23,7 @@ func TestDecodeFlags(t *testing.T) { ClientID: "google-client-id", ClientSecret: "google-client-secret", }, - "myGithub": { + "github": { ClientID: "github-client-id", ClientSecret: "github-client-secret", }, @@ -31,7 +31,7 @@ func TestDecodeFlags(t *testing.T) { } // Execute - result, err := decoders.DecodeFlags[config.Providers, config.OAuthServiceConfig](flags, "providers") + result, err := decoders.DecodeFlags[config.Providers](args) assert.NilError(t, err) assert.DeepEqual(t, result, expected) } diff --git a/internal/utils/decoders/label_decoder.go b/internal/utils/decoders/label_decoder.go index e83e275..2a866fb 100644 --- a/internal/utils/decoders/label_decoder.go +++ b/internal/utils/decoders/label_decoder.go @@ -1,19 +1,17 @@ package decoders import ( - "tinyauth/internal/config" - "github.com/traefik/paerser/parser" ) -func DecodeLabels(labels map[string]string) (config.Apps, error) { - var appLabels config.Apps +func DecodeLabels[T any](labels map[string]string) (T, error) { + var target T - err := parser.Decode(labels, &appLabels, "tinyauth", "tinyauth.apps") + err := parser.Decode(labels, &target, "tinyauth") if err != nil { - return config.Apps{}, err + return target, err } - return appLabels, nil + return target, nil } diff --git a/internal/utils/decoders/label_decoder_test.go b/internal/utils/decoders/label_decoder_test.go index 63189d1..670a39c 100644 --- a/internal/utils/decoders/label_decoder_test.go +++ b/internal/utils/decoders/label_decoder_test.go @@ -9,7 +9,24 @@ import ( ) func TestDecodeLabels(t *testing.T) { - // Variables + // Setup + labels := map[string]string{ + "tinyauth.apps.foo.config.domain": "example.com", + "tinyauth.apps.foo.users.allow": "user1,user2", + "tinyauth.apps.foo.users.block": "user3", + "tinyauth.apps.foo.oauth.whitelist": "somebody@example.com", + "tinyauth.apps.foo.oauth.groups": "group3", + "tinyauth.apps.foo.ip.allow": "10.71.0.1/24,10.71.0.2", + "tinyauth.apps.foo.ip.block": "10.10.10.10,10.0.0.0/24", + "tinyauth.apps.foo.ip.bypass": "192.168.1.1", + "tinyauth.apps.foo.response.headers": "X-Foo=Bar,X-Baz=Qux", + "tinyauth.apps.foo.response.basicauth.username": "admin", + "tinyauth.apps.foo.response.basicauth.password": "password", + "tinyauth.apps.foo.response.basicauth.passwordfile": "/path/to/passwordfile", + "tinyauth.apps.foo.path.allow": "/public", + "tinyauth.apps.foo.path.block": "/private", + } + expected := config.Apps{ Apps: map[string]config.App{ "foo": { @@ -44,25 +61,9 @@ func TestDecodeLabels(t *testing.T) { }, }, } - test := map[string]string{ - "tinyauth.apps.foo.config.domain": "example.com", - "tinyauth.apps.foo.users.allow": "user1,user2", - "tinyauth.apps.foo.users.block": "user3", - "tinyauth.apps.foo.oauth.whitelist": "somebody@example.com", - "tinyauth.apps.foo.oauth.groups": "group3", - "tinyauth.apps.foo.ip.allow": "10.71.0.1/24,10.71.0.2", - "tinyauth.apps.foo.ip.block": "10.10.10.10,10.0.0.0/24", - "tinyauth.apps.foo.ip.bypass": "192.168.1.1", - "tinyauth.apps.foo.response.headers": "X-Foo=Bar,X-Baz=Qux", - "tinyauth.apps.foo.response.basicauth.username": "admin", - "tinyauth.apps.foo.response.basicauth.password": "password", - "tinyauth.apps.foo.response.basicauth.passwordfile": "/path/to/passwordfile", - "tinyauth.apps.foo.path.allow": "/public", - "tinyauth.apps.foo.path.block": "/private", - } // Test - result, err := decoders.DecodeLabels(test) + result, err := decoders.DecodeLabels[config.Apps](labels) assert.NilError(t, err) assert.DeepEqual(t, expected, result) }