Skip to content
Merged
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: 2 additions & 2 deletions internal/bootstrap/service_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package bootstrap

import (
"fmt"

"os"

"github.com/tinyauthapp/tinyauth/internal/service"
Expand Down Expand Up @@ -126,7 +125,8 @@ func (app *BootstrapApp) setupPolicyEngine() error {
Config: app.config,
})
policyEngine.RegisterRule(service.RuleIPBypassed, &service.IPBypassedRule{
Log: app.log,
Log: app.log,
Config: app.config,
})

app.services.policyEngine = policyEngine
Expand Down
5 changes: 3 additions & 2 deletions internal/model/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,9 @@ type AddressClaim struct {
}

type IPConfig struct {
Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow"`
Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block"`
Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow"`
Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block"`
Bypass []string `description:"List of IPs or CIDR ranges that bypass authentication entirely." yaml:"bypass"`
}

type OAuthConfig struct {
Expand Down
24 changes: 14 additions & 10 deletions internal/service/access_controls_rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,14 @@ type IPAllowedRule struct {
}

func (rule *IPAllowedRule) Evaluate(ctx *ACLContext) Effect {
if ctx.ACLs == nil {
return EffectAbstain
}
// merge global and per-app block/allow lists
blockedIps := append([]string{}, rule.Config.Auth.IP.Block...)
allowedIPs := append([]string{}, rule.Config.Auth.IP.Allow...)

// Merge the global and app IP filter
blockedIps := append(ctx.ACLs.IP.Block, rule.Config.Auth.IP.Block...)
allowedIPs := append(ctx.ACLs.IP.Allow, rule.Config.Auth.IP.Allow...)
if ctx.ACLs != nil {
blockedIps = append(blockedIps, ctx.ACLs.IP.Block...)
allowedIPs = append(allowedIPs, ctx.ACLs.IP.Allow...)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

for _, blocked := range blockedIps {
match, err := utils.CheckIPFilter(blocked, ctx.IP.String())
Expand Down Expand Up @@ -224,15 +225,18 @@ func (rule *IPAllowedRule) Evaluate(ctx *ACLContext) Effect {
}

type IPBypassedRule struct {
Log *logger.Logger
Log *logger.Logger
Config model.Config
}

func (rule *IPBypassedRule) Evaluate(ctx *ACLContext) Effect {
if ctx.ACLs == nil {
return EffectDeny
// merge global and per-app bypass lists
bypassList := append([]string{}, rule.Config.Auth.IP.Bypass...)
if ctx.ACLs != nil {
bypassList = append(bypassList, ctx.ACLs.IP.Bypass...)
}

for _, bypassed := range ctx.ACLs.IP.Bypass {
for _, bypassed := range bypassList {
match, err := utils.CheckIPFilter(bypassed, ctx.IP.String())
if err != nil {
rule.Log.App.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list")
Expand Down
61 changes: 56 additions & 5 deletions internal/service/access_controls_rules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
)
Expand Down Expand Up @@ -558,12 +559,12 @@ func TestIPAllowedRule(t *testing.T) {
expected Effect
}{
{
name: "abstains when ACLs are nil",
name: "allows when ACLs are nil and no global lists configured",
ctx: &ACLContext{
ACLs: nil,
IP: net.ParseIP("10.0.0.1"),
},
expected: EffectAbstain,
expected: EffectAllow,
},
{
name: "denies when IP matches app block list",
Expand Down Expand Up @@ -669,23 +670,70 @@ func TestIPBypassedRule(t *testing.T) {
log := logger.NewLogger().WithTestConfig()
log.Init()

rule := &IPBypassedRule{Log: log}
defaultIPBR := &IPBypassedRule{Log: log}
globBypassIPBR := &IPBypassedRule{
Log: log,
Config: model.Config{Auth: model.AuthConfig{IP: model.IPConfig{Bypass: []string{"10.0.0.0/24"}}}},
}

tests := []struct {
name string
rule *IPBypassedRule
ctx *ACLContext
expected Effect
}{
{
name: "deny when ACLs are nil",
name: "deny when ACLs are nil and no global bypass",
rule: defaultIPBR,
ctx: &ACLContext{
ACLs: nil,
IP: net.ParseIP("10.0.0.1"),
},
expected: EffectDeny,
},
{
name: "allows when ACLs are nil but IP matches global bypass",
rule: globBypassIPBR,
ctx: &ACLContext{
ACLs: nil,
IP: net.ParseIP("10.0.0.5"),
},
expected: EffectAllow,
},
{
name: "denies when ACLs are nil and IP does not match global bypass",
rule: globBypassIPBR,
ctx: &ACLContext{
ACLs: nil,
IP: net.ParseIP("192.168.1.1"),
},
expected: EffectDeny,
},
{
name: "allows when IP matches per-app bypass but not global bypass",
rule: defaultIPBR,
ctx: &ACLContext{
ACLs: &model.App{
IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}},
},
IP: net.ParseIP("10.0.0.5"),
},
expected: EffectAllow,
},
{
name: "allows when IP matches global bypass but not per-app bypass",
rule: globBypassIPBR,
ctx: &ACLContext{
ACLs: &model.App{
IP: model.AppIP{Bypass: []string{"172.16.0.0/24"}},
},
IP: net.ParseIP("10.0.0.5"),
},
expected: EffectAllow,
},
{
name: "allows when IP matches bypass list",
rule: defaultIPBR,
ctx: &ACLContext{
ACLs: &model.App{
IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}},
Expand All @@ -696,6 +744,7 @@ func TestIPBypassedRule(t *testing.T) {
},
{
name: "denies when IP does not match bypass list",
rule: defaultIPBR,
ctx: &ACLContext{
ACLs: &model.App{
IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}},
Expand All @@ -706,6 +755,7 @@ func TestIPBypassedRule(t *testing.T) {
},
{
name: "denies when bypass list is empty",
rule: defaultIPBR,
ctx: &ACLContext{
ACLs: &model.App{},
IP: net.ParseIP("10.0.0.1"),
Expand All @@ -714,6 +764,7 @@ func TestIPBypassedRule(t *testing.T) {
},
{
name: "skips invalid bypass entries and allows on later match",
rule: defaultIPBR,
ctx: &ACLContext{
ACLs: &model.App{
IP: model.AppIP{Bypass: []string{"not-an-ip", "10.0.0.1"}},
Expand All @@ -726,7 +777,7 @@ func TestIPBypassedRule(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, rule.Evaluate(tt.ctx))
assert.Equal(t, tt.expected, tt.rule.Evaluate(tt.ctx))
})
}
}
Loading