Merge lp:~danilo/landscape-charm/license-file-plain-text into lp:~landscape/landscape-charm/trunk

Proposed by Данило Шеган
Status: Merged
Approved by: Данило Шеган
Approved revision: 314
Merged at revision: 315
Proposed branch: lp:~danilo/landscape-charm/license-file-plain-text
Merge into: lp:~landscape/landscape-charm/trunk
Diff against target: 186 lines (+94/-27)
2 files modified
lib/callbacks/filesystem.py (+19/-2)
lib/callbacks/tests/test_filesystem.py (+75/-25)
To merge this branch: bzr merge lp:~danilo/landscape-charm/license-file-plain-text
Reviewer Review Type Date Requested Status
Björn Tillenius (community) Approve
Geoff Teale (community) Approve
🤖 Landscape Builder test results Approve
Review via email: mp+261837@code.launchpad.net

Commit message

Allow license files to be put in as plain text as well

After some discussion, we've decided to support both plain text and base64-encoded data for license-file charm config option (along with URLs to fetch the license from).

This means that we need to base64-decode only when really needed, and base64 module is not helping there.

Description of the change

Allow license files to be put in as plain text as well

After some discussion, we've decided to support both plain text and base64-encoded data for license-file charm config option (along with URLs to fetch the license from).

This means that we need to base64-decode only when really needed, and base64 module is not helping there.

To test, you can use landscape.yaml from https://pastebin.canonical.com/133087/ (and feel free to change license-file values) and then deploy with

  juju-deployer -vdW -w90 -c landscape.yaml landscape

/etc/landscape/license.txt should have the actual value.

To allow people to input license-file field with newlines and accidental padding, we are also allowing whitespaces (\t\r\n and space) in our base64 alphabet. Maybe that's too broad.

Note: there seems to be a Python documentation bug. https://docs.python.org/2.7/library/base64.html talks about b64decode throwing TypeError "if there are non-alphabet characters present in the string", but that doesn't happen:
https://pastebin.canonical.com/133085/ — I will report a Python doc bug later (it doesn't happen on 2.7.3 either). There's actually https://bugs.python.org/issue1466065 already, but I can't reach bugs.python.org atm.

To post a comment you must log in.
Revision history for this message
Geoff Teale (tealeg) wrote :

I might be missing something, but this seems like an incredible brittle way to decide whether something is base64 encoded or not.

review: Needs Fixing
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: make ci-test
Result: Success
Revno: 313
Branch: lp:~danilo/landscape-charm/license-file-plain-text
Jenkins: https://ci.lscape.net/job/latch-test/1395/

review: Approve (test results)
Revision history for this message
Björn Tillenius (bjornt) :
314. By Данило Шеган

Treat whitespace mid-line in license-file value as an indication of plain-text data.

Revision history for this message
Данило Шеган (danilo) wrote :

Addressed your review comments, gents. Let me know what you think?

Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: make ci-test
Result: Success
Revno: 314
Branch: lp:~danilo/landscape-charm/license-file-plain-text
Jenkins: https://ci.lscape.net/job/latch-test/1412/

review: Approve (test results)
Revision history for this message
Geoff Teale (tealeg) wrote :

OK, the explanation is good enough for me - by killing whitespace in the middle of lines we've killed of a lot of cases.

I still don't like this kind of mechanism, but I guess there isn't too much we can do.

review: Approve
Revision history for this message
Данило Шеган (danilo) wrote :

Oh, I agree I don't like it either: I would have we rather went one way or the other (base64 or plain text), but since we've got SSL certs being base64 already (inspired by apache2 [and haproxy] charms), I'd hate us to not support the same format for license-file as well, even though I believe plain-text is way more natural (I'd like SSL certs to support plain text as well, but not with this branch :)).

Revision history for this message
Björn Tillenius (bjornt) wrote :

