def user_reference(username): user = model.user.get_namespace_user(username) if not user: return username if user.robot: parts = parse_robot_username(username) user = model.user.get_namespace_user(parts[0]) return """<span><img src="%s" alt="Robot"> <b>%s</b></span>""" % ( icon_path("wrench"), username, ) avatar_html = avatar.get_mail_html( user.username, user.email, 24, "org" if user.organization else "user" ) return """ <span> %s <b>%s</b> </span>""" % ( avatar_html, username, )
def test_syncing(user_creation, invite_only_user_creation, starting_membership, group_membership, expected_membership, blacklisted_emails, app): org = model.organization.get_organization('buynlarge') # Necessary for the fake auth entries to be created in FederatedLogin. database.LoginService.create(name=_FAKE_AUTH) # Assert the team is empty, so we have a clean slate. sync_team_info = model.team.get_team_sync_information('buynlarge', 'synced') assert len(list(model.team.list_team_users(sync_team_info.team))) == 0 # Add the existing starting members to the team. for starting_member in starting_membership: (quay_username, fakeauth_username) = starting_member if '+' in quay_username: # Add a robot. (_, shortname) = parse_robot_username(quay_username) robot, _ = model.user.create_robot(shortname, org) model.team.add_user_to_team(robot, sync_team_info.team) else: email = quay_username + '@devtable.com' if fakeauth_username is None: quay_user = model.user.create_user_noverify(quay_username, email) else: quay_user = model.user.create_federated_user(quay_username, email, _FAKE_AUTH, fakeauth_username, False) model.team.add_user_to_team(quay_user, sync_team_info.team) # Call syncing on the team. fake_auth = FakeUsers(group_membership) assert sync_team(fake_auth, sync_team_info) # Ensure the last updated time and transaction_id's have changed. updated_sync_info = model.team.get_team_sync_information('buynlarge', 'synced') assert updated_sync_info.last_updated is not None assert updated_sync_info.transaction_id != sync_team_info.transaction_id users_expected = set([name for name in expected_membership if '+' not in name]) robots_expected = set([name for name in expected_membership if '+' in name]) assert len(users_expected) + len(robots_expected) == len(expected_membership) # Check that the team's users match those expected. service_user_map = model.team.get_federated_team_member_mapping(sync_team_info.team, _FAKE_AUTH) assert set(service_user_map.keys()) == users_expected quay_users = model.team.list_team_users(sync_team_info.team) assert len(quay_users) == len(users_expected) for quay_user in quay_users: fakeauth_record = model.user.lookup_federated_login(quay_user, _FAKE_AUTH) assert fakeauth_record is not None assert fakeauth_record.service_ident in users_expected assert service_user_map[fakeauth_record.service_ident] == quay_user.id # Check that the team's robots match those expected. robots_found = set([r.username for r in model.team.list_team_robots(sync_team_info.team)]) assert robots_expected == robots_found
def change_username(user_id, new_username): (username_valid, username_issue) = validate_username(new_username) if not username_valid: raise InvalidUsernameException("Invalid username %s: %s" % (new_username, username_issue)) with db_transaction(): # Reload the user for update user = db_for_update(User.select().where(User.id == user_id)).get() # Rename the robots for robot in db_for_update( _list_entity_robots(user.username, include_metadata=False, include_token=False)): _, robot_shortname = parse_robot_username(robot.username) new_robot_name = format_robot_username(new_username, robot_shortname) robot.username = new_robot_name robot.save() # Rename the user user.username = new_username user.save() # Remove any prompts for username. remove_user_prompt(user, "confirm_username") return user
def test_filter_repositories(username, include_public, filter_to_namespace, repo_kind, initialized_db): namespace = username if filter_to_namespace else None if '+' in username and filter_to_namespace: namespace, _ = parse_robot_username(username) user = get_namespace_user(username) query = (Repository.select().distinct().join( Namespace, on=(Repository.namespace_user == Namespace.id )).switch(Repository).join(RepositoryPermission, JOIN.LEFT_OUTER)) # Prime the cache. Repository.kind.get_id('image') with assert_query_count(1): found = list( filter_to_repos_for_user(query, user.id, namespace=namespace, include_public=include_public, repo_kind=repo_kind)) expected = list( _get_visible_repositories_for_user(user, repo_kind=repo_kind, namespace=namespace, include_public=include_public)) assert len(found) == len(expected) assert {r.id for r in found} == {r.id for r in expected}
def search_entity_view(username, entity, get_short_name=None): kind = "user" title = "user" avatar_data = avatar.get_data_for_user(entity) href = "/user/" + entity.username if entity.organization: kind = "organization" title = "org" avatar_data = avatar.get_data_for_org(entity) href = "/organization/" + entity.username elif entity.robot: parts = parse_robot_username(entity.username) if parts[0] == username: href = "/user/" + username + "?tab=robots&showRobot=" + entity.username else: href = "/organization/" + parts[0] + "?tab=robots&showRobot=" + entity.username kind = "robot" title = "robot" avatar_data = None data = { "title": title, "kind": kind, "avatar": avatar_data, "name": entity.username, "score": ENTITY_SEARCH_SCORE, "href": href, } if get_short_name: data["short_name"] = get_short_name(entity.username) return data
def verify_robot(robot_username, password): try: password = remove_unicode(password) except UnicodeEncodeError: msg = ('Could not find robot with username: %s and supplied password.' % robot_username) raise InvalidRobotException(msg) result = parse_robot_username(robot_username) if result is None: raise InvalidRobotException('%s is an invalid robot name' % robot_username) robot = lookup_robot(robot_username) assert robot.robot # Lookup the token for the robot. try: token_data = RobotAccountToken.get(robot_account=robot) if not token_data.token.matches(password): msg = ('Could not find robot with username: %s and supplied password.' % robot_username) raise InvalidRobotException(msg) except RobotAccountToken.DoesNotExist: # TODO(remove-unenc): Remove once migrated. if not ActiveDataMigration.has_flag(ERTMigrationFlags.READ_OLD_FIELDS): raise InvalidRobotException(msg) if password.find('robot:') >= 0: # Just to be sure. raise InvalidRobotException(msg) query = (User .select() .join(FederatedLogin) .join(LoginService) .where(FederatedLogin.service_ident == password, LoginService.name == 'quayrobot', User.username == robot_username)) try: robot = query.get() except User.DoesNotExist: msg = ('Could not find robot with username: %s and supplied password.' % robot_username) raise InvalidRobotException(msg) # Find the owner user and ensure it is not disabled. try: owner = User.get(User.username == result[0]) except User.DoesNotExist: raise InvalidRobotException('Robot %s owner does not exist' % robot_username) if not owner.enabled: raise InvalidRobotException('This user has been disabled. Please contact your administrator.') # Mark that the robot was accessed. _basequery.update_last_accessed(robot) return robot
def enable_mirroring_for_repository( repository, root_rule, internal_robot, external_reference, sync_interval, external_registry_username=None, external_registry_password=None, external_registry_config=None, is_enabled=True, sync_start_date=None, ): """ Create a RepoMirrorConfig and set the Repository to the MIRROR state. """ assert internal_robot.robot namespace, _ = parse_robot_username(internal_robot.username) if namespace != repository.namespace_user.username: raise DataModelException("Cannot use robot for mirroring") with db_transaction(): # Create the RepoMirrorConfig try: username = ( DecryptedValue(external_registry_username) if external_registry_username else None ) password = ( DecryptedValue(external_registry_password) if external_registry_password else None ) mirror = RepoMirrorConfig.create( repository=repository, root_rule=root_rule, is_enabled=is_enabled, internal_robot=internal_robot, external_reference=external_reference, external_registry_username=username, external_registry_password=password, external_registry_config=external_registry_config or {}, sync_interval=sync_interval, sync_start_date=sync_start_date or datetime.utcnow(), ) except IntegrityError: return RepoMirrorConfig.get(repository=repository) # Change Repository state to mirroring mode as needed if repository.state != RepositoryState.MIRROR: query = Repository.update(state=RepositoryState.MIRROR).where( Repository.id == repository.id ) if not query.execute(): raise DataModelException("Could not change the state of the repository") return mirror
def wrapper(self, *args, **kwargs): # Team syncing can only be enabled if we have a federated service. if features.TEAM_SYNCING and authentication.federated_service: orgname = kwargs["orgname"] teamname = kwargs["teamname"] if model.team.get_team_sync_information(orgname, teamname): if not except_robots or not parse_robot_username( kwargs.get("membername", "")): raise InvalidRequest( "Cannot call this method on an auth-synced team") return func(self, *args, **kwargs)
def set_mirroring_robot(repository, robot): """ Sets the mirroring robot for the repository. """ assert robot.robot namespace, _ = parse_robot_username(robot.username) if namespace != repository.namespace_user.username: raise DataModelException("Cannot use robot for mirroring") mirror = get_mirror(repository) mirror.internal_robot = robot mirror.save()
def get(self, orgname, membername): """ Retrieves the details of a member of the organization. """ permission = AdministerOrganizationPermission(orgname) if permission.can(): # Lookup the user. member = model.user.get_user(membername) if not member: raise NotFound() organization = model.user.get_user_or_org(orgname) if not organization: raise NotFound() # Lookup the user's information in the organization. teams = list( model.team.get_user_teams_within_org(membername, organization)) if not teams: # 404 if the user is not a robot under the organization, as that means the referenced # user or robot is not a member of this organization. if not member.robot: raise NotFound() namespace, _ = parse_robot_username(member.username) if namespace != orgname: raise NotFound() repo_permissions = model.permission.list_organization_member_permissions( organization, member) def local_team_view(team): return { "name": team.name, "avatar": avatar.get_data_for_team(team), } return { "name": member.username, "kind": "robot" if member.robot else "user", "avatar": avatar.get_data_for_user(member), "teams": [local_team_view(team) for team in teams], "repositories": [ permission.repository.name for permission in repo_permissions ], } raise Unauthorized()
def _setup_robot_for_mirroring(self, namespace_name, repo_name, robot_username): """ Validate robot exists and give write permissions. """ robot = model.user.lookup_robot(robot_username) assert robot.robot namespace, _ = parse_robot_username(robot_username) if namespace != namespace_name: raise model.DataModelException('Invalid robot') # Ensure the robot specified has access to the repository. If not, grant it. permissions = model.permission.get_user_repository_permissions(robot, namespace_name, repo_name) if not permissions or permissions[0].role.name == 'read': model.permission.set_user_repo_permission(robot.username, namespace_name, repo_name, 'write') return robot
def cleanup_old_robots(page_size=50, force=False): """ Deletes any robots that live under namespaces that no longer exist. """ if not force and not app.config.get("SETUP_COMPLETE", False): return # Collect the robot accounts to delete. page_number = 1 to_delete = [] encountered_namespaces = {} while True: found_bots = False for robot in list(User.select().where(User.robot == True).paginate( page_number, page_size)): found_bots = True logger.info("Checking robot %s (page %s)", robot.username, page_number) parsed = parse_robot_username(robot.username) if parsed is None: continue namespace, _ = parsed if namespace in encountered_namespaces: if not encountered_namespaces[namespace]: logger.info("Marking %s to be deleted", robot.username) to_delete.append(robot) else: try: User.get(username=namespace) encountered_namespaces[namespace] = True except User.DoesNotExist: # Save the robot account for deletion. logger.info("Marking %s to be deleted", robot.username) to_delete.append(robot) encountered_namespaces[namespace] = False if not found_bots: break page_number = page_number + 1 # Cleanup any robot accounts whose corresponding namespace doesn't exist. logger.info("Found %s robots to delete", len(to_delete)) for index, robot in enumerate(to_delete): logger.info("Deleting robot %s of %s (%s)", index, len(to_delete), robot.username) robot.delete_instance(recursive=True, delete_nullable=True)
def set_user_repo_permission(username, namespace_name, repository_name, role_name): if username == namespace_name: raise DataModelException('Namespace owner must always be admin.') try: user = User.get(User.username == username) except User.DoesNotExist: raise DataModelException('Invalid username: %s' % username) if user.robot: parts = parse_robot_username(user.username) if not parts: raise DataModelException('Invalid robot: %s' % username) robot_namespace, _ = parts if robot_namespace != namespace_name: raise DataModelException('Cannot add robot %s under namespace %s' % (username, namespace_name)) return __set_entity_repo_permission(user, 'user', namespace_name, repository_name, role_name)
def verify_robot(robot_username, password): try: password.encode("ascii") except UnicodeEncodeError: msg = "Could not find robot with username: %s and supplied password." % robot_username raise InvalidRobotException(msg) result = parse_robot_username(robot_username) if result is None: raise InvalidRobotException("%s is an invalid robot name" % robot_username) robot = lookup_robot(robot_username) assert robot.robot # Lookup the token for the robot. try: token_data = RobotAccountToken.get(robot_account=robot) if not token_data.token.matches(password): msg = "Could not find robot with username: %s and supplied password." % robot_username raise InvalidRobotException(msg) except RobotAccountToken.DoesNotExist: msg = "Could not find robot with username: %s and supplied password." % robot_username raise InvalidRobotException(msg) # Find the owner user and ensure it is not disabled. try: owner = User.get(User.username == result[0]) except User.DoesNotExist: raise InvalidRobotException("Robot %s owner does not exist" % robot_username) if not owner.enabled: raise InvalidRobotException( "This user has been disabled. Please contact your administrator.") # Mark that the robot was accessed. _basequery.update_last_accessed(robot) return robot
def test_retrieve_robots(endpoint, params, bot_endpoint, include_token, limit, app, client): params['token'] = 'true' if include_token else 'false' if limit is not None: params['limit'] = limit with client_with_identity('devtable', client) as cl: result = conduct_api_call(cl, endpoint, 'GET', params, None) if limit is not None: assert len(result.json['robots']) <= limit for robot in result.json['robots']: assert (robot.get('token') is not None) == include_token if include_token: bot_params = dict(params) bot_params['robot_shortname'] = parse_robot_username( robot['name'])[1] result = conduct_api_call(cl, bot_endpoint, 'GET', bot_params, None) assert robot.get('token') == result.json['token']
def test_retrieve_robots(endpoint, params, bot_endpoint, include_token, limit, app, client): params["token"] = "true" if include_token else "false" if limit is not None: params["limit"] = limit with client_with_identity("devtable", client) as cl: result = conduct_api_call(cl, endpoint, "GET", params, None) if limit is not None: assert len(result.json["robots"]) <= limit for robot in result.json["robots"]: assert (robot.get("token") is not None) == include_token if include_token: bot_params = dict(params) bot_params["robot_shortname"] = parse_robot_username( robot["name"])[1] result = conduct_api_call(cl, bot_endpoint, "GET", bot_params, None) assert robot.get("token") == result.json["token"]
def search_entity_view(username, entity, get_short_name=None): kind = 'user' title = 'user' avatar_data = avatar.get_data_for_user(entity) href = '/user/' + entity.username if entity.organization: kind = 'organization' title = 'org' avatar_data = avatar.get_data_for_org(entity) href = '/organization/' + entity.username elif entity.robot: parts = parse_robot_username(entity.username) if parts[0] == username: href = '/user/' + username + '?tab=robots&showRobot=' + entity.username else: href = '/organization/' + parts[ 0] + '?tab=robots&showRobot=' + entity.username kind = 'robot' title = 'robot' avatar_data = None data = { 'title': title, 'kind': kind, 'avatar': avatar_data, 'name': entity.username, 'score': ENTITY_SEARCH_SCORE, 'href': href } if get_short_name: data['short_name'] = get_short_name(entity.username) return data
def get_short_name(name): return parse_robot_username(name)[1]
def validate_credentials(auth_username, auth_password_or_token): """ Validates a pair of auth username and password/token credentials. """ # Check for access tokens. if auth_username == ACCESS_TOKEN_USERNAME: logger.debug('Found credentials for access token') try: token = model.token.load_token_data(auth_password_or_token) logger.debug( 'Successfully validated credentials for access token %s', token.id) return ValidateResult(AuthKind.credentials, token=token), CredentialKind.token except model.DataModelException: logger.warning( 'Failed to validate credentials for access token %s', auth_password_or_token) return (ValidateResult(AuthKind.credentials, error_message='Invalid access token'), CredentialKind.token) # Check for App Specific tokens. if features.APP_SPECIFIC_TOKENS and auth_username == APP_SPECIFIC_TOKEN_USERNAME: logger.debug('Found credentials for app specific auth token') token = model.appspecifictoken.access_valid_token( auth_password_or_token) if token is None: logger.debug( 'Failed to validate credentials for app specific token: %s', auth_password_or_token) return (ValidateResult(AuthKind.credentials, error_message='Invalid token'), CredentialKind.app_specific_token) if not token.user.enabled: logger.debug( 'Tried to use an app specific token for a disabled user: %s', token.uuid) return (ValidateResult( AuthKind.credentials, error_message= 'This user has been disabled. Please contact your administrator.' ), CredentialKind.app_specific_token) logger.debug( 'Successfully validated credentials for app specific token %s', token.id) return (ValidateResult(AuthKind.credentials, appspecifictoken=token), CredentialKind.app_specific_token) # Check for OAuth tokens. if auth_username == OAUTH_TOKEN_USERNAME: return validate_oauth_token( auth_password_or_token), CredentialKind.oauth_token # Check for robots and users. is_robot = parse_robot_username(auth_username) if is_robot: logger.debug('Found credentials header for robot %s', auth_username) try: robot = model.user.verify_robot(auth_username, auth_password_or_token) logger.debug('Successfully validated credentials for robot %s', auth_username) return ValidateResult(AuthKind.credentials, robot=robot), CredentialKind.robot except model.InvalidRobotException as ire: logger.warning('Failed to validate credentials for robot %s: %s', auth_username, ire) return ValidateResult(AuthKind.credentials, error_message=str(ire)), CredentialKind.robot # Otherwise, treat as a standard user. (authenticated, err) = authentication.verify_and_link_user(auth_username, auth_password_or_token, basic_auth=True) if authenticated: logger.debug('Successfully validated credentials for user %s', authenticated.username) return ValidateResult(AuthKind.credentials, user=authenticated), CredentialKind.user else: logger.warning('Failed to validate credentials for user %s: %s', auth_username, err) return ValidateResult(AuthKind.credentials, error_message=err), CredentialKind.user
def post(self, namespace_name, repo_name, trigger_uuid): """ Activate the specified build trigger. """ trigger = get_trigger(trigger_uuid) handler = BuildTriggerHandler.get_handler(trigger) if handler.is_active(): raise InvalidRequest("Trigger config is not sufficient for activation.") user_permission = UserAdminPermission(trigger.connected_user.username) if user_permission.can(): # Update the pull robot (if any). pull_robot_name = request.get_json().get("pull_robot", None) if pull_robot_name: try: pull_robot = model.user.lookup_robot(pull_robot_name) except model.InvalidRobotException: raise NotFound() # Make sure the user has administer permissions for the robot's namespace. (robot_namespace, _) = parse_robot_username(pull_robot_name) if not AdministerOrganizationPermission(robot_namespace).can(): raise Unauthorized() # Make sure the namespace matches that of the trigger. if robot_namespace != namespace_name: raise Unauthorized() # Set the pull robot. trigger.pull_robot = pull_robot # Update the config. new_config_dict = request.get_json()["config"] write_token_name = "Build Trigger: %s" % trigger.service.name write_token = model.token.create_delegate_token( namespace_name, repo_name, write_token_name, "write" ) try: path = url_for("webhooks.build_trigger_webhook", trigger_uuid=trigger.uuid) authed_url = _prepare_webhook_url( app.config["PREFERRED_URL_SCHEME"], "$token", write_token.get_code(), app.config["SERVER_HOSTNAME"], path, ) handler = BuildTriggerHandler.get_handler(trigger, new_config_dict) final_config, private_config = handler.activate(authed_url) if "private_key" in private_config: trigger.secure_private_key = DecryptedValue(private_config["private_key"]) except TriggerException as exc: write_token.delete_instance() raise request_error(message=exc.message) # Save the updated config. update_build_trigger(trigger, final_config, write_token=write_token) # Log the trigger setup. repo = model.repository.get_repository(namespace_name, repo_name) log_action( "setup_repo_trigger", namespace_name, { "repo": repo_name, "namespace": namespace_name, "trigger_id": trigger.uuid, "service": trigger.service.name, "pull_robot": trigger.pull_robot.username if trigger.pull_robot else None, "config": final_config, }, repo=repo, ) return trigger_view(trigger, can_admin=True) else: raise Unauthorized()
def post(self, namespace, repository): """ Request that a repository be built and pushed from the specified input. """ logger.debug("User requested repository initialization.") request_json = request.get_json() dockerfile_id = request_json.get("file_id", None) archive_url = request_json.get("archive_url", None) if not dockerfile_id and not archive_url: raise InvalidRequest("file_id or archive_url required") if archive_url: archive_match = None try: archive_match = urlparse(archive_url) except ValueError: pass if not archive_match: raise InvalidRequest( "Invalid Archive URL: Must be a valid URI") scheme = archive_match.scheme if scheme != "http" and scheme != "https": raise InvalidRequest( "Invalid Archive URL: Must be http or https") context, subdir = self.get_dockerfile_context(request_json) tags = request_json.get("docker_tags", ["latest"]) pull_robot_name = request_json.get("pull_robot", None) # Verify the security behind the pull robot. if pull_robot_name: result = parse_robot_username(pull_robot_name) if result: try: model.user.lookup_robot(pull_robot_name) except model.InvalidRobotException: raise NotFound() # Make sure the user has administer permissions for the robot's namespace. (robot_namespace, _) = result if not AdministerOrganizationPermission(robot_namespace).can(): raise Unauthorized() else: raise Unauthorized() # Check if the dockerfile resource has already been used. If so, then it # can only be reused if the user has access to the repository in which the # dockerfile was previously built. if dockerfile_id: associated_repository = model.build.get_repository_for_resource( dockerfile_id) if associated_repository: if not ModifyRepositoryPermission( associated_repository.namespace_user.username, associated_repository.name): raise Unauthorized() # Start the build. repo = model.repository.get_repository(namespace, repository) if repo is None: raise NotFound() try: build_name = (user_files.get_file_checksum(dockerfile_id) if dockerfile_id else hashlib.sha224( archive_url.encode("ascii")).hexdigest()[0:7]) except IOError: raise InvalidRequest("File %s could not be found or is invalid" % dockerfile_id) prepared = PreparedBuild() prepared.build_name = build_name prepared.dockerfile_id = dockerfile_id prepared.archive_url = archive_url prepared.tags = tags prepared.subdirectory = subdir prepared.context = context prepared.is_manual = True prepared.metadata = {} try: build_request = start_build(repo, prepared, pull_robot_name=pull_robot_name) except MaximumBuildsQueuedException: abort(429, message="Maximum queued build rate exceeded.") except BuildTriggerDisabledException: abort(400, message="Build trigger is disabled") resp = build_status_view(build_request) repo_string = "%s/%s" % (namespace, repository) headers = { "Location": api.url_for(RepositoryBuildStatus, repository=repo_string, build_uuid=build_request.uuid), } return resp, 201, headers
def post(self, namespace_name, repo_name, trigger_uuid): """ Activate the specified build trigger. """ trigger = get_trigger(trigger_uuid) handler = BuildTriggerHandler.get_handler(trigger) if handler.is_active(): raise InvalidRequest('Trigger config is not sufficient for activation.') user_permission = UserAdminPermission(trigger.connected_user.username) if user_permission.can(): # Update the pull robot (if any). pull_robot_name = request.get_json().get('pull_robot', None) if pull_robot_name: try: pull_robot = model.user.lookup_robot(pull_robot_name) except model.InvalidRobotException: raise NotFound() # Make sure the user has administer permissions for the robot's namespace. (robot_namespace, _) = parse_robot_username(pull_robot_name) if not AdministerOrganizationPermission(robot_namespace).can(): raise Unauthorized() # Make sure the namespace matches that of the trigger. if robot_namespace != namespace_name: raise Unauthorized() # Set the pull robot. trigger.pull_robot = pull_robot # Update the config. new_config_dict = request.get_json()['config'] write_token_name = 'Build Trigger: %s' % trigger.service.name write_token = model.token.create_delegate_token(namespace_name, repo_name, write_token_name, 'write') try: path = url_for('webhooks.build_trigger_webhook', trigger_uuid=trigger.uuid) authed_url = _prepare_webhook_url(app.config['PREFERRED_URL_SCHEME'], '$token', write_token.get_code(), app.config['SERVER_HOSTNAME'], path) handler = BuildTriggerHandler.get_handler(trigger, new_config_dict) final_config, private_config = handler.activate(authed_url) if 'private_key' in private_config: trigger.secure_private_key = DecryptedValue(private_config['private_key']) # TODO(remove-unenc): Remove legacy field. if ActiveDataMigration.has_flag(ERTMigrationFlags.WRITE_OLD_FIELDS): trigger.private_key = private_config['private_key'] except TriggerException as exc: write_token.delete_instance() raise request_error(message=exc.message) # Save the updated config. update_build_trigger(trigger, final_config, write_token=write_token) # Log the trigger setup. repo = model.repository.get_repository(namespace_name, repo_name) log_action('setup_repo_trigger', namespace_name, {'repo': repo_name, 'namespace': namespace_name, 'trigger_id': trigger.uuid, 'service': trigger.service.name, 'pull_robot': trigger.pull_robot.username if trigger.pull_robot else None, 'config': final_config}, repo=repo) return trigger_view(trigger, can_admin=True) else: raise Unauthorized()