Skip to content

Commit

Permalink
Add OAS IPAccessControl
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffy-mathew committed Jan 10, 2025
1 parent daf76a8 commit 6801e5a
Show file tree
Hide file tree
Showing 11 changed files with 374 additions and 4 deletions.
1 change: 1 addition & 0 deletions apidef/api_definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,7 @@ type APIDefinition struct {
AllowedIPs []string `mapstructure:"allowed_ips" bson:"allowed_ips" json:"allowed_ips"`
EnableIpBlacklisting bool `mapstructure:"enable_ip_blacklisting" bson:"enable_ip_blacklisting" json:"enable_ip_blacklisting"`
BlacklistedIPs []string `mapstructure:"blacklisted_ips" bson:"blacklisted_ips" json:"blacklisted_ips"`
IPAccessControlDisabled bool `mapstructure:"ip_access_control_disabled" bson:"ip_access_control_disabled" json:"ip_access_control_disabled"`
DontSetQuotasOnCreate bool `mapstructure:"dont_set_quota_on_create" bson:"dont_set_quota_on_create" json:"dont_set_quota_on_create"`
ExpireAnalyticsAfter int64 `mapstructure:"expire_analytics_after" bson:"expire_analytics_after" json:"expire_analytics_after"` // must have an expireAt TTL index set (http://docs.mongodb.org/manual/tutorial/expire-data/)
ResponseProcessors []ResponseProcessor `bson:"response_processors" json:"response_processors"`
Expand Down
14 changes: 14 additions & 0 deletions apidef/migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ func (a *APIDefinition) Migrate() (versions []APIDefinition, err error) {
a.migrateScopeToPolicy()
a.migrateResponseProcessors()
a.migrateGlobalRateLimit()
a.migrateIPAccessControl()

versions, err = a.MigrateVersioning()
if err != nil {
Expand Down Expand Up @@ -471,6 +472,7 @@ func (a *APIDefinition) SetDisabledFlags() {
a.DoNotTrack = true

a.setEventHandlersDisabledFlags()

}

func (a *APIDefinition) setEventHandlersDisabledFlags() {
Expand Down Expand Up @@ -517,3 +519,15 @@ func (a *APIDefinition) migrateGlobalRateLimit() {
a.GlobalRateLimit.Disabled = true
}
}

func (a *APIDefinition) migrateIPAccessControl() {
if a.EnableIpBlacklisting && len(a.BlacklistedIPs) > 0 {
return
}

if a.EnableIpWhiteListing && len(a.AllowedIPs) > 0 {
return
}

a.IPAccessControlDisabled = true
}
75 changes: 75 additions & 0 deletions apidef/migration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -836,3 +836,78 @@ func TestAPIDefinition_migrateGlobalRateLimit(t *testing.T) {
assert.False(t, base.GlobalRateLimit.Disabled)
})
}

func TestAPIDefinition_migrateIPAccessControl(t *testing.T) {
t.Run("whitelisting", func(t *testing.T) {
t.Run("EnableIpWhitelisting=true, no whitelist", func(t *testing.T) {
base := oldTestAPI()
base.EnableIpWhiteListing = true
_, err := base.Migrate()
assert.NoError(t, err)
assert.True(t, base.IPAccessControlDisabled)
})

t.Run("IPWhiteListEnabled=true, non-emtpy whitelist", func(t *testing.T) {
base := oldTestAPI()
base.EnableIpWhiteListing = true
base.AllowedIPs = []string{"127.0.0.1"}
_, err := base.Migrate()
assert.NoError(t, err)
assert.False(t, base.IPAccessControlDisabled)
})

t.Run("EnableIpWhitelisting=false, no whitelist", func(t *testing.T) {
base := oldTestAPI()
base.EnableIpWhiteListing = false
_, err := base.Migrate()
assert.NoError(t, err)
assert.True(t, base.IPAccessControlDisabled)
})

t.Run("IPWhiteListEnabled=false, non-emtpy whitelist", func(t *testing.T) {
base := oldTestAPI()
base.EnableIpWhiteListing = false
base.AllowedIPs = []string{"127.0.0.1"}
_, err := base.Migrate()
assert.NoError(t, err)
assert.True(t, base.IPAccessControlDisabled)
})
})

t.Run("blacklisting", func(t *testing.T) {
t.Run("EnableIpBlacklisting=true, no blacklist", func(t *testing.T) {
base := oldTestAPI()
base.EnableIpBlacklisting = true
_, err := base.Migrate()
assert.NoError(t, err)
assert.True(t, base.IPAccessControlDisabled)
})

t.Run("EnableIpBlacklisting=true, non-emtpy blacklist", func(t *testing.T) {
base := oldTestAPI()
base.EnableIpBlacklisting = true
base.BlacklistedIPs = []string{"127.0.0.1"}
_, err := base.Migrate()
assert.NoError(t, err)
assert.False(t, base.IPAccessControlDisabled)
})

t.Run("EnableIpBlacklisting=false, no blacklist", func(t *testing.T) {
base := oldTestAPI()
base.EnableIpBlacklisting = false
_, err := base.Migrate()
assert.NoError(t, err)
assert.True(t, base.IPAccessControlDisabled)
})

t.Run("IPWhiteListEnabled=false, non-emtpy blacklist", func(t *testing.T) {
base := oldTestAPI()
base.EnableIpBlacklisting = false
base.BlacklistedIPs = []string{"127.0.0.1"}
_, err := base.Migrate()
assert.NoError(t, err)
assert.True(t, base.IPAccessControlDisabled)
})
})

}
3 changes: 1 addition & 2 deletions apidef/oas/oas_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ func TestOAS_ExtractTo_ResetAPIDefinition(t *testing.T) {
a.EnableContextVars = false
a.DisableRateLimit = false
a.DoNotTrack = false
a.IPAccessControlDisabled = false

// deprecated fields
a.Auth = apidef.AuthConfig{}
Expand Down Expand Up @@ -263,9 +264,7 @@ func TestOAS_ExtractTo_ResetAPIDefinition(t *testing.T) {
"APIDefinition.SessionProvider.Meta[0]",
"APIDefinition.EnableBatchRequestSupport",
"APIDefinition.EnableIpWhiteListing",
"APIDefinition.AllowedIPs[0]",
"APIDefinition.EnableIpBlacklisting",
"APIDefinition.BlacklistedIPs[0]",
"APIDefinition.DontSetQuotasOnCreate",
"APIDefinition.ExpireAnalyticsAfter",
"APIDefinition.ResponseProcessors[0].Name",
Expand Down
23 changes: 23 additions & 0 deletions apidef/oas/schema/x-tyk-api-gateway.json
Original file line number Diff line number Diff line change
Expand Up @@ -1244,6 +1244,9 @@
},
"contextVariables": {
"$ref": "#/definitions/X-Tyk-ContextVariables"
},
"ipAccessControl": {
"$ref": "#/definitions/X-Tyk-IPAccessControl"
}
},
"required": [
Expand Down Expand Up @@ -2159,6 +2162,26 @@
"type": "string"
}
}
},
"X-Tyk-IPAccessControl": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
},
"allow": {
"type": "array",
"items": {
"type": "string"
}
},
"block": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
51 changes: 51 additions & 0 deletions apidef/oas/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ type Server struct {
//
// Tyk classic API definition: `event_handlers`
EventHandlers EventHandlers `bson:"eventHandlers,omitempty" json:"eventHandlers,omitempty"`

// IPAccessControl configures IP access control for this API.
//
// Tyk classic API definition: `allowed_ips` and `blacklisted_ips`.
IPAccessControl *IPAccessControl `bson:"ipAccessControl" json:"ipAccessControl,omitempty"`
}

