summaryrefslogtreecommitdiff
path: root/src/json_user.c
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
committercashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
commit4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch)
treec296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /src/json_user.c
downloadrelic-master.tar.gz
initial commitHEADmaster
Diffstat (limited to 'src/json_user.c')
-rw-r--r--src/json_user.c438
1 files changed, 438 insertions, 0 deletions
diff --git a/src/json_user.c b/src/json_user.c
new file mode 100644
index 0000000..d60ac08
--- /dev/null
+++ b/src/json_user.c
@@ -0,0 +1,438 @@
+#ifdef FOSSIL_ENABLE_JSON
+/*
+** Copyright (c) 2011 D. Richard Hipp
+**
+** This program is free software; you can redistribute it and/or
+** modify it under the terms of the Simplified BSD License (also
+** known as the "2-Clause License" or "FreeBSD License".)
+**
+** This program is distributed in the hope that it will be useful,
+** but without any warranty; without even the implied warranty of
+** merchantability or fitness for a particular purpose.
+**
+** Author contact information:
+** drh@hwaci.com
+** http://www.hwaci.com/drh/
+**
+*/
+#include "VERSION.h"
+#include "config.h"
+#include "json_user.h"
+
+#if INTERFACE
+#include "json_detail.h"
+#endif
+
+static cson_value * json_user_get(void);
+static cson_value * json_user_list(void);
+static cson_value * json_user_save(void);
+
+/*
+** Mapping of /json/user/XXX commands/paths to callbacks.
+*/
+static const JsonPageDef JsonPageDefs_User[] = {
+{"save", json_user_save, 0},
+{"get", json_user_get, 0},
+{"list", json_user_list, 0},
+/* Last entry MUST have a NULL name. */
+{NULL,NULL,0}
+};
+
+
+/*
+** Implements the /json/user family of pages/commands.
+**
+*/
+cson_value * json_page_user(void){
+ return json_page_dispatch_helper(&JsonPageDefs_User[0]);
+}
+
+
+/*
+** Impl of /json/user/list. Requires admin/setup rights.
+*/
+static cson_value * json_user_list(void){
+ cson_value * payV = NULL;
+ Stmt q;
+ if(!g.perm.Admin && !g.perm.Setup){
+ json_set_err(FSL_JSON_E_DENIED,
+ "Requires 'a' or 's' privileges.");
+ return NULL;
+ }
+ db_prepare(&q,"SELECT uid AS uid,"
+ " login AS name,"
+ " cap AS capabilities,"
+ " info AS info,"
+ " mtime AS timestamp"
+ " FROM user ORDER BY login");
+ payV = json_stmt_to_array_of_obj(&q, NULL);
+ db_finalize(&q);
+ if(NULL == payV){
+ json_set_err(FSL_JSON_E_UNKNOWN,
+ "Could not convert user list to JSON.");
+ }
+ return payV;
+}
+
+/*
+** Creates a new JSON Object based on the db state of
+** the given user name. On error (no record found)
+** it returns NULL, else the caller owns the returned
+** object.
+*/
+static cson_value * json_load_user_by_name(char const * zName){
+ cson_value * u = NULL;
+ Stmt q;
+ db_prepare(&q,"SELECT uid AS uid,"
+ " login AS name,"
+ " cap AS capabilities,"
+ " info AS info,"
+ " mtime AS timestamp"
+ " FROM user"
+ " WHERE login=%Q",
+ zName);
+ if( (SQLITE_ROW == db_step(&q)) ){
+ u = cson_sqlite3_row_to_object(q.pStmt);
+ }
+ db_finalize(&q);
+ return u;
+}
+
+/*
+** Identical to json_load_user_by_name(), but expects a user ID. Returns
+** NULL if no user found with that ID.
+*/
+static cson_value * json_load_user_by_id(int uid){
+ cson_value * u = NULL;
+ Stmt q;
+ db_prepare(&q,"SELECT uid AS uid,"
+ " login AS name,"
+ " cap AS capabilities,"
+ " info AS info,"
+ " mtime AS timestamp"
+ " FROM user"
+ " WHERE uid=%d",
+ uid);
+ if( (SQLITE_ROW == db_step(&q)) ){
+ u = cson_sqlite3_row_to_object(q.pStmt);
+ }
+ db_finalize(&q);
+ return u;
+}
+
+
+/*
+** Impl of /json/user/get. Requires admin or setup rights.
+*/
+static cson_value * json_user_get(void){
+ cson_value * payV = NULL;
+ char const * pUser = NULL;
+ if(!g.perm.Admin && !g.perm.Setup){
+ json_set_err(FSL_JSON_E_DENIED,
+ "Requires 'a' or 's' privileges.");
+ return NULL;
+ }
+ pUser = json_find_option_cstr2("name", NULL, NULL, g.json.dispatchDepth+1);
+ if(!pUser || !*pUser){
+ json_set_err(FSL_JSON_E_MISSING_ARGS,"Missing 'name' property.");
+ return NULL;
+ }
+ payV = json_load_user_by_name(pUser);
+ if(!payV){
+ json_set_err(FSL_JSON_E_RESOURCE_NOT_FOUND,"User not found.");
+ }
+ return payV;
+}
+
+/*
+** Expects pUser to contain fossil user fields in JSON form: name,
+** uid, info, capabilities, password.
+**
+** At least one of (name, uid) must be included. All others are
+** optional and their db fields will not be updated if those fields
+** are not included in pUser.
+**
+** If uid is specified then name may refer to a _new_ name
+** for a user, otherwise the name must refer to an existing user.
+** If uid=-1 then the name must be specified and a new user is
+** created (fails if one already exists).
+**
+** If uid is not set, this function might modify pUser to contain the
+** db-found (or inserted) user ID.
+**
+** On error g.json's error state is set and one of the FSL_JSON_E_xxx
+** values from FossilJsonCodes is returned.
+**
+** On success the db record for the given user is updated.
+**
+** Requires either Admin, Setup, or Password access. Non-admin/setup
+** users can only change their own information. Non-setup users may
+** not modify the 's' permission. Admin users without setup
+** permissions may not edit any other user who has the 's' permission.
+**
+*/
+int json_user_update_from_json( cson_object * pUser ){
+#define CSTR(X) cson_string_cstr(cson_value_get_string( cson_object_get(pUser, \
+ X ) ))
+ char const * zName = CSTR("name");
+ char const * zNameNew = zName;
+ char * zNameFree = NULL;
+ char const * zInfo = CSTR("info");
+ char const * zCap = CSTR("capabilities");
+ char const * zPW = CSTR("password");
+ cson_value const * forceLogout = cson_object_get(pUser, "forceLogout");
+ int gotFields = 0;
+#undef CSTR
+ cson_int_t uid = cson_value_get_integer( cson_object_get(pUser, "uid") );
+ char const tgtHasSetup = zCap && (NULL!=strchr(zCap, 's'));
+ char tgtHadSetup = 0;
+ Blob sql = empty_blob;
+ Stmt q = empty_Stmt;
+
+ if(uid<=0 && (!zName||!*zName)){
+ return json_set_err(FSL_JSON_E_MISSING_ARGS,
+ "One of 'uid' or 'name' is required.");
+ }else if(uid>0){
+ zNameFree = db_text(NULL, "SELECT login FROM user WHERE uid=%d",uid);
+ if(!zNameFree){
+ return json_set_err(FSL_JSON_E_RESOURCE_NOT_FOUND,
+ "No login found for uid %d.", uid);
+ }
+ zName = zNameFree;
+ }else if(-1==uid){
+ /* try to create a new user */
+ if(!g.perm.Admin && !g.perm.Setup){
+ json_set_err(FSL_JSON_E_DENIED,
+ "Requires 'a' or 's' privileges.");
+ goto error;
+ }else if(!zName || !*zName){
+ json_set_err(FSL_JSON_E_MISSING_ARGS,
+ "No name specified for new user.");
+ goto error;
+ }else if( db_exists("SELECT 1 FROM user WHERE login=%Q", zName) ){
+ json_set_err(FSL_JSON_E_RESOURCE_ALREADY_EXISTS,
+ "User %s already exists.", zName);
+ goto error;
+ }else{
+ Stmt ins = empty_Stmt;
+ db_unprotect(PROTECT_USER);
+ db_prepare(&ins, "INSERT INTO user (login) VALUES(%Q)",zName);
+ db_step( &ins );
+ db_finalize(&ins);
+ db_protect_pop();
+ uid = db_int(0,"SELECT uid FROM user WHERE login=%Q", zName);
+ assert(uid>0);
+ zNameNew = zName;
+ cson_object_set( pUser, "uid", cson_value_new_integer(uid) );
+ }
+ }else{
+ uid = db_int(0,"SELECT uid FROM user WHERE login=%Q", zName);
+ if(uid<=0){
+ json_set_err(FSL_JSON_E_RESOURCE_NOT_FOUND,
+ "No login found for user [%s].", zName);
+ goto error;
+ }
+ cson_object_set( pUser, "uid", cson_value_new_integer(uid) );
+ }
+
+ /* Maintenance note: all error-returns from here on out should go
+ via 'goto error' in order to clean up.
+ */
+
+ if(uid != g.userUid){
+ if(!g.perm.Admin && !g.perm.Setup){
+ json_set_err(FSL_JSON_E_DENIED,
+ "Changing another user's data requires "
+ "'a' or 's' privileges.");
+ goto error;
+ }
+ }
+ /* check if the target uid currently has setup rights. */
+ tgtHadSetup = db_int(0,"SELECT 1 FROM user where uid=%d"
+ " AND cap GLOB '*s*'", uid);
+
+ if((tgtHasSetup || tgtHadSetup) && !g.perm.Setup){
+ /*
+ Do not allow a non-setup user to set or remove setup
+ privileges. setup.c uses similar logic.
+ */
+ json_set_err(FSL_JSON_E_DENIED,
+ "Modifying 's' users/privileges requires "
+ "'s' privileges.");
+ goto error;
+ }
+ /*
+ Potential todo: do not allow a setup user to remove 's' from
+ himself, to avoid locking himself out?
+ */
+
+ blob_append(&sql, "UPDATE user SET",-1 );
+ blob_append(&sql, " mtime=cast(strftime('%s') AS INTEGER)", -1);
+
+ if((uid>0) && zNameNew){
+ /* Check for name change... */
+ if(0!=strcmp(zName,zNameNew)){
+ if( (!g.perm.Admin && !g.perm.Setup)
+ && (zName != zNameNew)){
+ json_set_err( FSL_JSON_E_DENIED,
+ "Modifying user names requires 'a' or 's' privileges.");
+ goto error;
+ }
+ forceLogout = cson_value_true()
+ /* reminders: 1) does not allocate.
+ 2) we do this because changing a name
+ invalidates any login token because the old name
+ is part of the token hash.
+ */;
+ blob_append_sql(&sql, ", login=%Q", zNameNew);
+ ++gotFields;
+ }
+ }
+
+ if( zCap && *zCap ){
+ if(!g.perm.Admin || !g.perm.Setup){
+ /* we "could" arguably silently ignore cap in this case. */
+ json_set_err(FSL_JSON_E_DENIED,
+ "Changing capabilities requires 'a' or 's' privileges.");
+ goto error;
+ }
+ blob_append_sql(&sql, ", cap=%Q", zCap);
+ ++gotFields;
+ }
+
+ if( zPW && *zPW ){
+ if(!g.perm.Admin && !g.perm.Setup && !g.perm.Password){
+ json_set_err( FSL_JSON_E_DENIED,
+ "Password change requires 'a', 's', "
+ "or 'p' permissions.");
+ goto error;
+ }else{
+#define TRY_LOGIN_GROUP 0 /* login group support is not yet implemented. */
+#if !TRY_LOGIN_GROUP
+ char * zPWHash = NULL;
+ ++gotFields;
+ zPWHash = sha1_shared_secret(zPW, zNameNew ? zNameNew : zName, NULL);
+ blob_append_sql(&sql, ", pw=%Q", zPWHash);
+ free(zPWHash);
+#else
+ ++gotFields;
+ blob_append_sql(&sql, ", pw=coalesce(shared_secret(%Q,%Q,"
+ "(SELECT value FROM config WHERE name='project-code')))",
+ zPW, zNameNew ? zNameNew : zName);
+ /* shared_secret() func is undefined? */
+#endif
+ }
+ }
+
+ if( zInfo ){
+ blob_append_sql(&sql, ", info=%Q", zInfo);
+ ++gotFields;
+ }
+
+ if((g.perm.Admin || g.perm.Setup)
+ && forceLogout && cson_value_get_bool(forceLogout)){
+ blob_append(&sql, ", cookie=NULL, cexpire=NULL", -1);
+ ++gotFields;
+ }
+
+ if(!gotFields){
+ json_set_err( FSL_JSON_E_MISSING_ARGS,
+ "Required user data are missing.");
+ goto error;
+ }
+ assert(uid>0);
+#if !TRY_LOGIN_GROUP
+ blob_append_sql(&sql, " WHERE uid=%d", uid);
+#else /* need name for login group support :/ */
+ blob_append_sql(&sql, " WHERE login=%Q", zName);
+#endif
+#if 0
+ puts(blob_str(&sql));
+ cson_output_FILE( cson_object_value(pUser), stdout, NULL );
+#endif
+ db_unprotect(PROTECT_USER);
+ db_prepare(&q, "%s", blob_sql_text(&sql));
+ db_exec(&q);
+ db_finalize(&q);
+ db_protect_pop();
+#if TRY_LOGIN_GROUP
+ if( zPW || cson_value_get_bool(forceLogout) ){
+ Blob groupSql = empty_blob;
+ char * zErr = NULL;
+ blob_append_sql(&groupSql,
+ "INSERT INTO user(login)"
+ " SELECT %Q WHERE NOT EXISTS(SELECT 1 FROM user WHERE login=%Q);",
+ zName, zName
+ );
+ blob_append(&groupSql, blob_str(&sql), blob_size(&sql));
+ db_unprotect(PROTECT_USER);
+ login_group_sql(blob_str(&groupSql), NULL, NULL, &zErr);
+ db_protect_pop();
+ blob_reset(&groupSql);
+ if( zErr ){
+ json_set_err( FSL_JSON_E_UNKNOWN,
+ "Repo-group update at least partially failed: %s",
+ zErr);
+ free(zErr);
+ goto error;
+ }
+ }
+#endif /* TRY_LOGIN_GROUP */
+
+#undef TRY_LOGIN_GROUP
+
+ free( zNameFree );
+ blob_reset(&sql);
+ return 0;
+
+ error:
+ assert(0 != g.json.resultCode);
+ free(zNameFree);
+ blob_reset(&sql);
+ return g.json.resultCode;
+}
+
+
+/*
+** Impl of /json/user/save.
+*/
+static cson_value * json_user_save(void){
+ /* try to get user info from GET/CLI args and construct
+ a JSON form of it... */
+ cson_object * u = cson_new_object();
+ char const * str = NULL;
+ int b = -1;
+ int i = -1;
+ int uid = -1;
+ cson_value * payload = NULL;
+ /* String properties... */
+#define PROP(LK,SK) str = json_find_option_cstr(LK,NULL,SK); \
+ if(str){ cson_object_set(u, LK, json_new_string(str)); } (void)0
+ PROP("name","n");
+ PROP("password","p");
+ PROP("info","i");
+ PROP("capabilities","c");
+#undef PROP
+ /* Boolean properties... */
+#define PROP(LK,DFLT) b = json_find_option_bool(LK,NULL,NULL,DFLT); \
+ if(DFLT!=b){ cson_object_set(u, LK, cson_value_new_bool(b ? 1 : 0)); } (void)0
+ PROP("forceLogout",-1);
+#undef PROP
+
+#define PROP(LK,DFLT) i = json_find_option_int(LK,NULL,NULL,DFLT); \
+ if(DFLT != i){ cson_object_set(u, LK, cson_value_new_integer(i)); } (void)0
+ PROP("uid",-99);
+#undef PROP
+ if( g.json.reqPayload.o ){
+ cson_object_merge( u, g.json.reqPayload.o, CSON_MERGE_NO_RECURSE );
+ }
+ json_user_update_from_json( u );
+ if(!g.json.resultCode){
+ uid = cson_value_get_integer( cson_object_get(u, "uid") );
+ assert((uid>0) && "Something went wrong in json_user_update_from_json()");
+ payload = json_load_user_by_id(uid);
+ }
+ cson_free_object(u);
+ return payload;
+}
+#endif /* FOSSIL_ENABLE_JSON */