From c4a6273e00abcd015b3b8434633e4958e920eef2 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 9 Mar 2018 13:31:29 -0500 Subject: [PATCH 1/6] Add creation date to User table --- data/database.py | 2 ++ ...323c78b_add_creation_date_to_user_table.py | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 data/migrations/versions/0cf50323c78b_add_creation_date_to_user_table.py diff --git a/data/database.py b/data/database.py index 4e767c9b0..27bbfb7f8 100644 --- a/data/database.py +++ b/data/database.py @@ -441,6 +441,8 @@ class User(BaseModel): maximum_queued_builds_count = IntegerField(null=True) + creation_date = DateTimeField(default=datetime.utcnow, null=True) + def delete_instance(self, recursive=False, delete_nullable=False): # If we are deleting a robot account, only execute the subset of queries necessary. if self.robot: diff --git a/data/migrations/versions/0cf50323c78b_add_creation_date_to_user_table.py b/data/migrations/versions/0cf50323c78b_add_creation_date_to_user_table.py new file mode 100644 index 000000000..775c5caa7 --- /dev/null +++ b/data/migrations/versions/0cf50323c78b_add_creation_date_to_user_table.py @@ -0,0 +1,26 @@ +"""Add creation date to User table + +Revision ID: 0cf50323c78b +Revises: 87fbbc224f10 +Create Date: 2018-03-09 13:19:41.903196 + +""" + +# revision identifiers, used by Alembic. +revision = '0cf50323c78b' +down_revision = '87fbbc224f10' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(tables): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('user', sa.Column('creation_date', sa.DateTime(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(tables): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('user', 'creation_date') + # ### end Alembic commands ### From a693771345aa9c1611d78b7657f31292b6956a7b Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 9 Mar 2018 13:55:19 -0500 Subject: [PATCH 2/6] Add creation date information to robots API and UI Fixes https://jira.coreos.com/browse/QUAY-846 --- data/model/user.py | 2 +- endpoints/api/robot_models_interface.py | 10 +++++++++- endpoints/api/robot_models_pre_oci.py | 18 ++++++++++-------- static/directives/robots-manager.html | 6 ++++++ static/js/directives/ui/robots-manager.js | 2 ++ 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/data/model/user.py b/data/model/user.py index 0c86b5a7b..59a15c9da 100644 --- a/data/model/user.py +++ b/data/model/user.py @@ -375,7 +375,7 @@ def _list_entity_robots(entity_name): def list_entity_robot_permission_teams(entity_name, include_permissions=False): query = (_list_entity_robots(entity_name)) - fields = [User.username, FederatedLogin.service_ident] + fields = [User.username, User.creation_date, FederatedLogin.service_ident] if include_permissions: query = (query .join(RepositoryPermission, JOIN_LEFT_OUTER, diff --git a/endpoints/api/robot_models_interface.py b/endpoints/api/robot_models_interface.py index 98b641663..f0ca57354 100644 --- a/endpoints/api/robot_models_interface.py +++ b/endpoints/api/robot_models_interface.py @@ -3,6 +3,8 @@ from collections import namedtuple from six import add_metaclass +from endpoints.api import format_date + class Permission(namedtuple('Permission', ['repository_name', 'repository_visibility_name', 'role_name'])): """ @@ -36,6 +38,7 @@ class RobotWithPermissions( namedtuple('RobotWithPermissions', [ 'name', 'password', + 'created', 'teams', 'repository_names', ])): @@ -43,6 +46,7 @@ class RobotWithPermissions( RobotWithPermissions is a list of robot entries. :type name: string :type password: string + :type created: datetime|None :type teams: [Team] :type repository_names: [string] @@ -52,6 +56,7 @@ class RobotWithPermissions( return { 'name': self.name, 'token': self.password, + 'created': format_date(self.created) if self.created is not None else None, 'teams': [team.to_dict() for team in self.teams], 'repositories': self.repository_names } @@ -61,18 +66,21 @@ class Robot( namedtuple('Robot', [ 'name', 'password', + 'created', ])): """ Robot represents a robot entity. :type name: string :type password: string + :type created: datetime|None """ def to_dict(self): return { 'name': self.name, - 'token': self.password + 'token': self.password, + 'created': format_date(self.created) if self.created is not None else None, } diff --git a/endpoints/api/robot_models_pre_oci.py b/endpoints/api/robot_models_pre_oci.py index abbd75a62..f325591ca 100644 --- a/endpoints/api/robot_models_pre_oci.py +++ b/endpoints/api/robot_models_pre_oci.py @@ -23,6 +23,7 @@ class RobotPreOCIModel(RobotInterface): robot_dict = { 'name': robot_name, 'token': robot_tuple.get(FederatedLogin.service_ident), + 'created': robot_tuple.get(User.creation_date), } if include_permissions: @@ -30,7 +31,7 @@ class RobotPreOCIModel(RobotInterface): 'teams': [], 'repositories': [] }) - robots[robot_name] = Robot(robot_dict['name'], robot_dict['token']) + robots[robot_name] = Robot(robot_dict['name'], robot_dict['token'], robot_dict['created']) if include_permissions: team_name = robot_tuple.get(TeamTable.name) repository_name = robot_tuple.get(Repository.name) @@ -48,40 +49,41 @@ class RobotPreOCIModel(RobotInterface): if repository_name is not None: if repository_name not in robot_dict['repositories']: robot_dict['repositories'].append(repository_name) - robots[robot_name] = RobotWithPermissions(robot_dict['name'], robot_dict['token'], robot_dict['teams'], + robots[robot_name] = RobotWithPermissions(robot_dict['name'], robot_dict['token'], + robot_dict['created'], robot_dict['teams'], robot_dict['repositories']) return robots.values() def regenerate_user_robot_token(self, robot_shortname, owning_user): robot, password = model.user.regenerate_robot_token(robot_shortname, owning_user) - return Robot(robot.username, password) + return Robot(robot.username, password, robot.creation_date) def regenerate_org_robot_token(self, robot_shortname, orgname): parent = model.organization.get_organization(orgname) robot, password = model.user.regenerate_robot_token(robot_shortname, parent) - return Robot(robot.username, password) + return Robot(robot.username, password, robot.creation_date) def delete_robot(self, robot_username): model.user.delete_robot(robot_username) def create_user_robot(self, robot_shortname, owning_user): robot, password = model.user.create_robot(robot_shortname, owning_user) - return Robot(robot.username, password) + return Robot(robot.username, password, robot.creation_date) def create_org_robot(self, robot_shortname, orgname): parent = model.organization.get_organization(orgname) robot, password = model.user.create_robot(robot_shortname, parent) - return Robot(robot.username, password) + return Robot(robot.username, password, robot.creation_date) def get_org_robot(self, robot_shortname, orgname): parent = model.organization.get_organization(orgname) robot, password = model.user.get_robot(robot_shortname, parent) - return Robot(robot.username, password) + return Robot(robot.username, password, robot.creation_date) def get_user_robot(self, robot_shortname, owning_user): robot, password = model.user.get_robot(robot_shortname, owning_user) - return Robot(robot.username, password) + return Robot(robot.username, password, robot.creation_date) pre_oci_model = RobotPreOCIModel() diff --git a/static/directives/robots-manager.html b/static/directives/robots-manager.html index 65908bd21..1890a3ed1 100644 --- a/static/directives/robots-manager.html +++ b/static/directives/robots-manager.html @@ -40,6 +40,9 @@ Teams Repositories + + Created + @@ -78,6 +81,9 @@ + + + diff --git a/static/js/directives/ui/robots-manager.js b/static/js/directives/ui/robots-manager.js index 67bf312ae..eca191984 100644 --- a/static/js/directives/ui/robots-manager.js +++ b/static/js/directives/ui/robots-manager.js @@ -39,6 +39,8 @@ angular.module('quay').directive('robotsManager', function () { robot['teams_string'] = robot.teams.map(function(team) { return team['name'] || ''; }).join(','); + + robot['created_datetime'] = robot.created ? TableService.getReversedTimestamp(robot.created) : null; }); $scope.orderedRobots = TableService.buildOrderedItems(robots, $scope.options, From 254cdfe43af031bb95fbfc7f6304f842cf087395 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 9 Mar 2018 15:55:51 -0500 Subject: [PATCH 3/6] Add support for metadata on robot accounts Fixes https://jira.coreos.com/browse/QUAY-847 Fixes https://jira.coreos.com/browse/QUAY-816 --- data/database.py | 6 ++ ...bc139ad8_add_robotaccountmetadata_table.py | 35 ++++++++++ data/model/user.py | 65 ++++++++++++++----- endpoints/api/__init__.py | 9 +-- endpoints/api/robot.py | 58 ++++++++++++++--- endpoints/api/robot_models_interface.py | 21 ++++-- endpoints/api/robot_models_pre_oci.py | 49 +++++++++----- endpoints/api/test/test_robot.py | 38 +++++++++++ 8 files changed, 229 insertions(+), 52 deletions(-) create mode 100644 data/migrations/versions/b547bc139ad8_add_robotaccountmetadata_table.py create mode 100644 endpoints/api/test/test_robot.py diff --git a/data/database.py b/data/database.py index 27bbfb7f8..ea1353830 100644 --- a/data/database.py +++ b/data/database.py @@ -479,6 +479,12 @@ class User(BaseModel): Namespace = User.alias() +class RobotAccountMetadata(BaseModel): + robot_account = QuayUserField(index=True, allows_robots=True, unique=True) + description = CharField() + unstructured_json = JSONField() + + class DeletedNamespace(BaseModel): namespace = QuayUserField(index=True, allows_robots=False, unique=True) marked = DateTimeField(default=datetime.now) diff --git a/data/migrations/versions/b547bc139ad8_add_robotaccountmetadata_table.py b/data/migrations/versions/b547bc139ad8_add_robotaccountmetadata_table.py new file mode 100644 index 000000000..0530963e7 --- /dev/null +++ b/data/migrations/versions/b547bc139ad8_add_robotaccountmetadata_table.py @@ -0,0 +1,35 @@ +"""Add RobotAccountMetadata table + +Revision ID: b547bc139ad8 +Revises: 0cf50323c78b +Create Date: 2018-03-09 15:50:48.298880 + +""" + +# revision identifiers, used by Alembic. +revision = 'b547bc139ad8' +down_revision = '0cf50323c78b' + +from alembic import op +import sqlalchemy as sa +from util.migrate import UTF8CharField + + +def upgrade(tables): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('robotaccountmetadata', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('robot_account_id', sa.Integer(), nullable=False), + sa.Column('description', UTF8CharField(length=255), nullable=False), + sa.Column('unstructured_json', sa.Text(), nullable=False), + sa.ForeignKeyConstraint(['robot_account_id'], ['user.id'], name=op.f('fk_robotaccountmetadata_robot_account_id_user')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_robotaccountmetadata')) + ) + op.create_index('robotaccountmetadata_robot_account_id', 'robotaccountmetadata', ['robot_account_id'], unique=True) + # ### end Alembic commands ### + + +def downgrade(tables): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('robotaccountmetadata') + # ### end Alembic commands ### diff --git a/data/model/user.py b/data/model/user.py index 59a15c9da..654c55e64 100644 --- a/data/model/user.py +++ b/data/model/user.py @@ -14,7 +14,8 @@ from data.database import (User, LoginService, FederatedLogin, RepositoryPermiss EmailConfirmation, Role, db_for_update, random_string_generator, UserRegion, ImageStorageLocation, ServiceKeyApproval, OAuthApplication, RepositoryBuildTrigger, - UserPromptKind, UserPrompt, UserPromptTypes, DeletedNamespace) + UserPromptKind, UserPrompt, UserPromptTypes, DeletedNamespace, + RobotAccountMetadata) from data.model import (DataModelException, InvalidPasswordException, InvalidRobotException, InvalidUsernameException, InvalidEmailAddressException, TooManyLoginAttemptsException, db_transaction, @@ -231,7 +232,7 @@ def update_enabled(user, set_enabled): user.save() -def create_robot(robot_shortname, parent): +def create_robot(robot_shortname, parent, description='', unstructured_metadata=None): (username_valid, username_issue) = validate_username(robot_shortname) if not username_valid: raise InvalidRobotException('The name for the robot \'%s\' is invalid: %s' % @@ -245,33 +246,59 @@ def create_robot(robot_shortname, parent): msg = 'Existing robot with name: %s' % username logger.info(msg) raise InvalidRobotException(msg) - except User.DoesNotExist: pass + service = LoginService.get(name='quayrobot') try: - created = User.create(username=username, robot=True) + with db_transaction(): + created = User.create(username=username, robot=True) + password = created.email - service = LoginService.get(name='quayrobot') - password = created.email - FederatedLogin.create(user=created, service=service, - service_ident=password) - - return created, password + FederatedLogin.create(user=created, service=service, service_ident=password) + RobotAccountMetadata.create(robot_account=created, description=description[0:255], + unstructured_json=unstructured_metadata or {}) + return created, password except Exception as ex: raise DataModelException(ex.message) +def get_or_create_robot_metadata(robot): + try: + return RobotAccountMetadata.get(robot_account=robot) + except RobotAccountMetadata.DoesNotExist: + try: + return RobotAccountMetadata.create(robot_account=robot, description='', + unstructured_json='{}') + except IntegrityError: + return RobotAccountMetadata.get(robot_account=robot) + + +def update_robot_metadata(robot, description='', unstructured_json=None): + """ Updates the description and user-specified unstructured metadata associated + with a robot account to that specified. """ + metadata = get_or_create_robot_metadata(robot) + metadata.description = description + metadata.unstructured_json = unstructured_json or metadata.unstructured_json or {} + metadata.save() + + def get_robot(robot_shortname, parent): robot_username = format_robot_username(parent.username, robot_shortname) robot = lookup_robot(robot_username) return robot, robot.email +def get_robot_and_metadata(robot_shortname, parent): + robot_username = format_robot_username(parent.username, robot_shortname) + robot, metadata = lookup_robot_and_metadata(robot_username) + return robot, robot.email, metadata + + def lookup_robot(robot_username): try: return (User - .select() + .select(User, FederatedLogin) .join(FederatedLogin) .join(LoginService) .where(LoginService.name == 'quayrobot', User.username == robot_username, @@ -281,6 +308,11 @@ def lookup_robot(robot_username): raise InvalidRobotException('Could not find robot with username: %s' % robot_username) +def lookup_robot_and_metadata(robot_username): + robot = lookup_robot(robot_username) + return robot, get_or_create_robot_metadata(robot) + + def get_matching_robots(name_prefix, username, limit=10): admined_orgs = (_basequery.get_user_organizations(username) .switch(Team) @@ -333,7 +365,7 @@ def verify_robot(robot_username, password): def regenerate_robot_token(robot_shortname, parent): robot_username = format_robot_username(parent.username, robot_shortname) - robot = lookup_robot(robot_username) + robot, metadata = lookup_robot_and_metadata(robot_username) password = random_string_generator(length=64)() robot.email = password robot.uuid = str(uuid4()) @@ -345,7 +377,7 @@ def regenerate_robot_token(robot_shortname, parent): login.save() robot.save() - return robot, password + return robot, password, metadata def delete_robot(robot_username): try: @@ -367,15 +399,18 @@ def _list_entity_robots(entity_name): materialized list so that callers can use db_for_update. """ return (User - .select() + .select(User, FederatedLogin, RobotAccountMetadata) .join(FederatedLogin) + .switch(User) + .join(RobotAccountMetadata, JOIN_LEFT_OUTER) .where(User.robot == True, User.username ** (entity_name + '+%'))) def list_entity_robot_permission_teams(entity_name, include_permissions=False): query = (_list_entity_robots(entity_name)) - fields = [User.username, User.creation_date, FederatedLogin.service_ident] + fields = [User.username, User.creation_date, FederatedLogin.service_ident, + RobotAccountMetadata.description, RobotAccountMetadata.unstructured_json] if include_permissions: query = (query .join(RepositoryPermission, JOIN_LEFT_OUTER, diff --git a/endpoints/api/__init__.py b/endpoints/api/__init__.py index cf660b41c..b9c501b2c 100644 --- a/endpoints/api/__init__.py +++ b/endpoints/api/__init__.py @@ -321,7 +321,7 @@ def require_scope(scope_object): return wrapper -def validate_json_request(schema_name): +def validate_json_request(schema_name, optional=False): def wrapper(func): @add_method_metadata('request_schema', schema_name) @wraps(func) @@ -330,9 +330,10 @@ def validate_json_request(schema_name): try: json_data = request.get_json() if json_data is None: - raise InvalidRequest('Missing JSON body') - - validate(json_data, schema) + if not optional: + raise InvalidRequest('Missing JSON body') + else: + validate(json_data, schema) return func(self, *args, **kwargs) except ValidationError as ex: raise InvalidRequest(ex.message) diff --git a/endpoints/api/robot.py b/endpoints/api/robot.py index 0e0cc64fc..59b6779bb 100644 --- a/endpoints/api/robot.py +++ b/endpoints/api/robot.py @@ -2,14 +2,31 @@ from endpoints.api import (resource, nickname, ApiResource, log_action, related_user_resource, require_user_admin, require_scope, path_param, parse_args, - truthy_bool, query_param) + truthy_bool, query_param, validate_json_request) from endpoints.api.robot_models_pre_oci import pre_oci_model as model from endpoints.exception import Unauthorized from auth.permissions import AdministerOrganizationPermission, OrganizationMemberPermission from auth.auth_context import get_authenticated_user from auth import scopes from util.names import format_robot_username -from flask import abort +from flask import abort, request + + +CREATE_ROBOT_SCHEMA = { + 'type': 'object', + 'description': 'Optional data for creating a robot', + 'properties': { + 'description': { + 'type': 'string', + 'description': 'Optional text description for the robot', + 'maxLength': 255, + }, + 'unstructured_metadata': { + 'type': 'object', + 'description': 'Optional unstructured metadata for the robot', + }, + }, +} def robots_list(prefix, include_permissions=False): @@ -38,6 +55,9 @@ class UserRobotList(ApiResource): 'The short name for the robot, without any user or organization prefix') class UserRobot(ApiResource): """ Resource for managing a user's robots. """ + schemas = { + 'CreateRobot': CREATE_ROBOT_SCHEMA, + } @require_user_admin @nickname('getUserRobot') @@ -45,16 +65,23 @@ class UserRobot(ApiResource): """ Returns the user's robot with the specified name. """ parent = get_authenticated_user() robot = model.get_user_robot(robot_shortname, parent) - return robot.to_dict() + return robot.to_dict(include_metadata=True) @require_user_admin @nickname('createUserRobot') + @validate_json_request('CreateRobot', optional=True) def put(self, robot_shortname): """ Create a new user robot with the specified name. """ parent = get_authenticated_user() - robot = model.create_user_robot(robot_shortname, parent) - log_action('create_robot', parent.username, {'robot': robot_shortname}) - return robot.to_dict(), 201 + create_data = request.get_json() or {} + robot = model.create_user_robot(robot_shortname, parent, create_data.get('description'), + create_data.get('unstructured_metadata')) + log_action('create_robot', parent.username, { + 'robot': robot_shortname, + 'description': create_data.get('description'), + 'unstructured_metadata': create_data.get('unstructured_metadata'), + }) + return robot.to_dict(include_metadata=True), 201 @require_user_admin @nickname('deleteUserRobot') @@ -82,6 +109,7 @@ class OrgRobotList(ApiResource): """ List the organization's robots. """ permission = OrganizationMemberPermission(orgname) if permission.can(): + include_metadata = AdministerOrganizationPermission(orgname).can() return robots_list(orgname, include_permissions=parsed_args.get('permissions', False)) raise Unauthorized() @@ -94,6 +122,9 @@ class OrgRobotList(ApiResource): @related_user_resource(UserRobot) class OrgRobot(ApiResource): """ Resource for managing an organization's robots. """ + schemas = { + 'CreateRobot': CREATE_ROBOT_SCHEMA, + } @require_scope(scopes.ORG_ADMIN) @nickname('getOrgRobot') @@ -102,19 +133,26 @@ class OrgRobot(ApiResource): permission = AdministerOrganizationPermission(orgname) if permission.can(): robot = model.get_org_robot(robot_shortname, orgname) - return robot.to_dict() + return robot.to_dict(include_metadata=True) raise Unauthorized() @require_scope(scopes.ORG_ADMIN) @nickname('createOrgRobot') + @validate_json_request('CreateRobot', optional=True) def put(self, orgname, robot_shortname): """ Create a new robot in the organization. """ permission = AdministerOrganizationPermission(orgname) if permission.can(): - robot = model.create_org_robot(robot_shortname, orgname) - log_action('create_robot', orgname, {'robot': robot_shortname}) - return robot.to_dict(), 201 + create_data = request.get_json() or {} + robot = model.create_org_robot(robot_shortname, orgname, create_data.get('description'), + create_data.get('unstructured_metadata')) + log_action('create_robot', orgname, { + 'robot': robot_shortname, + 'description': create_data.get('description'), + 'unstructured_metadata': create_data.get('unstructured_metadata'), + }) + return robot.to_dict(include_metadata=True), 201 raise Unauthorized() diff --git a/endpoints/api/robot_models_interface.py b/endpoints/api/robot_models_interface.py index f0ca57354..5783d6d5c 100644 --- a/endpoints/api/robot_models_interface.py +++ b/endpoints/api/robot_models_interface.py @@ -41,6 +41,7 @@ class RobotWithPermissions( 'created', 'teams', 'repository_names', + 'description', ])): """ RobotWithPermissions is a list of robot entries. @@ -49,7 +50,7 @@ class RobotWithPermissions( :type created: datetime|None :type teams: [Team] :type repository_names: [string] - + :type description: string """ def to_dict(self): @@ -58,7 +59,8 @@ class RobotWithPermissions( 'token': self.password, 'created': format_date(self.created) if self.created is not None else None, 'teams': [team.to_dict() for team in self.teams], - 'repositories': self.repository_names + 'repositories': self.repository_names, + 'description': self.description, } @@ -67,22 +69,31 @@ class Robot( 'name', 'password', 'created', + 'description', + 'unstructured_metadata', ])): """ Robot represents a robot entity. :type name: string :type password: string :type created: datetime|None - + :type description: string + :type unstructured_metadata: dict """ - def to_dict(self): - return { + def to_dict(self, include_metadata=False): + data = { 'name': self.name, 'token': self.password, 'created': format_date(self.created) if self.created is not None else None, + 'description': self.description, } + if include_metadata: + data['unstructured_metadata'] = self.unstructured_metadata + + return data + @add_metaclass(ABCMeta) class RobotInterface(object): diff --git a/endpoints/api/robot_models_pre_oci.py b/endpoints/api/robot_models_pre_oci.py index f325591ca..46ff1f4f3 100644 --- a/endpoints/api/robot_models_pre_oci.py +++ b/endpoints/api/robot_models_pre_oci.py @@ -1,6 +1,6 @@ from app import avatar from data import model -from data.database import User, FederatedLogin, Team as TeamTable, Repository +from data.database import User, FederatedLogin, Team as TeamTable, Repository, RobotAccountMetadata from endpoints.api.robot_models_interface import (RobotInterface, Robot, RobotWithPermissions, Team, Permission) @@ -24,14 +24,18 @@ class RobotPreOCIModel(RobotInterface): 'name': robot_name, 'token': robot_tuple.get(FederatedLogin.service_ident), 'created': robot_tuple.get(User.creation_date), + 'description': robot_tuple.get(RobotAccountMetadata.description), + 'unstructured_metadata': robot_tuple.get(RobotAccountMetadata.unstructured_json), } if include_permissions: robot_dict.update({ 'teams': [], - 'repositories': [] + 'repositories': [], }) - robots[robot_name] = Robot(robot_dict['name'], robot_dict['token'], robot_dict['created']) + + robots[robot_name] = Robot(robot_dict['name'], robot_dict['token'], robot_dict['created'], + robot_dict['description'], robot_dict['unstructured_metadata']) if include_permissions: team_name = robot_tuple.get(TeamTable.name) repository_name = robot_tuple.get(Repository.name) @@ -51,39 +55,48 @@ class RobotPreOCIModel(RobotInterface): robot_dict['repositories'].append(repository_name) robots[robot_name] = RobotWithPermissions(robot_dict['name'], robot_dict['token'], robot_dict['created'], robot_dict['teams'], - robot_dict['repositories']) + robot_dict['repositories'], + robot_dict['description']) return robots.values() def regenerate_user_robot_token(self, robot_shortname, owning_user): - robot, password = model.user.regenerate_robot_token(robot_shortname, owning_user) - return Robot(robot.username, password, robot.creation_date) + robot, password, metadata = model.user.regenerate_robot_token(robot_shortname, owning_user) + return Robot(robot.username, password, robot.creation_date, metadata.description, + metadata.unstructured_json) def regenerate_org_robot_token(self, robot_shortname, orgname): parent = model.organization.get_organization(orgname) - robot, password = model.user.regenerate_robot_token(robot_shortname, parent) - return Robot(robot.username, password, robot.creation_date) + robot, password, metadata = model.user.regenerate_robot_token(robot_shortname, parent) + return Robot(robot.username, password, robot.creation_date, metadata.description, + metadata.unstructured_json) def delete_robot(self, robot_username): model.user.delete_robot(robot_username) - def create_user_robot(self, robot_shortname, owning_user): - robot, password = model.user.create_robot(robot_shortname, owning_user) - return Robot(robot.username, password, robot.creation_date) + def create_user_robot(self, robot_shortname, owning_user, description, unstructured_metadata): + robot, password = model.user.create_robot(robot_shortname, owning_user, description or '', + unstructured_metadata) + return Robot(robot.username, password, robot.creation_date, description or '', + unstructured_metadata) - def create_org_robot(self, robot_shortname, orgname): + def create_org_robot(self, robot_shortname, orgname, description, unstructured_metadata): parent = model.organization.get_organization(orgname) - robot, password = model.user.create_robot(robot_shortname, parent) - return Robot(robot.username, password, robot.creation_date) + robot, password = model.user.create_robot(robot_shortname, parent, description or '', + unstructured_metadata) + return Robot(robot.username, password, robot.creation_date, description or '', + unstructured_metadata) def get_org_robot(self, robot_shortname, orgname): parent = model.organization.get_organization(orgname) - robot, password = model.user.get_robot(robot_shortname, parent) - return Robot(robot.username, password, robot.creation_date) + robot, password, metadata = model.user.get_robot_and_metadata(robot_shortname, parent) + return Robot(robot.username, password, robot.creation_date, metadata.description, + metadata.unstructured_json) def get_user_robot(self, robot_shortname, owning_user): - robot, password = model.user.get_robot(robot_shortname, owning_user) - return Robot(robot.username, password, robot.creation_date) + robot, password, metadata = model.user.get_robot_and_metadata(robot_shortname, owning_user) + return Robot(robot.username, password, robot.creation_date, metadata.description, + metadata.unstructured_json) pre_oci_model = RobotPreOCIModel() diff --git a/endpoints/api/test/test_robot.py b/endpoints/api/test/test_robot.py new file mode 100644 index 000000000..2e3821176 --- /dev/null +++ b/endpoints/api/test/test_robot.py @@ -0,0 +1,38 @@ +import pytest +import json + +from data import model +from endpoints.api import api +from endpoints.api.test.shared import conduct_api_call +from endpoints.api.robot import UserRobot, OrgRobot +from endpoints.test.shared import client_with_identity + +from test.test_ldap import mock_ldap + +from test.fixtures import * + +@pytest.mark.parametrize('endpoint', [ + UserRobot, + OrgRobot, +]) +@pytest.mark.parametrize('body', [ + {}, + {'description': 'this is a description'}, + {'unstructured_metadata': {'foo': 'bar'}}, + {'description': 'this is a description', 'unstructured_metadata': {'foo': 'bar'}}, +]) +def test_create_robot_with_metadata(endpoint, body, client): + with client_with_identity('devtable', client) as cl: + # Create the robot with the specified body. + conduct_api_call(cl, endpoint, 'PUT', {'orgname': 'buynlarge', 'robot_shortname': 'somebot'}, + body, expected_code=201) + + # Ensure the create succeeded. + resp = conduct_api_call(cl, endpoint, 'GET', { + 'orgname': 'buynlarge', + 'robot_shortname': 'somebot', + }) + + body = body or {} + assert resp.json['description'] == (body.get('description') or '') + assert resp.json['unstructured_metadata'] == (body.get('unstructured_metadata') or {}) From 96fafcdffb0616a000d202242c441f6baab792c9 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 9 Mar 2018 16:28:52 -0500 Subject: [PATCH 4/6] Add UI for viewing and setting the description of a robot account --- static/directives/create-entity-dialog.html | 8 ++++++++ static/directives/create-robot-dialog.html | 3 ++- static/directives/robots-manager.html | 5 +++++ static/js/directives/ui/create-entity-dialog.js | 3 +++ static/js/directives/ui/create-robot-dialog.js | 8 ++++++-- 5 files changed, 24 insertions(+), 3 deletions(-) diff --git a/static/directives/create-entity-dialog.html b/static/directives/create-entity-dialog.html index 5e8bec9bf..100913817 100644 --- a/static/directives/create-entity-dialog.html +++ b/static/directives/create-entity-dialog.html @@ -36,6 +36,14 @@ Choose a name to inform your teammates about this {{ entityTitle }}. Must match {{ entityNameRegex }}. + +
+ + +
+ Enter a description to provide extran information to your teammates about this {{ entityTitle }}. +
+
\ No newline at end of file diff --git a/static/directives/robots-manager.html b/static/directives/robots-manager.html index 1890a3ed1..9acbfa5aa 100644 --- a/static/directives/robots-manager.html +++ b/static/directives/robots-manager.html @@ -36,6 +36,7 @@ Robot Account Name + Description Teams @@ -53,6 +54,10 @@ + + (None) + {{ ::robotInfo.description }} + No teams diff --git a/static/js/directives/ui/create-entity-dialog.js b/static/js/directives/ui/create-entity-dialog.js index 605701eda..142090a38 100644 --- a/static/js/directives/ui/create-entity-dialog.js +++ b/static/js/directives/ui/create-entity-dialog.js @@ -15,6 +15,7 @@ angular.module('quay').directive('createEntityDialog', function () { 'entityTitle': '@entityTitle', 'entityIcon': '@entityIcon', 'entityNameRegex': '@entityNameRegex', + 'allowEntityDescription': '@allowEntityDescription', 'entityCreateRequested': '&entityCreateRequested', 'entityCreateCompleted': '&entityCreateCompleted' @@ -41,6 +42,7 @@ angular.module('quay').directive('createEntityDialog', function () { $scope.show = function() { $scope.entityName = null; + $scope.entityDescription = null; $scope.entity = null; $scope.entityForPermissions = null; $scope.creating = false; @@ -67,6 +69,7 @@ angular.module('quay').directive('createEntityDialog', function () { $scope.view = 'creating'; $scope.entityCreateRequested({ 'name': $scope.entityName, + 'description': $scope.entityDescription, 'callback': entityCreateCallback }); }; diff --git a/static/js/directives/ui/create-robot-dialog.js b/static/js/directives/ui/create-robot-dialog.js index 6b2a24b23..3ef3912cc 100644 --- a/static/js/directives/ui/create-robot-dialog.js +++ b/static/js/directives/ui/create-robot-dialog.js @@ -19,7 +19,7 @@ angular.module('quay').directive('createRobotDialog', function () { $scope.robotCreated({'robot': robot}); }; - $scope.createRobot = function(name, callback) { + $scope.createRobot = function(name, description, callback) { var organization = $scope.info.namespace; if (!UserService.isOrganization(organization)) { organization = null; @@ -29,11 +29,15 @@ angular.module('quay').directive('createRobotDialog', function () { 'robot_shortname': name }; + var data = { + 'description': description + }; + var errorDisplay = ApiService.errorDisplay('Cannot create robot account', function() { callback(null); }); - ApiService.createRobot(organization, null, params).then(function(resp) { + ApiService.createRobot(organization, data, params).then(function(resp) { callback(resp); }, errorDisplay); }; From f1da3c452ff2420c34fc5b4bcc81bec28d28ccd8 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 9 Mar 2018 16:46:29 -0500 Subject: [PATCH 5/6] Remove unused code --- endpoints/appr/models_oci.py | 11 ----------- endpoints/v1/models_interface.py | 16 ---------------- endpoints/v1/models_pre_oci.py | 13 ------------- 3 files changed, 40 deletions(-) diff --git a/endpoints/appr/models_oci.py b/endpoints/appr/models_oci.py index 75d5050fa..3f1c7d19c 100644 --- a/endpoints/appr/models_oci.py +++ b/endpoints/appr/models_oci.py @@ -268,17 +268,6 @@ class OCIAppModel(AppRegistryDataInterface): channel = oci_model.channel.create_or_update_channel(repo, channel_name, release) return ChannelView(current=channel.linked_tag.name, name=channel.name) - def get_user(self, username, password): - err_msg = None - if parse_robot_username(username) is not None: - try: - user = data.model.user.verify_robot(username, password) - except data.model.InvalidRobotException as exc: - return (None, exc.message) - else: - user, err_msg = authentication.verify_and_link_user(username, password) - return (user, err_msg) - def get_blob_locations(self, digest): return oci_model.blob.get_blob_locations(digest) diff --git a/endpoints/v1/models_interface.py b/endpoints/v1/models_interface.py index 93aed61a6..0d03d7005 100644 --- a/endpoints/v1/models_interface.py +++ b/endpoints/v1/models_interface.py @@ -160,22 +160,6 @@ class DockerRegistryV1DataInterface(object): """ pass - @abstractmethod - def load_token(self, token): - """ - Loads the data associated with the given (deprecated) access token, and, if - found returns True. - """ - pass - - @abstractmethod - def verify_robot(self, username, token): - """ - Returns True if the given robot username and token match an existing robot - account. - """ - pass - @abstractmethod def change_user_password(self, user, new_password): """ diff --git a/endpoints/v1/models_pre_oci.py b/endpoints/v1/models_pre_oci.py index 2de743e70..816369089 100644 --- a/endpoints/v1/models_pre_oci.py +++ b/endpoints/v1/models_pre_oci.py @@ -137,19 +137,6 @@ class PreOCIModel(DockerRegistryV1DataInterface): def delete_tag(self, namespace_name, repo_name, tag_name): model.tag.delete_tag(namespace_name, repo_name, tag_name) - def load_token(self, token): - try: - model.token.load_token_data(token) - return True - except model.InvalidTokenException: - return False - - def verify_robot(self, username, token): - try: - return bool(model.user.verify_robot(username, token)) - except model.InvalidRobotException: - return False - def change_user_password(self, user, new_password): model.user.change_password(user, new_password) From 2ea13e86a01526895b8b808b6162682747fb32e0 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 12 Mar 2018 20:30:19 -0400 Subject: [PATCH 6/6] Add last_accessed information to User and expose for robot accounts Fixes https://jira.coreos.com/browse/QUAY-848 --- config.py | 4 ++ data/database.py | 2 +- ...f_add_last_accessed_field_to_user_table.py | 28 ++++++++ data/model/_basequery.py | 43 +++++++++++- data/model/appspecifictoken.py | 22 +----- data/model/user.py | 64 ++++++++++-------- endpoints/api/robot.py | 1 - endpoints/api/robot_models_interface.py | 6 ++ endpoints/api/robot_models_pre_oci.py | 32 +++++---- static/directives/robots-manager.html | 6 ++ static/js/directives/ui/robots-manager.js | 1 + test/data/test.db | Bin 1740800 -> 1753088 bytes util/config/schema.py | 1 + 13 files changed, 143 insertions(+), 67 deletions(-) create mode 100644 data/migrations/versions/224ce4c72c2f_add_last_accessed_field_to_user_table.py diff --git a/config.py b/config.py index 369c850c2..f91637992 100644 --- a/config.py +++ b/config.py @@ -527,3 +527,7 @@ class DefaultConfig(ImmutableConfig): # Defines the number of successive internal errors of a build trigger's build before the # trigger is automatically disabled. SUCCESSIVE_TRIGGER_INTERNAL_ERROR_DISABLE_THRESHOLD = 5 + + # Defines the delay required (in seconds) before the last_accessed field of a user/robot or access + # token will be updated after the previous update. + LAST_ACCESSED_UPDATE_THRESHOLD_S = 60 diff --git a/data/database.py b/data/database.py index ea1353830..422275f84 100644 --- a/data/database.py +++ b/data/database.py @@ -440,8 +440,8 @@ class User(BaseModel): location = CharField(null=True) maximum_queued_builds_count = IntegerField(null=True) - creation_date = DateTimeField(default=datetime.utcnow, null=True) + last_accessed = DateTimeField(null=True, index=True) def delete_instance(self, recursive=False, delete_nullable=False): # If we are deleting a robot account, only execute the subset of queries necessary. diff --git a/data/migrations/versions/224ce4c72c2f_add_last_accessed_field_to_user_table.py b/data/migrations/versions/224ce4c72c2f_add_last_accessed_field_to_user_table.py new file mode 100644 index 000000000..06326c566 --- /dev/null +++ b/data/migrations/versions/224ce4c72c2f_add_last_accessed_field_to_user_table.py @@ -0,0 +1,28 @@ +"""Add last_accessed field to User table + +Revision ID: 224ce4c72c2f +Revises: b547bc139ad8 +Create Date: 2018-03-12 22:44:07.070490 + +""" + +# revision identifiers, used by Alembic. +revision = '224ce4c72c2f' +down_revision = 'b547bc139ad8' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(tables): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('user', sa.Column('last_accessed', sa.DateTime(), nullable=True)) + op.create_index('user_last_accessed', 'user', ['last_accessed'], unique=False) + # ### end Alembic commands ### + + +def downgrade(tables): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index('user_last_accessed', table_name='user') + op.drop_column('user', 'last_accessed') + # ### end Alembic commands ### diff --git a/data/model/_basequery.py b/data/model/_basequery.py index 28b0b1952..571b8ac9b 100644 --- a/data/model/_basequery.py +++ b/data/model/_basequery.py @@ -1,11 +1,17 @@ -from peewee import fn +import logging + +from peewee import fn, PeeweeException from cachetools import lru_cache -from data.model import DataModelException +from datetime import datetime, timedelta + +from data.model import DataModelException, config from data.database import (Repository, User, Team, TeamMember, RepositoryPermission, TeamRole, Namespace, Visibility, ImageStorage, Image, RepositoryKind, db_for_update) +logger = logging.getLogger(__name__) + def reduce_as_tree(queries_to_reduce): """ This method will split a list of queries into halves recursively until we reach individual queries, at which point it will start unioning the queries, or the already unioned subqueries. @@ -164,3 +170,36 @@ def calculate_image_aggregate_size(ancestors_str, image_size, parent_image): return None return ancestor_size + image_size + + +def update_last_accessed(token_or_user): + """ Updates the `last_accessed` field on the given token or user. If the existing field's value + is within the configured threshold, the update is skipped. """ + threshold = timedelta(seconds=config.app_config.get('LAST_ACCESSED_UPDATE_THRESHOLD_S', 120)) + if (token_or_user.last_accessed is not None and + datetime.utcnow() - token_or_user.last_accessed < threshold): + # Skip updating, as we don't want to put undue pressure on the database. + return + + model_class = token_or_user.__class__ + last_accessed = datetime.utcnow() + + try: + (model_class + .update(last_accessed=last_accessed) + .where(model_class.id == token_or_user.id) + .execute()) + token_or_user.last_accessed = last_accessed + except PeeweeException as ex: + # If there is any form of DB exception, only fail if strict logging is enabled. + strict_logging_disabled = config.app_config.get('ALLOW_PULLS_WITHOUT_STRICT_LOGGING') + if strict_logging_disabled: + data = { + 'exception': ex, + 'token_or_user': token_or_user.id, + 'class': str(model_class), + } + + logger.exception('update last_accessed for token/user failed', extra=data) + else: + raise diff --git a/data/model/appspecifictoken.py b/data/model/appspecifictoken.py index 4a1d1c686..10130c785 100644 --- a/data/model/appspecifictoken.py +++ b/data/model/appspecifictoken.py @@ -3,10 +3,10 @@ import logging from datetime import datetime from cachetools import lru_cache -from peewee import PeeweeException from data.database import AppSpecificAuthToken, User, db_transaction from data.model import config +from data.model._basequery import update_last_accessed from util.timedeltastring import convert_to_timedelta logger = logging.getLogger(__name__) @@ -104,23 +104,7 @@ def access_valid_token(token_code): ((AppSpecificAuthToken.expiration > datetime.now()) | (AppSpecificAuthToken.expiration >> None))) .get()) + update_last_accessed(token) + return token except AppSpecificAuthToken.DoesNotExist: return None - - token.last_accessed = datetime.now() - - try: - token.save() - except PeeweeException as ex: - strict_logging_disabled = config.app_config.get('ALLOW_PULLS_WITHOUT_STRICT_LOGGING') - if strict_logging_disabled: - data = { - 'exception': ex, - 'token': token.id, - } - - logger.exception('update last_accessed for token failed', extra=data) - else: - raise - - return token diff --git a/data/model/user.py b/data/model/user.py index 654c55e64..28942898b 100644 --- a/data/model/user.py +++ b/data/model/user.py @@ -173,7 +173,7 @@ def change_username(user_id, new_username): 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)): + for robot in db_for_update(_list_entity_robots(user.username, include_metadata=False)): _, robot_shortname = parse_robot_username(robot.username) new_robot_name = format_robot_username(new_username, robot_shortname) robot.username = new_robot_name @@ -264,15 +264,10 @@ def create_robot(robot_shortname, parent, description='', unstructured_metadata= def get_or_create_robot_metadata(robot): - try: - return RobotAccountMetadata.get(robot_account=robot) - except RobotAccountMetadata.DoesNotExist: - try: - return RobotAccountMetadata.create(robot_account=robot, description='', - unstructured_json='{}') - except IntegrityError: - return RobotAccountMetadata.get(robot_account=robot) - + defaults = dict(description='', unstructured_json='{}') + metadata, _ = RobotAccountMetadata.get_or_create(robot_account=robot, defaults=defaults) + return metadata + def update_robot_metadata(robot, description='', unstructured_json=None): """ Updates the description and user-specified unstructured metadata associated @@ -281,13 +276,7 @@ def update_robot_metadata(robot, description='', unstructured_json=None): metadata.description = description metadata.unstructured_json = unstructured_json or metadata.unstructured_json or {} metadata.save() - - -def get_robot(robot_shortname, parent): - robot_username = format_robot_username(parent.username, robot_shortname) - robot = lookup_robot(robot_username) - return robot, robot.email - + def get_robot_and_metadata(robot_shortname, parent): robot_username = format_robot_username(parent.username, robot_shortname) @@ -360,6 +349,9 @@ def verify_robot(robot_username, password): 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 regenerate_robot_token(robot_shortname, parent): @@ -394,22 +386,27 @@ def list_namespace_robots(namespace): return _list_entity_robots(namespace) -def _list_entity_robots(entity_name): +def _list_entity_robots(entity_name, include_metadata=True): """ Return the list of robots for the specified entity. This MUST return a query, not a materialized list so that callers can use db_for_update. - """ - return (User - .select(User, FederatedLogin, RobotAccountMetadata) - .join(FederatedLogin) - .switch(User) - .join(RobotAccountMetadata, JOIN_LEFT_OUTER) - .where(User.robot == True, User.username ** (entity_name + '+%'))) + """ + query = (User + .select(User, FederatedLogin) + .join(FederatedLogin) + .where(User.robot == True, User.username ** (entity_name + '+%'))) + + if include_metadata: + query = (query.switch(User) + .join(RobotAccountMetadata, JOIN_LEFT_OUTER) + .select(User, FederatedLogin, RobotAccountMetadata)) + + return query def list_entity_robot_permission_teams(entity_name, include_permissions=False): query = (_list_entity_robots(entity_name)) - fields = [User.username, User.creation_date, FederatedLogin.service_ident, + fields = [User.username, User.creation_date, User.last_accessed, FederatedLogin.service_ident, RobotAccountMetadata.description, RobotAccountMetadata.unstructured_json] if include_permissions: query = (query @@ -484,6 +481,10 @@ def verify_federated_login(service_id, service_ident): .switch(FederatedLogin).join(User) .where(FederatedLogin.service_ident == service_ident, LoginService.name == service_id) .get()) + + # Mark that the user was accessed. + _basequery.update_last_accessed(found.user) + return found.user except FederatedLogin.DoesNotExist: return None @@ -749,6 +750,9 @@ def verify_user(username_or_email, password): .where(User.id == fetched.id) .execute()) + # Mark that the user was accessed. + _basequery.update_last_accessed(fetched) + # Return the valid user. return fetched @@ -990,10 +994,10 @@ def get_pull_credentials(robotname): return None return { - 'username': robot.username, - 'password': login_info.service_ident, - 'registry': '%s://%s/v1/' % (config.app_config['PREFERRED_URL_SCHEME'], - config.app_config['SERVER_HOSTNAME']), + 'username': robot.username, + 'password': login_info.service_ident, + 'registry': '%s://%s/v1/' % (config.app_config['PREFERRED_URL_SCHEME'], + config.app_config['SERVER_HOSTNAME']), } def get_region_locations(user): diff --git a/endpoints/api/robot.py b/endpoints/api/robot.py index 59b6779bb..e815498ce 100644 --- a/endpoints/api/robot.py +++ b/endpoints/api/robot.py @@ -109,7 +109,6 @@ class OrgRobotList(ApiResource): """ List the organization's robots. """ permission = OrganizationMemberPermission(orgname) if permission.can(): - include_metadata = AdministerOrganizationPermission(orgname).can() return robots_list(orgname, include_permissions=parsed_args.get('permissions', False)) raise Unauthorized() diff --git a/endpoints/api/robot_models_interface.py b/endpoints/api/robot_models_interface.py index 5783d6d5c..707fc976f 100644 --- a/endpoints/api/robot_models_interface.py +++ b/endpoints/api/robot_models_interface.py @@ -39,6 +39,7 @@ class RobotWithPermissions( 'name', 'password', 'created', + 'last_accessed', 'teams', 'repository_names', 'description', @@ -48,6 +49,7 @@ class RobotWithPermissions( :type name: string :type password: string :type created: datetime|None + :type last_accessed: datetime|None :type teams: [Team] :type repository_names: [string] :type description: string @@ -58,6 +60,7 @@ class RobotWithPermissions( 'name': self.name, 'token': self.password, 'created': format_date(self.created) if self.created is not None else None, + 'last_accessed': format_date(self.last_accessed) if self.last_accessed is not None else None, 'teams': [team.to_dict() for team in self.teams], 'repositories': self.repository_names, 'description': self.description, @@ -69,6 +72,7 @@ class Robot( 'name', 'password', 'created', + 'last_accessed', 'description', 'unstructured_metadata', ])): @@ -77,6 +81,7 @@ class Robot( :type name: string :type password: string :type created: datetime|None + :type last_accessed: datetime|None :type description: string :type unstructured_metadata: dict """ @@ -86,6 +91,7 @@ class Robot( 'name': self.name, 'token': self.password, 'created': format_date(self.created) if self.created is not None else None, + 'last_accessed': format_date(self.last_accessed) if self.last_accessed is not None else None, 'description': self.description, } diff --git a/endpoints/api/robot_models_pre_oci.py b/endpoints/api/robot_models_pre_oci.py index 46ff1f4f3..492455e81 100644 --- a/endpoints/api/robot_models_pre_oci.py +++ b/endpoints/api/robot_models_pre_oci.py @@ -24,6 +24,7 @@ class RobotPreOCIModel(RobotInterface): 'name': robot_name, 'token': robot_tuple.get(FederatedLogin.service_ident), 'created': robot_tuple.get(User.creation_date), + 'last_accessed': robot_tuple.get(User.last_accessed), 'description': robot_tuple.get(RobotAccountMetadata.description), 'unstructured_metadata': robot_tuple.get(RobotAccountMetadata.unstructured_json), } @@ -35,7 +36,8 @@ class RobotPreOCIModel(RobotInterface): }) robots[robot_name] = Robot(robot_dict['name'], robot_dict['token'], robot_dict['created'], - robot_dict['description'], robot_dict['unstructured_metadata']) + robot_dict['last_accessed'], robot_dict['description'], + robot_dict['unstructured_metadata']) if include_permissions: team_name = robot_tuple.get(TeamTable.name) repository_name = robot_tuple.get(Repository.name) @@ -54,7 +56,9 @@ class RobotPreOCIModel(RobotInterface): if repository_name not in robot_dict['repositories']: robot_dict['repositories'].append(repository_name) robots[robot_name] = RobotWithPermissions(robot_dict['name'], robot_dict['token'], - robot_dict['created'], robot_dict['teams'], + robot_dict['created'], + robot_dict['last_accessed'], + robot_dict['teams'], robot_dict['repositories'], robot_dict['description']) @@ -62,14 +66,14 @@ class RobotPreOCIModel(RobotInterface): def regenerate_user_robot_token(self, robot_shortname, owning_user): robot, password, metadata = model.user.regenerate_robot_token(robot_shortname, owning_user) - return Robot(robot.username, password, robot.creation_date, metadata.description, - metadata.unstructured_json) + return Robot(robot.username, password, robot.creation_date, robot.last_accessed, + metadata.description, metadata.unstructured_json) def regenerate_org_robot_token(self, robot_shortname, orgname): parent = model.organization.get_organization(orgname) robot, password, metadata = model.user.regenerate_robot_token(robot_shortname, parent) - return Robot(robot.username, password, robot.creation_date, metadata.description, - metadata.unstructured_json) + return Robot(robot.username, password, robot.creation_date, robot.last_accessed, + metadata.description, metadata.unstructured_json) def delete_robot(self, robot_username): model.user.delete_robot(robot_username) @@ -77,26 +81,26 @@ class RobotPreOCIModel(RobotInterface): def create_user_robot(self, robot_shortname, owning_user, description, unstructured_metadata): robot, password = model.user.create_robot(robot_shortname, owning_user, description or '', unstructured_metadata) - return Robot(robot.username, password, robot.creation_date, description or '', - unstructured_metadata) + return Robot(robot.username, password, robot.creation_date, robot.last_accessed, + description or '', unstructured_metadata) def create_org_robot(self, robot_shortname, orgname, description, unstructured_metadata): parent = model.organization.get_organization(orgname) robot, password = model.user.create_robot(robot_shortname, parent, description or '', unstructured_metadata) - return Robot(robot.username, password, robot.creation_date, description or '', - unstructured_metadata) + return Robot(robot.username, password, robot.creation_date, robot.last_accessed, + description or '', unstructured_metadata) def get_org_robot(self, robot_shortname, orgname): parent = model.organization.get_organization(orgname) robot, password, metadata = model.user.get_robot_and_metadata(robot_shortname, parent) - return Robot(robot.username, password, robot.creation_date, metadata.description, - metadata.unstructured_json) + return Robot(robot.username, password, robot.creation_date, robot.last_accessed, + metadata.description, metadata.unstructured_json) def get_user_robot(self, robot_shortname, owning_user): robot, password, metadata = model.user.get_robot_and_metadata(robot_shortname, owning_user) - return Robot(robot.username, password, robot.creation_date, metadata.description, - metadata.unstructured_json) + return Robot(robot.username, password, robot.creation_date, robot.last_accessed, + metadata.description, metadata.unstructured_json) pre_oci_model = RobotPreOCIModel() diff --git a/static/directives/robots-manager.html b/static/directives/robots-manager.html index 9acbfa5aa..54053d1b2 100644 --- a/static/directives/robots-manager.html +++ b/static/directives/robots-manager.html @@ -44,6 +44,9 @@ Created + + Last Accessed + @@ -89,6 +92,9 @@ + + + diff --git a/static/js/directives/ui/robots-manager.js b/static/js/directives/ui/robots-manager.js index eca191984..bf42eb2d2 100644 --- a/static/js/directives/ui/robots-manager.js +++ b/static/js/directives/ui/robots-manager.js @@ -41,6 +41,7 @@ angular.module('quay').directive('robotsManager', function () { }).join(','); robot['created_datetime'] = robot.created ? TableService.getReversedTimestamp(robot.created) : null; + robot['last_accessed_datetime'] = robot.last_accessed ? TableService.getReversedTimestamp(robot.last_accessed) : null; }); $scope.orderedRobots = TableService.buildOrderedItems(robots, $scope.options, diff --git a/test/data/test.db b/test/data/test.db index 46d05acbb92d62636c98e674703559d5e1155634..20181466964c5c8b63fe2863561a54c15f4472ff 100644 GIT binary patch delta 81311 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 delta 79748 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 diff --git a/util/config/schema.py b/util/config/schema.py index 4cc4a825d..f68a78e73 100644 --- a/util/config/schema.py +++ b/util/config/schema.py @@ -66,6 +66,7 @@ INTERNAL_ONLY_PROPERTIES = { 'LOCAL_OAUTH_HANDLER', 'USE_CDN', 'ANALYTICS_TYPE', + 'LAST_ACCESSED_UPDATE_THRESHOLD_S', 'EXCEPTION_LOG_TYPE', 'SENTRY_DSN',