Fossil SCM

fossil-scm / src / json_user.c
Blame History Raw 439 lines
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

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button