// Fill fills *Server from apidef.APIDefinition.
Expand Down Expand Up @@ -94,6 +99,15 @@ func (s *Server) Fill(api apidef.APIDefinition) {
if ShouldOmit(s.EventHandlers) {
s.EventHandlers = nil
}

if s.IPAccessControl == nil {
s.IPAccessControl = &IPAccessControl{}
}

s.IPAccessControl.Fill(api)
if ShouldOmit(s.IPAccessControl) {
s.IPAccessControl = nil
}
}

// ExtractTo extracts *Server into *apidef.APIDefinition.
Expand Down Expand Up @@ -153,6 +167,15 @@ func (s *Server) ExtractTo(api *apidef.APIDefinition) {
}

s.EventHandlers.ExtractTo(api)

if s.IPAccessControl == nil {
s.IPAccessControl = &IPAccessControl{}
defer func() {
s.IPAccessControl = nil
}()
}

s.IPAccessControl.ExtractTo(api)
}

// ListenPath is the base path on Tyk to which requests for this API
Expand Down Expand Up @@ -287,3 +310,31 @@ func (dt *DetailedTracing) Fill(api apidef.APIDefinition) {
func (dt *DetailedTracing) ExtractTo(api *apidef.APIDefinition) {
api.DetailedTracing = dt.Enabled
}

// IPAccessControl represents IP access control configuration.
type IPAccessControl struct {
// Enabled indicates whether IP access control is enabled.
Enabled bool `json:"enabled"`

// Allow is a list of allowed IP addresses or CIDR blocks (e.g. "192.168.1.0/24").
// Note that if an IP address is present in both Allow and Block, the Block rule will take precedence.
Allow []string `json:"allow"`

// Block is a list of blocked IP addresses or CIDR blocks (e.g. "192.168.1.100/32").
// If an IP address is present in both Allow and Block, the Block rule will take precedence.
Block []string `json:"block"`
}

// Fill fills *IPAccessControl from apidef.APIDefinition.
func (i *IPAccessControl) Fill(api apidef.APIDefinition) {
i.Enabled = !api.IPAccessControlDisabled
i.Block = api.BlacklistedIPs
i.Allow = api.AllowedIPs
}

// ExtractTo extracts *IPAccessControl into *apidef.APIDefinition.
func (i *IPAccessControl) ExtractTo(api *apidef.APIDefinition) {
api.IPAccessControlDisabled = !i.Enabled
api.BlacklistedIPs = i.Block
api.AllowedIPs = i.Allow
}
34 changes: 34 additions & 0 deletions apidef/oas/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,3 +323,37 @@ func TestExportDetailedTracing(t *testing.T) {
})
}
}

