Files
kubesphere/pkg/apiserver/authorization/rbac/rbac.go
KubeSphere CI Bot 447a51f08b feat: kubesphere 4.0 (#6115)
* feat: kubesphere 4.0

Signed-off-by: ci-bot <ci-bot@kubesphere.io>

* feat: kubesphere 4.0

Signed-off-by: ci-bot <ci-bot@kubesphere.io>

---------

Signed-off-by: ci-bot <ci-bot@kubesphere.io>
Co-authored-by: ks-ci-bot <ks-ci-bot@example.com>
Co-authored-by: joyceliu <joyceliu@yunify.com>
2024-09-06 11:05:52 +08:00

442 lines
13 KiB
Go

// NOTE: This file is copied from k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac.
package rbac
import (
"bytes"
"context"
"fmt"
"github.com/open-policy-agent/opa/rego"
rbacv1 "k8s.io/api/rbac/v1"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apiserver/pkg/authentication/serviceaccount"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/klog/v2"
corev1alpha1 "kubesphere.io/api/core/v1alpha1"
iamv1beta1 "kubesphere.io/api/iam/v1beta1"
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer"
"kubesphere.io/kubesphere/pkg/apiserver/request"
"kubesphere.io/kubesphere/pkg/models/iam/am"
ksserviceaccount "kubesphere.io/kubesphere/pkg/utils/serviceaccount"
"kubesphere.io/kubesphere/pkg/utils/sliceutil"
)
const (
defaultRegoQuery = "data.authz.allow"
defaultRegoFileName = "authz.rego"
)
type Authorizer struct {
am am.AccessManagementInterface
}
// authorizingVisitor short-circuits once allowed, and collects any resolution errors encountered
type authorizingVisitor struct {
requestAttributes authorizer.Attributes
allowed bool
reason string
errors []error
}
func (v *authorizingVisitor) visit(source fmt.Stringer, regoPolicy string, rule *rbacv1.PolicyRule, err error) bool {
if regoPolicy != "" && regoPolicyAllows(v.requestAttributes, regoPolicy) {
v.allowed = true
v.reason = fmt.Sprintf("RBAC: allowed by %s", source.String())
return false
}
if rule != nil && ruleAllows(v.requestAttributes, rule) {
v.allowed = true
v.reason = fmt.Sprintf("RBAC: allowed by %s", source.String())
return false
}
if err != nil {
v.errors = append(v.errors, err)
}
return true
}
type ruleAccumulator struct {
rules []rbacv1.PolicyRule
errors []error
}
func (r *ruleAccumulator) visit(_ fmt.Stringer, _ string, rule *rbacv1.PolicyRule, err error) bool {
if rule != nil {
r.rules = append(r.rules, *rule)
}
if err != nil {
r.errors = append(r.errors, err)
}
return true
}
func (r *Authorizer) Authorize(requestAttributes authorizer.Attributes) (authorizer.Decision, string, error) {
ruleCheckingVisitor := &authorizingVisitor{requestAttributes: requestAttributes}
r.visitRulesFor(requestAttributes, ruleCheckingVisitor.visit)
if ruleCheckingVisitor.allowed {
return authorizer.DecisionAllow, ruleCheckingVisitor.reason, nil
}
// Build a detailed log of the denial.
// Make the whole block conditional, so we don't do a lot of string-building we won't use.
if klog.V(4).Enabled() {
var operation string
if requestAttributes.IsResourceRequest() {
b := &bytes.Buffer{}
b.WriteString(`"`)
b.WriteString(requestAttributes.GetVerb())
b.WriteString(`" resource "`)
b.WriteString(requestAttributes.GetResource())
if len(requestAttributes.GetAPIGroup()) > 0 {
b.WriteString(`.`)
b.WriteString(requestAttributes.GetAPIGroup())
}
if len(requestAttributes.GetSubresource()) > 0 {
b.WriteString(`/`)
b.WriteString(requestAttributes.GetSubresource())
}
b.WriteString(`"`)
if len(requestAttributes.GetName()) > 0 {
b.WriteString(` named "`)
b.WriteString(requestAttributes.GetName())
b.WriteString(`"`)
}
operation = b.String()
} else {
operation = fmt.Sprintf("%q nonResourceURL %q", requestAttributes.GetVerb(), requestAttributes.GetPath())
}
var scope string
if requestAttributes.GetResourceScope() == request.NamespaceScope {
scope = fmt.Sprintf("in namespace %q", requestAttributes.GetNamespace())
} else if requestAttributes.GetResourceScope() == request.WorkspaceScope {
scope = fmt.Sprintf("in workspace %q", requestAttributes.GetWorkspace())
} else if requestAttributes.GetResourceScope() == request.ClusterScope {
scope = "cluster scope"
} else {
scope = "global-wide"
}
klog.V(4).Infof("RBAC: no rules authorize user %q with groups %q to %s %s", requestAttributes.GetUser().GetName(), requestAttributes.GetUser().GetGroups(), operation, scope)
}
reason := ""
if len(ruleCheckingVisitor.errors) > 0 {
reason = fmt.Sprintf("RBAC: %v", utilerrors.NewAggregate(ruleCheckingVisitor.errors))
}
return authorizer.DecisionNoOpinion, reason, nil
}
func NewRBACAuthorizer(am am.AccessManagementInterface) *Authorizer {
return &Authorizer{am: am}
}
func ruleAllows(requestAttributes authorizer.Attributes, rule *rbacv1.PolicyRule) bool {
if requestAttributes.IsResourceRequest() {
combinedResource := requestAttributes.GetResource()
if len(requestAttributes.GetSubresource()) > 0 {
combinedResource = requestAttributes.GetResource() + "/" + requestAttributes.GetSubresource()
}
return VerbMatches(rule, requestAttributes.GetVerb()) &&
APIGroupMatches(rule, requestAttributes.GetAPIGroup()) &&
ResourceMatches(rule, combinedResource, requestAttributes.GetSubresource()) &&
ResourceNameMatches(rule, requestAttributes.GetName())
}
return VerbMatches(rule, requestAttributes.GetVerb()) &&
NonResourceURLMatches(rule, requestAttributes.GetPath())
}
func regoPolicyAllows(requestAttributes authorizer.Attributes, regoPolicy string) bool {
// Call the rego.New function to create an object that can be prepared or evaluated
// After constructing a new rego.Rego object you can call PrepareForEval() to obtain an executable query
query, err := rego.New(rego.Query(defaultRegoQuery), rego.Module(defaultRegoFileName, regoPolicy)).PrepareForEval(context.Background())
if err != nil {
klog.Warningf("syntax error:%s, content: %s", err, regoPolicy)
return false
}
// The policy decision is contained in the results returned by the Eval() call. You can inspect the decision and handle it accordingly.
results, err := query.Eval(context.Background(), rego.EvalInput(requestAttributes))
if err != nil {
klog.Warningf("syntax error:%s, content: %s", err, regoPolicy)
return false
}
if len(results) > 0 && results[0].Expressions[0].Value == true {
return true
}
return false
}
func (r *Authorizer) rulesFor(requestAttributes authorizer.Attributes) ([]rbacv1.PolicyRule, error) {
visitor := &ruleAccumulator{}
r.visitRulesFor(requestAttributes, visitor.visit)
return visitor.rules, utilerrors.NewAggregate(visitor.errors)
}
func (r *Authorizer) visitRulesFor(requestAttributes authorizer.Attributes, visitor func(source fmt.Stringer, regoPolicy string, rule *rbacv1.PolicyRule, err error) bool) {
if globalRoleBindings, err := r.am.ListGlobalRoleBindings("", ""); err != nil {
visitor(nil, "", nil, err)
return
} else {
sourceDescriber := &globalRoleBindingDescriber{}
for _, globalRoleBinding := range globalRoleBindings {
subjectIndex, applies := appliesTo(requestAttributes.GetUser(), globalRoleBinding.Subjects, "")
if !applies {
continue
}
regoPolicy, rules, err := r.am.GetRoleReferenceRules(globalRoleBinding.RoleRef, "")
if err != nil {
visitor(nil, "", nil, err)
continue
}
sourceDescriber.binding = &globalRoleBinding
sourceDescriber.subject = &globalRoleBinding.Subjects[subjectIndex]
if !visitor(sourceDescriber, regoPolicy, nil, nil) {
return
}
for i := range rules {
if !visitor(sourceDescriber, "", &rules[i], nil) {
return
}
}
}
if requestAttributes.GetResourceScope() == request.GlobalScope {
return
}
}
var targetWorkspace string
if requestAttributes.GetResourceScope() == request.NamespaceScope {
if workspace, err := r.am.GetNamespaceControlledWorkspace(requestAttributes.GetNamespace()); err != nil {
visitor(nil, "", nil, err)
return
} else {
targetWorkspace = workspace
}
}
if requestAttributes.GetResourceScope() == request.WorkspaceScope {
targetWorkspace = requestAttributes.GetWorkspace()
}
// workspace managed resources
if targetWorkspace != "" {
if workspaceRoleBindings, err := r.am.ListWorkspaceRoleBindings("", "", nil, targetWorkspace); err != nil {
visitor(nil, "", nil, err)
return
} else {
sourceDescriber := &workspaceRoleBindingDescriber{}
for _, workspaceRoleBinding := range workspaceRoleBindings {
subjectIndex, applies := appliesTo(requestAttributes.GetUser(), workspaceRoleBinding.Subjects, "")
if !applies {
continue
}
regoPolicy, rules, err := r.am.GetRoleReferenceRules(workspaceRoleBinding.RoleRef, "")
if err != nil {
visitor(nil, "", nil, err)
return
}
sourceDescriber.binding = &workspaceRoleBinding
sourceDescriber.subject = &workspaceRoleBinding.Subjects[subjectIndex]
if !visitor(sourceDescriber, regoPolicy, nil, nil) {
return
}
for i := range rules {
if !visitor(sourceDescriber, "", &rules[i], nil) {
return
}
}
}
}
}
var targetNamespace string
if requestAttributes.GetResourceScope() == request.NamespaceScope {
targetNamespace = requestAttributes.GetNamespace()
}
if targetNamespace != "" {
if roleBindings, err := r.am.ListRoleBindings("", "", nil, targetNamespace); err != nil {
visitor(nil, "", nil, err)
return
} else {
sourceDescriber := &roleBindingDescriber{}
for _, roleBinding := range roleBindings {
subjectIndex, applies := appliesTo(requestAttributes.GetUser(), roleBinding.Subjects, targetNamespace)
if !applies {
continue
}
regoPolicy, rules, err := r.am.GetRoleReferenceRules(roleBinding.RoleRef, targetNamespace)
if err != nil {
visitor(nil, "", nil, err)
return
}
sourceDescriber.binding = &roleBinding
sourceDescriber.subject = &roleBinding.Subjects[subjectIndex]
if !visitor(sourceDescriber, regoPolicy, nil, nil) {
return
}
for i := range rules {
if !visitor(sourceDescriber, "", &rules[i], nil) {
return
}
}
}
}
}
if clusterRoleBindings, err := r.am.ListClusterRoleBindings("", ""); err != nil {
visitor(nil, "", nil, err)
return
} else {
sourceDescriber := &clusterRoleBindingDescriber{}
for _, clusterRoleBinding := range clusterRoleBindings {
subjectIndex, applies := appliesTo(requestAttributes.GetUser(), clusterRoleBinding.Subjects, "")
if !applies {
continue
}
regoPolicy, rules, err := r.am.GetRoleReferenceRules(clusterRoleBinding.RoleRef, "")
if err != nil {
visitor(nil, "", nil, err)
return
}
sourceDescriber.binding = &clusterRoleBinding
sourceDescriber.subject = &clusterRoleBinding.Subjects[subjectIndex]
if !visitor(sourceDescriber, regoPolicy, nil, nil) {
return
}
for i := range rules {
if !visitor(sourceDescriber, "", &rules[i], nil) {
return
}
}
}
}
}
// appliesTo returns whether any of the bindingSubjects applies to the specified subject,
// and if true, the index of the first subject that applies
func appliesTo(user user.Info, bindingSubjects []rbacv1.Subject, namespace string) (int, bool) {
for i, bindingSubject := range bindingSubjects {
if appliesToUser(user, bindingSubject, namespace) {
return i, true
}
}
return 0, false
}
func appliesToUser(user user.Info, subject rbacv1.Subject, namespace string) bool {
switch subject.Kind {
case rbacv1.UserKind:
return user.GetName() == subject.Name
case rbacv1.GroupKind:
return sliceutil.HasString(user.GetGroups(), subject.Name)
case rbacv1.ServiceAccountKind:
// Default the namespace to namespace we're working in if it's available.
// This allows role bindings that reference
// SAs in the local namespace to avoid having to qualify them.
saNamespace := namespace
if len(subject.Namespace) > 0 {
saNamespace = subject.Namespace
}
if len(saNamespace) == 0 {
return false
}
switch subject.APIGroup {
case rbacv1.GroupName:
// use a more efficient comparison for RBAC checking
return serviceaccount.MatchesUsername(saNamespace, subject.Name, user.GetName())
case corev1alpha1.GroupName:
return ksserviceaccount.MatchesUsername(saNamespace, subject.Name, user.GetName())
default:
return false
}
default:
return false
}
}
type globalRoleBindingDescriber struct {
binding *iamv1beta1.GlobalRoleBinding
subject *rbacv1.Subject
}
func (d *globalRoleBindingDescriber) String() string {
return fmt.Sprintf("GlobalRoleBinding %q of %s %q to %s",
d.binding.Name,
d.binding.RoleRef.Kind,
d.binding.RoleRef.Name,
describeSubject(d.subject, ""),
)
}
type clusterRoleBindingDescriber struct {
binding *iamv1beta1.ClusterRoleBinding
subject *rbacv1.Subject
}
func (d *clusterRoleBindingDescriber) String() string {
return fmt.Sprintf("ClusterRoleBinding %q of %s %q to %s",
d.binding.Name,
d.binding.RoleRef.Kind,
d.binding.RoleRef.Name,
describeSubject(d.subject, ""),
)
}
type workspaceRoleBindingDescriber struct {
binding *iamv1beta1.WorkspaceRoleBinding
subject *rbacv1.Subject
}
func (d *workspaceRoleBindingDescriber) String() string {
return fmt.Sprintf("GlobalRoleBinding %q of %s %q to %s",
d.binding.Name,
d.binding.RoleRef.Kind,
d.binding.RoleRef.Name,
describeSubject(d.subject, ""),
)
}
type roleBindingDescriber struct {
binding *iamv1beta1.RoleBinding
subject *rbacv1.Subject
}
func (d *roleBindingDescriber) String() string {
return fmt.Sprintf("RoleBinding %q of %s %q to %s",
d.binding.Name+"/"+d.binding.Namespace,
d.binding.RoleRef.Kind,
d.binding.RoleRef.Name,
describeSubject(d.subject, d.binding.Namespace),
)
}
func describeSubject(s *rbacv1.Subject, bindingNamespace string) string {
switch s.Kind {
case rbacv1.ServiceAccountKind:
if len(s.Namespace) > 0 {
return fmt.Sprintf("%s %q", s.Kind, s.Name+"/"+s.Namespace)
}
return fmt.Sprintf("%s %q", s.Kind, s.Name+"/"+bindingNamespace)
default:
return fmt.Sprintf("%s %q", s.Kind, s.Name)
}
}