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
15 changes: 15 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 @@ -517,3 +518,17 @@ func (a *APIDefinition) migrateGlobalRateLimit() {
a.GlobalRateLimit.Disabled = true
}
}

func (a *APIDefinition) migrateIPAccessControl() {
a.IPAccessControlDisabled = false

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 @@ 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-empty 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-empty 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-empty 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-empty 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
25 changes: 24 additions & 1 deletion 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"
}
}
}
}
}
}
}
59 changes: 59 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,omitempty" json:"ipAccessControl,omitempty"`
}

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

s.fillIPAccessControl(api)
}

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

s.EventHandlers.ExtractTo(api)

s.extractIPAccessControlTo(api)
}

// ListenPath is the base path on Tyk to which requests for this API
Expand Down Expand Up @@ -287,3 +296,53 @@ 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 `bson:"enabled" 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 `bson:"allow,omitempty" json:"allow,omitempty"`

// 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 `bson:"block,omitempty" json:"block,omitempty"`
}

// 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
}

func (s *Server) fillIPAccessControl(api apidef.APIDefinition) {
if s.IPAccessControl == nil {
s.IPAccessControl = &IPAccessControl{}
}

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

func (s *Server) extractIPAccessControlTo(api *apidef.APIDefinition) {
if s.IPAccessControl == nil {
s.IPAccessControl = &IPAccessControl{}
defer func() {
s.IPAccessControl = nil
}()
}

s.IPAccessControl.ExtractTo(api)
}
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)
})
}
3 changes: 3 additions & 0 deletions apidef/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
"tags_disabled": {
"type": "boolean"
},
"ip_access_control_disabled": {
"type": "boolean"
},
"enable_ip_whitelisting": {
"type": "boolean"
},
Expand Down
8 changes: 6 additions & 2 deletions docs/dev/apidef-oas.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ To ensure feature parity between Tyk OAS APIs and Tyk classic API definitions, f

Define the necessary structs or add the necessary fields in the `apidef/oas` package.

Make sure `json` and `bson` tags are added to the fields.

If an `enabled` flag is specified in the OAS contract, make sure a corresponding `disabled` or `enabled` flag is added in the classic API definition.

Also make sure that `disabled`/`enabled` flag toggles the feature on or off.
Expand Down Expand Up @@ -42,9 +44,11 @@ For fields that are required:

For optional fields:

1. Add the `omitempty` tag.
1. Add the `omitempty` tag

2. Use pointer types for structs.
2. Use pointer types for structs.

3. Make sure that `omitempty` tag is added for slice fields that are optional.

## Add Go Doc Comments

Expand Down
4 changes: 3 additions & 1 deletion gateway/mw_ip_blacklist.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ func (i *IPBlackListMiddleware) Name() string {
}

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

return enabled && 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
Loading
Loading