From 11ff3e9b597af5ed3fff82340c8f464d29a2e6d9 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 1 Apr 2016 13:55:29 -0400 Subject: [PATCH] keys ui WIP --- data/database.py | 10 +- data/model/__init__.py | 3 +- data/model/log.py | 23 +- data/model/service_keys.py | 57 ++-- endpoints/api/superuser.py | 149 ++++++++-- endpoints/key_server.py | 31 ++- external_libraries.py | 9 +- initdb.py | 49 +++- static/css/core-ui.css | 24 ++ static/css/directives/ui/markdown-editor.css | 31 +++ .../directives/ui/service-keys-manager.css | 89 ++++++ static/css/quay.css | 18 +- static/directives/datetime-picker.html | 3 + static/directives/markdown-editor.html | 11 + static/directives/service-keys-manager.html | 260 ++++++++++++++++++ static/js/directives/ui/datetime-picker.js | 51 ++++ static/js/directives/ui/logs-view.js | 28 ++ static/js/directives/ui/markdown-editor.js | 32 +++ .../js/directives/ui/service-keys-manager.js | 229 +++++++++++++++ static/js/pages/superuser.js | 5 + static/js/services/string-builder-service.js | 42 ++- static/partials/super-user.html | 9 + test/data/test.db | Bin 1175552 -> 1175552 bytes test/test_api_security.py | 63 ++++- util/__init__.py | 2 +- 25 files changed, 1154 insertions(+), 74 deletions(-) create mode 100644 static/css/directives/ui/markdown-editor.css create mode 100644 static/css/directives/ui/service-keys-manager.css create mode 100644 static/directives/datetime-picker.html create mode 100644 static/directives/markdown-editor.html create mode 100644 static/directives/service-keys-manager.html create mode 100644 static/js/directives/ui/datetime-picker.js create mode 100644 static/js/directives/ui/markdown-editor.js create mode 100644 static/js/directives/ui/service-keys-manager.js diff --git a/data/database.py b/data/database.py index 6a06d0996..ed1d248ee 100644 --- a/data/database.py +++ b/data/database.py @@ -694,9 +694,11 @@ class LogEntryKind(BaseModel): name = CharField(index=True, unique=True) +_LogEntryAccountProxy = Proxy() + class LogEntry(BaseModel): kind = ForeignKeyField(LogEntryKind, index=True) - account = QuayUserField(index=True, related_name='account') + account = ForeignKeyField(_LogEntryAccountProxy, index=True, null=True, related_name='account') performer = QuayUserField(allows_robots=True, index=True, null=True, related_name='performer', robot_null_delete=True) repository = ForeignKeyField(Repository, index=True, null=True) @@ -715,6 +717,8 @@ class LogEntry(BaseModel): (('repository', 'datetime', 'kind'), False), ) +_LogEntryAccountProxy.initialize(User) + class RepositoryActionCount(BaseModel): repository = ForeignKeyField(Repository, index=True) @@ -875,12 +879,14 @@ class ServiceKeyApprovalType(Enum): SUPERUSER = 'Super User API' KEY_ROTATION = 'Key Rotation' + _ServiceKeyApproverProxy = Proxy() class ServiceKeyApproval(BaseModel): approver = ForeignKeyField(_ServiceKeyApproverProxy, null=True) approval_type = CharField(index=True) approved_date = DateTimeField(default=datetime.utcnow) - notes = TextField() + notes = TextField(default='') + _ServiceKeyApproverProxy.initialize(User) diff --git a/data/model/__init__.py b/data/model/__init__.py index 36a33d0c7..d8f772f45 100644 --- a/data/model/__init__.py +++ b/data/model/__init__.py @@ -103,4 +103,5 @@ config = Config() # moving the minimal number of things to _basequery # TODO document the methods and modules for each one of the submodules below. from data.model import (blob, build, image, log, notification, oauth, organization, permission, - repository, storage, tag, team, token, user, release, modelutil) + repository, service_keys, storage, tag, team, token, user, release, + modelutil) diff --git a/data/model/log.py b/data/model/log.py index 56c029a1e..f13719855 100644 --- a/data/model/log.py +++ b/data/model/log.py @@ -1,5 +1,6 @@ import json +from calendar import timegm from peewee import JOIN_LEFT_OUTER, SQL, fn from datetime import datetime, timedelta, date from cachetools import lru_cache @@ -53,15 +54,33 @@ def get_logs_query(start_time, end_time, performer=None, repository=None, namesp return query +def get_log_action_date(dtdata): + if dtdata is None: + return None + + return + + +def _json_serialize(obj): + if isinstance(obj, datetime): + return timegm(obj.utctimetuple()) + + return obj + + def log_action(kind_name, user_or_organization_name, performer=None, repository=None, ip=None, metadata={}, timestamp=None): if not timestamp: timestamp = datetime.today() + account = None + if user_or_organization_name is not None: + account = User.get(User.username == user_or_organization_name).id + kind = LogEntryKind.get(LogEntryKind.name == kind_name) - account = User.get(User.username == user_or_organization_name) + metadata_json = json.dumps(metadata, default=_json_serialize) LogEntry.create(kind=kind, account=account, performer=performer, - repository=repository, ip=ip, metadata_json=json.dumps(metadata), + repository=repository, ip=ip, metadata_json=metadata_json, datetime=timestamp) diff --git a/data/model/service_keys.py b/data/model/service_keys.py index 5286c0199..7da5bf8d6 100644 --- a/data/model/service_keys.py +++ b/data/model/service_keys.py @@ -1,26 +1,21 @@ from calendar import timegm from datetime import datetime, timedelta - from peewee import JOIN_LEFT_OUTER -from app import app from data.database import db_for_update, User, ServiceKey, ServiceKeyApproval -from data.model import ServiceKeyDoesNotExist, ServiceKeyAlreadyApproved, db_transaction +from data.model import ServiceKeyDoesNotExist, ServiceKeyAlreadyApproved, db_transaction, config from data.model.notification import create_notification, delete_all_notifications_by_path_prefix -# TODO ACTION_LOGS for keys -UNAPPROVED_TTL = timedelta(seconds=app.config['UNAPPROVED_SERVICE_KEY_TTL_SEC']) - - def _expired_keys_clause(service): return ((ServiceKey.service == service) & (ServiceKey.expiration_date <= datetime.utcnow())) def _stale_unapproved_keys_clause(service): + unapproved_ttl = timedelta(seconds=config.app_config['UNAPPROVED_SERVICE_KEY_TTL_SEC']) return ((ServiceKey.service == service) & (ServiceKey.approval >> None) & - (ServiceKey.created_date <= (datetime.utcnow() - UNAPPROVED_TTL))) + (ServiceKey.created_date <= (datetime.utcnow() - unapproved_ttl))) def _gc_expired(service): @@ -36,10 +31,12 @@ def _notify_superusers(key): 'jwk': key.jwk, 'metadata': key.metadata, 'created_date': timegm(key.created_date.utctimetuple()), - 'expiration_date': timegm(key.created_date.utctimetuple()), } - superusers = User.select().where(User.username << app.config['SUPER_USERS']) + if key.expiration_date is not None: + notification_metadata['expiration_date'] = timegm(key.expiration_date.utctimetuple()) + + superusers = User.select().where(User.username << config.app_config['SUPER_USERS']) for superuser in superusers: create_notification('service_key_submitted', superuser, metadata=notification_metadata, lookup_path='/service_key_approval/{0}'.format(key.kid)) @@ -57,9 +54,11 @@ def replace_service_key(old_kid, kid, jwk, metadata, expiration_date): try: with db_transaction(): key = db_for_update(ServiceKey.select().where(ServiceKey.kid == old_kid)).get() - metadata = key.metadata.update(metadata) + key.metadata.update(metadata) + ServiceKey.create(name=key.name, kid=kid, service=key.service, jwk=jwk, - metadata=metadata, expiration_date=expiration_date, approval=key.approval) + metadata=key.metadata, expiration_date=expiration_date, + approval=key.approval) key.delete_instance() except ServiceKey.DoesNotExist: raise ServiceKeyDoesNotExist @@ -69,13 +68,16 @@ def replace_service_key(old_kid, kid, jwk, metadata, expiration_date): _gc_expired(key.service) -def update_service_key(name, kid, metadata, expiration_date): +def update_service_key(kid, name=None, metadata=None): try: with db_transaction(): key = db_for_update(ServiceKey.select().where(ServiceKey.kid == kid)).get() - key.name = name - key.metadata.update(metadata) - key.expiration_date = expiration_date + if name is not None: + key.name = name + + if metadata is not None: + key.metadata.update(metadata) + key.save() except ServiceKey.DoesNotExist: raise ServiceKeyDoesNotExist @@ -83,15 +85,26 @@ def update_service_key(name, kid, metadata, expiration_date): _gc_expired(key.service) -def delete_service_key(service, kid): +def delete_service_key(kid): try: - ServiceKey.delete().where(ServiceKey.service == service, - ServiceKey.kid == kid).execute() + key = ServiceKey.get(kid=kid) + ServiceKey.delete().where(ServiceKey.kid == kid).execute() except ServiceKey.DoesNotExist: - raise ServiceKeyDoesNotExist() + raise ServiceKeyDoesNotExist delete_all_notifications_by_path_prefix('/service_key_approval/{0}'.format(kid)) - _gc_expired(service) + _gc_expired(key.service) + return key + + +def set_key_expiration(kid, expiration_date): + try: + service_key = get_service_key(kid) + except ServiceKey.DoesNotExist: + raise ServiceKeyDoesNotExist + + service_key.expiration_date = expiration_date + service_key.save() def approve_service_key(kid, approver, approval_type, notes=''): @@ -129,7 +142,7 @@ def _list_service_keys_query(kid=None, service=None, approved_only=False): return query -def list_keys(): +def list_all_keys(): return list(_list_service_keys_query()) diff --git a/endpoints/api/superuser.py b/endpoints/api/superuser.py index a0248f68c..0312a27ea 100644 --- a/endpoints/api/superuser.py +++ b/endpoints/api/superuser.py @@ -5,6 +5,7 @@ import logging import os import string +from calendar import timegm from datetime import datetime from hashlib import sha256 from random import SystemRandom @@ -22,7 +23,7 @@ from auth.permissions import SuperUserPermission from endpoints.api import (ApiResource, nickname, resource, validate_json_request, internal_only, require_scope, show_if, parse_args, query_param, abort, require_fresh_login, path_param, verify_not_prod, - page_support) + page_support, log_action) from endpoints.api.logs import get_logs, get_aggregate_logs from data import model from data.database import ServiceKeyApprovalType @@ -148,6 +149,8 @@ def org_view(org): def user_view(user, password=None): user_data = { + 'kind': 'user', + 'name': user.username, 'username': user.username, 'email': user.email, 'verified': user.verified, @@ -506,7 +509,7 @@ class SuperUserServiceKeyManagement(ApiResource): """ Resource for managing service keys.""" schemas = { 'CreateServiceKey': { - 'id': 'PutServiceKey', + 'id': 'CreateServiceKey', 'type': 'object', 'description': 'Description of creation of a service key', 'required': ['service', 'expiration'], @@ -523,9 +526,13 @@ class SuperUserServiceKeyManagement(ApiResource): 'type': 'object', 'description': 'The key/value pairs of this key\'s metadata', }, + 'notes': { + 'type': 'string', + 'description': 'If specified, the extra notes for the key', + }, 'expiration': { 'description': 'The expiration date as a unix timestamp', - 'anyOf': [{'type': 'string'}, {'type': 'null'}], + 'anyOf': [{'type': 'number'}, {'type': 'null'}], }, }, }, @@ -536,46 +543,72 @@ class SuperUserServiceKeyManagement(ApiResource): @require_scope(scopes.SUPERUSER) def get(self): if SuperUserPermission().can(): - keys = model.service_keys.list_keys() - return jsonify({'keys': [key_view(key) for key in keys]}) + keys = model.service_keys.list_all_keys() + + return jsonify({ + 'keys': [key_view(key) for key in keys], + }) + abort(403) + @require_fresh_login @verify_not_prod @nickname('createServiceKey') @require_scope(scopes.SUPERUSER) - @validate_json_request('PutServiceKey') + @validate_json_request('CreateServiceKey') def post(self): if SuperUserPermission().can(): body = request.get_json() + # Ensure we have a valid expiration date if specified. expiration_date = body.get('expiration', None) - if expiration_date == '': - expiration_date = None if expiration_date is not None: try: expiration_date = datetime.utcfromtimestamp(float(expiration_date)) except ValueError: abort(400) - + # Create the metadata for the key. user = get_authenticated_user() metadata = body.get('metadata', {}) metadata.update({ - 'created_by': 'Quay SuperUser Panel', + 'created_by': 'Quay Superuser Panel', 'creator': user.username, 'ip': request.remote_addr, }) + # Generate the private key but *do not save it on the server anywhere*. private_key = RSA.generate(2048) jwk = RSAKey(key=private_key.publickey()).serialize() kid = sha256(json.dumps(canonicalize(jwk), separators=(',', ':'))).hexdigest() + # Create the service key. model.service_keys.create_service_key(body.get('name', ''), kid, body['service'], jwk, metadata, expiration_date) - model.service_keys.approve_service_key(kid, user, ServiceKeyApprovalType.SUPERUSER) - return jsonify({'public_key': private_key.publickey().exportKey('PEM'), - 'private_key': private_key.exportKey('PEM')}) + # Auto-approve the service key. + model.service_keys.approve_service_key(kid, user, ServiceKeyApprovalType.SUPERUSER, + notes=body.get('notes', '')) + + # Log the creation and auto-approval of the service key. + key_log_metadata = { + 'kid': kid, + 'preshared': True, + 'service': body['service'], + 'name': body.get('name', ''), + 'expiration_date': expiration_date, + 'auto_approved': True, + } + + log_action('service_key_create', user.username, key_log_metadata) + log_action('service_key_approve', user.username, key_log_metadata) + + return jsonify({ + 'kid': kid, + 'name': body.get('name', ''), + 'public_key': private_key.publickey().exportKey('PEM'), + 'private_key': private_key.exportKey('PEM'), + }) abort(403) @@ -590,7 +623,6 @@ class SuperUserServiceKeyUpdater(ApiResource): 'id': 'PutServiceKey', 'type': 'object', 'description': 'Description of updates for a service key', - 'required': ['name', 'metadata', 'expiration'], 'properties': { 'name': { 'type': 'string', @@ -602,28 +634,78 @@ class SuperUserServiceKeyUpdater(ApiResource): }, 'expiration': { 'description': 'The expiration date as a unix timestamp', - 'anyOf': [{'type': 'string'}, {'type': 'null'}], + 'anyOf': [{'type': 'number'}, {'type': 'null'}], }, }, }, } + @require_fresh_login @verify_not_prod - @nickname('putServiceKey') + @nickname('updateServiceKey') @require_scope(scopes.SUPERUSER) @validate_json_request('PutServiceKey') def put(self, kid): if SuperUserPermission().can(): body = request.get_json() + try: + key = model.service_keys.get_service_key(kid) + except model.service_keys.ServiceKeyDoesNotExist: + abort(404) - expiration_date = body['expiration'] - if expiration_date is not None and expiration_date != '': - try: - expiration_date = datetime.utcfromtimestamp(float(expiration_date)) - except ValueError: - abort(400) + user = get_authenticated_user() - model.service_keys.update_service_key(body['name'], kid, body['metadata'], expiration_date) + key_log_metadata = { + 'kid': key.kid, + 'service': key.service, + 'name': body.get('name', key.name), + 'expiration_date': key.expiration_date, + } + + if 'expiration' in body: + expiration_date = body['expiration'] + if expiration_date is not None and expiration_date != '': + try: + expiration_date = datetime.utcfromtimestamp(float(expiration_date)) + except ValueError: + abort(400) + + key_log_metadata.update({ + 'old_expiration_date': key.expiration_date, + 'expiration_date': expiration_date, + }) + + log_action('service_key_extend', user.username, key_log_metadata) + model.service_keys.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')) + log_action('service_key_modify', user.username, key_log_metadata) + + return jsonify(key_view(model.service_keys.get_service_key(kid))) + + abort(403) + + @require_fresh_login + @verify_not_prod + @nickname('deleteServiceKey') + @require_scope(scopes.SUPERUSER) + def delete(self, kid): + if SuperUserPermission().can(): + key = model.service_keys.delete_service_key(kid) + + key_log_metadata = { + 'kid': kid, + 'service': key.service, + 'name': key.name, + 'created_date': key.created_date, + 'expiration_date': key.expiration_date, + } + + user = get_authenticated_user() + log_action('service_key_delete', user.username, key_log_metadata) + return make_response('', 201) abort(403) @@ -634,19 +716,36 @@ class SuperUserServiceKeyUpdater(ApiResource): class SuperUserServiceKeyApproval(ApiResource): """ Resource for approving service keys. """ + schemas = { + 'ApproveServiceKey': { + 'id': 'ApproveServiceKey', + 'type': 'object', + 'description': 'Information for approving service keys', + 'properties': { + 'notes': { + 'type': 'string', + 'description': 'Optional approval notes', + }, + }, + }, + } + + @require_fresh_login @verify_not_prod @nickname('approveServiceKey') @require_scope(scopes.SUPERUSER) def put(self, kid): if SuperUserPermission().can(): + notes = request.get_json().get('notes', '') approver = get_authenticated_user() try: - model.service_keys.approve_service_key(kid, approver, ServiceKeyApprovalType.SUPERUSER) + model.service_keys.approve_service_key(kid, approver, ServiceKeyApprovalType.SUPERUSER, + notes=notes) except model.ServiceKeyDoesNotExist: abort(404) except model.ServiceKeyAlreadyApproved: pass - return make_response('', 200) + return make_response('', 201) abort(403) diff --git a/endpoints/key_server.py b/endpoints/key_server.py index de029c489..8b7ca1596 100644 --- a/endpoints/key_server.py +++ b/endpoints/key_server.py @@ -12,6 +12,7 @@ from jwkest.jwk import keyrep, RSAKey, ECKey import data.model import data.model.service_keys +from data.model.log import log_action from app import app from auth.registry_jwt_auth import TOKEN_REGEX @@ -59,13 +60,14 @@ def _validate_jwt(encoded_jwt, jwk, service): strictjwt.decode(encoded_jwt, public_key, algorithms=['RS256'], audience=JWT_AUDIENCE, issuer=service) except strictjwt.InvalidTokenError: + logger.exception('JWT validation failure') abort(400) def _signer_kid(encoded_jwt): decoded_jwt = jwt.decode(encoded_jwt, verify=False) logger.debug(decoded_jwt) - return decoded_jwt.get('signer_kid', None) + return decoded_jwt.get('kid', None) def _signer_key(service, signer_kid): @@ -104,6 +106,7 @@ def put_service_key(service, kid): try: expiration_date = datetime.utcfromtimestamp(float(expiration_date)) except ValueError: + logger.exception('Error parsing expiration date on key') abort(400) rotation_ttl = request.args.get('rotation', None) @@ -113,6 +116,7 @@ def put_service_key(service, kid): try: jwk = request.get_json() except ValueError: + logger.exception('Error parsing JWK') abort(400) logger.debug(jwk) @@ -120,7 +124,9 @@ def put_service_key(service, kid): jwt_header = request.headers.get(JWT_HEADER_NAME, '') match = TOKEN_REGEX.match(jwt_header) if match is None: + logger.error('Could not find matching bearer token') abort(400) + encoded_jwt = match.group(1) _validate_jwk(jwk, kid) @@ -131,6 +137,18 @@ def put_service_key(service, kid): # The key is self-signed. Create a new instance and await approval. _validate_jwt(encoded_jwt, jwk, service) data.model.service_keys.create_service_key('', kid, service, jwk, metadata, expiration_date) + + key_log_metadata = { + 'kid': kid, + 'preshared': False, + 'service': service, + 'name': '', + 'expiration_date': expiration_date, + 'user_agent': request.headers.get('User-Agent'), + 'ip': request.remote_addr, + } + + log_action('service_key_create', None, metadata=key_log_metadata, ip=request.remote_addr) return make_response('', 202) metadata.update({'created_by': 'Key Rotation'}) @@ -146,6 +164,17 @@ def put_service_key(service, kid): except data.model.ServiceKeyDoesNotExist: abort(404) + key_log_metadata = { + 'kid': kid, + 'signer_kid': signer_key.kid, + 'service': service, + 'name': signer_key.name, + 'expiration_date': expiration_date, + 'user_agent': request.headers.get('User-Agent'), + 'ip': request.remote_addr, + } + + log_action('service_key_rotate', None, metadata=key_log_metadata, ip=request.remote_addr) return make_response('', 200) diff --git a/external_libraries.py b/external_libraries.py index 5cbf53f99..45d7f7059 100644 --- a/external_libraries.py +++ b/external_libraries.py @@ -11,16 +11,19 @@ EXTERNAL_JS = [ 'ajax.googleapis.com/ajax/libs/angularjs/1.2.9/angular-route.min.js', 'ajax.googleapis.com/ajax/libs/angularjs/1.2.9/angular-sanitize.min.js', 'ajax.googleapis.com/ajax/libs/angularjs/1.2.9/angular-animate.min.js', + 'cdn.jsdelivr.net/g/momentjs', 'cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.2.0/js/bootstrap-datepicker.min.js', - 'cdn.jsdelivr.net/g/bootbox@4.1.0,underscorejs@1.5.2,restangular@1.2.0,d3js@3.3.3,momentjs', + 'cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.17.37/js/bootstrap-datetimepicker.min.js', + 'cdn.jsdelivr.net/g/bootbox@4.1.0,underscorejs@1.5.2,restangular@1.2.0,d3js@3.3.3', 'cdn.ravenjs.com/1.1.14/jquery,native/raven.min.js', ] EXTERNAL_CSS = [ 'netdna.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.css', 'netdna.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css', - 'fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700', - 's3.amazonaws.com/cdn.core-os.net/icons/core-icons.css' + 'fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,700', + 's3.amazonaws.com/cdn.core-os.net/icons/core-icons.css', + 'cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.17.37/css/bootstrap-datetimepicker.min.css', ] EXTERNAL_FONTS = [ diff --git a/initdb.py b/initdb.py index abc06d866..0f032fe45 100644 --- a/initdb.py +++ b/initdb.py @@ -12,18 +12,24 @@ from peewee import (SqliteDatabase, create_model_tables, drop_model_tables, save from itertools import count from uuid import UUID, uuid4 from threading import Event +from hashlib import sha256 +from Crypto.PublicKey import RSA +from jwkest.jwk import RSAKey from email.utils import formatdate from data.database import (db, all_models, Role, TeamRole, Visibility, LoginService, BuildTriggerService, AccessTokenKind, LogEntryKind, ImageStorageLocation, ImageStorageTransformation, ImageStorageSignatureKind, ExternalNotificationEvent, ExternalNotificationMethod, NotificationKind, - QuayRegion, QuayService, UserRegion, OAuthAuthorizationCode) + QuayRegion, QuayService, UserRegion, OAuthAuthorizationCode, + ServiceKeyApprovalType) from data import model from data.queue import WorkQueue from app import app, storage as store, tf from storage.basestorage import StoragePaths from endpoints.v2.manifest import _generate_and_store_manifest +from util import canonicalize + from workers import repositoryactioncounter @@ -150,6 +156,32 @@ def __create_subtree(with_storage, repo, structure, creator_username, parent, ta __create_subtree(with_storage, repo, subtree, creator_username, new_image, tag_map) +def __generate_service_key(name, user, timestamp, approval_type, expiration=None, metadata=None): + private_key = RSA.generate(1024) + jwk = RSAKey(key=private_key.publickey()).serialize() + kid = sha256(json.dumps(canonicalize(jwk), separators=(',', ':'))).hexdigest() + + metadata = metadata or {} + model.service_keys.create_service_key(name, kid, 'sample_service', jwk, metadata, expiration) + model.service_keys.approve_service_key(kid, user, approval_type, + notes='The **test** apporval') + + key_metadata = { + 'kid': kid, + 'preshared': True, + 'service': 'sample_service', + 'name': name, + 'expiration_date': expiration, + 'auto_approved': True + } + + model.log.log_action('service_key_approve', None, performer=user, + timestamp=timestamp, metadata=key_metadata) + + model.log.log_action('service_key_create', None, performer=user, + timestamp=timestamp, metadata=key_metadata) + + def __generate_repository(with_storage, user_obj, name, description, is_public, permissions, structure): repo = model.repository.create_repository(user_obj.username, name, user_obj) @@ -305,6 +337,13 @@ def initialize_database(): LogEntryKind.create(name='repo_verb') + LogEntryKind.create(name='service_key_create') + LogEntryKind.create(name='service_key_approve') + LogEntryKind.create(name='service_key_delete') + LogEntryKind.create(name='service_key_modify') + LogEntryKind.create(name='service_key_extend') + LogEntryKind.create(name='service_key_rotate') + ImageStorageLocation.create(name='local_eu') ImageStorageLocation.create(name='local_us') @@ -614,6 +653,14 @@ def populate_database(minimal=False, with_storage=False): six_ago = today - timedelta(5) four_ago = today - timedelta(4) + __generate_service_key('somesamplekey', new_user_1, today, ServiceKeyApprovalType.SUPERUSER) + __generate_service_key('someexpiringkey', new_user_1, week_ago, ServiceKeyApprovalType.SUPERUSER, + today + timedelta(14)) + + __generate_service_key('autorotatingkey', new_user_1, six_ago, + ServiceKeyApprovalType.KEY_ROTATION, today + timedelta(1), + dict(rotation_ttl=timedelta(hours=12).total_seconds())) + model.log.log_action('org_create_team', org.username, performer=new_user_1, timestamp=week_ago, metadata={'team': 'readers'}) diff --git a/static/css/core-ui.css b/static/css/core-ui.css index 3fd80f895..8278eb438 100644 --- a/static/css/core-ui.css +++ b/static/css/core-ui.css @@ -55,6 +55,30 @@ a:focus { outline: none !important; } +.co-form-table label { + white-space: nowrap; +} + +.co-form-table td { + padding: 8px; +} + +.co-form-table td:first-child { + vertical-align: top; + padding-top: 14px; +} + +.co-form-table td .co-help-text { + margin-top: 10px; + margin-bottom: 4px; +} + +.co-help-text { + margin-top: 6px; + color: #aaa; + display: inline-block; +} + .co-options-menu .fa-gear { color: #999; cursor: pointer; diff --git a/static/css/directives/ui/markdown-editor.css b/static/css/directives/ui/markdown-editor.css new file mode 100644 index 000000000..bf5602db7 --- /dev/null +++ b/static/css/directives/ui/markdown-editor.css @@ -0,0 +1,31 @@ +.markdown-editor-element .wmd-panel .btn { + background-color: #ddd; +} + +.markdown-editor-element .wmd-panel .btn:hover { + background-color: #eee; +} + +.markdown-editor-element .wmd-panel .btn:active { + background-color: #ccc; +} + +.markdown-editor-element .preview-btn { + float: right; +} + +.markdown-editor-element .preview-btn.active { + box-shadow: inset 0 3px 5px rgba(0,0,0,.125); +} + +.markdown-editor-element .preview-panel .markdown-view { + border: 1px solid #eee; + padding: 4px; + min-height: 150px; +} + +.markdown-editor-element .preview-top-bar { + height: 43px; + line-height: 43px; + color: #ddd; +} \ No newline at end of file diff --git a/static/css/directives/ui/service-keys-manager.css b/static/css/directives/ui/service-keys-manager.css new file mode 100644 index 000000000..2574cb1ba --- /dev/null +++ b/static/css/directives/ui/service-keys-manager.css @@ -0,0 +1,89 @@ +.service-keys-manager-element .co-filter-box { + float: right; +} + +.service-keys-manager-element .manager-header { + margin-bottom: 20px; +} + +@media (max-width: 767px) { + .service-keys-manager-element .co-filter-box { + float: none; + display: block; + } +} + +.service-keys-manager-element .approval-user .pretext { + vertical-align: middle; + margin-right: 4px; + font-size: 12px; + color: #777; +} + +.service-keys-manager-element .expired a { + color: #D64456; +} + +.service-keys-manager-element .critical a { + color: #F77454; +} + +.service-keys-manager-element .warning a { + color: #FCA657; +} + +.service-keys-manager-element .info a { + color: #2FC98E; +} + +.service-keys-manager-element .rotation { + color: #777; +} + +.service-keys-manager-element .no-expiration { + color: #145884; +} + +.service-keys-manager-element i.fa { + margin-right: 4px; +} + +.service-keys-manager-element .approval-rotation { + font-size: 12px; + color: #777; +} + +.service-keys-manager-element .approval-rotation i.fa { + margin-right: 6px; +} + +.service-keys-manager-element .subtitle { + color: #999; + font-size: 90%; + text-transform: uppercase; + font-weight: 300; + padding-top: 0!important; + text-align: left; + margin-bottom: 6px; + margin-top: 10px; +} + +.service-keys-manager-element .approval-required i.fa { + margin-right: 4px; +} + +.service-keys-manager-element .approval-required a { + margin-left: 10px; +} + +.service-keys-manager-element .unnamed { + color: #ddd; +} + +.service-keys-manager-element .key-display { + margin-top: 10px; + font-size: 12px; + font-family: Menlo,Monaco,Consolas,"Courier New",monospace; + background: white; + min-height: 500px; +} \ No newline at end of file diff --git a/static/css/quay.css b/static/css/quay.css index f2095b9e7..e6c8b8c66 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -1382,7 +1382,6 @@ p.editable:hover i { .modal-body textarea { width: 100%; height: 150px; - border: 0px; } .tag-specific-images-view .image-listings { @@ -4034,13 +4033,28 @@ i.rocket-icon { text-align: center; } - .section-description-header { +.section-description-header { position: relative; margin-bottom: 10px; min-height: 50px; padding-bottom: 10px; } +.section-description-header.twenty { + margin-top: -20px; +} + +.section-description-header:before { + font-family: FontAwesome; + content: "\f05a"; + position: absolute; + top: 50%; + left: 10px; + font-size: 22px; + color: #D2D2D2; + transform: translateY(-50%); +} + .nvtooltip h3 { margin: 0; padding: 4px 14px; diff --git a/static/directives/datetime-picker.html b/static/directives/datetime-picker.html new file mode 100644 index 000000000..25f57d701 --- /dev/null +++ b/static/directives/datetime-picker.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/static/directives/markdown-editor.html b/static/directives/markdown-editor.html new file mode 100644 index 000000000..4f838dffe --- /dev/null +++ b/static/directives/markdown-editor.html @@ -0,0 +1,11 @@ +
+ Preview +
+
+ +
+
+
Viewing preview
+
+
+
\ No newline at end of file diff --git a/static/directives/service-keys-manager.html b/static/directives/service-keys-manager.html new file mode 100644 index 000000000..5f2c836c5 --- /dev/null +++ b/static/directives/service-keys-manager.html @@ -0,0 +1,260 @@ +
+
+
+ +
+ +
+ Service keys provide a recognized means of authentication between Quay Enterprise and external services, as well as between external services.
Example services include Quay Security Scanner speaking to a Clair cluster, or Quay Enterprise speaking to its + build workers. +
+ + + + Showing {{ orderedKeys.entries.length }} of {{ keys.length }} keys + + + + + +
+
No service keys defined
+
There are no keys defined for working with external services
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ Name + + Service Name + + Created + + Expires + + Approval Status +
+ + + + + + (Unnamed) + + + + + + Automatically rotated + + + + + + Expires + + + + + Does not expire + + + + Approved by + + + Approved via key rotation + + + + Awaiting Approval Approve Now + + + + + Set Friendly Name + + + Change Expiration Time + + + Approve Key + + + Delete Key + + +
+
Full Key ID
+ + +
+
Approval notes
+
+
+
+ +
+
No matching keys found.
+
Try expanding your filtering terms.
+
+
+ + + +
+
+ + + If specified, the date and time that the key expires. It is highly recommended to have an expiration date. + +
+
+ + +
+ Are you sure you want to delete service key {{ getKeyTitle(deleteKeyInfo.key) }}?

