diff --git a/acls.go b/acls.go new file mode 100644 index 0000000..f4ed4c0 --- /dev/null +++ b/acls.go @@ -0,0 +1,263 @@ +package headscale + +import ( + "encoding/json" + "fmt" + "io" + "log" + "os" + "strconv" + "strings" + + "github.com/tailscale/hujson" + "inet.af/netaddr" + "tailscale.com/tailcfg" +) + +const errorEmptyPolicy = Error("empty policy") +const errorInvalidAction = Error("invalid action") +const errorInvalidUserSection = Error("invalid user section") +const errorInvalidGroup = Error("invalid group") +const errorInvalidTag = Error("invalid tag") +const errorInvalidNamespace = Error("invalid namespace") +const errorInvalidPortFormat = Error("invalid port format") + +// LoadACLPolicy loads the ACL policy from the specify path, and generates the ACL rules +func (h *Headscale) LoadACLPolicy(path string) error { + policyFile, err := os.Open(path) + if err != nil { + return err + } + defer policyFile.Close() + + var policy ACLPolicy + b, err := io.ReadAll(policyFile) + if err != nil { + return err + } + err = hujson.Unmarshal(b, &policy) + if err != nil { + return err + } + if policy.IsZero() { + return errorEmptyPolicy + } + + h.aclPolicy = &policy + rules, err := h.generateACLRules() + if err != nil { + return err + } + h.aclRules = rules + return nil +} + +func (h *Headscale) generateACLRules() (*[]tailcfg.FilterRule, error) { + rules := []tailcfg.FilterRule{} + + for i, a := range h.aclPolicy.ACLs { + if a.Action != "accept" { + return nil, errorInvalidAction + } + + r := tailcfg.FilterRule{} + + srcIPs := []string{} + for j, u := range a.Users { + srcs, err := h.generateACLPolicySrcIP(u) + if err != nil { + log.Printf("Error parsing ACL %d, User %d", i, j) + return nil, err + } + srcIPs = append(srcIPs, *srcs...) + } + r.SrcIPs = srcIPs + + destPorts := []tailcfg.NetPortRange{} + for j, d := range a.Ports { + dests, err := h.generateACLPolicyDestPorts(d) + if err != nil { + log.Printf("Error parsing ACL %d, Port %d", i, j) + return nil, err + } + destPorts = append(destPorts, *dests...) + } + + rules = append(rules, tailcfg.FilterRule{ + SrcIPs: srcIPs, + DstPorts: destPorts, + }) + } + + return &rules, nil +} + +func (h *Headscale) generateACLPolicySrcIP(u string) (*[]string, error) { + return h.expandAlias(u) +} + +func (h *Headscale) generateACLPolicyDestPorts(d string) (*[]tailcfg.NetPortRange, error) { + tokens := strings.Split(d, ":") + if len(tokens) < 2 || len(tokens) > 3 { + return nil, errorInvalidPortFormat + } + + var alias string + // We can have here stuff like: + // git-server:* + // 192.168.1.0/24:22 + // tag:montreal-webserver:80,443 + // tag:api-server:443 + // example-host-1:* + if len(tokens) == 2 { + alias = tokens[0] + } else { + alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1]) + } + + expanded, err := h.expandAlias(alias) + if err != nil { + return nil, err + } + ports, err := h.expandPorts(tokens[len(tokens)-1]) + if err != nil { + return nil, err + } + + dests := []tailcfg.NetPortRange{} + for _, d := range *expanded { + for _, p := range *ports { + pr := tailcfg.NetPortRange{ + IP: d, + Ports: p, + } + dests = append(dests, pr) + } + } + return &dests, nil +} + +func (h *Headscale) expandAlias(s string) (*[]string, error) { + if s == "*" { + return &[]string{"*"}, nil + } + + if strings.HasPrefix(s, "group:") { + if _, ok := h.aclPolicy.Groups[s]; !ok { + return nil, errorInvalidGroup + } + ips := []string{} + for _, n := range h.aclPolicy.Groups[s] { + nodes, err := h.ListMachinesInNamespace(n) + if err != nil { + return nil, errorInvalidNamespace + } + for _, node := range *nodes { + ips = append(ips, node.IPAddress) + } + } + return &ips, nil + } + + if strings.HasPrefix(s, "tag:") { + if _, ok := h.aclPolicy.TagOwners[s]; !ok { + return nil, errorInvalidTag + } + + // This will have HORRIBLE performance. + // We need to change the data model to better store tags + machines := []Machine{} + if err := h.db.Where("registered").Find(&machines).Error; err != nil { + return nil, err + } + ips := []string{} + for _, m := range machines { + hostinfo := tailcfg.Hostinfo{} + if len(m.HostInfo) != 0 { + hi, err := m.HostInfo.MarshalJSON() + if err != nil { + return nil, err + } + err = json.Unmarshal(hi, &hostinfo) + if err != nil { + return nil, err + } + + // FIXME: Check TagOwners allows this + for _, t := range hostinfo.RequestTags { + if s[4:] == t { + ips = append(ips, m.IPAddress) + break + } + } + } + } + return &ips, nil + } + + n, err := h.GetNamespace(s) + if err == nil { + nodes, err := h.ListMachinesInNamespace(n.Name) + if err != nil { + return nil, err + } + ips := []string{} + for _, n := range *nodes { + ips = append(ips, n.IPAddress) + } + return &ips, nil + } + + if h, ok := h.aclPolicy.Hosts[s]; ok { + return &[]string{h.String()}, nil + } + + ip, err := netaddr.ParseIP(s) + if err == nil { + return &[]string{ip.String()}, nil + } + + cidr, err := netaddr.ParseIPPrefix(s) + if err == nil { + return &[]string{cidr.String()}, nil + } + + return nil, errorInvalidUserSection +} + +func (h *Headscale) expandPorts(s string) (*[]tailcfg.PortRange, error) { + if s == "*" { + return &[]tailcfg.PortRange{{First: 0, Last: 65535}}, nil + } + + ports := []tailcfg.PortRange{} + for _, p := range strings.Split(s, ",") { + rang := strings.Split(p, "-") + if len(rang) == 1 { + pi, err := strconv.ParseUint(rang[0], 10, 16) + if err != nil { + return nil, err + } + ports = append(ports, tailcfg.PortRange{ + First: uint16(pi), + Last: uint16(pi), + }) + } else if len(rang) == 2 { + start, err := strconv.ParseUint(rang[0], 10, 16) + if err != nil { + return nil, err + } + last, err := strconv.ParseUint(rang[1], 10, 16) + if err != nil { + return nil, err + } + ports = append(ports, tailcfg.PortRange{ + First: uint16(start), + Last: uint16(last), + }) + } else { + return nil, errorInvalidPortFormat + } + } + return &ports, nil +} diff --git a/acls_test.go b/acls_test.go new file mode 100644 index 0000000..dc5b4b3 --- /dev/null +++ b/acls_test.go @@ -0,0 +1,160 @@ +package headscale + +import ( + "gopkg.in/check.v1" +) + +func (s *Suite) TestWrongPath(c *check.C) { + err := h.LoadACLPolicy("asdfg") + c.Assert(err, check.NotNil) +} + +func (s *Suite) TestBrokenHuJson(c *check.C) { + err := h.LoadACLPolicy("./tests/acls/broken.hujson") + c.Assert(err, check.NotNil) + +} + +func (s *Suite) TestInvalidPolicyHuson(c *check.C) { + err := h.LoadACLPolicy("./tests/acls/invalid.hujson") + c.Assert(err, check.NotNil) + c.Assert(err, check.Equals, errorEmptyPolicy) +} + +func (s *Suite) TestParseHosts(c *check.C) { + var hs Hosts + err := hs.UnmarshalJSON([]byte(`{"example-host-1": "100.100.100.100","example-host-2": "100.100.101.100/24"}`)) + c.Assert(hs, check.NotNil) + c.Assert(err, check.IsNil) +} + +func (s *Suite) TestParseInvalidCIDR(c *check.C) { + var hs Hosts + err := hs.UnmarshalJSON([]byte(`{"example-host-1": "100.100.100.100/42"}`)) + c.Assert(hs, check.IsNil) + c.Assert(err, check.NotNil) +} + +func (s *Suite) TestRuleInvalidGeneration(c *check.C) { + err := h.LoadACLPolicy("./tests/acls/acl_policy_invalid.hujson") + c.Assert(err, check.NotNil) +} + +func (s *Suite) TestBasicRule(c *check.C) { + err := h.LoadACLPolicy("./tests/acls/acl_policy_basic_1.hujson") + c.Assert(err, check.IsNil) + + rules, err := h.generateACLRules() + c.Assert(err, check.IsNil) + c.Assert(rules, check.NotNil) +} + +func (s *Suite) TestPortRange(c *check.C) { + err := h.LoadACLPolicy("./tests/acls/acl_policy_basic_range.hujson") + c.Assert(err, check.IsNil) + + rules, err := h.generateACLRules() + c.Assert(err, check.IsNil) + c.Assert(rules, check.NotNil) + + c.Assert(*rules, check.HasLen, 1) + c.Assert((*rules)[0].DstPorts, check.HasLen, 1) + c.Assert((*rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(5400)) + c.Assert((*rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(5500)) +} + +func (s *Suite) TestPortWildcard(c *check.C) { + err := h.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.hujson") + c.Assert(err, check.IsNil) + + rules, err := h.generateACLRules() + c.Assert(err, check.IsNil) + c.Assert(rules, check.NotNil) + + c.Assert(*rules, check.HasLen, 1) + c.Assert((*rules)[0].DstPorts, check.HasLen, 1) + c.Assert((*rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(0)) + c.Assert((*rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535)) + c.Assert((*rules)[0].SrcIPs, check.HasLen, 1) + c.Assert((*rules)[0].SrcIPs[0], check.Equals, "*") +} + +func (s *Suite) TestPortNamespace(c *check.C) { + n, err := h.CreateNamespace("testnamespace") + c.Assert(err, check.IsNil) + + pak, err := h.CreatePreAuthKey(n.Name, false, false, nil) + c.Assert(err, check.IsNil) + + _, err = h.GetMachine("testnamespace", "testmachine") + c.Assert(err, check.NotNil) + ip, _ := h.getAvailableIP() + m := Machine{ + ID: 0, + MachineKey: "foo", + NodeKey: "bar", + DiscoKey: "faa", + Name: "testmachine", + NamespaceID: n.ID, + Registered: true, + RegisterMethod: "authKey", + IPAddress: ip.String(), + AuthKeyID: uint(pak.ID), + } + h.db.Save(&m) + + err = h.LoadACLPolicy("./tests/acls/acl_policy_basic_namespace_as_user.hujson") + c.Assert(err, check.IsNil) + + rules, err := h.generateACLRules() + c.Assert(err, check.IsNil) + c.Assert(rules, check.NotNil) + + c.Assert(*rules, check.HasLen, 1) + c.Assert((*rules)[0].DstPorts, check.HasLen, 1) + c.Assert((*rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(0)) + c.Assert((*rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535)) + c.Assert((*rules)[0].SrcIPs, check.HasLen, 1) + c.Assert((*rules)[0].SrcIPs[0], check.Not(check.Equals), "not an ip") + c.Assert((*rules)[0].SrcIPs[0], check.Equals, ip.String()) +} + +func (s *Suite) TestPortGroup(c *check.C) { + n, err := h.CreateNamespace("testnamespace") + c.Assert(err, check.IsNil) + + pak, err := h.CreatePreAuthKey(n.Name, false, false, nil) + c.Assert(err, check.IsNil) + + _, err = h.GetMachine("testnamespace", "testmachine") + c.Assert(err, check.NotNil) + ip, _ := h.getAvailableIP() + m := Machine{ + ID: 0, + MachineKey: "foo", + NodeKey: "bar", + DiscoKey: "faa", + Name: "testmachine", + NamespaceID: n.ID, + Registered: true, + RegisterMethod: "authKey", + IPAddress: ip.String(), + AuthKeyID: uint(pak.ID), + } + h.db.Save(&m) + + err = h.LoadACLPolicy("./tests/acls/acl_policy_basic_groups.hujson") + c.Assert(err, check.IsNil) + + rules, err := h.generateACLRules() + c.Assert(err, check.IsNil) + c.Assert(rules, check.NotNil) + + c.Assert(*rules, check.HasLen, 1) + c.Assert((*rules)[0].DstPorts, check.HasLen, 1) + c.Assert((*rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(0)) + c.Assert((*rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535)) + c.Assert((*rules)[0].SrcIPs, check.HasLen, 1) + c.Assert((*rules)[0].SrcIPs[0], check.Not(check.Equals), "not an ip") + c.Assert((*rules)[0].SrcIPs[0], check.Equals, ip.String()) +} diff --git a/acls_types.go b/acls_types.go new file mode 100644 index 0000000..01e42d5 --- /dev/null +++ b/acls_types.go @@ -0,0 +1,70 @@ +package headscale + +import ( + "strings" + + "github.com/tailscale/hujson" + "inet.af/netaddr" +) + +// ACLPolicy represents a Tailscale ACL Policy +type ACLPolicy struct { + Groups Groups `json:"Groups"` + Hosts Hosts `json:"Hosts"` + TagOwners TagOwners `json:"TagOwners"` + ACLs []ACL `json:"ACLs"` + Tests []ACLTest `json:"Tests"` +} + +// ACL is a basic rule for the ACL Policy +type ACL struct { + Action string `json:"Action"` + Users []string `json:"Users"` + Ports []string `json:"Ports"` +} + +// Groups references a series of alias in the ACL rules +type Groups map[string][]string + +// Hosts are alias for IP addresses or subnets +type Hosts map[string]netaddr.IPPrefix + +// TagOwners specify what users (namespaces?) 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 +type ACLTest struct { + User string `json:"User"` + Allow []string `json:"Allow"` + Deny []string `json:"Deny,omitempty"` +} + +// UnmarshalJSON allows to parse the Hosts directly into netaddr objects +func (h *Hosts) UnmarshalJSON(data []byte) error { + hosts := Hosts{} + hs := make(map[string]string) + err := hujson.Unmarshal(data, &hs) + if err != nil { + return err + } + for k, v := range hs { + if !strings.Contains(v, "/") { + v = v + "/32" + } + prefix, err := netaddr.ParseIPPrefix(v) + if err != nil { + return err + } + hosts[k] = prefix + } + *h = hosts + return nil +} + +// 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 +} diff --git a/api.go b/api.go index 8629573..e5bcaed 100644 --- a/api.go +++ b/api.go @@ -361,7 +361,7 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Mac DNS: []netaddr.IP{}, SearchPaths: []string{}, Domain: "foobar@example.com", - PacketFilter: tailcfg.FilterAllowAll, + PacketFilter: *h.aclRules, DERPMap: h.cfg.DerpMap, UserProfiles: []tailcfg.UserProfile{}, } diff --git a/app.go b/app.go index 8ff6029..071b8b4 100644 --- a/app.go +++ b/app.go @@ -51,6 +51,9 @@ type Headscale struct { publicKey *wgkey.Key privateKey *wgkey.Private + aclPolicy *ACLPolicy + aclRules *[]tailcfg.FilterRule + pollMu sync.Mutex clientsPolling map[uint64]chan []byte // this is by all means a hackity hack } @@ -84,7 +87,9 @@ func NewHeadscale(cfg Config) (*Headscale, error) { dbString: dbString, privateKey: privKey, publicKey: &pubKey, + aclRules: &tailcfg.FilterAllowAll, // default allowall } + err = h.initDB() if err != nil { return nil, err diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 52a9368..a872ec5 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -119,6 +119,13 @@ func getHeadscaleApp() (*headscale.Headscale, error) { if err != nil { return nil, err } + + // We are doing this here, as in the future could be cool to have it also hot-reload + err = h.LoadACLPolicy(absPath(viper.GetString("acl_policy_path"))) + if err != nil { + log.Printf("Could not load the ACL policy: %s", err) + } + return h, nil } diff --git a/config.json.postgres.example b/config.json.postgres.example index 7b283c4..481e1a9 100644 --- a/config.json.postgres.example +++ b/config.json.postgres.example @@ -14,5 +14,6 @@ "tls_letsencrypt_cache_dir": ".cache", "tls_letsencrypt_challenge_type": "HTTP-01", "tls_cert_path": "", - "tls_key_path": "" + "tls_key_path": "", + "acl_policy_path": "" } diff --git a/config.json.sqlite.example b/config.json.sqlite.example index 787e3e1..0724f59 100644 --- a/config.json.sqlite.example +++ b/config.json.sqlite.example @@ -10,5 +10,6 @@ "tls_letsencrypt_cache_dir": ".cache", "tls_letsencrypt_challenge_type": "HTTP-01", "tls_cert_path": "", - "tls_key_path": "" + "tls_key_path": "", + "acl_policy_path": "" } diff --git a/go.mod b/go.mod index 14146a1..9e88a5f 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/mattn/go-sqlite3 v1.14.7 // indirect github.com/spf13/cobra v1.1.3 github.com/spf13/viper v1.8.1 + github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2 golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/yaml.v2 v2.4.0 diff --git a/go.sum b/go.sum index 08d8a17..9453ebe 100644 --- a/go.sum +++ b/go.sum @@ -746,6 +746,8 @@ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3/go.mod h1:2P+hpOwd53e7JMX/L4f3VXkv1G+33ES6IWZSrkIeWNs= github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= +github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2 h1:reREUgl2FG+o7YCsrZB8XLjnuKv5hEIWtnOdAbRAXZI= +github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2/go.mod h1:STqf+YV0ADdzk4ejtXFsGqDpATP9JoL0OB+hiFQbkdE= github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8= github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM= github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM= diff --git a/tests/acls/acl_policy_1.hujson b/tests/acls/acl_policy_1.hujson new file mode 100644 index 0000000..8f70148 --- /dev/null +++ b/tests/acls/acl_policy_1.hujson @@ -0,0 +1,127 @@ +{ + // Declare static groups of users beyond those in the identity service. + "Groups": { + "group:example": [ + "user1@example.com", + "user2@example.com", + ], + "group:example2": [ + "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:example", + ], + // Only a few admins are allowed to create API servers. + "tag:production": [ + "group:example", + "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:example2", + "192.168.1.0/24" + ], + "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:example" + ], + "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": [ + "example-host-2", + ], + "Ports": [ + "example-host-1:*", + "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": [ + "example-host-1" + ], + "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/acl_policy_basic_1.hujson b/tests/acls/acl_policy_basic_1.hujson new file mode 100644 index 0000000..4f86af3 --- /dev/null +++ b/tests/acls/acl_policy_basic_1.hujson @@ -0,0 +1,24 @@ +// This ACL is a very basic example to validate the +// expansion of hosts + + +{ + "Hosts": { + "host-1": "100.100.100.100", + "subnet-1": "100.100.101.100/24", + }, + + "ACLs": [ + { + "Action": "accept", + "Users": [ + "subnet-1", + "192.168.1.0/24" + ], + "Ports": [ + "*:22,3389", + "host-1:*", + ], + }, + ], +} \ No newline at end of file diff --git a/tests/acls/acl_policy_basic_groups.hujson b/tests/acls/acl_policy_basic_groups.hujson new file mode 100644 index 0000000..ed11a0d --- /dev/null +++ b/tests/acls/acl_policy_basic_groups.hujson @@ -0,0 +1,26 @@ +// This ACL is used to test group expansion + +{ + "Groups": { + "group:example": [ + "testnamespace", + ], + }, + + "Hosts": { + "host-1": "100.100.100.100", + "subnet-1": "100.100.101.100/24", + }, + + "ACLs": [ + { + "Action": "accept", + "Users": [ + "group:example", + ], + "Ports": [ + "host-1:*", + ], + }, + ], +} \ No newline at end of file diff --git a/tests/acls/acl_policy_basic_namespace_as_user.hujson b/tests/acls/acl_policy_basic_namespace_as_user.hujson new file mode 100644 index 0000000..881349f --- /dev/null +++ b/tests/acls/acl_policy_basic_namespace_as_user.hujson @@ -0,0 +1,20 @@ +// This ACL is used to test namespace expansion + +{ + "Hosts": { + "host-1": "100.100.100.100", + "subnet-1": "100.100.101.100/24", + }, + + "ACLs": [ + { + "Action": "accept", + "Users": [ + "testnamespace", + ], + "Ports": [ + "host-1:*", + ], + }, + ], +} \ No newline at end of file diff --git a/tests/acls/acl_policy_basic_range.hujson b/tests/acls/acl_policy_basic_range.hujson new file mode 100644 index 0000000..8bcbc79 --- /dev/null +++ b/tests/acls/acl_policy_basic_range.hujson @@ -0,0 +1,20 @@ +// This ACL is used to test the port range expansion + +{ + "Hosts": { + "host-1": "100.100.100.100", + "subnet-1": "100.100.101.100/24", + }, + + "ACLs": [ + { + "Action": "accept", + "Users": [ + "subnet-1", + ], + "Ports": [ + "host-1:5400-5500", + ], + }, + ], +} \ No newline at end of file diff --git a/tests/acls/acl_policy_basic_wildcards.hujson b/tests/acls/acl_policy_basic_wildcards.hujson new file mode 100644 index 0000000..ec5ce46 --- /dev/null +++ b/tests/acls/acl_policy_basic_wildcards.hujson @@ -0,0 +1,20 @@ +// This ACL is used to test wildcards + +{ + "Hosts": { + "host-1": "100.100.100.100", + "subnet-1": "100.100.101.100/24", + }, + + "ACLs": [ + { + "Action": "accept", + "Users": [ + "*", + ], + "Ports": [ + "host-1:*", + ], + }, + ], +} \ No newline at end of file diff --git a/tests/acls/acl_policy_invalid.hujson b/tests/acls/acl_policy_invalid.hujson new file mode 100644 index 0000000..ad640df --- /dev/null +++ b/tests/acls/acl_policy_invalid.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", + "example-host-1", + ], + }, + // 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 0000000..98232c6 --- /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 0000000..733f692 --- /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