diff options
author | Jason A. Donenfeld | 2015-03-10 02:21:31 +0100 |
---|---|---|
committer | Jason A. Donenfeld | 2015-03-13 14:51:22 +0100 |
commit | 7511f4b4df521656d422010b06e2b5b96b21eb84 (patch) | |
tree | bdb2c66a9e048b75496aa83921043620646d6df3 | |
parent | f3ab1f178f6b7822fc701efbfc9dea8c294cb04f (diff) | |
download | cgit-7511f4b4df521656d422010b06e2b5b96b21eb84.tar.gz cgit-7511f4b4df521656d422010b06e2b5b96b21eb84.tar.bz2 cgit-7511f4b4df521656d422010b06e2b5b96b21eb84.zip |
filters: Add sample gentoo script
-rw-r--r-- | filters/gentoo-ldap-authentication.lua | 320 |
1 files changed, 320 insertions, 0 deletions
diff --git a/filters/gentoo-ldap-authentication.lua b/filters/gentoo-ldap-authentication.lua new file mode 100644 index 0000000..fce5632 --- /dev/null +++ b/filters/gentoo-ldap-authentication.lua | |||
@@ -0,0 +1,320 @@ | |||
1 | -- This script may be used with the auth-filter. Be sure to configure it as you wish. | ||
2 | -- | ||
3 | -- Requirements: | ||
4 | -- luacrypto >= 0.3 | ||
5 | -- <http://mkottman.github.io/luacrypto/> | ||
6 | -- lualdap >= 1.2 | ||
7 | -- <http://git.zx2c4.com/lualdap/about/> | ||
8 | -- | ||
9 | |||
10 | |||
11 | -- | ||
12 | -- | ||
13 | -- Configure these variables for your settings. | ||
14 | -- | ||
15 | -- | ||
16 | |||
17 | -- A list of password protected repositories, with which gentooAccess | ||
18 | -- group is allowed to access each one. | ||
19 | local protected_repos = { | ||
20 | glouglou = "infra", | ||
21 | portage = "dev" | ||
22 | } | ||
23 | |||
24 | |||
25 | -- All cookies will be authenticated based on this secret. Make it something | ||
26 | -- totally random and impossible to guess. It should be large. | ||
27 | local secret = "BE SURE TO CUSTOMIZE THIS STRING TO SOMETHING BIG AND RANDOM" | ||
28 | |||
29 | |||
30 | |||
31 | -- | ||
32 | -- | ||
33 | -- Authentication functions follow below. Swap these out if you want different authentication semantics. | ||
34 | -- | ||
35 | -- | ||
36 | |||
37 | -- Sets HTTP cookie headers based on post and sets up redirection. | ||
38 | function authenticate_post() | ||
39 | local redirect = validate_value("redirect", post["redirect"]) | ||
40 | |||
41 | if redirect == nil then | ||
42 | not_found() | ||
43 | return 0 | ||
44 | end | ||
45 | |||
46 | redirect_to(redirect) | ||
47 | |||
48 | local groups = gentoo_ldap_user_groups(post["username"], post["password"]) | ||
49 | if groups == nil then | ||
50 | set_cookie("cgitauth", "") | ||
51 | else | ||
52 | -- One week expiration time | ||
53 | set_cookie("cgitauth", secure_value("gentoogroups", table.concat(groups, ","), os.time() + 604800)) | ||
54 | end | ||
55 | |||
56 | html("\n") | ||
57 | return 0 | ||
58 | end | ||
59 | |||
60 | |||
61 | -- Returns 1 if the cookie is valid and 0 if it is not. | ||
62 | function authenticate_cookie() | ||
63 | local required_group = protected_repos[cgit["repo"]] | ||
64 | if required_group == nil then | ||
65 | -- We return as valid if the repo is not protected. | ||
66 | return 1 | ||
67 | end | ||
68 | |||
69 | local user_groups = validate_value("gentoogroups", get_cookie(http["cookie"], "cgitauth")) | ||
70 | if user_groups == nil or user_groups == "" then | ||
71 | return 0 | ||
72 | end | ||
73 | for group in string.gmatch(user_groups, "[^,]+") do | ||
74 | if group == required_group then | ||
75 | return 1 | ||
76 | end | ||
77 | end | ||
78 | return 0 | ||
79 | end | ||
80 | |||
81 | -- Prints the html for the login form. | ||
82 | function body() | ||
83 | html("<h2>Gentoo LDAP Authentication Required</h2>") | ||
84 | html("<form method='post' action='") | ||
85 | html_attr(cgit["login"]) | ||
86 | html("'>") | ||
87 | html("<input type='hidden' name='redirect' value='") | ||
88 | html_attr(secure_value("redirect", cgit["url"], 0)) | ||
89 | html("' />") | ||
90 | html("<table>") | ||
91 | html("<tr><td><label for='username'>Username:</label></td><td><input id='username' name='username' autofocus /></td></tr>") | ||
92 | html("<tr><td><label for='password'>Password:</label></td><td><input id='password' name='password' type='password' /></td></tr>") | ||
93 | html("<tr><td colspan='2'><input value='Login' type='submit' /></td></tr>") | ||
94 | html("</table></form>") | ||
95 | |||
96 | return 0 | ||
97 | end | ||
98 | |||
99 | -- | ||
100 | -- | ||
101 | -- Gentoo LDAP support. | ||
102 | -- | ||
103 | -- | ||
104 | |||
105 | local lualdap = require("lualdap") | ||
106 | |||
107 | function gentoo_ldap_user_groups(username, password) | ||
108 | -- Ensure the user is alphanumeric | ||
109 | if username:match("%W") then | ||
110 | return nil | ||
111 | end | ||
112 | |||
113 | local who = "uid=" .. username .. ",ou=devs,dc=gentoo,dc=org" | ||
114 | |||
115 | local ldap, err = lualdap.open_simple { | ||
116 | uri = "ldap://ldap1.gentoo.org", | ||
117 | who = who, | ||
118 | password = password, | ||
119 | starttls = true, | ||
120 | certfile = "/var/www/uwsgi/cgit/gentoo-ldap/star.gentoo.org.crt", | ||
121 | keyfile = "/var/www/uwsgi/cgit/gentoo-ldap/star.gentoo.org.key", | ||
122 | cacertfile = "/var/www/uwsgi/cgit/gentoo-ldap/ca.pem" | ||
123 | } | ||
124 | if ldap == nil then | ||
125 | return nil | ||
126 | end | ||
127 | |||
128 | local group_suffix = ".group" | ||
129 | local group_suffix_len = group_suffix:len() | ||
130 | local groups = {} | ||
131 | for dn, attribs in ldap:search { base = who, scope = "subtree" } do | ||
132 | local access = attribs["gentooAccess"] | ||
133 | if dn == who and access ~= nil then | ||
134 | for i, v in ipairs(access) do | ||
135 | local vlen = v:len() | ||
136 | if vlen > group_suffix_len and v:sub(-group_suffix_len) == group_suffix then | ||
137 | table.insert(groups, v:sub(1, vlen - group_suffix_len)) | ||
138 | end | ||
139 | end | ||
140 | end | ||
141 | end | ||
142 | |||
143 | ldap:close() | ||
144 | |||
145 | return groups | ||
146 | end | ||
147 | |||
148 | -- | ||
149 | -- | ||
150 | -- Wrapper around filter API, exposing the http table, the cgit table, and the post table to the above functions. | ||
151 | -- | ||
152 | -- | ||
153 | |||
154 | local actions = {} | ||
155 | actions["authenticate-post"] = authenticate_post | ||
156 | actions["authenticate-cookie"] = authenticate_cookie | ||
157 | actions["body"] = body | ||
158 | |||
159 | function filter_open(...) | ||
160 | action = actions[select(1, ...)] | ||
161 | |||
162 | http = {} | ||
163 | http["cookie"] = select(2, ...) | ||
164 | http["method"] = select(3, ...) | ||
165 | http["query"] = select(4, ...) | ||
166 | http["referer"] = select(5, ...) | ||
167 | http["path"] = select(6, ...) | ||
168 | http["host"] = select(7, ...) | ||
169 | http["https"] = select(8, ...) | ||
170 | |||
171 | cgit = {} | ||
172 | cgit["repo"] = select(9, ...) | ||
173 | cgit["page"] = select(10, ...) | ||
174 | cgit["url"] = select(11, ...) | ||
175 | cgit["login"] = select(12, ...) | ||
176 | |||
177 | end | ||
178 | |||
179 | function filter_close() | ||
180 | return action() | ||
181 | end | ||
182 | |||
183 | function filter_write(str) | ||
184 | post = parse_qs(str) | ||
185 | end | ||
186 | |||
187 | |||
188 | -- | ||
189 | -- | ||
190 | -- Utility functions based on keplerproject/wsapi. | ||
191 | -- | ||
192 | -- | ||
193 | |||
194 | function url_decode(str) | ||
195 | if not str then | ||
196 | return "" | ||
197 | end | ||
198 | str = string.gsub(str, "+", " ") | ||
199 | str = string.gsub(str, "%%(%x%x)", function(h) return string.char(tonumber(h, 16)) end) | ||
200 | str = string.gsub(str, "\r\n", "\n") | ||
201 | return str | ||
202 | end | ||
203 | |||
204 | function url_encode(str) | ||
205 | if not str then | ||
206 | return "" | ||
207 | end | ||
208 | str = string.gsub(str, "\n", "\r\n") | ||
209 | str = string.gsub(str, "([^%w ])", function(c) return string.format("%%%02X", string.byte(c)) end) | ||
210 | str = string.gsub(str, " ", "+") | ||
211 | return str | ||
212 | end | ||
213 | |||
214 | function parse_qs(qs) | ||
215 | local tab = {} | ||
216 | for key, val in string.gmatch(qs, "([^&=]+)=([^&=]*)&?") do | ||
217 | tab[url_decode(key)] = url_decode(val) | ||
218 | end | ||
219 | return tab | ||
220 | end | ||
221 | |||
222 | function get_cookie(cookies, name) | ||
223 | cookies = string.gsub(";" .. cookies .. ";", "%s*;%s*", ";") | ||
224 | return string.match(cookies, ";" .. name .. "=(.-);") | ||
225 | end | ||
226 | |||
227 | |||
228 | -- | ||
229 | -- | ||
230 | -- Cookie construction and validation helpers. | ||
231 | -- | ||
232 | -- | ||
233 | |||
234 | local crypto = require("crypto") | ||
235 | |||
236 | -- Returns value of cookie if cookie is valid. Otherwise returns nil. | ||
237 | function validate_value(expected_field, cookie) | ||
238 | local i = 0 | ||
239 | local value = "" | ||
240 | local field = "" | ||
241 | local expiration = 0 | ||
242 | local salt = "" | ||
243 | local hmac = "" | ||
244 | |||
245 | if cookie == nil or cookie:len() < 3 or cookie:sub(1, 1) == "|" then | ||
246 | return nil | ||
247 | end | ||
248 | |||
249 | for component in string.gmatch(cookie, "[^|]+") do | ||
250 | if i == 0 then | ||
251 | field = component | ||
252 | elseif i == 1 then | ||
253 | value = component | ||
254 | elseif i == 2 then | ||
255 | expiration = tonumber(component) | ||
256 | if expiration == nil then | ||
257 | expiration = -1 | ||
258 | end | ||
259 | elseif i == 3 then | ||
260 | salt = component | ||
261 | elseif i == 4 then | ||
262 | hmac = component | ||
263 | else | ||
264 | break | ||
265 | end | ||
266 | i = i + 1 | ||
267 | end | ||
268 | |||
269 | if hmac == nil or hmac:len() == 0 then | ||
270 | return nil | ||
271 | end | ||
272 | |||
273 | -- Lua hashes strings, so these comparisons are time invariant. | ||
274 | if hmac ~= crypto.hmac.digest("sha1", field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt, secret) then | ||
275 | return nil | ||
276 | end | ||
277 | |||
278 | if expiration == -1 or (expiration ~= 0 and expiration <= os.time()) then | ||
279 | return nil | ||
280 | end | ||
281 | |||
282 | if url_decode(field) ~= expected_field then | ||
283 | return nil | ||
284 | end | ||
285 | |||
286 | return url_decode(value) | ||
287 | end | ||
288 | |||
289 | function secure_value(field, value, expiration) | ||
290 | if value == nil or value:len() <= 0 then | ||
291 | return "" | ||
292 | end | ||
293 | |||
294 | local authstr = "" | ||
295 | local salt = crypto.hex(crypto.rand.bytes(16)) | ||
296 | value = url_encode(value) | ||
297 | field = url_encode(field) | ||
298 | authstr = field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt | ||
299 | authstr = authstr .. "|" .. crypto.hmac.digest("sha1", authstr, secret) | ||
300 | return authstr | ||
301 | end | ||
302 | |||
303 | function set_cookie(cookie, value) | ||
304 | html("Set-Cookie: " .. cookie .. "=" .. value .. "; HttpOnly") | ||
305 | if http["https"] == "yes" or http["https"] == "on" or http["https"] == "1" then | ||
306 | html("; secure") | ||
307 | end | ||
308 | html("\n") | ||
309 | end | ||
310 | |||
311 | function redirect_to(url) | ||
312 | html("Status: 302 Redirect\n") | ||
313 | html("Cache-Control: no-cache, no-store\n") | ||
314 | html("Location: " .. url .. "\n") | ||
315 | end | ||
316 | |||
317 | function not_found() | ||
318 | html("Status: 404 Not Found\n") | ||
319 | html("Cache-Control: no-cache, no-store\n\n") | ||
320 | end | ||