Basic ACL file.

Added function to read basic ACL from a CSV file.
Added implementation of Authorize based on that file's policies.
Added docs on authentication and authorization.
Added example file and tested it.
This commit is contained in:
Eric Tune
2014-10-06 16:11:04 -07:00
parent f4cffdc7cf
commit 6e81e8c896
9 changed files with 457 additions and 57 deletions

View File

@@ -0,0 +1,124 @@
/*
Copyright 2014 Google Inc. All rights reserved.
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 abac
// Policy authorizes Kubernetes API actions using an Attribute-based access
// control scheme.
import (
"bufio"
"encoding/json"
"errors"
"os"
"github.com/GoogleCloudPlatform/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" yaml:"user,omitempty"`
// TODO: add support for groups as well as users.
// 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 (minions, 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" yaml:"readonly,omitempty"`
Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
Namespace string `json:"namespace,omitempty" yaml:"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 policyList []policy
// TODO: Have policies be created via an API call and stored in REST storage.
func NewFromFile(path string) (policyList, error) {
// File format is one map per line. This allows easy concatentation of files,
// comments in files, and identification of errors by line number.
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
pl := make(policyList, 0)
var p policy
for scanner.Scan() {
b := scanner.Bytes()
// TODO: skip comment lines.
err = json.Unmarshal(b, &p)
if err != nil {
// TODO: line number in errors.
return nil, err
}
pl = append(pl, p)
}
if err := scanner.Err(); err != nil {
return nil, err
}
return pl, nil
}
func (p policy) matches(a authorizer.Attributes) bool {
if p.User == "" || p.User == a.GetUserName() {
if p.Readonly == false || (p.Readonly == a.IsReadOnly()) {
if p.Kind == "" || (p.Kind == a.GetKind()) {
if p.Namespace == "" || (p.Namespace == a.GetNamespace()) {
return true
}
}
}
}
return false
}
// Authorizer implements authorizer.Authorize
func (pl policyList) Authorize(a authorizer.Attributes) error {
for _, p := range pl {
if p.matches(a) {
return nil
}
}
return errors.New("No policy matched.")
// TODO: Benchmark how much time policy matching takes with a medium size
// policy file, compared to other steps such as encoding/decoding.
// Then, add Caching only if needed.
}

View File

@@ -0,0 +1,148 @@
/*
Copyright 2014 Google Inc. All rights reserved.
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 abac
import (
"io/ioutil"
"os"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user"
)
func TestEmptyFile(t *testing.T) {
_, err := newWithContents(t, "")
if err != nil {
t.Errorf("unable to read policy file: %v", err)
}
}
func TestOneLineFileNoNewLine(t *testing.T) {
_, err := newWithContents(t, `{"user":"scheduler", "readonly": true, "kind": "pods", "namespace":"ns1"}`)
if err != nil {
t.Errorf("unable to read policy file: %v", err)
}
}
func TestTwoLineFile(t *testing.T) {
_, err := newWithContents(t, `{"user":"scheduler", "readonly": true, "kind": "pods"}
{"user":"scheduler", "readonly": true, "kind": "services"}
`)
if err != nil {
t.Errorf("unable to read policy file: %v", err)
}
}
// Test the file that we will point users at as an example.
func TestExampleFile(t *testing.T) {
_, err := NewFromFile("./example_policy_file.jsonl")
if err != nil {
t.Errorf("unable to read policy file: %v", err)
}
}
func NotTestAuthorize(t *testing.T) {
a, err := newWithContents(t, `{ "readonly": true, "kind": "events"}
{"user":"scheduler", "readonly": true, "kind": "pods"}
{"user":"scheduler", "kind": "bindings"}
{"user":"kubelet", "readonly": true, "kind": "bindings"}
{"user":"kubelet", "kind": "events"}
{"user":"alice", "ns": "projectCaribou"}
{"user":"bob", "readonly": true, "ns": "projectCaribou"}
`)
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"}
testCases := []struct {
User user.DefaultInfo
RO bool
Kind string
NS string
ExpectAllow bool
}{
// Scheduler can read pods
{User: uScheduler, RO: true, Kind: "pods", NS: "ns1", ExpectAllow: true},
{User: uScheduler, RO: true, Kind: "pods", NS: "", ExpectAllow: true},
// Scheduler cannot write pods
{User: uScheduler, RO: false, Kind: "pods", NS: "ns1", ExpectAllow: false},
{User: uScheduler, RO: false, Kind: "pods", NS: "", ExpectAllow: false},
// Scheduler can write bindings
{User: uScheduler, RO: true, Kind: "bindings", NS: "ns1", ExpectAllow: true},
{User: uScheduler, RO: true, Kind: "bindings", NS: "", ExpectAllow: true},
// Alice can read and write anything in the right namespace.
{User: uAlice, RO: true, Kind: "pods", NS: "projectCaribou", ExpectAllow: true},
{User: uAlice, RO: true, Kind: "widgets", NS: "projectCaribou", ExpectAllow: true},
{User: uAlice, RO: true, Kind: "", NS: "projectCaribou", ExpectAllow: true},
{User: uAlice, RO: false, Kind: "pods", NS: "projectCaribou", ExpectAllow: true},
{User: uAlice, RO: false, Kind: "widgets", NS: "projectCaribou", ExpectAllow: true},
{User: uAlice, RO: false, Kind: "", NS: "projectCaribou", ExpectAllow: true},
// .. but not the wrong namespace.
{User: uAlice, RO: true, Kind: "pods", NS: "ns1", ExpectAllow: false},
{User: uAlice, RO: true, Kind: "widgets", NS: "ns1", ExpectAllow: false},
{User: uAlice, RO: true, Kind: "", NS: "ns1", ExpectAllow: false},
// Chuck can read events, since anyone can.
{User: uChuck, RO: true, Kind: "events", NS: "ns1", ExpectAllow: true},
{User: uChuck, RO: true, Kind: "events", NS: "", ExpectAllow: true},
// Chuck can't do other things.
{User: uChuck, RO: false, Kind: "events", NS: "ns1", ExpectAllow: false},
{User: uChuck, RO: true, Kind: "pods", NS: "ns1", ExpectAllow: false},
{User: uChuck, RO: true, Kind: "floop", NS: "ns1", ExpectAllow: false},
// Chunk can't access things with no kind or namespace
// TODO: find a way to give someone access to miscelaneous endpoints, such as
// /healthz, /version, etc.
{User: uChuck, RO: true, Kind: "", NS: "", ExpectAllow: false},
}
for _, tc := range testCases {
attr := authorizer.AttributesRecord{
User: &tc.User,
ReadOnly: tc.RO,
Kind: tc.Kind,
Namespace: tc.NS,
}
t.Logf("tc: %v -> attr %v", tc, attr)
err := a.Authorize(attr)
actualAllow := bool(err == nil)
if tc.ExpectAllow != actualAllow {
t.Errorf("Expected allowed=%v but actually allowed=%v, for case %v",
tc.ExpectAllow, actualAllow, tc)
}
}
}
func newWithContents(t *testing.T, contents string) (authorizer.Authorizer, error) {
f, err := ioutil.TempFile("", "abac_test")
if err != nil {
t.Fatalf("unexpected error creating policyfile: %v", err)
}
f.Close()
defer os.Remove(f.Name())
if err := ioutil.WriteFile(f.Name(), []byte(contents), 0700); err != nil {
t.Fatalf("unexpected error writing policyfile: %v", err)
}
pl, err := NewFromFile(f.Name())
return pl, err
}

View File

@@ -0,0 +1,7 @@
{"user":"admin"}
{"user":"scheduler", "readonly": true, "kind": "pods"}
{"user":"scheduler", "kind": "bindings"}
{"user":"kubelet", "readonly": true, "kind": "bindings"}
{"user":"kubelet", "kind": "events"}
{"user":"alice", "ns": "projectCaribou"}
{"user":"bob", "readonly": true, "ns": "projectCaribou"}

View File

@@ -54,18 +54,18 @@ type AttributesRecord struct {
Kind string
}
func (a *AttributesRecord) GetUserName() string {
func (a AttributesRecord) GetUserName() string {
return a.User.GetName()
}
func (a *AttributesRecord) IsReadOnly() bool {
func (a AttributesRecord) IsReadOnly() bool {
return a.ReadOnly
}
func (a *AttributesRecord) GetNamespace() string {
func (a AttributesRecord) GetNamespace() string {
return a.Namespace
}
func (a *AttributesRecord) GetKind() string {
func (a AttributesRecord) GetKind() string {
return a.Kind
}