return reason for allowed rbac authorizations

includes the binding, role, and subject that allowed a request so audit can make use of it
pull/6/head
Jordan Liggitt 2018-01-19 13:17:32 -05:00
parent f9bb978ad6
commit b4fb25261e
No known key found for this signature in database
GPG Key ID: 39928704103C7229
3 changed files with 83 additions and 19 deletions

View File

@ -42,7 +42,7 @@ type AuthorizationRuleResolver interface {
// VisitRulesFor invokes visitor() with each rule that applies to a given user in a given namespace, and each error encountered resolving those rules. // VisitRulesFor invokes visitor() with each rule that applies to a given user in a given namespace, and each error encountered resolving those rules.
// If visitor() returns false, visiting is short-circuited. // If visitor() returns false, visiting is short-circuited.
VisitRulesFor(user user.Info, namespace string, visitor func(rule *rbac.PolicyRule, err error) bool) VisitRulesFor(user user.Info, namespace string, visitor func(source fmt.Stringer, rule *rbac.PolicyRule, err error) bool)
} }
// ConfirmNoEscalation determines if the roles for a given user in a given namespace encompass the provided role. // ConfirmNoEscalation determines if the roles for a given user in a given namespace encompass the provided role.
@ -107,7 +107,7 @@ type ruleAccumulator struct {
errors []error errors []error
} }
func (r *ruleAccumulator) visit(rule *rbac.PolicyRule, err error) bool { func (r *ruleAccumulator) visit(source fmt.Stringer, rule *rbac.PolicyRule, err error) bool {
if rule != nil { if rule != nil {
r.rules = append(r.rules, *rule) r.rules = append(r.rules, *rule)
} }
@ -117,25 +117,69 @@ func (r *ruleAccumulator) visit(rule *rbac.PolicyRule, err error) bool {
return true return true
} }
func (r *DefaultRuleResolver) VisitRulesFor(user user.Info, namespace string, visitor func(rule *rbac.PolicyRule, err error) bool) { func describeSubject(s *rbac.Subject, bindingNamespace string) string {
switch s.Kind {
case rbac.ServiceAccountKind:
if len(s.Namespace) > 0 {
return fmt.Sprintf("%s %q", s.Kind, s.Name+"/"+s.Namespace)
}
return fmt.Sprintf("%s %q", s.Kind, s.Name+"/"+bindingNamespace)
default:
return fmt.Sprintf("%s %q", s.Kind, s.Name)
}
}
type clusterRoleBindingDescriber struct {
binding *rbac.ClusterRoleBinding
subject *rbac.Subject
}
func (d *clusterRoleBindingDescriber) String() string {
return fmt.Sprintf("ClusterRoleBinding %q of %s %q to %s",
d.binding.Name,
d.binding.RoleRef.Kind,
d.binding.RoleRef.Name,
describeSubject(d.subject, ""),
)
}
type roleBindingDescriber struct {
binding *rbac.RoleBinding
subject *rbac.Subject
}
func (d *roleBindingDescriber) String() string {
return fmt.Sprintf("RoleBinding %q of %s %q to %s",
d.binding.Name+"/"+d.binding.Namespace,
d.binding.RoleRef.Kind,
d.binding.RoleRef.Name,
describeSubject(d.subject, d.binding.Namespace),
)
}
func (r *DefaultRuleResolver) VisitRulesFor(user user.Info, namespace string, visitor func(source fmt.Stringer, rule *rbac.PolicyRule, err error) bool) {
if clusterRoleBindings, err := r.clusterRoleBindingLister.ListClusterRoleBindings(); err != nil { if clusterRoleBindings, err := r.clusterRoleBindingLister.ListClusterRoleBindings(); err != nil {
if !visitor(nil, err) { if !visitor(nil, nil, err) {
return return
} }
} else { } else {
sourceDescriber := &clusterRoleBindingDescriber{}
for _, clusterRoleBinding := range clusterRoleBindings { for _, clusterRoleBinding := range clusterRoleBindings {
if !appliesTo(user, clusterRoleBinding.Subjects, "") { subjectIndex, applies := appliesTo(user, clusterRoleBinding.Subjects, "")
if !applies {
continue continue
} }
rules, err := r.GetRoleReferenceRules(clusterRoleBinding.RoleRef, "") rules, err := r.GetRoleReferenceRules(clusterRoleBinding.RoleRef, "")
if err != nil { if err != nil {
if !visitor(nil, err) { if !visitor(nil, nil, err) {
return return
} }
continue continue
} }
sourceDescriber.binding = clusterRoleBinding
sourceDescriber.subject = &clusterRoleBinding.Subjects[subjectIndex]
for i := range rules { for i := range rules {
if !visitor(&rules[i], nil) { if !visitor(sourceDescriber, &rules[i], nil) {
return return
} }
} }
@ -144,23 +188,27 @@ func (r *DefaultRuleResolver) VisitRulesFor(user user.Info, namespace string, vi
if len(namespace) > 0 { if len(namespace) > 0 {
if roleBindings, err := r.roleBindingLister.ListRoleBindings(namespace); err != nil { if roleBindings, err := r.roleBindingLister.ListRoleBindings(namespace); err != nil {
if !visitor(nil, err) { if !visitor(nil, nil, err) {
return return
} }
} else { } else {
sourceDescriber := &roleBindingDescriber{}
for _, roleBinding := range roleBindings { for _, roleBinding := range roleBindings {
if !appliesTo(user, roleBinding.Subjects, namespace) { subjectIndex, applies := appliesTo(user, roleBinding.Subjects, namespace)
if !applies {
continue continue
} }
rules, err := r.GetRoleReferenceRules(roleBinding.RoleRef, namespace) rules, err := r.GetRoleReferenceRules(roleBinding.RoleRef, namespace)
if err != nil { if err != nil {
if !visitor(nil, err) { if !visitor(nil, nil, err) {
return return
} }
continue continue
} }
sourceDescriber.binding = roleBinding
sourceDescriber.subject = &roleBinding.Subjects[subjectIndex]
for i := range rules { for i := range rules {
if !visitor(&rules[i], nil) { if !visitor(sourceDescriber, &rules[i], nil) {
return return
} }
} }
@ -190,13 +238,16 @@ func (r *DefaultRuleResolver) GetRoleReferenceRules(roleRef rbac.RoleRef, bindin
return nil, fmt.Errorf("unsupported role reference kind: %q", kind) return nil, fmt.Errorf("unsupported role reference kind: %q", kind)
} }
} }
func appliesTo(user user.Info, bindingSubjects []rbac.Subject, namespace string) bool {
for _, bindingSubject := range bindingSubjects { // appliesTo returns whether any of the bindingSubjects applies to the specified subject,
// and if true, the index of the first subject that applies
func appliesTo(user user.Info, bindingSubjects []rbac.Subject, namespace string) (int, bool) {
for i, bindingSubject := range bindingSubjects {
if appliesToUser(user, bindingSubject, namespace) { if appliesToUser(user, bindingSubject, namespace) {
return true return i, true
} }
} }
return false return 0, false
} }
func appliesToUser(user user.Info, subject rbac.Subject, namespace string) bool { func appliesToUser(user user.Info, subject rbac.Subject, namespace string) bool {

View File

@ -168,6 +168,7 @@ func TestAppliesTo(t *testing.T) {
user user.Info user user.Info
namespace string namespace string
appliesTo bool appliesTo bool
index int
testCase string testCase string
}{ }{
{ {
@ -176,6 +177,7 @@ func TestAppliesTo(t *testing.T) {
}, },
user: &user.DefaultInfo{Name: "foobar"}, user: &user.DefaultInfo{Name: "foobar"},
appliesTo: true, appliesTo: true,
index: 0,
testCase: "single subject that matches username", testCase: "single subject that matches username",
}, },
{ {
@ -185,6 +187,7 @@ func TestAppliesTo(t *testing.T) {
}, },
user: &user.DefaultInfo{Name: "foobar"}, user: &user.DefaultInfo{Name: "foobar"},
appliesTo: true, appliesTo: true,
index: 1,
testCase: "multiple subjects, one that matches username", testCase: "multiple subjects, one that matches username",
}, },
{ {
@ -203,6 +206,7 @@ func TestAppliesTo(t *testing.T) {
}, },
user: &user.DefaultInfo{Name: "zimzam", Groups: []string{"foobar"}}, user: &user.DefaultInfo{Name: "zimzam", Groups: []string{"foobar"}},
appliesTo: true, appliesTo: true,
index: 1,
testCase: "multiple subjects, one that match group", testCase: "multiple subjects, one that match group",
}, },
{ {
@ -213,6 +217,7 @@ func TestAppliesTo(t *testing.T) {
user: &user.DefaultInfo{Name: "zimzam", Groups: []string{"foobar"}}, user: &user.DefaultInfo{Name: "zimzam", Groups: []string{"foobar"}},
namespace: "namespace1", namespace: "namespace1",
appliesTo: true, appliesTo: true,
index: 1,
testCase: "multiple subjects, one that match group, should ignore namespace", testCase: "multiple subjects, one that match group, should ignore namespace",
}, },
{ {
@ -224,6 +229,7 @@ func TestAppliesTo(t *testing.T) {
user: &user.DefaultInfo{Name: "system:serviceaccount:kube-system:default"}, user: &user.DefaultInfo{Name: "system:serviceaccount:kube-system:default"},
namespace: "default", namespace: "default",
appliesTo: true, appliesTo: true,
index: 2,
testCase: "multiple subjects with a service account that matches", testCase: "multiple subjects with a service account that matches",
}, },
{ {
@ -243,6 +249,7 @@ func TestAppliesTo(t *testing.T) {
user: &user.DefaultInfo{Name: "foobar", Groups: []string{user.AllAuthenticated}}, user: &user.DefaultInfo{Name: "foobar", Groups: []string{user.AllAuthenticated}},
namespace: "default", namespace: "default",
appliesTo: true, appliesTo: true,
index: 0,
testCase: "binding to all authenticated and unauthenticated subjects matches authenticated user", testCase: "binding to all authenticated and unauthenticated subjects matches authenticated user",
}, },
{ {
@ -253,14 +260,18 @@ func TestAppliesTo(t *testing.T) {
user: &user.DefaultInfo{Name: "system:anonymous", Groups: []string{user.AllUnauthenticated}}, user: &user.DefaultInfo{Name: "system:anonymous", Groups: []string{user.AllUnauthenticated}},
namespace: "default", namespace: "default",
appliesTo: true, appliesTo: true,
index: 1,
testCase: "binding to all authenticated and unauthenticated subjects matches anonymous user", testCase: "binding to all authenticated and unauthenticated subjects matches anonymous user",
}, },
} }
for _, tc := range tests { for _, tc := range tests {
got := appliesTo(tc.user, tc.subjects, tc.namespace) gotIndex, got := appliesTo(tc.user, tc.subjects, tc.namespace)
if got != tc.appliesTo { if got != tc.appliesTo {
t.Errorf("case %q want appliesTo=%t, got appliesTo=%t", tc.testCase, tc.appliesTo, got) t.Errorf("case %q want appliesTo=%t, got appliesTo=%t", tc.testCase, tc.appliesTo, got)
} }
if gotIndex != tc.index {
t.Errorf("case %q want index %d, got %d", tc.testCase, tc.index, gotIndex)
}
} }
} }

View File

@ -43,7 +43,7 @@ type RequestToRuleMapper interface {
// VisitRulesFor invokes visitor() with each rule that applies to a given user in a given namespace, // VisitRulesFor invokes visitor() with each rule that applies to a given user in a given namespace,
// and each error encountered resolving those rules. Rule may be nil if err is non-nil. // and each error encountered resolving those rules. Rule may be nil if err is non-nil.
// If visitor() returns false, visiting is short-circuited. // If visitor() returns false, visiting is short-circuited.
VisitRulesFor(user user.Info, namespace string, visitor func(rule *rbac.PolicyRule, err error) bool) VisitRulesFor(user user.Info, namespace string, visitor func(source fmt.Stringer, rule *rbac.PolicyRule, err error) bool)
} }
type RBACAuthorizer struct { type RBACAuthorizer struct {
@ -55,12 +55,14 @@ type authorizingVisitor struct {
requestAttributes authorizer.Attributes requestAttributes authorizer.Attributes
allowed bool allowed bool
reason string
errors []error errors []error
} }
func (v *authorizingVisitor) visit(rule *rbac.PolicyRule, err error) bool { func (v *authorizingVisitor) visit(source fmt.Stringer, rule *rbac.PolicyRule, err error) bool {
if rule != nil && RuleAllows(v.requestAttributes, rule) { if rule != nil && RuleAllows(v.requestAttributes, rule) {
v.allowed = true v.allowed = true
v.reason = fmt.Sprintf("allowed by %s", source.String())
return false return false
} }
if err != nil { if err != nil {
@ -74,7 +76,7 @@ func (r *RBACAuthorizer) Authorize(requestAttributes authorizer.Attributes) (aut
r.authorizationRuleResolver.VisitRulesFor(requestAttributes.GetUser(), requestAttributes.GetNamespace(), ruleCheckingVisitor.visit) r.authorizationRuleResolver.VisitRulesFor(requestAttributes.GetUser(), requestAttributes.GetNamespace(), ruleCheckingVisitor.visit)
if ruleCheckingVisitor.allowed { if ruleCheckingVisitor.allowed {
return authorizer.DecisionAllow, "", nil return authorizer.DecisionAllow, ruleCheckingVisitor.reason, nil
} }
// Build a detailed log of the denial. // Build a detailed log of the denial.