policy/v2: add localpart:*@domain SSH user compilation
Add support for localpart:*@<domain> entries in SSH policy users. When a user SSHes into a target, their email local-part becomes the OS username (e.g. alice@example.com → OS user alice). Type system (types.go): - SSHUser.IsLocalpart() and ParseLocalpart() for validation - SSHUsers.LocalpartEntries(), NormalUsers(), ContainsLocalpart() - Enforces format: localpart:*@<domain> (wildcard-only) - UserWildcard.Resolve for user:*@domain SSH source aliases - acceptEnv passthrough for SSH rules Compilation (filter.go): - resolveLocalparts: pure function mapping users to local-parts by email domain. No node walking, easy to test. - groupSourcesByUser: single walk producing per-user principals with sorted user IDs, and tagged principals separately. - ipSetToPrincipals: shared helper replacing 6 inline copies. - selfPrincipalsForNode: self-access using pre-computed byUser. The approach separates data gathering from rule assembly. Localpart rules are interleaved per source user to match Tailscale SaaS first-match-wins ordering. Updates #3049
This commit is contained in:
parent
414d3bbbd8
commit
0acf09bdd2
5 changed files with 1452 additions and 220 deletions
|
|
@ -1077,6 +1077,8 @@ func TestSSHPolicyRules(t *testing.T) {
|
|||
{Name: "user1", Model: gorm.Model{ID: 1}},
|
||||
{Name: "user2", Model: gorm.Model{ID: 2}},
|
||||
{Name: "user3", Model: gorm.Model{ID: 3}},
|
||||
{Name: "alice", Email: "alice@example.com", Model: gorm.Model{ID: 4}},
|
||||
{Name: "bob", Email: "bob@example.com", Model: gorm.Model{ID: 5}},
|
||||
}
|
||||
|
||||
// Create standard node setups used across tests
|
||||
|
|
@ -1110,6 +1112,20 @@ func TestSSHPolicyRules(t *testing.T) {
|
|||
Tags: []string{"tag:server"},
|
||||
}
|
||||
|
||||
// Nodes for localpart tests (users with email addresses)
|
||||
nodeAlice := types.Node{
|
||||
Hostname: "alice-device",
|
||||
IPv4: ap("100.64.0.6"),
|
||||
UserID: new(uint(4)),
|
||||
User: new(users[3]),
|
||||
}
|
||||
nodeBob := types.Node{
|
||||
Hostname: "bob-device",
|
||||
IPv4: ap("100.64.0.7"),
|
||||
UserID: new(uint(5)),
|
||||
User: new(users[4]),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
targetNode types.Node
|
||||
|
|
@ -1446,6 +1462,7 @@ func TestSSHPolicyRules(t *testing.T) {
|
|||
},
|
||||
SSHUsers: map[string]string{
|
||||
"debian": "debian",
|
||||
"root": "",
|
||||
},
|
||||
Action: &tailcfg.SSHAction{
|
||||
Accept: true,
|
||||
|
|
@ -1456,6 +1473,108 @@ func TestSSHPolicyRules(t *testing.T) {
|
|||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "localpart-maps-email-to-os-user",
|
||||
targetNode: nodeTaggedServer,
|
||||
peers: types.Nodes{&nodeAlice, &nodeBob},
|
||||
policy: `{
|
||||
"tagOwners": {
|
||||
"tag:server": ["alice@example.com"]
|
||||
},
|
||||
"ssh": [
|
||||
{
|
||||
"action": "accept",
|
||||
"src": ["autogroup:member"],
|
||||
"dst": ["tag:server"],
|
||||
"users": ["localpart:*@example.com"]
|
||||
}
|
||||
]
|
||||
}`,
|
||||
// Per-user common+localpart interleaved: each user gets root deny then localpart.
|
||||
wantSSH: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{
|
||||
{
|
||||
Principals: []*tailcfg.SSHPrincipal{{NodeIP: "100.64.0.6"}},
|
||||
SSHUsers: map[string]string{"root": ""},
|
||||
Action: &tailcfg.SSHAction{
|
||||
Accept: true,
|
||||
AllowAgentForwarding: true,
|
||||
AllowLocalPortForwarding: true,
|
||||
AllowRemotePortForwarding: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Principals: []*tailcfg.SSHPrincipal{{NodeIP: "100.64.0.6"}},
|
||||
SSHUsers: map[string]string{"alice": "alice"},
|
||||
Action: &tailcfg.SSHAction{
|
||||
Accept: true,
|
||||
AllowAgentForwarding: true,
|
||||
AllowLocalPortForwarding: true,
|
||||
AllowRemotePortForwarding: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Principals: []*tailcfg.SSHPrincipal{{NodeIP: "100.64.0.7"}},
|
||||
SSHUsers: map[string]string{"root": ""},
|
||||
Action: &tailcfg.SSHAction{
|
||||
Accept: true,
|
||||
AllowAgentForwarding: true,
|
||||
AllowLocalPortForwarding: true,
|
||||
AllowRemotePortForwarding: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Principals: []*tailcfg.SSHPrincipal{{NodeIP: "100.64.0.7"}},
|
||||
SSHUsers: map[string]string{"bob": "bob"},
|
||||
Action: &tailcfg.SSHAction{
|
||||
Accept: true,
|
||||
AllowAgentForwarding: true,
|
||||
AllowLocalPortForwarding: true,
|
||||
AllowRemotePortForwarding: true,
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "localpart-combined-with-root",
|
||||
targetNode: nodeTaggedServer,
|
||||
peers: types.Nodes{&nodeAlice},
|
||||
policy: `{
|
||||
"tagOwners": {
|
||||
"tag:server": ["alice@example.com"]
|
||||
},
|
||||
"ssh": [
|
||||
{
|
||||
"action": "accept",
|
||||
"src": ["autogroup:member"],
|
||||
"dst": ["tag:server"],
|
||||
"users": ["localpart:*@example.com", "root"]
|
||||
}
|
||||
]
|
||||
}`,
|
||||
// Common root rule followed by alice's per-user localpart rule (interleaved).
|
||||
wantSSH: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{
|
||||
{
|
||||
Principals: []*tailcfg.SSHPrincipal{{NodeIP: "100.64.0.6"}},
|
||||
SSHUsers: map[string]string{"root": "root"},
|
||||
Action: &tailcfg.SSHAction{
|
||||
Accept: true,
|
||||
AllowAgentForwarding: true,
|
||||
AllowLocalPortForwarding: true,
|
||||
AllowRemotePortForwarding: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Principals: []*tailcfg.SSHPrincipal{{NodeIP: "100.64.0.6"}},
|
||||
SSHUsers: map[string]string{"alice": "alice"},
|
||||
Action: &tailcfg.SSHAction{
|
||||
Accept: true,
|
||||
AllowAgentForwarding: true,
|
||||
AllowLocalPortForwarding: true,
|
||||
AllowRemotePortForwarding: true,
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue