Unverified Commit d3307752 authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #53273 from mikedanese/authtristate

Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. add support for short-circuit deny in union authorizer This change has no behavioral changes. Fixes https://github.com/kubernetes/kubernetes/issues/51862 ```release-note Add support for the webhook authorizer to make a Deny decision that short-circuits the union authorizer and immediately returns Deny. ```
parents ef8746af 90d551a0
......@@ -69539,7 +69539,11 @@
],
"properties": {
"allowed": {
"description": "Allowed is required. True if the action would be allowed, false otherwise.",
"description": "Allowed is required. True if the action would be allowed, false otherwise.",
"type": "boolean"
},
"denied": {
"description": "Denied is optional. True if the action would be denied, otherwise false. If both allowed is false and denied is false, then the authorizer has no opinion on whether to authorize the action. Denied may not be true if Allowed is true.",
"type": "boolean"
},
"evaluationError": {
......@@ -69890,7 +69894,11 @@
],
"properties": {
"allowed": {
"description": "Allowed is required. True if the action would be allowed, false otherwise.",
"description": "Allowed is required. True if the action would be allowed, false otherwise.",
"type": "boolean"
},
"denied": {
"description": "Denied is optional. True if the action would be denied, otherwise false. If both allowed is false and denied is false, then the authorizer has no opinion on whether to authorize the action. Denied may not be true if Allowed is true.",
"type": "boolean"
},
"evaluationError": {
......@@ -635,7 +635,11 @@
"properties": {
"allowed": {
"type": "boolean",
"description": "Allowed is required. True if the action would be allowed, false otherwise."
"description": "Allowed is required. True if the action would be allowed, false otherwise."
},
"denied": {
"type": "boolean",
"description": "Denied is optional. True if the action would be denied, otherwise false. If both allowed is false and denied is false, then the authorizer has no opinion on whether to authorize the action. Denied may not be true if Allowed is true."
},
"reason": {
"type": "string",
......
......@@ -635,7 +635,11 @@
"properties": {
"allowed": {
"type": "boolean",
"description": "Allowed is required. True if the action would be allowed, false otherwise."
"description": "Allowed is required. True if the action would be allowed, false otherwise."
},
"denied": {
"type": "boolean",
"description": "Denied is optional. True if the action would be denied, otherwise false. If both allowed is false and denied is false, then the authorizer has no opinion on whether to authorize the action. Denied may not be true if Allowed is true."
},
"reason": {
"type": "string",
......
......@@ -850,12 +850,19 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; }
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">allowed</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Allowed is required. True if the action would be allowed, false otherwise.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Allowed is required. True if the action would be allowed, false otherwise.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">true</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">boolean</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">false</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">denied</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Denied is optional. True if the action would be denied, otherwise false. If both allowed is false and denied is false, then the authorizer has no opinion on whether to authorize the action. Denied may not be true if Allowed is true.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">false</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">boolean</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">false</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">reason</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Reason is optional. It indicates why a request was allowed or denied.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">false</p></td>
......
......@@ -1094,12 +1094,19 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; }
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">allowed</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Allowed is required. True if the action would be allowed, false otherwise.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Allowed is required. True if the action would be allowed, false otherwise.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">true</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">boolean</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">false</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">denied</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Denied is optional. True if the action would be denied, otherwise false. If both allowed is false and denied is false, then the authorizer has no opinion on whether to authorize the action. Denied may not be true if Allowed is true.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">false</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">boolean</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">false</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">reason</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Reason is optional. It indicates why a request was allowed or denied.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">false</p></td>
......
......@@ -140,8 +140,13 @@ type SelfSubjectAccessReviewSpec struct {
// SubjectAccessReviewStatus
type SubjectAccessReviewStatus struct {
// Allowed is required. True if the action would be allowed, false otherwise.
// Allowed is required. True if the action would be allowed, false otherwise.
Allowed bool
// Denied is optional. True if the action would be denied, otherwise
// false. If both allowed is false and denied is false, then the
// authorizer has no opinion on whether to authorize the action. Denied
// may not be true if Allowed is true.
Denied bool
// Reason is optional. It indicates why a request was allowed or denied.
Reason string
// EvaluationError is an indication that some error occurred during the authorization check.
......
......@@ -369,6 +369,7 @@ func Convert_authorization_SubjectAccessReviewSpec_To_v1_SubjectAccessReviewSpec
func autoConvert_v1_SubjectAccessReviewStatus_To_authorization_SubjectAccessReviewStatus(in *v1.SubjectAccessReviewStatus, out *authorization.SubjectAccessReviewStatus, s conversion.Scope) error {
out.Allowed = in.Allowed
out.Denied = in.Denied
out.Reason = in.Reason
out.EvaluationError = in.EvaluationError
return nil
......@@ -381,6 +382,7 @@ func Convert_v1_SubjectAccessReviewStatus_To_authorization_SubjectAccessReviewSt
func autoConvert_authorization_SubjectAccessReviewStatus_To_v1_SubjectAccessReviewStatus(in *authorization.SubjectAccessReviewStatus, out *v1.SubjectAccessReviewStatus, s conversion.Scope) error {
out.Allowed = in.Allowed
out.Denied = in.Denied
out.Reason = in.Reason
out.EvaluationError = in.EvaluationError
return nil
......
......@@ -369,6 +369,7 @@ func Convert_authorization_SubjectAccessReviewSpec_To_v1beta1_SubjectAccessRevie
func autoConvert_v1beta1_SubjectAccessReviewStatus_To_authorization_SubjectAccessReviewStatus(in *v1beta1.SubjectAccessReviewStatus, out *authorization.SubjectAccessReviewStatus, s conversion.Scope) error {
out.Allowed = in.Allowed
out.Denied = in.Denied
out.Reason = in.Reason
out.EvaluationError = in.EvaluationError
return nil
......@@ -381,6 +382,7 @@ func Convert_v1beta1_SubjectAccessReviewStatus_To_authorization_SubjectAccessRev
func autoConvert_authorization_SubjectAccessReviewStatus_To_v1beta1_SubjectAccessReviewStatus(in *authorization.SubjectAccessReviewStatus, out *v1beta1.SubjectAccessReviewStatus, s conversion.Scope) error {
out.Allowed = in.Allowed
out.Denied = in.Denied
out.Reason = in.Reason
out.EvaluationError = in.EvaluationError
return nil
......
......@@ -221,13 +221,13 @@ func resourceMatches(p abac.Policy, a authorizer.Attributes) bool {
}
// Authorizer implements authorizer.Authorize
func (pl policyList) Authorize(a authorizer.Attributes) (bool, string, error) {
func (pl policyList) Authorize(a authorizer.Attributes) (authorizer.Decision, string, error) {
for _, p := range pl {
if matches(*p, a) {
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
}
return false, "No policy matched.", nil
return authorizer.DecisionNoOpinion, "No policy matched.", nil
// 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.
......
......@@ -236,14 +236,14 @@ func (s *Server) InstallAuthFilter() {
attrs := s.auth.GetRequestAttributes(u, req.Request)
// Authorize
authorized, _, err := s.auth.Authorize(attrs)
decision, _, err := s.auth.Authorize(attrs)
if err != nil {
msg := fmt.Sprintf("Authorization error (user=%s, verb=%s, resource=%s, subresource=%s)", u.GetName(), attrs.GetVerb(), attrs.GetResource(), attrs.GetSubresource())
glog.Errorf(msg, err)
resp.WriteErrorString(http.StatusInternalServerError, msg)
return
}
if !authorized {
if decision != authorizer.DecisionAllow {
msg := fmt.Sprintf("Forbidden (user=%s, verb=%s, resource=%s, subresource=%s)", u.GetName(), attrs.GetVerb(), attrs.GetResource(), attrs.GetSubresource())
glog.V(2).Info(msg)
resp.WriteErrorString(http.StatusForbidden, msg)
......
......@@ -183,7 +183,7 @@ func (_ *fakeKubelet) GetCgroupStats(cgroupName string) (*statsapi.ContainerStat
type fakeAuth struct {
authenticateFunc func(*http.Request) (user.Info, bool, error)
attributesFunc func(user.Info, *http.Request) authorizer.Attributes
authorizeFunc func(authorizer.Attributes) (authorized bool, reason string, err error)
authorizeFunc func(authorizer.Attributes) (authorized authorizer.Decision, reason string, err error)
}
func (f *fakeAuth) AuthenticateRequest(req *http.Request) (user.Info, bool, error) {
......@@ -192,7 +192,7 @@ func (f *fakeAuth) AuthenticateRequest(req *http.Request) (user.Info, bool, erro
func (f *fakeAuth) GetRequestAttributes(u user.Info, req *http.Request) authorizer.Attributes {
return f.attributesFunc(u, req)
}
func (f *fakeAuth) Authorize(a authorizer.Attributes) (authorized bool, reason string, err error) {
func (f *fakeAuth) Authorize(a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
return f.authorizeFunc(a)
}
......@@ -232,8 +232,8 @@ func newServerTestWithDebug(enableDebugging bool) *serverTestFramework {
attributesFunc: func(u user.Info, req *http.Request) authorizer.Attributes {
return &authorizer.AttributesRecord{User: u}
},
authorizeFunc: func(a authorizer.Attributes) (authorized bool, reason string, err error) {
return true, "", nil
authorizeFunc: func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
return authorizer.DecisionAllow, "", nil
},
}
fw.criHandler = &utiltesting.FakeHandler{
......@@ -688,12 +688,12 @@ Otherwise, add it to the expected list of paths that map to the "proxy" subresou
}
return attributesGetter.GetRequestAttributes(u, req)
}
fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (authorized bool, reason string, err error) {
fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
calledAuthorize = true
if a != expectedAttributes {
t.Fatalf("%s: expected attributes\n\t%#v\ngot\n\t%#v", tc.Path, expectedAttributes, a)
}
return false, "", nil
return authorizer.DecisionNoOpinion, "", nil
}
req, err := http.NewRequest(tc.Method, fw.testHTTPServer.URL+tc.Path, nil)
......@@ -747,9 +747,9 @@ func TestAuthenticationError(t *testing.T) {
calledAttributes = true
return expectedAttributes
}
fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (authorized bool, reason string, err error) {
fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
calledAuthorize = true
return false, "", errors.New("Failed")
return authorizer.DecisionNoOpinion, "", errors.New("Failed")
}
assertHealthFails(t, fw.testHTTPServer.URL+"/healthz", http.StatusInternalServerError)
......@@ -785,9 +785,9 @@ func TestAuthenticationFailure(t *testing.T) {
calledAttributes = true
return expectedAttributes
}
fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (authorized bool, reason string, err error) {
fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
calledAuthorize = true
return false, "", nil
return authorizer.DecisionNoOpinion, "", nil
}
assertHealthFails(t, fw.testHTTPServer.URL+"/healthz", http.StatusUnauthorized)
......@@ -823,9 +823,9 @@ func TestAuthorizationSuccess(t *testing.T) {
calledAttributes = true
return expectedAttributes
}
fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (authorized bool, reason string, err error) {
fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
calledAuthorize = true
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
assertHealthIsOk(t, fw.testHTTPServer.URL+"/healthz")
......
......@@ -58,10 +58,11 @@ func (r *REST) Create(ctx genericapirequest.Context, obj runtime.Object, createV
}
authorizationAttributes := authorizationutil.AuthorizationAttributesFrom(localSubjectAccessReview.Spec)
allowed, reason, evaluationErr := r.authorizer.Authorize(authorizationAttributes)
decision, reason, evaluationErr := r.authorizer.Authorize(authorizationAttributes)
localSubjectAccessReview.Status = authorizationapi.SubjectAccessReviewStatus{
Allowed: allowed,
Allowed: (decision == authorizer.DecisionAllow),
Denied: (decision == authorizer.DecisionDeny),
Reason: reason,
}
if evaluationErr != nil {
......
......@@ -61,10 +61,11 @@ func (r *REST) Create(ctx genericapirequest.Context, obj runtime.Object, createV
authorizationAttributes = authorizationutil.NonResourceAttributesFrom(userToCheck, *selfSAR.Spec.NonResourceAttributes)
}
allowed, reason, evaluationErr := r.authorizer.Authorize(authorizationAttributes)
decision, reason, evaluationErr := r.authorizer.Authorize(authorizationAttributes)
selfSAR.Status = authorizationapi.SubjectAccessReviewStatus{
Allowed: allowed,
Allowed: (decision == authorizer.DecisionAllow),
Denied: (decision == authorizer.DecisionDeny),
Reason: reason,
}
if evaluationErr != nil {
......
......@@ -51,10 +51,11 @@ func (r *REST) Create(ctx genericapirequest.Context, obj runtime.Object, createV
}
authorizationAttributes := authorizationutil.AuthorizationAttributesFrom(subjectAccessReview.Spec)
allowed, reason, evaluationErr := r.authorizer.Authorize(authorizationAttributes)
decision, reason, evaluationErr := r.authorizer.Authorize(authorizationAttributes)
subjectAccessReview.Status = authorizationapi.SubjectAccessReviewStatus{
Allowed: allowed,
Allowed: (decision == authorizer.DecisionAllow),
Denied: (decision == authorizer.DecisionDeny),
Reason: reason,
}
if evaluationErr != nil {
......
......@@ -33,22 +33,22 @@ import (
type fakeAuthorizer struct {
attrs authorizer.Attributes
ok bool
reason string
err error
decision authorizer.Decision
reason string
err error
}
func (f *fakeAuthorizer) Authorize(attrs authorizer.Attributes) (bool, string, error) {
func (f *fakeAuthorizer) Authorize(attrs authorizer.Attributes) (authorizer.Decision, string, error) {
f.attrs = attrs
return f.ok, f.reason, f.err
return f.decision, f.reason, f.err
}
func TestCreate(t *testing.T) {
testcases := map[string]struct {
spec authorizationapi.SubjectAccessReviewSpec
ok bool
reason string
err error
spec authorizationapi.SubjectAccessReviewSpec
decision authorizer.Decision
reason string
err error
expectedErr string
expectedAttrs authorizer.Attributes
......@@ -63,9 +63,9 @@ func TestCreate(t *testing.T) {
User: "bob",
NonResourceAttributes: &authorizationapi.NonResourceAttributes{Verb: "get", Path: "/mypath"},
},
ok: false,
reason: "myreason",
err: errors.New("myerror"),
decision: authorizer.DecisionNoOpinion,
reason: "myreason",
err: errors.New("myerror"),
expectedAttrs: authorizer.AttributesRecord{
User: &user.DefaultInfo{Name: "bob"},
Verb: "get",
......@@ -84,9 +84,9 @@ func TestCreate(t *testing.T) {
User: "bob",
NonResourceAttributes: &authorizationapi.NonResourceAttributes{Verb: "get", Path: "/mypath"},
},
ok: true,
reason: "allowed",
err: nil,
decision: authorizer.DecisionAllow,
reason: "allowed",
err: nil,
expectedAttrs: authorizer.AttributesRecord{
User: &user.DefaultInfo{Name: "bob"},
Verb: "get",
......@@ -113,9 +113,9 @@ func TestCreate(t *testing.T) {
Name: "mydeployment",
},
},
ok: false,
reason: "myreason",
err: errors.New("myerror"),
decision: authorizer.DecisionNoOpinion,
reason: "myreason",
err: errors.New("myerror"),
expectedAttrs: authorizer.AttributesRecord{
User: &user.DefaultInfo{Name: "bob"},
Namespace: "myns",
......@@ -129,6 +129,7 @@ func TestCreate(t *testing.T) {
},
expectedStatus: authorizationapi.SubjectAccessReviewStatus{
Allowed: false,
Denied: false,
Reason: "myreason",
EvaluationError: "myerror",
},
......@@ -147,9 +148,9 @@ func TestCreate(t *testing.T) {
Name: "mydeployment",
},
},
ok: true,
reason: "allowed",
err: nil,
decision: authorizer.DecisionAllow,
reason: "allowed",
err: nil,
expectedAttrs: authorizer.AttributesRecord{
User: &user.DefaultInfo{Name: "bob"},
Namespace: "myns",
......@@ -163,17 +164,34 @@ func TestCreate(t *testing.T) {
},
expectedStatus: authorizationapi.SubjectAccessReviewStatus{
Allowed: true,
Denied: false,
Reason: "allowed",
EvaluationError: "",
},
},
"resource denied": {
spec: authorizationapi.SubjectAccessReviewSpec{
User: "bob",
ResourceAttributes: &authorizationapi.ResourceAttributes{},
},
decision: authorizer.DecisionDeny,
expectedAttrs: authorizer.AttributesRecord{
User: &user.DefaultInfo{Name: "bob"},
ResourceRequest: true,
},
expectedStatus: authorizationapi.SubjectAccessReviewStatus{
Allowed: false,
Denied: true,
},
},
}
for k, tc := range testcases {
auth := &fakeAuthorizer{
ok: tc.ok,
reason: tc.reason,
err: tc.err,
decision: tc.decision,
reason: tc.reason,
err: tc.err,
}
storage := NewREST(auth)
......
......@@ -79,12 +79,12 @@ func BindingAuthorized(ctx genericapirequest.Context, roleRef rbac.RoleRef, bind
return false
}
ok, _, err := a.Authorize(attrs)
decision, _, err := a.Authorize(attrs)
if err != nil {
utilruntime.HandleError(fmt.Errorf(
"error authorizing user %#v to bind %#v in namespace %s: %v",
user, roleRef, bindingNamespace, err,
))
}
return ok
return decision == authorizer.DecisionAllow
}
......@@ -104,8 +104,8 @@ func (a *gcPermissionsEnforcement) Validate(attributes admission.Attributes) (er
ResourceRequest: true,
Path: "",
}
allowed, reason, err := a.authorizer.Authorize(deleteAttributes)
if !allowed {
decision, reason, err := a.authorizer.Authorize(deleteAttributes)
if decision != authorizer.DecisionAllow {
return admission.NewForbidden(attributes, fmt.Errorf("cannot set an ownerRef on a resource you can't delete: %v, %v", reason, err))
}
......@@ -122,8 +122,8 @@ func (a *gcPermissionsEnforcement) Validate(attributes admission.Attributes) (er
// resources. User needs to have delete permission on all the
// matched Resources.
for _, record := range records {
allowed, reason, err := a.authorizer.Authorize(record)
if !allowed {
decision, reason, err := a.authorizer.Authorize(record)
if decision != authorizer.DecisionAllow {
return admission.NewForbidden(attributes, fmt.Errorf("cannot set blockOwnerDeletion if an ownerReference refers to a resource you can't set finalizers on: %v, %v", reason, err))
}
}
......
......@@ -34,40 +34,40 @@ import (
type fakeAuthorizer struct{}
func (fakeAuthorizer) Authorize(a authorizer.Attributes) (bool, string, error) {
func (fakeAuthorizer) Authorize(a authorizer.Attributes) (authorizer.Decision, string, error) {
username := a.GetUser().GetName()
if username == "non-deleter" {
if a.GetVerb() == "delete" {
return false, "", nil
return authorizer.DecisionNoOpinion, "", nil
}
if a.GetVerb() == "update" && a.GetSubresource() == "finalizers" {
return false, "", nil
return authorizer.DecisionNoOpinion, "", nil
}
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
if username == "non-pod-deleter" {
if a.GetVerb() == "delete" && a.GetResource() == "pods" {
return false, "", nil
return authorizer.DecisionNoOpinion, "", nil
}
if a.GetVerb() == "update" && a.GetResource() == "pods" && a.GetSubresource() == "finalizers" {
return false, "", nil
return authorizer.DecisionNoOpinion, "", nil
}
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
if username == "non-rc-deleter" {
if a.GetVerb() == "delete" && a.GetResource() == "replicationcontrollers" {
return false, "", nil
return authorizer.DecisionNoOpinion, "", nil
}
if a.GetVerb() == "update" && a.GetResource() == "replicationcontrollers" && a.GetSubresource() == "finalizers" {
return false, "", nil
return authorizer.DecisionNoOpinion, "", nil
}
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
// newGCPermissionsEnforcement returns the admission controller configured for testing.
......
......@@ -313,11 +313,11 @@ func authorizedForPolicy(info user.Info, namespace string, policy *extensions.Po
return false
}
attr := buildAttributes(info, namespace, policy)
allowed, reason, err := authz.Authorize(attr)
decision, reason, err := authz.Authorize(attr)
if err != nil {
glog.V(5).Infof("cannot authorize for policy: %v,%v", reason, err)
}
return allowed
return (decision == authorizer.DecisionAllow)
}
// buildAttributes builds an attributes record for a SAR based on the user info and policy.
......
......@@ -66,13 +66,16 @@ type TestAuthorizer struct {
usernameToNamespaceToAllowedPSPs map[string]map[string]map[string]bool
}
func (t *TestAuthorizer) Authorize(a authorizer.Attributes) (authorized bool, reason string, err error) {
func (t *TestAuthorizer) Authorize(a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
if t.usernameToNamespaceToAllowedPSPs == nil {
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
allowedInNamespace := t.usernameToNamespaceToAllowedPSPs[a.GetUser().GetName()][a.GetNamespace()][a.GetName()]
allowedClusterWide := t.usernameToNamespaceToAllowedPSPs[a.GetUser().GetName()][""][a.GetName()]
return (allowedInNamespace || allowedClusterWide), "", nil
if allowedInNamespace || allowedClusterWide {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionNoOpinion, "", nil
}
var _ authorizer.Authorizer = &TestAuthorizer{}
......
......@@ -64,16 +64,16 @@ var (
pvResource = api.Resource("persistentvolumes")
)
func (r *NodeAuthorizer) Authorize(attrs authorizer.Attributes) (bool, string, error) {
func (r *NodeAuthorizer) Authorize(attrs authorizer.Attributes) (authorizer.Decision, string, error) {
nodeName, isNode := r.identifier.NodeIdentity(attrs.GetUser())
if !isNode {
// reject requests from non-nodes
return false, "", nil
return authorizer.DecisionNoOpinion, "", nil
}
if len(nodeName) == 0 {
// reject requests from unidentifiable nodes
glog.V(2).Infof("NODE DENY: unknown node for user %q", attrs.GetUser().GetName())
return false, fmt.Sprintf("unknown node for user %q", attrs.GetUser().GetName()), nil
return authorizer.DecisionNoOpinion, fmt.Sprintf("unknown node for user %q", attrs.GetUser().GetName()), nil
}
// subdivide access to specific resources
......@@ -92,31 +92,34 @@ func (r *NodeAuthorizer) Authorize(attrs authorizer.Attributes) (bool, string, e
}
// Access to other resources is not subdivided, so just evaluate against the statically defined node rules
return rbac.RulesAllow(attrs, r.nodeRules...), "", nil
if rbac.RulesAllow(attrs, r.nodeRules...) {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionNoOpinion, "", nil
}
// authorizeGet authorizes "get" requests to objects of the specified type if they are related to the specified node
func (r *NodeAuthorizer) authorizeGet(nodeName string, startingType vertexType, attrs authorizer.Attributes) (bool, string, error) {
func (r *NodeAuthorizer) authorizeGet(nodeName string, startingType vertexType, attrs authorizer.Attributes) (authorizer.Decision, string, error) {
if attrs.GetVerb() != "get" || len(attrs.GetName()) == 0 {
glog.V(2).Infof("NODE DENY: %s %#v", nodeName, attrs)
return false, "can only get individual resources of this type", nil
return authorizer.DecisionNoOpinion, "can only get individual resources of this type", nil
}
if len(attrs.GetSubresource()) > 0 {
glog.V(2).Infof("NODE DENY: %s %#v", nodeName, attrs)
return false, "cannot get subresource", nil
return authorizer.DecisionNoOpinion, "cannot get subresource", nil
}
ok, err := r.hasPathFrom(nodeName, startingType, attrs.GetNamespace(), attrs.GetName())
if err != nil {
glog.V(2).Infof("NODE DENY: %v", err)
return false, "no path found to object", nil
return authorizer.DecisionNoOpinion, "no path found to object", nil
}
if !ok {
glog.V(2).Infof("NODE DENY: %q %#v", nodeName, attrs)
return false, "no path found to object", nil
return authorizer.DecisionNoOpinion, "no path found to object", nil
}
return ok, "", nil
return authorizer.DecisionAllow, "", nil
}
// hasPathFrom returns true if there is a directed path from the specified type/namespace/name to the specified Node
......
......@@ -57,71 +57,71 @@ func TestAuthorizer(t *testing.T) {
tests := []struct {
name string
attrs authorizer.AttributesRecord
expect bool
expect authorizer.Decision
}{
{
name: "allowed configmap",
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "configmaps", Name: "configmap0-pod0-node0", Namespace: "ns0"},
expect: true,
expect: authorizer.DecisionAllow,
},
{
name: "allowed secret via pod",
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "secrets", Name: "secret0-pod0-node0", Namespace: "ns0"},
expect: true,
expect: authorizer.DecisionAllow,
},
{
name: "allowed shared secret via pod",
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "secrets", Name: "secret0-shared", Namespace: "ns0"},
expect: true,
expect: authorizer.DecisionAllow,
},
{
name: "allowed shared secret via pvc",
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "secrets", Name: "secret-pv0-pod0-node0-ns0", Namespace: "ns0"},
expect: true,
expect: authorizer.DecisionAllow,
},
{
name: "allowed pvc",
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "persistentvolumeclaims", Name: "pvc0-pod0-node0", Namespace: "ns0"},
expect: true,
expect: authorizer.DecisionAllow,
},
{
name: "allowed pv",
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "persistentvolumes", Name: "pv0-pod0-node0-ns0", Namespace: ""},
expect: true,
expect: authorizer.DecisionAllow,
},
{
name: "disallowed configmap",
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "configmaps", Name: "configmap0-pod0-node1", Namespace: "ns0"},
expect: false,
expect: authorizer.DecisionNoOpinion,
},
{
name: "disallowed secret via pod",
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "secrets", Name: "secret0-pod0-node1", Namespace: "ns0"},
expect: false,
expect: authorizer.DecisionNoOpinion,
},
{
name: "disallowed shared secret via pvc",
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "secrets", Name: "secret-pv0-pod0-node1-ns0", Namespace: "ns0"},
expect: false,
expect: authorizer.DecisionNoOpinion,
},
{
name: "disallowed pvc",
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "persistentvolumeclaims", Name: "pvc0-pod0-node1", Namespace: "ns0"},
expect: false,
expect: authorizer.DecisionNoOpinion,
},
{
name: "disallowed pv",
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "persistentvolumes", Name: "pv0-pod0-node1-ns0", Namespace: ""},
expect: false,
expect: authorizer.DecisionNoOpinion,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ok, _, _ := authz.Authorize(tc.attrs)
if ok != tc.expect {
t.Errorf("expected %v, got %v", tc.expect, ok)
decision, _, _ := authz.Authorize(tc.attrs)
if decision != tc.expect {
t.Errorf("expected %v, got %v", tc.expect, decision)
}
})
}
......@@ -186,13 +186,13 @@ func TestAuthorizerSharedResources(t *testing.T) {
}
for i, tc := range testcases {
ok, _, err := authz.Authorize(authorizer.AttributesRecord{User: tc.User, ResourceRequest: true, Verb: "get", Resource: "secrets", Namespace: "ns1", Name: tc.Secret})
decision, _, err := authz.Authorize(authorizer.AttributesRecord{User: tc.User, ResourceRequest: true, Verb: "get", Resource: "secrets", Namespace: "ns1", Name: tc.Secret})
if err != nil {
t.Errorf("%d: unexpected error: %v", i, err)
continue
}
if ok != tc.ExpectAllowed {
t.Errorf("%d: expected %v, got %v", i, tc.ExpectAllowed, ok)
if (decision == authorizer.DecisionAllow) != tc.ExpectAllowed {
t.Errorf("%d: expected %v, got %v", i, tc.ExpectAllowed, decision)
}
}
}
......@@ -301,47 +301,47 @@ func BenchmarkAuthorization(b *testing.B) {
tests := []struct {
name string
attrs authorizer.AttributesRecord
expect bool
expect authorizer.Decision
}{
{
name: "allowed configmap",
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "configmaps", Name: "configmap0-pod0-node0", Namespace: "ns0"},
expect: true,
expect: authorizer.DecisionAllow,
},
{
name: "allowed secret via pod",
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "secrets", Name: "secret0-pod0-node0", Namespace: "ns0"},
expect: true,
expect: authorizer.DecisionAllow,
},
{
name: "allowed shared secret via pod",
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "secrets", Name: "secret0-shared", Namespace: "ns0"},
expect: true,
expect: authorizer.DecisionAllow,
},
{
name: "disallowed configmap",
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "configmaps", Name: "configmap0-pod0-node1", Namespace: "ns0"},
expect: false,
expect: authorizer.DecisionNoOpinion,
},
{
name: "disallowed secret via pod",
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "secrets", Name: "secret0-pod0-node1", Namespace: "ns0"},
expect: false,
expect: authorizer.DecisionNoOpinion,
},
{
name: "disallowed shared secret via pvc",
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "secrets", Name: "secret-pv0-pod0-node1-ns0", Namespace: "ns0"},
expect: false,
expect: authorizer.DecisionNoOpinion,
},
{
name: "disallowed pvc",
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "persistentvolumeclaims", Name: "pvc0-pod0-node1", Namespace: "ns0"},
expect: false,
expect: authorizer.DecisionNoOpinion,
},
{
name: "disallowed pv",
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "persistentvolumes", Name: "pv0-pod0-node1-ns0", Namespace: ""},
expect: false,
expect: authorizer.DecisionNoOpinion,
},
}
......@@ -349,9 +349,9 @@ func BenchmarkAuthorization(b *testing.B) {
for _, tc := range tests {
b.Run(tc.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
ok, _, _ := authz.Authorize(tc.attrs)
if ok != tc.expect {
b.Errorf("expected %v, got %v", tc.expect, ok)
decision, _, _ := authz.Authorize(tc.attrs)
if decision != tc.expect {
b.Errorf("expected %v, got %v", tc.expect, decision)
}
}
})
......
......@@ -69,12 +69,12 @@ func (v *authorizingVisitor) visit(rule *rbac.PolicyRule, err error) bool {
return true
}
func (r *RBACAuthorizer) Authorize(requestAttributes authorizer.Attributes) (bool, string, error) {
func (r *RBACAuthorizer) Authorize(requestAttributes authorizer.Attributes) (authorizer.Decision, string, error) {
ruleCheckingVisitor := &authorizingVisitor{requestAttributes: requestAttributes}
r.authorizationRuleResolver.VisitRulesFor(requestAttributes.GetUser(), requestAttributes.GetNamespace(), ruleCheckingVisitor.visit)
if ruleCheckingVisitor.allowed {
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
// Build a detailed log of the denial.
......@@ -120,7 +120,7 @@ func (r *RBACAuthorizer) Authorize(requestAttributes authorizer.Attributes) (boo
if len(ruleCheckingVisitor.errors) > 0 {
reason = fmt.Sprintf("%v", utilerrors.NewAggregate(ruleCheckingVisitor.errors))
}
return false, reason, nil
return authorizer.DecisionNoOpinion, reason, nil
}
func (r *RBACAuthorizer) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
......
......@@ -247,13 +247,13 @@ func TestAuthorizer(t *testing.T) {
ruleResolver, _ := rbacregistryvalidation.NewTestRuleResolver(tt.roles, tt.roleBindings, tt.clusterRoles, tt.clusterRoleBindings)
a := RBACAuthorizer{ruleResolver}
for _, attr := range tt.shouldPass {
if authorized, _, _ := a.Authorize(attr); !authorized {
if decision, _, _ := a.Authorize(attr); decision != authorizer.DecisionAllow {
t.Errorf("case %d: incorrectly restricted %s", i, attr)
}
}
for _, attr := range tt.shouldFail {
if authorized, _, _ := a.Authorize(attr); authorized {
if decision, _, _ := a.Authorize(attr); decision == authorizer.DecisionAllow {
t.Errorf("case %d: incorrectly passed %s", i, attr)
}
}
......
......@@ -226,9 +226,16 @@ message SubjectAccessReviewSpec {
// SubjectAccessReviewStatus
message SubjectAccessReviewStatus {
// Allowed is required. True if the action would be allowed, false otherwise.
// Allowed is required. True if the action would be allowed, false otherwise.
optional bool allowed = 1;
// Denied is optional. True if the action would be denied, otherwise
// false. If both allowed is false and denied is false, then the
// authorizer has no opinion on whether to authorize the action. Denied
// may not be true if Allowed is true.
// +optional
optional bool denied = 4;
// Reason is optional. It indicates why a request was allowed or denied.
// +optional
optional string reason = 2;
......
......@@ -169,8 +169,14 @@ type SelfSubjectAccessReviewSpec struct {
// SubjectAccessReviewStatus
type SubjectAccessReviewStatus struct {
// Allowed is required. True if the action would be allowed, false otherwise.
// Allowed is required. True if the action would be allowed, false otherwise.
Allowed bool `json:"allowed" protobuf:"varint,1,opt,name=allowed"`
// Denied is optional. True if the action would be denied, otherwise
// false. If both allowed is false and denied is false, then the
// authorizer has no opinion on whether to authorize the action. Denied
// may not be true if Allowed is true.
// +optional
Denied bool `json:"denied,omitempty" protobuf:"varint,4,opt,name=denied"`
// Reason is optional. It indicates why a request was allowed or denied.
// +optional
Reason string `json:"reason,omitempty" protobuf:"bytes,2,opt,name=reason"`
......
......@@ -148,7 +148,8 @@ func (SubjectAccessReviewSpec) SwaggerDoc() map[string]string {
var map_SubjectAccessReviewStatus = map[string]string{
"": "SubjectAccessReviewStatus",
"allowed": "Allowed is required. True if the action would be allowed, false otherwise.",
"allowed": "Allowed is required. True if the action would be allowed, false otherwise.",
"denied": "Denied is optional. True if the action would be denied, otherwise false. If both allowed is false and denied is false, then the authorizer has no opinion on whether to authorize the action. Denied may not be true if Allowed is true.",
"reason": "Reason is optional. It indicates why a request was allowed or denied.",
"evaluationError": "EvaluationError is an indication that some error occurred during the authorization check. It is entirely possible to get an error and be able to continue determine authorization status in spite of it. For instance, RBAC can be missing a role, but enough roles are still present and bound to reason about the request.",
}
......
......@@ -226,9 +226,16 @@ message SubjectAccessReviewSpec {
// SubjectAccessReviewStatus
message SubjectAccessReviewStatus {
// Allowed is required. True if the action would be allowed, false otherwise.
// Allowed is required. True if the action would be allowed, false otherwise.
optional bool allowed = 1;
// Denied is optional. True if the action would be denied, otherwise
// false. If both allowed is false and denied is false, then the
// authorizer has no opinion on whether to authorize the action. Denied
// may not be true if Allowed is true.
// +optional
optional bool denied = 4;
// Reason is optional. It indicates why a request was allowed or denied.
// +optional
optional string reason = 2;
......
......@@ -169,8 +169,14 @@ type SelfSubjectAccessReviewSpec struct {
// SubjectAccessReviewStatus
type SubjectAccessReviewStatus struct {
// Allowed is required. True if the action would be allowed, false otherwise.
// Allowed is required. True if the action would be allowed, false otherwise.
Allowed bool `json:"allowed" protobuf:"varint,1,opt,name=allowed"`
// Denied is optional. True if the action would be denied, otherwise
// false. If both allowed is false and denied is false, then the
// authorizer has no opinion on whether to authorize the action. Denied
// may not be true if Allowed is true.
// +optional
Denied bool `json:"denied,omitempty" protobuf:"varint,4,opt,name=denied"`
// Reason is optional. It indicates why a request was allowed or denied.
// +optional
Reason string `json:"reason,omitempty" protobuf:"bytes,2,opt,name=reason"`
......
......@@ -148,7 +148,8 @@ func (SubjectAccessReviewSpec) SwaggerDoc() map[string]string {
var map_SubjectAccessReviewStatus = map[string]string{
"": "SubjectAccessReviewStatus",
"allowed": "Allowed is required. True if the action would be allowed, false otherwise.",
"allowed": "Allowed is required. True if the action would be allowed, false otherwise.",
"denied": "Denied is optional. True if the action would be denied, otherwise false. If both allowed is false and denied is false, then the authorizer has no opinion on whether to authorize the action. Denied may not be true if Allowed is true.",
"reason": "Reason is optional. It indicates why a request was allowed or denied.",
"evaluationError": "EvaluationError is an indication that some error occurred during the authorization check. It is entirely possible to get an error and be able to continue determine authorization status in spite of it. For instance, RBAC can be missing a role, but enough roles are still present and bound to reason about the request.",
}
......
......@@ -133,8 +133,8 @@ var _ initializer.WantsAuthorizer = &WantAuthorizerAdmission{}
// TestAuthorizer is a test stub that fulfills the WantsAuthorizer interface.
type TestAuthorizer struct{}
func (t *TestAuthorizer) Authorize(a authorizer.Attributes) (authorized bool, reason string, err error) {
return false, "", nil
func (t *TestAuthorizer) Authorize(a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
return authorizer.DecisionNoOpinion, "", nil
}
// wantClientCert is a test stub for testing that fulfulls the WantsClientCert interface.
......
......@@ -260,7 +260,7 @@ func (i *initializer) Admit(a admission.Attributes) (err error) {
func (i *initializer) canInitialize(a admission.Attributes, message string) error {
// caller must have the ability to mutate un-initialized resources
authorized, reason, err := i.authorizer.Authorize(authorizer.AttributesRecord{
decision, reason, err := i.authorizer.Authorize(authorizer.AttributesRecord{
Name: a.GetName(),
ResourceRequest: true,
User: a.GetUserInfo(),
......@@ -273,7 +273,7 @@ func (i *initializer) canInitialize(a admission.Attributes, message string) erro
if err != nil {
return err
}
if !authorized {
if decision != authorizer.DecisionAllow {
return errors.NewForbidden(a.GetResource().GroupResource(), a.GetName(), fmt.Errorf("%s: %s", message, reason))
}
return nil
......
......@@ -109,11 +109,11 @@ type fakeAuthorizer struct {
accept bool
}
func (f *fakeAuthorizer) Authorize(a authorizer.Attributes) (bool, string, error) {
func (f *fakeAuthorizer) Authorize(a authorizer.Attributes) (authorizer.Decision, string, error) {
if f.accept {
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
return false, "denied", nil
return authorizer.DecisionNoOpinion, "denied", nil
}
func TestAdmitUpdate(t *testing.T) {
......
......@@ -67,12 +67,12 @@ type Attributes interface {
// zero or more calls to methods of the Attributes interface. It returns nil when an action is
// authorized, otherwise it returns an error.
type Authorizer interface {
Authorize(a Attributes) (authorized bool, reason string, err error)
Authorize(a Attributes) (authorized Decision, reason string, err error)
}
type AuthorizerFunc func(a Attributes) (bool, string, error)
type AuthorizerFunc func(a Attributes) (Decision, string, error)
func (f AuthorizerFunc) Authorize(a Attributes) (bool, string, error) {
func (f AuthorizerFunc) Authorize(a Attributes) (Decision, string, error) {
return f(a)
}
......@@ -144,3 +144,15 @@ func (a AttributesRecord) IsResourceRequest() bool {
func (a AttributesRecord) GetPath() string {
return a.Path
}
type Decision int
const (
// DecisionDeny means that an authorizer decided to deny the action.
DecisionDeny Decision = iota
// DecisionAllow means that an authorizer decided to allow the action.
DecisionAllow
// DecisionNoOpionion means that an authorizer has no opinion on wether
// to allow or deny an action.
DecisionNoOpinion
)
......@@ -27,7 +27,7 @@ import (
// and always return nil.
func TestNewAlwaysAllowAuthorizer(t *testing.T) {
aaa := NewAlwaysAllowAuthorizer()
if authorized, _, _ := aaa.Authorize(nil); !authorized {
if decision, _, _ := aaa.Authorize(nil); decision != authorizer.DecisionAllow {
t.Errorf("AlwaysAllowAuthorizer.Authorize did not authorize successfully.")
}
}
......@@ -36,7 +36,7 @@ func TestNewAlwaysAllowAuthorizer(t *testing.T) {
// and always return an error as everything is forbidden.
func TestNewAlwaysDenyAuthorizer(t *testing.T) {
ada := NewAlwaysDenyAuthorizer()
if authorized, _, _ := ada.Authorize(nil); authorized {
if decision, _, _ := ada.Authorize(nil); decision == authorizer.DecisionAllow {
t.Errorf("AlwaysDenyAuthorizer.Authorize returned nil instead of error.")
}
}
......@@ -47,10 +47,10 @@ func TestPrivilegedGroupAuthorizer(t *testing.T) {
yes := authorizer.AttributesRecord{User: &user.DefaultInfo{Groups: []string{"no", "allow-01"}}}
no := authorizer.AttributesRecord{User: &user.DefaultInfo{Groups: []string{"no", "deny-01"}}}
if authorized, _, _ := auth.Authorize(yes); !authorized {
if authorized, _, _ := auth.Authorize(yes); authorized != authorizer.DecisionAllow {
t.Errorf("failed")
}
if authorized, _, _ := auth.Authorize(no); authorized {
if authorized, _, _ := auth.Authorize(no); authorized == authorizer.DecisionAllow {
t.Errorf("failed")
}
}
......@@ -28,8 +28,8 @@ import (
// It is useful in tests and when using kubernetes in an open manner.
type alwaysAllowAuthorizer struct{}
func (alwaysAllowAuthorizer) Authorize(a authorizer.Attributes) (authorized bool, reason string, err error) {
return true, "", nil
func (alwaysAllowAuthorizer) Authorize(a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
return authorizer.DecisionAllow, "", nil
}
func (alwaysAllowAuthorizer) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
......@@ -56,8 +56,8 @@ func NewAlwaysAllowAuthorizer() *alwaysAllowAuthorizer {
// It is useful in unit tests to force an operation to be forbidden.
type alwaysDenyAuthorizer struct{}
func (alwaysDenyAuthorizer) Authorize(a authorizer.Attributes) (authorized bool, reason string, err error) {
return false, "Everything is forbidden.", nil
func (alwaysDenyAuthorizer) Authorize(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
return authorizer.DecisionNoOpinion, "Everything is forbidden.", nil
}
func (alwaysDenyAuthorizer) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
......@@ -73,8 +73,8 @@ func NewAlwaysDenyAuthorizer() *alwaysDenyAuthorizer {
// It is useful in unit tests to force an operation to fail with error.
type alwaysFailAuthorizer struct{}
func (alwaysFailAuthorizer) Authorize(a authorizer.Attributes) (authorized bool, reason string, err error) {
return false, "", errors.New("Authorization failure.")
func (alwaysFailAuthorizer) Authorize(a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
return authorizer.DecisionNoOpinion, "", errors.New("Authorization failure.")
}
func NewAlwaysFailAuthorizer() authorizer.Authorizer {
......@@ -85,18 +85,18 @@ type privilegedGroupAuthorizer struct {
groups []string
}
func (r *privilegedGroupAuthorizer) Authorize(attr authorizer.Attributes) (bool, string, error) {
func (r *privilegedGroupAuthorizer) Authorize(attr authorizer.Attributes) (authorizer.Decision, string, error) {
if attr.GetUser() == nil {
return false, "Error", errors.New("no user on request.")
return authorizer.DecisionNoOpinion, "Error", errors.New("no user on request.")
}
for _, attr_group := range attr.GetUser().GetGroups() {
for _, priv_group := range r.groups {
if priv_group == attr_group {
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
}
}
return false, "", nil
return authorizer.DecisionNoOpinion, "", nil
}
// NewPrivilegedGroups is for use in loopback scenarios
......
......@@ -14,6 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// Package union implements an authorizer that combines multiple subauthorizer.
// The union authorizer iterates over each subauthorizer and returns the first
// decision that is either an Allow decision or a Deny decision. If a
// subauthorizer returns a NoOpinion, then the union authorizer moves onto the
// next authorizer or, if the subauthorizer was the last authorizer, returns
// NoOpinion as the aggregate decision. I.e. union authorizer creates an
// aggregate decision and supports short-circut allows and denies from
// subauthorizers.
package union
import (
......@@ -33,14 +41,14 @@ func New(authorizationHandlers ...authorizer.Authorizer) authorizer.Authorizer {
}
// Authorizes against a chain of authorizer.Authorizer objects and returns nil if successful and returns error if unsuccessful
func (authzHandler unionAuthzHandler) Authorize(a authorizer.Attributes) (bool, string, error) {
func (authzHandler unionAuthzHandler) Authorize(a authorizer.Attributes) (authorizer.Decision, string, error) {
var (
errlist []error
reasonlist []string
)
for _, currAuthzHandler := range authzHandler {
authorized, reason, err := currAuthzHandler.Authorize(a)
decision, reason, err := currAuthzHandler.Authorize(a)
if err != nil {
errlist = append(errlist, err)
......@@ -48,13 +56,15 @@ func (authzHandler unionAuthzHandler) Authorize(a authorizer.Attributes) (bool,
if len(reason) != 0 {
reasonlist = append(reasonlist, reason)
}
if !authorized {
continue
switch decision {
case authorizer.DecisionAllow, authorizer.DecisionDeny:
return decision, reason, err
case authorizer.DecisionNoOpinion:
// continue to the next authorizer
}
return true, reason, nil
}
return false, strings.Join(reasonlist, "\n"), utilerrors.NewAggregate(errlist)
return authorizer.DecisionNoOpinion, strings.Join(reasonlist, "\n"), utilerrors.NewAggregate(errlist)
}
// unionAuthzRulesHandler authorizer against a chain of authorizer.RuleResolver
......
......@@ -17,6 +17,7 @@ limitations under the License.
package union
import (
"errors"
"fmt"
"reflect"
"testing"
......@@ -26,49 +27,43 @@ import (
)
type mockAuthzHandler struct {
isAuthorized bool
err error
decision authorizer.Decision
err error
}
func (mock *mockAuthzHandler) Authorize(a authorizer.Attributes) (bool, string, error) {
if mock.err != nil {
return false, "", mock.err
}
if !mock.isAuthorized {
return false, "", nil
}
return true, "", nil
func (mock *mockAuthzHandler) Authorize(a authorizer.Attributes) (authorizer.Decision, string, error) {
return mock.decision, "", mock.err
}
func TestAuthorizationSecondPasses(t *testing.T) {
handler1 := &mockAuthzHandler{isAuthorized: false}
handler2 := &mockAuthzHandler{isAuthorized: true}
handler1 := &mockAuthzHandler{decision: authorizer.DecisionNoOpinion}
handler2 := &mockAuthzHandler{decision: authorizer.DecisionAllow}
authzHandler := New(handler1, handler2)
authorized, _, _ := authzHandler.Authorize(nil)
if !authorized {
if authorized != authorizer.DecisionAllow {
t.Errorf("Unexpected authorization failure")
}
}
func TestAuthorizationFirstPasses(t *testing.T) {
handler1 := &mockAuthzHandler{isAuthorized: true}
handler2 := &mockAuthzHandler{isAuthorized: false}
handler1 := &mockAuthzHandler{decision: authorizer.DecisionAllow}
handler2 := &mockAuthzHandler{decision: authorizer.DecisionNoOpinion}
authzHandler := New(handler1, handler2)
authorized, _, _ := authzHandler.Authorize(nil)
if !authorized {
if authorized != authorizer.DecisionAllow {
t.Errorf("Unexpected authorization failure")
}
}
func TestAuthorizationNonePasses(t *testing.T) {
handler1 := &mockAuthzHandler{isAuthorized: false}
handler2 := &mockAuthzHandler{isAuthorized: false}
handler1 := &mockAuthzHandler{decision: authorizer.DecisionNoOpinion}
handler2 := &mockAuthzHandler{decision: authorizer.DecisionNoOpinion}
authzHandler := New(handler1, handler2)
authorized, _, _ := authzHandler.Authorize(nil)
if authorized {
if authorized == authorizer.DecisionAllow {
t.Errorf("Expected failed authorization")
}
}
......@@ -223,3 +218,49 @@ func getNonResourceRules(infos []authorizer.NonResourceRuleInfo) []authorizer.De
}
return rules
}
func TestAuthorizationUnequivocalDeny(t *testing.T) {
cs := []struct {
authorizers []authorizer.Authorizer
decision authorizer.Decision
}{
{
authorizers: []authorizer.Authorizer{},
decision: authorizer.DecisionNoOpinion,
},
{
authorizers: []authorizer.Authorizer{
&mockAuthzHandler{decision: authorizer.DecisionNoOpinion},
&mockAuthzHandler{decision: authorizer.DecisionAllow},
&mockAuthzHandler{decision: authorizer.DecisionDeny},
},
decision: authorizer.DecisionAllow,
},
{
authorizers: []authorizer.Authorizer{
&mockAuthzHandler{decision: authorizer.DecisionNoOpinion},
&mockAuthzHandler{decision: authorizer.DecisionDeny},
&mockAuthzHandler{decision: authorizer.DecisionAllow},
},
decision: authorizer.DecisionDeny,
},
{
authorizers: []authorizer.Authorizer{
&mockAuthzHandler{decision: authorizer.DecisionNoOpinion},
&mockAuthzHandler{decision: authorizer.DecisionDeny, err: errors.New("webhook failed closed")},
&mockAuthzHandler{decision: authorizer.DecisionAllow},
},
decision: authorizer.DecisionDeny,
},
}
for i, c := range cs {
t.Run(fmt.Sprintf("case %v", i), func(t *testing.T) {
authzHandler := New(c.authorizers...)
decision, _, _ := authzHandler.Authorize(nil)
if decision != c.decision {
t.Errorf("Unexpected authorization failure: %v, expected: %v", decision, c.decision)
}
})
}
}
......@@ -47,7 +47,7 @@ func WithAuthorization(handler http.Handler, requestContextMapper request.Reques
return
}
authorized, reason, err := a.Authorize(attributes)
if authorized {
if authorized == authorizer.DecisionAllow {
handler.ServeHTTP(w, req)
return
}
......
......@@ -110,8 +110,8 @@ func WithImpersonation(handler http.Handler, requestContextMapper request.Reques
return
}
allowed, reason, err := a.Authorize(actingAsAttributes)
if err != nil || !allowed {
decision, reason, err := a.Authorize(actingAsAttributes)
if err != nil || decision != authorizer.DecisionAllow {
glog.V(4).Infof("Forbidden: %#v, Reason: %s, Error: %v", req.RequestURI, reason, err)
responsewriters.Forbidden(ctx, actingAsAttributes, w, req, reason, s)
return
......
......@@ -35,50 +35,50 @@ import (
type impersonateAuthorizer struct{}
func (impersonateAuthorizer) Authorize(a authorizer.Attributes) (authorized bool, reason string, err error) {
func (impersonateAuthorizer) Authorize(a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
user := a.GetUser()
switch {
case user.GetName() == "system:admin":
return true, "", nil
return authorizer.DecisionAllow, "", nil
case user.GetName() == "tester":
return false, "", fmt.Errorf("works on my machine")
return authorizer.DecisionNoOpinion, "", fmt.Errorf("works on my machine")
case user.GetName() == "deny-me":
return false, "denied", nil
return authorizer.DecisionNoOpinion, "denied", nil
}
if len(user.GetGroups()) > 0 && user.GetGroups()[0] == "wheel" && a.GetVerb() == "impersonate" && a.GetResource() == "users" {
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
if len(user.GetGroups()) > 0 && user.GetGroups()[0] == "sa-impersonater" && a.GetVerb() == "impersonate" && a.GetResource() == "serviceaccounts" {
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
if len(user.GetGroups()) > 0 && user.GetGroups()[0] == "regular-impersonater" && a.GetVerb() == "impersonate" && a.GetResource() == "users" {
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
if len(user.GetGroups()) > 1 && user.GetGroups()[1] == "group-impersonater" && a.GetVerb() == "impersonate" && a.GetResource() == "groups" {
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
if len(user.GetGroups()) > 1 && user.GetGroups()[1] == "extra-setter-scopes" && a.GetVerb() == "impersonate" && a.GetResource() == "userextras" && a.GetSubresource() == "scopes" {
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
if len(user.GetGroups()) > 1 && user.GetGroups()[1] == "extra-setter-particular-scopes" &&
a.GetVerb() == "impersonate" && a.GetResource() == "userextras" && a.GetSubresource() == "scopes" && a.GetName() == "scope-a" {
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
if len(user.GetGroups()) > 1 && user.GetGroups()[1] == "extra-setter-project" && a.GetVerb() == "impersonate" && a.GetResource() == "userextras" && a.GetSubresource() == "project" {
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
return false, "deny by default", nil
return authorizer.DecisionNoOpinion, "deny by default", nil
}
func TestImpersonationFilter(t *testing.T) {
......
......@@ -437,9 +437,9 @@ type mockAuthorizer struct {
lastURI string
}
func (authz *mockAuthorizer) Authorize(a authorizer.Attributes) (authorized bool, reason string, err error) {
func (authz *mockAuthorizer) Authorize(a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
authz.lastURI = a.GetPath()
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
type mockAuthenticator struct {
......
......@@ -50,6 +50,7 @@ type WebhookAuthorizer struct {
authorizedTTL time.Duration
unauthorizedTTL time.Duration
initialBackoff time.Duration
decisionOnError authorizer.Decision
}
// NewFromInterface creates a WebhookAuthorizer using the given subjectAccessReview client
......@@ -93,6 +94,7 @@ func newWithBackoff(subjectAccessReview authorizationclient.SubjectAccessReviewI
authorizedTTL: authorizedTTL,
unauthorizedTTL: unauthorizedTTL,
initialBackoff: initialBackoff,
decisionOnError: authorizer.DecisionNoOpinion,
}, nil
}
......@@ -140,7 +142,10 @@ func newWithBackoff(subjectAccessReview authorizationclient.SubjectAccessReviewI
// }
// }
//
func (w *WebhookAuthorizer) Authorize(attr authorizer.Attributes) (authorized bool, reason string, err error) {
// TODO(mikedanese): We should eventually support failing closed when we
// encounter an error. We are failing open now to preserve backwards compatible
// behavior.
func (w *WebhookAuthorizer) Authorize(attr authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
r := &authorization.SubjectAccessReview{}
if user := attr.GetUser(); user != nil {
r.Spec = authorization.SubjectAccessReviewSpec{
......@@ -169,7 +174,7 @@ func (w *WebhookAuthorizer) Authorize(attr authorizer.Attributes) (authorized bo
}
key, err := json.Marshal(r.Spec)
if err != nil {
return false, "", err
return w.decisionOnError, "", err
}
if entry, ok := w.responseCache.Get(string(key)); ok {
r.Status = entry.(authorization.SubjectAccessReviewStatus)
......@@ -185,7 +190,7 @@ func (w *WebhookAuthorizer) Authorize(attr authorizer.Attributes) (authorized bo
if err != nil {
// An error here indicates bad configuration or an outage. Log for debugging.
glog.Errorf("Failed to make webhook authorizer request: %v", err)
return false, "", err
return w.decisionOnError, "", err
}
r.Status = result.Status
if r.Status.Allowed {
......@@ -194,7 +199,17 @@ func (w *WebhookAuthorizer) Authorize(attr authorizer.Attributes) (authorized bo
w.responseCache.Add(string(key), r.Status, w.unauthorizedTTL)
}
}
return r.Status.Allowed, r.Status.Reason, nil
switch {
case r.Status.Denied && r.Status.Allowed:
return authorizer.DecisionDeny, r.Status.Reason, fmt.Errorf("webhook subject access review returned both allow and deny response")
case r.Status.Denied:
return authorizer.DecisionDeny, r.Status.Reason, nil
case r.Status.Allowed:
return authorizer.DecisionAllow, r.Status.Reason, nil
default:
return authorizer.DecisionNoOpinion, r.Status.Reason, nil
}
}
//TODO: need to finish the method to get the rules when using webhook mode
......
......@@ -396,13 +396,13 @@ func TestTLSConfig(t *testing.T) {
// Allow all and see if we get an error.
service.Allow()
authorized, _, err := wh.Authorize(attr)
decision, _, err := wh.Authorize(attr)
if tt.wantAuth {
if !authorized {
if decision != authorizer.DecisionAllow {
t.Errorf("expected successful authorization")
}
} else {
if authorized {
if decision == authorizer.DecisionAllow {
t.Errorf("expected failed authorization")
}
}
......@@ -418,7 +418,7 @@ func TestTLSConfig(t *testing.T) {
}
service.Deny()
if authorized, _, _ := wh.Authorize(attr); authorized {
if decision, _, _ := wh.Authorize(attr); decision == authorizer.DecisionAllow {
t.Errorf("%s: incorrectly authorized with DenyAll policy", tt.test)
}
}()
......@@ -522,11 +522,11 @@ func TestWebhook(t *testing.T) {
}
for i, tt := range tests {
authorized, _, err := wh.Authorize(tt.attr)
decision, _, err := wh.Authorize(tt.attr)
if err != nil {
t.Fatal(err)
}
if !authorized {
if decision != authorizer.DecisionAllow {
t.Errorf("case %d: authorization failed", i)
continue
}
......@@ -567,7 +567,7 @@ func testWebhookCacheCases(t *testing.T, serv *mockService, wh *WebhookAuthorize
continue
}
if test.expectedAuthorized != authorized {
if test.expectedAuthorized != (authorized == authorizer.DecisionAllow) {
t.Errorf("%d: expected authorized=%v, got %v", i, test.expectedAuthorized, authorized)
}
......
......@@ -39,12 +39,12 @@ import (
// TODO(etune): remove this test once a more comprehensive built-in authorizer is implemented.
type sarAuthorizer struct{}
func (sarAuthorizer) Authorize(a authorizer.Attributes) (bool, string, error) {
func (sarAuthorizer) Authorize(a authorizer.Attributes) (authorizer.Decision, string, error) {
if a.GetUser().GetName() == "dave" {
return false, "no", errors.New("I'm sorry, Dave")
return authorizer.DecisionNoOpinion, "no", errors.New("I'm sorry, Dave")
}
return true, "you're not dave", nil
return authorizer.DecisionAllow, "you're not dave", nil
}
func alwaysAlice(req *http.Request) (user.Info, bool, error) {
......
......@@ -535,11 +535,11 @@ func TestAuthModeAlwaysDeny(t *testing.T) {
// TODO(etune): remove this test once a more comprehensive built-in authorizer is implemented.
type allowAliceAuthorizer struct{}
func (allowAliceAuthorizer) Authorize(a authorizer.Attributes) (bool, string, error) {
func (allowAliceAuthorizer) Authorize(a authorizer.Attributes) (authorizer.Decision, string, error) {
if a.GetUser() != nil && a.GetUser().GetName() == "alice" {
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
return false, "I can't allow that. Go ask alice.", nil
return authorizer.DecisionNoOpinion, "I can't allow that. Go ask alice.", nil
}
// TestAliceNotForbiddenOrUnauthorized tests a user who is known to
......@@ -702,24 +702,24 @@ func TestUnknownUserIsUnauthorized(t *testing.T) {
type impersonateAuthorizer struct{}
// alice can't act as anyone and bob can't do anything but act-as someone
func (impersonateAuthorizer) Authorize(a authorizer.Attributes) (bool, string, error) {
func (impersonateAuthorizer) Authorize(a authorizer.Attributes) (authorizer.Decision, string, error) {
// alice can impersonate service accounts and do other actions
if a.GetUser() != nil && a.GetUser().GetName() == "alice" && a.GetVerb() == "impersonate" && a.GetResource() == "serviceaccounts" {
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
if a.GetUser() != nil && a.GetUser().GetName() == "alice" && a.GetVerb() != "impersonate" {
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
// bob can impersonate anyone, but that it
if a.GetUser() != nil && a.GetUser().GetName() == "bob" && a.GetVerb() == "impersonate" {
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
// service accounts can do everything
if a.GetUser() != nil && strings.HasPrefix(a.GetUser().GetName(), serviceaccount.ServiceAccountUsernamePrefix) {
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
return false, "I can't allow that. Go ask alice.", nil
return authorizer.DecisionNoOpinion, "I can't allow that. Go ask alice.", nil
}
func TestImpersonateIsForbidden(t *testing.T) {
......@@ -861,9 +861,9 @@ type trackingAuthorizer struct {
requestAttributes []authorizer.Attributes
}
func (a *trackingAuthorizer) Authorize(attributes authorizer.Attributes) (bool, string, error) {
func (a *trackingAuthorizer) Authorize(attributes authorizer.Attributes) (authorizer.Decision, string, error) {
a.requestAttributes = append(a.requestAttributes, attributes)
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
// TestAuthorizationAttributeDetermination tests that authorization attributes are built correctly
......
......@@ -46,6 +46,7 @@ import (
"k8s.io/apiserver/pkg/authentication/authenticatorfactory"
authenticatorunion "k8s.io/apiserver/pkg/authentication/request/union"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
authauthorizer "k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/authorization/authorizerfactory"
authorizerunion "k8s.io/apiserver/pkg/authorization/union"
......@@ -148,8 +149,8 @@ func NewMasterComponents(c *Config) *MasterComponents {
// alwaysAllow always allows an action
type alwaysAllow struct{}
func (alwaysAllow) Authorize(requestAttributes authauthorizer.Attributes) (bool, string, error) {
return true, "always allow", nil
func (alwaysAllow) Authorize(requestAttributes authauthorizer.Attributes) (authorizer.Decision, string, error) {
return authorizer.DecisionAllow, "always allow", nil
}
// alwaysEmpty simulates "no authentication" for old tests
......
......@@ -58,11 +58,11 @@ const (
type allowAliceAuthorizer struct{}
func (allowAliceAuthorizer) Authorize(a authorizer.Attributes) (bool, string, error) {
func (allowAliceAuthorizer) Authorize(a authorizer.Attributes) (authorizer.Decision, string, error) {
if a.GetUser() != nil && a.GetUser().GetName() == "alice" {
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
return false, "I can't allow that. Go ask alice.", nil
return authorizer.DecisionNoOpinion, "I can't allow that. Go ask alice.", nil
}
func testPrefix(t *testing.T, prefix string) {
......
......@@ -372,7 +372,7 @@ func startServiceAccountTestServer(t *testing.T) (*clientset.Clientset, restclie
// 1. The "root" user is allowed to do anything
// 2. ServiceAccounts named "ro" are allowed read-only operations in their namespace
// 3. ServiceAccounts named "rw" are allowed any operation in their namespace
authorizer := authorizer.AuthorizerFunc(func(attrs authorizer.Attributes) (bool, string, error) {
authorizer := authorizer.AuthorizerFunc(func(attrs authorizer.Attributes) (authorizer.Decision, string, error) {
username := ""
if user := attrs.GetUser(); user != nil {
username = user.GetName()
......@@ -382,7 +382,7 @@ func startServiceAccountTestServer(t *testing.T) (*clientset.Clientset, restclie
// If the user is "root"...
if username == rootUserName {
// allow them to do anything
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
// If the user is a service account...
......@@ -392,15 +392,15 @@ func startServiceAccountTestServer(t *testing.T) (*clientset.Clientset, restclie
switch serviceAccountName {
case readOnlyServiceAccountName:
if attrs.IsReadOnly() {
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
case readWriteServiceAccountName:
return true, "", nil
return authorizer.DecisionAllow, "", nil
}
}
}
return false, fmt.Sprintf("User %s is denied (ns=%s, readonly=%v, resource=%s)", username, ns, attrs.IsReadOnly(), attrs.GetResource()), nil
return authorizer.DecisionNoOpinion, fmt.Sprintf("User %s is denied (ns=%s, readonly=%v, resource=%s)", username, ns, attrs.IsReadOnly(), attrs.GetResource()), nil
})
// Set up admission plugin to auto-assign serviceaccounts to pods
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment