github.com/argoproj/argo-cd@v1.8.7/util/rbac/rbac.go (about) 1 package rbac 2 3 import ( 4 "context" 5 "encoding/csv" 6 "errors" 7 "fmt" 8 "strings" 9 "time" 10 11 "github.com/argoproj/argo-cd/util/assets" 12 "github.com/argoproj/argo-cd/util/glob" 13 jwtutil "github.com/argoproj/argo-cd/util/jwt" 14 15 "github.com/casbin/casbin" 16 "github.com/casbin/casbin/model" 17 jwt "github.com/dgrijalva/jwt-go/v4" 18 log "github.com/sirupsen/logrus" 19 "google.golang.org/grpc/codes" 20 "google.golang.org/grpc/status" 21 apiv1 "k8s.io/api/core/v1" 22 apierr "k8s.io/apimachinery/pkg/api/errors" 23 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 "k8s.io/apimachinery/pkg/fields" 25 v1 "k8s.io/client-go/informers/core/v1" 26 "k8s.io/client-go/kubernetes" 27 "k8s.io/client-go/tools/cache" 28 ) 29 30 const ( 31 ConfigMapPolicyCSVKey = "policy.csv" 32 ConfigMapPolicyDefaultKey = "policy.default" 33 ConfigMapScopesKey = "scopes" 34 35 defaultRBACSyncPeriod = 10 * time.Minute 36 ) 37 38 // Enforcer is a wrapper around an Casbin enforcer that: 39 // * is backed by a kubernetes config map 40 // * has a predefined RBAC model 41 // * supports a built-in policy 42 // * supports a user-defined policy 43 // * supports a custom JWT claims enforce function 44 type Enforcer struct { 45 *casbin.Enforcer 46 adapter *argocdAdapter 47 clientset kubernetes.Interface 48 namespace string 49 configmap string 50 claimsEnforcerFunc ClaimsEnforcerFunc 51 model model.Model 52 defaultRole string 53 } 54 55 // ClaimsEnforcerFunc is func template to enforce a JWT claims. The subject is replaced 56 type ClaimsEnforcerFunc func(claims jwt.Claims, rvals ...interface{}) bool 57 58 func newEnforcerSafe(params ...interface{}) (e *casbin.Enforcer, err error) { 59 enfs, err := casbin.NewEnforcerSafe(params...) 60 if err != nil { 61 return nil, err 62 } 63 enfs.AddFunction("globMatch", func(args ...interface{}) (interface{}, error) { 64 if len(args) < 2 { 65 return false, nil 66 } 67 val, ok := args[0].(string) 68 if !ok { 69 return false, nil 70 } 71 72 pattern, ok := args[1].(string) 73 if !ok { 74 return false, nil 75 } 76 77 return glob.Match(pattern, val), nil 78 }) 79 return enfs, nil 80 } 81 82 func NewEnforcer(clientset kubernetes.Interface, namespace, configmap string, claimsEnforcer ClaimsEnforcerFunc) *Enforcer { 83 adapter := newAdapter("", "", "") 84 builtInModel := newBuiltInModel() 85 enf, err := newEnforcerSafe(builtInModel, adapter) 86 if err != nil { 87 panic(err) 88 } 89 enf.EnableLog(false) 90 return &Enforcer{ 91 Enforcer: enf, 92 adapter: adapter, 93 clientset: clientset, 94 namespace: namespace, 95 configmap: configmap, 96 model: builtInModel, 97 claimsEnforcerFunc: claimsEnforcer, 98 } 99 } 100 101 // SetDefaultRole sets a default role to use during enforcement. Will fall back to this role if 102 // normal enforcement fails 103 func (e *Enforcer) SetDefaultRole(roleName string) { 104 e.defaultRole = roleName 105 } 106 107 // SetClaimsEnforcerFunc sets a claims enforce function during enforcement. The claims enforce function 108 // can extract claims from JWT token and do the proper enforcement based on user, group or any information 109 // available in the input parameter list 110 func (e *Enforcer) SetClaimsEnforcerFunc(claimsEnforcer ClaimsEnforcerFunc) { 111 e.claimsEnforcerFunc = claimsEnforcer 112 } 113 114 // Enforce is a wrapper around casbin.Enforce to additionally enforce a default role and a custom 115 // claims function 116 func (e *Enforcer) Enforce(rvals ...interface{}) bool { 117 return enforce(e.Enforcer, e.defaultRole, e.claimsEnforcerFunc, rvals...) 118 } 119 120 // EnforceErr is a convenience helper to wrap a failed enforcement with a detailed error about the request 121 func (e *Enforcer) EnforceErr(rvals ...interface{}) error { 122 if !e.Enforce(rvals...) { 123 errMsg := "permission denied" 124 if len(rvals) > 0 { 125 rvalsStrs := make([]string, len(rvals)-1) 126 for i, rval := range rvals[1:] { 127 rvalsStrs[i] = fmt.Sprintf("%s", rval) 128 } 129 switch s := rvals[0].(type) { 130 case jwt.Claims: 131 claims, err := jwtutil.MapClaims(s) 132 if err != nil { 133 break 134 } 135 if sub := jwtutil.StringField(claims, "sub"); sub != "" { 136 rvalsStrs = append(rvalsStrs, fmt.Sprintf("sub: %s", sub)) 137 } 138 if issuedAtTime, err := jwtutil.IssuedAtTime(claims); err == nil { 139 rvalsStrs = append(rvalsStrs, fmt.Sprintf("iat: %s", issuedAtTime.Format(time.RFC3339))) 140 } 141 } 142 errMsg = fmt.Sprintf("%s: %s", errMsg, strings.Join(rvalsStrs, ", ")) 143 } 144 return status.Error(codes.PermissionDenied, errMsg) 145 } 146 return nil 147 } 148 149 // EnforceRuntimePolicy enforces a policy defined at run-time which augments the built-in and 150 // user-defined policy. This allows any explicit denies of the built-in, and user-defined policies 151 // to override the run-time policy. Runs normal enforcement if run-time policy is empty. 152 func (e *Enforcer) EnforceRuntimePolicy(policy string, rvals ...interface{}) bool { 153 var enf *casbin.Enforcer 154 var err error 155 if policy == "" { 156 enf = e.Enforcer 157 } else { 158 enf, err = newEnforcerSafe(newBuiltInModel(), newAdapter(e.adapter.builtinPolicy, e.adapter.userDefinedPolicy, policy)) 159 if err != nil { 160 log.Warnf("invalid runtime policy: %s", policy) 161 enf = e.Enforcer 162 } 163 } 164 return enforce(enf, e.defaultRole, e.claimsEnforcerFunc, rvals...) 165 } 166 167 // enforce is a helper to additionally check a default role and invoke a custom claims enforcement function 168 func enforce(enf *casbin.Enforcer, defaultRole string, claimsEnforcerFunc ClaimsEnforcerFunc, rvals ...interface{}) bool { 169 // check the default role 170 if defaultRole != "" && len(rvals) >= 2 { 171 if enf.Enforce(append([]interface{}{defaultRole}, rvals[1:]...)...) { 172 return true 173 } 174 } 175 if len(rvals) == 0 { 176 return false 177 } 178 // check if subject is jwt.Claims vs. a normal subject string and run custom claims 179 // enforcement func (if set) 180 sub := rvals[0] 181 switch s := sub.(type) { 182 case string: 183 // noop 184 case jwt.Claims: 185 if claimsEnforcerFunc != nil && claimsEnforcerFunc(s, rvals...) { 186 return true 187 } 188 rvals = append([]interface{}{""}, rvals[1:]...) 189 default: 190 rvals = append([]interface{}{""}, rvals[1:]...) 191 } 192 return enf.Enforce(rvals...) 193 } 194 195 // SetBuiltinPolicy sets a built-in policy, which augments any user defined policies 196 func (e *Enforcer) SetBuiltinPolicy(policy string) error { 197 e.adapter.builtinPolicy = policy 198 return e.LoadPolicy() 199 } 200 201 // SetUserPolicy sets a user policy, augmenting the built-in policy 202 func (e *Enforcer) SetUserPolicy(policy string) error { 203 e.adapter.userDefinedPolicy = policy 204 return e.LoadPolicy() 205 } 206 207 // newInformers returns an informer which watches updates on the rbac configmap 208 func (e *Enforcer) newInformer() cache.SharedIndexInformer { 209 tweakConfigMap := func(options *metav1.ListOptions) { 210 cmFieldSelector := fields.ParseSelectorOrDie(fmt.Sprintf("metadata.name=%s", e.configmap)) 211 options.FieldSelector = cmFieldSelector.String() 212 } 213 indexers := cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc} 214 return v1.NewFilteredConfigMapInformer(e.clientset, e.namespace, defaultRBACSyncPeriod, indexers, tweakConfigMap) 215 } 216 217 // RunPolicyLoader runs the policy loader which watches policy updates from the configmap and reloads them 218 func (e *Enforcer) RunPolicyLoader(ctx context.Context, onUpdated func(cm *apiv1.ConfigMap) error) error { 219 cm, err := e.clientset.CoreV1().ConfigMaps(e.namespace).Get(ctx, e.configmap, metav1.GetOptions{}) 220 if err != nil { 221 if !apierr.IsNotFound(err) { 222 return err 223 } 224 } else { 225 err = e.syncUpdate(cm, onUpdated) 226 if err != nil { 227 return err 228 } 229 } 230 e.runInformer(ctx, onUpdated) 231 return nil 232 } 233 234 func (e *Enforcer) runInformer(ctx context.Context, onUpdated func(cm *apiv1.ConfigMap) error) { 235 cmInformer := e.newInformer() 236 cmInformer.AddEventHandler( 237 cache.ResourceEventHandlerFuncs{ 238 AddFunc: func(obj interface{}) { 239 if cm, ok := obj.(*apiv1.ConfigMap); ok { 240 err := e.syncUpdate(cm, onUpdated) 241 if err != nil { 242 log.Error(err) 243 } else { 244 log.Infof("RBAC ConfigMap '%s' added", e.configmap) 245 } 246 } 247 }, 248 UpdateFunc: func(old, new interface{}) { 249 oldCM := old.(*apiv1.ConfigMap) 250 newCM := new.(*apiv1.ConfigMap) 251 if oldCM.ResourceVersion == newCM.ResourceVersion { 252 return 253 } 254 err := e.syncUpdate(newCM, onUpdated) 255 if err != nil { 256 log.Error(err) 257 } else { 258 log.Infof("RBAC ConfigMap '%s' updated", e.configmap) 259 } 260 }, 261 }, 262 ) 263 log.Info("Starting rbac config informer") 264 cmInformer.Run(ctx.Done()) 265 log.Info("rbac configmap informer cancelled") 266 } 267 268 // syncUpdate updates the enforcer 269 func (e *Enforcer) syncUpdate(cm *apiv1.ConfigMap, onUpdated func(cm *apiv1.ConfigMap) error) error { 270 e.SetDefaultRole(cm.Data[ConfigMapPolicyDefaultKey]) 271 policyCSV, ok := cm.Data[ConfigMapPolicyCSVKey] 272 if !ok { 273 policyCSV = "" 274 } 275 if err := onUpdated(cm); err != nil { 276 return err 277 } 278 return e.SetUserPolicy(policyCSV) 279 } 280 281 // ValidatePolicy verifies a policy string is acceptable to casbin 282 func ValidatePolicy(policy string) error { 283 _, err := newEnforcerSafe(newBuiltInModel(), newAdapter("", "", policy)) 284 if err != nil { 285 return fmt.Errorf("policy syntax error: %s", policy) 286 } 287 return nil 288 } 289 290 // newBuiltInModel is a helper to return a brand new casbin model from the built-in model string. 291 // This is needed because it is not safe to re-use the same casbin Model when instantiating new 292 // casbin enforcers. 293 func newBuiltInModel() model.Model { 294 return casbin.NewModel(assets.ModelConf) 295 } 296 297 // Casbin adapter which satisfies persist.Adapter interface 298 type argocdAdapter struct { 299 builtinPolicy string 300 userDefinedPolicy string 301 runtimePolicy string 302 } 303 304 func newAdapter(builtinPolicy, userDefinedPolicy, runtimePolicy string) *argocdAdapter { 305 return &argocdAdapter{ 306 builtinPolicy: builtinPolicy, 307 userDefinedPolicy: userDefinedPolicy, 308 runtimePolicy: runtimePolicy, 309 } 310 } 311 312 func (a *argocdAdapter) LoadPolicy(model model.Model) error { 313 for _, policyStr := range []string{a.builtinPolicy, a.userDefinedPolicy, a.runtimePolicy} { 314 for _, line := range strings.Split(policyStr, "\n") { 315 if err := loadPolicyLine(strings.TrimSpace(line), model); err != nil { 316 return err 317 } 318 } 319 } 320 return nil 321 } 322 323 // The modified version of LoadPolicyLine function defined in "persist" package of github.com/casbin/casbin. 324 // Uses CVS parser to correctly handle quotes in policy line. 325 func loadPolicyLine(line string, model model.Model) error { 326 if line == "" || strings.HasPrefix(line, "#") { 327 return nil 328 } 329 330 reader := csv.NewReader(strings.NewReader(line)) 331 reader.TrimLeadingSpace = true 332 tokens, err := reader.Read() 333 if err != nil { 334 return err 335 } 336 337 key := tokens[0] 338 sec := key[:1] 339 model[sec][key].Policy = append(model[sec][key].Policy, tokens[1:]) 340 return nil 341 } 342 343 func (a *argocdAdapter) SavePolicy(model model.Model) error { 344 return errors.New("not implemented") 345 } 346 347 func (a *argocdAdapter) AddPolicy(sec string, ptype string, rule []string) error { 348 return errors.New("not implemented") 349 } 350 351 func (a *argocdAdapter) RemovePolicy(sec string, ptype string, rule []string) error { 352 return errors.New("not implemented") 353 } 354 355 func (a *argocdAdapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error { 356 return errors.New("not implemented") 357 }