mirror of
https://github.com/juanfont/headscale.git
synced 2025-05-21 17:43:51 -04:00
policy/v2: validate that no undefined group or tag is used (#2576)
* policy/v2: allow Username as ssh source Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * policy/v2: validate that no undefined group or tag is used Fixes #2570 Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * policy: fixup tests which violated tag constraing Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> --------- Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
parent
833e0f66f1
commit
56db4ed0f1
@ -709,6 +709,9 @@ func TestReduceFilterRules(t *testing.T) {
|
|||||||
name: "1817-reduce-breaks-32-mask",
|
name: "1817-reduce-breaks-32-mask",
|
||||||
pol: `
|
pol: `
|
||||||
{
|
{
|
||||||
|
"tagOwners": {
|
||||||
|
"tag:access-servers": ["user100@"],
|
||||||
|
},
|
||||||
"groups": {
|
"groups": {
|
||||||
"group:access": [
|
"group:access": [
|
||||||
"user1@"
|
"user1@"
|
||||||
@ -1688,6 +1691,9 @@ func TestSSHPolicyRules(t *testing.T) {
|
|||||||
targetNode: taggedServer,
|
targetNode: taggedServer,
|
||||||
peers: types.Nodes{&nodeUser1, &nodeUser2},
|
peers: types.Nodes{&nodeUser1, &nodeUser2},
|
||||||
policy: `{
|
policy: `{
|
||||||
|
"tagOwners": {
|
||||||
|
"tag:server": ["user3@"],
|
||||||
|
},
|
||||||
"groups": {
|
"groups": {
|
||||||
"group:users": ["user1@", "user2@"]
|
"group:users": ["user1@", "user2@"]
|
||||||
},
|
},
|
||||||
@ -1726,6 +1732,9 @@ func TestSSHPolicyRules(t *testing.T) {
|
|||||||
targetNode: nodeUser1,
|
targetNode: nodeUser1,
|
||||||
peers: types.Nodes{&taggedClient},
|
peers: types.Nodes{&taggedClient},
|
||||||
policy: `{
|
policy: `{
|
||||||
|
"tagOwners": {
|
||||||
|
"tag:client": ["user1@"],
|
||||||
|
},
|
||||||
"ssh": [
|
"ssh": [
|
||||||
{
|
{
|
||||||
"action": "accept",
|
"action": "accept",
|
||||||
@ -1756,6 +1765,10 @@ func TestSSHPolicyRules(t *testing.T) {
|
|||||||
targetNode: taggedServer,
|
targetNode: taggedServer,
|
||||||
peers: types.Nodes{&taggedClient},
|
peers: types.Nodes{&taggedClient},
|
||||||
policy: `{
|
policy: `{
|
||||||
|
"tagOwners": {
|
||||||
|
"tag:client": ["user2@"],
|
||||||
|
"tag:server": ["user3@"],
|
||||||
|
},
|
||||||
"ssh": [
|
"ssh": [
|
||||||
{
|
{
|
||||||
"action": "accept",
|
"action": "accept",
|
||||||
@ -1818,29 +1831,14 @@ func TestSSHPolicyRules(t *testing.T) {
|
|||||||
// we skip this test for v1 and not let it hold up v2 replacing it.
|
// we skip this test for v1 and not let it hold up v2 replacing it.
|
||||||
skipV1: true,
|
skipV1: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "invalid-source-user-not-allowed",
|
|
||||||
targetNode: nodeUser1,
|
|
||||||
peers: types.Nodes{&nodeUser2},
|
|
||||||
policy: `{
|
|
||||||
"ssh": [
|
|
||||||
{
|
|
||||||
"action": "accept",
|
|
||||||
"src": ["user2@"],
|
|
||||||
"dst": ["user1@"],
|
|
||||||
"users": ["autogroup:nonroot"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`,
|
|
||||||
expectErr: true,
|
|
||||||
errorMessage: "not supported",
|
|
||||||
skipV1: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "check-period-specified",
|
name: "check-period-specified",
|
||||||
targetNode: nodeUser1,
|
targetNode: nodeUser1,
|
||||||
peers: types.Nodes{&taggedClient},
|
peers: types.Nodes{&taggedClient},
|
||||||
policy: `{
|
policy: `{
|
||||||
|
"tagOwners": {
|
||||||
|
"tag:client": ["user1@"],
|
||||||
|
},
|
||||||
"ssh": [
|
"ssh": [
|
||||||
{
|
{
|
||||||
"action": "check",
|
"action": "check",
|
||||||
@ -1873,6 +1871,9 @@ func TestSSHPolicyRules(t *testing.T) {
|
|||||||
targetNode: nodeUser2,
|
targetNode: nodeUser2,
|
||||||
peers: types.Nodes{&nodeUser1},
|
peers: types.Nodes{&nodeUser1},
|
||||||
policy: `{
|
policy: `{
|
||||||
|
"tagOwners": {
|
||||||
|
"tag:client": ["user1@"],
|
||||||
|
},
|
||||||
"ssh": [
|
"ssh": [
|
||||||
{
|
{
|
||||||
"action": "accept",
|
"action": "accept",
|
||||||
@ -1926,6 +1927,9 @@ func TestSSHPolicyRules(t *testing.T) {
|
|||||||
targetNode: nodeUser1,
|
targetNode: nodeUser1,
|
||||||
peers: types.Nodes{&taggedClient},
|
peers: types.Nodes{&taggedClient},
|
||||||
policy: `{
|
policy: `{
|
||||||
|
"tagOwners": {
|
||||||
|
"tag:client": ["user1@"],
|
||||||
|
},
|
||||||
"ssh": [
|
"ssh": [
|
||||||
{
|
{
|
||||||
"action": "accept",
|
"action": "accept",
|
||||||
|
@ -720,6 +720,20 @@ type Usernames []Username
|
|||||||
// Groups are a map of Group to a list of Username.
|
// Groups are a map of Group to a list of Username.
|
||||||
type Groups map[Group]Usernames
|
type Groups map[Group]Usernames
|
||||||
|
|
||||||
|
func (g Groups) Contains(group *Group) error {
|
||||||
|
if group == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for defined := range map[Group]Usernames(g) {
|
||||||
|
if defined == *group {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf(`Group %q is not defined in the Policy, please define or remove the reference to it`, group)
|
||||||
|
}
|
||||||
|
|
||||||
// UnmarshalJSON overrides the default JSON unmarshalling for Groups to ensure
|
// UnmarshalJSON overrides the default JSON unmarshalling for Groups to ensure
|
||||||
// that each group name is validated using the isGroup function. This ensures
|
// that each group name is validated using the isGroup function. This ensures
|
||||||
// that all group names conform to the expected format, which is always prefixed
|
// that all group names conform to the expected format, which is always prefixed
|
||||||
@ -791,6 +805,20 @@ func (h Hosts) exist(name Host) bool {
|
|||||||
// TagOwners are a map of Tag to a list of the UserEntities that own the tag.
|
// TagOwners are a map of Tag to a list of the UserEntities that own the tag.
|
||||||
type TagOwners map[Tag]Owners
|
type TagOwners map[Tag]Owners
|
||||||
|
|
||||||
|
func (to TagOwners) Contains(tagOwner *Tag) error {
|
||||||
|
if tagOwner == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for defined := range map[Tag]Owners(to) {
|
||||||
|
if defined == *tagOwner {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf(`Tag %q is not defined in the Policy, please define or remove the reference to it`, tagOwner)
|
||||||
|
}
|
||||||
|
|
||||||
// resolveTagOwners resolves the TagOwners to a map of Tag to netipx.IPSet.
|
// resolveTagOwners resolves the TagOwners to a map of Tag to netipx.IPSet.
|
||||||
// The resulting map can be used to quickly look up the IPSet for a given Tag.
|
// The resulting map can be used to quickly look up the IPSet for a given Tag.
|
||||||
// It is intended for internal use in a PolicyManager.
|
// It is intended for internal use in a PolicyManager.
|
||||||
@ -1047,6 +1075,16 @@ func (p *Policy) validate() error {
|
|||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
case *Group:
|
||||||
|
g := src.(*Group)
|
||||||
|
if err := p.Groups.Contains(g); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
case *Tag:
|
||||||
|
tagOwner := src.(*Tag)
|
||||||
|
if err := p.TagOwners.Contains(tagOwner); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1069,6 +1107,16 @@ func (p *Policy) validate() error {
|
|||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
case *Group:
|
||||||
|
g := dst.Alias.(*Group)
|
||||||
|
if err := p.Groups.Contains(g); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
case *Tag:
|
||||||
|
tagOwner := dst.Alias.(*Tag)
|
||||||
|
if err := p.TagOwners.Contains(tagOwner); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1102,6 +1150,16 @@ func (p *Policy) validate() error {
|
|||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
case *Group:
|
||||||
|
g := src.(*Group)
|
||||||
|
if err := p.Groups.Contains(g); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
case *Tag:
|
||||||
|
tagOwner := src.(*Tag)
|
||||||
|
if err := p.TagOwners.Contains(tagOwner); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, dst := range ssh.Destinations {
|
for _, dst := range ssh.Destinations {
|
||||||
@ -1117,6 +1175,55 @@ func (p *Policy) validate() error {
|
|||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
case *Tag:
|
||||||
|
tagOwner := dst.(*Tag)
|
||||||
|
if err := p.TagOwners.Contains(tagOwner); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tagOwners := range p.TagOwners {
|
||||||
|
for _, tagOwner := range tagOwners {
|
||||||
|
switch tagOwner.(type) {
|
||||||
|
case *Group:
|
||||||
|
g := tagOwner.(*Group)
|
||||||
|
if err := p.Groups.Contains(g); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, approvers := range p.AutoApprovers.Routes {
|
||||||
|
for _, approver := range approvers {
|
||||||
|
switch approver.(type) {
|
||||||
|
case *Group:
|
||||||
|
g := approver.(*Group)
|
||||||
|
if err := p.Groups.Contains(g); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
case *Tag:
|
||||||
|
tagOwner := approver.(*Tag)
|
||||||
|
if err := p.TagOwners.Contains(tagOwner); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, approver := range p.AutoApprovers.ExitNode {
|
||||||
|
switch approver.(type) {
|
||||||
|
case *Group:
|
||||||
|
g := approver.(*Group)
|
||||||
|
if err := p.Groups.Contains(g); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
case *Tag:
|
||||||
|
tagOwner := approver.(*Tag)
|
||||||
|
if err := p.TagOwners.Contains(tagOwner); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1152,7 +1259,7 @@ func (a *SSHSrcAliases) UnmarshalJSON(b []byte) error {
|
|||||||
*a = make([]Alias, len(aliases))
|
*a = make([]Alias, len(aliases))
|
||||||
for i, alias := range aliases {
|
for i, alias := range aliases {
|
||||||
switch alias.Alias.(type) {
|
switch alias.Alias.(type) {
|
||||||
case *Group, *Tag, *AutoGroup:
|
case *Username, *Group, *Tag, *AutoGroup:
|
||||||
(*a)[i] = alias.Alias
|
(*a)[i] = alias.Alias
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("type %T not supported", alias.Alias)
|
return fmt.Errorf("type %T not supported", alias.Alias)
|
||||||
|
@ -511,6 +511,201 @@ func TestUnmarshalPolicy(t *testing.T) {
|
|||||||
`,
|
`,
|
||||||
wantErr: `"autogroup:internet" used in SSH destination, it can only be used in ACL destinations`,
|
wantErr: `"autogroup:internet" used in SSH destination, it can only be used in ACL destinations`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "group-must-be-defined-acl-src",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"acls": [
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"src": [
|
||||||
|
"group:notdefined"
|
||||||
|
],
|
||||||
|
"dst": [
|
||||||
|
"autogroup:internet:*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
wantErr: `Group "group:notdefined" is not defined in the Policy, please define or remove the reference to it`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "group-must-be-defined-acl-dst",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"acls": [
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"src": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"dst": [
|
||||||
|
"group:notdefined:*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
wantErr: `Group "group:notdefined" is not defined in the Policy, please define or remove the reference to it`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "group-must-be-defined-acl-ssh-src",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"ssh": [
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"src": [
|
||||||
|
"group:notdefined"
|
||||||
|
],
|
||||||
|
"dst": [
|
||||||
|
"user@"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
wantErr: `Group "group:notdefined" is not defined in the Policy, please define or remove the reference to it`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "group-must-be-defined-acl-tagOwner",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"tagOwners": {
|
||||||
|
"tag:test": ["group:notdefined"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
wantErr: `Group "group:notdefined" is not defined in the Policy, please define or remove the reference to it`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "group-must-be-defined-acl-autoapprover-route",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"autoApprovers": {
|
||||||
|
"routes": {
|
||||||
|
"10.0.0.0/16": ["group:notdefined"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
wantErr: `Group "group:notdefined" is not defined in the Policy, please define or remove the reference to it`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "group-must-be-defined-acl-autoapprover-exitnode",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"autoApprovers": {
|
||||||
|
"exitNode": ["group:notdefined"]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
wantErr: `Group "group:notdefined" is not defined in the Policy, please define or remove the reference to it`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tag-must-be-defined-acl-src",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"acls": [
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"src": [
|
||||||
|
"tag:notdefined"
|
||||||
|
],
|
||||||
|
"dst": [
|
||||||
|
"autogroup:internet:*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
wantErr: `Tag "tag:notdefined" is not defined in the Policy, please define or remove the reference to it`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tag-must-be-defined-acl-dst",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"acls": [
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"src": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"dst": [
|
||||||
|
"tag:notdefined:*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
wantErr: `Tag "tag:notdefined" is not defined in the Policy, please define or remove the reference to it`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tag-must-be-defined-acl-ssh-src",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"ssh": [
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"src": [
|
||||||
|
"tag:notdefined"
|
||||||
|
],
|
||||||
|
"dst": [
|
||||||
|
"user@"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
wantErr: `Tag "tag:notdefined" is not defined in the Policy, please define or remove the reference to it`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tag-must-be-defined-acl-ssh-dst",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"groups": {
|
||||||
|
"group:defined": ["user@"],
|
||||||
|
},
|
||||||
|
"ssh": [
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"src": [
|
||||||
|
"group:defined"
|
||||||
|
],
|
||||||
|
"dst": [
|
||||||
|
"tag:notdefined",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
wantErr: `Tag "tag:notdefined" is not defined in the Policy, please define or remove the reference to it`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tag-must-be-defined-acl-autoapprover-route",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"autoApprovers": {
|
||||||
|
"routes": {
|
||||||
|
"10.0.0.0/16": ["tag:notdefined"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
wantErr: `Tag "tag:notdefined" is not defined in the Policy, please define or remove the reference to it`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tag-must-be-defined-acl-autoapprover-exitnode",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"autoApprovers": {
|
||||||
|
"exitNode": ["tag:notdefined"]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
wantErr: `Tag "tag:notdefined" is not defined in the Policy, please define or remove the reference to it`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
cmps := append(util.Comparers, cmp.Comparer(func(x, y Prefix) bool {
|
cmps := append(util.Comparers, cmp.Comparer(func(x, y Prefix) bool {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user