Skip to content

Commit

Permalink
feat: add support for node attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
jsiebens committed Jan 5, 2024
1 parent 8a3f474 commit 9b5f045
Show file tree
Hide file tree
Showing 11 changed files with 455 additions and 69 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/integration.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
name: Integration Tests

on: workflow_dispatch
on:
workflow_dispatch: {}
pull_request:
branches:
- main

jobs:
integration:
Expand Down
50 changes: 50 additions & 0 deletions internal/domain/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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{
Expand Down Expand Up @@ -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

Expand Down
95 changes: 95 additions & 0 deletions internal/domain/acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
13 changes: 13 additions & 0 deletions internal/mapping/mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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{}
Expand All @@ -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 {
Expand Down
Loading

0 comments on commit 9b5f045

Please sign in to comment.