|
1
|
#ifdef FOSSIL_ENABLE_JSON |
|
2
|
/* |
|
3
|
** Copyright (c) 2011 D. Richard Hipp |
|
4
|
** |
|
5
|
** This program is free software; you can redistribute it and/or |
|
6
|
** modify it under the terms of the Simplified BSD License (also |
|
7
|
** known as the "2-Clause License" or "FreeBSD License".) |
|
8
|
** |
|
9
|
** This program is distributed in the hope that it will be useful, |
|
10
|
** but without any warranty; without even the implied warranty of |
|
11
|
** merchantability or fitness for a particular purpose. |
|
12
|
** |
|
13
|
** Author contact information: |
|
14
|
** [email protected] |
|
15
|
** http://www.hwaci.com/drh/ |
|
16
|
** |
|
17
|
*/ |
|
18
|
#include "VERSION.h" |
|
19
|
#include "config.h" |
|
20
|
#include "json_user.h" |
|
21
|
|
|
22
|
#if INTERFACE |
|
23
|
#include "json_detail.h" |
|
24
|
#endif |
|
25
|
|
|
26
|
static cson_value * json_user_get(void); |
|
27
|
static cson_value * json_user_list(void); |
|
28
|
static cson_value * json_user_save(void); |
|
29
|
|
|
30
|
/* |
|
31
|
** Mapping of /json/user/XXX commands/paths to callbacks. |
|
32
|
*/ |
|
33
|
static const JsonPageDef JsonPageDefs_User[] = { |
|
34
|
{"save", json_user_save, 0}, |
|
35
|
{"get", json_user_get, 0}, |
|
36
|
{"list", json_user_list, 0}, |
|
37
|
/* Last entry MUST have a NULL name. */ |
|
38
|
{NULL,NULL,0} |
|
39
|
}; |
|
40
|
|
|
41
|
|
|
42
|
/* |
|
43
|
** Implements the /json/user family of pages/commands. |
|
44
|
** |
|
45
|
*/ |
|
46
|
cson_value * json_page_user(void){ |
|
47
|
return json_page_dispatch_helper(&JsonPageDefs_User[0]); |
|
48
|
} |
|
49
|
|
|
50
|
|
|
51
|
/* |
|
52
|
** Impl of /json/user/list. Requires admin/setup rights. |
|
53
|
*/ |
|
54
|
static cson_value * json_user_list(void){ |
|
55
|
cson_value * payV = NULL; |
|
56
|
Stmt q; |
|
57
|
if(!g.perm.Admin && !g.perm.Setup){ |
|
58
|
json_set_err(FSL_JSON_E_DENIED, |
|
59
|
"Requires 'a' or 's' privileges."); |
|
60
|
return NULL; |
|
61
|
} |
|
62
|
db_prepare(&q,"SELECT uid AS uid," |
|
63
|
" login AS name," |
|
64
|
" cap AS capabilities," |
|
65
|
" info AS info," |
|
66
|
" mtime AS timestamp" |
|
67
|
" FROM user ORDER BY login"); |
|
68
|
payV = json_stmt_to_array_of_obj(&q, NULL); |
|
69
|
db_finalize(&q); |
|
70
|
if(NULL == payV){ |
|
71
|
json_set_err(FSL_JSON_E_UNKNOWN, |
|
72
|
"Could not convert user list to JSON."); |
|
73
|
} |
|
74
|
return payV; |
|
75
|
} |
|
76
|
|
|
77
|
/* |
|
78
|
** Creates a new JSON Object based on the db state of |
|
79
|
** the given user name. On error (no record found) |
|
80
|
** it returns NULL, else the caller owns the returned |
|
81
|
** object. |
|
82
|
*/ |
|
83
|
static cson_value * json_load_user_by_name(char const * zName){ |
|
84
|
cson_value * u = NULL; |
|
85
|
Stmt q; |
|
86
|
db_prepare(&q,"SELECT uid AS uid," |
|
87
|
" login AS name," |
|
88
|
" cap AS capabilities," |
|
89
|
" info AS info," |
|
90
|
" mtime AS timestamp" |
|
91
|
" FROM user" |
|
92
|
" WHERE login=%Q", |
|
93
|
zName); |
|
94
|
if( (SQLITE_ROW == db_step(&q)) ){ |
|
95
|
u = cson_sqlite3_row_to_object(q.pStmt); |
|
96
|
} |
|
97
|
db_finalize(&q); |
|
98
|
return u; |
|
99
|
} |
|
100
|
|
|
101
|
/* |
|
102
|
** Identical to json_load_user_by_name(), but expects a user ID. Returns |
|
103
|
** NULL if no user found with that ID. |
|
104
|
*/ |
|
105
|
static cson_value * json_load_user_by_id(int uid){ |
|
106
|
cson_value * u = NULL; |
|
107
|
Stmt q; |
|
108
|
db_prepare(&q,"SELECT uid AS uid," |
|
109
|
" login AS name," |
|
110
|
" cap AS capabilities," |
|
111
|
" info AS info," |
|
112
|
" mtime AS timestamp" |
|
113
|
" FROM user" |
|
114
|
" WHERE uid=%d", |
|
115
|
uid); |
|
116
|
if( (SQLITE_ROW == db_step(&q)) ){ |
|
117
|
u = cson_sqlite3_row_to_object(q.pStmt); |
|
118
|
} |
|
119
|
db_finalize(&q); |
|
120
|
return u; |
|
121
|
} |
|
122
|
|
|
123
|
|
|
124
|
/* |
|
125
|
** Impl of /json/user/get. Requires admin or setup rights. |
|
126
|
*/ |
|
127
|
static cson_value * json_user_get(void){ |
|
128
|
cson_value * payV = NULL; |
|
129
|
char const * pUser = NULL; |
|
130
|
if(!g.perm.Admin && !g.perm.Setup){ |
|
131
|
json_set_err(FSL_JSON_E_DENIED, |
|
132
|
"Requires 'a' or 's' privileges."); |
|
133
|
return NULL; |
|
134
|
} |
|
135
|
pUser = json_find_option_cstr2("name", NULL, NULL, g.json.dispatchDepth+1); |
|
136
|
if(!pUser || !*pUser){ |
|
137
|
json_set_err(FSL_JSON_E_MISSING_ARGS,"Missing 'name' property."); |
|
138
|
return NULL; |
|
139
|
} |
|
140
|
payV = json_load_user_by_name(pUser); |
|
141
|
if(!payV){ |
|
142
|
json_set_err(FSL_JSON_E_RESOURCE_NOT_FOUND,"User not found."); |
|
143
|
} |
|
144
|
return payV; |
|
145
|
} |
|
146
|
|
|
147
|
/* |
|
148
|
** Expects pUser to contain fossil user fields in JSON form: name, |
|
149
|
** uid, info, capabilities, password. |
|
150
|
** |
|
151
|
** At least one of (name, uid) must be included. All others are |
|
152
|
** optional and their db fields will not be updated if those fields |
|
153
|
** are not included in pUser. |
|
154
|
** |
|
155
|
** If uid is specified then name may refer to a _new_ name |
|
156
|
** for a user, otherwise the name must refer to an existing user. |
|
157
|
** If uid=-1 then the name must be specified and a new user is |
|
158
|
** created (fails if one already exists). |
|
159
|
** |
|
160
|
** If uid is not set, this function might modify pUser to contain the |
|
161
|
** db-found (or inserted) user ID. |
|
162
|
** |
|
163
|
** On error g.json's error state is set and one of the FSL_JSON_E_xxx |
|
164
|
** values from FossilJsonCodes is returned. |
|
165
|
** |
|
166
|
** On success the db record for the given user is updated. |
|
167
|
** |
|
168
|
** Requires either Admin, Setup, or Password access. Non-admin/setup |
|
169
|
** users can only change their own information. Non-setup users may |
|
170
|
** not modify the 's' permission. Admin users without setup |
|
171
|
** permissions may not edit any other user who has the 's' permission. |
|
172
|
** |
|
173
|
*/ |
|
174
|
int json_user_update_from_json( cson_object * pUser ){ |
|
175
|
#define CSTR(X) cson_string_cstr(cson_value_get_string( cson_object_get(pUser, \ |
|
176
|
X ) )) |
|
177
|
char const * zName = CSTR("name"); |
|
178
|
char const * zNameNew = zName; |
|
179
|
char * zNameFree = NULL; |
|
180
|
char const * zInfo = CSTR("info"); |
|
181
|
char const * zCap = CSTR("capabilities"); |
|
182
|
char const * zPW = CSTR("password"); |
|
183
|
cson_value const * forceLogout = cson_object_get(pUser, "forceLogout"); |
|
184
|
int gotFields = 0; |
|
185
|
#undef CSTR |
|
186
|
cson_int_t uid = cson_value_get_integer( cson_object_get(pUser, "uid") ); |
|
187
|
char const tgtHasSetup = zCap && (NULL!=strchr(zCap, 's')); |
|
188
|
char tgtHadSetup = 0; |
|
189
|
Blob sql = empty_blob; |
|
190
|
Stmt q = empty_Stmt; |
|
191
|
|
|
192
|
if(uid<=0 && (!zName||!*zName)){ |
|
193
|
return json_set_err(FSL_JSON_E_MISSING_ARGS, |
|
194
|
"One of 'uid' or 'name' is required."); |
|
195
|
}else if(uid>0){ |
|
196
|
zNameFree = db_text(NULL, "SELECT login FROM user WHERE uid=%d",uid); |
|
197
|
if(!zNameFree){ |
|
198
|
return json_set_err(FSL_JSON_E_RESOURCE_NOT_FOUND, |
|
199
|
"No login found for uid %d.", uid); |
|
200
|
} |
|
201
|
zName = zNameFree; |
|
202
|
}else if(-1==uid){ |
|
203
|
/* try to create a new user */ |
|
204
|
if(!g.perm.Admin && !g.perm.Setup){ |
|
205
|
json_set_err(FSL_JSON_E_DENIED, |
|
206
|
"Requires 'a' or 's' privileges."); |
|
207
|
goto error; |
|
208
|
}else if(!zName || !*zName){ |
|
209
|
json_set_err(FSL_JSON_E_MISSING_ARGS, |
|
210
|
"No name specified for new user."); |
|
211
|
goto error; |
|
212
|
}else if( db_exists("SELECT 1 FROM user WHERE login=%Q", zName) ){ |
|
213
|
json_set_err(FSL_JSON_E_RESOURCE_ALREADY_EXISTS, |
|
214
|
"User %s already exists.", zName); |
|
215
|
goto error; |
|
216
|
}else{ |
|
217
|
Stmt ins = empty_Stmt; |
|
218
|
db_unprotect(PROTECT_USER); |
|
219
|
db_prepare(&ins, "INSERT INTO user (login) VALUES(%Q)",zName); |
|
220
|
db_step( &ins ); |
|
221
|
db_finalize(&ins); |
|
222
|
db_protect_pop(); |
|
223
|
uid = db_int(0,"SELECT uid FROM user WHERE login=%Q", zName); |
|
224
|
assert(uid>0); |
|
225
|
zNameNew = zName; |
|
226
|
cson_object_set( pUser, "uid", cson_value_new_integer(uid) ); |
|
227
|
} |
|
228
|
}else{ |
|
229
|
uid = db_int(0,"SELECT uid FROM user WHERE login=%Q", zName); |
|
230
|
if(uid<=0){ |
|
231
|
json_set_err(FSL_JSON_E_RESOURCE_NOT_FOUND, |
|
232
|
"No login found for user [%s].", zName); |
|
233
|
goto error; |
|
234
|
} |
|
235
|
cson_object_set( pUser, "uid", cson_value_new_integer(uid) ); |
|
236
|
} |
|
237
|
|
|
238
|
/* Maintenance note: all error-returns from here on out should go |
|
239
|
via 'goto error' in order to clean up. |
|
240
|
*/ |
|
241
|
|
|
242
|
if(uid != g.userUid){ |
|
243
|
if(!g.perm.Admin && !g.perm.Setup){ |
|
244
|
json_set_err(FSL_JSON_E_DENIED, |
|
245
|
"Changing another user's data requires " |
|
246
|
"'a' or 's' privileges."); |
|
247
|
goto error; |
|
248
|
} |
|
249
|
} |
|
250
|
/* check if the target uid currently has setup rights. */ |
|
251
|
tgtHadSetup = db_int(0,"SELECT 1 FROM user where uid=%d" |
|
252
|
" AND cap GLOB '*s*'", uid); |
|
253
|
|
|
254
|
if((tgtHasSetup || tgtHadSetup) && !g.perm.Setup){ |
|
255
|
/* |
|
256
|
Do not allow a non-setup user to set or remove setup |
|
257
|
privileges. setup.c uses similar logic. |
|
258
|
*/ |
|
259
|
json_set_err(FSL_JSON_E_DENIED, |
|
260
|
"Modifying 's' users/privileges requires " |
|
261
|
"'s' privileges."); |
|
262
|
goto error; |
|
263
|
} |
|
264
|
/* |
|
265
|
Potential todo: do not allow a setup user to remove 's' from |
|
266
|
himself, to avoid locking himself out? |
|
267
|
*/ |
|
268
|
|
|
269
|
blob_append(&sql, "UPDATE user SET",-1 ); |
|
270
|
blob_append(&sql, " mtime=cast(strftime('%s') AS INTEGER)", -1); |
|
271
|
|
|
272
|
if((uid>0) && zNameNew){ |
|
273
|
/* Check for name change... */ |
|
274
|
if(0!=strcmp(zName,zNameNew)){ |
|
275
|
if( (!g.perm.Admin && !g.perm.Setup) |
|
276
|
&& (zName != zNameNew)){ |
|
277
|
json_set_err( FSL_JSON_E_DENIED, |
|
278
|
"Modifying user names requires 'a' or 's' privileges."); |
|
279
|
goto error; |
|
280
|
} |
|
281
|
forceLogout = cson_value_true() |
|
282
|
/* reminders: 1) does not allocate. |
|
283
|
2) we do this because changing a name |
|
284
|
invalidates any login token because the old name |
|
285
|
is part of the token hash. |
|
286
|
*/; |
|
287
|
blob_append_sql(&sql, ", login=%Q", zNameNew); |
|
288
|
++gotFields; |
|
289
|
} |
|
290
|
} |
|
291
|
|
|
292
|
if( zCap && *zCap ){ |
|
293
|
if(!g.perm.Admin || !g.perm.Setup){ |
|
294
|
/* we "could" arguably silently ignore cap in this case. */ |
|
295
|
json_set_err(FSL_JSON_E_DENIED, |
|
296
|
"Changing capabilities requires 'a' or 's' privileges."); |
|
297
|
goto error; |
|
298
|
} |
|
299
|
blob_append_sql(&sql, ", cap=%Q", zCap); |
|
300
|
++gotFields; |
|
301
|
} |
|
302
|
|
|
303
|
if( zPW && *zPW ){ |
|
304
|
if(!g.perm.Admin && !g.perm.Setup && !g.perm.Password){ |
|
305
|
json_set_err( FSL_JSON_E_DENIED, |
|
306
|
"Password change requires 'a', 's', " |
|
307
|
"or 'p' permissions."); |
|
308
|
goto error; |
|
309
|
}else{ |
|
310
|
#define TRY_LOGIN_GROUP 0 /* login group support is not yet implemented. */ |
|
311
|
#if !TRY_LOGIN_GROUP |
|
312
|
char * zPWHash = NULL; |
|
313
|
++gotFields; |
|
314
|
zPWHash = sha1_shared_secret(zPW, zNameNew ? zNameNew : zName, NULL); |
|
315
|
blob_append_sql(&sql, ", pw=%Q", zPWHash); |
|
316
|
free(zPWHash); |
|
317
|
#else |
|
318
|
++gotFields; |
|
319
|
blob_append_sql(&sql, ", pw=coalesce(shared_secret(%Q,%Q," |
|
320
|
"(SELECT value FROM config WHERE name='project-code')))", |
|
321
|
zPW, zNameNew ? zNameNew : zName); |
|
322
|
/* shared_secret() func is undefined? */ |
|
323
|
#endif |
|
324
|
} |
|
325
|
} |
|
326
|
|
|
327
|
if( zInfo ){ |
|
328
|
blob_append_sql(&sql, ", info=%Q", zInfo); |
|
329
|
++gotFields; |
|
330
|
} |
|
331
|
|
|
332
|
if((g.perm.Admin || g.perm.Setup) |
|
333
|
&& forceLogout && cson_value_get_bool(forceLogout)){ |
|
334
|
blob_append(&sql, ", cookie=NULL, cexpire=NULL", -1); |
|
335
|
++gotFields; |
|
336
|
} |
|
337
|
|
|
338
|
if(!gotFields){ |
|
339
|
json_set_err( FSL_JSON_E_MISSING_ARGS, |
|
340
|
"Required user data are missing."); |
|
341
|
goto error; |
|
342
|
} |
|
343
|
assert(uid>0); |
|
344
|
#if !TRY_LOGIN_GROUP |
|
345
|
blob_append_sql(&sql, " WHERE uid=%d", uid); |
|
346
|
#else /* need name for login group support :/ */ |
|
347
|
blob_append_sql(&sql, " WHERE login=%Q", zName); |
|
348
|
#endif |
|
349
|
#if 0 |
|
350
|
puts(blob_str(&sql)); |
|
351
|
cson_output_FILE( cson_object_value(pUser), stdout, NULL ); |
|
352
|
#endif |
|
353
|
db_unprotect(PROTECT_USER); |
|
354
|
db_prepare(&q, "%s", blob_sql_text(&sql)); |
|
355
|
db_exec(&q); |
|
356
|
db_finalize(&q); |
|
357
|
db_protect_pop(); |
|
358
|
#if TRY_LOGIN_GROUP |
|
359
|
if( zPW || cson_value_get_bool(forceLogout) ){ |
|
360
|
Blob groupSql = empty_blob; |
|
361
|
char * zErr = NULL; |
|
362
|
blob_append_sql(&groupSql, |
|
363
|
"INSERT INTO user(login)" |
|
364
|
" SELECT %Q WHERE NOT EXISTS(SELECT 1 FROM user WHERE login=%Q);", |
|
365
|
zName, zName |
|
366
|
); |
|
367
|
blob_append(&groupSql, blob_str(&sql), blob_size(&sql)); |
|
368
|
db_unprotect(PROTECT_USER); |
|
369
|
login_group_sql(blob_str(&groupSql), NULL, NULL, &zErr); |
|
370
|
db_protect_pop(); |
|
371
|
blob_reset(&groupSql); |
|
372
|
if( zErr ){ |
|
373
|
json_set_err( FSL_JSON_E_UNKNOWN, |
|
374
|
"Repo-group update at least partially failed: %s", |
|
375
|
zErr); |
|
376
|
free(zErr); |
|
377
|
goto error; |
|
378
|
} |
|
379
|
} |
|
380
|
#endif /* TRY_LOGIN_GROUP */ |
|
381
|
|
|
382
|
#undef TRY_LOGIN_GROUP |
|
383
|
|
|
384
|
free( zNameFree ); |
|
385
|
blob_reset(&sql); |
|
386
|
return 0; |
|
387
|
|
|
388
|
error: |
|
389
|
assert(0 != g.json.resultCode); |
|
390
|
free(zNameFree); |
|
391
|
blob_reset(&sql); |
|
392
|
return g.json.resultCode; |
|
393
|
} |
|
394
|
|
|
395
|
|
|
396
|
/* |
|
397
|
** Impl of /json/user/save. |
|
398
|
*/ |
|
399
|
static cson_value * json_user_save(void){ |
|
400
|
/* try to get user info from GET/CLI args and construct |
|
401
|
a JSON form of it... */ |
|
402
|
cson_object * u = cson_new_object(); |
|
403
|
char const * str = NULL; |
|
404
|
int b = -1; |
|
405
|
int i = -1; |
|
406
|
int uid = -1; |
|
407
|
cson_value * payload = NULL; |
|
408
|
/* String properties... */ |
|
409
|
#define PROP(LK,SK) str = json_find_option_cstr(LK,NULL,SK); \ |
|
410
|
if(str){ cson_object_set(u, LK, json_new_string(str)); } (void)0 |
|
411
|
PROP("name","n"); |
|
412
|
PROP("password","p"); |
|
413
|
PROP("info","i"); |
|
414
|
PROP("capabilities","c"); |
|
415
|
#undef PROP |
|
416
|
/* Boolean properties... */ |
|
417
|
#define PROP(LK,DFLT) b = json_find_option_bool(LK,NULL,NULL,DFLT); \ |
|
418
|
if(DFLT!=b){ cson_object_set(u, LK, cson_value_new_bool(b ? 1 : 0)); } (void)0 |
|
419
|
PROP("forceLogout",-1); |
|
420
|
#undef PROP |
|
421
|
|
|
422
|
#define PROP(LK,DFLT) i = json_find_option_int(LK,NULL,NULL,DFLT); \ |
|
423
|
if(DFLT != i){ cson_object_set(u, LK, cson_value_new_integer(i)); } (void)0 |
|
424
|
PROP("uid",-99); |
|
425
|
#undef PROP |
|
426
|
if( g.json.reqPayload.o ){ |
|
427
|
cson_object_merge( u, g.json.reqPayload.o, CSON_MERGE_NO_RECURSE ); |
|
428
|
} |
|
429
|
json_user_update_from_json( u ); |
|
430
|
if(!g.json.resultCode){ |
|
431
|
uid = cson_value_get_integer( cson_object_get(u, "uid") ); |
|
432
|
assert((uid>0) && "Something went wrong in json_user_update_from_json()"); |
|
433
|
payload = json_load_user_by_id(uid); |
|
434
|
} |
|
435
|
cson_free_object(u); |
|
436
|
return payload; |
|
437
|
} |
|
438
|
#endif /* FOSSIL_ENABLE_JSON */ |
|
439
|
|