inline old acl hujson tests

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
Kristoffer Dalby 2023-05-10 10:19:16 +02:00 committed by Juan Font
parent f2c1d1b8f9
commit 2d365c8c9c
16 changed files with 359 additions and 474 deletions

View file

@ -59,8 +59,8 @@ const (
var featureEnableSSH = envknob.RegisterBool("HEADSCALE_EXPERIMENTAL_FEATURE_SSH") var featureEnableSSH = envknob.RegisterBool("HEADSCALE_EXPERIMENTAL_FEATURE_SSH")
// LoadACLPolicy loads the ACL policy from the specify path, and generates the ACL rules. // LoadACLPolicyFromPath loads the ACL policy from the specify path, and generates the ACL rules.
func (h *Headscale) LoadACLPolicy(path string) error { func (h *Headscale) LoadACLPolicyFromPath(path string) error {
log.Debug(). log.Debug().
Str("func", "LoadACLPolicy"). Str("func", "LoadACLPolicy").
Str("path", path). Str("path", path).
@ -72,37 +72,42 @@ func (h *Headscale) LoadACLPolicy(path string) error {
} }
defer policyFile.Close() defer policyFile.Close()
var policy ACLPolicy
policyBytes, err := io.ReadAll(policyFile) policyBytes, err := io.ReadAll(policyFile)
if err != nil { if err != nil {
return err return err
} }
switch filepath.Ext(path) {
case ".yml", ".yaml":
log.Debug(). log.Debug().
Str("path", path). Str("path", path).
Bytes("file", policyBytes). Bytes("file", policyBytes).
Msg("Loading ACLs from YAML") Msg("Loading ACLs")
err := yaml.Unmarshal(policyBytes, &policy) switch filepath.Ext(path) {
case ".yml", ".yaml":
return h.LoadACLPolicyFromBytes(policyBytes, "yaml")
}
return h.LoadACLPolicyFromBytes(policyBytes, "hujson")
}
func (h *Headscale) LoadACLPolicyFromBytes(acl []byte, format string) error {
var policy ACLPolicy
switch format {
case "yaml":
err := yaml.Unmarshal(acl, &policy)
if err != nil { if err != nil {
return err return err
} }
log.Trace().
Interface("policy", policy).
Msg("Loaded policy from YAML")
default: default:
ast, err := hujson.Parse(policyBytes) ast, err := hujson.Parse(acl)
if err != nil { if err != nil {
return err return err
} }
ast.Standardize() ast.Standardize()
policyBytes = ast.Pack() acl = ast.Pack()
err = json.Unmarshal(policyBytes, &policy) err = json.Unmarshal(acl, &policy)
if err != nil { if err != nil {
return err return err
} }

View file

@ -15,17 +15,26 @@ import (
) )
func (s *Suite) TestWrongPath(c *check.C) { func (s *Suite) TestWrongPath(c *check.C) {
err := app.LoadACLPolicy("asdfg") err := app.LoadACLPolicyFromPath("asdfg")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
} }
func (s *Suite) TestBrokenHuJson(c *check.C) { func (s *Suite) TestBrokenHuJson(c *check.C) {
err := app.LoadACLPolicy("./tests/acls/broken.hujson") acl := []byte(`
{
`)
err := app.LoadACLPolicyFromBytes(acl, "hujson")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
} }
func (s *Suite) TestInvalidPolicyHuson(c *check.C) { func (s *Suite) TestInvalidPolicyHuson(c *check.C) {
err := app.LoadACLPolicy("./tests/acls/invalid.hujson") acl := []byte(`
{
"valid_json": true,
"but_a_policy_though": false
}
`)
err := app.LoadACLPolicyFromBytes(acl, "hujson")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
c.Assert(err, check.Equals, errEmptyPolicy) c.Assert(err, check.Equals, errEmptyPolicy)
} }
@ -49,12 +58,161 @@ func (s *Suite) TestParseInvalidCIDR(c *check.C) {
} }
func (s *Suite) TestRuleInvalidGeneration(c *check.C) { func (s *Suite) TestRuleInvalidGeneration(c *check.C) {
err := app.LoadACLPolicy("./tests/acls/acl_policy_invalid.hujson") acl := []byte(`
{
// 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",
"src": [
"group:engineering",
"president@example.com"
],
"dst": [
"*:22,3389",
"git-server:*",
"ci-server:*"
],
},
// Allow engineer users to access any port on a device tagged with
// tag:production.
{
"action": "accept",
"src": [
"group:engineers"
],
"dst": [
"tag:production:*"
],
},
// Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts
// on both networks.
{
"action": "accept",
"src": [
"my-subnet",
"192.168.1.0/24"
],
"dst": [
"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",
"src": [
"*"
],
"dst": [
"*:*"
],
},
// All users in Montreal are allowed to access the Montreal web
// servers.
{
"action": "accept",
"src": [
"group:montreal-users"
],
"dst": [
"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",
"src": [
"tag:montreal-webserver"
],
"dst": [
"tag:api-server:443"
],
},
],
// Declare tests to check functionality of ACL rules
"tests": [
{
"src": "user1@example.com",
"accept": [
"example-host-1:22",
"example-host-2:80"
],
"deny": [
"exapmle-host-2:100"
],
},
{
"src": "user2@example.com",
"accept": [
"100.60.3.4:22"
],
},
],
}
`)
err := app.LoadACLPolicyFromBytes(acl, "hujson")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
} }
func (s *Suite) TestBasicRule(c *check.C) { func (s *Suite) TestBasicRule(c *check.C) {
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_1.hujson") acl := []byte(`
{
"hosts": {
"host-1": "100.100.100.100",
"subnet-1": "100.100.101.100/24",
},
"acls": [
{
"action": "accept",
"src": [
"subnet-1",
"192.168.1.0/24"
],
"dst": [
"*:22,3389",
"host-1:*",
],
},
],
}
`)
err := app.LoadACLPolicyFromBytes(acl, "hujson")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
rules, err := app.aclPolicy.generateFilterRules([]Machine{}, false) rules, err := app.aclPolicy.generateFilterRules([]Machine{}, false)
@ -411,7 +569,27 @@ func (s *Suite) TestValidTagInvalidUser(c *check.C) {
} }
func (s *Suite) TestPortRange(c *check.C) { func (s *Suite) TestPortRange(c *check.C) {
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_range.hujson") acl := []byte(`
{
"hosts": {
"host-1": "100.100.100.100",
"subnet-1": "100.100.101.100/24",
},
"acls": [
{
"action": "accept",
"src": [
"subnet-1",
],
"dst": [
"host-1:5400-5500",
],
},
],
}
`)
err := app.LoadACLPolicyFromBytes(acl, "hujson")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
rules, err := app.aclPolicy.generateFilterRules([]Machine{}, false) rules, err := app.aclPolicy.generateFilterRules([]Machine{}, false)
@ -425,7 +603,48 @@ func (s *Suite) TestPortRange(c *check.C) {
} }
func (s *Suite) TestProtocolParsing(c *check.C) { func (s *Suite) TestProtocolParsing(c *check.C) {
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_protocols.hujson") acl := []byte(`
{
"hosts": {
"host-1": "100.100.100.100",
"subnet-1": "100.100.101.100/24",
},
"acls": [
{
"Action": "accept",
"src": [
"*",
],
"proto": "tcp",
"dst": [
"host-1:*",
],
},
{
"Action": "accept",
"src": [
"*",
],
"proto": "udp",
"dst": [
"host-1:53",
],
},
{
"Action": "accept",
"src": [
"*",
],
"proto": "icmp",
"dst": [
"host-1:*",
],
},
],
}
`)
err := app.LoadACLPolicyFromBytes(acl, "hujson")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
rules, err := app.aclPolicy.generateFilterRules([]Machine{}, false) rules, err := app.aclPolicy.generateFilterRules([]Machine{}, false)
@ -439,7 +658,27 @@ func (s *Suite) TestProtocolParsing(c *check.C) {
} }
func (s *Suite) TestPortWildcard(c *check.C) { func (s *Suite) TestPortWildcard(c *check.C) {
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.hujson") acl := []byte(`
{
"hosts": {
"host-1": "100.100.100.100",
"subnet-1": "100.100.101.100/24",
},
"acls": [
{
"Action": "accept",
"src": [
"*",
],
"dst": [
"host-1:*",
],
},
],
}
`)
err := app.LoadACLPolicyFromBytes(acl, "hujson")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
rules, err := app.aclPolicy.generateFilterRules([]Machine{}, false) rules, err := app.aclPolicy.generateFilterRules([]Machine{}, false)
@ -455,7 +694,19 @@ func (s *Suite) TestPortWildcard(c *check.C) {
} }
func (s *Suite) TestPortWildcardYAML(c *check.C) { func (s *Suite) TestPortWildcardYAML(c *check.C) {
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.yaml") acl := []byte(`
---
hosts:
host-1: 100.100.100.100/32
subnet-1: 100.100.101.100/24
acls:
- action: accept
src:
- "*"
dst:
- host-1:*
`)
err := app.LoadACLPolicyFromBytes(acl, "yaml")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
rules, err := app.aclPolicy.generateFilterRules([]Machine{}, false) rules, err := app.aclPolicy.generateFilterRules([]Machine{}, false)
@ -493,9 +744,27 @@ func (s *Suite) TestPortUser(c *check.C) {
} }
app.db.Save(&machine) app.db.Save(&machine)
err = app.LoadACLPolicy( acl := []byte(`
"./tests/acls/acl_policy_basic_user_as_user.hujson", {
) "hosts": {
"host-1": "100.100.100.100",
"subnet-1": "100.100.101.100/24",
},
"acls": [
{
"action": "accept",
"src": [
"testuser",
],
"dst": [
"host-1:*",
],
},
],
}
`)
err = app.LoadACLPolicyFromBytes(acl, "hujson")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
machines, err := app.ListMachines() machines, err := app.ListMachines()
@ -538,7 +807,33 @@ func (s *Suite) TestPortGroup(c *check.C) {
} }
app.db.Save(&machine) app.db.Save(&machine)
err = app.LoadACLPolicy("./tests/acls/acl_policy_basic_groups.hujson") acl := []byte(`
{
"groups": {
"group:example": [
"testuser",
],
},
"hosts": {
"host-1": "100.100.100.100",
"subnet-1": "100.100.101.100/24",
},
"acls": [
{
"action": "accept",
"src": [
"group:example",
],
"dst": [
"host-1:*",
],
},
],
}
`)
err = app.LoadACLPolicyFromBytes(acl, "hujson")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
machines, err := app.ListMachines() machines, err := app.ListMachines()

View file

@ -21,6 +21,7 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
grpcMiddleware "github.com/grpc-ecosystem/go-grpc-middleware" grpcMiddleware "github.com/grpc-ecosystem/go-grpc-middleware"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1" v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/patrickmn/go-cache" "github.com/patrickmn/go-cache"
zerolog "github.com/philip-bui/grpc-zerolog" zerolog "github.com/philip-bui/grpc-zerolog"
@ -507,8 +508,10 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router {
router.HandleFunc("/windows", h.WindowsConfigMessage).Methods(http.MethodGet) router.HandleFunc("/windows", h.WindowsConfigMessage).Methods(http.MethodGet)
router.HandleFunc("/windows/tailscale.reg", h.WindowsRegConfig). router.HandleFunc("/windows/tailscale.reg", h.WindowsRegConfig).
Methods(http.MethodGet) Methods(http.MethodGet)
router.HandleFunc("/swagger", SwaggerUI).Methods(http.MethodGet)
router.HandleFunc("/swagger/v1/openapiv2.json", SwaggerAPIv1). // TODO(kristoffer): move swagger into a package
router.HandleFunc("/swagger", headscale.SwaggerUI).Methods(http.MethodGet)
router.HandleFunc("/swagger/v1/openapiv2.json", headscale.SwaggerAPIv1).
Methods(http.MethodGet) Methods(http.MethodGet)
if h.cfg.DERP.ServerEnabled { if h.cfg.DERP.ServerEnabled {
@ -758,7 +761,7 @@ func (h *Headscale) Serve() error {
if h.cfg.ACL.PolicyPath != "" { if h.cfg.ACL.PolicyPath != "" {
aclPath := AbsolutePathFromConfigPath(h.cfg.ACL.PolicyPath) aclPath := AbsolutePathFromConfigPath(h.cfg.ACL.PolicyPath)
err := h.LoadACLPolicy(aclPath) err := h.LoadACLPolicyFromPath(aclPath)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to reload ACL policy") log.Error().Err(err).Msg("Failed to reload ACL policy")
} }

View file

@ -1212,7 +1212,31 @@ func TestHeadscale_generateGivenName(t *testing.T) {
} }
func (s *Suite) TestAutoApproveRoutes(c *check.C) { func (s *Suite) TestAutoApproveRoutes(c *check.C) {
err := app.LoadACLPolicy("./tests/acls/acl_policy_autoapprovers.hujson") acl := []byte(`
{
"tagOwners": {
"tag:exit": ["test"],
},
"groups": {
"group:test": ["test"]
},
"acls": [
{"action": "accept", "users": ["*"], "ports": ["*:*"]},
],
"autoApprovers": {
"exitNode": ["tag:exit"],
"routes": {
"10.10.0.0/16": ["group:test"],
"10.11.0.0/16": ["test"],
}
}
}
`)
err := app.LoadACLPolicyFromBytes(acl, "hujson")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
user, err := app.CreateUser("test") user, err := app.CreateUser("test")

View file

@ -1,127 +0,0 @@
{
// 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",
"src": [
"group:example2",
"192.168.1.0/24"
],
"dst": [
"*:22,3389",
"git-server:*",
"ci-server:*"
],
},
// Allow engineer users to access any port on a device tagged with
// tag:production.
{
"action": "accept",
"src": [
"group:example"
],
"dst": [
"tag:production:*"
],
},
// Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts
// on both networks.
{
"action": "accept",
"src": [
"example-host-2",
],
"dst": [
"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",
"src": [
"*"
],
"dst": [
"*:*"
],
},
// All users in Montreal are allowed to access the Montreal web
// servers.
{
"action": "accept",
"src": [
"example-host-1"
],
"dst": [
"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",
"src": [
"tag:montreal-webserver"
],
"dst": [
"tag:api-server:443"
],
},
],
// Declare tests to check functionality of ACL rules
"tests": [
{
"src": "user1@example.com",
"accept": [
"example-host-1:22",
"example-host-2:80"
],
"deny": [
"exapmle-host-2:100"
],
},
{
"src": "user2@example.com",
"accept": [
"100.60.3.4:22"
],
},
],
}

View file

@ -1,24 +0,0 @@
// This ACL validates autoApprovers support for
// exit nodes and advertised routes
{
"tagOwners": {
"tag:exit": ["test"],
},
"groups": {
"group:test": ["test"]
},
"acls": [
{"action": "accept", "users": ["*"], "ports": ["*:*"]},
],
"autoApprovers": {
"exitNode": ["tag:exit"],
"routes": {
"10.10.0.0/16": ["group:test"],
"10.11.0.0/16": ["test"],
}
}
}

View file

@ -1,24 +0,0 @@
// 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",
"src": [
"subnet-1",
"192.168.1.0/24"
],
"dst": [
"*:22,3389",
"host-1:*",
],
},
],
}

View file

@ -1,26 +0,0 @@
// This ACL is used to test group expansion
{
"groups": {
"group:example": [
"testuser",
],
},
"hosts": {
"host-1": "100.100.100.100",
"subnet-1": "100.100.101.100/24",
},
"acls": [
{
"action": "accept",
"src": [
"group:example",
],
"dst": [
"host-1:*",
],
},
],
}

View file

@ -1,41 +0,0 @@
// This ACL is used to test wildcards
{
"hosts": {
"host-1": "100.100.100.100",
"subnet-1": "100.100.101.100/24",
},
"acls": [
{
"Action": "accept",
"src": [
"*",
],
"proto": "tcp",
"dst": [
"host-1:*",
],
},
{
"Action": "accept",
"src": [
"*",
],
"proto": "udp",
"dst": [
"host-1:53",
],
},
{
"Action": "accept",
"src": [
"*",
],
"proto": "icmp",
"dst": [
"host-1:*",
],
},
],
}

View file

@ -1,20 +0,0 @@
// 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",
"src": [
"subnet-1",
],
"dst": [
"host-1:5400-5500",
],
},
],
}

View file

@ -1,20 +0,0 @@
// 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",
"src": [
"testuser",
],
"dst": [
"host-1:*",
],
},
],
}

View file

@ -1,20 +0,0 @@
// This ACL is used to test wildcards
{
"hosts": {
"host-1": "100.100.100.100",
"subnet-1": "100.100.101.100/24",
},
"acls": [
{
"Action": "accept",
"src": [
"*",
],
"dst": [
"host-1:*",
],
},
],
}

View file

@ -1,10 +0,0 @@
---
hosts:
host-1: 100.100.100.100/32
subnet-1: 100.100.101.100/24
acls:
- action: accept
src:
- "*"
dst:
- host-1:*

View file

@ -1,125 +0,0 @@
{
// 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",
"src": [
"group:engineering",
"president@example.com"
],
"dst": [
"*:22,3389",
"git-server:*",
"ci-server:*"
],
},
// Allow engineer users to access any port on a device tagged with
// tag:production.
{
"action": "accept",
"src": [
"group:engineers"
],
"dst": [
"tag:production:*"
],
},
// Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts
// on both networks.
{
"action": "accept",
"src": [
"my-subnet",
"192.168.1.0/24"
],
"dst": [
"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",
"src": [
"*"
],
"dst": [
"*:*"
],
},
// All users in Montreal are allowed to access the Montreal web
// servers.
{
"action": "accept",
"src": [
"group:montreal-users"
],
"dst": [
"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",
"src": [
"tag:montreal-webserver"
],
"dst": [
"tag:api-server:443"
],
},
],
// Declare tests to check functionality of ACL rules
"tests": [
{
"src": "user1@example.com",
"accept": [
"example-host-1:22",
"example-host-2:80"
],
"deny": [
"exapmle-host-2:100"
],
},
{
"src": "user2@example.com",
"accept": [
"100.60.3.4:22"
],
},
],
}

View file

@ -1 +0,0 @@
{

View file

@ -1,4 +0,0 @@
{
"valid_json": true,
"but_a_policy_though": false
}