Skip to content

Commit e6582d3

Browse files
committed
feat(config): allow global bypass by ip
1 parent 8849d7e commit e6582d3

4 files changed

Lines changed: 75 additions & 19 deletions

File tree

internal/bootstrap/service_bootstrap.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package bootstrap
22

33
import (
44
"fmt"
5-
65
"os"
76

87
"github.com/tinyauthapp/tinyauth/internal/service"
@@ -126,7 +125,8 @@ func (app *BootstrapApp) setupPolicyEngine() error {
126125
Config: app.config,
127126
})
128127
policyEngine.RegisterRule(service.RuleIPBypassed, &service.IPBypassedRule{
129-
Log: app.log,
128+
Log: app.log,
129+
Config: app.config,
130130
})
131131

132132
app.services.policyEngine = policyEngine

internal/model/config.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,9 @@ type AddressClaim struct {
154154
}
155155

156156
type IPConfig struct {
157-
Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow"`
158-
Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block"`
157+
Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow"`
158+
Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block"`
159+
Bypass []string `description:"List of IPs or CIDR ranges that bypass authentication entirely." yaml:"bypass"`
159160
}
160161

161162
type OAuthConfig struct {

internal/service/access_controls_rules.go

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -182,13 +182,14 @@ type IPAllowedRule struct {
182182
}
183183

184184
func (rule *IPAllowedRule) Evaluate(ctx *ACLContext) Effect {
185-
if ctx.ACLs == nil {
186-
return EffectAbstain
187-
}
185+
// merge global and per-app block/allow lists
186+
blockedIps := rule.Config.Auth.IP.Block
187+
allowedIPs := rule.Config.Auth.IP.Allow
188188

189-
// Merge the global and app IP filter
190-
blockedIps := append(ctx.ACLs.IP.Block, rule.Config.Auth.IP.Block...)
191-
allowedIPs := append(ctx.ACLs.IP.Allow, rule.Config.Auth.IP.Allow...)
189+
if ctx.ACLs != nil {
190+
blockedIps = append(ctx.ACLs.IP.Block, blockedIps...)
191+
allowedIPs = append(ctx.ACLs.IP.Allow, allowedIPs...)
192+
}
192193

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

226227
type IPBypassedRule struct {
227-
Log *logger.Logger
228+
Log *logger.Logger
229+
Config model.Config
228230
}
229231

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

235-
for _, bypassed := range ctx.ACLs.IP.Bypass {
239+
for _, bypassed := range bypassList {
236240
match, err := utils.CheckIPFilter(bypassed, ctx.IP.String())
237241
if err != nil {
238242
rule.Log.App.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list")

internal/service/access_controls_rules_test.go

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"testing"
66

77
"github.com/stretchr/testify/assert"
8+
89
"github.com/tinyauthapp/tinyauth/internal/model"
910
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
1011
)
@@ -558,12 +559,12 @@ func TestIPAllowedRule(t *testing.T) {
558559
expected Effect
559560
}{
560561
{
561-
name: "abstains when ACLs are nil",
562+
name: "allows when ACLs are nil and no global lists configured",
562563
ctx: &ACLContext{
563564
ACLs: nil,
564565
IP: net.ParseIP("10.0.0.1"),
565566
},
566-
expected: EffectAbstain,
567+
expected: EffectAllow,
567568
},
568569
{
569570
name: "denies when IP matches app block list",
@@ -669,23 +670,70 @@ func TestIPBypassedRule(t *testing.T) {
669670
log := logger.NewLogger().WithTestConfig()
670671
log.Init()
671672

672-
rule := &IPBypassedRule{Log: log}
673+
defaultIPBR := &IPBypassedRule{Log: log}
674+
globBypassIPBR := &IPBypassedRule{
675+
Log: log,
676+
Config: model.Config{Auth: model.AuthConfig{IP: model.IPConfig{Bypass: []string{"10.0.0.0/24"}}}},
677+
}
673678

674679
tests := []struct {
675680
name string
681+
rule *IPBypassedRule
676682
ctx *ACLContext
677683
expected Effect
678684
}{
679685
{
680-
name: "deny when ACLs are nil",
686+
name: "deny when ACLs are nil and no global bypass",
687+
rule: defaultIPBR,
681688
ctx: &ACLContext{
682689
ACLs: nil,
683690
IP: net.ParseIP("10.0.0.1"),
684691
},
685692
expected: EffectDeny,
686693
},
694+
{
695+
name: "allows when ACLs are nil but IP matches global bypass",
696+
rule: globBypassIPBR,
697+
ctx: &ACLContext{
698+
ACLs: nil,
699+
IP: net.ParseIP("10.0.0.5"),
700+
},
701+
expected: EffectAllow,
702+
},
703+
{
704+
name: "denies when ACLs are nil and IP does not match global bypass",
705+
rule: globBypassIPBR,
706+
ctx: &ACLContext{
707+
ACLs: nil,
708+
IP: net.ParseIP("192.168.1.1"),
709+
},
710+
expected: EffectDeny,
711+
},
712+
{
713+
name: "allows when IP matches per-app bypass but not global bypass",
714+
rule: defaultIPBR,
715+
ctx: &ACLContext{
716+
ACLs: &model.App{
717+
IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}},
718+
},
719+
IP: net.ParseIP("10.0.0.5"),
720+
},
721+
expected: EffectAllow,
722+
},
723+
{
724+
name: "allows when IP matches global bypass but not per-app bypass",
725+
rule: globBypassIPBR,
726+
ctx: &ACLContext{
727+
ACLs: &model.App{
728+
IP: model.AppIP{Bypass: []string{"172.16.0.0/24"}},
729+
},
730+
IP: net.ParseIP("10.0.0.5"),
731+
},
732+
expected: EffectAllow,
733+
},
687734
{
688735
name: "allows when IP matches bypass list",
736+
rule: defaultIPBR,
689737
ctx: &ACLContext{
690738
ACLs: &model.App{
691739
IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}},
@@ -696,6 +744,7 @@ func TestIPBypassedRule(t *testing.T) {
696744
},
697745
{
698746
name: "denies when IP does not match bypass list",
747+
rule: defaultIPBR,
699748
ctx: &ACLContext{
700749
ACLs: &model.App{
701750
IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}},
@@ -706,6 +755,7 @@ func TestIPBypassedRule(t *testing.T) {
706755
},
707756
{
708757
name: "denies when bypass list is empty",
758+
rule: defaultIPBR,
709759
ctx: &ACLContext{
710760
ACLs: &model.App{},
711761
IP: net.ParseIP("10.0.0.1"),
@@ -714,6 +764,7 @@ func TestIPBypassedRule(t *testing.T) {
714764
},
715765
{
716766
name: "skips invalid bypass entries and allows on later match",
767+
rule: defaultIPBR,
717768
ctx: &ACLContext{
718769
ACLs: &model.App{
719770
IP: model.AppIP{Bypass: []string{"not-an-ip", "10.0.0.1"}},
@@ -726,7 +777,7 @@ func TestIPBypassedRule(t *testing.T) {
726777

727778
for _, tt := range tests {
728779
t.Run(tt.name, func(t *testing.T) {
729-
assert.Equal(t, tt.expected, rule.Evaluate(tt.ctx))
780+
assert.Equal(t, tt.expected, tt.rule.Evaluate(tt.ctx))
730781
})
731782
}
732783
}

0 commit comments

Comments
 (0)