Skip to content

Commit

Permalink
pkg/authorization: add audit logging
Browse files Browse the repository at this point in the history
  • Loading branch information
s-urbaniak committed Aug 26, 2022
1 parent 12c88ee commit 802fb93
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 19 deletions.
59 changes: 59 additions & 0 deletions pkg/authorization/apibinding_authorizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/kcp-dev/logicalcluster/v2"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kaudit "k8s.io/apiserver/pkg/audit"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
Expand All @@ -39,6 +40,12 @@ const (
byWorkspaceIndex = "apiBindingAuthorizer-byWorkspace"
)

const (
APIBindingContentAuditPrefix = "apibinding.authorization.kcp.dev/"
APIBindingContentAuditDecision = APIBindingContentAuditPrefix + "decision"
APIBindingContentAuditReason = APIBindingContentAuditPrefix + "reason"
)

// NewAPIBindingAccessAuthorizer returns an authorizer that checks if the 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 resources workspace. If it is not allowed we will return NoDecision, if allowed we
Expand Down Expand Up @@ -98,33 +105,73 @@ func (a *apiBindingAccessAuthorizer) Authorize(ctx context.Context, attr authori
// 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),
)
return authorizer.DecisionNoOpinion, apiBindingAccessDenied, err
}

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

if !bound {
kaudit.AddAuditAnnotations(
ctx,
APIBindingContentAuditDecision, DecisionAllowed,
APIBindingContentAuditReason, "no API binding found",
)
return a.delegate.Authorize(ctx, attr)
}

apiExport, found, err := a.getAPIExport(bindingLogicalCluster)
if err != nil {
kaudit.AddAuditAnnotations(
ctx,
APIBindingContentAuditDecision, DecisionNoOpinion,
APIBindingContentAuditReason, fmt.Sprintf("error getting API export: %v", err),
)
return authorizer.DecisionNoOpinion, apiBindingAccessDenied, err
}

exportName := "unknown"
if bindingLogicalCluster.Workspace != nil {
exportName = bindingLogicalCluster.Workspace.ExportName
}

// If we can't find the export default to close
if !found {
kaudit.AddAuditAnnotations(
ctx,
APIBindingContentAuditDecision, DecisionNoOpinion,
APIBindingContentAuditReason, fmt.Sprintf("API export %q not found", exportName),
)
return authorizer.DecisionNoOpinion, apiBindingAccessDenied, err
}

if apiExport.Spec.MaximalPermissionPolicy == nil {
kaudit.AddAuditAnnotations(
ctx,
APIBindingContentAuditDecision, DecisionAllowed,
APIBindingContentAuditReason, fmt.Sprintf("no maximal permission policy present in API export %q", apiExport.Name),
)
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", apiExport.Name),
)
return a.delegate.Authorize(ctx, attr)
}

Expand All @@ -145,9 +192,21 @@ func (a *apiBindingAccessAuthorizer) Authorize(ctx context.Context, attr authori
}
dec, reason, err := clusterAuthorizer.Authorize(ctx, prefixedAttr)
if err != nil {
kaudit.AddAuditAnnotations(
ctx,
APIBindingContentAuditDecision, DecisionNoOpinion,
APIBindingContentAuditReason, fmt.Sprintf("error authorizing RBAC in API export cluster %q: %v", logicalcluster.From(apiExport), err),
)
return authorizer.DecisionNoOpinion, reason, err
}

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

if dec == authorizer.DecisionAllow {
return a.delegate.Authorize(ctx, attr)
}
Expand Down
36 changes: 34 additions & 2 deletions pkg/authorization/bootstrap_policy_authorizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ limitations under the License.
package authorization

