go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/lucicfg/starlark/stdlib/internal/luci/lib/acl.star (about) 1 # Copyright 2018 The LUCI Authors. 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 15 """Helper library for defining LUCI ACLs.""" 16 17 load("@stdlib//internal/validate.star", "validate") 18 19 # TODO(vadimsh): Add support for 'anonymous' when/if needed. 20 21 # A constructor for acl.role structs. 22 # 23 # Such structs are seen through public API as predefined symbols, e.g. 24 # acl.LOGDOG_READER. There's no way for an end-user to define a new role. 25 # 26 # Expected to be used as roles in acl.entry(role=...) definitions, and maybe 27 # printed (when debugging). 28 # 29 # Fields: 30 # name: name of the role. 31 # project_level_only: True if the role can be set only in project(...) rule. 32 # groups_only: True if the role should be assigned only to groups, not users. 33 _role_ctor = __native__.genstruct("acl.role") 34 35 # A constructor for acl.entry structs. 36 # 37 # Such structs are created via public acl.entry(...) API. To make their 38 # printable representation useful and not confusing to end users, their 39 # structure somewhat resembles acl.entry(...) arguments list. 40 # 41 # They are not convenient though when generating configs. For that reason 42 # there's another representation of ACLs: as a list of elementary 43 # (role, principal) tuples, where principals can be of few different types 44 # (e.g. groups or users). Internal API function 'normalize_acls' converts 45 # from the user-friendly acl.entry representation to the generator-friendly 46 # acl.elementary representation. 47 # 48 # Fields: 49 # roles: a list of acl.role in the entry, at least one. 50 # users: a list of user emails to apply roles to, may be empty. 51 # groups: a list of group names to apply roles to, may be empty. 52 # projects: a list of project names to apply roles to, may be empty. 53 _entry_ctor = __native__.genstruct("acl.entry") 54 55 # A constructor for acl.elementary structs. 56 # 57 # This is conceptually a sum type: (Role, User | Group | Project). For 58 # convenience it is represented as a tuple where only one of 'user', 'group' or 59 # 'project' is set. 60 # 61 # Fields: 62 # role: an acl.role, always set. 63 # user: an user email. 64 # group: a group name. 65 # project: a project name. 66 _elementary_ctor = __native__.genstruct("acl.elementary") 67 68 def _role( 69 name, 70 *, 71 realms_role, 72 project_level_only = False, 73 groups_only = False): 74 """Defines a role. 75 76 Internal API. Only predefined roles are available publicly, see the bottom 77 of this file. 78 79 Args: 80 name: string name of the role. 81 realms_role: matching predefined Realms role. 82 project_level_only: True if it can be used only in project(...) ACLs. 83 groups_only: True if role supports only group-based ACL (not user-based). 84 85 Returns: 86 acl.role struct. 87 """ 88 return _role_ctor( 89 name = name, 90 realms_role = realms_role, 91 project_level_only = project_level_only, 92 groups_only = groups_only, 93 ) 94 95 def _entry( 96 roles, 97 *, 98 groups = None, 99 users = None, 100 projects = None): 101 """Returns a new ACL binding. 102 103 It assign the given role (or roles) to given individuals, groups or LUCI 104 projects. 105 106 Lists of acl.entry structs are passed to `acls` fields of luci.project(...) 107 and luci.bucket(...) rules. 108 109 An empty ACL binding is allowed. It is ignored everywhere. Useful for things 110 like: 111 112 ```python 113 luci.project( 114 acls = [ 115 acl.entry(acl.PROJECT_CONFIGS_READER, groups = [ 116 # TODO: members will be added later 117 ]) 118 ] 119 ) 120 ``` 121 122 Args: 123 roles: a single role or a list of roles to assign. Required. 124 groups: a single group name or a list of groups to assign the role to. 125 users: a single user email or a list of emails to assign the role to. 126 projects: a single LUCI project name or a list of project names to assign 127 the role to. 128 129 Returns: 130 acl.entry object, should be treated as opaque. 131 """ 132 if __native__.ctor(roles) == _role_ctor: 133 roles = [roles] 134 elif roles != None and type(roles) != "list": 135 validate.struct("roles", roles, _role_ctor) 136 137 if type(groups) == "string": 138 groups = [groups] 139 elif groups != None and type(groups) != "list": 140 validate.string("groups", groups) 141 142 if type(users) == "string": 143 users = [users] 144 elif users != None and type(users) != "list": 145 validate.string("users", users) 146 147 if type(projects) == "string": 148 projects = [projects] 149 elif projects != None and type(projects) != "list": 150 validate.string("projects", projects) 151 152 roles = validate.list("roles", roles, required = True) 153 groups = validate.list("groups", groups) 154 users = validate.list("users", users) 155 projects = validate.list("projects", projects) 156 157 for r in roles: 158 validate.struct("roles", r, _role_ctor) 159 for g in groups: 160 validate.string("groups", g) 161 for u in users: 162 validate.string("users", u) 163 for p in projects: 164 validate.string("projects", p) 165 166 # Some ACLs (e.g. LogDog) can be formulated only in terms of groups, 167 # check this. 168 for r in roles: 169 if r.groups_only and (users or projects): 170 fail("role %s can be assigned only to groups" % r.name) 171 172 return _entry_ctor( 173 roles = roles, 174 groups = groups, 175 users = users, 176 projects = projects, 177 ) 178 179 def _validate_acls( 180 acls, 181 *, 182 project_level = False, 183 allowed_roles = None): 184 """Validates the given list of acl.entry structs. 185 186 Checks that project level roles are set only on the project level. 187 188 Args: 189 acls: an iterable of acl.entry structs to validate, or None. 190 project_level: True to accept project_level_only=True roles. 191 allowed_roles: an optional whitelist of roles to accept. 192 193 Returns: 194 A list of validated acl.entry structs or [], never None. 195 """ 196 acls = validate.list("acls", acls) 197 for e in acls: 198 validate.struct("acls", e, _entry_ctor) 199 for r in e.roles: 200 if r.project_level_only and not project_level: 201 fail('bad "acls": role %s can only be set at the project level' % r.name) 202 if allowed_roles and r not in allowed_roles: 203 fail('bad "acls": role %s is not allowed in this context' % r.name) 204 return acls 205 206 def _normalize_acls(acls): 207 """Expands, dedups and sorts ACLs from the given list of acl.entry structs. 208 209 Expands plural 'roles', 'groups', 'users' and 'projects' fields in acl.entry 210 into multiple acl.elementary structs: elementary pairs of (role, principal), 211 where principal is either a user, a group or a project. 212 213 Args: 214 acls: an iterable of acl.entry structs to expand, assumed to be validated. 215 216 Returns: 217 A sorted deduped list of acl.elementary structs. 218 """ 219 out = [] 220 for e in (acls or []): 221 for r in e.roles: 222 for u in e.users: 223 out.append(_elementary_ctor(role = r, user = u, group = None, project = None)) 224 for g in e.groups: 225 out.append(_elementary_ctor(role = r, user = None, group = g, project = None)) 226 for p in e.projects: 227 out.append(_elementary_ctor(role = r, user = None, group = None, project = p)) 228 return sorted(set(out), key = _sort_key) 229 230 def _sort_key(e): 231 """acl.elementary -> tuple to sort it by.""" 232 if e.user: 233 order, ident = 0, e.user 234 elif e.group: 235 order, ident = 1, e.group 236 elif e.project: 237 order, ident = 2, e.project 238 else: 239 fail("impossible") 240 return (e.role.name, order, ident) 241 242 def _binding_dicts(acls): 243 """Takes a list of validated acl.entry structs and returns a list of dicts. 244 245 Each dict contains keyword arguments for a luci.binding(...) rule. Together 246 they represent the same ACL entries as `acls`. 247 """ 248 per_role = {} # role -> {roles: [role], groups: [], users: [], projects: []}. 249 for e in _normalize_acls(acls): 250 role = e.role.realms_role 251 if not role: 252 continue 253 254 binding = per_role.get(role) 255 if not binding: 256 binding = { 257 "roles": [role], 258 "groups": [], 259 "users": [], 260 "projects": [], 261 } 262 per_role[role] = binding 263 264 # `e` is acl.elementary which is a "union" struct: one and only one field is 265 # set. 266 if e.user: 267 binding["users"].append(e.user) 268 elif e.group: 269 binding["groups"].append(e.group) 270 elif e.project: 271 binding["projects"].append(e.project) 272 273 return per_role.values() 274 275 ################################################################################ 276 277 acl = struct( 278 entry = _entry, 279 280 # Note: the information in the comments is extracted by the documentation 281 # generator. That's the reason there's a bit of repetition here. 282 283 # Reading contents of project configs through LUCI Config API/UI. 284 # 285 # DocTags: 286 # project_level_only. 287 PROJECT_CONFIGS_READER = _role( 288 "PROJECT_CONFIGS_READER", 289 realms_role = "role/configs.reader", 290 project_level_only = True, 291 ), 292 293 # Reading logs under project's logdog prefix. 294 # 295 # DocTags: 296 # project_level_only, groups_only. 297 LOGDOG_READER = _role( 298 "LOGDOG_READER", 299 realms_role = "role/logdog.reader", 300 project_level_only = True, 301 groups_only = True, 302 ), 303 304 # Writing logs under project's logdog prefix. 305 # 306 # DocTags: 307 # project_level_only, groups_only. 308 LOGDOG_WRITER = _role( 309 "LOGDOG_WRITER", 310 realms_role = "role/logdog.writer", 311 project_level_only = True, 312 groups_only = True, 313 ), 314 315 # Fetching info about a build, searching for builds in a bucket. 316 BUILDBUCKET_READER = _role( 317 "BUILDBUCKET_READER", 318 realms_role = "role/buildbucket.reader", 319 ), 320 # Same as `BUILDBUCKET_READER` + scheduling and canceling builds. 321 BUILDBUCKET_TRIGGERER = _role( 322 "BUILDBUCKET_TRIGGERER", 323 realms_role = "role/buildbucket.triggerer", 324 ), 325 # Full access to the bucket (should be used rarely). 326 BUILDBUCKET_OWNER = _role( 327 "BUILDBUCKET_OWNER", 328 realms_role = "role/buildbucket.owner", 329 ), 330 331 # Viewing Scheduler jobs, invocations and their debug logs. 332 SCHEDULER_READER = _role( 333 "SCHEDULER_READER", 334 realms_role = "role/scheduler.reader", 335 ), 336 # Same as `SCHEDULER_READER` + ability to trigger jobs. 337 SCHEDULER_TRIGGERER = _role( 338 "SCHEDULER_TRIGGERER", 339 realms_role = "role/scheduler.triggerer", 340 ), 341 # Full access to Scheduler jobs, including ability to abort them. 342 SCHEDULER_OWNER = _role( 343 "SCHEDULER_OWNER", 344 realms_role = "role/scheduler.owner", 345 ), 346 347 # Committing approved CLs via CQ. 348 # 349 # DocTags: 350 # cq_role, groups_only. 351 CQ_COMMITTER = _role( 352 "CQ_COMMITTER", 353 groups_only = True, 354 realms_role = "role/cq.committer", 355 ), 356 357 # Executing presubmit tests for CLs via CQ. 358 # 359 # DocTags: 360 # cq_role, groups_only. 361 CQ_DRY_RUNNER = _role( 362 "CQ_DRY_RUNNER", 363 groups_only = True, 364 realms_role = "role/cq.dryRunner", 365 ), 366 367 # Having CV automatically run certain tryjobs (e.g. static analyzers) when 368 # a member uploads a new patchset to a CL monitored by CV and the feature 369 # is enabled. 370 # 371 # DocTags: 372 # cq_role, groups_only. 373 CQ_NEW_PATCHSET_RUN_TRIGGERER = _role( 374 "CQ_NEW_PATCHSET_RUN_TRIGGERER", 375 groups_only = True, 376 realms_role = None, 377 ), 378 ) 379 380 aclimpl = struct( 381 validate_acls = _validate_acls, 382 normalize_acls = _normalize_acls, 383 binding_dicts = _binding_dicts, 384 )