mirror of
https://github.com/gravitational/teleport
synced 2024-10-22 02:03:24 +00:00
2580 lines
70 KiB
Go
2580 lines
70 KiB
Go
/*
|
|
Copyright 2015-2020 Gravitational, Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package services
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
"github.com/gravitational/teleport"
|
|
"github.com/gravitational/teleport/api/types"
|
|
"github.com/gravitational/teleport/api/types/wrappers"
|
|
"github.com/gravitational/teleport/lib/defaults"
|
|
"github.com/gravitational/teleport/lib/fixtures"
|
|
"github.com/gravitational/teleport/lib/tlsca"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/gravitational/trace"
|
|
"github.com/pborman/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestConnAndSessLimits verifies that role sets correctly calculate
|
|
// a user's MaxConnections and MaxSessions values from multiple
|
|
// roles with different individual values. These are tested together since
|
|
// both values use the same resolution rules.
|
|
func TestConnAndSessLimits(t *testing.T) {
|
|
utils.InitLoggerForTests(testing.Verbose())
|
|
tts := []struct {
|
|
desc string
|
|
vals []int64
|
|
want int64
|
|
}{
|
|
{
|
|
desc: "smallest nonzero value is selected from mixed values",
|
|
vals: []int64{8, 6, 7, 5, 3, 0, 9},
|
|
want: 3,
|
|
},
|
|
{
|
|
desc: "smallest value selected from all nonzero values",
|
|
vals: []int64{5, 6, 7, 8},
|
|
want: 5,
|
|
},
|
|
{
|
|
desc: "all zero values results in a zero value",
|
|
vals: []int64{0, 0, 0, 0, 0, 0, 0},
|
|
want: 0,
|
|
},
|
|
}
|
|
for ti, tt := range tts {
|
|
cmt := fmt.Sprintf("test case %d: %s", ti, tt.desc)
|
|
var set RoleSet
|
|
for i, val := range tt.vals {
|
|
role := &RoleV3{
|
|
Kind: KindRole,
|
|
Version: V3,
|
|
Metadata: Metadata{
|
|
Name: fmt.Sprintf("role-%d", i),
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
MaxConnections: val,
|
|
MaxSessions: val,
|
|
},
|
|
},
|
|
}
|
|
require.NoError(t, role.CheckAndSetDefaults(), cmt)
|
|
set = append(set, role)
|
|
}
|
|
require.Equal(t, tt.want, set.MaxConnections(), cmt)
|
|
require.Equal(t, tt.want, set.MaxSessions(), cmt)
|
|
}
|
|
}
|
|
|
|
func TestRoleParse(t *testing.T) {
|
|
utils.InitLoggerForTests(testing.Verbose())
|
|
testCases := []struct {
|
|
name string
|
|
in string
|
|
role RoleV3
|
|
error error
|
|
matchMessage string
|
|
}{
|
|
{
|
|
name: "no input, should not parse",
|
|
in: ``,
|
|
role: RoleV3{},
|
|
error: trace.BadParameter("empty input"),
|
|
},
|
|
{
|
|
name: "validation error, no name",
|
|
in: `{}`,
|
|
role: RoleV3{},
|
|
error: trace.BadParameter("failed to validate: name: name is required"),
|
|
},
|
|
{
|
|
name: "validation error, no name",
|
|
in: `{"kind": "role"}`,
|
|
role: RoleV3{},
|
|
error: trace.BadParameter("failed to validate: name: name is required"),
|
|
},
|
|
|
|
{
|
|
name: "validation error, missing resources",
|
|
in: `{
|
|
"kind": "role",
|
|
"version": "v3",
|
|
"metadata": {"name": "name1"},
|
|
"spec": {
|
|
"allow": {
|
|
"node_labels": {"a": "b"},
|
|
"namespaces": ["default"],
|
|
"rules": [
|
|
{
|
|
"verbs": ["read", "list"]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}`,
|
|
error: trace.BadParameter(""),
|
|
matchMessage: "missing resources",
|
|
},
|
|
{
|
|
name: "validation error, missing verbs",
|
|
in: `{
|
|
"kind": "role",
|
|
"version": "v3",
|
|
"metadata": {"name": "name1"},
|
|
"spec": {
|
|
"allow": {
|
|
"node_labels": {"a": "b"},
|
|
"namespaces": ["default"],
|
|
"rules": [
|
|
{
|
|
"resources": ["role"]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}`,
|
|
error: trace.BadParameter(""),
|
|
matchMessage: "missing verbs",
|
|
},
|
|
{
|
|
name: "role with no spec still gets defaults",
|
|
in: `{"kind": "role", "version": "v3", "metadata": {"name": "defrole"}, "spec": {}}`,
|
|
role: RoleV3{
|
|
Kind: KindRole,
|
|
Version: V3,
|
|
Metadata: Metadata{
|
|
Name: "defrole",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
CertificateFormat: teleport.CertificateFormatStandard,
|
|
MaxSessionTTL: NewDuration(defaults.MaxCertDuration),
|
|
PortForwarding: NewBoolOption(true),
|
|
BPF: defaults.EnhancedEvents(),
|
|
},
|
|
Allow: RoleConditions{
|
|
NodeLabels: Labels{},
|
|
AppLabels: Labels{Wildcard: []string{Wildcard}},
|
|
KubernetesLabels: Labels{Wildcard: []string{Wildcard}},
|
|
DatabaseLabels: Labels{Wildcard: []string{Wildcard}},
|
|
Namespaces: []string{defaults.Namespace},
|
|
},
|
|
Deny: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
},
|
|
},
|
|
},
|
|
error: nil,
|
|
},
|
|
{
|
|
name: "full valid role",
|
|
in: `{
|
|
"kind": "role",
|
|
"version": "v3",
|
|
"metadata": {"name": "name1", "labels": {"a-b": "c"}},
|
|
"spec": {
|
|
"options": {
|
|
"cert_format": "standard",
|
|
"max_session_ttl": "20h",
|
|
"port_forwarding": true,
|
|
"client_idle_timeout": "17m",
|
|
"disconnect_expired_cert": "yes",
|
|
"enhanced_recording": ["command", "network"]
|
|
},
|
|
"allow": {
|
|
"node_labels": {"a": "b", "c-d": "e"},
|
|
"app_labels": {"a": "b", "c-d": "e"},
|
|
"kubernetes_labels": {"a": "b", "c-d": "e"},
|
|
"db_labels": {"a": "b", "c-d": "e"},
|
|
"db_names": ["postgres"],
|
|
"db_users": ["postgres"],
|
|
"namespaces": ["default"],
|
|
"rules": [
|
|
{
|
|
"resources": ["role"],
|
|
"verbs": ["read", "list"],
|
|
"where": "contains(user.spec.traits[\"groups\"], \"prod\")",
|
|
"actions": [
|
|
"log(\"info\", \"log entry\")"
|
|
]
|
|
}
|
|
]
|
|
},
|
|
"deny": {
|
|
"logins": ["c"]
|
|
}
|
|
}
|
|
}`,
|
|
role: RoleV3{
|
|
Kind: KindRole,
|
|
Version: V3,
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: defaults.Namespace,
|
|
Labels: map[string]string{"a-b": "c"},
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
CertificateFormat: teleport.CertificateFormatStandard,
|
|
MaxSessionTTL: NewDuration(20 * time.Hour),
|
|
PortForwarding: NewBoolOption(true),
|
|
ClientIdleTimeout: NewDuration(17 * time.Minute),
|
|
DisconnectExpiredCert: NewBool(true),
|
|
BPF: defaults.EnhancedEvents(),
|
|
},
|
|
Allow: RoleConditions{
|
|
NodeLabels: Labels{"a": []string{"b"}, "c-d": []string{"e"}},
|
|
AppLabels: Labels{"a": []string{"b"}, "c-d": []string{"e"}},
|
|
KubernetesLabels: Labels{"a": []string{"b"}, "c-d": []string{"e"}},
|
|
DatabaseLabels: Labels{"a": []string{"b"}, "c-d": []string{"e"}},
|
|
DatabaseNames: []string{"postgres"},
|
|
DatabaseUsers: []string{"postgres"},
|
|
Namespaces: []string{"default"},
|
|
Rules: []Rule{
|
|
{
|
|
Resources: []string{KindRole},
|
|
Verbs: []string{VerbRead, VerbList},
|
|
Where: "contains(user.spec.traits[\"groups\"], \"prod\")",
|
|
Actions: []string{
|
|
"log(\"info\", \"log entry\")",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Deny: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
Logins: []string{"c"},
|
|
},
|
|
},
|
|
},
|
|
error: nil,
|
|
},
|
|
{
|
|
name: "alternative options form",
|
|
in: `{
|
|
"kind": "role",
|
|
"version": "v3",
|
|
"metadata": {"name": "name1"},
|
|
"spec": {
|
|
"options": {
|
|
"cert_format": "standard",
|
|
"max_session_ttl": "20h",
|
|
"port_forwarding": "yes",
|
|
"forward_agent": "yes",
|
|
"client_idle_timeout": "never",
|
|
"disconnect_expired_cert": "no",
|
|
"enhanced_recording": ["command", "network"]
|
|
},
|
|
"allow": {
|
|
"node_labels": {"a": "b"},
|
|
"app_labels": {"a": "b"},
|
|
"kubernetes_labels": {"c": "d"},
|
|
"db_labels": {"e": "f"},
|
|
"namespaces": ["default"],
|
|
"rules": [
|
|
{
|
|
"resources": ["role"],
|
|
"verbs": ["read", "list"],
|
|
"where": "contains(user.spec.traits[\"groups\"], \"prod\")",
|
|
"actions": [
|
|
"log(\"info\", \"log entry\")"
|
|
]
|
|
}
|
|
]
|
|
},
|
|
"deny": {
|
|
"logins": ["c"]
|
|
}
|
|
}
|
|
}`,
|
|
role: RoleV3{
|
|
Kind: KindRole,
|
|
Version: V3,
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
CertificateFormat: teleport.CertificateFormatStandard,
|
|
ForwardAgent: NewBool(true),
|
|
MaxSessionTTL: NewDuration(20 * time.Hour),
|
|
PortForwarding: NewBoolOption(true),
|
|
ClientIdleTimeout: NewDuration(0),
|
|
DisconnectExpiredCert: NewBool(false),
|
|
BPF: defaults.EnhancedEvents(),
|
|
},
|
|
Allow: RoleConditions{
|
|
NodeLabels: Labels{"a": []string{"b"}},
|
|
AppLabels: Labels{"a": []string{"b"}},
|
|
KubernetesLabels: Labels{"c": []string{"d"}},
|
|
DatabaseLabels: Labels{"e": []string{"f"}},
|
|
Namespaces: []string{"default"},
|
|
Rules: []Rule{
|
|
{
|
|
Resources: []string{KindRole},
|
|
Verbs: []string{VerbRead, VerbList},
|
|
Where: "contains(user.spec.traits[\"groups\"], \"prod\")",
|
|
Actions: []string{
|
|
"log(\"info\", \"log entry\")",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Deny: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
Logins: []string{"c"},
|
|
},
|
|
},
|
|
},
|
|
error: nil,
|
|
},
|
|
{
|
|
name: "non-scalar and scalar values of labels",
|
|
in: `{
|
|
"kind": "role",
|
|
"version": "v3",
|
|
"metadata": {"name": "name1"},
|
|
"spec": {
|
|
"options": {
|
|
"cert_format": "standard",
|
|
"max_session_ttl": "20h",
|
|
"port_forwarding": "yes",
|
|
"forward_agent": "yes",
|
|
"client_idle_timeout": "never",
|
|
"disconnect_expired_cert": "no",
|
|
"enhanced_recording": ["command", "network"]
|
|
},
|
|
"allow": {
|
|
"node_labels": {"a": "b", "key": ["val"], "key2": ["val2", "val3"]},
|
|
"app_labels": {"a": "b", "key": ["val"], "key2": ["val2", "val3"]},
|
|
"kubernetes_labels": {"a": "b", "key": ["val"], "key2": ["val2", "val3"]},
|
|
"db_labels": {"a": "b", "key": ["val"], "key2": ["val2", "val3"]}
|
|
},
|
|
"deny": {
|
|
"logins": ["c"]
|
|
}
|
|
}
|
|
}`,
|
|
role: RoleV3{
|
|
Kind: KindRole,
|
|
Version: V3,
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
CertificateFormat: teleport.CertificateFormatStandard,
|
|
ForwardAgent: NewBool(true),
|
|
MaxSessionTTL: NewDuration(20 * time.Hour),
|
|
PortForwarding: NewBoolOption(true),
|
|
ClientIdleTimeout: NewDuration(0),
|
|
DisconnectExpiredCert: NewBool(false),
|
|
BPF: defaults.EnhancedEvents(),
|
|
},
|
|
Allow: RoleConditions{
|
|
NodeLabels: Labels{
|
|
"a": []string{"b"},
|
|
"key": []string{"val"},
|
|
"key2": []string{"val2", "val3"},
|
|
},
|
|
AppLabels: Labels{
|
|
"a": []string{"b"},
|
|
"key": []string{"val"},
|
|
"key2": []string{"val2", "val3"},
|
|
},
|
|
KubernetesLabels: Labels{
|
|
"a": []string{"b"},
|
|
"key": []string{"val"},
|
|
"key2": []string{"val2", "val3"},
|
|
},
|
|
DatabaseLabels: Labels{
|
|
"a": []string{"b"},
|
|
"key": []string{"val"},
|
|
"key2": []string{"val2", "val3"},
|
|
},
|
|
Namespaces: []string{"default"},
|
|
},
|
|
Deny: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
Logins: []string{"c"},
|
|
},
|
|
},
|
|
},
|
|
error: nil,
|
|
},
|
|
}
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
role, err := UnmarshalRole([]byte(tc.in))
|
|
if tc.error != nil {
|
|
require.Error(t, err)
|
|
if tc.matchMessage != "" {
|
|
require.Contains(t, err.Error(), tc.matchMessage)
|
|
}
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.Empty(t, cmp.Diff(*role, tc.role))
|
|
|
|
err := ValidateRole(role)
|
|
require.NoError(t, err)
|
|
|
|
out, err := json.Marshal(role)
|
|
require.NoError(t, err)
|
|
|
|
role2, err := UnmarshalRole(out)
|
|
require.NoError(t, err)
|
|
require.Empty(t, cmp.Diff(*role2, tc.role))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateRole(t *testing.T) {
|
|
var tests = []struct {
|
|
name string
|
|
spec RoleSpecV3
|
|
err error
|
|
matchMessage string
|
|
}{
|
|
{
|
|
name: "valid syntax",
|
|
spec: RoleSpecV3{
|
|
Allow: RoleConditions{
|
|
Logins: []string{`{{external["http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"]}}`},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "invalid role condition login syntax",
|
|
spec: RoleSpecV3{
|
|
Allow: RoleConditions{
|
|
Logins: []string{"{{foo"},
|
|
},
|
|
},
|
|
err: trace.BadParameter(""),
|
|
matchMessage: "invalid login found",
|
|
},
|
|
{
|
|
name: "unsupported function in actions",
|
|
spec: RoleSpecV3{
|
|
Allow: RoleConditions{
|
|
Logins: []string{`{{external["http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"]}}`},
|
|
Rules: []Rule{
|
|
{
|
|
Resources: []string{"role"},
|
|
Verbs: []string{"read", "list"},
|
|
Where: "containz(user.spec.traits[\"groups\"], \"prod\")",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
err: trace.BadParameter(""),
|
|
matchMessage: "unsupported function: containz",
|
|
},
|
|
{
|
|
name: "unsupported function in where",
|
|
spec: RoleSpecV3{
|
|
Allow: RoleConditions{
|
|
Logins: []string{`{{external["http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"]}}`},
|
|
Rules: []Rule{
|
|
{
|
|
Resources: []string{"role"},
|
|
Verbs: []string{"read", "list"},
|
|
Where: "contains(user.spec.traits[\"groups\"], \"prod\")",
|
|
Actions: []string{"zzz(\"info\", \"log entry\")"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
err: trace.BadParameter(""),
|
|
matchMessage: "unsupported function: zzz",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
err := ValidateRole(&types.RoleV3{
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: tc.spec,
|
|
})
|
|
if tc.err != nil {
|
|
require.Error(t, err, tc.name)
|
|
if tc.matchMessage != "" {
|
|
require.Contains(t, err.Error(), tc.matchMessage)
|
|
}
|
|
} else {
|
|
require.NoError(t, err, tc.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestLabelCompatibility makes sure that labels
|
|
// are serialized in format understood by older servers with
|
|
// scalar labels
|
|
func TestLabelCompatibility(t *testing.T) {
|
|
labels := Labels{
|
|
"key": []string{"val"},
|
|
}
|
|
data, err := json.Marshal(labels)
|
|
require.NoError(t, err)
|
|
|
|
var out map[string]string
|
|
err = json.Unmarshal(data, &out)
|
|
require.NoError(t, err)
|
|
require.Equal(t, map[string]string{"key": "val"}, out)
|
|
}
|
|
|
|
func TestCheckAccessToServer(t *testing.T) {
|
|
type check struct {
|
|
server Server
|
|
hasAccess bool
|
|
login string
|
|
}
|
|
serverA := &ServerV2{
|
|
Metadata: Metadata{
|
|
Name: "a",
|
|
},
|
|
}
|
|
serverB := &ServerV2{
|
|
Metadata: Metadata{
|
|
Name: "b",
|
|
Namespace: defaults.Namespace,
|
|
Labels: map[string]string{"role": "worker", "status": "follower"},
|
|
},
|
|
}
|
|
namespaceC := "namespace-c"
|
|
serverC := &ServerV2{
|
|
Metadata: Metadata{
|
|
Name: "c",
|
|
Namespace: namespaceC,
|
|
Labels: map[string]string{"role": "db", "status": "follower"},
|
|
},
|
|
}
|
|
serverC2 := &ServerV2{
|
|
Metadata: Metadata{
|
|
Name: "c2",
|
|
Namespace: namespaceC,
|
|
Labels: map[string]string{"role": "db01", "status": "follower01"},
|
|
},
|
|
}
|
|
testCases := []struct {
|
|
name string
|
|
roles []RoleV3
|
|
checks []check
|
|
}{
|
|
{
|
|
name: "empty role set has access to nothing",
|
|
roles: []RoleV3{},
|
|
checks: []check{
|
|
{server: serverA, login: "root", hasAccess: false},
|
|
{server: serverB, login: "root", hasAccess: false},
|
|
{server: serverC, login: "root", hasAccess: false},
|
|
},
|
|
},
|
|
{
|
|
name: "role is limited to default namespace",
|
|
roles: []RoleV3{
|
|
{
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
MaxSessionTTL: Duration(20 * time.Hour),
|
|
},
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
Logins: []string{"admin"},
|
|
NodeLabels: Labels{Wildcard: []string{Wildcard}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
checks: []check{
|
|
{server: serverA, login: "root", hasAccess: false},
|
|
{server: serverA, login: "admin", hasAccess: true},
|
|
{server: serverB, login: "root", hasAccess: false},
|
|
{server: serverB, login: "admin", hasAccess: true},
|
|
{server: serverC, login: "root", hasAccess: false},
|
|
{server: serverC, login: "admin", hasAccess: false},
|
|
},
|
|
},
|
|
{
|
|
name: "role is limited to labels in default namespace",
|
|
roles: []RoleV3{
|
|
{
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
MaxSessionTTL: Duration(20 * time.Hour),
|
|
},
|
|
Allow: RoleConditions{
|
|
Logins: []string{"admin"},
|
|
NodeLabels: Labels{"role": []string{"worker"}},
|
|
Namespaces: []string{defaults.Namespace},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
checks: []check{
|
|
{server: serverA, login: "root", hasAccess: false},
|
|
{server: serverA, login: "admin", hasAccess: false},
|
|
{server: serverB, login: "root", hasAccess: false},
|
|
{server: serverB, login: "admin", hasAccess: true},
|
|
{server: serverC, login: "root", hasAccess: false},
|
|
{server: serverC, login: "admin", hasAccess: false},
|
|
},
|
|
},
|
|
{
|
|
name: "role matches any label out of multiple labels",
|
|
roles: []RoleV3{
|
|
{
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
MaxSessionTTL: Duration(20 * time.Hour),
|
|
},
|
|
Allow: RoleConditions{
|
|
Logins: []string{"admin"},
|
|
NodeLabels: Labels{"role": []string{"worker2", "worker"}},
|
|
Namespaces: []string{defaults.Namespace},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
checks: []check{
|
|
{server: serverA, login: "root", hasAccess: false},
|
|
{server: serverA, login: "admin", hasAccess: false},
|
|
{server: serverB, login: "root", hasAccess: false},
|
|
{server: serverB, login: "admin", hasAccess: true},
|
|
{server: serverC, login: "root", hasAccess: false},
|
|
{server: serverC, login: "admin", hasAccess: false},
|
|
},
|
|
},
|
|
{
|
|
name: "node_labels with empty list value matches nothing",
|
|
roles: []RoleV3{
|
|
{
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
MaxSessionTTL: Duration(20 * time.Hour),
|
|
},
|
|
Allow: RoleConditions{
|
|
Logins: []string{"admin"},
|
|
NodeLabels: Labels{"role": []string{}},
|
|
Namespaces: []string{defaults.Namespace},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
checks: []check{
|
|
{server: serverA, login: "root", hasAccess: false},
|
|
{server: serverA, login: "admin", hasAccess: false},
|
|
{server: serverB, login: "root", hasAccess: false},
|
|
{server: serverB, login: "admin", hasAccess: false},
|
|
{server: serverC, login: "root", hasAccess: false},
|
|
{server: serverC, login: "admin", hasAccess: false},
|
|
},
|
|
},
|
|
{
|
|
name: "one role is more permissive than another",
|
|
roles: []RoleV3{
|
|
{
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
MaxSessionTTL: Duration(20 * time.Hour),
|
|
},
|
|
Allow: RoleConditions{
|
|
Logins: []string{"admin"},
|
|
NodeLabels: Labels{"role": []string{"worker"}},
|
|
Namespaces: []string{defaults.Namespace},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
MaxSessionTTL: Duration(20 * time.Hour),
|
|
},
|
|
Allow: RoleConditions{
|
|
Logins: []string{"root", "admin"},
|
|
NodeLabels: Labels{Wildcard: []string{Wildcard}},
|
|
Namespaces: []string{Wildcard},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
checks: []check{
|
|
{server: serverA, login: "root", hasAccess: true},
|
|
{server: serverA, login: "admin", hasAccess: true},
|
|
{server: serverB, login: "root", hasAccess: true},
|
|
{server: serverB, login: "admin", hasAccess: true},
|
|
{server: serverC, login: "root", hasAccess: true},
|
|
{server: serverC, login: "admin", hasAccess: true},
|
|
},
|
|
},
|
|
{
|
|
name: "one role needs to access servers sharing the partially same label value",
|
|
roles: []RoleV3{
|
|
{
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: namespaceC,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
MaxSessionTTL: Duration(20 * time.Hour),
|
|
},
|
|
Allow: RoleConditions{
|
|
Logins: []string{"admin"},
|
|
NodeLabels: Labels{"role": []string{"^db(.*)$"}, "status": []string{"follow*"}},
|
|
Namespaces: []string{namespaceC},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
checks: []check{
|
|
{server: serverA, login: "root", hasAccess: false},
|
|
{server: serverA, login: "admin", hasAccess: false},
|
|
{server: serverB, login: "root", hasAccess: false},
|
|
{server: serverB, login: "admin", hasAccess: false},
|
|
{server: serverC, login: "root", hasAccess: false},
|
|
{server: serverC, login: "admin", hasAccess: true},
|
|
{server: serverC2, login: "root", hasAccess: false},
|
|
{server: serverC2, login: "admin", hasAccess: true},
|
|
},
|
|
},
|
|
{
|
|
name: "no logins means no access",
|
|
roles: []RoleV3{
|
|
{
|
|
Metadata: Metadata{
|
|
Name: "somerole",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
MaxSessionTTL: Duration(20 * time.Hour),
|
|
},
|
|
Allow: RoleConditions{
|
|
Logins: nil,
|
|
NodeLabels: Labels{Wildcard: []string{Wildcard}},
|
|
Namespaces: []string{Wildcard},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
checks: []check{
|
|
{server: serverA, login: "root", hasAccess: false},
|
|
{server: serverA, login: "admin", hasAccess: false},
|
|
{server: serverB, login: "root", hasAccess: false},
|
|
{server: serverB, login: "admin", hasAccess: false},
|
|
{server: serverC, login: "root", hasAccess: false},
|
|
{server: serverC, login: "admin", hasAccess: false},
|
|
},
|
|
},
|
|
}
|
|
for i, tc := range testCases {
|
|
|
|
var set RoleSet
|
|
for i := range tc.roles {
|
|
set = append(set, &tc.roles[i])
|
|
}
|
|
for j, check := range tc.checks {
|
|
comment := fmt.Sprintf("test case %v '%v', check %v", i, tc.name, j)
|
|
result := set.CheckAccessToServer(check.login, check.server)
|
|
if check.hasAccess {
|
|
require.NoError(t, result, comment)
|
|
} else {
|
|
require.True(t, trace.IsAccessDenied(result), comment)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCheckAccessToRemoteCluster(t *testing.T) {
|
|
type check struct {
|
|
rc RemoteCluster
|
|
hasAccess bool
|
|
}
|
|
rcA := &RemoteClusterV3{
|
|
Metadata: Metadata{
|
|
Name: "a",
|
|
},
|
|
}
|
|
rcB := &RemoteClusterV3{
|
|
Metadata: Metadata{
|
|
Name: "b",
|
|
Labels: map[string]string{"role": "worker", "status": "follower"},
|
|
},
|
|
}
|
|
rcC := &RemoteClusterV3{
|
|
Metadata: Metadata{
|
|
Name: "c",
|
|
Labels: map[string]string{"role": "db", "status": "follower"},
|
|
},
|
|
}
|
|
testCases := []struct {
|
|
name string
|
|
roles []RoleV3
|
|
checks []check
|
|
}{
|
|
{
|
|
name: "empty role set has access to nothing",
|
|
roles: []RoleV3{},
|
|
checks: []check{
|
|
{rc: rcA, hasAccess: false},
|
|
{rc: rcB, hasAccess: false},
|
|
{rc: rcC, hasAccess: false},
|
|
},
|
|
},
|
|
{
|
|
name: "role matches any label out of multiple labels",
|
|
roles: []RoleV3{
|
|
{
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
MaxSessionTTL: Duration(20 * time.Hour),
|
|
},
|
|
Allow: RoleConditions{
|
|
Logins: []string{"admin"},
|
|
ClusterLabels: Labels{"role": []string{"worker2", "worker"}},
|
|
Namespaces: []string{defaults.Namespace},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
checks: []check{
|
|
{rc: rcA, hasAccess: false},
|
|
{rc: rcB, hasAccess: true},
|
|
{rc: rcC, hasAccess: false},
|
|
},
|
|
},
|
|
{
|
|
name: "wildcard matches anything",
|
|
roles: []RoleV3{
|
|
{
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
MaxSessionTTL: Duration(20 * time.Hour),
|
|
},
|
|
Allow: RoleConditions{
|
|
Logins: []string{"admin"},
|
|
ClusterLabels: Labels{Wildcard: []string{Wildcard}},
|
|
Namespaces: []string{defaults.Namespace},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
checks: []check{
|
|
{rc: rcA, hasAccess: true},
|
|
{rc: rcB, hasAccess: true},
|
|
{rc: rcC, hasAccess: true},
|
|
},
|
|
},
|
|
{
|
|
name: "role with no labels will match clusters with no labels, but no others",
|
|
roles: []RoleV3{
|
|
{
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
MaxSessionTTL: Duration(20 * time.Hour),
|
|
},
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
checks: []check{
|
|
{rc: rcA, hasAccess: true},
|
|
{rc: rcB, hasAccess: false},
|
|
{rc: rcC, hasAccess: false},
|
|
},
|
|
},
|
|
{
|
|
name: "any role in the set with labels in the set makes the set to match labels",
|
|
roles: []RoleV3{
|
|
{
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
MaxSessionTTL: Duration(20 * time.Hour),
|
|
},
|
|
Allow: RoleConditions{
|
|
ClusterLabels: Labels{"role": []string{"worker"}},
|
|
Namespaces: []string{defaults.Namespace},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Metadata: Metadata{
|
|
Name: "name2",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
MaxSessionTTL: Duration(20 * time.Hour),
|
|
},
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
checks: []check{
|
|
{rc: rcA, hasAccess: false},
|
|
{rc: rcB, hasAccess: true},
|
|
{rc: rcC, hasAccess: false},
|
|
},
|
|
},
|
|
{
|
|
name: "cluster_labels with empty list value matches nothing",
|
|
roles: []RoleV3{
|
|
{
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
MaxSessionTTL: Duration(20 * time.Hour),
|
|
},
|
|
Allow: RoleConditions{
|
|
Logins: []string{"admin"},
|
|
ClusterLabels: Labels{"role": []string{}},
|
|
Namespaces: []string{defaults.Namespace},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
checks: []check{
|
|
{rc: rcA, hasAccess: false},
|
|
{rc: rcB, hasAccess: false},
|
|
{rc: rcC, hasAccess: false},
|
|
},
|
|
},
|
|
{
|
|
name: "one role is more permissive than another",
|
|
roles: []RoleV3{
|
|
{
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
MaxSessionTTL: Duration(20 * time.Hour),
|
|
},
|
|
Allow: RoleConditions{
|
|
Logins: []string{"admin"},
|
|
ClusterLabels: Labels{"role": []string{"worker"}},
|
|
Namespaces: []string{defaults.Namespace},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Metadata: Metadata{
|
|
Name: "name2",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
MaxSessionTTL: Duration(20 * time.Hour),
|
|
},
|
|
Allow: RoleConditions{
|
|
Logins: []string{"root", "admin"},
|
|
ClusterLabels: Labels{Wildcard: []string{Wildcard}},
|
|
Namespaces: []string{Wildcard},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
checks: []check{
|
|
{rc: rcA, hasAccess: true},
|
|
{rc: rcB, hasAccess: true},
|
|
{rc: rcC, hasAccess: true},
|
|
},
|
|
},
|
|
{
|
|
name: "regexp label match",
|
|
roles: []RoleV3{
|
|
{
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{
|
|
MaxSessionTTL: Duration(20 * time.Hour),
|
|
},
|
|
Allow: RoleConditions{
|
|
Logins: []string{"admin"},
|
|
ClusterLabels: Labels{"role": []string{"^db(.*)$"}, "status": []string{"follow*"}},
|
|
Namespaces: []string{defaults.Namespace},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
checks: []check{
|
|
{rc: rcA, hasAccess: false},
|
|
{rc: rcB, hasAccess: false},
|
|
{rc: rcC, hasAccess: true},
|
|
},
|
|
},
|
|
}
|
|
for i, tc := range testCases {
|
|
var set RoleSet
|
|
for i := range tc.roles {
|
|
set = append(set, &tc.roles[i])
|
|
}
|
|
for j, check := range tc.checks {
|
|
comment := fmt.Sprintf("test case %v '%v', check %v", i, tc.name, j)
|
|
result := set.CheckAccessToRemoteCluster(check.rc)
|
|
if check.hasAccess {
|
|
require.NoError(t, result, comment)
|
|
} else {
|
|
require.True(t, trace.IsAccessDenied(result), fmt.Sprintf("%v: %v", comment, result))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// testContext overrides context and captures log writes in action
|
|
type testContext struct {
|
|
Context
|
|
// Buffer captures log writes
|
|
buffer *bytes.Buffer
|
|
}
|
|
|
|
// Write is implemented explicitly to avoid collision
|
|
// of String methods when embedding
|
|
func (t *testContext) Write(data []byte) (int, error) {
|
|
return t.buffer.Write(data)
|
|
}
|
|
|
|
func TestCheckRuleAccess(t *testing.T) {
|
|
type check struct {
|
|
hasAccess bool
|
|
verb string
|
|
namespace string
|
|
rule string
|
|
context testContext
|
|
matchBuffer string
|
|
}
|
|
testCases := []struct {
|
|
name string
|
|
roles []RoleV3
|
|
checks []check
|
|
}{
|
|
{
|
|
name: "0 - empty role set has access to nothing",
|
|
roles: []RoleV3{},
|
|
checks: []check{
|
|
{rule: KindUser, verb: ActionWrite, namespace: defaults.Namespace, hasAccess: false},
|
|
},
|
|
},
|
|
{
|
|
name: "1 - user can read session but can't list in default namespace",
|
|
roles: []RoleV3{
|
|
{
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
Rules: []Rule{
|
|
NewRule(KindSSHSession, []string{VerbRead}),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
checks: []check{
|
|
{rule: KindSSHSession, verb: VerbRead, namespace: defaults.Namespace, hasAccess: true},
|
|
{rule: KindSSHSession, verb: VerbList, namespace: defaults.Namespace, hasAccess: false},
|
|
},
|
|
},
|
|
{
|
|
name: "2 - user can read sessions in system namespace and create stuff in default namespace",
|
|
roles: []RoleV3{
|
|
{
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{"system"},
|
|
Rules: []Rule{
|
|
NewRule(KindSSHSession, []string{VerbRead}),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Metadata: Metadata{
|
|
Name: "name2",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
Rules: []Rule{
|
|
NewRule(KindSSHSession, []string{VerbCreate, VerbRead}),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
checks: []check{
|
|
{rule: KindSSHSession, verb: VerbRead, namespace: defaults.Namespace, hasAccess: true},
|
|
{rule: KindSSHSession, verb: VerbCreate, namespace: defaults.Namespace, hasAccess: true},
|
|
{rule: KindSSHSession, verb: VerbCreate, namespace: "system", hasAccess: false},
|
|
{rule: KindRole, verb: VerbRead, namespace: defaults.Namespace, hasAccess: false},
|
|
},
|
|
},
|
|
{
|
|
name: "3 - deny rules override allow rules",
|
|
roles: []RoleV3{
|
|
{
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Deny: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
Rules: []Rule{
|
|
NewRule(KindSSHSession, []string{VerbCreate}),
|
|
},
|
|
},
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
Rules: []Rule{
|
|
NewRule(KindSSHSession, []string{VerbCreate}),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
checks: []check{
|
|
{rule: KindSSHSession, verb: VerbCreate, namespace: defaults.Namespace, hasAccess: false},
|
|
},
|
|
},
|
|
{
|
|
name: "4 - user can read sessions if trait matches",
|
|
roles: []RoleV3{
|
|
{
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
Rules: []Rule{
|
|
{
|
|
Resources: []string{KindSession},
|
|
Verbs: []string{VerbRead},
|
|
Where: `contains(user.spec.traits["group"], "prod")`,
|
|
Actions: []string{
|
|
`log("info", "4 - tc match for user %v", user.metadata.name)`,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
checks: []check{
|
|
{rule: KindSession, verb: VerbRead, namespace: defaults.Namespace, hasAccess: false},
|
|
{rule: KindSession, verb: VerbList, namespace: defaults.Namespace, hasAccess: false},
|
|
{
|
|
context: testContext{
|
|
buffer: &bytes.Buffer{},
|
|
Context: Context{
|
|
User: &UserV2{
|
|
Metadata: Metadata{
|
|
Name: "bob",
|
|
},
|
|
Spec: UserSpecV2{
|
|
Traits: map[string][]string{
|
|
"group": {"dev", "prod"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
rule: KindSession,
|
|
verb: VerbRead,
|
|
namespace: defaults.Namespace,
|
|
hasAccess: true,
|
|
},
|
|
{
|
|
context: testContext{
|
|
buffer: &bytes.Buffer{},
|
|
Context: Context{
|
|
User: &UserV2{
|
|
Spec: UserSpecV2{
|
|
Traits: map[string][]string{
|
|
"group": {"dev"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
rule: KindSession,
|
|
verb: VerbRead,
|
|
namespace: defaults.Namespace,
|
|
hasAccess: false,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "5 - user can read role if role has label",
|
|
roles: []RoleV3{
|
|
{
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
Rules: []Rule{
|
|
{
|
|
Resources: []string{KindRole},
|
|
Verbs: []string{VerbRead},
|
|
Where: `equals(resource.metadata.labels["team"], "dev")`,
|
|
Actions: []string{
|
|
`log("error", "4 - tc match")`,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
checks: []check{
|
|
{rule: KindRole, verb: VerbRead, namespace: defaults.Namespace, hasAccess: false},
|
|
{rule: KindRole, verb: VerbList, namespace: defaults.Namespace, hasAccess: false},
|
|
{
|
|
context: testContext{
|
|
buffer: &bytes.Buffer{},
|
|
Context: Context{
|
|
Resource: &RoleV3{
|
|
Metadata: Metadata{
|
|
Labels: map[string]string{"team": "dev"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
rule: KindRole,
|
|
verb: VerbRead,
|
|
namespace: defaults.Namespace,
|
|
hasAccess: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "More specific rule wins",
|
|
roles: []RoleV3{
|
|
{
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
Rules: []Rule{
|
|
{
|
|
Resources: []string{Wildcard},
|
|
Verbs: []string{Wildcard},
|
|
},
|
|
{
|
|
Resources: []string{KindRole},
|
|
Verbs: []string{VerbRead},
|
|
Where: `equals(resource.metadata.labels["team"], "dev")`,
|
|
Actions: []string{
|
|
`log("info", "matched more specific rule")`,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
checks: []check{
|
|
{
|
|
context: testContext{
|
|
buffer: &bytes.Buffer{},
|
|
Context: Context{
|
|
Resource: &RoleV3{
|
|
Metadata: Metadata{
|
|
Labels: map[string]string{"team": "dev"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
rule: KindRole,
|
|
verb: VerbRead,
|
|
namespace: defaults.Namespace,
|
|
hasAccess: true,
|
|
matchBuffer: "more specific rule",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for i, tc := range testCases {
|
|
var set RoleSet
|
|
for i := range tc.roles {
|
|
set = append(set, &tc.roles[i])
|
|
}
|
|
for j, check := range tc.checks {
|
|
comment := fmt.Sprintf("test case %v '%v', check %v", i, tc.name, j)
|
|
result := set.CheckAccessToRule(&check.context, check.namespace, check.rule, check.verb, false)
|
|
if check.hasAccess {
|
|
require.NoError(t, result, comment)
|
|
} else {
|
|
require.True(t, trace.IsAccessDenied(result), comment)
|
|
}
|
|
if check.matchBuffer != "" {
|
|
require.Contains(t, check.context.buffer.String(), check.matchBuffer, comment)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCheckRuleSorting(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
rules []Rule
|
|
set RuleSet
|
|
}{
|
|
{
|
|
name: "single rule set sorts OK",
|
|
rules: []Rule{
|
|
{
|
|
Resources: []string{KindUser},
|
|
Verbs: []string{VerbCreate},
|
|
},
|
|
},
|
|
set: RuleSet{
|
|
KindUser: []Rule{
|
|
{
|
|
Resources: []string{KindUser},
|
|
Verbs: []string{VerbCreate},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "rule with where section is more specific",
|
|
rules: []Rule{
|
|
{
|
|
Resources: []string{KindUser},
|
|
Verbs: []string{VerbCreate},
|
|
},
|
|
{
|
|
Resources: []string{KindUser},
|
|
Verbs: []string{VerbCreate},
|
|
Where: "contains(user.spec.traits[\"groups\"], \"prod\")",
|
|
},
|
|
},
|
|
set: RuleSet{
|
|
KindUser: []Rule{
|
|
{
|
|
Resources: []string{KindUser},
|
|
Verbs: []string{VerbCreate},
|
|
Where: "contains(user.spec.traits[\"groups\"], \"prod\")",
|
|
},
|
|
{
|
|
Resources: []string{KindUser},
|
|
Verbs: []string{VerbCreate},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "rule with action is more specific",
|
|
rules: []Rule{
|
|
{
|
|
Resources: []string{KindUser},
|
|
Verbs: []string{VerbCreate},
|
|
|
|
Where: "contains(user.spec.traits[\"groups\"], \"prod\")",
|
|
},
|
|
{
|
|
Resources: []string{KindUser},
|
|
Verbs: []string{VerbCreate},
|
|
Where: "contains(user.spec.traits[\"groups\"], \"prod\")",
|
|
Actions: []string{
|
|
"log(\"info\", \"log entry\")",
|
|
},
|
|
},
|
|
},
|
|
set: RuleSet{
|
|
KindUser: []Rule{
|
|
{
|
|
Resources: []string{KindUser},
|
|
Verbs: []string{VerbCreate},
|
|
Where: "contains(user.spec.traits[\"groups\"], \"prod\")",
|
|
Actions: []string{
|
|
"log(\"info\", \"log entry\")",
|
|
},
|
|
},
|
|
{
|
|
Resources: []string{KindUser},
|
|
Verbs: []string{VerbCreate},
|
|
Where: "contains(user.spec.traits[\"groups\"], \"prod\")",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for i, tc := range testCases {
|
|
comment := fmt.Sprintf("test case %v '%v'", i, tc.name)
|
|
out := MakeRuleSet(tc.rules)
|
|
require.Equal(t, tc.set, out, comment)
|
|
}
|
|
}
|
|
|
|
func TestApplyTraits(t *testing.T) {
|
|
type rule struct {
|
|
inLogins []string
|
|
outLogins []string
|
|
inLabels Labels
|
|
outLabels Labels
|
|
inKubeGroups []string
|
|
outKubeGroups []string
|
|
inKubeUsers []string
|
|
outKubeUsers []string
|
|
inDBNames []string
|
|
outDBNames []string
|
|
inDBUsers []string
|
|
outDBUsers []string
|
|
}
|
|
var tests = []struct {
|
|
comment string
|
|
inTraits map[string][]string
|
|
allow rule
|
|
deny rule
|
|
}{
|
|
|
|
{
|
|
comment: "logins substitute in allow rule",
|
|
inTraits: map[string][]string{
|
|
"foo": {"bar"},
|
|
},
|
|
allow: rule{
|
|
inLogins: []string{`{{external.foo}}`, "root"},
|
|
outLogins: []string{"bar", "root"},
|
|
},
|
|
},
|
|
{
|
|
comment: "logins substitute in allow rule with function",
|
|
inTraits: map[string][]string{
|
|
"foo": {"Bar <bar@example.com>"},
|
|
},
|
|
allow: rule{
|
|
inLogins: []string{`{{email.local(external.foo)}}`, "root"},
|
|
outLogins: []string{"bar", "root"},
|
|
},
|
|
},
|
|
{
|
|
comment: "logins substitute in deny rule",
|
|
inTraits: map[string][]string{
|
|
"foo": {"bar"},
|
|
},
|
|
deny: rule{
|
|
inLogins: []string{`{{external.foo}}`},
|
|
outLogins: []string{"bar"},
|
|
},
|
|
},
|
|
{
|
|
comment: "kube group substitute in allow rule",
|
|
inTraits: map[string][]string{
|
|
"foo": {"bar"},
|
|
},
|
|
allow: rule{
|
|
inKubeGroups: []string{`{{external.foo}}`, "root"},
|
|
outKubeGroups: []string{"bar", "root"},
|
|
},
|
|
},
|
|
{
|
|
comment: "kube group substitute in deny rule",
|
|
inTraits: map[string][]string{
|
|
"foo": {"bar"},
|
|
},
|
|
deny: rule{
|
|
inKubeGroups: []string{`{{external.foo}}`, "root"},
|
|
outKubeGroups: []string{"bar", "root"},
|
|
},
|
|
},
|
|
{
|
|
comment: "kube user interpolation in allow rule",
|
|
inTraits: map[string][]string{
|
|
"foo": {"bar"},
|
|
},
|
|
allow: rule{
|
|
inKubeUsers: []string{`IAM#{{external.foo}};`},
|
|
outKubeUsers: []string{"IAM#bar;"},
|
|
},
|
|
},
|
|
{
|
|
comment: "kube users interpolation in deny rule",
|
|
inTraits: map[string][]string{
|
|
"foo": {"bar"},
|
|
},
|
|
deny: rule{
|
|
inKubeUsers: []string{`IAM#{{external.foo}};`},
|
|
outKubeUsers: []string{"IAM#bar;"},
|
|
},
|
|
},
|
|
{
|
|
comment: "database name/user external vars in allow rule",
|
|
inTraits: map[string][]string{
|
|
"foo": {"bar"},
|
|
},
|
|
allow: rule{
|
|
inDBNames: []string{"{{external.foo}}", "{{external.baz}}", "postgres"},
|
|
outDBNames: []string{"bar", "postgres"},
|
|
inDBUsers: []string{"{{external.foo}}", "{{external.baz}}", "postgres"},
|
|
outDBUsers: []string{"bar", "postgres"},
|
|
},
|
|
},
|
|
{
|
|
comment: "database name/user external vars in deny rule",
|
|
inTraits: map[string][]string{
|
|
"foo": {"bar"},
|
|
},
|
|
deny: rule{
|
|
inDBNames: []string{"{{external.foo}}", "{{external.baz}}", "postgres"},
|
|
outDBNames: []string{"bar", "postgres"},
|
|
inDBUsers: []string{"{{external.foo}}", "{{external.baz}}", "postgres"},
|
|
outDBUsers: []string{"bar", "postgres"},
|
|
},
|
|
},
|
|
{
|
|
comment: "database name/user internal vars in allow rule",
|
|
inTraits: map[string][]string{
|
|
"db_names": {"db1", "db2"},
|
|
"db_users": {"alice"},
|
|
},
|
|
allow: rule{
|
|
inDBNames: []string{"{{internal.db_names}}", "{{internal.foo}}", "postgres"},
|
|
outDBNames: []string{"db1", "db2", "postgres"},
|
|
inDBUsers: []string{"{{internal.db_users}}", "{{internal.foo}}", "postgres"},
|
|
outDBUsers: []string{"alice", "postgres"},
|
|
},
|
|
},
|
|
{
|
|
comment: "database name/user internal vars in deny rule",
|
|
inTraits: map[string][]string{
|
|
"db_names": {"db1", "db2"},
|
|
"db_users": {"alice"},
|
|
},
|
|
deny: rule{
|
|
inDBNames: []string{"{{internal.db_names}}", "{{internal.foo}}", "postgres"},
|
|
outDBNames: []string{"db1", "db2", "postgres"},
|
|
inDBUsers: []string{"{{internal.db_users}}", "{{internal.foo}}", "postgres"},
|
|
outDBUsers: []string{"alice", "postgres"},
|
|
},
|
|
},
|
|
{
|
|
comment: "no variable in logins",
|
|
inTraits: map[string][]string{
|
|
"foo": {"bar"},
|
|
},
|
|
allow: rule{
|
|
inLogins: []string{"root"},
|
|
outLogins: []string{"root"},
|
|
},
|
|
},
|
|
{
|
|
comment: "invalid variable in logins does not get passed along",
|
|
inTraits: map[string][]string{
|
|
"foo": {"bar"},
|
|
},
|
|
allow: rule{
|
|
inLogins: []string{`external.foo}}`},
|
|
},
|
|
},
|
|
{
|
|
comment: "invalid function call in logins does not get passed along",
|
|
inTraits: map[string][]string{
|
|
"foo": {"bar"},
|
|
},
|
|
allow: rule{
|
|
inLogins: []string{`{{email.local(external.foo, 1)}}`},
|
|
},
|
|
},
|
|
{
|
|
comment: "invalid function call in logins does not get passed along",
|
|
inTraits: map[string][]string{
|
|
"foo": {"bar"},
|
|
},
|
|
allow: rule{
|
|
inLogins: []string{`{{email.local()}}`},
|
|
},
|
|
},
|
|
{
|
|
comment: "invalid function call in logins does not get passed along",
|
|
inTraits: map[string][]string{
|
|
"foo": {"bar"},
|
|
},
|
|
allow: rule{
|
|
inLogins: []string{`{{email.local(email.local)}}`, `{{email.local(email.local())}}`},
|
|
},
|
|
},
|
|
{
|
|
comment: "variable in logins, none in traits",
|
|
inTraits: map[string][]string{
|
|
"foo": {"bar"},
|
|
},
|
|
allow: rule{
|
|
inLogins: []string{`{{internal.bar}}`, "root"},
|
|
outLogins: []string{"root"},
|
|
},
|
|
},
|
|
{
|
|
comment: "multiple variables in traits",
|
|
inTraits: map[string][]string{
|
|
"logins": {"bar", "baz"},
|
|
},
|
|
allow: rule{
|
|
inLogins: []string{`{{internal.logins}}`, "root"},
|
|
outLogins: []string{"bar", "baz", "root"},
|
|
},
|
|
},
|
|
{
|
|
comment: "deduplicate",
|
|
inTraits: map[string][]string{
|
|
"foo": {"bar"},
|
|
},
|
|
allow: rule{
|
|
inLogins: []string{`{{external.foo}}`, "bar"},
|
|
outLogins: []string{"bar"},
|
|
},
|
|
},
|
|
{
|
|
comment: "invalid unix login",
|
|
inTraits: map[string][]string{
|
|
"foo": {"-foo"},
|
|
},
|
|
allow: rule{
|
|
inLogins: []string{`{{external.foo}}`, "bar"},
|
|
outLogins: []string{"bar"},
|
|
},
|
|
},
|
|
{
|
|
comment: "label substitute in allow and deny rule",
|
|
inTraits: map[string][]string{
|
|
"foo": {"bar"},
|
|
"hello": {"there"},
|
|
},
|
|
allow: rule{
|
|
inLabels: Labels{`{{external.foo}}`: []string{"{{external.hello}}"}},
|
|
outLabels: Labels{`bar`: []string{"there"}},
|
|
},
|
|
deny: rule{
|
|
inLabels: Labels{`{{external.hello}}`: []string{"{{external.foo}}"}},
|
|
outLabels: Labels{`there`: []string{"bar"}},
|
|
},
|
|
},
|
|
|
|
{
|
|
comment: "missing node variables are set to empty during substitution",
|
|
inTraits: map[string][]string{
|
|
"foo": {"bar"},
|
|
},
|
|
allow: rule{
|
|
inLabels: Labels{
|
|
`{{external.foo}}`: []string{"value"},
|
|
`{{external.missing}}`: []string{"missing"},
|
|
`missing`: []string{"{{external.missing}}", "othervalue"},
|
|
},
|
|
outLabels: Labels{
|
|
`bar`: []string{"value"},
|
|
"missing": []string{"", "othervalue"},
|
|
"": []string{"missing"},
|
|
},
|
|
},
|
|
},
|
|
|
|
{
|
|
comment: "the first variable value is picked for label keys",
|
|
inTraits: map[string][]string{
|
|
"foo": {"bar", "baz"},
|
|
},
|
|
allow: rule{
|
|
inLabels: Labels{`{{external.foo}}`: []string{"value"}},
|
|
outLabels: Labels{`bar`: []string{"value"}},
|
|
},
|
|
},
|
|
|
|
{
|
|
comment: "all values are expanded for label values",
|
|
inTraits: map[string][]string{
|
|
"foo": {"bar", "baz"},
|
|
},
|
|
allow: rule{
|
|
inLabels: Labels{`key`: []string{`{{external.foo}}`}},
|
|
outLabels: Labels{`key`: []string{"bar", "baz"}},
|
|
},
|
|
},
|
|
}
|
|
|
|
for i, tt := range tests {
|
|
comment := fmt.Sprintf("Test %v %v", i, tt.comment)
|
|
|
|
role := &RoleV3{
|
|
Kind: KindRole,
|
|
Version: V3,
|
|
Metadata: Metadata{
|
|
Name: "name1",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Allow: RoleConditions{
|
|
Logins: tt.allow.inLogins,
|
|
NodeLabels: tt.allow.inLabels,
|
|
ClusterLabels: tt.allow.inLabels,
|
|
KubeGroups: tt.allow.inKubeGroups,
|
|
KubeUsers: tt.allow.inKubeUsers,
|
|
DatabaseNames: tt.allow.inDBNames,
|
|
DatabaseUsers: tt.allow.inDBUsers,
|
|
},
|
|
Deny: RoleConditions{
|
|
Logins: tt.deny.inLogins,
|
|
NodeLabels: tt.deny.inLabels,
|
|
ClusterLabels: tt.deny.inLabels,
|
|
KubeGroups: tt.deny.inKubeGroups,
|
|
KubeUsers: tt.deny.inKubeUsers,
|
|
DatabaseNames: tt.deny.inDBNames,
|
|
DatabaseUsers: tt.deny.inDBUsers,
|
|
},
|
|
},
|
|
}
|
|
|
|
outRole := ApplyTraits(role, tt.inTraits)
|
|
require.Equal(t, outRole.GetLogins(Allow), tt.allow.outLogins, comment)
|
|
require.Equal(t, outRole.GetNodeLabels(Allow), tt.allow.outLabels, comment)
|
|
require.Equal(t, outRole.GetClusterLabels(Allow), tt.allow.outLabels, comment)
|
|
require.Equal(t, outRole.GetKubeGroups(Allow), tt.allow.outKubeGroups, comment)
|
|
require.Equal(t, outRole.GetKubeUsers(Allow), tt.allow.outKubeUsers, comment)
|
|
require.Equal(t, outRole.GetDatabaseNames(Allow), tt.allow.outDBNames, comment)
|
|
require.Equal(t, outRole.GetDatabaseUsers(Allow), tt.allow.outDBUsers, comment)
|
|
|
|
require.Equal(t, outRole.GetLogins(Deny), tt.deny.outLogins, comment)
|
|
require.Equal(t, outRole.GetNodeLabels(Deny), tt.deny.outLabels, comment)
|
|
require.Equal(t, outRole.GetClusterLabels(Deny), tt.deny.outLabels, comment)
|
|
require.Equal(t, outRole.GetKubeGroups(Deny), tt.deny.outKubeGroups, comment)
|
|
require.Equal(t, outRole.GetKubeUsers(Deny), tt.deny.outKubeUsers, comment)
|
|
require.Equal(t, outRole.GetDatabaseNames(Deny), tt.deny.outDBNames, comment)
|
|
require.Equal(t, outRole.GetDatabaseUsers(Deny), tt.deny.outDBUsers, comment)
|
|
}
|
|
}
|
|
|
|
// TestExtractFrom makes sure roles and traits are extracted from SSH and TLS
|
|
// certificates not services.User.
|
|
func TestExtractFrom(t *testing.T) {
|
|
origRoles := []string{"admin"}
|
|
origTraits := wrappers.Traits(map[string][]string{
|
|
"login": {"foo"},
|
|
})
|
|
|
|
// Create a SSH certificate.
|
|
pubkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(fixtures.UserCertificateStandard))
|
|
require.NoError(t, err)
|
|
cert, ok := pubkey.(*ssh.Certificate)
|
|
require.True(t, ok)
|
|
|
|
// Create a TLS identity.
|
|
identity := &tlsca.Identity{
|
|
Username: "foo",
|
|
Groups: origRoles,
|
|
Traits: origTraits,
|
|
}
|
|
|
|
// At this point, services.User and the certificate/identity are still in
|
|
// sync. The roles and traits returned should be the same as the original.
|
|
roles, traits, err := ExtractFromCertificate(&userGetter{
|
|
roles: origRoles,
|
|
traits: origTraits,
|
|
}, cert)
|
|
require.NoError(t, err)
|
|
require.Equal(t, roles, origRoles)
|
|
require.Equal(t, traits, origTraits)
|
|
|
|
roles, traits, err = ExtractFromIdentity(&userGetter{
|
|
roles: origRoles,
|
|
traits: origTraits,
|
|
}, *identity)
|
|
require.NoError(t, err)
|
|
require.Equal(t, roles, origRoles)
|
|
require.Equal(t, traits, origTraits)
|
|
|
|
// The backend now returns new roles and traits, however because the roles
|
|
// and traits are extracted from the certificate/identity, the original
|
|
// roles and traits will be returned.
|
|
roles, traits, err = ExtractFromCertificate(&userGetter{
|
|
roles: []string{"intern"},
|
|
traits: wrappers.Traits(map[string][]string{
|
|
"login": {"bar"},
|
|
}),
|
|
}, cert)
|
|
require.NoError(t, err)
|
|
require.Equal(t, roles, origRoles)
|
|
require.Equal(t, traits, origTraits)
|
|
|
|
roles, traits, err = ExtractFromIdentity(&userGetter{
|
|
roles: origRoles,
|
|
traits: origTraits,
|
|
}, *identity)
|
|
require.NoError(t, err)
|
|
require.Equal(t, roles, origRoles)
|
|
require.Equal(t, traits, origTraits)
|
|
}
|
|
|
|
// TestExtractFromLegacy verifies that roles and traits are fetched
|
|
// from services.User for SSH certificates is the legacy format and TLS
|
|
// certificates that don't contain traits.
|
|
func TestExtractFromLegacy(t *testing.T) {
|
|
origRoles := []string{"admin"}
|
|
origTraits := wrappers.Traits(map[string][]string{
|
|
"login": {"foo"},
|
|
})
|
|
|
|
// Create a SSH certificate in the legacy format.
|
|
pubkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(fixtures.UserCertificateLegacy))
|
|
require.NoError(t, err)
|
|
cert, ok := pubkey.(*ssh.Certificate)
|
|
require.True(t, ok)
|
|
|
|
// Create a TLS identity with only roles.
|
|
identity := &tlsca.Identity{
|
|
Username: "foo",
|
|
Groups: origRoles,
|
|
}
|
|
|
|
// At this point, services.User and the certificate/identity are still in
|
|
// sync. The roles and traits returned should be the same as the original.
|
|
roles, traits, err := ExtractFromCertificate(&userGetter{
|
|
roles: origRoles,
|
|
traits: origTraits,
|
|
}, cert)
|
|
require.NoError(t, err)
|
|
require.Equal(t, roles, origRoles)
|
|
require.Equal(t, traits, origTraits)
|
|
roles, traits, err = ExtractFromIdentity(&userGetter{
|
|
roles: origRoles,
|
|
traits: origTraits,
|
|
}, *identity)
|
|
require.NoError(t, err)
|
|
require.Equal(t, roles, origRoles)
|
|
require.Equal(t, traits, origTraits)
|
|
|
|
// The backend now returns new roles and traits, because the SSH certificate
|
|
// is in the old standard format and the TLS identity is missing traits.
|
|
newRoles := []string{"intern"}
|
|
newTraits := wrappers.Traits(map[string][]string{
|
|
"login": {"bar"},
|
|
})
|
|
roles, traits, err = ExtractFromCertificate(&userGetter{
|
|
roles: newRoles,
|
|
traits: newTraits,
|
|
}, cert)
|
|
require.NoError(t, err)
|
|
require.Equal(t, roles, newRoles)
|
|
require.Equal(t, traits, newTraits)
|
|
roles, traits, err = ExtractFromIdentity(&userGetter{
|
|
roles: newRoles,
|
|
traits: newTraits,
|
|
}, *identity)
|
|
require.NoError(t, err)
|
|
require.Equal(t, roles, newRoles)
|
|
require.Equal(t, traits, newTraits)
|
|
}
|
|
|
|
// TestBoolOptions makes sure that bool options (like agent forwarding and
|
|
// port forwarding) can be disabled in a role.
|
|
func TestBoolOptions(t *testing.T) {
|
|
var tests = []struct {
|
|
inOptions RoleOptions
|
|
outCanPortForward bool
|
|
outCanForwardAgents bool
|
|
}{
|
|
// Setting options explicitly off should remain off.
|
|
{
|
|
inOptions: RoleOptions{
|
|
ForwardAgent: NewBool(false),
|
|
PortForwarding: NewBoolOption(false),
|
|
},
|
|
outCanPortForward: false,
|
|
outCanForwardAgents: false,
|
|
},
|
|
// Not setting options should set port forwarding to true (default enabled)
|
|
// and agent forwarding false (default disabled).
|
|
{
|
|
inOptions: RoleOptions{},
|
|
outCanPortForward: true,
|
|
outCanForwardAgents: false,
|
|
},
|
|
// Explicitly enabling should enable them.
|
|
{
|
|
inOptions: RoleOptions{
|
|
ForwardAgent: NewBool(true),
|
|
PortForwarding: NewBoolOption(true),
|
|
},
|
|
outCanPortForward: true,
|
|
outCanForwardAgents: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
set := NewRoleSet(&RoleV3{
|
|
Kind: KindRole,
|
|
Version: V3,
|
|
Metadata: Metadata{
|
|
Name: "role-name",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Options: tt.inOptions,
|
|
},
|
|
})
|
|
require.Equal(t, tt.outCanPortForward, set.CanPortForward())
|
|
require.Equal(t, tt.outCanForwardAgents, set.CanForwardAgents())
|
|
}
|
|
}
|
|
|
|
func TestCheckAccessToDatabase(t *testing.T) {
|
|
utils.InitLoggerForTests(testing.Verbose())
|
|
dbStage := types.NewDatabaseServerV3("stage",
|
|
map[string]string{"env": "stage"},
|
|
types.DatabaseServerSpecV3{})
|
|
dbProd := types.NewDatabaseServerV3("prod",
|
|
map[string]string{"env": "prod"},
|
|
types.DatabaseServerSpecV3{})
|
|
roleDevStage := &RoleV3{
|
|
Metadata: Metadata{Name: "dev-stage", Namespace: defaults.Namespace},
|
|
Spec: RoleSpecV3{
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
DatabaseLabels: Labels{"env": []string{"stage"}},
|
|
DatabaseNames: []string{Wildcard},
|
|
DatabaseUsers: []string{Wildcard},
|
|
},
|
|
Deny: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
DatabaseLabels: Labels{"env": []string{"stage"}},
|
|
DatabaseNames: []string{"supersecret"},
|
|
},
|
|
},
|
|
}
|
|
roleDevProd := &RoleV3{
|
|
Metadata: Metadata{Name: "dev-prod", Namespace: defaults.Namespace},
|
|
Spec: RoleSpecV3{
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
DatabaseLabels: Labels{"env": []string{"prod"}},
|
|
DatabaseNames: []string{"test"},
|
|
DatabaseUsers: []string{"dev"},
|
|
},
|
|
},
|
|
}
|
|
// Database labels are not set in allow/deny rules on purpose to test
|
|
// that they're set during check and set defaults below.
|
|
roleDeny := &types.RoleV3{
|
|
Metadata: Metadata{Name: "deny", Namespace: defaults.Namespace},
|
|
Spec: RoleSpecV3{
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
DatabaseNames: []string{Wildcard},
|
|
DatabaseUsers: []string{Wildcard},
|
|
},
|
|
Deny: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
DatabaseNames: []string{"postgres"},
|
|
DatabaseUsers: []string{"postgres"},
|
|
},
|
|
},
|
|
}
|
|
require.NoError(t, roleDeny.CheckAndSetDefaults())
|
|
type access struct {
|
|
server types.DatabaseServer
|
|
dbName string
|
|
dbUser string
|
|
access bool
|
|
}
|
|
testCases := []struct {
|
|
name string
|
|
roles []*RoleV3
|
|
access []access
|
|
}{
|
|
{
|
|
name: "developer allowed any username/database in stage database except one database",
|
|
roles: []*RoleV3{roleDevStage, roleDevProd},
|
|
access: []access{
|
|
{server: dbStage, dbName: "superdb", dbUser: "superuser", access: true},
|
|
{server: dbStage, dbName: "test", dbUser: "dev", access: true},
|
|
{server: dbStage, dbName: "supersecret", dbUser: "dev", access: false},
|
|
},
|
|
},
|
|
{
|
|
name: "developer allowed only specific username/database in prod database",
|
|
roles: []*RoleV3{roleDevStage, roleDevProd},
|
|
access: []access{
|
|
{server: dbProd, dbName: "superdb", dbUser: "superuser", access: false},
|
|
{server: dbProd, dbName: "test", dbUser: "dev", access: true},
|
|
{server: dbProd, dbName: "superdb", dbUser: "dev", access: false},
|
|
{server: dbProd, dbName: "test", dbUser: "superuser", access: false},
|
|
},
|
|
},
|
|
{
|
|
name: "deny role denies access to specific database",
|
|
roles: []*RoleV3{roleDeny},
|
|
access: []access{
|
|
{server: dbProd, dbName: "test", dbUser: "test", access: true},
|
|
{server: dbProd, dbName: "postgres", dbUser: "postgres", access: false},
|
|
},
|
|
},
|
|
}
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var set RoleSet
|
|
for _, r := range tc.roles {
|
|
set = append(set, r)
|
|
}
|
|
for _, access := range tc.access {
|
|
err := set.CheckAccessToDatabase(access.server, access.dbName, access.dbUser)
|
|
if access.access {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckDatabaseNamesAndUsers(t *testing.T) {
|
|
roleEmpty := &RoleV3{
|
|
Metadata: Metadata{Name: "roleA", Namespace: defaults.Namespace},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{MaxSessionTTL: Duration(time.Hour)},
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
},
|
|
},
|
|
}
|
|
roleA := &RoleV3{
|
|
Metadata: Metadata{Name: "roleA", Namespace: defaults.Namespace},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{MaxSessionTTL: Duration(2 * time.Hour)},
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
DatabaseNames: []string{"postgres", "main"},
|
|
DatabaseUsers: []string{"postgres", "alice"},
|
|
},
|
|
},
|
|
}
|
|
roleB := &RoleV3{
|
|
Metadata: Metadata{Name: "roleB", Namespace: defaults.Namespace},
|
|
Spec: RoleSpecV3{
|
|
Options: RoleOptions{MaxSessionTTL: Duration(time.Hour)},
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
DatabaseNames: []string{"metrics"},
|
|
DatabaseUsers: []string{"bob"},
|
|
},
|
|
Deny: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
DatabaseNames: []string{"postgres"},
|
|
DatabaseUsers: []string{"postgres"},
|
|
},
|
|
},
|
|
}
|
|
testCases := []struct {
|
|
name string
|
|
roles []*RoleV3
|
|
ttl time.Duration
|
|
overrideTTL bool
|
|
namesOut []string
|
|
usersOut []string
|
|
accessDenied bool
|
|
notFound bool
|
|
}{
|
|
{
|
|
name: "single role",
|
|
roles: []*RoleV3{roleA},
|
|
ttl: time.Hour,
|
|
namesOut: []string{"postgres", "main"},
|
|
usersOut: []string{"postgres", "alice"},
|
|
},
|
|
{
|
|
name: "combined roles",
|
|
roles: []*RoleV3{roleA, roleB},
|
|
ttl: time.Hour,
|
|
namesOut: []string{"main", "metrics"},
|
|
usersOut: []string{"alice", "bob"},
|
|
},
|
|
{
|
|
name: "ttl doesn't match",
|
|
roles: []*RoleV3{roleA},
|
|
ttl: 5 * time.Hour,
|
|
accessDenied: true,
|
|
},
|
|
{
|
|
name: "empty role",
|
|
roles: []*RoleV3{roleEmpty},
|
|
ttl: time.Hour,
|
|
notFound: true,
|
|
},
|
|
}
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var set RoleSet
|
|
for _, r := range tc.roles {
|
|
set = append(set, r)
|
|
}
|
|
names, users, err := set.CheckDatabaseNamesAndUsers(tc.ttl, tc.overrideTTL)
|
|
if tc.accessDenied {
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
} else if tc.notFound {
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsNotFound(err))
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, tc.namesOut, names)
|
|
require.ElementsMatch(t, tc.usersOut, users)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckAccessToDatabaseService(t *testing.T) {
|
|
utils.InitLoggerForTests(testing.Verbose())
|
|
dbNoLabels := types.NewDatabaseServerV3("test",
|
|
nil,
|
|
types.DatabaseServerSpecV3{})
|
|
dbStage := types.NewDatabaseServerV3("stage",
|
|
map[string]string{"env": "stage"},
|
|
types.DatabaseServerSpecV3{
|
|
DynamicLabels: map[string]CommandLabelV2{"arch": {Result: "x86"}},
|
|
})
|
|
dbStage2 := types.NewDatabaseServerV3("stage2",
|
|
map[string]string{"env": "stage"},
|
|
types.DatabaseServerSpecV3{
|
|
DynamicLabels: map[string]CommandLabelV2{"arch": {Result: "amd64"}},
|
|
})
|
|
dbProd := types.NewDatabaseServerV3("prod",
|
|
map[string]string{"env": "prod"},
|
|
types.DatabaseServerSpecV3{})
|
|
roleAdmin := &RoleV3{
|
|
Metadata: Metadata{Name: "admin", Namespace: defaults.Namespace},
|
|
Spec: RoleSpecV3{
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
DatabaseLabels: Labels{Wildcard: []string{Wildcard}},
|
|
},
|
|
},
|
|
}
|
|
roleDev := &RoleV3{
|
|
Metadata: Metadata{Name: "dev", Namespace: defaults.Namespace},
|
|
Spec: RoleSpecV3{
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
DatabaseLabels: Labels{"env": []string{"stage"}},
|
|
},
|
|
Deny: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
DatabaseLabels: Labels{"arch": []string{"amd64"}},
|
|
},
|
|
},
|
|
}
|
|
roleIntern := &RoleV3{
|
|
Metadata: Metadata{Name: "intern", Namespace: defaults.Namespace},
|
|
Spec: RoleSpecV3{
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
},
|
|
},
|
|
}
|
|
type access struct {
|
|
server types.DatabaseServer
|
|
access bool
|
|
}
|
|
testCases := []struct {
|
|
name string
|
|
roles []*RoleV3
|
|
access []access
|
|
}{
|
|
{
|
|
name: "empty role doesn't have access to any databases",
|
|
roles: nil,
|
|
access: []access{
|
|
{server: dbNoLabels, access: false},
|
|
{server: dbStage, access: false},
|
|
{server: dbStage2, access: false},
|
|
{server: dbProd, access: false},
|
|
},
|
|
},
|
|
{
|
|
name: "intern doesn't have access to any databases",
|
|
roles: []*RoleV3{roleIntern},
|
|
access: []access{
|
|
{server: dbNoLabels, access: false},
|
|
{server: dbStage, access: false},
|
|
{server: dbStage2, access: false},
|
|
{server: dbProd, access: false},
|
|
},
|
|
},
|
|
{
|
|
name: "developer only has access to one of stage database",
|
|
roles: []*RoleV3{roleDev},
|
|
access: []access{
|
|
{server: dbNoLabels, access: false},
|
|
{server: dbStage, access: true},
|
|
{server: dbStage2, access: false},
|
|
{server: dbProd, access: false},
|
|
},
|
|
},
|
|
{
|
|
name: "admin has access to all databases",
|
|
roles: []*RoleV3{roleAdmin},
|
|
access: []access{
|
|
{server: dbNoLabels, access: true},
|
|
{server: dbStage, access: true},
|
|
{server: dbStage2, access: true},
|
|
{server: dbProd, access: true},
|
|
},
|
|
},
|
|
}
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var set RoleSet
|
|
for _, r := range tc.roles {
|
|
set = append(set, r)
|
|
}
|
|
for _, access := range tc.access {
|
|
err := set.CheckAccessToDatabaseServer(access.server)
|
|
if access.access {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckAccessToKubernetes(t *testing.T) {
|
|
clusterNoLabels := &KubernetesCluster{
|
|
Name: "no-labels",
|
|
}
|
|
clusterWithLabels := &KubernetesCluster{
|
|
Name: "no-labels",
|
|
StaticLabels: map[string]string{"foo": "bar"},
|
|
DynamicLabels: map[string]CommandLabelV2{"baz": {Result: "qux"}},
|
|
}
|
|
wildcardRole := &RoleV3{
|
|
Metadata: Metadata{
|
|
Name: "wildcard-labels",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
KubernetesLabels: Labels{Wildcard: []string{Wildcard}},
|
|
},
|
|
},
|
|
}
|
|
matchingLabelsRole := &RoleV3{
|
|
Metadata: Metadata{
|
|
Name: "matching-labels",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
KubernetesLabels: Labels{
|
|
"foo": utils.Strings{"bar"},
|
|
"baz": utils.Strings{"qux"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
noLabelsRole := &RoleV3{
|
|
Metadata: Metadata{
|
|
Name: "no-labels",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
},
|
|
},
|
|
}
|
|
mismatchingLabelsRole := &RoleV3{
|
|
Metadata: Metadata{
|
|
Name: "mismatching-labels",
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Allow: RoleConditions{
|
|
Namespaces: []string{defaults.Namespace},
|
|
KubernetesLabels: Labels{
|
|
"qux": utils.Strings{"baz"},
|
|
"bar": utils.Strings{"foo"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
testCases := []struct {
|
|
name string
|
|
roles []*RoleV3
|
|
cluster *KubernetesCluster
|
|
hasAccess bool
|
|
}{
|
|
{
|
|
name: "empty role set has access to nothing",
|
|
roles: nil,
|
|
cluster: clusterNoLabels,
|
|
hasAccess: false,
|
|
},
|
|
{
|
|
name: "role with no labels has access to nothing",
|
|
roles: []*RoleV3{noLabelsRole},
|
|
cluster: clusterNoLabels,
|
|
hasAccess: false,
|
|
},
|
|
{
|
|
name: "role with wildcard labels matches cluster without labels",
|
|
roles: []*RoleV3{wildcardRole},
|
|
cluster: clusterNoLabels,
|
|
hasAccess: true,
|
|
},
|
|
{
|
|
name: "role with wildcard labels matches cluster with labels",
|
|
roles: []*RoleV3{wildcardRole},
|
|
cluster: clusterWithLabels,
|
|
hasAccess: true,
|
|
},
|
|
{
|
|
name: "role with labels does not match cluster with no labels",
|
|
roles: []*RoleV3{matchingLabelsRole},
|
|
cluster: clusterNoLabels,
|
|
hasAccess: false,
|
|
},
|
|
{
|
|
name: "role with labels matches cluster with labels",
|
|
roles: []*RoleV3{matchingLabelsRole},
|
|
cluster: clusterWithLabels,
|
|
hasAccess: true,
|
|
},
|
|
{
|
|
name: "role with mismatched labels does not match cluster with labels",
|
|
roles: []*RoleV3{mismatchingLabelsRole},
|
|
cluster: clusterWithLabels,
|
|
hasAccess: false,
|
|
},
|
|
{
|
|
name: "one role in the roleset matches",
|
|
roles: []*RoleV3{mismatchingLabelsRole, noLabelsRole, matchingLabelsRole},
|
|
cluster: clusterWithLabels,
|
|
hasAccess: true,
|
|
},
|
|
}
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var set RoleSet
|
|
for _, r := range tc.roles {
|
|
set = append(set, r)
|
|
}
|
|
err := set.CheckAccessToKubernetes(defaults.Namespace, tc.cluster)
|
|
if tc.hasAccess {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// BenchmarkCheckAccessToServer tests how long it takes to run
|
|
// CheckAccessToServer across 4,000 nodes for 5 roles each with 5 logins each.
|
|
//
|
|
// To run benchmark:
|
|
//
|
|
// go test -bench=.
|
|
//
|
|
// To run benchmark and obtain CPU and memory profiling:
|
|
//
|
|
// go test -bench=. -cpuprofile=cpu.prof -memprofile=mem.prof
|
|
//
|
|
// To use the command line tool to read the profile:
|
|
//
|
|
// go tool pprof cpu.prof
|
|
// go tool pprof cpu.prof
|
|
//
|
|
// To generate a graph:
|
|
//
|
|
// go tool pprof --pdf cpu.prof > cpu.pdf
|
|
// go tool pprof --pdf mem.prof > mem.pdf
|
|
//
|
|
func BenchmarkCheckAccessToServer(b *testing.B) {
|
|
servers := make([]*ServerV2, 0, 4000)
|
|
|
|
// Create 4,000 servers with random IDs.
|
|
for i := 0; i < 4000; i++ {
|
|
hostname := uuid.NewUUID().String()
|
|
servers = append(servers, &ServerV2{
|
|
Kind: KindNode,
|
|
Version: V2,
|
|
Metadata: Metadata{
|
|
Name: hostname,
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: ServerSpecV2{
|
|
Addr: "127.0.0.1:3022",
|
|
Hostname: hostname,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Create RoleSet with five roles: one admin role and four generic roles
|
|
// that have five logins each and only have access to the foo:bar label.
|
|
var set RoleSet
|
|
set = append(set, NewAdminRole())
|
|
for i := 0; i < 4; i++ {
|
|
set = append(set, &RoleV3{
|
|
Kind: KindRole,
|
|
Version: V3,
|
|
Metadata: Metadata{
|
|
Name: strconv.Itoa(i),
|
|
Namespace: defaults.Namespace,
|
|
},
|
|
Spec: RoleSpecV3{
|
|
Allow: RoleConditions{
|
|
Logins: []string{"admin", "one", "two", "three", "four"},
|
|
NodeLabels: Labels{"a": []string{"b"}},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
// Initialization is complete, start the benchmark timer.
|
|
b.ResetTimer()
|
|
|
|
// Build a map of all allowed logins.
|
|
allowLogins := map[string]bool{}
|
|
for _, role := range set {
|
|
for _, login := range role.GetLogins(Allow) {
|
|
allowLogins[login] = true
|
|
}
|
|
}
|
|
|
|
// Check access to all 4,000 nodes.
|
|
for n := 0; n < b.N; n++ {
|
|
for i := 0; i < 4000; i++ {
|
|
for login := range allowLogins {
|
|
if err := set.CheckAccessToServer(login, servers[i]); err != nil {
|
|
b.Error(err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// userGetter is used in tests to return a user with the specified roles and
|
|
// traits.
|
|
type userGetter struct {
|
|
roles []string
|
|
traits map[string][]string
|
|
}
|
|
|
|
func (f *userGetter) GetUser(name string, _ bool) (User, error) {
|
|
user, err := NewUser(name)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
user.SetRoles(f.roles)
|
|
user.SetTraits(f.traits)
|
|
return user, nil
|
|
}
|