blob: f4a33afd774a8f1a8dd598c67f899d76b8a6c7be [file] [log] [blame]
# Copyright 2018 The LUCI Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
load('@stdlib//internal/luci/lib/validate.star', 'validate')
# TODO(vadimsh): Add support for 'anonymous' when/if needed.
# See https://chromium.googlesource.com/infra/luci/luci-py/+/master/appengine/components/components/auth/model.py
_GROUP_RE = r'^([a-z\-]+/)?[0-9a-zA-Z_][0-9a-zA-Z_\-\.\ @]{1,80}[0-9a-zA-Z_\-\.]$'
_USER_RE = r'^[0-9a-zA-Z_\-\.\+\%]+@[0-9a-zA-Z_\-\.]+$'
# A constructor for acl.role structs.
#
# Such structs are seen through public API as predefined symbols, e.g.
# acl.LOGDOG_READER. There's no way for an end-user to define a new role.
#
# Expected to be used as roles in acl.entry(role=...) definitions, and maybe
# printed (when debugging).
#
# Fields:
# name: name of the role.
# project_level_only: True if the role can be set only in project(...) rule.
# groups_only: True if the role should be assigned only to groups, not users.
_role_ctor = genstruct('acl.role')
# A constructor for acl.entry structs.
#
# Such structs are created via public acl.entry(...) API. To make their
# printable representation useful and not confusing to end users, their
# structure somewhat resembles acl.entry(...) arguments list.
#
# They are not convenient though when generating configs. For that reason
# there's another representation of ACLs: as a list of elementary
# (role, principal) tuples, where principals can be of few different types
# (e.g. groups or users). Internal API function 'normalize_acls' converts
# from the user-friendly acl.entry representation to the generator-friendly
# acl.elementary representation.
#
# Fields:
# roles: a list of acl.role in the entry, at least one.
# users: a list of user emails to apply roles to, may be empty.
# groups: a list of group names to apply roles to, may be empty.
_entry_ctor = genstruct('acl.entry')
# A constructor for acl.elementary structs.
#
# This is conceptually a sum type: (Role, User | Group). For convenience it is
# represented as a triple where either 'user' or 'group' is set, but not both.
#
# Fields:
# role: an acl.role, always set.
# user: an user email.
# group: a group name.
_elementary_ctor = genstruct('acl.elementary')
def _role(name, project_level_only=False, groups_only=False):
"""Defines a role.
Internal API. Only predefined roles are available publicly, see the bottom of
this file.
Args:
name: string name of the role.
project_level_only: True if it can be used only in project(...) ACLs.
groups_only: True if role supports only group-based ACL (not user-based).
Returns:
acl.role struct.
"""
return _role_ctor(
name = name,
project_level_only = project_level_only,
groups_only = groups_only,
)
def _entry(roles, groups=None, users=None):
"""An ACL entry: assigns given role (or roles) to given individuals or groups.
Specifying an empty ACL entry is allowed. It is ignored everywhere. Useful for
things like:
core.project(
acl = [
acl.entry(acl.PROJECT_CONFIGS_READER, groups = [
# TODO: fill me in
])
]
)
Args:
roles: a single role (as acl.role) or a list of roles to assign.
groups: a single group name or a list of groups to assign the role to.
users: a single user email or a list of emails to assign the role to.
Returns:
acl.entry struct, consider it opaque.
"""
if ctor(roles) == _role_ctor:
roles = [roles]
elif roles != None and type(roles) != 'list':
validate.struct('roles', roles, _role_ctor)
if type(groups) == 'string':
groups = [groups]
elif groups != None and type(groups) != 'list':
validate.string('groups', groups, regexp=_GROUP_RE)
if type(users) == 'string':
users = [users]
elif users != None and type(users) != 'list':
validate.string('users', users, regexp=_USER_RE)
roles = validate.list('roles', roles, required=True)
groups = validate.list('groups', groups)
users = validate.list('users', users)
for r in roles:
validate.struct('roles', r, _role_ctor)
for g in groups:
validate.string('groups', g, regexp=_GROUP_RE)
for u in users:
validate.string('users', u, regexp=_USER_RE)
# Some ACLs (e.g. LogDog) can be formulated only in terms of groups,
# check this.
for r in roles:
if r.groups_only and users:
fail('role %s can be assigned only to groups, not individual users' % r.name)
return _entry_ctor(
roles = roles,
groups = groups,
users = users,
)
def _validate_acls(acls, project_level=False):
"""Validates the given list of acl.entry structs.
Checks that project level roles are set only on the project level.
Args:
acls: an iterable of acl.entry structs to validate, or None.
project_level: True to accept project_level_only=True roles.
Returns:
A list of validated acl.entry structs or [], never None.
"""
acls = validate.list('acls', acls)
for e in acls:
validate.struct('acls', e, _entry_ctor)
for r in e.roles:
if r.project_level_only and not project_level:
fail('bad "acls": role %s can only be set at the project level' % r.name)
return acls
def _normalize_acls(acls):
"""Expands, dedups and sorts ACLs from the given list of acl.entry structs.
Expands plural 'roles', 'groups' and 'users' fields in acl.entry into multiple
acl.elementary structs: elementary pairs of (role, principal), where principal
is either a user or a group.
Args:
acls: an iterable of acl.entry structs to expand, assumed to be validated.
Returns:
A sorted deduped list of acl.elementary structs.
"""
out = []
for e in acls:
for r in e.roles:
for u in e.users:
out.append(_elementary_ctor(role=r, user=u, group=None))
for g in e.groups:
out.append(_elementary_ctor(role=r, user=None, group=g))
return sorted(set(out), key=_sort_key)
def _sort_key(e):
"""acl.elementary -> tuple to sort it by."""
return (e.role.name, 'u:' + e.user if e.user else 'g:' + e.group)
################################################################################
# Helper to avoid retyping role names.
def _roles_dict(l):
return {r.name: r for r in l}
# Public API exposed to end-users and to other LUCI modules.
acl = struct(
entry = _entry,
# All predefined roles.
**_roles_dict([
# Reading contents of project configs through LUCI Config API/UI.
_role('PROJECT_CONFIGS_READER', project_level_only=True),
# Reading logs under project's logdog prefix.
_role('LOGDOG_READER', project_level_only=True, groups_only=True),
# Writing logs under project's logdog prefix.
_role('LOGDOG_WRITER', project_level_only=True, groups_only=True),
# Fetching info about a build, searching for builds in a bucket.
_role('BUILDBUCKET_READER'),
# Same as BUILDBUCKET_READER + scheduling and canceling builds.
_role('BUILDBUCKET_TRIGGERER'),
# Have full access to the bucket (should be used rarely).
_role('BUILDBUCKET_OWNER'),
# Viewing Scheduler jobs, invocations and their debug logs.
_role('SCHEDULER_READER'),
# Same as SCHEDULER_READER + ability to trigger jobs.
_role('SCHEDULER_TRIGGERER'),
# Have full access to jobs, including ability to abort them.
_role('SCHEDULER_OWNER'),
])
)
# Additional internal API used by other LUCI modules.
aclimpl = struct(
validate_acls = _validate_acls,
normalize_acls = _normalize_acls,
)