From 637809c66349c1da973a0cb5461d7344fe89c5f9 Mon Sep 17 00:00:00 2001 From: Mateusz Filipowicz Date: Thu, 20 Feb 2025 18:48:04 +0100 Subject: [PATCH] feat: support Zone-Based Firewalls (#33) * feat: support Zone-Based Firewalls * chore: add usage example of zone-based firewall * chore: add note to readme to support unifi controller v2 * fix: invalid path for reordering firewall zone policies --- README.md | 6 + codegen/customizations.yml | 20 +++ codegen/v2/FirewallZone.json | 4 + codegen/v2/FirewallZoneMatrix.json | 8 + codegen/v2/FirewallZonePolicy.json | 58 ++++++ docs/usage_examples.md | 42 +++++ unifi/client.generated.go | 40 +++++ unifi/firewall_zone.generated.go | 100 +++++++++++ unifi/firewall_zone.go | 23 +++ unifi/firewall_zone_matrix.generated.go | 125 +++++++++++++ unifi/firewall_zone_matrix.go | 9 + unifi/firewall_zone_policy.generated.go | 224 ++++++++++++++++++++++++ unifi/firewall_zone_policy.go | 44 +++++ 13 files changed, 703 insertions(+) create mode 100644 codegen/v2/FirewallZone.json create mode 100644 codegen/v2/FirewallZoneMatrix.json create mode 100644 codegen/v2/FirewallZonePolicy.json create mode 100644 unifi/firewall_zone.generated.go create mode 100644 unifi/firewall_zone.go create mode 100644 unifi/firewall_zone_matrix.generated.go create mode 100644 unifi/firewall_zone_matrix.go create mode 100644 unifi/firewall_zone_policy.generated.go create mode 100644 unifi/firewall_zone_policy.go diff --git a/README.md b/README.md index c47ffb4..1df3506 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,12 @@ user, err := c.CreateUser(ctx, "site-name", &unifi.User{ ## Plans +- [ ] Support Unifi Controller API V2 + - [x] AP Groups + - [x] DNS Records + - [x] Zone-based firewalls + - [ ] Traffic management + - [ ] other...? - [x] Increase API coverage, or modify code generation to rely on the official UniFi Controller API specifications - [x] Improve error handling (currently only basic error handling is implemented and error details are not propagated) - [x] Improve client code for better usability diff --git a/codegen/customizations.yml b/codegen/customizations.yml index 6d8aaae..6884c86 100644 --- a/codegen/customizations.yml +++ b/codegen/customizations.yml @@ -3,6 +3,7 @@ customizations: client: excludeResources: - "Dpi*" + - "FirewallZoneMatrix" functions: - name: "Login" comment: "Login logs in to the controller. Useful only for user/password authentication." @@ -285,6 +286,16 @@ customizations: type: "int" returns: - "error" + - name: "ListFirewallZoneMatrix" + resourceName: "FirewallZoneMatrix" + params: + - name: "ctx" + type: "context.Context" + - name: "site" + type: "string" + returns: + - "[]FirewallZoneMatrix" + - "error" resources: Account: fields: @@ -335,6 +346,15 @@ customizations: customUnmarshalType: "booleanishString" PortOverrides: omitEmpty: false + FirewallZone: + resourcePath: "firewall/zone" + fields: + NetworkIDs: + omitEmpty: false + FirewallZoneMatrix: + resourcePath: "firewall/zone-matrix" + FirewallZonePolicy: + resourcePath: "firewall-policies" Network: fields: InternetAccessEnabled: diff --git a/codegen/v2/FirewallZone.json b/codegen/v2/FirewallZone.json new file mode 100644 index 0000000..d6b0c6b --- /dev/null +++ b/codegen/v2/FirewallZone.json @@ -0,0 +1,4 @@ +{ + "name": "", + "network_ids": [""] +} \ No newline at end of file diff --git a/codegen/v2/FirewallZoneMatrix.json b/codegen/v2/FirewallZoneMatrix.json new file mode 100644 index 0000000..841450e --- /dev/null +++ b/codegen/v2/FirewallZoneMatrix.json @@ -0,0 +1,8 @@ +{ + "data": [{ + "action": "", + "policy_count": "\\d+" + }], + "name": "", + "zone_key": "" +} \ No newline at end of file diff --git a/codegen/v2/FirewallZonePolicy.json b/codegen/v2/FirewallZonePolicy.json new file mode 100644 index 0000000..8bd3e9b --- /dev/null +++ b/codegen/v2/FirewallZonePolicy.json @@ -0,0 +1,58 @@ +{ + "action": "ALLOW|BLOCK|REJECT", + "connection_state_type": "ALL|RESPOND_ONLY|CUSTOM", + "connection_states": ["ESTABLISHED|NEW|RELATED|INVALID"], + "enabled": "true|false", + "predefined": "true|false", + "name": "", + "description": "", + "destination": { + "app_category_ids": [""], + "app_ids": [""], + "ips": ["^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$|^$"], + "match_mac": "true|false", + "match_opposite_ips": "true|false", + "match_opposite_ports": "true|false", + "matching_target": "ANY|APP|APP_CATEGORY|IP|REGION|WEB", + "matching_target_type": "ANY|OBJECT|SPECIFIC", + "port": "^[0-9][0-9]?$|^", + "port_group_id": "", + "port_matching_type": "ANY|SPECIFIC|OBJECT", + "regions": [""], + "web_domains": [""], + "zone_id": "" + }, + "index": "^[0-9][0-9]?$|^", + "ip_version": "BOTH|IPV4|IPV6", + "logging": "true|false", + "match_ip_sec": "true|false", + "match_ip_sec_type": "MATCH_IP_SEC|MATCH_NON_IP_SEC", + "match_opposite_protocol": "true|false", + "protocol": "all|tcp_udp|tcp|udp|ah|dccp|eigrp|esp|gre|icmp|icmpv6|igmp|igp|ip|ipcomp|ipip|ipv6|isis|l2tp|manet|mobility-header|mpls-in-ip|number|ospf|pim|pup|rdp|rohc|rspf|rcvp|sctp|shim6|skip|st|vmtp|vrrp|wesp|xtp", + "schedule": { + "mode": "ALWAYS|EVERY_DAY|EVERY_WEEK|ONE_TIME_ONLY|CUSTOM", + "date": "^$|^(20[0-9]{2})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "date_end": "^$|^(20[0-9]{2})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "date_start": "^$|^(20[0-9]{2})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "repeat_on_days": ["mon|tue|wed|thu|fri|sat|sun"], + "time_range_end": "^[0-9][0-9]:[0-9][0-9]$", + "time_range_start": "^[0-9][0-9]:[0-9][0-9]$", + "time_all_day": "true|false" + }, + "source": { + "client_macs": ["^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$"], + "ips": ["^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$|^$"], + "mac": "^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$", + "match_mac": "true|false", + "match_opposite_ports": "true|false", + "match_opposite_ips": "true|false", + "match_opposite_networks": "true|false", + "matching_target": "ANY|CLIENT|NETWORK|IP|MAC", + "matching_target_type": "OBJECT|SPECIFIC", + "network_ids": [""], + "port": "^[0-9][0-9]?$|^", + "port_group_id": "", + "port_matching_type": "ANY|SPECIFIC|OBJECT", + "zone_id": "" + } +} \ No newline at end of file diff --git a/docs/usage_examples.md b/docs/usage_examples.md index a00cf7d..34e57d1 100644 --- a/docs/usage_examples.md +++ b/docs/usage_examples.md @@ -52,4 +52,46 @@ if err != nil { log.Fatalf("Error updating guest access setting: %v", err) } // Use the updated setting +``` + +## Create a Firewall Zone + +To create firewall zone: + +```go +fz, err := c.CreateFirewallZone(ctx, "default", &unifi.FirewallZone{ + Name: "my-zone", + NetworkIDs: []string{}, +}) +if err != nil { + fmt.Printf("Error: %v\n", err) +} else { + fmt.Printf("Firewall Zone created: %v\n", fz) +} +``` + +Then you can create a firewall zone policy (minimal example): + +```go +fzp, err := c.CreateFirewallZonePolicy(ctx, "default", &unifi.FirewallZonePolicy{ + Name: "my-zone-policy", + Action: "REJECT", + Enabled: true, + IPVersion: "BOTH", + Source: unifi.FirewallZonePolicySource{ + ZoneID: fz.ID, + }, + Destination: unifi.FirewallZonePolicyDestination{ + ZoneID: fz.ID, + }, + Schedule: unifi.FirewallZonePolicySchedule{ + Mode: "ALWAYS", + }, +}) +if err != nil { + fmt.Printf("Error: %v\n", err) + return +} else { + fmt.Printf("Firewall Zone Policy created: %v\n", fzp) +} ``` \ No newline at end of file diff --git a/unifi/client.generated.go b/unifi/client.generated.go index 20c2d10..741e718 100644 --- a/unifi/client.generated.go +++ b/unifi/client.generated.go @@ -255,6 +255,46 @@ type Client interface { // ==== end of client methods for FirewallRule resource ==== + // ==== client methods for FirewallZone resource ==== + + // CreateFirewallZone creates a resource + CreateFirewallZone(ctx context.Context, site string, f *FirewallZone) (*FirewallZone, error) + + // DeleteFirewallZone deletes a resource + DeleteFirewallZone(ctx context.Context, site string, id string) error + + // GetFirewallZone retrieves a resource + GetFirewallZone(ctx context.Context, site string, id string) (*FirewallZone, error) + + // ListFirewallZone lists the resources + ListFirewallZone(ctx context.Context, site string) ([]FirewallZone, error) + + // UpdateFirewallZone updates a resource + UpdateFirewallZone(ctx context.Context, site string, f *FirewallZone) (*FirewallZone, error) + + ListFirewallZoneMatrix(ctx context.Context, site string) ([]FirewallZoneMatrix, error) + + // ==== client methods for FirewallZonePolicy resource ==== + + // CreateFirewallZonePolicy creates a resource + CreateFirewallZonePolicy(ctx context.Context, site string, f *FirewallZonePolicy) (*FirewallZonePolicy, error) + + // DeleteFirewallZonePolicy deletes a resource + DeleteFirewallZonePolicy(ctx context.Context, site string, id string) error + + // GetFirewallZonePolicy retrieves a resource + GetFirewallZonePolicy(ctx context.Context, site string, id string) (*FirewallZonePolicy, error) + + // ListFirewallZonePolicy lists the resources + ListFirewallZonePolicy(ctx context.Context, site string) ([]FirewallZonePolicy, error) + + // UpdateFirewallZonePolicy updates a resource + UpdateFirewallZonePolicy(ctx context.Context, site string, f *FirewallZonePolicy) (*FirewallZonePolicy, error) + + // ==== end of client methods for FirewallZonePolicy resource ==== + + // ==== end of client methods for FirewallZone resource ==== + // ==== client methods for HeatMap resource ==== // CreateHeatMap creates a resource diff --git a/unifi/firewall_zone.generated.go b/unifi/firewall_zone.generated.go new file mode 100644 index 0000000..92001fd --- /dev/null +++ b/unifi/firewall_zone.generated.go @@ -0,0 +1,100 @@ +// Code generated from ace.jar fields *.json files +// DO NOT EDIT. + +package unifi + +import ( + "context" + "encoding/json" + "fmt" +) + +// just to fix compile issues with the import +var ( + _ context.Context + _ fmt.Formatter + _ json.Marshaler +) + +type FirewallZone struct { + ID string `json:"_id,omitempty"` + SiteID string `json:"site_id,omitempty"` + + Hidden bool `json:"attr_hidden,omitempty"` + HiddenID string `json:"attr_hidden_id,omitempty"` + NoDelete bool `json:"attr_no_delete,omitempty"` + NoEdit bool `json:"attr_no_edit,omitempty"` + + Name string `json:"name,omitempty"` + NetworkIDs []string `json:"network_ids"` +} + +func (dst *FirewallZone) UnmarshalJSON(b []byte) error { + type Alias FirewallZone + aux := &struct { + *Alias + }{ + Alias: (*Alias)(dst), + } + + err := json.Unmarshal(b, &aux) + if err != nil { + return fmt.Errorf("unable to unmarshal alias: %w", err) + } + + return nil +} + +func (c *client) listFirewallZone(ctx context.Context, site string) ([]FirewallZone, error) { + var respBody []FirewallZone + + err := c.Get(ctx, fmt.Sprintf("%s/site/%s/firewall/zone", c.apiPaths.ApiV2Path, site), nil, &respBody) + if err != nil { + return nil, err + } + + return respBody, nil +} + +func (c *client) getFirewallZone(ctx context.Context, site, id string) (*FirewallZone, error) { + var respBody FirewallZone + + err := c.Get(ctx, fmt.Sprintf("%s/site/%s/firewall/zone/%s", c.apiPaths.ApiV2Path, site, id), nil, &respBody) + + if err != nil { + return nil, err + } + if respBody.ID == "" { + return nil, ErrNotFound + } + return &respBody, nil +} + +func (c *client) deleteFirewallZone(ctx context.Context, site, id string) error { + err := c.Delete(ctx, fmt.Sprintf("%s/site/%s/firewall/zone/%s", c.apiPaths.ApiV2Path, site, id), struct{}{}, nil) + if err != nil { + return err + } + return nil +} + +func (c *client) createFirewallZone(ctx context.Context, site string, d *FirewallZone) (*FirewallZone, error) { + var respBody FirewallZone + + err := c.Post(ctx, fmt.Sprintf("%s/site/%s/firewall/zone", c.apiPaths.ApiV2Path, site), d, &respBody) + if err != nil { + return nil, err + } + + return &respBody, nil +} + +func (c *client) updateFirewallZone(ctx context.Context, site string, d *FirewallZone) (*FirewallZone, error) { + var respBody FirewallZone + + err := c.Put(ctx, fmt.Sprintf("%s/site/%s/firewall/zone/%s", c.apiPaths.ApiV2Path, site, d.ID), d, &respBody) + if err != nil { + return nil, err + } + return &respBody, nil +} diff --git a/unifi/firewall_zone.go b/unifi/firewall_zone.go new file mode 100644 index 0000000..b3972f6 --- /dev/null +++ b/unifi/firewall_zone.go @@ -0,0 +1,23 @@ +package unifi + +import "context" + +func (c *client) ListFirewallZone(ctx context.Context, site string) ([]FirewallZone, error) { + return c.listFirewallZone(ctx, site) +} + +func (c *client) GetFirewallZone(ctx context.Context, site, id string) (*FirewallZone, error) { + return c.getFirewallZone(ctx, site, id) +} + +func (c *client) DeleteFirewallZone(ctx context.Context, site, id string) error { + return c.deleteFirewallZone(ctx, site, id) +} + +func (c *client) CreateFirewallZone(ctx context.Context, site string, d *FirewallZone) (*FirewallZone, error) { + return c.createFirewallZone(ctx, site, d) +} + +func (c *client) UpdateFirewallZone(ctx context.Context, site string, d *FirewallZone) (*FirewallZone, error) { + return c.updateFirewallZone(ctx, site, d) +} diff --git a/unifi/firewall_zone_matrix.generated.go b/unifi/firewall_zone_matrix.generated.go new file mode 100644 index 0000000..8074ec3 --- /dev/null +++ b/unifi/firewall_zone_matrix.generated.go @@ -0,0 +1,125 @@ +// Code generated from ace.jar fields *.json files +// DO NOT EDIT. + +package unifi + +import ( + "context" + "encoding/json" + "fmt" +) + +// just to fix compile issues with the import +var ( + _ context.Context + _ fmt.Formatter + _ json.Marshaler +) + +type FirewallZoneMatrix struct { + ID string `json:"_id,omitempty"` + SiteID string `json:"site_id,omitempty"` + + Hidden bool `json:"attr_hidden,omitempty"` + HiddenID string `json:"attr_hidden_id,omitempty"` + NoDelete bool `json:"attr_no_delete,omitempty"` + NoEdit bool `json:"attr_no_edit,omitempty"` + + Data []FirewallZoneMatrixData `json:"data,omitempty"` + Name string `json:"name,omitempty"` + ZoneKey string `json:"zone_key,omitempty"` +} + +func (dst *FirewallZoneMatrix) UnmarshalJSON(b []byte) error { + type Alias FirewallZoneMatrix + aux := &struct { + *Alias + }{ + Alias: (*Alias)(dst), + } + + err := json.Unmarshal(b, &aux) + if err != nil { + return fmt.Errorf("unable to unmarshal alias: %w", err) + } + + return nil +} + +type FirewallZoneMatrixData struct { + Action string `json:"action,omitempty"` + PolicyCount int `json:"policy_count,omitempty"` +} + +func (dst *FirewallZoneMatrixData) UnmarshalJSON(b []byte) error { + type Alias FirewallZoneMatrixData + aux := &struct { + PolicyCount emptyStringInt `json:"policy_count"` + + *Alias + }{ + Alias: (*Alias)(dst), + } + + err := json.Unmarshal(b, &aux) + if err != nil { + return fmt.Errorf("unable to unmarshal alias: %w", err) + } + dst.PolicyCount = int(aux.PolicyCount) + + return nil +} + +func (c *client) listFirewallZoneMatrix(ctx context.Context, site string) ([]FirewallZoneMatrix, error) { + var respBody []FirewallZoneMatrix + + err := c.Get(ctx, fmt.Sprintf("%s/site/%s/firewall/zone-matrix", c.apiPaths.ApiV2Path, site), nil, &respBody) + if err != nil { + return nil, err + } + + return respBody, nil +} + +func (c *client) getFirewallZoneMatrix(ctx context.Context, site, id string) (*FirewallZoneMatrix, error) { + var respBody FirewallZoneMatrix + + err := c.Get(ctx, fmt.Sprintf("%s/site/%s/firewall/zone-matrix/%s", c.apiPaths.ApiV2Path, site, id), nil, &respBody) + + if err != nil { + return nil, err + } + if respBody.ID == "" { + return nil, ErrNotFound + } + return &respBody, nil +} + +func (c *client) deleteFirewallZoneMatrix(ctx context.Context, site, id string) error { + err := c.Delete(ctx, fmt.Sprintf("%s/site/%s/firewall/zone-matrix/%s", c.apiPaths.ApiV2Path, site, id), struct{}{}, nil) + if err != nil { + return err + } + return nil +} + +func (c *client) createFirewallZoneMatrix(ctx context.Context, site string, d *FirewallZoneMatrix) (*FirewallZoneMatrix, error) { + var respBody FirewallZoneMatrix + + err := c.Post(ctx, fmt.Sprintf("%s/site/%s/firewall/zone-matrix", c.apiPaths.ApiV2Path, site), d, &respBody) + if err != nil { + return nil, err + } + + return &respBody, nil +} + +func (c *client) updateFirewallZoneMatrix(ctx context.Context, site string, d *FirewallZoneMatrix) (*FirewallZoneMatrix, error) { + var respBody FirewallZoneMatrix + + err := c.Put(ctx, fmt.Sprintf("%s/site/%s/firewall/zone-matrix/%s", c.apiPaths.ApiV2Path, site, d.ID), d, &respBody) + if err != nil { + return nil, err + } + return &respBody, nil +} diff --git a/unifi/firewall_zone_matrix.go b/unifi/firewall_zone_matrix.go new file mode 100644 index 0000000..7471601 --- /dev/null +++ b/unifi/firewall_zone_matrix.go @@ -0,0 +1,9 @@ +package unifi + +import ( + "context" +) + +func (c *client) ListFirewallZoneMatrix(ctx context.Context, site string) ([]FirewallZoneMatrix, error) { + return c.listFirewallZoneMatrix(ctx, site) +} diff --git a/unifi/firewall_zone_policy.generated.go b/unifi/firewall_zone_policy.generated.go new file mode 100644 index 0000000..454f72a --- /dev/null +++ b/unifi/firewall_zone_policy.generated.go @@ -0,0 +1,224 @@ +// Code generated from ace.jar fields *.json files +// DO NOT EDIT. + +package unifi + +import ( + "context" + "encoding/json" + "fmt" +) + +// just to fix compile issues with the import +var ( + _ context.Context + _ fmt.Formatter + _ json.Marshaler +) + +type FirewallZonePolicy struct { + ID string `json:"_id,omitempty"` + SiteID string `json:"site_id,omitempty"` + + Hidden bool `json:"attr_hidden,omitempty"` + HiddenID string `json:"attr_hidden_id,omitempty"` + NoDelete bool `json:"attr_no_delete,omitempty"` + NoEdit bool `json:"attr_no_edit,omitempty"` + + Action string `json:"action,omitempty" validate:"omitempty,oneof=ALLOW BLOCK REJECT"` // ALLOW|BLOCK|REJECT + ConnectionStateType string `json:"connection_state_type,omitempty" validate:"omitempty,oneof=ALL RESPOND_ONLY CUSTOM"` // ALL|RESPOND_ONLY|CUSTOM + ConnectionStates []string `json:"connection_states,omitempty" validate:"omitempty,oneof=ESTABLISHED NEW RELATED INVALID"` // ESTABLISHED|NEW|RELATED|INVALID + Description string `json:"description,omitempty"` + Destination FirewallZonePolicyDestination `json:"destination,omitempty"` + Enabled bool `json:"enabled"` + IPVersion string `json:"ip_version,omitempty" validate:"omitempty,oneof=BOTH IPV4 IPV6"` // BOTH|IPV4|IPV6 + Index int `json:"index,omitempty"` // ^[0-9][0-9]?$|^ + Logging bool `json:"logging"` + MatchIPSec bool `json:"match_ip_sec"` + MatchIPSecType string `json:"match_ip_sec_type,omitempty" validate:"omitempty,oneof=MATCH_IP_SEC MATCH_NON_IP_SEC"` // MATCH_IP_SEC|MATCH_NON_IP_SEC + MatchOppositeProtocol bool `json:"match_opposite_protocol"` + Name string `json:"name,omitempty"` + Predefined bool `json:"predefined"` + Protocol string `json:"protocol,omitempty" validate:"omitempty,oneof=all tcp_udp tcp udp ah dccp eigrp esp gre icmp icmpv6 igmp igp ip ipcomp ipip ipv6 isis l2tp manet mobility-header mpls-in-ip number ospf pim pup rdp rohc rspf rcvp sctp shim6 skip st vmtp vrrp wesp xtp"` // all|tcp_udp|tcp|udp|ah|dccp|eigrp|esp|gre|icmp|icmpv6|igmp|igp|ip|ipcomp|ipip|ipv6|isis|l2tp|manet|mobility-header|mpls-in-ip|number|ospf|pim|pup|rdp|rohc|rspf|rcvp|sctp|shim6|skip|st|vmtp|vrrp|wesp|xtp + Schedule FirewallZonePolicySchedule `json:"schedule,omitempty"` + Source FirewallZonePolicySource `json:"source,omitempty"` +} + +func (dst *FirewallZonePolicy) UnmarshalJSON(b []byte) error { + type Alias FirewallZonePolicy + aux := &struct { + Index emptyStringInt `json:"index"` + + *Alias + }{ + Alias: (*Alias)(dst), + } + + err := json.Unmarshal(b, &aux) + if err != nil { + return fmt.Errorf("unable to unmarshal alias: %w", err) + } + dst.Index = int(aux.Index) + + return nil +} + +type FirewallZonePolicyDestination struct { + AppCategoryIDs []string `json:"app_category_ids,omitempty"` + AppIDs []string `json:"app_ids,omitempty"` + IPs []string `json:"ips,omitempty" validate:"omitempty,ipv4"` // ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$|^$ + MatchMAC bool `json:"match_mac"` + MatchOppositeIPs bool `json:"match_opposite_ips"` + MatchOppositePorts bool `json:"match_opposite_ports"` + MatchingTarget string `json:"matching_target,omitempty" validate:"omitempty,oneof=ANY APP APP_CATEGORY IP REGION WEB"` // ANY|APP|APP_CATEGORY|IP|REGION|WEB + MatchingTargetType string `json:"matching_target_type,omitempty" validate:"omitempty,oneof=ANY OBJECT SPECIFIC"` // ANY|OBJECT|SPECIFIC + Port int `json:"port,omitempty"` // ^[0-9][0-9]?$|^ + PortGroupID string `json:"port_group_id"` + PortMatchingType string `json:"port_matching_type,omitempty" validate:"omitempty,oneof=ANY SPECIFIC OBJECT"` // ANY|SPECIFIC|OBJECT + Regions []string `json:"regions,omitempty"` + WebDomains []string `json:"web_domains,omitempty"` + ZoneID string `json:"zone_id"` +} + +func (dst *FirewallZonePolicyDestination) UnmarshalJSON(b []byte) error { + type Alias FirewallZonePolicyDestination + aux := &struct { + Port emptyStringInt `json:"port"` + + *Alias + }{ + Alias: (*Alias)(dst), + } + + err := json.Unmarshal(b, &aux) + if err != nil { + return fmt.Errorf("unable to unmarshal alias: %w", err) + } + dst.Port = int(aux.Port) + + return nil +} + +type FirewallZonePolicySchedule struct { + Date int `json:"date,omitempty"` // ^$|^(20[0-9]{2})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$ + DateEnd int `json:"date_end,omitempty"` // ^$|^(20[0-9]{2})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$ + DateStart int `json:"date_start,omitempty"` // ^$|^(20[0-9]{2})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$ + Mode string `json:"mode,omitempty" validate:"omitempty,oneof=ALWAYS EVERY_DAY EVERY_WEEK ONE_TIME_ONLY CUSTOM"` // ALWAYS|EVERY_DAY|EVERY_WEEK|ONE_TIME_ONLY|CUSTOM + RepeatOnDays []string `json:"repeat_on_days,omitempty" validate:"omitempty,oneof=mon tue wed thu fri sat sun"` // mon|tue|wed|thu|fri|sat|sun + TimeAllDay bool `json:"time_all_day"` + TimeRangeEnd string `json:"time_range_end,omitempty"` // ^[0-9][0-9]:[0-9][0-9]$ + TimeRangeStart string `json:"time_range_start,omitempty"` // ^[0-9][0-9]:[0-9][0-9]$ +} + +func (dst *FirewallZonePolicySchedule) UnmarshalJSON(b []byte) error { + type Alias FirewallZonePolicySchedule + aux := &struct { + Date emptyStringInt `json:"date"` + DateEnd emptyStringInt `json:"date_end"` + DateStart emptyStringInt `json:"date_start"` + + *Alias + }{ + Alias: (*Alias)(dst), + } + + err := json.Unmarshal(b, &aux) + if err != nil { + return fmt.Errorf("unable to unmarshal alias: %w", err) + } + dst.Date = int(aux.Date) + dst.DateEnd = int(aux.DateEnd) + dst.DateStart = int(aux.DateStart) + + return nil +} + +type FirewallZonePolicySource struct { + ClientMACs []string `json:"client_macs,omitempty" validate:"omitempty,mac"` // ^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$ + IPs []string `json:"ips,omitempty" validate:"omitempty,ipv4"` // ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$|^$ + MAC string `json:"mac,omitempty" validate:"omitempty,mac"` // ^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$ + MatchMAC bool `json:"match_mac"` + MatchOppositeIPs bool `json:"match_opposite_ips"` + MatchOppositeNetworks bool `json:"match_opposite_networks"` + MatchOppositePorts bool `json:"match_opposite_ports"` + MatchingTarget string `json:"matching_target,omitempty" validate:"omitempty,oneof=ANY CLIENT NETWORK IP MAC"` // ANY|CLIENT|NETWORK|IP|MAC + MatchingTargetType string `json:"matching_target_type,omitempty" validate:"omitempty,oneof=OBJECT SPECIFIC"` // OBJECT|SPECIFIC + NetworkIDs []string `json:"network_ids,omitempty"` + Port int `json:"port,omitempty"` // ^[0-9][0-9]?$|^ + PortGroupID string `json:"port_group_id"` + PortMatchingType string `json:"port_matching_type,omitempty" validate:"omitempty,oneof=ANY SPECIFIC OBJECT"` // ANY|SPECIFIC|OBJECT + ZoneID string `json:"zone_id"` +} + +func (dst *FirewallZonePolicySource) UnmarshalJSON(b []byte) error { + type Alias FirewallZonePolicySource + aux := &struct { + Port emptyStringInt `json:"port"` + + *Alias + }{ + Alias: (*Alias)(dst), + } + + err := json.Unmarshal(b, &aux) + if err != nil { + return fmt.Errorf("unable to unmarshal alias: %w", err) + } + dst.Port = int(aux.Port) + + return nil +} + +func (c *client) listFirewallZonePolicy(ctx context.Context, site string) ([]FirewallZonePolicy, error) { + var respBody []FirewallZonePolicy + + err := c.Get(ctx, fmt.Sprintf("%s/site/%s/firewall-policies", c.apiPaths.ApiV2Path, site), nil, &respBody) + if err != nil { + return nil, err + } + + return respBody, nil +} + +func (c *client) getFirewallZonePolicy(ctx context.Context, site, id string) (*FirewallZonePolicy, error) { + var respBody FirewallZonePolicy + + err := c.Get(ctx, fmt.Sprintf("%s/site/%s/firewall-policies/%s", c.apiPaths.ApiV2Path, site, id), nil, &respBody) + + if err != nil { + return nil, err + } + if respBody.ID == "" { + return nil, ErrNotFound + } + return &respBody, nil +} + +func (c *client) deleteFirewallZonePolicy(ctx context.Context, site, id string) error { + err := c.Delete(ctx, fmt.Sprintf("%s/site/%s/firewall-policies/%s", c.apiPaths.ApiV2Path, site, id), struct{}{}, nil) + if err != nil { + return err + } + return nil +} + +func (c *client) createFirewallZonePolicy(ctx context.Context, site string, d *FirewallZonePolicy) (*FirewallZonePolicy, error) { + var respBody FirewallZonePolicy + + err := c.Post(ctx, fmt.Sprintf("%s/site/%s/firewall-policies", c.apiPaths.ApiV2Path, site), d, &respBody) + if err != nil { + return nil, err + } + + return &respBody, nil +} + +func (c *client) updateFirewallZonePolicy(ctx context.Context, site string, d *FirewallZonePolicy) (*FirewallZonePolicy, error) { + var respBody FirewallZonePolicy + + err := c.Put(ctx, fmt.Sprintf("%s/site/%s/firewall-policies/%s", c.apiPaths.ApiV2Path, site, d.ID), d, &respBody) + if err != nil { + return nil, err + } + return &respBody, nil +} diff --git a/unifi/firewall_zone_policy.go b/unifi/firewall_zone_policy.go new file mode 100644 index 0000000..e75baa0 --- /dev/null +++ b/unifi/firewall_zone_policy.go @@ -0,0 +1,44 @@ +package unifi + +import ( + "context" + "fmt" +) + +type FirewallPolicyOrderUpdate struct { + DestinationZoneId string `json:"destination_zone_id"` + SourceZoneId string `json:"source_zone_id"` + AfterPredefinedIds []string `json:"after_predefined_ids"` + BeforePredefinedIds []string `json:"before_predefined_ids"` +} + +func (c *client) ListFirewallZonePolicy(ctx context.Context, site string) ([]FirewallZonePolicy, error) { + return c.listFirewallZonePolicy(ctx, site) +} + +func (c *client) GetFirewallZonePolicy(ctx context.Context, site, id string) (*FirewallZonePolicy, error) { + return c.getFirewallZonePolicy(ctx, site, id) +} + +func (c *client) DeleteFirewallZonePolicy(ctx context.Context, site, id string) error { + return c.deleteFirewallZonePolicy(ctx, site, id) +} + +func (c *client) CreateFirewallZonePolicy(ctx context.Context, site string, d *FirewallZonePolicy) (*FirewallZonePolicy, error) { + return c.createFirewallZonePolicy(ctx, site, d) +} + +func (c *client) UpdateFirewallZonePolicy(ctx context.Context, site string, d *FirewallZonePolicy) (*FirewallZonePolicy, error) { + return c.updateFirewallZonePolicy(ctx, site, d) +} + +func (c *client) ReorderFirewallPolicies(ctx context.Context, site string, d *FirewallPolicyOrderUpdate) ([]FirewallZonePolicy, error) { + var res []FirewallZonePolicy + err := c.Put(ctx, fmt.Sprintf("%s/site/%s/firewall-policies/batch-reorder", c.apiPaths.ApiV2Path, site), d, res) + if err != nil { + return nil, err + } + + // TODO raise error if returned length is not equal to the length of the reordered policies? + return res, nil +}