306 lines
8.2 KiB
Go
306 lines
8.2 KiB
Go
/*
|
|
* Copyright 2024 the KubeSphere Authors.
|
|
* Please refer to the LICENSE file in the root directory of the project.
|
|
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
|
*/
|
|
|
|
package rbac
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/go-logr/logr"
|
|
"github.com/open-policy-agent/opa/ast"
|
|
corev1 "k8s.io/api/core/v1"
|
|
rbacv1 "k8s.io/api/rbac/v1"
|
|
"k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/client-go/tools/record"
|
|
iamv1beta1 "kubesphere.io/api/iam/v1beta1"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
"kubesphere.io/kubesphere/pkg/utils/sliceutil"
|
|
)
|
|
|
|
const defaultRegoFileName = "authz.rego"
|
|
|
|
const (
|
|
AggregateRoleTemplateFailed = "AggregateRoleTemplateFailed"
|
|
MessageResourceSynced = "Aggregating roleTemplates successfully"
|
|
)
|
|
|
|
type Helper struct {
|
|
client.Client
|
|
}
|
|
|
|
func NewHelper(c client.Client) *Helper {
|
|
return &Helper{c}
|
|
}
|
|
|
|
func (h *Helper) aggregateRoleTemplateRule(roleTemplates []iamv1beta1.RoleTemplate) ([]rbacv1.PolicyRule, []string, error) {
|
|
rules := []rbacv1.PolicyRule{}
|
|
newTemplateNames := []string{}
|
|
for _, rt := range roleTemplates {
|
|
newTemplateNames = append(newTemplateNames, rt.Name)
|
|
for _, rule := range rt.Spec.Rules {
|
|
if !ruleExists(rules, rule) {
|
|
rules = append(rules, rule)
|
|
}
|
|
}
|
|
}
|
|
|
|
return rules, newTemplateNames, nil
|
|
}
|
|
|
|
func (h *Helper) aggregateRoleTemplateRegoPolicy(roleTemplates []iamv1beta1.RoleTemplate) (string, error) {
|
|
mergedPolicy := &ast.Module{
|
|
Rules: make([]*ast.Rule, 0),
|
|
}
|
|
for _, rt := range roleTemplates {
|
|
rawPolicy := rt.Annotations[iamv1beta1.RegoOverrideAnnotation]
|
|
if rawPolicy == "" {
|
|
continue
|
|
}
|
|
module, err := ast.ParseModule(defaultRegoFileName, rawPolicy)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if mergedPolicy.Package == nil {
|
|
mergedPolicy.Package = module.Package
|
|
}
|
|
if module != nil {
|
|
mergedPolicy.Rules = append(mergedPolicy.Rules, module.Rules...)
|
|
}
|
|
}
|
|
|
|
if len(mergedPolicy.Rules) == 0 {
|
|
return "", nil
|
|
}
|
|
|
|
seenRules := make(map[string]struct{})
|
|
|
|
uniqueMergedPolicy := &ast.Module{
|
|
Package: mergedPolicy.Package,
|
|
Rules: make([]*ast.Rule, 0),
|
|
}
|
|
|
|
for _, rule := range mergedPolicy.Rules {
|
|
ruleString := rule.String()
|
|
if _, seen := seenRules[ruleString]; !seen {
|
|
uniqueMergedPolicy.Rules = append(uniqueMergedPolicy.Rules, rule)
|
|
seenRules[ruleString] = struct{}{}
|
|
}
|
|
}
|
|
return uniqueMergedPolicy.String(), nil
|
|
}
|
|
|
|
func (h *Helper) getRoleTemplates(ctx context.Context, owner RuleOwner) ([]iamv1beta1.RoleTemplate, error) {
|
|
aggregationRule := owner.GetAggregationRule()
|
|
logger := logr.FromContextOrDiscard(ctx)
|
|
|
|
if aggregationRule.RoleSelector == nil {
|
|
roletemplates := []iamv1beta1.RoleTemplate{}
|
|
for _, templateName := range aggregationRule.TemplateNames {
|
|
roleTemplate := &iamv1beta1.RoleTemplate{}
|
|
if err := h.Get(ctx, types.NamespacedName{Name: templateName}, roleTemplate); err != nil {
|
|
if errors.IsNotFound(err) {
|
|
logger.V(4).Info("aggregation role template not found", "name", templateName, "role", owner.GetObject())
|
|
continue
|
|
}
|
|
return nil, err
|
|
}
|
|
roletemplates = append(roletemplates, *roleTemplate)
|
|
}
|
|
return roletemplates, nil
|
|
}
|
|
|
|
selector := aggregationRule.RoleSelector.DeepCopy()
|
|
roleTemplateList := &iamv1beta1.RoleTemplateList{}
|
|
// Ensure the roleTemplate can be aggregated at the specific role scope
|
|
selector.MatchLabels = labels.Merge(selector.MatchLabels, map[string]string{iamv1beta1.ScopeLabel: owner.GetRuleOwnerScope()})
|
|
asSelector, err := metav1.LabelSelectorAsSelector(selector)
|
|
if err != nil {
|
|
logger.Error(err, "failed to parse role selector", "scope", owner.GetRuleOwnerScope(), "name", owner.GetName())
|
|
return nil, err
|
|
}
|
|
if err = h.List(ctx, roleTemplateList, &client.ListOptions{LabelSelector: asSelector}); err != nil {
|
|
return nil, err
|
|
}
|
|
return roleTemplateList.Items, nil
|
|
}
|
|
|
|
func (h *Helper) AggregationRole(ctx context.Context, ruleOwner RuleOwner, recorder record.EventRecorder) error {
|
|
var needUpdate bool
|
|
if ruleOwner.GetAggregationRule() == nil {
|
|
return nil
|
|
}
|
|
templates, err := h.getRoleTemplates(ctx, ruleOwner)
|
|
if err != nil {
|
|
recorder.Event(ruleOwner.GetObject(), corev1.EventTypeWarning, AggregateRoleTemplateFailed, err.Error())
|
|
return err
|
|
}
|
|
newPolicyRules, newTemplateNames, err := h.aggregateRoleTemplateRule(templates)
|
|
if err != nil {
|
|
recorder.Event(ruleOwner.GetObject(), corev1.EventTypeWarning, AggregateRoleTemplateFailed, err.Error())
|
|
return err
|
|
}
|
|
|
|
cover, uncovered := Covers(ruleOwner.GetRules(), newPolicyRules)
|
|
|
|
aggregationRule := ruleOwner.GetAggregationRule()
|
|
templateNamesEqual := false
|
|
if aggregationRule != nil {
|
|
templateNamesEqual = sliceutil.Equal(aggregationRule.TemplateNames, newTemplateNames)
|
|
}
|
|
|
|
if !cover {
|
|
needUpdate = true
|
|
newRule := append(ruleOwner.GetRules(), uncovered...)
|
|
ruleOwner.SetRules(newRule)
|
|
}
|
|
|
|
if !templateNamesEqual {
|
|
needUpdate = true
|
|
aggregationRule.TemplateNames = newTemplateNames
|
|
ruleOwner.SetAggregationRule(aggregationRule)
|
|
}
|
|
|
|
newRegoPolicy, err := h.aggregateRoleTemplateRegoPolicy(templates)
|
|
if err != nil {
|
|
recorder.Event(ruleOwner.GetObject(), corev1.EventTypeWarning, AggregateRoleTemplateFailed, err.Error())
|
|
return err
|
|
}
|
|
|
|
policyCover, err := regoPolicyCover(ruleOwner.GetRegoPolicy(), newRegoPolicy)
|
|
if err != nil {
|
|
recorder.Event(ruleOwner.GetObject(), corev1.EventTypeWarning, AggregateRoleTemplateFailed, err.Error())
|
|
return err
|
|
}
|
|
if !policyCover {
|
|
needUpdate = true
|
|
ruleOwner.SetRegoPolicy(newRegoPolicy)
|
|
}
|
|
if needUpdate {
|
|
if err = h.Update(ctx, ruleOwner.GetObject().(client.Object)); err != nil {
|
|
recorder.Event(ruleOwner.GetObject(), corev1.EventTypeWarning, AggregateRoleTemplateFailed, err.Error())
|
|
return err
|
|
}
|
|
recorder.Event(ruleOwner.GetObject(), corev1.EventTypeNormal, "Synced", MessageResourceSynced)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ruleExists(haystack []rbacv1.PolicyRule, needle rbacv1.PolicyRule) bool {
|
|
covers, _ := Covers(haystack, []rbacv1.PolicyRule{needle})
|
|
return covers
|
|
}
|
|
|
|
func regoPolicyCover(owner, servant string) (bool, error) {
|
|
if servant == "" {
|
|
return true, nil
|
|
}
|
|
|
|
if owner == "" && servant != "" {
|
|
return false, nil
|
|
}
|
|
|
|
ownerModule, err := ast.ParseModule(defaultRegoFileName, owner)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
servantModule, err := ast.ParseModule(defaultRegoFileName, servant)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
cover := ownerModule.Compare(servantModule) >= 0
|
|
|
|
return cover, nil
|
|
}
|
|
|
|
func SquashRules(deep int, rules []rbacv1.PolicyRule) []rbacv1.PolicyRule {
|
|
var resultRules []rbacv1.PolicyRule
|
|
for _, rule := range rules {
|
|
merged := false
|
|
if cover, _ := Covers(resultRules, []rbacv1.PolicyRule{rule}); cover {
|
|
continue
|
|
}
|
|
for i, rRule := range resultRules {
|
|
if (containRules(rRule.APIGroups, rule.APIGroups) && equalRules(rRule.Resources, rule.Resources)) ||
|
|
(containRules(rRule.APIGroups, rule.APIGroups) && equalRules(rRule.Verbs, rule.Verbs)) {
|
|
merged = true
|
|
resultRules[i] = mergeRules(rRule, rule)
|
|
break
|
|
}
|
|
}
|
|
|
|
if !merged {
|
|
resultRules = append(resultRules, rule)
|
|
}
|
|
}
|
|
|
|
if len(resultRules) == deep {
|
|
return resultRules
|
|
}
|
|
return SquashRules(len(resultRules), resultRules)
|
|
}
|
|
|
|
func mergeRules(base, rule rbacv1.PolicyRule) rbacv1.PolicyRule {
|
|
if !sliceutil.HasString(base.APIGroups, "*") {
|
|
base.APIGroups = merge(base.APIGroups, rule.APIGroups)
|
|
}
|
|
if !sliceutil.HasString(base.Resources, "*") {
|
|
base.Resources = merge(base.Resources, rule.Resources)
|
|
}
|
|
if !sliceutil.HasString(base.Verbs, "*") {
|
|
base.Verbs = merge(base.Verbs, rule.Verbs)
|
|
}
|
|
return base
|
|
}
|
|
|
|
func merge(base, rule []string) []string {
|
|
for _, r := range rule {
|
|
if !sliceutil.HasString(base, r) {
|
|
base = append(base, r)
|
|
}
|
|
}
|
|
return base
|
|
}
|
|
|
|
func containRules(base, rule []string) bool {
|
|
if sliceutil.HasString(base, "*") {
|
|
return true
|
|
}
|
|
|
|
for _, b := range base {
|
|
if !sliceutil.HasString(rule, b) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func equalRules(base, rule []string) bool {
|
|
if len(base) != len(rule) {
|
|
return false
|
|
}
|
|
|
|
baseMap := make(map[string]int)
|
|
for _, item := range base {
|
|
baseMap[item]++
|
|
}
|
|
|
|
for _, item := range rule {
|
|
count, exists := baseMap[item]
|
|
if !exists || count == 0 {
|
|
return false
|
|
}
|
|
baseMap[item]--
|
|
}
|
|
|
|
return true
|
|
}
|