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>
189 lines
5.2 KiB
Go
189 lines
5.2 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
|
)
|
|
|
|
type mockProvider struct {
|
|
acls map[string]model.App
|
|
shouldError bool
|
|
}
|
|
|
|
func newMockProvider(acls map[string]model.App, shouldError bool) *mockProvider {
|
|
return &mockProvider{acls: acls, shouldError: shouldError}
|
|
}
|
|
|
|
func (m *mockProvider) Lookup(_ string, locator func(name string, app *model.App) bool) error {
|
|
if m.shouldError {
|
|
return errors.New("mock error")
|
|
}
|
|
for name, app := range m.acls {
|
|
if ok := locator(name, &app); ok {
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestAccessControlsService(t *testing.T) {
|
|
log := logger.NewLogger().WithTestConfig()
|
|
log.Init()
|
|
|
|
tests := []struct {
|
|
name string
|
|
domain string
|
|
acls map[string]model.App
|
|
want *model.App
|
|
}{
|
|
{
|
|
name: "returns ACLs for domain",
|
|
domain: "example.com",
|
|
acls: map[string]model.App{
|
|
"foo": {Config: model.AppConfig{Domain: "example.com"}},
|
|
},
|
|
want: &model.App{Config: model.AppConfig{Domain: "example.com"}},
|
|
},
|
|
{
|
|
name: "returns ACLs for domain with port",
|
|
domain: "example.com:8080",
|
|
acls: map[string]model.App{
|
|
"foo": {Config: model.AppConfig{Domain: "example.com"}},
|
|
},
|
|
want: &model.App{Config: model.AppConfig{Domain: "example.com"}},
|
|
},
|
|
{
|
|
name: "returns ACLs for domain with trailing dot",
|
|
domain: "example.com.",
|
|
acls: map[string]model.App{
|
|
"foo": {Config: model.AppConfig{Domain: "example.com"}},
|
|
},
|
|
want: &model.App{Config: model.AppConfig{Domain: "example.com"}},
|
|
},
|
|
{
|
|
name: "returns ACLs for non-ascii domain",
|
|
domain: "bücher.example.com",
|
|
acls: map[string]model.App{
|
|
"foo": {Config: model.AppConfig{Domain: "bücher.example.com"}},
|
|
},
|
|
want: &model.App{Config: model.AppConfig{Domain: "bücher.example.com"}},
|
|
},
|
|
{
|
|
name: "returns ACLs for punycode domain and non-ascii config",
|
|
domain: "bücher.example.com",
|
|
acls: map[string]model.App{
|
|
"foo": {Config: model.AppConfig{Domain: "xn--bcher-kva.example.com"}},
|
|
},
|
|
want: &model.App{Config: model.AppConfig{Domain: "xn--bcher-kva.example.com"}},
|
|
},
|
|
{
|
|
name: "returns ACLs with case-insensitive matching",
|
|
domain: "Example.com",
|
|
acls: map[string]model.App{
|
|
"foo": {Config: model.AppConfig{Domain: "example.com"}},
|
|
},
|
|
want: &model.App{Config: model.AppConfig{Domain: "example.com"}},
|
|
},
|
|
{
|
|
name: "falls back to name matching when domain fails",
|
|
domain: "app.example.com",
|
|
acls: map[string]model.App{
|
|
"app": {Path: model.AppPath{Allow: "/foo"}},
|
|
},
|
|
want: &model.App{Path: model.AppPath{Allow: "/foo"}},
|
|
},
|
|
{
|
|
name: "name matching is case-insensitive",
|
|
domain: "aPp.example.com",
|
|
acls: map[string]model.App{
|
|
"APP": {Path: model.AppPath{Allow: "/foo"}},
|
|
},
|
|
want: &model.App{Path: model.AppPath{Allow: "/foo"}},
|
|
},
|
|
{
|
|
name: "returns nil when no ACLs are found",
|
|
domain: "example.com",
|
|
acls: map[string]model.App{},
|
|
want: nil,
|
|
},
|
|
}
|
|
|
|
// run once for a mock provider
|
|
for _, test := range tests {
|
|
t.Run(test.name+"(getACLs)", func(t *testing.T) {
|
|
mock := newMockProvider(test.acls, false)
|
|
acls := NewAccessControlsService(AccessControlServiceInput{
|
|
Log: log,
|
|
Config: &model.Config{},
|
|
LabelProvider: mock,
|
|
})
|
|
app, err := acls.GetAccessControls(test.domain)
|
|
require.NoError(t, err)
|
|
require.Equal(t, test.want, app)
|
|
})
|
|
}
|
|
|
|
// run again for static acls
|
|
for _, test := range tests {
|
|
t.Run(test.name+"(staticACLs)", func(t *testing.T) {
|
|
acls := NewAccessControlsService(AccessControlServiceInput{
|
|
Log: log,
|
|
Config: &model.Config{
|
|
Apps: test.acls,
|
|
},
|
|
})
|
|
app, err := acls.lookupStaticACLs(test.domain)
|
|
require.NoError(t, err)
|
|
require.Equal(t, test.want, app)
|
|
})
|
|
}
|
|
|
|
// get acls should return an error when the provider fails
|
|
mock := newMockProvider(map[string]model.App{}, true)
|
|
acls := NewAccessControlsService(AccessControlServiceInput{
|
|
Log: log,
|
|
Config: &model.Config{},
|
|
LabelProvider: mock,
|
|
})
|
|
_, err := acls.GetAccessControls("example.com")
|
|
require.Error(t, err)
|
|
|
|
// get access controls should get acls from
|
|
// static when static acls are configured
|
|
acls = NewAccessControlsService(AccessControlServiceInput{
|
|
Log: log,
|
|
Config: &model.Config{
|
|
Apps: map[string]model.App{
|
|
"foo": {Config: model.AppConfig{Domain: "foo.example.com"}},
|
|
},
|
|
},
|
|
})
|
|
app, err := acls.GetAccessControls("foo.example.com")
|
|
require.NoError(t, err)
|
|
require.Equal(t, &model.App{Config: model.AppConfig{Domain: "foo.example.com"}}, app)
|
|
|
|
// should return nil for no apps
|
|
app, err = acls.GetAccessControls("bar.example.com")
|
|
require.NoError(t, err)
|
|
require.Nil(t, app)
|
|
|
|
// Should use label provider if available
|
|
mock = newMockProvider(map[string]model.App{
|
|
"bar": {
|
|
Config: model.AppConfig{Domain: "bar.example.com"},
|
|
},
|
|
}, false)
|
|
acls = NewAccessControlsService(AccessControlServiceInput{
|
|
Log: log,
|
|
Config: &model.Config{},
|
|
LabelProvider: mock,
|
|
})
|
|
app, err = acls.GetAccessControls("bar.example.com")
|
|
require.NoError(t, err)
|
|
require.Equal(t, &model.App{Config: model.AppConfig{Domain: "bar.example.com"}}, app)
|
|
}
|