feat: implements apis for managing headscale policy (#1792)
This commit is contained in:
parent
00ff288f0c
commit
58bd38a609
39 changed files with 1875 additions and 567 deletions
|
@ -7,18 +7,17 @@ import (
|
|||
"io"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/tailscale/hujson"
|
||||
"go4.org/netipx"
|
||||
"gopkg.in/yaml.v3"
|
||||
"tailscale.com/tailcfg"
|
||||
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -108,35 +107,22 @@ func LoadACLPolicyFromPath(path string) (*ACLPolicy, error) {
|
|||
Bytes("file", policyBytes).
|
||||
Msg("Loading ACLs")
|
||||
|
||||
switch filepath.Ext(path) {
|
||||
case ".yml", ".yaml":
|
||||
return LoadACLPolicyFromBytes(policyBytes, "yaml")
|
||||
}
|
||||
|
||||
return LoadACLPolicyFromBytes(policyBytes, "hujson")
|
||||
return LoadACLPolicyFromBytes(policyBytes)
|
||||
}
|
||||
|
||||
func LoadACLPolicyFromBytes(acl []byte, format string) (*ACLPolicy, error) {
|
||||
func LoadACLPolicyFromBytes(acl []byte) (*ACLPolicy, error) {
|
||||
var policy ACLPolicy
|
||||
switch format {
|
||||
case "yaml":
|
||||
err := yaml.Unmarshal(acl, &policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
default:
|
||||
ast, err := hujson.Parse(acl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ast, err := hujson.Parse(acl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing hujson, err: %w", err)
|
||||
}
|
||||
|
||||
ast.Standardize()
|
||||
acl = ast.Pack()
|
||||
err = json.Unmarshal(acl, &policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ast.Standardize()
|
||||
acl = ast.Pack()
|
||||
|
||||
if err := json.Unmarshal(acl, &policy); err != nil {
|
||||
return nil, fmt.Errorf("unmarshalling policy, err: %w", err)
|
||||
}
|
||||
|
||||
if policy.IsZero() {
|
||||
|
@ -846,7 +832,7 @@ func (pol *ACLPolicy) expandIPsFromUser(
|
|||
|
||||
// shortcurcuit if we have no nodes to get ips from.
|
||||
if len(filteredNodes) == 0 {
|
||||
return nil, nil //nolint
|
||||
return nil, nil // nolint
|
||||
}
|
||||
|
||||
for _, node := range filteredNodes {
|
||||
|
|
|
@ -6,14 +6,15 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go4.org/netipx"
|
||||
"gopkg.in/check.v1"
|
||||
"tailscale.com/tailcfg"
|
||||
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
)
|
||||
|
||||
var iap = func(ipStr string) *netip.Addr {
|
||||
|
@ -321,44 +322,27 @@ func TestParsing(t *testing.T) {
|
|||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "port-wildcard-yaml",
|
||||
format: "yaml",
|
||||
name: "ipv6",
|
||||
format: "hujson",
|
||||
acl: `
|
||||
---
|
||||
hosts:
|
||||
host-1: 100.100.100.100/32
|
||||
subnet-1: 100.100.101.100/24
|
||||
acls:
|
||||
- action: accept
|
||||
src:
|
||||
- "*"
|
||||
dst:
|
||||
- host-1:*
|
||||
`,
|
||||
want: []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"0.0.0.0/0", "::/0"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
"hosts": {
|
||||
"host-1": "100.100.100.100/32",
|
||||
"subnet-1": "100.100.101.100/24",
|
||||
},
|
||||
|
||||
"acls": [
|
||||
{
|
||||
name: "ipv6-yaml",
|
||||
format: "yaml",
|
||||
acl: `
|
||||
---
|
||||
hosts:
|
||||
host-1: 100.100.100.100/32
|
||||
subnet-1: 100.100.101.100/24
|
||||
acls:
|
||||
- action: accept
|
||||
src:
|
||||
- "*"
|
||||
dst:
|
||||
- host-1:*
|
||||
"action": "accept",
|
||||
"src": [
|
||||
"*",
|
||||
],
|
||||
"dst": [
|
||||
"host-1:*",
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
`,
|
||||
want: []tailcfg.FilterRule{
|
||||
{
|
||||
|
@ -374,7 +358,7 @@ acls:
|
|||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pol, err := LoadACLPolicyFromBytes([]byte(tt.acl), tt.format)
|
||||
pol, err := LoadACLPolicyFromBytes([]byte(tt.acl))
|
||||
|
||||
if tt.wantErr && err == nil {
|
||||
t.Errorf("parsing() error = %v, wantErr %v", err, tt.wantErr)
|
||||
|
@ -544,7 +528,7 @@ func (s *Suite) TestRuleInvalidGeneration(c *check.C) {
|
|||
],
|
||||
}
|
||||
`)
|
||||
pol, err := LoadACLPolicyFromBytes(acl, "hujson")
|
||||
pol, err := LoadACLPolicyFromBytes(acl)
|
||||
c.Assert(pol.ACLs, check.HasLen, 6)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
|
|
|
@ -6,26 +6,25 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/tailscale/hujson"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ACLPolicy represents a Tailscale ACL Policy.
|
||||
type ACLPolicy struct {
|
||||
Groups Groups `json:"groups" yaml:"groups"`
|
||||
Hosts Hosts `json:"hosts" yaml:"hosts"`
|
||||
TagOwners TagOwners `json:"tagOwners" yaml:"tagOwners"`
|
||||
ACLs []ACL `json:"acls" yaml:"acls"`
|
||||
Tests []ACLTest `json:"tests" yaml:"tests"`
|
||||
AutoApprovers AutoApprovers `json:"autoApprovers" yaml:"autoApprovers"`
|
||||
SSHs []SSH `json:"ssh" yaml:"ssh"`
|
||||
Groups Groups `json:"groups" `
|
||||
Hosts Hosts `json:"hosts"`
|
||||
TagOwners TagOwners `json:"tagOwners"`
|
||||
ACLs []ACL `json:"acls"`
|
||||
Tests []ACLTest `json:"tests"`
|
||||
AutoApprovers AutoApprovers `json:"autoApprovers"`
|
||||
SSHs []SSH `json:"ssh"`
|
||||
}
|
||||
|
||||
// ACL is a basic rule for the ACL Policy.
|
||||
type ACL struct {
|
||||
Action string `json:"action" yaml:"action"`
|
||||
Protocol string `json:"proto" yaml:"proto"`
|
||||
Sources []string `json:"src" yaml:"src"`
|
||||
Destinations []string `json:"dst" yaml:"dst"`
|
||||
Action string `json:"action"`
|
||||
Protocol string `json:"proto"`
|
||||
Sources []string `json:"src"`
|
||||
Destinations []string `json:"dst"`
|
||||
}
|
||||
|
||||
// Groups references a series of alias in the ACL rules.
|
||||
|
@ -37,27 +36,27 @@ type Hosts map[string]netip.Prefix
|
|||
// TagOwners specify what users (users?) are allow to use certain tags.
|
||||
type TagOwners map[string][]string
|
||||
|
||||
// ACLTest is not implemented, but should be use to check if a certain rule is allowed.
|
||||
// ACLTest is not implemented, but should be used to check if a certain rule is allowed.
|
||||
type ACLTest struct {
|
||||
Source string `json:"src" yaml:"src"`
|
||||
Accept []string `json:"accept" yaml:"accept"`
|
||||
Deny []string `json:"deny,omitempty" yaml:"deny,omitempty"`
|
||||
Source string `json:"src"`
|
||||
Accept []string `json:"accept"`
|
||||
Deny []string `json:"deny,omitempty"`
|
||||
}
|
||||
|
||||
// AutoApprovers specify which users (users?), groups or tags have their advertised routes
|
||||
// or exit node status automatically enabled.
|
||||
type AutoApprovers struct {
|
||||
Routes map[string][]string `json:"routes" yaml:"routes"`
|
||||
ExitNode []string `json:"exitNode" yaml:"exitNode"`
|
||||
Routes map[string][]string `json:"routes"`
|
||||
ExitNode []string `json:"exitNode"`
|
||||
}
|
||||
|
||||
// SSH controls who can ssh into which machines.
|
||||
type SSH struct {
|
||||
Action string `json:"action" yaml:"action"`
|
||||
Sources []string `json:"src" yaml:"src"`
|
||||
Destinations []string `json:"dst" yaml:"dst"`
|
||||
Users []string `json:"users" yaml:"users"`
|
||||
CheckPeriod string `json:"checkPeriod,omitempty" yaml:"checkPeriod,omitempty"`
|
||||
Action string `json:"action"`
|
||||
Sources []string `json:"src"`
|
||||
Destinations []string `json:"dst"`
|
||||
Users []string `json:"users"`
|
||||
CheckPeriod string `json:"checkPeriod,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON allows to parse the Hosts directly into netip objects.
|
||||
|
@ -89,27 +88,6 @@ func (hosts *Hosts) UnmarshalJSON(data []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML allows to parse the Hosts directly into netip objects.
|
||||
func (hosts *Hosts) UnmarshalYAML(data []byte) error {
|
||||
newHosts := Hosts{}
|
||||
hostIPPrefixMap := make(map[string]string)
|
||||
|
||||
err := yaml.Unmarshal(data, &hostIPPrefixMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for host, prefixStr := range hostIPPrefixMap {
|
||||
prefix, err := netip.ParsePrefix(prefixStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newHosts[host] = prefix
|
||||
}
|
||||
*hosts = newHosts
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsZero is perhaps a bit naive here.
|
||||
func (pol ACLPolicy) IsZero() bool {
|
||||
if len(pol.Groups) == 0 && len(pol.Hosts) == 0 && len(pol.ACLs) == 0 {
|
||||
|
@ -119,7 +97,7 @@ func (pol ACLPolicy) IsZero() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// Returns the list of autoApproving users, groups or tags for a given IPPrefix.
|
||||
// GetRouteApprovers returns the list of autoApproving users, groups or tags for a given IPPrefix.
|
||||
func (autoApprovers *AutoApprovers) GetRouteApprovers(
|
||||
prefix netip.Prefix,
|
||||
) ([]string, error) {
|
||||
|
@ -127,7 +105,7 @@ func (autoApprovers *AutoApprovers) GetRouteApprovers(
|
|||
return autoApprovers.ExitNode, nil // 0.0.0.0/0, ::/0 or equivalent
|
||||
}
|
||||
|
||||
approverAliases := []string{}
|
||||
approverAliases := make([]string, 0)
|
||||
|
||||
for autoApprovedPrefix, autoApproverAliases := range autoApprovers.Routes {
|
||||
autoApprovedPrefix, err := netip.ParsePrefix(autoApprovedPrefix)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue