mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-09-17 07:03:31 +00:00
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:
124
pkg/auth/authorizer/abac/abac.go
Normal file
124
pkg/auth/authorizer/abac/abac.go
Normal 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.
|
||||
}
|
148
pkg/auth/authorizer/abac/abac_test.go
Normal file
148
pkg/auth/authorizer/abac/abac_test.go
Normal 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
|
||||
}
|
7
pkg/auth/authorizer/abac/example_policy_file.jsonl
Normal file
7
pkg/auth/authorizer/abac/example_policy_file.jsonl
Normal 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"}
|
@@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user