github.com/argoproj/argo-cd/v3@v3.2.1/util/rbac/rbac.go (about) 1 package rbac 2 3 import ( 4 "context" 5 "encoding/csv" 6 "errors" 7 "fmt" 8 "sort" 9 "strings" 10 "sync" 11 "time" 12 13 "github.com/argoproj/argo-cd/v3/util/assets" 14 "github.com/argoproj/argo-cd/v3/util/glob" 15 jwtutil "github.com/argoproj/argo-cd/v3/util/jwt" 16 17 "github.com/casbin/casbin/v2" 18 "github.com/casbin/casbin/v2/model" 19 "github.com/casbin/casbin/v2/util" 20 "github.com/casbin/govaluate" 21 "github.com/golang-jwt/jwt/v5" 22 gocache "github.com/patrickmn/go-cache" 23 log "github.com/sirupsen/logrus" 24 "google.golang.org/grpc/codes" 25 "google.golang.org/grpc/status" 26 corev1 "k8s.io/api/core/v1" 27 apierrors "k8s.io/apimachinery/pkg/api/errors" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/apimachinery/pkg/fields" 30 informersv1 "k8s.io/client-go/informers/core/v1" 31 "k8s.io/client-go/kubernetes" 32 "k8s.io/client-go/tools/cache" 33 ) 34 35 const ( 36 ConfigMapPolicyCSVKey = "policy.csv" 37 ConfigMapPolicyDefaultKey = "policy.default" 38 ConfigMapScopesKey = "scopes" 39 ConfigMapMatchModeKey = "policy.matchMode" 40 GlobMatchMode = "glob" 41 RegexMatchMode = "regex" 42 43 defaultRBACSyncPeriod = 10 * time.Minute 44 ) 45 46 // CasbinEnforcer represents methods that must be implemented by a Casbin enforces 47 type CasbinEnforcer interface { 48 EnableLog(bool) 49 Enforce(rvals ...any) (bool, error) 50 LoadPolicy() error 51 EnableEnforce(bool) 52 AddFunction(name string, function govaluate.ExpressionFunction) 53 GetGroupingPolicy() ([][]string, error) 54 GetAllRoles() ([]string, error) 55 GetImplicitPermissionsForUser(user string, domain ...string) ([][]string, error) 56 } 57 58 const ( 59 // please add new items to Resources 60 ResourceClusters = "clusters" 61 ResourceProjects = "projects" 62 ResourceApplications = "applications" 63 ResourceApplicationSets = "applicationsets" 64 ResourceRepositories = "repositories" 65 ResourceWriteRepositories = "write-repositories" 66 ResourceCertificates = "certificates" 67 ResourceAccounts = "accounts" 68 ResourceGPGKeys = "gpgkeys" 69 ResourceLogs = "logs" 70 ResourceExec = "exec" 71 ResourceExtensions = "extensions" 72 73 // please add new items to Actions 74 ActionGet = "get" 75 ActionCreate = "create" 76 ActionUpdate = "update" 77 ActionDelete = "delete" 78 ActionSync = "sync" 79 ActionOverride = "override" 80 ActionAction = "action" 81 ActionInvoke = "invoke" 82 ) 83 84 var ( 85 DefaultScopes = []string{"groups"} 86 Resources = []string{ 87 ResourceClusters, 88 ResourceProjects, 89 ResourceApplications, 90 ResourceApplicationSets, 91 ResourceRepositories, 92 ResourceWriteRepositories, 93 ResourceCertificates, 94 ResourceAccounts, 95 ResourceGPGKeys, 96 ResourceLogs, 97 ResourceExec, 98 ResourceExtensions, 99 } 100 Actions = []string{ 101 ActionGet, 102 ActionCreate, 103 ActionUpdate, 104 ActionDelete, 105 ActionSync, 106 ActionOverride, 107 ActionAction, 108 ActionInvoke, 109 } 110 ) 111 112 var ProjectScoped = map[string]bool{ 113 ResourceApplications: true, 114 ResourceApplicationSets: true, 115 ResourceLogs: true, 116 ResourceExec: true, 117 ResourceClusters: true, 118 ResourceRepositories: true, 119 } 120 121 // Enforcer is a wrapper around an Casbin enforcer that: 122 // * is backed by a kubernetes config map 123 // * has a predefined RBAC model 124 // * supports a built-in policy 125 // * supports a user-defined policy 126 // * supports a custom JWT claims enforce function 127 type Enforcer struct { 128 lock sync.Mutex 129 enforcerCache *gocache.Cache 130 adapter *argocdAdapter 131 enableLog bool 132 enabled bool 133 clientset kubernetes.Interface 134 namespace string 135 configmap string 136 claimsEnforcerFunc ClaimsEnforcerFunc 137 model model.Model 138 defaultRole string 139 matchMode string 140 } 141 142 // cachedEnforcer holds the Casbin enforcer instances and optional custom project policy 143 type cachedEnforcer struct { 144 enforcer CasbinEnforcer 145 policy string 146 } 147 148 func (e *Enforcer) invalidateCache(actions ...func()) { 149 e.lock.Lock() 150 defer e.lock.Unlock() 151 152 for _, action := range actions { 153 action() 154 } 155 e.enforcerCache.Flush() 156 } 157 158 func (e *Enforcer) getCasbinEnforcer(project string, policy string) CasbinEnforcer { 159 res, err := e.tryGetCasbinEnforcer(project, policy) 160 if err != nil { 161 panic(err) 162 } 163 return res 164 } 165 166 // tryGetCasbinEnforcer returns the cached enforcer for the given optional project and project policy. 167 func (e *Enforcer) tryGetCasbinEnforcer(project string, policy string) (CasbinEnforcer, error) { 168 e.lock.Lock() 169 defer e.lock.Unlock() 170 var cached *cachedEnforcer 171 val, ok := e.enforcerCache.Get(project) 172 if ok { 173 if c, ok := val.(*cachedEnforcer); ok && c.policy == policy { 174 cached = c 175 } 176 } 177 if cached != nil { 178 return cached.enforcer, nil 179 } 180 matchFunc := globMatchFunc 181 if e.matchMode == RegexMatchMode { 182 matchFunc = util.RegexMatchFunc 183 } 184 185 var err error 186 var enforcer CasbinEnforcer 187 if policy != "" { 188 if enforcer, err = newEnforcerSafe(matchFunc, e.model, newAdapter(e.adapter.builtinPolicy, e.adapter.userDefinedPolicy, policy)); err != nil { 189 // fallback to default policy if project policy is invalid 190 log.Errorf("Failed to load project '%s' policy", project) 191 enforcer, err = newEnforcerSafe(matchFunc, e.model, e.adapter) 192 } 193 } else { 194 enforcer, err = newEnforcerSafe(matchFunc, e.model, e.adapter) 195 } 196 if err != nil { 197 return nil, err 198 } 199 200 enforcer.AddFunction("globOrRegexMatch", matchFunc) 201 enforcer.EnableLog(e.enableLog) 202 enforcer.EnableEnforce(e.enabled) 203 e.enforcerCache.SetDefault(project, &cachedEnforcer{enforcer: enforcer, policy: policy}) 204 return enforcer, nil 205 } 206 207 // ClaimsEnforcerFunc is func template to enforce a JWT claims. The subject is replaced 208 type ClaimsEnforcerFunc func(claims jwt.Claims, rvals ...any) bool 209 210 func newEnforcerSafe(matchFunction govaluate.ExpressionFunction, params ...any) (e CasbinEnforcer, err error) { 211 defer func() { 212 if r := recover(); r != nil { 213 err = fmt.Errorf("%v", r) 214 e = nil 215 } 216 }() 217 enfs, err := casbin.NewCachedEnforcer(params...) 218 if err != nil { 219 return nil, err 220 } 221 enfs.AddFunction("globOrRegexMatch", matchFunction) 222 return enfs, nil 223 } 224 225 func NewEnforcer(clientset kubernetes.Interface, namespace, configmap string, claimsEnforcer ClaimsEnforcerFunc) *Enforcer { 226 adapter := newAdapter("", "", "") 227 builtInModel := newBuiltInModel() 228 return &Enforcer{ 229 enforcerCache: gocache.New(time.Hour, time.Hour), 230 adapter: adapter, 231 clientset: clientset, 232 namespace: namespace, 233 configmap: configmap, 234 model: builtInModel, 235 claimsEnforcerFunc: claimsEnforcer, 236 enabled: true, 237 } 238 } 239 240 // EnableLog executes casbin.Enforcer functionality. 241 func (e *Enforcer) EnableLog(s bool) { 242 e.invalidateCache(func() { 243 e.enableLog = s 244 }) 245 } 246 247 // EnableEnforce executes casbin.Enforcer functionality and will invalidate cache if required. 248 func (e *Enforcer) EnableEnforce(s bool) { 249 e.invalidateCache(func() { 250 e.enabled = s 251 }) 252 } 253 254 // LoadPolicy executes casbin.Enforcer functionality and will invalidate cache if required. 255 func (e *Enforcer) LoadPolicy() error { 256 _, err := e.tryGetCasbinEnforcer("", "") 257 return err 258 } 259 260 // CheckUserDefinedRoleReferentialIntegrity iterates over roles and policies to validate the existence of a matching policy subject for every defined role 261 func CheckUserDefinedRoleReferentialIntegrity(e CasbinEnforcer) error { 262 allRoles, err := e.GetAllRoles() 263 if err != nil { 264 return err 265 } 266 notFound := make([]string, 0) 267 for _, roleName := range allRoles { 268 permissions, err := e.GetImplicitPermissionsForUser(roleName) 269 if err != nil { 270 return err 271 } 272 if len(permissions) == 0 { 273 notFound = append(notFound, roleName) 274 } 275 } 276 if len(notFound) > 0 { 277 return fmt.Errorf("user defined roles not found in policies: %s", strings.Join(notFound, ",")) 278 } 279 return nil 280 } 281 282 // Glob match func 283 func globMatchFunc(args ...any) (any, error) { 284 if len(args) < 2 { 285 return false, nil 286 } 287 val, ok := args[0].(string) 288 if !ok { 289 return false, nil 290 } 291 292 pattern, ok := args[1].(string) 293 if !ok { 294 return false, nil 295 } 296 297 return glob.Match(pattern, val), nil 298 } 299 300 // SetMatchMode set match mode on runtime, glob match or regex match 301 func (e *Enforcer) SetMatchMode(mode string) { 302 e.invalidateCache(func() { 303 if mode == RegexMatchMode { 304 e.matchMode = RegexMatchMode 305 } else { 306 e.matchMode = GlobMatchMode 307 } 308 }) 309 } 310 311 // SetDefaultRole sets a default role to use during enforcement. Will fall back to this role if 312 // normal enforcement fails 313 func (e *Enforcer) SetDefaultRole(roleName string) { 314 e.defaultRole = roleName 315 } 316 317 // SetClaimsEnforcerFunc sets a claims enforce function during enforcement. The claims enforce function 318 // can extract claims from JWT token and do the proper enforcement based on user, group or any information 319 // available in the input parameter list 320 func (e *Enforcer) SetClaimsEnforcerFunc(claimsEnforcer ClaimsEnforcerFunc) { 321 e.claimsEnforcerFunc = claimsEnforcer 322 } 323 324 // Enforce is a wrapper around casbin.Enforce to additionally enforce a default role and a custom 325 // claims function 326 func (e *Enforcer) Enforce(rvals ...any) bool { 327 return enforce(e.getCasbinEnforcer("", ""), e.defaultRole, e.claimsEnforcerFunc, rvals...) 328 } 329 330 // EnforceErr is a convenience helper to wrap a failed enforcement with a detailed error about the request 331 func (e *Enforcer) EnforceErr(rvals ...any) error { 332 if !e.Enforce(rvals...) { 333 errMsg := "permission denied" 334 335 if len(rvals) > 0 { 336 rvalsStrs := make([]string, len(rvals)-1) 337 for i, rval := range rvals[1:] { 338 rvalsStrs[i] = fmt.Sprintf("%s", rval) 339 } 340 if s, ok := rvals[0].(jwt.Claims); ok { 341 claims, err := jwtutil.MapClaims(s) 342 if err == nil { 343 userId := jwtutil.GetUserIdentifier(claims) 344 if userId != "" { 345 rvalsStrs = append(rvalsStrs, "sub: "+userId) 346 } 347 if issuedAtTime, err := jwtutil.IssuedAtTime(claims); err == nil { 348 rvalsStrs = append(rvalsStrs, "iat: "+issuedAtTime.Format(time.RFC3339)) 349 } 350 } 351 } 352 errMsg = fmt.Sprintf("%s: %s", errMsg, strings.Join(rvalsStrs, ", ")) 353 } 354 355 return status.Error(codes.PermissionDenied, errMsg) 356 } 357 return nil 358 } 359 360 // EnforceRuntimePolicy enforces a policy defined at run-time which augments the built-in and 361 // user-defined policy. This allows any explicit denies of the built-in, and user-defined policies 362 // to override the run-time policy. Runs normal enforcement if run-time policy is empty. 363 func (e *Enforcer) EnforceRuntimePolicy(project string, policy string, rvals ...any) bool { 364 enf := e.CreateEnforcerWithRuntimePolicy(project, policy) 365 return e.EnforceWithCustomEnforcer(enf, rvals...) 366 } 367 368 // CreateEnforcerWithRuntimePolicy creates an enforcer with a policy defined at run-time which augments the built-in and 369 // user-defined policy. This allows any explicit denies of the built-in, and user-defined policies 370 // to override the run-time policy. Runs normal enforcement if run-time policy is empty. 371 func (e *Enforcer) CreateEnforcerWithRuntimePolicy(project string, policy string) CasbinEnforcer { 372 return e.getCasbinEnforcer(project, policy) 373 } 374 375 // EnforceWithCustomEnforcer wraps enforce with an custom enforcer 376 func (e *Enforcer) EnforceWithCustomEnforcer(enf CasbinEnforcer, rvals ...any) bool { 377 return enforce(enf, e.defaultRole, e.claimsEnforcerFunc, rvals...) 378 } 379 380 // enforce is a helper to additionally check a default role and invoke a custom claims enforcement function 381 func enforce(enf CasbinEnforcer, defaultRole string, claimsEnforcerFunc ClaimsEnforcerFunc, rvals ...any) bool { 382 // check the default role 383 if defaultRole != "" && len(rvals) >= 2 { 384 if ok, err := enf.Enforce(append([]any{defaultRole}, rvals[1:]...)...); ok && err == nil { 385 return true 386 } 387 } 388 if len(rvals) == 0 { 389 return false 390 } 391 // check if subject is jwt.Claims vs. a normal subject string and run custom claims 392 // enforcement func (if set) 393 sub := rvals[0] 394 switch s := sub.(type) { 395 case string: 396 // noop 397 case jwt.Claims: 398 if claimsEnforcerFunc != nil && claimsEnforcerFunc(s, rvals...) { 399 return true 400 } 401 rvals = append([]any{""}, rvals[1:]...) 402 default: 403 rvals = append([]any{""}, rvals[1:]...) 404 } 405 ok, err := enf.Enforce(rvals...) 406 return ok && err == nil 407 } 408 409 // SetBuiltinPolicy sets a built-in policy, which augments any user defined policies 410 func (e *Enforcer) SetBuiltinPolicy(policy string) error { 411 e.invalidateCache(func() { 412 e.adapter.builtinPolicy = policy 413 }) 414 return e.LoadPolicy() 415 } 416 417 // SetUserPolicy sets a user policy, augmenting the built-in policy 418 func (e *Enforcer) SetUserPolicy(policy string) error { 419 e.invalidateCache(func() { 420 e.adapter.userDefinedPolicy = policy 421 }) 422 return e.LoadPolicy() 423 } 424 425 // newInformer returns an informer which watches updates on the rbac configmap 426 func (e *Enforcer) newInformer() cache.SharedIndexInformer { 427 tweakConfigMap := func(options *metav1.ListOptions) { 428 cmFieldSelector := fields.ParseSelectorOrDie("metadata.name=" + e.configmap) 429 options.FieldSelector = cmFieldSelector.String() 430 } 431 indexers := cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc} 432 return informersv1.NewFilteredConfigMapInformer(e.clientset, e.namespace, defaultRBACSyncPeriod, indexers, tweakConfigMap) 433 } 434 435 // RunPolicyLoader runs the policy loader which watches policy updates from the configmap and reloads them 436 func (e *Enforcer) RunPolicyLoader(ctx context.Context, onUpdated func(cm *corev1.ConfigMap) error) error { 437 cm, err := e.clientset.CoreV1().ConfigMaps(e.namespace).Get(ctx, e.configmap, metav1.GetOptions{}) 438 if err != nil { 439 if !apierrors.IsNotFound(err) { 440 return err 441 } 442 } else { 443 err = e.syncUpdate(cm, onUpdated) 444 if err != nil { 445 return err 446 } 447 } 448 e.runInformer(ctx, onUpdated) 449 return nil 450 } 451 452 func (e *Enforcer) runInformer(ctx context.Context, onUpdated func(cm *corev1.ConfigMap) error) { 453 cmInformer := e.newInformer() 454 _, err := cmInformer.AddEventHandler( 455 cache.ResourceEventHandlerFuncs{ 456 AddFunc: func(obj any) { 457 if cm, ok := obj.(*corev1.ConfigMap); ok { 458 err := e.syncUpdate(cm, onUpdated) 459 if err != nil { 460 log.Error(err) 461 } else { 462 log.Infof("RBAC ConfigMap '%s' added", e.configmap) 463 } 464 } 465 }, 466 UpdateFunc: func(old, new any) { 467 oldCM := old.(*corev1.ConfigMap) 468 newCM := new.(*corev1.ConfigMap) 469 if oldCM.ResourceVersion == newCM.ResourceVersion { 470 return 471 } 472 err := e.syncUpdate(newCM, onUpdated) 473 if err != nil { 474 log.Error(err) 475 } else { 476 log.Infof("RBAC ConfigMap '%s' updated", e.configmap) 477 } 478 }, 479 }, 480 ) 481 if err != nil { 482 log.Error(err) 483 } 484 log.Info("Starting rbac config informer") 485 cmInformer.Run(ctx.Done()) 486 log.Info("rbac configmap informer cancelled") 487 } 488 489 // PolicyCSV will generate the final policy csv to be used 490 // by Argo CD RBAC. It will find entries in the given data 491 // that matches the policy key name convention: 492 // 493 // policy[.overlay].csv 494 func PolicyCSV(data map[string]string) string { 495 var strBuilder strings.Builder 496 // add the main policy first 497 if p, ok := data[ConfigMapPolicyCSVKey]; ok { 498 strBuilder.WriteString(p) 499 } 500 501 keys := make([]string, 0, len(data)) 502 for k := range data { 503 keys = append(keys, k) 504 } 505 sort.Strings(keys) 506 507 // append additional policies at the end of the csv 508 for _, key := range keys { 509 value := data[key] 510 if strings.HasPrefix(key, "policy.") && 511 strings.HasSuffix(key, ".csv") && 512 key != ConfigMapPolicyCSVKey { 513 strBuilder.WriteString("\n") 514 strBuilder.WriteString(value) 515 } 516 } 517 return strBuilder.String() 518 } 519 520 // syncUpdate updates the enforcer 521 func (e *Enforcer) syncUpdate(cm *corev1.ConfigMap, onUpdated func(cm *corev1.ConfigMap) error) error { 522 e.SetDefaultRole(cm.Data[ConfigMapPolicyDefaultKey]) 523 e.SetMatchMode(cm.Data[ConfigMapMatchModeKey]) 524 policyCSV := PolicyCSV(cm.Data) 525 if err := onUpdated(cm); err != nil { 526 return err 527 } 528 return e.SetUserPolicy(policyCSV) 529 } 530 531 // ValidatePolicy verifies a policy string is acceptable to casbin 532 func ValidatePolicy(policy string) error { 533 casbinEnforcer, err := newEnforcerSafe(globMatchFunc, newBuiltInModel(), newAdapter("", "", policy)) 534 if err != nil { 535 return fmt.Errorf("policy syntax error: %s", policy) 536 } 537 538 // Check for referential integrity 539 if err := CheckUserDefinedRoleReferentialIntegrity(casbinEnforcer); err != nil { 540 log.Warning(err.Error()) 541 } 542 return nil 543 } 544 545 // newBuiltInModel is a helper to return a brand new casbin model from the built-in model string. 546 // This is needed because it is not safe to re-use the same casbin Model when instantiating new 547 // casbin enforcers. 548 func newBuiltInModel() model.Model { 549 m, err := model.NewModelFromString(assets.ModelConf) 550 if err != nil { 551 panic(err) 552 } 553 return m 554 } 555 556 // Casbin adapter which satisfies persist.Adapter interface 557 type argocdAdapter struct { 558 builtinPolicy string 559 userDefinedPolicy string 560 runtimePolicy string 561 } 562 563 func newAdapter(builtinPolicy, userDefinedPolicy, runtimePolicy string) *argocdAdapter { 564 return &argocdAdapter{ 565 builtinPolicy: builtinPolicy, 566 userDefinedPolicy: userDefinedPolicy, 567 runtimePolicy: runtimePolicy, 568 } 569 } 570 571 func (a *argocdAdapter) LoadPolicy(model model.Model) error { 572 for _, policyStr := range []string{a.builtinPolicy, a.userDefinedPolicy, a.runtimePolicy} { 573 for _, line := range strings.Split(policyStr, "\n") { 574 if err := loadPolicyLine(strings.TrimSpace(line), model); err != nil { 575 return err 576 } 577 } 578 } 579 return nil 580 } 581 582 // The modified version of LoadPolicyLine function defined in "persist" package of github.com/casbin/casbin. 583 // Uses CVS parser to correctly handle quotes in policy line. 584 func loadPolicyLine(line string, model model.Model) error { 585 if line == "" || strings.HasPrefix(line, "#") { 586 return nil 587 } 588 589 reader := csv.NewReader(strings.NewReader(line)) 590 reader.TrimLeadingSpace = true 591 tokens, err := reader.Read() 592 if err != nil { 593 return err 594 } 595 596 tokenLen := len(tokens) 597 598 if tokenLen < 1 || 599 tokens[0] == "" || 600 (tokens[0] == "g" && tokenLen != 3) || 601 (tokens[0] == "p" && tokenLen != 6) { 602 return fmt.Errorf("invalid RBAC policy: %s", line) 603 } 604 605 key := tokens[0] 606 sec := key[:1] 607 if _, ok := model[sec]; !ok { 608 return fmt.Errorf("invalid RBAC policy: %s", line) 609 } 610 if _, ok := model[sec][key]; !ok { 611 return fmt.Errorf("invalid RBAC policy: %s", line) 612 } 613 model[sec][key].Policy = append(model[sec][key].Policy, tokens[1:]) 614 return nil 615 } 616 617 func (a *argocdAdapter) SavePolicy(_ model.Model) error { 618 return errors.New("not implemented") 619 } 620 621 func (a *argocdAdapter) AddPolicy(_ string, _ string, _ []string) error { 622 return errors.New("not implemented") 623 } 624 625 func (a *argocdAdapter) RemovePolicy(_ string, _ string, _ []string) error { 626 return errors.New("not implemented") 627 } 628 629 func (a *argocdAdapter) RemoveFilteredPolicy(_ string, _ string, _ int, _ ...string) error { 630 return errors.New("not implemented") 631 }