Merge ~cjwatson/squid-reverseproxy-charm:auth-helper into squid-reverseproxy-charm:master

Proposed by Colin Watson
Status: Merged
Approved by: Tom Haddon
Approved revision: bd6a9ec1ed48e75b59d9f0e446d87e4d3ea611b8
Merged at revision: 1b2ba3fe0f47a49721f445db9d31f9154e2b8144
Proposed branch: ~cjwatson/squid-reverseproxy-charm:auth-helper
Merge into: squid-reverseproxy-charm:master
Diff against target: 345 lines (+181/-2)
11 files modified
README.md (+30/-0)
config.yaml (+7/-0)
hooks/auth-helper-relation-broken (+1/-0)
hooks/auth-helper-relation-changed (+1/-0)
hooks/auth-helper-relation-departed (+1/-0)
hooks/auth-helper-relation-joined (+1/-0)
hooks/hooks.py (+56/-1)
hooks/tests/test_config_changed_hooks.py (+47/-1)
hooks/tests/test_helpers.py (+25/-0)
metadata.yaml (+4/-0)
templates/main_config.template (+8/-0)
Reviewer Review Type Date Requested Status
Tom Haddon Approve
Canonical IS Reviewers Pending
Review via email: mp+407896@code.launchpad.net

Commit message

Add authentication helper support

Description of the change

This allows relating a subordinate authentication helper charm to set up custom user authentication. We need this for Launchpad's builder proxy, which maintains a database of valid tokens that have been authorized by Launchpad's buildd-manager.

This commit is based on modifications to the squid-forwardproxy charm by Kit Randel in 2015. I've reworked them for squid-reverseproxy, moved most of the helper configuration into the subordinate charm's relation data, and added tests.

To post a comment you must log in.
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

This merge proposal is being monitored by mergebot. Change the status to Approved to merge.

Revision history for this message
Colin Watson (cjwatson) wrote (last edit ):

The implementation I've been working on for the other end of this can be found in https://code.launchpad.net/~cjwatson/rutabaga/+git/rutabaga/+merge/408430.

Revision history for this message
Tom Haddon (mthaddon) wrote :

LGTM, thx

