Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TT-11896] Add OAS IPAccessControl #6824

Merged
merged 10 commits into from
Jan 10, 2025
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()

jeffy-mathew marked this conversation as resolved.
Show resolved Hide resolved
}

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
jeffy-mathew marked this conversation as resolved.
Show resolved Hide resolved
}
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 @@
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) {

Check failure on line 850 in apidef/migration_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

`emtpy` is a misspelling of `empty` (misspell)
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) {

Check failure on line 867 in apidef/migration_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

`emtpy` is a misspelling of `empty` (misspell)
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) {

Check failure on line 886 in apidef/migration_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

`emtpy` is a misspelling of `empty` (misspell)
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) {

Check failure on line 903 in apidef/migration_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

`emtpy` is a misspelling of `empty` (misspell)
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"
}
}
}
}
}
}
jeffy-mathew marked this conversation as resolved.
Show resolved Hide resolved
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 {
jeffy-mathew marked this conversation as resolved.
Show resolved Hide resolved
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"`
jeffy-mathew marked this conversation as resolved.
Show resolved Hide resolved

// 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)
})
}
4 changes: 4 additions & 0 deletions gateway/mw_ip_blacklist.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ func (i *IPBlackListMiddleware) Name() string {
}

func (i *IPBlackListMiddleware) EnabledForSpec() bool {
if i.Spec.APIDefinition.IPAccessControlDisabled {
return false
}

return i.Spec.EnableIpBlacklisting && len(i.Spec.BlacklistedIPs) > 0
}

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"

jeffy-mathew marked this conversation as resolved.
Show resolved Hide resolved
"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)
jeffy-mathew marked this conversation as resolved.
Show resolved Hide resolved
}
})
}
}
func TestIPBlacklistMiddleware(t *testing.T) {
spec := testPrepareIPBlacklistMiddleware()

Expand Down
Loading
Loading