import (
"context"
"fmt"

kaudit "k8s.io/apiserver/pkg/audit"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
clientgoinformers "k8s.io/client-go/informers"
"k8s.io/kubernetes/pkg/genericcontrolplane"
Expand All @@ -25,15 +30,42 @@ import (
rbacwrapper "github.com/kcp-dev/kcp/pkg/virtual/framework/wrappers/rbac"
)

const (
BootstrapPolicyAuditPrefix = "bootstrap.authorization.kcp.dev/"
BootstrapPolicyAuditDecision = BootstrapPolicyAuditPrefix + "decision"
BootstrapPolicyAuditReason = BootstrapPolicyAuditPrefix + "reason"
)

type BootstrapPolicyAuthorizer struct {
delegate *rbac.RBACAuthorizer
}

func NewBootstrapPolicyAuthorizer(informers clientgoinformers.SharedInformerFactory) (authorizer.Authorizer, authorizer.RuleResolver) {
filteredInformer := rbacwrapper.FilterInformers(genericcontrolplane.LocalAdminCluster, informers.Rbac().V1())

a := rbac.New(
a := &BootstrapPolicyAuthorizer{delegate: rbac.New(
&rbac.RoleGetter{Lister: filteredInformer.Roles().Lister()},
&rbac.RoleBindingLister{Lister: filteredInformer.RoleBindings().Lister()},
&rbac.ClusterRoleGetter{Lister: filteredInformer.ClusterRoles().Lister()},
&rbac.ClusterRoleBindingLister{Lister: filteredInformer.ClusterRoleBindings().Lister()},
)
)}

return a, a
}

func (a *BootstrapPolicyAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
dec, reason, err := a.delegate.Authorize(ctx, attr)

decString := decisionString(dec)
kaudit.AddAuditAnnotations(
ctx,
BootstrapPolicyAuditDecision, decString,
BootstrapPolicyAuditReason, fmt.Sprintf("bootstrap policy reason: %v", reason),
)

return dec, reason, err
}

func (a *BootstrapPolicyAuthorizer) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
return a.delegate.RulesFor(user, namespace)
}
37 changes: 37 additions & 0 deletions pkg/authorization/decision.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
Copyright 2022 The KCP 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 authorization

import "k8s.io/apiserver/pkg/authorization/authorizer"

const (
DecisionNoOpinion = "NoOpinion"
DecisionAllowed = "Allowed"
DecisionDenied = "Denied"
)

func decisionString(dec authorizer.Decision) string {
switch dec {
case authorizer.DecisionNoOpinion:
return DecisionNoOpinion
case authorizer.DecisionAllow:
return DecisionAllowed
case authorizer.DecisionDeny:
return DecisionDenied
}
return ""
}
24 changes: 23 additions & 1 deletion pkg/authorization/local_authorizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ package authorization

