Compare commits

..
Author SHA1 Message Date
dependabot[bot]andGitHub 68903ffc64 chore(deps): bump docker/setup-docker-action from 5.4.0 to 5.5.0
Bumps [docker/setup-docker-action](https://github.com/docker/setup-docker-action) from 5.4.0 to 5.5.0.
- [Release notes](https://github.com/docker/setup-docker-action/releases)
- [Commits](https://github.com/docker/setup-docker-action/compare/77e84dbf09b47d1e29270283c22f16145aa85ca1...2bf61fb9464cc67f0cbdeabed6aa0380accd1c70)

---
updated-dependencies:
- dependency-name: docker/setup-docker-action
  dependency-version: 5.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-21 08:15:09 +00:00
17 changed files with 865 additions and 1203 deletions
-3
View File
@@ -33,9 +33,6 @@ jobs:
with: with:
sqlc-version: "1.31.1" sqlc-version: "1.31.1"
- name: Setup controller-gen
run: go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.22.0
- name: Check codegen is up to date - name: Check codegen is up to date
run: | run: |
sqlc generate sqlc generate
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
package_json_file: ./e2e/package.json package_json_file: ./e2e/package.json
- name: Set up Docker - name: Set up Docker
uses: docker/setup-docker-action@77e84dbf09b47d1e29270283c22f16145aa85ca1 # v5.4.0 uses: docker/setup-docker-action@2bf61fb9464cc67f0cbdeabed6aa0380accd1c70 # v5.5.0
- name: Install dependencies - name: Install dependencies
run: pnpm ci run: pnpm ci
+1 -6
View File
@@ -19,12 +19,7 @@ PROD_COMPOSE := $(shell test -f "docker-compose.test.prod.yml" && echo "docker-c
.DEFAULT_GOAL := binary .DEFAULT_GOAL := binary
.PHONY: deps clean-data clean-webui webui binary binary-linux-amd64 binary-linux-arm64 test vet test-race dev dev-infisical prod prod-infisical sql generate docker docker-distroless tools .PHONY: deps clean-data clean-webui webui binary binary-linux-amd64 binary-linux-arm64 test vet test-race dev dev-infisical prod prod-infisical sql generate docker docker-distroless
# Tools
tools:
go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.22.0
go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1
# Deps # Deps
deps: deps:
-18
View File
@@ -28,7 +28,6 @@ require (
golang.org/x/oauth2 v0.36.0 golang.org/x/oauth2 v0.36.0
golang.org/x/tools v0.49.0 golang.org/x/tools v0.49.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.37.0
k8s.io/apimachinery v0.37.0 k8s.io/apimachinery v0.37.0
k8s.io/client-go v0.37.0 k8s.io/client-go v0.37.0
modernc.org/sqlite v1.58.0 modernc.org/sqlite v1.58.0
@@ -70,7 +69,6 @@ require (
github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.9.1 // indirect github.com/fxamacker/cbor/v2 v2.9.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect
@@ -78,26 +76,11 @@ require (
github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect
github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v1.0.0 // indirect
github.com/go-openapi/jsonreference v1.0.0 // indirect
github.com/go-openapi/swag v0.27.1 // indirect
github.com/go-openapi/swag/cmdutils v0.27.1 // indirect
github.com/go-openapi/swag/conv v0.27.1 // indirect
github.com/go-openapi/swag/fileutils v0.27.1 // indirect
github.com/go-openapi/swag/jsonutils v0.27.1 // indirect
github.com/go-openapi/swag/loading v0.27.1 // indirect
github.com/go-openapi/swag/mangling v0.27.1 // indirect
github.com/go-openapi/swag/netutils v0.27.1 // indirect
github.com/go-openapi/swag/pools v0.27.1 // indirect
github.com/go-openapi/swag/stringutils v0.27.1 // indirect
github.com/go-openapi/swag/typeutils v0.27.1 // indirect
github.com/go-openapi/swag/yamlutils v0.27.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect github.com/goccy/go-yaml v1.19.2 // indirect
github.com/google/gnostic-models v0.7.1 // indirect
github.com/huandu/xstrings v1.5.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa // indirect github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
@@ -157,7 +140,6 @@ require (
golang.org/x/text v0.41.0 // indirect golang.org/x/text v0.41.0 // indirect
golang.org/x/time v0.15.0 // indirect golang.org/x/time v0.15.0 // indirect
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gotest.tools/v3 v3.5.2 // indirect gotest.tools/v3 v3.5.2 // indirect
k8s.io/klog/v2 v2.140.0 // indirect k8s.io/klog/v2 v2.140.0 // indirect
+2 -8
View File
@@ -138,8 +138,6 @@ github.com/go-openapi/swag/fileutils v0.27.1 h1:QQqBSoi5mW4XpU85nS0mLcA+zAE6vLzr
github.com/go-openapi/swag/fileutils v0.27.1/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8= github.com/go-openapi/swag/fileutils v0.27.1/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8=
github.com/go-openapi/swag/jsonutils v0.27.1 h1:SVgK3i4USzCU5mibOOS/l4ea2h9UQXy7J7RNLTjuXjU= github.com/go-openapi/swag/jsonutils v0.27.1 h1:SVgK3i4USzCU5mibOOS/l4ea2h9UQXy7J7RNLTjuXjU=
github.com/go-openapi/swag/jsonutils v0.27.1/go.mod h1:tdlEpZqdcQ17uj6J4YdK9vd8It5qWMwjWXOs0tjpRlk= github.com/go-openapi/swag/jsonutils v0.27.1/go.mod h1:tdlEpZqdcQ17uj6J4YdK9vd8It5qWMwjWXOs0tjpRlk=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.1 h1:mJu3COL9WEaZVp/Kf2PRMi7tPszPEJfSr/OO75ynCs8=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.1/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY=
github.com/go-openapi/swag/loading v0.27.1 h1:/DxUgDXKbBX4bcn7r9uEXfJyzN5XpiJmZplzQTjrRCY= github.com/go-openapi/swag/loading v0.27.1 h1:/DxUgDXKbBX4bcn7r9uEXfJyzN5XpiJmZplzQTjrRCY=
github.com/go-openapi/swag/loading v0.27.1/go.mod h1:jvGh3iA2+zyUUycB5fgJWzeHnhrpvGnJJM0RVE9ZShE= github.com/go-openapi/swag/loading v0.27.1/go.mod h1:jvGh3iA2+zyUUycB5fgJWzeHnhrpvGnJJM0RVE9ZShE=
github.com/go-openapi/swag/mangling v0.27.1 h1:yC9D0HyUE8gbP+BfmGx9+AA89ikwZTMjESK3OnnoaqA= github.com/go-openapi/swag/mangling v0.27.1 h1:yC9D0HyUE8gbP+BfmGx9+AA89ikwZTMjESK3OnnoaqA=
@@ -154,10 +152,6 @@ github.com/go-openapi/swag/typeutils v0.27.1 h1:KSTdFlfnse4r6dP9IrEnwMldjE+zs71U
github.com/go-openapi/swag/typeutils v0.27.1/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ= github.com/go-openapi/swag/typeutils v0.27.1/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ=
github.com/go-openapi/swag/yamlutils v0.27.1 h1:ftxv6xvXb1E3zohUc+okZ9nSqNb9StQX/FXnKZ98sQA= github.com/go-openapi/swag/yamlutils v0.27.1 h1:ftxv6xvXb1E3zohUc+okZ9nSqNb9StQX/FXnKZ98sQA=
github.com/go-openapi/swag/yamlutils v0.27.1/go.mod h1:bnxFIB1qewGRiZHypXGZ3fNgf13/0HfRgnS/iZBDrOo= github.com/go-openapi/swag/yamlutils v0.27.1/go.mod h1:bnxFIB1qewGRiZHypXGZ3fNgf13/0HfRgnS/iZBDrOo=
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0=
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo=
github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug=
github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -174,8 +168,8 @@ github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63Y
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA= github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE= github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 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/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+7 -9
View File
@@ -42,7 +42,7 @@ func NewAccessControlsService(i AccessControlServiceInput) *AccessControlsServic
} }
} }
func ensureAscii(str string) bool { func (service *AccessControlsService) ensureAscii(str string) bool {
for i := 0; i < len(str); i++ { for i := 0; i < len(str); i++ {
if str[i] > unicode.MaxASCII { if str[i] > unicode.MaxASCII {
return false return false
@@ -51,7 +51,7 @@ func ensureAscii(str string) bool {
return true return true
} }
func normalizeDomain(domain string) string { func (service *AccessControlsService) normalizeDomain(domain string) string {
if host, _, err := net.SplitHostPort(domain); err == nil { if host, _, err := net.SplitHostPort(domain); err == nil {
domain = host domain = host
} }
@@ -60,11 +60,11 @@ func normalizeDomain(domain string) string {
} }
func (service *AccessControlsService) getACLs(domain string, lookup func(locator func(name string, app *model.App) bool) error) (*model.App, error) { func (service *AccessControlsService) getACLs(domain string, lookup func(locator func(name string, app *model.App) bool) error) (*model.App, error) {
if !ensureAscii(domain) { if !service.ensureAscii(domain) {
return nil, errors.New("domain contains non-ascii characters") return nil, errors.New("domain contains non-ascii characters")
} }
normalizedDomain := normalizeDomain(domain) normalizedDomain := service.normalizeDomain(domain)
if !strings.HasSuffix(normalizedDomain, "."+service.runtime.CookieDomain) && normalizedDomain != service.runtime.CookieDomain { if !strings.HasSuffix(normalizedDomain, "."+service.runtime.CookieDomain) && normalizedDomain != service.runtime.CookieDomain {
return nil, fmt.Errorf("domain does not match cookie domain, expected %s (or a subdomain), got %s", service.runtime.CookieDomain, domain) return nil, fmt.Errorf("domain does not match cookie domain, expected %s (or a subdomain), got %s", service.runtime.CookieDomain, domain)
@@ -76,11 +76,11 @@ func (service *AccessControlsService) getACLs(domain string, lookup func(locator
locatorFunc := func(name string, app *model.App) bool { locatorFunc := func(name string, app *model.App) bool {
if app.Config.Domain != "" { if app.Config.Domain != "" {
if !ensureAscii(app.Config.Domain) { if !service.ensureAscii(app.Config.Domain) {
service.log.App.Warn().Str("name", name).Str("domain", app.Config.Domain).Msg("Domain contains non-ascii characters, skipping") service.log.App.Warn().Str("name", name).Str("domain", app.Config.Domain).Msg("Domain contains non-ascii characters, skipping")
return false return false
} }
if normalizedDomain == normalizeDomain(app.Config.Domain) { if normalizedDomain == service.normalizeDomain(app.Config.Domain) {
service.log.App.Debug().Str("name", name).Msg("Found matching container by domain") service.log.App.Debug().Str("name", name).Msg("Found matching container by domain")
domainMatch = app domainMatch = app
return true return true
@@ -145,9 +145,7 @@ func (service *AccessControlsService) GetAccessControls(domain string) (*model.A
// If we have a label provider configured, try to get ACLs from it // If we have a label provider configured, try to get ACLs from it
if service.labelProvider != nil { if service.labelProvider != nil {
return service.getACLs(domain, func(locator func(name string, app *model.App) bool) error { return service.getACLs(domain, service.labelProvider.Lookup)
return service.labelProvider.Lookup(locator)
})
} }
// No labels // No labels
@@ -186,10 +186,9 @@ func TestAccessControlsService(t *testing.T) {
// get acls should return an error when the provider fails // get acls should return an error when the provider fails
mock := newMockProvider(map[string]model.App{}, true) mock := newMockProvider(map[string]model.App{}, true)
acls := NewAccessControlsService(AccessControlServiceInput{ acls := NewAccessControlsService(AccessControlServiceInput{
Log: log, Log: log,
Runtime: &runtime, Runtime: &runtime,
Config: &model.Config{}, Config: &model.Config{},
LabelProvider: mock,
}) })
_, err := acls.getACLs("example.com", mock.Lookup) _, err := acls.getACLs("example.com", mock.Lookup)
assert.Error(t, err) assert.Error(t, err)
@@ -1,83 +0,0 @@
package service
import (
"context"
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"github.com/tinyauthapp/tinyauth/pkg/apis/tinyauth/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
type KubernetesCRDInput struct {
Log *logger.Logger
Client kubernetes.Interface
}
type KubernetesCRDExtractor struct {
log *logger.Logger
client kubernetes.Interface
}
func NewKubernetesCRDExtractor(i KubernetesCRDInput) *KubernetesCRDExtractor {
return &KubernetesCRDExtractor{
log: i.Log,
client: i.Client,
}
}
func (k *KubernetesCRDExtractor) Extract(app *v1alpha1.Application) ExtractionResult {
meta := &ResourceMeta{
Typ: ResourceTypeCRD,
Name: app.GetName(),
Namespace: app.GetNamespace(),
}
if !ensureResourceMeta(meta) {
k.log.App.Warn().Str("namespace", meta.Namespace).Str("name", meta.Name).Msg("Resource has no namespace or name, skipping")
return ExtractionResult{}
}
if app.Spec.Config.Domain == "" {
k.log.App.Warn().Str("name", meta.Name).Str("namespace", meta.Namespace).Msg("Application has no domain, skipping")
return ExtractionResult{
Meta: meta,
Apps: nil,
}
}
if !ensureAscii(app.Spec.Config.Domain) {
k.log.App.Warn().Str("name", meta.Name).Str("namespace", meta.Namespace).Str("domain", app.Spec.Config.Domain).Msg("Domain is invalid, skipping")
return ExtractionResult{
Meta: meta,
Apps: nil,
}
}
// Convert the CRD to the internal representation.
internalApp := app.Spec.ToInternalApp()
passwordRef := app.Spec.Response.BasicAuth.PasswordSecretRef
if passwordRef != nil {
secret, err := k.client.CoreV1().Secrets(meta.Namespace).Get(context.Background(), passwordRef.Name, metav1.GetOptions{})
if err != nil {
k.log.App.Warn().Err(err).Str("namespace", meta.Namespace).Str("name", meta.Name).Str("secret", passwordRef.Name).Str("key", passwordRef.Key).Msg("Failed to read basic auth password Secret, skipping")
return ExtractionResult{Meta: meta}
}
password, ok := secret.Data[passwordRef.Key]
if !ok {
k.log.App.Warn().Str("namespace", meta.Namespace).Str("name", meta.Name).Str("secret", passwordRef.Name).Str("key", passwordRef.Key).Msg("Basic auth password Secret key does not exist, skipping")
return ExtractionResult{Meta: meta}
}
internalApp.Response.BasicAuth.Password = string(password)
}
return ExtractionResult{
Meta: meta,
Apps: map[string]model.App{
meta.Name: internalApp,
},
}
}
@@ -1,134 +0,0 @@
package service
import (
"slices"
"strings"
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/decoders"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
networking "k8s.io/api/networking/v1"
)
func hostMatchesHostname(host string, hostname string) bool {
host = normalizeDomain(host)
hostname = normalizeDomain(hostname)
if suffix, ok := strings.CutPrefix(host, "*."); ok {
return strings.HasSuffix(hostname, "."+suffix)
}
return host == hostname
}
func hostCoversName(host string, name string) bool {
host = strings.ToLower(host)
if strings.HasPrefix(host, "*.") {
return true
}
return strings.HasPrefix(host, strings.ToLower(name+"."))
}
type KubernetesIngressExtractor struct {
log *logger.Logger
}
type KubernetesIngressExtractorInput struct {
Log *logger.Logger
}
func NewKubernetesIngressExtractor(i KubernetesIngressExtractorInput) *KubernetesIngressExtractor {
return &KubernetesIngressExtractor{
log: i.Log,
}
}
func (k *KubernetesIngressExtractor) getPaths(rule networking.IngressRule) []string {
var paths []string
if rule.HTTP == nil {
return paths
}
for _, path := range rule.HTTP.Paths {
paths = append(paths, path.Path)
}
return paths
}
func (k *KubernetesIngressExtractor) getHosts(rules []networking.IngressRule) []string {
var hosts []string
for _, rule := range rules {
hosts = append(hosts, rule.Host)
paths := k.getPaths(rule)
if len(paths) == 0 {
continue
}
if !slices.Contains(paths, "/") {
k.log.App.Warn().Strs("hosts", hosts).Strs("paths", paths).Msg("Ingress rule does not contain a catch-all path, another ingress may be able to bypass auth checks if it routes the same host with a different path. Consider adding a catch-all path to this rule to ensure auth checks are applied to all paths for this host.")
}
}
return hosts
}
func (k *KubernetesIngressExtractor) Extract(ingress *networking.Ingress) ExtractionResult {
meta := &ResourceMeta{
Typ: ResourceTypeIngress,
Name: ingress.GetName(),
Namespace: ingress.GetNamespace(),
}
if !ensureResourceMeta(meta) {
k.log.App.Warn().Str("namespace", meta.Namespace).Str("name", meta.Name).Msg("Resource has no namespace or name, skipping")
return ExtractionResult{}
}
annotations := ingress.GetAnnotations()
hosts := k.getHosts(ingress.Spec.Rules)
if len(hosts) == 0 {
k.log.App.Warn().Str("namespace", meta.Namespace).Str("name", meta.Name).Msg("No hosts found in resource, skipping")
return ExtractionResult{
Meta: meta,
}
}
labels, err := decoders.DecodeLabels[model.Apps](annotations, "apps")
if err != nil {
k.log.App.Warn().Err(err).Str("namespace", meta.Namespace).Str("name", meta.Name).Msg("Failed to decode resource labels, skipping")
return ExtractionResult{
Meta: meta,
}
}
apps := make(map[string]model.App)
for name, config := range labels.Apps {
if config.Config.Domain != "" {
if !ensureAscii(config.Config.Domain) {
k.log.App.Warn().Err(err).Str("namespace", meta.Namespace).Str("name", meta.Name).Str("domain", config.Config.Domain).Msg("Domain is invalid, matching will rely on app name")
} else {
if slices.ContainsFunc(hosts, func(host string) bool {
return hostMatchesHostname(host, config.Config.Domain)
}) {
apps[name] = config
continue
}
}
}
if slices.ContainsFunc(hosts, func(host string) bool {
return hostCoversName(host, name)
}) {
apps[name] = config
}
}
return ExtractionResult{
Meta: meta,
Apps: apps,
}
}
+200 -209
View File
@@ -3,123 +3,43 @@ package service
import ( import (
"context" "context"
"fmt" "fmt"
"slices"
"strings"
"sync" "sync"
"time" "time"
"github.com/steveiliop56/ding" "github.com/steveiliop56/ding"
"github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/decoders"
"github.com/tinyauthapp/tinyauth/internal/utils/logger" "github.com/tinyauthapp/tinyauth/internal/utils/logger"
"github.com/tinyauthapp/tinyauth/pkg/apis/tinyauth/v1alpha1" "github.com/tinyauthapp/tinyauth/pkg/validators"
"go.uber.org/dig" "go.uber.org/dig"
networking "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
) )
type watchedResource struct { type ingressEntry struct {
gvr schema.GroupVersionResource name string
typ ResourceType app model.App
} }
func (w watchedResource) pretty() string { type ingressKey struct {
return w.gvr.Group + "/" + w.gvr.Version + "/" + w.gvr.Resource namespace string
} name string
func ensureResourceMeta(meta *ResourceMeta) bool {
return meta.Name != "" && meta.Namespace != ""
}
type ResourceMeta struct {
Typ ResourceType
Name string
Namespace string
}
type ExtractionResult struct {
Meta *ResourceMeta
Apps map[string]model.App
}
type ResourceType string
const (
ResourceTypeIngress ResourceType = "ingress"
ResourceTypeCRD ResourceType = "crd"
)
var supportedResources = []watchedResource{
{
gvr: schema.GroupVersionResource{
Group: "networking.k8s.io",
Version: "v1",
Resource: "ingresses",
},
typ: ResourceTypeIngress,
},
{
gvr: schema.GroupVersionResource{
Group: "tinyauth.app",
Version: "v1alpha1",
Resource: "applications",
},
},
}
type typedItem struct {
typ ResourceType
ingress *networking.Ingress
crd *v1alpha1.Application
}
func convertFromUnstructured[T any](obj *unstructured.Unstructured) (*T, error) {
var typed *T
err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &typed)
if err != nil {
var zero *T
return zero, fmt.Errorf("failed to convert ingress to typed object: %w", err)
}
return typed, nil
}
func (ti *typedItem) fromUnstructured(typ ResourceType, obj *unstructured.Unstructured) (*typedItem, error) {
switch typ {
case ResourceTypeIngress:
typed, err := convertFromUnstructured[networking.Ingress](obj)
if err != nil {
return nil, err
}
return &typedItem{
typ: ResourceTypeIngress,
ingress: typed,
}, nil
case ResourceTypeCRD:
typed, err := convertFromUnstructured[v1alpha1.Application](obj)
if err != nil {
return nil, err
}
return &typedItem{
typ: ResourceTypeCRD,
crd: typed,
}, nil
default:
return nil, fmt.Errorf("unknown resource type %s", typ)
}
} }
type KubernetesService struct { type KubernetesService struct {
log *logger.Logger log *logger.Logger
apps map[ResourceMeta]map[string]model.App client dynamic.Interface
client dynamic.Interface connected bool
typedClient kubernetes.Interface mu sync.RWMutex
mu sync.RWMutex ingressEntries map[ingressKey][]ingressEntry
connected bool
} }
type KubernetesServiceInput struct { type KubernetesServiceInput struct {
@@ -140,43 +60,33 @@ func NewKubernetesService(i KubernetesServiceInput) (*KubernetesService, error)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create kubernetes client: %w", err) return nil, fmt.Errorf("failed to create kubernetes client: %w", err)
} }
typedClient, err := kubernetes.NewForConfig(cfg)
if err != nil { gvr := schema.GroupVersionResource{
return nil, fmt.Errorf("failed to create typed kubernetes client: %w", err) Group: "networking.k8s.io",
Version: "v1",
Resource: "ingresses",
} }
accessCtx, accessCancel := context.WithTimeout(i.Ctx, 5*time.Second)
defer accessCancel()
_, err = client.Resource(gvr).List(accessCtx, metav1.ListOptions{Limit: 1})
if err != nil {
i.Log.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Failed to access Ingress API, Kubernetes label provider will be disabled")
return nil, fmt.Errorf("failed to access ingress api: %w", err)
}
i.Log.App.Debug().Str("api", gvr.GroupVersion().String()).Msg("Successfully accessed Ingress API, starting watcher")
service := &KubernetesService{ service := &KubernetesService{
log: i.Log, log: i.Log,
client: client, client: client,
typedClient: typedClient, ingressEntries: make(map[ingressKey][]ingressEntry),
apps: make(map[ResourceMeta]map[string]model.App),
} }
watchedGVRs := make(map[string]bool) i.Ding.Go(func(ctx context.Context) {
service.watchGVR(gvr, ctx)
for _, res := range supportedResources { }, ding.RingMajor)
ctx, cancel := context.WithTimeout(i.Ctx, 5*time.Second)
_, err := client.Resource(res.gvr).List(ctx, metav1.ListOptions{Limit: 1})
cancel()
if err != nil {
// The CRD may not be available yet, so we'll fall back to ingress
i.Log.App.Warn().Err(err).Str("res", res.pretty()).Msg("Failed to access resource, skipping watcher")
continue
}
i.Log.App.Debug().Str("res", res.pretty()).Msg("Successfully accessed resource, starting watcher")
i.Ding.Go(func(ctx context.Context) {
service.watchGVR(res, ctx)
}, ding.RingMajor)
watchedGVRs[res.gvr.Resource] = true
}
if len(watchedGVRs) == 0 {
return nil, fmt.Errorf("failed to access any supported kubernetes api (ingresses, httproutes, grpcroutes)")
}
service.connected = true service.connected = true
i.Log.App.Debug().Msg("Kubernetes label provider started successfully") i.Log.App.Debug().Msg("Kubernetes label provider started successfully")
@@ -184,101 +94,182 @@ func NewKubernetesService(i KubernetesServiceInput) (*KubernetesService, error)
return service, nil return service, nil
} }
func (k *KubernetesService) addResource(result ExtractionResult) { func (k *KubernetesService) addIngressEntries(key ingressKey, entries []ingressEntry) {
k.mu.Lock() k.mu.Lock()
defer k.mu.Unlock() defer k.mu.Unlock()
k.apps[*result.Meta] = result.Apps k.ingressEntries[key] = entries
} }
func (k *KubernetesService) removeResource(meta ResourceMeta) { func (k *KubernetesService) removeIngress(key ingressKey) {
k.mu.Lock() k.mu.Lock()
defer k.mu.Unlock() defer k.mu.Unlock()
delete(k.apps, meta) delete(k.ingressEntries, key)
} }
func (k *KubernetesService) getEntry(locator func(name string, app *model.App) bool) { func (k *KubernetesService) getEntry(locator func(name string, app *model.App) bool) {
k.mu.RLock() k.mu.RLock()
defer k.mu.RUnlock() defer k.mu.RUnlock()
for _, apps := range k.apps { // O(n^2) is not great but the number of ingress entries is expected to be small
for name, app := range apps { for _, entries := range k.ingressEntries {
if ok := locator(name, &app); ok { for _, entry := range entries {
if ok := locator(entry.name, &entry.app); ok {
return return
} }
} }
} }
} }
func (k *KubernetesService) watchedItemChange(res watchedResource, typedItem *typedItem, event watch.EventType) { func (k *KubernetesService) extractPaths(rule map[string]any) ([]string, error) {
if typedItem == nil { http, found, err := unstructured.NestedMap(rule, "http")
k.log.App.Warn().Str("res", res.pretty()).Msg("Resource is nil, skipping") if err != nil {
return return nil, fmt.Errorf("reading http from rule: %w", err)
} }
if !found {
var result ExtractionResult return nil, nil
switch typedItem.typ {
case ResourceTypeIngress:
if typedItem.ingress == nil {
k.log.App.Warn().Str("res", res.pretty()).Msg("Ingress is nil, skipping")
return
}
extractor := NewKubernetesIngressExtractor(KubernetesIngressExtractorInput{
Log: k.log,
})
result = extractor.Extract(typedItem.ingress)
case ResourceTypeCRD:
if typedItem.crd == nil {
k.log.App.Warn().Str("res", res.pretty()).Msg("CRD is nil, skipping")
return
}
extractor := NewKubernetesCRDExtractor(KubernetesCRDInput{
Log: k.log,
Client: k.typedClient,
})
result = extractor.Extract(typedItem.crd)
} }
paths, found, err := unstructured.NestedSlice(http, "paths")
if event == watch.Deleted { if err != nil {
if result.Meta != nil { return nil, fmt.Errorf("reading http.paths: %w", err)
k.removeResource(*result.Meta)
}
return
} }
if !found {
if result.Apps == nil { return nil, nil
k.log.App.Warn().Str("res", res.pretty()).Msg("Failed to extract resource, skipping")
if result.Meta != nil {
k.removeResource(*result.Meta)
}
return
} }
var result []string
k.addResource(result) for _, p := range paths {
path, ok := p.(map[string]any)
if !ok {
continue
}
if p, ok := path["path"].(string); ok && p != "" {
result = append(result, p)
}
}
return result, nil
} }
func (k *KubernetesService) resyncGVR(res watchedResource, ctx context.Context) error { func (k *KubernetesService) extractHosts(item *unstructured.Unstructured) ([]string, error) {
rules, found, err := unstructured.NestedSlice(item.Object, "spec", "rules")
if err != nil {
return nil, fmt.Errorf("reading spec.rules: %w", err)
}
if !found {
return nil, nil
}
var hosts []string
for _, r := range rules {
rule, ok := r.(map[string]any)
if !ok {
continue
}
if host, ok := rule["host"].(string); ok && host != "" {
hosts = append(hosts, host)
}
paths, err := k.extractPaths(rule)
if err != nil {
// This is purely to warn users
// It doesn't affect our ability to extract hosts, so we won't fail the whole operation
k.log.App.Warn().Err(err).Str("namespace", item.GetNamespace()).Str("name", item.GetName()).Msg("Failed to extract paths from ingress rule")
continue
}
if len(paths) == 0 {
continue
}
if !slices.Contains(paths, "/") {
k.log.App.Warn().Str("namespace", item.GetNamespace()).Str("name", item.GetName()).Strs("paths", paths).Msg("Ingress rule does not contain a catch-all path, another ingress may be able to bypass auth checks if it routes the same host with a different path. Consider adding a catch-all path to this rule to ensure auth checks are applied to all paths for this host.")
}
}
k.log.App.Trace().Strs("hosts", hosts).Msg("Extracted hosts from ingress rules")
return hosts, nil
}
func (k *KubernetesService) updateFromItem(item *unstructured.Unstructured) {
key := ingressKey{
namespace: item.GetNamespace(),
name: item.GetName(),
}
annotations := item.GetAnnotations()
if annotations == nil {
k.removeIngress(key)
return
}
hosts, err := k.extractHosts(item)
if err != nil {
k.removeIngress(key)
return
}
if len(hosts) == 0 {
k.log.App.Warn().Str("namespace", key.namespace).Str("name", key.name).Msg("No hosts found in ingress, skipping")
k.removeIngress(key)
return
}
labels, err := decoders.DecodeLabels[model.Apps](annotations, "apps")
if err != nil {
k.log.App.Warn().Err(err).Str("namespace", key.namespace).Str("name", key.name).Msg("Failed to decode ingress labels, skipping")
k.removeIngress(key)
return
}
var entries []ingressEntry
v := validators.NewDomainValidator(validators.DomainValidatorOptions{})
for name, config := range labels.Apps {
if config.Config.Domain != "" {
hostname, err := v.SafeHostname(config.Config.Domain)
if err != nil {
k.log.App.Warn().Err(err).Str("namespace", key.namespace).Str("name", key.name).Str("domain", config.Config.Domain).Msg("Domain is invalid, matching will rely on app name")
} else if slices.Contains(hosts, hostname) {
entries = append(entries, ingressEntry{
name: name,
app: config,
})
continue
}
}
for _, host := range hosts {
if strings.HasPrefix(strings.ToLower(host), strings.ToLower(name+".")) {
entries = append(entries, ingressEntry{
name: name,
app: config,
})
break
}
}
}
if len(entries) == 0 {
k.removeIngress(key)
return
}
k.addIngressEntries(key, entries)
}
func (k *KubernetesService) resyncGVR(gvr schema.GroupVersionResource, ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 30*time.Second) ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel() defer cancel()
list, err := k.client.Resource(res.gvr).List(ctx, metav1.ListOptions{}) list, err := k.client.Resource(gvr).List(ctx, metav1.ListOptions{})
if err != nil { if err != nil {
k.log.App.Warn().Err(err).Str("res", res.pretty()).Msg("Failed to list resources for resync") k.log.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Failed to list resources for resync")
return err return err
} }
for _, item := range list.Items { for i := range list.Items {
newTypedItem, err := new(typedItem).fromUnstructured(res.typ, &item) k.updateFromItem(&list.Items[i])
if err != nil {
k.log.App.Warn().Err(err).Str("res", res.pretty()).Msg("Failed to decode resource, skipping")
continue
}
k.watchedItemChange(res, newTypedItem, watch.Modified)
} }
k.log.App.Debug().Str("res", res.pretty()).Int("count", len(list.Items)).Msg("Resync complete") k.log.App.Debug().Str("api", gvr.GroupVersion().String()).Int("count", len(list.Items)).Msg("Resync complete")
return nil return nil
} }
func (k *KubernetesService) runWatcher(res watchedResource, w watch.Interface, resyncTicker *time.Ticker, ctx context.Context) bool { // runWatcher drains events from an active watcher until it closes or the context is done.
// Returns true if the caller should restart the watcher, false if it should exit.
func (k *KubernetesService) runWatcher(gvr schema.GroupVersionResource, w watch.Interface, resyncTicker *time.Ticker, ctx context.Context) bool {
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
@@ -286,62 +277,62 @@ func (k *KubernetesService) runWatcher(res watchedResource, w watch.Interface, r
return false return false
case event, ok := <-w.ResultChan(): case event, ok := <-w.ResultChan():
if !ok { if !ok {
k.log.App.Warn().Str("res", res.pretty()).Msg("Watcher channel closed, restarting watcher") k.log.App.Warn().Str("api", gvr.GroupVersion().String()).Msg("Watcher channel closed, restarting watcher")
w.Stop() w.Stop()
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
return true return true
} }
item, ok := event.Object.(*unstructured.Unstructured) item, ok := event.Object.(*unstructured.Unstructured)
if !ok { if !ok {
k.log.App.Warn().Str("res", res.pretty()).Msg("Received unexpected event object, skipping") k.log.App.Warn().Str("api", gvr.GroupVersion().String()).Msg("Received unexpected event object, skipping")
continue
}
newTypedItem, err := new(typedItem).fromUnstructured(res.typ, item)
if err != nil {
k.log.App.Warn().Err(err).Str("res", res.pretty()).Msg("Failed to decode resource, skipping")
continue continue
} }
switch event.Type { switch event.Type {
case watch.Added, watch.Modified, watch.Deleted: case watch.Added, watch.Modified:
k.watchedItemChange(res, newTypedItem, event.Type) k.updateFromItem(item)
case watch.Deleted:
k.removeIngress(ingressKey{
namespace: item.GetNamespace(),
name: item.GetName(),
})
} }
case <-resyncTicker.C: case <-resyncTicker.C:
if err := k.resyncGVR(res, ctx); err != nil { if err := k.resyncGVR(gvr, ctx); err != nil {
k.log.App.Warn().Err(err).Str("res", res.pretty()).Msg("Periodic resync failed during watcher run") k.log.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Periodic resync failed during watcher run")
} }
} }
} }
} }
func (k *KubernetesService) watchGVR(res watchedResource, ctx context.Context) { func (k *KubernetesService) watchGVR(gvr schema.GroupVersionResource, ctx context.Context) {
resyncTicker := time.NewTicker(5 * time.Minute) resyncTicker := time.NewTicker(5 * time.Minute)
defer resyncTicker.Stop() defer resyncTicker.Stop()
if err := k.resyncGVR(res, ctx); err != nil { if err := k.resyncGVR(gvr, ctx); err != nil {
k.log.App.Warn().Err(err).Str("res", res.pretty()).Msg("Initial resync failed, will retry") k.log.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Initial resync failed, will retry")
time.Sleep(30 * time.Second) time.Sleep(30 * time.Second)
} }
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
k.log.App.Debug().Str("res", res.pretty()).Msg("Shutting down kubernetes watcher") k.log.App.Debug().Str("api", gvr.GroupVersion().String()).Msg("Shutting down kubernetes watcher")
return return
case <-resyncTicker.C: case <-resyncTicker.C:
if err := k.resyncGVR(res, ctx); err != nil { if err := k.resyncGVR(gvr, ctx); err != nil {
k.log.App.Warn().Err(err).Str("res", res.pretty()).Msg("Periodic resync failed, will retry") k.log.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Periodic resync failed, will retry")
} }
default: default:
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
watcher, err := k.client.Resource(res.gvr).Watch(ctx, metav1.ListOptions{}) watcher, err := k.client.Resource(gvr).Watch(ctx, metav1.ListOptions{})
if err != nil { if err != nil {
k.log.App.Warn().Err(err).Str("res", res.pretty()).Msg("Failed to start watcher, will retry") k.log.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Failed to start watcher, will retry")
cancel() cancel()
time.Sleep(10 * time.Second) time.Sleep(10 * time.Second)
continue continue
} }
k.log.App.Debug().Str("res", res.pretty()).Msg("Watcher started successfully") k.log.App.Debug().Str("api", gvr.GroupVersion().String()).Msg("Watcher started successfully")
if !k.runWatcher(res, watcher, resyncTicker, ctx) { if !k.runWatcher(gvr, watcher, resyncTicker, ctx) {
cancel() cancel()
return return
} }
+651 -215
View File
@@ -1,243 +1,679 @@
//go:build ignore
package service package service
import ( import (
"strings" "strings"
"testing" "testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/logger" "github.com/tinyauthapp/tinyauth/internal/utils/logger"
networking "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
) )
func watchedResourceForTest(t *testing.T, typ ResourceType) watchedResource { func TestKubernetesService(t *testing.T) {
t.Helper()
for _, resource := range supportedResources {
if resource.typ == typ {
return resource
}
}
t.Fatalf("unsupported resource type %q", typ)
return watchedResource{}
}
func newKubernetesServiceForTest(log *logger.Logger) *KubernetesService {
service := &KubernetesService{
apps: make(map[resourceKey]routedApps),
log: log,
}
service.extractors.ingress = NewKubernetesIngressExtractor(KubernetesIngressExtractorInput{Log: log})
return service
}
func testIngress(name string, annotations map[string]string, hosts ...string) *typedItem {
rules := make([]networking.IngressRule, 0, len(hosts))
for _, host := range hosts {
rules = append(rules, networking.IngressRule{Host: host})
}
return &typedItem{
typ: ResourceTypeIngress,
ingress: &networking.Ingress{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "default", Annotations: annotations},
Spec: networking.IngressSpec{Rules: rules},
},
}
}
func lookupApp(service *KubernetesService, domain string) *model.App {
var app *model.App
service.getEntry(domain, func(name string, candidate *model.App) bool {
if candidate.Config.Domain == domain || strings.HasPrefix(domain, name+".") {
app = candidate
return true
}
return false
})
return app
}
func TestKubernetesServiceUpdateFromItem(t *testing.T) {
log := logger.NewLogger().WithTestConfig() log := logger.NewLogger().WithTestConfig()
log.Init() log.Init()
tests := []struct { type testCase struct {
name string description string
resource ResourceType run func(t *testing.T, svc *KubernetesService)
item *typedItem }
domain string
wantConfigDomain string tests := []testCase{
allow string
}{
{ {
name: "Ingress matches a configured domain", description: "Cache by domain returns app and misses unknown domain",
resource: ResourceTypeIngress, run: func(t *testing.T, svc *KubernetesService) {
item: testIngress("ingress", map[string]string{ app := model.App{Config: model.AppConfig{Domain: "foo.example.com"}}
"tinyauth.apps.dashboard.config.domain": "dashboard.example.com", svc.addIngressEntries(ingressKey{
"tinyauth.apps.dashboard.users.allow": "alice", namespace: "default",
}, "dashboard.example.com"), name: "my-ingress",
domain: "dashboard.example.com", wantConfigDomain: "dashboard.example.com", allow: "alice", }, []ingressEntry{
{
app: app,
name: "foo",
},
})
var got *model.App
svc.getEntry(func(name string, app *model.App) bool {
if app.Config.Domain == "foo.example.com" {
got = app
return true
}
return false
})
require.NotNil(t, got)
assert.Equal(t, "foo.example.com", got.Config.Domain)
},
}, },
{ {
name: "Ingress matches an app name case insensitively", description: "RemoveIngress clears domain and app name entries",
resource: ResourceTypeIngress, run: func(t *testing.T, svc *KubernetesService) {
item: testIngress("ingress", map[string]string{ app := model.App{Config: model.AppConfig{Domain: "foo.example.com"}}
"tinyauth.apps.dashboard.users.allow": "alice", svc.addIngressEntries(ingressKey{
}, "Dashboard.example.com"), namespace: "default",
domain: "dashboard.example.com", allow: "alice", name: "my-ingress",
}, []ingressEntry{
{
app: app,
name: "foo",
},
})
var got *model.App
svc.getEntry(func(name string, app *model.App) bool {
if app.Config.Domain == "foo.example.com" {
got = app
return true
}
return false
})
require.NotNil(t, got)
assert.Equal(t, "foo.example.com", got.Config.Domain)
got = nil
svc.removeIngress(ingressKey{
namespace: "default",
name: "my-ingress",
})
svc.getEntry(func(name string, app *model.App) bool {
if app.Config.Domain == "foo.example.com" {
got = app
return true
}
return false
})
assert.Nil(t, got)
},
}, },
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
service := newKubernetesServiceForTest(log)
service.updateFromItem(watchedResourceForTest(t, test.resource), test.item)
app := lookupApp(service, test.domain)
require.NotNil(t, app)
assert.Equal(t, test.allow, app.Users.Allow)
assert.Equal(t, test.wantConfigDomain, app.Config.Domain)
})
}
}
func TestKubernetesServiceUpdateFromItemRemovesStaleEntries(t *testing.T) {
log := logger.NewLogger().WithTestConfig()
log.Init()
tests := []struct {
name string
resource ResourceType
item *typedItem
}{
{"Ingress without annotations", ResourceTypeIngress, testIngress("route", nil, "app.example.com")},
{"Ingress without hosts", ResourceTypeIngress, testIngress("route", map[string]string{"tinyauth.apps.app.users.allow": "alice"})},
{"Ingress with invalid annotations", ResourceTypeIngress, testIngress("route", map[string]string{"tinyauth.apps.app.users.break": "invalid"}, "app.example.com")},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
service := newKubernetesServiceForTest(log)
key := resourceKey{typ: test.resource, namespace: "default", name: "route"}
service.addResourceEntries(key, []string{"app.example.com"}, []resourceEntry{{
name: "app",
app: model.App{Config: model.AppConfig{Domain: "app.example.com"}},
}})
service.updateFromItem(watchedResourceForTest(t, test.resource), test.item)
assert.Nil(t, lookupApp(service, "app.example.com"))
})
}
}
func TestTypedItemFromUnstructured(t *testing.T) {
tests := []struct {
name string
resource ResourceType
item unstructured.Unstructured
assert func(t *testing.T, item *typedItem)
}{
{ {
name: "Ingress", description: "AddIngressApps replaces stale entries for the same ingress",
resource: ResourceTypeIngress, run: func(t *testing.T, svc *KubernetesService) {
item: unstructured.Unstructured{Object: map[string]any{ old := model.App{Config: model.AppConfig{Domain: "old.example.com"}}
"metadata": map[string]any{"name": "ingress", "namespace": "default"}, svc.addIngressEntries(ingressKey{
"spec": map[string]any{"rules": []any{map[string]any{"host": "app.example.com"}}}, namespace: "default",
}}, name: "my-ingress",
assert: func(t *testing.T, item *typedItem) { }, []ingressEntry{
require.NotNil(t, item.ingress) {
assert.Equal(t, "app.example.com", item.ingress.Spec.Rules[0].Host) app: old,
name: "foo",
},
})
updated := model.App{Config: model.AppConfig{Domain: "new.example.com"}}
svc.addIngressEntries(ingressKey{
namespace: "default",
name: "my-ingress",
}, []ingressEntry{
{
app: updated,
name: "foo",
},
})
var got *model.App
svc.getEntry(func(name string, app *model.App) bool {
if app.Config.Domain == "old.example.com" {
got = app
return true
}
return false
})
assert.Nil(t, got)
svc.getEntry(func(name string, app *model.App) bool {
if app.Config.Domain == "new.example.com" {
got = app
return true
}
return false
})
require.NotNil(t, got)
assert.Equal(t, "new.example.com", got.Config.Domain)
},
},
{
description: "GetLabels returns app from cache when connected",
run: func(t *testing.T, svc *KubernetesService) {
svc.connected = true
app := model.App{Config: model.AppConfig{Domain: "hit.example.com"}}
svc.addIngressEntries(ingressKey{
namespace: "default",
name: "my-ingress",
}, []ingressEntry{
{
app: app,
name: "foo",
},
})
var got *model.App
err := svc.Lookup(func(name string, app *model.App) bool {
if app.Config.Domain == "hit.example.com" {
got = app
return true
}
return false
})
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, "hit.example.com", got.Config.Domain)
},
},
{
description: "GetLabels returns empty app on cache miss when started",
run: func(t *testing.T, svc *KubernetesService) {
svc.connected = true
var got *model.App
err := svc.Lookup(func(name string, app *model.App) bool {
if app.Config.Domain == "notfound.example.com" {
got = app
return true
}
return false
})
require.NoError(t, err)
require.Nil(t, got)
},
},
{
description: "GetLabels resolves app by app name",
run: func(t *testing.T, svc *KubernetesService) {
svc.connected = true
app := model.App{Path: model.AppPath{Allow: "/foo"}}
svc.addIngressEntries(ingressKey{
namespace: "default",
name: "my-ingress",
}, []ingressEntry{
{
app: app,
name: "foo",
},
})
var got *model.App
err := svc.Lookup(func(name string, app *model.App) bool {
if strings.HasPrefix("foo.internal.example.com", "foo.") {
got = app
return true
}
return false
})
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, "/foo", got.Path.Allow)
},
},
{
description: "GetLabels returns empty app when service not yet started",
run: func(t *testing.T, svc *KubernetesService) {
var got *model.App
err := svc.Lookup(func(name string, app *model.App) bool {
return false
})
require.NoError(t, err)
assert.Nil(t, got)
},
},
{
description: "UpdateFromItem parses annotations and populates cache",
run: func(t *testing.T, svc *KubernetesService) {
item := unstructured.Unstructured{}
item.SetNamespace("default")
item.SetName("test-ingress")
item.SetAnnotations(map[string]string{
"tinyauth.apps.myapp.config.domain": "myapp.example.com",
"tinyauth.apps.myapp.users.allow": "alice",
})
item.Object["spec"] = map[string]any{
"rules": []any{
map[string]any{
"host": "myapp.example.com",
},
},
}
svc.updateFromItem(&item)
var got *model.App
svc.getEntry(func(name string, app *model.App) bool {
if app.Config.Domain == "myapp.example.com" {
got = app
return true
}
return false
})
require.NotNil(t, got)
assert.Equal(t, "myapp.example.com", got.Config.Domain)
assert.Equal(t, "alice", got.Users.Allow)
},
},
{
description: "Update from item skips annotations with no hosts",
run: func(t *testing.T, svc *KubernetesService) {
item := unstructured.Unstructured{}
item.SetNamespace("default")
item.SetName("test-ingress")
item.SetAnnotations(map[string]string{
"tinyauth.apps.myapp.config.domain": "myapp.example.com",
})
svc.updateFromItem(&item)
var got *model.App
svc.getEntry(func(name string, app *model.App) bool {
if app.Config.Domain == "myapp.example.com" {
got = app
return true
}
return false
})
assert.Nil(t, got)
},
},
{
description: "UpdateFromItem fails when label parsing fails",
run: func(t *testing.T, svc *KubernetesService) {
item := unstructured.Unstructured{}
item.SetNamespace("default")
item.SetName("test-ingress")
item.SetAnnotations(map[string]string{
"tinyauth.apps.myapp.config.domain": "myapp.example.com",
"tinyauth.apps.myapp.users.break": "i-dont-exist",
})
item.Object["spec"] = map[string]any{
"rules": []any{
map[string]any{
"host": "myapp.example.com",
},
},
}
svc.updateFromItem(&item)
var got *model.App
svc.getEntry(func(name string, app *model.App) bool {
if app.Config.Domain == "myapp.example.com" {
got = app
return true
}
return false
})
require.Nil(t, got)
},
},
{
description: "UpdateFromItem with no annotations removes existing cache entries",
run: func(t *testing.T, svc *KubernetesService) {
app := model.App{Config: model.AppConfig{Domain: "todelete.example.com"}}
svc.addIngressEntries(ingressKey{
namespace: "default",
name: "my-ingress",
}, []ingressEntry{
{
app: app,
name: "foo",
},
})
item := unstructured.Unstructured{}
item.SetNamespace("default")
item.SetName("my-ingress")
svc.updateFromItem(&item)
var got *model.App
svc.getEntry(func(name string, app *model.App) bool {
if app.Config.Domain == "todelete.example.com" {
got = app
return true
}
return false
})
assert.Nil(t, got)
},
},
{
description: "ExtractPaths returns all non empty paths from a rule",
run: func(t *testing.T, svc *KubernetesService) {
rule := map[string]any{
"http": map[string]any{
"paths": []any{
map[string]any{"path": "/"},
map[string]any{"path": "/api"},
map[string]any{"path": ""},
map[string]any{"pathType": "Prefix"},
"not-a-map",
},
},
}
paths, err := svc.extractPaths(rule)
require.NoError(t, err)
assert.Equal(t, []string{"/", "/api"}, paths)
},
},
{
description: "ExtractPaths returns nothing when http or paths are missing",
run: func(t *testing.T, svc *KubernetesService) {
paths, err := svc.extractPaths(map[string]any{})
require.NoError(t, err)
assert.Empty(t, paths)
paths, err = svc.extractPaths(map[string]any{
"http": map[string]any{},
})
require.NoError(t, err)
assert.Empty(t, paths)
},
},
{
description: "ExtractPaths errors when http is not a map",
run: func(t *testing.T, svc *KubernetesService) {
paths, err := svc.extractPaths(map[string]any{
"http": "invalid",
})
require.Error(t, err)
assert.Nil(t, paths)
},
},
{
description: "ExtractPaths errors when paths is not a slice",
run: func(t *testing.T, svc *KubernetesService) {
paths, err := svc.extractPaths(map[string]any{
"http": map[string]any{
"paths": "invalid",
},
})
require.Error(t, err)
assert.Nil(t, paths)
},
},
{
description: "ExtractHosts returns hosts from all rules",
run: func(t *testing.T, svc *KubernetesService) {
item := unstructured.Unstructured{}
item.SetNamespace("default")
item.SetName("test-ingress")
require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{
map[string]any{
"host": "foo.example.com",
"http": map[string]any{
"paths": []any{
map[string]any{"path": "/"},
},
},
},
map[string]any{
"host": "bar.example.com",
},
map[string]any{
"host": "",
},
"not-a-map",
}, "spec", "rules"))
hosts, err := svc.extractHosts(&item)
require.NoError(t, err)
assert.Equal(t, []string{"foo.example.com", "bar.example.com"}, hosts)
},
},
{
description: "ExtractHosts still returns hosts when a rule has no catch all path",
run: func(t *testing.T, svc *KubernetesService) {
item := unstructured.Unstructured{}
item.SetNamespace("default")
item.SetName("test-ingress")
require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{
map[string]any{
"host": "foo.example.com",
"http": map[string]any{
"paths": []any{
map[string]any{"path": "/api"},
},
},
},
}, "spec", "rules"))
hosts, err := svc.extractHosts(&item)
require.NoError(t, err)
assert.Equal(t, []string{"foo.example.com"}, hosts)
},
},
{
description: "ExtractHosts still returns hosts when path extraction fails",
run: func(t *testing.T, svc *KubernetesService) {
item := unstructured.Unstructured{}
item.SetNamespace("default")
item.SetName("test-ingress")
require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{
map[string]any{
"host": "foo.example.com",
"http": "invalid",
},
}, "spec", "rules"))
hosts, err := svc.extractHosts(&item)
require.NoError(t, err)
assert.Equal(t, []string{"foo.example.com"}, hosts)
},
},
{
description: "ExtractHosts returns nothing when spec.rules is missing",
run: func(t *testing.T, svc *KubernetesService) {
item := unstructured.Unstructured{}
item.SetNamespace("default")
item.SetName("test-ingress")
hosts, err := svc.extractHosts(&item)
require.NoError(t, err)
assert.Empty(t, hosts)
},
},
{
description: "ExtractHosts errors when spec.rules is not a slice",
run: func(t *testing.T, svc *KubernetesService) {
item := unstructured.Unstructured{}
item.SetNamespace("default")
item.SetName("test-ingress")
require.NoError(t, unstructured.SetNestedField(item.Object, "invalid", "spec", "rules"))
hosts, err := svc.extractHosts(&item)
require.Error(t, err)
assert.Nil(t, hosts)
},
},
{
description: "UpdateFromItem registers app when its domain matches an ingress host",
run: func(t *testing.T, svc *KubernetesService) {
item := unstructured.Unstructured{}
item.SetNamespace("default")
item.SetName("test-ingress")
item.SetAnnotations(map[string]string{
"tinyauth.apps.myapp.config.domain": "myapp.example.com",
})
require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{
map[string]any{
"host": "myapp.example.com",
},
}, "spec", "rules"))
svc.updateFromItem(&item)
var got *model.App
svc.getEntry(func(name string, app *model.App) bool {
if name == "myapp" {
got = app
return true
}
return false
})
require.NotNil(t, got)
assert.Equal(t, "myapp.example.com", got.Config.Domain)
},
},
{
description: "UpdateFromItem registers app when its name matches an ingress host prefix",
run: func(t *testing.T, svc *KubernetesService) {
item := unstructured.Unstructured{}
item.SetNamespace("default")
item.SetName("test-ingress")
item.SetAnnotations(map[string]string{
"tinyauth.apps.myapp.users.allow": "alice",
})
require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{
map[string]any{
"host": "MyApp.example.com",
},
}, "spec", "rules"))
svc.updateFromItem(&item)
var got *model.App
svc.getEntry(func(name string, app *model.App) bool {
if name == "myapp" {
got = app
return true
}
return false
})
require.NotNil(t, got)
assert.Equal(t, "alice", got.Users.Allow)
},
},
{
description: "UpdateFromItem skips apps that match neither host nor name",
run: func(t *testing.T, svc *KubernetesService) {
item := unstructured.Unstructured{}
item.SetNamespace("default")
item.SetName("test-ingress")
item.SetAnnotations(map[string]string{
"tinyauth.apps.myapp.config.domain": "myapp.example.com",
})
require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{
map[string]any{
"host": "other.example.com",
},
}, "spec", "rules"))
svc.updateFromItem(&item)
var got *model.App
svc.getEntry(func(name string, app *model.App) bool {
got = app
return true
})
assert.Nil(t, got)
},
},
{
description: "UpdateFromItem falls back to app name when the domain is invalid",
run: func(t *testing.T, svc *KubernetesService) {
item := unstructured.Unstructured{}
item.SetNamespace("default")
item.SetName("test-ingress")
item.SetAnnotations(map[string]string{
"tinyauth.apps.myapp.config.domain": "not a domain",
})
require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{
map[string]any{
"host": "myapp.example.com",
},
}, "spec", "rules"))
svc.updateFromItem(&item)
var got *model.App
svc.getEntry(func(name string, app *model.App) bool {
if name == "myapp" {
got = app
return true
}
return false
})
require.NotNil(t, got)
},
},
{
description: "UpdateFromItem removes entries when host extraction fails",
run: func(t *testing.T, svc *KubernetesService) {
key := ingressKey{
namespace: "default",
name: "test-ingress",
}
svc.addIngressEntries(key, []ingressEntry{
{
app: model.App{Config: model.AppConfig{Domain: "stale.example.com"}},
name: "foo",
},
})
item := unstructured.Unstructured{}
item.SetNamespace(key.namespace)
item.SetName(key.name)
item.SetAnnotations(map[string]string{
"tinyauth.apps.myapp.config.domain": "myapp.example.com",
})
require.NoError(t, unstructured.SetNestedField(item.Object, "invalid", "spec", "rules"))
svc.updateFromItem(&item)
var got *model.App
svc.getEntry(func(name string, app *model.App) bool {
got = app
return true
})
assert.Nil(t, got)
},
},
{
description: "UpdateFromItem removes entries when annotations are not decodable",
run: func(t *testing.T, svc *KubernetesService) {
key := ingressKey{
namespace: "default",
name: "test-ingress",
}
svc.addIngressEntries(key, []ingressEntry{
{
app: model.App{Config: model.AppConfig{Domain: "stale.example.com"}},
name: "foo",
},
})
item := unstructured.Unstructured{}
item.SetNamespace(key.namespace)
item.SetName(key.name)
item.SetAnnotations(map[string]string{
"tinyauth.apps.myapp.config.oauthWhitelist": "[",
})
svc.updateFromItem(&item)
var got *model.App
svc.getEntry(func(name string, app *model.App) bool {
got = app
return true
})
assert.Nil(t, got)
}, },
}, },
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.description, func(t *testing.T) {
item, err := new(typedItem).fromUnstructured(test.resource, &test.item) svc := &KubernetesService{
require.NoError(t, err) ingressEntries: make(map[ingressKey][]ingressEntry),
assert.Equal(t, test.resource, item.typ) log: log,
test.assert(t, item) }
}) test.run(t, svc)
}
}
func TestKubernetesServiceLookup(t *testing.T) {
log := logger.NewLogger().WithTestConfig()
log.Init()
tests := []struct {
name string
connected bool
domain string
wantApp bool
}{
{"Returns a matching app when connected", true, "app.example.com", true},
{"Skips the cache before the service is connected", false, "app.example.com", false},
{"Skips an invalid domain", true, "app.example.com\xC3\xA9", false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
service := newKubernetesServiceForTest(log)
service.connected = test.connected
service.addResourceEntries(resourceKey{typ: ResourceTypeIngress, namespace: "default", name: "route"}, []string{"app.example.com"}, []resourceEntry{{
name: "app",
app: model.App{Config: model.AppConfig{Domain: "app.example.com"}},
}})
var app *model.App
err := service.Lookup(test.domain, func(_ string, candidate *model.App) bool {
app = candidate
return true
})
require.NoError(t, err)
assert.Equal(t, test.wantApp, app != nil)
})
}
}
func TestKubernetesServiceKeepsResourceTypesSeparate(t *testing.T) {
log := logger.NewLogger().WithTestConfig()
log.Init()
service := newKubernetesServiceForTest(log)
resources := []struct {
resource ResourceType
item *typedItem
domain string
}{
{ResourceTypeIngress, testIngress("shared", map[string]string{"tinyauth.apps.ingress.config.domain": "ingress.example.com"}, "ingress.example.com"), "ingress.example.com"},
}
for _, resource := range resources {
service.updateFromItem(watchedResourceForTest(t, resource.resource), resource.item)
}
for _, resource := range resources {
assert.NotNil(t, lookupApp(service, resource.domain))
}
}
func TestKubernetesHostMatching(t *testing.T) {
tests := []struct {
name string
host string
domain string
want bool
}{
{"Exact host", "app.example.com", "app.example.com", true},
{"Case insensitive exact host", "App.Example.com", "app.example.com", true},
{"Wildcard host", "*.example.com", "deep.app.example.com", true},
{"Wildcard does not match its apex", "*.example.com", "example.com", false},
{"Different host", "app.example.com", "other.example.com", false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.want, hostMatchesHostname(test.host, test.domain))
}) })
} }
} }
-98
View File
@@ -1,98 +0,0 @@
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
//go:generate controller-gen object paths=$GOFILE
//go:generate controller-gen rbac:roleName=tinyauth crd paths=./... output:crd:dir=./crds output:stdout
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// Application is a set of access control rules that can be applied to a
// specific domain. It is an alternative to environment variable or config-based
// access controls.
type Application struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ApplicationSpec `json:"spec,omitempty"`
}
// +k8s:deepcopy-gen=true
// ApplicationSpec describes the application to which this rule applies to
type ApplicationSpec struct {
Config AppConfig `json:"config,omitempty"`
Users AppUsers `json:"users,omitempty"`
OAuth AppOAuth `json:"oauth,omitempty"`
IP AppIP `json:"ip,omitempty"`
Response AppResponse `json:"response,omitempty"`
Path AppPath `json:"path,omitempty"`
LDAP AppLDAP `json:"ldap,omitempty"`
}
// +k8s:deepcopy-gen=true
// AppConfig specifies configuration for the application
type AppConfig struct {
// +required
Domain string `json:"domain,omitempty"`
}
// +k8s:deepcopy-gen=true
// AppUsers specifies user access control rules
type AppUsers struct {
Allow string `json:"allow,omitempty"`
Block string `json:"block,omitempty"`
}
// +k8s:deepcopy-gen=true
// AppOAuth specifies OAuth access control rules
type AppOAuth struct {
Whitelist string `json:"whitelist,omitempty"`
Groups string `json:"groups,omitempty"`
}
// +k8s:deepcopy-gen=true
// AppLDAP specifies LDAP access control rules
type AppLDAP struct {
Groups string `json:"groups,omitempty"`
}
// +k8s:deepcopy-gen=true
// AppIP specifies IP access control rules
type AppIP struct {
Allow []string `json:"allow,omitempty"`
Block []string `json:"block,omitempty"`
Bypass []string `json:"bypass,omitempty"`
}
// +k8s:deepcopy-gen=true
// AppResponse specifies response headers and basic auth credentials
type AppResponse struct {
Headers []string `json:"headers,omitempty"`
BasicAuth AppBasicAuth `json:"basicAuth,omitempty"`
}
// +k8s:deepcopy-gen=true
// AppBasicAuth specifies basic auth credentials
type AppBasicAuth struct {
Username string `json:"username,omitempty"`
PasswordSecretRef *corev1.SecretKeySelector `json:"passwordSecretRef,omitempty"`
}
// +k8s:deepcopy-gen=true
// AppPath specifies path-based access control rules
type AppPath struct {
Allow string `json:"allow,omitempty"`
Block string `json:"block,omitempty"`
}
@@ -1,142 +0,0 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.22.0
name: applications.tinyauth.app
spec:
group: tinyauth.app
names:
kind: Application
listKind: ApplicationList
plural: applications
singular: application
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: |-
Application is a set of access control rules that can be applied to a
specific domain. It is an alternative to environment variable or config-based
access controls.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: ApplicationSpec describes the application to which this rule
applies to
properties:
config:
description: AppConfig specifies configuration for the application
properties:
domain:
type: string
required:
- domain
type: object
ip:
description: AppIP specifies IP access control rules
properties:
allow:
items:
type: string
type: array
block:
items:
type: string
type: array
bypass:
items:
type: string
type: array
type: object
ldap:
description: AppLDAP specifies LDAP access control rules
properties:
groups:
type: string
type: object
oauth:
description: AppOAuth specifies OAuth access control rules
properties:
groups:
type: string
whitelist:
type: string
type: object
path:
description: AppPath specifies path-based access control rules
properties:
allow:
type: string
block:
type: string
type: object
response:
description: AppResponse specifies response headers and basic auth
credentials
properties:
basicAuth:
description: AppBasicAuth specifies basic auth credentials
properties:
passwordSecretRef:
description: SecretKeySelector selects a key of a Secret.
properties:
key:
description: The key of the secret to select from. Must
be a valid secret key.
type: string
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
optional:
description: Specify whether the Secret or its key must
be defined
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
username:
type: string
type: object
headers:
items:
type: string
type: array
type: object
users:
description: AppUsers specifies user access control rules
properties:
allow:
type: string
block:
type: string
type: object
type: object
type: object
served: true
storage: true
-3
View File
@@ -1,3 +0,0 @@
package v1alpha1
//+groupName=tinyauth.app
-40
View File
@@ -1,40 +0,0 @@
package v1alpha1
import (
"github.com/tinyauthapp/tinyauth/internal/model"
)
// ToInternalApp converts the ApplicationSpec to the internal App structure
func (s *ApplicationSpec) ToInternalApp() model.App {
return model.App{
Config: model.AppConfig{
Domain: s.Config.Domain,
},
Users: model.AppUsers{
Allow: s.Users.Allow,
Block: s.Users.Block,
},
OAuth: model.AppOAuth{
Whitelist: s.OAuth.Whitelist,
Groups: s.OAuth.Groups,
},
IP: model.AppIP{
Allow: s.IP.Allow,
Block: s.IP.Block,
Bypass: s.IP.Bypass,
},
Response: model.AppResponse{
Headers: s.Response.Headers,
BasicAuth: model.AppBasicAuth{
Username: s.Response.BasicAuth.Username,
},
},
Path: model.AppPath{
Allow: s.Path.Allow,
Block: s.Path.Block,
},
LDAP: model.AppLDAP{
Groups: s.LDAP.Groups,
},
}
}
-26
View File
@@ -1,26 +0,0 @@
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
const GroupName = "tinyauth.app"
const GroupVersion = "v1alpha1"
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: GroupVersion}
var (
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
AddToScheme = SchemeBuilder.AddToScheme
)
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Application{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}
@@ -1,204 +0,0 @@
//go:build !ignore_autogenerated
// Code generated by controller-gen. DO NOT EDIT.
package v1alpha1
import (
"k8s.io/api/core/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AppBasicAuth) DeepCopyInto(out *AppBasicAuth) {
*out = *in
if in.PasswordSecretRef != nil {
in, out := &in.PasswordSecretRef, &out.PasswordSecretRef
*out = new(v1.SecretKeySelector)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppBasicAuth.
func (in *AppBasicAuth) DeepCopy() *AppBasicAuth {
if in == nil {
return nil
}
out := new(AppBasicAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AppConfig) DeepCopyInto(out *AppConfig) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppConfig.
func (in *AppConfig) DeepCopy() *AppConfig {
if in == nil {
return nil
}
out := new(AppConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AppIP) DeepCopyInto(out *AppIP) {
*out = *in
if in.Allow != nil {
in, out := &in.Allow, &out.Allow
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Block != nil {
in, out := &in.Block, &out.Block
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Bypass != nil {
in, out := &in.Bypass, &out.Bypass
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppIP.
func (in *AppIP) DeepCopy() *AppIP {
if in == nil {
return nil
}
out := new(AppIP)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AppLDAP) DeepCopyInto(out *AppLDAP) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppLDAP.
func (in *AppLDAP) DeepCopy() *AppLDAP {
if in == nil {
return nil
}
out := new(AppLDAP)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AppOAuth) DeepCopyInto(out *AppOAuth) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppOAuth.
func (in *AppOAuth) DeepCopy() *AppOAuth {
if in == nil {
return nil
}
out := new(AppOAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AppPath) DeepCopyInto(out *AppPath) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppPath.
func (in *AppPath) DeepCopy() *AppPath {
if in == nil {
return nil
}
out := new(AppPath)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AppResponse) DeepCopyInto(out *AppResponse) {
*out = *in
if in.Headers != nil {
in, out := &in.Headers, &out.Headers
*out = make([]string, len(*in))
copy(*out, *in)
}
in.BasicAuth.DeepCopyInto(&out.BasicAuth)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppResponse.
func (in *AppResponse) DeepCopy() *AppResponse {
if in == nil {
return nil
}
out := new(AppResponse)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AppUsers) DeepCopyInto(out *AppUsers) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppUsers.
func (in *AppUsers) DeepCopy() *AppUsers {
if in == nil {
return nil
}
out := new(AppUsers)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Application) DeepCopyInto(out *Application) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Application.
func (in *Application) DeepCopy() *Application {
if in == nil {
return nil
}
out := new(Application)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Application) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationSpec) DeepCopyInto(out *ApplicationSpec) {
*out = *in
out.Config = in.Config
out.Users = in.Users
out.OAuth = in.OAuth
in.IP.DeepCopyInto(&out.IP)
in.Response.DeepCopyInto(&out.Response)
out.Path = in.Path
out.LDAP = in.LDAP
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSpec.
func (in *ApplicationSpec) DeepCopy() *ApplicationSpec {
if in == nil {
return nil
}
out := new(ApplicationSpec)
in.DeepCopyInto(out)
return out
}