def check_can_create_token(user_id, subtoken): """Checks that caller is allowed to mint a given root token. Args: caller_id: identity string of a current caller. subtoken: instance of delegation_pb2.Subtoken describing root token. Raises: auth.AuthorizationError if such token is not allowed for the caller. """ # Empty subtoken.services means the token applies to all services. rule = get_delegation_rule(user_id, subtoken.services or ['*']) if subtoken.validity_duration > rule.max_validity_duration: raise auth.AuthorizationError( 'Maximum allowed validity_duration is %d sec, %d requested.' % (rule.max_validity_duration, subtoken.validity_duration)) # Just delegating one's own identity (not impersonating someone else)? Allow. if subtoken.issuer_id == user_id: return # Verify it's OK to impersonate a given user. impersonated = auth.Identity.from_bytes(subtoken.issuer_id) for principal_set in rule.allowed_to_impersonate: if is_identity_in_principal_set(impersonated, principal_set): return raise auth.AuthorizationError( '"%s" is not allowed to impersonate "%s" on %s' % (user_id, subtoken.issuer_id, subtoken.services or ['*']))
def _check_dimension_acls(request): """Raises AuthorizationError if some requested dimensions are forbidden. Uses 'dimension_acls' field from the settings. See proto/config.proto. """ dim_acls = config.settings().dimension_acls if not dim_acls or not dim_acls.entry: return # not configured, this is fine ident = request.authenticated dims = request.properties.dimensions assert 'id' in dims or 'pool' in dims, dims # see _validate_dimensions assert ident is not None # see task_request.init_new_request # Forbid targeting individual bots for non-admins, but allow using 'id' if # 'pool' is used as well (so whoever can posts tasks to 'pool', can target an # individual bot in that pool). if 'id' in dims and 'pool' not in dims: if not acl.is_admin(): raise auth.AuthorizationError( 'Only Swarming administrators can post tasks with "id" dimension ' 'without specifying a "pool" dimension.') for k, v in sorted(dims.iteritems()): if not _can_use_dimension(dim_acls, ident, k, v): raise auth.AuthorizationError( 'User %s is not allowed to schedule tasks with dimension "%s:%s"' % (ident.to_bytes(), k, v))
def _check_permission(perm, realms, identity=None): """Checks if the caller has the realm permission. Args: perm: An instance of auth.Permission. realms: List of realms. identity: An instance of auth.Identity to check permission. default is auth.get_current_identity(). Returns: None Raises: auth.AuthorizationError: if the caller is not allowed or realm is missing. """ # Remove None from list realms = [r for r in realms if r] if not identity: identity = auth.get_current_identity() if not realms: raise auth.AuthorizationError('Realm is missing') if not auth.has_permission(perm, realms, identity=identity): logging.warning( '[realms] %s "%s" does not have permission "%s" in any realms %s', identity.kind, identity.name, perm.name, realms) raise auth.AuthorizationError( '%s "%s" does not have permission "%s"' % (identity.kind, identity.name, perm.name)) logging.info('[realms] %s "%s" has permission "%s" in any realms %s', identity.kind, identity.name, perm.name, realms)
def check_bot_code_access(self, bot_id, generate_token): """Raises AuthorizationError if caller is not authorized to access bot code. Four variants here: 1. A valid bootstrap token is passed as '?tok=...' parameter. 2. An user, allowed to do a bootstrap, is using their credentials. 3. An IP whitelisted machine is making this call. 4. A bot (with given bot_id) is using it's own machine credentials. In later three cases we optionally generate and return a new bootstrap token, that can be used to authorize /bot_code calls. """ existing_token = self.request.get('tok') if existing_token: payload = bot_code.validate_bootstrap_token(existing_token) if payload is None: raise auth.AuthorizationError('Invalid bootstrap token') logging.debug('Using bootstrap token %r', payload) return existing_token machine_type = None if bot_id: bot_info = bot_management.get_info_key(bot_id).get() if bot_info: machine_type = bot_info.machine_type # TODO(vadimsh): Remove is_ip_whitelisted_machine check once all bots are # using auth for bootstrap and updating. if (not acl.can_create_bot() and not acl.is_ip_whitelisted_machine() and not (bot_id and bot_auth.is_authenticated_bot( bot_id, machine_type))): raise auth.AuthorizationError('Not allowed to access the bot code') return bot_code.generate_bootstrap_token() if generate_token else None
def check_schedule_request_acl(request): """Verifies the current caller can schedule a given task request. Arguments: - request: TaskRequest entity with information about the new task. Raises: auth.AuthorizationError if the caller is not allowed to schedule this task. """ # Only terminate tasks don't have a pool. ACLs for them are handled through # 'acl.can_edit_bot', see 'terminate' RPC handler. Such tasks do not end up # hitting this function, and so we can assume there's a pool set (this is # checked in TaskProperties's pre put hook). # # It is guaranteed that at most one item can be specified in 'pool' and that # every TaskSlice property dimensions use the same pool and id dimensions. pool = request.task_slice(0).properties.dimensions[u'pool'][0] pool_cfg = pools_config.get_pool_config(pool) # request.service_account can be 'bot' or 'none'. We don't care about these, # they are always allowed. We care when the service account is a real email. has_service_account = service_accounts.is_service_account( request.service_account) if not pool_cfg: logging.info('Pool "%s" is not in pools.cfg', pool) # Deployments that use pools.cfg may forbid unknown pools completely. # This is a safeguard against anyone executing tasks in a new not fully # configured pool. if pools_config.forbid_unknown_pools(): raise auth.AuthorizationError( 'Can\'t submit tasks to pool "%s" not defined in pools.cfg' % pool) # To be backward compatible with Swarming deployments that do not care about # pools isolation pools without pools.cfg entry are relatively wide open # (anyone with 'can_create_task' permission can use them). As a downside, # they are not allowed to use service accounts, since Swarming doesn't know # what accounts are allowed in such unknown pools. if has_service_account: raise auth.AuthorizationError( 'Can\'t use account "%s" in the pool "%s" not defined in pools.cfg' % (request.service_account, pool)) return logging.info( 'Looking at the pool "%s" in pools.cfg, rev "%s"', pool, pool_cfg.rev) # Verify the caller can use the pool at all. if not _is_allowed_to_schedule(pool_cfg): raise auth.AuthorizationError( 'User "%s" is not allowed to schedule tasks in the pool "%s", ' 'see pools.cfg' % (auth.get_current_identity().to_bytes(), pool)) # Verify the requested task service account is allowed in this pool. if (has_service_account and not _is_allowed_service_account(request.service_account, pool_cfg)): raise auth.AuthorizationError( 'Service account "%s" is not allowed in the pool "%s", see pools.cfg' % (request.service_account, pool))
def validate_bot_id_and_fetch_config(bot_id): """Verifies ID reported by a bot matches the credentials being used. Expected to be called in a context of some bot API request handler. Uses bots.cfg config to look up what credentials are expected to be used by the bot with given ID. Raises auth.AuthorizationError if bot_id is unknown or bot is using invalid credentials. On success returns the configuration for this bot (BotGroupConfig tuple), as defined in bots.cfg """ cfg = bot_groups_config.get_bot_group_config(bot_id) if not cfg: logging.error( 'bot_auth: unknown bot_id, not in the config\nbot_id: "%s"', bot_id) raise auth.AuthorizationError('Unknown bot ID, not in config') peer_ident = auth.get_peer_identity() if cfg.require_luci_machine_token: if not _is_valid_ident_for_bot(peer_ident, bot_id): logging.error( 'bot_auth: bot ID doesn\'t match the machine token used\n' 'bot_id: "%s", peer_ident: "%s"', bot_id, peer_ident.to_bytes()) raise auth.AuthorizationError( 'Bot ID doesn\'t match the token used') elif cfg.require_service_account: expected_id = auth.Identity(auth.IDENTITY_USER, cfg.require_service_account) if peer_ident != expected_id: logging.error( 'bot_auth: bot is not using expected service account\n' 'bot_id: "%s", expected_id: "%s", peer_ident: "%s"', bot_id, expected_id.to_bytes(), peer_ident.to_bytes()) raise auth.AuthorizationError( 'bot is not using expected service account') elif not cfg.ip_whitelist: # This branch should not be hit for validated configs. logging.error( 'bot_auth: invalid bot group config, no auth method defined\n' 'bot_id: "%s"', bot_id) raise auth.AuthorizationError('Invalid bot group config') # Check that IP whitelist applies (in addition to credentials). if cfg.ip_whitelist: ip = auth.get_peer_ip() if not auth.is_in_ip_whitelist(cfg.ip_whitelist, ip): logging.error( 'bot_auth: bot IP is not whitelisted\n' 'bot_id: "%s", peer_ip: "%s", ip_whitelist: "%s"', bot_id, ipaddr.ip_to_string(ip), cfg.ip_whitelist) raise auth.AuthorizationError('Not IP whitelisted') return cfg
def ingest_tarball(name, content): """Handles upload of tarballs specified in 'tarball_upload' config entries. Expected to be called in an auth context of the upload PUT request. Args: name: name of the corresponding 'tarball_upload' entry (defines ACLs). content: raw byte buffer with *.tar.gz file data. Returns: (list of modified groups, new AuthDB revision number or 0 if no changes). Raises: auth.AuthorizationError if caller is not authorized to do this upload. BundleImportError if the tarball can't be imported (e.g. wrong format). """ # Return generic HTTP 403 error unless we can verify the caller to avoid # leaking information about our config. config = load_config() if not config: logging.error('Group imports are not configured') raise auth.AuthorizationError() # Same here. We should not leak config entry names to untrusted callers. entry = None for entry in config.tarball_upload: if entry.name == name: break else: logging.error('No such tarball_upload entry in the config: "%s"', name) raise auth.AuthorizationError() # The caller must be specified in 'authorized_uploader' field. caller = auth.get_current_identity() for email in entry.authorized_uploader: if caller == model.Identity(model.IDENTITY_USER, email): break else: logging.error('Caller %s is not authorized to upload tarball "%s"', caller.to_bytes(), entry.name) raise auth.AuthorizationError() # Authorization check passed. Now parse the tarball, converting it into # {system -> {group -> identities}} map (aka "bundles set") and import it into # the datastore. logging.info('Ingesting tarball "%s" uploaded by %s', name, caller.to_bytes()) bundles = load_tarball(content, entry.systems, entry.groups, entry.domain) return import_bundles(bundles, caller, 'Uploaded as "%s" tarball' % entry.name)
def search_instances(self, request): """Returns package instances with given tag (in no particular order).""" tag = validate_instance_tag(request.tag) if request.package_name: package_name = validate_package_name(request.package_name) else: package_name = None caller = auth.get_current_identity() callback = None if package_name: # If search is limited to one package, check its ACL only once. if not acl.can_fetch_instance(package_name, caller): raise auth.AuthorizationError() else: # Filter out packages not allowed by ACL. acl_cache = {} def check_readable(package_name, _instance_id): if package_name not in acl_cache: acl_cache[package_name] = acl.can_fetch_instance(package_name, caller) return acl_cache[package_name] callback = check_readable found = self.service.search_by_tag(tag, package_name, callback) return SearchResponse(instances=[instance_to_proto(i) for i in found])
def register_instance(self, request): """Registers a new package instance in the repository.""" package_name = validate_package_name(request.package_name) instance_id = validate_instance_id(request.instance_id) caller = auth.get_current_identity() if not acl.can_register_instance(package_name, caller): raise auth.AuthorizationError() instance = self.service.get_instance(package_name, instance_id) if instance is not None: return RegisterInstanceResponse( status=Status.ALREADY_REGISTERED, instance=instance_to_proto(instance)) # Need to upload to CAS first? Open an upload session. Caller must use # CASServiceApi to finish the upload and then call registerInstance again. if not self.service.is_instance_file_uploaded(package_name, instance_id): upload_url, upload_session_id = self.service.create_upload_session( package_name, instance_id, caller) return RegisterInstanceResponse( status=Status.UPLOAD_FIRST, upload_session_id=upload_session_id, upload_url=upload_url) # Package data is in the store. Make an entity. instance, registered = self.service.register_instance( package_name=package_name, instance_id=instance_id, caller=caller, now=utils.utcnow()) return RegisterInstanceResponse( status=Status.REGISTERED if registered else Status.ALREADY_REGISTERED, instance=instance_to_proto(instance))
def current_identity_cannot(action_format, *args): # pragma: no cover """Returns AuthorizationError.""" action = action_format % args msg = 'User %s cannot %s' % (auth.get_current_identity().to_bytes(), action) logging.warning(msg) return auth.AuthorizationError(msg)
def fetch_client_binary(self, request): """Returns signed URL that can be used to fetch CIPD client binary.""" package_name = validate_package_name(request.package_name) if not client.is_cipd_client_package(package_name): raise ValidationError('Not a CIPD client package') instance_id = validate_instance_id(request.instance_id) caller = auth.get_current_identity() if not acl.can_fetch_instance(package_name, caller): raise auth.AuthorizationError() # Grab the location of the extracted binary. instance = self.get_instance(package_name, instance_id) client_info, error_message = self.service.get_client_binary_info(instance) if error_message: raise Error(error_message) if client_info is None: return FetchClientBinaryResponse( status=Status.NOT_EXTRACTED_YET, instance=instance_to_proto(instance)) return FetchClientBinaryResponse( instance=instance_to_proto(instance), client_binary=FetchClientBinaryResponse.ClientBinary( sha1=client_info.sha1, size=client_info.size, fetch_url=client_info.fetch_url, file_name=client.get_cipd_client_filename(package_name)))
def init_new_request(request, allow_high_priority): """Initializes a new TaskRequest but doesn't store it. Fills up some values and does minimal checks. If parent_task_id is set, properties for the parent are used: - priority: defaults to parent.priority - 1 - user: overridden by parent.user """ assert request.__class__ is TaskRequest, request if request.parent_task_id: run_result_key = task_pack.unpack_run_result_key(request.parent_task_id) result_summary_key = task_pack.run_result_key_to_result_summary_key( run_result_key) request_key = task_pack.result_summary_key_to_request_key( result_summary_key) parent = request_key.get() if not parent: raise ValueError('parent_task_id is not a valid task') request.priority = max(min(request.priority, parent.priority - 1), 0) # Drop the previous user. request.user = parent.user # If the priority is below 100, make sure the user has right to do so. if request.priority < 100 and not allow_high_priority: # Special case for terminate request. if not request.properties.is_terminate: # Silently drop the priority of normal users. request.priority = 100 request.authenticated = auth.get_current_identity() if (not request.properties.is_terminate and request.properties.grace_period_secs is None): request.properties.grace_period_secs = 30 if request.properties.idempotent is None: request.properties.idempotent = False request.service_account = 'none' if request.service_account_token and request.service_account_token != 'none': if request.service_account_token == 'bot': request.service_account = 'bot' else: # TODO(vadimsh): Check the token signature, verify it can be used by the # current user, extract service account email. raise auth.AuthorizationError('service_account_token is not implemented') request.tags.append('priority:%s' % request.priority) request.tags.append('user:%s' % request.user) request.tags.append('service_account:%s' % request.service_account) for key, value in request.properties.dimensions.iteritems(): request.tags.append('%s:%s' % (key, value)) request.tags = sorted(set(request.tags)) if request.properties.idempotent: request.properties_hash = request.HASHING_ALGO( utils.encode_to_json(request.properties)).digest() else: request.properties_hash = None
def check_task_cancel_acl(task_request): """Checks if the caller is allowed to cancel the task. Checks if the caller has global permission using acl.can_edit_task(). If the caller doesn't have any global permissions, Checks if the caller has either of 'swarming.pools.cancelTask' or 'swarming.tasks.cancel' permission. Args: task_request: An instance of TaskRequest. Returns: None Raises: auth.AuthorizationError: if the caller is not allowed. """ # check global permission. if acl.can_edit_task(task_request): return # check 'swarming.pools.cancelTask' permission of the pool in task dimensions. if task_request.pool: pool_cfg = pools_config.get_pool_config(task_request.pool) if not pool_cfg: raise endpoints.InternalServerErrorException( 'Pool cfg not found. pool: %s' % task_request.pool) if pool_cfg.realm and auth.has_permission( get_permission(realms_pb2.REALM_PERMISSION_POOLS_CANCEL_TASK), [pool_cfg.realm]): return # check 'swarming.pools.cancelTask' permission of the pool in bot dimensions. if task_request.bot_id: pools = bot_management.get_pools_from_dimensions_flat( _retrieve_bot_dimensions(task_request.bot_id)) pool_realms = [ p.realm for p in map(pools_config.get_pool_config, pools) if p.realm ] if pool_realms and auth.has_permission( get_permission(realms_pb2.REALM_PERMISSION_POOLS_CANCEL_TASK), pool_realms): return # check 'swarming.tasks.cancel' permission. task_realm = task_request.realm if task_realm and auth.has_permission( get_permission(realms_pb2.REALM_PERMISSION_TASKS_CANCEL), [task_realm]): return raise auth.AuthorizationError('Task "%s" is not accessible' % task_request.task_id)
def _get_bot_group_config(bot_id): """Finds a bot group config for given bot_id A dockerized bot may contain the magic word '--' in the bot_id. At the first attempt, the full bot_id is used to get bot group config. If it didn't find one, the hostname is used. Args: bot_id: ID of the bot. Returns: auth_bot_id: ID which will be used for authentication. bot_group_config: matched bot group config. Raises: auth.AuthorizationError: if the bot_id doesn't match any groups in the bots.cfg. """ # In many cases, hostname == bot_id. But dockeriezed bots contain # magic word '--' to represent host name. e.g. bot_id=foo--bar > hostname=foo # Those bots should be authenticated using the hostname. hostname = _extract_primary_hostname(bot_id) # At first, try to get bot group config with given bot_id # return the config if it's not default config cfg = bot_groups_config.get_bot_group_config(bot_id) if cfg and not cfg.is_default: logging.debug( 'bot_auth: found a bot group cfg for bot_id: "%s"\n' 'hostname: "%s" will be used for authentication', bot_id, hostname) return hostname, cfg # if hostname == bot_id, the same config would be returned at next attempt. if cfg and hostname == bot_id: logging.debug( 'bot_auth: found a bot group cfg for bot_id: "%s"\n' 'bot_id will be used for authentication', bot_id) return hostname, cfg # For a dockerized bot which has magic word '--' in the bot_id, # try to get bot group config with the host name cfg = bot_groups_config.get_bot_group_config(hostname) if cfg: logging.debug( 'bot_auth: found a bot group cfg for bot_id: "%s"' ' using hostname: "%s"\nhostname will be used for authentication', bot_id, hostname) return hostname, cfg # This path isn't reachable when bots.cfg has a default group. logging.error( 'bot_auth: unknown bot_id, not in the config\n' 'bot_id: "%s" hostname: "%s"', bot_id, hostname) raise auth.AuthorizationError('Unknown bot ID, not in config')
def delete_package(self, request): """Deletes a package along with all its instances.""" package_name = validate_package_name(request.package_name) caller = auth.get_current_identity() if not acl.can_delete_package(package_name, caller): raise auth.AuthorizationError() deleted = self.service.delete_package(package_name) if not deleted: raise PackageNotFoundError() return DeletePackageResponse()
def fetch_package(self, request): """Returns information about a package.""" package_name = validate_package_name(request.package_name) caller = auth.get_current_identity() if not acl.can_fetch_package(package_name, caller): raise auth.AuthorizationError() pkg = self.service.get_package(package_name) if pkg is None: raise PackageNotFoundError() return FetchPackageResponse(package=package_to_proto(pkg))
def fetch_acl(self, request): """Returns access control list for a given package path.""" package_path = validate_package_path(request.package_path) caller = auth.get_current_identity() if not acl.can_fetch_acl(package_path, caller): raise auth.AuthorizationError() return FetchACLResponse( acls=package_acls_to_proto({ role: acl.get_package_acls(package_path, role) for role in acl.ROLES }))
def post(self): # Forbid usage of delegation tokens for this particular call. Using # delegation when creating delegation tokens is too deep. Redelegation will # be done as separate explicit API call that accept existing delegation # token via request body, not via headers. if auth.get_current_identity() != auth.get_peer_identity(): raise auth.AuthorizationError( 'This API call must not be used with active delegation token') # Convert request body to proto (with validation). Verify IP format. try: body = self.parse_body() subtoken = subtoken_from_jsonish(body) intent = body.get('intent') or '' if not isinstance(intent, basestring): raise TypeError('"intent" must be string') except (TypeError, ValueError) as exc: self.abort_with_error(400, text=str(exc)) # Fill in defaults. assert not subtoken.requestor_identity user_id = auth.get_current_identity().to_bytes() subtoken.requestor_identity = user_id if not subtoken.delegated_identity: subtoken.delegated_identity = user_id subtoken.creation_time = int(utils.time_time()) if not subtoken.validity_duration: subtoken.validity_duration = DEF_VALIDITY_DURATION_SEC if '*' in subtoken.services: subtoken.services[:] = get_default_allowed_services(user_id) # Check ACL (raises auth.AuthorizationError on errors). rule = check_can_create_token(user_id, subtoken) # Register the token in the datastore, generate its ID. subtoken.subtoken_id = register_subtoken(subtoken, rule, intent, auth.get_peer_ip()) # Create and sign the token. try: token = delegation.serialize_token(delegation.seal_token(subtoken)) except delegation.BadTokenError as exc: # This happens if resulting token is too large. self.abort_with_error(400, text=str(exc)) self.send_response(response={ 'delegation_token': token, 'subtoken_id': str(subtoken.subtoken_id), 'validity_duration': subtoken.validity_duration, }, http_code=201)
def fetch_instance(self, request): """Returns signed URL that can be used to fetch a package instance.""" package_name = validate_package_name(request.package_name) instance_id = validate_instance_id(request.instance_id) caller = auth.get_current_identity() if not acl.can_fetch_instance(package_name, caller): raise auth.AuthorizationError() instance = self.get_instance(package_name, instance_id) return FetchInstanceResponse( instance=instance_to_proto(instance), fetch_url=self.service.generate_fetch_url(instance), processors=processors_protos(instance))
def test_schedule_build_requests(self, add_many_async): add_many_async.return_value = future([ (test_util.build(id=42), None), (test_util.build(id=43), None), (None, errors.InvalidInputError('bad')), (None, Exception('unexpected')), (None, auth.AuthorizationError('bad')), ]) user.can_async.side_effect = ( lambda bucket_id, _: future('forbidden' not in bucket_id)) linux_builder = dict(project='chromium', bucket='try', builder='linux') win_builder = dict(project='chromium', bucket='try', builder='windows') req = rpc_pb2.BatchRequest( requests=[ dict(schedule_build=dict(builder=linux_builder)), dict(schedule_build=dict(builder=linux_builder, fields=dict(paths=['tags']))), dict(schedule_build=dict(builder=linux_builder, fields=dict(paths=['wrong-field']))), dict(schedule_build=dict(builder=win_builder)), dict(schedule_build=dict(builder=win_builder)), dict(schedule_build=dict(builder=win_builder)), dict(schedule_build=dict(builder=dict(project='chromium', bucket='forbidden', builder='nope'), )), dict( schedule_build=dict(), # invalid request ), ], ) res = self.call(self.api.Batch, req) codes = [r.error.code for r in res.responses] self.assertEqual(codes, [ prpc.StatusCode.OK.value, prpc.StatusCode.OK.value, prpc.StatusCode.INVALID_ARGUMENT.value, prpc.StatusCode.INVALID_ARGUMENT.value, prpc.StatusCode.INTERNAL.value, prpc.StatusCode.PERMISSION_DENIED.value, prpc.StatusCode.PERMISSION_DENIED.value, prpc.StatusCode.INVALID_ARGUMENT.value, ]) self.assertEqual(res.responses[0].schedule_build.id, 42) self.assertFalse(len(res.responses[0].schedule_build.tags)) self.assertTrue(len(res.responses[1].schedule_build.tags))
def detach_tags(self, request): """Removes given tags from a package instance.""" package_name = validate_package_name(request.package_name) instance_id = validate_instance_id(request.instance_id) tags = validate_instance_tag_list(request.tag) caller = auth.get_current_identity() for tag in tags: if not acl.can_detach_tag(package_name, tag, caller): raise auth.AuthorizationError('Not authorized to detach "%s"' % tag) self.verify_instance_exists(package_name, instance_id) self.service.detach_tags( package_name=package_name, instance_id=instance_id, tags=tags) return DetachTagsResponse()
def post(self): # Forbid usage of delegation tokens for this particular call. Using # delegation when creating delegation tokens is too deep. Redelegation will # be done as separate explicit API call that accept existing delegation # token via request body, not via headers. if auth.get_current_identity() != auth.get_peer_identity(): raise auth.AuthorizationError( 'This API call must not be used with active delegation token') # Convert request body to proto (with validation). try: subtoken = subtoken_from_jsonish(self.parse_body()) except (TypeError, ValueError) as exc: self.abort_with_error(400, text=str(exc)) # Fill in defaults. assert not subtoken.impersonator_id user_id = auth.get_current_identity().to_bytes() if not subtoken.issuer_id: subtoken.issuer_id = user_id if subtoken.issuer_id != user_id: subtoken.impersonator_id = user_id subtoken.creation_time = int(utils.time_time()) if not subtoken.validity_duration: subtoken.validity_duration = DEF_VALIDITY_DURATION_SEC if not subtoken.services or '*' in subtoken.services: subtoken.services[:] = get_default_allowed_services(user_id) # Check ACL (raises auth.AuthorizationError on errors). check_can_create_token(user_id, subtoken) # Create and sign the token. try: token = delegation.serialize_token( delegation.seal_token( delegation_pb2.SubtokenList(subtokens=[subtoken]))) except delegation.BadTokenError as exc: # This happens if resulting token is too large. self.abort_with_error(400, text=str(exc)) self.send_response(response={ 'delegation_token': token, 'validity_duration': subtoken.validity_duration, }, http_code=201)
def set_ref(self, request): """Creates a ref or moves an existing one.""" package_name = validate_package_name(request.package_name) ref = validate_package_ref(request.ref) instance_id = validate_instance_id(request.instance_id) caller = auth.get_current_identity() if not acl.can_move_ref(package_name, ref, caller): raise auth.AuthorizationError('Not authorized to move "%s"' % ref) self.verify_instance_is_ready(package_name, instance_id) ref_entity = self.service.set_package_ref( package_name=package_name, ref=ref, instance_id=instance_id, caller=caller, now=utils.utcnow()) return SetRefResponse(ref=package_ref_to_proto(ref_entity))
def set_package_hidden(self, package_name, hidden): """Common implementation for hide_package and unhide_package.""" package_name = validate_package_name(package_name) caller = auth.get_current_identity() if not acl.can_modify_hidden(package_name, caller): raise auth.AuthorizationError() def mutation(pkg): if pkg.hidden == hidden: return False pkg.hidden = hidden return True pkg = self.service.modify_package(package_name, mutation) if pkg is None: raise PackageNotFoundError() return PackageResponse(package=package_to_proto(pkg))
def attach_tags(self, request): """Attaches a set of tags to a package instance.""" package_name = validate_package_name(request.package_name) instance_id = validate_instance_id(request.instance_id) tags = validate_instance_tag_list(request.tags) caller = auth.get_current_identity() for tag in tags: if not acl.can_attach_tag(package_name, tag, caller): raise auth.AuthorizationError('Not authorized to attach "%s"' % tag) self.verify_instance_is_ready(package_name, instance_id) attached = self.service.attach_tags( package_name=package_name, instance_id=instance_id, tags=tags, caller=caller, now=utils.utcnow()) return AttachTagsResponse(tags=[tag_to_proto(attached[t]) for t in tags])
def fetch_tags(self, request): """Lists package instance tags (in no particular order).""" package_name = validate_package_name(request.package_name) instance_id = validate_instance_id(request.instance_id) tags = validate_instance_tag_list(request.tag) if request.tag else None caller = auth.get_current_identity() if not acl.can_fetch_instance(package_name, caller): raise auth.AuthorizationError() self.verify_instance_exists(package_name, instance_id) if not tags: # Fetch all. attached = self.service.query_tags(package_name, instance_id) else: # Fetch selected only. "Is tagged by?" check essentially. found = self.service.get_tags(package_name, instance_id, tags) attached = [found[tag] for tag in tags if found[tag]] return FetchTagsResponse(tags=[tag_to_proto(tag) for tag in attached])
def modify_acl(self, request): """Changes access control list for a given package path.""" package_path = validate_package_path(request.package_path) try: changes = [ role_change_from_proto(msg, package_path) for msg in request.changes ] except ValueError as exc: raise ValidationError('Invalid role change request: %s' % exc) caller = auth.get_current_identity() if not acl.can_modify_acl(package_path, caller): raise auth.AuthorizationError() # Apply changes. Do not catch ValueError. Validation above should be # sufficient. If it is not, HTTP 500 and an uncaught exception in logs is # exactly what is needed. acl.modify_roles(changes, caller, utils.utcnow()) return ModifyACLResponse()
def increment_counter(self, request): """Increments a counter on a package instance.""" package_name = validate_package_name(request.package_name) instance_id = validate_instance_id(request.instance_id) counter_name = validate_counter_name(request.counter_name) delta = request.delta if delta not in (0, 1): raise ValidationError('Delta must be either 0 or 1') caller = auth.get_current_identity() if not acl.can_modify_counter(package_name, caller): raise auth.AuthorizationError() self.verify_instance_exists(package_name, instance_id) self.service.increment_counter( package_name=package_name, instance_id=instance_id, counter_name=counter_name, delta=delta) return IncrementCounterResponse()
def resolve_version(self, request): """Returns instance ID of an existing instance given a ref or a tag.""" package_name = validate_package_name(request.package_name) version = validate_instance_version(request.version) caller = auth.get_current_identity() if not acl.can_fetch_instance(package_name, caller): raise auth.AuthorizationError() pkg = self.service.get_package(package_name) if pkg is None: raise PackageNotFoundError() ids = self.service.resolve_version(package_name, version, limit=2) if not ids: raise InstanceNotFoundError() if len(ids) > 1: return ResolveVersionResponse( status=Status.AMBIGUOUS_VERSION, error_message='More than one instance has tag "%s" set' % version) return ResolveVersionResponse(instance_id=ids[0])
def _check_pools_filters_acl(perm_enum, pools): """Checks if the caller has the permission in the specified pools. The caller needs to have the permission in *all* pools. Raises: auth.AuthorizationError: if the caller is not allowed. """ # Pool dimension is required if the caller doesn't have global permission. if not pools: raise auth.AuthorizationError('No pool is specified') perm = get_permission(perm_enum) # the caller needs to have all permissions of the pools. for p in pools: pool_cfg = pools_config.get_pool_config(p) if not pool_cfg: raise endpoints.BadRequestException('Pool "%s" not found' % p) _check_permission(perm, [pool_cfg.realm])