import (
"context"
"fmt"

kaudit "k8s.io/apiserver/pkg/audit"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
Expand All @@ -30,6 +32,12 @@ import (
rbacwrapper "github.com/kcp-dev/kcp/pkg/virtual/framework/wrappers/rbac"
)

const (
LocalAuditPrefix = "local.authorization.kcp.dev/"
LocalAuditDecision = LocalAuditPrefix + "decision"
LocalAuditReason = LocalAuditPrefix + "reason"
)

type LocalAuthorizer struct {
roleLister rbacv1listers.RoleLister
roleBindingLister rbacv1listers.RoleBindingLister
Expand Down Expand Up @@ -60,6 +68,11 @@ func (a *LocalAuthorizer) RulesFor(user user.Info, namespace string) ([]authoriz
func (a *LocalAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
cluster, err := genericapirequest.ValidClusterFrom(ctx)
if err != nil {
kaudit.AddAuditAnnotations(
ctx,
LocalAuditDecision, DecisionNoOpinion,
LocalAuditReason, fmt.Sprintf("error getting cluster from request: %v", err),
)
return authorizer.DecisionNoOpinion, "", err
}
if cluster == nil || cluster.Name.Empty() {
Expand All @@ -80,5 +93,14 @@ func (a *LocalAuthorizer) Authorize(ctx context.Context, attr authorizer.Attribu
&rbac.ClusterRoleBindingLister{Lister: filteredInformer.ClusterRoleBindings().Lister()},
)

return scopedAuth.Authorize(ctx, attr)
dec, reason, err := scopedAuth.Authorize(ctx, attr)

decString := decisionString(dec)
kaudit.AddAuditAnnotations(
ctx,
LocalAuditDecision, decString,
LocalAuditReason, fmt.Sprintf("cluster %q or bootstrap policy reason: %v", cluster.Name, reason),
)

return dec, reason, err
}
28 changes: 28 additions & 0 deletions pkg/authorization/system_crd_authorizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,21 @@ package authorization

import (
"context"
"fmt"

kaudit "k8s.io/apiserver/pkg/audit"
"k8s.io/apiserver/pkg/authorization/authorizer"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"

apisv1alpha1 "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1"
)

const (
SystemCRDAuditPrefix = "systemcrd.authorization.kcp.dev/"
SystemCRDAuditDecision = SystemCRDAuditPrefix + "decision"
SystemCRDAuditReason = SystemCRDAuditPrefix + "reason"
)

// SystemCRDAuthorizer protects the system CRDs from users who are admins in their workspaces.
type SystemCRDAuthorizer struct {
delegate authorizer.Authorizer
Expand All @@ -39,18 +47,38 @@ func NewSystemCRDAuthorizer(delegate authorizer.Authorizer) authorizer.Authorize
func (a *SystemCRDAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
cluster, err := genericapirequest.ValidClusterFrom(ctx)
if err != nil {
kaudit.AddAuditAnnotations(
ctx,
SystemCRDAuditDecision, DecisionNoOpinion,
SystemCRDAuditReason, fmt.Sprintf("error getting cluster from request: %v", err),
)
return authorizer.DecisionNoOpinion, "", err
}
if cluster == nil || cluster.Name.Empty() {
kaudit.AddAuditAnnotations(
ctx,
SystemCRDAuditDecision, DecisionNoOpinion,
SystemCRDAuditReason, "empty cluster name",
)
return authorizer.DecisionNoOpinion, "", nil
}

switch {
case attr.GetAPIGroup() == apisv1alpha1.SchemeGroupVersion.Group:
switch {
case attr.GetResource() == "apibindings" && attr.GetSubresource() == "status":
kaudit.AddAuditAnnotations(
ctx,
SystemCRDAuditDecision, DecisionDenied,
SystemCRDAuditReason, "apibinding status updates not permitted",
)
return authorizer.DecisionDeny, "status update not permitted", nil
case attr.GetResource() == "apiexports" && attr.GetSubresource() == "status":
kaudit.AddAuditAnnotations(
ctx,
SystemCRDAuditDecision, DecisionDenied,
SystemCRDAuditReason, "apiexport status updates not permitted",
)
return authorizer.DecisionDeny, "status update not permitted", nil
}
}
Expand Down
17 changes: 5 additions & 12 deletions pkg/authorization/toplevel_org_authorizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ type topLevelOrgAccessAuthorizer struct {

func (a *topLevelOrgAccessAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
if IsDeepSubjectAccessReviewFrom(ctx, attr) {
kaudit.AddAuditAnnotations(
ctx,
TopLevelContentAuditDecision, DecisionAllowed,
TopLevelContentAuditReason, "deep SAR request",
)
// this is a deep SAR request, we have to skip the checks here and delegate to the subsequent authorizer.
return a.delegate.Authorize(ctx, attr)
}
Expand Down Expand Up @@ -232,15 +237,3 @@ func topLevelOrg(clusterName logicalcluster.Name) (string, bool) {
clusterName = parent
}
}

func decisionString(dec authorizer.Decision) string {
switch dec {
case authorizer.DecisionNoOpinion:
return DecisionNoOpinion
case authorizer.DecisionAllow:
return DecisionAllowed
case authorizer.DecisionDeny:
return DecisionDenied
}
return ""
}
9 changes: 5 additions & 4 deletions pkg/authorization/workspace_content_authorizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,6 @@ import (
const (
WorkspaceAcccessNotPermittedReason = "workspace access not permitted"

DecisionNoOpinion = "NoOpinion"
DecisionAllowed = "Allowed"
DecisionDenied = "Denied"

WorkspaceContentAuditPrefix = "content.authorization.kcp.dev/"
WorkspaceContentAuditDecision = WorkspaceContentAuditPrefix + "decision"
WorkspaceContentAuditReason = WorkspaceContentAuditPrefix + "reason"
Expand Down Expand Up @@ -126,6 +122,11 @@ func (a *workspaceContentAuthorizer) Authorize(ctx context.Context, attr authori
Groups: []string{"system:authenticated"},
}
}
kaudit.AddAuditAnnotations(
ctx,
WorkspaceContentAuditDecision, DecisionAllowed,
WorkspaceContentAuditReason, "deep SAR request",
)
return a.delegate.Authorize(ctx, attr)
}

Expand Down

0 comments on commit 802fb93

Please sign in to comment.