a240964d6dfdd6f8661487b64538d33518d88501 angie Mon May 16 09:38:09 2016 -0700 Added checking of wiki.loggedInCookie (numeric user ID) using random numbers as suggested by Max if the new centralDb table gbMemberToken exists, otherwise checking the ID vs gbMembers.idx. refs #17327 diff --git src/hg/lib/wikiLink.c src/hg/lib/wikiLink.c index 1281dbf..c46e5ad 100644 --- src/hg/lib/wikiLink.c +++ src/hg/lib/wikiLink.c @@ -1,230 +1,426 @@ /* wikiLink - interoperate with a wiki site (share user identities). */ /* Copyright (C) 2014 The Regents of the University of California * See README in this or parent directory for licensing information. */ #include "common.h" #include "hash.h" #include "htmshell.h" #include "cheapcgi.h" #include "hgConfig.h" #include "hui.h" #include "cart.h" #include "web.h" #include "wikiLink.h" +// Flag to indicate that loginSystemValidateCookies has been called: +static boolean alreadyAuthenticated = FALSE; +// If centralDb has table gbMemberToken, then loginSystemValidateCookies will set this +// to a random token that validates the user; otherwise if the cookie has the same +// value as gbMembers.idx, this is set to that ID; otherwise it stays 0 and the user +// is not logged in. +static uint authToken = 0; + +// If a random token in gbMemberToken is more than this many seconds old, make a new +// random token and delete the old: +#define TOKEN_LIFESPAN 300 + char *loginSystemName() /* Return the wiki host specified in hg.conf, or NULL. Allocd here. */ { return cloneString(cfgOption(CFG_LOGIN_SYSTEM_NAME)); } boolean loginSystemEnabled() /* Return TRUE if login.systemName parameter is defined in hg.conf . */ { #ifdef USE_SSL return (cfgOption(CFG_LOGIN_SYSTEM_NAME) != NULL); #else return FALSE; #endif } +static char *wikiLinkLoggedInCookie() +/* Return the cookie name specified in hg.conf as the wiki logged-in cookie. */ +{ +return cfgOption(CFG_WIKI_LOGGED_IN_COOKIE); +} + +static char *wikiLinkUserNameCookie() +/* Return the cookie name specified in hg.conf as the wiki user name cookie. */ +{ +return cfgOption(CFG_WIKI_USER_NAME_COOKIE); +} + +static uint getCookieToken() +/* If the cookie holding the login token exists, return its uint value, else 0. */ +{ +char *cookieTokenStr = findCookieData(wikiLinkLoggedInCookie()); +return cookieTokenStr ? (uint)atoll(cookieTokenStr) : 0; +} + +static uint getMemberIdx(struct sqlConnection *conn, char *userName) +/* Return userName's idx value in gbMembers. Return 0 if not found. */ +{ +char query[512]; +sqlSafef(query, sizeof(query), "select idx from gbMembers where userName='%s'", userName); +return (uint)sqlQuickLongLong(conn, query); +} + +static boolean haveTokenTable(struct sqlConnection *conn) +/* Return true if centralDb has table gbMemberToken. */ +{ +return sqlTableExists(conn, "gbMemberToken"); +} + +static boolean isValidToken(struct sqlConnection *conn, uint token, char *userName, + boolean *retMakeNewToken) +/* Return TRUE if gbMemberToken has an entry that maps token to userName. + * If retMakeNewToken is non-NULL, set it to TRUE if the token is older than TOKEN_LIFESPAN. */ +{ +boolean isValid = FALSE; +char query[512]; +sqlSafef(query, sizeof(query), "select userName, createTime from gbMemberToken where token = %u", + token); +struct sqlResult *sr = sqlGetResult(conn, query); +char **row; +if ((row = sqlNextRow(sr)) != NULL) + { + char *userForToken = cloneString(row[0]); + if (retMakeNewToken != NULL) + { + long createTime = sqlDateToUnixTime(row[1]); + *retMakeNewToken = (time(NULL) - createTime > TOKEN_LIFESPAN); + } + isValid = sameString(userForToken, userName); + } +sqlFreeResult(&sr); +return isValid; +} + +static void deleteToken(struct sqlConnection *conn, uint token) +/* Remove token's entry from gbMemberToken. */ +{ +char query[512]; +sqlSafef(query, sizeof(query), "delete from gbMemberToken where token = %u", token); +sqlUpdate(conn, query); +} + +static void insertToken(struct sqlConnection *conn, uint token, char *userName) +/* Add a new entry to gbMemberToken mapping token to userName. */ +{ +char query[512]; +sqlSafef(query, sizeof(query), "insert into gbMemberToken values (%u, '%s', now())", + token, userName); +sqlUpdate(conn, query); +} + +static uint newToken() +/* Return a random nonzero positive integer. */ +{ +srand(clock1000()); +uint token = (uint)rand(); +if (token == 0) + // highly unlikely - try again. + token = (uint)rand(); +return token; +} + +uint loginSystemLoginUser(char *userName) +/* Return a nonzero token which caller must set as the value of CFG_WIKI_LOGGED_IN_COOKIE. + * Call this when userName's password has been validated. */ +{ +struct sqlConnection *conn = hConnectCentral(); +alreadyAuthenticated = TRUE; +if (haveTokenTable(conn)) + { + authToken = newToken(); + insertToken(conn, authToken, userName); + } +else + // Fall back on gbMembers.idx + authToken = getMemberIdx(conn, userName); +hDisconnectCentral(&conn); +return authToken; +} + +static char *loginCookieDate() +/* For now, don't expire (before we retire :) Consider changing this to 6 months in the + * future or something like that, maybe under hg.conf control (for CIRM vs GB?). */ +{ +return "Thu, 31-Dec-2037 23:59:59 GMT"; +} + +static char *expiredCookieDate() +/* Return a date that passed long ago. */ +{ +return "Thu, 01-Jan-1970 00:00:00 GMT"; +} + +char *makeAuthCookieString() +/* Return a cookie string that sets cookie to authToken if token is valid and + * deletes/invalidates both cookies if not. */ +{ +struct dyString *dy = dyStringCreate("%s=", wikiLinkLoggedInCookie()); +if (authToken) + // Validated; send new token in cookie + dyStringPrintf(dy, "%u; path=/; domain=%s; expires=%s\r\n", + authToken, cgiServerName(), loginCookieDate()); +else + { + // Remove both cookies + dyStringPrintf(dy, "; path=/; domain=%s; expires=%s\r\n", + cgiServerName(), expiredCookieDate()); + dyStringPrintf(dy, "%s=; path=/; domain=%s; expires=%s\r\n", + wikiLinkUserNameCookie(), + cgiServerName(), expiredCookieDate()); + } +return dyStringCannibalize(&dy); +} + +char *loginSystemValidateCookies() +/* Return a cookie string or NULL. If login cookies are present and valid, but the current + * token has aged out, the returned cookie string sets a cookie to a new token value. + * If login cookies are present but invalid, the cookie string deletes/expires the cookies. + * Otherwise returns NULL. */ +{ +if (alreadyAuthenticated) + return makeAuthCookieString(); +alreadyAuthenticated = TRUE; +authToken = 0; +char *userName = findCookieData(wikiLinkUserNameCookie()); +uint cookieToken = getCookieToken(); +if (userName && cookieToken) + { + struct sqlConnection *conn = hConnectCentral(); + uint memberIdx = getMemberIdx(conn, userName); + char *cookieString = NULL; + if (haveTokenTable(conn)) + { + // Look up cookieToken and userName in gbMemberToken + boolean makeNewToken = FALSE; + boolean tokenIsValid = isValidToken(conn, cookieToken, userName, &makeNewToken); + if (tokenIsValid + // Also accept gbMembers.idx to smooth the transition; TODO: remove in July 2016 + || (cookieToken == memberIdx)) // TODO: remove in July 2016 + { + if (makeNewToken + || ! tokenIsValid) // TODO: remove in July 2016 + { + // Delete the old token, create and store a new token, and make a cookie string + // with the new token. + deleteToken(conn, cookieToken); + authToken = newToken(); + insertToken(conn, authToken, userName); + cookieString = makeAuthCookieString(); + } + else + // Keep using this token, no change to cookie + authToken = cookieToken; + } + else + // Invalid token; delete both cookies + cookieString = makeAuthCookieString(); + } + else if (cookieToken == memberIdx) + // centralDb does not have gbMemberToken table -- fall back on gbMembers.idx + authToken = cookieToken; + hDisconnectCentral(&conn); + return cookieString; + } +return NULL; +} + char *wikiLinkHost() /* Return the wiki host specified in hg.conf, or NULL. Allocd here. - * Returns hostname from http request if hg.conf entry is HTTPHOSTNAME. + * Returns hostname from http request if hg.conf entry is HTTPHOST. * */ { char *wikiHost = cfgOption(CFG_WIKI_HOST); if ((wikiHost!=NULL) && sameString(wikiHost, "HTTPHOST")) wikiHost = hHttpHost(); return cloneString(wikiHost); } boolean wikiLinkEnabled() /* Return TRUE if all wiki.* parameters are defined in hg.conf . */ { return ((cfgOption(CFG_WIKI_HOST) != NULL) && (cfgOption(CFG_WIKI_USER_NAME_COOKIE) != NULL) && (cfgOption(CFG_WIKI_LOGGED_IN_COOKIE) != NULL)); } -static char *wikiLinkUserNameCookie() -/* Return the cookie name specified in hg.conf as the wiki user name cookie. */ -{ -return cfgOption(CFG_WIKI_USER_NAME_COOKIE); -} - -static char *wikiLinkLoggedInCookie() -/* Return the cookie name specified in hg.conf as the wiki logged-in cookie. */ -{ -return cfgOption(CFG_WIKI_LOGGED_IN_COOKIE); -} - char *wikiLinkUserName() /* Return the user name specified in cookies from the browser, or NULL if * the user doesn't appear to be logged in. */ { if (wikiLinkEnabled()) { char *wikiUserName = findCookieData(wikiLinkUserNameCookie()); char *wikiLoggedIn = findCookieData(wikiLinkLoggedInCookie()); if (isNotEmpty(wikiLoggedIn) && isNotEmpty(wikiUserName)) { if (loginSystemEnabled()) { + if (! alreadyAuthenticated) + errAbort("wikiLinkUserName: loginSystemValidateCookies must be called first."); + if (authToken == 0) + return NULL; + } + else + { + // Wiki only: fall back on checking ID cookie vs. gbMembers.idx + uint cookieId = getCookieToken(); struct sqlConnection *conn = hConnectCentral(); - char query[512]; - sqlSafef(query, sizeof(query), "select idx from gbMembers where userName='%s'", - wikiUserName); - char buf[512]; - char *userId = sqlQuickQuery(conn, query, buf, sizeof(buf)); + uint memberIdx = getMemberIdx(conn, wikiUserName); hDisconnectCentral(&conn); - if (!sameString(userId, wikiLoggedIn)) + if (cookieId != memberIdx) return NULL; } return cloneString(wikiUserName); } } else errAbort("wikiLinkUserName called when wiki is not enabled (specified " "in hg.conf)."); return NULL; } static char *encodedHgSessionReturnUrl(char *hgsid) /* Return a CGI-encoded hgSession URL with hgsid. Free when done. */ { char retBuf[1024]; char *cgiDir = cgiScriptDirUrl(); safef(retBuf, sizeof(retBuf), "http%s://%s%shgSession?hgsid=%s", cgiAppendSForHttps(), cgiServerNamePort(), cgiDir, hgsid); return cgiEncode(retBuf); } char *wikiLinkUserLoginUrlReturning(char *hgsid, char *returnUrl) /* Return the URL for the wiki user login page. */ { char buf[2048]; if (loginSystemEnabled()) { if (! wikiLinkEnabled()) errAbort("wikiLinkUserLoginUrl called when login system is not enabled " "(specified in hg.conf)."); safef(buf, sizeof(buf), "http%s://%s/cgi-bin/hgLogin?hgLogin.do.displayLoginPage=1&returnto=%s", cgiAppendSForHttps(), wikiLinkHost(), returnUrl); } else { if (! wikiLinkEnabled()) errAbort("wikiLinkUserLoginUrl called when wiki is not enabled (specified " "in hg.conf)."); safef(buf, sizeof(buf), "http://%s/index.php?title=Special:UserloginUCSC&returnto=%s", wikiLinkHost(), returnUrl); } return(cloneString(buf)); } char *wikiLinkUserLoginUrl(char *hgsid) /* Return the URL for the wiki user login page with return going to hgSessions. */ { char *retUrl = encodedHgSessionReturnUrl(hgsid); char *result = wikiLinkUserLoginUrlReturning(hgsid, retUrl); freez(&retUrl); return result; } char *wikiLinkUserLogoutUrlReturning(char *hgsid, char *returnUrl) /* Return the URL for the wiki user logout page. */ { char buf[2048]; if (loginSystemEnabled()) { if (! wikiLinkEnabled()) errAbort("wikiLinkUserLogoutUrl called when login system is not enabled " "(specified in hg.conf)."); safef(buf, sizeof(buf), "http%s://%s/cgi-bin/hgLogin?hgLogin.do.displayLogout=1&returnto=%s", cgiAppendSForHttps(), wikiLinkHost(), returnUrl); } else { if (! wikiLinkEnabled()) errAbort("wikiLinkUserLogoutUrl called when wiki is not enable (specified " "in hg.conf)."); safef(buf, sizeof(buf), "http://%s/index.php?title=Special:UserlogoutUCSC&returnto=%s", wikiLinkHost(), returnUrl); } return(cloneString(buf)); } char *wikiLinkUserLogoutUrl(char *hgsid) /* Return the URL for the wiki user logout page that returns to hgSessions. */ { char *retEnc = encodedHgSessionReturnUrl(hgsid); char *result = wikiLinkUserLogoutUrlReturning(hgsid, retEnc); freez(&retEnc); return result; } char *wikiLinkUserSignupUrl(char *hgsid) /* Return the URL for the user signup page. */ { char buf[2048]; char *retEnc = encodedHgSessionReturnUrl(hgsid); if (loginSystemEnabled()) { if (! wikiLinkEnabled()) errAbort("wikiLinkUserSignupUrl called when login system is not enabled " "(specified in hg.conf)."); safef(buf, sizeof(buf), "http%s://%s/cgi-bin/hgLogin?hgLogin.do.signupPage=1&returnto=%s", cgiAppendSForHttps(), wikiLinkHost(), retEnc); } else { if (! wikiLinkEnabled()) errAbort("wikiLinkUserLogoutUrl called when wiki is not enable (specified " "in hg.conf)."); safef(buf, sizeof(buf), "http://%s/index.php?title=Special:UserlogoutUCSC&returnto=%s", wikiLinkHost(), retEnc); } freez(&retEnc); return(cloneString(buf)); } char *wikiLinkChangePasswordUrl(char *hgsid) /* Return the URL for the user change password page. */ { char buf[2048]; char *retEnc = encodedHgSessionReturnUrl(hgsid); if (loginSystemEnabled()) { if (! wikiLinkEnabled()) errAbort("wikiLinkChangePasswordUrl called when login system is not enabled " "(specified in hg.conf)."); safef(buf, sizeof(buf), "http%s://%s/cgi-bin/hgLogin?hgLogin.do.changePasswordPage=1&returnto=%s", cgiAppendSForHttps(), wikiLinkHost(), retEnc); } else { if (! wikiLinkEnabled()) errAbort("wikiLinkUserLogoutUrl called when wiki is not enable (specified " "in hg.conf)."); safef(buf, sizeof(buf), "http://%s/index.php?title=Special:UserlogoutUCSC&returnto=%s", wikiLinkHost(), retEnc); } freez(&retEnc); return(cloneString(buf)); }