From b161a92e5806d9ed4e35b585c5985d3ddfdfdfd6 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sat, 3 Jul 2021 11:55:32 +0200 Subject: [PATCH] Initial work on ACLs --- acls.go | 30 ++++++++ acls_test.go | 33 +++++++++ acls_types.go | 59 ++++++++++++++++ tests/acls/acl_policy_1.hujson | 125 +++++++++++++++++++++++++++++++++ tests/acls/broken.hujson | 1 + tests/acls/invalid.hujson | 4 ++ 6 files changed, 252 insertions(+) create mode 100644 acls.go create mode 100644 acls_test.go create mode 100644 acls_types.go create mode 100644 tests/acls/acl_policy_1.hujson create mode 100644 tests/acls/broken.hujson create mode 100644 tests/acls/invalid.hujson diff --git a/acls.go b/acls.go new file mode 100644 index 00000000..d698e269 --- /dev/null +++ b/acls.go @@ -0,0 +1,30 @@ +package headscale + +import ( + "io" + "os" + + "github.com/tailscale/hujson" +) + +const errorInvalidPolicy = Error("invalid policy") + +func (h *Headscale) ParsePolicy(path string) (*ACLPolicy, error) { + policyFile, err := os.Open(path) + if err != nil { + return nil, err + } + defer policyFile.Close() + + var policy ACLPolicy + b, err := io.ReadAll(policyFile) + if err != nil { + return nil, err + } + err = hujson.Unmarshal(b, &policy) + if policy.IsZero() { + return nil, errorInvalidPolicy + } + + return &policy, err +} diff --git a/acls_test.go b/acls_test.go new file mode 100644 index 00000000..6b01242c --- /dev/null +++ b/acls_test.go @@ -0,0 +1,33 @@ +package headscale + +import ( + "gopkg.in/check.v1" +) + +func (s *Suite) TestWrongPath(c *check.C) { + _, err := h.ParsePolicy("asdfg") + c.Assert(err, check.NotNil) +} + +func (s *Suite) TestBrokenHuJson(c *check.C) { + _, err := h.ParsePolicy("./tests/acls/broken.hujson") + c.Assert(err, check.NotNil) + +} + +func (s *Suite) TestInvalidPolicyHuson(c *check.C) { + _, err := h.ParsePolicy("./tests/acls/invalid.hujson") + c.Assert(err, check.NotNil) + c.Assert(err, check.Equals, errorInvalidPolicy) +} + +func (s *Suite) TestValidCheckHosts(c *check.C) { + p, err := h.ParsePolicy("./tests/acls/acl_policy_1.hujson") + c.Assert(err, check.IsNil) + c.Assert(p, check.NotNil) + c.Assert(p.IsZero(), check.Equals, false) + + hosts, err := p.GetHosts() + c.Assert(err, check.IsNil) + c.Assert(*hosts, check.HasLen, 2) +} diff --git a/acls_types.go b/acls_types.go new file mode 100644 index 00000000..08b383cc --- /dev/null +++ b/acls_types.go @@ -0,0 +1,59 @@ +package headscale + +import ( + "strings" + + "inet.af/netaddr" +) + +type ACLPolicy struct { + Groups Groups `json:"Groups"` + Hosts Hosts `json:"Hosts"` + TagOwners TagOwners `json:"TagOwners"` + ACLs []ACL `json:"ACLs"` + Tests []ACLTest `json:"Tests"` +} + +type ACL struct { + Action string `json:"Action"` + Users []string `json:"Users"` + Ports []string `json:"Ports"` +} + +type Groups map[string][]string + +type Hosts map[string]string + +type TagOwners struct { + TagMontrealWebserver []string `json:"tag:montreal-webserver"` + TagAPIServer []string `json:"tag:api-server"` +} + +type ACLTest struct { + User string `json:"User"` + Allow []string `json:"Allow"` + Deny []string `json:"Deny,omitempty"` +} + +// IsZero is perhaps a bit naive here +func (p ACLPolicy) IsZero() bool { + if len(p.Groups) == 0 && len(p.Hosts) == 0 && len(p.ACLs) == 0 { + return true + } + return false +} + +func (p ACLPolicy) GetHosts() (*map[string]netaddr.IPPrefix, error) { + hosts := make(map[string]netaddr.IPPrefix) + for k, v := range p.Hosts { + if !strings.Contains(v, "/") { + v = v + "/32" + } + prefix, err := netaddr.ParseIPPrefix(v) + if err != nil { + return nil, err + } + hosts[k] = prefix + } + return &hosts, nil +} diff --git a/tests/acls/acl_policy_1.hujson b/tests/acls/acl_policy_1.hujson new file mode 100644 index 00000000..3e12bf48 --- /dev/null +++ b/tests/acls/acl_policy_1.hujson @@ -0,0 +1,125 @@ +{ + // Declare static groups of users beyond those in the identity service. + "Groups": { + "group:example": [ + "user1@example.com", + "user2@example.com", + ], + }, + // Declare hostname aliases to use in place of IP addresses or subnets. + "Hosts": { + "example-host-1": "100.100.100.100", + "example-host-2": "100.100.101.100/24", + }, + // Define who is allowed to use which tags. + "TagOwners": { + // Everyone in the montreal-admins or global-admins group are + // allowed to tag servers as montreal-webserver. + "tag:montreal-webserver": [ + "group:montreal-admins", + "group:global-admins", + ], + // Only a few admins are allowed to create API servers. + "tag:api-server": [ + "group:global-admins", + "president@example.com", + ], + }, + // Access control lists. + "ACLs": [ + // Engineering users, plus the president, can access port 22 (ssh) + // and port 3389 (remote desktop protocol) on all servers, and all + // ports on git-server or ci-server. + { + "Action": "accept", + "Users": [ + "group:engineering", + "president@example.com" + ], + "Ports": [ + "*:22,3389", + "git-server:*", + "ci-server:*" + ], + }, + // Allow engineer users to access any port on a device tagged with + // tag:production. + { + "Action": "accept", + "Users": [ + "group:engineers" + ], + "Ports": [ + "tag:production:*" + ], + }, + // Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts + // on both networks. + { + "Action": "accept", + "Users": [ + "my-subnet", + "192.168.1.0/24" + ], + "Ports": [ + "my-subnet:*", + "192.168.1.0/24:*" + ], + }, + // Allow every user of your network to access anything on the network. + // Comment out this section if you want to define specific ACL + // restrictions above. + { + "Action": "accept", + "Users": [ + "*" + ], + "Ports": [ + "*:*" + ], + }, + // All users in Montreal are allowed to access the Montreal web + // servers. + { + "Action": "accept", + "Users": [ + "group:montreal-users" + ], + "Ports": [ + "tag:montreal-webserver:80,443" + ], + }, + // Montreal web servers are allowed to make outgoing connections to + // the API servers, but only on https port 443. + // In contrast, this doesn't grant API servers the right to initiate + // any connections. + { + "Action": "accept", + "Users": [ + "tag:montreal-webserver" + ], + "Ports": [ + "tag:api-server:443" + ], + }, + ], + // Declare tests to check functionality of ACL rules + "Tests": [ + { + "User": "user1@example.com", + "Allow": [ + "example-host-1:22", + "example-host-2:80" + ], + "Deny": [ + "exapmle-host-2:100" + ], + }, + { + "User": "user2@example.com", + "Allow": [ + "100.60.3.4:22" + ], + }, + ], +} \ No newline at end of file diff --git a/tests/acls/broken.hujson b/tests/acls/broken.hujson new file mode 100644 index 00000000..98232c64 --- /dev/null +++ b/tests/acls/broken.hujson @@ -0,0 +1 @@ +{ diff --git a/tests/acls/invalid.hujson b/tests/acls/invalid.hujson new file mode 100644 index 00000000..733f6924 --- /dev/null +++ b/tests/acls/invalid.hujson @@ -0,0 +1,4 @@ +{ + "valid_json": true, + "but_a_policy_though": false +} \ No newline at end of file