Skip to content

Commit

Permalink
Merge branch 'master' into svn
Browse files Browse the repository at this point in the history
  • Loading branch information
tsimonq2 committed Jun 22, 2016
2 parents 544ebf1 + 44156fb commit e4af6d1
Show file tree
Hide file tree
Showing 13 changed files with 425 additions and 96 deletions.
45 changes: 45 additions & 0 deletions integration_tests/test_store_register.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright (C) 2016 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import os
import subprocess
import uuid

import integration_tests
from snapcraft.tests import fixture_setup


class RegisterTestCase(integration_tests.TestCase):

def setUp(self):
super().setUp()
if os.getenv('TEST_USER_PASSWORD', None) is None:
self.useFixture(fixture_setup.FakeStore())
else:
self.useFixture(fixture_setup.StagingStore())

def test_successful_registration(self):
self.login(expect_success=True)
snap_name = 'u1test{}'.format(uuid.uuid4().int)
self.run_snapcraft(['register', snap_name])

def test_failed_registration(self):
self.login(expect_success=True)
# The snap name is already registered.
error = self.assertRaises(
subprocess.CalledProcessError,
self.run_snapcraft, ['register', 'test-bad-snap-name'])
self.assertIn('Registration failed.', str(error.output))
24 changes: 16 additions & 8 deletions integration_tests/test_store_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,20 @@ def setUp(self):
super().setUp()
self.deb_arch = snapcraft.ProjectOptions().deb_arch

def _update_version(self, project_dir, version=None):
# Change to a random version.
# The maximum size is 32 chars.
def _update_name_and_version(self, project_dir, name=None, version=None):
unique_id = uuid.uuid4().int
if name is None:
name = 'u1test+{}'.format(unique_id)
if version is None:
version = str(uuid.uuid4().int)[:32]
# The maximum size is 32 chars.
version = str(unique_id)[:32]
updated_project_dir = self.copy_project_to_tmp(project_dir)
yaml_file = os.path.join(project_dir, 'snapcraft.yaml')
for line in fileinput.input(yaml_file, inplace=True):
if 'name: ' in line:
print('name: {}'.format(name))
if 'version: ' in line:
print('version: ' + version)
print('version: {}'.format(version))
else:
print(line)
return updated_project_dir
Expand Down Expand Up @@ -72,10 +76,14 @@ def test_upload_with_login(self):
self.addCleanup(self.logout)
self.login()

# Change to a random version.
# Change to a random name and version.
unique_id = uuid.uuid4().int
new_name = 'u1test+{}'.format(unique_id)
# The maximum size is 32 chars.
new_version = str(uuid.uuid4().int)[:32]
project_dir = self._update_version(project_dir, new_version)
new_version = str(unique_id)[:32]

project_dir = self._update_name_and_version(
project_dir, new_name, new_version)

self.run_snapcraft('snap', project_dir)

Expand Down
1 change: 1 addition & 0 deletions snapcraft/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
download,
login,
logout,
register,
upload
)
from snapcraft import common # noqa
Expand Down
35 changes: 27 additions & 8 deletions snapcraft/_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,16 @@ def login():

logger.info('Authenticating against Ubuntu One SSO.')
store = storeapi.StoreClient()
response = store.login(
email, password, one_time_password=one_time_password)
success = response.get('success', False)

if success:
logger.info('Login successful.')
else:
try:
store.login(
email, password, one_time_password=one_time_password)
except (storeapi.errors.InvalidCredentialsError,
storeapi.errors.StoreAuthenticationError):
logger.info('Login failed.')
return success
return False
else:
logger.info('Login successful.')
return True


def logout():
Expand All @@ -52,6 +53,24 @@ def logout():
logger.info('Credentials cleared.')


def register(snap_name):
logger.info('Registering {}.'.format(snap_name))
store = storeapi.StoreClient()
try:
response = store.register(snap_name)
except storeapi.errors.InvalidCredentialsError:
logger.error('No valid credentials found.'
' Have you run "snapcraft login"?')
raise
if response.ok:
logger.info(
"Congratulations! You're now the publisher for {!r}.".format(
snap_name))
else:
logger.error('Registration failed.')
raise RuntimeError()


def upload(snap_filename):
logger.info('Uploading existing {}.'.format(snap_filename))

Expand Down
30 changes: 22 additions & 8 deletions snapcraft/internal/yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ def __init__(self, project_options=None):
self._project_options = project_options
self.after_requests = {}

