Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🌱 pkg/authorization: rename apibinding_authorizer to maximal_permission_policy_authorizer #2224

Merged
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
4 changes: 2 additions & 2 deletions config/crds/apis.kcp.dev_apiexports.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ spec:
- local
properties:
local:
description: local is policy that is defined in same namespace
as API Export.
description: local is the policy that is defined in same workspace
as the API Export.
type: object
type: object
permissionClaims:
Expand Down
55 changes: 28 additions & 27 deletions docs/content/en/main/concepts/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,39 +27,40 @@ The details are outlined below.

The following authorizers are configured in kcp:

| Authorizer | Description |
|----------------------------------------|----------------------------------------------------------------|
| Top-Level organization authorizer | checks that the user is allowed to access the organization |
| Workspace content authorizer | determines additional groups a user gets inside of a workspace |
| API binding authorizer | validates the RBAC policy in the api exporters workspace |
| Local Policy authorizer | validates the RBAC policy in the workspace that is accessed |
| Kubernetes Bootstrap Policy authorizer | validates the RBAC Kubernetes standard policy |
| Authorizer | Description |
|----------------------------------------|-----------------------------------------------------------------------------------|
| Top-Level organization authorizer | checks that the user is allowed to access the organization |
| Workspace content authorizer | determines additional groups a user gets inside of a workspace |
| Maximal permission policy authorizer | validates the maximal permission policy RBAC policy in the API exporter workspace |
| Local Policy authorizer | validates the RBAC policy in the workspace that is accessed |
| Kubernetes Bootstrap Policy authorizer | validates the RBAC Kubernetes standard policy |

They are related in the following way:

1. top-level organization authorizer must allow
2. workspace content authorizer must allow, and adds additional (virtual per-request) groups to the request user influencing the follow authorizers.
3. api binding authorizer must allow
3. maximal permission policy authorizer must allow
4. one of the local authorizer or bootstrap policy authorizer must allow.

```
┌──────────────┐
│ │
┌─────────────────┐ ┌───────────────┐ ┌─────────────┐ ┌────►│ Local Policy ├──┐
│ Top-level │ │ │ │ │ │ │ authorizer │ │
request │ Organization │ │ Workspace ┌───┴───┐ │ API Binding │ │ │ │ │
──────────►│ authorizer ├────►│ Content │+groups├───►│ authorizer ├───┤ └──────────────┘ │
│ │ │ authorizer└───┬───┘ │ │ │ ▼
│ │ │ │ └─────────────┘ │ OR───►
└─────────────────┘ └───────────────┘ │ ┌──────────────┐ ▲
│ │ Bootstrap │ │
└────►│ Policy ├──┘
│ authorizer │
│ │
└──────────────┘
┌──────────────┐
│ │
┌─────────────────┐ ┌───────────────┐ ┌───────────────────┐ ┌────►│ Local Policy ├──┐
│ Top-level │ │ │ │ │ │ │ authorizer │ │
request │ Organization │ │ Workspace ┌───┴───┐ │ Max. Permission │ │ │ │ │
──────────►│ authorizer ├────►│ Content │+groups├───►│ Policy authorizer ├───┤ └──────────────┘ │
│ │ │ authorizer└───┬───┘ │ │ │ ▼
│ │ │ │ └───────────────────┘ │ OR───►
└─────────────────┘ └───────────────┘ │ ┌──────────────┐ ▲
│ │ Bootstrap │ │
└────►│ Policy ├──┘
│ authorizer │
│ │
└──────────────┘

```

