From a455a874add8b559eaa287c4d380ecda0196b357 Mon Sep 17 00:00:00 2001 From: Adrien Raffin-Caboisse Date: Tue, 1 Mar 2022 21:01:46 +0100 Subject: [PATCH 1/4] feat(acls): normalize the group name --- acls.go | 46 ++++++++++++++++----- acls_test.go | 115 +++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 124 insertions(+), 37 deletions(-) diff --git a/acls.go b/acls.go index 1a597c24..7b93ad76 100644 --- a/acls.go +++ b/acls.go @@ -160,7 +160,7 @@ func (h *Headscale) generateACLPolicySrcIP( aclPolicy ACLPolicy, u string, ) ([]string, error) { - return expandAlias(machines, aclPolicy, u) + return expandAlias(machines, aclPolicy, u, h.cfg.OIDC.StripEmaildomain) } func (h *Headscale) generateACLPolicyDestPorts( @@ -186,7 +186,12 @@ func (h *Headscale) generateACLPolicyDestPorts( alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1]) } - expanded, err := expandAlias(machines, aclPolicy, alias) + expanded, err := expandAlias( + machines, + aclPolicy, + alias, + h.cfg.OIDC.StripEmaildomain, + ) if err != nil { return nil, err } @@ -218,6 +223,7 @@ func expandAlias( machines []Machine, aclPolicy ACLPolicy, alias string, + stripEmailDomain bool, ) ([]string, error) { ips := []string{} if alias == "*" { @@ -225,7 +231,7 @@ func expandAlias( } if strings.HasPrefix(alias, "group:") { - namespaces, err := expandGroup(aclPolicy, alias) + namespaces, err := expandGroup(aclPolicy, alias, stripEmailDomain) if err != nil { return ips, err } @@ -240,7 +246,7 @@ func expandAlias( } if strings.HasPrefix(alias, "tag:") { - owners, err := expandTagOwners(aclPolicy, alias) + owners, err := expandTagOwners(aclPolicy, alias, stripEmailDomain) if err != nil { return ips, err } @@ -396,7 +402,11 @@ func filterMachinesByNamespace(machines []Machine, namespace string) []Machine { // expandTagOwners will return a list of namespace. An owner can be either a namespace or a group // a group cannot be composed of groups. -func expandTagOwners(aclPolicy ACLPolicy, tag string) ([]string, error) { +func expandTagOwners( + aclPolicy ACLPolicy, + tag string, + stripEmailDomain bool, +) ([]string, error) { var owners []string ows, ok := aclPolicy.TagOwners[tag] if !ok { @@ -408,7 +418,7 @@ func expandTagOwners(aclPolicy ACLPolicy, tag string) ([]string, error) { } for _, owner := range ows { if strings.HasPrefix(owner, "group:") { - gs, err := expandGroup(aclPolicy, owner) + gs, err := expandGroup(aclPolicy, owner, stripEmailDomain) if err != nil { return []string{}, err } @@ -423,8 +433,13 @@ func expandTagOwners(aclPolicy ACLPolicy, tag string) ([]string, error) { // expandGroup will return the list of namespace inside the group // after some validation. -func expandGroup(aclPolicy ACLPolicy, group string) ([]string, error) { - groups, ok := aclPolicy.Groups[group] +func expandGroup( + aclPolicy ACLPolicy, + group string, + stripEmailDomain bool, +) ([]string, error) { + outGroups := []string{} + aclGroups, ok := aclPolicy.Groups[group] if !ok { return []string{}, fmt.Errorf( "group %v isn't registered. %w", @@ -432,14 +447,23 @@ func expandGroup(aclPolicy ACLPolicy, group string) ([]string, error) { errInvalidGroup, ) } - for _, g := range groups { - if strings.HasPrefix(g, "group:") { + for _, group := range aclGroups { + if strings.HasPrefix(group, "group:") { return []string{}, fmt.Errorf( "%w. A group cannot be composed of groups. https://tailscale.com/kb/1018/acls/#groups", errInvalidGroup, ) } + grp, err := NormalizeNamespaceName(group, stripEmailDomain) + if err != nil { + return []string{}, fmt.Errorf( + "failed to normalize group %q, err: %w", + group, + errInvalidGroup, + ) + } + outGroups = append(outGroups, grp) } - return groups, nil + return outGroups, nil } diff --git a/acls_test.go b/acls_test.go index 9f0432a7..6cee02c9 100644 --- a/acls_test.go +++ b/acls_test.go @@ -430,8 +430,9 @@ func (s *Suite) TestPortGroup(c *check.C) { func Test_expandGroup(t *testing.T) { type args struct { - aclPolicy ACLPolicy - group string + aclPolicy ACLPolicy + group string + stripEmailDomain bool } tests := []struct { name string @@ -448,7 +449,8 @@ func Test_expandGroup(t *testing.T) { "group:foo": []string{"user2", "user3"}, }, }, - group: "group:test", + group: "group:test", + stripEmailDomain: true, }, want: []string{"user1", "user2", "user3"}, wantErr: false, @@ -462,15 +464,54 @@ func Test_expandGroup(t *testing.T) { "group:foo": []string{"user2", "user3"}, }, }, - group: "group:undefined", + group: "group:undefined", + stripEmailDomain: true, }, want: []string{}, wantErr: true, }, + { + name: "Expand emails in group", + args: args{ + aclPolicy: ACLPolicy{ + Groups: Groups{ + "group:admin": []string{ + "joe.bar@gmail.com", + "john.doe@yahoo.fr", + }, + }, + }, + group: "group:admin", + stripEmailDomain: true, + }, + want: []string{"joe.bar", "john.doe"}, + wantErr: false, + }, + { + name: "Expand emails in group", + args: args{ + aclPolicy: ACLPolicy{ + Groups: Groups{ + "group:admin": []string{ + "joe.bar@gmail.com", + "john.doe@yahoo.fr", + }, + }, + }, + group: "group:admin", + stripEmailDomain: false, + }, + want: []string{"joe.bar.gmail.com", "john.doe.yahoo.fr"}, + wantErr: false, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - got, err := expandGroup(test.args.aclPolicy, test.args.group) + got, err := expandGroup( + test.args.aclPolicy, + test.args.group, + test.args.stripEmailDomain, + ) if (err != nil) != test.wantErr { t.Errorf("expandGroup() error = %v, wantErr %v", err, test.wantErr) @@ -485,8 +526,9 @@ func Test_expandGroup(t *testing.T) { func Test_expandTagOwners(t *testing.T) { type args struct { - aclPolicy ACLPolicy - tag string + aclPolicy ACLPolicy + tag string + stripEmailDomain bool } tests := []struct { name string @@ -500,7 +542,8 @@ func Test_expandTagOwners(t *testing.T) { aclPolicy: ACLPolicy{ TagOwners: TagOwners{"tag:test": []string{"user1"}}, }, - tag: "tag:test", + tag: "tag:test", + stripEmailDomain: true, }, want: []string{"user1"}, wantErr: false, @@ -512,7 +555,8 @@ func Test_expandTagOwners(t *testing.T) { Groups: Groups{"group:foo": []string{"user1", "user2"}}, TagOwners: TagOwners{"tag:test": []string{"group:foo"}}, }, - tag: "tag:test", + tag: "tag:test", + stripEmailDomain: true, }, want: []string{"user1", "user2"}, wantErr: false, @@ -524,7 +568,8 @@ func Test_expandTagOwners(t *testing.T) { Groups: Groups{"group:foo": []string{"user1", "user2"}}, TagOwners: TagOwners{"tag:test": []string{"group:foo", "user3"}}, }, - tag: "tag:test", + tag: "tag:test", + stripEmailDomain: true, }, want: []string{"user1", "user2", "user3"}, wantErr: false, @@ -535,7 +580,8 @@ func Test_expandTagOwners(t *testing.T) { aclPolicy: ACLPolicy{ TagOwners: TagOwners{"tag:foo": []string{"group:foo", "user1"}}, }, - tag: "tag:test", + tag: "tag:test", + stripEmailDomain: true, }, want: []string{}, wantErr: true, @@ -547,7 +593,8 @@ func Test_expandTagOwners(t *testing.T) { Groups: Groups{"group:bar": []string{"user1", "user2"}}, TagOwners: TagOwners{"tag:test": []string{"group:foo", "user2"}}, }, - tag: "tag:test", + tag: "tag:test", + stripEmailDomain: true, }, want: []string{}, wantErr: true, @@ -555,7 +602,11 @@ func Test_expandTagOwners(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - got, err := expandTagOwners(test.args.aclPolicy, test.args.tag) + got, err := expandTagOwners( + test.args.aclPolicy, + test.args.tag, + test.args.stripEmailDomain, + ) if (err != nil) != test.wantErr { t.Errorf("expandTagOwners() error = %v, wantErr %v", err, test.wantErr) @@ -717,9 +768,10 @@ func Test_listMachinesInNamespace(t *testing.T) { // nolint func Test_expandAlias(t *testing.T) { type args struct { - machines []Machine - aclPolicy ACLPolicy - alias string + machines []Machine + aclPolicy ACLPolicy + alias string + stripEmailDomain bool } tests := []struct { name string @@ -739,7 +791,8 @@ func Test_expandAlias(t *testing.T) { }, }, }, - aclPolicy: ACLPolicy{}, + aclPolicy: ACLPolicy{}, + stripEmailDomain: true, }, want: []string{"*"}, wantErr: false, @@ -777,6 +830,7 @@ func Test_expandAlias(t *testing.T) { aclPolicy: ACLPolicy{ Groups: Groups{"group:accountant": []string{"joe", "marc"}}, }, + stripEmailDomain: true, }, want: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"}, wantErr: false, @@ -814,6 +868,7 @@ func Test_expandAlias(t *testing.T) { aclPolicy: ACLPolicy{ Groups: Groups{"group:accountant": []string{"joe", "marc"}}, }, + stripEmailDomain: true, }, want: []string{}, wantErr: true, @@ -821,9 +876,10 @@ func Test_expandAlias(t *testing.T) { { name: "simple ipaddress", args: args{ - alias: "10.0.0.3", - machines: []Machine{}, - aclPolicy: ACLPolicy{}, + alias: "10.0.0.3", + machines: []Machine{}, + aclPolicy: ACLPolicy{}, + stripEmailDomain: true, }, want: []string{"10.0.0.3"}, wantErr: false, @@ -838,6 +894,7 @@ func Test_expandAlias(t *testing.T) { "homeNetwork": netaddr.MustParseIPPrefix("192.168.1.0/24"), }, }, + stripEmailDomain: true, }, want: []string{"192.168.1.0/24"}, wantErr: false, @@ -845,9 +902,10 @@ func Test_expandAlias(t *testing.T) { { name: "simple host", args: args{ - alias: "10.0.0.1", - machines: []Machine{}, - aclPolicy: ACLPolicy{}, + alias: "10.0.0.1", + machines: []Machine{}, + aclPolicy: ACLPolicy{}, + stripEmailDomain: true, }, want: []string{"10.0.0.1"}, wantErr: false, @@ -855,9 +913,10 @@ func Test_expandAlias(t *testing.T) { { name: "simple CIDR", args: args{ - alias: "10.0.0.0/16", - machines: []Machine{}, - aclPolicy: ACLPolicy{}, + alias: "10.0.0.0/16", + machines: []Machine{}, + aclPolicy: ACLPolicy{}, + stripEmailDomain: true, }, want: []string{"10.0.0.0/16"}, wantErr: false, @@ -901,6 +960,7 @@ func Test_expandAlias(t *testing.T) { aclPolicy: ACLPolicy{ TagOwners: TagOwners{"tag:hr-webserver": []string{"joe"}}, }, + stripEmailDomain: true, }, want: []string{"100.64.0.1", "100.64.0.2"}, wantErr: false, @@ -941,6 +1001,7 @@ func Test_expandAlias(t *testing.T) { "tag:accountant-webserver": []string{"group:accountant"}, }, }, + stripEmailDomain: true, }, want: []string{}, wantErr: true, @@ -984,6 +1045,7 @@ func Test_expandAlias(t *testing.T) { aclPolicy: ACLPolicy{ TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}}, }, + stripEmailDomain: true, }, want: []string{"100.64.0.4"}, wantErr: false, @@ -995,6 +1057,7 @@ func Test_expandAlias(t *testing.T) { test.args.machines, test.args.aclPolicy, test.args.alias, + test.args.stripEmailDomain, ) if (err != nil) != test.wantErr { t.Errorf("expandAlias() error = %v, wantErr %v", err, test.wantErr) From b2dca80e7a2f0364d476972dc0d6de2e653939e5 Mon Sep 17 00:00:00 2001 From: Adrien Raffin-Caboisse Date: Tue, 1 Mar 2022 21:16:33 +0100 Subject: [PATCH 2/4] docs: update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2b93ef6..c1b3ee8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ **Features**: - Add support for writing ACL files with YAML [#359](https://github.com/juanfont/headscale/pull/359) +- Users can now use emails in ACL's groups [#372](https://github.com/juanfont/headscale/issues/372) **Changes**: From 361b4f7f4f48fc399da149ffc2bf4b8d5e76c074 Mon Sep 17 00:00:00 2001 From: Adrien Raffin-Caboisse Date: Tue, 1 Mar 2022 22:43:25 +0100 Subject: [PATCH 3/4] fix(machine): allow to use * in ACL sources --- machine.go | 20 ++++- machine_test.go | 205 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 223 insertions(+), 2 deletions(-) diff --git a/machine.go b/machine.go index 3106d0b2..479380a0 100644 --- a/machine.go +++ b/machine.go @@ -173,6 +173,12 @@ func getFilteredByACLPeers( machine.IPAddresses.ToStringSlice(), peer.IPAddresses.ToStringSlice(), ) || // match source and destination + matchSourceAndDestinationWithRule( + rule.SrcIPs, + dst, + peer.IPAddresses.ToStringSlice(), + machine.IPAddresses.ToStringSlice(), + ) || // match return path matchSourceAndDestinationWithRule( rule.SrcIPs, dst, @@ -182,9 +188,21 @@ func getFilteredByACLPeers( matchSourceAndDestinationWithRule( rule.SrcIPs, dst, + []string{"*"}, + []string{"*"}, + ) || // match source and all destination + matchSourceAndDestinationWithRule( + rule.SrcIPs, + dst, + []string{"*"}, peer.IPAddresses.ToStringSlice(), + ) || // match source and all destination + matchSourceAndDestinationWithRule( + rule.SrcIPs, + dst, + []string{"*"}, machine.IPAddresses.ToStringSlice(), - ) { // match return path + ) { // match all sources and source peers[peer.ID] = peer } } diff --git a/machine_test.go b/machine_test.go index e9c91f8b..ed95aaf7 100644 --- a/machine_test.go +++ b/machine_test.go @@ -296,6 +296,7 @@ func (s *Suite) TestSerdeAddressStrignSlice(c *check.C) { } } +// nolint func Test_getFilteredByACLPeers(t *testing.T) { type args struct { machines []Machine @@ -443,7 +444,7 @@ func Test_getFilteredByACLPeers(t *testing.T) { }, }, machine: &Machine{ // current machine - ID: 1, + ID: 2, IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")}, Namespace: Namespace{Name: "marc"}, }, @@ -456,6 +457,208 @@ func Test_getFilteredByACLPeers(t *testing.T) { }, }, }, + { + name: "rules allows all hosts to reach one destination", + args: args{ + machines: []Machine{ // list of all machines in the database + { + ID: 1, + IPAddresses: MachineAddresses{ + netaddr.MustParseIP("100.64.0.1"), + }, + Namespace: Namespace{Name: "joe"}, + }, + { + ID: 2, + IPAddresses: MachineAddresses{ + netaddr.MustParseIP("100.64.0.2"), + }, + Namespace: Namespace{Name: "marc"}, + }, + { + ID: 3, + IPAddresses: MachineAddresses{ + netaddr.MustParseIP("100.64.0.3"), + }, + Namespace: Namespace{Name: "mickael"}, + }, + }, + rules: []tailcfg.FilterRule{ // list of all ACLRules registered + { + SrcIPs: []string{"*"}, + DstPorts: []tailcfg.NetPortRange{ + {IP: "100.64.0.2"}, + }, + }, + }, + machine: &Machine{ // current machine + ID: 1, + IPAddresses: MachineAddresses{ + netaddr.MustParseIP("100.64.0.1"), + }, + Namespace: Namespace{Name: "joe"}, + }, + }, + want: Machines{ + { + ID: 2, + IPAddresses: MachineAddresses{ + netaddr.MustParseIP("100.64.0.2"), + }, + Namespace: Namespace{Name: "marc"}, + }, + }, + }, + { + name: "rules allows all hosts to reach one destination, destination can reach all hosts", + args: args{ + machines: []Machine{ // list of all machines in the database + { + ID: 1, + IPAddresses: MachineAddresses{ + netaddr.MustParseIP("100.64.0.1"), + }, + Namespace: Namespace{Name: "joe"}, + }, + { + ID: 2, + IPAddresses: MachineAddresses{ + netaddr.MustParseIP("100.64.0.2"), + }, + Namespace: Namespace{Name: "marc"}, + }, + { + ID: 3, + IPAddresses: MachineAddresses{ + netaddr.MustParseIP("100.64.0.3"), + }, + Namespace: Namespace{Name: "mickael"}, + }, + }, + rules: []tailcfg.FilterRule{ // list of all ACLRules registered + { + SrcIPs: []string{"*"}, + DstPorts: []tailcfg.NetPortRange{ + {IP: "100.64.0.2"}, + }, + }, + }, + machine: &Machine{ // current machine + ID: 2, + IPAddresses: MachineAddresses{ + netaddr.MustParseIP("100.64.0.2"), + }, + Namespace: Namespace{Name: "marc"}, + }, + }, + want: Machines{ + { + ID: 1, + IPAddresses: MachineAddresses{ + netaddr.MustParseIP("100.64.0.1"), + }, + Namespace: Namespace{Name: "joe"}, + }, + { + ID: 3, + IPAddresses: MachineAddresses{ + netaddr.MustParseIP("100.64.0.3"), + }, + Namespace: Namespace{Name: "mickael"}, + }, + }, + }, + { + name: "rule allows all hosts to reach all destinations", + args: args{ + machines: []Machine{ // list of all machines in the database + { + ID: 1, + IPAddresses: MachineAddresses{ + netaddr.MustParseIP("100.64.0.1"), + }, + Namespace: Namespace{Name: "joe"}, + }, + { + ID: 2, + IPAddresses: MachineAddresses{ + netaddr.MustParseIP("100.64.0.2"), + }, + Namespace: Namespace{Name: "marc"}, + }, + { + ID: 3, + IPAddresses: MachineAddresses{ + netaddr.MustParseIP("100.64.0.3"), + }, + Namespace: Namespace{Name: "mickael"}, + }, + }, + rules: []tailcfg.FilterRule{ // list of all ACLRules registered + { + SrcIPs: []string{"*"}, + DstPorts: []tailcfg.NetPortRange{ + {IP: "*"}, + }, + }, + }, + machine: &Machine{ // current machine + ID: 2, + IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")}, + Namespace: Namespace{Name: "marc"}, + }, + }, + want: Machines{ + { + ID: 1, + IPAddresses: MachineAddresses{ + netaddr.MustParseIP("100.64.0.1"), + }, + Namespace: Namespace{Name: "joe"}, + }, + { + ID: 3, + IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.3")}, + Namespace: Namespace{Name: "mickael"}, + }, + }, + }, + { + name: "without rule all communications are forbidden", + args: args{ + machines: []Machine{ // list of all machines in the database + { + ID: 1, + IPAddresses: MachineAddresses{ + netaddr.MustParseIP("100.64.0.1"), + }, + Namespace: Namespace{Name: "joe"}, + }, + { + ID: 2, + IPAddresses: MachineAddresses{ + netaddr.MustParseIP("100.64.0.2"), + }, + Namespace: Namespace{Name: "marc"}, + }, + { + ID: 3, + IPAddresses: MachineAddresses{ + netaddr.MustParseIP("100.64.0.3"), + }, + Namespace: Namespace{Name: "mickael"}, + }, + }, + rules: []tailcfg.FilterRule{ // list of all ACLRules registered + }, + machine: &Machine{ // current machine + ID: 2, + IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")}, + Namespace: Namespace{Name: "marc"}, + }, + }, + want: Machines{}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 77fe0b01f7d42f51e67ea0307cf52c2c975844ab Mon Sep 17 00:00:00 2001 From: Adrien Raffin-Caboisse Date: Tue, 1 Mar 2022 22:50:22 +0100 Subject: [PATCH 4/4] docs: update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1b3ee8f..d75388b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ **Changes**: - Fix a bug were the same IP could be assigned to multiple hosts if joined in quick succession [#346](https://github.com/juanfont/headscale/pull/346) +- Fix a limitation in the ACLs that prevented users to write rules with `*` as source [#374](https://github.com/juanfont/headscale/issues/374) **0.14.0 (2022-02-24):**