self.data = _snapcraft_yaml_load()
self._snapcraft_yaml = _get_snapcraft_yaml()
self.data = _snapcraft_yaml_load(self._snapcraft_yaml)

self._validator = Validator(self.data)
self._validator.validate()
Expand Down Expand Up @@ -254,7 +255,7 @@ def validate_parts(self, part_names):
if part_name not in self._part_names:
raise EnvironmentError(
'The part named {!r} is not defined in '
'\'snapcraft.yaml\''.format(part_name))
'{!r}'.format(part_name, self._snapcraft_yaml))

def load_plugin(self, part_name, plugin_name, properties):
part = pluginhandler.load_plugin(
Expand Down Expand Up @@ -514,12 +515,25 @@ def _expand_filesets_for(step, properties):
return new_step_set


def _snapcraft_yaml_load(yaml_file='snapcraft.yaml'):
try:
with open(yaml_file, 'rb') as fp:
bs = fp.read(2)
except FileNotFoundError:
raise SnapcraftYamlFileError(yaml_file)
def _get_snapcraft_yaml():
visible_yaml_exists = os.path.exists('snapcraft.yaml')
hidden_yaml_exists = os.path.exists('.snapcraft.yaml')

if visible_yaml_exists and hidden_yaml_exists:
raise EnvironmentError(
"Found a 'snapcraft.yaml' and a '.snapcraft.yaml', "
"please remove one")
elif visible_yaml_exists:
return 'snapcraft.yaml'
elif hidden_yaml_exists:
return '.snapcraft.yaml'
else:
raise SnapcraftYamlFileError('snapcraft.yaml')


def _snapcraft_yaml_load(yaml_file):
with open(yaml_file, 'rb') as fp:
bs = fp.read(2)

if bs == codecs.BOM_UTF16_LE or bs == codecs.BOM_UTF16_BE:
encoding = 'utf-16'
Expand Down
33 changes: 25 additions & 8 deletions snapcraft/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
snapcraft [options] cleanbuild
snapcraft [options] login
snapcraft [options] logout
snapcraft [options] register <snap-name>
snapcraft [options] upload <snap-file>
snapcraft [options] list-plugins
snapcraft [options] tour [<directory>]
Expand Down Expand Up @@ -71,6 +72,7 @@
list-plugins List the available plugins that handle different types of part.
login Authenticate session against Ubuntu One SSO.
logout Clear session credentials.
register Register the package name in the store.
tour Setup the snapcraft examples tour in the specified directory,
or ./snapcraft-tour/.
upload Upload a snap to the Ubuntu Store.
Expand Down Expand Up @@ -237,16 +239,11 @@ def run(args, project_options):
elif argless_command:
argless_command()
elif args['clean']:
step = args['--step']
if step == 'strip':
logger.warning('DEPRECATED: Use `prime` instead of `strip` '
'as the step to clean')
step = 'prime'
lifecycle.clean(project_options, args['<part>'], step)
elif args['upload']:
snapcraft.upload(args['<snap-file>'])
_run_clean(args, project_options)
elif args['cleanbuild']:
lifecycle.cleanbuild(project_options),
elif _is_store_command(args):
_run_store_command(args)
elif args['tour']:
_scaffold_examples(args['<directory>'] or _SNAPCRAFT_TOUR_DIR)
elif args['help']:
Expand All @@ -258,5 +255,25 @@ def run(args, project_options):
return project_options


def _run_clean(args, project_options):
step = args['--step']
if step == 'strip':
logger.warning('DEPRECATED: Use `prime` instead of `strip` '
'as the step to clean')
step = 'prime'
lifecycle.clean(project_options, args['<part>'], step)


def _is_store_command(args):
return args['register'] or args['upload']


def _run_store_command(args):
if args['register']:
snapcraft.register(args['<snap-name>'])
elif args['upload']:
snapcraft.upload(args['<snap-file>'])


if __name__ == '__main__': # pragma: no cover
main() # pragma: no cover
72 changes: 42 additions & 30 deletions snapcraft/storeapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,14 @@ def _macaroon_auth(conf):
"""
root_macaroon_raw = conf.get('macaroon')
if root_macaroon_raw is None:
raise errors.InvalidCredentialsError(
'Root macaroon not in the config file')
unbound_raw = conf.get('unbound_discharge')
if unbound_raw is None:
raise errors.InvalidCredentialsError(
'Unbound discharge not in the config file')

root_macaroon = _deserialize_macaroon(root_macaroon_raw)
unbound = _deserialize_macaroon(unbound_raw)
bound = root_macaroon.prepare_for_request(unbound)
Expand All @@ -74,7 +81,7 @@ def _deserialize_macaroon(value):
try:
return macaroons.Macaroon.deserialize(value)
except:
raise errors.InvalidCredentialsError()
raise errors.InvalidCredentialsError('Failed to deserialize macaroon')


class Client():
Expand Down Expand Up @@ -120,29 +127,17 @@ def __init__(self):

def login(self, email, password, one_time_password=None):
"""Log in via the Ubuntu One SSO API."""
result = dict(success=False, body=None)
# Ask the store for the needed capabalities to be associated with the
# macaroon.
macaroon, error = self.sca.get_macaroon(
macaroon = self.sca.get_macaroon(
['package_upload', 'package_access'])
if error:
result['errors'] = error
else:
caveat_id = self._extract_caveat_id(macaroon)
if caveat_id is None:
error = 'Invalid macaroon'
else:
unbound_discharge, error = self.sso.get_unbound_discharge(
email, password, one_time_password, caveat_id)
if error:
result['errors'] = error
else:
# The macaroon has been discharged, save it in the config
self.conf.set('macaroon', macaroon)
self.conf.set('unbound_discharge', unbound_discharge)
self.conf.save()
result['success'] = True
return result
caveat_id = self._extract_caveat_id(macaroon)
unbound_discharge = self.sso.get_unbound_discharge(
email, password, one_time_password, caveat_id)
# The macaroon has been discharged, save it in the config
self.conf.set('macaroon', macaroon)
self.conf.set('unbound_discharge', unbound_discharge)
self.conf.save()

def _extract_caveat_id(self, root_macaroon):
macaroon = macaroons.Macaroon.deserialize(root_macaroon)
Expand All @@ -152,19 +147,27 @@ def _extract_caveat_id(self, root_macaroon):
for caveat in macaroon.caveats:
if caveat.location == sso_host:
return macaroons.convert_to_string(caveat.caveat_id)
return None
else:
raise errors.InvalidCredentialsError('Invalid root macaroon')

def logout(self):
self.conf.clear()
self.conf.save()

def register(self, snap_name):
return self.sca.register(snap_name, constants.DEFAULT_SERIES)

def upload(self, snap_filename):
if not os.path.exists(snap_filename):
raise FileNotFoundError(snap_filename)
snap_name = _get_name_from_snap_file(snap_filename)

# FIXME This should be raised by the function that uses the
# discharge. --elopio -2016-06-20
if self.conf.get('unbound_discharge') is None:
raise errors.InvalidCredentialsError()
raise errors.InvalidCredentialsError(
'Unbound discharge not in the config file')

data = _upload.upload_files(snap_filename, self.updown)
success = data.get('success', False)
if not success:
Expand Down Expand Up @@ -237,9 +240,10 @@ def get_unbound_discharge(self, email, password, one_time_password,
headers={'Content-Type': 'application/json',
'Accept': 'application/json'})
if response.ok:
return response.json()['discharge_macaroon'], None
return response.json()['discharge_macaroon']
else:
return None, response.text
raise errors.StoreAuthenticationError(
'Failed to get unbound discharge: '.format(response.text))


class SnapIndexClient(Client):
Expand All @@ -255,9 +259,6 @@ def __init__(self, conf):
constants.UBUNTU_STORE_SEARCH_ROOT_URL))

def search_package(self, snap_name, channel, arch):
if self.conf.get('unbound_discharge') is None:
raise errors.InvalidCredentialsError()

headers = {
'Accept': 'application/hal+json',
'X-Ubuntu-Architecture': arch,
Expand Down Expand Up @@ -315,9 +316,20 @@ def get_macaroon(self, acls):
headers={'Content-Type': 'application/json',
'Accept': 'application/json'})
if response.ok:
return response.json()['macaroon'], None
return response.json()['macaroon']
else:
return None, response.text
raise errors.StoreAuthenticationError('Failed to get macaroon')

def register(self, snap_name, series):
auth = _macaroon_auth(self.conf)
data = dict(snap_name=snap_name, series=series)
response = self.post(
'register-name/', data=json.dumps(data),
headers={'Authorization': auth,
'Content-Type': 'application/json'})
# TODO handle macaroon refresh
# TODO raise different exceptions based on the response error codes.
return response

def snap_upload(self, data):
auth = _macaroon_auth(self.conf)
Expand Down
Loading

0 comments on commit e4af6d1

Please sign in to comment.