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:
Eric Tune
2014-10-16 14:18:16 -07:00
parent 893291d81d
commit 55c2d6bbbb
9 changed files with 433 additions and 4 deletions

View File

@@ -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)
}
}
}

View File

@@ -44,6 +44,7 @@ func TestClient(t *testing.T) {
EnableLogsSupport: false,
EnableUISupport: false,
APIPrefix: "/api",
AuthorizationMode: "AlwaysAllow",
})
s := httptest.NewServer(m.Handler)