Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ TINYAUTH_APPS_name_CONFIG_DOMAIN=
TINYAUTH_APPS_name_USERS_ALLOW=
# Comma-separated list of blocked users.
TINYAUTH_APPS_name_USERS_BLOCK=
# Comma-separated list of allowed OAuth groups.
# Comma-separated list of allowed OAuth emails.
TINYAUTH_APPS_name_OAUTH_WHITELIST=
# Path to the OAuth whitelist file for this app.
TINYAUTH_APPS_name_OAUTH_WHITELISTFILE=
# Comma-separated list of required OAuth groups.
TINYAUTH_APPS_name_OAUTH_GROUPS=
# List of allowed IPs or CIDR ranges.
Expand Down
5 changes: 3 additions & 2 deletions internal/model/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,9 @@ type AppUsers struct {
}

type AppOAuth struct {
Whitelist string `description:"Comma-separated list of allowed OAuth groups." yaml:"whitelist"`
Groups string `description:"Comma-separated list of required OAuth groups." yaml:"groups"`
Whitelist string `description:"Comma-separated list of allowed OAuth emails." yaml:"whitelist"`
WhitelistFile string `description:"Path to the OAuth whitelist file for this app." yaml:"whitelistFile"`
Groups string `description:"Comma-separated list of required OAuth groups." yaml:"groups"`
}

type AppLDAP struct {
Expand Down
24 changes: 22 additions & 2 deletions internal/service/access_controls_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"strings"

"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
)

Expand Down Expand Up @@ -53,14 +54,33 @@ func (service *AccessControlsService) GetAccessControls(domain string) (*model.A

if app != nil {
service.log.App.Debug().Msg("Using static ACLs for app")
return app, nil
return service.resolveAppOAuthWhitelist(app)
}

// If we have a label provider configured, try to get ACLs from it
if service.labelProvider != nil && *service.labelProvider != nil {
return (*service.labelProvider).GetLabels(domain)
app, err := (*service.labelProvider).GetLabels(domain)
if err != nil {
return nil, err
}
return service.resolveAppOAuthWhitelist(app)
}

// no labels
return nil, nil
}

func (service *AccessControlsService) resolveAppOAuthWhitelist(app *model.App) (*model.App, error) {
if app == nil || app.OAuth.WhitelistFile == "" {
return app, nil
}

values, err := utils.GetStringList([]string{app.OAuth.Whitelist}, app.OAuth.WhitelistFile)
if err != nil {
return nil, err
}

resolved := *app
resolved.OAuth.Whitelist = strings.Join(values, ",")
return &resolved, nil
}
52 changes: 52 additions & 0 deletions internal/service/access_controls_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package service

import (
"errors"
"os"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -162,6 +163,38 @@ func TestGetAccessControls(t *testing.T) {
assert.Equal(t, 1, mock.callCount)
})

t.Run("loads app oauth whitelist from file for static config", func(t *testing.T) {
tmpDir := t.TempDir()
whitelistPath := tmpDir + "/oauth-whitelist.txt"
file, err := os.Create(whitelistPath)
require.NoError(t, err)

_, err = file.WriteString("second@example.com\nthird@example.com\n")
require.NoError(t, err)

err = file.Close()
require.NoError(t, err)

config := model.Config{
Apps: map[string]model.App{
"foo": {
Config: model.AppConfig{Domain: "foo.example.com"},
OAuth: model.AppOAuth{
Whitelist: "first@example.com",
WhitelistFile: whitelistPath,
},
},
},
}
svc := NewAccessControlsService(log, config, nil)

got, err := svc.GetAccessControls("foo.example.com")

require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, "first@example.com,second@example.com,third@example.com", got.OAuth.Whitelist)
})

t.Run("does not call label provider when static match found", func(t *testing.T) {
mock := &mockLabelProvider{}
var provider LabelProvider = mock
Expand Down Expand Up @@ -196,4 +229,23 @@ func TestGetAccessControls(t *testing.T) {
assert.ErrorIs(t, err, providerErr)
assert.Equal(t, 1, mock.callCount)
})

t.Run("returns whitelist file errors from static config", func(t *testing.T) {
config := model.Config{
Apps: map[string]model.App{
"foo": {
Config: model.AppConfig{Domain: "foo.example.com"},
OAuth: model.AppOAuth{
WhitelistFile: t.TempDir() + "/missing.txt",
},
},
},
}
svc := NewAccessControlsService(log, config, nil)

got, err := svc.GetAccessControls("foo.example.com")

assert.Nil(t, got)
assert.ErrorContains(t, err, "no such file or directory")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid OS-specific error-string assertions.

Line 249 matches "no such file or directory", which is platform-dependent. Prefer semantic error checks.

Suggested fix
-		assert.ErrorContains(t, err, "no such file or directory")
+		assert.ErrorIs(t, err, os.ErrNotExist)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert.ErrorContains(t, err, "no such file or directory")
assert.ErrorIs(t, err, os.ErrNotExist)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/access_controls_service_test.go` at line 249, Replace the
OS-specific substring assertion on err with a semantic existence check: instead
of assert.ErrorContains(t, err, "no such file or directory"), assert that the
error is os.ErrNotExist (e.g. assert.ErrorIs(t, err, os.ErrNotExist) or
assert.True(t, errors.Is(err, os.ErrNotExist))). Update imports to include "os"
(and "errors" if using errors.Is) and keep the same err variable and test
context in access_controls_service_test.go.

})
}
6 changes: 4 additions & 2 deletions internal/utils/decoders/label_decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ func TestDecodeLabels(t *testing.T) {
Block: "user3",
},
OAuth: model.AppOAuth{
Whitelist: "somebody@example.com",
Groups: "group3",
Whitelist: "somebody@example.com",
WhitelistFile: "/path/to/whitelistfile",
Groups: "group3",
},
IP: model.AppIP{
Allow: []string{"10.71.0.1/24", "10.71.0.2"},
Expand All @@ -49,6 +50,7 @@ func TestDecodeLabels(t *testing.T) {
"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.whitelistfile": "/path/to/whitelistfile",
"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",
Expand Down