Merge lp:~canonical-platform-qa/snappy-ecosystem-tests/assertions-signing-mechanism into lp:snappy-ecosystem-tests

Proposed by Heber Parrucci
Status: Merged
Approved by: Heber Parrucci
Approved revision: 56
Merged at revision: 58
Proposed branch: lp:~canonical-platform-qa/snappy-ecosystem-tests/assertions-signing-mechanism
Merge into: lp:snappy-ecosystem-tests
Diff against target: 736 lines (+569/-9)
10 files modified
README.rst (+13/-2)
pylint.cfg (+2/-2)
snappy_ecosystem_tests/helpers/snapcraft/client.py (+80/-0)
snappy_ecosystem_tests/helpers/snapd/snapd.py (+125/-0)
snappy_ecosystem_tests/helpers/store_apis/rest_apis.py (+34/-0)
snappy_ecosystem_tests/models/assertion.py (+116/-0)
snappy_ecosystem_tests/models/key.py (+41/-0)
snappy_ecosystem_tests/models/snap.py (+4/-0)
snappy_ecosystem_tests/tests/test_share_snap.py (+143/-0)
snappy_ecosystem_tests/utils/storeconfig.py (+11/-5)
To merge this branch: bzr merge lp:~canonical-platform-qa/snappy-ecosystem-tests/assertions-signing-mechanism
Reviewer Review Type Date Requested Status
platform-qa-bot continuous-integration Approve
Snappy ecosystem tests developer Pending
Review via email: mp+322144@code.launchpad.net

Commit message

Adding a generic mechanism to sign assertions

Description of the change

The objective of this change is to provide a mechanism for generating and signing user's assertions. That will be useful, for example for sharing snaps to other users.

The change also contains a test for sharing a snap that is currently skipped because the assertion type: 'snap-developer' is not released yet. So if you run the test you will get:
RuntimeError: Unable to sign assertion: spawn /tmp/tmp.86c1gleSxW/sign_assertion.sh
error: invalid assertion type: snap-developer

Important note: To allow supporting multiple users, a change was made in get_remote_credentials method. It will required you to change your local config file, It should look like this:

[store_user_0]
user_email=
user_password=

[store_user_1]
user_email=
user_password=

[user]
snapd_hostname_remote=
snapd_username_remote=
snapd_port_remote=
snapcraft_hostname_remote=
snapcraft_username_remote=
snapcraft_port_remote=

See README file for more details.

@run_tests: snappy_ecosystem_tests/tests/test_snapd.py

To post a comment you must log in.
Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
56. By Heber Parrucci

