Просмотр исходного кода

HTTP authenfication fixes (#2204)

* Security fixes when HTTP user does not exist in FreshRSS
* Accept HTTP header X-WebAuth-User for delegated HTTP Authentication (e.g. Træfik)
* Document delegated HTTP authentication from https://github.com/FreshRSS/FreshRSS/pull/2202
Alexandre Alapetite 7 лет назад
Родитель
Сommit
945cf832ad

+ 36 - 0
Docker/README.md

@@ -205,6 +205,42 @@ sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
 
 ## More deployment options
 
+### Use HTTP-based login (advanced users)
+
+FreshRSS allows logins using either a Web form (easiest) or based on HTTP authentication.
+If you want HTTP authentication, [Træfik can do it](https://docs.traefik.io/configuration/entrypoints/#authentication) (otherwise, see section below for giving this task to Apache inside the FreshRSS Docker image):
+
+```
+sudo docker run ...
+  --label traefik.frontend.auth.basic.users='admin:$2y$05$BJ3eexf8gkyfHR1L38nVMeQ2RbQ5PF6KW4/PlttXeR6IOGZKH4sbC,alice:$2y$05$0vv8eexRq4qujzyBCYh6a.bo/KUvuXCmjJ54RqEHBApaHdQrpzFJC' \
+  --label traefik.frontend.auth.removeheader=true \
+  --label traefik.frontend.auth.headerField=X-WebAuth-User \
+  --name freshrss freshrss/freshrss
+```
+
+N.B.: You can create password hashes for instance with: `htpasswd -nB alice`
+
+### Custom Apache configuration (advanced users)
+
+Changes in Apache `.htaccess` files are applied when restarting the container.
+In particular, if you want FreshRSS to use HTTP-based login (instead of the easier Web form login, and instead of letting Træfik do it), you can mount your own `./FreshRSS/p/i/.htaccess`:
+
+```
+sudo docker run ...
+  -v ./your/.htaccess:/var/www/FreshRSS/p/i/.htaccess \
+  -v ./your/.htpasswd:/var/www/FreshRSS/data/.htpasswd \
+  ...
+  --name freshrss freshrss/freshrss
+```
+
+Example of `./your/.htaccess` referring to `./your/.htpasswd`:
+```
+AuthUserFile /var/www/FreshRSS/data/.htpasswd
+AuthName "FreshRSS"
+AuthType Basic
+Require valid-user
+```
+
 ### Example with [docker-compose](https://docs.docker.com/compose/)
 
 A [docker-compose.yml](docker-compose.yml) file is given as an example, using PostgreSQL. In order to use it, you have to adapt:

+ 5 - 1
app/Controllers/authController.php

@@ -79,8 +79,12 @@ class FreshRSS_auth_Controller extends Minz_ActionController {
 			Minz_Request::forward(array('c' => 'auth', 'a' => 'formLogin'));
 			break;
 		case 'http_auth':
+			Minz_Error::error(403, array('error' => array(_t('feedback.access.denied'),
+					' [HTTP Remote-User=' . htmlspecialchars(httpAuthUser(), ENT_NOQUOTES, 'UTF-8') . ']'
+				)), false);
+			break;
 		case 'none':
-			// It should not happened!
+			// It should not happen!
 			Minz_Error::error(404);
 		default:
 			// TODO load plugin instead

+ 5 - 4
app/Models/Auth.php

@@ -28,13 +28,13 @@ class FreshRSS_Auth {
 
 		if (self::$login_ok) {
 			self::giveAccess();
-		} elseif (self::accessControl()) {
-			self::giveAccess();
+		} elseif (self::accessControl() && self::giveAccess()) {
 			FreshRSS_UserDAO::touch();
 		} else {
 			// Be sure all accesses are removed!
 			self::removeAccess();
 		}
+		return self::$login_ok;
 	}
 
 	/**
@@ -60,7 +60,7 @@ class FreshRSS_Auth {
 			return $current_user != '';
 		case 'http_auth':
 			$current_user = httpAuthUser();
-			$login_ok = $current_user != '';
+			$login_ok = $current_user != '' && FreshRSS_UserDAO::exists($current_user);
 			if ($login_ok) {
 				Minz_Session::_param('currentUser', $current_user);
 			}
@@ -81,7 +81,7 @@ class FreshRSS_Auth {
 		$user_conf = get_user_configuration($current_user);
 		if ($user_conf == null) {
 			self::$login_ok = false;
-			return;
+			return false;
 		}
 		$system_conf = Minz_Configuration::get('system');
 
@@ -102,6 +102,7 @@ class FreshRSS_Auth {
 
 		Minz_Session::_param('loginOk', self::$login_ok);
 		Minz_Session::_param('REMOTE_USER', httpAuthUser());
+		return self::$login_ok;
 	}
 
 	/**

+ 5 - 5
app/Models/UserDAO.php

@@ -65,7 +65,7 @@ class FreshRSS_UserDAO extends Minz_ModelPdo {
 		require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
 
 		if ($db['type'] === 'sqlite') {
-			return unlink(join_path(DATA_PATH, 'users', $username, 'db.sqlite'));
+			return unlink(USERS_PATH . '/' . $username . '/db.sqlite');
 		} else {
 			$userPDO = new Minz_ModelPdo($username);
 
@@ -81,18 +81,18 @@ class FreshRSS_UserDAO extends Minz_ModelPdo {
 		}
 	}
 
-	public static function exist($username) {
-		return is_dir(join_path(DATA_PATH, 'users', $username));
+	public static function exists($username) {
+		return is_dir(USERS_PATH . '/' . $username);
 	}
 
 	public static function touch($username = '') {
 		if (!FreshRSS_user_Controller::checkUsername($username)) {
 			$username = Minz_Session::param('currentUser', '_');
 		}
-		return touch(join_path(DATA_PATH, 'users', $username, 'config.php'));
+		return touch(USERS_PATH . '/' . $username . '/config.php');
 	}
 
 	public static function mtime($username) {
-		return @filemtime(join_path(DATA_PATH, 'users', $username, 'config.php'));
+		return @filemtime(USERS_PATH . '/' . $username . '/config.php');
 	}
 }

+ 5 - 12
lib/Minz/Configuration.php

@@ -27,23 +27,16 @@ class Minz_Configuration {
 	/**
 	 * Parse a file and return its data.
 	 *
-	 * If the file does not contain a valid PHP code returning an array, an
-	 * empty array is returned anyway.
-	 *
 	 * @param $filename the name of the file to parse.
 	 * @return an array of values
-	 * @throws Minz_FileNotExistException if the file does not exist.
+	 * @throws Minz_FileNotExistException if the file does not exist or is invalid.
 	 */
 	public static function load($filename) {
-		if (!file_exists($filename)) {
-			throw new Minz_FileNotExistException($filename);
-		}
-
-		$data = include($filename);
+		$data = @include($filename);
 		if (is_array($data)) {
 			return $data;
 		} else {
-			return array();
+			throw new Minz_FileNotExistException($filename);
 		}
 	}
 
@@ -117,7 +110,7 @@ class Minz_Configuration {
 		$this->default_filename = $default_filename;
 		$this->_configurationSetter($configuration_setter);
 
-		if (!is_null($this->default_filename)) {
+		if ($this->default_filename != null) {
 			$this->data = self::load($this->default_filename);
 		}
 
@@ -126,7 +119,7 @@ class Minz_Configuration {
 				$this->data, self::load($this->config_filename)
 			);
 		} catch (Minz_FileNotExistException $e) {
-			if (is_null($this->default_filename)) {
+			if ($this->default_filename == null) {
 				throw $e;
 			}
 		}

+ 6 - 7
lib/lib_rss.php

@@ -364,9 +364,9 @@ function get_user_configuration($username) {
 		                             join_path(FRESHRSS_PATH, 'config-user.default.php'));
 	} catch (Minz_ConfigurationNamespaceException $e) {
 		// namespace already exists, do nothing.
-		Minz_Log::warning($e->getMessage());
+		Minz_Log::warning($e->getMessage(), USERS_PATH . '/_/log.txt');
 	} catch (Minz_FileNotExistException $e) {
-		Minz_Log::warning($e->getMessage());
+		Minz_Log::warning($e->getMessage(), USERS_PATH . '/_/log.txt');
 		return null;
 	}
 
@@ -375,14 +375,13 @@ function get_user_configuration($username) {
 
 
 function httpAuthUser() {
-	if (isset($_SERVER['REMOTE_USER'])) {
+	if (!empty($_SERVER['REMOTE_USER'])) {
 		return $_SERVER['REMOTE_USER'];
-	}
-
-	if (isset($_SERVER['REDIRECT_REMOTE_USER'])) {
+	} elseif (!empty($_SERVER['REDIRECT_REMOTE_USER'])) {
 		return $_SERVER['REDIRECT_REMOTE_USER'];
+	} elseif (!empty($_SERVER['HTTP_X_WEBAUTH_USER'])) {
+		return $_SERVER['HTTP_X_WEBAUTH_USER'];
 	}
-
 	return '';
 }