Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions pkg/authorization/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import (

const (
// Policy is a singleton and this is its name
PolicyName = "default"
ResourceAll = "*"
VerbAll = "*"
PolicyName = "default"
ResourceAll = "*"
VerbAll = "*"
NonResourceAll = "*"
)

const (
Expand Down Expand Up @@ -77,6 +78,9 @@ type PolicyRule struct {
Resources []string
// ResourceNames is an optional white list of names that the rule applies to. An empty set means that everything is allowed.
ResourceNames kutil.StringSet
// NonResourceURLs is a set of partial urls that a user should have access to. *s are allowed, but only as the full, final step in the path
// If an action is not a resource API request, then the URL is split on '/' and is checked against the NonResourceURLs to look for a match.
NonResourceURLs kutil.StringSet
}

// Role is a logical grouping of PolicyRules that can be referenced as a unit by RoleBindings.
Expand Down
8 changes: 6 additions & 2 deletions pkg/authorization/api/v1beta1/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ package v1beta1
import (
"sort"

"github.com/GoogleCloudPlatform/kubernetes/pkg/conversion"

"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/conversion"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"

newer "github.com/openshift/origin/pkg/authorization/api"
)

Expand All @@ -26,6 +26,8 @@ func init() {

out.ResourceNames = util.NewStringSet(in.ResourceNames...)

out.NonResourceURLs = util.NewStringSet(in.NonResourceURLsSlice...)

return nil
},
func(in *newer.PolicyRule, out *PolicyRule, s conversion.Scope) error {
Expand All @@ -41,6 +43,8 @@ func init() {

out.ResourceNames = in.ResourceNames.List()

out.NonResourceURLsSlice = in.NonResourceURLs.List()

return nil
},
func(in *Policy, out *newer.Policy, s conversion.Scope) error {
Expand Down
3 changes: 3 additions & 0 deletions pkg/authorization/api/v1beta1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ type PolicyRule struct {
Resources []string `json:"resources"`
// ResourceNames is an optional white list of names that the rule applies to. An empty set means that everything is allowed.
ResourceNames []string `json:"resourceNames,omitempty"`
// NonResourceURLsSlice is a set of partial urls that a user should have access to. *s are allowed, but only as the full, final step in the path
// This name is intentionally different than the internal type so that the DefaultConvert works nicely and because the ordering may be different.
NonResourceURLsSlice []string `json:"nonResourceURLs,omitempty"`
}

// Role is a logical grouping of PolicyRules that can be referenced as a unit by RoleBindings.
Expand Down
127 changes: 116 additions & 11 deletions pkg/authorization/authorizer/authorizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net/http"
"path"
"strings"

kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
Expand All @@ -28,13 +29,22 @@ type AuthorizationAttributeBuilder interface {
}

type AuthorizationAttributes interface {
// GetUserInfo returns the user requesting the action
GetUserInfo() user.Info
// GetVerb returns the verb associated with the action. For resource requests, this verb is the logical kube verb. For non-resource requests it is the http method tolowered.
GetVerb() string
// GetResource returns the resource type. If IsNonResourceURL() is true, then GetResource() is "".
GetResource() string
// GetNamespace returns the namespace of a resource request. If IsNonResourceURL() is true, then GetNamespace() is "".
GetNamespace() string
// GetResourceName returns the name of the resource being acted upon. Not all resource actions have one (list as a for instance). If IsNonResourceURL() is true, then GetResourceName() is "".
GetResourceName() string
// GetRequestAttributes is of type interface{} because different verbs and different Authorizer/AuthorizationAttributeBuilder pairs may have different contract requirements
// GetRequestAttributes is of type interface{} because different verbs and different Authorizer/AuthorizationAttributeBuilder pairs may have different contract requirements.
GetRequestAttributes() interface{}
// IsNonResourceURL returns true if this is not an action performed against the resource API
IsNonResourceURL() bool
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Godoc

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also note in the doc for namespace/resource/resourcename that they will be "" when IsNonResourceURL is true

// GetURL returns the URL split on '/'s
GetURL() string
}

type openshiftAuthorizer struct {
Expand All @@ -54,6 +64,8 @@ type DefaultAuthorizationAttributes struct {
ResourceName string
Namespace string
RequestAttributes interface{}
NonResourceURL bool
URL string
}

type openshiftAuthorizationAttributeBuilder struct {
Expand Down Expand Up @@ -289,16 +301,28 @@ func coerceToDefaultAuthorizationAttributes(passedAttributes AuthorizationAttrib
Resource: passedAttributes.GetResource(),
ResourceName: passedAttributes.GetResourceName(),
User: passedAttributes.GetUserInfo(),
NonResourceURL: passedAttributes.IsNonResourceURL(),
URL: passedAttributes.GetURL(),
}
}

return attributes
}

func (a DefaultAuthorizationAttributes) RuleMatches(rule authorizationapi.PolicyRule) (bool, error) {
allowedResourceTypes := resolveResources(rule)
if a.IsNonResourceURL() {
if a.nonResourceMatches(rule) {
if a.verbMatches(util.NewStringSet(rule.Verbs...)) {
return true, nil
}
}

return false, nil
}

if a.verbMatches(util.NewStringSet(rule.Verbs...)) {
allowedResourceTypes := resolveResources(rule)

if a.resourceMatches(allowedResourceTypes) {
if a.nameMatches(rule.ResourceNames) {
return true, nil
Expand Down Expand Up @@ -360,6 +384,35 @@ func (a DefaultAuthorizationAttributes) GetUserInfo() user.Info {
func (a DefaultAuthorizationAttributes) GetVerb() string {
return a.Verb
}

// nonResourceMatches take the remainer of a URL and attempts to match it against a series of explicitly allowed steps that can end in a wildcard
func (a DefaultAuthorizationAttributes) nonResourceMatches(rule authorizationapi.PolicyRule) bool {
for allowedNonResourcePath := range rule.NonResourceURLs {
// if the allowed resource path ends in a wildcard, check to see if the URL starts with it
if strings.HasSuffix(allowedNonResourcePath, "*") {
if strings.HasPrefix(a.GetURL(), allowedNonResourcePath[0:len(allowedNonResourcePath)-1]) {
return true
}
}

// if we have an exact match, return true
if a.GetURL() == allowedNonResourcePath {
return true
}
}

return false
}

// splitPath returns the segments for a URL path.
func splitPath(thePath string) []string {
thePath = strings.Trim(path.Clean(thePath), "/")
if thePath == "" {
return []string{}
}
return strings.Split(thePath, "/")
}

func (a DefaultAuthorizationAttributes) GetResource() string {
return a.Resource
}
Expand All @@ -375,16 +428,15 @@ func (a DefaultAuthorizationAttributes) GetRequestAttributes() interface{} {
return a.RequestAttributes
}

func (a *openshiftAuthorizationAttributeBuilder) GetAttributes(req *http.Request) (AuthorizationAttributes, error) {
requestInfo, err := a.infoResolver.GetAPIRequestInfo(req)
if err != nil {
return nil, err
}
func (a DefaultAuthorizationAttributes) IsNonResourceURL() bool {
return a.NonResourceURL
}

if (requestInfo.Resource == "projects") && (len(requestInfo.Name) > 0) {
requestInfo.Namespace = requestInfo.Name
}
func (a DefaultAuthorizationAttributes) GetURL() string {
return a.URL
}

func (a *openshiftAuthorizationAttributeBuilder) GetAttributes(req *http.Request) (AuthorizationAttributes, error) {
ctx, ok := a.contextMapper.Get(req)
if !ok {
return nil, errors.New("could not get request context")
Expand All @@ -394,17 +446,43 @@ func (a *openshiftAuthorizationAttributeBuilder) GetAttributes(req *http.Request
return nil, errors.New("could not get user")
}

// any url that starts with an API prefix and is more than one step long is considered to be a resource URL.
// That means that /api is non-resource, /api/v1beta1 is resource, /healthz is non-resource, and /swagger/anything is non-resource
urlSegments := splitPath(req.URL.Path)
isResourceURL := (len(urlSegments) > 1) && a.infoResolver.APIPrefixes.Has(urlSegments[0])

if !isResourceURL {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much clearer, thanks

return DefaultAuthorizationAttributes{
User: userInfo,
Verb: strings.ToLower(req.Method),
NonResourceURL: true,
URL: req.URL.Path,
}, nil
}

requestInfo, err := a.infoResolver.GetAPIRequestInfo(req)
if err != nil {
return nil, err
}

// TODO reconsider special casing this. Having the special case hereallow us to fully share the kube
// APIRequestInfoResolver without any modification or customization.
if (requestInfo.Resource == "projects") && (len(requestInfo.Name) > 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Special case needed forever? Add todo

requestInfo.Namespace = requestInfo.Name
}

return DefaultAuthorizationAttributes{
User: userInfo,
Verb: requestInfo.Verb,
Resource: requestInfo.Resource,
ResourceName: requestInfo.Name,
Namespace: requestInfo.Namespace,
RequestAttributes: nil,
NonResourceURL: false,
URL: req.URL.Path,
}, nil
}

// TODO enumerate all resources and verbs instead of using *
func GetBootstrapPolicy(masterNamespace string) *authorizationapi.Policy {
return &authorizationapi.Policy{
ObjectMeta: kapi.ObjectMeta{
Expand All @@ -424,6 +502,10 @@ func GetBootstrapPolicy(masterNamespace string) *authorizationapi.Policy {
Verbs: []string{authorizationapi.VerbAll},
Resources: []string{authorizationapi.ResourceAll},
},
{
Verbs: []string{authorizationapi.VerbAll},
NonResourceURLs: util.NewStringSet(authorizationapi.NonResourceAll),
},
},
},
"admin": {
Expand Down Expand Up @@ -480,6 +562,18 @@ func GetBootstrapPolicy(masterNamespace string) *authorizationapi.Policy {
{Verbs: []string{"list"}, Resources: []string{"projects"}},
},
},
"cluster-status": {
ObjectMeta: kapi.ObjectMeta{
Name: "cluster-status",
Namespace: masterNamespace,
},
Rules: []authorizationapi.PolicyRule{
{
Verbs: []string{"get"},
NonResourceURLs: util.NewStringSet("/healthz", "/version", "/api", "/osapi"),
},
},
},
"system:deployer": {
ObjectMeta: kapi.ObjectMeta{
Name: "system:deployer",
Expand Down Expand Up @@ -597,6 +691,17 @@ func GetBootstrapPolicyBinding(masterNamespace string) *authorizationapi.PolicyB
},
GroupNames: []string{"system:authenticated", "system:unauthenticated"},
},
"cluster-status-binding": {
ObjectMeta: kapi.ObjectMeta{
Name: "cluster-status-binding",
Namespace: masterNamespace,
},
RoleRef: kapi.ObjectReference{
Name: "cluster-status",
Namespace: masterNamespace,
},
GroupNames: []string{"system:authenticated", "system:unauthenticated"},
},
},
}
}
Loading