Merge pull request #2122 from erictune/moar_attribs

Moar authorization attributes
pull/6/head
Daniel Smith 2014-11-04 13:17:47 -08:00
commit e4dcd4a131
9 changed files with 374 additions and 60 deletions

View File

@ -145,6 +145,12 @@ func main() {
}
n := net.IPNet(portalNet)
authorizer, err := apiserver.NewAuthorizerFromAuthorizationConfig(*authorizationMode)
if err != nil {
glog.Fatalf("Invalid Authorization Config: %v", err)
}
config := &master.Config{
Client: client,
Cloud: cloud,
@ -161,7 +167,7 @@ func main() {
ReadOnlyPort: *readOnlyPort,
ReadWritePort: *port,
PublicAddress: *publicAddressOverride,
AuthorizationMode: *authorizationMode,
Authorizer: authorizer,
}
m := master.New(config)

View File

@ -36,6 +36,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi"
"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
minionControllerPkg "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/controller"
replicationControllerPkg "github.com/GoogleCloudPlatform/kubernetes/pkg/controller"
@ -146,7 +147,7 @@ func startComponents(manifestURL string) (apiServerURL string) {
KubeletClient: fakeKubeletClient{},
EnableLogsSupport: false,
APIPrefix: "/api",
AuthorizationMode: "AlwaysAllow",
Authorizer: apiserver.NewAlwaysAllowAuthorizer(),
ReadWritePort: portNumber,
ReadOnlyPort: portNumber,

View File

@ -107,6 +107,7 @@ func (g *APIGroup) InstallREST(mux Mux, paths ...string) {
prefix = strings.TrimRight(prefix, "/")
proxyHandler := &ProxyHandler{prefix + "/proxy/", g.handler.storage, g.handler.codec}
mux.Handle(prefix+"/", http.StripPrefix(prefix, restHandler))
// Note: update GetAttribs() when adding a handler.
mux.Handle(prefix+"/watch/", http.StripPrefix(prefix+"/watch/", watchHandler))
mux.Handle(prefix+"/proxy/", http.StripPrefix(prefix+"/proxy/", proxyHandler))
mux.Handle(prefix+"/redirect/", http.StripPrefix(prefix+"/redirect/", redirectHandler))

View File

@ -36,6 +36,10 @@ func (alwaysAllowAuthorizer) Authorize(a authorizer.Attributes) (err error) {
return nil
}
func NewAlwaysAllowAuthorizer() authorizer.Authorizer {
return new(alwaysAllowAuthorizer)
}
// alwaysDenyAuthorizer is an implementation of authorizer.Attributes
// which always says no to an authorization request.
// It is useful in unit tests to force an operation to be forbidden.
@ -45,6 +49,10 @@ func (alwaysDenyAuthorizer) Authorize(a authorizer.Attributes) (err error) {
return errors.New("Everything is forbidden.")
}
func NewAlwaysDenyAuthorizer() authorizer.Authorizer {
return new(alwaysDenyAuthorizer)
}
const (
ModeAlwaysAllow string = "AlwaysAllow"
ModeAlwaysDeny string = "AlwaysDeny"
@ -59,9 +67,9 @@ func NewAuthorizerFromAuthorizationConfig(authorizationMode string) (authorizer.
// Keep cases in sync with constant list above.
switch authorizationMode {
case ModeAlwaysAllow:
return new(alwaysAllowAuthorizer), nil
return NewAlwaysAllowAuthorizer(), nil
case ModeAlwaysDeny:
return new(alwaysDenyAuthorizer), nil
return NewAlwaysDenyAuthorizer(), nil
default:
return nil, errors.New("Unknown authorization mode")
}

View File

@ -30,10 +30,49 @@ import (
"github.com/golang/glog"
)
// specialVerbs contains just strings which are used in REST paths for special actions that don't fall under the normal
// CRUDdy GET/POST/PUT/DELETE actions on REST objects.
// TODO: find a way to keep this up to date automatically. Maybe dynamically populate list as handlers added to
// master's Mux.
var specialVerbs = map[string]bool{
"proxy": true,
"redirect": true,
"watch": true,
}
// KindFromRequest returns Kind if Kind can be extracted from the request. Otherwise, the empty string.
func KindFromRequest(req http.Request) string {
// TODO: find a way to keep this code's assumptions about paths up to date with changes in the code. Maybe instead
// of directly adding handler's code to the master's Mux, have a function which forces the structure when adding
// them.
parts := splitPath(req.URL.Path)
if len(parts) > 2 && parts[0] == "api" {
if _, ok := specialVerbs[parts[2]]; ok {
if len(parts) > 3 {
return parts[3]
}
} else {
return parts[2]
}
}
return ""
}
// IsReadOnlyReq() is true for any (or at least many) request which has no observable
// side effects on state of apiserver (though there may be internal side effects like
// caching and logging).
func IsReadOnlyReq(req http.Request) bool {
if req.Method == "GET" {
// TODO: add OPTIONS and HEAD if we ever support those.
return true
}
return false
}
// ReadOnly passes all GET requests on to handler, and returns an error on all other requests.
func ReadOnly(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.Method == "GET" {
if IsReadOnlyReq(*req) {
handler.ServeHTTP(w, req)
return
}
@ -143,6 +182,17 @@ func (r *requestAttributeGetter) GetAttribs(req *http.Request) authorizer.Attrib
attribs.User = user
}
attribs.ReadOnly = IsReadOnlyReq(*req)
// If a path follows the conventions of the REST object store, then
// we can extract the object Kind. Otherwise, not.
attribs.Kind = KindFromRequest(*req)
// If the request specifies a namespace, then the namespace is filled in.
// Assumes there is no empty string namespace. Unspecified results
// in empty (does not understand defaulting rules.)
attribs.Namespace = req.URL.Query().Get("namespace")
return &attribs
}

View File

@ -23,7 +23,20 @@ import (
// Attributes is an interface used by an Authorizer to get information about a request
// that is used to make an authorization decision.
type Attributes interface {
// The user string which the request was authenticated as, or empty if
// no authentication occured and the request was allowed to proceed.
GetUserName() string
// TODO: add groups, e.g. GetGroups() []string
// When IsReadOnly() == true, the request has no side effects, other than
// caching, logging, and other incidentals.
IsReadOnly() bool
// The namespace of the object, if a request is for a REST object.
GetNamespace() string
// The kind of object, if a request is for a REST object.
GetKind() string
}
// Authorizer makes an authorization decision based on information gained by making
@ -35,9 +48,24 @@ type Authorizer interface {
// AttributesRecord implements Attributes interface.
type AttributesRecord struct {
User user.Info
User user.Info
ReadOnly bool
Namespace string
Kind string
}
func (a *AttributesRecord) GetUserName() string {
return a.User.GetName()
}
func (a *AttributesRecord) IsReadOnly() bool {
return a.ReadOnly
}
func (a *AttributesRecord) GetNamespace() string {
return a.Namespace
}
func (a *AttributesRecord) GetKind() string {
return a.Kind
}

View File

@ -67,8 +67,7 @@ type Config struct {
APIPrefix string
CorsAllowedOriginList util.StringList
TokenAuthFile string
AuthorizationMode string
AuthorizerForTesting authorizer.Authorizer
Authorizer authorizer.Authorizer
// Number of masters running; all masters must be started with the
// same value for this field. (Numbers > 1 currently untested.)
@ -104,7 +103,7 @@ type Master struct {
apiPrefix string
corsAllowedOriginList util.StringList
tokenAuthFile string
authorizationzMode string
authorizer authorizer.Authorizer
masterCount int
// "Outputs"
@ -227,7 +226,7 @@ func New(c *Config) *Master {
apiPrefix: c.APIPrefix,
corsAllowedOriginList: c.CorsAllowedOriginList,
tokenAuthFile: c.TokenAuthFile,
authorizationzMode: c.AuthorizationMode,
authorizer: c.Authorizer,
masterCount: c.MasterCount,
readOnlyServer: net.JoinHostPort(c.PublicAddress, strconv.Itoa(int(c.ReadOnlyPort))),
@ -319,19 +318,8 @@ func (m *Master) init(c *Config) {
handler = apiserver.CORS(handler, allowedOriginRegexps, nil, nil, "true")
}
// Install Authorizer
var authorizer authorizer.Authorizer
if c.AuthorizerForTesting != nil {
authorizer = c.AuthorizerForTesting
} else {
var err error
authorizer, err = apiserver.NewAuthorizerFromAuthorizationConfig(m.authorizationzMode)
if err != nil {
glog.Fatal(err)
}
}
attributeGetter := apiserver.NewRequestAttributeGetter(userContexts)
handler = apiserver.WithAuthorizationCheck(handler, attributeGetter, authorizer)
handler = apiserver.WithAuthorizationCheck(handler, attributeGetter, m.authorizer)
// Install Authenticator
if authenticator != nil {

View File

@ -32,6 +32,7 @@ import (
"os"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/master"
@ -88,7 +89,7 @@ func TestWhoAmI(t *testing.T) {
EnableUISupport: false,
APIPrefix: "/api",
TokenAuthFile: tokenFilename,
AuthorizationMode: "AlwaysAllow",
Authorizer: apiserver.NewAlwaysAllowAuthorizer(),
})
s := httptest.NewServer(m.Handler)
@ -237,6 +238,7 @@ var aEndpoints string = `
var code200or202 = map[int]bool{200: true, 202: true} // Unpredicatable which will be returned.
var code400 = map[int]bool{400: true}
var code403 = map[int]bool{403: true}
var code404 = map[int]bool{404: true}
var code409 = map[int]bool{409: true}
var code422 = map[int]bool{422: true}
@ -372,7 +374,7 @@ func TestAuthModeAlwaysAllow(t *testing.T) {
EnableLogsSupport: false,
EnableUISupport: false,
APIPrefix: "/api",
AuthorizationMode: "AlwaysAllow",
Authorizer: apiserver.NewAlwaysAllowAuthorizer(),
})
s := httptest.NewServer(m.Handler)
@ -417,7 +419,7 @@ func TestAuthModeAlwaysDeny(t *testing.T) {
EnableLogsSupport: false,
EnableUISupport: false,
APIPrefix: "/api",
AuthorizationMode: "AlwaysDeny",
Authorizer: apiserver.NewAlwaysDenyAuthorizer(),
})
s := httptest.NewServer(m.Handler)
@ -465,8 +467,6 @@ func TestAliceNotForbiddenOrUnauthorized(t *testing.T) {
defer os.Remove(tokenFilename)
// This file has alice and bob in it.
aaa := allowAliceAuthorizer{}
// Set up a master
helper, err := master.NewEtcdHelper(newEtcdClient(), "v1beta1")
@ -475,22 +475,19 @@ func TestAliceNotForbiddenOrUnauthorized(t *testing.T) {
}
m := master.New(&master.Config{
EtcdHelper: helper,
KubeletClient: client.FakeKubeletClient{},
EnableLogsSupport: false,
EnableUISupport: false,
APIPrefix: "/api",
TokenAuthFile: tokenFilename,
AuthorizerForTesting: aaa,
EtcdHelper: helper,
KubeletClient: client.FakeKubeletClient{},
EnableLogsSupport: false,
EnableUISupport: false,
APIPrefix: "/api",
TokenAuthFile: tokenFilename,
Authorizer: allowAliceAuthorizer{},
})
s := httptest.NewServer(m.Handler)
defer s.Close()
transport := http.DefaultTransport
// Alice is authorized.
//
for _, r := range getTestRequests() {
token := AliceToken
t.Logf("case %v", r)
@ -524,8 +521,6 @@ func TestBobIsForbidden(t *testing.T) {
defer os.Remove(tokenFilename)
// This file has alice and bob in it.
aaa := allowAliceAuthorizer{}
// Set up a master
helper, err := master.NewEtcdHelper(newEtcdClient(), "v1beta1")
@ -534,22 +529,19 @@ func TestBobIsForbidden(t *testing.T) {
}
m := master.New(&master.Config{
EtcdHelper: helper,
KubeletClient: client.FakeKubeletClient{},
EnableLogsSupport: false,
EnableUISupport: false,
APIPrefix: "/api",
TokenAuthFile: tokenFilename,
AuthorizerForTesting: aaa,
EtcdHelper: helper,
KubeletClient: client.FakeKubeletClient{},
EnableLogsSupport: false,
EnableUISupport: false,
APIPrefix: "/api",
TokenAuthFile: tokenFilename,
Authorizer: allowAliceAuthorizer{},
})
s := httptest.NewServer(m.Handler)
defer s.Close()
transport := http.DefaultTransport
// Alice is authorized.
//
for _, r := range getTestRequests() {
token := BobToken
t.Logf("case %v", r)
@ -585,8 +577,6 @@ func TestUnknownUserIsUnauthorized(t *testing.T) {
defer os.Remove(tokenFilename)
// This file has alice and bob in it.
aaa := allowAliceAuthorizer{}
// Set up a master
helper, err := master.NewEtcdHelper(newEtcdClient(), "v1beta1")
@ -595,13 +585,13 @@ func TestUnknownUserIsUnauthorized(t *testing.T) {
}
m := master.New(&master.Config{
EtcdHelper: helper,
KubeletClient: client.FakeKubeletClient{},
EnableLogsSupport: false,
EnableUISupport: false,
APIPrefix: "/api",
TokenAuthFile: tokenFilename,
AuthorizerForTesting: aaa,
EtcdHelper: helper,
KubeletClient: client.FakeKubeletClient{},
EnableLogsSupport: false,
EnableUISupport: false,
APIPrefix: "/api",
TokenAuthFile: tokenFilename,
Authorizer: allowAliceAuthorizer{},
})
s := httptest.NewServer(m.Handler)
@ -625,7 +615,248 @@ func TestUnknownUserIsUnauthorized(t *testing.T) {
}
// Expect all of unauthenticated user's request to be "Unauthorized"
if resp.StatusCode != http.StatusUnauthorized {
t.Errorf("Expected status Unauthorized, but got %s", resp.Status)
t.Errorf("Expected status %v, but got %v", http.StatusUnauthorized, resp.StatusCode)
b, _ := ioutil.ReadAll(resp.Body)
t.Errorf("Body: %v", string(b))
}
}
}
}
// Inject into master an authorizer that uses namespace information.
// TODO(etune): remove this test once a more comprehensive built-in authorizer is implemented.
type allowFooNamespaceAuthorizer struct{}
func (allowFooNamespaceAuthorizer) Authorize(a authorizer.Attributes) error {
if a.GetNamespace() == "foo" {
return nil
}
return errors.New("I can't allow that. Try another namespace, buddy.")
}
// TestNamespaceAuthorization tests that authorization can be controlled
// by namespace.
func TestNamespaceAuthorization(t *testing.T) {
deleteAllEtcdKeys()
tokenFilename := writeTestTokenFile()
defer os.Remove(tokenFilename)
// This file has alice and bob in it.
// 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,
KubeletClient: client.FakeKubeletClient{},
EnableLogsSupport: false,
EnableUISupport: false,
APIPrefix: "/api",
TokenAuthFile: tokenFilename,
Authorizer: allowFooNamespaceAuthorizer{},
})
s := httptest.NewServer(m.Handler)
defer s.Close()
transport := http.DefaultTransport
requests := []struct {
verb string
URL string
body string
statusCodes map[int]bool // allowed status codes.
}{
{"POST", "/api/v1beta1/pods?namespace=foo", aPod, code200or202},
{"GET", "/api/v1beta1/pods?namespace=foo", "", code200or202},
{"GET", "/api/v1beta1/pods/a?namespace=foo", "", code200or202},
{"DELETE", "/api/v1beta1/pods/a?namespace=foo", "", code200or202},
{"POST", "/api/v1beta1/pods?namespace=bar", aPod, code403},
{"GET", "/api/v1beta1/pods?namespace=bar", "", code403},
{"GET", "/api/v1beta1/pods/a?namespace=bar", "", code403},
{"DELETE", "/api/v1beta1/pods/a?namespace=bar", "", code403},
{"POST", "/api/v1beta1/pods", aPod, code403},
{"GET", "/api/v1beta1/pods", "", code403},
{"GET", "/api/v1beta1/pods/a", "", code403},
{"DELETE", "/api/v1beta1/pods/a", "", code403},
}
for _, r := range requests {
token := BobToken
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)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
{
resp, err := transport.RoundTrip(req)
defer resp.Body.Close()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if _, ok := r.statusCodes[resp.StatusCode]; !ok {
t.Errorf("Expected status one of %v, but got %v", r.statusCodes, resp.StatusCode)
}
}
}
}
// Inject into master an authorizer that uses kind information.
// TODO(etune): remove this test once a more comprehensive built-in authorizer is implemented.
type allowServicesAuthorizer struct{}
func (allowServicesAuthorizer) Authorize(a authorizer.Attributes) error {
if a.GetKind() == "services" {
return nil
}
return errors.New("I can't allow that. Hint: try services.")
}
// TestKindAuthorization tests that authorization can be controlled
// by namespace.
func TestKindAuthorization(t *testing.T) {
deleteAllEtcdKeys()
tokenFilename := writeTestTokenFile()
defer os.Remove(tokenFilename)
// This file has alice and bob in it.
// 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,
KubeletClient: client.FakeKubeletClient{},
EnableLogsSupport: false,
EnableUISupport: false,
APIPrefix: "/api",
TokenAuthFile: tokenFilename,
Authorizer: allowServicesAuthorizer{},
})
s := httptest.NewServer(m.Handler)
defer s.Close()
transport := http.DefaultTransport
requests := []struct {
verb string
URL string
body string
statusCodes map[int]bool // allowed status codes.
}{
{"POST", "/api/v1beta1/services", aService, code200or202},
{"GET", "/api/v1beta1/services", "", code200or202},
{"GET", "/api/v1beta1/services/a", "", code200or202},
{"DELETE", "/api/v1beta1/services/a", "", code200or202},
{"POST", "/api/v1beta1/pods", aPod, code403},
{"GET", "/api/v1beta1/pods", "", code403},
{"GET", "/api/v1beta1/pods/a", "", code403},
{"DELETE", "/api/v1beta1/pods/a", "", code403},
}
for _, r := range requests {
token := BobToken
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)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
{
resp, err := transport.RoundTrip(req)
defer resp.Body.Close()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if _, ok := r.statusCodes[resp.StatusCode]; !ok {
t.Errorf("Expected status one of %v, but got %v", r.statusCodes, resp.StatusCode)
}
}
}
}
// Inject into master an authorizer that uses ReadOnly information.
// TODO(etune): remove this test once a more comprehensive built-in authorizer is implemented.
type allowReadAuthorizer struct{}
func (allowReadAuthorizer) Authorize(a authorizer.Attributes) error {
if a.IsReadOnly() {
return nil
}
return errors.New("I'm afraid I can't let you do that.")
}
// TestReadOnlyAuthorization tests that authorization can be controlled
// by namespace.
func TestReadOnlyAuthorization(t *testing.T) {
deleteAllEtcdKeys()
tokenFilename := writeTestTokenFile()
defer os.Remove(tokenFilename)
// This file has alice and bob in it.
// 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,
KubeletClient: client.FakeKubeletClient{},
EnableLogsSupport: false,
EnableUISupport: false,
APIPrefix: "/api",
TokenAuthFile: tokenFilename,
Authorizer: allowReadAuthorizer{},
})
s := httptest.NewServer(m.Handler)
defer s.Close()
transport := http.DefaultTransport
requests := []struct {
verb string
URL string
body string
statusCodes map[int]bool // allowed status codes.
}{
{"POST", "/api/v1beta1/pods", aPod, code403},
{"GET", "/api/v1beta1/pods", "", code200or202},
{"GET", "/api/v1beta1/pods/a", "", code404},
}
for _, r := range requests {
token := BobToken
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)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
{
resp, err := transport.RoundTrip(req)
defer resp.Body.Close()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if _, ok := r.statusCodes[resp.StatusCode]; !ok {
t.Errorf("Expected status one of %v, but got %v", r.statusCodes, resp.StatusCode)
}
}
}

View File

@ -24,6 +24,7 @@ import (
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/master"
@ -45,7 +46,7 @@ func TestClient(t *testing.T) {
EnableLogsSupport: false,
EnableUISupport: false,
APIPrefix: "/api",
AuthorizationMode: "AlwaysAllow",
Authorizer: apiserver.NewAlwaysAllowAuthorizer(),
})
s := httptest.NewServer(m.Handler)