/dylan/access.dylan

http://github.com/cgay/wiki · Unknown · 380 lines · 333 code · 47 blank · 0 comment · 0 complexity · 5d7237e0501cf227b6943b0023e9b863 MD5 · raw file

  1. Module: %wiki
  2. //// Access Control Lists
  3. /*
  4. How ACLs work...
  5. There are three types of access defined:
  6. * read-content
  7. * modify-content
  8. * modify-acls
  9. In the future it may be useful to define additional types of access.
  10. Permissions may be granted to specific users, groups of users, and
  11. three pre-defined groups:
  12. * anyone -- anyone at all
  13. * trusted -- anyone who can login
  14. * owner -- owner of the page
  15. Any access rule may be negated, so that for example "deny anyone"
  16. or "deny <user>" may be specified. In the code this is represented
  17. as a sequence such as #($deny, <user>).
  18. If no ACLs are set for a page then the default ACLs are used instead.
  19. (The defaults need to be made configurable.)
  20. Once you set any ACLs at all for a page, they fully determine the
  21. access rights. For example, there is no global default for everyone
  22. to be able to read a page unless explicitly disabled. This means,
  23. for example, that if you want to prevent a specific group from
  24. viewing a page you must specify a read-content ACL like this:
  25. deny <group>
  26. allow anyone
  27. If "anyone" isn't included then no read access is granted to anyone
  28. (including the group).
  29. ACLs are tested in order; the first access rule to match either
  30. grants or denies the permission. For example, if an admin or the
  31. page owner wants to quickly disable a page they could simply add
  32. "deny anyone" at the front of the ACLs.
  33. Note that admins are always allowed all access. There is also no
  34. mechanism to deny access to the owner of a page via acls. If you
  35. need to do that, disable the owner's account or change the ownership
  36. of the page.
  37. Example: Anyone but cgay and group1 can view content
  38. view-content: list(list($deny, <user cgay>),
  39. list($deny, <group group1>),
  40. list($allow, $anyone))
  41. */
  42. define constant $view-content = #"view-content";
  43. define constant $modify-content = #"modify-content";
  44. define constant $modify-acls = #"modify-acls";
  45. define constant <acl-operation> :: <type>
  46. = one-of($view-content, $modify-content, $modify-acls);
  47. define constant $anyone = #"anyone";
  48. define constant $trusted = #"trusted";
  49. define constant $owner = #"owner";
  50. define constant $allow = #"allow";
  51. define constant $deny = #"deny";
  52. // The compiler barfs on this when I uncomment things.
  53. //
  54. define constant <rule-target> //:: <type>
  55. = type-union(<symbol>, //one-of($anyone, $trusted, $owner),
  56. <wiki-user>,
  57. <wiki-group>);
  58. define class <rule> (<object>)
  59. constant slot rule-action :: one-of($allow, $deny),
  60. required-init-keyword: action:;
  61. constant slot rule-target :: <rule-target>,
  62. required-init-keyword: target:;
  63. end;
  64. define class <acls> (<object>)
  65. // Must hold this lock before modifying the values in any
  66. // of the other slots.
  67. //constant slot acls-lock :: <simple-lock> = make(<simple-lock>);
  68. // The following are sequences of <rule>, but limited collections
  69. // are too broken to use in my experience. :-(
  70. slot view-content-rules :: <sequence> = #(),
  71. init-keyword: view-content:;
  72. slot modify-content-rules :: <sequence> = #(),
  73. init-keyword: modify-content:;
  74. slot modify-acls-rules :: <sequence> = #(),
  75. init-keyword: modify-acls:;
  76. end class <acls>;
  77. define method make
  78. (class :: subclass(<acls>), #key view-content, modify-content, modify-acls)
  79. => (object :: <acls>)
  80. // Make sure the ACLs can't be modified by mutating the sequences
  81. // after creation.
  82. apply(next-method, class,
  83. view-content: slice(view-content, 0, #f),
  84. modify-content: slice(modify-content, 0, #f),
  85. modify-acls: slice(modify-acls, 0, #f),
  86. #())
  87. end method make;
  88. define method remove-rules-for-target
  89. (acls :: <acls>, target :: <rule-target>)
  90. local method not-for-target (rule)
  91. rule.rule-target ~= target
  92. end;
  93. //with-lock ($acls-lock /* acls.acls-lock */)
  94. acls.view-content-rules := choose(not-for-target, acls.view-content-rules);
  95. acls.modify-content-rules := choose(not-for-target, acls.modify-content-rules);
  96. acls.modify-acls-rules := choose(not-for-target, acls.modify-acls-rules);
  97. //end;
  98. end method remove-rules-for-target;
  99. // Default access controls applied to pages that don't otherwise specify
  100. // any ACLs. (But admins are omnipotent.)
  101. //
  102. define constant $default-access-controls
  103. = make(<acls>,
  104. view-content: list(make(<rule>, action: $allow, target: $anyone)),
  105. modify-content: list(make(<rule>, action: $allow, target: $trusted)),
  106. modify-acls: list(make(<rule>, action: $allow, target: $owner)));
  107. define method has-permission?
  108. (user :: false-or(<wiki-user>),
  109. page :: false-or(<wiki-page>),
  110. requested-operation :: <acl-operation>)
  111. => (has-permission? :: <boolean>)
  112. // If user is #f then they're not logged in.
  113. // If page is #f then it's a new page being created.
  114. // Admins can do anything for now. Eventually it may be useful to have
  115. // two levels of admins: those who can modify any content and those who
  116. // can do anything.
  117. // Page owner cannot be denied access either. (If an admin wants to do
  118. // that they should be able to handle it via other means, such as disabling
  119. // the account or changing the page owner.) I'm actually not totally sure
  120. // this is the right design decision. My thinking is that it's too easy
  121. // for the owner to accidentally lock him/herself out, and it could generate
  122. // a lot of admin requests.
  123. if (user & (~page | administrator?(user) | page.page-owner = user))
  124. #t
  125. else
  126. let acls :: <acls> = iff(page,
  127. page.page-access-controls,
  128. $default-access-controls);
  129. let rules :: <sequence> = select (requested-operation)
  130. $view-content => acls.view-content-rules;
  131. $modify-content => acls.modify-content-rules;
  132. $modify-acls => acls.modify-acls-rules;
  133. end;
  134. block (return)
  135. for (rule in rules)
  136. let action = rule.rule-action;
  137. let target = rule.rule-target;
  138. // First match wins
  139. if (target = $anyone
  140. | (user
  141. & (target = user
  142. | (target = $trusted & user = authenticated-user())
  143. | (target = $owner & page & page.page-owner = user)
  144. | (instance?(target, <wiki-group>)
  145. & member?(user, target.group-members)))))
  146. return(action = $allow)
  147. end if;
  148. end for;
  149. #f // default is no permission if no rule matches
  150. end block
  151. end if
  152. end method has-permission?;
  153. // Turn a user-entered string into the internal representation of rules.
  154. // The string is one rule per line. '!' means deny. Lack of '!' means
  155. // allow. e.g., "!cgay\n!foo\ntrusted". Blank lines and '!' on a line
  156. // by itself are removed. If there's an error parsing a rule, such as
  157. // user not found, then instead of the parsed rule a list of
  158. // #(original-rule, error-message) is returned. For example:
  159. // "!no-such-user"
  160. // =>
  161. // #(#("!no-such-user", "User no-such-user not found."))
  162. // The caller can use this for error reporting purposes.
  163. //
  164. define method parse-rules
  165. (rules :: <string>) => (rules :: <sequence>, error? :: <boolean>)
  166. let error? = #f;
  167. local method parse-one-rule (rule)
  168. let (parsed-rule, err?) = parse-rule(rule);
  169. if (parsed-rule)
  170. error? := error? | err?;
  171. parsed-rule
  172. end
  173. end;
  174. values(choose(identity, map(parse-one-rule, split(rules, '\n'))),
  175. error?)
  176. end method parse-rules;
  177. // Return two values: a parsed rule and whether or not there were any errors.
  178. // If there is an error, such as group not found, include
  179. // #(original-rule, error-message) for the erring rule. (This is crufty and
  180. // should be improved.)
  181. //
  182. define method parse-rule
  183. (rule :: <string>)
  184. => (rule, errors? :: <boolean>)
  185. let rule = strip(rule);
  186. let action = $allow;
  187. if (rule.size > 0)
  188. if (rule[0] = '!')
  189. action := $deny;
  190. rule := copy-sequence(rule, start: 1);
  191. end;
  192. if (rule.size > 0)
  193. select (as-lowercase(rule) by \=)
  194. "trusted" => make(<rule>, action: action, target: $trusted);
  195. "anyone" => make(<rule>, action: action, target: $anyone);
  196. "owner" => make(<rule>, action: action, target: $owner);
  197. otherwise =>
  198. let target = find-user(rule) | find-group(rule);
  199. if (target)
  200. make(<rule>, action: action, target: target)
  201. else
  202. let msg = format-to-string("No user or group named %s was found.",
  203. rule);
  204. values(list(rule, msg), #t)
  205. end;
  206. end
  207. end
  208. end
  209. end method parse-rule;
  210. // Turn the internal representation of rules into something users
  211. // can read and edit.
  212. //
  213. define method unparse-rules
  214. (rules :: <sequence>) => (rules :: <string>)
  215. join(map(unparse-rule, rules), "\n")
  216. end;
  217. define method unparse-rule
  218. (rule :: <rule>) => (rule :: <string>)
  219. let action = rule.rule-action;
  220. let target = rule.rule-target;
  221. concatenate(select (action)
  222. $allow => "";
  223. $deny => "!";
  224. end,
  225. select (target by instance?)
  226. <symbol> => as-lowercase(as(<string>, target));
  227. <wiki-user> => target.user-name;
  228. <wiki-group> => target.group-name;
  229. otherwise => error("Invalid rule target: %s", target);
  230. end)
  231. end method unparse-rule;
  232. define class <acls-page> (<wiki-dsp>)
  233. end;
  234. define method respond-to-get
  235. (acls-page :: <acls-page>, #key title :: <string>)
  236. let wiki-page = find-or-load-page(percent-decode(title));
  237. if (wiki-page)
  238. set-attribute(page-context(), "owner-name", wiki-page.page-owner.user-name);
  239. dynamic-bind (*page* = wiki-page)
  240. next-method()
  241. end;
  242. else
  243. redirect-to(*non-existing-page-page*);
  244. end;
  245. end method respond-to-get;
  246. // Handle the page access form submission.
  247. // todo -- Redisplay the user-entered text when there's an error, but with
  248. // a * next to the broken entries? <wiki:show-rules> needs to display
  249. // this text instead of the actual page rules.
  250. // Display an error message too.
  251. //
  252. define method respond-to-post
  253. (acls-page :: <acls-page>, #key title :: <string>)
  254. let wiki-page = find-or-load-page(percent-decode(title));
  255. if (~wiki-page)
  256. // Someone used an old URL or typed it in by hand...
  257. resource-not-found-error(url: request-url(current-request()));
  258. end;
  259. with-query-values (view-content, modify-content, modify-acls, comment, owner-name)
  260. let owner = strip(owner-name);
  261. let new-owner = ~empty?(owner) & find-user(owner);
  262. let owner-err? = ~empty?(owner) & ~new-owner;
  263. let (vc-rules, vc-err?) = parse-rules(view-content);
  264. let (mc-rules, mc-err?) = parse-rules(modify-content);
  265. let (ma-rules, ma-err?) = parse-rules(modify-acls);
  266. if (owner-err? | vc-err? | mc-err? | ma-err?)
  267. if (owner-err?)
  268. add-field-error("owner-name",
  269. "Cannot set owner to %s; user not found.", owner);
  270. end;
  271. local method note-errors (rules, field-name)
  272. for (rule in rules)
  273. if (~instance?(rule, <rule>))
  274. let (rule, msg) = apply(values, rule);
  275. add-field-error(field-name, msg);
  276. end;
  277. end;
  278. end;
  279. note-errors(vc-rules, "view-content");
  280. note-errors(mc-rules, "modify-content");
  281. note-errors(ma-rules, "modify-acls");
  282. respond-to-get(acls-page, title: title);
  283. else
  284. // todo -- Probably should save a <wiki-change> of some sort.
  285. // I haven't figured out what the Master Plan was yet.
  286. if (new-owner & new-owner ~= wiki-page.page-owner)
  287. wiki-page.page-owner := new-owner;
  288. end;
  289. wiki-page.page-access-controls := make(<acls>,
  290. view-content: vc-rules,
  291. modify-content: mc-rules,
  292. modify-acls: ma-rules);
  293. redirect-to(wiki-page);
  294. end;
  295. end;
  296. end method respond-to-post;
  297. // Show the unparsed ACL rules for a wiki page. The 'name' parameter
  298. // determines what rules to show. If there's a query value by the
  299. // same name then that is used instead because it is what the user just
  300. // typed into the input field and they may need to edit it. Note that
  301. // this means the name of the <textarea> field must be one of "view-content"
  302. // "modify-content", or "modify-acls".
  303. //
  304. define tag show-rules in wiki
  305. (acls-page :: <acls-page>)
  306. (name :: <string>)
  307. let name = as-lowercase(name);
  308. let text = get-query-value(name);
  309. if (text)
  310. output("%s", quote-html(text));
  311. else
  312. let acls = *page*.page-access-controls;
  313. output("%s", unparse-rules(select (name by \=)
  314. "view-content" => acls.view-content-rules;
  315. "modify-content" => acls.modify-content-rules;
  316. "modify-acls" => acls.modify-acls-rules;
  317. otherwise =>
  318. error("Invalid rule type: %s", name);
  319. end));
  320. end;
  321. end tag show-rules;
  322. define named-method can-view-content? in wiki
  323. (page :: <wiki-dsp>)
  324. has-permission?(authenticated-user(), *page*, $view-content)
  325. end;
  326. define named-method can-modify-content? in wiki
  327. (page :: <wiki-dsp>)
  328. has-permission?(authenticated-user(), *page*, $modify-content)
  329. end;
  330. define named-method can-modify-acls? in wiki
  331. (acls-page :: <wiki-dsp>)
  332. has-permission?(authenticated-user(), *page*, $modify-acls)
  333. end;