Add non-resource and API group support to ABAC authorizer, version ABAC policy rules

This commit is contained in:
Jordan Liggitt
2015-11-20 01:14:49 -05:00
parent 8c182c2713
commit 2321651518
16 changed files with 1492 additions and 213 deletions

View File

@@ -21,50 +21,35 @@ package abac
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"github.com/golang/glog"
"k8s.io/kubernetes/pkg/apis/abac"
"k8s.io/kubernetes/pkg/apis/abac/latest"
"k8s.io/kubernetes/pkg/apis/abac/v0"
_ "k8s.io/kubernetes/pkg/apis/abac/v1beta1"
"k8s.io/kubernetes/pkg/auth/authorizer"
)
// TODO: make this into a real API object. Note that when that happens, it
// will get MetaData. However, the Kind and Namespace in the struct below
// will be separate from the Kind and Namespace in the Metadata. Obviously,
// meta.Kind will be something like policy, and policy.Kind has to be allowed
// to be different. Less obviously, namespace needs to be different as well.
// This will allow wildcard matching strings to be used in the future for the
// body.Namespace, if we want to add that feature, without affecting the
// meta.Namespace.
type policy struct {
User string `json:"user,omitempty"`
Group string `json:"group,omitempty"`
// TODO: add support for robot accounts as well as human user accounts.
// TODO: decide how to namespace user names when multiple authentication
// providers are in use. Either add "Realm", or assume "user@example.com"
// format.
// TODO: Make the "cluster" Kinds be one API group (nodes, bindings,
// events, endpoints). The "user" Kinds are another (pods, services,
// replicationControllers, operations) Make a "plugin", e.g. build
// controller, be another group. That way when we add a new object to a
// the API, we don't have to add lots of policy?
// TODO: make this a proper REST object with its own registry.
Readonly bool `json:"readonly,omitempty"`
Resource string `json:"resource,omitempty"`
Namespace string `json:"namespace,omitempty"`
// TODO: "expires" string in RFC3339 format.
// TODO: want a way to allow some users to restart containers of a pod but
// not delete or modify it.
// TODO: want a way to allow a controller to create a pod based only on a
// certain podTemplates.
type policyLoadError struct {
path string
line int
data []byte
err error
}
type policyList []policy
func (p policyLoadError) Error() string {
if p.line >= 0 {
return fmt.Sprintf("error reading policy file %s, line %d: %s: %v", p.path, p.line, string(p.data), p.err)
}
return fmt.Sprintf("error reading policy file %s: %v", p.path, p.err)
}
type policyList []*api.Policy
// TODO: Have policies be created via an API call and stored in REST storage.
func NewFromFile(path string) (policyList, error) {
@@ -79,29 +64,151 @@ func NewFromFile(path string) (policyList, error) {
scanner := bufio.NewScanner(file)
pl := make(policyList, 0)
i := 0
unversionedLines := 0
for scanner.Scan() {
var p policy
i++
p := &api.Policy{}
b := scanner.Bytes()
// TODO: skip comment lines.
err = json.Unmarshal(b, &p)
if err != nil {
// TODO: line number in errors.
return nil, err
// skip comment lines and blank lines
trimmed := strings.TrimSpace(string(b))
if len(trimmed) == 0 || strings.HasPrefix(trimmed, "#") {
continue
}
version, kind, err := api.Scheme.DataVersionAndKind(b)
if err != nil {
return nil, policyLoadError{path, i, b, err}
}
if version == "" && kind == "" {
unversionedLines++
// Migrate unversioned policy object
oldPolicy := &v0.Policy{}
if err := latest.Codec.DecodeInto(b, oldPolicy); err != nil {
return nil, policyLoadError{path, i, b, err}
}
if err := api.Scheme.Convert(oldPolicy, p); err != nil {
return nil, policyLoadError{path, i, b, err}
}
} else {
decodedObj, err := latest.Codec.Decode(b)
if err != nil {
return nil, policyLoadError{path, i, b, err}
}
decodedPolicy, ok := decodedObj.(*api.Policy)
if !ok {
return nil, policyLoadError{path, i, b, fmt.Errorf("unrecognized object: %#v", decodedObj)}
}
p = decodedPolicy
}
pl = append(pl, p)
}
if unversionedLines > 0 {
glog.Warningf(`Policy file %s contained unversioned rules. See docs/admin/authorization.md#abac-mode for ABAC file format details.`, path)
}
if err := scanner.Err(); err != nil {
return nil, err
return nil, policyLoadError{path, -1, nil, err}
}
return pl, nil
}
func (p policy) matches(a authorizer.Attributes) bool {
if p.subjectMatches(a) {
if p.Readonly == false || (p.Readonly == a.IsReadOnly()) {
if p.Resource == "" || (p.Resource == a.GetResource()) {
if p.Namespace == "" || (p.Namespace == a.GetNamespace()) {
func matches(p api.Policy, a authorizer.Attributes) bool {
if subjectMatches(p, a) {
if verbMatches(p, a) {
// Resource and non-resource requests are mutually exclusive, at most one will match a policy
if resourceMatches(p, a) {
return true
}
if nonResourceMatches(p, a) {
return true
}
}
}
return false
}
// subjectMatches returns true if specified user and group properties in the policy match the attributes
func subjectMatches(p api.Policy, a authorizer.Attributes) bool {
matched := false
// If the policy specified a user, ensure it matches
if len(p.Spec.User) > 0 {
if p.Spec.User == "*" {
matched = true
} else {
matched = p.Spec.User == a.GetUserName()
if !matched {
return false
}
}
}
// If the policy specified a group, ensure it matches
if len(p.Spec.Group) > 0 {
if p.Spec.Group == "*" {
matched = true
} else {
matched = false
for _, group := range a.GetGroups() {
if p.Spec.Group == group {
matched = true
}
}
if !matched {
return false
}
}
}
return matched
}
func verbMatches(p api.Policy, a authorizer.Attributes) bool {
// TODO: match on verb
// All policies allow read only requests
if a.IsReadOnly() {
return true
}
// Allow if policy is not readonly
if !p.Spec.Readonly {
return true
}
return false
}
func nonResourceMatches(p api.Policy, a authorizer.Attributes) bool {
// A non-resource policy cannot match a resource request
if !a.IsResourceRequest() {
// Allow wildcard match
if p.Spec.NonResourcePath == "*" {
return true
}
// Allow exact match
if p.Spec.NonResourcePath == a.GetPath() {
return true
}
// Allow a trailing * subpath match
if strings.HasSuffix(p.Spec.NonResourcePath, "*") && strings.HasPrefix(a.GetPath(), strings.TrimRight(p.Spec.NonResourcePath, "*")) {
return true
}
}
return false
}
func resourceMatches(p api.Policy, a authorizer.Attributes) bool {
// A resource policy cannot match a non-resource request
if a.IsResourceRequest() {
if p.Spec.Namespace == "*" || p.Spec.Namespace == a.GetNamespace() {
if p.Spec.Resource == "*" || p.Spec.Resource == a.GetResource() {
if p.Spec.APIGroup == "*" || p.Spec.APIGroup == a.GetAPIGroup() {
return true
}
}
@@ -110,31 +217,10 @@ func (p policy) matches(a authorizer.Attributes) bool {
return false
}
func (p policy) subjectMatches(a authorizer.Attributes) bool {
if p.User != "" {
// Require user match
if p.User != a.GetUserName() {
return false
}
}
if p.Group != "" {
// Require group match
for _, group := range a.GetGroups() {
if p.Group == group {
return true
}
}
return false
}
return true
}
// Authorizer implements authorizer.Authorize
func (pl policyList) Authorize(a authorizer.Attributes) error {
for _, p := range pl {
if p.matches(a) {
if matches(*p, a) {
return nil
}
}

View File

@@ -21,8 +21,12 @@ import (
"os"
"testing"
"k8s.io/kubernetes/pkg/apis/abac"
"k8s.io/kubernetes/pkg/apis/abac/v0"
"k8s.io/kubernetes/pkg/apis/abac/v1beta1"
"k8s.io/kubernetes/pkg/auth/authorizer"
"k8s.io/kubernetes/pkg/auth/user"
"k8s.io/kubernetes/pkg/runtime"
)
func TestEmptyFile(t *testing.T) {
@@ -56,7 +60,7 @@ func TestExampleFile(t *testing.T) {
}
}
func TestNotAuthorized(t *testing.T) {
func TestAuthorizeV0(t *testing.T) {
a, err := newWithContents(t, `{ "readonly": true, "resource": "events" }
{"user":"scheduler", "readonly": true, "resource": "pods" }
{"user":"scheduler", "resource": "bindings" }
@@ -78,6 +82,102 @@ func TestNotAuthorized(t *testing.T) {
Verb string
Resource string
NS string
APIGroup string
Path string
ExpectAllow bool
}{
// Scheduler can read pods
{User: uScheduler, Verb: "list", Resource: "pods", NS: "ns1", ExpectAllow: true},
{User: uScheduler, Verb: "list", Resource: "pods", NS: "", ExpectAllow: true},
// Scheduler cannot write pods
{User: uScheduler, Verb: "create", Resource: "pods", NS: "ns1", ExpectAllow: false},
{User: uScheduler, Verb: "create", Resource: "pods", NS: "", ExpectAllow: false},
// Scheduler can write bindings
{User: uScheduler, Verb: "get", Resource: "bindings", NS: "ns1", ExpectAllow: true},
{User: uScheduler, Verb: "get", Resource: "bindings", NS: "", ExpectAllow: true},
// Alice can read and write anything in the right namespace.
{User: uAlice, Verb: "get", Resource: "pods", NS: "projectCaribou", ExpectAllow: true},
{User: uAlice, Verb: "get", Resource: "widgets", NS: "projectCaribou", ExpectAllow: true},
{User: uAlice, Verb: "get", Resource: "", NS: "projectCaribou", ExpectAllow: true},
{User: uAlice, Verb: "update", Resource: "pods", NS: "projectCaribou", ExpectAllow: true},
{User: uAlice, Verb: "update", Resource: "widgets", NS: "projectCaribou", ExpectAllow: true},
{User: uAlice, Verb: "update", Resource: "", NS: "projectCaribou", ExpectAllow: true},
{User: uAlice, Verb: "update", Resource: "foo", NS: "projectCaribou", APIGroup: "bar", ExpectAllow: true},
// .. but not the wrong namespace.
{User: uAlice, Verb: "get", Resource: "pods", NS: "ns1", ExpectAllow: false},
{User: uAlice, Verb: "get", Resource: "widgets", NS: "ns1", ExpectAllow: false},
{User: uAlice, Verb: "get", Resource: "", NS: "ns1", ExpectAllow: false},
// Chuck can read events, since anyone can.
{User: uChuck, Verb: "get", Resource: "events", NS: "ns1", ExpectAllow: true},
{User: uChuck, Verb: "get", Resource: "events", NS: "", ExpectAllow: true},
// Chuck can't do other things.
{User: uChuck, Verb: "update", Resource: "events", NS: "ns1", ExpectAllow: false},
{User: uChuck, Verb: "get", Resource: "pods", NS: "ns1", ExpectAllow: false},
{User: uChuck, Verb: "get", Resource: "floop", NS: "ns1", ExpectAllow: false},
// Chunk can't access things with no kind or namespace
{User: uChuck, Verb: "get", Path: "/", Resource: "", NS: "", ExpectAllow: false},
}
for i, tc := range testCases {
attr := authorizer.AttributesRecord{
User: &tc.User,
Verb: tc.Verb,
Resource: tc.Resource,
Namespace: tc.NS,
APIGroup: tc.APIGroup,
Path: tc.Path,
ResourceRequest: len(tc.NS) > 0 || len(tc.Resource) > 0,
}
err := a.Authorize(attr)
actualAllow := bool(err == nil)
if tc.ExpectAllow != actualAllow {
t.Logf("tc: %v -> attr %v", tc, attr)
t.Errorf("%d: Expected allowed=%v but actually allowed=%v\n\t%v",
i, tc.ExpectAllow, actualAllow, tc)
}
}
}
func TestAuthorizeV1beta1(t *testing.T) {
a, err := newWithContents(t,
`
# Comment line, after a blank line
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"*", "readonly": true, "nonResourcePath": "/api"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"*", "nonResourcePath": "/custom"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"*", "nonResourcePath": "/root/*"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"noresource", "nonResourcePath": "*"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"*", "readonly": true, "resource": "events", "namespace": "*"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"scheduler", "readonly": true, "resource": "pods", "namespace": "*"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"scheduler", "resource": "bindings", "namespace": "*"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"kubelet", "readonly": true, "resource": "bindings", "namespace": "*"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"kubelet", "resource": "events", "namespace": "*"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"alice", "resource": "*", "namespace": "projectCaribou"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"bob", "readonly": true, "resource": "*", "namespace": "projectCaribou"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"debbie", "resource": "pods", "namespace": "projectCaribou"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"apigroupuser", "resource": "*", "namespace": "projectAnyGroup", "apiGroup": "*"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"apigroupuser", "resource": "*", "namespace": "projectEmptyGroup", "apiGroup": "" }}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"apigroupuser", "resource": "*", "namespace": "projectXGroup", "apiGroup": "x"}}`)
if err != nil {
t.Fatalf("unable to read policy file: %v", err)
}
uScheduler := user.DefaultInfo{Name: "scheduler", UID: "uid1"}
uAlice := user.DefaultInfo{Name: "alice", UID: "uid3"}
uChuck := user.DefaultInfo{Name: "chuck", UID: "uid5"}
uDebbie := user.DefaultInfo{Name: "debbie", UID: "uid6"}
uNoResource := user.DefaultInfo{Name: "noresource", UID: "uid7"}
uAPIGroup := user.DefaultInfo{Name: "apigroupuser", UID: "uid8"}
testCases := []struct {
User user.DefaultInfo
Verb string
Resource string
APIGroup string
NS string
Path string
ExpectAllow bool
}{
// Scheduler can read pods
@@ -102,6 +202,9 @@ func TestNotAuthorized(t *testing.T) {
{User: uAlice, Verb: "get", Resource: "widgets", NS: "ns1", ExpectAllow: false},
{User: uAlice, Verb: "get", Resource: "", NS: "ns1", ExpectAllow: false},
// Debbie can write to pods in the right namespace
{User: uDebbie, Verb: "update", Resource: "pods", NS: "projectCaribou", ExpectAllow: true},
// Chuck can read events, since anyone can.
{User: uChuck, Verb: "get", Resource: "events", NS: "ns1", ExpectAllow: true},
{User: uChuck, Verb: "get", Resource: "events", NS: "", ExpectAllow: true},
@@ -109,24 +212,49 @@ func TestNotAuthorized(t *testing.T) {
{User: uChuck, Verb: "update", Resource: "events", NS: "ns1", ExpectAllow: false},
{User: uChuck, Verb: "get", Resource: "pods", NS: "ns1", ExpectAllow: false},
{User: uChuck, Verb: "get", Resource: "floop", NS: "ns1", ExpectAllow: false},
// Chunk can't access things with no kind or namespace
// TODO: find a way to give someone access to miscellaneous endpoints, such as
// /healthz, /version, etc.
{User: uChuck, Verb: "get", Resource: "", NS: "", ExpectAllow: false},
// Chuck can't access things with no resource or namespace
{User: uChuck, Verb: "get", Path: "/", Resource: "", NS: "", ExpectAllow: false},
// but can access /api
{User: uChuck, Verb: "get", Path: "/api", Resource: "", NS: "", ExpectAllow: true},
// though he cannot write to it
{User: uChuck, Verb: "create", Path: "/api", Resource: "", NS: "", ExpectAllow: false},
// while he can write to /custom
{User: uChuck, Verb: "update", Path: "/custom", Resource: "", NS: "", ExpectAllow: true},
// he cannot get "/root"
{User: uChuck, Verb: "get", Path: "/root", Resource: "", NS: "", ExpectAllow: false},
// but can get any subpath
{User: uChuck, Verb: "get", Path: "/root/", Resource: "", NS: "", ExpectAllow: true},
{User: uChuck, Verb: "get", Path: "/root/test/1/2/3", Resource: "", NS: "", ExpectAllow: true},
// the user "noresource" can get any non-resource request
{User: uNoResource, Verb: "get", Path: "", Resource: "", NS: "", ExpectAllow: true},
{User: uNoResource, Verb: "get", Path: "/", Resource: "", NS: "", ExpectAllow: true},
{User: uNoResource, Verb: "get", Path: "/foo/bar/baz", Resource: "", NS: "", ExpectAllow: true},
// but cannot get any request where IsResourceRequest() == true
{User: uNoResource, Verb: "get", Path: "/", Resource: "", NS: "bar", ExpectAllow: false},
{User: uNoResource, Verb: "get", Path: "/foo/bar/baz", Resource: "foo", NS: "bar", ExpectAllow: false},
// Test APIGroup matching
{User: uAPIGroup, Verb: "get", APIGroup: "x", Resource: "foo", NS: "projectAnyGroup", ExpectAllow: true},
{User: uAPIGroup, Verb: "get", APIGroup: "x", Resource: "foo", NS: "projectEmptyGroup", ExpectAllow: false},
{User: uAPIGroup, Verb: "get", APIGroup: "x", Resource: "foo", NS: "projectXGroup", ExpectAllow: true},
}
for i, tc := range testCases {
attr := authorizer.AttributesRecord{
User: &tc.User,
Verb: tc.Verb,
Resource: tc.Resource,
Namespace: tc.NS,
User: &tc.User,
Verb: tc.Verb,
Resource: tc.Resource,
APIGroup: tc.APIGroup,
Namespace: tc.NS,
ResourceRequest: len(tc.NS) > 0 || len(tc.Resource) > 0,
Path: tc.Path,
}
t.Logf("tc: %v -> attr %v", tc, attr)
// t.Logf("tc %2v: %v -> attr %v", i, tc, attr)
err := a.Authorize(attr)
actualAllow := bool(err == nil)
if tc.ExpectAllow != actualAllow {
t.Errorf("%d: Expected allowed=%v but actually allowed=%v\n\t%v",
i, tc.ExpectAllow, actualAllow, tc)
t.Errorf("%d: Expected allowed=%v but actually allowed=%v, for case %+v & %+v",
i, tc.ExpectAllow, actualAllow, tc, attr)
}
}
}
@@ -134,116 +262,316 @@ func TestNotAuthorized(t *testing.T) {
func TestSubjectMatches(t *testing.T) {
testCases := map[string]struct {
User user.DefaultInfo
PolicyUser string
PolicyGroup string
Policy runtime.Object
ExpectMatch bool
}{
"empty policy matches unauthed user": {
User: user.DefaultInfo{},
PolicyUser: "",
PolicyGroup: "",
"v0 empty policy matches unauthed user": {
User: user.DefaultInfo{},
Policy: &v0.Policy{
User: "",
Group: "",
},
ExpectMatch: true,
},
"empty policy matches authed user": {
User: user.DefaultInfo{Name: "Foo"},
PolicyUser: "",
PolicyGroup: "",
"v0 empty policy matches authed user": {
User: user.DefaultInfo{Name: "Foo"},
Policy: &v0.Policy{
User: "",
Group: "",
},
ExpectMatch: true,
},
"empty policy matches authed user with groups": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"a", "b"}},
PolicyUser: "",
PolicyGroup: "",
"v0 empty policy matches authed user with groups": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"a", "b"}},
Policy: &v0.Policy{
User: "",
Group: "",
},
ExpectMatch: true,
},
"user policy does not match unauthed user": {
User: user.DefaultInfo{},
PolicyUser: "Foo",
PolicyGroup: "",
"v0 user policy does not match unauthed user": {
User: user.DefaultInfo{},
Policy: &v0.Policy{
User: "Foo",
Group: "",
},
ExpectMatch: false,
},
"user policy does not match different user": {
User: user.DefaultInfo{Name: "Bar"},
PolicyUser: "Foo",
PolicyGroup: "",
"v0 user policy does not match different user": {
User: user.DefaultInfo{Name: "Bar"},
Policy: &v0.Policy{
User: "Foo",
Group: "",
},
ExpectMatch: false,
},
"user policy is case-sensitive": {
User: user.DefaultInfo{Name: "foo"},
PolicyUser: "Foo",
PolicyGroup: "",
"v0 user policy is case-sensitive": {
User: user.DefaultInfo{Name: "foo"},
Policy: &v0.Policy{
User: "Foo",
Group: "",
},
ExpectMatch: false,
},
"user policy does not match substring": {
User: user.DefaultInfo{Name: "FooBar"},
PolicyUser: "Foo",
PolicyGroup: "",
"v0 user policy does not match substring": {
User: user.DefaultInfo{Name: "FooBar"},
Policy: &v0.Policy{
User: "Foo",
Group: "",
},
ExpectMatch: false,
},
"user policy matches username": {
User: user.DefaultInfo{Name: "Foo"},
PolicyUser: "Foo",
PolicyGroup: "",
"v0 user policy matches username": {
User: user.DefaultInfo{Name: "Foo"},
Policy: &v0.Policy{
User: "Foo",
Group: "",
},
ExpectMatch: true,
},
"group policy does not match unauthed user": {
User: user.DefaultInfo{},
PolicyUser: "",
PolicyGroup: "Foo",
"v0 group policy does not match unauthed user": {
User: user.DefaultInfo{},
Policy: &v0.Policy{
User: "",
Group: "Foo",
},
ExpectMatch: false,
},
"group policy does not match user in different group": {
User: user.DefaultInfo{Name: "FooBar", Groups: []string{"B"}},
PolicyUser: "",
PolicyGroup: "A",
"v0 group policy does not match user in different group": {
User: user.DefaultInfo{Name: "FooBar", Groups: []string{"B"}},
Policy: &v0.Policy{
User: "",
Group: "A",
},
ExpectMatch: false,
},
"group policy is case-sensitive": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
PolicyUser: "",
PolicyGroup: "b",
"v0 group policy is case-sensitive": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
Policy: &v0.Policy{
User: "",
Group: "b",
},
ExpectMatch: false,
},
"group policy does not match substring": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "BBB", "C"}},
PolicyUser: "",
PolicyGroup: "B",
"v0 group policy does not match substring": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "BBB", "C"}},
Policy: &v0.Policy{
User: "",
Group: "B",
},
ExpectMatch: false,
},
"group policy matches user in group": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
PolicyUser: "",
PolicyGroup: "B",
"v0 group policy matches user in group": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
Policy: &v0.Policy{
User: "",
Group: "B",
},
ExpectMatch: true,
},
"user and group policy requires user match": {
User: user.DefaultInfo{Name: "Bar", Groups: []string{"A", "B", "C"}},
PolicyUser: "Foo",
PolicyGroup: "B",
"v0 user and group policy requires user match": {
User: user.DefaultInfo{Name: "Bar", Groups: []string{"A", "B", "C"}},
Policy: &v0.Policy{
User: "Foo",
Group: "B",
},
ExpectMatch: false,
},
"user and group policy requires group match": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
PolicyUser: "Foo",
PolicyGroup: "D",
"v0 user and group policy requires group match": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
Policy: &v0.Policy{
User: "Foo",
Group: "D",
},
ExpectMatch: false,
},
"user and group policy matches": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
PolicyUser: "Foo",
PolicyGroup: "B",
"v0 user and group policy matches": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
Policy: &v0.Policy{
User: "Foo",
Group: "B",
},
ExpectMatch: true,
},
"v1 empty policy does not match unauthed user": {
User: user.DefaultInfo{},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "",
Group: "",
},
},
ExpectMatch: false,
},
"v1 empty policy does not match authed user": {
User: user.DefaultInfo{Name: "Foo"},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "",
Group: "",
},
},
ExpectMatch: false,
},
"v1 empty policy does not match authed user with groups": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"a", "b"}},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "",
Group: "",
},
},
ExpectMatch: false,
},
"v1 user policy does not match unauthed user": {
User: user.DefaultInfo{},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "Foo",
Group: "",
},
},
ExpectMatch: false,
},
"v1 user policy does not match different user": {
User: user.DefaultInfo{Name: "Bar"},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "Foo",
Group: "",
},
},
ExpectMatch: false,
},
"v1 user policy is case-sensitive": {
User: user.DefaultInfo{Name: "foo"},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "Foo",
Group: "",
},
},
ExpectMatch: false,
},
"v1 user policy does not match substring": {
User: user.DefaultInfo{Name: "FooBar"},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "Foo",
Group: "",
},
},
ExpectMatch: false,
},
"v1 user policy matches username": {
User: user.DefaultInfo{Name: "Foo"},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "Foo",
Group: "",
},
},
ExpectMatch: true,
},
"v1 group policy does not match unauthed user": {
User: user.DefaultInfo{},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "",
Group: "Foo",
},
},
ExpectMatch: false,
},
"v1 group policy does not match user in different group": {
User: user.DefaultInfo{Name: "FooBar", Groups: []string{"B"}},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "",
Group: "A",
},
},
ExpectMatch: false,
},
"v1 group policy is case-sensitive": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "",
Group: "b",
},
},
ExpectMatch: false,
},
"v1 group policy does not match substring": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "BBB", "C"}},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "",
Group: "B",
},
},
ExpectMatch: false,
},
"v1 group policy matches user in group": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "",
Group: "B",
},
},
ExpectMatch: true,
},
"v1 user and group policy requires user match": {
User: user.DefaultInfo{Name: "Bar", Groups: []string{"A", "B", "C"}},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "Foo",
Group: "B",
},
},
ExpectMatch: false,
},
"v1 user and group policy requires group match": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "Foo",
Group: "D",
},
},
ExpectMatch: false,
},
"v1 user and group policy matches": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "Foo",
Group: "B",
},
},
ExpectMatch: true,
},
}
for k, tc := range testCases {
policy := &api.Policy{}
if err := api.Scheme.Convert(tc.Policy, policy); err != nil {
t.Errorf("%s: error converting: %v", k, err)
continue
}
attr := authorizer.AttributesRecord{
User: &tc.User,
}
actualMatch := policy{User: tc.PolicyUser, Group: tc.PolicyGroup}.subjectMatches(attr)
actualMatch := subjectMatches(*policy, attr)
if tc.ExpectMatch != actualMatch {
t.Errorf("%v: Expected actorMatches=%v but actually got=%v",
k, tc.ExpectMatch, actualMatch)
@@ -269,27 +597,30 @@ func newWithContents(t *testing.T, contents string) (authorizer.Authorizer, erro
func TestPolicy(t *testing.T) {
tests := []struct {
policy policy
policy runtime.Object
attr authorizer.Attributes
matches bool
name string
}{
// v0
{
policy: policy{},
policy: &v0.Policy{},
attr: authorizer.AttributesRecord{},
matches: true,
name: "null",
name: "v0 null",
},
// v0 mismatches
{
policy: policy{
policy: &v0.Policy{
Readonly: true,
},
attr: authorizer.AttributesRecord{},
matches: false,
name: "read-only mismatch",
name: "v0 read-only mismatch",
},
{
policy: policy{
policy: &v0.Policy{
User: "foo",
},
attr: authorizer.AttributesRecord{
@@ -298,20 +629,21 @@ func TestPolicy(t *testing.T) {
},
},
matches: false,
name: "user name mis-match",
name: "v0 user name mis-match",
},
{
policy: policy{
policy: &v0.Policy{
Resource: "foo",
},
attr: authorizer.AttributesRecord{
Resource: "bar",
Resource: "bar",
ResourceRequest: true,
},
matches: false,
name: "resource mis-match",
name: "v0 resource mis-match",
},
{
policy: policy{
policy: &v0.Policy{
User: "foo",
Resource: "foo",
Namespace: "foo",
@@ -320,27 +652,314 @@ func TestPolicy(t *testing.T) {
User: &user.DefaultInfo{
Name: "foo",
},
Resource: "foo",
Namespace: "foo",
Resource: "foo",
Namespace: "foo",
ResourceRequest: true,
},
matches: true,
name: "namespace mis-match",
name: "v0 namespace mis-match",
},
// v0 matches
{
policy: &v0.Policy{},
attr: authorizer.AttributesRecord{ResourceRequest: true},
matches: true,
name: "v0 null resource",
},
{
policy: policy{
Namespace: "foo",
policy: &v0.Policy{
Readonly: true,
},
attr: authorizer.AttributesRecord{
Namespace: "bar",
Verb: "get",
},
matches: true,
name: "v0 read-only match",
},
{
policy: &v0.Policy{
User: "foo",
},
attr: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: "foo",
},
},
matches: true,
name: "v0 user name match",
},
{
policy: &v0.Policy{
Resource: "foo",
},
attr: authorizer.AttributesRecord{
Resource: "foo",
ResourceRequest: true,
},
matches: true,
name: "v0 resource match",
},
// v1 mismatches
{
policy: &v1beta1.Policy{},
attr: authorizer.AttributesRecord{
ResourceRequest: true,
},
matches: false,
name: "resource mis-match",
name: "v1 null",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "foo",
},
},
attr: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: "bar",
},
ResourceRequest: true,
},
matches: false,
name: "v1 user name mis-match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "*",
Readonly: true,
},
},
attr: authorizer.AttributesRecord{
ResourceRequest: true,
},
matches: false,
name: "v1 read-only mismatch",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "*",
Resource: "foo",
},
},
attr: authorizer.AttributesRecord{
Resource: "bar",
ResourceRequest: true,
},
matches: false,
name: "v1 resource mis-match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "foo",
Namespace: "barr",
Resource: "baz",
},
},
attr: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: "foo",
},
Namespace: "bar",
Resource: "baz",
ResourceRequest: true,
},
matches: false,
name: "v1 namespace mis-match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "*",
NonResourcePath: "/api",
},
},
attr: authorizer.AttributesRecord{
Path: "/api2",
ResourceRequest: false,
},
matches: false,
name: "v1 non-resource mis-match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "*",
NonResourcePath: "/api/*",
},
},
attr: authorizer.AttributesRecord{
Path: "/api2/foo",
ResourceRequest: false,
},
matches: false,
name: "v1 non-resource wildcard subpath mis-match",
},
// v1 matches
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "foo",
},
},
attr: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: "foo",
},
ResourceRequest: true,
},
matches: true,
name: "v1 user match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "*",
},
},
attr: authorizer.AttributesRecord{
ResourceRequest: true,
},
matches: true,
name: "v1 user wildcard match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
Group: "bar",
},
},
attr: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: "foo",
Groups: []string{"bar"},
},
ResourceRequest: true,
},
matches: true,
name: "v1 group match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
Group: "*",
},
},
attr: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: "foo",
Groups: []string{"bar"},
},
ResourceRequest: true,
},
matches: true,
name: "v1 group wildcard match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "*",
Readonly: true,
},
},
attr: authorizer.AttributesRecord{
Verb: "get",
ResourceRequest: true,
},
matches: true,
name: "v1 read-only match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "*",
Resource: "foo",
},
},
attr: authorizer.AttributesRecord{
Resource: "foo",
ResourceRequest: true,
},
matches: true,
name: "v1 resource match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "foo",
Namespace: "bar",
Resource: "baz",
},
},
attr: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: "foo",
},
Namespace: "bar",
Resource: "baz",
ResourceRequest: true,
},
matches: true,
name: "v1 namespace match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "*",
NonResourcePath: "/api",
},
},
attr: authorizer.AttributesRecord{
Path: "/api",
ResourceRequest: false,
},
matches: true,
name: "v1 non-resource match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "*",
NonResourcePath: "*",
},
},
attr: authorizer.AttributesRecord{
Path: "/api",
ResourceRequest: false,
},
matches: true,
name: "v1 non-resource wildcard match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "*",
NonResourcePath: "/api/*",
},
},
attr: authorizer.AttributesRecord{
Path: "/api/foo",
ResourceRequest: false,
},
matches: true,
name: "v1 non-resource wildcard subpath match",
},
}
for _, test := range tests {
matches := test.policy.matches(test.attr)
policy := &api.Policy{}
if err := api.Scheme.Convert(test.policy, policy); err != nil {
t.Errorf("%s: error converting: %v", test.name, err)
continue
}
matches := matches(*policy, test.attr)
if test.matches != matches {
t.Errorf("unexpected value for %s, expected: %t, saw: %t", test.name, test.matches, matches)
t.Errorf("%s: expected: %t, saw: %t", test.name, test.matches, matches)
continue
}
}
}

View File

@@ -1,9 +1,10 @@
{"user":"admin"}
{"user":"scheduler", "readonly": true, "resource": "pods"}
{"user":"scheduler", "resource": "bindings"}
{"user":"kubelet", "readonly": true, "resource": "pods"}
{"user":"kubelet", "readonly": true, "resource": "services"}
{"user":"kubelet", "readonly": true, "resource": "endpoints"}
{"user":"kubelet", "resource": "events"}
{"user":"alice", "namespace": "projectCaribou"}
{"user":"bob", "readonly": true, "namespace": "projectCaribou"}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"*", "nonResourcePath": "*", "readonly": true}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"admin", "namespace": "*", "resource": "*", "apiGroup": "*" }
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"scheduler", "namespace": "*", "resource": "pods", "readonly": true }
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"scheduler", "namespace": "*", "resource": "bindings" }
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"kubelet", "namespace": "*", "resource": "pods", "readonly": true }
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"kubelet", "namespace": "*", "resource": "services", "readonly": true }
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"kubelet", "namespace": "*", "resource": "endpoints", "readonly": true }
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"kubelet", "namespace": "*", "resource": "events" }
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"alice", "namespace": "projectCaribou", "resource": "*", "apiGroup": "*" }
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"bob", "namespace": "projectCaribou", "resource": "*", "apiGroup": "*", "readonly": true }

View File

@@ -50,6 +50,13 @@ type Attributes interface {
// The group of the resource, if a request is for a REST object.
GetAPIGroup() string
// IsResourceRequest returns true for requests to API resources, like /api/v1/nodes,
// and false for non-resource endpoints like /api, /healthz, and /swaggerapi
IsResourceRequest() bool
// GetPath returns the path of the request
GetPath() string
}
// Authorizer makes an authorization decision based on information gained by making
@@ -72,11 +79,13 @@ type RequestAttributesGetter interface {
// AttributesRecord implements Attributes interface.
type AttributesRecord struct {
User user.Info
Verb string
Namespace string
APIGroup string
Resource string
User user.Info
Verb string
Namespace string
APIGroup string
Resource string
ResourceRequest bool
Path string
}
func (a AttributesRecord) GetUserName() string {
@@ -106,3 +115,11 @@ func (a AttributesRecord) GetResource() string {
func (a AttributesRecord) GetAPIGroup() string {
return a.APIGroup
}
func (a AttributesRecord) IsResourceRequest() bool {
return a.ResourceRequest
}
func (a AttributesRecord) GetPath() string {
return a.Path
}