88
99from logging .config import dictConfig
1010
11+ RESOURCE_NAME_SEP = "/resourceName::"
12+ NON_RESOURCE_URL_PREFIX = "nonResourceURL::"
13+
1114dictConfig ({
1215 "version" : 1 ,
1316 "formatters" : {"default" : {"format" : "[%(asctime)s] %(levelname)s in %(module)s: %(message)s" }},
@@ -70,16 +73,16 @@ def _parse_permission_rules(self, perms):
7073 resources .append (resource .strip ())
7174 rule_group = {}
7275 if api_group == "non-apigroup" :
73- if "nonResourceURL" in resource :
74- parts = resource .split ("nonResourceURL::" )
76+ if NON_RESOURCE_URL_PREFIX in resource :
77+ parts = resource .split (NON_RESOURCE_URL_PREFIX )
7578 non_res = parts [1 ].strip () if len (parts ) > 1 else parts [0 ].strip ()
7679 rule_group ["nonResourceURLs" ] = [non_res ]
7780 rule_group ["verbs" ] = verbs
7881 else :
7982 rule_group ["apiGroups" ] = [api_group ]
8083 rule_group ["verbs" ] = verbs
81- if "/resourceName::" in resource :
82- parts = resource .split ("/resourceName::" )
84+ if RESOURCE_NAME_SEP in resource :
85+ parts = resource .split (RESOURCE_NAME_SEP )
8386 rule_group ["resources" ] = [parts [0 ].strip ()]
8487 rule_group ["resourceNames" ] = [parts [1 ].strip ()]
8588 else :
@@ -208,14 +211,14 @@ def _strip_verbs(self, existing_verbs, revoke_verbs):
208211 def _permission_keys (self , resource , resource_names ):
209212 """Permission-file key form used by _parse_permission_rules / the perms configmap."""
210213 if resource_names :
211- return [resource + "/resourceName::" + rn for rn in resource_names ]
214+ return [resource + RESOURCE_NAME_SEP + rn for rn in resource_names ]
212215 return [resource ]
213216
214217 def _retained_keys_for_rule (self , rule ):
215218 """All permission-file keys covered by an (unmodified) k8s rule."""
216219 keys = []
217220 for url in rule .get ("nonResourceURLs" ) or []:
218- keys .append ("nonResourceURL::" + url )
221+ keys .append (NON_RESOURCE_URL_PREFIX + url )
219222 resource_names = rule .get ("resourceNames" ) or []
220223 for res in rule .get ("resources" ) or []:
221224 keys .extend (self ._permission_keys (res , resource_names ))
@@ -236,13 +239,13 @@ def _rule_matches_revoke(self, existing_norm, revoke_norm_rule):
236239 RBAC has no 'deny', so we cannot trim a named cell out of a rule that
237240 grants all names.
238241 """
242+ if revoke_norm_rule ["nonResourceURLs" ]:
243+ return bool (set (revoke_norm_rule ["nonResourceURLs" ]).intersection (existing_norm ["nonResourceURLs" ]))
239244 if revoke_norm_rule ["resourceNames" ]:
240245 if not existing_norm ["resourceNames" ]:
241246 return False
242247 if not set (revoke_norm_rule ["resourceNames" ]).intersection (existing_norm ["resourceNames" ]):
243248 return False
244- if revoke_norm_rule ["nonResourceURLs" ]:
245- return bool (set (revoke_norm_rule ["nonResourceURLs" ]).intersection (existing_norm ["nonResourceURLs" ]))
246249 if not self ._dim_matches (revoke_norm_rule ["apiGroups" ], existing_norm ["apiGroups" ]):
247250 return False
248251 if not self ._dim_matches (revoke_norm_rule ["resources" ], existing_norm ["resources" ]):
@@ -824,51 +827,31 @@ def _split_rule_after_revoke(self, rule, matched_revoke_rules, retained_keys):
824827 cell_verbs = {url : set (existing_verbs ) for url in existing_urls }
825828 for mr in matched_revoke_rules :
826829 mr_urls = set (mr ["nonResourceURLs" ])
830+ mr_verbs = mr ["verbs" ]
827831 for url in cell_verbs :
828832 if url not in mr_urls :
829833 continue
830- cell_verbs [url ] = self ._strip_to_set (cell_verbs [url ], mr ["verbs" ])
831- by_verbs = {}
832- for url , cv in cell_verbs .items ():
833- if not cv :
834- continue
835- by_verbs .setdefault (tuple (sorted (cv )), []).append (url )
834+ cell_verbs [url ] = self ._strip_to_set (cell_verbs [url ], mr_verbs )
836835 out_rules = []
837- for verbs_key , urls in by_verbs . items ():
836+ for verbs_key , urls in sorted ( self . _bucket_cells_by_verbs ( cell_verbs ). items () ):
838837 updated = dict (rule )
839838 updated ["nonResourceURLs" ] = urls
840839 updated ["verbs" ] = list (verbs_key )
841840 out_rules .append (updated )
842841 for url in urls :
843- retained_keys .add ("nonResourceURL::" + url )
842+ retained_keys .add (NON_RESOURCE_URL_PREFIX + url )
844843 return out_rules
845844
846845 existing_api_groups = rule .get ("apiGroups" ) or []
847846 existing_resources = rule .get ("resources" ) or []
848847 existing_resource_names = rule .get ("resourceNames" ) or []
849848
850- # Wildcard scope: we cannot enumerate "everything except X". Treat atomic.
851- if "*" in existing_api_groups or "*" in existing_resources :
852- combined_verbs = [v for mr in matched_revoke_rules for v in mr ["verbs" ]]
853- stripped = self ._strip_verbs (existing_verbs , combined_verbs )
854- if not stripped :
855- return []
856- updated = dict (rule )
857- updated ["verbs" ] = stripped
858- for key in self ._retained_keys_for_rule (rule ):
859- retained_keys .add (key )
860- return [updated ]
861-
862- if not existing_resources :
863- combined_verbs = [v for mr in matched_revoke_rules for v in mr ["verbs" ]]
864- stripped = self ._strip_verbs (existing_verbs , combined_verbs )
865- if not stripped :
866- return []
867- updated = dict (rule )
868- updated ["verbs" ] = stripped
869- return [updated ]
849+ # Wildcard scope or no resources: can't enumerate "all except X" — treat atomic.
850+ if "*" in existing_api_groups or "*" in existing_resources or not existing_resources :
851+ return self ._atomic_rule_after_revoke (rule , matched_revoke_rules , retained_keys )
870852
871853 names_axis = existing_resource_names or [None ]
854+ has_names = bool (existing_resource_names )
872855 cell_verbs = {}
873856 for ag in existing_api_groups :
874857 for res in existing_resources :
@@ -878,6 +861,7 @@ def _split_rule_after_revoke(self, rule, matched_revoke_rules, retained_keys):
878861 mr_apis = set (mr ["apiGroups" ])
879862 mr_ress = set (mr ["resources" ])
880863 mr_names = set (mr ["resourceNames" ]) if mr ["resourceNames" ] else None
864+ mr_verbs = mr ["verbs" ]
881865 if not mr_apis or not mr_ress :
882866 continue
883867 for cell in cell_verbs :
@@ -886,19 +870,12 @@ def _split_rule_after_revoke(self, rule, matched_revoke_rules, retained_keys):
886870 continue
887871 if mr_names is not None and rn not in mr_names :
888872 continue
889- cell_verbs [cell ] = self ._strip_to_set (cell_verbs [cell ], mr ["verbs" ])
890-
891- by_verbs = {}
892- for cell , cv in cell_verbs .items ():
893- if not cv :
894- continue
895- by_verbs .setdefault (tuple (sorted (cv )), []).append (cell )
873+ cell_verbs [cell ] = self ._strip_to_set (cell_verbs [cell ], mr_verbs )
896874
897875 out_rules = []
898- for verbs_key , cells in by_verbs . items ():
876+ for verbs_key , cells in sorted ( self . _bucket_cells_by_verbs ( cell_verbs ). items () ):
899877 apis = sorted ({c [0 ] for c in cells })
900878 ress = sorted ({c [1 ] for c in cells })
901- has_names = cells [0 ][2 ] is not None
902879 names = sorted ({c [2 ] for c in cells }) if has_names else []
903880 expected = len (apis ) * len (ress ) * (len (names ) if has_names else 1 )
904881 if len (cells ) == expected :
@@ -930,6 +907,30 @@ def _split_rule_after_revoke(self, rule, matched_revoke_rules, retained_keys):
930907 for key in self ._permission_keys (res , cell_names ):
931908 retained_keys .add (key )
932909 return out_rules
910+
911+ def _atomic_rule_after_revoke (self , rule , matched_revoke_rules , retained_keys ):
912+ """Atomic path: strip the union of matched revoke verbs from the whole rule.
913+ Used when the existing rule has '*' scope or no resource dimension —
914+ RBAC cannot express 'everything except X', so we trim globally or drop.
915+ """
916+ combined_verbs = [v for mr in matched_revoke_rules for v in mr ["verbs" ]]
917+ stripped = self ._strip_verbs (rule .get ("verbs" , []), combined_verbs )
918+ if not stripped :
919+ return []
920+ updated = dict (rule )
921+ updated ["verbs" ] = stripped
922+ for key in self ._retained_keys_for_rule (rule ):
923+ retained_keys .add (key )
924+ return [updated ]
925+
926+ def _bucket_cells_by_verbs (self , cell_verbs ):
927+ """Group non-empty cells by their sorted-verb tuple for regrouping."""
928+ buckets = {}
929+ for cell , cv in cell_verbs .items ():
930+ if not cv :
931+ continue
932+ buckets .setdefault (tuple (sorted (cv )), []).append (cell )
933+ return buckets
933934
934935
935936 def _apply_rbac (self , sa , namespace , entity = '' , kubeconfig = '' ):
0 commit comments