Looks good, +1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/callbacks/filesystem.py'
2--- lib/callbacks/filesystem.py 2015-06-11 12:39:51 +0000
3+++ lib/callbacks/filesystem.py 2015-06-15 10:08:40 +0000
4@@ -2,6 +2,7 @@
5
6 import os
7 import base64
8+import re
9 import urllib2
10
11 from charmhelpers.core import host
12@@ -12,6 +13,12 @@
13 from lib.utils import get_required_data
14
15
16+# Python base64 module will decode strings which might contain non-base64
17+# alphabet characters. To ensure we can support both plain text and
18+# base64-encoded values, we pre-check strings against a base64 alphabet.
19+BASE64_ALPHABET_RE = re.compile("^[A-Za-z0-9+/]+[=]{0,2}$")
20+
21+
22 class LicenseFileUnreadableError(CharmError):
23 """Unable to read the license file."""
24
25@@ -108,11 +115,21 @@
26 except urllib2.URLError:
27 raise LicenseFileUnreadableError(license_file_value)
28 else:
29+ use_plain_text = False
30+ # Strip leading and trailing whitespace from each line
31+ # to check for base64-encoded data.
32+ stripped_license_value = "".join(
33+ [line.strip() for line in license_file_value.splitlines()])
34 try:
35- license_data = base64.b64decode(license_file_value)
36+ if BASE64_ALPHABET_RE.match(stripped_license_value):
37+ license_data = base64.b64decode(stripped_license_value)
38+ else:
39+ use_plain_text = True
40 except TypeError:
41- raise LicenseDataBase64DecodeError()
42+ use_plain_text = True
43
44+ if use_plain_text:
45+ license_data = license_file_value
46 self._host.write_file(
47 self._paths.license_file(), license_data,
48 owner="landscape", group="root", perms=0o640)
49
50=== modified file 'lib/callbacks/tests/test_filesystem.py'
51--- lib/callbacks/tests/test_filesystem.py 2015-06-05 09:47:11 +0000
52+++ lib/callbacks/tests/test_filesystem.py 2015-06-15 10:08:40 +0000
53@@ -10,7 +10,7 @@
54 from lib.tests.rootdir import RootDir
55 from lib.callbacks.filesystem import (
56 EnsureConfigDir, WriteCustomSSLCertificate, WriteLicenseFile,
57- LicenseFileUnreadableError, LicenseDataBase64DecodeError)
58+ LicenseFileUnreadableError)
59
60
61 class EnsureConfigDirTest(HookenvTest):
62@@ -91,7 +91,7 @@
63
64 self.assertEqual([], self.host.calls)
65
66- def test_license_file_data(self):
67+ def test_license_file_base64_data(self):
68 """
69 If the config specifies a license file data directly as
70 the base64-encoded value, it is decoded and written
71@@ -113,25 +113,76 @@
72 {'owner': 'landscape', 'group': 'root', 'perms': 0o640})
73 ], self.host.calls)
74
75- def test_license_file_bad_data(self):
76- """
77- When license-file is not a URL and not base64-encoded data, fails
78- with LicenseDataBase64DecodeError.
79- """
80- self.addCleanup(setattr, urllib2, "urlopen", urllib2.urlopen)
81-
82- manager = ServiceManager([{
83- "service": "landscape",
84- "required_data": [
85- {"config": {
86- "license-file": "bad data",
87- }},
88- ],
89- }])
90- with self.assertRaises(LicenseDataBase64DecodeError) as error:
91- self.callback(manager, "landscape", None)
92- self.assertEqual(
93- "Error base64-decoding license-file data.", str(error.exception))
94+ def test_license_file_base64_with_whitespaces(self):
95+ """
96+ If the base64-encoded data is formatted into multiple lines with
97+ some leading and trailing whitespace, it is still detected as base64
98+ and decoded accordingly.
99+ """
100+ license_data = base64.b64encode('Test license data')
101+ formatted_data = " \t" + license_data[:12] + "\t\n" + license_data[12:]
102+ manager = ServiceManager([{
103+ "service": "landscape",
104+ "required_data": [
105+ {"config": {
106+ "license-file": formatted_data,
107+ }},
108+ ],
109+ }])
110+ self.callback(manager, "landscape", None)
111+
112+ self.assertEqual([
113+ ("write_file", ('/etc/landscape/license.txt', 'Test license data'),
114+ {'owner': 'landscape', 'group': 'root', 'perms': 0o640})
115+ ], self.host.calls)
116+
117+ def test_license_file_bad_base64_padding(self):
118+ """
119+ When license-file is a base64-decoded string with missing padding,
120+ it is written out as plain text string.
121+ """
122+ self.addCleanup(setattr, urllib2, "urlopen", urllib2.urlopen)
123+ # Strip '=\n' from the end of the base64-encoded string.
124+ license_data = base64.b64encode("Test license data")[:-2]
125+ manager = ServiceManager([{
126+ "service": "landscape",
127+ "required_data": [
128+ {"config": {
129+ "license-file": license_data,
130+ }},
131+ ],
132+ }])
133+ self.callback(manager, "landscape", None)
134+ self.assertEqual([
135+ ("write_file",
136+ ('/etc/landscape/license.txt', license_data),
137+ {'owner': 'landscape', 'group': 'root', 'perms': 0o640})
138+ ], self.host.calls)
139+
140+ def test_license_file_plain_text(self):
141+ """
142+ When license-file is neither an URL nor base64-encoded data,
143+ it is written out as plain text string.
144+ """
145+ self.addCleanup(setattr, urllib2, "urlopen", urllib2.urlopen)
146+
147+ # Whitespaces in the middle of the text indicate it is not
148+ # base64-encoded data.
149+ license_data = "plain text license data"
150+ manager = ServiceManager([{
151+ "service": "landscape",
152+ "required_data": [
153+ {"config": {
154+ "license-file": license_data,
155+ }},
156+ ],
157+ }])
158+ self.callback(manager, "landscape", None)
159+ self.assertEqual([
160+ ("write_file",
161+ ('/etc/landscape/license.txt', 'plain text license data'),
162+ {'owner': 'landscape', 'group': 'root', 'perms': 0o640})
163+ ], self.host.calls)
164
165 def test_license_file_file_url(self):
166 """
167@@ -162,7 +213,7 @@
168
169 def test_license_file_http_url(self):
170 """
171- If the config specifies a license file using a local file:// URL,
172+ If the config specifies a license file using a http:// URL,
173 contents of that file are transferred verbatim to the license file
174 on the unit.
175 """
176@@ -191,9 +242,8 @@
177
178 def test_license_file_bad_url(self):
179 """
180- If the config specifies a license file using a local file:// URL,
181- contents of that file are transferred verbatim to the license file
182- on the unit.
183+ If the config specifies a license file using a non-existent http URL,
184+ LicenseFileUnreadableError is raised.
185 """
186 self.addCleanup(setattr, urllib2, "urlopen", urllib2.urlopen)
187

Subscribers

People subscribed via source and target branches