merge from trunk

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'README.rst'
2--- README.rst 2017-03-27 13:16:17 +0000
3+++ README.rst 2017-04-13 13:43:35 +0000
4@@ -66,9 +66,20 @@
5 XDG_USER_CONFIG_HOME=^DIR_PATH^
6
7 The config file should look like:
8+
9+[store_user_0]
10+user_email=^USER_0_NAME^
11+user_password=^USER_0_PASSWORD^
12+
13+[store_user_1]
14+user_email=^USER_1_NAME^
15+user_password=^USER_1_PASSWORD^
16+
17+[store_user_n]
18+user_email=^USER_n_NAME^
19+user_password=^USER_n_PASSWORD^
20+
21 [user]
22-user_email=^USER_NAME^
23-user_password=^USER_PASSWORD^
24 snapd_hostname_remote=^SNAPD_SSH_HOSTNAME^
25 snapd_username_remote=^SNAPD_SSH_USERNAME^
26 snapd_port_remote=^SNAPD_SSH_PORT^
27
28=== modified file 'pylint.cfg'
29--- pylint.cfg 2017-03-29 17:35:36 +0000
30+++ pylint.cfg 2017-04-13 13:43:35 +0000
31@@ -206,7 +206,7 @@
32
33 # Regular expression which should only match function or class names that do
34 # not require a docstring.
35-no-docstring-rgx=.*TestCase$|setUp|tearDown
36+no-docstring-rgx=.*TestCase$|setUp|tearDown|__getattr__
37
38 # Minimum line length for functions/classes that require docstrings, shorter
39 # ones are exempt.
40@@ -364,7 +364,7 @@
41 min-public-methods=1
42
43 # Maximum number of public methods for a class (see R0904).
44-max-public-methods=20
45+max-public-methods=25
46
47 # Maximum number of boolean expressions in a if statement
48 max-bool-expr=5
49
50=== modified file 'snappy_ecosystem_tests/helpers/snapcraft/client.py'
51--- snappy_ecosystem_tests/helpers/snapcraft/client.py 2017-04-04 20:17:51 +0000
52+++ snappy_ecosystem_tests/helpers/snapcraft/client.py 2017-04-13 13:43:35 +0000
53@@ -27,6 +27,7 @@
54
55 from pathlib import PurePath
56
57+from snappy_ecosystem_tests.models.key import Key
58 from snappy_ecosystem_tests.models.snap import Snap
59 from snappy_ecosystem_tests.utils.commands import build_command
60 from snappy_ecosystem_tests.utils.filters import filter_list
61@@ -53,12 +54,36 @@
62 -c 'interact'\
63 """
64
65+COMMANDS_CREATE_KEY = """\
66+/usr/bin/expect \
67+-c 'spawn {snapcraft} create-key {name}' \
68+-c 'expect Passphrase:' \
69+-c 'send {passphrase}\\r' \
70+-c 'expect Confirm passphrase:' \
71+-c 'send {passphrase}\\r' \
72+-c 'interact'\
73+"""
74+
75+COMMANDS_REGISTER_KEY = """\
76+/usr/bin/expect \
77+-c 'spawn {snapcraft} register-key {name}' \
78+-c 'expect Email:' \
79+-c 'send {email}\\r' \
80+-c 'expect Password:' \
81+-c 'send {password}\\r' \
82+-c 'expect Enter passphrase:' \
83+-c 'send {passphrase}\\r' \
84+-c 'interact'\
85+"""
86+
87 COMMAND_LOGOUT = 'logout'
88
89 COMMAND_REGISTER = 'register'
90 COMMAND_LIST_REGISTERED = 'list-registered'
91+COMMAND_LIST_KEYS = 'keys'
92 COMMAND_PUSH = 'push'
93 COMMAND_RELEASE = 'release'
94+
95 LOGGER = logging.getLogger(__name__)
96
97 HOSTNAME, USERNAME, PORT = get_snapcraft_remote_host_credentials()
98@@ -350,6 +375,61 @@
99 self.ssh.run_command('touch {}'.format(file_name), cwd=tempdir)
100 return os.path.join(tempdir, file_name)
101
102+ def create_key(self, name, passphrase='default', register=True,
103+ email=LOGIN_EMAIL, password=LOGIN_PASSWORD):
104+ """Create a user key
105+
106+ :param name: the key name
107+ :param passphrase: the key passphrase
108+ :param register: whether to register the key in the store
109+ :param email: user email
110+ :param password: user password
111+ """
112+ key = self.get_key(name)
113+ if key and key.is_registered():
114+ LOGGER.info('Key %s exists and is registered', name)
115+ return
116+ elif not key:
117+ LOGGER.info('Key %s does not exist. Creating...', name)
118+ self.ssh.run_command(
119+ COMMANDS_CREATE_KEY.format(name=name,
120+ passphrase=passphrase,
121+ snapcraft=PATH_SNAPCRAFT))
122+ if register:
123+ self.register_key(name, email, password)
124+
125+ def register_key(self, name, passphrase='default',
126+ email=LOGIN_EMAIL, password=LOGIN_PASSWORD):
127+ """Register a key in the store
128+
129+ :param name: the key name
130+ :param passphrase: the key passphrase
131+ :param email: the user email
132+ :param password: the user password
133+ """
134+ LOGGER.info('Registering key %s.', name)
135+ return self.ssh.run_command(
136+ COMMANDS_REGISTER_KEY.format(email=email,
137+ password=password,
138+ name=name,
139+ passphrase=passphrase,
140+ snapcraft=PATH_SNAPCRAFT))
141+
142+ def get_key(self, name):
143+ """Get a user key
144+
145+ :param name: key name
146+ :return: The key instance if found, None otherwise
147+ """
148+ user_keys = self._run_snapcraft_command_ssh(
149+ COMMAND_LIST_KEYS).splitlines()
150+ for line in user_keys:
151+ if name in line:
152+ registered = False if 'not registered' in line.lower() else True
153+ fingerprint = line.split()[2]
154+ return Key(name, fingerprint, registered)
155+ return None
156+
157
158 def build_snapcraft_command(*args, base_command=PATH_SNAPCRAFT):
159 """
160
161=== modified file 'snappy_ecosystem_tests/helpers/snapd/snapd.py'
162--- snappy_ecosystem_tests/helpers/snapd/snapd.py 2017-03-22 12:29:03 +0000
163+++ snappy_ecosystem_tests/helpers/snapd/snapd.py 2017-04-13 13:43:35 +0000
164@@ -22,10 +22,13 @@
165
166 import json
167 import logging
168+import os
169 from time import sleep
170
171 import yaml
172
173+from snappy_ecosystem_tests.models.assertion import AssertionFactory
174+from snappy_ecosystem_tests.models.key import Key
175 from snappy_ecosystem_tests.utils.ssh import SSHManager
176 from snappy_ecosystem_tests.utils.user import get_snapd_remote_host_credentials
177
178@@ -39,7 +42,9 @@
179 COMMAND_LOGOUT = 'logout'
180 COMMAND_REFRESH = 'refresh {snap} --channel={channel}'
181 COMMAND_REMOVE = 'remove {snap}'
182+COMMAND_SIGN = 'sign'
183 CHANNEL_STABLE = 'stable'
184+COMMAND_LIST_KEYS = 'keys'
185 COMMANDS_LOGIN = """\
186 sudo /usr/bin/expect \
187 -c 'spawn snap login {email}' \
188@@ -47,6 +52,33 @@
189 -c 'send {password}\\r' \
190 -c 'interact'\
191 """
192+
193+COMMANDS_CREATE_KEY = """\
194+/usr/bin/expect \
195+-c 'spawn {snap} create-key {name}' \
196+-c 'expect Passphrase:' \
197+-c 'send {passphrase}\\r' \
198+-c 'expect Confirm passphrase:' \
199+-c 'send {passphrase}\\r' \
200+-c 'interact'\
201+"""
202+
203+COMMANDS_SIGN_KEY = """\
204+/usr/bin/expect \
205+-c 'spawn {sign_script}' \
206+-c 'expect Enter passphrase:' \
207+-c 'send {passphrase}\\r' \
208+-c 'interact'\
209+"""
210+
211+COMMANDS_EXPORT_KEY = """\
212+/usr/bin/expect \
213+-c 'spawn {export_key_script}' \
214+-c 'expect Enter passphrase:' \
215+-c 'send {passphrase}\\r' \
216+-c 'interact'\
217+"""
218+
219 LOGGER = logging.getLogger(__name__)
220
221 HOSTNAME, USERNAME, PORT = get_snapd_remote_host_credentials()
222@@ -203,3 +235,96 @@
223 else:
224 raise ValueError('Snap not published, waited {} seconds.'.format(
225 retry_attempts * retry_interval))
226+
227+ def create_key(self, name, passphrase='default'):
228+ """Create a key if if it does not exists
229+ :param name: the key name
230+ :param passphrase: the key passphrase
231+ :return: the instance of the key
232+ """
233+ if not self.get_key(name):
234+ self.ssh.run_command(
235+ COMMANDS_CREATE_KEY.format(name=name,
236+ passphrase=passphrase,
237+ snap=PATH_SNAP))
238+ return self.get_key(name)
239+
240+ def get_key(self, name):
241+ """Get a user key
242+
243+ :param name: key name
244+ :return: The key instance if found, None otherwise
245+ """
246+ user_keys = self.run_snapd_command_ssh(COMMAND_LIST_KEYS).splitlines()
247+ for line in user_keys:
248+ if name in line:
249+ _name, fingerprint = line.split()
250+ return Key(_name, fingerprint)
251+ return None
252+
253+ def export_key(self, account_id, name, passphrase='default'):
254+ """Export a user's key
255+
256+ :param account_id: user's account_id
257+ :param name: name of the key to be exported
258+ :param passphrase: the passphrase to unlock the key
259+ :return:
260+ """
261+ tempdir = self.ssh.run_command('mktemp -d')
262+ self.ssh.run_command(
263+ 'echo "{snap} export-key --account={account} {key} > assertion_key"'
264+ ' > export_key.sh'.format(
265+ snap=PATH_SNAP,
266+ account=account_id,
267+ key=name),
268+ cwd=tempdir)
269+ script_path = os.path.join(tempdir, 'export_key.sh')
270+ self.ssh.run_command('chmod +x {}'.format(script_path))
271+ self.ssh.run_command(
272+ COMMANDS_EXPORT_KEY.format(
273+ export_key_script=script_path,
274+ passphrase=passphrase), cwd=tempdir)
275+ return self.ssh.run_command('cat {}'.format(
276+ os.path.join(tempdir, 'assertion_key')))
277+
278+ def build_assertion(self, _type, **kwargs):
279+ """
280+ Build a new assertion in the remote machine
281+
282+ :param _type: the assertion type
283+ :param kwargs: the arguments for the given assertion type
284+ :return: the remote full path of the assertion
285+ """
286+ assertion = AssertionFactory.create(_type, **kwargs)
287+ tempdir = self.ssh.run_command('mktemp -d')
288+ self.ssh.run_command(
289+ 'echo {} > assertion'.format(str(assertion)), cwd=tempdir)
290+ return os.path.join(tempdir, 'assertion')
291+
292+ def sign_assertion(self, assertion, key='default', passphrase='default'):
293+ """Sign an assertion with a developer private key
294+
295+ :param assertion: the path of the assertion to be signed
296+ :param key: the developer's private key.
297+ :param passphrase: passphrase to unlock the key
298+ """
299+ tempdir = self.ssh.run_command('mktemp -d')
300+ self.ssh.run_command(
301+ 'echo "cat {} | {} {} -k {} > assertion_signed" '
302+ '> sign_assertion.sh'.format(
303+ assertion,
304+ PATH_SNAP,
305+ COMMAND_SIGN,
306+ key),
307+ cwd=tempdir)
308+ script_path = os.path.join(tempdir, 'sign_assertion.sh')
309+ self.ssh.run_command('chmod +x {}'.format(script_path))
310+ output = self.ssh.run_command(
311+ COMMANDS_SIGN_KEY.format(
312+ sign_script=script_path,
313+ passphrase=passphrase), cwd=tempdir)
314+ if 'invalid assertion' not in output.lower():
315+ return self.ssh.run_command('cat {}'.format(
316+ os.path.join(tempdir, 'assertion_signed')))
317+ else:
318+ raise RuntimeError('Unable to sign assertion: {}'.format(output))
319
320=== modified file 'snappy_ecosystem_tests/helpers/store_apis/rest_apis.py'
321--- snappy_ecosystem_tests/helpers/store_apis/rest_apis.py 2017-03-29 17:35:36 +0000
322+++ snappy_ecosystem_tests/helpers/store_apis/rest_apis.py 2017-04-13 13:43:35 +0000
323@@ -398,6 +398,12 @@
324 """Sign developer agreement to be able to do operations in the store"""
325 return self.sca.sign_developer_agreement(latest_tos_accepted)
326
327+ def share_snap(self, snap_name, assertion, series=None):
328+ """Share a snap with the given assertion
329+ First refresh macaroon if necessary"""
330+ return self._refresh_if_necessary(
331+ self.sca.share_snap, snap_name, assertion, series)
332+
333
334 class SSOClient(Client):
335 """The Single Sign On server deals with authentication.
336@@ -874,6 +880,34 @@
337 raise errors.DeveloperAgreementSignError(response)
338 return response.json()
339
340+ def share_snap(self, snap_name, assertion, series=None):
341+ """
342+ Share a snap trough the given signed assertion.
343+
344+ :param snap_name: name of the snap to be shared.
345+ :param assertion: the snap-developer assertion.
346+ The assertion’s publisher-id must be the authorized user’s account ID.
347+ The assertion’s authority-id must be the same as the publisher-id,
348+ i.e. the assertion must be signed by one of the authorized user’s
349+ private keys.
350+ :param series: the snap series
351+ """
352+ if series is None:
353+ series = constants.DEFAULT_SERIES
354+ snap_id = self.filter_snaps(series=series, name=snap_name)[0].get_id()
355+ url = 'snaps/' + snap_id + '/developers'
356+ auth = _macaroon_auth(self.conf)
357+ # TODO: Investigate if it is necessary to push the assertion
358+ # before calling to the API
359+ data = {
360+ 'snap_developer': assertion,
361+ }
362+ response = self.put(
363+ url,
364+ headers=dict(**{'Authorization': auth}, **JSON_HEADERS),
365+ data=json.dumps(data))
366+ return response
367+
368
369 class StatusTracker:
370
371
372=== added file 'snappy_ecosystem_tests/models/assertion.py'
373--- snappy_ecosystem_tests/models/assertion.py 1970-01-01 00:00:00 +0000
374+++ snappy_ecosystem_tests/models/assertion.py 2017-04-13 13:43:35 +0000
375@@ -0,0 +1,116 @@
376+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
377+
378+#
379+# Snappy Ecosystem Tests
380+# Copyright (C) 2017 Canonical
381+#
382+# This program is free software: you can redistribute it and/or modify
383+# it under the terms of the GNU General Public License as published by
384+# the Free Software Foundation, either version 3 of the License, or
385+# (at your option) any later version.
386+#
387+# This program is distributed in the hope that it will be useful,
388+# but WITHOUT ANY WARRANTY; without even the implied warranty of
389+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
390+# GNU General Public License for more details.
391+#
392+# You should have received a copy of the GNU General Public License
393+# along with this program. If not, see <http://www.gnu.org/licenses/>.
394+#
395+
396+"""Business model classes for assertions"""
397+
398+import json
399+
400+from datetime import datetime
401+
402+
403+class Assertion(dict):
404+ """Represents an assertion entity."""
405+
406+ def __init__(self, _type):
407+ super().__init__()
408+ self.type = _type
409+
410+ def __getattr__(self, item):
411+ return self.get(item)
412+
413+ def __setattr__(self, key, value):
414+ self[key] = value
415+
416+ def __str__(self, encoding='utf-8'):
417+ return str(json.dumps(self).encode(encoding=encoding)).lstrip('b')
418+
419+
420+class SnapDeveloperAssertion(Assertion):
421+ """Represents a snap developer assertion entity."""
422+
423+ def __init__(self, snap_id, developers,
424+ revision, publisher_id='canonical', authority_id=None):
425+ super().__init__('snap-developer')
426+ self['publisher-id'] = publisher_id
427+ self['authority-id'] = authority_id or publisher_id
428+ self['snap-id'] = snap_id
429+ self['revision'] = revision
430+ self._parse_developers(developers)
431+
432+ def _parse_developers(self, _developers):
433+ """Parse the developers list to a well-known format for the assertion.
434+ Examples ('now' is replaced for the current time):
435+
436+ - [{'dev_id': ('now',)}] --> [{developer-id: 'dev-id', 'since': 'now'}]
437+
438+ - [{'dev_id': ('now', 'now')}] --> [{developer-id: 'dev-id',
439+ 'since': 'now', 'until': 'now'}]
440+
441+ - [{'dev_id': ('2025-04-04T15:49:27.864208',
442+ '2026-04-04T15:49:27.864208')}] -->
443+ [{developer-id: 'dev-id', 'since': '2025-04-04T15:49:27.864208',
444+ 'until': '2026-04-04T15:49:27.864208'}]
445+
446+ :param _developers: the list of developers to be parsed
447+ """
448+ self.developers = []
449+ for dev in _developers:
450+ for dev_id, time in dev.items():
451+ _dev = {'developer-id': dev_id}
452+ if len(time) == 1:
453+ _dev['since'] = self._parse_time(time[0])
454+ elif len(time) == 2:
455+ _dev['since'] = self._parse_time(time[0])
456+ _dev['until'] = self._parse_time(time[1])
457+ self.developers.append(_dev)
458+
459+ @staticmethod
460+ def _parse_time(time):
461+ """Parse and return UTC time in ISO format
462+ :param: the time to be parsed. Examples:
463+ - 'now'
464+ - 'datetime.datetime(2017, 4, 5, 13, 14, 9, 178673)'
465+ - '2017-04-05T13:11:57'
466+ """
467+ if time.lower() == 'now':
468+ return datetime.utcnow().isoformat() + 'Z'
469+ elif isinstance(time, datetime):
470+ return time.isoformat() + 'Z'
471+ elif isinstance(time, str):
472+ return datetime.strptime(time,
473+ "%Y-%m-%dT%H:%M:%S").isoformat() + 'Z'
474+
475+
476+class AssertionFactory:
477+ """Factory to create assertion objects"""
478+
479+ mapping = {'snap-developer': SnapDeveloperAssertion}
480+
481+ @classmethod
482+ def create(cls, _type, **kwargs):
483+ """Create the assertion object
484+ :param _type: assertion type. It must exist in the mapping dict
485+ :param kwargs: arguments to initialize the assertion object
486+ :raise RuntimeError: when assertion type does not exist in the mapping
487+ """
488+ try:
489+ return cls.mapping[_type](**kwargs)
490+ except KeyError:
491+ raise RuntimeError('Assertion type {} does not exist'.format(_type))
492
493=== added file 'snappy_ecosystem_tests/models/key.py'
494--- snappy_ecosystem_tests/models/key.py 1970-01-01 00:00:00 +0000
495+++ snappy_ecosystem_tests/models/key.py 2017-04-13 13:43:35 +0000
496@@ -0,0 +1,41 @@
497+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
498+
499+#
500+# Snappy Ecosystem Tests
501+# Copyright (C) 2017 Canonical
502+#
503+# This program is free software: you can redistribute it and/or modify
504+# it under the terms of the GNU General Public License as published by
505+# the Free Software Foundation, either version 3 of the License, or
506+# (at your option) any later version.
507+#
508+# This program is distributed in the hope that it will be useful,
509+# but WITHOUT ANY WARRANTY; without even the implied warranty of
510+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
511+# GNU General Public License for more details.
512+#
513+# You should have received a copy of the GNU General Public License
514+# along with this program. If not, see <http://www.gnu.org/licenses/>.
515+#
516+
517+"""Business model classes for User's Key"""
518+
519+from textwrap import dedent
520+
521+
522+class Key:
523+ """Represents a key entity."""
524+
525+ def __init__(self, name, fingerprint, registered=False):
526+ self.name = name
527+ self.fingerprint = fingerprint
528+ self.registered = registered
529+
530+ def is_registered(self):
531+ """Return True if the key is registered, False otherwise"""
532+ return self.registered
533+
534+ def __str__(self):
535+ return dedent(
536+ '''name: {}\npublic-key-sha3-384: {}'''.format(self.name,
537+ self.fingerprint))
538
539=== modified file 'snappy_ecosystem_tests/models/snap.py'
540--- snappy_ecosystem_tests/models/snap.py 2017-02-21 19:06:58 +0000
541+++ snappy_ecosystem_tests/models/snap.py 2017-04-13 13:43:35 +0000
542@@ -37,3 +37,7 @@
543 def is_private(self):
544 """Return True if the snap is private, False otherwise"""
545 return self.private
546+
547+ def get_id(self):
548+ """Return the snap id"""
549+ return self._id
550
551=== added file 'snappy_ecosystem_tests/tests/test_share_snap.py'
552--- snappy_ecosystem_tests/tests/test_share_snap.py 1970-01-01 00:00:00 +0000
553+++ snappy_ecosystem_tests/tests/test_share_snap.py 2017-04-13 13:43:35 +0000
554@@ -0,0 +1,143 @@
555+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
556+
557+#
558+# Snappy Ecosystem Tests
559+# Copyright (C) 2017 Canonical
560+#
561+# This program is free software: you can redistribute it and/or modify
562+# it under the terms of the GNU General Public License as published by
563+# the Free Software Foundation, either version 3 of the License, or
564+# (at your option) any later version.
565+#
566+# This program is distributed in the hope that it will be useful,
567+# but WITHOUT ANY WARRANTY; without even the implied warranty of
568+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
569+# GNU General Public License for more details.
570+#
571+# You should have received a copy of the GNU General Public License
572+# along with this program. If not, see <http://www.gnu.org/licenses/>.
573+#
574+
575+"""Tests for sharing Snaps in the store"""
576+
577+from testtools import skip
578+
579+from snappy_ecosystem_tests.helpers.snapcraft.client import Snapcraft
580+from snappy_ecosystem_tests.helpers.snapd.snapd import Snapd
581+from snappy_ecosystem_tests.helpers.store_apis import errors
582+from snappy_ecosystem_tests.helpers.store_apis.rest_apis import Store
583+from snappy_ecosystem_tests.helpers.test_base import SnappyEcosystemTestCase
584+from snappy_ecosystem_tests.tests.commons import get_random_snap_name
585+from snappy_ecosystem_tests.utils.storeconfig import get_store_credentials
586+
587+
588+class ShareSnapTestCase(SnappyEcosystemTestCase):
589+
590+ def setUp(self):
591+ super().setUp()
592+ self.acls = ['package_manage', 'package_upload', 'package_access',
593+ 'modify_account_key']
594+ self.snapcraft = Snapcraft()
595+ self.snapd = Snapd()
596+ self.store = Store()
597+ email, password = get_store_credentials()
598+ self.assertTrue(self.snapcraft.login(email, password))
599+ self.addCleanup(self.snapcraft.logout)
600+ self.store.login(email, password, acls=self.acls)
601+ self.addCleanup(self.store.logout)
602+ self.assertTrue(self.snapd.login(email, password))
603+ self.addCleanup(self.snapd.logout, email)
604+
605+ @staticmethod
606+ def _get_developer_id(index=0):
607+ """Return the developer_id (aka account_id)
608+ :param index: the index of the user's list that want
609+ to be returned from the config stack
610+ """
611+ store = Store()
612+ store.login(*get_store_credentials(index=index))
613+ developer_id = store.get_account_information()['account_id']
614+ store.logout()
615+ return developer_id
616+
617+ def _ensure_key_registered(self, user_index, name, passphrase='default'):
618+ """Ensure the given key exists and is registered in the store
619+ :param user_index: the index of the user you want to
620+ register the key for
621+ :param name: the key name
622+ :param passphrase: the key passphrase
623+ :return: True if key was registered successfully or if it is already
624+ registered in the store, False otherwise
625+ """
626+ # Create the key if it does not exist
627+ self.snapd.create_key(name, passphrase)
628+ # export the key
629+ key_exported = self.snapd.export_key(self._get_developer_id(user_index),
630+ name, passphrase)
631+ # Register the key in the store
632+ try:
633+ self.store.register_key(key_exported)
634+ except errors.StoreKeyRegistrationError as kre:
635+ if 'already the current revision' in str(kre).lower():
636+ return True
637+ elif 'authorization required' in str(kre).lower():
638+ user, email = get_store_credentials(user_index)
639+ self.store.logout()
640+ self.store.login(user, email, acls=self.acls)
641+ self.store.register_key(key_exported)
642+ return True
643+ else:
644+ return False
645+
646+ @skip('The assertion type "snap-developer" is still in development')
647+ def test_push_and_release_a_shared_snap(self):
648+ """A user to whom a snap was shared via RESTful API pushes and releases
649+ a new revision and it is available on the store
650+ """
651+ version = '0.1'
652+ revision = 1
653+ snap_name = get_random_snap_name()
654+ user_key = 'default'
655+ self.assertTrue(self.store.register(snap_name),
656+ 'Unable to register snap')
657+ snap_id = self.store.filter_snaps(name=snap_name)[0].get_id()
658+
659+ # Upload the snap and wait for its processing on server to finish.
660+ tracker = self.store.upload(
661+ snap_name, self.snapcraft.build_and_pull(snap_name, version))
662+ tracker.track()
663+
664+ # Build the snap-developer assertion
665+ developers = [{self._get_developer_id(index=1): ('now',)}]
666+ assertion = self.snapd.build_assertion(
667+ 'snap-developer',
668+ publisher_id=self._get_developer_id(),
669+ snap_id=snap_id,
670+ developers=developers,
671+ revision=revision)
672+
673+ # Ensure Key is registered for current user
674+ self.assertTrue(self._ensure_key_registered(0, user_key))
675+ # Sign the snap-developer assertion
676+ assertion_signed = self.snapd.sign_assertion(assertion,
677+ key=user_key)
678+ self.store.share_snap(snap_name, assertion_signed)
679+
680+ # Login with the user to whom the snap was shared
681+ # and verify he can push and release a new version of the snap
682+ new_version = '0.2'
683+ new_revision = 2
684+ channels = ['candidate', 'stable']
685+ self.store.logout()
686+ self.snapcraft.logout()
687+ self.store.login(*get_store_credentials(index=1))
688+ self.snapcraft.login(*get_store_credentials(index=1))
689+
690+ # Push and release the snap to multiple channels
691+ self.snapcraft.push(self.snapcraft.build(snap_name, new_version),
692+ channels=channels)
693+ # Check using REST API if snap was released to all channels
694+ self.assertTrue(self.store.is_snap_published(snap_name,
695+ new_version,
696+ new_revision,
697+ channels))
698
699=== modified file 'snappy_ecosystem_tests/utils/storeconfig.py'
700--- snappy_ecosystem_tests/utils/storeconfig.py 2017-02-22 17:41:52 +0000
701+++ snappy_ecosystem_tests/utils/storeconfig.py 2017-04-13 13:43:35 +0000
702@@ -21,6 +21,7 @@
703 """Get all the store related data like credentials, urls"""
704
705 import os
706+from configparser import NoSectionError
707
708 from snappy_ecosystem_tests.commons.config import (
709 CONFIG_STACK,
710@@ -31,16 +32,21 @@
711 URL_WEB_STORE_STAGING = CONFIG_STACK.get('staging_urls', 'web')
712
713
714-def get_store_credentials():
715+def get_store_credentials(index=0):
716 """Get user store credentials.
717 It will retrieve the credentials stored in a user config file,
718 if the config is not found,
719 then it will return the ones provided in environment variables
720+
721+ :param index: the index of the user that want
722+ to be returned from the user config stack
723 """
724- return (USER_CONFIG_STACK.get('user', 'user_email',
725- default=os.environ.get('user_email')),
726- USER_CONFIG_STACK.get('user', 'user_password',
727- default=os.environ.get('user_password')))
728+ try:
729+ section = USER_CONFIG_STACK.get_section('store_user_{}'.format(index))
730+ except (NoSectionError, KeyError):
731+ section = USER_CONFIG_STACK.get_section('user')
732+ return (section.get('user_email') or os.environ.get('user_email'),
733+ section.get('user_password') or os.environ.get('user_password'))
734
735
736 def is_staging():

Subscribers

People subscribed via source and target branches