feat: use hujson as data format for ACL and IAM policy

This commit is contained in:
Johan Siebens
2024-03-14 08:23:24 +01:00
parent a1debdffb8
commit 6173621730
36 changed files with 752 additions and 1415 deletions
+4 -30
View File
@@ -2,7 +2,6 @@ package cmd
import (
"bytes"
"encoding/json"
"fmt"
"github.com/bufbuild/connect-go"
"github.com/jsiebens/go-edit/editor"
@@ -25,12 +24,7 @@ func getACLConfigCommand() *cobra.Command {
return err
}
marshal, err := json.MarshalIndent(resp.Msg.Policy, "", " ")
if err != nil {
return err
}
fmt.Println(string(marshal))
fmt.Println(resp.Msg.Policy)
return nil
}
@@ -53,12 +47,7 @@ func editACLConfigCommand() *cobra.Command {
return err
}
previous, err := json.MarshalIndent(resp.Msg.Policy, "", " ")
if err != nil {
return err
}
next, s, err := edit.LaunchTempFile("ionscale", ".json", bytes.NewReader(previous))
next, s, err := edit.LaunchTempFile("ionscale", ".json", bytes.NewReader([]byte(resp.Msg.Policy)))
if err != nil {
return err
}
@@ -70,12 +59,7 @@ func editACLConfigCommand() *cobra.Command {
return err
}
var policy = &api.ACLPolicy{}
if err := json.Unmarshal(next, policy); err != nil {
return err
}
_, err = tc.Client().SetACLPolicy(cmd.Context(), connect.NewRequest(&api.SetACLPolicyRequest{TailnetId: tc.TailnetID(), Policy: policy}))
_, err = tc.Client().SetACLPolicy(cmd.Context(), connect.NewRequest(&api.SetACLPolicyRequest{TailnetId: tc.TailnetID(), Policy: string(next)}))
if err != nil {
return err
}
@@ -105,17 +89,7 @@ func setACLConfigCommand() *cobra.Command {
return err
}
rawJson, err := hujson.Standardize(content)
if err != nil {
return err
}
var policy = &api.ACLPolicy{}
if err := json.Unmarshal(rawJson, policy); err != nil {
return err
}
_, err = tc.Client().SetACLPolicy(cmd.Context(), connect.NewRequest(&api.SetACLPolicyRequest{TailnetId: tc.TailnetID(), Policy: policy}))
_, err = tc.Client().SetACLPolicy(cmd.Context(), connect.NewRequest(&api.SetACLPolicyRequest{TailnetId: tc.TailnetID(), Policy: string(content)}))
if err != nil {
return err
}
+4 -36
View File
@@ -2,13 +2,11 @@ package cmd
import (
"bytes"
"encoding/json"
"fmt"
"github.com/bufbuild/connect-go"
"github.com/jsiebens/go-edit/editor"
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
"github.com/spf13/cobra"
"github.com/tailscale/hujson"
"os"
)
@@ -25,12 +23,7 @@ func getIAMPolicyCommand() *cobra.Command {
return err
}
marshal, err := json.MarshalIndent(resp.Msg.Policy, "", " ")
if err != nil {
return err
}
fmt.Println(string(marshal))
fmt.Println(resp.Msg.Policy)
return nil
}
@@ -53,29 +46,14 @@ func editIAMPolicyCommand() *cobra.Command {
return err
}
previous, err := json.MarshalIndent(resp.Msg.Policy, "", " ")
if err != nil {
return err
}
next, s, err := edit.LaunchTempFile("ionscale", ".json", bytes.NewReader(previous))
if err != nil {
return err
}
next, err = hujson.Standardize(next)
next, s, err := edit.LaunchTempFile("ionscale", ".json", bytes.NewReader([]byte(resp.Msg.Policy)))
if err != nil {
return err
}
defer os.Remove(s)
var policy = &api.IAMPolicy{}
if err := json.Unmarshal(next, policy); err != nil {
return err
}
_, err = tc.Client().SetIAMPolicy(cmd.Context(), connect.NewRequest(&api.SetIAMPolicyRequest{TailnetId: tc.TailnetID(), Policy: policy}))
_, err = tc.Client().SetIAMPolicy(cmd.Context(), connect.NewRequest(&api.SetIAMPolicyRequest{TailnetId: tc.TailnetID(), Policy: string(next)}))
if err != nil {
return err
}
@@ -105,17 +83,7 @@ func setIAMPolicyCommand() *cobra.Command {
return err
}
rawJson, err := hujson.Standardize(content)
if err != nil {
return err
}
var policy = &api.IAMPolicy{}
if err := json.Unmarshal(rawJson, policy); err != nil {
return err
}
_, err = tc.Client().SetIAMPolicy(cmd.Context(), connect.NewRequest(&api.SetIAMPolicyRequest{TailnetId: tc.TailnetID(), Policy: policy}))
_, err = tc.Client().SetIAMPolicy(cmd.Context(), connect.NewRequest(&api.SetIAMPolicyRequest{TailnetId: tc.TailnetID(), Policy: string(content)}))
if err != nil {
return err
}
+13 -4
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"github.com/bufbuild/connect-go"
idomain "github.com/jsiebens/ionscale/internal/domain"
"github.com/jsiebens/ionscale/pkg/client/ionscale"
"github.com/jsiebens/ionscale/pkg/defaults"
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
"github.com/rodaine/table"
@@ -102,24 +103,32 @@ func createTailnetsCommand() *cobra.Command {
command.RunE = func(cmd *cobra.Command, args []string) error {
dnsConfig := defaults.DefaultDNSConfig()
aclPolicy := defaults.DefaultACLPolicy()
iamPolicy := &api.IAMPolicy{}
aclPolicy := defaults.DefaultACLPolicy().Marshal()
iamPolicy := "{}"
if len(domain) != 0 {
domainToLower := strings.ToLower(domain)
iamPolicy = &api.IAMPolicy{
m, err := json.MarshalIndent(&ionscale.IAMPolicy{
Filters: []string{fmt.Sprintf("domain == %s", domainToLower)},
}, "", " ")
if err != nil {
return err
}
iamPolicy = string(m)
}
if len(email) != 0 {
emailToLower := strings.ToLower(email)
iamPolicy = &api.IAMPolicy{
m, err := json.MarshalIndent(&ionscale.IAMPolicy{
Emails: []string{emailToLower},
Roles: map[string]string{
emailToLower: string(idomain.UserRoleAdmin),
},
}, "", " ")
if err != nil {
return err
}
iamPolicy = string(m)
}
resp, err := tc.Client().CreateTailnet(cmd.Context(), connect.NewRequest(&api.CreateTailnetRequest{
@@ -0,0 +1,29 @@
package migration
import (
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
func m202403130830_json_to_text() *gormigrate.Migration {
return &gormigrate.Migration{
ID: "202403130830",
Migrate: func(db *gorm.DB) error {
type Tailnet struct {
IAMPolicy string
ACLPolicy string
}
if err := db.Migrator().AlterColumn(&Tailnet{}, "IAMPolicy"); err != nil {
return err
}
if err := db.Migrator().AlterColumn(&Tailnet{}, "ACLPolicy"); err != nil {
return err
}
return nil
},
Rollback: nil,
}
}
@@ -20,6 +20,7 @@ func Migrations() []*gormigrate.Migration {
m202312290900_machine_indeces(),
m202401061400_machine_indeces(),
m202402120800_user_last_authenticated(),
m202403130830_json_to_text(),
}
return migrations
}
+2 -35
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"github.com/hashicorp/go-multierror"
"github.com/jsiebens/ionscale/pkg/client/ionscale"
"gorm.io/gorm"
"gorm.io/gorm/schema"
"net/netip"
@@ -30,41 +31,7 @@ type AutoApprovers struct {
}
type ACLPolicy struct {
Groups map[string][]string `json:"groups,omitempty"`
Hosts map[string]string `json:"hosts,omitempty"`
ACLs []ACL `json:"acls,omitempty"`
TagOwners map[string][]string `json:"tagowners,omitempty"`
AutoApprovers *AutoApprovers `json:"autoApprovers,omitempty"`
SSHRules []SSHRule `json:"ssh,omitempty"`
NodeAttrs []NodeAttr `json:"nodeAttrs,omitempty"`
Grants []Grant `json:"grants,omitempty"`
}
type ACL struct {
Action string `json:"action"`
Proto string `json:"proto"`
Src []string `json:"src"`
Dst []string `json:"dst"`
}
type SSHRule struct {
Action string `json:"action"`
Src []string `json:"src"`
Dst []string `json:"dst"`
Users []string `json:"users"`
CheckPeriod string `json:"checkPeriod,omitempty"`
}
type NodeAttr struct {
Target []string `json:"target"`
Attr []string `json:"attr"`
}
type Grant struct {
Src []string `json:"src"`
Dst []string `json:"dst"`
IP []tailcfg.ProtoPortRange `json:"ip"`
App tailcfg.PeerCapMap `json:"app"`
ionscale.ACLPolicy
}
func (a *ACLPolicy) Equal(x *ACLPolicy) bool {
+16 -15
View File
@@ -1,6 +1,7 @@
package domain
import (
"github.com/jsiebens/ionscale/pkg/client/ionscale"
"net/netip"
"strings"
"tailscale.com/tailcfg"
@@ -12,16 +13,16 @@ func (a ACLPolicy) IsValidPeer(src *Machine, dest *Machine) bool {
}
for _, acl := range a.ACLs {
selfDestPorts, allDestPorts := a.translateDestinationAliasesToMachineNetPortRanges(acl.Dst, dest)
selfDestPorts, allDestPorts := a.translateDestinationAliasesToMachineNetPortRanges(acl.Destination, dest)
if len(selfDestPorts) != 0 {
for _, alias := range acl.Src {
for _, alias := range acl.Source {
if len(a.translateSourceAliasToMachineIPs(alias, src, &dest.User)) != 0 {
return true
}
}
}
if len(allDestPorts) != 0 {
for _, alias := range acl.Src {
for _, alias := range acl.Source {
if len(a.translateSourceAliasToMachineIPs(alias, src, nil)) != 0 {
return true
}
@@ -30,16 +31,16 @@ func (a ACLPolicy) IsValidPeer(src *Machine, dest *Machine) bool {
}
for _, grant := range a.Grants {
selfIps, otherIps := a.translateDestinationAliasesToMachineIPs(grant.Dst, dest)
selfIps, otherIps := a.translateDestinationAliasesToMachineIPs(grant.Destination, dest)
if len(selfIps) != 0 {
for _, alias := range grant.Src {
for _, alias := range grant.Source {
if len(a.translateSourceAliasToMachineIPs(alias, src, &dest.User)) != 0 {
return true
}
}
}
if len(otherIps) != 0 {
for _, alias := range grant.Src {
for _, alias := range grant.Source {
if len(a.translateSourceAliasToMachineIPs(alias, src, nil)) != 0 {
return true
}
@@ -89,23 +90,23 @@ func (a ACLPolicy) BuildFilterRules(peers []Machine, dst *Machine) []tailcfg.Fil
for _, acl := range a.ACLs {
self, other := a.prepareFilterRulesFromACL(dst, acl)
rules = matchSourceAndAppendRule(rules, acl.Src, self, &dst.User)
rules = matchSourceAndAppendRule(rules, acl.Src, other, nil)
rules = matchSourceAndAppendRule(rules, acl.Source, self, &dst.User)
rules = matchSourceAndAppendRule(rules, acl.Source, other, nil)
}
for _, acl := range a.Grants {
self, other := a.prepareFilterRulesFromGrant(dst, acl)
rules = matchSourceAndAppendRule(rules, acl.Src, self, &dst.User)
rules = matchSourceAndAppendRule(rules, acl.Src, other, nil)
rules = matchSourceAndAppendRule(rules, acl.Source, self, &dst.User)
rules = matchSourceAndAppendRule(rules, acl.Source, other, nil)
}
return rules
}
func (a ACLPolicy) prepareFilterRulesFromACL(candidate *Machine, acl ACL) ([]tailcfg.FilterRule, []tailcfg.FilterRule) {
proto := parseProtocol(acl.Proto)
func (a ACLPolicy) prepareFilterRulesFromACL(candidate *Machine, acl ionscale.ACLEntry) ([]tailcfg.FilterRule, []tailcfg.FilterRule) {
proto := parseProtocol(acl.Protocol)
selfDstPorts, otherDstPorts := a.translateDestinationAliasesToMachineNetPortRanges(acl.Dst, candidate)
selfDstPorts, otherDstPorts := a.translateDestinationAliasesToMachineNetPortRanges(acl.Destination, candidate)
var selfFilterRules []tailcfg.FilterRule
var otherFilterRules []tailcfg.FilterRule
@@ -121,8 +122,8 @@ func (a ACLPolicy) prepareFilterRulesFromACL(candidate *Machine, acl ACL) ([]tai
return selfFilterRules, otherFilterRules
}
func (a ACLPolicy) prepareFilterRulesFromGrant(candidate *Machine, grant Grant) ([]tailcfg.FilterRule, []tailcfg.FilterRule) {
selfIPs, otherIPs := a.translateDestinationAliasesToMachineIPs(grant.Dst, candidate)
func (a ACLPolicy) prepareFilterRulesFromGrant(candidate *Machine, grant ionscale.ACLGrant) ([]tailcfg.FilterRule, []tailcfg.FilterRule) {
selfIPs, otherIPs := a.translateDestinationAliasesToMachineIPs(grant.Destination, candidate)
var selfFilterRules []tailcfg.FilterRule
var otherFilterRules []tailcfg.FilterRule
+6 -5
View File
@@ -1,6 +1,7 @@
package domain
import (
"github.com/jsiebens/ionscale/pkg/client/ionscale"
"strings"
"tailscale.com/tailcfg"
)
@@ -28,7 +29,7 @@ func (a ACLPolicy) BuildSSHPolicy(srcs []Machine, dst *Machine) *tailcfg.SSHPoli
return result
}
for _, rule := range a.SSHRules {
for _, rule := range a.SSH {
if rule.Action != "accept" && rule.Action != "check" {
continue
}
@@ -48,7 +49,7 @@ func (a ACLPolicy) BuildSSHPolicy(srcs []Machine, dst *Machine) *tailcfg.SSHPoli
selfUsers, otherUsers := a.expandSSHDstToSSHUsers(dst, rule)
if len(selfUsers) != 0 {
principals := expandSrcAliases(rule.Src, rule.Action, &dst.User)
principals := expandSrcAliases(rule.Source, rule.Action, &dst.User)
if len(principals) != 0 {
rules = append(rules, &tailcfg.SSHRule{
Principals: principals,
@@ -59,7 +60,7 @@ func (a ACLPolicy) BuildSSHPolicy(srcs []Machine, dst *Machine) *tailcfg.SSHPoli
}
if len(otherUsers) != 0 {
principals := expandSrcAliases(rule.Src, rule.Action, nil)
principals := expandSrcAliases(rule.Source, rule.Action, nil)
if len(principals) != 0 {
rules = append(rules, &tailcfg.SSHRule{
Principals: principals,
@@ -113,13 +114,13 @@ func (a ACLPolicy) expandSSHSrcAlias(m *Machine, alias string, dstUser *User) []
return []string{}
}
func (a ACLPolicy) expandSSHDstToSSHUsers(m *Machine, rule SSHRule) (map[string]string, map[string]string) {
func (a ACLPolicy) expandSSHDstToSSHUsers(m *Machine, rule ionscale.ACLSSH) (map[string]string, map[string]string) {
users := buildSSHUsers(rule.Users)
var selfUsers map[string]string
var otherUsers map[string]string
for _, d := range rule.Dst {
for _, d := range rule.Destination {
if strings.HasPrefix(d, "tag:") && m.HasTag(d) {
otherUsers = users
}
+96 -73
View File
@@ -3,6 +3,7 @@ package domain
import (
"encoding/json"
"fmt"
"github.com/jsiebens/ionscale/pkg/client/ionscale"
"github.com/stretchr/testify/assert"
"tailscale.com/tailcfg"
"testing"
@@ -13,12 +14,14 @@ func TestACLPolicy_BuildSSHPolicy_(t *testing.T) {
p2 := createMachine("jane@example.com")
policy := ACLPolicy{
SSHRules: []SSHRule{
{
Action: "accept",
Src: []string{"autogroup:members"},
Dst: []string{"autogroup:self"},
Users: []string{"autogroup:nonroot"},
ionscale.ACLPolicy{
SSH: []ionscale.ACLSSH{
{
Action: "accept",
Source: []string{"autogroup:members"},
Destination: []string{"autogroup:self"},
Users: []string{"autogroup:nonroot"},
},
},
},
}
@@ -52,17 +55,19 @@ func TestACLPolicy_BuildSSHPolicy_WithGroup(t *testing.T) {
p2 := createMachine("jane@example.com")
policy := ACLPolicy{
Groups: map[string][]string{
"group:sre": {
"john@example.com",
ionscale.ACLPolicy{
Groups: map[string][]string{
"group:sre": {
"john@example.com",
},
},
},
SSHRules: []SSHRule{
{
Action: "accept",
Src: []string{"group:sre"},
Dst: []string{"tag:web"},
Users: []string{"autogroup:nonroot", "root"},
SSH: []ionscale.ACLSSH{
{
Action: "accept",
Source: []string{"group:sre"},
Destination: []string{"tag:web"},
Users: []string{"autogroup:nonroot", "root"},
},
},
},
}
@@ -96,12 +101,14 @@ func TestACLPolicy_BuildSSHPolicy_WithMatchingUsers(t *testing.T) {
p2 := createMachine("jane@example.com")
policy := ACLPolicy{
SSHRules: []SSHRule{
{
Action: "accept",
Src: []string{"john@example.com"},
Dst: []string{"john@example.com"},
Users: []string{"autogroup:nonroot", "root"},
ionscale.ACLPolicy{
SSH: []ionscale.ACLSSH{
{
Action: "accept",
Source: []string{"john@example.com"},
Destination: []string{"john@example.com"},
Users: []string{"autogroup:nonroot", "root"},
},
},
},
}
@@ -132,15 +139,17 @@ func TestACLPolicy_BuildSSHPolicy_WithMatchingUsersInGroup(t *testing.T) {
p2 := createMachine("jane@example.com")
policy := ACLPolicy{
Groups: map[string][]string{
"group:sre": {"jane@example.com", "john@example.com"},
},
SSHRules: []SSHRule{
{
Action: "accept",
Src: []string{"group:sre"},
Dst: []string{"john@example.com"},
Users: []string{"autogroup:nonroot", "root"},
ionscale.ACLPolicy{
Groups: map[string][]string{
"group:sre": {"jane@example.com", "john@example.com"},
},
SSH: []ionscale.ACLSSH{
{
Action: "accept",
Source: []string{"group:sre"},
Destination: []string{"john@example.com"},
Users: []string{"autogroup:nonroot", "root"},
},
},
},
}
@@ -171,12 +180,14 @@ func TestACLPolicy_BuildSSHPolicy_WithNoMatchingUsers(t *testing.T) {
p2 := createMachine("jane@example.com")
policy := ACLPolicy{
SSHRules: []SSHRule{
{
Action: "accept",
Src: []string{"jane@example.com"},
Dst: []string{"john@example.com"},
Users: []string{"autogroup:nonroot", "root"},
ionscale.ACLPolicy{
SSH: []ionscale.ACLSSH{
{
Action: "accept",
Source: []string{"jane@example.com"},
Destination: []string{"john@example.com"},
Users: []string{"autogroup:nonroot", "root"},
},
},
},
}
@@ -194,12 +205,14 @@ func TestACLPolicy_BuildSSHPolicy_WithTags(t *testing.T) {
p3 := createMachine("nick@example.com", "tag:web")
policy := ACLPolicy{
SSHRules: []SSHRule{
{
Action: "accept",
Src: []string{"john@example.com", "tag:web"},
Dst: []string{"tag:web"},
Users: []string{"ubuntu"},
ionscale.ACLPolicy{
SSH: []ionscale.ACLSSH{
{
Action: "accept",
Source: []string{"john@example.com", "tag:web"},
Destination: []string{"tag:web"},
Users: []string{"ubuntu"},
},
},
},
}
@@ -230,12 +243,14 @@ func TestACLPolicy_BuildSSHPolicy_WithTagsInDstAndAutogroupMemberInSrc(t *testin
p3 := createMachine("nick@example.com", "tag:web")
policy := ACLPolicy{
SSHRules: []SSHRule{
{
Action: "accept",
Src: []string{"autogroup:members"},
Dst: []string{"tag:web"},
Users: []string{"ubuntu"},
ionscale.ACLPolicy{
SSH: []ionscale.ACLSSH{
{
Action: "accept",
Source: []string{"autogroup:members"},
Destination: []string{"tag:web"},
Users: []string{"ubuntu"},
},
},
},
}
@@ -265,12 +280,14 @@ func TestACLPolicy_BuildSSHPolicy_WithUserInDstAndNonMatchingSrc(t *testing.T) {
p2 := createMachine("jane@example.com")
policy := ACLPolicy{
SSHRules: []SSHRule{
{
Action: "accept",
Src: []string{"jane@example.com"},
Dst: []string{"john@example.com"},
Users: []string{"autogroup:nonroot"},
ionscale.ACLPolicy{
SSH: []ionscale.ACLSSH{
{
Action: "accept",
Source: []string{"jane@example.com"},
Destination: []string{"john@example.com"},
Users: []string{"autogroup:nonroot"},
},
},
},
}
@@ -287,12 +304,14 @@ func TestACLPolicy_BuildSSHPolicy_WithUserInDstAndAutogroupMembersSrc(t *testing
p2 := createMachine("jane@example.com")
policy := ACLPolicy{
SSHRules: []SSHRule{
{
Action: "accept",
Src: []string{"autogroup:members"},
Dst: []string{"john@example.com"},
Users: []string{"autogroup:nonroot"},
ionscale.ACLPolicy{
SSH: []ionscale.ACLSSH{
{
Action: "accept",
Source: []string{"autogroup:members"},
Destination: []string{"john@example.com"},
Users: []string{"autogroup:nonroot"},
},
},
},
}
@@ -323,12 +342,14 @@ func TestACLPolicy_BuildSSHPolicy_WithAutogroupSelfAndTagSrc(t *testing.T) {
p2 := createMachine("jane@example.com", "tag:web")
policy := ACLPolicy{
SSHRules: []SSHRule{
{
Action: "accept",
Src: []string{"tag:web"},
Dst: []string{"autogroup:self"},
Users: []string{"autogroup:nonroot"},
ionscale.ACLPolicy{
SSH: []ionscale.ACLSSH{
{
Action: "accept",
Source: []string{"tag:web"},
Destination: []string{"autogroup:self"},
Users: []string{"autogroup:nonroot"},
},
},
},
}
@@ -345,12 +366,14 @@ func TestACLPolicy_BuildSSHPolicy_WithTagsAndActionCheck(t *testing.T) {
p2 := createMachine("jane@example.com", "tag:web")
policy := ACLPolicy{
SSHRules: []SSHRule{
{
Action: "check",
Src: []string{"tag:web"},
Dst: []string{"tag:web"},
Users: []string{"autogroup:nonroot"},
ionscale.ACLPolicy{
SSH: []ionscale.ACLSSH{
{
Action: "check",
Source: []string{"tag:web"},
Destination: []string{"tag:web"},
Users: []string{"autogroup:nonroot"},
},
},
},
}
+206 -160
View File
@@ -3,6 +3,7 @@ package domain
import (
"encoding/json"
"github.com/jsiebens/ionscale/internal/addr"
"github.com/jsiebens/ionscale/pkg/client/ionscale"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"net/netip"
@@ -15,18 +16,20 @@ func TestACLPolicy_NodeAttributesWithWildcards(t *testing.T) {
p1 := createMachine("john@example.com")
policy := ACLPolicy{
NodeAttrs: []NodeAttr{
{
Target: []string{"*"},
Attr: []string{
"attr1",
"attr2",
ionscale.ACLPolicy{
NodeAttrs: []ionscale.ACLNodeAttrGrant{
{
Target: []string{"*"},
Attr: []string{
"attr1",
"attr2",
},
},
},
{
Target: []string{"*"},
Attr: []string{
"attr3",
{
Target: []string{"*"},
Attr: []string{
"attr3",
},
},
},
},
@@ -46,21 +49,23 @@ 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",
},
ionscale.ACLPolicy{
Groups: map[string][]string{
"group:admins": []string{"john@example.com"},
},
{
Target: []string{"jane@example.com", "group:analytics", "group:admins"},
Attr: []string{
"attr3",
NodeAttrs: []ionscale.ACLNodeAttrGrant{
{
Target: []string{"john@example.com"},
Attr: []string{
"attr1",
"attr2",
},
},
{
Target: []string{"jane@example.com", "group:analytics", "group:admins"},
Attr: []string{
"attr3",
},
},
},
},
@@ -80,21 +85,23 @@ 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",
},
ionscale.ACLPolicy{
Groups: map[string][]string{
"group:admins": []string{"john@example.com"},
},
{
Target: []string{"jane@example.com", "tag:web"},
Attr: []string{
"attr3",
NodeAttrs: []ionscale.ACLNodeAttrGrant{
{
Target: []string{"john@example.com"},
Attr: []string{
"attr1",
"attr2",
},
},
{
Target: []string{"jane@example.com", "tag:web"},
Attr: []string{
"attr3",
},
},
},
},
@@ -111,7 +118,9 @@ func TestACLPolicy_BuildFilterRulesEmptyACL(t *testing.T) {
p2 := createMachine("jane@example.com")
policy := ACLPolicy{
ACLs: []ACL{},
ionscale.ACLPolicy{
ACLs: []ionscale.ACLEntry{},
},
}
dst := createMachine("john@example.com")
@@ -127,11 +136,13 @@ func TestACLPolicy_BuildFilterRulesWildcards(t *testing.T) {
p2 := createMachine("jane@example.com")
policy := ACLPolicy{
ACLs: []ACL{
{
Action: "accept",
Src: []string{"*"},
Dst: []string{"*:*"},
ionscale.ACLPolicy{
ACLs: []ionscale.ACLEntry{
{
Action: "accept",
Source: []string{"*"},
Destination: []string{"*:*"},
},
},
},
}
@@ -162,17 +173,19 @@ func TestACLPolicy_BuildFilterRulesProto(t *testing.T) {
p2 := createMachine("jane@example.com")
policy := ACLPolicy{
ACLs: []ACL{
{
Action: "accept",
Src: []string{"*"},
Dst: []string{"*:22"},
},
{
Action: "accept",
Src: []string{"*"},
Dst: []string{"*:*"},
Proto: "igmp",
ionscale.ACLPolicy{
ACLs: []ionscale.ACLEntry{
{
Action: "accept",
Source: []string{"*"},
Destination: []string{"*:22"},
},
{
Action: "accept",
Source: []string{"*"},
Destination: []string{"*:*"},
Protocol: "igmp",
},
},
},
}
@@ -217,20 +230,22 @@ func TestACLPolicy_BuildFilterRulesWithGroups(t *testing.T) {
p3 := createMachine("joe@example.com")
policy := ACLPolicy{
Groups: map[string][]string{
"group:admin": []string{"jane@example.com"},
"group:audit": []string{"nick@example.com"},
},
ACLs: []ACL{
{
Action: "accept",
Src: []string{"group:admin"},
Dst: []string{"*:22"},
ionscale.ACLPolicy{
Groups: map[string][]string{
"group:admin": []string{"jane@example.com"},
"group:audit": []string{"nick@example.com"},
},
{
Action: "accept",
Src: []string{"group:audit"},
Dst: []string{"*:8000-8080"},
ACLs: []ionscale.ACLEntry{
{
Action: "accept",
Source: []string{"group:admin"},
Destination: []string{"*:22"},
},
{
Action: "accept",
Source: []string{"group:audit"},
Destination: []string{"*:8000-8080"},
},
},
},
}
@@ -280,11 +295,13 @@ func TestACLPolicy_BuildFilterRulesWithAutoGroupMembers(t *testing.T) {
p3 := createMachine("joe@example.com", "tag:web")
policy := ACLPolicy{
ACLs: []ACL{
{
Action: "accept",
Src: []string{"autogroup:members"},
Dst: []string{"*:22"},
ionscale.ACLPolicy{
ACLs: []ionscale.ACLEntry{
{
Action: "accept",
Source: []string{"autogroup:members"},
Destination: []string{"*:22"},
},
},
},
}
@@ -323,11 +340,13 @@ func TestACLPolicy_BuildFilterRulesWithAutoGroupMember(t *testing.T) {
p3 := createMachine("joe@example.com", "tag:web")
policy := ACLPolicy{
ACLs: []ACL{
{
Action: "accept",
Src: []string{"autogroup:member"},
Dst: []string{"*:22"},
ionscale.ACLPolicy{
ACLs: []ionscale.ACLEntry{
{
Action: "accept",
Source: []string{"autogroup:member"},
Destination: []string{"*:22"},
},
},
},
}
@@ -367,11 +386,13 @@ func TestACLPolicy_BuildFilterRulesWithAutoGroupTagged(t *testing.T) {
p3 := createMachine("joe@example.com", "tag:web")
policy := ACLPolicy{
ACLs: []ACL{
{
Action: "accept",
Src: []string{"autogroup:tagged"},
Dst: []string{"*:22"},
ionscale.ACLPolicy{
ACLs: []ionscale.ACLEntry{
{
Action: "accept",
Source: []string{"autogroup:tagged"},
Destination: []string{"*:22"},
},
},
},
}
@@ -408,11 +429,13 @@ func TestACLPolicy_BuildFilterRulesAutogroupSelf(t *testing.T) {
p2 := createMachine("jane@example.com")
policy := ACLPolicy{
ACLs: []ACL{
{
Action: "accept",
Src: []string{"*"},
Dst: []string{"autogroup:self:*"},
ionscale.ACLPolicy{
ACLs: []ionscale.ACLEntry{
{
Action: "accept",
Source: []string{"*"},
Destination: []string{"autogroup:self:*"},
},
},
},
}
@@ -453,11 +476,13 @@ func TestACLPolicy_BuildFilterRulesAutogroupSelfAndTags(t *testing.T) {
p2 := createMachine("john@example.com", "tag:web")
policy := ACLPolicy{
ACLs: []ACL{
{
Action: "accept",
Src: []string{"*"},
Dst: []string{"autogroup:self:*"},
ionscale.ACLPolicy{
ACLs: []ionscale.ACLEntry{
{
Action: "accept",
Source: []string{"*"},
Destination: []string{"autogroup:self:*"},
},
},
},
}
@@ -499,11 +524,13 @@ func TestACLPolicy_BuildFilterRulesAutogroupSelfAndOtherDestinations(t *testing.
p3 := createMachine("jane@example.com")
policy := ACLPolicy{
ACLs: []ACL{
{
Action: "accept",
Src: []string{"*"},
Dst: []string{"autogroup:self:22", "john@example.com:80"},
ionscale.ACLPolicy{
ACLs: []ionscale.ACLEntry{
{
Action: "accept",
Source: []string{"*"},
Destination: []string{"autogroup:self:22", "john@example.com:80"},
},
},
},
}
@@ -560,11 +587,13 @@ func TestACLPolicy_BuildFilterRulesAutogroupInternet(t *testing.T) {
p2 := createMachine("jane@example.com")
policy := ACLPolicy{
ACLs: []ACL{
{
Action: "accept",
Src: []string{"nick@example.com"},
Dst: []string{"autogroup:internet:*"},
ionscale.ACLPolicy{
ACLs: []ionscale.ACLEntry{
{
Action: "accept",
Source: []string{"nick@example.com"},
Destination: []string{"autogroup:internet:*"},
},
},
},
}
@@ -601,11 +630,13 @@ func TestACLPolicy_BuildFilterRulesAutogroupInternet(t *testing.T) {
func TestWithUser(t *testing.T) {
policy := ACLPolicy{
ACLs: []ACL{
{
Action: "accept",
Src: []string{"*"},
Dst: []string{"john@example.com:*"},
ionscale.ACLPolicy{
ACLs: []ionscale.ACLEntry{
{
Action: "accept",
Source: []string{"*"},
Destination: []string{"john@example.com:*"},
},
},
},
}
@@ -618,14 +649,16 @@ func TestWithUser(t *testing.T) {
func TestWithGroup(t *testing.T) {
policy := ACLPolicy{
Groups: map[string][]string{
"group:admin": {"john@example.com"},
},
ACLs: []ACL{
{
Action: "accept",
Src: []string{"*"},
Dst: []string{"group:admin:*"},
ionscale.ACLPolicy{
Groups: map[string][]string{
"group:admin": {"john@example.com"},
},
ACLs: []ionscale.ACLEntry{
{
Action: "accept",
Source: []string{"*"},
Destination: []string{"group:admin:*"},
},
},
},
}
@@ -637,11 +670,13 @@ func TestWithGroup(t *testing.T) {
func TestWithTags(t *testing.T) {
policy := ACLPolicy{
ACLs: []ACL{
{
Action: "accept",
Src: []string{"*"},
Dst: []string{"tag:web:*"},
ionscale.ACLPolicy{
ACLs: []ionscale.ACLEntry{
{
Action: "accept",
Source: []string{"*"},
Destination: []string{"tag:web:*"},
},
},
},
}
@@ -657,15 +692,17 @@ func TestWithHosts(t *testing.T) {
dst2 := createMachine("john@example.com")
policy := ACLPolicy{
Hosts: map[string]string{
"dst1": dst1.IPv4.String(),
},
ACLs: []ACL{
ionscale.ACLPolicy{
Hosts: map[string]string{
"dst1": dst1.IPv4.String(),
},
ACLs: []ionscale.ACLEntry{
{
Action: "accept",
Src: []string{"*"},
Dst: []string{"dst1:*"},
{
Action: "accept",
Source: []string{"*"},
Destination: []string{"dst1:*"},
},
},
},
}
@@ -695,12 +732,13 @@ func createMachine(user string, tags ...string) *Machine {
func TestACLPolicy_IsTagOwner(t *testing.T) {
policy := ACLPolicy{
Groups: map[string][]string{
"group:engineers": {"jane@example.com"},
},
TagOwners: map[string][]string{
"tag:web": {"john@example.com", "group:engineers"},
}}
ionscale.ACLPolicy{
Groups: map[string][]string{
"group:engineers": {"jane@example.com"},
},
TagOwners: map[string][]string{
"tag:web": {"john@example.com", "group:engineers"},
}}}
testCases := []struct {
name string
@@ -780,15 +818,17 @@ func TestACLPolicy_FindAutoApprovedIPs(t *testing.T) {
route3 := netip.MustParsePrefix("10.162.0.0/20")
policy := ACLPolicy{
Groups: map[string][]string{
"group:admins": {"jane@example.com"},
},
AutoApprovers: &AutoApprovers{
Routes: map[string][]string{
route1.String(): {"group:admins"},
route2.String(): {"john@example.com", "tag:router"},
ionscale.ACLPolicy{
Groups: map[string][]string{
"group:admins": {"jane@example.com"},
},
AutoApprovers: &ionscale.ACLAutoApprovers{
Routes: map[string][]string{
route1.String(): {"group:admins"},
route2.String(): {"john@example.com", "tag:router"},
},
ExitNode: []string{"nick@example.com"},
},
ExitNode: []string{"nick@example.com"},
},
}
@@ -872,11 +912,13 @@ func TestACLPolicy_BuildFilterRulesWithAdvertisedRoutes(t *testing.T) {
p1 := createMachine("john@example.com", "tag:trusted")
policy := ACLPolicy{
ACLs: []ACL{
{
Action: "accept",
Src: []string{"tag:trusted"},
Dst: []string{"fd7a:115c:a1e0:b1a:0:1:a3c:0/120:*"},
ionscale.ACLPolicy{
ACLs: []ionscale.ACLEntry{
{
Action: "accept",
Source: []string{"tag:trusted"},
Destination: []string{"fd7a:115c:a1e0:b1a:0:1:a3c:0/120:*"},
},
},
},
}
@@ -911,11 +953,13 @@ func TestACLPolicy_BuildFilterRulesWildcardGrants(t *testing.T) {
p2 := createMachine("jane@example.com")
policy := ACLPolicy{
Grants: []Grant{
{
Src: []string{"*"},
Dst: []string{"*"},
IP: ranges,
ionscale.ACLPolicy{
Grants: []ionscale.ACLGrant{
{
Source: []string{"*"},
Destination: []string{"*"},
IP: ranges,
},
},
},
}
@@ -955,12 +999,14 @@ func TestACLPolicy_BuildFilterRulesWithAppGrants(t *testing.T) {
marshal, _ := json.Marshal(mycap)
policy := ACLPolicy{
Grants: []Grant{
{
Src: []string{"*"},
Dst: []string{"*"},
App: map[tailcfg.PeerCapability][]tailcfg.RawMessage{
tailcfg.PeerCapability("localtest.me/cap/test"): {tailcfg.RawMessage(marshal)},
ionscale.ACLPolicy{
Grants: []ionscale.ACLGrant{
{
Source: []string{"*"},
Destination: []string{"*"},
App: map[tailcfg.PeerCapability][]tailcfg.RawMessage{
tailcfg.PeerCapability("localtest.me/cap/test"): {tailcfg.RawMessage(marshal)},
},
},
},
},
+87
View File
@@ -0,0 +1,87 @@
package domain
import (
"database/sql/driver"
"encoding/json"
"fmt"
"github.com/tailscale/hujson"
)
func NewHuJSON[T any](t *T) HuJSON[T] {
marshal, _ := json.Marshal(t)
return HuJSON[T]{
v: string(marshal),
t: t,
}
}
func ParseHuJson[T any](v string) (*HuJSON[T], error) {
ast, err := hujson.Parse([]byte(v))
if err != nil {
return nil, err
}
ast.Format()
formatted := string(ast.Pack())
ast.Standardize()
t := new(T)
if err := json.Unmarshal(ast.Pack(), t); err != nil {
return nil, err
}
return &HuJSON[T]{v: formatted, t: t}, nil
}
type HuJSON[T any] struct {
v string
t *T
}
func (h *HuJSON[T]) Get() *T {
return h.t
}
func (h *HuJSON[T]) String() string {
return h.v
}
func (i *HuJSON[T]) Equal(x *HuJSON[T]) bool {
if i == nil && x == nil {
return true
}
if (i == nil) != (x == nil) {
return false
}
return i.v == x.v
}
func (h HuJSON[T]) Value() (driver.Value, error) {
if len(h.v) == 0 {
return nil, nil
}
return h.v, nil
}
func (h *HuJSON[T]) Scan(destination interface{}) error {
var v string
switch value := destination.(type) {
case string:
v = value
case []byte:
v = string(value)
default:
return fmt.Errorf("unexpected data type %T", destination)
}
next, err := hujson.Standardize([]byte(v))
if err != nil {
return err
}
var n = new(T)
if err := json.Unmarshal(next, n); err != nil {
return err
}
h.v = v
h.t = n
return nil
}
+2 -2
View File
@@ -13,8 +13,8 @@ type Tailnet struct {
ID uint64 `gorm:"primary_key"`
Name string
DNSConfig DNSConfig
IAMPolicy IAMPolicy
ACLPolicy ACLPolicy
IAMPolicy HuJSON[IAMPolicy]
ACLPolicy HuJSON[ACLPolicy]
DERPMap DERPMap
ServiceCollectionEnabled bool
FileSharingEnabled bool
+3 -3
View File
@@ -447,7 +447,7 @@ func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, form
ephemeral = false
}
if err := tailnet.ACLPolicy.CheckTagOwners(registrationRequest.Data.Hostinfo.RequestTags, user); err != nil {
if err := tailnet.ACLPolicy.Get().CheckTagOwners(registrationRequest.Data.Hostinfo.RequestTags, user); err != nil {
registrationRequest.Authenticated = false
registrationRequest.Error = err.Error()
if err := h.repository.SaveRegistrationRequest(ctx, registrationRequest); err != nil {
@@ -456,7 +456,7 @@ func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, form
return c.Redirect(http.StatusFound, "/a/error?e=nto")
}
autoAllowIPs := tailnet.ACLPolicy.FindAutoApprovedIPs(req.Hostinfo.RoutableIPs, tags, user)
autoAllowIPs := tailnet.ACLPolicy.Get().FindAutoApprovedIPs(req.Hostinfo.RoutableIPs, tags, user)
var m *domain.Machine
@@ -573,7 +573,7 @@ func (h *AuthenticationHandlers) listAvailableTailnets(ctx context.Context, u *a
return nil, err
}
for _, t := range tailnets {
approved, err := t.IAMPolicy.EvaluatePolicy(&domain.Identity{UserID: u.ID, Email: u.Name, Attr: u.Attr})
approved, err := t.IAMPolicy.Get().EvaluatePolicy(&domain.Identity{UserID: u.ID, Email: u.Name, Attr: u.Attr})
if err != nil {
return nil, err
}
+2 -2
View File
@@ -160,7 +160,7 @@ func (h *RegistrationHandlers) authenticateMachineWithAuthKey(c echo.Context, ma
tailnet := authKey.Tailnet
user := authKey.User
if err := tailnet.ACLPolicy.CheckTagOwners(req.Hostinfo.RequestTags, &user); err != nil {
if err := tailnet.ACLPolicy.Get().CheckTagOwners(req.Hostinfo.RequestTags, &user); err != nil {
response := tailcfg.RegisterResponse{MachineAuthorized: false, Error: err.Error()}
return c.JSON(http.StatusOK, response)
}
@@ -169,7 +169,7 @@ func (h *RegistrationHandlers) authenticateMachineWithAuthKey(c echo.Context, ma
advertisedTags := domain.SanitizeTags(req.Hostinfo.RequestTags)
tags := append(registeredTags, advertisedTags...)
autoAllowIPs := tailnet.ACLPolicy.FindAutoApprovedIPs(req.Hostinfo.RoutableIPs, tags, &user)
autoAllowIPs := tailnet.ACLPolicy.Get().FindAutoApprovedIPs(req.Hostinfo.RoutableIPs, tags, &user)
var m *domain.Machine
+2 -2
View File
@@ -85,7 +85,7 @@ func ToDNSConfig(m *domain.Machine, tailnet *domain.Tailnet, c *domain.DNSConfig
}
func ToNode(capVer tailcfg.CapabilityVersion, m *domain.Machine, tailnet *domain.Tailnet, taggedDevicesUser *domain.User, peer bool, connected bool, routeFilter func(m *domain.Machine) []netip.Prefix) (*tailcfg.Node, *tailcfg.UserProfile, error) {
role := tailnet.IAMPolicy.GetRole(m.User)
role := tailnet.IAMPolicy.Get().GetRole(m.User)
nKey, err := util.ParseNodePublicKey(m.NodeKey)
if err != nil {
@@ -179,7 +179,7 @@ func ToNode(capVer tailcfg.CapabilityVersion, m *domain.Machine, tailnet *domain
var capabilities []tailcfg.NodeCapability
capMap := make(tailcfg.NodeCapMap)
for _, c := range tailnet.ACLPolicy.NodeCapabilities(m) {
for _, c := range tailnet.ACLPolicy.Get().NodeCapabilities(m) {
capabilities = append(capabilities, c)
capMap[c] = []tailcfg.RawMessage{}
}
+1 -1
View File
@@ -53,7 +53,7 @@ func (h *PollNetMapper) CreateMapResponse(ctx context.Context, delta bool) (*Map
hostinfo := tailcfg.Hostinfo(m.HostInfo)
tailnet := m.Tailnet
policies := tailnet.ACLPolicy
policies := tailnet.ACLPolicy.Get()
dnsConfig := tailnet.DNSConfig
serviceUser, _, err := h.repository.GetOrCreateServiceUser(ctx, &tailnet)
+8 -13
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"github.com/bufbuild/connect-go"
"github.com/jsiebens/ionscale/internal/domain"
"github.com/jsiebens/ionscale/internal/mapping"
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
)
@@ -23,12 +22,7 @@ func (s *Service) GetACLPolicy(ctx context.Context, req *connect.Request[api.Get
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("tailnet does not exist"))
}
var policy api.ACLPolicy
if err := mapping.CopyViaJson(&tailnet.ACLPolicy, &policy); err != nil {
return nil, logError(err)
}
return connect.NewResponse(&api.GetACLPolicyResponse{Policy: &policy}), nil
return connect.NewResponse(&api.GetACLPolicyResponse{Policy: tailnet.ACLPolicy.String()}), nil
}
func (s *Service) SetACLPolicy(ctx context.Context, req *connect.Request[api.SetACLPolicyRequest]) (*connect.Response[api.SetACLPolicyResponse], error) {
@@ -45,17 +39,18 @@ func (s *Service) SetACLPolicy(ctx context.Context, req *connect.Request[api.Set
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("tailnet does not exist"))
}
oldPolicy := tailnet.ACLPolicy
var newPolicy domain.ACLPolicy
if err := mapping.CopyViaJson(req.Msg.Policy, &newPolicy); err != nil {
return nil, logError(err)
newPolicy, err := domain.ParseHuJson[domain.ACLPolicy](req.Msg.Policy)
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("invalid acl policy: %w", err))
}
if oldPolicy.Equal(&newPolicy) {
oldPolicy := tailnet.ACLPolicy
if oldPolicy.Equal(newPolicy) {
return connect.NewResponse(&api.SetACLPolicyResponse{}), nil
}
tailnet.ACLPolicy = newPolicy
tailnet.ACLPolicy = *newPolicy
if err := s.repository.SaveTailnet(ctx, tailnet); err != nil {
return nil, logError(err)
}
+1 -1
View File
@@ -135,7 +135,7 @@ func (s *Service) CreateAuthKey(ctx context.Context, req *connect.Request[api.Cr
}
if !principal.IsSystemAdmin() {
if err := tailnet.ACLPolicy.CheckTagOwners(req.Msg.Tags, principal.User); err != nil {
if err := tailnet.ACLPolicy.Get().CheckTagOwners(req.Msg.Tags, principal.User); err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, err)
}
}
+9 -34
View File
@@ -22,14 +22,7 @@ func (s *Service) GetIAMPolicy(ctx context.Context, req *connect.Request[api.Get
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("tailnet does not exist"))
}
policy := &api.IAMPolicy{
Subs: tailnet.IAMPolicy.Subs,
Emails: tailnet.IAMPolicy.Emails,
Filters: tailnet.IAMPolicy.Filters,
Roles: domainRolesMapToApiRolesMap(tailnet.IAMPolicy.Roles),
}
return connect.NewResponse(&api.GetIAMPolicyResponse{Policy: policy}), nil
return connect.NewResponse(&api.GetIAMPolicyResponse{Policy: tailnet.IAMPolicy.String()}), nil
}
func (s *Service) SetIAMPolicy(ctx context.Context, req *connect.Request[api.SetIAMPolicyRequest]) (*connect.Response[api.SetIAMPolicyResponse], error) {
@@ -46,23 +39,21 @@ func (s *Service) SetIAMPolicy(ctx context.Context, req *connect.Request[api.Set
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("tailnet does not exist"))
}
if err := validateIamPolicy(req.Msg.Policy); err != nil {
newPolicy, err := domain.ParseHuJson[domain.IAMPolicy](req.Msg.Policy)
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("invalid iam policy: %w", err))
}
if err := validateIamPolicy(newPolicy.Get()); err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("invalid iam policy: %w", err))
}
oldPolicy := tailnet.IAMPolicy
newPolicy := domain.IAMPolicy{
Subs: req.Msg.Policy.Subs,
Emails: req.Msg.Policy.Emails,
Filters: req.Msg.Policy.Filters,
Roles: apiRolesMapToDomainRolesMap(req.Msg.Policy.Roles),
}
if oldPolicy.Equal(&newPolicy) {
if oldPolicy.Equal(newPolicy) {
return connect.NewResponse(&api.SetIAMPolicyResponse{}), nil
}
tailnet.IAMPolicy = newPolicy
tailnet.IAMPolicy = *newPolicy
if err := s.repository.SaveTailnet(ctx, tailnet); err != nil {
return nil, logError(err)
@@ -70,19 +61,3 @@ func (s *Service) SetIAMPolicy(ctx context.Context, req *connect.Request[api.Set
return connect.NewResponse(&api.SetIAMPolicyResponse{}), nil
}
func apiRolesMapToDomainRolesMap(values map[string]string) map[string]domain.UserRole {
var result = map[string]domain.UserRole{}
for k, v := range values {
result[k] = domain.UserRole(v)
}
return result
}
func domainRolesMapToApiRolesMap(values map[string]domain.UserRole) map[string]string {
var result = map[string]string{}
for k, v := range values {
result[k] = string(v)
}
return result
}
+1 -1
View File
@@ -64,7 +64,7 @@ func exchangeToken(ctx context.Context, systemAdminKey *key.ServerPrivate, repos
if err == nil && apiKey != nil {
user := apiKey.User
tailnet := apiKey.Tailnet
role := tailnet.IAMPolicy.GetRole(user)
role := tailnet.IAMPolicy.Get().GetRole(user)
return &domain.Principal{User: &apiKey.User, SystemRole: domain.SystemRoleNone, UserRole: role}
}
+1 -1
View File
@@ -41,7 +41,7 @@ func (s *Service) GetVersion(_ context.Context, _ *connect.Request[api.GetVersio
}), nil
}
func validateIamPolicy(p *api.IAMPolicy) error {
func validateIamPolicy(p *domain.IAMPolicy) error {
var mErr *multierror.Error
for i, exp := range p.Filters {
if _, err := grammar.Parse(fmt.Sprintf("filter %d", i), []byte(exp)); err != nil {
+33 -39
View File
@@ -6,7 +6,6 @@ import (
"fmt"
"github.com/bufbuild/connect-go"
"github.com/jsiebens/ionscale/internal/domain"
"github.com/jsiebens/ionscale/internal/mapping"
"github.com/jsiebens/ionscale/internal/util"
"github.com/jsiebens/ionscale/pkg/defaults"
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
@@ -17,8 +16,8 @@ func domainTailnetToApiTailnet(tailnet *domain.Tailnet) (*api.Tailnet, error) {
t := &api.Tailnet{
Id: tailnet.ID,
Name: tailnet.Name,
IamPolicy: new(api.IAMPolicy),
AclPolicy: new(api.ACLPolicy),
IamPolicy: tailnet.IAMPolicy.String(),
AclPolicy: tailnet.ACLPolicy.String(),
DnsConfig: domainDNSConfigToApiDNSConfig(tailnet),
ServiceCollectionEnabled: tailnet.ServiceCollectionEnabled,
FileSharingEnabled: tailnet.FileSharingEnabled,
@@ -26,14 +25,6 @@ func domainTailnetToApiTailnet(tailnet *domain.Tailnet) (*api.Tailnet, error) {
MachineAuthorizationEnabled: tailnet.MachineAuthorizationEnabled,
}
if err := mapping.CopyViaJson(tailnet.IAMPolicy, t.IamPolicy); err != nil {
return nil, err
}
if err := mapping.CopyViaJson(tailnet.ACLPolicy, t.AclPolicy); err != nil {
return nil, err
}
return t, nil
}
@@ -51,12 +42,26 @@ func (s *Service) CreateTailnet(ctx context.Context, req *connect.Request[api.Cr
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("tailnet with name '%s' already exists", req.Msg.Name))
}
if req.Msg.IamPolicy == nil {
req.Msg.IamPolicy = defaults.DefaultIAMPolicy()
iamPolicy := domain.NewHuJSON(&domain.IAMPolicy{})
aclPolicy := domain.NewHuJSON(&domain.ACLPolicy{ACLPolicy: *defaults.DefaultACLPolicy()})
if req.Msg.IamPolicy != "" {
newPolicy, err := domain.ParseHuJson[domain.IAMPolicy](req.Msg.IamPolicy)
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("invalid iam policy: %w", err))
}
if err := validateIamPolicy(newPolicy.Get()); err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("invalid iam policy: %w", err))
}
iamPolicy = *newPolicy
}
if req.Msg.AclPolicy == nil {
req.Msg.AclPolicy = defaults.DefaultACLPolicy()
if req.Msg.AclPolicy != "" {
newPolicy, err := domain.ParseHuJson[domain.ACLPolicy](req.Msg.AclPolicy)
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("invalid acl policy: %w", err))
}
aclPolicy = *newPolicy
}
if req.Msg.DnsConfig == nil {
@@ -66,8 +71,8 @@ func (s *Service) CreateTailnet(ctx context.Context, req *connect.Request[api.Cr
tailnet := &domain.Tailnet{
ID: util.NextID(),
Name: req.Msg.Name,
IAMPolicy: domain.IAMPolicy{},
ACLPolicy: domain.ACLPolicy{},
IAMPolicy: iamPolicy,
ACLPolicy: aclPolicy,
DNSConfig: apiDNSConfigToDomainDNSConfig(req.Msg.DnsConfig),
ServiceCollectionEnabled: req.Msg.ServiceCollectionEnabled,
FileSharingEnabled: req.Msg.FileSharingEnabled,
@@ -75,18 +80,6 @@ func (s *Service) CreateTailnet(ctx context.Context, req *connect.Request[api.Cr
MachineAuthorizationEnabled: req.Msg.MachineAuthorizationEnabled,
}
if err := validateIamPolicy(req.Msg.IamPolicy); err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("invalid iam policy: %w", err))
}
if err := mapping.CopyViaJson(req.Msg.IamPolicy, &tailnet.IAMPolicy); err != nil {
return nil, logError(err)
}
if err := mapping.CopyViaJson(req.Msg.AclPolicy, &tailnet.ACLPolicy); err != nil {
return nil, logError(err)
}
if err := s.repository.SaveTailnet(ctx, tailnet); err != nil {
return nil, logError(err)
}
@@ -116,22 +109,23 @@ func (s *Service) UpdateTailnet(ctx context.Context, req *connect.Request[api.Up
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("tailnet not found"))
}
if req.Msg.IamPolicy != nil {
if err := validateIamPolicy(req.Msg.IamPolicy); err != nil {
if req.Msg.IamPolicy != "" {
newPolicy, err := domain.ParseHuJson[domain.IAMPolicy](req.Msg.IamPolicy)
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("invalid iam policy: %w", err))
}
tailnet.IAMPolicy = domain.IAMPolicy{}
if err := mapping.CopyViaJson(req.Msg.IamPolicy, &tailnet.IAMPolicy); err != nil {
return nil, logError(err)
if err := validateIamPolicy(newPolicy.Get()); err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("invalid iam policy: %w", err))
}
tailnet.IAMPolicy = *newPolicy
}
if req.Msg.AclPolicy != nil {
tailnet.ACLPolicy = domain.ACLPolicy{}
if err := mapping.CopyViaJson(req.Msg.AclPolicy, &tailnet.ACLPolicy); err != nil {
return nil, logError(err)
if req.Msg.AclPolicy != "" {
newPolicy, err := domain.ParseHuJson[domain.ACLPolicy](req.Msg.AclPolicy)
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("invalid acl policy: %w", err))
}
tailnet.ACLPolicy = *newPolicy
}
if req.Msg.DnsConfig != nil {
+1 -1
View File
@@ -34,7 +34,7 @@ func (s *Service) ListUsers(ctx context.Context, req *connect.Request[api.ListUs
resp.Users = append(resp.Users, &api.User{
Id: u.ID,
Name: u.Name,
Role: string(tailnet.IAMPolicy.GetRole(u)),
Role: string(tailnet.IAMPolicy.Get().GetRole(u)),
})
}