Proxy authentication

This commit is contained in:
Daniel Brendel
2024-09-05 12:30:05 +02:00
parent c5105f0898
commit ecc7cdd5d7
11 changed files with 225 additions and 3 deletions

View File

@@ -8,6 +8,19 @@
* Command handler class
*/
class MigrationUpgrade implements Asatru\Commands\Command {
/**
* @return void
*/
public function upgradeTo3dot5()
{
AppModel::raw('ALTER TABLE `' . AppModel::tableName() . '` ADD COLUMN IF NOT EXISTS auth_proxy_enable BOOLEAN NOT NULL DEFAULT 0');
AppModel::raw('ALTER TABLE `' . AppModel::tableName() . '` ADD COLUMN IF NOT EXISTS auth_proxy_header_email VARCHAR(512) NULL');
AppModel::raw('ALTER TABLE `' . AppModel::tableName() . '` ADD COLUMN IF NOT EXISTS auth_proxy_header_username VARCHAR(512) NULL');
AppModel::raw('ALTER TABLE `' . AppModel::tableName() . '` ADD COLUMN IF NOT EXISTS auth_proxy_sign_up BOOLEAN NOT NULL DEFAULT 0');
AppModel::raw('ALTER TABLE `' . AppModel::tableName() . '` ADD COLUMN IF NOT EXISTS auth_proxy_whitelist TEXT NULL');
AppModel::raw('ALTER TABLE `' . AppModel::tableName() . '` ADD COLUMN IF NOT EXISTS auth_proxy_hide_logout BOOLEAN NOT NULL DEFAULT 0');
}
/**
* @return void
*/

View File

@@ -115,6 +115,7 @@ return [
array('/admin/location/add', 'POST', 'admin@add_location'),
array('/admin/location/update', 'POST', 'admin@update_location'),
array('/admin/location/remove', 'ANY', 'admin@remove_location'),
array('/admin/auth/proxy/save', 'POST', 'admin@save_proxy_auth_settings'),
array('/admin/attribute/schema/add', 'POST', 'admin@add_attribute_schema'),
array('/admin/attribute/schema/edit', 'POST', 'admin@edit_attribute_schema'),
array('/admin/attribute/schema/remove', 'ANY', 'admin@remove_attribute_schema'),

View File

@@ -26,6 +26,15 @@ class BaseController extends Asatru\Controller\Controller {
app_mail_config();
app_set_timezone();
if (app('auth_proxy_enable')) {
try {
UserModel::performProxyAuth();
} catch (\Exception $e) {
http_response_code(401);
exit($e->getMessage());
}
}
$auth_user = UserModel::getAuthUser();
if (!$auth_user) {
$url = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

View File

@@ -302,6 +302,42 @@ class AdminController extends BaseController {
}
}
/**
* Handles URL: /admin/auth/proxy/save
*
* @param Asatru\Controller\ControllerArg $request
* @return Asatru\View\RedirectHandler
*/
public function save_proxy_auth_settings($request)
{
try {
$auth_proxy_enable = (bool)$request->params()->query('auth_proxy_enable', 0);
$auth_proxy_header_email = $request->params()->query('auth_proxy_header_email', '');
$auth_proxy_header_username = $request->params()->query('auth_proxy_header_username', '');
$auth_proxy_sign_up = (bool)$request->params()->query('auth_proxy_sign_up', 0);
$auth_proxy_whitelist = $request->params()->query('auth_proxy_whitelist', '');
$auth_proxy_hide_logout = (bool)$request->params()->query('auth_proxy_hide_logout', 0);
$set = [
'auth_proxy_enable' => $auth_proxy_enable,
'auth_proxy_header_email' => $auth_proxy_header_email,
'auth_proxy_header_username' => $auth_proxy_header_username,
'auth_proxy_sign_up' => $auth_proxy_sign_up,
'auth_proxy_whitelist' => $auth_proxy_whitelist,
'auth_proxy_hide_logout' => $auth_proxy_hide_logout
];
AppModel::updateSet($set);
FlashMessage::setMsg('success', __('app.proxy_auth_settings_saved_successfully'));
return redirect('/admin?tab=auth');
} catch (\Exception $e) {
FlashMessage::setMsg('error', $e->getMessage());
return redirect('/admin?tab=auth');
}
}
/**
* Handles URL: /admin/attribute/schema/add
*

View File

@@ -423,5 +423,14 @@ return [
'confirm_remove_shared_photo' => 'Do you really want to remove this item?',
'share_photo_public' => 'Share this photo publicly',
'share_photo_description' => 'Description',
'share_photo_keywords' => 'Keywords'
'share_photo_keywords' => 'Keywords',
'auth' => 'Auth',
'auth_proxy_enable' => 'Enable Proxy Authentication',
'auth_proxy_header_email' => 'HTTP Header for E-mail',
'auth_proxy_header_username' => 'HTTP Header for Username',
'auth_proxy_sign_up' => 'Automatically register unknown users',
'auth_proxy_whitelist' => 'Proxy Address Whitelist',
'auth_proxy_hide_logout' => 'Hide logout button',
'proxy_auth_settings_saved_successfully' => 'Proxy authentication settings saved successfully',
'auth_proxy_warning' => 'Warning: Be sure that your workspace instance is protected behind a proxy before enabling this feature'
];

View File

@@ -61,6 +61,12 @@ class AppModel_Migration {
$this->database->add('system_message_plant_log BOOLEAN NOT NULL DEFAULT 1');
$this->database->add('auto_backup BOOLEAN NOT NULL DEFAULT 0');
$this->database->add('backup_path VARCHAR(1024) NULL');
$this->database->add('auth_proxy_enable BOOLEAN NOT NULL DEFAULT 0');
$this->database->add('auth_proxy_header_email VARCHAR(512) NULL');
$this->database->add('auth_proxy_header_username VARCHAR(512) NULL');
$this->database->add('auth_proxy_sign_up BOOLEAN NOT NULL DEFAULT 0');
$this->database->add('auth_proxy_whitelist TEXT NULL');
$this->database->add('auth_proxy_hide_logout BOOLEAN NOT NULL DEFAULT 0');
$this->database->add('created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP');
$this->database->create();
}

View File

@@ -28,6 +28,66 @@ class UserModel extends \Asatru\Database\Model {
}
}
/**
* @return void
* @throws \Exception
*/
public static function performProxyAuth()
{
try {
if (!app('auth_proxy_enable')) {
throw new \Exception('Feature is currently not active');
}
$whitelist = app('auth_proxy_whitelist');
if ((is_string($whitelist)) && (strlen($whitelist) > 0)) {
$accepted = false;
$remote_addr = $_SERVER['REMOTE_ADDR'];
$whitelist = explode(PHP_EOL, $whitelist);
foreach ($whitelist as $address) {
if ($address === $remote_addr) {
$accepted = true;
break;
}
}
if (!$accepted) {
throw new \Exception('Unauthorized Proxy: ' . $remote_addr);
}
}
$header_email = $_SERVER['HTTP_' . strtoupper(str_replace('-', '_', app('auth_proxy_header_email')))] ?? null;
if ((!is_string($header_email)) || (strlen($header_email) == 0)) {
throw new \Exception('Invalid E-Mail header value provided: ' . $header_email);
}
$header_username = $_SERVER['HTTP_' . strtoupper(str_replace('-', '_', app('auth_proxy_header_username')))] ?? null;
if ((!is_string($header_username)) || (strlen($header_username) == 0)) {
throw new \Exception('Invalid username header value provided: ' . $header_username);
}
$authuser = static::raw('SELECT * FROM `' . self::tableName() . '` WHERE email = ?', [$header_email])->first();
if (!$authuser) {
if (app('auth_proxy_sign_up')) {
static::createUser($header_username, $header_email, false);
$authuser = static::raw('SELECT * FROM `' . self::tableName() . '` WHERE email = ?', [$header_email])->first();
} else {
throw new \Exception('User not found: ' . $header_email);
}
}
if (!$header_username !== $authuser->get('name')) {
static::raw('UPDATE `' . self::tableName() . '` SET name = ? WHERE id = ?', [$header_username, $authuser->get('id')]);
}
SessionModel::loginSession($authuser->get('id'), session_id());
} catch (\Exception $e) {
throw $e;
}
}
/**
* @param $email
* @param $password

View File

@@ -2443,6 +2443,36 @@ fieldset .field {
margin-top: 20px;
}
.admin-auth {
position: relative;
}
.admin-auth h2 {
margin-top: 20px;
margin-bottom: 10px;
}
.admin-auth label, .admin-auth span {
color: rgb(150, 150, 150);
}
.admin-auth input, .admin-auth textarea {
color: rgb(150, 150, 150);
background-color: rgb(50, 50, 50);
}
.admin-auth-warning {
position: relative;
display: none;
padding: 20px;
margin-top: 30px;
margin-bottom: 25px;
color: rgb(250, 250, 250);
background-color: rgba(202, 102, 102, 0.76);
border: 1px solid rgb(235, 150, 150);
border-radius: 5px;
}
.admin-attribute-schema-list {
position: relative;
}
@@ -2861,7 +2891,7 @@ fieldset .field {
margin-top: 30px;
color: rgb(250, 250, 250);
background-color: rgba(102, 202, 160, 0.76);
border: 1px solid rgb(150, 236, 200);
border: 1px solid rgb(150, 235, 200);
border-radius: 10px;
}

View File

@@ -8,6 +8,7 @@
<li class="admin-tab-media {{ ((isset($_GET['tab'])) && ($_GET['tab'] === 'media')) ? 'is-active' : ''}}"><a href="javascript:void(0);" onclick="window.vue.switchAdminTab('media');">{{ __('app.admin_media') }}</a></li>
<li class="admin-tab-users {{ ((isset($_GET['tab'])) && ($_GET['tab'] === 'users')) ? 'is-active' : ''}}"><a href="javascript:void(0);" onclick="window.vue.switchAdminTab('users');">{{ __('app.users') }}</a></li>
<li class="admin-tab-locations {{ ((isset($_GET['tab'])) && ($_GET['tab'] === 'locations')) ? 'is-active' : ''}}"><a href="javascript:void(0);" onclick="window.vue.switchAdminTab('locations');">{{ __('app.locations') }}</a></li>
<li class="admin-tab-auth {{ ((isset($_GET['tab'])) && ($_GET['tab'] === 'auth')) ? 'is-active' : ''}}"><a href="javascript:void(0);" onclick="window.vue.switchAdminTab('auth');">{{ __('app.auth') }}</a></li>
<li class="admin-tab-attributes {{ ((isset($_GET['tab'])) && ($_GET['tab'] === 'attributes')) ? 'is-active' : ''}}"><a href="javascript:void(0);" onclick="window.vue.switchAdminTab('attributes');">{{ __('app.attributes') }}</a></li>
<li class="admin-tab-calendar {{ ((isset($_GET['tab'])) && ($_GET['tab'] === 'calendar')) ? 'is-active' : ''}}"><a href="javascript:void(0);" onclick="window.vue.switchAdminTab('calendar');">{{ __('app.calendar') }}</a></li>
<li class="admin-tab-mail {{ ((isset($_GET['tab'])) && ($_GET['tab'] === 'mail')) ? 'is-active' : ''}}"><a href="javascript:void(0);" onclick="window.vue.switchAdminTab('mail');">{{ __('app.mail') }}</a></li>
@@ -324,6 +325,61 @@
</div>
</div>
<div class="admin-auth {{ ((!isset($_GET['tab'])) || ($_GET['tab'] !== 'auth')) ? 'is-hidden' : ''}}">
<h2>{{ __('app.auth') }}</h2>
<form method="POST" action="{{ url('/admin/auth/proxy/save') }}">
@csrf
<div class="field">
<div class="control">
<input type="checkbox" name="auth_proxy_enable" value="1" onclick="if (this.checked) { document.querySelector('.admin-auth-warning').style.display = 'block'; } else { document.querySelector('.admin-auth-warning').style.display = 'none'; }" {{ ((app('auth_proxy_enable')) ? 'checked' : '') }}/>&nbsp;<span>{{ __('app.auth_proxy_enable') }}</span>
</div>
</div>
<div class="admin-auth-warning">{{ __('app.auth_proxy_warning') }}</div>
<div class="field">
<label class="label">{{ __('app.auth_proxy_header_email') }}</label>
<div class="control">
<input type="text" class="input" name="auth_proxy_header_email" value="{{ app('auth_proxy_header_email') }}">
</div>
</div>
<div class="field">
<label class="label">{{ __('app.auth_proxy_header_username') }}</label>
<div class="control">
<input type="text" class="input" name="auth_proxy_header_username" value="{{ app('auth_proxy_header_username') }}">
</div>
</div>
<div class="field">
<div class="control">
<input type="checkbox" name="auth_proxy_sign_up" value="1" {{ ((app('auth_proxy_sign_up')) ? 'checked' : '') }}/>&nbsp;<span>{{ __('app.auth_proxy_sign_up') }}</span>
</div>
</div>
<div class="field">
<label class="label">{{ __('app.auth_proxy_whitelist') }}</label>
<div class="control">
<textarea class="textarea" name="auth_proxy_whitelist">{{ app('auth_proxy_whitelist') }}</textarea>
</div>
</div>
<div class="field">
<div class="control">
<input type="checkbox" name="auth_proxy_hide_logout" value="1" {{ ((app('auth_proxy_hide_logout')) ? 'checked' : '') }}/>&nbsp;<span>{{ __('app.auth_proxy_hide_logout') }}</span>
</div>
</div>
<div class="field">
<div class="control">
<input type="submit" class="button is-success" value="{{ __('app.save') }}" />
</div>
</div>
</form>
</div>
<div class="admin-attributes {{ ((!isset($_GET['tab'])) || ($_GET['tab'] !== 'attributes')) ? 'is-hidden' : ''}}">
<h2>{{ __('app.attributes') }}</h2>

View File

@@ -100,11 +100,13 @@
</div>
@endif
@if ((!app('auth_proxy_enable')) || (!app('auth_proxy_hide_logout')))
<div class="navbar-item">
<a href="{{ url('/logout') }}">
<i class="fas fa-sign-out-alt" title="{{ __('app.logout') }}"></i><span class="navbar-item-only-mobile">&nbsp;{{ __('app.logout') }}</span>
</a>
</div>
@endif
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link navbar-dropdown-minwidth">

2
public/js/app.js vendored

File diff suppressed because one or more lines are too long