From 0ae8ccf657757a00c48fe45add77e98119e3c5fe Mon Sep 17 00:00:00 2001 From: Michael Peter Christen Date: Mon, 25 Jan 2021 11:49:21 +0100 Subject: [PATCH] Make it possible to set an empty password disabling the authentication protocol completely If you set now an empty password, then the http server will not ask to authentify. This is required for environment where we attach an outside authentification service like keycloak or similar using authentication in an ingress proxy. This change is part of the approach to run YaCy inside of a kubernetes cluster where we do not want individual authentication of peers and want to apply a ingress authentication. --- htroot/ConfigAccounts_p.java | 13 ++--- htroot/ConfigUser_p.java | 7 +-- htroot/SettingsAck_p.java | 2 +- htroot/User.java | 6 +-- source/net/yacy/data/TransactionManager.java | 52 +++++++++++-------- .../yacy/http/Jetty9YaCySecurityHandler.java | 11 ++-- source/net/yacy/search/Switchboard.java | 19 +++++++ source/net/yacy/yacy.java | 2 +- 8 files changed, 68 insertions(+), 44 deletions(-) diff --git a/htroot/ConfigAccounts_p.java b/htroot/ConfigAccounts_p.java index 5c8fc4e7f..1df7ca608 100644 --- a/htroot/ConfigAccounts_p.java +++ b/htroot/ConfigAccounts_p.java @@ -74,11 +74,11 @@ public class ConfigAccounts_p { final String pw2 = post.get("adminpw2", ""); int inputerror=0; // may be overwritten if new password is given - if (user.length() > 0 && pw1.length() > 2 && pw1.equals(pw2)) { + if (user.length() > 0 && pw1.equals(pw2)) { String oldusername = env.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME,user); // check passed. set account: // old: // env.setConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, Digest.encodeMD5Hex(Base64Order.standardCoder.encodeString(user + ":" + pw1))); - env.setConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, "MD5:"+Digest.encodeMD5Hex(user + ":" + sb.getConfig(SwitchboardConstants.ADMIN_REALM,"YaCy")+":"+ pw1)); + env.setConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, sb.encodeDigestAuth(user, pw1)); env.setConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME,user); // make sure server accepts new credentials Jetty9HttpServerImpl jhttpserver = (Jetty9HttpServerImpl)sb.getHttpServer(); @@ -210,7 +210,7 @@ public class ConfigAccounts_p { if (!"".equals(pw1)) { //change only if set // MD5 according to HTTP Digest RFC 2617 (3.2.2) name:realm:pwd (use seed.hash as realm) // with prefix of encoding method (supported MD5: ) - mem.put(UserDB.Entry.MD5ENCODED_USERPWD_STRING, "MD5:"+Digest.encodeMD5Hex(username + ":" + sb.getConfig(SwitchboardConstants.ADMIN_REALM,"YaCy")+":"+pw1)); + mem.put(UserDB.Entry.MD5ENCODED_USERPWD_STRING, sb.encodeDigestAuth(username, pw1)); } mem.put(UserDB.Entry.USER_FIRSTNAME, firstName); @@ -238,11 +238,8 @@ public class ConfigAccounts_p { if (entry != null) { try{ - if (!"".equals(pw1)) { - // with prefix of encoding method (supported MD5: ) - entry.setProperty(UserDB.Entry.MD5ENCODED_USERPWD_STRING, "MD5:"+Digest.encodeMD5Hex(username+ ":" + sb.getConfig(SwitchboardConstants.ADMIN_REALM,"YaCy") + ":"+pw1)); - } - + // with prefix of encoding method (supported MD5: ) + entry.setProperty(UserDB.Entry.MD5ENCODED_USERPWD_STRING, sb.encodeDigestAuth(username, pw1)); entry.setProperty(UserDB.Entry.USER_FIRSTNAME, firstName); entry.setProperty(UserDB.Entry.USER_LASTNAME, lastName); entry.setProperty(UserDB.Entry.USER_ADDRESS, address); diff --git a/htroot/ConfigUser_p.java b/htroot/ConfigUser_p.java index 134b1bf16..6818dc5e9 100644 --- a/htroot/ConfigUser_p.java +++ b/htroot/ConfigUser_p.java @@ -121,11 +121,8 @@ public class ConfigUser_p { if (entry != null) { try { - if (!"".equals(pw1)) { - // with prefix of encoding method (supported MD5: ) - entry.setProperty(UserDB.Entry.MD5ENCODED_USERPWD_STRING, "MD5:" + Digest.encodeMD5Hex(username + ":" + sb.getConfig(SwitchboardConstants.ADMIN_REALM, "YaCy") + ":" + pw1)); - } - + // with prefix of encoding method (supported MD5: ) + entry.setProperty(UserDB.Entry.MD5ENCODED_USERPWD_STRING, sb.encodeDigestAuth(username, pw1)); entry.setProperty(UserDB.Entry.USER_FIRSTNAME, firstName); entry.setProperty(UserDB.Entry.USER_LASTNAME, lastName); entry.setProperty(UserDB.Entry.USER_ADDRESS, address); diff --git a/htroot/SettingsAck_p.java b/htroot/SettingsAck_p.java index 745367801..13f16fafd 100644 --- a/htroot/SettingsAck_p.java +++ b/htroot/SettingsAck_p.java @@ -91,7 +91,7 @@ public class SettingsAck_p { return prop; } // check passed. set account: - env.setConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, "MD5:"+Digest.encodeMD5Hex(user + ":" + sb.getConfig(SwitchboardConstants.ADMIN_REALM,"YaCy") + ":" + pw1)); + env.setConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, sb.encodeDigestAuth(user, pw1)); env.setConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME, user); prop.put("info", "5");//admin account changed prop.putHTML("info_user", user); diff --git a/htroot/User.java b/htroot/User.java index f38bb276d..5f6fb9daa 100644 --- a/htroot/User.java +++ b/htroot/User.java @@ -134,12 +134,12 @@ public class User{ prop.put("status", "1"); //password if (entry.getMD5EncodedUserPwd().startsWith("MD5:") ? - entry.getMD5EncodedUserPwd().equals("MD5:"+Digest.encodeMD5Hex(entry.getUserName() + ":" + sb.getConfig(SwitchboardConstants.ADMIN_REALM,"YaCy") + ":" + post.get("oldpass", ""))) : - entry.getMD5EncodedUserPwd().equals(Digest.encodeMD5Hex(entry.getUserName() + ":" + post.get("oldpass", "")))) { + entry.getMD5EncodedUserPwd().equals(sb.encodeDigestAuth(entry.getUserName(), post.get("oldpass", ""))) : + entry.getMD5EncodedUserPwd().equals(sb.encodeBasicAuth(entry.getUserName() , post.get("oldpass", "")))) { if (post.get("newpass").equals(post.get("newpass2"))) { if (!post.get("newpass", "").equals("")) { try { - entry.setProperty(UserDB.Entry.MD5ENCODED_USERPWD_STRING, "MD5:" + Digest.encodeMD5Hex(entry.getUserName() + ":" + sb.getConfig(SwitchboardConstants.ADMIN_REALM,"YaCy") + ":" + post.get("newpass", ""))); + entry.setProperty(UserDB.Entry.MD5ENCODED_USERPWD_STRING, sb.encodeDigestAuth(entry.getUserName(), post.get("newpass", ""))); prop.put("status_password", "0"); //changes } catch (final Exception e) { ConcurrentLog.logException(e); diff --git a/source/net/yacy/data/TransactionManager.java b/source/net/yacy/data/TransactionManager.java index 2a5487ba5..6b5f21c18 100644 --- a/source/net/yacy/data/TransactionManager.java +++ b/source/net/yacy/data/TransactionManager.java @@ -63,28 +63,36 @@ public class TransactionManager { */ private static String getUserName(final RequestHeader header) { String userName = header.getRemoteUser(); - - if (userName == null && header.accessFromLocalhost() && Switchboard.getSwitchboard() != null) { - final String adminAccountUserName = Switchboard.getSwitchboard().getConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME, "admin"); - final String adminAccountBase64MD5 = Switchboard.getSwitchboard().getConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, ""); - - if(Switchboard.getSwitchboard() - .getConfigBool(SwitchboardConstants.ADMIN_ACCOUNT_FOR_LOCALHOST, false)) { - /* Unauthenticated local access as administrator can be enabled */ - userName = adminAccountUserName; - } else { - /* authorization by encoded password, only for localhost access (used by bash scripts)*/ - String pass = Base64Order.standardCoder.encodeString(adminAccountUserName + ":" + adminAccountBase64MD5); - - /* get the authorization string from the header */ - final String realmProp = (header.get(RequestHeader.AUTHORIZATION, "")).trim(); - final String realmValue = realmProp.isEmpty() ? null : realmProp.substring(6); // take out "BASIC " - - if (pass.equals(realmValue)) { // assume realmValue as is in cfg - userName = adminAccountUserName; - } - } - } + Switchboard sb = Switchboard.getSwitchboard(); + + if (sb != null) { + final String adminAccountBase64MD5 = sb.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, ""); + final String adminAccountUserName = sb.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME, "admin"); + if (adminAccountBase64MD5.equals(sb.emptyPasswordAdminAccount)) { + // admin users with empty passwords do not need to authentify, thus do not have + // this header present. We just consoder the name is "admin" + userName = adminAccountUserName; + } + + if (userName == null && header.accessFromLocalhost()) { + + if (sb.getConfigBool(SwitchboardConstants.ADMIN_ACCOUNT_FOR_LOCALHOST, false)) { + /* Unauthenticated local access as administrator can be enabled */ + userName = adminAccountUserName; + } else { + /* authorization by encoded password, only for localhost access (used by bash scripts)*/ + String pass = Base64Order.standardCoder.encodeString(adminAccountUserName + ":" + adminAccountBase64MD5); + + /* get the authorization string from the header */ + final String realmProp = (header.get(RequestHeader.AUTHORIZATION, "")).trim(); + final String realmValue = realmProp.isEmpty() ? null : realmProp.substring(6); // take out "BASIC " + + if (pass.equals(realmValue)) { // assume realmValue as is in cfg + userName = adminAccountUserName; + } + } + } + } return userName; } diff --git a/source/net/yacy/http/Jetty9YaCySecurityHandler.java b/source/net/yacy/http/Jetty9YaCySecurityHandler.java index cb58ef7ff..3196a618d 100644 --- a/source/net/yacy/http/Jetty9YaCySecurityHandler.java +++ b/source/net/yacy/http/Jetty9YaCySecurityHandler.java @@ -76,14 +76,18 @@ public class Jetty9YaCySecurityHandler extends ConstraintSecurityHandler { // ! note : accessFromLocalhost compares localhost ip pattern final boolean grantedForLocalhost = adminAccountGrantedForLocalhost && accessFromLocalhost; - /* Even when all pages are protected, we don't want to block those used for peer-to-peer or cluster communication (except in private robinson mode) - * (examples : /yacy/hello.html is required for p2p and cluster network presence and /solr/select for remote Solr search requests) */ + // Even when all pages are protected, we don't want to block those used for peer-to-peer or cluster communication (except in private robinson mode) + // (examples : /yacy/hello.html is required for p2p and cluster network presence and /solr/select for remote Solr search requests) boolean protectedPage = (adminAccountNeededForAllPages && ((sb.isRobinsonMode() && !sb.isPublicRobinson()) || !(pathInContext.startsWith("/yacy/") || pathInContext.startsWith("/solr/")))); - /* Pages suffixed with "_p" are by the way always considered protected */ + // Pages suffixed with "_p" are by the way always considered protected protectedPage = protectedPage || (pathInContext.indexOf("_p.") > 0); + // ..except that the password for the admin account is empty + final String adminAccountBase64MD5 = sb.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, ""); + protectedPage = protectedPage && !adminAccountBase64MD5.equals(sb.emptyPasswordAdminAccount); + // check "/gsa" and "/solr" if not publicSearchpage if (!protectedPage && !sb.getConfigBool(SwitchboardConstants.PUBLIC_SEARCHPAGE, true)) { protectedPage = pathInContext.startsWith("/solr/") || pathInContext.startsWith("/gsa/"); @@ -97,7 +101,6 @@ public class Jetty9YaCySecurityHandler extends ConstraintSecurityHandler { final String credentials = request.getHeader(RequestHeader.AUTHORIZATION); if (credentials != null && credentials.length() < 120 && credentials.startsWith("Basic ")) { // Basic credentials are short "Basic " + b64(user:pwd) final String foruser = sb.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME, "admin"); - final String adminAccountBase64MD5 = sb.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, ""); final String b64 = Base64Order.standardCoder.encodeString(foruser + ":" + adminAccountBase64MD5); // TODO: is this valid? ; consider "MD5:" prefixed config if ((credentials.substring(6)).equals(b64)) return null; // lazy authentication for local access with credential from config (only a user with read access to DATA can do that) } diff --git a/source/net/yacy/search/Switchboard.java b/source/net/yacy/search/Switchboard.java index 352548bfe..4f61ce188 100644 --- a/source/net/yacy/search/Switchboard.java +++ b/source/net/yacy/search/Switchboard.java @@ -324,6 +324,7 @@ public final class Switchboard extends serverSwitch { private boolean startupAction = true; // this is set to false after the first event private static Switchboard sb; public HashMap crawlJobsStatus = new HashMap(); + public String emptyPasswordAdminAccount; public Switchboard(final File dataPath, final File appPath, final String initPath, final String configPath) { super(dataPath, appPath, initPath, configPath); @@ -456,6 +457,9 @@ public final class Switchboard extends serverSwitch { } }.start(); + // define the "non-password password" + emptyPasswordAdminAccount = encodeDigestAuth(getConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME,"admin"), ""); + // init the language detector this.log.config("Loading language profiles"); try { @@ -3980,6 +3984,8 @@ public final class Switchboard extends serverSwitch { * - access from localhost is granted and access comes from localhost: auth-level 3 * - a password is configured and access comes from localhost and the realm-value * of a http-authentify String is equal to the stored base64MD5: auth-level 3 + * - an empty password is configured an access comes from anywhere: auth-level 3 + * This may be used in cluster installations where the cluster has an outside protection but inside is none needed. * - a password is configured and access comes with matching http-authentify: auth-level 4 * * @param requestHeader @@ -4006,6 +4012,11 @@ public final class Switchboard extends serverSwitch { return 2; // no password stored; this should not happen for older peers } + // authorization in case that administrators have stored an empty password; this authorizes all users as admin regardless of the give auth + if (adminAccountBase64MD5.equals(emptyPasswordAdminAccount)) { + return 3; // everyone is admin from everywhere + } + // authorization for localhost, only if flag is set to grant localhost access as admin final boolean accessFromLocalhost = requestHeader.accessFromLocalhost(); if (accessFromLocalhost && getConfigBool(SwitchboardConstants.ADMIN_ACCOUNT_FOR_LOCALHOST, false)) { @@ -4083,6 +4094,14 @@ public final class Switchboard extends serverSwitch { return 1; } + public String encodeDigestAuth(String user, String pw) { + return "MD5:" + Digest.encodeMD5Hex(user + ":" + sb.getConfig(SwitchboardConstants.ADMIN_REALM,"YaCy") + ":" + pw); + } + + public String encodeBasicAuth(String user, String pw) { + return Digest.encodeMD5Hex(user + ":" + pw); + } + /** * @param header servlet request headers * @return true when the headers contains valid admin authentication information diff --git a/source/net/yacy/yacy.java b/source/net/yacy/yacy.java index d114fef92..6b9a5e0d2 100644 --- a/source/net/yacy/yacy.java +++ b/source/net/yacy/yacy.java @@ -791,7 +791,7 @@ public final class yacy { } else { username = ss.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME, "admin"); } - ss.setConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, "MD5:" + Digest.encodeMD5Hex(username + ":" + ss.getConfig(SwitchboardConstants.ADMIN_REALM, "YaCy") + ":" + pwdtxt)); + ss.setConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, sb.encodeDigestAuth(username, pwdtxt)); System.out.println("Set property " + SwitchboardConstants.ADMIN_ACCOUNT_B64MD5 + " = " + ss.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, "")); } } else {