review: Approve
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change successfully merged at revision 1b2ba3fe0f47a49721f445db9d31f9154e2b8144

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/README.md b/README.md
2index c4b3b56..71e7744 100644
3--- a/README.md
4+++ b/README.md
5@@ -81,6 +81,36 @@ A current implementation limitation is that it doesn't support multiple https vh
6 This charm provides relations that support monitoring via Nagios using
7 nrpe_external_master as a subordinate charm.
8
9+## Authentication Helpers
10+
11+To set up user authentication to the proxy, you need an authentication
12+helper, which you may need to supply yourself if none of the [built-in
13+helpers](https://wiki.squid-cache.org/Features/Authentication) are suitable.
14+You can do this by relating a subordinate charm providing the
15+`squid-auth-helper` interface to this charm. Such a charm may publish
16+relation data like this:
17+
18+ 'auth-params': yaml.dump([
19+ {
20+ 'scheme': 'basic',
21+ 'program': '/path/to/auth/helper',
22+ 'credentialsttl': '10 seconds',
23+ 'casesensitive': 'on',
24+ }
25+ ])
26+
27+This charm will turn that relation data into corresponding
28+[`auth_param`](http://www.squid-cache.org/Doc/config/auth_param/)
29+directives. You may need to use something like this as one of your
30+`auth_list` items to cause Squid to require authentication:
31+
32+ {"!proxy_auth": [REQUIRED], http_access: deny}
33+
34+If you do this, you should also set `wait_for_auth_helper: true` to cause
35+this charm to wait for the `auth-helper` relation before starting Squid, as
36+Squid will fail to start if it has a `proxy_auth` ACL without an
37+authentication scheme being configured.
38+
39 ## Caveats
40 The example above is just for reference. In order to make it usable, you
41 will have to supply a proper virtual host configuration for apache2.
42diff --git a/config.yaml b/config.yaml
43index e2232df..99267bb 100644
44--- a/config.yaml
45+++ b/config.yaml
46@@ -195,6 +195,13 @@ options:
47 stdin:
48
49 python3 -c 'import sys, yaml; print(yaml.dump(yaml.safe_load(sys.stdin.read())))'
50+ wait_for_auth_helper:
51+ type: boolean
52+ default: false
53+ description: >
54+ If true, do not start squid until an auth-helper relation is joined.
55+ This is useful if auth_list configuration (e.g. "proxy_auth REQUIRED")
56+ will cause squid to fail to start until an auth helper is available.
57 metrics_target:
58 default: ""
59 type: string
60diff --git a/hooks/auth-helper-relation-broken b/hooks/auth-helper-relation-broken
61new file mode 120000
62index 0000000..9416ca6
63--- /dev/null
64+++ b/hooks/auth-helper-relation-broken
65@@ -0,0 +1 @@
66+hooks.py
67\ No newline at end of file
68diff --git a/hooks/auth-helper-relation-changed b/hooks/auth-helper-relation-changed
69new file mode 120000
70index 0000000..9416ca6
71--- /dev/null
72+++ b/hooks/auth-helper-relation-changed
73@@ -0,0 +1 @@
74+hooks.py
75\ No newline at end of file
76diff --git a/hooks/auth-helper-relation-departed b/hooks/auth-helper-relation-departed
77new file mode 120000
78index 0000000..9416ca6
79--- /dev/null
80+++ b/hooks/auth-helper-relation-departed
81@@ -0,0 +1 @@
82+hooks.py
83\ No newline at end of file
84diff --git a/hooks/auth-helper-relation-joined b/hooks/auth-helper-relation-joined
85new file mode 120000
86index 0000000..9416ca6
87--- /dev/null
88+++ b/hooks/auth-helper-relation-joined
89@@ -0,0 +1 @@
90+hooks.py
91\ No newline at end of file
92diff --git a/hooks/hooks.py b/hooks/hooks.py
93index da59000..73672a0 100755
94--- a/hooks/hooks.py
95+++ b/hooks/hooks.py
96@@ -148,6 +148,17 @@ def get_reverse_sites():
97 return all_sites
98
99
100+def get_auth_params():
101+ reldata = []
102+ for relid in get_relation_ids('auth-helper'):
103+ units = related_units(relid)
104+ for unit in units:
105+ data = relation_get(rid=relid, unit=unit)
106+ if 'auth-params' in data:
107+ reldata.extend(yaml.safe_load(data['auth-params']))
108+ return reldata
109+
110+
111 def ensure_package_status(packages, status):
112 if status in ['install', 'hold']:
113 selections = ''.join(['{} {}\n'.format(package, status)
114@@ -342,6 +353,7 @@ def construct_squid3_config():
115 'cache_dir': cache_dir,
116 'dhparams_file': dhparams_file,
117 'snmp_allowed_ips': snmp_allowed_ips,
118+ 'auth_params': get_auth_params(),
119 }
120 template = render_template('main_config.template', templ_vars)
121 write_squid3_config('\n'.join(
122@@ -548,6 +560,21 @@ def install_hook():
123 return True
124
125
126+def service_can_start():
127+ # If wait_for_auth_helper is set, wait until squid has started and the
128+ # squid-auth-helper relation has been joined.
129+ config_data = config_get()
130+ if not config_data["wait_for_auth_helper"]:
131+ return True
132+ if is_relation_made("auth-helper", keys=["auth-params"]):
133+ log("Squid auth helper available, squid may start...")
134+ return True
135+ else:
136+ log("Squid not ready, waiting for auth helper...")
137+ service_squid3("stop")
138+ return False
139+
140+
141 def config_changed():
142 old_service_ports = get_service_ports()
143 old_sitenames = get_sitenames()
144@@ -560,10 +587,16 @@ def config_changed():
145 metrics_config_path,
146 metrics_cronjob_path)
147
148+ if not service_can_start():
149+ return
150+
151 if service_squid3("check"):
152 updated_service_ports = get_service_ports()
153 update_service_ports(old_service_ports, updated_service_ports)
154- service_squid3("reload")
155+ if service_squid3("status"):
156+ service_squid3("reload")
157+ else:
158+ service_squid3("start")
159 if not (old_sitenames == get_sitenames()):
160 notify_cached_website()
161 else:
162@@ -575,6 +608,12 @@ def config_changed():
163
164
165 def start_hook():
166+ if service_can_start():
167+ if not service_squid3("check"):
168+ sys.exit(1)
169+ else:
170+ return
171+
172 if service_squid3("status"):
173 return service_squid3("restart")
174 else:
175@@ -603,6 +642,13 @@ def cached_website_interface(hook_name=None):
176 config_changed()
177
178
179+def auth_helper_interface(hook_name=None):
180+ if hook_name is None:
181+ return
182+ if hook_name in ("joined", "changed", "broken", "departed"):
183+ config_changed()
184+
185+
186 def get_hostname(host=None):
187 my_host = socket.gethostname()
188 if host is None or host == "0.0.0.0":
189@@ -668,6 +714,15 @@ def main(hook_name):
190 elif hook_name == "cached-website-relation-departed":
191 cached_website_interface("departed")
192
193+ elif hook_name == "auth-helper-relation-joined":
194+ auth_helper_interface("joined")
195+ elif hook_name == "auth-helper-relation-changed":
196+ auth_helper_interface("changed")
197+ elif hook_name == "auth-helper-relation-broken":
198+ auth_helper_interface("broken")
199+ elif hook_name == "auth-helper-relation-departed":
200+ auth_helper_interface("departed")
201+
202 elif hook_name in ("nrpe-external-master-relation-joined",
203 "local-monitors-relation-joined"):
204 update_nrpe_checks()
205diff --git a/hooks/tests/test_config_changed_hooks.py b/hooks/tests/test_config_changed_hooks.py
206index 4b7e66f..34ba268 100644
207--- a/hooks/tests/test_config_changed_hooks.py
208+++ b/hooks/tests/test_config_changed_hooks.py
209@@ -1,4 +1,7 @@
210-from unittest.mock import patch
211+from unittest.mock import (
212+ call,
213+ patch,
214+)
215
216 from testtools import TestCase
217
218@@ -15,6 +18,7 @@ class ConfigChangedTest(TestCase):
219 "construct_squid3_config")
220 self.update_nrpe_checks = self.patch_hook(
221 "update_nrpe_checks")
222+ self.is_relation_made = self.patch_hook("is_relation_made")
223 self.update_service_ports = self.patch_hook(
224 "update_service_ports")
225 self.service_squid3 = self.patch_hook(
226@@ -61,3 +65,45 @@ class ConfigChangedTest(TestCase):
227 hooks.config_changed()
228
229 self.assertFalse(self.notify_cached_website.called)
230+
231+ def test_config_changed_no_wait_for_auth_helper(self):
232+ self.config_get.return_value = {"wait_for_auth_helper": False}
233+ self.service_squid3.side_effect = (
234+ True, # check
235+ False, # status
236+ True, # start
237+ )
238+
239+ hooks.config_changed()
240+
241+ self.is_relation_made.assert_not_called()
242+ self.assertEqual(
243+ [call("check"), call("status"), call("start")],
244+ self.service_squid3.call_args_list)
245+
246+ def test_config_changed_wait_for_auth_helper_no_relation(self):
247+ self.config_get.return_value = {"wait_for_auth_helper": True}
248+ self.is_relation_made.return_value = False
249+
250+ hooks.config_changed()
251+
252+ self.is_relation_made.assert_called_once_with(
253+ "auth-helper", keys=["auth-params"])
254+ self.service_squid3.assert_called_once_with("stop")
255+
256+ def test_config_changed_wait_for_auth_helper_relation(self):
257+ self.config_get.return_value = {"wait_for_auth_helper": True}
258+ self.is_relation_made.return_value = True
259+ self.service_squid3.side_effect = (
260+ True, # check
261+ False, # status
262+ True, # start
263+ )
264+
265+ hooks.config_changed()
266+
267+ self.is_relation_made.assert_called_once_with(
268+ "auth-helper", keys=["auth-params"])
269+ self.assertEqual(
270+ [call("check"), call("status"), call("start")],
271+ self.service_squid3.call_args_list)
272diff --git a/hooks/tests/test_helpers.py b/hooks/tests/test_helpers.py
273index f52117f..0f91738 100644
274--- a/hooks/tests/test_helpers.py
275+++ b/hooks/tests/test_helpers.py
276@@ -45,6 +45,7 @@ class SquidConfigTest(TestCase):
277 super(SquidConfigTest, self).setUp()
278 self.get_reverse_sites = self._apply_patch('hooks.get_reverse_sites')
279 self.get_forward_sites = self._apply_patch('hooks.get_forward_sites')
280+ self.get_auth_params = self._apply_patch('hooks.get_auth_params')
281 self.config_get = self._apply_patch('hooks.config_get')
282 self.mkdir = self._apply_patch('os.mkdir')
283 self.chmod = self._apply_patch('os.chmod')
284@@ -569,3 +570,27 @@ class SquidConfigTest(TestCase):
285 """,
286 )
287 hooks.construct_squid3_config()
288+
289+ def test_auth_params(self):
290+ self.config_get.return_value = Serializable({
291+ "refresh_patterns": "",
292+ "cache_size_mb": 1024,
293+ "cache_mem_mb": 256,
294+ "target_objs_per_dir": 1024,
295+ "avg_obj_size_kb": 1024,
296+ })
297+ self.get_auth_params.return_value = [{
298+ "scheme": "basic",
299+ "program": "/path/to/helper argument",
300+ "credentialsttl": "1 second",
301+ "casesensitive": "on",
302+ }]
303+ self.get_reverse_sites.return_value = None
304+ self.write_squid3_config.side_effect = self._assert_contents(
305+ """
306+ auth_param basic program /path/to/helper argument
307+ auth_param basic credentialsttl 1 second
308+ auth_param basic casesensitive on
309+ """,
310+ )
311+ hooks.construct_squid3_config()
312diff --git a/metadata.yaml b/metadata.yaml
313index dd673af..09d34cf 100644
314--- a/metadata.yaml
315+++ b/metadata.yaml
316@@ -31,6 +31,10 @@ provides:
317 local-monitors:
318 interface: local-monitors
319 scope: container
320+ auth-helper:
321+ interface: squid-auth-helper
322+ scope: container
323+ optional: true
324 requires:
325 website:
326 interface: http
327diff --git a/templates/main_config.template b/templates/main_config.template
328index cbe1ebf..b331488 100644
329--- a/templates/main_config.template
330+++ b/templates/main_config.template
331@@ -79,6 +79,14 @@ refresh_pattern {{ rp }} {{ refresh_patterns[rp]['min'] }} {{ refresh_patterns[r
332 {% endfor -%}
333 refresh_pattern . {{default_refresh_pattern.min}} {{default_refresh_pattern.percent}}% {{default_refresh_pattern.max}} {{ ' '.join(default_refresh_pattern.options) }}
334
335+{% for auth in auth_params %}
336+ {% for key, val in auth.items() -%}
337+ {% if key != 'scheme' -%}
338+auth_param {{ auth['scheme'] }} {{ key }} {{ val }}
339+ {% endif -%}
340+ {% endfor -%}
341+{% endfor %}
342+
343 # These are needed by the "squidpeers" Nagios check. We place them before
344 # custom ACLs since it's otherwise difficult to write custom ACLs that do
345 # things like "deny all unauthenticated requests" without breaking that

Subscribers

People subscribed via source and target branches

to all changes: