diff options
Diffstat (limited to 'src/setupuser.c')
| -rw-r--r-- | src/setupuser.c | 1192 |
1 files changed, 1192 insertions, 0 deletions
diff --git a/src/setupuser.c b/src/setupuser.c new file mode 100644 index 0000000..667d9de --- /dev/null +++ b/src/setupuser.c @@ -0,0 +1,1192 @@ +/* +** Copyright (c) 2007 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/ +** +******************************************************************************* +** +** Setup pages associated with user management. The code in this +** file was formerly part of the "setup.c" module, but has been broken +** out into its own module to improve maintainability. +** +** Note: Do not confuse "Users" with "Subscribers". Code to deal with +** subscribers is over in the "alerts.c" source file. +*/ +#include "config.h" +#include <assert.h> +#include "setupuser.h" + +/* +** WEBPAGE: setup_ulist +** +** Show a list of users. Clicking on any user jumps to the edit +** screen for that user. Requires Admin privileges. +** +** Query parameters: +** +** with=CAP Only show users that have one or more capabilities in CAP. +** ubg Color backgrounds by username hash +*/ +void setup_ulist(void){ + Stmt s; + double rNow; + const char *zWith = P("with"); + int bUnusedOnly = P("unused")!=0; + int bUbg = P("ubg")!=0; + int bHaveAlerts; + + login_check_credentials(); + if( !g.perm.Admin ){ + login_needed(0); + return; + } + bHaveAlerts = alert_tables_exist(); + style_submenu_element("Add", "setup_uedit"); + style_submenu_element("Log", "access_log"); + style_submenu_element("Help", "setup_ulist_notes"); + if( bHaveAlerts ){ + style_submenu_element("Subscribers", "subscribers"); + } + style_set_current_feature("setup"); + style_header("User List"); + if( (zWith==0 || zWith[0]==0) && !bUnusedOnly ){ + @ <table border=1 cellpadding=2 cellspacing=0 class='userTable'> + @ <thead><tr> + @ <th>Category + @ <th>Capabilities (<a href='%R/setup_ucap_list'>key</a>) + @ <th>Info <th>Last Change</tr></thead> + @ <tbody> + db_prepare(&s, + "SELECT uid, login, cap, date(mtime,'unixepoch')" + " FROM user" + " WHERE login IN ('anonymous','nobody','developer','reader')" + " ORDER BY login" + ); + while( db_step(&s)==SQLITE_ROW ){ + int uid = db_column_int(&s, 0); + const char *zLogin = db_column_text(&s, 1); + const char *zCap = db_column_text(&s, 2); + const char *zDate = db_column_text(&s, 4); + @ <tr> + @ <td><a href='setup_uedit?id=%d(uid)'>%h(zLogin)</a> + @ <td>%h(zCap) + + if( fossil_strcmp(zLogin,"anonymous")==0 ){ + @ <td>All logged-in users + }else if( fossil_strcmp(zLogin,"developer")==0 ){ + @ <td>Users with '<b>v</b>' capability + }else if( fossil_strcmp(zLogin,"nobody")==0 ){ + @ <td>All users without login + }else if( fossil_strcmp(zLogin,"reader")==0 ){ + @ <td>Users with '<b>u</b>' capability + }else{ + @ <td> + } + if( zDate && zDate[0] ){ + @ <td>%h(zDate) + }else{ + @ <td> + } + @ </tr> + } + db_finalize(&s); + @ </tbody></table> + @ <div class='section'>Users</div> + }else{ + style_submenu_element("All Users", "setup_ulist"); + if( bUnusedOnly ){ + @ <div class='section'>Unused logins</div> + }else if( zWith ){ + if( zWith[1]==0 ){ + @ <div class='section'>Users with capability "%h(zWith)"</div> + }else{ + @ <div class='section'>Users with any capability in "%h(zWith)"</div> + } + } + } + if( !bUnusedOnly ){ + style_submenu_element("Unused", "setup_ulist?unused"); + } + @ <table border=1 cellpadding=2 cellspacing=0 class='userTable sortable' \ + @ data-column-types='ktxKTKt' data-init-sort='4'> + @ <thead><tr> + @ <th>Login Name<th>Caps<th>Info<th>Date<th>Expire<th>Last Login\ + @ <th>Alerts</tr></thead> + @ <tbody> + db_multi_exec( + "CREATE TEMP TABLE lastAccess(uname TEXT PRIMARY KEY, atime REAL)" + "WITHOUT ROWID;" + ); + if( db_table_exists("repository","accesslog") ){ + db_multi_exec( + "INSERT INTO lastAccess(uname, atime)" + " SELECT uname, max(mtime) FROM (" + " SELECT uname, mtime FROM accesslog WHERE success" + " UNION ALL" + " SELECT login AS uname, rcvfrom.mtime AS mtime" + " FROM rcvfrom JOIN user USING(uid))" + " GROUP BY 1;" + ); + } + if( !db_table_exists("repository","subscriber") ){ + db_multi_exec( + "CREATE TEMP TABLE subscriber(suname PRIMARY KEY, ssub, subscriberId)" + "WITHOUT ROWID;" + ); + } + if( bUnusedOnly ){ + zWith = mprintf( + " AND login NOT IN (" + "SELECT user FROM event WHERE user NOT NULL " + "UNION ALL SELECT euser FROM event WHERE euser NOT NULL%s)" + " AND uid NOT IN (SELECT uid FROM rcvfrom)", + bHaveAlerts ? + " UNION ALL SELECT suname FROM subscriber WHERE suname NOT NULL":""); + }else if( zWith && zWith[0] ){ + zWith = mprintf(" AND fullcap(cap) GLOB '*[%q]*'", zWith); + }else{ + zWith = ""; + } + db_prepare(&s, + /*0-4*/"SELECT uid, login, cap, info, date(user.mtime,'unixepoch')," + /* 5 */"lower(login) AS sortkey, " + /* 6 */"CASE WHEN info LIKE '%%expires 20%%'" + " THEN substr(info,instr(lower(info),'expires')+8,10)" + " END AS exp," + /* 7 */"atime," + /* 8 */"user.mtime AS sorttime," + /*9-11*/"%s" + " FROM user LEFT JOIN lastAccess ON login=uname" + " LEFT JOIN subscriber ON login=suname" + " WHERE login NOT IN ('anonymous','nobody','developer','reader') %s" + " ORDER BY sorttime DESC", + bHaveAlerts + ? "subscriber.ssub, subscriber.subscriberId, subscriber.semail" + : "null, null, null", + zWith/*safe-for-%s*/ + ); + rNow = db_double(0.0, "SELECT julianday('now');"); + while( db_step(&s)==SQLITE_ROW ){ + int uid = db_column_int(&s, 0); + const char *zLogin = db_column_text(&s, 1); + const char *zCap = db_column_text(&s, 2); + const char *zInfo = db_column_text(&s, 3); + const char *zDate = db_column_text(&s, 4); + const char *zSortKey = db_column_text(&s,5); + const char *zExp = db_column_text(&s,6); + double rATime = db_column_double(&s,7); + char *zAge = 0; + const char *zSub; + int sid = db_column_int(&s,10); + sqlite3_int64 sorttime = db_column_int64(&s, 8); + if( rATime>0.0 ){ + zAge = human_readable_age(rNow - rATime); + } + if( bUbg ){ + @ <tr style='background-color: %h(user_color(zLogin));'> + }else{ + @ <tr> + } + @ <td data-sortkey='%h(zSortKey)'>\ + @ <a href='setup_uedit?id=%d(uid)'>%h(zLogin)</a> + @ <td>%h(zCap) + @ <td>%h(zInfo) + @ <td data-sortkey='%09llx(sorttime)'>%h(zDate?zDate:"") + @ <td>%h(zExp?zExp:"") + @ <td data-sortkey='%f(rATime)' style='white-space:nowrap'>%s(zAge?zAge:"") + if( db_column_type(&s,9)==SQLITE_NULL ){ + @ <td> + }else if( (zSub = db_column_text(&s,9))==0 || zSub[0]==0 ){ + @ <td><a href="%R/alerts?sid=%d(sid)"><i>off</i></a> + }else{ + const char *zEmail = db_column_text(&s, 11); + char * zAt = zEmail ? mprintf(" → %h", zEmail) : mprintf(""); + @ <td><a href="%R/alerts?sid=%d(sid)">%h(zSub)</a> %z(zAt) + } + + @ </tr> + fossil_free(zAge); + } + @ </tbody></table> + db_finalize(&s); + style_table_sorter(); + style_finish_page(); +} + +/* +** WEBPAGE: setup_ulist_notes +** +** A documentation page showing notes about user configuration. This +** information used to be a side-bar on the user list page, but has been +** factored out for improved presentation. +*/ +void setup_ulist_notes(void){ + style_set_current_feature("setup"); + style_header("User Configuration Notes"); + @ <h1>User Configuration Notes:</h1> + @ <ol> + @ <li><p> + @ Every user, logged in or not, inherits the privileges of + @ <span class="usertype">nobody</span>. + @ </p></li> + @ + @ <li><p> + @ Any human can login as <span class="usertype">anonymous</span> since the + @ password is clearly displayed on the login page for them to type. The + @ purpose of requiring anonymous to log in is to prevent access by spiders. + @ Every logged-in user inherits the combined privileges of + @ <span class="usertype">anonymous</span> and + @ <span class="usertype">nobody</span>. + @ </p></li> + @ + @ <li><p> + @ Users with privilege <span class="capability">u</span> inherit the combined + @ privileges of <span class="usertype">reader</span>, + @ <span class="usertype">anonymous</span>, and + @ <span class="usertype">nobody</span>. + @ </p></li> + @ + @ <li><p> + @ Users with privilege <span class="capability">v</span> inherit the combined + @ privileges of <span class="usertype">developer</span>, + @ <span class="usertype">anonymous</span>, and + @ <span class="usertype">nobody</span>. + @ </p></li> + @ + @ <li><p>The permission flags are as follows:</p> + capabilities_table(CAPCLASS_ALL); + @ </li> + @ </ol> + style_finish_page(); +} + +/* +** WEBPAGE: setup_ucap_list +** +** A documentation page showing the meaning of the various user capabilities +** code letters. +*/ +void setup_ucap_list(void){ + style_set_current_feature("setup"); + style_header("User Capability Codes"); + @ <h1>All capabilities</h1> + capabilities_table(CAPCLASS_ALL); + @ <h1>Capabilities associated with checked-in content</h1> + capabilities_table(CAPCLASS_CODE); + @ <h1>Capabilities associated with data transfer and sync</h1> + capabilities_table(CAPCLASS_DATA); + @ <h1>Capabilities associated with the forum</h1> + capabilities_table(CAPCLASS_FORUM); + @ <h1>Capabilities associated with tickets</h1> + capabilities_table(CAPCLASS_TKT); + @ <h1>Capabilities associated with wiki</h1> + capabilities_table(CAPCLASS_WIKI); + @ <h1>Administrative capabilities</h1> + capabilities_table(CAPCLASS_SUPER); + @ <h1>Miscellaneous capabilities</h1> + capabilities_table(CAPCLASS_OTHER); + style_finish_page(); +} + +/* +** Return true if zPw is a valid password string. A valid +** password string is: +** +** (1) A zero-length string, or +** (2) a string that contains a character other than '*'. +*/ +static int isValidPwString(const char *zPw){ + if( zPw==0 ) return 0; + if( zPw[0]==0 ) return 1; + while( zPw[0]=='*' ){ zPw++; } + return zPw[0]!=0; +} + +/* +** Return true if user capability strings zOrig and zNew materially +** differ, taking into account that they may be sorted in an arbitary +** order. This does not take inherited permissions into +** account. Either argument may be NULL. A NULL and an empty string +** are considered equivalent here. e.g. "abc" and "cab" are equivalent +** for this purpose, but "aCb" and "acb" are not. +*/ +static int userCapsChanged(const char *zOrig, const char *zNew){ + if( !zOrig ){ + return zNew ? (0!=*zNew) : 0; + }else if( !zNew ){ + return 0!=*zOrig; + }else if( 0==fossil_strcmp(zOrig, zNew) ){ + return 0; + }else{ + /* We don't know that zOrig and zNew are sorted equivalently. The + ** following steps will compare strings which contain all the same + ** capabilities letters as equivalent, regardless of the letters' + ** order in their strings. */ + char aOrig[128]; /* table of zOrig bytes */ + int nOrig = 0, nNew = 0; + + memset( &aOrig[0], 0, sizeof(aOrig) ); + for( ; *zOrig; ++zOrig, ++nOrig ){ + if( 0==(*zOrig & 0x80) ){ + aOrig[(int)*zOrig] = 1; + } + } + for( ; *zNew; ++zNew, ++nNew ){ + if( 0==(*zNew & 0x80) && !aOrig[(int)*zNew] ){ + return 1; + } + } + return nOrig!=nNew; + } +} + +/* +** COMMAND: test-user-caps-changed +** +** Usage: %fossil test-user-caps-changed caps1 caps2 +** +*/ +void test_user_caps_changed(void){ + + char const * zOld = g.argc>2 ? g.argv[2] : NULL; + char const * zNew = g.argc>3 ? g.argv[3] : NULL; + fossil_print("Has changes? = %d\n", + userCapsChanged( zOld, zNew )); +} + +/* +** Sends notification of user permission elevation changes to all +** subscribers with a "u" subscription. This is a no-op if alerts are +** not enabled. +** +** These subscriptions differ from most, in that: +** +** - They currently lack an "unsubscribe" link. +** +** - Only an admin can assign this subscription, but if a non-admin +** edits their subscriptions after an admin assigns them this one, +** this particular one will be lost. "Feature or bug?" is unclear, +** but it would be odd for a non-admin to be assigned this +** capability. +*/ +static void alert_user_cap_change(const char *zLogin, /*Affected user*/ + int uid, /*[user].uid*/ + int bIsNew, /*true if new user*/ + const char *zOrigCaps,/*Old caps*/ + const char *zNewCaps /*New caps*/){ + Blob hdr, body; + Stmt q; + int nBody; + AlertSender *pSender; + char *zSubname; + char *zURL; + char * zSubject; + + if( !alert_enabled() ) return; + zSubject = bIsNew + ? mprintf("New user created: [%q]", zLogin) + : mprintf("User [%q] capabilities changed", zLogin); + zURL = db_get("email-url",0); + zSubname = db_get("email-subname", "[Fossil Repo]"); + blob_init(&body, 0, 0); + blob_init(&hdr, 0, 0); + if( bIsNew ){ + blob_appendf(&body, "User [%q] was created with " + "permissions [%q] by user [%q].\n", + zLogin, zNewCaps, g.zLogin); + } else { + blob_appendf(&body, "Permissions for user [%q] were changed " + "from [%q] to [%q] by user [%q].\n", + zLogin, zOrigCaps, zNewCaps, g.zLogin); + } + if( zURL ){ + blob_appendf(&body, "\nUser editor: %s/setup_uedit?id=%d\n", zURL, uid); + } + nBody = blob_size(&body); + pSender = alert_sender_new(0, 0); + db_prepare(&q, + "SELECT semail, hex(subscriberCode)" + " FROM subscriber, user " + " WHERE sverified AND NOT sdonotcall" + " AND suname=login" + " AND ssub GLOB '*u*'"); + while( !pSender->zErr && db_step(&q)==SQLITE_ROW ){ + const char *zTo = db_column_text(&q, 0); + blob_truncate(&hdr, 0); + blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", + zTo, zSubname, zSubject); + if( zURL ){ + const char *zCode = db_column_text(&q, 1); + blob_truncate(&body, nBody); + blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n", + zURL, zCode); + } + alert_send(pSender, &hdr, &body, 0); + } + db_finalize(&q); + alert_sender_free(pSender); + fossil_free(zURL); + fossil_free(zSubname); + fossil_free(zSubject); +} + +/* +** WEBPAGE: setup_uedit +** +** Edit information about a user or create a new user. +** Requires Admin privileges. +*/ +void user_edit(void){ + const char *zId, *zLogin, *zInfo, *zCap, *zPw; + const char *zGroup; + const char *zOldLogin; + int uid, i; + char *zOldCaps = 0; /* Capabilities before edit */ + char *zDeleteVerify = 0; /* Delete user verification text */ + int higherUser = 0; /* True if user being edited is SETUP and the */ + /* user doing the editing is ADMIN. Disallow editing */ + const char *inherit[128]; + int a[128]; + const char *oa[128]; + + /* Must have ADMIN privileges to access this page + */ + login_check_credentials(); + if( !g.perm.Admin ){ login_needed(0); return; } + + /* Check to see if an ADMIN user is trying to edit a SETUP account. + ** Don't allow that. + */ + zId = PD("id", "0"); + uid = atoi(zId); + if( uid>0 ){ + zOldCaps = db_text("", "SELECT cap FROM user WHERE uid=%d",uid); + if( zId && !g.perm.Setup ){ + higherUser = zOldCaps && strchr(zOldCaps,'s'); + } + } + + if( P("can") ){ + /* User pressed the cancel button */ + cgi_redirect(cgi_referer("setup_ulist")); + return; + } + + /* Check for requests to delete the user */ + if( P("delete") && cgi_csrf_safe(2) ){ + int n; + if( P("verifydelete") ){ + /* Verified delete user request */ + db_unprotect(PROTECT_USER); + if( alert_tables_exist() ){ + /* Also delete any subscriptions associated with this user */ + db_multi_exec("DELETE FROM subscriber WHERE suname=" + "(SELECT login FROM user WHERE uid=%d)", uid); + } + db_multi_exec("DELETE FROM user WHERE uid=%d", uid); + db_protect_pop(); + moderation_disapprove_for_missing_users(); + admin_log("Deleted user [%s] (uid %d).", + PD("login","???")/*safe-for-%s*/, uid); + cgi_redirect(cgi_referer("setup_ulist")); + return; + } + n = db_int(0, "SELECT count(*) FROM event" + " WHERE user=%Q AND objid NOT IN private", + P("login")); + if( n==0 ){ + zDeleteVerify = mprintf("Check this box and press \"Delete User\" again"); + }else{ + zDeleteVerify = mprintf( + "User \"%s\" has %d or more artifacts in the block-chain. " + "Delete anyhow?", + P("login")/*safe-for-%s*/, n); + } + } + + style_set_current_feature("setup"); + + /* If we have all the necessary information, write the new or + ** modified user record. After writing the user record, redirect + ** to the page that displays a list of users. + */ + if( !cgi_all("login","info","pw","apply") ){ + /* need all of the above properties to make a change. Since one or + ** more are missing, no-op */ + }else if( higherUser ){ + /* An Admin (a) user cannot edit a Superuser (s) */ + }else if( zDeleteVerify!=0 ){ + /* Need to verify a delete request */ + }else if( !cgi_csrf_safe(2) ){ + /* This might be a cross-site request forgery, so ignore it */ + }else{ + /* We have all the information we need to make the change to the user */ + char c; + int bCapsChanged = 0 /* 1 if user's permissions changed */; + const int bIsNew = uid<=0; + char aCap[70], zNm[4]; + zNm[0] = 'a'; + zNm[2] = 0; + for(i=0, c='a'; c<='z'; c++){ + zNm[1] = c; + a[c&0x7f] = ((c!='s' && c!='y') || g.perm.Setup) && P(zNm)!=0; + if( a[c&0x7f] ) aCap[i++] = c; + } + for(c='0'; c<='9'; c++){ + zNm[1] = c; + a[c&0x7f] = P(zNm)!=0; + if( a[c&0x7f] ) aCap[i++] = c; + } + for(c='A'; c<='Z'; c++){ + zNm[1] = c; + a[c&0x7f] = P(zNm)!=0; + if( a[c&0x7f] ) aCap[i++] = c; + } + + aCap[i] = 0; + bCapsChanged = bIsNew || userCapsChanged(zOldCaps, &aCap[0]); + zPw = P("pw"); + zLogin = P("login"); + if( strlen(zLogin)==0 ){ + const char *zRef = cgi_referer("setup_ulist"); + style_header("User Creation Error"); + @ <span class="loginError">Empty login not allowed.</span> + @ + @ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)"> + @ [Bummer]</a></p> + style_finish_page(); + return; + } + if( isValidPwString(zPw) ){ + zPw = sha1_shared_secret(zPw, zLogin, 0); + }else{ + zPw = db_text(0, "SELECT pw FROM user WHERE uid=%d", uid); + } + zOldLogin = db_text(0, "SELECT login FROM user WHERE uid=%d", uid); + if( db_exists("SELECT 1 FROM user WHERE login=%Q AND uid!=%d",zLogin,uid) ){ + const char *zRef = cgi_referer("setup_ulist"); + style_header("User Creation Error"); + @ <span class="loginError">Login "%h(zLogin)" is already used by + @ a different user.</span> + @ + @ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)"> + @ [Bummer]</a></p> + style_finish_page(); + return; + } + cgi_csrf_verify(); + db_unprotect(PROTECT_USER); + uid = db_int(0, + "REPLACE INTO user(uid,login,info,pw,cap,mtime) " + "VALUES(nullif(%d,0),%Q,%Q,%Q,%Q,now()) " + "RETURNING uid", + uid, zLogin, P("info"), zPw, &aCap[0]); + assert( uid>0 ); + if( zOldLogin && fossil_strcmp(zLogin, zOldLogin)!=0 ){ + if( alert_tables_exist() ){ + /* Rename matching subscriber entry, else the user cannot + re-subscribe with their same email address. */ + db_multi_exec("UPDATE subscriber SET suname=%Q WHERE suname=%Q", + zLogin, zOldLogin); + } + admin_log( "Renamed user [%q] to [%q].", zOldLogin, zLogin ); + } + db_protect_pop(); + setup_incr_cfgcnt(); + admin_log( "%s user [%q] with capabilities [%q].", + bIsNew ? "Added" : "Updated", + zLogin, &aCap[0] ); + if( atoi(PD("all","0"))>0 ){ + Blob sql; + char *zErr = 0; + blob_zero(&sql); + if( zOldLogin==0 ){ + blob_appendf(&sql, + "INSERT INTO user(login)" + " SELECT %Q WHERE NOT EXISTS(SELECT 1 FROM user WHERE login=%Q);", + zLogin, zLogin + ); + zOldLogin = zLogin; + } +#if 0 + /* Problem: when renaming a user we need to update the subscriber + ** names to match but we cannot know from here if each member of + ** the login group has the subscriber tables, so we cannot blindly + ** include this SQL. */ + else if( fossil_strcmp(zLogin, zOldLogin)!=0 + && alert_tables_exist() ){ + /* Rename matching subscriber entry, else the user cannot + re-subscribe with their same email address. */ + blob_appendf(&sql, + "UPDATE subscriber SET suname=%Q WHERE suname=%Q;", + zLogin, zOldLogin); + } +#endif + blob_appendf(&sql, + "UPDATE user SET login=%Q," + " pw=coalesce(shared_secret(%Q,%Q," + "(SELECT value FROM config WHERE name='project-code')),pw)," + " info=%Q," + " cap=%Q," + " mtime=now()" + " WHERE login=%Q;", + zLogin, P("pw"), zLogin, P("info"), &aCap[0], + zOldLogin + ); + db_unprotect(PROTECT_USER); + login_group_sql(blob_str(&sql), "<li> ", " </li>\n", &zErr); + db_protect_pop(); + blob_reset(&sql); + admin_log( "Updated user [%q] in all login groups " + "with capabilities [%q].", + zLogin, &aCap[0] ); + if( zErr ){ + const char *zRef = cgi_referer("setup_ulist"); + style_header("User Change Error"); + admin_log( "Error updating user '%q': %s'.", zLogin, zErr ); + @ <span class="loginError">%h(zErr)</span> + @ + @ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)"> + @ [Bummer]</a></p> + style_finish_page(); + if( bCapsChanged ){ + /* It's possible that caps were updated locally even if + ** login group updates failed. */ + alert_user_cap_change(zLogin, uid, bIsNew, zOldCaps, &aCap[0]); + } + return; + } + } + if( bCapsChanged ){ + alert_user_cap_change(zLogin, uid, bIsNew, zOldCaps, &aCap[0]); + } + cgi_redirect(cgi_referer("setup_ulist")); + return; + } + + /* Load the existing information about the user, if any + */ + zLogin = ""; + zInfo = ""; + zCap = zOldCaps; + zPw = ""; + for(i='a'; i<='z'; i++) oa[i] = ""; + for(i='0'; i<='9'; i++) oa[i] = ""; + for(i='A'; i<='Z'; i++) oa[i] = ""; + if( uid ){ + assert( zCap ); + zLogin = db_text("", "SELECT login FROM user WHERE uid=%d", uid); + zInfo = db_text("", "SELECT info FROM user WHERE uid=%d", uid); + zPw = db_text("", "SELECT pw FROM user WHERE uid=%d", uid); + for(i=0; zCap[i]; i++){ + char c = zCap[i]; + if( (c>='a' && c<='z') || (c>='0' && c<='9') || (c>='A' && c<='Z') ){ + oa[c&0x7f] = " checked=\"checked\""; + } + } + } + + /* figure out inherited permissions */ + memset((char *)inherit, 0, sizeof(inherit)); + if( fossil_strcmp(zLogin, "developer") ){ + char *z1, *z2; + z1 = z2 = db_text(0,"SELECT cap FROM user WHERE login='developer'"); + while( z1 && *z1 ){ + inherit[0x7f & *(z1++)] = + "<span class=\"ueditInheritDeveloper\"><sub>[D]</sub></span>"; + } + free(z2); + } + if( fossil_strcmp(zLogin, "reader") ){ + char *z1, *z2; + z1 = z2 = db_text(0,"SELECT cap FROM user WHERE login='reader'"); + while( z1 && *z1 ){ + inherit[0x7f & *(z1++)] = + "<span class=\"ueditInheritReader\"><sub>[R]</sub></span>"; + } + free(z2); + } + if( fossil_strcmp(zLogin, "anonymous") ){ + char *z1, *z2; + z1 = z2 = db_text(0,"SELECT cap FROM user WHERE login='anonymous'"); + while( z1 && *z1 ){ + inherit[0x7f & *(z1++)] = + "<span class=\"ueditInheritAnonymous\"><sub>[A]</sub></span>"; + } + free(z2); + } + if( fossil_strcmp(zLogin, "nobody") ){ + char *z1, *z2; + z1 = z2 = db_text(0,"SELECT cap FROM user WHERE login='nobody'"); + while( z1 && *z1 ){ + inherit[0x7f & *(z1++)] = + "<span class=\"ueditInheritNobody\"><sub>[N]</sub></span>"; + } + free(z2); + } + + /* Begin generating the page + */ + style_submenu_element("Cancel", "%s", cgi_referer("setup_ulist")); + if( uid ){ + style_header("Edit User %h", zLogin); + if( !login_is_special(zLogin) ){ + style_submenu_element("Access Log", "%R/access_log?u=%t", zLogin); + style_submenu_element("Timeline","%R/timeline?u=%t", zLogin); + } + }else{ + style_header("Add A New User"); + } + @ <div class="ueditCapBox"> + @ <form action="%s(g.zPath)" method="post"><div> + login_insert_csrf_secret(); + if( login_is_special(zLogin) ){ + @ <input type="hidden" name="login" value="%s(zLogin)"> + @ <input type="hidden" name="info" value=""> + @ <input type="hidden" name="pw" value="*"> + } + @ <input type="hidden" name="referer" value="%h(cgi_referer("setup_ulist"))"> + @ <table width="100%%"> + @ <tr> + @ <td class="usetupEditLabel" id="suuid">User ID:</td> + if( uid ){ + @ <td>%d(uid) <input aria-labelledby="suuid" type="hidden" \ + @ name="id" value="%d(uid)"/>\ + @ </td> + }else{ + @ <td>(new user)<input aria-labelledby="suuid" type="hidden" name="id" \ + @ value="0"></td> + } + @ </tr> + @ <tr> + @ <td class="usetupEditLabel" id="sulgn">Login:</td> + if( login_is_special(zLogin) ){ + @ <td><b>%h(zLogin)</b></td> + }else{ + @ <td><input aria-labelledby="sulgn" type="text" name="login" \ + @ value="%h(zLogin)"> + if( alert_tables_exist() ){ + int sid; + sid = db_int(0, "SELECT subscriberId FROM subscriber" + " WHERE suname=%Q", zLogin); + if( sid>0 ){ + @ <a href="%R/alerts?sid=%d(sid)">\ + @ (subscription info for %h(zLogin))</a>\ + } + } + @ </td></tr> + @ <tr> + @ <td class="usetupEditLabel" id="sucnfo">Contact Info:</td> + @ <td><textarea aria-labelledby="sucnfo" name="info" cols="40" \ + @ rows="2">%h(zInfo)</textarea></td> + } + @ </tr> + @ <tr> + @ <td class="usetupEditLabel">Capabilities:</td> + @ <td width="100%%"> +#define B(x) inherit[x] + @ <div class="columns" style="column-width:13em;"> + @ <ul style="list-style-type: none;"> + if( g.perm.Setup ){ + @ <li><label><input type="checkbox" name="as"%s(oa['s'])> + @ Setup%s(B('s'))</label> + } + @ <li><label><input type="checkbox" name="aa"%s(oa['a'])> + @ Admin%s(B('a'))</label> + @ <li><label><input type="checkbox" name="au"%s(oa['u'])> + @ Reader%s(B('u'))</label> + @ <li><label><input type="checkbox" name="av"%s(oa['v'])> + @ Developer%s(B('v'))</label> +#if 0 /* Not Used */ + @ <li><label><input type="checkbox" name="ad"%s(oa['d'])> + @ Delete%s(B('d'))</label> +#endif + @ <li><label><input type="checkbox" name="ae"%s(oa['e'])> + @ View-PII%s(B('e'))</label> + @ <li><label><input type="checkbox" name="ap"%s(oa['p'])> + @ Password%s(B('p'))</label> + @ <li><label><input type="checkbox" name="ai"%s(oa['i'])> + @ Check-In%s(B('i'))</label> + @ <li><label><input type="checkbox" name="ao"%s(oa['o'])> + @ Check-Out%s(B('o'))</label> + @ <li><label><input type="checkbox" name="ah"%s(oa['h'])> + @ Hyperlinks%s(B('h'))</label> + @ <li><label><input type="checkbox" name="ab"%s(oa['b'])> + @ Attachments%s(B('b'))</label> + @ <li><label><input type="checkbox" name="aB"%s(oa['B'])> + @ Forum Attachments%s(B('B'))</label> + @ <li><label><input type="checkbox" name="ag"%s(oa['g'])> + @ Clone%s(B('g'))</label> + @ <li><label><input type="checkbox" name="aj"%s(oa['j'])> + @ Read Wiki%s(B('j'))</label> + @ <li><label><input type="checkbox" name="af"%s(oa['f'])> + @ New Wiki%s(B('f'))</label> + @ <li><label><input type="checkbox" name="am"%s(oa['m'])> + @ Append Wiki%s(B('m'))</label> + @ <li><label><input type="checkbox" name="ak"%s(oa['k'])> + @ Write Wiki%s(B('k'))</label> + @ <li><label><input type="checkbox" name="al"%s(oa['l'])> + @ Moderate Wiki%s(B('l'))</label> + @ <li><label><input type="checkbox" name="ar"%s(oa['r'])> + @ Read Ticket%s(B('r'))</label> + @ <li><label><input type="checkbox" name="an"%s(oa['n'])> + @ New Tickets%s(B('n'))</label> + @ <li><label><input type="checkbox" name="ac"%s(oa['c'])> + @ Append To Ticket%s(B('c'))</label> + @ <li><label><input type="checkbox" name="aw"%s(oa['w'])> + @ Write Tickets%s(B('w'))</label> + @ <li><label><input type="checkbox" name="aq"%s(oa['q'])> + @ Moderate Tickets%s(B('q'))</label> + @ <li><label><input type="checkbox" name="at"%s(oa['t'])> + @ Ticket Report%s(B('t'))</label> + @ <li><label><input type="checkbox" name="ax"%s(oa['x'])> + @ Private%s(B('x'))</label> + @ <li><label><input type="checkbox" name="ay"%s(oa['y'])> + @ Write Unversioned%s(B('y'))</label> + @ <li><label><input type="checkbox" name="az"%s(oa['z'])> + @ Download Zip%s(B('z'))</label> + @ <li><label><input type="checkbox" name="a2"%s(oa['2'])> + @ Read Forum%s(B('2'))</label> + @ <li><label><input type="checkbox" name="a3"%s(oa['3'])> + @ Write Forum%s(B('3'))</label> + @ <li><label><input type="checkbox" name="a4"%s(oa['4'])> + @ WriteTrusted Forum%s(B('4'))</label> + @ <li><label><input type="checkbox" name="aB"%s(oa['B'])> + @ Attach to Forum%s(B('B'))</label> + @ <li><label><input type="checkbox" name="a5"%s(oa['5'])> + @ Moderate Forum%s(B('5'))</label> + @ <li><label><input type="checkbox" name="a6"%s(oa['6'])> + @ Supervise Forum%s(B('6'))</label> + @ <li><label><input type="checkbox" name="a7"%s(oa['7'])> + @ Email Alerts%s(B('7'))</label> + @ <li><label><input type="checkbox" name="aA"%s(oa['A'])> + @ Send Announcements%s(B('A'))</label> + @ <li><label><input type="checkbox" name="aC"%s(oa['C'])> + @ Chatroom%s(B('C'))</label> + @ <li><label><input type="checkbox" name="aD"%s(oa['D'])> + @ Enable Debug%s(B('D'))</label> + @ </ul></div> + @ </td> + @ </tr> + @ <tr> + @ <td class="usetupEditLabel">Selected Cap:</td> + @ <td> + @ <span id="usetupEditCapability">(missing JS?)</span> + @ <a href="%R/setup_ucap_list">(key)</a> + @ </td> + @ </tr> + if( !login_is_special(zLogin) ){ + @ <tr> + @ <td align="right" id="supw">Password:</td> + if( zPw[0] ){ + /* Obscure the password for all users */ + @ <td><input aria-labelledby="supw" type="password" autocomplete="off" \ + @ name="pw" value="**********"> + @ (Leave unchanged to retain password)</td> + }else{ + /* Show an empty password as an empty input field */ + char *zRPW = fossil_random_password(12); + @ <td><input aria-labelledby="supw" type="password" name="pw" \ + @ autocomplete="off" value=""> Password suggestion: %z(zRPW)</td> + } + @ </tr> + } + zGroup = login_group_name(); + if( zGroup ){ + @ <tr> + @ <td valign="top" align="right">Scope:</td> + @ <td valign="top"> + @ <input type="radio" name="all" checked value="0"> + @ Apply changes to this repository only.<br> + @ <input type="radio" name="all" value="1"> + @ Apply changes to all repositories in the "<b>%h(zGroup)</b>" + @ login group.</td></tr> + } + if( !higherUser ){ + if( zDeleteVerify ){ + @ <tr> + @ <td valign="top" align="right">Verify:</td> + @ <td><label><input type="checkbox" name="verifydelete">\ + @ Confirm Delete \ + @ <span class="loginError">← %h(zDeleteVerify)</span> + @ </label></td> + @ <tr> + } + @ <tr> + @ <td> </td> + @ <td><input type="submit" name="apply" value="Apply Changes"> + if( !login_is_special(zLogin) ){ + @ <input type="submit" name="delete" value="Delete User"> + } + @ <input type="submit" name="can" value="Cancel"></td> + @ </tr> + } + @ </table> + @ </div></form> + @ </div> + builtin_request_js("useredit.js"); + @ <hr> + @ <h1>Notes On Privileges And Capabilities:</h1> + @ <ul> + if( higherUser ){ + @ <li><p class="missingPriv"> + @ User %h(zLogin) has Setup privileges and you only have Admin privileges + @ so you are not permitted to make changes to %h(zLogin). + @ </p></li> + @ + } + @ <li><p> + @ The <span class="capability">Setup</span> user can make arbitrary + @ configuration changes. An <span class="usertype">Admin</span> user + @ can add other users and change user privileges + @ and reset user passwords. Both automatically get all other privileges + @ listed below. Use these two settings with discretion. + @ </p></li> + @ + @ <li><p> + @ The "<span class="ueditInheritNobody"><sub>N</sub></span>" subscript suffix + @ indicates the privileges of <span class="usertype">nobody</span> that + @ are available to all users regardless of whether or not they are logged in. + @ </p></li> + @ + @ <li><p> + @ The "<span class="ueditInheritAnonymous"><sub>A</sub></span>" + @ subscript suffix + @ indicates the privileges of <span class="usertype">anonymous</span> that + @ are inherited by all logged-in users. + @ </p></li> + @ + @ <li><p> + @ The "<span class="ueditInheritDeveloper"><sub>D</sub></span>" + @ subscript suffix indicates the privileges of + @ <span class="usertype">developer</span> that + @ are inherited by all users with the + @ <span class="capability">Developer</span> privilege. + @ </p></li> + @ + @ <li><p> + @ The "<span class="ueditInheritReader"><sub>R</sub></span>" subscript suffix + @ indicates the privileges of <span class="usertype">reader</span> that + @ are inherited by all users with the <span class="capability">Reader</span> + @ privilege. + @ </p></li> + @ + @ <li><p> + @ The <span class="capability">Delete</span> privilege give the user the + @ ability to erase wiki, tickets, and attachments that have been added + @ by anonymous users. This capability is intended for deletion of spam. + @ The delete capability is only in effect for 24 hours after the item + @ is first posted. The <span class="usertype">Setup</span> user can + @ delete anything at any time. + @ </p></li> + @ + @ <li><p> + @ The <span class="capability">Hyperlinks</span> privilege allows a user + @ to see most hyperlinks. This is recommended ON for most logged-in users + @ but OFF for user "nobody" to avoid problems with spiders trying to walk + @ every diff and annotation of every historical check-in and file. + @ </p></li> + @ + @ <li><p> + @ The <span class="capability">Zip</span> privilege allows a user to + @ see the "download as ZIP" + @ hyperlink and permits access to the <tt>/zip</tt> page. This allows + @ users to download ZIP archives without granting other rights like + @ <span class="capability">Read</span> or + @ <span class="capability">Hyperlink</span>. The "z" privilege is recommended + @ for user <span class="usertype">nobody</span> so that automatic package + @ downloaders can obtain the sources without going through the login + @ procedure. + @ </p></li> + @ + @ <li><p> + @ The <span class="capability">Check-in</span> privilege allows remote + @ users to "push". The <span class="capability">Check-out</span> privilege + @ allows remote users to "pull". The <span class="capability">Clone</span> + @ privilege allows remote users to "clone". + @ </p></li> + @ + @ <li><p> + @ The <span class="capability">Read Wiki</span>, + @ <span class="capability">New Wiki</span>, + @ <span class="capability">Append Wiki</span>, and + @ <b>Write Wiki</b> privileges control access to wiki pages. The + @ <span class="capability">Read Ticket</span>, + @ <span class="capability">New Ticket</span>, + @ <span class="capability">Append Ticket</span>, and + @ <span class="capability">Write Ticket</span> privileges control access + @ to trouble tickets. + @ The <span class="capability">Ticket Report</span> privilege allows + @ the user to create or edit ticket report formats. + @ </p></li> + @ + @ <li><p> + @ Users with the <span class="capability">Password</span> privilege + @ are allowed to change their own password. Recommended ON for most + @ users but OFF for special users <span class="usertype">developer</span>, + @ <span class="usertype">anonymous</span>, + @ and <span class="usertype">nobody</span>. + @ </p></li> + @ + @ <li><p> + @ The <span class="capability">View-PII</span> privilege allows the display + @ of personally-identifiable information information such as the + @ email address of users and contact + @ information on tickets. Recommended OFF for + @ <span class="usertype">anonymous</span> and for + @ <span class="usertype">nobody</span> but ON for + @ <span class="usertype">developer</span>. + @ </p></li> + @ + @ <li><p> + @ The <span class="capability">Attachment</span> privilege is needed in + @ order to add attachments to tickets or wiki. Write privilege on the + @ ticket or wiki is also required. + @ </p></li> + @ + @ <li><p> + @ Login is prohibited if the password is an empty string. + @ </p></li> + @ </ul> + @ + @ <h2>Special Logins</h2> + @ + @ <ul> + @ <li><p> + @ No login is required for user <span class="usertype">nobody</span>. The + @ capabilities of the <span class="usertype">nobody</span> user are + @ inherited by all users, regardless of whether or not they are logged in. + @ To disable universal access to the repository, make sure that the + @ <span class="usertype">nobody</span> user has no capabilities + @ enabled. The password for <span class="usertype">nobody</span> is ignored. + @ </p></li> + @ + @ <li><p> + @ Login is required for user <span class="usertype">anonymous</span> but the + @ password is displayed on the login screen beside the password entry box + @ so anybody who can read should be able to login as anonymous. + @ On the other hand, spiders and web-crawlers will typically not + @ be able to login. Set the capabilities of the + @ <span class="usertype">anonymous</span> + @ user to things that you want any human to be able to do, but not any + @ spider. Every other logged-in user inherits the privileges of + @ <span class="usertype">anonymous</span>. + @ </p></li> + @ + @ <li><p> + @ The <span class="usertype">developer</span> user is intended as a template + @ for trusted users with check-in privileges. When adding new trusted users, + @ simply select the <span class="capability">developer</span> privilege to + @ cause the new user to inherit all privileges of the + @ <span class="usertype">developer</span> + @ user. Similarly, the <span class="usertype">reader</span> user is a + @ template for users who are allowed more access than + @ <span class="usertype">anonymous</span>, + @ but less than a <span class="usertype">developer</span>. + @ </p></li> + @ </ul> + style_finish_page(); +} + +/* +** WEBPAGE: setup_uinfo +** +** Detailed information about a user account, available to administrators +** only. +** +** u=UID +** l=LOGIN +*/ +void setup_uinfo_page(void){ + Stmt q; + Blob sql; + const char *zLogin; + int uid; + + /* Must have ADMIN privileges to access this page + */ + login_check_credentials(); + if( !g.perm.Admin ){ login_needed(0); return; } + style_set_current_feature("setup"); + zLogin = P("l"); + uid = atoi(PD("u","0")); + if( zLogin==0 && uid==0 ){ + uid = db_int(1,"SELECT uid FROM user"); + } + blob_init(&sql, 0, 0); + blob_append_sql(&sql, + "SELECT " + /* 0 */ "uid," + /* 1 */ "login," + /* 2 */ "cap," + /* 3 */ "cookie," + /* 4 */ "datetime(cexpire)," + /* 5 */ "info," + /* 6 */ "datetime(user.mtime,'unixepoch')," + ); + if( db_table_exists("repository","subscriber") ){ + blob_append_sql(&sql, + /* 7 */ "subscriberId," + /* 8 */ "semail," + /* 9 */ "sverified," + /* 10 */ "date(lastContact+2440587.5)" + " FROM user LEFT JOIN subscriber ON suname=login" + ); + }else{ + blob_append_sql(&sql, + /* 7 */ "NULL," + /* 8 */ "NULL," + /* 9 */ "NULL," + /* 10 */ "NULL" + " FROM user" + ); + } + if( zLogin!=0 ){ + blob_append_sql(&sql, " WHERE login=%Q", zLogin); + }else{ + blob_append_sql(&sql, " WHERE uid=%d", uid); + } + db_prepare(&q, "%s", blob_sql_text(&sql)); + blob_zero(&sql); + if( db_step(&q)!=SQLITE_ROW ){ + style_header("No Such User"); + if( zLogin ){ + @ <p>Cannot find any information on user %h(zLogin). + }else{ + @ <p>Cannot find any information on userid %d(uid). + } + style_finish_page(); + db_finalize(&q); + return; + } + style_header("User %h", db_column_text(&q,1)); + @ <table class="label-value"> + @ <tr><th>uid:</th><td>%d(db_column_int(&q,0)) + @ (<a href="%R/setup_uedit?id=%d(db_column_int(&q,0))">edit</a>)</td></tr> + @ <tr><th>login:</th><td>%h(db_column_text(&q,1))</td></tr> + @ <tr><th>capabilities:</th><td>%h(db_column_text(&q,2))</td></tr> + @ <tr><th valign="top">info:</th> + @ <td valign="top"><span style='white-space:pre-line;'>\ + @ %h(db_column_text(&q,5))</span></td></tr> + @ <tr><th>user.mtime:</th><td>%h(db_column_text(&q,6))</td></tr> + if( db_column_type(&q,7)!=SQLITE_NULL ){ + @ <tr><th>subscriberId:</th><td>%d(db_column_int(&q,7)) + @ (<a href="%R/alerts?sid=%d(db_column_int(&q,7))">edit</a>)</td></tr> + @ <tr><th>semail:</th><td>%h(db_column_text(&q,8))</td></tr> + @ <tr><th>verified:</th><td>%s(db_column_int(&q,9)?"yes":"no")</td></th> + @ <tr><th>lastContact:</th><td>%h(db_column_text(&q,10))</td></tr> + } + @ </table> + db_finalize(&q); + style_finish_page(); +} |
