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
=== modified file 'README.rst'
--- README.rst 2017-03-27 13:16:17 +0000
+++ README.rst 2017-04-13 13:43:35 +0000
@@ -66,9 +66,20 @@
66XDG_USER_CONFIG_HOME=^DIR_PATH^66XDG_USER_CONFIG_HOME=^DIR_PATH^
6767
68The config file should look like:68The config file should look like:
69
70[store_user_0]
71user_email=^USER_0_NAME^
72user_password=^USER_0_PASSWORD^
73
74[store_user_1]
75user_email=^USER_1_NAME^
76user_password=^USER_1_PASSWORD^
77
78[store_user_n]
79user_email=^USER_n_NAME^
80user_password=^USER_n_PASSWORD^
81
69[user]82[user]
70user_email=^USER_NAME^
71user_password=^USER_PASSWORD^
72snapd_hostname_remote=^SNAPD_SSH_HOSTNAME^83snapd_hostname_remote=^SNAPD_SSH_HOSTNAME^
73snapd_username_remote=^SNAPD_SSH_USERNAME^84snapd_username_remote=^SNAPD_SSH_USERNAME^
74snapd_port_remote=^SNAPD_SSH_PORT^85snapd_port_remote=^SNAPD_SSH_PORT^
7586
=== modified file 'pylint.cfg'
--- pylint.cfg 2017-03-29 17:35:36 +0000
+++ pylint.cfg 2017-04-13 13:43:35 +0000
@@ -206,7 +206,7 @@
206206
207# Regular expression which should only match function or class names that do207# Regular expression which should only match function or class names that do
208# not require a docstring.208# not require a docstring.
209no-docstring-rgx=.*TestCase$|setUp|tearDown209no-docstring-rgx=.*TestCase$|setUp|tearDown|__getattr__
210210
211# Minimum line length for functions/classes that require docstrings, shorter211# Minimum line length for functions/classes that require docstrings, shorter
212# ones are exempt.212# ones are exempt.
@@ -364,7 +364,7 @@
364min-public-methods=1364min-public-methods=1
365365
366# Maximum number of public methods for a class (see R0904).366# Maximum number of public methods for a class (see R0904).
367max-public-methods=20367max-public-methods=25
368368
369# Maximum number of boolean expressions in a if statement369# Maximum number of boolean expressions in a if statement
370max-bool-expr=5370max-bool-expr=5
371371
=== modified file 'snappy_ecosystem_tests/helpers/snapcraft/client.py'
--- snappy_ecosystem_tests/helpers/snapcraft/client.py 2017-04-04 20:17:51 +0000
+++ snappy_ecosystem_tests/helpers/snapcraft/client.py 2017-04-13 13:43:35 +0000
@@ -27,6 +27,7 @@
2727
28from pathlib import PurePath28from pathlib import PurePath
2929
30from snappy_ecosystem_tests.models.key import Key
30from snappy_ecosystem_tests.models.snap import Snap31from snappy_ecosystem_tests.models.snap import Snap
31from snappy_ecosystem_tests.utils.commands import build_command32from snappy_ecosystem_tests.utils.commands import build_command
32from snappy_ecosystem_tests.utils.filters import filter_list33from snappy_ecosystem_tests.utils.filters import filter_list
@@ -53,12 +54,36 @@
53-c 'interact'\54-c 'interact'\
54"""55"""
5556
57COMMANDS_CREATE_KEY = """\
58/usr/bin/expect \
59-c 'spawn {snapcraft} create-key {name}' \
60-c 'expect Passphrase:' \
61-c 'send {passphrase}\\r' \
62-c 'expect Confirm passphrase:' \
63-c 'send {passphrase}\\r' \
64-c 'interact'\
65"""
66
67COMMANDS_REGISTER_KEY = """\
68/usr/bin/expect \
69-c 'spawn {snapcraft} register-key {name}' \
70-c 'expect Email:' \
71-c 'send {email}\\r' \
72-c 'expect Password:' \
73-c 'send {password}\\r' \
74-c 'expect Enter passphrase:' \
75-c 'send {passphrase}\\r' \
76-c 'interact'\
77"""
78
56COMMAND_LOGOUT = 'logout'79COMMAND_LOGOUT = 'logout'
5780
58COMMAND_REGISTER = 'register'81COMMAND_REGISTER = 'register'
59COMMAND_LIST_REGISTERED = 'list-registered'82COMMAND_LIST_REGISTERED = 'list-registered'
83COMMAND_LIST_KEYS = 'keys'
60COMMAND_PUSH = 'push'84COMMAND_PUSH = 'push'
61COMMAND_RELEASE = 'release'85COMMAND_RELEASE = 'release'
86
62LOGGER = logging.getLogger(__name__)87LOGGER = logging.getLogger(__name__)
6388
64HOSTNAME, USERNAME, PORT = get_snapcraft_remote_host_credentials()89HOSTNAME, USERNAME, PORT = get_snapcraft_remote_host_credentials()
@@ -350,6 +375,61 @@
350 self.ssh.run_command('touch {}'.format(file_name), cwd=tempdir)375 self.ssh.run_command('touch {}'.format(file_name), cwd=tempdir)
351 return os.path.join(tempdir, file_name)376 return os.path.join(tempdir, file_name)
352377
378 def create_key(self, name, passphrase='default', register=True,
379 email=LOGIN_EMAIL, password=LOGIN_PASSWORD):
380 """Create a user key
381
382 :param name: the key name
383 :param passphrase: the key passphrase
384 :param register: whether to register the key in the store
385 :param email: user email
386 :param password: user password
387 """
388 key = self.get_key(name)
389 if key and key.is_registered():
390 LOGGER.info('Key %s exists and is registered', name)
391 return
392 elif not key:
393 LOGGER.info('Key %s does not exist. Creating...', name)
394 self.ssh.run_command(
395 COMMANDS_CREATE_KEY.format(name=name,
396 passphrase=passphrase,
397 snapcraft=PATH_SNAPCRAFT))
398 if register:
399 self.register_key(name, email, password)
400
401 def register_key(self, name, passphrase='default',
402 email=LOGIN_EMAIL, password=LOGIN_PASSWORD):
403 """Register a key in the store
404
405 :param name: the key name
406 :param passphrase: the key passphrase
407 :param email: the user email
408 :param password: the user password
409 """
410 LOGGER.info('Registering key %s.', name)
411 return self.ssh.run_command(
412 COMMANDS_REGISTER_KEY.format(email=email,
413 password=password,
414 name=name,
415 passphrase=passphrase,
416 snapcraft=PATH_SNAPCRAFT))
417
418 def get_key(self, name):
419 """Get a user key
420
421 :param name: key name
422 :return: The key instance if found, None otherwise
423 """
424 user_keys = self._run_snapcraft_command_ssh(
425 COMMAND_LIST_KEYS).splitlines()
426 for line in user_keys:
427 if name in line:
428 registered = False if 'not registered' in line.lower() else True
429 fingerprint = line.split()[2]
430 return Key(name, fingerprint, registered)
431 return None
432
353433
354def build_snapcraft_command(*args, base_command=PATH_SNAPCRAFT):434def build_snapcraft_command(*args, base_command=PATH_SNAPCRAFT):
355 """435 """
356436
=== modified file 'snappy_ecosystem_tests/helpers/snapd/snapd.py'
--- snappy_ecosystem_tests/helpers/snapd/snapd.py 2017-03-22 12:29:03 +0000
+++ snappy_ecosystem_tests/helpers/snapd/snapd.py 2017-04-13 13:43:35 +0000
@@ -22,10 +22,13 @@
2222
23import json23import json
24import logging24import logging
25import os
25from time import sleep26from time import sleep
2627
27import yaml28import yaml
2829
30from snappy_ecosystem_tests.models.assertion import AssertionFactory
31from snappy_ecosystem_tests.models.key import Key
29from snappy_ecosystem_tests.utils.ssh import SSHManager32from snappy_ecosystem_tests.utils.ssh import SSHManager
30from snappy_ecosystem_tests.utils.user import get_snapd_remote_host_credentials33from snappy_ecosystem_tests.utils.user import get_snapd_remote_host_credentials
3134
@@ -39,7 +42,9 @@
39COMMAND_LOGOUT = 'logout'42COMMAND_LOGOUT = 'logout'
40COMMAND_REFRESH = 'refresh {snap} --channel={channel}'43COMMAND_REFRESH = 'refresh {snap} --channel={channel}'
41COMMAND_REMOVE = 'remove {snap}'44COMMAND_REMOVE = 'remove {snap}'
45COMMAND_SIGN = 'sign'
42CHANNEL_STABLE = 'stable'46CHANNEL_STABLE = 'stable'
47COMMAND_LIST_KEYS = 'keys'
43COMMANDS_LOGIN = """\48COMMANDS_LOGIN = """\
44sudo /usr/bin/expect \49sudo /usr/bin/expect \
45-c 'spawn snap login {email}' \50-c 'spawn snap login {email}' \
@@ -47,6 +52,33 @@
47-c 'send {password}\\r' \52-c 'send {password}\\r' \
48-c 'interact'\53-c 'interact'\
49"""54"""
55
56COMMANDS_CREATE_KEY = """\
57/usr/bin/expect \
58-c 'spawn {snap} create-key {name}' \
59-c 'expect Passphrase:' \
60-c 'send {passphrase}\\r' \
61-c 'expect Confirm passphrase:' \
62-c 'send {passphrase}\\r' \
63-c 'interact'\
64"""
65
66COMMANDS_SIGN_KEY = """\
67/usr/bin/expect \
68-c 'spawn {sign_script}' \
69-c 'expect Enter passphrase:' \
70-c 'send {passphrase}\\r' \
71-c 'interact'\
72"""
73
74COMMANDS_EXPORT_KEY = """\
75/usr/bin/expect \
76-c 'spawn {export_key_script}' \
77-c 'expect Enter passphrase:' \
78-c 'send {passphrase}\\r' \
79-c 'interact'\
80"""
81
50LOGGER = logging.getLogger(__name__)82LOGGER = logging.getLogger(__name__)
5183
52HOSTNAME, USERNAME, PORT = get_snapd_remote_host_credentials()84HOSTNAME, USERNAME, PORT = get_snapd_remote_host_credentials()
@@ -203,3 +235,96 @@
203 else:235 else:
204 raise ValueError('Snap not published, waited {} seconds.'.format(236 raise ValueError('Snap not published, waited {} seconds.'.format(
205 retry_attempts * retry_interval))237 retry_attempts * retry_interval))
238
239 def create_key(self, name, passphrase='default'):
240 """Create a key if if it does not exists
241 :param name: the key name
242 :param passphrase: the key passphrase
243 :return: the instance of the key
244 """
245 if not self.get_key(name):
246 self.ssh.run_command(
247 COMMANDS_CREATE_KEY.format(name=name,
248 passphrase=passphrase,
249 snap=PATH_SNAP))
250 return self.get_key(name)
251
252 def get_key(self, name):
253 """Get a user key
254
255 :param name: key name
256 :return: The key instance if found, None otherwise
257 """
258 user_keys = self.run_snapd_command_ssh(COMMAND_LIST_KEYS).splitlines()
259 for line in user_keys:
260 if name in line:
261 _name, fingerprint = line.split()
262 return Key(_name, fingerprint)
263 return None
264
265 def export_key(self, account_id, name, passphrase='default'):
266 """Export a user's key
267
268 :param account_id: user's account_id
269 :param name: name of the key to be exported
270 :param passphrase: the passphrase to unlock the key
271 :return:
272 """
273 tempdir = self.ssh.run_command('mktemp -d')
274 self.ssh.run_command(
275 'echo "{snap} export-key --account={account} {key} > assertion_key"'
276 ' > export_key.sh'.format(
277 snap=PATH_SNAP,
278 account=account_id,
279 key=name),
280 cwd=tempdir)
281 script_path = os.path.join(tempdir, 'export_key.sh')
282 self.ssh.run_command('chmod +x {}'.format(script_path))
283 self.ssh.run_command(
284 COMMANDS_EXPORT_KEY.format(
285 export_key_script=script_path,
286 passphrase=passphrase), cwd=tempdir)
287 return self.ssh.run_command('cat {}'.format(
288 os.path.join(tempdir, 'assertion_key')))
289
290 def build_assertion(self, _type, **kwargs):
291 """
292 Build a new assertion in the remote machine
293
294 :param _type: the assertion type
295 :param kwargs: the arguments for the given assertion type
296 :return: the remote full path of the assertion
297 """
298 assertion = AssertionFactory.create(_type, **kwargs)
299 tempdir = self.ssh.run_command('mktemp -d')
300 self.ssh.run_command(
301 'echo {} > assertion'.format(str(assertion)), cwd=tempdir)
302 return os.path.join(tempdir, 'assertion')
303
304 def sign_assertion(self, assertion, key='default', passphrase='default'):
305 """Sign an assertion with a developer private key
306
307 :param assertion: the path of the assertion to be signed
308 :param key: the developer's private key.
309 :param passphrase: passphrase to unlock the key
310 """
311 tempdir = self.ssh.run_command('mktemp -d')
312 self.ssh.run_command(
313 'echo "cat {} | {} {} -k {} > assertion_signed" '
314 '> sign_assertion.sh'.format(
315 assertion,
316 PATH_SNAP,
317 COMMAND_SIGN,
318 key),
319 cwd=tempdir)
320 script_path = os.path.join(tempdir, 'sign_assertion.sh')
321 self.ssh.run_command('chmod +x {}'.format(script_path))
322 output = self.ssh.run_command(
323 COMMANDS_SIGN_KEY.format(
324 sign_script=script_path,
325 passphrase=passphrase), cwd=tempdir)
326 if 'invalid assertion' not in output.lower():
327 return self.ssh.run_command('cat {}'.format(
328 os.path.join(tempdir, 'assertion_signed')))
329 else:
330 raise RuntimeError('Unable to sign assertion: {}'.format(output))
206331
=== modified file 'snappy_ecosystem_tests/helpers/store_apis/rest_apis.py'
--- snappy_ecosystem_tests/helpers/store_apis/rest_apis.py 2017-03-29 17:35:36 +0000
+++ snappy_ecosystem_tests/helpers/store_apis/rest_apis.py 2017-04-13 13:43:35 +0000
@@ -398,6 +398,12 @@
398 """Sign developer agreement to be able to do operations in the store"""398 """Sign developer agreement to be able to do operations in the store"""
399 return self.sca.sign_developer_agreement(latest_tos_accepted)399 return self.sca.sign_developer_agreement(latest_tos_accepted)
400400
401 def share_snap(self, snap_name, assertion, series=None):
402 """Share a snap with the given assertion
403 First refresh macaroon if necessary"""
404 return self._refresh_if_necessary(
405 self.sca.share_snap, snap_name, assertion, series)
406
401407
402class SSOClient(Client):408class SSOClient(Client):
403 """The Single Sign On server deals with authentication.409 """The Single Sign On server deals with authentication.
@@ -874,6 +880,34 @@
874 raise errors.DeveloperAgreementSignError(response)880 raise errors.DeveloperAgreementSignError(response)
875 return response.json()881 return response.json()
876882
883 def share_snap(self, snap_name, assertion, series=None):
884 """
885 Share a snap trough the given signed assertion.
886
887 :param snap_name: name of the snap to be shared.
888 :param assertion: the snap-developer assertion.
889 The assertion’s publisher-id must be the authorized user’s account ID.
890 The assertion’s authority-id must be the same as the publisher-id,
891 i.e. the assertion must be signed by one of the authorized user’s
892 private keys.
893 :param series: the snap series
894 """
895 if series is None:
896 series = constants.DEFAULT_SERIES
897 snap_id = self.filter_snaps(series=series, name=snap_name)[0].get_id()
898 url = 'snaps/' + snap_id + '/developers'
899 auth = _macaroon_auth(self.conf)
900 # TODO: Investigate if it is necessary to push the assertion
901 # before calling to the API
902 data = {
903 'snap_developer': assertion,
904 }
905 response = self.put(
906 url,
907 headers=dict(**{'Authorization': auth}, **JSON_HEADERS),
908 data=json.dumps(data))
909 return response
910
877911
878class StatusTracker:912class StatusTracker:
879913
880914
=== added file 'snappy_ecosystem_tests/models/assertion.py'
--- snappy_ecosystem_tests/models/assertion.py 1970-01-01 00:00:00 +0000
+++ snappy_ecosystem_tests/models/assertion.py 2017-04-13 13:43:35 +0000
@@ -0,0 +1,116 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2
3#
4# Snappy Ecosystem Tests
5# Copyright (C) 2017 Canonical
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20
21"""Business model classes for assertions"""
22
23import json
24
25from datetime import datetime
26
27
28class Assertion(dict):
29 """Represents an assertion entity."""
30
31 def __init__(self, _type):
32 super().__init__()
33 self.type = _type
34
35 def __getattr__(self, item):
36 return self.get(item)
37
38 def __setattr__(self, key, value):
39 self[key] = value
40
41 def __str__(self, encoding='utf-8'):
42 return str(json.dumps(self).encode(encoding=encoding)).lstrip('b')
43
44
45class SnapDeveloperAssertion(Assertion):
46 """Represents a snap developer assertion entity."""
47
48 def __init__(self, snap_id, developers,
49 revision, publisher_id='canonical', authority_id=None):
50 super().__init__('snap-developer')
51 self['publisher-id'] = publisher_id
52 self['authority-id'] = authority_id or publisher_id
53 self['snap-id'] = snap_id
54 self['revision'] = revision
55 self._parse_developers(developers)
56
57 def _parse_developers(self, _developers):
58 """Parse the developers list to a well-known format for the assertion.
59 Examples ('now' is replaced for the current time):
60
61 - [{'dev_id': ('now',)}] --> [{developer-id: 'dev-id', 'since': 'now'}]
62
63 - [{'dev_id': ('now', 'now')}] --> [{developer-id: 'dev-id',
64 'since': 'now', 'until': 'now'}]
65
66 - [{'dev_id': ('2025-04-04T15:49:27.864208',
67 '2026-04-04T15:49:27.864208')}] -->
68 [{developer-id: 'dev-id', 'since': '2025-04-04T15:49:27.864208',
69 'until': '2026-04-04T15:49:27.864208'}]
70
71 :param _developers: the list of developers to be parsed
72 """
73 self.developers = []
74 for dev in _developers:
75 for dev_id, time in dev.items():
76 _dev = {'developer-id': dev_id}
77 if len(time) == 1:
78 _dev['since'] = self._parse_time(time[0])
79 elif len(time) == 2:
80 _dev['since'] = self._parse_time(time[0])
81 _dev['until'] = self._parse_time(time[1])
82 self.developers.append(_dev)
83
84 @staticmethod
85 def _parse_time(time):
86 """Parse and return UTC time in ISO format
87 :param: the time to be parsed. Examples:
88 - 'now'
89 - 'datetime.datetime(2017, 4, 5, 13, 14, 9, 178673)'
90 - '2017-04-05T13:11:57'
91 """
92 if time.lower() == 'now':
93 return datetime.utcnow().isoformat() + 'Z'
94 elif isinstance(time, datetime):
95 return time.isoformat() + 'Z'
96 elif isinstance(time, str):
97 return datetime.strptime(time,
98 "%Y-%m-%dT%H:%M:%S").isoformat() + 'Z'
99
100
101class AssertionFactory:
102 """Factory to create assertion objects"""
103
104 mapping = {'snap-developer': SnapDeveloperAssertion}
105
106 @classmethod
107 def create(cls, _type, **kwargs):
108 """Create the assertion object
109 :param _type: assertion type. It must exist in the mapping dict
110 :param kwargs: arguments to initialize the assertion object
111 :raise RuntimeError: when assertion type does not exist in the mapping
112 """
113 try:
114 return cls.mapping[_type](**kwargs)
115 except KeyError:
116 raise RuntimeError('Assertion type {} does not exist'.format(_type))
0117
=== added file 'snappy_ecosystem_tests/models/key.py'
--- snappy_ecosystem_tests/models/key.py 1970-01-01 00:00:00 +0000
+++ snappy_ecosystem_tests/models/key.py 2017-04-13 13:43:35 +0000
@@ -0,0 +1,41 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2
3#
4# Snappy Ecosystem Tests
5# Copyright (C) 2017 Canonical
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20
21"""Business model classes for User's Key"""
22
23from textwrap import dedent
24
25
26class Key:
27 """Represents a key entity."""
28
29 def __init__(self, name, fingerprint, registered=False):
30 self.name = name
31 self.fingerprint = fingerprint
32 self.registered = registered
33
34 def is_registered(self):
35 """Return True if the key is registered, False otherwise"""
36 return self.registered
37
38 def __str__(self):
39 return dedent(
40 '''name: {}\npublic-key-sha3-384: {}'''.format(self.name,
41 self.fingerprint))
042
=== modified file 'snappy_ecosystem_tests/models/snap.py'
--- snappy_ecosystem_tests/models/snap.py 2017-02-21 19:06:58 +0000
+++ snappy_ecosystem_tests/models/snap.py 2017-04-13 13:43:35 +0000
@@ -37,3 +37,7 @@
37 def is_private(self):37 def is_private(self):
38 """Return True if the snap is private, False otherwise"""38 """Return True if the snap is private, False otherwise"""
39 return self.private39 return self.private
40
41 def get_id(self):
42 """Return the snap id"""
43 return self._id
4044
=== added file 'snappy_ecosystem_tests/tests/test_share_snap.py'
--- snappy_ecosystem_tests/tests/test_share_snap.py 1970-01-01 00:00:00 +0000
+++ snappy_ecosystem_tests/tests/test_share_snap.py 2017-04-13 13:43:35 +0000
@@ -0,0 +1,143 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2
3#
4# Snappy Ecosystem Tests
5# Copyright (C) 2017 Canonical
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20
21"""Tests for sharing Snaps in the store"""
22
23from testtools import skip
24
25from snappy_ecosystem_tests.helpers.snapcraft.client import Snapcraft
26from snappy_ecosystem_tests.helpers.snapd.snapd import Snapd
27from snappy_ecosystem_tests.helpers.store_apis import errors
28from snappy_ecosystem_tests.helpers.store_apis.rest_apis import Store
29from snappy_ecosystem_tests.helpers.test_base import SnappyEcosystemTestCase
30from snappy_ecosystem_tests.tests.commons import get_random_snap_name
31from snappy_ecosystem_tests.utils.storeconfig import get_store_credentials
32
33
34class ShareSnapTestCase(SnappyEcosystemTestCase):
35
36 def setUp(self):
37 super().setUp()
38 self.acls = ['package_manage', 'package_upload', 'package_access',
39 'modify_account_key']
40 self.snapcraft = Snapcraft()
41 self.snapd = Snapd()
42 self.store = Store()
43 email, password = get_store_credentials()
44 self.assertTrue(self.snapcraft.login(email, password))
45 self.addCleanup(self.snapcraft.logout)
46 self.store.login(email, password, acls=self.acls)
47 self.addCleanup(self.store.logout)
48 self.assertTrue(self.snapd.login(email, password))
49 self.addCleanup(self.snapd.logout, email)
50
51 @staticmethod
52 def _get_developer_id(index=0):
53 """Return the developer_id (aka account_id)
54 :param index: the index of the user's list that want
55 to be returned from the config stack
56 """
57 store = Store()
58 store.login(*get_store_credentials(index=index))
59 developer_id = store.get_account_information()['account_id']
60 store.logout()
61 return developer_id
62
63 def _ensure_key_registered(self, user_index, name, passphrase='default'):
64 """Ensure the given key exists and is registered in the store
65 :param user_index: the index of the user you want to
66 register the key for
67 :param name: the key name
68 :param passphrase: the key passphrase
69 :return: True if key was registered successfully or if it is already
70 registered in the store, False otherwise
71 """
72 # Create the key if it does not exist
73 self.snapd.create_key(name, passphrase)
74 # export the key
75 key_exported = self.snapd.export_key(self._get_developer_id(user_index),
76 name, passphrase)
77 # Register the key in the store
78 try:
79 self.store.register_key(key_exported)
80 except errors.StoreKeyRegistrationError as kre:
81 if 'already the current revision' in str(kre).lower():
82 return True
83 elif 'authorization required' in str(kre).lower():
84 user, email = get_store_credentials(user_index)
85 self.store.logout()
86 self.store.login(user, email, acls=self.acls)
87 self.store.register_key(key_exported)
88 return True
89 else:
90 return False
91
92 @skip('The assertion type "snap-developer" is still in development')
93 def test_push_and_release_a_shared_snap(self):
94 """A user to whom a snap was shared via RESTful API pushes and releases
95 a new revision and it is available on the store
96 """
97 version = '0.1'
98 revision = 1
99 snap_name = get_random_snap_name()
100 user_key = 'default'
101 self.assertTrue(self.store.register(snap_name),
102 'Unable to register snap')
103 snap_id = self.store.filter_snaps(name=snap_name)[0].get_id()
104
105 # Upload the snap and wait for its processing on server to finish.
106 tracker = self.store.upload(
107 snap_name, self.snapcraft.build_and_pull(snap_name, version))
108 tracker.track()
109
110 # Build the snap-developer assertion
111 developers = [{self._get_developer_id(index=1): ('now',)}]
112 assertion = self.snapd.build_assertion(
113 'snap-developer',
114 publisher_id=self._get_developer_id(),
115 snap_id=snap_id,
116 developers=developers,
117 revision=revision)
118
119 # Ensure Key is registered for current user
120 self.assertTrue(self._ensure_key_registered(0, user_key))
121 # Sign the snap-developer assertion
122 assertion_signed = self.snapd.sign_assertion(assertion,
123 key=user_key)
124 self.store.share_snap(snap_name, assertion_signed)
125
126 # Login with the user to whom the snap was shared
127 # and verify he can push and release a new version of the snap
128 new_version = '0.2'
129 new_revision = 2
130 channels = ['candidate', 'stable']
131 self.store.logout()
132 self.snapcraft.logout()
133 self.store.login(*get_store_credentials(index=1))
134 self.snapcraft.login(*get_store_credentials(index=1))
135
136 # Push and release the snap to multiple channels
137 self.snapcraft.push(self.snapcraft.build(snap_name, new_version),
138 channels=channels)
139 # Check using REST API if snap was released to all channels
140 self.assertTrue(self.store.is_snap_published(snap_name,
141 new_version,
142 new_revision,
143 channels))
0144
=== modified file 'snappy_ecosystem_tests/utils/storeconfig.py'
--- snappy_ecosystem_tests/utils/storeconfig.py 2017-02-22 17:41:52 +0000
+++ snappy_ecosystem_tests/utils/storeconfig.py 2017-04-13 13:43:35 +0000
@@ -21,6 +21,7 @@
21"""Get all the store related data like credentials, urls"""21"""Get all the store related data like credentials, urls"""
2222
23import os23import os
24from configparser import NoSectionError
2425
25from snappy_ecosystem_tests.commons.config import (26from snappy_ecosystem_tests.commons.config import (
26 CONFIG_STACK,27 CONFIG_STACK,
@@ -31,16 +32,21 @@
31URL_WEB_STORE_STAGING = CONFIG_STACK.get('staging_urls', 'web')32URL_WEB_STORE_STAGING = CONFIG_STACK.get('staging_urls', 'web')
3233
3334
34def get_store_credentials():35def get_store_credentials(index=0):
35 """Get user store credentials.36 """Get user store credentials.
36 It will retrieve the credentials stored in a user config file,37 It will retrieve the credentials stored in a user config file,
37 if the config is not found,38 if the config is not found,
38 then it will return the ones provided in environment variables39 then it will return the ones provided in environment variables
40
41 :param index: the index of the user that want
42 to be returned from the user config stack
39 """43 """
40 return (USER_CONFIG_STACK.get('user', 'user_email',44 try:
41 default=os.environ.get('user_email')),45 section = USER_CONFIG_STACK.get_section('store_user_{}'.format(index))
42 USER_CONFIG_STACK.get('user', 'user_password',46 except (NoSectionError, KeyError):
43 default=os.environ.get('user_password')))47 section = USER_CONFIG_STACK.get_section('user')
48 return (section.get('user_email') or os.environ.get('user_email'),
49 section.get('user_password') or os.environ.get('user_password'))
4450
4551
46def is_staging():52def is_staging():

Subscribers

People subscribed via source and target branches