feat: add support for node attributes

This commit is contained in:
Johan Siebens
2024-01-04 15:53:26 +01:00
parent 8a3f47490e
commit 9b5f045849
11 changed files with 455 additions and 69 deletions
+50
View File
@@ -34,6 +34,7 @@ type ACLPolicy struct {
TagOwners map[string][]string `json:"tagowners,omitempty"`
AutoApprovers *AutoApprovers `json:"autoApprovers,omitempty"`
SSHRules []SSHRule `json:"ssh,omitempty"`
NodeAttrs []NodeAttr `json:"nodeAttrs,omitempty"`
}
type ACL struct {
@@ -50,6 +51,11 @@ type SSHRule struct {
CheckPeriod string `json:"checkPeriod,omitempty"`
}
type NodeAttr struct {
Target []string `json:"target"`
Attr []string `json:"attr"`
}
func DefaultACLPolicy() ACLPolicy {
return ACLPolicy{
ACLs: []ACL{
@@ -201,6 +207,50 @@ func (a ACLPolicy) IsValidPeer(src *Machine, dest *Machine) bool {
return false
}
func (a ACLPolicy) NodeCapabilities(m *Machine) []tailcfg.NodeCapability {
var result = &StringSet{}
matches := func(targets []string) bool {
for _, alias := range targets {
if alias == "*" {
return true
}
if strings.Contains(alias, "@") && !m.HasTags() && m.HasUser(alias) {
return true
}
if strings.HasPrefix(alias, "tag:") && m.HasTag(alias) {
return true
}
if strings.HasPrefix(alias, "group:") && !m.HasTags() {
for _, u := range a.Groups[alias] {
if m.HasUser(u) {
return true
}
}
}
}
return false
}
for _, nodeAddr := range a.NodeAttrs {
if matches(nodeAddr.Target) {
result.Add(nodeAddr.Attr...)
}
}
items := result.Items()
caps := make([]tailcfg.NodeCapability, len(items))
for i, c := range items {
caps[i] = tailcfg.NodeCapability(c)
}
return caps
}
func (a ACLPolicy) BuildFilterRules(srcs []Machine, dst *Machine) []tailcfg.FilterRule {
var rules []tailcfg.FilterRule
+95
View File
@@ -9,6 +9,101 @@ import (
"testing"
)
func TestACLPolicy_NodeAttributesWithWildcards(t *testing.T) {
p1 := createMachine("john@example.com")
policy := ACLPolicy{
NodeAttrs: []NodeAttr{
{
Target: []string{"*"},
Attr: []string{
"attr1",
"attr2",
},
},
{
Target: []string{"*"},
Attr: []string{
"attr3",
},
},
},
}
actualAttrs := policy.NodeCapabilities(p1)
expectedAttrs := []tailcfg.NodeCapability{
tailcfg.NodeCapability("attr1"),
tailcfg.NodeCapability("attr2"),
tailcfg.NodeCapability("attr3"),
}
assert.Equal(t, expectedAttrs, actualAttrs)
}
func TestACLPolicy_NodeAttributesWithUserAndGroups(t *testing.T) {
p1 := createMachine("john@example.com")
policy := ACLPolicy{
Groups: map[string][]string{
"group:admins": []string{"john@example.com"},
},
NodeAttrs: []NodeAttr{
{
Target: []string{"john@example.com"},
Attr: []string{
"attr1",
"attr2",
},
},
{
Target: []string{"jane@example.com", "group:analytics", "group:admins"},
Attr: []string{
"attr3",
},
},
},
}
actualAttrs := policy.NodeCapabilities(p1)
expectedAttrs := []tailcfg.NodeCapability{
tailcfg.NodeCapability("attr1"),
tailcfg.NodeCapability("attr2"),
tailcfg.NodeCapability("attr3"),
}
assert.Equal(t, expectedAttrs, actualAttrs)
}
func TestACLPolicy_NodeAttributesWithUserAndTags(t *testing.T) {
p1 := createMachine("john@example.com", "tag:web")
policy := ACLPolicy{
Groups: map[string][]string{
"group:admins": []string{"john@example.com"},
},
NodeAttrs: []NodeAttr{
{
Target: []string{"john@example.com"},
Attr: []string{
"attr1",
"attr2",
},
},
{
Target: []string{"jane@example.com", "tag:web"},
Attr: []string{
"attr3",
},
},
},
}
actualAttrs := policy.NodeCapabilities(p1)
expectedAttrs := []tailcfg.NodeCapability{tailcfg.NodeCapability("attr3")}
assert.Equal(t, expectedAttrs, actualAttrs)
}
func TestACLPolicy_BuildFilterRulesWildcards(t *testing.T) {
p1 := createMachine("john@example.com")
p2 := createMachine("jane@example.com")
+13
View File
@@ -7,6 +7,7 @@ import (
"github.com/jsiebens/ionscale/internal/domain"
"github.com/jsiebens/ionscale/internal/util"
"net/netip"
"slices"
"strconv"
"tailscale.com/tailcfg"
"tailscale.com/types/dnstype"
@@ -176,6 +177,12 @@ func ToNode(capVer tailcfg.CapabilityVersion, m *domain.Machine, tailnet *domain
if !peer {
var capabilities []tailcfg.NodeCapability
capMap := make(tailcfg.NodeCapMap)
for _, c := range tailnet.ACLPolicy.NodeCapabilities(m) {
capabilities = append(capabilities, c)
capMap[c] = []tailcfg.RawMessage{}
}
if !m.HasTags() && role == domain.UserRoleAdmin {
capabilities = append(capabilities, tailcfg.CapabilityAdmin)
capMap[tailcfg.CapabilityAdmin] = []tailcfg.RawMessage{}
@@ -196,6 +203,12 @@ func ToNode(capVer tailcfg.CapabilityVersion, m *domain.Machine, tailnet *domain
capMap[tailcfg.CapabilityHTTPS] = []tailcfg.RawMessage{}
}
// ionscale has no support for Funnel yet, so remove Funnel attribute if set via ACL policy
{
slices.DeleteFunc(capabilities, func(c tailcfg.NodeCapability) bool { return c == tailcfg.NodeAttrFunnel })
delete(capMap, tailcfg.NodeAttrFunnel)
}
if capVer >= 74 {
n.CapMap = capMap
} else {