def test_api_key_already_revoked_by_developer(self): self.key.update(is_active=None) tasks.revoke_api_key(self.key.id) # If the key has already been revoked, there is no active key, # so `get_jwt_key` raises `DoesNotExist`. with pytest.raises(APIKey.DoesNotExist): APIKey.get_jwt_key(user_id=self.user.id)
def test_coauthor_api_key_in_submission_is_found(self): coauthor = user_factory() AddonUser.objects.create(addon=self.addon, user_id=coauthor.id) upload = self.get_upload(abspath=self.file, with_validation=False, addon=self.addon, user=coauthor) tasks.validate(upload, listed=True) upload.refresh_from_db() assert upload.processed_validation['errors'] == 1 messages = upload.processed_validation['messages'] assert len(messages) == 1 assert messages[0]['id'] == [ 'validation', 'messages', 'api_key_detected' ] assert ('The developer API key of a coauthor was found in the ' 'submitted file.' in messages[0]['message']) assert not upload.valid # If the key has been revoked, there is no active key, # so `get_jwt_key` raises `DoesNotExist`. with pytest.raises(APIKey.DoesNotExist): APIKey.get_jwt_key(user_id=self.user.id) assert len(mail.outbox) == 1 assert 'Your AMO API credentials have been revoked' in mail.outbox[ 0].subject assert 'never share your credentials' in mail.outbox[0].body # We submit as the coauthor, the leaked key is the one from 'self.user' assert mail.outbox[0].to[0] == self.user.email
def test_api_key_in_submission_is_found(self): upload = FileUpload.objects.create(path=self.file, addon=self.addon, user=self.user) tasks.validate(upload, listed=True) upload.refresh_from_db() assert upload.processed_validation['errors'] == 1 messages = upload.processed_validation['messages'] assert len(messages) == 1 assert messages[0]['id'] == [ u'validation', u'messages', u'api_key_detected' ] assert ('Your developer API key was found in the submitted ' 'file.' in messages[0]['message']) assert not upload.valid # If the key has been revoked, there is no active key, # so `get_jwt_key` raises `DoesNotExist`. with pytest.raises(APIKey.DoesNotExist): APIKey.get_jwt_key(user_id=self.user.id) assert len(mail.outbox) == 1 assert ('Your AMO API credentials have been revoked' in mail.outbox[0].subject) assert ('never share your credentials' in mail.outbox[0].body) assert mail.outbox[0].to[0] == self.user.email
def test_api_key_already_regenerated_by_developer(self): self.key.update(is_active=None) current_key = APIKey.new_jwt_credentials(user=self.user) tasks.revoke_api_key(self.key.id) key_from_db = APIKey.get_jwt_key(user_id=self.user.id) assert current_key.key == key_from_db.key assert current_key.secret == key_from_db.secret
def test_coauthor_api_key_in_submission_is_found(self): coauthor = user_factory() AddonUser.objects.create(addon=self.addon, user_id=coauthor.id) upload = FileUpload.objects.create(path=self.file, addon=self.addon, user=coauthor) tasks.validate(upload, listed=True) upload.refresh_from_db() assert upload.processed_validation['errors'] == 1 messages = upload.processed_validation['messages'] assert len(messages) == 1 assert messages[0]['id'] == [ u'validation', u'messages', u'api_key_detected'] assert ('The developer API key of a coauthor was found in the ' 'submitted file.' in messages[0]['message']) assert not upload.valid # If the key has been revoked, there is no active key, # so `get_jwt_key` raises `DoesNotExist`. with pytest.raises(APIKey.DoesNotExist): APIKey.get_jwt_key(user_id=self.user.id) assert len(mail.outbox) == 1 assert ('Your AMO API credentials have been revoked' in mail.outbox[0].subject) assert ('never share your credentials' in mail.outbox[0].body) # We submit as the coauthor, the leaked key is the one from 'self.user' assert mail.outbox[0].to[0] == self.user.email
def revoke_api_key(key_id): try: # Fetch the original key, do not use `get_jwt_key` # so we get access to a user object for logging later. original_key = APIKey.objects.get(type=SYMMETRIC_JWT_TYPE, id=key_id) # Fetch the current key to compare to the original, # throws if the key has been revoked, which also means # `original_key` is not active. current_key = APIKey.get_jwt_key(user_id=original_key.user.id) if current_key.key != original_key.key: log.info( 'User %s has already regenerated the key, nothing to be ' 'done.' % original_key.user ) else: with transaction.atomic(): log.info('Revoking key for user %s.' % current_key.user) current_key.update(is_active=None) send_api_key_revocation_email(emails=[current_key.user.email]) except APIKey.DoesNotExist: log.info( 'User %s has already revoked the key, nothing to be done.' % original_key.user ) pass
def check_for_api_keys_in_file(results, upload_pk): upload = FileUpload.objects.get(pk=upload_pk) if upload.addon: users = upload.addon.authors.all() else: users = [upload.user] if upload.user else [] keys = [] for user in users: try: key = APIKey.get_jwt_key(user_id=user.id) keys.append(key) except APIKey.DoesNotExist: pass try: if len(keys) > 0: zipfile = SafeZip(source=upload.path) for zipinfo in zipfile.info_list: if zipinfo.file_size >= 64: file_ = zipfile.read(zipinfo) for key in keys: if key.secret in file_.decode(errors='ignore'): log.info('Developer API key for user %s found in ' 'submission.' % key.user) if key.user == upload.user: msg = gettext('Your developer API key was ' 'found in the submitted file. ' 'To protect your account, the ' 'key will be revoked.') else: msg = gettext('The developer API key of a ' 'coauthor was found in the ' 'submitted file. To protect ' 'your add-on, the key will be ' 'revoked.') annotations.insert_validation_message( results, type_='error', message=msg, msg_id='api_key_detected', compatibility_type=None, ) # Revoke after 2 minutes to allow the developer to # fetch the validation results revoke_api_key.apply_async( kwargs={'key_id': key.id}, countdown=120) zipfile.close() except (ValidationError, BadZipFile, IOError): pass return results
def test_incorrect_signature(self): api_key = self.create_api_key(self.user) token = self.create_auth_token(api_key.user, api_key.key, api_key.secret) decoy_api_key = APIKey( # Don't save in database, it would conflict. user=self.user, key=api_key.key, secret='another-secret') with self.assertRaises(jwt.DecodeError) as ctx: jwt_auth.jwt_decode_handler( token, get_api_key=lambda **k: decoy_api_key) assert str(ctx.exception) == 'Signature verification failed'
def handle(self, *args, **options): revoked_count = 0 with open(options['csv_file']) as csvfile: for idx, (key, secret) in enumerate(csv.reader(csvfile), start=1): try: apikey = APIKey.objects.get(key=key, is_active=True) except APIKey.DoesNotExist: self.stdout.write( f'Ignoring APIKey {key}, it does not exist.\n') continue if apikey.secret != secret: self.stdout.write( f'Ignoring APIKey {key}, secret differs.\n') continue else: with transaction.atomic(): apikey.update(is_active=None) APIKey.new_jwt_credentials(user=apikey.user) revoked_count += 1 self.stdout.write(f'Revoked APIKey {key}.\n') self.stdout.write( f'Done. Revoked {revoked_count} keys out of {idx} entries.')
def handle(self, *args, **options): user_data = {} # Do quick and dirty validation if --noinput if not options.get('interactive', True): # Stolen from django's `createsuperuser` implementation. try: for field_name in self.required_fields: if options.get(field_name, None): field = self.UserModel._meta.get_field(field_name) user_data[field_name] = field.clean(options[field_name], None) else: raise CommandError( 'You must use --%s with --noinput.' % field_name ) except exceptions.ValidationError as exc: raise CommandError('; '.join(exc.messages)) else: user_data = { field_name: self.get_value(field_name) for field_name in self.required_fields } if options.get('fxa_id', None): field = self.UserModel._meta.get_field('fxa_id') user_data['fxa_id'] = field.clean(options['fxa_id'], None) user = get_user_model()._default_manager.create_superuser(**user_data) if options.get('add_to_supercreate_group', False): user.read_dev_agreement = datetime.utcnow() user.save(update_fields=('read_dev_agreement',)) group, _ = Group.objects.get_or_create( rules='Accounts:SuperCreate', defaults={'name': 'Account Super Creators'}, ) GroupUser.objects.create(user=user, group=group) apikey = APIKey.new_jwt_credentials(user=user) self.stdout.write( json.dumps( { 'username': user.username, 'email': user.email, 'api-key': apikey.key, 'api-secret': apikey.secret, 'fxa-id': user.fxa_id, } ) )
def test_api_key_in_new_submission_is_found(self): upload = FileUpload.objects.create(path=self.file, user=self.user) tasks.validate(upload, listed=True) upload.refresh_from_db() assert upload.processed_validation['errors'] == 1 messages = upload.processed_validation['messages'] assert len(messages) == 1 assert messages[0]['id'] == [ u'validation', u'messages', u'api_key_detected'] assert ('Your developer API key was found in the submitted ' 'file.' in messages[0]['message']) assert not upload.valid # If the key has been revoked, there is no active key, # so `get_jwt_key` raises `DoesNotExist`. with pytest.raises(APIKey.DoesNotExist): APIKey.get_jwt_key(user_id=self.user.id) assert len(mail.outbox) == 1 assert ('Your AMO API credentials have been revoked' in mail.outbox[0].subject) assert mail.outbox[0].to[0] == self.user.email
def handle(self, *args, **options): revoked_count = 0 with open(options['csv_file'], 'rb') as csvfile: for idx, (key, secret) in enumerate(csv.reader(csvfile), start=1): try: apikey = APIKey.objects.get(key=key, is_active=True) except APIKey.DoesNotExist: self.stdout.write( 'Ignoring APIKey {}, it does not exist.\n'.format(key)) continue if apikey.secret != secret: self.stdout.write( 'Ignoring APIKey {}, secret differs.\n'.format(key)) continue else: with transaction.atomic(): apikey.update(is_active=None) APIKey.new_jwt_credentials(user=apikey.user) revoked_count += 1 self.stdout.write( 'Revoked APIKey {}.\n'.format(key)) self.stdout.write( 'Done. Revoked {} keys out of {} entries.'.format( revoked_count, idx))
def check_for_api_keys_in_file(results, upload): if upload.addon: users = upload.addon.authors.all() else: users = [upload.user] if upload.user else [] keys = [] for user in users: try: key = APIKey.get_jwt_key(user_id=user.id) keys.append(key) except APIKey.DoesNotExist: pass if len(keys) > 0: zipfile = SafeZip(source=upload.path) zipfile.is_valid() for zipinfo in zipfile.info_list: if zipinfo.file_size >= 64: file_ = zipfile.read(zipinfo) for key in keys: if key.secret in file_.decode(encoding='unicode-escape', errors="ignore"): log.info('Developer API key for user %s found in ' 'submission.' % key.user) if key.user == upload.user: msg = ugettext('Your developer API key was found ' 'in the submitted file. To protect ' 'your account, the key will be ' 'revoked.') else: msg = ugettext('The developer API key of a ' 'coauthor was found in the ' 'submitted file. To protect your ' 'add-on, the key will be revoked.') insert_validation_message( results, type_='error', message=msg, msg_id='api_key_detected', compatibility_type=None) # Revoke after 2 minutes to allow the developer to # fetch the validation results revoke_api_key.apply_async( kwargs={'key_id': key.id}, countdown=120) zipfile.close() return results
def handle(self, *args, **options): user_data = {} # Do quick and dirty validation if --noinput if not options.get('interactive', True): # Stolen from django's `createsuperuser` implementation. try: for field_name in self.required_fields: if options.get(field_name, None): field = self.UserModel._meta.get_field(field_name) user_data[field_name] = field.clean( options[field_name], None) else: raise CommandError( 'You must use --%s with --noinput.' % field_name) except exceptions.ValidationError as exc: raise CommandError('; '.join(exc.messages)) else: user_data = { field_name: self.get_value(field_name) for field_name in self.required_fields } if options.get('fxa_id', None): field = self.UserModel._meta.get_field('fxa_id') user_data['fxa_id'] = field.clean( options['fxa_id'], None) user = get_user_model()._default_manager.create_superuser(**user_data) if options.get('add_to_supercreate_group', False): user.read_dev_agreement = datetime.utcnow() user.save(update_fields=('read_dev_agreement',)) group, _ = Group.objects.get_or_create( rules='Accounts:SuperCreate', defaults={'name': 'Account Super Creators'}) GroupUser.objects.create(user=user, group=group) apikey = APIKey.new_jwt_credentials(user=user) self.stdout.write(json.dumps({ 'username': user.username, 'email': user.email, 'api-key': apikey.key, 'api-secret': apikey.secret, 'fxa-id': user.fxa_id, }))
def test_api_key_does_not_exist(self): user = user_factory() # The test csv does not contain an entry for this user. apikey = APIKey.new_jwt_credentials(user=user) old_secret = apikey.secret stdout = io.StringIO() call_command('revoke_api_keys', self.csv_path, stdout=stdout) stdout.seek(0) output = stdout.readlines() assert output[0] == ('Ignoring APIKey user:12345:666, it does not exist.\n') assert output[1] == ('Ignoring APIKey user:67890:333, it does not exist.\n') # APIKey is still active, secret hasn't changed, there are no # additional APIKeys. apikey.reload() assert apikey.secret == old_secret assert apikey.is_active assert APIKey.objects.filter(user=user).count() == 1
def authenticate_credentials(self, payload): """ Returns a verified AMO user who is active and allowed to make API requests. """ try: api_key = APIKey.get_jwt_key(key=payload['iss']) except APIKey.DoesNotExist: msg = 'Invalid API Key.' raise exceptions.AuthenticationFailed(msg) if api_key.user.deleted: msg = 'User account is disabled.' raise exceptions.AuthenticationFailed(msg) if not api_key.user.read_dev_agreement: msg = 'User has not read developer agreement.' raise exceptions.AuthenticationFailed(msg) return api_key.user
def test_api_key_does_not_exist(self): user = user_factory() # The test csv does not contain an entry for this user. apikey = APIKey.new_jwt_credentials(user=user) old_secret = apikey.secret stdout = StringIO() call_command('revoke_api_keys', self.csv_path, stdout=stdout) stdout.seek(0) output = stdout.readlines() assert output[0] == ( 'Ignoring APIKey user:12345:666, it does not exist.\n') assert output[1] == ( 'Ignoring APIKey user:67890:333, it does not exist.\n') # APIKey is still active, secret hasn't changed, there are no # additional APIKeys. apikey.reload() assert apikey.secret == old_secret assert apikey.is_active assert APIKey.objects.filter(user=user).count() == 1
def revoke_api_key(key_id): try: # Fetch the original key, do not use `get_jwt_key` # so we get access to a user object for logging later. original_key = APIKey.objects.get( type=SYMMETRIC_JWT_TYPE, id=key_id) # Fetch the current key to compare to the original, # throws if the key has been revoked, which also means # `original_key` is not active. current_key = APIKey.get_jwt_key(user_id=original_key.user.id) if current_key.key != original_key.key: log.info('User %s has already regenerated the key, nothing to be ' 'done.' % original_key.user) else: with transaction.atomic(): log.info('Revoking key for user %s.' % current_key.user) current_key.update(is_active=None) send_api_key_revocation_email(emails=[current_key.user.email]) except APIKey.DoesNotExist: log.info('User %s has already revoked the key, nothing to be done.' % original_key.user) pass
def authenticate_credentials(self, payload): """ Returns a verified AMO user who is active and allowed to make API requests. """ if 'orig_iat' in payload: msg = ("API key based tokens are not refreshable, don't include " '`orig_iat` in their payload.') raise exceptions.AuthenticationFailed(msg) try: api_key = APIKey.get_jwt_key(key=payload['iss']) except APIKey.DoesNotExist: msg = 'Invalid API Key.' raise exceptions.AuthenticationFailed(msg) if api_key.user.deleted: msg = 'User account is disabled.' raise exceptions.AuthenticationFailed(msg) if not api_key.user.read_dev_agreement: msg = 'User has not read developer agreement.' raise exceptions.AuthenticationFailed(msg) core.set_user(api_key.user) return api_key.user
def authenticate_credentials(self, payload): """ Returns a verified AMO user who is active and allowed to make API requests. """ if 'orig_iat' in payload: msg = ("API key based tokens are not refreshable, don't include " "`orig_iat` in their payload.") raise exceptions.AuthenticationFailed(msg) try: api_key = APIKey.get_jwt_key(key=payload['iss']) except APIKey.DoesNotExist: msg = 'Invalid API Key.' raise exceptions.AuthenticationFailed(msg) if api_key.user.deleted: msg = 'User account is disabled.' raise exceptions.AuthenticationFailed(msg) if not api_key.user.read_dev_agreement: msg = 'User has not read developer agreement.' raise exceptions.AuthenticationFailed(msg) amo.set_user(api_key.user) return api_key.user
def handle(self, *args, **options): user_data = {} # Do quick and dirty validation if --noinput if not options.get('interactive', True): # Stolen from django's `createsuperuser` implementation. try: for field_name in self.required_fields: if options.get(field_name, None): field = self.UserModel._meta.get_field(field_name) user_data[field_name] = field.clean( options[field_name], None) else: raise CommandError( 'You must use --%s with --noinput.' % field_name) except exceptions.ValidationError as exc: raise CommandError('; '.join(exc.messages)) else: user_data = { field_name: self.get_value(field_name) for field_name in self.required_fields } user = get_user_model()._default_manager.create_superuser(**user_data) if options.get('add_to_supercreate_group', False): user.read_dev_agreement = datetime.utcnow() user.save(update_fields=('read_dev_agreement',)) group, _ = Group.objects.get_or_create( rules='Accounts:SuperCreate', defaults={'name': 'Account Super Creators'}) GroupUser.objects.create(user=user, group=group) apikey = APIKey.new_jwt_credentials(user=user) self.stdout.write(json.dumps({ 'username': user.username, 'email': user.email, 'api-key': apikey.key, 'api-secret': apikey.secret })) if options.get('save_api_credentials', False): hostname = options.get('hostname', os.environ.get( 'PYTEST_BASE_URL', False)) # json object for variables file # set hostname to stdin or env variable if hostname: credentials = { 'api': { hostname: { 'username': user.username, 'jwt_issuer': apikey.key, 'jwt_secret': apikey.secret, } } } # write to json file with open(options.get('save_api_credentials'), 'w') as outfile: json.dump(credentials, outfile, indent=2)
def handle(self, *args, **options): user_data = {} # Do quick and dirty validation if --noinput if not options.get('interactive', True): # Stolen from django's `createsuperuser` implementation. try: for field_name in self.required_fields: if options.get(field_name, None): field = self.UserModel._meta.get_field(field_name) user_data[field_name] = field.clean( options[field_name], None) else: raise CommandError( 'You must use --%s with --noinput.' % field_name) except exceptions.ValidationError as exc: raise CommandError('; '.join(exc.messages)) else: user_data = { field_name: self.get_value(field_name) for field_name in self.required_fields } user = get_user_model()._default_manager.create_superuser(**user_data) if options.get('add_to_supercreate_group', False): user.read_dev_agreement = datetime.utcnow() user.save(update_fields=('read_dev_agreement', )) group, _ = Group.objects.get_or_create( rules='Accounts:SuperCreate', defaults={'name': 'Account Super Creators'}) GroupUser.objects.create(user=user, group=group) apikey = APIKey.new_jwt_credentials(user=user) self.stdout.write( json.dumps({ 'username': user.username, 'email': user.email, 'api-key': apikey.key, 'api-secret': apikey.secret })) if options.get('save_api_credentials', False): hostname = options.get('hostname', os.environ.get('PYTEST_BASE_URL', False)) # json object for variables file # set hostname to stdin or env variable if hostname: credentials = { 'api': { hostname: { 'username': user.username, 'jwt_issuer': apikey.key, 'jwt_secret': apikey.secret, } } } # write to json file with open(options.get('save_api_credentials'), 'w') as outfile: json.dump(credentials, outfile, indent=2)