mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-05-28 04:04:39 -04:00
Extend the Authorizer interface with conditions-aware methods.
This commit is contained in:
parent
b1c17fc745
commit
6d78dfd60c
3 changed files with 425 additions and 4 deletions
|
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
Copyright The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package authorizer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
)
|
||||
|
||||
// ErrorConditionEvaluationNotSupported is returned by authorizer implementations
|
||||
// that do not support condition evaluation.
|
||||
var ErrorConditionEvaluationNotSupported = errors.New("condition evaluation not supported")
|
||||
|
||||
// ConditionsAwareDecision models an authorization decision that is conditions-aware.
|
||||
// It is an enum type of the following five variants:
|
||||
// - Allow: unconditional Allow.
|
||||
// - Deny: unconditional Deny.
|
||||
// - NoOpinion: unconditional NoOpinion.
|
||||
// - Conditional: conditional on some previously-unseen data.
|
||||
// - Union: an ordered list of sub-decisions, which forms a tree of decisions.
|
||||
//
|
||||
// The zero value (ConditionsAwareDecision{}) is equivalent to ConditionsAwareDecisionDeny().
|
||||
// A ConditionsAwareDecision is passed by value.
|
||||
type ConditionsAwareDecision struct {
|
||||
unconditionalDecision Decision
|
||||
|
||||
reason string
|
||||
err error
|
||||
}
|
||||
|
||||
// ConditionsAwareDecisionDeny constructs a Deny decision with the given reason and error.
|
||||
func ConditionsAwareDecisionDeny(reason string, err error) ConditionsAwareDecision {
|
||||
return ConditionsAwareDecision{
|
||||
// DecisionDeny == int(0) == zero value
|
||||
// => ConditionsAwareDecision{} == ConditionsAwareDecisionDeny()
|
||||
unconditionalDecision: DecisionDeny,
|
||||
reason: reason,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// ConditionsAwareDecisionAllow constructs an Allow decision with the given reason and error.
|
||||
func ConditionsAwareDecisionAllow(reason string, err error) ConditionsAwareDecision {
|
||||
return ConditionsAwareDecision{
|
||||
unconditionalDecision: DecisionAllow,
|
||||
reason: reason,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// ConditionsAwareDecisionNoOpinion constructs a NoOpinion decision with the given reason and error.
|
||||
func ConditionsAwareDecisionNoOpinion(reason string, err error) ConditionsAwareDecision {
|
||||
return ConditionsAwareDecision{
|
||||
unconditionalDecision: DecisionNoOpinion,
|
||||
reason: reason,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// ConditionsAwareDecisionFromParts is meant to be used by conditions-unaware Authorizer implementations
|
||||
// in order to implement Authorizer.ConditionsAwareAuthorize as:
|
||||
// "return authorizer.ConditionsAwareDecisionFromParts(self.Authorize(ctx, a))"
|
||||
func ConditionsAwareDecisionFromParts(unconditional Decision, reason string, err error) ConditionsAwareDecision {
|
||||
switch unconditional {
|
||||
case DecisionAllow:
|
||||
return ConditionsAwareDecisionAllow(reason, err)
|
||||
case DecisionNoOpinion:
|
||||
return ConditionsAwareDecisionNoOpinion(reason, err)
|
||||
case DecisionDeny:
|
||||
return ConditionsAwareDecisionDeny(reason, err)
|
||||
default:
|
||||
return ConditionsAwareDecisionDeny(reason, utilerrors.NewAggregate(
|
||||
[]error{
|
||||
err,
|
||||
fmt.Errorf("unknown unconditional decision type: %d", unconditional),
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// INVARIANT: Exactly one of Is* must return true at all times.
|
||||
|
||||
// IsAllowed returns true if the decision is an unconditional Allow.
|
||||
func (d ConditionsAwareDecision) IsAllowed() bool {
|
||||
return d.unconditionalDecision == DecisionAllow
|
||||
}
|
||||
|
||||
// IsNoOpinion returns true if the decision is an unconditional NoOpinion.
|
||||
func (d ConditionsAwareDecision) IsNoOpinion() bool {
|
||||
return d.unconditionalDecision == DecisionNoOpinion
|
||||
}
|
||||
|
||||
// IsDenied returns true if the decision is an unconditional Deny.
|
||||
func (d ConditionsAwareDecision) IsDenied() bool {
|
||||
return d.unconditionalDecision == DecisionDeny // == 0 == zero value
|
||||
}
|
||||
|
||||
// IsUnconditional is true if d is Allowed, Denied or NoOpinion.
|
||||
func (d ConditionsAwareDecision) IsUnconditional() bool {
|
||||
return d.IsAllowed() || d.IsDenied() || d.IsNoOpinion()
|
||||
}
|
||||
|
||||
// UnconditionalParts turns a ConditionsAwareDecision into the
|
||||
// triple that Authorizer.Authorize expects. If the decision is
|
||||
// conditional, the returned condition is Deny if there were at least
|
||||
// some Deny condition, otherwise NoOpinion.
|
||||
// This function is meant to be called when IsUnconditional() == true.
|
||||
//
|
||||
// If the authorizer is conditions-aware, it can choose to only implement
|
||||
// real business logic in the ConditionsAwareAuthorize method, and implement
|
||||
// Authorize() as "return self.ConditionsAwareAuthorize(ctx, attrs).UnconditionalParts()"
|
||||
func (d ConditionsAwareDecision) UnconditionalParts() (Decision, string, error) {
|
||||
switch {
|
||||
case d.IsAllowed():
|
||||
return DecisionAllow, d.Reason(), d.Error()
|
||||
case d.IsDenied():
|
||||
return DecisionDeny, d.Reason(), d.Error()
|
||||
case d.IsNoOpinion():
|
||||
return DecisionNoOpinion, d.Reason(), d.Error()
|
||||
default:
|
||||
// An error is not returned here, as that could yield a HTTP response code of 500 instead of 403.
|
||||
// For the use-case described above with regards to calling this function in Authorize, not returning
|
||||
// an error is important, as it is valid to always fail closed, as if this happens, no unconditional
|
||||
// permissions were given the requestor.
|
||||
return d.FailClosedDecision(), "failed closed: tried to return conditional decision to conditions-unaware authorizer", nil
|
||||
}
|
||||
}
|
||||
|
||||
// FailClosedDecision returns either a Deny or NoOpinion decision to fail closed
|
||||
// whenever processing a decision fails. If the decision contains one or
|
||||
// more Deny decisions or conditions, one must fail closed with Deny, as that could or would
|
||||
// have been the if the condition evaluation did not error. Otherwise, NoOpinion is returned.
|
||||
func (d ConditionsAwareDecision) FailClosedDecision() Decision {
|
||||
if d.IsAllowed() || d.IsNoOpinion() {
|
||||
return DecisionNoOpinion
|
||||
}
|
||||
// TODO(luxas): In the follow-up PR, add the logic for ConditionsMap and Union here, which
|
||||
// makes this function useful.
|
||||
// => d.IsDenied() == true
|
||||
return DecisionDeny
|
||||
}
|
||||
|
||||
// Reason returns the reason associated with the decision.
|
||||
func (d ConditionsAwareDecision) Reason() string {
|
||||
return d.reason
|
||||
}
|
||||
|
||||
// Error returns the error associated with the decision.
|
||||
func (d ConditionsAwareDecision) Error() error {
|
||||
return d.err
|
||||
}
|
||||
|
||||
// String returns a human-readable representation of the decision.
|
||||
func (d ConditionsAwareDecision) String() string {
|
||||
params := []string{}
|
||||
if len(d.reason) != 0 {
|
||||
params = append(params, fmt.Sprintf("reason=%q", d.reason))
|
||||
}
|
||||
if d.err != nil {
|
||||
params = append(params, fmt.Sprintf("err=%q", d.err.Error()))
|
||||
}
|
||||
paramsStr := func() string {
|
||||
if len(params) == 0 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("(%s)", strings.Join(params, ", "))
|
||||
}
|
||||
if d.IsAllowed() {
|
||||
return fmt.Sprintf("Allow%s", paramsStr())
|
||||
}
|
||||
if d.IsNoOpinion() {
|
||||
return fmt.Sprintf("NoOpinion%s", paramsStr())
|
||||
}
|
||||
// Deny is written such that if none of the other modes apply,
|
||||
// IsDenied() is true.
|
||||
return fmt.Sprintf("Deny%s", paramsStr())
|
||||
}
|
||||
|
||||
// ConditionsData is an enum type for various evaluation targets conditions
|
||||
// can be written against.
|
||||
// TODO(luxas): Implement this in the follow-up PR.
|
||||
type ConditionsData struct {
|
||||
}
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
/*
|
||||
Copyright The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package authorizer_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
)
|
||||
|
||||
func TestConditionsAwareDecision(t *testing.T) {
|
||||
unexpectedErr := fmt.Errorf("unexpected things happened")
|
||||
otherErr := fmt.Errorf("other error")
|
||||
|
||||
ctx := t.Context()
|
||||
sampleAttrs := authorizer.AttributesRecord{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
testDecisions []authorizer.ConditionsAwareDecision
|
||||
wantIsAllowed bool
|
||||
wantIsNoOpinion bool
|
||||
wantIsDenied bool
|
||||
wantIsUnconditional bool
|
||||
wantFailClosedIsDeny bool
|
||||
wantReason string
|
||||
wantAnyError bool
|
||||
wantErrorIs error
|
||||
wantString string
|
||||
}{
|
||||
{
|
||||
name: "zero value",
|
||||
testDecisions: []authorizer.ConditionsAwareDecision{
|
||||
{},
|
||||
authorizer.ConditionsAwareDecisionFromParts(0, "", nil),
|
||||
authorizer.AuthorizerFunc(func(_ context.Context, _ authorizer.Attributes) (named1 authorizer.Decision, named2 string, named3 error) {
|
||||
return
|
||||
}).ConditionsAwareAuthorize(ctx, sampleAttrs),
|
||||
},
|
||||
wantIsDenied: true,
|
||||
wantIsUnconditional: true,
|
||||
wantFailClosedIsDeny: true,
|
||||
wantReason: "",
|
||||
wantErrorIs: nil,
|
||||
wantString: `Deny`,
|
||||
},
|
||||
{
|
||||
name: "deny constructor",
|
||||
testDecisions: []authorizer.ConditionsAwareDecision{
|
||||
authorizer.ConditionsAwareDecisionDeny("foo", unexpectedErr),
|
||||
authorizer.ConditionsAwareDecisionFromParts(authorizer.DecisionDeny, "foo", unexpectedErr),
|
||||
authorizer.AuthorizerFunc(func(_ context.Context, _ authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
return authorizer.DecisionDeny, "foo", unexpectedErr
|
||||
}).ConditionsAwareAuthorize(ctx, sampleAttrs),
|
||||
},
|
||||
wantIsDenied: true,
|
||||
wantIsUnconditional: true,
|
||||
wantFailClosedIsDeny: true,
|
||||
wantReason: "foo",
|
||||
wantErrorIs: unexpectedErr,
|
||||
wantString: `Deny(reason="foo", err="unexpected things happened")`,
|
||||
},
|
||||
{
|
||||
name: "allow constructor",
|
||||
testDecisions: []authorizer.ConditionsAwareDecision{
|
||||
authorizer.ConditionsAwareDecisionAllow("ok", nil),
|
||||
authorizer.ConditionsAwareDecisionFromParts(authorizer.DecisionAllow, "ok", nil),
|
||||
authorizer.AuthorizerFunc(func(_ context.Context, _ authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
return authorizer.DecisionAllow, "ok", nil
|
||||
}).ConditionsAwareAuthorize(ctx, sampleAttrs),
|
||||
},
|
||||
wantIsAllowed: true,
|
||||
wantIsUnconditional: true,
|
||||
wantReason: "ok",
|
||||
wantErrorIs: nil,
|
||||
wantString: `Allow(reason="ok")`,
|
||||
},
|
||||
{
|
||||
name: "noopinion constructor",
|
||||
testDecisions: []authorizer.ConditionsAwareDecision{
|
||||
authorizer.ConditionsAwareDecisionNoOpinion("", nil),
|
||||
authorizer.ConditionsAwareDecisionFromParts(authorizer.DecisionNoOpinion, "", nil),
|
||||
authorizer.AuthorizerFunc(func(_ context.Context, _ authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}).ConditionsAwareAuthorize(ctx, sampleAttrs),
|
||||
},
|
||||
wantIsNoOpinion: true,
|
||||
wantIsUnconditional: true,
|
||||
wantReason: "",
|
||||
wantErrorIs: nil,
|
||||
wantString: `NoOpinion`,
|
||||
},
|
||||
{
|
||||
name: "from parts: unsupported mode",
|
||||
testDecisions: []authorizer.ConditionsAwareDecision{
|
||||
authorizer.ConditionsAwareDecisionFromParts(42, "", nil),
|
||||
authorizer.AuthorizerFunc(func(_ context.Context, _ authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
return 42, "", nil
|
||||
}).ConditionsAwareAuthorize(ctx, sampleAttrs),
|
||||
},
|
||||
wantIsDenied: true,
|
||||
wantIsUnconditional: true,
|
||||
wantFailClosedIsDeny: true,
|
||||
wantReason: "",
|
||||
wantAnyError: true,
|
||||
wantString: `Deny(err="unknown unconditional decision type: 42")`,
|
||||
},
|
||||
{
|
||||
name: "from parts: unsupported mode with other error",
|
||||
testDecisions: []authorizer.ConditionsAwareDecision{
|
||||
authorizer.ConditionsAwareDecisionFromParts(42, "foo", otherErr),
|
||||
authorizer.AuthorizerFunc(func(_ context.Context, _ authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
return 42, "foo", otherErr
|
||||
}).ConditionsAwareAuthorize(ctx, sampleAttrs),
|
||||
},
|
||||
wantIsDenied: true,
|
||||
wantIsUnconditional: true,
|
||||
wantFailClosedIsDeny: true,
|
||||
wantReason: "foo",
|
||||
wantErrorIs: otherErr,
|
||||
wantString: `Deny(reason="foo", err="[other error, unknown unconditional decision type: 42]")`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
for i, d := range tt.testDecisions {
|
||||
t.Run(fmt.Sprint(i), func(t *testing.T) {
|
||||
isAllowed := d.IsAllowed()
|
||||
if isAllowed != tt.wantIsAllowed {
|
||||
t.Errorf("IsAllowed() = %v, want %v", isAllowed, tt.wantIsAllowed)
|
||||
}
|
||||
isNoOpinion := d.IsNoOpinion()
|
||||
if isNoOpinion != tt.wantIsNoOpinion {
|
||||
t.Errorf("IsNoOpinion() = %v, want %v", isNoOpinion, tt.wantIsNoOpinion)
|
||||
}
|
||||
isDenied := d.IsDenied()
|
||||
if isDenied != tt.wantIsDenied {
|
||||
t.Errorf("IsDenied() = %v, want %v", isDenied, tt.wantIsDenied)
|
||||
}
|
||||
isUnconditional := d.IsUnconditional()
|
||||
if isUnconditional != tt.wantIsUnconditional {
|
||||
t.Errorf("IsUnconditional() = %v, want %v", isUnconditional, tt.wantIsUnconditional)
|
||||
}
|
||||
gotReason := d.Reason()
|
||||
if gotReason != tt.wantReason {
|
||||
t.Errorf("Reason() = %v, want %v", gotReason, tt.wantReason)
|
||||
}
|
||||
gotError := d.Error()
|
||||
if tt.wantAnyError {
|
||||
if gotError == nil {
|
||||
t.Errorf("Error() = %v, want some error", nil)
|
||||
}
|
||||
} else {
|
||||
if !errors.Is(gotError, tt.wantErrorIs) {
|
||||
t.Errorf("Error() = %v, want %v", gotError, tt.wantErrorIs)
|
||||
}
|
||||
}
|
||||
failClosed := d.FailClosedDecision()
|
||||
if tt.wantFailClosedIsDeny {
|
||||
if failClosed != authorizer.DecisionDeny {
|
||||
t.Errorf("want FailClosedDecision() == Deny; got %s", failClosed)
|
||||
}
|
||||
} else {
|
||||
if failClosed != authorizer.DecisionNoOpinion {
|
||||
t.Errorf("want FailClosedDecision() == NoOpinion; got %s", failClosed)
|
||||
}
|
||||
}
|
||||
|
||||
gotString := d.String()
|
||||
if gotString != tt.wantString {
|
||||
t.Errorf("String() = %v, want %v", gotString, tt.wantString)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -26,8 +26,9 @@ import (
|
|||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
)
|
||||
|
||||
// Attributes is an interface used by an Authorizer to get information about a request
|
||||
// that is used to make an authorization decision.
|
||||
// Attributes is an interface used by an Authorizer to get information about a
|
||||
// request's metadata, that is used to compute an unconditional or conditional
|
||||
// authorization decision.
|
||||
type Attributes interface {
|
||||
// GetUser returns the user.Info object to authorize
|
||||
GetUser() user.Info
|
||||
|
|
@ -99,9 +100,24 @@ type UnconditionalAuthorizer interface {
|
|||
type Authorizer interface {
|
||||
UnconditionalAuthorizer
|
||||
|
||||
// TODO(luxas): Add the conditions-aware methods in a follow-up PR.
|
||||
// ConditionsAwareAuthorize returns an unconditional, conditional, or unioned
|
||||
// decision, where the error and reason is part of the Decision struct.
|
||||
//
|
||||
// An authorizer who is not conditions-aware MUST implement this function as
|
||||
// "return authorizer.ConditionsAwareDecisionFromParts(self.Authorize(ctx, a))",
|
||||
// such that conditions-aware callers to this authorizer get the same output
|
||||
// as if they called Authorize. Callers are only expected to call one of
|
||||
// Authorize or ConditionsAwareAuthorize, not both.
|
||||
ConditionsAwareAuthorize(ctx context.Context, a Attributes) ConditionsAwareDecision
|
||||
|
||||
// EvaluateConditions evaluates a conditional or unioned ConditionsAwareDecision against previously-unknown data.
|
||||
//
|
||||
// An authorizer who does not support conditions should fail closed and
|
||||
// return authorizer.DecisionDeny, "", authorizer.ErrorConditionEvaluationNotSupported
|
||||
EvaluateConditions(ctx context.Context, decision ConditionsAwareDecision, data ConditionsData) (authorized Decision, reason string, err error)
|
||||
}
|
||||
|
||||
// AuthorizerFunc implements Authorizer
|
||||
var _ Authorizer = AuthorizerFunc(nil)
|
||||
|
||||
type AuthorizerFunc func(ctx context.Context, a Attributes) (Decision, string, error)
|
||||
|
|
@ -110,6 +126,14 @@ func (f AuthorizerFunc) Authorize(ctx context.Context, a Attributes) (Decision,
|
|||
return f(ctx, a)
|
||||
}
|
||||
|
||||
func (f AuthorizerFunc) ConditionsAwareAuthorize(ctx context.Context, a Attributes) ConditionsAwareDecision {
|
||||
return ConditionsAwareDecisionFromParts(f.Authorize(ctx, a))
|
||||
}
|
||||
|
||||
func (f AuthorizerFunc) EvaluateConditions(_ context.Context, _ ConditionsAwareDecision, _ ConditionsData) (Decision, string, error) {
|
||||
return DecisionDeny, "", ErrorConditionEvaluationNotSupported
|
||||
}
|
||||
|
||||
// RuleResolver provides a mechanism for resolving the list of rules that apply to a given user within a namespace.
|
||||
type RuleResolver interface {
|
||||
// RulesFor get the list of cluster wide rules, the list of rules in the specific namespace, incomplete status and errors.
|
||||
|
|
@ -192,6 +216,8 @@ func (a AttributesRecord) GetLabelSelector() (labels.Requirements, error) {
|
|||
return a.LabelSelectorRequirements, a.LabelSelectorParsingErr
|
||||
}
|
||||
|
||||
// Decision represents an final, unconditional authorization decision.
|
||||
// The zero value (0) of Decision is DecisionDeny.
|
||||
type Decision int
|
||||
|
||||
const (
|
||||
|
|
@ -200,7 +226,8 @@ const (
|
|||
// DecisionAllow means that an authorizer decided to allow the action.
|
||||
DecisionAllow
|
||||
// DecisionNoOpinion means that an authorizer has no opinion on whether
|
||||
// to allow or deny an action.
|
||||
// to allow or deny an action. If there are multiple unioned authorizers,
|
||||
// this means that the request can thus get allowed by some later authorizer.
|
||||
DecisionNoOpinion
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue