Merge lp:~canonical-platform-qa/snappy-ecosystem-tests/assertions-signing-mechanism into lp:snappy-ecosystem-tests
- assertions-signing-mechanism
- Merge into trunk
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 |
Related bugs: |
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.
error: invalid assertion type: snap-developer
Important note: To allow supporting multiple users, a change was made in get_remote_
[store_user_0]
user_email=
user_password=
[store_user_1]
user_email=
user_password=
[user]
snapd_hostname_
snapd_username_
snapd_port_remote=
snapcraft_
snapcraft_
snapcraft_
See README file for more details.
@run_tests: snappy_
platform-qa-bot (platform-qa-bot) wrote : | # |
platform-qa-bot (platform-qa-bot) wrote : | # |
PASSED: Continuous integration, rev:55
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
- 56. By Heber Parrucci
-
merge from trunk
platform-qa-bot (platform-qa-bot) wrote : | # |
PASSED: Continuous integration, rev:56
https:/
Executed test runs:
UNSTABLE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
platform-qa-bot (platform-qa-bot) wrote : | # |
FAILED: Continuous integration, rev:56
https:/
Executed test runs:
UNSTABLE: https:/
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
platform-qa-bot (platform-qa-bot) wrote : | # |
PASSED: Continuous integration, rev:56
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Preview Diff
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(): |
FAILED: Continuous integration, rev:55 /platform- qa-jenkins. ubuntu. com/job/ snappy- ecosystem- tests-ci/ 349/ /platform- qa-jenkins. ubuntu. com/job/ generic- update- mp/2338/ console
https:/
Executed test runs:
None: https:/
Click here to trigger a rebuild: /platform- qa-jenkins. ubuntu. com/job/ snappy- ecosystem- tests-ci/ 349/rebuild
https:/