Add basic Authorization.
Added basic interface for authorizer implementations. Added default "authorize everything" and "authorize nothing implementations. Added authorization check immediately after authentication check. Added an integration test of authorization at the HTTP level of abstraction.
This commit is contained in:
@@ -23,6 +23,7 @@ package integration
|
||||
// to work for any client of the HTTP interface.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
@@ -70,6 +71,7 @@ xyz987,bob,2
|
||||
EnableUISupport: false,
|
||||
APIPrefix: "/api",
|
||||
TokenAuthFile: f.Name(),
|
||||
AuthorizationMode: "AlwaysAllow",
|
||||
})
|
||||
|
||||
s := httptest.NewServer(m.Handler)
|
||||
@@ -118,3 +120,293 @@ xyz987,bob,2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bodies for requests used in subsequent tests.
|
||||
var aPod string = `
|
||||
{
|
||||
"kind": "Pod",
|
||||
"apiVersion": "v1beta1",
|
||||
"id": "a",
|
||||
"desiredState": {
|
||||
"manifest": {
|
||||
"version": "v1beta1",
|
||||
"id": "a",
|
||||
"containers": [{ "name": "foo", "image": "bar/foo", }]
|
||||
}
|
||||
},
|
||||
}
|
||||
`
|
||||
var aRC string = `
|
||||
{
|
||||
"kind": "ReplicationController",
|
||||
"apiVersion": "v1beta1",
|
||||
"id": "a",
|
||||
"desiredState": {
|
||||
"replicas": 2,
|
||||
"replicaSelector": {"name": "a"},
|
||||
"podTemplate": {
|
||||
"desiredState": {
|
||||
"manifest": {
|
||||
"version": "v1beta1",
|
||||
"id": "a",
|
||||
"containers": [{
|
||||
"name": "foo",
|
||||
"image": "bar/foo",
|
||||
}]
|
||||
}
|
||||
},
|
||||
"labels": {"name": "a"}
|
||||
}},
|
||||
"labels": {"name": "a"}
|
||||
}
|
||||
`
|
||||
var aService string = `
|
||||
{
|
||||
"kind": "Service",
|
||||
"apiVersion": "v1beta1",
|
||||
"id": "a",
|
||||
"port": 8000,
|
||||
"labels": { "name": "a" },
|
||||
"selector": { "name": "a" }
|
||||
}
|
||||
`
|
||||
var aMinion string = `
|
||||
{
|
||||
"kind": "Minion",
|
||||
"apiVersion": "v1beta1",
|
||||
"id": "a",
|
||||
"hostIP": "10.10.10.10",
|
||||
}
|
||||
`
|
||||
|
||||
var aEvent string = `
|
||||
{
|
||||
"kind": "Binding",
|
||||
"apiVersion": "v1beta1",
|
||||
"id": "a",
|
||||
"involvedObject": {
|
||||
{
|
||||
"kind": "Minion",
|
||||
"name": "a"
|
||||
"apiVersion": "v1beta1",
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
var aBinding string = `
|
||||
{
|
||||
"kind": "Binding",
|
||||
"apiVersion": "v1beta1",
|
||||
"id": "a",
|
||||
"host": "10.10.10.10",
|
||||
"podID": "a"
|
||||
}
|
||||
`
|
||||
|
||||
var aEndpoints string = `
|
||||
{
|
||||
"kind": "Endpoints",
|
||||
"apiVersion": "v1beta1",
|
||||
"id": "a",
|
||||
"endpoints": ["10.10.1.1:1909"],
|
||||
}
|
||||
`
|
||||
|
||||
// Requests to try. Each one should be forbidden or not forbidden
|
||||
// depending on the authentication and authorization setup of the master.
|
||||
|
||||
func getTestRequests() []struct {
|
||||
verb string
|
||||
URL string
|
||||
body string
|
||||
} {
|
||||
requests := []struct {
|
||||
verb string
|
||||
URL string
|
||||
body string
|
||||
}{
|
||||
// Normal methods on pods
|
||||
{"GET", "/api/v1beta1/pods", ""},
|
||||
{"GET", "/api/v1beta1/pods/a", ""},
|
||||
{"POST", "/api/v1beta1/pods", aPod},
|
||||
{"PUT", "/api/v1beta1/pods", aPod},
|
||||
{"GET", "/api/v1beta1/pods", ""},
|
||||
{"GET", "/api/v1beta1/pods/a", ""},
|
||||
{"DELETE", "/api/v1beta1/pods", ""},
|
||||
|
||||
// Non-standard methods (not expected to work,
|
||||
// but expected to pass/fail authorization prior to
|
||||
// failing validation.
|
||||
{"PATCH", "/api/v1beta1/pods/a", ""},
|
||||
{"OPTIONS", "/api/v1beta1/pods", ""},
|
||||
{"OPTIONS", "/api/v1beta1/pods/a", ""},
|
||||
{"HEAD", "/api/v1beta1/pods", ""},
|
||||
{"HEAD", "/api/v1beta1/pods/a", ""},
|
||||
{"TRACE", "/api/v1beta1/pods", ""},
|
||||
{"TRACE", "/api/v1beta1/pods/a", ""},
|
||||
{"NOSUCHVERB", "/api/v1beta1/pods", ""},
|
||||
|
||||
// Normal methods on services
|
||||
{"GET", "/api/v1beta1/services", ""},
|
||||
{"GET", "/api/v1beta1/services/a", ""},
|
||||
{"POST", "/api/v1beta1/services", aService},
|
||||
{"PUT", "/api/v1beta1/services", aService},
|
||||
{"GET", "/api/v1beta1/services", ""},
|
||||
{"GET", "/api/v1beta1/services/a", ""},
|
||||
{"DELETE", "/api/v1beta1/services", ""},
|
||||
|
||||
// Normal methods on replicationControllers
|
||||
{"GET", "/api/v1beta1/replicationControllers", ""},
|
||||
{"GET", "/api/v1beta1/replicationControllers/a", ""},
|
||||
{"POST", "/api/v1beta1/replicationControllers", aRC},
|
||||
{"PUT", "/api/v1beta1/replicationControllers", aRC},
|
||||
{"GET", "/api/v1beta1/replicationControllers", ""},
|
||||
{"GET", "/api/v1beta1/replicationControllers/a", ""},
|
||||
{"DELETE", "/api/v1beta1/replicationControllers", ""},
|
||||
|
||||
// Normal methods on endpoints
|
||||
{"GET", "/api/v1beta1/endpoints", ""},
|
||||
{"GET", "/api/v1beta1/endpoints/a", ""},
|
||||
{"POST", "/api/v1beta1/endpoints", aEndpoints},
|
||||
{"PUT", "/api/v1beta1/endpoints", aEndpoints},
|
||||
{"GET", "/api/v1beta1/endpoints", ""},
|
||||
{"GET", "/api/v1beta1/endpoints/a", ""},
|
||||
{"DELETE", "/api/v1beta1/endpoints", ""},
|
||||
|
||||
// Normal methods on minions
|
||||
{"GET", "/api/v1beta1/minions", ""},
|
||||
{"GET", "/api/v1beta1/minions/a", ""},
|
||||
{"POST", "/api/v1beta1/minions", aMinion},
|
||||
{"PUT", "/api/v1beta1/minions", aMinion},
|
||||
{"GET", "/api/v1beta1/minions", ""},
|
||||
{"GET", "/api/v1beta1/minions/a", ""},
|
||||
{"DELETE", "/api/v1beta1/minions", ""},
|
||||
|
||||
// Normal methods on events
|
||||
{"GET", "/api/v1beta1/events", ""},
|
||||
{"GET", "/api/v1beta1/events/a", ""},
|
||||
{"POST", "/api/v1beta1/events", aEvent},
|
||||
{"PUT", "/api/v1beta1/events", aEvent},
|
||||
{"GET", "/api/v1beta1/events", ""},
|
||||
{"GET", "/api/v1beta1/events/a", ""},
|
||||
{"DELETE", "/api/v1beta1/events", ""},
|
||||
|
||||
// Normal methods on bindings
|
||||
{"GET", "/api/v1beta1/events", ""},
|
||||
{"GET", "/api/v1beta1/events/a", ""},
|
||||
{"POST", "/api/v1beta1/events", aBinding},
|
||||
{"PUT", "/api/v1beta1/events", aBinding},
|
||||
{"GET", "/api/v1beta1/events", ""},
|
||||
{"GET", "/api/v1beta1/events/a", ""},
|
||||
{"DELETE", "/api/v1beta1/events", ""},
|
||||
|
||||
// Non-existent object type.
|
||||
{"GET", "/api/v1beta1/foo", ""},
|
||||
{"GET", "/api/v1beta1/foo/a", ""},
|
||||
{"POST", "/api/v1beta1/foo", `{"foo": "foo"}`},
|
||||
{"PUT", "/api/v1beta1/foo", `{"foo": "foo"}`},
|
||||
{"GET", "/api/v1beta1/foo", ""},
|
||||
{"GET", "/api/v1beta1/foo/a", ""},
|
||||
{"DELETE", "/api/v1beta1/foo", ""},
|
||||
|
||||
// Operations
|
||||
{"GET", "/api/v1beta1/operations", ""},
|
||||
{"GET", "/api/v1beta1/operations/1234567890", ""},
|
||||
|
||||
// Special verbs on pods
|
||||
{"GET", "/api/v1beta1/proxy/pods/a", ""},
|
||||
{"GET", "/api/v1beta1/redirect/pods/a", ""},
|
||||
// TODO: test .../watch/..., which doesn't end before the test timeout.
|
||||
|
||||
// Non-object endpoints
|
||||
{"GET", "/", ""},
|
||||
{"GET", "/healthz", ""},
|
||||
{"GET", "/versions", ""},
|
||||
}
|
||||
return requests
|
||||
}
|
||||
|
||||
// The TestAuthMode* tests tests a large number of URLs and checks that they
|
||||
// are FORBIDDEN or not, depending on the mode. They do not attempt to do
|
||||
// detailed verification of behaviour beyond authorization. They are not
|
||||
// fuzz tests.
|
||||
//
|
||||
// TODO(etune): write a fuzz test of the REST API.
|
||||
func TestAuthModeAlwaysAllow(t *testing.T) {
|
||||
deleteAllEtcdKeys()
|
||||
|
||||
// Set up a master
|
||||
|
||||
helper, err := master.NewEtcdHelper(newEtcdClient(), "v1beta1")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
m := master.New(&master.Config{
|
||||
EtcdHelper: helper,
|
||||
EnableLogsSupport: false,
|
||||
EnableUISupport: false,
|
||||
APIPrefix: "/api",
|
||||
AuthorizationMode: "AlwaysAllow",
|
||||
})
|
||||
|
||||
s := httptest.NewServer(m.Handler)
|
||||
defer s.Close()
|
||||
transport := http.DefaultTransport
|
||||
|
||||
for _, r := range getTestRequests() {
|
||||
t.Logf("case %v", r)
|
||||
bodyBytes := bytes.NewReader([]byte(r.body))
|
||||
req, err := http.NewRequest(r.verb, s.URL+r.URL, bodyBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
resp, err := transport.RoundTrip(req)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if resp.StatusCode == http.StatusForbidden {
|
||||
t.Errorf("Expected status other than Forbidden")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthModeAlwaysDeny(t *testing.T) {
|
||||
deleteAllEtcdKeys()
|
||||
|
||||
// Set up a master
|
||||
|
||||
helper, err := master.NewEtcdHelper(newEtcdClient(), "v1beta1")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
m := master.New(&master.Config{
|
||||
EtcdHelper: helper,
|
||||
EnableLogsSupport: false,
|
||||
EnableUISupport: false,
|
||||
APIPrefix: "/api",
|
||||
AuthorizationMode: "AlwaysDeny",
|
||||
})
|
||||
|
||||
s := httptest.NewServer(m.Handler)
|
||||
defer s.Close()
|
||||
transport := http.DefaultTransport
|
||||
|
||||
for _, r := range getTestRequests() {
|
||||
t.Logf("case %v", r)
|
||||
bodyBytes := bytes.NewReader([]byte(r.body))
|
||||
req, err := http.NewRequest(r.verb, s.URL+r.URL, bodyBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
resp, err := transport.RoundTrip(req)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusForbidden {
|
||||
t.Errorf("Expected status Forbidden but got status %v", resp.Status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -44,6 +44,7 @@ func TestClient(t *testing.T) {
|
||||
EnableLogsSupport: false,
|
||||
EnableUISupport: false,
|
||||
APIPrefix: "/api",
|
||||
AuthorizationMode: "AlwaysAllow",
|
||||
})
|
||||
|
||||
s := httptest.NewServer(m.Handler)
|
||||
|
Reference in New Issue
Block a user