[ASCIIFlow document](https://asciiflow.com/#/share/eJyrVspLzE1VslLydg5QcCwtycgvyqxKLVLSUcpJrATSVkrVMUoVMUpWlpaGOjFKlUCWkaUZkFWSWlEC5MQoKdAAPJrS82hKA9FoQkxMHm2c0YQhgGoViQ5FuJhM3RPIsXgCFvXTdoE855OfnJijEJCfk5lcCVQyB0eAgpSG5Bfo5qSWpeaghQ1GGCGLoEtC%2BMhaE%2BFJDiYBDeOi1MLS1OISqKh%2FUXpiXmZVYklmfh667eH5RdnFBYnJqWie3IIebiDFjgGeCk6ZeSmZeelYXIPpWIhrCIQwJDBRvALWPweLKuf8vJLUPKi%2FtNOL8ksLilFUYhqGatASqOFTSEk3M7CmXfSYwxU1qJatQTWXUCxjgkfT9pDmEuwyJIbCDLxu8g9CjgF055EU1qiBQ4buGTjciBIqpBWQoEDfRPVSEiWOnPLzS4oVihILFFCyDrVtRA1MSGaBlWBQJfDcMoOW9QJqDqW%2BV5GsQhOgmVWkFSkxSrVKtQBJrg9s)
[ASCIIFlow document](https://asciiflow.com/#/share/eJy1VUFLwzAU%2FislV6egB6G96Y4qKyJ4ySWUMItdU9NM2o2BePbQQxk97OjRk3iS%2FZr9Ets125omxWaQEmj68vLe974vL52DEE0wcMDN0LWupuyJUH%2BGKRiAAKXl2wFzCBIIHNs%2BH0CQlrML%2B7KcMZyw8gMCy9izyT82%2BVvvkUEYmgTzLhnEhJpwD7iP3J3pJV53jEwRYflblXtLPBRYLgl8Ly1dVh1EV64PJDoN8CsOWmxJrDUt8uLB2gyA9sdyt8C5p%2FhlimPGrSM6RqE%2FQ8wnYRvDI6HPcYQ83Cr1p81n5XyHkjPLxXTix3EdS8YkQ64x%2FaNCTaxQ0Hb%2FSuE1JCHDIa%2FuZEzJNIoFz9qN69OIKYb75ClynbNVKM%2B3LJdaJjHZlxi3n%2B6K9eVaD496pR8XXc3CS%2BhGObpv6tMGrKWBSNoRu4sOjAq29C7aSoxvQ7etoOA1ISxmFEWW0GRm8or01s216y7usu%2Brwvy%2FpnlJmCu7kbBlMJxQ7zqCYAEWf%2BVCNyA%3D))

### Top-Level Organization authorizer

Expand Down Expand Up @@ -172,9 +173,9 @@ parent workspace.

Service accounts declared within a workspace don't have access to initializing workspaces.

### API binding authorizer
### Maximal permission policy authorizer

If the requested resource type is part of an API binding, then the API binding authorizer verifies that
If the requested resource type is part of an API binding, then this authorizer verifies that
the request is not exceeding the maximum permission policy of the related API export.
Currently, the "local policy" maximum permission policy type is supported.

Expand All @@ -188,9 +189,9 @@ Example:

Given an API binding for type `foo` declared in workspace `consumer` that refers to an API export declared in workspace `provider`
and a user `user-1` having the group `group-1` requesting a `create` of `foo` in the `default` namespace in the `consumer` workspace,
the API binding authorizer verifies that `user-1` is allowed to execute this request by delegating to `provider`'s RBAC using prefixed attributes.
this authorizer verifies that `user-1` is allowed to execute this request by delegating to `provider`'s RBAC using prefixed attributes.

Here, the API binding authorizer prepends the `apis.kcp.dev:binding:` prefix to the username and all groups the user belongs to.
Here, this authorizer prepends the `apis.kcp.dev:binding:` prefix to the username and all groups the user belongs to.
Using prefixed attributes prevents RBAC collisions i.e. if `user-1` is granted to execute requests within the `provider` workspace directly.

For the given example RBAC request looks as follows:
Expand Down
9 changes: 6 additions & 3 deletions pkg/apis/apis/v1alpha1/types_apiexport.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,13 +169,16 @@ type Identity struct {

// MaximalPermissionPolicy is a wrapper type around the multiple options that would be allowed.
type MaximalPermissionPolicy struct {
// local is policy that is defined in same namespace as API Export.
// local is the policy that is defined in same workspace as the API Export.
// +optional
Local *LocalAPIExportPolicy `json:"local,omitempty"`
}

// LocalAPIExportPolicy will tell the APIBinding authorizer to check policy in the local namespace
// of the API Export
// LocalAPIExportPolicy is a maximal permission policy
// that checks RBAC in the workspace of the API Export.
//
// In order to avoid conflicts the user and group name will be prefixed
// with "apis.kcp.dev:binding:".
type LocalAPIExportPolicy struct{}

const (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,16 @@ import (
)

const (
APIBindingContentAuditPrefix = "apibinding.authorization.kcp.dev/"
APIBindingContentAuditDecision = APIBindingContentAuditPrefix + "decision"
APIBindingContentAuditReason = APIBindingContentAuditPrefix + "reason"
MaximalPermissionPolicyAccessNotPermittedReason = "access not permitted by maximal permission policy"

MaximalPermissionPolicyAuditPrefix = "maxpermissionpolicy.authorization.kcp.dev/"
MaximalPermissionPolicyAuditDecision = MaximalPermissionPolicyAuditPrefix + "decision"
MaximalPermissionPolicyAuditReason = MaximalPermissionPolicyAuditPrefix + "reason"
)

// NewAPIBindingAccessAuthorizer returns an authorizer that checks if the request is for a
// bound resource or not. If the resource is bound we will check the user has RBAC access in the
// exported resource workspace. If it is not allowed we will return NoDecision, if allowed we
// will call the delegate authorizer.
func NewAPIBindingAccessAuthorizer(kubeInformers kubernetesinformers.SharedInformerFactory, kcpInformers kcpinformers.SharedInformerFactory, delegate authorizer.Authorizer) (authorizer.Authorizer, error) {
// NewMaximalPermissionPolicyAuthorizer returns an authorizer that first checks if the request is for a
// bound resource or not. If the resource is bound it checks the maximal permission policy of the underlying API export.
func NewMaximalPermissionPolicyAuthorizer(kubeInformers kubernetesinformers.SharedInformerFactory, kcpInformers kcpinformers.SharedInformerFactory, delegate authorizer.Authorizer) (authorizer.Authorizer, error) {
apiBindingIndexer := kcpInformers.Apis().V1alpha1().APIBindings().Informer().GetIndexer()
apiExportIndexer := kcpInformers.Apis().V1alpha1().APIExports().Informer().GetIndexer()

Expand All @@ -57,7 +57,7 @@ func NewAPIBindingAccessAuthorizer(kubeInformers kubernetesinformers.SharedInfor
kubeInformers.Rbac().V1().ClusterRoles().Lister()
kubeInformers.Rbac().V1().ClusterRoleBindings().Lister()

return &apiBindingAccessAuthorizer{
return &MaximalPermissionPolicyAuthorizer{
getAPIBindingReferenceForAttributes: func(attr authorizer.Attributes, clusterName logicalcluster.Name) (*apisv1alpha1.ExportReference, bool, error) {
return getAPIBindingReferenceForAttributes(apiBindingIndexer, attr, clusterName)
},
Expand All @@ -83,43 +83,41 @@ func NewAPIBindingAccessAuthorizer(kubeInformers kubernetesinformers.SharedInfor
}, nil
}

type apiBindingAccessAuthorizer struct {
type MaximalPermissionPolicyAuthorizer struct {
delegate authorizer.Authorizer

getAPIBindingReferenceForAttributes func(attr authorizer.Attributes, clusterName logicalcluster.Name) (ref *apisv1alpha1.ExportReference, found bool, err error)
getAPIExportByReference func(exportRef *apisv1alpha1.ExportReference) (ref *apisv1alpha1.APIExport, found bool, err error)
newAuthorizer func(clusterName logicalcluster.Name) authorizer.Authorizer
}

func (a *apiBindingAccessAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (authorizer.Decision, string, error) {
apiBindingAccessDenied := "bound api access is not permitted"

func (a *MaximalPermissionPolicyAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (authorizer.Decision, string, error) {
// get the cluster from the ctx.
lcluster, err := genericapirequest.ClusterNameFrom(ctx)
if err != nil {
kaudit.AddAuditAnnotations(
ctx,
APIBindingContentAuditDecision, DecisionNoOpinion,
APIBindingContentAuditReason, fmt.Sprintf("error getting cluster from request: %v", err),
MaximalPermissionPolicyAuditDecision, DecisionNoOpinion,
MaximalPermissionPolicyAuditReason, fmt.Sprintf("error getting cluster from request: %v", err),
)
return authorizer.DecisionNoOpinion, apiBindingAccessDenied, err
return authorizer.DecisionNoOpinion, MaximalPermissionPolicyAccessNotPermittedReason, err
}

bindingLogicalCluster, bound, err := a.getAPIBindingReferenceForAttributes(attr, lcluster)
if err != nil {
kaudit.AddAuditAnnotations(
ctx,
APIBindingContentAuditDecision, DecisionNoOpinion,
APIBindingContentAuditReason, fmt.Sprintf("error getting API binding reference: %v", err),
MaximalPermissionPolicyAuditDecision, DecisionNoOpinion,
MaximalPermissionPolicyAuditReason, fmt.Sprintf("error getting API binding reference: %v", err),
)
return authorizer.DecisionNoOpinion, apiBindingAccessDenied, err
return authorizer.DecisionNoOpinion, MaximalPermissionPolicyAccessNotPermittedReason, err
}

if !bound {
kaudit.AddAuditAnnotations(
ctx,
APIBindingContentAuditDecision, DecisionAllowed,
APIBindingContentAuditReason, "no API binding bound",
MaximalPermissionPolicyAuditDecision, DecisionAllowed,
MaximalPermissionPolicyAuditReason, "no API binding bound",
)
return a.delegate.Authorize(ctx, attr)
}
Expand All @@ -128,10 +126,10 @@ func (a *apiBindingAccessAuthorizer) Authorize(ctx context.Context, attr authori
if err != nil {
kaudit.AddAuditAnnotations(
ctx,
APIBindingContentAuditDecision, DecisionNoOpinion,
APIBindingContentAuditReason, fmt.Sprintf("error getting API export: %v", err),
MaximalPermissionPolicyAuditDecision, DecisionNoOpinion,
MaximalPermissionPolicyAuditReason, fmt.Sprintf("error getting API export: %v", err),
)
return authorizer.DecisionNoOpinion, apiBindingAccessDenied, err
return authorizer.DecisionNoOpinion, MaximalPermissionPolicyAccessNotPermittedReason, err
}

path := "unknown"
Expand All @@ -145,26 +143,26 @@ func (a *apiBindingAccessAuthorizer) Authorize(ctx context.Context, attr authori
if !found {
kaudit.AddAuditAnnotations(
ctx,
APIBindingContentAuditDecision, DecisionNoOpinion,
APIBindingContentAuditReason, fmt.Sprintf("API export %q not found, path: %q", exportName, path),
MaximalPermissionPolicyAuditDecision, DecisionNoOpinion,
MaximalPermissionPolicyAuditReason, fmt.Sprintf("API export %q not found, path: %q", exportName, path),
)
return authorizer.DecisionNoOpinion, apiBindingAccessDenied, err
return authorizer.DecisionNoOpinion, MaximalPermissionPolicyAccessNotPermittedReason, err
}

if apiExport.Spec.MaximalPermissionPolicy == nil {
kaudit.AddAuditAnnotations(
ctx,
APIBindingContentAuditDecision, DecisionAllowed,
APIBindingContentAuditReason, fmt.Sprintf("no maximal permission policy present in API export %q, path: %q, owning cluster: %q", exportName, path, logicalcluster.From(apiExport)),
MaximalPermissionPolicyAuditDecision, DecisionAllowed,
MaximalPermissionPolicyAuditReason, fmt.Sprintf("no maximal permission policy present in API export %q, path: %q, owning cluster: %q", exportName, path, logicalcluster.From(apiExport)),
)
return a.delegate.Authorize(ctx, attr)
}

if apiExport.Spec.MaximalPermissionPolicy.Local == nil {
kaudit.AddAuditAnnotations(
ctx,
APIBindingContentAuditDecision, DecisionAllowed,
APIBindingContentAuditReason, fmt.Sprintf("no maximal local permission policy present in API export %q, path: %q, owning cluster: %q", apiExport.Name, path, logicalcluster.From(apiExport)),
MaximalPermissionPolicyAuditDecision, DecisionAllowed,
MaximalPermissionPolicyAuditReason, fmt.Sprintf("no maximal local permission policy present in API export %q, path: %q, owning cluster: %q", apiExport.Name, path, logicalcluster.From(apiExport)),
)
return a.delegate.Authorize(ctx, attr)
}
Expand All @@ -182,16 +180,16 @@ func (a *apiBindingAccessAuthorizer) Authorize(ctx context.Context, attr authori
if err != nil {
kaudit.AddAuditAnnotations(
ctx,
APIBindingContentAuditDecision, DecisionNoOpinion,
APIBindingContentAuditReason, fmt.Sprintf("error authorizing RBAC in API export cluster %q: %v", logicalcluster.From(apiExport), err),
MaximalPermissionPolicyAuditDecision, DecisionNoOpinion,
MaximalPermissionPolicyAuditReason, fmt.Sprintf("error authorizing RBAC in API export cluster %q: %v", logicalcluster.From(apiExport), err),
)
return authorizer.DecisionNoOpinion, reason, err
}

kaudit.AddAuditAnnotations(
ctx,
APIBindingContentAuditDecision, DecisionString(dec),
APIBindingContentAuditReason, fmt.Sprintf("API export cluster %q reason: %v", logicalcluster.From(apiExport), reason),
MaximalPermissionPolicyAuditDecision, DecisionString(dec),
MaximalPermissionPolicyAuditReason, fmt.Sprintf("API export cluster %q reason: %v", logicalcluster.From(apiExport), reason),
)

if dec == authorizer.DecisionAllow {
Expand Down
4 changes: 2 additions & 2 deletions pkg/openapi/zz_generated.openapi.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading