Chris@0
|
1 /* $Id$
|
Chris@0
|
2
|
Chris@0
|
3 Part of SWI-Prolog
|
Chris@0
|
4
|
Chris@0
|
5 Author: Jan Wielemaker
|
Chris@0
|
6 E-mail: wielemak@scienc.uva.nl
|
Chris@0
|
7 WWW: http://www.swi-prolog.org
|
Chris@0
|
8 Copyright (C): 1985-2007, University of Amsterdam
|
Chris@0
|
9
|
Chris@0
|
10 This program is free software; you can redistribute it and/or
|
Chris@0
|
11 modify it under the terms of the GNU General Public License
|
Chris@0
|
12 as published by the Free Software Foundation; either version 2
|
Chris@0
|
13 of the License, or (at your option) any later version.
|
Chris@0
|
14
|
Chris@0
|
15 This program is distributed in the hope that it will be useful,
|
Chris@0
|
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
|
Chris@0
|
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
Chris@0
|
18 GNU General Public License for more details.
|
Chris@0
|
19
|
Chris@0
|
20 You should have received a copy of the GNU Lesser General Public
|
Chris@0
|
21 License along with this library; if not, write to the Free Software
|
Chris@0
|
22 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
Chris@0
|
23
|
Chris@0
|
24 As a special exception, if you link this library with other files,
|
Chris@0
|
25 compiled with a Free Software compiler, to produce an executable, this
|
Chris@0
|
26 library does not by itself cause the resulting executable to be covered
|
Chris@0
|
27 by the GNU General Public License. This exception does not however
|
Chris@0
|
28 invalidate any other reasons why the executable file might be covered by
|
Chris@0
|
29 the GNU General Public License.
|
Chris@0
|
30 */
|
Chris@0
|
31
|
Chris@0
|
32 :- module(user_db,
|
Chris@0
|
33 [ set_user_database/1, % +File
|
Chris@0
|
34
|
Chris@0
|
35 user_add/2, % +Name, +Properties
|
Chris@0
|
36 user_del/1, % +Name,
|
Chris@0
|
37 set_user_property/2, % +Name, +Property
|
Chris@0
|
38
|
Chris@0
|
39 openid_add_server/2, % +Server, +Options
|
Chris@0
|
40 openid_del_server/1, % +Server
|
Chris@0
|
41 openid_set_property/2, % +Server, +Property
|
Chris@0
|
42 openid_current_server/1, % ?Server
|
Chris@0
|
43 openid_server_property/2, % ?Server, ?Property
|
Chris@0
|
44 openid_server_properties/2, % ?Server, ?Property
|
Chris@0
|
45
|
Chris@0
|
46 user_property/2, % ?Name, ?Property
|
Chris@0
|
47 check_permission/2, % +User, +Operation
|
Chris@0
|
48 validate_password/2, % +User, +Password
|
Chris@0
|
49 password_hash/2, % +Password, ?Hash
|
Chris@0
|
50
|
Chris@0
|
51 login/1, % +User
|
Chris@0
|
52 logout/1, % +User
|
Chris@0
|
53 current_user/1, % ?User
|
Chris@0
|
54 logged_on/1, % -User
|
Chris@0
|
55 ensure_logged_on/1, % -User
|
Chris@0
|
56 authorized/1, % +Action
|
Chris@0
|
57
|
Chris@0
|
58 deny_all_users/1 % +What
|
Chris@0
|
59 ]).
|
Chris@0
|
60 :- use_module(library('semweb/rdf_db')).
|
Chris@0
|
61 :- use_module(library('http/http_session')).
|
Chris@0
|
62 :- use_module(library('http/http_wrapper')).
|
Chris@0
|
63 :- use_module(library('http/http_openid')).
|
Chris@0
|
64 :- use_module(library(lists)).
|
Chris@0
|
65 :- use_module(library(settings)).
|
Chris@0
|
66 :- use_module(library(error)).
|
Chris@0
|
67 :- use_module(library(url)).
|
Chris@0
|
68 :- use_module(openid).
|
Chris@0
|
69 :- use_module(db).
|
Chris@0
|
70
|
Chris@0
|
71 /** <module> User administration
|
Chris@0
|
72
|
Chris@0
|
73 Core user administration. The user administration is based on the
|
Chris@0
|
74 following:
|
Chris@0
|
75
|
Chris@0
|
76 * A persistent fact user/2
|
Chris@0
|
77 * A dynamic fact logged_in/3
|
Chris@0
|
78 * Session management
|
Chris@0
|
79
|
Chris@0
|
80 @tbd Consider using the RDF database for login. Maybe requires
|
Chris@0
|
81 multiple RDF databases?
|
Chris@0
|
82
|
Chris@0
|
83 @author Jan Wielemaker
|
Chris@0
|
84 */
|
Chris@0
|
85
|
Chris@0
|
86 :- dynamic
|
Chris@0
|
87 logged_in/3, % Session, User, Time
|
Chris@0
|
88 user/2, % Name, Options
|
Chris@0
|
89 denied/1. % Deny to all users
|
Chris@0
|
90
|
Chris@0
|
91
|
Chris@0
|
92 /*******************************
|
Chris@0
|
93 * USER DATABASE *
|
Chris@0
|
94 *******************************/
|
Chris@0
|
95
|
Chris@0
|
96 :- db_term
|
Chris@0
|
97 user(_Name, _UserOptions),
|
Chris@0
|
98 grant_openid_server(_Server, _ServerOptions).
|
Chris@0
|
99
|
Chris@0
|
100 %% set_user_database(+File) is det.
|
Chris@0
|
101 %
|
Chris@0
|
102 % Load user/2 from File. Changes are fully synchronous.
|
Chris@0
|
103
|
Chris@0
|
104 set_user_database(File) :-
|
Chris@0
|
105 db_attach(File, [sync(close)]).
|
Chris@0
|
106
|
Chris@0
|
107 %% user_add(+Name, +Properties) is det.
|
Chris@0
|
108 %
|
Chris@0
|
109 % Add a new user with given properties.
|
Chris@0
|
110
|
Chris@0
|
111 user_add(Name, Options) :-
|
Chris@0
|
112 must_be(atom, Name),
|
Chris@0
|
113 db_assert(user(Name, Options)).
|
Chris@0
|
114
|
Chris@0
|
115 %% user_del(+Name)
|
Chris@0
|
116 %
|
Chris@0
|
117 % Delete named user from user-database.
|
Chris@0
|
118
|
Chris@0
|
119 user_del(Name) :-
|
Chris@0
|
120 must_be(atom, Name),
|
Chris@0
|
121 ( user(Name, _)
|
Chris@0
|
122 -> db_retractall(user(Name, _))
|
Chris@0
|
123 ; existence_error(user, Name)
|
Chris@0
|
124 ).
|
Chris@0
|
125
|
Chris@0
|
126 %% set_user_property(+Name, +Property) is det.
|
Chris@0
|
127 %
|
Chris@0
|
128 % Replace Property for user Name.
|
Chris@0
|
129
|
Chris@0
|
130 set_user_property(Name, Prop) :-
|
Chris@0
|
131 must_be(atom, Name),
|
Chris@0
|
132 ( user(Name, OldProps)
|
Chris@0
|
133 -> ( memberchk(Prop, OldProps)
|
Chris@0
|
134 -> true
|
Chris@0
|
135 ; functor(Prop, PropName, Arity),
|
Chris@0
|
136 functor(Unbound, PropName, Arity),
|
Chris@0
|
137 delete(OldProps, Unbound, NewProps),
|
Chris@0
|
138 db_retractall(user(Name, _)),
|
Chris@0
|
139 db_assert(user(Name, [Prop|NewProps]))
|
Chris@0
|
140 )
|
Chris@0
|
141 ; existence_error(user, Name)
|
Chris@0
|
142 ).
|
Chris@0
|
143
|
Chris@0
|
144
|
Chris@0
|
145 %% openid_add_server(+Server, +Options)
|
Chris@0
|
146 %
|
Chris@0
|
147 % Register an OpenID server.
|
Chris@0
|
148
|
Chris@0
|
149 openid_add_server(Server, _Options) :-
|
Chris@0
|
150 openid_current_server(Server), !,
|
Chris@0
|
151 throw(error(permission_error(create, openid_server, Server),
|
Chris@0
|
152 context(_, 'Already present'))).
|
Chris@0
|
153 openid_add_server(Server, Options) :-
|
Chris@0
|
154 db_assert(grant_openid_server(Server, Options)).
|
Chris@0
|
155
|
Chris@0
|
156
|
Chris@0
|
157 %% openid_del_server(+Server)
|
Chris@0
|
158 %
|
Chris@0
|
159 % Delete registration of an OpenID server.
|
Chris@0
|
160
|
Chris@0
|
161 openid_del_server(Server) :-
|
Chris@0
|
162 db_retractall(grant_openid_server(Server, _)).
|
Chris@0
|
163
|
Chris@0
|
164
|
Chris@0
|
165 %% openid_set_property(+Server, +Property) is det.
|
Chris@0
|
166 %
|
Chris@0
|
167 % Replace Property for OpenID Server
|
Chris@0
|
168
|
Chris@0
|
169 openid_set_property(Server, Prop) :-
|
Chris@0
|
170 must_be(atom, Server),
|
Chris@0
|
171 ( grant_openid_server(Server, OldProps)
|
Chris@0
|
172 -> ( memberchk(Prop, OldProps)
|
Chris@0
|
173 -> true
|
Chris@0
|
174 ; functor(Prop, PropName, Arity),
|
Chris@0
|
175 functor(Unbound, PropName, Arity),
|
Chris@0
|
176 delete(OldProps, Unbound, NewProps),
|
Chris@0
|
177 db_retractall(grant_openid_server(Server, _)),
|
Chris@0
|
178 db_assert(grant_openid_server(Server, [Prop|NewProps]))
|
Chris@0
|
179 )
|
Chris@0
|
180 ; existence_error(openid_server, Server)
|
Chris@0
|
181 ).
|
Chris@0
|
182
|
Chris@0
|
183
|
Chris@0
|
184 %% openid_current_server(?Server) is nondet.
|
Chris@0
|
185 %
|
Chris@0
|
186
|
Chris@0
|
187 openid_current_server(Server) :-
|
Chris@0
|
188 grant_openid_server(Server, _).
|
Chris@0
|
189
|
Chris@0
|
190 %% openid_server_properties(+Server, -Properties) is semidet.
|
Chris@0
|
191 %
|
Chris@0
|
192 % Try find properties for the given server. Note that we generally
|
Chris@0
|
193 % refer to a server using its domain. The actjual server may be a
|
Chris@0
|
194 % path on the server or a machine in the domain.
|
Chris@0
|
195
|
Chris@0
|
196 :- dynamic
|
Chris@0
|
197 registered_server/2.
|
Chris@0
|
198
|
Chris@0
|
199 openid_server_properties(Server, Properties) :-
|
Chris@0
|
200 ( registered_server(Server, Registered)
|
Chris@0
|
201 -> grant_openid_server(Registered, Properties)
|
Chris@0
|
202 ; grant_openid_server(Server, Properties)
|
Chris@0
|
203 -> true
|
Chris@0
|
204 ; grant_openid_server(Registered, Properties),
|
Chris@0
|
205 match_server(Server, Registered)
|
Chris@0
|
206 -> assert(registered_server(Server, Registered))
|
Chris@0
|
207 ; grant_openid_server(*, Properties)
|
Chris@0
|
208 ).
|
Chris@0
|
209
|
Chris@0
|
210 %% match_server(+ServerURL, +RegisteredURL) is semidet.
|
Chris@0
|
211 %
|
Chris@0
|
212 % True if ServerURL is in the domain of RegisteredURL.
|
Chris@0
|
213
|
Chris@0
|
214 match_server(Server, Registered) :-
|
Chris@0
|
215 parse_url(Server, SParts),
|
Chris@0
|
216 memberchk(host(SHost), SParts),
|
Chris@0
|
217 parse_url(Registered, RParts),
|
Chris@0
|
218 memberchk(host(RHost), RParts),
|
Chris@0
|
219 concat_atom(SL, '.', SHost),
|
Chris@0
|
220 concat_atom(RL, '.', RHost),
|
Chris@0
|
221 append(_, RL, SL), !.
|
Chris@0
|
222
|
Chris@0
|
223
|
Chris@0
|
224 openid_server_property(Server, Property) :-
|
Chris@0
|
225 openid_server_properties(Server, Properties),
|
Chris@0
|
226 ( var(Property)
|
Chris@0
|
227 -> member(Property, Properties)
|
Chris@0
|
228 ; memberchk(Property, Properties)
|
Chris@0
|
229 ).
|
Chris@0
|
230
|
Chris@0
|
231
|
Chris@0
|
232 /*******************************
|
Chris@0
|
233 * USER QUERY *
|
Chris@0
|
234 *******************************/
|
Chris@0
|
235
|
Chris@0
|
236 %% current_user(?User)
|
Chris@0
|
237 %
|
Chris@0
|
238 % True if User is a registered user.
|
Chris@0
|
239
|
Chris@0
|
240 current_user(User) :-
|
Chris@0
|
241 user(User, _).
|
Chris@0
|
242
|
Chris@0
|
243 %% user_property(?User, ?Property) is nondet.
|
Chris@0
|
244 %% user_property(+User, +Property) is semidet.
|
Chris@0
|
245 %
|
Chris@0
|
246 % True if Property is a defined property on User. In addition to
|
Chris@0
|
247 % properties explicitely stored with users, we define:
|
Chris@0
|
248 %
|
Chris@0
|
249 % * session(SessionID)
|
Chris@0
|
250 % * connection(LoginTime, Idle)
|
Chris@0
|
251 % * url(URL)
|
Chris@0
|
252 % Generates reference to our own OpenID server for local
|
Chris@0
|
253 % login
|
Chris@0
|
254 % * openid(OpenID)
|
Chris@0
|
255 % Refers to the official OpenID (possibly delegated)
|
Chris@0
|
256 % * openid_server(Server)
|
Chris@0
|
257 % Refers to the OpenID server that validated the login
|
Chris@0
|
258
|
Chris@0
|
259 user_property(User, Property) :-
|
Chris@0
|
260 nonvar(User), nonvar(Property), !,
|
Chris@0
|
261 uprop(Property, User), !.
|
Chris@0
|
262 user_property(User, Property) :-
|
Chris@0
|
263 uprop(Property, User).
|
Chris@0
|
264
|
Chris@0
|
265 uprop(session(SessionID), User) :-
|
Chris@0
|
266 ( nonvar(SessionID) % speedup
|
Chris@0
|
267 -> !
|
Chris@0
|
268 ; true
|
Chris@0
|
269 ),
|
Chris@0
|
270 logged_in(SessionID, User, _).
|
Chris@0
|
271 uprop(connection(LoginTime, Idle), User) :-
|
Chris@0
|
272 logged_in(SessionID, User, LoginTime),
|
Chris@0
|
273 http_current_session(SessionID, idle(Idle)).
|
Chris@0
|
274 uprop(url(URL), User) :-
|
Chris@0
|
275 user_url(User, URL).
|
Chris@0
|
276 uprop(Prop, User) :-
|
Chris@0
|
277 nonvar(User), !,
|
Chris@0
|
278 ( user(User, Properties)
|
Chris@0
|
279 -> true
|
Chris@0
|
280 ; openid_server(User, OpenID, Server),
|
Chris@0
|
281 openid_server_properties(Server, Properties0)
|
Chris@0
|
282 -> Properties = [type(openid),openid(OpenID),openid_server(Server)|Properties0]
|
Chris@0
|
283 ),
|
Chris@0
|
284 ( nonvar(Prop)
|
Chris@0
|
285 -> memberchk(Prop, Properties)
|
Chris@0
|
286 ; member(Prop, Properties)
|
Chris@0
|
287 ).
|
Chris@0
|
288 uprop(Prop, User) :-
|
Chris@0
|
289 user(User, Properties),
|
Chris@0
|
290 member(Prop, Properties).
|
Chris@0
|
291
|
Chris@0
|
292
|
Chris@0
|
293 user_url(User, URL) :-
|
Chris@0
|
294 is_absolute_url(User), !,
|
Chris@0
|
295 URL = User.
|
Chris@0
|
296 user_url(User, URL) :-
|
Chris@0
|
297 openid_for_local_user(User, URL).
|
Chris@0
|
298
|
Chris@0
|
299
|
Chris@0
|
300 /*******************************
|
Chris@0
|
301 * MISC ROUTINES *
|
Chris@0
|
302 *******************************/
|
Chris@0
|
303
|
Chris@0
|
304 %% validate_password(+User, +Password)
|
Chris@0
|
305 %
|
Chris@0
|
306 % Validate the password for the given user and password.
|
Chris@0
|
307
|
Chris@0
|
308 validate_password(User, Password) :-
|
Chris@0
|
309 user(User, Options),
|
Chris@0
|
310 memberchk(password(Hash), Options),
|
Chris@0
|
311 password_hash(Password, Hash).
|
Chris@0
|
312
|
Chris@0
|
313
|
Chris@0
|
314 %% password_hash(+Password, ?Hash)
|
Chris@0
|
315 %
|
Chris@0
|
316 % Generate a hash from a password or test a password against a
|
Chris@0
|
317 % hash. Like Unix we add a random 2 character prefix to make the
|
Chris@0
|
318 % same password return different hashes and thus obscure equal
|
Chris@0
|
319 % passwords.
|
Chris@0
|
320 %
|
Chris@0
|
321 % @tbd Use crypt/2 from library(crypt)
|
Chris@0
|
322
|
Chris@0
|
323 password_hash(Password, Hash) :-
|
Chris@0
|
324 var(Hash), !,
|
Chris@0
|
325 C1 is random(0'z-0'a) + 0'a,
|
Chris@0
|
326 C2 is random(0'z-0'a) + 0'a,
|
Chris@0
|
327 atom_codes(Password, Codes),
|
Chris@0
|
328 rdf_atom_md5([C1,C2|Codes], 100000, Hash0),
|
Chris@0
|
329 atom_codes(Prefix, [C1, C2]),
|
Chris@0
|
330 atom_concat(Prefix, Hash0, Hash).
|
Chris@0
|
331 password_hash(Password, Hash) :-
|
Chris@0
|
332 sub_atom(Hash, 0, 2, _, Prefix),
|
Chris@0
|
333 sub_atom(Hash, 2, _, 0, Hash0),
|
Chris@0
|
334 atom_codes(Prefix, [C1, C2]),
|
Chris@0
|
335 atom_codes(Password, Codes),
|
Chris@0
|
336 rdf_atom_md5([C1,C2|Codes], 100000, Hash0).
|
Chris@0
|
337
|
Chris@0
|
338
|
Chris@0
|
339 /*******************************
|
Chris@0
|
340 * LOGIN/PERMISSIONS *
|
Chris@0
|
341 *******************************/
|
Chris@0
|
342
|
Chris@0
|
343 %% logged_on(-User) is det.
|
Chris@0
|
344 %
|
Chris@0
|
345 % True if User is the name of the currently logged in user.
|
Chris@0
|
346 %
|
Chris@0
|
347 % @error context_error(not_logged_in)
|
Chris@0
|
348
|
Chris@0
|
349 logged_on(User) :-
|
Chris@0
|
350 http_session_id(SessionID),
|
Chris@0
|
351 user_property(User, session(SessionID)), !.
|
Chris@0
|
352 logged_on(_) :-
|
Chris@0
|
353 throw(error(context_error(not_logged_in), _)).
|
Chris@0
|
354
|
Chris@0
|
355 %% ensure_logged_on(-User)
|
Chris@0
|
356 %
|
Chris@0
|
357 % Make sure we are logged in and return the current user.
|
Chris@0
|
358 % See openid_user/3 for details.
|
Chris@0
|
359
|
Chris@0
|
360 ensure_logged_on(User) :-
|
Chris@0
|
361 http_current_request(Request),
|
Chris@0
|
362 ( catch(setting(http:prefix, Prefix), _, fail)
|
Chris@0
|
363 -> atom_concat(Prefix, '/openid/login', LoginURL),
|
Chris@0
|
364 openid_user(Request, User, [login_url(LoginURL)])
|
Chris@0
|
365 ; openid_user(Request, User, [])
|
Chris@0
|
366 ).
|
Chris@0
|
367
|
Chris@0
|
368
|
Chris@0
|
369 %% authorized(+Action) is det.
|
Chris@0
|
370 %
|
Chris@0
|
371 % validate the current user is allowed to perform Action. Throws
|
Chris@0
|
372 % a permission error if this is not the case. Never fails.
|
Chris@0
|
373 %
|
Chris@0
|
374 % @error permission_error(http_location, access, Path)
|
Chris@0
|
375
|
Chris@0
|
376 authorized(Action) :-
|
Chris@0
|
377 catch(check_permission(anonymous, Action), _, fail), !.
|
Chris@0
|
378 authorized(Action) :-
|
Chris@0
|
379 ensure_logged_on(User),
|
Chris@0
|
380 check_permission(User, Action).
|
Chris@0
|
381
|
Chris@0
|
382
|
Chris@0
|
383 %% check_permission(+User, +Operation)
|
Chris@0
|
384 %
|
Chris@0
|
385 % Validate that user is allowed to perform Operation.
|
Chris@0
|
386 %
|
Chris@0
|
387 % @error permission_error(http_location, access, Path)
|
Chris@0
|
388
|
Chris@0
|
389 check_permission(User, Operation) :-
|
Chris@0
|
390 \+ denied(User, Operation),
|
Chris@0
|
391 user_property(User, allow(Operations)),
|
Chris@0
|
392 memberchk(Operation, Operations), !.
|
Chris@0
|
393 check_permission(_, _) :-
|
Chris@0
|
394 http_current_request(Request),
|
Chris@0
|
395 memberchk(path(Path), Request),
|
Chris@0
|
396 permission_error(http_location, access, Path).
|
Chris@0
|
397
|
Chris@0
|
398 %% denied(+User, +Operation)
|
Chris@0
|
399 %
|
Chris@0
|
400 % Deny actions to all users but admin. This is a bit of a quick
|
Chris@0
|
401 % hack to avoid loosing data in a multi-user experiment. Do not
|
Chris@0
|
402 % yet rely on this,
|
Chris@0
|
403
|
Chris@0
|
404 denied(admin, _) :- !, fail.
|
Chris@0
|
405 denied(_, Operation) :-
|
Chris@0
|
406 denied(Operation).
|
Chris@0
|
407
|
Chris@0
|
408
|
Chris@0
|
409 %% deny_all_users(+Term)
|
Chris@0
|
410 %
|
Chris@0
|
411 % Deny some action to all users. See above.
|
Chris@0
|
412
|
Chris@0
|
413 deny_all_users(Term) :-
|
Chris@0
|
414 ( denied(X),
|
Chris@0
|
415 X =@= Term
|
Chris@0
|
416 -> true
|
Chris@0
|
417 ; assert(denied(Term))
|
Chris@0
|
418 ).
|
Chris@0
|
419
|
Chris@0
|
420
|
Chris@0
|
421 %% login(+User:atom)
|
Chris@0
|
422 %
|
Chris@0
|
423 % Accept user as a user that has logged on into the current
|
Chris@0
|
424 % session.
|
Chris@0
|
425
|
Chris@0
|
426 login(User) :-
|
Chris@0
|
427 must_be(atom, User),
|
Chris@0
|
428 get_time(Time),
|
Chris@0
|
429 http_session_id(Session),
|
Chris@0
|
430 retractall(logged_in(_, Session, _)),
|
Chris@0
|
431 assert(logged_in(Session, User, Time)).
|
Chris@0
|
432
|
Chris@0
|
433 %% logout(+User)
|
Chris@0
|
434 %
|
Chris@0
|
435 % Logout the specified user
|
Chris@0
|
436
|
Chris@0
|
437 logout(User) :-
|
Chris@0
|
438 must_be(atom, User),
|
Chris@0
|
439 retractall(logged_in(_Session, User, _Time)).
|