mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-28 17:26:52 +09:00
policy/v2: use Alias and SSHUser types in SSHPolicyTest
This commit is contained in:
1 parent
63de6b2d38
commit
9b4f68257d
5 files changed
+127
-55
No files matched your search
@@ -230,8 +230,13 @@ func runSSHPolicyTest(
|
|||||||
nodes views.Slice[types.NodeView],
|
nodes views.Slice[types.NodeView],
|
||||||
cache map[types.NodeID]*tailcfg.SSHPolicy,
|
cache map[types.NodeID]*tailcfg.SSHPolicy,
|
||||||
) SSHPolicyTestResult {
|
) SSHPolicyTestResult {
|
||||||
|
srcLabel := ""
|
||||||
|
if test.Src != nil {
|
||||||
|
srcLabel = test.Src.String()
|
||||||
|
}
|
||||||
|
|
||||||
res := SSHPolicyTestResult{
|
res := SSHPolicyTestResult{
|
||||||
Src: test.Src,
|
Src: srcLabel,
|
||||||
Passed: true,
|
Passed: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +244,7 @@ func runSSHPolicyTest(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
res.Passed = false
|
res.Passed = false
|
||||||
res.Errors = append(res.Errors,
|
res.Errors = append(res.Errors,
|
||||||
fmt.Sprintf("failed to resolve source %q: %v", test.Src, err))
|
fmt.Sprintf("failed to resolve source %q: %v", srcLabel, err))
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@@ -247,7 +252,7 @@ func runSSHPolicyTest(
|
|||||||
if len(srcAddrs) == 0 {
|
if len(srcAddrs) == 0 {
|
||||||
res.Passed = false
|
res.Passed = false
|
||||||
res.Errors = append(res.Errors,
|
res.Errors = append(res.Errors,
|
||||||
fmt.Sprintf("source %q resolved to no IP addresses", test.Src))
|
fmt.Sprintf("source %q resolved to no IP addresses", srcLabel))
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@@ -290,7 +295,7 @@ func runSSHPolicyTest(
|
|||||||
for _, user := range test.Accept {
|
for _, user := range test.Accept {
|
||||||
evaluateAssertion(
|
evaluateAssertion(
|
||||||
pol, users, nodes, cache,
|
pol, users, nodes, cache,
|
||||||
srcAddrs, dstNodes, user,
|
srcAddrs, dstNodes, user.String(),
|
||||||
assertAccept, &res,
|
assertAccept, &res,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -298,7 +303,7 @@ func runSSHPolicyTest(
|
|||||||
for _, user := range test.Deny {
|
for _, user := range test.Deny {
|
||||||
evaluateAssertion(
|
evaluateAssertion(
|
||||||
pol, users, nodes, cache,
|
pol, users, nodes, cache,
|
||||||
srcAddrs, dstNodes, user,
|
srcAddrs, dstNodes, user.String(),
|
||||||
assertDeny, &res,
|
assertDeny, &res,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -306,7 +311,7 @@ func runSSHPolicyTest(
|
|||||||
for _, user := range test.Check {
|
for _, user := range test.Check {
|
||||||
evaluateAssertion(
|
evaluateAssertion(
|
||||||
pol, users, nodes, cache,
|
pol, users, nodes, cache,
|
||||||
srcAddrs, dstNodes, user,
|
srcAddrs, dstNodes, user.String(),
|
||||||
assertCheck, &res,
|
assertCheck, &res,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -438,23 +443,22 @@ func appendUserDst(m map[string][]string, user, dst string) map[string][]string
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveSSHTestSource resolves the src alias into a list of
|
// resolveSSHTestSource resolves the typed src alias into a list of
|
||||||
// netip.Addr (one per principal address the SSH compiler would emit
|
// netip.Addr (one per principal address the SSH compiler would emit
|
||||||
// for the same source). For user-shaped sources, srcUserID returns the
|
// for the same source). For user-shaped sources, srcUserID returns the
|
||||||
// resolved user's ID so autogroup:self destinations can scope to the
|
// resolved user's ID so autogroup:self destinations can scope to the
|
||||||
// same user. Returns ID 0 when the source is a tag, host, or IP.
|
// same user. Returns ID 0 when the source is a tag, host, or IP.
|
||||||
func resolveSSHTestSource(
|
func resolveSSHTestSource(
|
||||||
src string,
|
src Alias,
|
||||||
pol *Policy,
|
pol *Policy,
|
||||||
users []types.User,
|
users []types.User,
|
||||||
nodes views.Slice[types.NodeView],
|
nodes views.Slice[types.NodeView],
|
||||||
) ([]netip.Addr, uint, error) {
|
) ([]netip.Addr, uint, error) {
|
||||||
alias, err := parseAlias(src)
|
if src == nil {
|
||||||
if err != nil {
|
return nil, 0, nil
|
||||||
return nil, 0, fmt.Errorf("invalid alias: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addrs, err := alias.Resolve(pol, users, nodes)
|
addrs, err := src.Resolve(pol, users, nodes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, fmt.Errorf("resolving: %w", err)
|
return nil, 0, fmt.Errorf("resolving: %w", err)
|
||||||
}
|
}
|
||||||
@@ -470,7 +474,7 @@ func resolveSSHTestSource(
|
|||||||
|
|
||||||
var userID uint
|
var userID uint
|
||||||
|
|
||||||
u, ok := alias.(*Username)
|
u, ok := src.(*Username)
|
||||||
if ok {
|
if ok {
|
||||||
resolved, rErr := u.resolveUser(users)
|
resolved, rErr := u.resolveUser(users)
|
||||||
if rErr == nil {
|
if rErr == nil {
|
||||||
@@ -490,7 +494,7 @@ func resolveSSHTestSource(
|
|||||||
// node's IPs via InIPSet (the same primitive the SSH compiler uses to
|
// node's IPs via InIPSet (the same primitive the SSH compiler uses to
|
||||||
// decide whether a node is a destination of a given rule).
|
// decide whether a node is a destination of a given rule).
|
||||||
func resolveSSHTestDestNodes(
|
func resolveSSHTestDestNodes(
|
||||||
dsts []string,
|
dsts SSHTestDestinations,
|
||||||
pol *Policy,
|
pol *Policy,
|
||||||
users []types.User,
|
users []types.User,
|
||||||
nodes views.Slice[types.NodeView],
|
nodes views.Slice[types.NodeView],
|
||||||
@@ -503,12 +507,8 @@ func resolveSSHTestDestNodes(
|
|||||||
emptyDsts []string
|
emptyDsts []string
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, dst := range dsts {
|
for _, alias := range dsts {
|
||||||
alias, err := parseAlias(dst)
|
dstLabel := alias.String()
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("invalid destination %q: %w", dst, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
matched := false
|
matched := false
|
||||||
|
|
||||||
if ag, ok := alias.(*AutoGroup); ok && ag.Is(AutoGroupSelf) {
|
if ag, ok := alias.(*AutoGroup); ok && ag.Is(AutoGroupSelf) {
|
||||||
@@ -519,7 +519,7 @@ func resolveSSHTestDestNodes(
|
|||||||
// (matches SaaS, which treats a no-node dst as a
|
// (matches SaaS, which treats a no-node dst as a
|
||||||
// failing assertion).
|
// failing assertion).
|
||||||
if srcUserID == 0 {
|
if srcUserID == 0 {
|
||||||
emptyDsts = append(emptyDsts, dst)
|
emptyDsts = append(emptyDsts, dstLabel)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -548,7 +548,7 @@ func resolveSSHTestDestNodes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !matched {
|
if !matched {
|
||||||
emptyDsts = append(emptyDsts, dst)
|
emptyDsts = append(emptyDsts, dstLabel)
|
||||||
}
|
}
|
||||||
|
|
||||||
continue
|
continue
|
||||||
@@ -556,11 +556,11 @@ func resolveSSHTestDestNodes(
|
|||||||
|
|
||||||
ips, err := alias.Resolve(pol, users, nodes)
|
ips, err := alias.Resolve(pol, users, nodes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("resolving destination %q: %w", dst, err)
|
return nil, nil, fmt.Errorf("resolving destination %q: %w", dstLabel, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ips == nil || ips.Empty() {
|
if ips == nil || ips.Empty() {
|
||||||
emptyDsts = append(emptyDsts, dst)
|
emptyDsts = append(emptyDsts, dstLabel)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -570,7 +570,7 @@ func resolveSSHTestDestNodes(
|
|||||||
// the resolved prefixes.
|
// the resolved prefixes.
|
||||||
set, err := prefixesToIPSet(ips.Prefixes())
|
set, err := prefixesToIPSet(ips.Prefixes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("building IPSet for %q: %w", dst, err)
|
return nil, nil, fmt.Errorf("building IPSet for %q: %w", dstLabel, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, n := range nodes.All() {
|
for _, n := range nodes.All() {
|
||||||
@@ -589,7 +589,7 @@ func resolveSSHTestDestNodes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !matched {
|
if !matched {
|
||||||
emptyDsts = append(emptyDsts, dst)
|
emptyDsts = append(emptyDsts, dstLabel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-json-experiment/json"
|
||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
"github.com/juanfont/headscale/hscontrol/util"
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
@@ -67,27 +68,92 @@ type PolicyTest struct {
|
|||||||
type SSHPolicyTest struct {
|
type SSHPolicyTest struct {
|
||||||
// Src is a single source alias (user, group, tag, host, or IP). Same
|
// Src is a single source alias (user, group, tag, host, or IP). Same
|
||||||
// shape as PolicyTest.Src — Tailscale only supports one src per entry.
|
// shape as PolicyTest.Src — Tailscale only supports one src per entry.
|
||||||
Src string `json:"src"`
|
Src Alias `json:"src"`
|
||||||
|
|
||||||
// Dst lists destination host aliases the test exercises. Tags, hosts,
|
// Dst lists destination host aliases the test exercises. Tags, hosts,
|
||||||
// and the SSH-compatible autogroups are valid; ports, CIDR ranges, and
|
// and the SSH-compatible autogroups are valid; ports, CIDR ranges, and
|
||||||
// autogroup:internet are rejected at parse time.
|
// autogroup:internet are rejected at parse time.
|
||||||
Dst []string `json:"dst"`
|
Dst SSHTestDestinations `json:"dst"`
|
||||||
|
|
||||||
// Accept lists SSH login users that must be allowed by an action:accept
|
// Accept lists SSH login users that must be allowed by an action:accept
|
||||||
// or action:check rule when Src connects to each entry in Dst.
|
// or action:check rule when Src connects to each entry in Dst.
|
||||||
Accept []string `json:"accept,omitempty"`
|
Accept []SSHUser `json:"accept,omitempty"`
|
||||||
|
|
||||||
// Deny lists SSH login users that must NOT be allowed by any rule when
|
// Deny lists SSH login users that must NOT be allowed by any rule when
|
||||||
// Src connects to each entry in Dst.
|
// Src connects to each entry in Dst.
|
||||||
Deny []string `json:"deny,omitempty"`
|
Deny []SSHUser `json:"deny,omitempty"`
|
||||||
|
|
||||||
// Check lists SSH login users that must reach every dst via an
|
// Check lists SSH login users that must reach every dst via an
|
||||||
// action:check rule specifically (the HoldAndDelegate signal on the
|
// action:check rule specifically (the HoldAndDelegate signal on the
|
||||||
// compiled SSH policy). An action:accept rule alone does not satisfy
|
// compiled SSH policy). An action:accept rule alone does not satisfy
|
||||||
// a check assertion — SaaS keeps the two categories distinct so
|
// a check assertion — SaaS keeps the two categories distinct so
|
||||||
// policy authors can pin sensitive logins to check rules.
|
// policy authors can pin sensitive logins to check rules.
|
||||||
Check []string `json:"check,omitempty"`
|
Check []SSHUser `json:"check,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSHTestDestinations is the list of destination aliases an sshTests entry
|
||||||
|
// targets. Unmarshalling reuses the same alias parser the rest of the
|
||||||
|
// policy engine drives so each element lands as a typed Alias; the parse-
|
||||||
|
// time shape rules in validateSSHTestDestination continue to enforce the
|
||||||
|
// SSH-specific restrictions (no :port, no CIDR, no autogroup:internet,
|
||||||
|
// known tag).
|
||||||
|
type SSHTestDestinations []Alias
|
||||||
|
|
||||||
|
// UnmarshalJSON walks the JSON array, dispatching each element through
|
||||||
|
// AliasEnc so trimming and prefix detection match the rest of the parser.
|
||||||
|
func (d *SSHTestDestinations) UnmarshalJSON(b []byte) error {
|
||||||
|
var aliases []AliasEnc
|
||||||
|
|
||||||
|
err := json.Unmarshal(b, &aliases, policyJSONOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*d = make([]Alias, len(aliases))
|
||||||
|
for i, a := range aliases {
|
||||||
|
(*d)[i] = a.Alias
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON drives the typed shape of SSHPolicyTest. The wire format
|
||||||
|
// is unchanged: src is a JSON string parsed through parseAlias; dst is an
|
||||||
|
// array of strings handled by SSHTestDestinations; accept/deny/check are
|
||||||
|
// arrays of strings handled per element by SSHUser.UnmarshalJSON. An
|
||||||
|
// empty src string lands as a nil Alias so the empty-src case stays a
|
||||||
|
// validation-time error with the SaaS-aligned ErrSSHTestEmptySrc body
|
||||||
|
// rather than a raw parser failure.
|
||||||
|
func (t *SSHPolicyTest) UnmarshalJSON(b []byte) error {
|
||||||
|
var raw struct {
|
||||||
|
Src string `json:"src"`
|
||||||
|
Dst SSHTestDestinations `json:"dst"`
|
||||||
|
Accept []SSHUser `json:"accept,omitempty"`
|
||||||
|
Deny []SSHUser `json:"deny,omitempty"`
|
||||||
|
Check []SSHUser `json:"check,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.Unmarshal(b, &raw, policyJSONOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
trimmedSrc := strings.TrimSpace(raw.Src)
|
||||||
|
if trimmedSrc != "" {
|
||||||
|
alias, parseErr := parseAlias(trimmedSrc)
|
||||||
|
if parseErr != nil {
|
||||||
|
return parseErr
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Src = alias
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Dst = raw.Dst
|
||||||
|
t.Accept = raw.Accept
|
||||||
|
t.Deny = raw.Deny
|
||||||
|
t.Check = raw.Check
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PolicyTestResult is the outcome of a single PolicyTest.
|
// PolicyTestResult is the outcome of a single PolicyTest.
|
||||||
|
|||||||
@@ -843,6 +843,12 @@ type Alias interface {
|
|||||||
Validate() error
|
Validate() error
|
||||||
UnmarshalJSON(b []byte) error
|
UnmarshalJSON(b []byte) error
|
||||||
|
|
||||||
|
// String renders the alias back to its policy-file form. Implementations
|
||||||
|
// are expected to return a value that round-trips through parseAlias for
|
||||||
|
// any alias the parser accepted, so callers can use it as a stable
|
||||||
|
// identity in rendered errors and logs.
|
||||||
|
String() string
|
||||||
|
|
||||||
// Resolve resolves the Alias to an IPSet. The IPSet will contain all the IP
|
// Resolve resolves the Alias to an IPSet. The IPSet will contain all the IP
|
||||||
// addresses that the Alias represents within Headscale. It is the product
|
// addresses that the Alias represents within Headscale. It is the product
|
||||||
// of the Alias and the Policy, Users and Nodes.
|
// of the Alias and the Policy, Users and Nodes.
|
||||||
@@ -3286,7 +3292,7 @@ func validateSSHTests(pol *Policy, tests []SSHPolicyTest) error {
|
|||||||
var errs []error
|
var errs []error
|
||||||
|
|
||||||
for i, t := range tests {
|
for i, t := range tests {
|
||||||
if t.Src == "" {
|
if t.Src == nil {
|
||||||
errs = append(errs, fmt.Errorf("sshTest %d: %w", i, ErrSSHTestEmptySrc))
|
errs = append(errs, fmt.Errorf("sshTest %d: %w", i, ErrSSHTestEmptySrc))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3317,11 +3323,8 @@ func validateSSHTests(pol *Policy, tests []SSHPolicyTest) error {
|
|||||||
// (only valid in ACL destinations, not SSH ones). Tag entries must
|
// (only valid in ACL destinations, not SSH ones). Tag entries must
|
||||||
// reference a tag that exists in tagOwners; bare hosts must resolve to a
|
// reference a tag that exists in tagOwners; bare hosts must resolve to a
|
||||||
// single-address prefix.
|
// single-address prefix.
|
||||||
func validateSSHTestDestination(pol *Policy, dst string) error {
|
func validateSSHTestDestination(pol *Policy, alias Alias) error {
|
||||||
alias, err := parseAlias(dst)
|
dst := alias.String()
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w %q", ErrSSHTestDstDisallowedElement, dst)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch a := alias.(type) {
|
switch a := alias.(type) {
|
||||||
case *AutoGroup:
|
case *AutoGroup:
|
||||||
@@ -3333,12 +3336,12 @@ func validateSSHTestDestination(pol *Policy, dst string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case *Prefix:
|
case *Prefix:
|
||||||
// A CIDR literal in dst is rejected. A bare IP parses as a Prefix
|
// IP / CIDR dsts are rejected: SSH dsts name hosts, tags, or
|
||||||
// with no slash in the input string — distinguish on the raw text
|
// SSH-compatible autogroups, not raw addresses. Bare IPs and
|
||||||
// the same way validateTestDestination does.
|
// `/BitLen` literals parse to the same Prefix value so this
|
||||||
if strings.Contains(dst, "/") {
|
// branch covers both with one disallowed-element answer.
|
||||||
return fmt.Errorf("%w %q", ErrSSHTestDstDisallowedElement, dst)
|
_ = a
|
||||||
}
|
return fmt.Errorf("%w %q", ErrSSHTestDstDisallowedElement, dst)
|
||||||
|
|
||||||
case *Tag:
|
case *Tag:
|
||||||
// A tag must be declared in tagOwners. The `tag:server:22` shape
|
// A tag must be declared in tagOwners. The `tag:server:22` shape
|
||||||
|
|||||||
@@ -6003,9 +6003,12 @@ func TestUnmarshalPolicySSHTests(t *testing.T) {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
require.Len(t, pol.SSHTests, 1)
|
require.Len(t, pol.SSHTests, 1)
|
||||||
got := pol.SSHTests[0]
|
got := pol.SSHTests[0]
|
||||||
require.Equal(t, "thor@example.org", got.Src)
|
require.IsType(t, (*Username)(nil), got.Src)
|
||||||
require.Equal(t, []string{"tag:server"}, got.Dst)
|
require.Equal(t, "thor@example.org", got.Src.String())
|
||||||
require.Equal(t, []string{"root"}, got.Accept)
|
require.Len(t, got.Dst, 1)
|
||||||
|
require.IsType(t, (*Tag)(nil), got.Dst[0])
|
||||||
|
require.Equal(t, "tag:server", got.Dst[0].String())
|
||||||
|
require.Equal(t, []SSHUser{"root"}, got.Accept)
|
||||||
require.Empty(t, got.Deny)
|
require.Empty(t, got.Deny)
|
||||||
require.Empty(t, got.Check)
|
require.Empty(t, got.Check)
|
||||||
},
|
},
|
||||||
@@ -6030,9 +6033,9 @@ func TestUnmarshalPolicySSHTests(t *testing.T) {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
require.Len(t, pol.SSHTests, 1)
|
require.Len(t, pol.SSHTests, 1)
|
||||||
got := pol.SSHTests[0]
|
got := pol.SSHTests[0]
|
||||||
require.Equal(t, []string{"root"}, got.Accept)
|
require.Equal(t, []SSHUser{"root"}, got.Accept)
|
||||||
require.Equal(t, []string{"nobody"}, got.Deny)
|
require.Equal(t, []SSHUser{"nobody"}, got.Deny)
|
||||||
require.Equal(t, []string{"alice"}, got.Check)
|
require.Equal(t, []SSHUser{"alice"}, got.Check) //nolint:goconst
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -200,9 +200,9 @@ func TestSSHTestsRejectFailingPolicy(t *testing.T) {
|
|||||||
},
|
},
|
||||||
SSHTests: []policyv2.SSHPolicyTest{
|
SSHTests: []policyv2.SSHPolicyTest{
|
||||||
{
|
{
|
||||||
Src: user1,
|
Src: usernamep(user1),
|
||||||
Dst: []string{"autogroup:member"},
|
Dst: policyv2.SSHTestDestinations{new(policyv2.AutoGroupMember)},
|
||||||
Accept: []string{"root"},
|
Accept: []policyv2.SSHUser{policyv2.SSHUser("root")},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -215,9 +215,9 @@ func TestSSHTestsRejectFailingPolicy(t *testing.T) {
|
|||||||
badPolicy := goodPolicy
|
badPolicy := goodPolicy
|
||||||
badPolicy.SSHTests = []policyv2.SSHPolicyTest{
|
badPolicy.SSHTests = []policyv2.SSHPolicyTest{
|
||||||
{
|
{
|
||||||
Src: user2,
|
Src: usernamep(user2),
|
||||||
Dst: []string{"autogroup:member"},
|
Dst: policyv2.SSHTestDestinations{new(policyv2.AutoGroupMember)},
|
||||||
Accept: []string{"root"},
|
Accept: []policyv2.SSHUser{policyv2.SSHUser("root")},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user