Skip to content

Commit

Permalink
Initial work on ACLs
Browse files Browse the repository at this point in the history
  • Loading branch information
juanfont committed Jul 3, 2021
1 parent 95fee5a commit b161a92
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 0 deletions.
30 changes: 30 additions & 0 deletions acls.go
Original file line number Diff line number Diff line change
@@ -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
}
33 changes: 33 additions & 0 deletions acls_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
59 changes: 59 additions & 0 deletions acls_types.go
Original file line number Diff line number Diff line change
@@ -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
}
125 changes: 125 additions & 0 deletions tests/acls/acl_policy_1.hujson
Original file line number Diff line number Diff line change
@@ -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"
],
},
],
}
1 change: 1 addition & 0 deletions tests/acls/broken.hujson
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{
4 changes: 4 additions & 0 deletions tests/acls/invalid.hujson
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"valid_json": true,
"but_a_policy_though": false
}

0 comments on commit b161a92

Please sign in to comment.