+ All external services that use this key for authentication will fail. +
+ + +
+
+
+ Approve service key {{ getKeyTitle(approvalKeyInfo.key) }}? +
+
+ + Enter optional notes for additional human-readable information about why the key was approved. + +
+
+ + + + + + +
\ No newline at end of file diff --git a/static/js/directives/ui/datetime-picker.js b/static/js/directives/ui/datetime-picker.js new file mode 100644 index 000000000..5c3822f49 --- /dev/null +++ b/static/js/directives/ui/datetime-picker.js @@ -0,0 +1,51 @@ +/** + * An element which displays a datetime picker. + */ +angular.module('quay').directive('datetimePicker', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/datetime-picker.html', + replace: false, + transclude: true, + restrict: 'C', + scope: { + 'datetime': '=datetime', + }, + controller: function($scope, $element) { + $scope.entered_datetime = null; + + $(function() { + $element.find('input').datetimepicker({ + 'format': 'LLL', + 'sideBySide': true, + 'showClear': true + }); + + $element.find('input').on("dp.change", function (e) { + $scope.datetime = e.date ? e.date.unix() : null; + }); + }); + + $scope.$watch('entered_datetime', function(value) { + if (!value) { + if ($scope.datetime) { + $scope.datetime = null; + } + return; + } + + $scope.datetime = (new Date(value)).getTime()/1000; + }); + + $scope.$watch('datetime', function(value) { + if (!value) { + $scope.entered_datetime = null; + return; + } + + $scope.entered_datetime = moment.unix(value).format('LLL'); + }); + } + }; + return directiveDefinitionObject; +}); \ No newline at end of file diff --git a/static/js/directives/ui/logs-view.js b/static/js/directives/ui/logs-view.js index 3bae2d67e..d791c1584 100644 --- a/static/js/directives/ui/logs-view.js +++ b/static/js/directives/ui/logs-view.js @@ -37,6 +37,14 @@ angular.module('quay').directive('logsView', function () { return ''; }; + var getServiceKeyTitle = function(metadata) { + if (metadata.name) { + return metadata.name; + } + + return metadata.kind.substr(0, 12); + }; + var logDescriptions = { 'account_change_plan': 'Change plan', 'account_change_cc': 'Update credit card', @@ -195,6 +203,20 @@ angular.module('quay').directive('logsView', function () { 'regenerate_robot_token': 'Regenerated token for robot {robot}', + 'service_key_create': function(metadata) { + if (metadata.preshared) { + return 'Manual creation of preshared service key {kid} for service {service}'; + } else { + return 'Creation of service key {kid} for service {service} by {user_agent}'; + } + }, + + 'service_key_approve': 'Approval of service key {kid}', + 'service_key_modify': 'Modification of service key {kid}', + 'service_key_delete': 'Deletion of service key {kid}', + 'service_key_extend': 'Change of expiration of service key {kid} from {old_expiration_date} to {expiration_date}', + 'service_key_rotate': 'Automatic rotation of service key {kid} by {user_agent}', + // Note: These are deprecated. 'add_repo_webhook': 'Add webhook in repository {repo}', 'delete_repo_webhook': 'Delete webhook in repository {repo}' @@ -245,6 +267,12 @@ angular.module('quay').directive('logsView', function () { 'add_repo_notification': 'Add repository notification', 'delete_repo_notification': 'Delete repository notification', 'regenerate_robot_token': 'Regenerate Robot Token', + 'service_key_create': 'Create Service Key', + 'service_key_approve': 'Approve Service Key', + 'service_key_modify': 'Modify Service Key', + 'service_key_delete': 'Delete Service Key', + 'service_key_extend': 'Extend Service Key Expiration', + 'service_key_rotate': 'Automatic rotation of Service Key', // Note: these are deprecated. 'add_repo_webhook': 'Add webhook', diff --git a/static/js/directives/ui/markdown-editor.js b/static/js/directives/ui/markdown-editor.js new file mode 100644 index 000000000..e68b36a3a --- /dev/null +++ b/static/js/directives/ui/markdown-editor.js @@ -0,0 +1,32 @@ +/** + * An element which display an inline editor for writing and previewing markdown text. + */ +angular.module('quay').directive('markdownEditor', function () { + var counter = 0; + + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/markdown-editor.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'content': '=content', + }, + controller: function($scope, $element, $timeout) { + $scope.id = (counter++); + $scope.previewing = false; + + $timeout(function() { + var converter = Markdown.getSanitizingConverter(); + var editor = new Markdown.Editor(converter, '-' + $scope.id); + editor.run(); + }); + + $scope.togglePreview = function() { + $scope.previewing = !$scope.previewing; + }; + } + }; + return directiveDefinitionObject; +}); diff --git a/static/js/directives/ui/service-keys-manager.js b/static/js/directives/ui/service-keys-manager.js new file mode 100644 index 000000000..7e4e58ab7 --- /dev/null +++ b/static/js/directives/ui/service-keys-manager.js @@ -0,0 +1,229 @@ +/** + * An element which displays a panel for managing keys for external services. + */ +angular.module('quay').directive('serviceKeysManager', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/service-keys-manager.html', + replace: false, + transclude: true, + restrict: 'C', + scope: { + 'isEnabled': '=isEnabled' + }, + controller: function($scope, $element, ApiService, TableService) { + $scope.options = { + 'filter': null, + 'predicate': 'expiration_datetime', + 'reverse': false, + }; + + $scope.TableService = TableService; + $scope.newKey = null; + $scope.creatingKey = false; + $scope.context = { + 'expirationChangeInfo': null + }; + + var buildOrderedKeys = function() { + if (!$scope.keys) { + return; + } + + var keys = $scope.keys.map(function(key) { + var expiration_datetime = -Number.MAX_VALUE; + if (key.expiration_date) { + expiration_datetime = new Date(key.expiration_date).valueOf() * (-1); + } + + return $.extend(key, { + 'creation_datetime': new Date(key.creation_date).valueOf() * (-1), + 'expiration_datetime': expiration_datetime, + 'expanded': false + }); + }); + + $scope.orderedKeys = TableService.buildOrderedItems(keys, $scope.options, + ['name', 'kid', 'service'], + ['creation_datetime', 'expiration_datetime']) + }; + + var loadServiceKeys = function() { + $scope.options.filter = null; + $scope.now = new Date(); + $scope.keysResource = ApiService.getServiceKeysAsResource().get(function(resp) { + $scope.keys = resp['keys']; + buildOrderedKeys(); + }); + }; + + $scope.getKeyTitle = function(key) { + if (!key) { return ''; } + return key.name || key.kid.substr(0, 12); + }; + + $scope.toggleDetails = function(key) { + key.expanded = !key.expanded; + }; + + $scope.getExpirationInfo = function(key) { + if (!key.expiration_date) { + return ''; + } + + if (key.metadata.rotation_ttl) { + var rotate_date = moment(key.created_date).add(key.metadata.rotation_ttl, 's') + if (moment().isBefore(rotate_date)) { + return {'className': 'rotation', 'icon': 'fa-refresh', 'rotateDate': rotate_date}; + } + } + + expiration_date = moment(key.expiration_date); + if (moment().isAfter(expiration_date)) { + return {'className': 'expired', 'icon': 'fa-warning'}; + } + + if (moment().add(1, 'week').isAfter(expiration_date)) { + return {'className': 'critical', 'icon': 'fa-warning'}; + } + + if (moment().add(1, 'month').isAfter(expiration_date)) { + return {'className': 'warning', 'icon': 'fa-warning'}; + } + + return {'className': 'info', 'icon': 'fa-info-circle'}; + }; + + $scope.showChangeName = function(key) { + bootbox.prompt('Enter a friendly name for key ' + $scope.getKeyTitle(key), function(value) { + if (value) { + var data = { + 'name': value + }; + + var params = { + 'kid': key.kid + }; + + ApiService.updateServiceKey(data, params).then(function(resp) { + loadServiceKeys(); + }, ApiService.errorDisplay('Could not update service key')); + } + }); + }; + + $scope.showChangeExpiration = function(key) { + $scope.context.expirationChangeInfo = { + 'key': key, + 'expiration_date': key.expiration_date ? (new Date(key.expiration_date).getTime() / 1000) : null + }; + }; + + $scope.changeKeyExpiration = function(changeInfo, callback) { + var errorHandler = ApiService.errorDisplay('Could not change expiration on service key', callback); + + var data = { + 'expiration': changeInfo.expiration_date + }; + + var params = { + 'kid': changeInfo.key.kid + }; + + ApiService.updateServiceKey(data, params).then(function(resp) { + loadServiceKeys(); + callback(true); + }, errorHandler); + }; + + $scope.createServiceKey = function() { + $scope.creatingKey = true; + ApiService.createServiceKey($scope.newKey).then(function(resp) { + $scope.creatingKey = false; + $('#createKeyModal').modal('hide'); + $scope.createdKey = resp; + $('#createdKeyModal').modal('show'); + loadServiceKeys(); + }, ApiService.errorDisplay('Could not create service key')); + }; + + $scope.showApproveKey = function(key) { + $scope.approvalKeyInfo = { + 'key': key, + 'notes': '' + }; + }; + + $scope.approveKey = function(approvalKeyInfo, callback) { + var errorHandler = ApiService.errorDisplay('Could not approve service key', callback); + + var data = { + 'notes': approvalKeyInfo.notes + }; + + var params = { + 'kid': approvalKeyInfo.key.kid + }; + + ApiService.approveServiceKey(data, params).then(function(resp) { + loadServiceKeys(); + callback(true); + }, errorHandler); + }; + + $scope.showCreateKey = function() { + $scope.newKey = { + 'expiration': null + }; + + $('#createKeyModal').modal('show'); + }; + + $scope.showDeleteKey = function(key) { + $scope.deleteKeyInfo = { + 'key': key + }; + }; + + $scope.deleteKey = function(deleteKeyInfo, callback) { + var errorHandler = ApiService.errorDisplay('Could not delete service key', callback); + + var params = { + 'kid': deleteKeyInfo.key.kid + }; + + ApiService.deleteServiceKey(null, params).then(function(resp) { + loadServiceKeys(); + callback(true); + }, errorHandler); + }; + + $scope.isDownloadSupported = function() { + var isSafari = /^((?!chrome).)*safari/i.test(navigator.userAgent); + if (isSafari) { + // Doesn't work properly in Safari, sadly. + return false; + } + + try { return !!new Blob(); } catch(e) {} + return false; + }; + + $scope.downloadPrivateKey = function(key) { + var blob = new Blob([key.private_key]); + saveAs(blob, $scope.getKeyTitle(key) + '.pem'); + }; + + $scope.$watch('options.filter', buildOrderedKeys); + $scope.$watch('options.predicate', buildOrderedKeys); + $scope.$watch('options.reverse', buildOrderedKeys); + + $scope.$watch('isEnabled', function(value) { + if (value) { + loadServiceKeys(); + } + }); + } + }; + return directiveDefinitionObject; +}); \ No newline at end of file diff --git a/static/js/pages/superuser.js b/static/js/pages/superuser.js index 740481b81..933c384ac 100644 --- a/static/js/pages/superuser.js +++ b/static/js/pages/superuser.js @@ -31,6 +31,7 @@ $scope.csrf_token = encodeURIComponent(window.__token); $scope.dashboardActive = false; $scope.currentConfig = null; + $scope.serviceKeysActive = false; $scope.setDashboardActive = function(active) { $scope.dashboardActive = active; @@ -46,6 +47,10 @@ $('#createUserModal').modal('show'); }; + $scope.loadServiceKeys = function() { + $scope.serviceKeysActive = true; + }; + $scope.viewSystemLogs = function(service) { if ($scope.pollChannel) { $scope.pollChannel.stop(); diff --git a/static/js/services/string-builder-service.js b/static/js/services/string-builder-service.js index 44251185e..7cec9f10e 100644 --- a/static/js/services/string-builder-service.js +++ b/static/js/services/string-builder-service.js @@ -25,6 +25,34 @@ angular.module('quay').factory('StringBuilderService', ['$sce', 'UtilService', f 'client_id': 'chain' }; + var filters = { + 'obj': function(value) { + if (!value) { return []; } + return Object.getOwnPropertyNames(value); + }, + + 'updated_tags': function(value) { + if (!value) { return []; } + return Object.getOwnPropertyNames(value); + }, + + 'kid': function(kid, metadata) { + if (metadata.name) { + return metadata.name; + } + + return metadata.kid.substr(0, 12); + }, + + 'expiration_date': function(value) { + return moment.unix(value).format('LLL'); + }, + + 'old_expiration_date': function(value) { + return moment.unix(value).format('LLL'); + } + }; + stringBuilderService.buildUrl = function(value_or_func, metadata) { var url = value_or_func; if (typeof url != 'string') { @@ -105,18 +133,6 @@ angular.module('quay').factory('StringBuilderService', ['$sce', 'UtilService', f } stringBuilderService.buildString = function(value_or_func, metadata, opt_codetag) { - var filters = { - 'obj': function(value) { - if (!value) { return []; } - return Object.getOwnPropertyNames(value); - }, - - 'updated_tags': function(value) { - if (!value) { return []; } - return Object.getOwnPropertyNames(value); - } - }; - var description = value_or_func; if (typeof description != 'string') { description = description(metadata); @@ -126,7 +142,7 @@ angular.module('quay').factory('StringBuilderService', ['$sce', 'UtilService', f if (metadata.hasOwnProperty(key)) { var value = metadata[key] != null ? metadata[key] : '(Unknown)'; if (filters[key]) { - value = filters[key](value); + value = filters[key](value, metadata); } description = stringBuilderService.replaceField(description, '', key, value, opt_codetag); diff --git a/static/partials/super-user.html b/static/partials/super-user.html index d124222b7..09c58dcee 100644 --- a/static/partials/super-user.html +++ b/static/partials/super-user.html @@ -24,6 +24,10 @@ tab-target="#organizations" tab-init="loadOrganizations()"> + + + @@ -50,6 +54,11 @@ configuration-saved="configurationSaved(config)"> + +
+
+
+
diff --git a/test/data/test.db b/test/data/test.db index 56a7cfb6925d6d280cf0c5b4fa39b1b9bfae8a35..dfdb696900ab77d98cb8e44df75ac0ef7e048aac 100644 GIT binary patch delta 64049 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

u*7ph<(b!++W`EI`{U#H1hgx!0N{JCG@oyn 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 F{||zdiP8W7 diff --git a/test/test_api_security.py b/test/test_api_security.py index c785aaee4..e6385fd92 100644 --- a/test/test_api_security.py +++ b/test/test_api_security.py @@ -48,7 +48,8 @@ from endpoints.api.permission import (RepositoryUserPermission, RepositoryTeamPe from endpoints.api.superuser import (SuperUserLogs, SuperUserList, SuperUserManagement, SuperUserSendRecoveryEmail, ChangeLog, SuperUserOrganizationManagement, SuperUserOrganizationList, - SuperUserAggregateLogs) + SuperUserAggregateLogs, SuperUserServiceKeyManagement, + SuperUserServiceKeyUpdater, SuperUserServiceKeyApproval) from endpoints.api.secscan import RepositoryImageSecurity @@ -3911,6 +3912,66 @@ class TestSuperUserSendRecoveryEmail(ApiTestCase): self._run_test('POST', 404, 'devtable', None) +class TestSuperUserServiceKeyManagement(ApiTestCase): + def setUp(self): + ApiTestCase.setUp(self) + self._set_url(SuperUserServiceKeyManagement) + + def test_get_anonymous(self): + self._run_test('GET', 403, None, None) + + def test_get_freshuser(self): + self._run_test('GET', 403, 'freshuser', None) + + def test_get_reader(self): + self._run_test('GET', 403, 'reader', None) + + def test_get_devtable(self): + self._run_test('GET', 200, 'devtable', None) + + def test_post_anonymous(self): + self._run_test('POST', 401, None, dict(service='someservice', expiration=None)) + + def test_post_freshuser(self): + self._run_test('POST', 403, 'freshuser', dict(service='someservice', expiration=None)) + + def test_post_reader(self): + self._run_test('POST', 403, 'reader', dict(service='someservice', expiration=None)) + + def test_post_devtable(self): + self._run_test('POST', 200, 'devtable', dict(service='someservice', expiration=None)) + + +class TestSuperUserServiceKeyUpdater(ApiTestCase): + def setUp(self): + ApiTestCase.setUp(self) + self._set_url(SuperUserServiceKeyUpdater, kid=1234) + + def test_delete_anonymous(self): + self._run_test('DELETE', 401, None, None) + + def test_delete_freshuser(self): + self._run_test('DELETE', 403, 'freshuser', None) + + def test_delete_reader(self): + self._run_test('DELETE', 403, 'reader', None) + + def test_delete_devtable(self): + self._run_test('DELETE', 400, 'devtable', None) + + def test_put_anonymous(self): + self._run_test('PUT', 401, None, {}) + + def test_put_freshuser(self): + self._run_test('PUT', 403, 'freshuser', {}) + + def test_put_reader(self): + self._run_test('PUT', 403, 'reader', {}) + + def test_put_devtable(self): + self._run_test('PUT', 404, 'devtable', {}) + + class TestTeamMemberInvite(ApiTestCase): def setUp(self): ApiTestCase.setUp(self) diff --git a/util/__init__.py b/util/__init__.py index e30fafb53..3b0d911d9 100644 --- a/util/__init__.py +++ b/util/__init__.py @@ -24,7 +24,7 @@ def slash_join(*args): def canonicalize(json_obj): """ Returns a JSON object sorted by key. """ if isinstance(json_obj, collections.MutableMapping): - sorted_obj = sorted({key: canonicalize(val) for key, val in json_obj}.items()) + sorted_obj = sorted({key: canonicalize(val) for key, val in json_obj.items()}.items()) return collections.OrderedDict(sorted_obj) elif isinstance(json_obj, (list, tuple)): return [canonicalize(val) for val in json_obj]