func TestIPAccessControl(t *testing.T) {
t.Run("empty", func(t *testing.T) {
var emptyIPAccessControl IPAccessControl

var convertedAPI apidef.APIDefinition
convertedAPI.SetDisabledFlags()
emptyIPAccessControl.ExtractTo(&convertedAPI)

var resultIPAccessControl IPAccessControl
resultIPAccessControl.Fill(convertedAPI)

assert.Equal(t, emptyIPAccessControl, resultIPAccessControl)
})

t.Run("valid", func(t *testing.T) {
ipAccessControl := IPAccessControl{
Enabled: true,
Allow: []string{"127.0.0.1"},
Block: []string{"10.0.0.1"},
}

var convertedAPI apidef.APIDefinition
convertedAPI.SetDisabledFlags()
ipAccessControl.ExtractTo(&convertedAPI)

assert.False(t, convertedAPI.IPAccessControlDisabled)

var resultIPAccessControl IPAccessControl
resultIPAccessControl.Fill(convertedAPI)

assert.Equal(t, ipAccessControl, resultIPAccessControl)
})
}
7 changes: 6 additions & 1 deletion gateway/mw_ip_blacklist.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ func (i *IPBlackListMiddleware) Name() string {
}

func (i *IPBlackListMiddleware) EnabledForSpec() bool {
return i.Spec.EnableIpBlacklisting && len(i.Spec.BlacklistedIPs) > 0
enabled := i.Spec.APIDefinition.EnableIpBlacklisting || !i.Spec.APIDefinition.IPAccessControlDisabled
if !enabled {
return false
}

return len(i.Spec.APIDefinition.BlacklistedIPs) > 0
}

// ProcessRequest will run any checks on the request on the way through the system, return an error to have the chain fail
Expand Down
81 changes: 81 additions & 0 deletions gateway/mw_ip_blacklist_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"net/http/httptest"
"testing"

"github.com/TykTechnologies/tyk/apidef"

"github.com/TykTechnologies/tyk/header"
)

Expand All @@ -27,6 +29,85 @@ func testPrepareIPBlacklistMiddleware() *APISpec {
})[0]
}

func TestIPBlackListMiddleware_EnabledForSpec(t *testing.T) {
tests := []struct {
name string
spec *APISpec
wantResult bool
}{
{
name: "IpBlacklisting enabled and BlacklistedIPs not empty",
spec: &APISpec{
APIDefinition: &apidef.APIDefinition{
EnableIpBlacklisting: true,
BlacklistedIPs: []string{"192.168.1.1"},
},
},
wantResult: true,
},
{
name: "IpBlacklisting disabled and IPAccessControl disabled and BlacklistedIPs not empty",
spec: &APISpec{
APIDefinition: &apidef.APIDefinition{
EnableIpBlacklisting: false,
IPAccessControlDisabled: true,
BlacklistedIPs: []string{"192.168.1.1"},
},
},
wantResult: false,
},
{
name: "IpBlacklisting enabled and BlacklistedIPs empty",
spec: &APISpec{
APIDefinition: &apidef.APIDefinition{
EnableIpBlacklisting: true,
BlacklistedIPs: []string{},
},
},
wantResult: false,
},
{
name: "IpBlacklisting disabled and BlacklistedIPs empty",
spec: &APISpec{
APIDefinition: &apidef.APIDefinition{
EnableIpBlacklisting: false,
BlacklistedIPs: []string{},
},
},
wantResult: false,
},
{
name: "IPAccessControlDisabled true and BlacklistedIPs not empty",
spec: &APISpec{
APIDefinition: &apidef.APIDefinition{
IPAccessControlDisabled: true,
BlacklistedIPs: []string{"192.168.1.1"},
},
},
wantResult: false,
},
{
name: "IPAccessControlDisabled false and BlacklistedIPs not empty",
spec: &APISpec{
APIDefinition: &apidef.APIDefinition{
IPAccessControlDisabled: false,
BlacklistedIPs: []string{"192.168.1.1"},
},
},
wantResult: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
middleware := &IPBlackListMiddleware{BaseMiddleware: &BaseMiddleware{Spec: tt.spec}}
gotResult := middleware.EnabledForSpec()
if gotResult != tt.wantResult {
t.Errorf("EnabledForSpec() = %v, want %v", gotResult, tt.wantResult)
}
})
}
}
func TestIPBlacklistMiddleware(t *testing.T) {
spec := testPrepareIPBlacklistMiddleware()

Expand Down
Loading

0 comments on commit 6801e5a

Please sign in to comment.