Fossil SCM

fossil-scm / src / login.c
Source Blame History 2859 lines
dbda8d6… drh 1 /*
c19f34c… drh 2 ** Copyright (c) 2007 D. Richard Hipp
dbda8d6… drh 3 **
dbda8d6… drh 4 ** This program is free software; you can redistribute it and/or
c06edd2… drh 5 ** modify it under the terms of the Simplified BSD License (also
c06edd2… drh 6 ** known as the "2-Clause License" or "FreeBSD License".)
c06edd2… drh 7
dbda8d6… drh 8 ** This program is distributed in the hope that it will be useful,
c06edd2… drh 9 ** but without any warranty; without even the implied warranty of
c06edd2… drh 10 ** merchantability or fitness for a particular purpose.
dbda8d6… drh 11 **
dbda8d6… drh 12 ** Author contact information:
dbda8d6… drh 13 ** [email protected]
dbda8d6… drh 14 ** http://www.hwaci.com/drh/
dbda8d6… drh 15 **
dbda8d6… drh 16 *******************************************************************************
dbda8d6… drh 17 **
dbda8d6… drh 18 ** This file contains code for generating the login and logout screens.
9c952d2… drh 19 **
9c952d2… drh 20 ** Notes:
9c952d2… drh 21 **
a257fde… drh 22 ** There are four special-case user-ids: "anonymous", "nobody",
a257fde… drh 23 ** "developer" and "reader".
a257fde… drh 24 **
9c952d2… drh 25 ** The capabilities of the nobody user are available to anyone,
9c952d2… drh 26 ** regardless of whether or not they are logged in. The capabilities
9c952d2… drh 27 ** of anonymous are only available after logging in, but the login
9c952d2… drh 28 ** screen displays the password for the anonymous login, so this
a257fde… drh 29 ** should not prevent a human user from doing so. The capabilities
a257fde… drh 30 ** of developer and reader are inherited by any user that has the
a257fde… drh 31 ** "v" and "u" capabilities, respectively.
9c952d2… drh 32 **
9c952d2… drh 33 ** The nobody user has capabilities that you want spiders to have.
9c952d2… drh 34 ** The anonymous user has capabilities that you want people without
9c952d2… drh 35 ** logins to have.
9c952d2… drh 36 **
9c952d2… drh 37 ** Of course, a sophisticated spider could easily circumvent the
9c952d2… drh 38 ** anonymous login requirement and walk the website. But that is
9c952d2… drh 39 ** not really the point. The anonymous login keeps search-engine
9c952d2… drh 40 ** crawlers and site download tools like wget from walking change
9c952d2… drh 41 ** logs and downloading diffs of very version of the archive that
9c952d2… drh 42 ** has ever existed, and things like that.
dbda8d6… drh 43 */
dbda8d6… drh 44 #include "config.h"
dbda8d6… drh 45 #include "login.h"
45f3516… jan.nijtmans 46 #if defined(_WIN32)
d0305b3… aku 47 # include <windows.h> /* for Sleep */
d2ba02e… ron 48 # if defined(__MINGW32__) || defined(_MSC_VER)
3564af0… drh 49 # define sleep Sleep /* windows does not have sleep, but Sleep */
3564af0… drh 50 # endif
d0305b3… aku 51 #endif
dbda8d6… drh 52 #include <time.h>
45f3516… jan.nijtmans 53
920ace1… drh 54 /*
920ace1… drh 55 ** Compute an appropriate Anti-CSRF token into g.zCsrfToken[].
920ace1… drh 56 */
920ace1… drh 57 static void login_create_csrf_secret(const char *zSeed){
920ace1… drh 58 unsigned char zResult[20];
ab05475… danield 59 unsigned int i;
920ace1… drh 60
920ace1… drh 61 sha1sum_binary(zSeed, zResult);
920ace1… drh 62 for(i=0; i<sizeof(g.zCsrfToken)-1; i++){
920ace1… drh 63 g.zCsrfToken[i] = "abcdefghijklmnopqrstuvwxyz"
920ace1… drh 64 "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
920ace1… drh 65 "0123456789-/"[zResult[i]%64];
920ace1… drh 66 }
920ace1… drh 67 g.zCsrfToken[i] = 0;
920ace1… drh 68 }
93d52a0… rberteig 69
93d52a0… rberteig 70 /*
a257fde… drh 71 ** Return the login-group name. Or return 0 if this repository is
a257fde… drh 72 ** not a member of a login-group.
a257fde… drh 73 */
a257fde… drh 74 const char *login_group_name(void){
a257fde… drh 75 static const char *zGroup = 0;
a257fde… drh 76 static int once = 1;
a257fde… drh 77 if( once ){
a257fde… drh 78 zGroup = db_get("login-group-name", 0);
a257fde… drh 79 once = 0;
a257fde… drh 80 }
a257fde… drh 81 return zGroup;
a257fde… drh 82 }
a257fde… drh 83
a257fde… drh 84 /*
a257fde… drh 85 ** Return a path appropriate for setting a cookie.
a257fde… drh 86 **
a257fde… drh 87 ** The path is g.zTop for single-repo cookies. It is "/" for
a257fde… drh 88 ** cookies of a login-group.
a257fde… drh 89 */
f8a2aa0… drh 90 const char *login_cookie_path(void){
a257fde… drh 91 if( login_group_name()==0 ){
a257fde… drh 92 return g.zTop;
a257fde… drh 93 }else{
a257fde… drh 94 return "/";
a257fde… drh 95 }
a257fde… drh 96 }
a257fde… drh 97
a257fde… drh 98 /*
a257fde… drh 99 ** Return the name of the login cookie.
a257fde… drh 100 **
a257fde… drh 101 ** The login cookie name is always of the form: fossil-XXXXXXXXXXXXXXXX
a257fde… drh 102 ** where the Xs are the first 16 characters of the login-group-code or
a257fde… drh 103 ** of the project-code if we are not a member of any login-group.
d0305b3… aku 104 */
796dcfe… drh 105 char *login_cookie_name(void){
d0305b3… aku 106 static char *zCookieName = 0;
d0305b3… aku 107 if( zCookieName==0 ){
a257fde… drh 108 zCookieName = db_text(0,
a257fde… drh 109 "SELECT 'fossil-' || substr(value,1,16)"
a257fde… drh 110 " FROM config"
a257fde… drh 111 " WHERE name IN ('project-code','login-group-code')"
fff43eb… drh 112 " ORDER BY name /*sort*/"
a257fde… drh 113 );
d0305b3… aku 114 }
c34003b… jan.nijtmans 115 return zCookieName;
d0305b3… aku 116 }
d0305b3… aku 117
d0305b3… aku 118 /*
0600b27… drh 119 ** Redirect to the page specified by the "g" query parameter.
0600b27… drh 120 ** Or if there is no "g" query parameter, redirect to the homepage.
0600b27… drh 121 */
1958448… drh 122 NORETURN void login_redirect_to_g(void){
0600b27… drh 123 const char *zGoto = P("g");
0600b27… drh 124 if( zGoto ){
3571c87… drh 125 cgi_redirectf("%R/%s",zGoto);
aa4159f… drh 126 }else if( (zGoto = P("fossil-goto"))!=0 && zGoto[0]!=0 ){
aa4159f… drh 127 cgi_set_cookie("fossil-goto","",0,1);
aa4159f… drh 128 cgi_redirect(zGoto);
0600b27… drh 129 }else{
0600b27… drh 130 fossil_redirect_home();
0600b27… drh 131 }
0600b27… drh 132 }
0600b27… drh 133
0600b27… drh 134 /*
a257fde… drh 135 ** Return an abbreviated project code. The abbreviation is the first
f7ce03e… drh 136 ** 16 characters of the project code.
a257fde… drh 137 **
a257fde… drh 138 ** Memory is obtained from malloc.
86cbb69… drh 139 */
a257fde… drh 140 static char *abbreviated_project_code(const char *zFullCode){
f7ce03e… drh 141 return mprintf("%.16s", zFullCode);
a257fde… drh 142 }
a257fde… drh 143
86cbb69… drh 144
86cbb69… drh 145 /*
b4a29fa… drh 146 ** Check to see if the anonymous login is valid. If it is valid, return
b4a29fa… drh 147 ** the userid of the anonymous user.
796dcfe… drh 148 **
796dcfe… drh 149 ** The zCS parameter is the "captcha seed" used for a specific
796dcfe… drh 150 ** anonymous login request.
b4a29fa… drh 151 */
796dcfe… drh 152 int login_is_valid_anonymous(
b4a29fa… drh 153 const char *zUsername, /* The username. Must be "anonymous" */
796dcfe… drh 154 const char *zPassword, /* The supplied password */
796dcfe… drh 155 const char *zCS /* The captcha seed value */
b4a29fa… drh 156 ){
b4a29fa… drh 157 const char *zPw; /* The correct password shown in the captcha */
b4a29fa… drh 158 int uid; /* The user ID of anonymous */
8659d84… drh 159 int n = 0; /* Counter of captcha-secrets */
b4a29fa… drh 160
b4a29fa… drh 161 if( zUsername==0 ) return 0;
796dcfe… drh 162 else if( zPassword==0 ) return 0;
796dcfe… drh 163 else if( zCS==0 ) return 0;
796dcfe… drh 164 else if( fossil_strcmp(zUsername,"anonymous")!=0 ) return 0;
7d2b47a… drh 165 else if( anon_cookie_lifespan()==0 ) return 0;
8659d84… drh 166 while( 1/*exit-by-break*/ ){
8659d84… drh 167 zPw = captcha_decode((unsigned int)atoi(zCS), n);
8659d84… drh 168 if( zPw==0 ) return 0;
8659d84… drh 169 if( fossil_stricmp(zPw, zPassword)==0 ) break;
8659d84… drh 170 n++;
8659d84… drh 171 }
b4a29fa… drh 172 uid = db_int(0, "SELECT uid FROM user WHERE login='anonymous'"
604e1a6… drh 173 " AND octet_length(pw)>0 AND octet_length(cap)>0");
6fdf529… drh 174 return uid;
c7de5f7… drh 175 }
c7de5f7… drh 176
c7de5f7… drh 177 /*
c7de5f7… drh 178 ** Make sure the accesslog table exists. Create it if it does not
c7de5f7… drh 179 */
c7de5f7… drh 180 void create_accesslog_table(void){
03e21b9… drh 181 if( !db_table_exists("repository","accesslog") ){
03e21b9… drh 182 db_unprotect(PROTECT_READONLY);
03e21b9… drh 183 db_multi_exec(
03e21b9… drh 184 "CREATE TABLE IF NOT EXISTS repository.accesslog("
03e21b9… drh 185 " uname TEXT,"
03e21b9… drh 186 " ipaddr TEXT,"
03e21b9… drh 187 " success BOOLEAN,"
03e21b9… drh 188 " mtime TIMESTAMP"
03e21b9… drh 189 ");"
03e21b9… drh 190 );
03e21b9… drh 191 db_protect_pop();
03e21b9… drh 192 }
6fdf529… drh 193 }
6fdf529… drh 194
6fdf529… drh 195 /*
6fdf529… drh 196 ** Make a record of a login attempt, if login record keeping is enabled.
6fdf529… drh 197 */
6fdf529… drh 198 static void record_login_attempt(
6fdf529… drh 199 const char *zUsername, /* Name of user logging in */
6fdf529… drh 200 const char *zIpAddr, /* IP address from which they logged in */
6fdf529… drh 201 int bSuccess /* True if the attempt was a success */
6fdf529… drh 202 ){
e31c2c0… drh 203 db_unprotect(PROTECT_READONLY);
fc79c57… drh 204 if( db_get_boolean("access-log", 1) ){
d7e10ce… drh 205 create_accesslog_table();
d7e10ce… drh 206 db_multi_exec(
d7e10ce… drh 207 "INSERT INTO accesslog(uname,ipaddr,success,mtime)"
d7e10ce… drh 208 "VALUES(%Q,%Q,%d,julianday('now'));",
d7e10ce… drh 209 zUsername, zIpAddr, bSuccess
d7e10ce… drh 210 );
d7e10ce… drh 211 }
d7e10ce… drh 212 if( bSuccess ){
d7e10ce… drh 213 alert_user_contact(zUsername);
d7e10ce… drh 214 }
e31c2c0… drh 215 db_protect_pop();
796dcfe… drh 216 }
796dcfe… drh 217
796dcfe… drh 218 /*
796dcfe… drh 219 ** Searches for the user ID matching the given name and password.
796dcfe… drh 220 ** On success it returns a positive value. On error it returns 0.
796dcfe… drh 221 ** On serious (DB-level) error it will probably exit.
796dcfe… drh 222 **
9b4e157… drh 223 ** zUsername uses double indirection because we may re-point *zUsername
9b4e157… drh 224 ** at a C string allocated with fossil_strdup() if you pass an email
9b4e157… drh 225 ** address instead and we find that address in the user table's info
9b4e157… drh 226 ** field, which is expected to contain a string of the form "Human Name
9b4e157… drh 227 ** <[email protected]>". In that case, *zUsername will point to that
9b4e157… drh 228 ** user's actual login name on return, causing a leak unless the caller
9b4e157… drh 229 ** is diligent enough to check whether its pointer was re-pointed.
9b4e157… drh 230 **
796dcfe… drh 231 ** zPassword may be either the plain-text form or the encrypted
796dcfe… drh 232 ** form of the user's password.
796dcfe… drh 233 */
9b4e157… drh 234 int login_search_uid(const char **pzUsername, const char *zPasswd){
9b4e157… drh 235 char *zSha1Pw = sha1_shared_secret(zPasswd, *pzUsername, 0);
8c91be8… drh 236 int uid = db_int(0,
8c91be8… drh 237 "SELECT uid FROM user"
8c91be8… drh 238 " WHERE login=%Q"
604e1a6… drh 239 " AND octet_length(cap)>0 AND octet_length(pw)>0"
8c91be8… drh 240 " AND login NOT IN ('anonymous','nobody','developer','reader')"
8c91be8… drh 241 " AND (pw=%Q OR (length(pw)<>40 AND pw=%Q))"
8c91be8… drh 242 " AND (info NOT LIKE '%%expires 20%%'"
8c91be8… drh 243 " OR substr(info,instr(lower(info),'expires')+8,10)>datetime('now'))",
9b4e157… drh 244 *pzUsername, zSha1Pw, zPasswd
8c91be8… drh 245 );
8c91be8… drh 246
8c91be8… drh 247 /* If we did not find a login on the first attempt, and the username
9b4e157… drh 248 ** looks like an email address, then perhaps the user entered their
8c91be8… drh 249 ** email address instead of their login. Try again to match the user
8c91be8… drh 250 ** against email addresses contained in the "info" field.
8c91be8… drh 251 */
9b4e157… drh 252 if( uid==0 && strchr(*pzUsername,'@')!=0 ){
8c91be8… drh 253 Stmt q;
8c91be8… drh 254 db_prepare(&q,
8c91be8… drh 255 "SELECT login FROM user"
8c91be8… drh 256 " WHERE find_emailaddr(info)=%Q"
8c91be8… drh 257 " AND instr(login,'@')==0",
9b4e157… drh 258 *pzUsername
8c91be8… drh 259 );
9b4e157… drh 260 while( db_step(&q)==SQLITE_ROW ){
9b4e157… drh 261 const char *zLogin = db_column_text(&q,0);
9b4e157… drh 262 if( (uid = login_search_uid(&zLogin, zPasswd) ) != 0 ){
9b4e157… drh 263 *pzUsername = fossil_strdup(zLogin);
9b4e157… drh 264 break;
9b4e157… drh 265 }
8c91be8… drh 266 }
8c91be8… drh 267 db_finalize(&q);
275da70… danield 268 }
796dcfe… drh 269 free(zSha1Pw);
796dcfe… drh 270 return uid;
796dcfe… drh 271 }
796dcfe… drh 272
796dcfe… drh 273 /*
796dcfe… drh 274 ** Generates a login cookie value for a non-anonymous user.
796dcfe… drh 275 **
796dcfe… drh 276 ** The zHash parameter must be a random value which must be
796dcfe… drh 277 ** subsequently stored in user.cookie for later validation.
796dcfe… drh 278 **
796dcfe… drh 279 ** The returned memory should be free()d after use.
796dcfe… drh 280 */
4e18dba… jan.nijtmans 281 char *login_gen_user_cookie_value(const char *zUsername, const char *zHash){
f7ce03e… drh 282 char *zProjCode = db_get("project-code",NULL);
f7ce03e… drh 283 char *zCode = abbreviated_project_code(zProjCode);
f7ce03e… drh 284 free(zProjCode);
796dcfe… drh 285 assert((zUsername && *zUsername) && "Invalid user data.");
f7ce03e… drh 286 return mprintf("%s/%z/%s", zHash, zCode, zUsername);
796dcfe… drh 287 }
796dcfe… drh 288
796dcfe… drh 289 /*
796dcfe… drh 290 ** Generates a login cookie for NON-ANONYMOUS users. Note that this
796dcfe… drh 291 ** function "could" figure out the uid by itself but it currently
796dcfe… drh 292 ** doesn't because the code which calls this already has the uid.
796dcfe… drh 293 **
796dcfe… drh 294 ** This function also updates the user.cookie, user.ipaddr,
796dcfe… drh 295 ** and user.cexpire fields for the given user.
796dcfe… drh 296 **
796dcfe… drh 297 ** If zDest is not NULL then the generated cookie is copied to
e2bdc10… danield 298 ** *zDdest and ownership is transferred to the caller (who should
796dcfe… drh 299 ** eventually pass it to free()).
6b7b323… drh 300 **
6b7b323… drh 301 ** If bSessionCookie is true, the cookie will be a session cookie,
6b7b323… drh 302 ** else a persistent cookie. If it's a session cookie, the
6b7b323… drh 303 ** [user].[cexpire] and [user].[cookie] entries will be modified as if
6b7b323… drh 304 ** it were a persistent cookie because doing so is necessary for
6b7b323… drh 305 ** fossil's own "is this cookie still valid?" checks to work.
796dcfe… drh 306 */
796dcfe… drh 307 void login_set_user_cookie(
4e18dba… jan.nijtmans 308 const char *zUsername, /* User's name */
796dcfe… drh 309 int uid, /* User's ID */
6b7b323… drh 310 char **zDest, /* Optional: store generated cookie value. */
6b7b323… drh 311 int bSessionCookie /* True for session-only cookie */
796dcfe… drh 312 ){
796dcfe… drh 313 const char *zCookieName = login_cookie_name();
796dcfe… drh 314 const char *zExpire = db_get("cookie-expire","8766");
6b7b323… drh 315 const int expires = atoi(zExpire)*3600;
6b7b323… drh 316 char *zHash = 0;
796dcfe… drh 317 char *zCookie;
4e18dba… jan.nijtmans 318 const char *zIpAddr = PD("REMOTE_ADDR","nil"); /* IP address of user */
73038ba… drh 319
796dcfe… drh 320 assert((zUsername && *zUsername) && (uid > 0) && "Invalid user data.");
73038ba… drh 321 zHash = db_text(0,
73038ba… drh 322 "SELECT cookie FROM user"
73038ba… drh 323 " WHERE uid=%d"
73038ba… drh 324 " AND cexpire>julianday('now')"
73038ba… drh 325 " AND length(cookie)>30",
7d18c40… drh 326 uid);
73038ba… drh 327 if( zHash==0 ) zHash = db_text(0, "SELECT hex(randomblob(25))");
796dcfe… drh 328 zCookie = login_gen_user_cookie_value(zUsername, zHash);
6b7b323… drh 329 cgi_set_cookie(zCookieName, zCookie, login_cookie_path(),
6b7b323… drh 330 bSessionCookie ? 0 : expires);
796dcfe… drh 331 record_login_attempt(zUsername, zIpAddr, 1);
f741baa… drh 332 db_unprotect(PROTECT_USER);
6b7b323… drh 333 db_multi_exec("UPDATE user SET cookie=%Q,"
796dcfe… drh 334 " cexpire=julianday('now')+%d/86400.0 WHERE uid=%d",
6b7b323… drh 335 zHash, expires, uid);
f741baa… drh 336 db_protect_pop();
6b7b323… drh 337 fossil_free(zHash);
796dcfe… drh 338 if( zDest ){
796dcfe… drh 339 *zDest = zCookie;
796dcfe… drh 340 }else{
796dcfe… drh 341 free(zCookie);
796dcfe… drh 342 }
796dcfe… drh 343 }
796dcfe… drh 344
68da478… drh 345 /*
7d2b47a… drh 346 ** SETTING: anon-cookie-lifespan width=10 default=480
7d2b47a… drh 347 ** The number of minutes for which an anonymous login cookie is
7d2b47a… drh 348 ** valid. Anonymous logins are prohibited if this value is zero.
7d2b47a… drh 349 */
7d2b47a… drh 350
7d2b47a… drh 351
7d2b47a… drh 352 /*
7d2b47a… drh 353 ** The default lifetime of an anoymous cookie, in minutes.
7d2b47a… drh 354 */
7d2b47a… drh 355 #define ANONYMOUS_COOKIE_LIFESPAN (8*60)
7d2b47a… drh 356
7d2b47a… drh 357 /*
7d2b47a… drh 358 ** Return the lifetime of an anonymous cookie, in minutes.
68da478… drh 359 */
7d2b47a… drh 360 int anon_cookie_lifespan(void){
7d2b47a… drh 361 static int lifespan = -1;
7d2b47a… drh 362 if( lifespan<0 ){
7d2b47a… drh 363 lifespan = db_get_int("anon-cookie-lifespan", ANONYMOUS_COOKIE_LIFESPAN);
7d2b47a… drh 364 if( lifespan<0 ) lifespan = 0;
7d2b47a… drh 365 }
7d2b47a… drh 366 return lifespan;
7d2b47a… drh 367 }
68da478… drh 368
796dcfe… drh 369 /* Sets a cookie for an anonymous user login, which looks like this:
796dcfe… drh 370 **
68da478… drh 371 ** HASH/TIME/anonymous
796dcfe… drh 372 **
0693766… drh 373 ** Where HASH is the sha1sum of TIME/USERAGENT/SECRET, in which SECRET
0693766… drh 374 ** is captcha-secret and USERAGENT is the HTTP_USER_AGENT value.
796dcfe… drh 375 **
796dcfe… drh 376 ** If zCookieDest is not NULL then the generated cookie is assigned to
796dcfe… drh 377 ** *zCookieDest and the caller must eventually free() it.
6b7b323… drh 378 **
6b7b323… drh 379 ** If bSessionCookie is true, the cookie will be a session cookie.
68da478… drh 380 **
68da478… drh 381 ** Search for tag-20250817a to find the code that recognizes this cookie.
796dcfe… drh 382 */
1958448… drh 383 void login_set_anon_cookie(char **zCookieDest, int bSessionCookie){
e2bdc10… danield 384 char *zNow; /* Current time (Julian day number) */
796dcfe… drh 385 char *zCookie; /* The login cookie */
0693766… drh 386 const char *zUserAgent; /* The user agent */
4e18dba… jan.nijtmans 387 const char *zCookieName; /* Name of the login cookie */
796dcfe… drh 388 Blob b; /* Blob used during cookie construction */
7d2b47a… drh 389 int expires = bSessionCookie ? 0 : anon_cookie_lifespan();
796dcfe… drh 390 zCookieName = login_cookie_name();
796dcfe… drh 391 zNow = db_text("0", "SELECT julianday('now')");
7d18c40… drh 392 assert( zCookieName && zNow );
796dcfe… drh 393 blob_init(&b, zNow, -1);
0693766… drh 394 zUserAgent = PD("HTTP_USER_AGENT","nil");
0693766… drh 395 blob_appendf(&b, "/%s/%z", zUserAgent, captcha_secret(0));
796dcfe… drh 396 sha1sum_blob(&b, &b);
68da478… drh 397 zCookie = mprintf("%s/%s/anonymous", blob_buffer(&b), zNow);
796dcfe… drh 398 blob_reset(&b);
6b7b323… drh 399 cgi_set_cookie(zCookieName, zCookie, login_cookie_path(), expires);
796dcfe… drh 400 if( zCookieDest ){
796dcfe… drh 401 *zCookieDest = zCookie;
796dcfe… drh 402 }else{
796dcfe… drh 403 free(zCookie);
796dcfe… drh 404 }
8659d84… drh 405 fossil_free(zNow);
796dcfe… drh 406 }
796dcfe… drh 407
796dcfe… drh 408 /*
796dcfe… drh 409 ** "Unsets" the login cookie (insofar as cookies can be unset) and
796dcfe… drh 410 ** clears the current user's (g.userUid) login information from the
796dcfe… drh 411 ** user table. Sets: user.cookie, user.ipaddr, user.cexpire.
796dcfe… drh 412 **
796dcfe… drh 413 ** We could/should arguably clear out g.userUid and g.perm here, but
796dcfe… drh 414 ** we don't currently do not.
796dcfe… drh 415 **
796dcfe… drh 416 ** This is a no-op if g.userUid is 0.
796dcfe… drh 417 */
796dcfe… drh 418 void login_clear_login_data(){
796dcfe… drh 419 if(!g.userUid){
796dcfe… drh 420 return;
796dcfe… drh 421 }else{
4e18dba… jan.nijtmans 422 const char *cookie = login_cookie_name();
796dcfe… drh 423 /* To logout, change the cookie value to an empty string */
796dcfe… drh 424 cgi_set_cookie(cookie, "",
796dcfe… drh 425 login_cookie_path(), -86400);
f741baa… drh 426 db_unprotect(PROTECT_USER);
796dcfe… drh 427 db_multi_exec("UPDATE user SET cookie=NULL, ipaddr=NULL, "
796dcfe… drh 428 " cexpire=0 WHERE uid=%d"
796dcfe… drh 429 " AND login NOT IN ('anonymous','nobody',"
796dcfe… drh 430 " 'developer','reader')", g.userUid);
f741baa… drh 431 db_protect_pop();
653dd40… drh 432 cgi_replace_parameter(cookie, NULL);
653dd40… drh 433 cgi_replace_parameter("anon", NULL);
653dd40… drh 434 }
e059e5a… drh 435 }
e059e5a… drh 436
e059e5a… drh 437 /*
e059e5a… drh 438 ** Look at the HTTP_USER_AGENT parameter and try to determine if the user agent
8328448… drh 439 ** is a manually operated browser or a bot. When in doubt, assume a bot.
8328448… drh 440 ** Return true if we believe the agent is a real person.
06e0cb7… drh 441 */
06e0cb7… drh 442 static int isHuman(const char *zAgent){
1167d7b… drh 443 if( zAgent==0 ) return 0; /* If no UserAgent, then probably a bot */
61a8b0e… drh 444 if( strstr(zAgent, "bot")!=0 ) return 0;
61a8b0e… drh 445 if( strstr(zAgent, "spider")!=0 ) return 0;
61a8b0e… drh 446 if( strstr(zAgent, "crawl")!=0 ) return 0;
61a8b0e… drh 447 /* If a URI appears in the User-Agent, it is probably a bot */
61a8b0e… drh 448 if( strstr(zAgent, "http")!=0 ) return 0;
e065d5b… drh 449 if( strncmp(zAgent, "Mozilla/", 8)==0 ){
8328448… drh 450 if( atoi(&zAgent[8])<4 ) return 0; /* Many bots advertise as Mozilla/3 */
61a8b0e… drh 451
61a8b0e… drh 452 /* Google AI Robot, maybe? */
61a8b0e… drh 453 if( strstr(zAgent, "GoogleOther)")!=0 ) return 0;
1e2d76e… drh 454
1e2d76e… drh 455 /* 2016-05-30: A pernicious spider that likes to walk Fossil timelines has
1e2d76e… drh 456 ** been detected on the SQLite website. The spider changes its user-agent
1e2d76e… drh 457 ** string frequently, but it always seems to include the following text:
1e2d76e… drh 458 */
61a8b0e… drh 459 if( strstr(zAgent, "Safari/537.36Mozilla/5.0")!=0 ) return 0;
1e2d76e… drh 460
2271ea4… jan.nijtmans 461 if( sqlite3_strglob("*Firefox/[1-9]*", zAgent)==0 ) return 1;
2271ea4… jan.nijtmans 462 if( sqlite3_strglob("*Chrome/[1-9]*", zAgent)==0 ) return 1;
2271ea4… jan.nijtmans 463 if( sqlite3_strglob("*(compatible;?MSIE?[1789]*", zAgent)==0 ) return 1;
cd11f92… drh 464 if( sqlite3_strglob("*Trident/[1-9]*;?rv:[1-9]*", zAgent)==0 ){
cd11f92… drh 465 return 1; /* IE11+ */
cd11f92… drh 466 }
2271ea4… jan.nijtmans 467 if( sqlite3_strglob("*AppleWebKit/[1-9]*(KHTML*", zAgent)==0 ) return 1;
6d0be55… mistachkin 468 if( sqlite3_strglob("*PaleMoon/[1-9]*", zAgent)==0 ) return 1;
4fdb63d… drh 469 return 0;
4fdb63d… drh 470 }
e065d5b… drh 471 if( strncmp(zAgent, "Opera/", 6)==0 ) return 1;
e065d5b… drh 472 if( strncmp(zAgent, "Safari/", 7)==0 ) return 1;
e065d5b… drh 473 if( strncmp(zAgent, "Lynx/", 5)==0 ) return 1;
e065d5b… drh 474 if( strncmp(zAgent, "NetSurf/", 8)==0 ) return 1;
29bab27… drh 475 return 0;
29bab27… drh 476 }
29bab27… drh 477
29bab27… drh 478 /*
29bab27… drh 479 ** Make a guess at whether or not the requestor is a mobile device or
29bab27… drh 480 ** a desktop device (narrow screen vs. wide screen) based the HTTP_USER_AGENT
29bab27… drh 481 ** parameter. Return true for mobile and false for desktop.
29bab27… drh 482 **
29bab27… drh 483 ** Caution: This is only a guess.
4de677d… drh 484 **
4de677d… drh 485 ** Algorithm derived from https://developer.mozilla.org/en-US/docs/Web/
4de677d… drh 486 ** HTTP/Browser_detection_using_the_user_agent#mobile_device_detection on
4de677d… drh 487 ** 2021-03-01
29bab27… drh 488 */
29bab27… drh 489 int user_agent_is_likely_mobile(void){
29bab27… drh 490 const char *zAgent = P("HTTP_USER_AGENT");
29bab27… drh 491 if( zAgent==0 ) return 0;
4de677d… drh 492 if( strstr(zAgent,"Mobi")!=0 ) return 1;
e059e5a… drh 493 return 0;
06e0cb7… drh 494 }
06e0cb7… drh 495
06e0cb7… drh 496 /*
06e0cb7… drh 497 ** COMMAND: test-ishuman
06e0cb7… drh 498 **
06e0cb7… drh 499 ** Read lines of text from standard input. Interpret each line of text
06e0cb7… drh 500 ** as a User-Agent string from an HTTP header. Label each line as HUMAN
06e0cb7… drh 501 ** or ROBOT.
06e0cb7… drh 502 */
06e0cb7… drh 503 void test_ishuman(void){
06e0cb7… drh 504 char zLine[3000];
06e0cb7… drh 505 while( fgets(zLine, sizeof(zLine), stdin) ){
06e0cb7… drh 506 fossil_print("%s %s", isHuman(zLine) ? "HUMAN" : "ROBOT", zLine);
06e0cb7… drh 507 }
d4a341b… dmitry 508 }
d4a341b… dmitry 509
d4a341b… dmitry 510 /*
d4a341b… dmitry 511 ** SQL function for constant time comparison of two values.
d4a341b… dmitry 512 ** Sets result to 0 if two values are equal.
d4a341b… dmitry 513 */
d4a341b… dmitry 514 static void constant_time_cmp_function(
d4a341b… dmitry 515 sqlite3_context *context,
d4a341b… dmitry 516 int argc,
d4a341b… dmitry 517 sqlite3_value **argv
d4a341b… dmitry 518 ){
d4a341b… dmitry 519 const unsigned char *buf1, *buf2;
d4a341b… dmitry 520 int len, i;
d4a341b… dmitry 521 unsigned char rc = 0;
d4a341b… dmitry 522
d4a341b… dmitry 523 assert( argc==2 );
d4a341b… dmitry 524 len = sqlite3_value_bytes(argv[0]);
d4a341b… dmitry 525 if( len==0 || len!=sqlite3_value_bytes(argv[1]) ){
d4a341b… dmitry 526 rc = 1;
d4a341b… dmitry 527 }else{
d4a341b… dmitry 528 buf1 = sqlite3_value_text(argv[0]);
d4a341b… dmitry 529 buf2 = sqlite3_value_text(argv[1]);
d4a341b… dmitry 530 for( i=0; i<len; i++ ){
d4a341b… dmitry 531 rc = rc | (buf1[i] ^ buf2[i]);
d4a341b… dmitry 532 }
d4a341b… dmitry 533 }
d4a341b… dmitry 534 sqlite3_result_int(context, rc);
d4a341b… dmitry 535 }
d4a341b… dmitry 536
d4a341b… dmitry 537 /*
653dd40… drh 538 ** Return true if the current page was reached by a redirect from the /login
653dd40… drh 539 ** page.
653dd40… drh 540 */
653dd40… drh 541 int referred_from_login(void){
653dd40… drh 542 const char *zReferer = P("HTTP_REFERER");
653dd40… drh 543 char *zPattern;
653dd40… drh 544 int rc;
653dd40… drh 545 if( zReferer==0 ) return 0;
653dd40… drh 546 zPattern = mprintf("%s/login*", g.zBaseURL);
653dd40… drh 547 rc = sqlite3_strglob(zPattern, zReferer)==0;
653dd40… drh 548 fossil_free(zPattern);
653dd40… drh 549 return rc;
653dd40… drh 550 }
653dd40… drh 551
653dd40… drh 552 /*
07bfe3f… drh 553 ** Return true if users are allowed to reset their own passwords.
07bfe3f… drh 554 */
07bfe3f… drh 555 int login_self_password_reset_available(void){
07bfe3f… drh 556 if( !db_get_boolean("self-pw-reset",0) ) return 0;
07bfe3f… drh 557 if( !alert_tables_exist() ) return 0;
07bfe3f… drh 558 return 1;
07bfe3f… drh 559 }
07bfe3f… drh 560
07bfe3f… drh 561 /*
99fcc43… drh 562 ** Return TRUE if self-registration is available. If the zNeeded
99fcc43… drh 563 ** argument is not NULL, then only return true if self-registration is
99fcc43… drh 564 ** available and any of the capabilities named in zNeeded are available
99fcc43… drh 565 ** to self-registered users.
99fcc43… drh 566 */
99fcc43… drh 567 int login_self_register_available(const char *zNeeded){
99fcc43… drh 568 CapabilityString *pCap;
99fcc43… drh 569 int rc;
99fcc43… drh 570 if( !db_get_boolean("self-register",0) ) return 0;
99fcc43… drh 571 if( zNeeded==0 ) return 1;
c00e912… drh 572 pCap = capability_add(0, db_get("default-perms", "u"));
99fcc43… drh 573 capability_expand(pCap);
99fcc43… drh 574 rc = capability_has_any(pCap, zNeeded);
99fcc43… drh 575 capability_free(pCap);
99fcc43… drh 576 return rc;
99fcc43… drh 577 }
99fcc43… drh 578
99fcc43… drh 579 /*
2f4a101… drh 580 ** There used to be a page named "my" that was designed to show information
2f4a101… drh 581 ** about a specific user. The "my" page was linked from the "Logged in as USER"
2f4a101… drh 582 ** line on the title bar. The "my" page was never completed so it is now
2f4a101… drh 583 ** removed. Use this page as a placeholder in older installations.
653dd40… drh 584 **
653dd40… drh 585 ** WEBPAGE: login
653dd40… drh 586 ** WEBPAGE: logout
653dd40… drh 587 ** WEBPAGE: my
653dd40… drh 588 **
653dd40… drh 589 ** The login/logout page. Parameters:
653dd40… drh 590 **
653dd40… drh 591 ** g=URL Jump back to this URL after login completes
653dd40… drh 592 ** anon The g=URL is not accessible by "nobody" but is
653dd40… drh 593 ** accessible by "anonymous"
2f4a101… drh 594 */
2f4a101… drh 595 void login_page(void){
2f4a101… drh 596 const char *zUsername, *zPasswd;
2f4a101… drh 597 const char *zNew1, *zNew2;
2f4a101… drh 598 const char *zAnonPw = 0;
79ef961… drh 599 const char *zGoto = P("g");
653dd40… drh 600 int anonFlag; /* Login as "anonymous" would be useful */
2f4a101… drh 601 char *zErrMsg = "";
db0c512… drh 602 int uid; /* User id logged in user */
2f4a101… drh 603 char *zSha1Pw;
2f4a101… drh 604 const char *zIpAddr; /* IP address of requestor */
6b7b323… drh 605 const int noAnon = P("noanon")!=0;
6b7b323… drh 606 int rememberMe; /* If true, use persistent cookie, else
6b7b323… drh 607 session cookie. Toggled per
6b7b323… drh 608 checkbox. */
4aba9ea… drh 609
07bfe3f… drh 610 if( P("pwreset")!=0 && login_self_password_reset_available() ){
07bfe3f… drh 611 /* If the "Reset Password" button in the form was pressed, render
07bfe3f… drh 612 ** the Request Password Reset page in place of this one. */
07bfe3f… drh 613 login_reqpwreset_page();
07bfe3f… drh 614 return;
07bfe3f… drh 615 }
e58112a… drh 616
e58112a… drh 617 /* If the "anon" query parameter is 1 or 2, that means rework the web-page
e58112a… drh 618 ** to make it a more user-friendly captcha. Extraneous text and boxes
e58112a… drh 619 ** are omitted. The user has just the captcha image and an entry box
e58112a… drh 620 ** and a "Verify" button. Underneath is the same login page for user
e58112a… drh 621 ** "anonymous", just displayed in an easier to digest format for one-time
e58112a… drh 622 ** visitors.
e58112a… drh 623 **
e58112a… drh 624 ** anon=1 is advisory and only has effect if there is not some other login
e58112a… drh 625 ** cookie. anon=2 means always show the captcha.
e58112a… drh 626 */
7d2b47a… drh 627 anonFlag = anon_cookie_lifespan()>0 ? atoi(PD("anon","0")) : 0;
e58112a… drh 628 if( anonFlag==2 ){
e58112a… drh 629 g.zLogin = 0;
e58112a… drh 630 }else{
e58112a… drh 631 login_check_credentials();
e58112a… drh 632 if( g.zLogin!=0 ) anonFlag = 0;
e58112a… drh 633 }
e58112a… drh 634
4aba9ea… drh 635 fossil_redirect_to_https_if_needed(1);
2f4a101… drh 636 sqlite3_create_function(g.db, "constant_time_cmp", 2, SQLITE_UTF8, 0,
b3e32c8… jan.nijtmans 637 constant_time_cmp_function, 0, 0);
2f4a101… drh 638 zUsername = P("u");
2f4a101… drh 639 zPasswd = P("p");
6c8c93a… drh 640
653dd40… drh 641 /* Handle log-out requests */
920ace1… drh 642 if( P("out") && cgi_csrf_safe(2) ){
796dcfe… drh 643 login_clear_login_data();
1958448… drh 644 login_redirect_to_g();
99fcc43… drh 645 return;
99fcc43… drh 646 }
99fcc43… drh 647
99fcc43… drh 648 /* Redirect for create-new-account requests */
99fcc43… drh 649 if( P("self") ){
99fcc43… drh 650 cgi_redirectf("%R/register");
653dd40… drh 651 return;
653dd40… drh 652 }
653dd40… drh 653
653dd40… drh 654 /* Deal with password-change requests */
2f4a101… drh 655 if( g.perm.Password && zPasswd
2f4a101… drh 656 && (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0
920ace1… drh 657 && cgi_csrf_safe(2)
8b562b9… mistachkin 658 ){
8b562b9… mistachkin 659 /* If there is not a "real" login, we cannot change any password. */
8b562b9… mistachkin 660 if( g.zLogin ){
8b562b9… mistachkin 661 /* The user requests a password change */
8b562b9… mistachkin 662 zSha1Pw = sha1_shared_secret(zPasswd, g.zLogin, 0);
8b562b9… mistachkin 663 if( db_int(1, "SELECT 0 FROM user"
8b562b9… mistachkin 664 " WHERE uid=%d"
8b562b9… mistachkin 665 " AND (constant_time_cmp(pw,%Q)=0"
8b562b9… mistachkin 666 " OR constant_time_cmp(pw,%Q)=0)",
8b562b9… mistachkin 667 g.userUid, zSha1Pw, zPasswd) ){
8b562b9… mistachkin 668 sleep(1);
8b562b9… mistachkin 669 zErrMsg =
8b562b9… mistachkin 670 @ <p><span class="loginError">
8b562b9… mistachkin 671 @ You entered an incorrect old password while attempting to change
8b562b9… mistachkin 672 @ your password. Your password is unchanged.
8b562b9… mistachkin 673 @ </span></p>
8b562b9… mistachkin 674 ;
8b562b9… mistachkin 675 }else if( fossil_strcmp(zNew1,zNew2)!=0 ){
8b562b9… mistachkin 676 zErrMsg =
8b562b9… mistachkin 677 @ <p><span class="loginError">
8b562b9… mistachkin 678 @ The two copies of your new passwords do not match.
8b562b9… mistachkin 679 @ Your password is unchanged.
8b562b9… mistachkin 680 @ </span></p>
8b562b9… mistachkin 681 ;
8b562b9… mistachkin 682 }else{
8b562b9… mistachkin 683 char *zNewPw = sha1_shared_secret(zNew1, g.zLogin, 0);
8b562b9… mistachkin 684 char *zChngPw;
8b562b9… mistachkin 685 char *zErr;
f741baa… drh 686 int rc;
f741baa… drh 687
07bfe3f… drh 688 /* vvvvvvv--- tag-20230106-1 ----vvvvvv
07bfe3f… drh 689 **
07bfe3f… drh 690 ** Replicate changes made below to tag-20230106-2
07bfe3f… drh 691 */
c9c7e8c… drh 692 admin_log("password change for user %s", g.zLogin);
f741baa… drh 693 db_unprotect(PROTECT_USER);
8b562b9… mistachkin 694 db_multi_exec(
8b562b9… mistachkin 695 "UPDATE user SET pw=%Q WHERE uid=%d", zNewPw, g.userUid
8b562b9… mistachkin 696 );
8b562b9… mistachkin 697 zChngPw = mprintf(
8b562b9… mistachkin 698 "UPDATE user"
8b562b9… mistachkin 699 " SET pw=shared_secret(%Q,%Q,"
8b562b9… mistachkin 700 " (SELECT value FROM config WHERE name='project-code'))"
8b562b9… mistachkin 701 " WHERE login=%Q",
8b562b9… mistachkin 702 zNew1, g.zLogin, g.zLogin
8b562b9… mistachkin 703 );
f741baa… drh 704 fossil_free(zNewPw);
f741baa… drh 705 rc = login_group_sql(zChngPw, "<p>", "</p>\n", &zErr);
f741baa… drh 706 db_protect_pop();
07bfe3f… drh 707 /*
07bfe3f… drh 708 ** ^^^^^^^^--- tag-20230106-1 ----^^^^^^^^^
07bfe3f… drh 709 **
07bfe3f… drh 710 ** Replicate changes above to tag-20230106-2
07bfe3f… drh 711 */
07bfe3f… drh 712
f741baa… drh 713 if( rc ){
8b562b9… mistachkin 714 zErrMsg = mprintf("<span class=\"loginError\">%s</span>", zErr);
8b562b9… mistachkin 715 fossil_free(zErr);
8b562b9… mistachkin 716 }else{
1958448… drh 717 login_redirect_to_g();
8b562b9… mistachkin 718 return;
8b562b9… mistachkin 719 }
8b562b9… mistachkin 720 }
8b562b9… mistachkin 721 }else{
8b562b9… mistachkin 722 zErrMsg =
8b562b9… mistachkin 723 @ <p><span class="loginError">
8b562b9… mistachkin 724 @ The password cannot be changed for this type of login.
8b562b9… mistachkin 725 @ The password is unchanged.
8b562b9… mistachkin 726 @ </span></p>
8b562b9… mistachkin 727 ;
2f4a101… drh 728 }
2f4a101… drh 729 }
2f4a101… drh 730 zIpAddr = PD("REMOTE_ADDR","nil"); /* Complete IP address for logging */
796dcfe… drh 731 uid = login_is_valid_anonymous(zUsername, zPasswd, P("cs"));
6b7b323… drh 732 if(zUsername==0){
6b7b323… drh 733 /* Initial login page hit. */
6b7b323… drh 734 rememberMe = 0;
6b7b323… drh 735 }else{
6b7b323… drh 736 rememberMe = P("remember")!=0;
6b7b323… drh 737 }
2f4a101… drh 738 if( uid>0 ){
1958448… drh 739 login_set_anon_cookie(NULL, rememberMe?0:1);
2f4a101… drh 740 record_login_attempt("anonymous", zIpAddr, 1);
1958448… drh 741 login_redirect_to_g();
2f4a101… drh 742 }
2f4a101… drh 743 if( zUsername!=0 && zPasswd!=0 && zPasswd[0]!=0 ){
2f4a101… drh 744 /* Attempting to log in as a user other than anonymous.
2f4a101… drh 745 */
9b4e157… drh 746 uid = login_search_uid(&zUsername, zPasswd);
2f4a101… drh 747 if( uid<=0 ){
2f4a101… drh 748 sleep(1);
45f3516… jan.nijtmans 749 zErrMsg =
2f4a101… drh 750 @ <p><span class="loginError">
2f4a101… drh 751 @ You entered an unknown user or an incorrect password.
2f4a101… drh 752 @ </span></p>
2f4a101… drh 753 ;
2f4a101… drh 754 record_login_attempt(zUsername, zIpAddr, 0);
39d7eb0… wyoung 755 cgi_set_status(401, "Unauthorized");
2f4a101… drh 756 }else{
2f4a101… drh 757 /* Non-anonymous login is successful. Set a cookie of the form:
2f4a101… drh 758 **
2f4a101… drh 759 ** HASH/PROJECT/LOGIN
2f4a101… drh 760 **
2f4a101… drh 761 ** where HASH is a random hex number, PROJECT is either project
2f4a101… drh 762 ** code prefix, and LOGIN is the user name.
2f4a101… drh 763 */
6b7b323… drh 764 login_set_user_cookie(zUsername, uid, NULL, rememberMe?0:1);
1958448… drh 765 login_redirect_to_g();
796dcfe… drh 766 }
796dcfe… drh 767 }
112c713… drh 768 style_set_current_feature("login");
2f4a101… drh 769 style_header("Login/Logout");
e58112a… drh 770 if( anonFlag==2 ) g.zLogin = 0;
ff78d6d… drh 771 style_adunit_config(ADUNIT_OFF);
2f4a101… drh 772 @ %s(zErrMsg)
99fcc43… drh 773 if( zGoto && !noAnon ){
653dd40… drh 774 char *zAbbrev = fossil_strdup(zGoto);
653dd40… drh 775 int i;
653dd40… drh 776 for(i=0; zAbbrev[i] && zAbbrev[i]!='?'; i++){}
653dd40… drh 777 zAbbrev[i] = 0;
653dd40… drh 778 if( g.zLogin ){
73ec21e… drh 779 @ <p>Use a different login with greater privilege than <b>%h(g.zLogin)</b>
653dd40… drh 780 @ to access <b>%h(zAbbrev)</b>.
653dd40… drh 781 }else if( anonFlag ){
6c8c93a… drh 782 @ <p><b>Verify that you are human by typing in the 8-character text
6c8c93a… drh 783 @ password shown below.</b></p>
653dd40… drh 784 }else{
653dd40… drh 785 @ <p>Login as a named user to access page <b>%h(zAbbrev)</b>.
653dd40… drh 786 }
6b7b323… drh 787 fossil_free(zAbbrev);
c6785fa… drh 788 }
2f4a101… drh 789 if( g.sslNotAvailable==0
e065d5b… drh 790 && strncmp(g.zBaseURL,"https:",6)!=0
2f4a101… drh 791 && db_get_boolean("https-login",0)
2f4a101… drh 792 ){
c6785fa… drh 793 form_begin(0, "https:%s/login", g.zBaseURL+5);
c6785fa… drh 794 }else{
c6785fa… drh 795 form_begin(0, "%R/login");
c6785fa… drh 796 }
c6785fa… drh 797 if( zGoto ){
f5482a0… wyoung 798 @ <input type="hidden" name="g" value="%h(zGoto)">
c6785fa… drh 799 }
c6785fa… drh 800 if( anonFlag ){
f5482a0… wyoung 801 @ <input type="hidden" name="anon" value="1">
6c8c93a… drh 802 @ <input type="hidden" name="u" value="anonymous">
c6785fa… drh 803 }
c6785fa… drh 804 if( g.zLogin ){
c6785fa… drh 805 @ <p>Currently logged in as <b>%h(g.zLogin)</b>.
bc05e6c… florian 806 @ <input type="submit" name="out" value="Logout" autofocus></p>
643123d… andybradford 807 @ </form>
00bed59… drh 808 }else{
a584491… drh 809 unsigned int uSeed = captcha_seed();
7d2b47a… drh 810 if( g.zLogin==0 && (anonFlag || zGoto==0) && anon_cookie_lifespan()>0 ){
a584491… drh 811 zAnonPw = db_text(0, "SELECT pw FROM user"
a584491… drh 812 " WHERE login='anonymous'"
a584491… drh 813 " AND cap!=''");
a584491… drh 814 }else{
a584491… drh 815 zAnonPw = 0;
a584491… drh 816 }
00bed59… drh 817 @ <table class="login_out">
6c8c93a… drh 818 if( P("HTTPS")==0 && !anonFlag ){
27769be… drh 819 @ <tr><td class="form_label">Warning:</td>
27769be… drh 820 @ <td><span class='securityWarning'>
275da70… danield 821 @ Login information, including the password,
6b7b323… drh 822 @ will be sent in the clear over an unencrypted connection.
6b7b323… drh 823 if( !g.sslNotAvailable ){
00bed59… drh 824 @ Consider logging in at
00bed59… drh 825 @ <a href='%s(g.zHttpsURL)'>%h(g.zHttpsURL)</a> instead.
00bed59… drh 826 }
27769be… drh 827 @ </span></td></tr>
27769be… drh 828 }
6c8c93a… drh 829 if( !anonFlag ){
6c8c93a… drh 830 @ <tr>
6c8c93a… drh 831 @ <td class="form_label" id="userlabel1">User ID:</td>
6c8c93a… drh 832 @ <td><input type="text" id="u" aria-labelledby="userlabel1" name="u" \
6c8c93a… drh 833 @ size="30" value="" autofocus></td>
6c8c93a… drh 834 @ </tr>
6c8c93a… drh 835 }
27769be… drh 836 @ <tr>
6b7b323… drh 837 @ <td class="form_label" id="pswdlabel">Password:</td>
6b7b323… drh 838 @ <td><input aria-labelledby="pswdlabel" type="password" id="p" \
b873148… florian 839 @ name="p" value="" size="30"%s(anonFlag ? " autofocus" : "")>
6c8c93a… drh 840 if( anonFlag ){
6c8c93a… drh 841 @ </td></tr>
6c8c93a… drh 842 @ <tr>
6c8c93a… drh 843 @ <td></td><td>\
6c8c93a… drh 844 captcha_speakit_button(uSeed, "Read the password out loud");
6c8c93a… drh 845 }else if( zAnonPw && !noAnon ){
6b7b323… drh 846 captcha_speakit_button(uSeed, "Speak password for \"anonymous\"");
6b7b323… drh 847 }
6b7b323… drh 848 @ </td>
27769be… drh 849 @ </tr>
6c8c93a… drh 850 if( !anonFlag ){
6c8c93a… drh 851 @ <tr>
6c8c93a… drh 852 @ <td></td>
6c8c93a… drh 853 @ <td><input type="checkbox" name="remember" value="1" \
6c8c93a… drh 854 @ id="remember-me" %s(rememberMe ? "checked=\"checked\"" : "")>
6c8c93a… drh 855 @ <label for="remember-me">Remember me?</label></td>
6c8c93a… drh 856 @ </tr>
6c8c93a… drh 857 @ <tr>
6c8c93a… drh 858 @ <td></td>
6c8c93a… drh 859 @ <td><input type="submit" name="in" value="Login">
6c8c93a… drh 860 @ </tr>
6c8c93a… drh 861 }else{
6c8c93a… drh 862 @ <tr>
6c8c93a… drh 863 @ <td></td>
6c8c93a… drh 864 @ <td><input type="submit" name="in" value="Verify that I am human">
6c8c93a… drh 865 @ </tr>
6c8c93a… drh 866 }
6c8c93a… drh 867 if( !anonFlag && !noAnon && login_self_register_available(0) ){
99fcc43… drh 868 @ <tr>
99fcc43… drh 869 @ <td></td>
99fcc43… drh 870 @ <td><input type="submit" name="self" value="Create A New Account">
27769be… drh 871 @ </tr>
27769be… drh 872 }
6c8c93a… drh 873 if( !anonFlag && login_self_password_reset_available() ){
07bfe3f… drh 874 @ <tr>
07bfe3f… drh 875 @ <td></td>
07bfe3f… drh 876 @ <td><input type="submit" name="pwreset" value="Reset My Password">
07bfe3f… drh 877 @ </tr>
07bfe3f… drh 878 }
99fcc43… drh 879 @ </table>
99fcc43… drh 880 if( zAnonPw && !noAnon ){
8659d84… drh 881 const char *zDecoded = captcha_decode(uSeed, 0);
99fcc43… drh 882 int bAutoCaptcha = db_get_boolean("auto-captcha", 0);
99fcc43… drh 883 char *zCaptcha = captcha_render(zDecoded);
275da70… danield 884
f5482a0… wyoung 885 @ <p><input type="hidden" name="cs" value="%u(uSeed)">
6c8c93a… drh 886 if( !anonFlag ){
6c8c93a… drh 887 @ Visitors may enter <b>anonymous</b> as the user-ID with
6c8c93a… drh 888 @ the 8-character hexadecimal password shown below:</p>
6c8c93a… drh 889 }
75c89de… drh 890 @ <div class="captcha"><table class="captcha"><tr><td>\
75c89de… drh 891 @ <pre class="captcha">
99fcc43… drh 892 @ %h(zCaptcha)
99fcc43… drh 893 @ </pre></td></tr></table>
6c8c93a… drh 894 if( bAutoCaptcha && !anonFlag ) {
99fcc43… drh 895 @ <input type="button" value="Fill out captcha" id='autofillButton' \
f5482a0… wyoung 896 @ data-af='%s(zDecoded)'>
036a9d5… drh 897 builtin_request_js("login.js");
00bed59… drh 898 }
00bed59… drh 899 @ </div>
00bed59… drh 900 free(zCaptcha);
00bed59… drh 901 }
00bed59… drh 902 @ </form>
00bed59… drh 903 }
6c8c93a… drh 904 if( login_is_individual() && !anonFlag ){
6898b3e… drh 905 if( g.perm.EmailAlert && alert_enabled() ){
b77f1aa… drh 906 @ <hr>
b77f1aa… drh 907 @ <p>Configure <a href="%R/alerts">Email Alerts</a>
b77f1aa… drh 908 @ for user <b>%h(g.zLogin)</b></p>
b77f1aa… drh 909 }
2d59385… stephan 910 if( db_table_exists("repository","forumpost") ){
2d59385… stephan 911 @ <hr><p>
2d59385… stephan 912 @ <a href="%R/timeline?ss=v&y=f&vfx&u=%t(g.zLogin)">Forum
2d59385… stephan 913 @ post timeline</a> for user <b>%h(g.zLogin)</b></p>
2d59385… stephan 914 }
8581e37… drh 915 }
6c8c93a… drh 916 if( !anonFlag ){
6c8c93a… drh 917 @ <hr><p>
6c8c93a… drh 918 @ Select your preferred <a href="%R/skins">site skin</a>.
6c8c93a… drh 919 @ </p>
6c8c93a… drh 920 @ <hr><p>
2a3d303… drh 921 @ Manage your <a href="%R/cookies">cookies</a> or your
2a3d303… drh 922 @ <a href="%R/tokens">access tokens</a>.</p>
6c8c93a… drh 923 }
8581e37… drh 924 if( login_is_individual() ){
6898b3e… drh 925 if( g.perm.Password ){
49f68be… drh 926 char *zRPW = fossil_random_password(12);
6898b3e… drh 927 @ <hr>
6898b3e… drh 928 @ <p>Change Password for user <b>%h(g.zLogin)</b>:</p>
6898b3e… drh 929 form_begin(0, "%R/login");
6898b3e… drh 930 @ <table>
7dd07b2… drh 931 @ <tr><td class="form_label" id="oldpw">Old Password:</td>
7dd07b2… drh 932 @ <td><input aria-labelledby="oldpw" type="password" name="p" \
7dd07b2… drh 933 @ size="30"/></td></tr>
7dd07b2… drh 934 @ <tr><td class="form_label" id="newpw">New Password:</td>
7dd07b2… drh 935 @ <td><input aria-labelledby="newpw" type="password" name="n1" \
f5482a0… wyoung 936 @ size="30"> Suggestion: %z(zRPW)</td></tr>
7dd07b2… drh 937 @ <tr><td class="form_label" id="reppw">Repeat New Password:</td>
7dd07b2… drh 938 @ <td><input aria-labledby="reppw" type="password" name="n2" \
f5482a0… wyoung 939 @ size="30"></td></tr>
6898b3e… drh 940 @ <tr><td></td>
f5482a0… wyoung 941 @ <td><input type="submit" value="Change Password"></td></tr>
6898b3e… drh 942 @ </table>
6898b3e… drh 943 @ </form>
6898b3e… drh 944 }
6898b3e… drh 945 }
07bfe3f… drh 946 style_finish_page();
07bfe3f… drh 947 }
07bfe3f… drh 948
07bfe3f… drh 949 /*
07bfe3f… drh 950 ** Construct an appropriate URL suffix for the /resetpw page. The
07bfe3f… drh 951 ** suffix will be of the form:
07bfe3f… drh 952 **
07bfe3f… drh 953 ** UID-TIMESTAMP-HASH
07bfe3f… drh 954 **
07bfe3f… drh 955 ** Where UID and TIMESTAMP are the parameters to this function, and HASH
07bfe3f… drh 956 ** is constructed from information that is unique to the user in question
07bfe3f… drh 957 ** and which is not publicly available. In particular, the HASH includes
07bfe3f… drh 958 ** the existing user password. Thus, in order to construct a URL that can
07bfe3f… drh 959 ** change a password, an attacker must know the current password, in which
07bfe3f… drh 960 ** case the attacker does not need to construct the URL in order to take
07bfe3f… drh 961 ** over the account.
07bfe3f… drh 962 **
07bfe3f… drh 963 ** Return a pointer to the resulting string in memory obtained
07bfe3f… drh 964 ** from fossil_malloc().
07bfe3f… drh 965 */
07bfe3f… drh 966 char *login_resetpw_suffix(int uid, i64 timestamp){
07bfe3f… drh 967 char *zHash;
07bfe3f… drh 968 char *zInnerSql;
07bfe3f… drh 969 char *zResult;
07bfe3f… drh 970 extern int sqlite3_shathree_init(sqlite3*,char**,const sqlite3_api_routines*);
07bfe3f… drh 971 if( timestamp<=0 ){ timestamp = time(0); }
07bfe3f… drh 972 sqlite3_shathree_init(g.db, 0, 0);
07bfe3f… drh 973 if( db_table_exists("repository","subscriber") ){
07bfe3f… drh 974 zInnerSql = mprintf(
07bfe3f… drh 975 "SELECT %lld, login, pw, cookie, user.mtime, user.info, subscriberCode"
07bfe3f… drh 976 " FROM user LEFT JOIN subscriber ON suname=login"
07bfe3f… drh 977 " WHERE uid=%d", timestamp, uid);
07bfe3f… drh 978 }else{
07bfe3f… drh 979 zInnerSql = mprintf(
07bfe3f… drh 980 "SELECT %lld, login, pw, cookie, user.mtime, user.info"
07bfe3f… drh 981 " FROM user WHERE uid=%d", timestamp, uid);
07bfe3f… drh 982 }
07bfe3f… drh 983 zHash = db_text(0, "SELECT lower(hex(sha3_query(%Q)))", zInnerSql);
07bfe3f… drh 984 fossil_free(zInnerSql);
07bfe3f… drh 985 zResult = mprintf("%x-%llx-%s", uid, timestamp, zHash);
07bfe3f… drh 986 if( strlen(zHash)<64 || strlen(zResult)<70 ){
07bfe3f… drh 987 /* This should never happen, but if it does, we don't want it to lead
07bfe3f… drh 988 ** to a security breach. */
07bfe3f… drh 989 fossil_panic("insecure password reset hash generated\n");
07bfe3f… drh 990 }
07bfe3f… drh 991 fossil_free(zHash);
07bfe3f… drh 992 return zResult;
07bfe3f… drh 993 }
07bfe3f… drh 994
07bfe3f… drh 995 /*
07bfe3f… drh 996 ** Check to see if the "name" query parameter is a valid resetpw suffix
07bfe3f… drh 997 ** for a user whose password we are allowed to reset. If it is, then return
07bfe3f… drh 998 ** the positive integer UID for that user. If the query parameter is not
07bfe3f… drh 999 ** valid, return 0.
07bfe3f… drh 1000 */
07bfe3f… drh 1001 static int login_resetpw_suffix_is_valid(const char *zName){
07bfe3f… drh 1002 int i, j;
07bfe3f… drh 1003 int uid;
07bfe3f… drh 1004 i64 timestamp;
07bfe3f… drh 1005 i64 now;
07bfe3f… drh 1006 char *zHash;
07bfe3f… drh 1007 if( zName==0 || strlen(zName)<70 ) goto not_valid_suffix;
07bfe3f… drh 1008 for(i=0; fossil_isxdigit(zName[i]); i++){}
07bfe3f… drh 1009 if( i<1 || zName[i]!='-' ) goto not_valid_suffix;
07bfe3f… drh 1010 for(j=i+1; fossil_isxdigit(zName[j]); j++){}
07bfe3f… drh 1011 if( j<=i+1 || zName[j]!='-' ) goto not_valid_suffix;
07bfe3f… drh 1012 uid = strtol(zName, 0, 16);
07bfe3f… drh 1013 if( uid<=0 ) goto not_valid_suffix;
07bfe3f… drh 1014 if( !db_exists("SELECT 1 FROM user WHERE uid=%d", uid) ){
07bfe3f… drh 1015 goto not_valid_suffix;
07bfe3f… drh 1016 }
07bfe3f… drh 1017 timestamp = strtoll(&zName[i+1], 0, 16);
07bfe3f… drh 1018 now = time(0);
07bfe3f… drh 1019 if( timestamp+3600 <= now ) goto not_valid_suffix;
07bfe3f… drh 1020 zHash = login_resetpw_suffix(uid,timestamp);
07bfe3f… drh 1021 if( fossil_strcmp(zHash, zName)!=0 ){
07bfe3f… drh 1022 fossil_free(zHash);
07bfe3f… drh 1023 goto not_valid_suffix;
07bfe3f… drh 1024 }
07bfe3f… drh 1025 fossil_free(zHash);
07bfe3f… drh 1026 return uid;
07bfe3f… drh 1027
07bfe3f… drh 1028 not_valid_suffix:
07bfe3f… drh 1029 return 0;
07bfe3f… drh 1030 }
07bfe3f… drh 1031
07bfe3f… drh 1032 /*
07bfe3f… drh 1033 ** COMMAND: test-resetpw-url
07bfe3f… drh 1034 ** Usage: fossil test-resetpw-url UID
07bfe3f… drh 1035 **
07bfe3f… drh 1036 ** Generate and verify a /resetpw URL for user UID.
07bfe3f… drh 1037 **
07bfe3f… drh 1038 ** This command is intended for unit testing the login_resetpw_suffix()
07bfe3f… drh 1039 ** and login_resetpw_suffix_is_valid() functions.
07bfe3f… drh 1040 */
07bfe3f… drh 1041 void test_resetpw_url(void){
07bfe3f… drh 1042 char *zSuffix;
07bfe3f… drh 1043 int uid;
07bfe3f… drh 1044 int xuid;
07bfe3f… drh 1045 char *zLogin;
07bfe3f… drh 1046 int i;
07bfe3f… drh 1047 db_find_and_open_repository(0, 0);
07bfe3f… drh 1048 verify_all_options();
07bfe3f… drh 1049 if( g.argc<3 ){
07bfe3f… drh 1050 usage("UID ...");
07bfe3f… drh 1051 }
07bfe3f… drh 1052 for(i=2; i<g.argc; i++){
07bfe3f… drh 1053 uid = atoi(g.argv[i]);
07bfe3f… drh 1054 zSuffix = login_resetpw_suffix(uid, 0);
07bfe3f… drh 1055 xuid = login_resetpw_suffix_is_valid(zSuffix);
07bfe3f… drh 1056 if( xuid>0 ){
07bfe3f… drh 1057 zLogin = db_text(0, "SELECT login FROM user WHERE uid=%d", xuid);
07bfe3f… drh 1058 }else{
07bfe3f… drh 1059 zLogin = 0;
07bfe3f… drh 1060 }
07bfe3f… drh 1061 fossil_print("/resetpw/%s %d (%s)\n",
07bfe3f… drh 1062 zSuffix, xuid, zLogin ? zLogin : "???");
07bfe3f… drh 1063 fossil_free(zSuffix);
07bfe3f… drh 1064 fossil_free(zLogin);
07bfe3f… drh 1065 }
07bfe3f… drh 1066 }
07bfe3f… drh 1067
07bfe3f… drh 1068 /*
07bfe3f… drh 1069 ** WEBPAGE: resetpw
07bfe3f… drh 1070 **
07bfe3f… drh 1071 ** The URL format must be like this:
07bfe3f… drh 1072 **
07bfe3f… drh 1073 ** /resetpw/UID-TIMESTAMP-HASH
07bfe3f… drh 1074 **
07bfe3f… drh 1075 ** Where UID is the uid of the user whose password is to be reset,
07bfe3f… drh 1076 ** TIMESTAMP is the unix timestamp when the request was made, and
07bfe3f… drh 1077 ** HASH is a hash based on UID, TIMESTAMP, and other information that
07bfe3f… drh 1078 ** is unavailable to an attacher.
07bfe3f… drh 1079 **
07bfe3f… drh 1080 ** With no other arguments, a form is present which allows the user to
07bfe3f… drh 1081 ** enter a new password. When the SUBMIT button is pressed, a POST request
07bfe3f… drh 1082 ** back to the same URL that will change the password.
07bfe3f… drh 1083 */
07bfe3f… drh 1084 void login_resetpw(void){
07bfe3f… drh 1085 const char *zName;
07bfe3f… drh 1086 int uid;
07bfe3f… drh 1087 char *zRPW;
07bfe3f… drh 1088 const char *zNew1, *zNew2;
07bfe3f… drh 1089
07bfe3f… drh 1090 style_set_current_feature("resetpw");
07bfe3f… drh 1091 style_header("Reset Password");
07bfe3f… drh 1092 style_adunit_config(ADUNIT_OFF);
07bfe3f… drh 1093 zName = PD("name","");
07bfe3f… drh 1094 uid = login_resetpw_suffix_is_valid(zName);
07bfe3f… drh 1095 if( uid==0 ){
07bfe3f… drh 1096 @ <p><span class="loginError">
07bfe3f… drh 1097 @ This password-reset URL is invalid, probably because it has expired.
07bfe3f… drh 1098 @ Password-reset URLs have a short lifespan.
07bfe3f… drh 1099 @ </span></p>
07bfe3f… drh 1100 style_finish_page();
275da70… danield 1101 sleep(1); /* Introduce a small delay on an invalid suffix as an
07bfe3f… drh 1102 ** extra defense against search attacks */
07bfe3f… drh 1103 return;
07bfe3f… drh 1104 }
3b1e8a0… drh 1105 fossil_redirect_to_https_if_needed(1);
07bfe3f… drh 1106 login_set_uid(uid, 0);
07bfe3f… drh 1107 if( g.perm.Setup || g.perm.Admin || !g.perm.Password || g.zLogin==0 ){
07bfe3f… drh 1108 @ <p><span class="loginError">
07bfe3f… drh 1109 @ Cannot change the password for user <b>%h(g.zLogin)</b>.
07bfe3f… drh 1110 @ </span></p>
07bfe3f… drh 1111 style_finish_page();
07bfe3f… drh 1112 return;
07bfe3f… drh 1113 }
07bfe3f… drh 1114 if( (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0 ){
07bfe3f… drh 1115 if( fossil_strcmp(zNew1,zNew2)!=0 ){
07bfe3f… drh 1116 @ <p><span class="loginError">
07bfe3f… drh 1117 @ The two copies of your new passwords do not match.
07bfe3f… drh 1118 @ Try again.
07bfe3f… drh 1119 @ </span></p>
07bfe3f… drh 1120 }else{
07bfe3f… drh 1121 char *zNewPw = sha1_shared_secret(zNew1, g.zLogin, 0);
07bfe3f… drh 1122 char *zChngPw;
07bfe3f… drh 1123 char *zErr;
07bfe3f… drh 1124 int rc;
07bfe3f… drh 1125
07bfe3f… drh 1126 /* vvvvvvv--- tag-20230106-2 ----vvvvvv
07bfe3f… drh 1127 **
07bfe3f… drh 1128 ** Replicate changes made below to tag-20230106-1
07bfe3f… drh 1129 */
c9c7e8c… drh 1130 admin_log("password change for user %s", g.zLogin);
07bfe3f… drh 1131 db_unprotect(PROTECT_USER);
07bfe3f… drh 1132 db_multi_exec(
07bfe3f… drh 1133 "UPDATE user SET pw=%Q WHERE uid=%d", zNewPw, g.userUid
07bfe3f… drh 1134 );
07bfe3f… drh 1135 zChngPw = mprintf(
07bfe3f… drh 1136 "UPDATE user"
07bfe3f… drh 1137 " SET pw=shared_secret(%Q,%Q,"
07bfe3f… drh 1138 " (SELECT value FROM config WHERE name='project-code'))"
07bfe3f… drh 1139 " WHERE login=%Q",
07bfe3f… drh 1140 zNew1, g.zLogin, g.zLogin
07bfe3f… drh 1141 );
07bfe3f… drh 1142 fossil_free(zNewPw);
07bfe3f… drh 1143 rc = login_group_sql(zChngPw, "<p>", "</p>\n", &zErr);
07bfe3f… drh 1144 db_protect_pop();
07bfe3f… drh 1145 /*
07bfe3f… drh 1146 ** ^^^^^^^^--- tag-20230106-2 ----^^^^^^^^^
07bfe3f… drh 1147 **
07bfe3f… drh 1148 ** Replicate changes above to tag-20230106-1
07bfe3f… drh 1149 */
07bfe3f… drh 1150
07bfe3f… drh 1151 if( rc ){
07bfe3f… drh 1152 @ <p><span class='loginError'>
07bfe3f… drh 1153 @ %s(zErr);
07bfe3f… drh 1154 @ </span></p>
07bfe3f… drh 1155 fossil_free(zErr);
07bfe3f… drh 1156 }else{
07bfe3f… drh 1157 @ <p>Password changed successfully. Go to the
07bfe3f… drh 1158 @ <a href="%R/login?u=%t(g.zLogin)">Login</a> page and log in
07bfe3f… drh 1159 @ using the new password to continue.
07bfe3f… drh 1160 @ </p>
07bfe3f… drh 1161 style_finish_page();
07bfe3f… drh 1162 return;
07bfe3f… drh 1163 }
07bfe3f… drh 1164 }
07bfe3f… drh 1165 }
07bfe3f… drh 1166 zRPW = fossil_random_password(12);
07bfe3f… drh 1167 @ <p>Change Password for user <b>%h(g.zLogin)</b>:</p>
07bfe3f… drh 1168 form_begin(0, "%R/resetpw");
07bfe3f… drh 1169 @ <input type='hidden' name='name' value='%h(zName)'>
07bfe3f… drh 1170 @ <table>
07bfe3f… drh 1171 @ <tr><td class="form_label" id="newpw">New Password:</td>
07bfe3f… drh 1172 @ <td><input aria-labelledby="newpw" type="password" name="n1" \
f5482a0… wyoung 1173 @ size="30"> Suggestion: %z(zRPW)</td></tr>
07bfe3f… drh 1174 @ <tr><td class="form_label" id="reppw">Repeat New Password:</td>
07bfe3f… drh 1175 @ <td><input aria-labledby="reppw" type="password" name="n2" \
f5482a0… wyoung 1176 @ size="30"></td></tr>
07bfe3f… drh 1177 @ <tr><td></td>
f5482a0… wyoung 1178 @ <td><input type="submit" value="Change Password"></td></tr>
07bfe3f… drh 1179 @ </table>
07bfe3f… drh 1180 @ </form>
112c713… drh 1181 style_finish_page();
2f4a101… drh 1182 }
2f4a101… drh 1183
2f4a101… drh 1184 /*
a257fde… drh 1185 ** Attempt to find login credentials for user zLogin on a peer repository
45f3516… jan.nijtmans 1186 ** with project code zCode. Transfer those credentials to the local
a257fde… drh 1187 ** repository.
a257fde… drh 1188 **
a257fde… drh 1189 ** Return true if a transfer was made and false if not.
a257fde… drh 1190 */
a257fde… drh 1191 static int login_transfer_credentials(
a257fde… drh 1192 const char *zLogin, /* Login we are looking for */
a257fde… drh 1193 const char *zCode, /* Project code of peer repository */
7d18c40… drh 1194 const char *zHash /* HASH from login cookie HASH/CODE/LOGIN */
a257fde… drh 1195 ){
a257fde… drh 1196 sqlite3 *pOther = 0; /* The other repository */
a257fde… drh 1197 sqlite3_stmt *pStmt; /* Query against the other repository */
a257fde… drh 1198 char *zSQL; /* SQL of the query against other repo */
a257fde… drh 1199 char *zOtherRepo; /* Filename of the other repository */
a257fde… drh 1200 int rc; /* Result code from SQLite library functions */
a257fde… drh 1201 int nXfer = 0; /* Number of credentials transferred */
a257fde… drh 1202
45f3516… jan.nijtmans 1203 zOtherRepo = db_text(0,
a257fde… drh 1204 "SELECT value FROM config WHERE name='peer-repo-%q'",
a257fde… drh 1205 zCode
a257fde… drh 1206 );
a257fde… drh 1207 if( zOtherRepo==0 ) return 0; /* No such peer repository */
a257fde… drh 1208
19de4b5… mistachkin 1209 rc = sqlite3_open_v2(
19de4b5… mistachkin 1210 zOtherRepo, &pOther,
19de4b5… mistachkin 1211 SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
19de4b5… mistachkin 1212 g.zVfsName
19de4b5… mistachkin 1213 );
a257fde… drh 1214 if( rc==SQLITE_OK ){
2c95802… jan.nijtmans 1215 sqlite3_create_function(pOther,"now",0,SQLITE_UTF8,0,db_now_function,0,0);
d4a341b… dmitry 1216 sqlite3_create_function(pOther, "constant_time_cmp", 2, SQLITE_UTF8, 0,
b3e32c8… jan.nijtmans 1217 constant_time_cmp_function, 0, 0);
74ecc4d… drh 1218 sqlite3_busy_timeout(pOther, 5000);
a257fde… drh 1219 zSQL = mprintf(
a257fde… drh 1220 "SELECT cexpire FROM user"
d4a341b… dmitry 1221 " WHERE login=%Q"
604e1a6… drh 1222 " AND octet_length(cap)>0"
604e1a6… drh 1223 " AND octet_length(pw)>0"
d4a341b… dmitry 1224 " AND cexpire>julianday('now')"
d4a341b… dmitry 1225 " AND constant_time_cmp(cookie,%Q)=0",
7d18c40… drh 1226 zLogin, zHash
a257fde… drh 1227 );
a257fde… drh 1228 pStmt = 0;
a257fde… drh 1229 rc = sqlite3_prepare_v2(pOther, zSQL, -1, &pStmt, 0);
a257fde… drh 1230 if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
f741baa… drh 1231 db_unprotect(PROTECT_USER);
a257fde… drh 1232 db_multi_exec(
7d18c40… drh 1233 "UPDATE user SET cookie=%Q, cexpire=%.17g"
a257fde… drh 1234 " WHERE login=%Q",
275da70… danield 1235 zHash,
a257fde… drh 1236 sqlite3_column_double(pStmt, 0), zLogin
a257fde… drh 1237 );
f741baa… drh 1238 db_protect_pop();
a257fde… drh 1239 nXfer++;
a257fde… drh 1240 }
a257fde… drh 1241 sqlite3_finalize(pStmt);
a257fde… drh 1242 }
a257fde… drh 1243 sqlite3_close(pOther);
a257fde… drh 1244 fossil_free(zOtherRepo);
a257fde… drh 1245 return nXfer;
a257fde… drh 1246 }
a257fde… drh 1247
a257fde… drh 1248 /*
1b4b8a9… drh 1249 ** Return TRUE if zLogin is one of the special usernames
1b4b8a9… drh 1250 */
1b4b8a9… drh 1251 int login_is_special(const char *zLogin){
1b4b8a9… drh 1252 if( fossil_strcmp(zLogin, "anonymous")==0 ) return 1;
1b4b8a9… drh 1253 if( fossil_strcmp(zLogin, "nobody")==0 ) return 1;
1b4b8a9… drh 1254 if( fossil_strcmp(zLogin, "developer")==0 ) return 1;
1b4b8a9… drh 1255 if( fossil_strcmp(zLogin, "reader")==0 ) return 1;
1b4b8a9… drh 1256 return 0;
1b4b8a9… drh 1257 }
1b4b8a9… drh 1258
1b4b8a9… drh 1259 /*
7d18c40… drh 1260 ** Lookup the uid for a non-built-in user with zLogin and zCookie.
7d18c40… drh 1261 ** Return 0 if not found.
796dcfe… drh 1262 **
796dcfe… drh 1263 ** Note that this only searches for logged-in entries with matching
7d18c40… drh 1264 ** zCookie (db: user.cookie) entries.
a257fde… drh 1265 */
a257fde… drh 1266 static int login_find_user(
a257fde… drh 1267 const char *zLogin, /* User name */
7d18c40… drh 1268 const char *zCookie /* Login cookie value */
a257fde… drh 1269 ){
a257fde… drh 1270 int uid;
1b4b8a9… drh 1271 if( login_is_special(zLogin) ) return 0;
45f3516… jan.nijtmans 1272 uid = db_int(0,
a257fde… drh 1273 "SELECT uid FROM user"
a257fde… drh 1274 " WHERE login=%Q"
a257fde… drh 1275 " AND cexpire>julianday('now')"
604e1a6… drh 1276 " AND octet_length(cap)>0"
604e1a6… drh 1277 " AND octet_length(pw)>0"
d4a341b… dmitry 1278 " AND constant_time_cmp(cookie,%Q)=0",
7d18c40… drh 1279 zLogin, zCookie
a257fde… drh 1280 );
2e76b99… drh 1281 return uid;
2e76b99… drh 1282 }
2e76b99… drh 1283
2e76b99… drh 1284 /*
2e76b99… drh 1285 ** Attempt to use Basic Authentication to establish the user. Return the
2e76b99… drh 1286 ** (non-zero) uid if successful. Return 0 if it does not work.
2e76b99… drh 1287 */
c921545… drh 1288 static int login_basic_authentication(const char *zIpAddr){
2e76b99… drh 1289 const char *zAuth = PD("HTTP_AUTHORIZATION", 0);
2e76b99… drh 1290 int i;
2e76b99… drh 1291 int uid = 0;
2e76b99… drh 1292 int nDecode = 0;
2e76b99… drh 1293 char *zDecode = 0;
2e76b99… drh 1294 const char *zUsername = 0;
2e76b99… drh 1295 const char *zPasswd = 0;
2e76b99… drh 1296
cd11f92… drh 1297 if( zAuth==0 ) return 0; /* Fail: No Authentication: header */
2e76b99… drh 1298 while( fossil_isspace(zAuth[0]) ) zAuth++; /* Skip leading whitespace */
cd11f92… drh 1299 if( strncmp(zAuth, "Basic ", 6)!=0 ){
cd11f92… drh 1300 return 0; /* Fail: Not Basic Authentication */
cd11f92… drh 1301 }
2e76b99… drh 1302
2e76b99… drh 1303 /* Parse out the username and password, separated by a ":" */
2e76b99… drh 1304 zAuth += 6;
2e76b99… drh 1305 while( fossil_isspace(zAuth[0]) ) zAuth++;
2e76b99… drh 1306 zDecode = decode64(zAuth, &nDecode);
2e76b99… drh 1307
2e76b99… drh 1308 for(i=0; zDecode[i] && zDecode[i]!=':'; i++){}
2e76b99… drh 1309 if( zDecode[i] ){
2e76b99… drh 1310 zDecode[i] = 0;
2e76b99… drh 1311 zUsername = zDecode;
2e76b99… drh 1312 zPasswd = &zDecode[i+1];
2e76b99… drh 1313
2e76b99… drh 1314 /* Attempting to log in as the user provided by HTTP
2e76b99… drh 1315 ** basic auth
2e76b99… drh 1316 */
9b4e157… drh 1317 uid = login_search_uid(&zUsername, zPasswd);
2e76b99… drh 1318 if( uid>0 ){
2e76b99… drh 1319 record_login_attempt(zUsername, zIpAddr, 1);
2e76b99… drh 1320 }else{
2e76b99… drh 1321 record_login_attempt(zUsername, zIpAddr, 0);
2e76b99… drh 1322
2e76b99… drh 1323 /* The user attempted to login specifically with HTTP basic
2e76b99… drh 1324 ** auth, but provided invalid credentials. Inform them of
2e76b99… drh 1325 ** the failed login attempt via 401.
2e76b99… drh 1326 */
2e76b99… drh 1327 cgi_set_status(401, "Unauthorized");
2e76b99… drh 1328 cgi_reply();
2e76b99… drh 1329 fossil_exit(0);
2e76b99… drh 1330 }
2e76b99… drh 1331 }
2e76b99… drh 1332 fossil_free(zDecode);
a257fde… drh 1333 return uid;
a4e7b86… drh 1334 }
a4e7b86… drh 1335
a4e7b86… drh 1336 /*
66b111a… drh 1337 ** When this routine is called, we know that the request does not
66b111a… drh 1338 ** have a login on the present repository. This routine checks to
66b111a… drh 1339 ** see if their login cookie might be for another member of the
66b111a… drh 1340 ** login-group.
66b111a… drh 1341 **
66b111a… drh 1342 ** If this repository is not a part of any login group, then this
66b111a… drh 1343 ** routine always returns false.
66b111a… drh 1344 **
66b111a… drh 1345 ** If this repository is part of a login group, and the login cookie
66b111a… drh 1346 ** appears to be well-formed, then return true. That might be a
66b111a… drh 1347 ** false-positive, as we don't actually check to see if the login
66b111a… drh 1348 ** cookie is valid for some other repository. But false-positives
66b111a… drh 1349 ** are ok. This routine is used for robot defense only.
66b111a… drh 1350 */
66b111a… drh 1351 int login_cookie_wellformed(void){
66b111a… drh 1352 const char *zCookie;
66b111a… drh 1353 int n;
66b111a… drh 1354 zCookie = P(login_cookie_name());
66b111a… drh 1355 if( zCookie==0 ){
66b111a… drh 1356 return 0;
66b111a… drh 1357 }
66b111a… drh 1358 if( !db_exists("SELECT 1 FROM config WHERE name='login-group-code'") ){
66b111a… drh 1359 return 0;
66b111a… drh 1360 }
66b111a… drh 1361 for(n=0; fossil_isXdigit(zCookie[n]); n++){}
66b111a… drh 1362 return n>48 && zCookie[n]=='/' && zCookie[n+1]!=0;
66b111a… drh 1363 }
1a0b304… drh 1364
1a0b304… drh 1365 /*
db0c512… drh 1366 ** This routine examines the login cookie to see if it exists and
796dcfe… drh 1367 ** is valid. If the login cookie checks out, it then sets global
e065d5b… drh 1368 ** variables appropriately.
79ef961… drh 1369 **
e065d5b… drh 1370 ** g.userUid Database USER.UID value. Might be -1 for "nobody"
e065d5b… drh 1371 ** g.zLogin Database USER.LOGIN value. NULL for user "nobody"
e065d5b… drh 1372 ** g.perm Permissions granted to this user
653dd40… drh 1373 ** g.anon Permissions that would be available to anonymous
16b3309… drh 1374 ** g.isRobot True if the client is known to be a spider or robot
9413395… drh 1375 ** g.perm Populated based on user account's capabilities
10006db… drh 1376 ** g.eAuthMethod The mechanism used for authentication
e065d5b… drh 1377 **
dbda8d6… drh 1378 */
dbda8d6… drh 1379 void login_check_credentials(void){
0be5482… drh 1380 int uid = 0; /* User id */
0be5482… drh 1381 const char *zCookie; /* Text of the login cookie */
e720f11… drh 1382 const char *zIpAddr; /* Raw IP address of the requestor */
0be5482… drh 1383 const char *zCap = 0; /* Capability string */
1460b74… andybradford 1384 const char *zLogin = 0; /* Login user for credentials */
dbda8d6… drh 1385
dbda8d6… drh 1386 /* Only run this check once. */
9c952d2… drh 1387 if( g.userUid!=0 ) return;
dbda8d6… drh 1388
d4a341b… dmitry 1389 sqlite3_create_function(g.db, "constant_time_cmp", 2, SQLITE_UTF8, 0,
b3e32c8… jan.nijtmans 1390 constant_time_cmp_function, 0, 0);
d4a341b… dmitry 1391
dbda8d6… drh 1392 /* If the HTTP connection is coming over 127.0.0.1 and if
45f3516… jan.nijtmans 1393 ** local login is disabled and if we are using HTTP and not HTTPS,
3da8a12… drh 1394 ** then there is no need to check user credentials.
3da8a12… drh 1395 **
a257fde… drh 1396 ** This feature allows the "fossil ui" command to give the user
a257fde… drh 1397 ** full access rights without having to log in.
dbda8d6… drh 1398 */
7d18c40… drh 1399 zIpAddr = PD("REMOTE_ADDR","nil");
96dcb7e… drh 1400 if( ( cgi_is_loopback(zIpAddr)
96dcb7e… drh 1401 || (g.fSshClient & CGI_SSH_CLIENT)!=0 )
f7a3c6d… drh 1402 && g.useLocalauth
00638d9… drh 1403 && db_get_boolean("localauth",0)==0
3da8a12… drh 1404 && P("HTTPS")==0
3da8a12… drh 1405 ){
920ace1… drh 1406 char *zSeed;
1460b74… andybradford 1407 if( g.localOpen ) zLogin = db_lget("default-user",0);
1460b74… andybradford 1408 if( zLogin!=0 ){
1460b74… andybradford 1409 uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zLogin);
1460b74… andybradford 1410 }else{
1460b74… andybradford 1411 uid = db_int(0, "SELECT uid FROM user WHERE cap LIKE '%%s%%'");
1460b74… andybradford 1412 }
dbda8d6… drh 1413 g.zLogin = db_text("?", "SELECT login FROM user WHERE uid=%d", uid);
b11359c… drh 1414 zCap = "sxy";
dbda8d6… drh 1415 g.noPswd = 1;
16b3309… drh 1416 g.isRobot = 0;
10006db… drh 1417 g.eAuthMethod = AUTH_LOCAL;
920ace1… drh 1418 zSeed = db_text("??", "SELECT uid||quote(login)||quote(pw)||quote(cookie)"
920ace1… drh 1419 " FROM user WHERE uid=%d", uid);
920ace1… drh 1420 login_create_csrf_secret(zSeed);
920ace1… drh 1421 fossil_free(zSeed);
dbda8d6… drh 1422 }
dbda8d6… drh 1423
dbda8d6… drh 1424 /* Check the login cookie to see if it matches a known valid user.
dbda8d6… drh 1425 */
9c952d2… drh 1426 if( uid==0 && (zCookie = P(login_cookie_name()))!=0 ){
a257fde… drh 1427 /* Parse the cookie value up into HASH/ARG/USER */
a257fde… drh 1428 char *zHash = fossil_strdup(zCookie);
a257fde… drh 1429 char *zArg = 0;
a257fde… drh 1430 char *zUser = 0;
a257fde… drh 1431 int i, c;
a257fde… drh 1432 for(i=0; (c = zHash[i])!=0; i++){
a257fde… drh 1433 if( c=='/' ){
a257fde… drh 1434 zHash[i++] = 0;
a257fde… drh 1435 if( zArg==0 ){
a257fde… drh 1436 zArg = &zHash[i];
a257fde… drh 1437 }else{
a257fde… drh 1438 zUser = &zHash[i];
a257fde… drh 1439 break;
a257fde… drh 1440 }
a257fde… drh 1441 }
a257fde… drh 1442 }
a257fde… drh 1443 if( zUser==0 ){
a257fde… drh 1444 /* Invalid cookie */
7d2b47a… drh 1445 }else if( fossil_strcmp(zUser, "anonymous")==0
7d2b47a… drh 1446 && anon_cookie_lifespan()>0 ){
68da478… drh 1447 /* Cookies of the form "HASH/TIME/anonymous". The TIME must
68da478… drh 1448 ** not be more than ANONYMOUS_COOKIE_LIFESPAN seconds ago and
0693766… drh 1449 ** the sha1 hash of TIME/USERAGENT/SECRET must match HASH. USERAGENT
0693766… drh 1450 ** is the HTTP_USER_AGENT of the client and SECRET is the
68da478… drh 1451 ** "captcha-secret" value in the repository. See tag-20250817a
68da478… drh 1452 ** for the code the creates this cookie.
6021279… drh 1453 */
a10f931… drh 1454 double rTime = atof(zArg);
0693766… drh 1455 const char *zUserAgent = PD("HTTP_USER_AGENT","nil");
6021279… drh 1456 Blob b;
8659d84… drh 1457 char *zSecret;
8659d84… drh 1458 int n = 0;
8659d84… drh 1459
8659d84… drh 1460 do{
8659d84… drh 1461 blob_zero(&b);
8659d84… drh 1462 zSecret = captcha_secret(n++);
8659d84… drh 1463 if( zSecret==0 ) break;
0693766… drh 1464 blob_appendf(&b, "%s/%s/%s", zArg, zUserAgent, zSecret);
8659d84… drh 1465 sha1sum_blob(&b, &b);
8659d84… drh 1466 if( fossil_strcmp(zHash, blob_str(&b))==0 ){
8659d84… drh 1467 uid = db_int(0,
8659d84… drh 1468 "SELECT uid FROM user WHERE login='anonymous'"
8659d84… drh 1469 " AND octet_length(cap)>0"
8659d84… drh 1470 " AND octet_length(pw)>0"
68da478… drh 1471 " AND %.17g>julianday('now')",
7d2b47a… drh 1472 rTime+anon_cookie_lifespan()/1440.0
8659d84… drh 1473 );
8659d84… drh 1474 }
8659d84… drh 1475 }while( uid==0 );
6021279… drh 1476 blob_reset(&b);
a257fde… drh 1477 }else{
a257fde… drh 1478 /* Cookies of the form "HASH/CODE/USER". Search first in the
a257fde… drh 1479 ** local user table, then the user table for project CODE if we
a257fde… drh 1480 ** are part of a login-group.
a257fde… drh 1481 */
7d18c40… drh 1482 uid = login_find_user(zUser, zHash);
7d18c40… drh 1483 if( uid==0 && login_transfer_credentials(zUser,zArg,zHash) ){
7d18c40… drh 1484 uid = login_find_user(zUser, zHash);
7df48cb… drh 1485 if( uid ){
7df48cb… drh 1486 record_login_attempt(zUser, zIpAddr, 1);
7df48cb… drh 1487 }else{
7df48cb… drh 1488 /* The login cookie is a valid login for project CODE, but no
7df48cb… drh 1489 ** user named USER exists on this repository. Cannot login as
7df48cb… drh 1490 ** USER, but at least give them "anonymous" login. */
d6bbf55… drh 1491 uid = db_int(0, "SELECT uid FROM user WHERE login='anonymous'"
d6bbf55… drh 1492 " AND octet_length(cap)>0"
d6bbf55… drh 1493 " AND octet_length(pw)>0");
7df48cb… drh 1494 }
a257fde… drh 1495 }
6454153… drh 1496 }
10006db… drh 1497 if( uid ) g.eAuthMethod = AUTH_COOKIE;
920ace1… drh 1498 login_create_csrf_secret(zHash);
6454153… drh 1499 }
6454153… drh 1500
6454153… drh 1501 /* If no user found and the REMOTE_USER environment variable is set,
796dcfe… drh 1502 ** then accept the value of REMOTE_USER as the user.
6454153… drh 1503 */
6454153… drh 1504 if( uid==0 ){
6454153… drh 1505 const char *zRemoteUser = P("REMOTE_USER");
6454153… drh 1506 if( zRemoteUser && db_get_boolean("remote_user_ok",0) ){
6454153… drh 1507 uid = db_int(0, "SELECT uid FROM user WHERE login=%Q"
604e1a6… drh 1508 " AND octet_length(cap)>0 AND octet_length(pw)>0",
604e1a6… drh 1509 zRemoteUser);
10006db… drh 1510 if( uid ) g.eAuthMethod = AUTH_ENV;
315cf24… drh 1511 }
315cf24… drh 1512 }
315cf24… drh 1513
315cf24… drh 1514 /* If the request didn't provide a login cookie or the login cookie didn't
315cf24… drh 1515 ** match a known valid user, check the HTTP "Authorization" header and
315cf24… drh 1516 ** see if those credentials are valid for a known user.
315cf24… drh 1517 */
2e76b99… drh 1518 if( uid==0 && db_get_boolean("http_authentication_ok",0) ){
c921545… drh 1519 uid = login_basic_authentication(zIpAddr);
10006db… drh 1520 if( uid ) g.eAuthMethod = AUTH_HTTP;
1e81049… drh 1521 }
1e81049… drh 1522
1e81049… drh 1523 /* Check for magic query parameters "resid" (for the username) and
1e81049… drh 1524 ** "token" for the password. Both values (if they exist) will be
1e81049… drh 1525 ** obfuscated.
1e81049… drh 1526 */
1e81049… drh 1527 if( uid==0 ){
1e81049… drh 1528 char *zUsr, *zPW;
1e81049… drh 1529 if( (zUsr = unobscure(P("resid")))!=0
1e81049… drh 1530 && (zPW = unobscure(P("token")))!=0
1e81049… drh 1531 ){
1e81049… drh 1532 char *zSha1Pw = sha1_shared_secret(zPW, zUsr, 0);
1e81049… drh 1533 uid = db_int(0, "SELECT uid FROM user"
1e81049… drh 1534 " WHERE login=%Q"
1e81049… drh 1535 " AND (constant_time_cmp(pw,%Q)=0"
1e81049… drh 1536 " OR constant_time_cmp(pw,%Q)=0)",
1e81049… drh 1537 zUsr, zSha1Pw, zPW);
1e81049… drh 1538 fossil_free(zSha1Pw);
10006db… drh 1539 if( uid ) g.eAuthMethod = AUTH_PW;
1e81049… drh 1540 }
6021279… drh 1541 }
6021279… drh 1542
6021279… drh 1543 /* If no user found yet, try to log in as "nobody" */
6021279… drh 1544 if( uid==0 ){
6021279… drh 1545 uid = db_int(0, "SELECT uid FROM user WHERE login='nobody'");
6021279… drh 1546 if( uid==0 ){
6021279… drh 1547 /* If there is no user "nobody", then make one up - with no privileges */
6021279… drh 1548 uid = -1;
6021279… drh 1549 zCap = "";
6021279… drh 1550 }
920ace1… drh 1551 login_create_csrf_secret("none");
07bfe3f… drh 1552 }
07bfe3f… drh 1553
07bfe3f… drh 1554 login_set_uid(uid, zCap);
1a0b304… drh 1555
16b3309… drh 1556 /* Maybe restrict access by robots */
16b3309… drh 1557 if( g.zLogin==0 && robot_restrict(g.zPath) ){
16b3309… drh 1558 cgi_reply();
16b3309… drh 1559 fossil_exit(0);
16b3309… drh 1560 }
07bfe3f… drh 1561 }
07bfe3f… drh 1562
07bfe3f… drh 1563 /*
07bfe3f… drh 1564 ** Set the current logged in user to be uid. zCap is precomputed
07bfe3f… drh 1565 ** (override) capabilities. If zCap==0, then look up the capabilities
07bfe3f… drh 1566 ** in the USER table.
07bfe3f… drh 1567 */
07bfe3f… drh 1568 int login_set_uid(int uid, const char *zCap){
07bfe3f… drh 1569 const char *zPublicPages = 0; /* GLOB patterns of public pages */
6021279… drh 1570
6021279… drh 1571 /* At this point, we know that uid!=0. Find the privileges associated
6021279… drh 1572 ** with user uid.
6021279… drh 1573 */
6021279… drh 1574 assert( uid!=0 );
6021279… drh 1575 if( zCap==0 ){
6021279… drh 1576 Stmt s;
6021279… drh 1577 db_prepare(&s, "SELECT login, cap FROM user WHERE uid=%d", uid);
6021279… drh 1578 if( db_step(&s)==SQLITE_ROW ){
6021279… drh 1579 g.zLogin = db_column_malloc(&s, 0);
6021279… drh 1580 zCap = db_column_malloc(&s, 1);
6021279… drh 1581 }
6021279… drh 1582 db_finalize(&s);
6021279… drh 1583 if( zCap==0 ){
6021279… drh 1584 zCap = "";
6021279… drh 1585 }
6021279… drh 1586 }
596f3c1… drh 1587 if( g.fHttpTrace && g.zLogin ){
596f3c1… drh 1588 fprintf(stderr, "# login: [%s] with capabilities [%s]\n", g.zLogin, zCap);
596f3c1… drh 1589 }
6021279… drh 1590
6021279… drh 1591 /* Set the global variables recording the userid and login. The
6021279… drh 1592 ** "nobody" user is a special case in that g.zLogin==0.
6021279… drh 1593 */
6021279… drh 1594 g.userUid = uid;
31c52c7… drh 1595 if( fossil_strcmp(g.zLogin,"nobody")==0 ){
6021279… drh 1596 g.zLogin = 0;
31c52c7… drh 1597 }
abcd5df… drh 1598 if( PB("isrobot") ){
16b3309… drh 1599 g.isRobot = 1;
abcd5df… drh 1600 }else if( g.zLogin==0 ){
16b3309… drh 1601 g.isRobot = !isHuman(P("HTTP_USER_AGENT"));
abcd5df… drh 1602 }else{
16b3309… drh 1603 g.isRobot = 0;
abcd5df… drh 1604 }
6021279… drh 1605
6021279… drh 1606 /* Set the capabilities */
796dcfe… drh 1607 login_replace_capabilities(zCap, 0);
96f3e83… drh 1608
96f3e83… drh 1609 /* The auto-hyperlink setting allows hyperlinks to be displayed for users
96f3e83… drh 1610 ** who do not have the "h" permission as long as their UserAgent string
96f3e83… drh 1611 ** makes it appear that they are human. Check to see if auto-hyperlink is
96f3e83… drh 1612 ** enabled for this repository and make appropriate adjustments to the
a2730fe… drh 1613 ** permission flags if it is. This should be done before the permissions
a2730fe… drh 1614 ** are (potentially) copied to the anonymous permission set; otherwise,
a2730fe… drh 1615 ** those will be out-of-sync.
a2730fe… drh 1616 */
16b3309… drh 1617 if( zCap[0] && !g.perm.Hyperlink && !g.isRobot ){
df337eb… drh 1618 int autoLink = db_get_int("auto-hyperlink",1);
df337eb… drh 1619 if( autoLink==1 ){
df337eb… drh 1620 g.jsHref = 1;
df337eb… drh 1621 g.perm.Hyperlink = 1;
df337eb… drh 1622 }else if( autoLink==2 ){
df337eb… drh 1623 g.perm.Hyperlink = 1;
df337eb… drh 1624 }
a2730fe… drh 1625 }
a2730fe… drh 1626
a2730fe… drh 1627 /*
a2730fe… drh 1628 ** At this point, the capabilities for the logged in user are not going
a2730fe… drh 1629 ** to be modified anymore; therefore, we can copy them over to the ones
a2730fe… drh 1630 ** for the anonymous user.
a2730fe… drh 1631 **
a2730fe… drh 1632 ** WARNING: In the future, please do not add code after this point that
a2730fe… drh 1633 ** modifies the capabilities for the logged in user.
96f3e83… drh 1634 */
a2730fe… drh 1635 login_set_anon_nobody_capabilities();
79ef961… drh 1636
79ef961… drh 1637 /* If the public-pages glob pattern is defined and REQUEST_URI matches
79ef961… drh 1638 ** one of the globs in public-pages, then also add in all default-perms
79ef961… drh 1639 ** permissions.
79ef961… drh 1640 */
79ef961… drh 1641 zPublicPages = db_get("public-pages",0);
79ef961… drh 1642 if( zPublicPages!=0 ){
d6cd147… drh 1643 const char *zUri = PD("REQUEST_URI","");
d6cd147… drh 1644 zUri += (int)strlen(g.zTop);
dc86831… drh 1645 if( glob_multi_match(zPublicPages, zUri) ){
c00e912… drh 1646 login_set_capabilities(db_get("default-perms", "u"), 0);
79ef961… drh 1647 }
e059e5a… drh 1648 }
07bfe3f… drh 1649 return g.zLogin!=0;
6021279… drh 1650 }
6021279… drh 1651
6021279… drh 1652 /*
ab48825… drh 1653 ** Memory of settings
ab48825… drh 1654 */
ab48825… drh 1655 static int login_anon_once = 1;
ab48825… drh 1656
ab48825… drh 1657 /*
653dd40… drh 1658 ** Add to g.perm the default privileges of users "nobody" and/or "anonymous"
653dd40… drh 1659 ** as appropriate for the user g.zLogin.
653dd40… drh 1660 **
653dd40… drh 1661 ** This routine also sets up g.anon to be either a copy of g.perm for
653dd40… drh 1662 ** all logged in uses, or the privileges that would be available to "anonymous"
653dd40… drh 1663 ** if g.zLogin==0 (meaning that the user is "nobody").
6021279… drh 1664 */
6021279… drh 1665 void login_set_anon_nobody_capabilities(void){
653dd40… drh 1666 if( login_anon_once ){
6021279… drh 1667 const char *zCap;
653dd40… drh 1668 /* All users get privileges from "nobody" */
6021279… drh 1669 zCap = db_text("", "SELECT cap FROM user WHERE login = 'nobody'");
3bd2de4… drh 1670 login_set_capabilities(zCap, 0);
653dd40… drh 1671 zCap = db_text("", "SELECT cap FROM user WHERE login = 'anonymous'");
653dd40… drh 1672 if( g.zLogin && fossil_strcmp(g.zLogin, "nobody")!=0 ){
6021279… drh 1673 /* All logged-in users inherit privileges from "anonymous" */
3bd2de4… drh 1674 login_set_capabilities(zCap, 0);
653dd40… drh 1675 g.anon = g.perm;
653dd40… drh 1676 }else{
653dd40… drh 1677 /* Record the privileges of anonymous in g.anon */
653dd40… drh 1678 g.anon = g.perm;
653dd40… drh 1679 login_set_capabilities(zCap, LOGIN_ANON);
ab48825… drh 1680 }
ab48825… drh 1681 login_anon_once = 0;
ab48825… drh 1682 }
ab48825… drh 1683 }
ab48825… drh 1684
ab48825… drh 1685 /*
796dcfe… drh 1686 ** Flags passed into the 2nd argument of login_set/replace_capabilities().
3bd2de4… drh 1687 */
3bd2de4… drh 1688 #if INTERFACE
49546c5… drh 1689 #define LOGIN_IGNORE_UV 0x01 /* Ignore "u" and "v" */
653dd40… drh 1690 #define LOGIN_ANON 0x02 /* Use g.anon instead of g.perm */
3bd2de4… drh 1691 #endif
3bd2de4… drh 1692
3bd2de4… drh 1693 /*
653dd40… drh 1694 ** Adds all capability flags in zCap to g.perm or g.anon.
ab48825… drh 1695 */
3bd2de4… drh 1696 void login_set_capabilities(const char *zCap, unsigned flags){
6021279… drh 1697 int i;
653dd40… drh 1698 FossilUserPerms *p = (flags & LOGIN_ANON) ? &g.anon : &g.perm;
796dcfe… drh 1699 if(NULL==zCap){
796dcfe… drh 1700 return;
796dcfe… drh 1701 }
6021279… drh 1702 for(i=0; zCap[i]; i++){
6021279… drh 1703 switch( zCap[i] ){
b241130… mistachkin 1704 case 's': p->Setup = 1; /* Fall thru into Admin */
653dd40… drh 1705 case 'a': p->Admin = p->RdTkt = p->WrTkt = p->Zip =
b241130… mistachkin 1706 p->RdWiki = p->WrWiki = p->NewWiki =
b241130… mistachkin 1707 p->ApndWiki = p->Hyperlink = p->Clone =
b241130… mistachkin 1708 p->NewTkt = p->Password = p->RdAddr =
b241130… mistachkin 1709 p->TktFmt = p->Attach = p->ApndTkt =
1274054… drh 1710 p->ModWiki = p->ModTkt =
9a2e5f4… drh 1711 p->RdForum = p->WrForum = p->ModForum =
275da70… danield 1712 p->WrTForum = p->AdminForum = p->Chat =
a6ffdaf… wyoung 1713 p->EmailAlert = p->Announce = p->Debug = 1;
b241130… mistachkin 1714 /* Fall thru into Read/Write */
b241130… mistachkin 1715 case 'i': p->Read = p->Write = 1; break;
653dd40… drh 1716 case 'o': p->Read = 1; break;
653dd40… drh 1717 case 'z': p->Zip = 1; break;
653dd40… drh 1718
653dd40… drh 1719 case 'h': p->Hyperlink = 1; break;
653dd40… drh 1720 case 'g': p->Clone = 1; break;
653dd40… drh 1721 case 'p': p->Password = 1; break;
653dd40… drh 1722
653dd40… drh 1723 case 'j': p->RdWiki = 1; break;
b241130… mistachkin 1724 case 'k': p->WrWiki = p->RdWiki = p->ApndWiki =1; break;
653dd40… drh 1725 case 'm': p->ApndWiki = 1; break;
653dd40… drh 1726 case 'f': p->NewWiki = 1; break;
653dd40… drh 1727 case 'l': p->ModWiki = 1; break;
653dd40… drh 1728
653dd40… drh 1729 case 'e': p->RdAddr = 1; break;
653dd40… drh 1730 case 'r': p->RdTkt = 1; break;
653dd40… drh 1731 case 'n': p->NewTkt = 1; break;
653dd40… drh 1732 case 'w': p->WrTkt = p->RdTkt = p->NewTkt =
653dd40… drh 1733 p->ApndTkt = 1; break;
653dd40… drh 1734 case 'c': p->ApndTkt = 1; break;
653dd40… drh 1735 case 'q': p->ModTkt = 1; break;
653dd40… drh 1736 case 't': p->TktFmt = 1; break;
653dd40… drh 1737 case 'b': p->Attach = 1; break;
653dd40… drh 1738 case 'x': p->Private = 1; break;
27d743e… drh 1739 case 'y': p->WrUnver = 1; break;
27d743e… drh 1740
9a2e5f4… drh 1741 case '6': p->AdminForum = 1;
9a2e5f4… drh 1742 case '5': p->ModForum = 1;
9a2e5f4… drh 1743 case '4': p->WrTForum = 1;
9a2e5f4… drh 1744 case '3': p->WrForum = 1;
9a2e5f4… drh 1745 case '2': p->RdForum = 1; break;
9a2e5f4… drh 1746
25eafed… drh 1747 case '7': p->EmailAlert = 1; break;
65f5754… drh 1748 case 'A': p->Announce = 1; break;
e8ba89b… drh 1749 case 'C': p->Chat = 1; break;
fd31983… drh 1750 case 'D': p->Debug = 1; break;
25eafed… drh 1751
397d23c… drh 1752 /* The "u" privilege recursively
6021279… drh 1753 ** inherits all privileges of the user named "reader" */
6021279… drh 1754 case 'u': {
397d23c… drh 1755 if( p->XReader==0 ){
3bd2de4… drh 1756 const char *zUser;
397d23c… drh 1757 p->XReader = 1;
6021279… drh 1758 zUser = db_text("", "SELECT cap FROM user WHERE login='reader'");
397d23c… drh 1759 login_set_capabilities(zUser, flags);
6021279… drh 1760 }
6021279… drh 1761 break;
6021279… drh 1762 }
6021279… drh 1763
397d23c… drh 1764 /* The "v" privilege recursively
6021279… drh 1765 ** inherits all privileges of the user named "developer" */
6021279… drh 1766 case 'v': {
397d23c… drh 1767 if( p->XDeveloper==0 ){
3bd2de4… drh 1768 const char *zDev;
397d23c… drh 1769 p->XDeveloper = 1;
6021279… drh 1770 zDev = db_text("", "SELECT cap FROM user WHERE login='developer'");
397d23c… drh 1771 login_set_capabilities(zDev, flags);
1f1d965… drh 1772 }
1f1d965… drh 1773 break;
1f1d965… drh 1774 }
1f1d965… drh 1775 }
1f1d965… drh 1776 }
1f1d965… drh 1777 }
1f1d965… drh 1778
1f1d965… drh 1779 /*
796dcfe… drh 1780 ** Zeroes out g.perm and calls login_set_capabilities(zCap,flags).
796dcfe… drh 1781 */
796dcfe… drh 1782 void login_replace_capabilities(const char *zCap, unsigned flags){
796dcfe… drh 1783 memset(&g.perm, 0, sizeof(g.perm));
24e298e… mistachkin 1784 login_set_capabilities(zCap, flags);
b1ffbfa… drh 1785 login_anon_once = 1;
796dcfe… drh 1786 }
796dcfe… drh 1787
796dcfe… drh 1788 /*
d0305b3… aku 1789 ** If the current login lacks any of the capabilities listed in
d0305b3… aku 1790 ** the input, then return 0. If all capabilities are present, then
d0305b3… aku 1791 ** return 1.
e5240c9… stephan 1792 **
e5240c9… stephan 1793 ** As a special case, the 'L' pseudo-capability ID means "is logged
e5240c9… stephan 1794 ** in" and will return true for any non-guest user.
d0305b3… aku 1795 */
653dd40… drh 1796 int login_has_capability(const char *zCap, int nCap, u32 flgs){
d0305b3… aku 1797 int i;
d0305b3… aku 1798 int rc = 1;
653dd40… drh 1799 FossilUserPerms *p = (flgs & LOGIN_ANON) ? &g.anon : &g.perm;
d0305b3… aku 1800 if( nCap<0 ) nCap = strlen(zCap);
d0305b3… aku 1801 for(i=0; i<nCap && rc && zCap[i]; i++){
d0305b3… aku 1802 switch( zCap[i] ){
653dd40… drh 1803 case 'a': rc = p->Admin; break;
653dd40… drh 1804 case 'b': rc = p->Attach; break;
653dd40… drh 1805 case 'c': rc = p->ApndTkt; break;
1274054… drh 1806 /* d unused: see comment in capabilities.c */
653dd40… drh 1807 case 'e': rc = p->RdAddr; break;
653dd40… drh 1808 case 'f': rc = p->NewWiki; break;
653dd40… drh 1809 case 'g': rc = p->Clone; break;
653dd40… drh 1810 case 'h': rc = p->Hyperlink; break;
653dd40… drh 1811 case 'i': rc = p->Write; break;
653dd40… drh 1812 case 'j': rc = p->RdWiki; break;
653dd40… drh 1813 case 'k': rc = p->WrWiki; break;
653dd40… drh 1814 case 'l': rc = p->ModWiki; break;
653dd40… drh 1815 case 'm': rc = p->ApndWiki; break;
653dd40… drh 1816 case 'n': rc = p->NewTkt; break;
653dd40… drh 1817 case 'o': rc = p->Read; break;
653dd40… drh 1818 case 'p': rc = p->Password; break;
653dd40… drh 1819 case 'q': rc = p->ModTkt; break;
653dd40… drh 1820 case 'r': rc = p->RdTkt; break;
653dd40… drh 1821 case 's': rc = p->Setup; break;
653dd40… drh 1822 case 't': rc = p->TktFmt; break;
355ee47… drh 1823 /* case 'u': READER */
355ee47… drh 1824 /* case 'v': DEVELOPER */
653dd40… drh 1825 case 'w': rc = p->WrTkt; break;
653dd40… drh 1826 case 'x': rc = p->Private; break;
1f8a667… mistachkin 1827 case 'y': rc = p->WrUnver; break;
653dd40… drh 1828 case 'z': rc = p->Zip; break;
65f5754… drh 1829 case '2': rc = p->RdForum; break;
65f5754… drh 1830 case '3': rc = p->WrForum; break;
65f5754… drh 1831 case '4': rc = p->WrTForum; break;
65f5754… drh 1832 case '5': rc = p->ModForum; break;
65f5754… drh 1833 case '6': rc = p->AdminForum;break;
65f5754… drh 1834 case '7': rc = p->EmailAlert;break;
65f5754… drh 1835 case 'A': rc = p->Announce; break;
e8ba89b… drh 1836 case 'C': rc = p->Chat; break;
fd31983… drh 1837 case 'D': rc = p->Debug; break;
e5240c9… stephan 1838 case 'L': rc = g.zLogin && *g.zLogin; break;
e5240c9… stephan 1839 /* Mainenance reminder: '@' should not be used because
e5240c9… stephan 1840 it would semantically collide with the @ in the
e5240c9… stephan 1841 capexpr TH1 command. */
b241130… mistachkin 1842 default: rc = 0; break;
d0305b3… aku 1843 }
d0305b3… aku 1844 }
d0305b3… aku 1845 return rc;
0be5482… drh 1846 }
0be5482… drh 1847
0be5482… drh 1848 /*
ab48825… drh 1849 ** Change the login to zUser.
ab48825… drh 1850 */
ab48825… drh 1851 void login_as_user(const char *zUser){
ab48825… drh 1852 char *zCap = ""; /* New capabilities */
ab48825… drh 1853
ab48825… drh 1854 /* Turn off all capabilities from prior logins */
b344d3c… drh 1855 memset( &g.perm, 0, sizeof(g.perm) );
ab48825… drh 1856
ab48825… drh 1857 /* Set the global variables recording the userid and login. The
ab48825… drh 1858 ** "nobody" user is a special case in that g.zLogin==0.
ab48825… drh 1859 */
ab48825… drh 1860 g.userUid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zUser);
ab48825… drh 1861 if( g.userUid==0 ){
ab48825… drh 1862 zUser = 0;
ab48825… drh 1863 g.userUid = db_int(0, "SELECT uid FROM user WHERE login='nobody'");
ab48825… drh 1864 }
ab48825… drh 1865 if( g.userUid ){
ab48825… drh 1866 zCap = db_text("", "SELECT cap FROM user WHERE uid=%d", g.userUid);
ab48825… drh 1867 }
ab48825… drh 1868 if( fossil_strcmp(zUser,"nobody")==0 ) zUser = 0;
ab48825… drh 1869 g.zLogin = fossil_strdup(zUser);
ab48825… drh 1870
ab48825… drh 1871 /* Set the capabilities */
3bd2de4… drh 1872 login_set_capabilities(zCap, 0);
ab48825… drh 1873 login_anon_once = 1;
ab48825… drh 1874 login_set_anon_nobody_capabilities();
3bd2de4… drh 1875 }
3bd2de4… drh 1876
3bd2de4… drh 1877 /*
840b762… drh 1878 ** Return true if the user is "nobody"
840b762… drh 1879 */
840b762… drh 1880 int login_is_nobody(void){
840b762… drh 1881 return g.zLogin==0 || g.zLogin[0]==0 || fossil_strcmp(g.zLogin,"nobody")==0;
840b762… drh 1882 }
840b762… drh 1883
840b762… drh 1884 /*
b77f1aa… drh 1885 ** Return true if the user is a specific individual, not "nobody" or
b77f1aa… drh 1886 ** "anonymous".
b77f1aa… drh 1887 */
b77f1aa… drh 1888 int login_is_individual(void){
b77f1aa… drh 1889 return g.zLogin!=0 && g.zLogin[0]!=0 && fossil_strcmp(g.zLogin,"nobody")!=0
b77f1aa… drh 1890 && fossil_strcmp(g.zLogin,"anonymous")!=0;
b77f1aa… drh 1891 }
b77f1aa… drh 1892
b77f1aa… drh 1893 /*
840b762… drh 1894 ** Return the login name. If no login name is specified, return "nobody".
840b762… drh 1895 */
840b762… drh 1896 const char *login_name(void){
840b762… drh 1897 return (g.zLogin && g.zLogin[0]) ? g.zLogin : "nobody";
840b762… drh 1898 }
840b762… drh 1899
840b762… drh 1900 /*
dbda8d6… drh 1901 ** Call this routine when the credential check fails. It causes
dbda8d6… drh 1902 ** a redirect to the "login" page.
dbda8d6… drh 1903 */
653dd40… drh 1904 void login_needed(int anonOk){
796dcfe… drh 1905 #ifdef FOSSIL_ENABLE_JSON
796dcfe… drh 1906 if(g.json.isJsonMode){
796dcfe… drh 1907 json_err( FSL_JSON_E_DENIED, NULL, 1 );
796dcfe… drh 1908 fossil_exit(0);
796dcfe… drh 1909 /* NOTREACHED */
796dcfe… drh 1910 assert(0);
796dcfe… drh 1911 }else
796dcfe… drh 1912 #endif /* FOSSIL_ENABLE_JSON */
796dcfe… drh 1913 {
653dd40… drh 1914 const char *zQS = P("QUERY_STRING");
3571c87… drh 1915 const char *zPathInfo = PD("PATH_INFO","");
653dd40… drh 1916 Blob redir;
653dd40… drh 1917 blob_init(&redir, 0, 0);
129ea22… mistachkin 1918 if( zPathInfo[0]=='/' ) zPathInfo++; /* skip leading slash */
4aba9ea… drh 1919 if( fossil_wants_https(1) ){
3571c87… drh 1920 blob_appendf(&redir, "%s/login?g=%T", g.zHttpsURL, zPathInfo);
653dd40… drh 1921 }else{
3571c87… drh 1922 blob_appendf(&redir, "%R/login?g=%T", zPathInfo);
653dd40… drh 1923 }
653dd40… drh 1924 if( zQS && zQS[0] ){
8d3d39f… drh 1925 blob_appendf(&redir, "%%3f%T", zQS);
653dd40… drh 1926 }
b873148… florian 1927 if( anonOk ) blob_append(&redir, "&anon=1", 7);
653dd40… drh 1928 cgi_redirect(blob_str(&redir));
796dcfe… drh 1929 /* NOTREACHED */
796dcfe… drh 1930 assert(0);
796dcfe… drh 1931 }
2b0d451… drh 1932 }
2b0d451… drh 1933
2b0d451… drh 1934 /*
433cde1… drh 1935 ** Call this routine if the user lacks g.perm.Hyperlink permission. If
e2bdc10… danield 1936 ** the anonymous user has Hyperlink permission, then paint a message
2b0d451… drh 1937 ** to inform the user that much more information is available by
2b0d451… drh 1938 ** logging in as anonymous.
2b0d451… drh 1939 */
2b0d451… drh 1940 void login_anonymous_available(void){
7d2b47a… drh 1941 if( !g.perm.Hyperlink && g.anon.Hyperlink && anon_cookie_lifespan()>0 ){
85f87c8… drh 1942 const char *zUrl = PD("PATH_INFO", "");
f5482a0… wyoung 1943 @ <p>Many <span class="disabled">hyperlinks are disabled.</span><br>
1fee037… drh 1944 @ Use <a href="%R/login?anon=1&amp;g=%T(zUrl)">anonymous login</a>
d57de28… drh 1945 @ to enable hyperlinks.</p>
d57de28… drh 1946 }
0be5482… drh 1947 }
0be5482… drh 1948
0be5482… drh 1949 /*
0be5482… drh 1950 ** While rendering a form, call this routine to add the Anti-CSRF token
0be5482… drh 1951 ** as a hidden element of the form.
0be5482… drh 1952 */
0be5482… drh 1953 void login_insert_csrf_secret(void){
f5482a0… wyoung 1954 @ <input type="hidden" name="csrf" value="%s(g.zCsrfToken)">
b13b651… drh 1955 }
b13b651… drh 1956
b13b651… drh 1957 /*
b13b651… drh 1958 ** Check to see if the candidate username zUserID is already used.
275da70… danield 1959 ** Return 1 if it is already in use. Return 0 if the name is
e2bdc10… danield 1960 ** available for a self-registration.
b13b651… drh 1961 */
d425d23… danield 1962 static int login_self_chosen_userid_already_exists(const char *zUserID){
b13b651… drh 1963 int rc = db_exists(
b13b651… drh 1964 "SELECT 1 FROM user WHERE login=%Q "
b13b651… drh 1965 "UNION ALL "
b13b651… drh 1966 "SELECT 1 FROM event WHERE user=%Q OR euser=%Q",
b13b651… drh 1967 zUserID, zUserID, zUserID
b13b651… drh 1968 );
b13b651… drh 1969 return rc;
c00e912… drh 1970 }
07bfe3f… drh 1971
07bfe3f… drh 1972 /*
07bfe3f… drh 1973 ** zEMail is an email address. (Example: "[email protected]".) This routine
07bfe3f… drh 1974 ** searches for a user or subscriber that has that email address. If the
07bfe3f… drh 1975 ** email address is used no-where in the system, return 0. If the email
07bfe3f… drh 1976 ** address is assigned to a particular user return the UID for that user.
07bfe3f… drh 1977 ** If the email address is used, but not by a particular user, return -1.
07bfe3f… drh 1978 */
07bfe3f… drh 1979 static int email_address_in_use(const char *zEMail){
07bfe3f… drh 1980 int uid;
275da70… danield 1981 uid = db_int(0,
07bfe3f… drh 1982 "SELECT uid FROM user"
07bfe3f… drh 1983 " WHERE info LIKE '%%<%q>%%'", zEMail);
07bfe3f… drh 1984 if( uid>0 ){
07bfe3f… drh 1985 if( db_exists("SELECT 1 FROM user WHERE uid=%d AND ("
07bfe3f… drh 1986 " cap GLOB '*[as]*' OR"
07bfe3f… drh 1987 " find_emailaddr(info)<>%Q COLLATE nocase)",
07bfe3f… drh 1988 uid, zEMail) ){
07bfe3f… drh 1989 uid = -1;
07bfe3f… drh 1990 }
07bfe3f… drh 1991 }
07bfe3f… drh 1992 if( uid==0 && alert_tables_exist() ){
07bfe3f… drh 1993 uid = db_int(0,
07bfe3f… drh 1994 "SELECT user.uid FROM subscriber JOIN user ON login=suname"
07bfe3f… drh 1995 " WHERE semail=%Q AND sverified", zEMail);
07bfe3f… drh 1996 if( uid ){
07bfe3f… drh 1997 if( db_exists("SELECT 1 FROM user WHERE uid=%d AND "
07bfe3f… drh 1998 " cap GLOB '*[as]*'",
07bfe3f… drh 1999 uid) ){
07bfe3f… drh 2000 uid = -1;
07bfe3f… drh 2001 }
07bfe3f… drh 2002 }
07bfe3f… drh 2003 }
07bfe3f… drh 2004 return uid;
07bfe3f… drh 2005 }
07bfe3f… drh 2006
07bfe3f… drh 2007 /*
07bfe3f… drh 2008 ** COMMAND: test-email-used
07bfe3f… drh 2009 ** Usage: fossil test-email-used EMAIL ...
275da70… danield 2010 **
07bfe3f… drh 2011 ** Given a list of email addresses, show the UID and LOGIN associated
07bfe3f… drh 2012 ** with each one.
07bfe3f… drh 2013 */
07bfe3f… drh 2014 void test_email_used(void){
07bfe3f… drh 2015 int i;
07bfe3f… drh 2016 db_find_and_open_repository(0, 0);
07bfe3f… drh 2017 verify_all_options();
07bfe3f… drh 2018 if( g.argc<3 ){
07bfe3f… drh 2019 usage("EMAIL ...");
07bfe3f… drh 2020 }
07bfe3f… drh 2021 for(i=2; i<g.argc; i++){
07bfe3f… drh 2022 const char *zEMail = g.argv[i];
07bfe3f… drh 2023 int uid = email_address_in_use(zEMail);
07bfe3f… drh 2024 if( uid==0 ){
07bfe3f… drh 2025 fossil_print("%s: not used\n", zEMail);
07bfe3f… drh 2026 }else if( uid<0 ){
07bfe3f… drh 2027 fossil_print("%s: used but no password reset is available\n", zEMail);
07bfe3f… drh 2028 }else{
07bfe3f… drh 2029 char *zLogin = db_text(0, "SELECT login FROM user WHERE uid=%d", uid);
07bfe3f… drh 2030 fossil_print("%s: UID %d (%s)\n", zEMail, uid, zLogin);
07bfe3f… drh 2031 fossil_free(zLogin);
07bfe3f… drh 2032 }
07bfe3f… drh 2033 }
07bfe3f… drh 2034 }
275da70… danield 2035
c00e912… drh 2036
c00e912… drh 2037 /*
c00e912… drh 2038 ** Check an email address and confirm that it is valid for self-registration.
c00e912… drh 2039 ** The email address is known already to be well-formed. Return true
c00e912… drh 2040 ** if the email address is on the allowed list.
c00e912… drh 2041 **
c00e912… drh 2042 ** The default behavior is that any valid email address is accepted.
c00e912… drh 2043 ** But if the "auth-sub-email" setting exists and is not empty, then
c00e912… drh 2044 ** it is a comma-separated list of GLOB patterns for email addresses
c00e912… drh 2045 ** that are authorized to self-register.
c00e912… drh 2046 */
c00e912… drh 2047 int authorized_subscription_email(const char *zEAddr){
c00e912… drh 2048 char *zGlob = db_get("auth-sub-email",0);
c00e912… drh 2049 char *zAddr;
c00e912… drh 2050 int rc;
c00e912… drh 2051
c00e912… drh 2052 if( zGlob==0 || zGlob[0]==0 ) return 1;
c00e912… drh 2053 zGlob = fossil_strtolwr(fossil_strdup(zGlob));
dc86831… drh 2054 zAddr = fossil_strtolwr(fossil_strdup(zEAddr));
dc86831… drh 2055 rc = glob_multi_match(zGlob, zAddr);
c00e912… drh 2056 fossil_free(zGlob);
c00e912… drh 2057 fossil_free(zAddr);
c00e912… drh 2058 return rc!=0;
9039a6a… drh 2059 }
9039a6a… drh 2060
9039a6a… drh 2061 /*
9039a6a… drh 2062 ** WEBPAGE: register
9039a6a… drh 2063 **
7ab0328… drh 2064 ** Page to allow users to self-register. The "self-register" setting
7ab0328… drh 2065 ** must be enabled for this page to operate.
9039a6a… drh 2066 */
9039a6a… drh 2067 void register_page(void){
99fcc43… drh 2068 const char *zUserID, *zPasswd, *zConfirm, *zEAddr;
99fcc43… drh 2069 const char *zDName;
372c725… drh 2070 unsigned int uSeed;
4e18dba… jan.nijtmans 2071 const char *zDecoded;
99fcc43… drh 2072 int iErrLine = -1;
014bb2d… mistachkin 2073 const char *zErr = 0;
07bfe3f… drh 2074 int uid = 0; /* User id with the same email */
c00e912… drh 2075 int captchaIsCorrect = 0; /* True on a correct captcha */
c00e912… drh 2076 char *zCaptcha = ""; /* Value of the captcha text */
2e30828… drh 2077 char *zPerms; /* Permissions for the default user */
2e30828… drh 2078 int canDoAlerts = 0; /* True if receiving email alerts is possible */
4c43f2c… drh 2079 int doAlerts = 0; /* True if subscription is wanted too */
07bfe3f… drh 2080
9039a6a… drh 2081 if( !db_get_boolean("self-register", 0) ){
9039a6a… drh 2082 style_header("Registration not possible");
9039a6a… drh 2083 @ <p>This project does not allow user self-registration. Please contact the
9039a6a… drh 2084 @ project administrator to obtain an account.</p>
112c713… drh 2085 style_finish_page();
112c713… drh 2086 return;
112c713… drh 2087 }
07bfe3f… drh 2088 if( P("pwreset")!=0 && login_self_password_reset_available() ){
07bfe3f… drh 2089 /* The "Request Password Reset" button was pressed, so render the
07bfe3f… drh 2090 ** "Request Password Reset" page instead of this one. */
07bfe3f… drh 2091 login_reqpwreset_page();
07bfe3f… drh 2092 return;
07bfe3f… drh 2093 }
c00e912… drh 2094 zPerms = db_get("default-perms", "u");
6ae9941… drh 2095 login_check_credentials();
99fcc43… drh 2096
2e30828… drh 2097 /* Prompt the user for email alerts if this repository is configured for
2e30828… drh 2098 ** email alerts and if the default permissions include "7" */
129ea22… mistachkin 2099 canDoAlerts = alert_tables_exist() && (db_int(0,
2e30828… drh 2100 "SELECT fullcap(%Q) GLOB '*7*'", zPerms
129ea22… mistachkin 2101 ) || db_get_boolean("selfreg-verify",0));
4c43f2c… drh 2102 doAlerts = canDoAlerts && atoi(PD("alerts","1"))!=0;
2e30828… drh 2103
99fcc43… drh 2104 zUserID = PDT("u","");
99fcc43… drh 2105 zPasswd = PDT("p","");
99fcc43… drh 2106 zConfirm = PDT("cp","");
99fcc43… drh 2107 zEAddr = PDT("ea","");
99fcc43… drh 2108 zDName = PDT("dn","");
99fcc43… drh 2109
a4419c6… drh 2110 /* Verify user imputs */
920ace1… drh 2111 if( P("new")==0 || !cgi_csrf_safe(2) ){
99fcc43… drh 2112 /* This is not a valid form submission. Fall through into
99fcc43… drh 2113 ** the form display */
c00e912… drh 2114 }else if( (captchaIsCorrect = captcha_is_correct(1))==0 ){
99fcc43… drh 2115 iErrLine = 6;
99fcc43… drh 2116 zErr = "Incorrect CAPTCHA";
b13b651… drh 2117 }else if( strlen(zUserID)<6 ){
99fcc43… drh 2118 iErrLine = 1;
b13b651… drh 2119 zErr = "User ID too short. Must be at least 6 characters.";
99fcc43… drh 2120 }else if( sqlite3_strglob("*[^-a-zA-Z0-9_.]*",zUserID)==0 ){
99fcc43… drh 2121 iErrLine = 1;
99fcc43… drh 2122 zErr = "User ID may not contain spaces or special characters.";
275da70… danield 2123 }else if( sqlite3_strlike("anonymous%", zUserID, 0)==0
a7e9dd5… drh 2124 || sqlite3_strlike("nobody%", zUserID, 0)==0
a7e9dd5… drh 2125 || sqlite3_strlike("reader%", zUserID, 0)==0
a7e9dd5… drh 2126 || sqlite3_strlike("developer%", zUserID, 0)==0
a7e9dd5… drh 2127 ){
a7e9dd5… drh 2128 iErrLine = 1;
a7e9dd5… drh 2129 zErr = "This User ID is reserved. Choose something different.";
99fcc43… drh 2130 }else if( zDName[0]==0 ){
99fcc43… drh 2131 iErrLine = 2;
99fcc43… drh 2132 zErr = "Required";
99fcc43… drh 2133 }else if( zEAddr[0]==0 ){
99fcc43… drh 2134 iErrLine = 3;
99fcc43… drh 2135 zErr = "Required";
32a8d11… drh 2136 }else if( email_address_is_valid(zEAddr,0)==0 ){
99fcc43… drh 2137 iErrLine = 3;
99fcc43… drh 2138 zErr = "Not a valid email address";
c00e912… drh 2139 }else if( authorized_subscription_email(zEAddr)==0 ){
c00e912… drh 2140 iErrLine = 3;
c00e912… drh 2141 zErr = "Not an authorized email address";
99fcc43… drh 2142 }else if( strlen(zPasswd)<6 ){
99fcc43… drh 2143 iErrLine = 4;
99fcc43… drh 2144 zErr = "Password must be at least 6 characters long";
99fcc43… drh 2145 }else if( fossil_strcmp(zPasswd,zConfirm)!=0 ){
99fcc43… drh 2146 iErrLine = 5;
99fcc43… drh 2147 zErr = "Passwords do not match";
07bfe3f… drh 2148 }else if( (uid = email_address_in_use(zEAddr))!=0 ){
07bfe3f… drh 2149 iErrLine = 3;
07bfe3f… drh 2150 zErr = "This email address is already associated with a user";
d425d23… danield 2151 }else if( login_self_chosen_userid_already_exists(zUserID) ){
99fcc43… drh 2152 iErrLine = 1;
99fcc43… drh 2153 zErr = "This User ID is already taken. Choose something different.";
99fcc43… drh 2154 }else{
a4419c6… drh 2155 /* If all of the tests above have passed, that means that the submitted
a4419c6… drh 2156 ** form contains valid data and we can proceed to create the new login */
99fcc43… drh 2157 Blob sql;
99fcc43… drh 2158 int uid;
99fcc43… drh 2159 char *zPass = sha1_shared_secret(zPasswd, zUserID, 0);
c00e912… drh 2160 const char *zStartPerms = zPerms;
c00e912… drh 2161 if( db_get_boolean("selfreg-verify",0) ){
33d3bf3… km 2162 /* If email verification is required for self-registration, initialize
c00e912… drh 2163 ** the new user capabilities to just "7" (Sign up for email). The
c00e912… drh 2164 ** full "default-perms" permissions will be added when they click
c00e912… drh 2165 ** the verification link on the email they are sent. */
c00e912… drh 2166 zStartPerms = "7";
c00e912… drh 2167 }
99fcc43… drh 2168 blob_init(&sql, 0, 0);
99fcc43… drh 2169 blob_append_sql(&sql,
99fcc43… drh 2170 "INSERT INTO user(login,pw,cap,info,mtime)\n"
99fcc43… drh 2171 "VALUES(%Q,%Q,%Q,"
99fcc43… drh 2172 "'%q <%q>\nself-register from ip %q on '||datetime('now'),now())",
c00e912… drh 2173 zUserID, zPass, zStartPerms, zDName, zEAddr, g.zIpAddr);
99fcc43… drh 2174 fossil_free(zPass);
f741baa… drh 2175 db_unprotect(PROTECT_USER);
99fcc43… drh 2176 db_multi_exec("%s", blob_sql_text(&sql));
f741baa… drh 2177 db_protect_pop();
99fcc43… drh 2178 uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zUserID);
6b7b323… drh 2179 login_set_user_cookie(zUserID, uid, NULL, 0);
4c43f2c… drh 2180 if( doAlerts ){
2e30828… drh 2181 /* Also make the new user a subscriber. */
2e30828… drh 2182 Blob hdr, body;
fc5c7d2… drh 2183 AlertSender *pSender;
2e30828… drh 2184 const char *zCode; /* New subscriber code (in hex) */
2e30828… drh 2185 const char *zGoto = P("g");
2e30828… drh 2186 int nsub = 0;
2e30828… drh 2187 char ssub[20];
15e1529… drh 2188 CapabilityString *pCap;
15e1529… drh 2189 pCap = capability_add(0, zPerms);
15e1529… drh 2190 capability_expand(pCap);
2e30828… drh 2191 ssub[nsub++] = 'a';
15e1529… drh 2192 if( capability_has_any(pCap,"o") ) ssub[nsub++] = 'c';
15e1529… drh 2193 if( capability_has_any(pCap,"2") ) ssub[nsub++] = 'f';
15e1529… drh 2194 if( capability_has_any(pCap,"r") ) ssub[nsub++] = 't';
15e1529… drh 2195 if( capability_has_any(pCap,"j") ) ssub[nsub++] = 'w';
2e30828… drh 2196 ssub[nsub] = 0;
15e1529… drh 2197 capability_free(pCap);
a4419c6… drh 2198 /* Also add the user to the subscriber table. */
8a3dc1a… drh 2199 zCode = db_text(0,
2e30828… drh 2200 "INSERT INTO subscriber(semail,suname,"
d7e10ce… drh 2201 " sverified,sdonotcall,sdigest,ssub,sctime,mtime,smip,lastContact)"
d7e10ce… drh 2202 " VALUES(%Q,%Q,%d,0,%d,%Q,now(),now(),%Q,now()/86400)"
4c43f2c… drh 2203 " ON CONFLICT(semail) DO UPDATE"
8a3dc1a… drh 2204 " SET suname=excluded.suname"
8a3dc1a… drh 2205 " RETURNING hex(subscriberCode);",
2e30828… drh 2206 /* semail */ zEAddr,
2e30828… drh 2207 /* suname */ zUserID,
2e30828… drh 2208 /* sverified */ 0,
2e30828… drh 2209 /* sdigest */ 0,
2e30828… drh 2210 /* ssub */ ssub,
2e30828… drh 2211 /* smip */ g.zIpAddr
2e30828… drh 2212 );
4c43f2c… drh 2213 if( db_exists("SELECT 1 FROM subscriber WHERE semail=%Q"
4c43f2c… drh 2214 " AND sverified", zEAddr) ){
4c43f2c… drh 2215 /* This the case where the user was formerly a verified subscriber
4c43f2c… drh 2216 ** and here they have also registered as a user as well. It is
e2bdc10… danield 2217 ** not necessary to repeat the verification step */
1958448… drh 2218 login_redirect_to_g();
4c43f2c… drh 2219 }
2e30828… drh 2220 /* A verification email */
fc5c7d2… drh 2221 pSender = alert_sender_new(0,0);
2e30828… drh 2222 blob_init(&hdr,0,0);
2e30828… drh 2223 blob_init(&body,0,0);
2e30828… drh 2224 blob_appendf(&hdr, "To: <%s>\n", zEAddr);
2e30828… drh 2225 blob_appendf(&hdr, "Subject: Subscription verification\n");
fc5c7d2… drh 2226 alert_append_confirmation_message(&body, zCode);
fc5c7d2… drh 2227 alert_send(pSender, &hdr, &body, 0);
2e30828… drh 2228 style_header("Email Verification");
2e30828… drh 2229 if( pSender->zErr ){
2e30828… drh 2230 @ <h1>Internal Error</h1>
2e30828… drh 2231 @ <p>The following internal error was encountered while trying
2e30828… drh 2232 @ to send the confirmation email:
2e30828… drh 2233 @ <blockquote><pre>
2e30828… drh 2234 @ %h(pSender->zErr)
2e30828… drh 2235 @ </pre></blockquote>
2e30828… drh 2236 }else{
2e30828… drh 2237 @ <p>An email has been sent to "%h(zEAddr)". That email contains a
c00e912… drh 2238 @ hyperlink that you must click to activate your account.</p>
2e30828… drh 2239 }
fc5c7d2… drh 2240 alert_sender_free(pSender);
2e30828… drh 2241 if( zGoto ){
2e30828… drh 2242 @ <p><a href='%h(zGoto)'>Continue</a>
2e30828… drh 2243 }
112c713… drh 2244 style_finish_page();
2e30828… drh 2245 return;
2e30828… drh 2246 }
1958448… drh 2247 login_redirect_to_g();
2e30828… drh 2248 }
2e30828… drh 2249
2e30828… drh 2250 /* Prepare the captcha. */
c00e912… drh 2251 if( captchaIsCorrect ){
c00e912… drh 2252 uSeed = strtoul(P("captchaseed"),0,10);
c00e912… drh 2253 }else{
c00e912… drh 2254 uSeed = captcha_seed();
c00e912… drh 2255 }
8659d84… drh 2256 zDecoded = captcha_decode(uSeed, 0);
2e30828… drh 2257 zCaptcha = captcha_render(zDecoded);
2e30828… drh 2258
2e30828… drh 2259 style_header("Register");
2e30828… drh 2260 /* Print out the registration form. */
8dd7542… drh 2261 g.perm.Hyperlink = 1; /* Artificially enable hyperlinks */
2e30828… drh 2262 form_begin(0, "%R/register");
2e30828… drh 2263 if( P("g") ){
f5482a0… wyoung 2264 @ <input type="hidden" name="g" value="%h(P("g"))">
2e30828… drh 2265 }
f5482a0… wyoung 2266 @ <p><input type="hidden" name="captchaseed" value="%u(uSeed)">
2e30828… drh 2267 @ <table class="login_out">
2e30828… drh 2268 @ <tr>
7dd07b2… drh 2269 @ <td class="form_label" align="right" id="uid">User ID:</td>
7dd07b2… drh 2270 @ <td><input aria-labelledby="uid" type="text" name="u" \
bc05e6c… florian 2271 @ value="%h(zUserID)" size="30" autofocus></td>
27769be… drh 2272 @
2e30828… drh 2273 if( iErrLine==1 ){
27769be… drh 2274 @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
2e30828… drh 2275 }
2e30828… drh 2276 @ <tr>
7dd07b2… drh 2277 @ <td class="form_label" align="right" id="dpyname">Display Name:</td>
7dd07b2… drh 2278 @ <td><input aria-labelledby="dpyname" type="text" name="dn" \
7dd07b2… drh 2279 @ value="%h(zDName)" size="30"></td>
27769be… drh 2280 @ </tr>
2e30828… drh 2281 if( iErrLine==2 ){
27769be… drh 2282 @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
2e30828… drh 2283 }
2e30828… drh 2284 @ </tr>
2e30828… drh 2285 @ <tr>
7dd07b2… drh 2286 @ <td class="form_label" align="right" id="emaddr">Email Address:</td>
7dd07b2… drh 2287 @ <td><input aria-labelledby="emaddr" type="text" name="ea" \
7dd07b2… drh 2288 @ value="%h(zEAddr)" size="30"></td>
27769be… drh 2289 @ </tr>
2e30828… drh 2290 if( iErrLine==3 ){
07bfe3f… drh 2291 @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span>
07bfe3f… drh 2292 if( uid>0 && login_self_password_reset_available() ){
f5482a0… wyoung 2293 @ <br>
07bfe3f… drh 2294 @ <input type="submit" name="pwreset" \
07bfe3f… drh 2295 @ value="Request Password Reset For %h(zEAddr)">
07bfe3f… drh 2296 }
07bfe3f… drh 2297 @ </td></tr>
2e30828… drh 2298 }
2e30828… drh 2299 if( canDoAlerts ){
2e30828… drh 2300 int a = atoi(PD("alerts","1"));
2e30828… drh 2301 @ <tr>
7dd07b2… drh 2302 @ <td class="form_label" align="right" id="emalrt">Email&nbsp;Alerts?</td>
7dd07b2… drh 2303 @ <td><select aria-labelledby="emalrt" size='1' name='alerts'>
2e30828… drh 2304 @ <option value="1" %s(a?"selected":"")>Yes</option>
2e30828… drh 2305 @ <option value="0" %s(!a?"selected":"")>No</option>
2e30828… drh 2306 @ </select></td></tr>
2e30828… drh 2307 }
2e30828… drh 2308 @ <tr>
7dd07b2… drh 2309 @ <td class="form_label" align="right" id="pswd">Password:</td>
7dd07b2… drh 2310 @ <td><input aria-labelledby="pswd" type="password" name="p" \
49f68be… drh 2311 @ value="%h(zPasswd)" size="30"> \
49f68be… drh 2312 if( zPasswd[0]==0 ){
49f68be… drh 2313 char *zRPW = fossil_random_password(12);
49f68be… drh 2314 @ Password suggestion: %z(zRPW)</td>
49f68be… drh 2315 }else{
49f68be… drh 2316 @ </td>
49f68be… drh 2317 }
27769be… drh 2318 @ <tr>
27769be… drh 2319 if( iErrLine==4 ){
27769be… drh 2320 @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
27769be… drh 2321 }
2e30828… drh 2322 @ <tr>
7dd07b2… drh 2323 @ <td class="form_label" align="right" id="pwcfrm">Confirm:</td>
7dd07b2… drh 2324 @ <td><input aria-labelledby="pwcfrm" type="password" name="cp" \
7dd07b2… drh 2325 @ value="%h(zConfirm)" size="30"></td>
27769be… drh 2326 @ </tr>
2e30828… drh 2327 if( iErrLine==5 ){
27769be… drh 2328 @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
2e30828… drh 2329 }
2e30828… drh 2330 @ <tr>
7dd07b2… drh 2331 @ <td class="form_label" align="right" id="cptcha">Captcha:</td>
7dd07b2… drh 2332 @ <td><input type="text" name="captcha" aria-labelledby="cptcha" \
c00e912… drh 2333 @ value="%h(captchaIsCorrect?zDecoded:"")" size="30">
5a7d449… drh 2334 captcha_speakit_button(uSeed, "Speak the captcha text");
5a7d449… drh 2335 @ </td>
27769be… drh 2336 @ </tr>
2e30828… drh 2337 if( iErrLine==6 ){
27769be… drh 2338 @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
27769be… drh 2339 }
27769be… drh 2340 @ <tr><td></td>
f5482a0… wyoung 2341 @ <td><input type="submit" name="new" value="Register"></td></tr>
112c713… drh 2342 @ </table>
112c713… drh 2343 @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
112c713… drh 2344 @ %h(zCaptcha)
112c713… drh 2345 @ </pre>
112c713… drh 2346 @ Enter this 8-letter code in the "Captcha" box above.
112c713… drh 2347 @ </td></tr></table></div>
112c713… drh 2348 @ </form>
112c713… drh 2349 style_finish_page();
112c713… drh 2350
07bfe3f… drh 2351 free(zCaptcha);
07bfe3f… drh 2352 }
07bfe3f… drh 2353
07bfe3f… drh 2354 /*
a90d3aa… drh 2355 ** WEBPAGE: reqpwreset
07bfe3f… drh 2356 **
07bfe3f… drh 2357 ** A web page to request a password reset.
a90d3aa… drh 2358 **
a90d3aa… drh 2359 ** A form is presented where the user can enter their email address
a90d3aa… drh 2360 ** and a captcha. If the email address entered corresponds to a known
a90d3aa… drh 2361 ** users, an email is sent to that address that contains a link to the
a90d3aa… drh 2362 ** /resetpw page that allows the users to enter a new password.
a90d3aa… drh 2363 **
a90d3aa… drh 2364 ** This page is only available if the self-pw-reset property is enabled
a90d3aa… drh 2365 ** and email notifications are configured and operating. Password resets
a90d3aa… drh 2366 ** are not available to users with Admin or Setup privilege.
07bfe3f… drh 2367 */
07bfe3f… drh 2368 void login_reqpwreset_page(void){
07bfe3f… drh 2369 const char *zEAddr;
07bfe3f… drh 2370 const char *zDecoded;
07bfe3f… drh 2371 unsigned int uSeed;
07bfe3f… drh 2372 int iErrLine = -1;
07bfe3f… drh 2373 const char *zErr = 0;
07bfe3f… drh 2374 int uid = 0; /* User id with the email zEAddr */
07bfe3f… drh 2375 int captchaIsCorrect = 0; /* True on a correct captcha */
07bfe3f… drh 2376 char *zCaptcha = ""; /* Value of the captcha text */
07bfe3f… drh 2377
07bfe3f… drh 2378 if( !login_self_password_reset_available() ){
07bfe3f… drh 2379 style_header("Password reset not possible");
07bfe3f… drh 2380 @ <p>This project does not allow users to reset their own passwords.
07bfe3f… drh 2381 @ If you need a password reset, you will have to negotiate that directly
07bfe3f… drh 2382 @ with the project administrator.
07bfe3f… drh 2383 style_finish_page();
07bfe3f… drh 2384 return;
07bfe3f… drh 2385 }
07bfe3f… drh 2386 zEAddr = PDT("ea","");
07bfe3f… drh 2387
e2bdc10… danield 2388 /* Verify user inputs */
ce8598b… drh 2389 if( !cgi_csrf_safe(1) || P("reqpwreset")==0 ){
07bfe3f… drh 2390 /* This is the initial display of the form. No processing or error
07bfe3f… drh 2391 ** checking is to be done. Fall through into the form display
ce8598b… drh 2392 **
ce8598b… drh 2393 ** cgi_csrf_safe(): Nothing interesting happens on this page without
ce8598b… drh 2394 ** a valid captcha solution, so we only need to check referrer and that
ce8598b… drh 2395 ** the request is a POST.
07bfe3f… drh 2396 */
07bfe3f… drh 2397 }else if( (captchaIsCorrect = captcha_is_correct(1))==0 ){
07bfe3f… drh 2398 iErrLine = 2;
07bfe3f… drh 2399 zErr = "Incorrect CAPTCHA";
07bfe3f… drh 2400 }else if( zEAddr[0]==0 ){
07bfe3f… drh 2401 iErrLine = 1;
07bfe3f… drh 2402 zErr = "Required";
07bfe3f… drh 2403 }else if( email_address_is_valid(zEAddr,0)==0 ){
07bfe3f… drh 2404 iErrLine = 1;
07bfe3f… drh 2405 zErr = "Not a valid email address";
07bfe3f… drh 2406 }else if( authorized_subscription_email(zEAddr)==0 ){
07bfe3f… drh 2407 iErrLine = 1;
07bfe3f… drh 2408 zErr = "Not an authorized email address";
07bfe3f… drh 2409 }else if( (uid = email_address_in_use(zEAddr))<=0 ){
07bfe3f… drh 2410 iErrLine = 1;
07bfe3f… drh 2411 zErr = "This email address is not associated with a user who has "
07bfe3f… drh 2412 "password reset privileges.";
07bfe3f… drh 2413 }else if( login_set_uid(uid,0)==0 || g.perm.Admin || g.perm.Setup
07bfe3f… drh 2414 || !g.perm.Password ){
07bfe3f… drh 2415 iErrLine = 1;
07bfe3f… drh 2416 zErr = "This email address is not associated with a user who has "
07bfe3f… drh 2417 "password reset privileges.";
07bfe3f… drh 2418 }else{
07bfe3f… drh 2419
07bfe3f… drh 2420 /* If all of the tests above have passed, that means that the submitted
07bfe3f… drh 2421 ** form contains valid data and we can proceed to issue the password
07bfe3f… drh 2422 ** reset email. */
07bfe3f… drh 2423 Blob hdr, body;
07bfe3f… drh 2424 AlertSender *pSender;
07bfe3f… drh 2425 char *zUrl = login_resetpw_suffix(uid, 0);
07bfe3f… drh 2426 pSender = alert_sender_new(0,0);
07bfe3f… drh 2427 blob_init(&hdr,0,0);
07bfe3f… drh 2428 blob_init(&body,0,0);
07bfe3f… drh 2429 blob_appendf(&hdr, "To: <%s>\n", zEAddr);
07bfe3f… drh 2430 blob_appendf(&hdr, "Subject: Password reset for %s\n", g.zBaseURL);
07bfe3f… drh 2431 blob_appendf(&body,
07bfe3f… drh 2432 "Someone has requested to reset the password for user \"%s\"\n",
07bfe3f… drh 2433 g.zLogin);
07bfe3f… drh 2434 blob_appendf(&body, "at %s.\n\n", g.zBaseURL);
07bfe3f… drh 2435 blob_appendf(&body,
07bfe3f… drh 2436 "If you did not request this password reset, ignore\n"
07bfe3f… drh 2437 "this email\n\n");
07bfe3f… drh 2438 blob_appendf(&body,
07bfe3f… drh 2439 "To reset the password, visit the following link:\n\n"
07bfe3f… drh 2440 " %s/resetpw/%s\n\n", g.zBaseURL, zUrl);
07bfe3f… drh 2441 fossil_free(zUrl);
07bfe3f… drh 2442 alert_send(pSender, &hdr, &body, 0);
07bfe3f… drh 2443 style_header("Email Verification");
07bfe3f… drh 2444 if( pSender->zErr ){
07bfe3f… drh 2445 @ <h1>Internal Error</h1>
07bfe3f… drh 2446 @ <p>The following internal error was encountered while trying
07bfe3f… drh 2447 @ to send the confirmation email:
07bfe3f… drh 2448 @ <blockquote><pre>
07bfe3f… drh 2449 @ %h(pSender->zErr)
07bfe3f… drh 2450 @ </pre></blockquote>
07bfe3f… drh 2451 }else{
07bfe3f… drh 2452 @ <p>An email containing a hyperlink that can be used to reset
07bfe3f… drh 2453 @ your password has been sent to "%h(zEAddr)".</p>
07bfe3f… drh 2454 }
07bfe3f… drh 2455 alert_sender_free(pSender);
07bfe3f… drh 2456 style_finish_page();
07bfe3f… drh 2457 return;
07bfe3f… drh 2458 }
07bfe3f… drh 2459
07bfe3f… drh 2460 /* Prepare the captcha. */
07bfe3f… drh 2461 if( captchaIsCorrect ){
07bfe3f… drh 2462 uSeed = strtoul(P("captchaseed"),0,10);
07bfe3f… drh 2463 }else{
07bfe3f… drh 2464 uSeed = captcha_seed();
07bfe3f… drh 2465 }
8659d84… drh 2466 zDecoded = captcha_decode(uSeed, 0);
07bfe3f… drh 2467 zCaptcha = captcha_render(zDecoded);
07bfe3f… drh 2468
07bfe3f… drh 2469 style_header("Request Password Reset");
07bfe3f… drh 2470 /* Print out the registration form. */
07bfe3f… drh 2471 g.perm.Hyperlink = 1; /* Artificially enable hyperlinks */
07bfe3f… drh 2472 form_begin(0, "%R/reqpwreset");
f5482a0… wyoung 2473 @ <p><input type="hidden" name="captchaseed" value="%u(uSeed)">
f5482a0… wyoung 2474 @ <p><input type="hidden" name="reqpwreset" value="1">
07bfe3f… drh 2475 @ <table class="login_out">
07bfe3f… drh 2476 @ <tr>
07bfe3f… drh 2477 @ <td class="form_label" align="right" id="emaddr">Email Address:</td>
07bfe3f… drh 2478 @ <td><input aria-labelledby="emaddr" type="text" name="ea" \
07bfe3f… drh 2479 @ value="%h(zEAddr)" size="30"></td>
07bfe3f… drh 2480 @ </tr>
07bfe3f… drh 2481 if( iErrLine==1 ){
07bfe3f… drh 2482 @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
07bfe3f… drh 2483 }
07bfe3f… drh 2484 @ <tr>
07bfe3f… drh 2485 @ <td class="form_label" align="right" id="cptcha">Captcha:</td>
07bfe3f… drh 2486 @ <td><input type="text" name="captcha" aria-labelledby="cptcha" \
07bfe3f… drh 2487 @ value="%h(captchaIsCorrect?zDecoded:"")" size="30">
07bfe3f… drh 2488 captcha_speakit_button(uSeed, "Speak the captcha text");
07bfe3f… drh 2489 @ </td>
07bfe3f… drh 2490 @ </tr>
07bfe3f… drh 2491 if( iErrLine==2 ){
07bfe3f… drh 2492 @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
07bfe3f… drh 2493 }
07bfe3f… drh 2494 @ <tr><td></td>
07bfe3f… drh 2495 @ <td><input type="submit" name="new" value="Request Password Reset"/>\
07bfe3f… drh 2496 @ </td></tr>
07bfe3f… drh 2497 @ </table>
07bfe3f… drh 2498 @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
07bfe3f… drh 2499 @ %h(zCaptcha)
07bfe3f… drh 2500 @ </pre>
07bfe3f… drh 2501 @ Enter this 8-letter code in the "Captcha" box above.
07bfe3f… drh 2502 @ </td></tr></table></div>
07bfe3f… drh 2503 @ </form>
07bfe3f… drh 2504 style_finish_page();
a257fde… drh 2505 free(zCaptcha);
a257fde… drh 2506 }
a257fde… drh 2507
a257fde… drh 2508 /*
a257fde… drh 2509 ** Run SQL on the repository database for every repository in our
a257fde… drh 2510 ** login group. The SQL is run in a separate database connection.
a257fde… drh 2511 **
a257fde… drh 2512 ** Any members of the login group whose repository database file
a257fde… drh 2513 ** cannot be found is silently removed from the group.
a257fde… drh 2514 **
a257fde… drh 2515 ** Error messages accumulate and are returned in *pzErrorMsg. The
a257fde… drh 2516 ** memory used to hold these messages should be freed using
a257fde… drh 2517 ** fossil_free() if one desired to avoid a memory leak. The
a257fde… drh 2518 ** zPrefix and zSuffix strings surround each error message.
a257fde… drh 2519 **
a257fde… drh 2520 ** Return the number of errors.
a257fde… drh 2521 */
a257fde… drh 2522 int login_group_sql(
a257fde… drh 2523 const char *zSql, /* The SQL to run */
a257fde… drh 2524 const char *zPrefix, /* Prefix to each error message */
a257fde… drh 2525 const char *zSuffix, /* Suffix to each error message */
a257fde… drh 2526 char **pzErrorMsg /* Write error message here, if not NULL */
a257fde… drh 2527 ){
a257fde… drh 2528 sqlite3 *pPeer; /* Connection to another database */
a257fde… drh 2529 int nErr = 0; /* Number of errors seen so far */
a257fde… drh 2530 int rc; /* Result code from subroutine calls */
a257fde… drh 2531 char *zErr; /* SQLite error text */
a257fde… drh 2532 char *zSelfCode; /* Project code for ourself */
a257fde… drh 2533 Blob err; /* Accumulate errors here */
a257fde… drh 2534 Stmt q; /* Query of all peer-* entries in CONFIG */
a257fde… drh 2535
a257fde… drh 2536 if( zPrefix==0 ) zPrefix = "";
a257fde… drh 2537 if( zSuffix==0 ) zSuffix = "";
a257fde… drh 2538 if( pzErrorMsg ) *pzErrorMsg = 0;
f7ce03e… drh 2539 zSelfCode = abbreviated_project_code(db_get("project-code", "x"));
a257fde… drh 2540 blob_zero(&err);
45f3516… jan.nijtmans 2541 db_prepare(&q,
a257fde… drh 2542 "SELECT name, value FROM config"
a257fde… drh 2543 " WHERE name GLOB 'peer-repo-*'"
a257fde… drh 2544 " AND name <> 'peer-repo-%q'"
a257fde… drh 2545 " ORDER BY +value",
a257fde… drh 2546 zSelfCode
a257fde… drh 2547 );
a257fde… drh 2548 while( db_step(&q)==SQLITE_ROW ){
a257fde… drh 2549 const char *zRepoName = db_column_text(&q, 1);
1772357… drh 2550 if( file_size(zRepoName, ExtFILE)<0 ){
db0c512… drh 2551 /* Silently remove non-existent repositories from the login group. */
a257fde… drh 2552 const char *zLabel = db_column_text(&q, 0);
f741baa… drh 2553 db_unprotect(PROTECT_CONFIG);
a257fde… drh 2554 db_multi_exec(
a257fde… drh 2555 "DELETE FROM config WHERE name GLOB 'peer-*-%q'",
a257fde… drh 2556 &zLabel[10]
a257fde… drh 2557 );
f741baa… drh 2558 db_protect_pop();
a257fde… drh 2559 continue;
a257fde… drh 2560 }
19de4b5… mistachkin 2561 rc = sqlite3_open_v2(
19de4b5… mistachkin 2562 zRepoName, &pPeer,
19de4b5… mistachkin 2563 SQLITE_OPEN_READWRITE,
19de4b5… mistachkin 2564 g.zVfsName
19de4b5… mistachkin 2565 );
a257fde… drh 2566 if( rc!=SQLITE_OK ){
a257fde… drh 2567 blob_appendf(&err, "%s%s: %s%s", zPrefix, zRepoName,
a257fde… drh 2568 sqlite3_errmsg(pPeer), zSuffix);
a257fde… drh 2569 nErr++;
a257fde… drh 2570 sqlite3_close(pPeer);
a257fde… drh 2571 continue;
a257fde… drh 2572 }
a257fde… drh 2573 sqlite3_create_function(pPeer, "shared_secret", 3, SQLITE_UTF8,
a257fde… drh 2574 0, sha1_shared_secret_sql_function, 0, 0);
2c95802… jan.nijtmans 2575 sqlite3_create_function(pPeer, "now", 0,SQLITE_UTF8,0,db_now_function,0,0);
74ecc4d… drh 2576 sqlite3_busy_timeout(pPeer, 5000);
a257fde… drh 2577 zErr = 0;
a257fde… drh 2578 rc = sqlite3_exec(pPeer, zSql, 0, 0, &zErr);
a257fde… drh 2579 if( zErr ){
a257fde… drh 2580 blob_appendf(&err, "%s%s: %s%s", zPrefix, zRepoName, zErr, zSuffix);
a257fde… drh 2581 sqlite3_free(zErr);
a257fde… drh 2582 nErr++;
a257fde… drh 2583 }else if( rc!=SQLITE_OK ){
a257fde… drh 2584 blob_appendf(&err, "%s%s: %s%s", zPrefix, zRepoName,
a257fde… drh 2585 sqlite3_errmsg(pPeer), zSuffix);
a257fde… drh 2586 nErr++;
a257fde… drh 2587 }
a257fde… drh 2588 sqlite3_close(pPeer);
a257fde… drh 2589 }
a257fde… drh 2590 db_finalize(&q);
a257fde… drh 2591 if( pzErrorMsg && blob_size(&err)>0 ){
a257fde… drh 2592 *pzErrorMsg = fossil_strdup(blob_str(&err));
a257fde… drh 2593 }
a257fde… drh 2594 blob_reset(&err);
a257fde… drh 2595 fossil_free(zSelfCode);
a257fde… drh 2596 return nErr;
a257fde… drh 2597 }
a257fde… drh 2598
a257fde… drh 2599 /*
a257fde… drh 2600 ** Attempt to join a login-group.
a257fde… drh 2601 **
a257fde… drh 2602 ** If problems arise, leave an error message in *pzErrMsg.
a257fde… drh 2603 */
a257fde… drh 2604 void login_group_join(
a257fde… drh 2605 const char *zRepo, /* Repository file in the login group */
c3ba504… drh 2606 int bPwRequired, /* True if the login,password is required */
a257fde… drh 2607 const char *zLogin, /* Login name for the other repo */
a257fde… drh 2608 const char *zPassword, /* Password to prove we are authorized to join */
a257fde… drh 2609 const char *zNewName, /* Name of new login group if making a new one */
a257fde… drh 2610 char **pzErrMsg /* Leave an error message here */
a257fde… drh 2611 ){
a257fde… drh 2612 Blob fullName; /* Blob for finding full pathnames */
a257fde… drh 2613 sqlite3 *pOther; /* The other repository */
a257fde… drh 2614 int rc; /* Return code from sqlite3 functions */
a257fde… drh 2615 char *zOtherProjCode; /* Project code for pOther */
a257fde… drh 2616 char *zSelfRepo; /* Name of our repository */
a257fde… drh 2617 char *zSelfLabel; /* Project-name for our repository */
a257fde… drh 2618 char *zSelfProjCode; /* Our project-code */
a257fde… drh 2619 char *zSql; /* SQL to run on all peers */
a257fde… drh 2620 const char *zSelf; /* The ATTACH name of our repository */
a257fde… drh 2621
a257fde… drh 2622 *pzErrMsg = 0; /* Default to no errors */
06aec61… drh 2623 zSelf = "repository";
a257fde… drh 2624
45f3516… jan.nijtmans 2625 /* Get the full pathname of the other repository */
135ed93… drh 2626 file_canonical_name(zRepo, &fullName, 0);
49b0ff1… drh 2627 zRepo = fossil_strdup(blob_str(&fullName));
a257fde… drh 2628 blob_reset(&fullName);
a257fde… drh 2629
a257fde… drh 2630 /* Get the full pathname for our repository. Also the project code
a257fde… drh 2631 ** and project name for ourself. */
135ed93… drh 2632 file_canonical_name(g.zRepositoryName, &fullName, 0);
49b0ff1… drh 2633 zSelfRepo = fossil_strdup(blob_str(&fullName));
a257fde… drh 2634 blob_reset(&fullName);
f7ce03e… drh 2635 zSelfProjCode = db_get("project-code", "unknown");
a257fde… drh 2636 zSelfLabel = db_get("project-name", 0);
a257fde… drh 2637 if( zSelfLabel==0 ){
a257fde… drh 2638 zSelfLabel = zSelfProjCode;
a257fde… drh 2639 }
a257fde… drh 2640
a257fde… drh 2641 /* Make sure we are not trying to join ourselves */
32ad9a1… drh 2642 if( fossil_strcmp(zRepo, zSelfRepo)==0 ){
a257fde… drh 2643 *pzErrMsg = mprintf("The \"other\" repository is the same as this one.");
a257fde… drh 2644 return;
a257fde… drh 2645 }
a257fde… drh 2646
a257fde… drh 2647 /* Make sure the other repository is a valid Fossil database */
1772357… drh 2648 if( file_size(zRepo, ExtFILE)<0 ){
a257fde… drh 2649 *pzErrMsg = mprintf("repository file \"%s\" does not exist", zRepo);
a257fde… drh 2650 return;
a257fde… drh 2651 }
19de4b5… mistachkin 2652 rc = sqlite3_open_v2(
19de4b5… mistachkin 2653 zRepo, &pOther,
19de4b5… mistachkin 2654 SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
19de4b5… mistachkin 2655 g.zVfsName
19de4b5… mistachkin 2656 );
a257fde… drh 2657 if( rc!=SQLITE_OK ){
49b0ff1… drh 2658 *pzErrMsg = fossil_strdup(sqlite3_errmsg(pOther));
a257fde… drh 2659 }else{
a257fde… drh 2660 rc = sqlite3_exec(pOther, "SELECT count(*) FROM user", 0, 0, pzErrMsg);
a257fde… drh 2661 }
a257fde… drh 2662 sqlite3_close(pOther);
a257fde… drh 2663 if( rc ) return;
a257fde… drh 2664
7c0f4ec… jan.nijtmans 2665 /* Attach the other repository. Make sure the username/password is
a257fde… drh 2666 ** valid and has Setup permission.
a257fde… drh 2667 */
75bcb48… mistachkin 2668 db_attach(zRepo, "other");
a257fde… drh 2669 zOtherProjCode = db_text("x", "SELECT value FROM other.config"
a257fde… drh 2670 " WHERE name='project-code'");
c3ba504… drh 2671 if( bPwRequired ){
c3ba504… drh 2672 char *zPwHash; /* Password hash on pOther */
c3ba504… drh 2673 zPwHash = sha1_shared_secret(zPassword, zLogin, zOtherProjCode);
c3ba504… drh 2674 if( !db_exists(
c3ba504… drh 2675 "SELECT 1 FROM other.user"
c3ba504… drh 2676 " WHERE login=%Q AND cap GLOB '*s*'"
c3ba504… drh 2677 " AND (pw=%Q OR pw=%Q)",
c3ba504… drh 2678 zLogin, zPassword, zPwHash)
c3ba504… drh 2679 ){
c3ba504… drh 2680 db_detach("other");
c3ba504… drh 2681 *pzErrMsg = "The supplied username/password does not correspond to a"
c3ba504… drh 2682 " user Setup permission on the other repository.";
c3ba504… drh 2683 return;
c3ba504… drh 2684 }
a257fde… drh 2685 }
a257fde… drh 2686
a257fde… drh 2687 /* Create all the necessary CONFIG table entries on both the
a257fde… drh 2688 ** other repository and on our own repository.
a257fde… drh 2689 */
f7ce03e… drh 2690 zSelfProjCode = abbreviated_project_code(zSelfProjCode);
a257fde… drh 2691 zOtherProjCode = abbreviated_project_code(zOtherProjCode);
e1962ef… drh 2692 db_begin_transaction();
ca5a5c7… stephan 2693 db_unprotect(PROTECT_CONFIG);
a257fde… drh 2694 db_multi_exec(
49b0ff1… drh 2695 "DELETE FROM \"%w\".config WHERE name GLOB 'peer-*';"
49b0ff1… drh 2696 "INSERT INTO \"%w\".config(name,value) VALUES('peer-repo-%q',%Q);"
49b0ff1… drh 2697 "INSERT INTO \"%w\".config(name,value) "
a257fde… drh 2698 " SELECT 'peer-name-%q', value FROM other.config"
a257fde… drh 2699 " WHERE name='project-name';",
a257fde… drh 2700 zSelf,
a257fde… drh 2701 zSelf, zOtherProjCode, zRepo,
a257fde… drh 2702 zSelf, zOtherProjCode
a257fde… drh 2703 );
a257fde… drh 2704 db_multi_exec(
a257fde… drh 2705 "INSERT OR IGNORE INTO other.config(name,value)"
a257fde… drh 2706 " VALUES('login-group-name',%Q);"
a257fde… drh 2707 "INSERT OR IGNORE INTO other.config(name,value)"
a257fde… drh 2708 " VALUES('login-group-code',lower(hex(randomblob(8))));",
a257fde… drh 2709 zNewName
a257fde… drh 2710 );
a257fde… drh 2711 db_multi_exec(
49b0ff1… drh 2712 "REPLACE INTO \"%w\".config(name,value)"
a257fde… drh 2713 " SELECT name, value FROM other.config"
a257fde… drh 2714 " WHERE name GLOB 'peer-*' OR name GLOB 'login-group-*'",
a257fde… drh 2715 zSelf
a257fde… drh 2716 );
ca5a5c7… stephan 2717 db_protect_pop();
a257fde… drh 2718 db_end_transaction(0);
a257fde… drh 2719 db_multi_exec("DETACH other");
a257fde… drh 2720
a257fde… drh 2721 /* Propagate the changes to all other members of the login-group */
a257fde… drh 2722 zSql = mprintf(
a257fde… drh 2723 "BEGIN;"
1654456… drh 2724 "REPLACE INTO config(name,value,mtime) VALUES('peer-name-%q',%Q,now());"
1654456… drh 2725 "REPLACE INTO config(name,value,mtime) VALUES('peer-repo-%q',%Q,now());"
a257fde… drh 2726 "COMMIT;",
a257fde… drh 2727 zSelfProjCode, zSelfLabel, zSelfProjCode, zSelfRepo
a257fde… drh 2728 );
f741baa… drh 2729 db_unprotect(PROTECT_CONFIG);
a257fde… drh 2730 login_group_sql(zSql, "<li> ", "</li>", pzErrMsg);
f741baa… drh 2731 db_protect_pop();
a257fde… drh 2732 fossil_free(zSql);
a257fde… drh 2733 }
a257fde… drh 2734
a257fde… drh 2735 /*
a257fde… drh 2736 ** Leave the login group that we are currently part of.
a257fde… drh 2737 */
a257fde… drh 2738 void login_group_leave(char **pzErrMsg){
a257fde… drh 2739 char *zProjCode;
a257fde… drh 2740 char *zSql;
a257fde… drh 2741
a257fde… drh 2742 *pzErrMsg = 0;
f7ce03e… drh 2743 zProjCode = abbreviated_project_code(db_get("project-code","x"));
a257fde… drh 2744 zSql = mprintf(
a257fde… drh 2745 "DELETE FROM config WHERE name GLOB 'peer-*-%q';"
a257fde… drh 2746 "DELETE FROM config"
a257fde… drh 2747 " WHERE name='login-group-name'"
a257fde… drh 2748 " AND (SELECT count(*) FROM config WHERE name GLOB 'peer-*')==0;",
a257fde… drh 2749 zProjCode
a257fde… drh 2750 );
a257fde… drh 2751 fossil_free(zProjCode);
f741baa… drh 2752 db_unprotect(PROTECT_CONFIG);
a257fde… drh 2753 login_group_sql(zSql, "<li> ", "</li>", pzErrMsg);
a257fde… drh 2754 fossil_free(zSql);
a257fde… drh 2755 db_multi_exec(
a257fde… drh 2756 "DELETE FROM config "
a257fde… drh 2757 " WHERE name GLOB 'peer-*'"
a257fde… drh 2758 " OR name GLOB 'login-group-*';"
a257fde… drh 2759 );
f741baa… drh 2760 db_protect_pop();
c3ba504… drh 2761 }
c3ba504… drh 2762
c3ba504… drh 2763 /*
fcec3ed… drh 2764 ** COMMAND: login-group*
fcec3ed… drh 2765 **
b9107e4… mgagnon 2766 ** Usage: %fossil login-group ?SUBCOMMAND? ?OPTIONS?
b9107e4… mgagnon 2767 **
b9107e4… mgagnon 2768 ** Run various subcommands to manage login-group related settings of the open
b9107e4… mgagnon 2769 ** repository or of the repository identified by the -R or --repository option.
b9107e4… mgagnon 2770 **
83bc81e… mark 2771 ** > fossil login-group ?-R REPO?
83bc81e… mark 2772 **
bc36fdc… danield 2773 ** Show the login-group to which REPO, or if invoked from within a check-out
bc36fdc… danield 2774 ** the repository on which the current check-out is based, belongs.
bc36fdc… danield 2775 **
edf0355… mgagnon 2776 ** > fossil login-group join ?-R REPO? ?--name NAME? REPO2
bc36fdc… danield 2777 **
edf0355… mgagnon 2778 ** This command will either: (1) add the repository on which the current
edf0355… mgagnon 2779 ** check-out is based, or the repository REPO specified with -R, to the
edf0355… mgagnon 2780 ** login group where REPO2 is a member, in which case the optional --name
edf0355… mgagnon 2781 ** argument is not required; or (2) create a new login group between the
edf0355… mgagnon 2782 ** repository on which the current check-out is based, or the repository
edf0355… mgagnon 2783 ** REPO specified with -R, and REPO2, in which case the new group NAME is
edf0355… mgagnon 2784 ** determined by the mandatory --name option. In both cases, the specified
edf0355… mgagnon 2785 ** repositories will first leave any group in which they are currently a
edf0355… mgagnon 2786 ** member before joining the new login group.
83bc81e… mark 2787 **
83bc81e… mark 2788 ** > fossil login-group leave ?-R REPO?
83bc81e… mark 2789 **
bc36fdc… danield 2790 ** Take the repository REPO, or if invoked from within a check-out the
bc36fdc… danield 2791 ** repository on which the current check-out is based, out of whatever
83bc81e… mark 2792 ** login group it is a member.
c3ba504… drh 2793 **
c3ba504… drh 2794 ** About Login Groups:
83bc81e… mark 2795 **
83bc81e… mark 2796 ** A login-group is a set of repositories that share user credentials.
c3ba504… drh 2797 ** If a user is logged into one member of the group, then that user can
b9107e4… mgagnon 2798 ** access any other group member as long as they have an entry in the USER
b9107e4… mgagnon 2799 ** table of that member. If a user changes their password using web
b9107e4… mgagnon 2800 ** interface, their password is also automatically changed in every other
b9107e4… mgagnon 2801 ** member of the login group.
c3ba504… drh 2802 */
c3ba504… drh 2803 void login_group_command(void){
c3ba504… drh 2804 const char *zLGName;
c3ba504… drh 2805 const char *zCmd;
c3ba504… drh 2806 int nCmd;
c3ba504… drh 2807 Stmt q;
b9107e4… mgagnon 2808 db_find_and_open_repository(0, 0);
c3ba504… drh 2809 if( g.argc>2 ){
c3ba504… drh 2810 zCmd = g.argv[2];
c3ba504… drh 2811 nCmd = (int)strlen(zCmd);
c3ba504… drh 2812 if( strncmp(zCmd,"join",nCmd)==0 && nCmd>=1 ){
c3ba504… drh 2813 const char *zNewName = find_option("name",0,1);
edf0355… mgagnon 2814 const char *zOther = 0;
c3ba504… drh 2815 char *zErr = 0;
c3ba504… drh 2816 verify_all_options();
edf0355… mgagnon 2817 if( g.argc!=4 ){
769a765… stephan 2818 fossil_fatal("unexpected argument count for \"login-group join\"");
c3ba504… drh 2819 }
edf0355… mgagnon 2820 zOther = g.argv[3];
535714e… drh 2821 login_group_leave(&zErr);
535714e… drh 2822 sqlite3_free(zErr);
535714e… drh 2823 zErr = 0;
c3ba504… drh 2824 login_group_join(zOther,0,0,0,zNewName,&zErr);
c3ba504… drh 2825 if( zErr ){
c3ba504… drh 2826 fossil_fatal("%s", zErr);
c3ba504… drh 2827 }
c3ba504… drh 2828 }else if( strncmp(zCmd,"leave",nCmd)==0 && nCmd>=1 ){
c3ba504… drh 2829 verify_all_options();
c3ba504… drh 2830 if( g.argc!=3 ){
c3ba504… drh 2831 fossil_fatal("unknown extra arguments to \"login-group leave\"");
c3ba504… drh 2832 }
c3ba504… drh 2833 zLGName = login_group_name();
c3ba504… drh 2834 if( zLGName ){
c3ba504… drh 2835 char *zErr = 0;
c3ba504… drh 2836 fossil_print("Leaving login-group \"%s\"\n", zLGName);
c3ba504… drh 2837 login_group_leave(&zErr);
535714e… drh 2838 if( zErr ) fossil_fatal("Oops: %s", zErr);
c3ba504… drh 2839 return;
c3ba504… drh 2840 }
c3ba504… drh 2841 }else{
09c65d7… wyoung 2842 fossil_fatal("unknown command \"%s\" - should be \"join\" or \"leave\"",
c3ba504… drh 2843 zCmd);
c3ba504… drh 2844 }
c3ba504… drh 2845 }
c3ba504… drh 2846 /* Show the current login group information */
c3ba504… drh 2847 zLGName = login_group_name();
c3ba504… drh 2848 if( zLGName==0 ){
c3ba504… drh 2849 fossil_print("Not currently a part of any login-group\n");
c3ba504… drh 2850 return;
c3ba504… drh 2851 }
c3ba504… drh 2852 fossil_print("Now part of login-group \"%s\" with:\n", zLGName);
83bc81e… mark 2853 db_prepare(&q, "SELECT value FROM config WHERE name LIKE 'peer-repo-%%'");
c3ba504… drh 2854 while( db_step(&q)==SQLITE_ROW ){
c3ba504… drh 2855 fossil_print(" %s\n", db_column_text(&q,0));
c3ba504… drh 2856 }
c3ba504… drh 2857 db_finalize(&q);
c3ba504… drh 2858
dbda8d6… drh 2859 }

Keyboard Shortcuts

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