diff --git a/data/model/user.py b/data/model/user.py index b2c475b78..758491ef1 100644 --- a/data/model/user.py +++ b/data/model/user.py @@ -217,6 +217,11 @@ def update_email(user, new_email, auto_verify=False): raise DataModelException('E-mail address already used') +def update_enabled(user, set_enabled): + user.enabled = set_enabled + user.save() + + def create_robot(robot_shortname, parent): (username_valid, username_issue) = validate_username(robot_shortname) if not username_valid: diff --git a/endpoints/api/superuser.py b/endpoints/api/superuser.py index 515a66b7d..f173d8587 100644 --- a/endpoints/api/superuser.py +++ b/endpoints/api/superuser.py @@ -29,7 +29,9 @@ from endpoints.api import (ApiResource, nickname, resource, validate_json_reques from endpoints.api.build import build_status_view, get_logs_or_log_url from data import model, database from data.database import ServiceKeyApprovalType -from endpoints.exception import NotFound +from endpoints.api.superuser_models_pre_oci import pre_oci_model, ServiceKeyDoesNotExist, ServiceKeyAlreadyApproved, \ + InvalidRepositoryBuildException +from endpoints.exception import NotFound, InvalidResponse from util.useremails import send_confirmation_email, send_recovery_email from util.license import decode_license, LicenseDecodeError from util.security.ssl import load_certificate, CertInvalidException @@ -39,11 +41,7 @@ from _init import ROOT_DIR logger = logging.getLogger(__name__) -def _validate_logs_arguments(start_time, end_time, performer_name): - performer = None - if performer_name: - performer = model.user.get_user(performer_name) - +def _validate_logs_arguments(start_time, end_time): if start_time: try: start_time = datetime.strptime(start_time + ' UTC', '%m/%d/%Y %Z') @@ -63,7 +61,7 @@ def _validate_logs_arguments(start_time, end_time, performer_name): if not end_time: end_time = datetime.today() - return start_time, end_time, performer + return start_time, end_time def get_immediate_subdirectories(directory): @@ -89,7 +87,7 @@ class SuperUserGetLogsForService(ApiResource): def get(self, service): """ Returns the logs for the specific service. """ if SuperUserPermission().can(): - if not service in get_services(): + if service not in get_services(): abort(404) logs = [] @@ -130,37 +128,6 @@ class SuperUserSystemLogServices(ApiResource): abort(403) -def aggregated_log_view(log, kinds, start_time): - # Because we aggregate based on the day of the month in SQL, we only have that information. - # Therefore, create a synthetic date based on the day and the month of the start time. - # Logs are allowed for a maximum period of one week, so this calculation should always work. - synthetic_date = datetime(start_time.year, start_time.month, int(log.day), tzinfo=get_localzone()) - if synthetic_date.day < start_time.day: - synthetic_date = synthetic_date + relativedelta(months=1) - - view = { - 'kind': kinds[log.kind_id], - 'count': log.count, - 'datetime': format_date(synthetic_date), - } - - return view - - -def get_aggregate_logs(start_time, end_time, performer_name=None, repository=None, namespace=None, - ignore=None): - (start_time, end_time, performer) = _validate_logs_arguments(start_time, end_time, performer_name) - - kinds = model.log.get_log_entry_kinds() - aggregated_logs = model.log.get_aggregated_logs(start_time, end_time, performer=performer, - repository=repository, namespace=namespace, - ignore=ignore) - - return { - 'aggregated': [aggregated_log_view(log, kinds, start_time) for log in aggregated_logs] - } - - @resource('/v1/superuser/aggregatelogs') @internal_only class SuperUserAggregateLogs(ApiResource): @@ -175,10 +142,12 @@ class SuperUserAggregateLogs(ApiResource): def get(self, parsed_args): """ Returns the aggregated logs for the current system. """ if SuperUserPermission().can(): - start_time = parsed_args['starttime'] - end_time = parsed_args['endtime'] + (start_time, end_time) = _validate_logs_arguments(parsed_args['starttime'], parsed_args['endtime']) + aggregated_logs = pre_oci_model.get_aggregated_logs(start_time, end_time) - return get_aggregate_logs(start_time, end_time) + return { + 'aggregated': [log.to_dict() for log in aggregated_logs] + } abort(403) @@ -186,59 +155,6 @@ class SuperUserAggregateLogs(ApiResource): LOGS_PER_PAGE = 20 -def log_view(log, kinds, include_namespace): - view = { - 'kind': kinds[log.kind_id], - 'metadata': json.loads(log.metadata_json), - 'ip': log.ip, - 'datetime': format_date(log.datetime), - } - - if log.performer and log.performer.username: - view['performer'] = { - 'kind': 'user', - 'name': log.performer.username, - 'is_robot': log.performer.robot, - 'avatar': avatar.get_data_for_user(log.performer), - } - - if include_namespace: - if log.account and log.account.username: - if log.account.organization: - view['namespace'] = { - 'kind': 'org', - 'name': log.account.username, - 'avatar': avatar.get_data_for_org(log.account), - } - else: - view['namespace'] = { - 'kind': 'user', - 'name': log.account.username, - 'avatar': avatar.get_data_for_user(log.account), - } - - return view - - -def get_logs(start_time, end_time, performer_name=None, repository=None, namespace=None, - page_token=None, ignore=None): - (start_time, end_time, performer) = _validate_logs_arguments(start_time, end_time, performer_name) - kinds = model.log.get_log_entry_kinds() - logs_query = model.log.get_logs_query(start_time, end_time, performer=performer, - repository=repository, namespace=namespace, - ignore=ignore) - - logs, next_page_token = model.modelutil.paginate(logs_query, database.LogEntry, descending=True, - page_token=page_token, limit=LOGS_PER_PAGE) - - include_namespace = namespace is None and repository is None - return { - 'start_time': format_date(start_time), - 'end_time': format_date(end_time), - 'logs': [log_view(log, kinds, include_namespace) for log in logs], - }, next_page_token - - @resource('/v1/superuser/logs') @internal_only @show_if(features.SUPER_USERS) @@ -259,8 +175,14 @@ class SuperUserLogs(ApiResource): if SuperUserPermission().can(): start_time = parsed_args['starttime'] end_time = parsed_args['endtime'] + (start_time, end_time) = _validate_logs_arguments(start_time, end_time) + log_page = pre_oci_model.get_logs_query(start_time, end_time, page_token=page_token) - return get_logs(start_time, end_time, page_token=page_token) + return { + 'start_time': format_date(start_time), + 'end_time': format_date(end_time), + 'logs': [log.to_dict() for log in log_page.logs], + }, log_page.next_page_token abort(403) @@ -325,9 +247,8 @@ class SuperUserOrganizationList(ApiResource): def get(self): """ Returns a list of all organizations in the system. """ if SuperUserPermission().can(): - orgs = model.organization.get_organizations() return { - 'organizations': [org_view(org) for org in orgs] + 'organizations': [org.to_dict() for org in pre_oci_model.get_organizations()] } abort(403) @@ -364,9 +285,9 @@ class SuperUserList(ApiResource): def get(self): """ Returns a list of all users in the system. """ if SuperUserPermission().can(): - users = model.user.get_active_users() + users = pre_oci_model.get_active_users() return { - 'users': [user_view(user) for user in users] + 'users': [user.to_dict() for user in users] } abort(403) @@ -391,14 +312,9 @@ class SuperUserList(ApiResource): # Create the user. username = user_information['username'] email = user_information.get('email') - prompts = model.user.get_default_user_prompts(features) - user = model.user.create_user(username, password, email, auto_verify=not features.MAILING, - email_required=features.MAILING, prompts=prompts) - - # If mailing is turned on, send the user a verification email. + install_user, confirmation_code = pre_oci_model.create_install_user(username, password, email) if features.MAILING: - confirmation = model.user.create_confirm_email_code(user) - send_confirmation_email(user.username, user.email, confirmation.code) + send_confirmation_email(install_user.username, install_user.email, confirmation_code) return { 'username': username, @@ -427,15 +343,15 @@ class SuperUserSendRecoveryEmail(ApiResource): abort(400) if SuperUserPermission().can(): - user = model.user.get_nonrobot_user(username) - if not user: + user = pre_oci_model.get_nonrobot_user(username) + if user is None: abort(404) if superusers.is_superuser(username): abort(403) - code = model.user.create_reset_password_email_code(user.email) - send_recovery_email(user.email, code.code) + code = pre_oci_model.create_reset_password_email_code(user.email) + send_recovery_email(user.email, code) return { 'email': user.email } @@ -478,11 +394,11 @@ class SuperUserManagement(ApiResource): def get(self, username): """ Returns information about the specified user. """ if SuperUserPermission().can(): - user = model.user.get_nonrobot_user(username) - if not user: + user = pre_oci_model.get_nonrobot_user(username) + if user is None: abort(404) - return user_view(user) + return user.to_dict() abort(403) @@ -493,14 +409,14 @@ class SuperUserManagement(ApiResource): def delete(self, username): """ Deletes the specified user. """ if SuperUserPermission().can(): - user = model.user.get_nonrobot_user(username) - if not user: + user = pre_oci_model.get_nonrobot_user(username) + if user is None: abort(404) if superusers.is_superuser(username): abort(403) - model.user.delete_user(user, all_queues, force=True) + pre_oci_model.delete_user(username) return '', 204 abort(403) @@ -513,8 +429,8 @@ class SuperUserManagement(ApiResource): def put(self, username): """ Updates information about the specified user. """ if SuperUserPermission().can(): - user = model.user.get_nonrobot_user(username) - if not user: + user = pre_oci_model.get_nonrobot_user(username) + if user is None: abort(404) if superusers.is_superuser(username): @@ -526,19 +442,18 @@ class SuperUserManagement(ApiResource): if app.config['AUTHENTICATION_TYPE'] != 'Database': abort(400) - model.user.change_password(user, user_data['password']) + pre_oci_model.change_password(username, user_data['password']) if 'email' in user_data: # Ensure that we are using database auth. if app.config['AUTHENTICATION_TYPE'] != 'Database': abort(400) - model.user.update_email(user, user_data['email'], auto_verify=True) + pre_oci_model.update_email(username, user_data['email'], auto_verify=True) if 'enabled' in user_data: # Disable/enable the user. - user.enabled = bool(user_data['enabled']) - user.save() + pre_oci_model.update_enabled(username, bool(user_data['enabled'])) if 'superuser' in user_data: config_object = config_provider.get_config() @@ -552,7 +467,11 @@ class SuperUserManagement(ApiResource): config_object['SUPER_USERS'] = list(superusers_set) config_provider.save_config(config_object) - return user_view(user, password=user_data.get('password')) + return_value = user.to_dict() + if user_data.get('password') is not None: + return_value['encrypted_password'] = authentication.encrypt_user_password(user_data.get('password')) + + return return_value abort(403) @@ -575,23 +494,14 @@ class SuperUserTakeOwnership(ApiResource): if superusers.is_superuser(namespace): abort(400) - entity = model.user.get_user_or_org(namespace) - if entity is None: - abort(404) - authed_user = get_authenticated_user() - was_user = not entity.organization - if entity.organization: - # Add the superuser as an admin to the owners team of the org. - model.organization.add_user_as_admin(authed_user, entity) - else: - # If the entity is a user, convert it to an organization and add the current superuser - # as the admin. - model.organization.convert_user_to_organization(entity, get_authenticated_user()) + entity_id, was_user = pre_oci_model.take_ownership(namespace, authed_user) + if entity_id is None: + abort(404) # Log the change. log_metadata = { - 'entity_id': entity.id, + 'entity_id': entity_id, 'namespace': namespace, 'was_user': was_user, 'superuser': authed_user.username, @@ -633,9 +543,7 @@ class SuperUserOrganizationManagement(ApiResource): def delete(self, name): """ Deletes the specified organization. """ if SuperUserPermission().can(): - org = model.organization.get_organization(name) - - model.user.delete_user(org, all_queues) + pre_oci_model.delete_organization(name) return '', 204 abort(403) @@ -648,13 +556,10 @@ class SuperUserOrganizationManagement(ApiResource): def put(self, name): """ Updates information about the specified user. """ if SuperUserPermission().can(): - org = model.organization.get_organization(name) org_data = request.get_json() - - if 'name' in org_data: - org = model.user.change_username(org.id, org_data['name']) - - return org_view(org) + old_name = org_data['name'] if 'name' in org_data else None + org = pre_oci_model.change_organization_name(name, old_name) + return org.to_dict() abort(403) @@ -722,10 +627,10 @@ class SuperUserServiceKeyManagement(ApiResource): @require_scope(scopes.SUPERUSER) def get(self): if SuperUserPermission().can(): - keys = model.service_keys.list_all_keys() + keys = pre_oci_model.list_all_service_keys() return jsonify({ - 'keys': [key_view(key) for key in keys], + 'keys': [key.to_dict() for key in keys], }) abort(403) @@ -760,16 +665,16 @@ class SuperUserServiceKeyManagement(ApiResource): }) # Generate a key with a private key that we *never save*. - (private_key, key) = model.service_keys.generate_service_key(body['service'], expiration_date, - metadata=metadata, - name=body.get('name', '')) + (private_key, key_id) = pre_oci_model.generate_service_key(body['service'], expiration_date, + metadata=metadata, + name=body.get('name', '')) # Auto-approve the service key. - model.service_keys.approve_service_key(key.kid, user, ServiceKeyApprovalType.SUPERUSER, - notes=body.get('notes', '')) + pre_oci_model.approve_service_key(key_id, user, ServiceKeyApprovalType.SUPERUSER, + notes=body.get('notes', '')) # Log the creation and auto-approval of the service key. key_log_metadata = { - 'kid': key.kid, + 'kid': key_id, 'preshared': True, 'service': body['service'], 'name': body.get('name', ''), @@ -781,7 +686,7 @@ class SuperUserServiceKeyManagement(ApiResource): log_action('service_key_approve', None, key_log_metadata) return jsonify({ - 'kid': key.kid, + 'kid': key_id, 'name': body.get('name', ''), 'service': body['service'], 'public_key': private_key.publickey().exportKey('PEM'), @@ -824,9 +729,9 @@ class SuperUserServiceKey(ApiResource): def get(self, kid): if SuperUserPermission().can(): try: - key = model.service_keys.get_service_key(kid, approved_only=False, alive_only=False) - return jsonify(key_view(key)) - except model.service_keys.ServiceKeyDoesNotExist: + key = pre_oci_model.get_service_key(kid, approved_only=False, alive_only=False) + return jsonify(key.to_dict()) + except ServiceKeyDoesNotExist: abort(404) abort(403) @@ -840,8 +745,8 @@ class SuperUserServiceKey(ApiResource): if SuperUserPermission().can(): body = request.get_json() try: - key = model.service_keys.get_service_key(kid, approved_only=False, alive_only=False) - except model.service_keys.ServiceKeyDoesNotExist: + key = pre_oci_model.get_service_key(kid, approved_only=False, alive_only=False) + except ServiceKeyDoesNotExist: abort(404) key_log_metadata = { @@ -868,14 +773,14 @@ class SuperUserServiceKey(ApiResource): }) log_action('service_key_extend', None, key_log_metadata) - model.service_keys.set_key_expiration(kid, expiration_date) + pre_oci_model.set_key_expiration(kid, expiration_date) if 'name' in body or 'metadata' in body: - model.service_keys.update_service_key(kid, body.get('name'), body.get('metadata')) + pre_oci_model.update_service_key(kid, body.get('name'), body.get('metadata')) log_action('service_key_modify', None, key_log_metadata) - updated_key = model.service_keys.get_service_key(kid, approved_only=False, alive_only=False) - return jsonify(key_view(updated_key)) + updated_key = pre_oci_model.get_service_key(kid, approved_only=False, alive_only=False) + return jsonify(updated_key.to_dict()) abort(403) @@ -886,8 +791,8 @@ class SuperUserServiceKey(ApiResource): def delete(self, kid): if SuperUserPermission().can(): try: - key = model.service_keys.delete_service_key(kid) - except model.service_keys.ServiceKeyDoesNotExist: + key = pre_oci_model.delete_service_key(kid) + except ServiceKeyDoesNotExist: abort(404) key_log_metadata = { @@ -934,8 +839,8 @@ class SuperUserServiceKeyApproval(ApiResource): notes = request.get_json().get('notes', '') approver = get_authenticated_user() try: - key = model.service_keys.approve_service_key(kid, approver, ServiceKeyApprovalType.SUPERUSER, - notes=notes) + key = pre_oci_model.approve_service_key(kid, approver, ServiceKeyApprovalType.SUPERUSER, + notes=notes) # Log the approval of the service key. key_log_metadata = { @@ -946,9 +851,9 @@ class SuperUserServiceKeyApproval(ApiResource): } log_action('service_key_approve', None, key_log_metadata) - except model.ServiceKeyDoesNotExist: + except ServiceKeyDoesNotExist: abort(404) - except model.ServiceKeyAlreadyApproved: + except ServiceKeyAlreadyApproved: pass return make_response('', 201) @@ -1153,8 +1058,11 @@ class SuperUserRepositoryBuildLogs(ApiResource): """ Return the build logs for the build specified by the build uuid. """ if not SuperUserPermission().can(): abort(403) - - return get_logs_or_log_url(model.build.get_repository_build(build_uuid)) + try: + repo_build = pre_oci_model.get_repository_build(build_uuid) + return get_logs_or_log_url(repo_build) + except InvalidRepositoryBuildException as e: + raise InvalidResponse(e.message) @resource('/v1/superuser//status') @@ -1172,8 +1080,11 @@ class SuperUserRepositoryBuildStatus(ApiResource): """ Return the status for the builds specified by the build uuids. """ if not SuperUserPermission().can(): abort(403) - build = model.build.get_repository_build(build_uuid) - return build_status_view(build) + try: + build = pre_oci_model.get_repository_build(build_uuid) + except InvalidRepositoryBuildException as e: + raise InvalidResponse(e.message) + return build.to_dict() @resource('/v1/superuser//build') @@ -1193,8 +1104,8 @@ class SuperUserRepositoryBuildResource(ApiResource): abort(403) try: - build = model.build.get_repository_build(build_uuid) - except model.build.InvalidRepositoryBuildException: + build = pre_oci_model.get_repository_build(build_uuid) + except InvalidRepositoryBuildException: raise NotFound() - return build_status_view(build) + return build.to_dict() diff --git a/endpoints/api/superuser_models_interface.py b/endpoints/api/superuser_models_interface.py new file mode 100644 index 000000000..ec83c7c48 --- /dev/null +++ b/endpoints/api/superuser_models_interface.py @@ -0,0 +1,443 @@ +import json +from abc import ABCMeta, abstractmethod +from collections import namedtuple +from datetime import datetime + +from dateutil.relativedelta import relativedelta +from six import add_metaclass +from tzlocal import get_localzone + +from app import avatar, superusers +from buildtrigger.basehandler import BuildTriggerHandler +from data import model +from endpoints.api import format_date +from util.morecollections import AttrDict + + +def user_view(user): + return { + 'name': user.username, + 'kind': 'user', + 'is_robot': user.robot, + } + + +class BuildTrigger( + namedtuple('BuildTrigger', ['uuid', 'service_name', 'pull_robot', 'can_read', 'can_admin', 'for_build'])): + """ + BuildTrigger represent a trigger that is associated with a build + :type uuid: string + :type service_name: string + :type pull_robot: User + :type can_read: boolean + :type can_admin: boolean + :type for_build: boolean + """ + + def to_dict(self): + if not self.uuid: + return None + + build_trigger = BuildTriggerHandler.get_handler(self) + build_source = build_trigger.config.get('build_source') + + repo_url = build_trigger.get_repository_url() if build_source else None + can_read = self.can_read or self.can_admin + + trigger_data = { + 'id': self.uuid, + 'service': self.service_name, + 'is_active': build_trigger.is_active(), + + 'build_source': build_source if can_read else None, + 'repository_url': repo_url if can_read else None, + + 'config': build_trigger.config if self.can_admin else {}, + 'can_invoke': self.can_admin, + } + + if not self.for_build and self.can_admin and self.pull_robot: + trigger_data['pull_robot'] = user_view(self.pull_robot) + + return trigger_data + + +class RepositoryBuild(namedtuple('RepositoryBuild', + ['uuid', 'logs_archived', 'repository_namespace_user_username', 'repository_name', + 'can_write', 'can_read', 'pull_robot', 'resource_key', 'trigger', 'display_name', + 'started', 'job_config', 'phase', 'status', 'error', 'archive_url'])): + """ + RepositoryBuild represents a build associated with a repostiory + :type uuid: string + :type logs_archived: boolean + :type repository_namespace_user_username: string + :type repository_name: string + :type can_write: boolean + :type can_write: boolean + :type pull_robot: User + :type resource_key: string + :type trigger: Trigger + :type display_name: string + :type started: boolean + :type job_config: {Any -> Any} + :type phase: string + :type status: string + :type error: string + :type archive_url: string + """ + + def to_dict(self): + + resp = { + 'id': self.uuid, + 'phase': self.phase, + 'started': format_date(self.started), + 'display_name': self.display_name, + 'status': self.status or {}, + 'subdirectory': self.job_config.get('build_subdir', ''), + 'dockerfile_path': self.job_config.get('build_subdir', ''), + 'context': self.job_config.get('context', ''), + 'tags': self.job_config.get('docker_tags', []), + 'manual_user': self.job_config.get('manual_user', None), + 'is_writer': self.can_write, + 'trigger': self.trigger.to_dict(), + 'trigger_metadata': self.job_config.get('trigger_metadata', None) if self.can_read else None, + 'resource_key': self.resource_key, + 'pull_robot': user_view(self.pull_robot) if self.pull_robot else None, + 'repository': { + 'namespace': self.repository_namespace_user_username, + 'name': self.repository_name + }, + 'error': self.error, + } + + if self.can_write: + if self.resource_key is not None: + resp['archive_url'] = self.archive_url + elif self.job_config.get('archive_url', None): + resp['archive_url'] = self.job_config['archive_url'] + + return resp + + +class Approval(namedtuple('Approval', ['approver', 'approval_type', 'approved_date', 'notes'])): + """ + Approval represents whether a key has been approved or not + :type approver: User + :type approval_type: string + :type approved_date: Date + :type notes: string + """ + + def to_dict(self): + return { + 'approver': self.approver.to_dict() if self.approver else None, + 'approval_type': self.approval_type, + 'approved_date': self.approved_date, + 'notes': self.notes, + } + + +class ServiceKey(namedtuple('ServiceKey', ['name', 'kid', 'service', 'jwk', 'metadata', 'created_date', + 'expiration_date', 'rotation_duration', 'approval'])): + """ + ServiceKey is an apostille signing key + :type name: string + :type kid: int + :type service: string + :type jwk: string + :type metadata: string + :type created_date: Date + :type expiration_date: Date + :type rotation_duration: Date + :type approval: Approval + + """ + + def to_dict(self): + return { + 'name': self.name, + 'kid': self.kid, + 'service': self.service, + 'jwk': self.jwk, + 'metadata': self.metadata, + 'created_date': self.created_date, + 'expiration_date': self.expiration_date, + 'rotation_duration': self.rotation_duration, + 'approval': self.approval.to_dict() if self.approval is not None else None, + } + + +class User(namedtuple('User', ['username', 'email', 'verified', 'enabled', 'robot'])): + """ + User represents a single user. + :type username: string + :type email: string + :type verified: boolean + :type enabled: boolean + :type robot: User + """ + + def to_dict(self): + user_data = { + 'kind': 'user', + 'name': self.username, + 'username': self.username, + 'email': self.email, + 'verified': self.verified, + 'avatar': avatar.get_data_for_user(self), + 'super_user': superusers.is_superuser(self.username), + 'enabled': self.enabled, + } + + return user_data + + +class Organization(namedtuple('Organization', ['username', 'email'])): + """ + Organization represents a single org. + :type username: string + :type email: string + """ + + def to_dict(self): + return { + 'name': self.username, + 'email': self.email, + 'avatar': avatar.get_data_for_org(self), + } + + +class LogEntry( + namedtuple('LogEntry', [ + 'metadata_json', 'ip', 'datetime', 'performer_email', 'performer_username', 'performer_robot', + 'account_organization', 'account_username', 'account_email', 'account_robot', 'kind', + ])): + """ + LogEntry a single log entry. + :type metadata_json: string + :type ip: string + :type datetime: string + :type performer_email: int + :type performer_username: string + :type performer_robot: boolean + :type account_organization: boolean + :type account_username: string + :type account_email: string + :type account_robot: boolean + :type kind_id: int + """ + + def to_dict(self): + view = { + 'kind': self.kind, + 'metadata': json.loads(self.metadata_json), + 'ip': self.ip, + 'datetime': format_date(self.datetime), + } + + if self.performer_username: + performer = AttrDict({'username': self.performer_username, 'email': self.performer_email}) + performer.robot = None + if self.performer_robot: + performer.robot = self.performer_robot + + view['performer'] = { + 'kind': 'user', + 'name': self.performer_username, + 'is_robot': self.performer_robot, + 'avatar': avatar.get_data_for_user(performer), + } + + if self.account_username: + account = AttrDict({'username': self.account_username, 'email': self.account_email}) + if self.account_organization: + + view['namespace'] = { + 'kind': 'org', + 'name': self.account_username, + 'avatar': avatar.get_data_for_org(account), + } + else: + account.robot = None + if self.account_robot: + account.robot = self.account_robot + view['namespace'] = { + 'kind': 'user', + 'name': self.account_username, + 'avatar': avatar.get_data_for_user(account), + } + + return view + + +class LogEntryPage( + namedtuple('LogEntryPage', ['logs', 'next_page_token'])): + """ + LogEntryPage represents a single page of logs. + :type logs: [LogEntry] + :type next_page_token: {any -> any} + """ + + +class AggregatedLogEntry( + namedtuple('AggregatedLogEntry', ['count', 'kind_id', 'day', 'start_time'])): + """ + AggregatedLogEntry represents an aggregated view of logs. + :type count: int + :type kind_id: int + :type day: string + :type start_time: Date + """ + + def to_dict(self): + synthetic_date = datetime(self.start_time.year, self.start_time.month, int(self.day), tzinfo=get_localzone()) + if synthetic_date.day < self.start_time.day: + synthetic_date = synthetic_date + relativedelta(months=1) + kinds = model.log.get_log_entry_kinds() + view = { + 'kind': kinds[self.kind_id], + 'count': self.count, + 'datetime': format_date(synthetic_date), + } + + return view + + +@add_metaclass(ABCMeta) +class SuperuserDataInterface(object): + """ + Interface that represents all data store interactions required by a superuser api. + """ + + @abstractmethod + def get_logs_query(self, start_time, end_time, page_token=None): + """ + Returns a LogEntryPage. + """ + + @abstractmethod + def get_aggregated_logs(self, start_time, end_time): + """ + Returns a list of AggregatedLogEntry + """ + + @abstractmethod + def get_organizations(self): + """ + Returns a list of Organization + """ + + @abstractmethod + def get_active_users(self): + """ + Returns a list of User + """ + + @abstractmethod + def create_install_user(self, username, password, email): + """ + Returns the created user and confirmation code for email confirmation + """ + + @abstractmethod + def get_nonrobot_user(self, username): + """ + Returns a User + """ + + @abstractmethod + def create_reset_password_email_code(self, email): + """ + Returns a recover password code + """ + + @abstractmethod + def delete_user(self, username): + """ + Returns None + """ + + @abstractmethod + def change_password(self, username, password): + """ + Returns None + """ + + @abstractmethod + def update_email(self, username, email, auto_verify): + """ + Returns None + """ + + @abstractmethod + def update_enabled(self, username, enabled): + """ + Returns None + """ + + @abstractmethod + def take_ownership(self, namespace, authed_user): + """ + Returns id of entity and whether the entity was a user + """ + + @abstractmethod + def delete_organization(self, name): + """ + Returns None + """ + + @abstractmethod + def change_organization_name(self, old_org_name, new_org_name): + """ + Returns updated Organization + """ + + @abstractmethod + def list_all_service_keys(self): + """ + Returns a list of service keys + """ + + @abstractmethod + def generate_service_key(self, service, expiration_date, kid=None, name='', metadata=None, rotation_duration=None): + """ + Returns a tuple of private key and public key id + """ + + @abstractmethod + def approve_service_key(self, kid, approver, approval_type, notes=''): + """ + Returns the approved Key + """ + + @abstractmethod + def get_service_key(self, kid, service=None, alive_only=True, approved_only=True): + """ + Returns ServiceKey + """ + + @abstractmethod + def set_key_expiration(self, kid, expiration_date): + """ + Returns None + """ + + @abstractmethod + def update_service_key(self, kid, name=None, metadata=None): + """ + Returns None + """ + + @abstractmethod + def delete_service_key(self, kid): + """ + Returns deleted ServiceKey + """ + + @abstractmethod + def get_repository_build(self, uuid): + """ + Returns RepositoryBuild + """ diff --git a/endpoints/api/superuser_models_pre_oci.py b/endpoints/api/superuser_models_pre_oci.py new file mode 100644 index 000000000..93a3ebe21 --- /dev/null +++ b/endpoints/api/superuser_models_pre_oci.py @@ -0,0 +1,215 @@ +import features +from app import all_queues, userfiles +from auth.permissions import ReadRepositoryPermission, ModifyRepositoryPermission, AdministerRepositoryPermission +from data import model, database +from endpoints.api.build import get_job_config, _get_build_status +from endpoints.api.superuser_models_interface import BuildTrigger +from endpoints.api.superuser_models_interface import SuperuserDataInterface, LogEntryPage, LogEntry, Organization, User, \ + ServiceKey, Approval, RepositoryBuild, AggregatedLogEntry + + +def _create_log(log, log_kind): + account_organization = None + account_username = None + account_email = None + account_robot = None + try: + account_organization = log.account.organization + account_username = log.account.username + account_email = log.account.email + account_robot = log.account.robot + except AttributeError: + pass + + performer_robot = None + performer_username = None + performer_email = None + + try: + performer_robot = log.performer.robot + performer_username = log.performer.username + performer_email = log.performer.email + except AttributeError: + pass + + return LogEntry(log.metadata_json, log.ip, log.datetime, performer_email, performer_username, + performer_robot, account_organization, account_username, + account_email, account_robot, log_kind[log.kind_id]) + + +def _create_user(user): + return User(user.username, user.email, user.verified, user.enabled, user.robot) + + +def _create_key(key): + approval = None + if key.approval is not None: + approval = Approval(_create_user(key.approval.approver), key.approval.approval_type, key.approval.approved_date, + key.approval.notes) + + return ServiceKey(key.name, key.kid, key.service, key.jwk, key.metadata, key.created_date, key.expiration_date, + key.rotation_duration, approval) + + +class ServiceKeyDoesNotExist(Exception): + pass + + +class ServiceKeyAlreadyApproved(Exception): + pass + + +class InvalidRepositoryBuildException(Exception): + pass + + +class PreOCIModel(SuperuserDataInterface): + """ + PreOCIModel implements the data model for the SuperUser using a database schema + before it was changed to support the OCI specification. + """ + + def get_repository_build(self, uuid): + try: + build = model.build.get_repository_build(uuid) + except model.InvalidRepositoryBuildException as e: + raise InvalidRepositoryBuildException(e.message) + + repo_namespace = build.repository_namespace_user_username + repo_name = build.repository_name + + can_read = ReadRepositoryPermission(repo_namespace, repo_name).can() + can_write = ModifyRepositoryPermission(repo_namespace, repo_name).can() + can_admin = AdministerRepositoryPermission(repo_namespace, repo_name).can() + job_config = get_job_config(build.job_config) + phase, status, error = _get_build_status(build) + url = userfiles.get_file_url(self.resource_key, requires_cors=True) + + return RepositoryBuild(build.uuid, build.logs_archived, repo_namespace, repo_name, can_write, can_read, + _create_user(build.pull_robot), build.resource_key, + BuildTrigger(build.trigger.uuid, build.trigger.service.name, + _create_user(build.trigger.pull_robot), can_read, can_admin, True), + build.display_name, build.display_name, build.started, job_config, phase, status, error, url) + + def delete_service_key(self, kid): + try: + key = model.service_keys.delete_service_key(kid) + except model.ServiceKeyDoesNotExist: + raise ServiceKeyDoesNotExist + return _create_key(key) + + def update_service_key(self, kid, name=None, metadata=None): + model.service_keys.update_service_key(kid, name, metadata) + + def set_key_expiration(self, kid, expiration_date): + model.service_keys.set_key_expiration(kid, expiration_date) + + def get_service_key(self, kid, service=None, alive_only=True, approved_only=True): + try: + key = model.service_keys.get_service_key(kid, approved_only=approved_only, alive_only=alive_only) + return _create_key(key) + except model.ServiceKeyDoesNotExist: + raise ServiceKeyDoesNotExist + + def approve_service_key(self, kid, approver, approval_type, notes=''): + try: + key = model.service_keys.approve_service_key(kid, approver, approval_type, notes=notes) + return _create_key(key) + except model.ServiceKeyDoesNotExist: + raise ServiceKeyDoesNotExist + except model.ServiceKeyAlreadyApproved: + raise ServiceKeyAlreadyApproved + + def generate_service_key(self, service, expiration_date, kid=None, name='', metadata=None, rotation_duration=None): + (private_key, key) = model.service_keys.generate_service_key(service, expiration_date, metadata=metadata, name=name) + + return private_key, key.kid + + def list_all_service_keys(self): + keys = model.service_keys.list_all_keys() + return [_create_key(key) for key in keys] + + def change_organization_name(self, old_org_name, new_org_name): + org = model.organization.get_organization(old_org_name) + if new_org_name is not None: + org = model.user.change_username(org.id, new_org_name) + + return Organization(org.username, org.email) + + def delete_organization(self, name): + org = model.organization.get_organization(name) + model.user.delete_user(org, all_queues) + + def take_ownership(self, namespace, authed_user): + entity = model.user.get_user_or_org(namespace) + if entity is None: + return None, False + + was_user = not entity.organization + if entity.organization: + # Add the superuser as an admin to the owners team of the org. + model.organization.add_user_as_admin(authed_user, entity) + else: + # If the entity is a user, convert it to an organization and add the current superuser + # as the admin. + model.organization.convert_user_to_organization(entity, authed_user) + return entity.id, was_user + + def update_enabled(self, username, enabled): + user = model.user.get_nonrobot_user(username) + model.user.update_enabled(user, bool(enabled)) + + def update_email(self, username, email, auto_verify): + user = model.user.get_nonrobot_user(username) + model.user.update_email(user, email, auto_verify) + + def change_password(self, username, password): + user = model.user.get_nonrobot_user(username) + model.user.change_password(user, password) + + def delete_user(self, username): + user = model.user.get_nonrobot_user(username) + model.user.delete_user(user, all_queues, force=True) + + def create_reset_password_email_code(self, email): + code = model.user.create_reset_password_email_code(email) + return code.code + + def get_nonrobot_user(self, username): + user = model.user.get_nonrobot_user(username) + if user is None: + return None + return _create_user(user) + + def create_install_user(self, username, password, email): + prompts = model.user.get_default_user_prompts(features) + user = model.user.create_user(username, password, email, auto_verify=not features.MAILING, + email_required=features.MAILING, prompts=prompts) + + return_user = _create_user(user) + # If mailing is turned on, send the user a verification email. + if features.MAILING: + confirmation = model.user.create_confirm_email_code(user) + return return_user, confirmation.code + return return_user, '' + + def get_active_users(self): + users = model.user.get_active_users() + return [_create_user(user) for user in users] + + def get_organizations(self): + return [Organization(org.username, org.email) for org in model.organization.get_organizations()] + + def get_aggregated_logs(self, start_time, end_time): + aggregated_logs = model.log.get_aggregated_logs(start_time, end_time) + return [AggregatedLogEntry(log.count, log.kind_id, log.day, start_time) for log in aggregated_logs] + + def get_logs_query(self, start_time, end_time, page_token=None): + logs_query = model.log.get_logs_query(start_time, end_time) + logs, next_page_token = model.modelutil.paginate(logs_query, database.LogEntry, descending=True, + page_token=page_token, limit=20) + kinds = model.log.get_log_entry_kinds() + return LogEntryPage([_create_log(log, kinds) for log in logs], next_page_token) + + +pre_oci_model = PreOCIModel()