mirror of
https://github.com/tinyauthapp/tinyauth.git
synced 2026-09-23 11:13:32 +08:00
The app name fallback matches any domain that starts with the app name, so an app named myapp served on myapp.example.com also defined the ACLs of myapp.evil.com. Behind a proxy with a catch-all route, a request can be authorized against the wrong app that way. Label providers now receive the domain being authorized. The Kubernetes provider keeps the hosts of every Ingress, HTTPRoute and GRPCRoute it watches and withholds the apps of the resources that do not route the domain, which bounds the name fallback to the hosts a resource actually serves. Wildcard hostnames keep matching as a suffix, so nested subdomains stay resolvable by app name. Container labels carry no routing information, so the Docker provider cannot narrow its results down and keeps yielding every app. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
128 lines
3.6 KiB
Go
128 lines
3.6 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
|
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
|
"github.com/tinyauthapp/tinyauth/pkg/validators"
|
|
"go.uber.org/dig"
|
|
)
|
|
|
|
// LabelProvider looks up the apps it knows about for the given domain. A
|
|
// provider that knows which hosts its apps are served on MUST only yield the
|
|
// ones that are actually served on domain, so that an unrelated app cannot
|
|
// claim it by name.
|
|
type LabelProvider interface {
|
|
Lookup(domain string, locator func(name string, app *model.App) bool) error
|
|
}
|
|
|
|
type AccessControlsService struct {
|
|
log *logger.Logger
|
|
config *model.Config
|
|
labelProvider LabelProvider
|
|
}
|
|
|
|
type AccessControlServiceInput struct {
|
|
dig.In
|
|
|
|
Log *logger.Logger
|
|
Config *model.Config
|
|
LabelProvider LabelProvider `optional:"true"`
|
|
}
|
|
|
|
func NewAccessControlsService(i AccessControlServiceInput) *AccessControlsService {
|
|
|
|
return &AccessControlsService{
|
|
log: i.Log,
|
|
config: i.Config,
|
|
labelProvider: i.LabelProvider,
|
|
}
|
|
}
|
|
|
|
func (service *AccessControlsService) getACLs(domain string, lookup func(locator func(name string, app *model.App) bool) error) (*model.App, error) {
|
|
v := validators.NewDomainValidator(validators.DomainValidatorOptions{})
|
|
|
|
var domainMatch *model.App
|
|
var nameMatch *model.App
|
|
var nameMatchedApps []string
|
|
|
|
locatorFunc := func(name string, app *model.App) bool {
|
|
if app.Config.Domain != "" {
|
|
err := v.Validate(app.Config.Domain, domain)
|
|
if err == nil {
|
|
service.log.App.Debug().Str("name", name).Msg("Found matching container by domain")
|
|
domainMatch = app
|
|
return true
|
|
} else if !errors.Is(err, validators.ErrHostnameMismatch) {
|
|
service.log.App.Debug().Str("name", name).Err(err).Msg("Domain validation failed")
|
|
}
|
|
}
|
|
if strings.HasPrefix(strings.ToLower(domain), strings.ToLower(name+".")) {
|
|
service.log.App.Debug().Str("name", name).Msg("Found matching container by app name")
|
|
nameMatch = app
|
|
nameMatchedApps = append(nameMatchedApps, name)
|
|
}
|
|
return false
|
|
}
|
|
|
|
err := lookup(locatorFunc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if domainMatch != nil {
|
|
service.log.App.Debug().Str("domain", domain).Msg("Found matching app by domain")
|
|
return domainMatch, nil
|
|
}
|
|
|
|
if nameMatch == nil {
|
|
service.log.App.Debug().Str("domain", domain).Msg("No match found for domain, skipping")
|
|
return nil, nil
|
|
}
|
|
|
|
if len(nameMatchedApps) > 1 {
|
|
service.log.App.Warn().Str("domain", domain).Strs("apps", nameMatchedApps).Msg("Multiple apps matched domain by name, app names must be unique, using last match")
|
|
}
|
|
|
|
service.log.App.Debug().Str("domain", domain).Msg("Found matching app by app name")
|
|
return nameMatch, nil
|
|
}
|
|
|
|
func (service *AccessControlsService) lookupStaticACLs(domain string) (*model.App, error) {
|
|
return service.getACLs(domain, func(locator func(name string, app *model.App) bool) error {
|
|
for app, config := range service.config.Apps {
|
|
if ok := locator(app, &config); ok {
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (service *AccessControlsService) GetAccessControls(domain string) (*model.App, error) {
|
|
// First check in the static config
|
|
app, err := service.lookupStaticACLs(domain)
|
|
|
|
// Will never return an error here, but we need to check it
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if app != nil {
|
|
service.log.App.Debug().Msg("Using static ACLs for app")
|
|
return app, nil
|
|
}
|
|
|
|
// If we have a label provider configured, try to get ACLs from it
|
|
if service.labelProvider != nil {
|
|
return service.getACLs(domain, func(locator func(name string, app *model.App) bool) error {
|
|
return service.labelProvider.Lookup(domain, locator)
|
|
})
|
|
}
|
|
|
|
// No labels
|
|
return nil, nil
|
|
}
|