New User Management (#9348)

* Rewrite user management.

Error management

Revert edituser legacy page

Connect user permissions button to legacy page for now.

Implement user creation
Refine form

Remove PingCheck.php accidental add :)

Fixes for redirection and deletion

More fixes: realname accidental validation setting, hide can modify for read-only auths

Use a panel to improve style

Add icon to panel-title

Not allowed to delete own user (at least via the click of a button)

Use request validation to reduce complexity of controller.
Improve protection against users doing things they should not.

Switch to horizontal form and not nearly as wide of layout :)

delete without refresh.
Fix for buttons

Include all users (not just from this auth)
Hide the auth column if there is only one auth type

Show username if real name isn't set

Don't allow creation of demo users via the webui

a fix to the lnms user:add command, it didn't set auth_id

update edituser.inc.php to current
just redirect to users page

* Remove TwoFactorTest for now

* Update edituser.inc.php

* Update .env.dusk.testing

* Enable 2fa for 2fa test...
This commit is contained in:
Tony Murray 2019-04-22 19:01:39 -05:00 committed by GitHub
parent 39ff4c7aaf
commit 6e6e54cb98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1149 additions and 289 deletions

View File

@ -2,4 +2,3 @@ APP_URL=http://localhost:8000
APP_KEY=base64:FSjpEaK3F9HnO40orj7FlbRI0loi1vtB3dVBcB9XaDk=
APP_ENV=testing
APP_DEBUG=true
#DB_CONNECTION=memory

View File

@ -13,7 +13,7 @@ matrix:
- php: 7.3
env: SKIP_STYLE_CHECK=1
- php: 7.2
env: SKIP_UNIT_CHECK=1 BROWSER_TEST=1
env: SKIP_UNIT_CHECK=1 BROWSER_TEST=1 CHROME_HEADLESS=1
- php: 7.1
env: SKIP_STYLE_CHECK=1 EXECUTE_BUILD_DOCS=true

View File

@ -28,6 +28,7 @@ namespace App\Console\Commands;
use App\Console\LnmsCommand;
use App\Models\User;
use Illuminate\Validation\Rule;
use LibreNMS\Authentication\LegacyAuth;
use LibreNMS\Config;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
@ -87,14 +88,18 @@ class AddUserCommand extends LnmsCommand
$user = new User([
'username' => $this->argument('username'),
'level' => $roles[$this->option('role')],
'descr' => (string)$this->option('descr'),
'email' => (string)$this->option('email'),
'realname' => (string)$this->option('full-name'),
'descr' => $this->option('descr'),
'email' => $this->option('email'),
'realname' => $this->option('full-name'),
'auth_type' => 'mysql',
]);
$user->setPassword($password);
$user->save();
$user->auth_id = LegacyAuth::get()->getUserid($user->username) ?: $user->user_id;
$user->save();
$this->info(__('commands.user:add.success', ['username' => $user->username]));
return 0;
}

View File

@ -1,7 +1,31 @@
<?php
/**
* TwoFactorController.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2018 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace App\Http\Controllers;
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\UserPref;
use Illuminate\Http\Request;
@ -72,6 +96,7 @@ class TwoFactorController extends Controller
/**
* Show the form for creating a new resource.
*
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function create(Request $request)
@ -80,14 +105,14 @@ class TwoFactorController extends Controller
'twofactor' => Rule::in('time', 'counter')
]);
$key = \LibreNMS\Authentication\TwoFactor::genKey();
$key = TwoFactor::genKey();
// assume time based
$settings = [
'key' => $key,
'fails' => 0,
'last' => 0,
'counter' => $request->get('twofactor') == 'counter' ? 0 : false,
'counter' => $request->get('twofactortype') == 'counter' ? 0 : false,
];
Session::put('twofactoradd', $settings);
@ -95,11 +120,10 @@ class TwoFactorController extends Controller
return redirect()->intended();
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function destroy(Request $request)
@ -113,7 +137,7 @@ class TwoFactorController extends Controller
/**
* Remove the specified resource from storage.
*
* @param int $id
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function cancelAdd(Request $request)
@ -126,8 +150,8 @@ class TwoFactorController extends Controller
/**
* @param User $user
* @param string $token
* @return true
* @throws AuthenticationException
* return true
*/
private function checkToken($user, $token)
{
@ -182,7 +206,7 @@ class TwoFactorController extends Controller
private function loadSettings($user)
{
if (Session::has('twofactoradd')) {
return Session::get('twofactoradd');
return Session::get('twofactoradd');
}
return UserPref::getPref($user, 'twofactor');
@ -190,7 +214,7 @@ class TwoFactorController extends Controller
private function genUri($user, $settings)
{
$title = urlencode("Librenms:" . $user->username);
$title = "LibreNMS:" . urlencode($user->username);
$key = $settings['key'];
// time based

View File

@ -0,0 +1,55 @@
<?php
/**
* TwoFactorManagementController.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2018 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\TwoFactorManagementRequest;
use App\Models\User;
use App\Models\UserPref;
class TwoFactorManagementController extends Controller
{
public function unlock(TwoFactorManagementRequest $request, User $user)
{
$twofactor = UserPref::getPref($user, 'twofactor');
$twofactor['fails'] = 0;
if (UserPref::setPref($user, 'twofactor', $twofactor)) {
return response()->json(['msg' => __('Two-Factor unlocked.')]);
}
return response()->json(['error' => __("Failed to unlock Two-Factor.")]);
}
public function destroy(TwoFactorManagementRequest $request, User $user)
{
if (UserPref::forgetPref($user, 'twofactor')) {
return response()->json(['msg' => __('Two-Factor removed.')]);
}
return response()->json(['error' => __("Failed to remove Two-Factor.")]);
}
}

View File

@ -34,6 +34,8 @@ class DeviceController extends SelectController
protected function rules()
{
return [
'access' => 'nullable|in:normal,inverted',
'user' => 'nullable|int',
'id' => 'nullable|in:device_id,hostname'
];
}
@ -46,6 +48,19 @@ class DeviceController extends SelectController
protected function baseQuery($request)
{
$this->id = $request->get('id', 'device_id');
$user_id = $request->get('user');
// list devices the user does not have access to
if ($request->get('access') == 'inverted' && $user_id && $request->user()->isAdmin()) {
return Device::query()
->select('device_id', 'hostname', 'sysName')
->whereNotIn('device_id', function ($query) use ($user_id) {
$query->select('device_id')
->from('devices_perms')
->where('user_id', $user_id);
})
->orderBy('hostname');
}
return Device::hasAccess($request->user())
->select('device_id', 'hostname', 'sysName')

View File

@ -0,0 +1,213 @@
<?php
/**
* UserController.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2018 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace App\Http\Controllers;
use App\Http\Requests\StoreUserRequest;
use App\Http\Requests\UpdateUserRequest;
use App\Models\Dashboard;
use App\Models\User;
use App\Models\UserPref;
use Hash;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use LibreNMS\Authentication\LegacyAuth;
use LibreNMS\Config;
use Toastr;
class UserController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function index()
{
$this->authorize('manage', User::class);
return view('user.index', [
'users' => User::orderBy('username')->get(),
'multiauth' => User::query()->distinct('auth_type')->count('auth_type') > 1,
]);
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function create()
{
$this->authorize('create', User::class);
$tmp_user = new User;
$tmp_user->can_modify_passwd = LegacyAuth::get()->canUpdatePasswords(); // default to true for new users
return view('user.create', [
'user' => $tmp_user,
'dashboard' => null,
'dashboards' => Dashboard::allAvailable($tmp_user)->get(),
]);
}
/**
* Store a newly created resource in storage.
*
* @param StoreUserRequest $request
* @return \Illuminate\Http\Response
*/
public function store(StoreUserRequest $request)
{
$user = $request->only(['username', 'realname', 'email', 'descr', 'level', 'can_modify_passwd']);
$user['auth_type'] = LegacyAuth::getType();
$user['can_modify_passwd'] = $request->get('can_modify_passwd'); // checkboxes are missing when unchecked
$user = User::create($user);
$user->setPassword($request->new_password);
$user->auth_id = LegacyAuth::get()->getUserid($user->username) ?: $user->user_id;
$this->updateDashboard($user, $request->get('dashboard'));
if ($user->save()) {
Toastr::success(__('User :username created', ['username', $user->username]));
return redirect(route('users.index'));
}
Toastr::error(__('Failed to create user'));
return redirect()->back();
}
/**
* Display the specified resource.
*
* @param User $user
* @return \Illuminate\Http\Response
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function show(User $user)
{
$this->authorize('view', $user);
return $user->username;
}
/**
* Show the form for editing the specified resource.
*
* @param User $user
* @return \Illuminate\Http\Response
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function edit(User $user)
{
$this->authorize('update', $user);
$data = [
'user' => $user,
'dashboard' => UserPref::getPref($user, 'dashboard'),
'dashboards' => Dashboard::allAvailable($user)->get(),
];
if (Config::get('twofactor')) {
$lockout_time = Config::get('twofactor_lock');
$twofactor = UserPref::getPref($user, 'twofactor');
$data['twofactor_enabled'] = isset($twofactor['key']);
// if enabled and 3 or more failures
$last_failure = isset($twofactor['last']) ? (time() - $twofactor['last']) : 0;
$data['twofactor_locked'] = isset($twofactor['fails']) && $twofactor['fails'] >= 3 && (!$lockout_time || $last_failure < $lockout_time);
}
return view('user.edit', $data);
}
/**
* Update the specified resource in storage.
*
* @param UpdateUserRequest $request
* @param User $user
* @return \Illuminate\Http\Response
*/
public function update(UpdateUserRequest $request, User $user)
{
if ($request->get('new_password') && $user->canSetPassword($request->user())) {
$user->setPassword($request->new_password);
}
$user->fill($request->all());
$user->can_modify_passwd = $request->get('can_modify_passwd'); // checkboxes are missing when unchecked
if ($this->updateDashboard($user, $request->get('dashboard'))) {
Toastr::success(__('Updated dashboard for :username', ['username' => $user->username]));
}
if ($user->isDirty()) {
if ($user->save()) {
Toastr::success(__('User :username updated', ['username' => $user->username]));
} else {
Toastr::error(__('Failed to update user :username', ['username' => $user->username]));
return redirect()->back();
}
}
return redirect(route('users.index'));
}
/**
* Remove the specified resource from storage.
*
* @param User $user
* @return \Illuminate\Http\Response
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function destroy(User $user)
{
$this->authorize('delete', $user);
$user->delete();
return response()->json(__('User :username deleted.', ['username' => $user->username]));
}
/**
* @param User $user
* @param $dashboard
* @return bool
*/
protected function updateDashboard(User $user, $dashboard)
{
if ($dashboard) {
$existing = UserPref::getPref($user, 'dashboard');
if ($dashboard != $existing) {
UserPref::setPref($user, 'dashboard', $dashboard);
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Http\Requests;
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use LibreNMS\Authentication\LegacyAuth;
use LibreNMS\Config;
class StoreUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return $this->user()->can('create', User::class);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'username' => [
'required',
'alpha_dash',
'max:255',
Rule::unique('users', 'username')->where('auth_type', LegacyAuth::getType()),
],
'realname' => 'max:64',
'email' => 'nullable|email|max:64',
'descr' => 'max:30',
'level' => 'int',
'new_password' => 'required|confirmed|min:' . Config::get('password.min_length', 8),
'dashboard' => 'int',
];
}
}

View File

@ -0,0 +1,45 @@
<?php
/**
* TwoFactorManagementRequest.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2018 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class TwoFactorManagementRequest extends FormRequest
{
public function rules()
{
return [];
}
public function authorize()
{
$user = $this->route()->parameter('user');
$auth_user = auth()->user();
// don't allow admins to bypass security for themselves
return $auth_user->isAdmin() && !$user->is($auth_user);
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace App\Http\Requests;
use Hash;
use Illuminate\Foundation\Http\FormRequest;
use LibreNMS\Config;
class UpdateUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
if ($this->user()->isAdmin()) {
return true;
}
$user = $this->route('user');
if ($user && $this->user()->can('update', $user)) {
// normal users cannot edit their level or ability to modify a password
unset($this['level'], $this['can_modify_passwd']);
return true;
}
return false;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'realname' => 'max:64',
'email' => 'nullable|email|max:64',
'descr' => 'max:30',
'level' => 'int',
'old_password' => 'nullable|string',
'new_password' => 'nullable|confirmed|min:' . Config::get('password.min_length', 8),
'dashboard' => 'int',
];
}
/**
* Configure the validator instance.
*
* @param \Illuminate\Validation\Validator $validator
* @return void
*/
public function withValidator($validator)
{
$validator->after(function ($validator) {
// if not an admin and new_password is set, check old password matches
if (!$this->user()->isAdmin()) {
if ($this->has('new_password')) {
if ($this->has('old_password')) {
$user = $this->route('user');
if ($user && !Hash::check($this->old_password, $user->password)) {
$validator->errors()->add('old_password', __('Existing password did not match'));
}
} else {
$validator->errors()->add('old_password', __('The :attribute field is required.', ['attribute' => 'old_password']));
}
}
}
});
}
}

View File

@ -244,20 +244,16 @@ class PingCheck implements ShouldQueue
$device->last_ping = Carbon::now();
$device->last_ping_timetaken = isset($data['rtt']) ? $data['rtt'] : 0;
if ($changed = $device->isDirty('status')) {
if ($device->isDirty('status')) {
// if changed, update reason
$device->status_reason = $device->status ? '' : 'icmp';
$type = $device->status ? 'up' : 'down';
log_event('Device status changed to ' . ucfirst($type) . " from icmp check.", $device->toArray(), $type);
echo "Device $device->hostname changed status to $type, running alerts\n";
}
$device->save();
if ($changed) {
RunRules($device->device_id);
}
$device->save(); // only saves if needed (which is every time because of last_ping)
// add data to rrd
data_update($device->toArray(), 'ping-perf', $this->rrd_tags, ['ping' => $device->last_ping_timetaken]);

View File

@ -19,12 +19,18 @@ class User extends Authenticatable
'descr' => '',
'realname' => '',
'email' => '',
'can_modify_passwd' => 0,
];
protected $dispatchesEvents = [
'created' => UserCreated::class,
];
protected $casts = [
'realname' => 'string',
'descr' => 'string',
'email' => 'string',
'can_modify_passwd' => 'integer',
];
// ---- Helper Functions ----
/**
@ -90,6 +96,25 @@ class User extends Authenticatable
$this->attributes['password'] = $password ? password_hash($password, PASSWORD_DEFAULT) : null;
}
/**
* Check if the given user can set the password for this user
*
* @param User $user
* @return bool
*/
public function canSetPassword($user)
{
if ($user && LegacyAuth::get()->canUpdatePasswords()) {
if ($user->isAdmin()) {
return true;
}
return $user->is($this) && $this->can_modify_passwd;
}
return false;
}
// ---- Query scopes ----
/**
@ -111,13 +136,33 @@ class User extends Authenticatable
});
}
// ---- Accessors/Mutators ----
public function setRealnameAttribute($realname)
{
$this->attributes['realname'] = (string)$realname;
}
public function setDescrAttribute($descr)
{
$this->attributes['descr'] = (string)$descr;
}
public function setEmailAttribute($email)
{
$this->attributes['email'] = (string)$email;
}
public function setCanModifyPasswdAttribute($modify)
{
$this->attributes['can_modify_passwd'] = $modify ? 1 : 0;
}
// ---- Define Relationships ----
public function devices()
{
if ($this->hasGlobalRead()) {
// $instance = $this->newRelatedInstance('App\Models\Device');
// return new HasAll($instance);
return Device::query();
} else {
return $this->belongsToMany('App\Models\Device', 'devices_perms', 'user_id', 'device_id');

View File

@ -0,0 +1,69 @@
<?php
namespace App\Policies;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class UserPolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can manage users.
*
* @param \App\Models\User $user
* @return bool
*/
public function manage(User $user)
{
return $user->isAdmin();
}
/**
* Determine whether the user can view the user.
*
* @param \App\Models\User $user
* @param \App\Models\User $target
* @return bool
*/
public function view(User $user, User $target)
{
return $user->isAdmin() || $target->is($user);
}
/**
* Determine whether the user can create users.
*
* @param \App\Models\User $user
* @return bool
*/
public function create(User $user)
{
return $user->isAdmin();
}
/**
* Determine whether the user can update the user.
*
* @param \App\Models\User $user
* @param \App\Models\User $target
* @return bool
*/
public function update(User $user, User $target)
{
return $user->isAdmin() || $target->is($user);
}
/**
* Determine whether the user can delete the user.
*
* @param \App\Models\User $user
* @param \App\Models\User $target
* @return bool
*/
public function delete(User $user, User $target)
{
return $user->isAdmin();
}
}

View File

@ -2,7 +2,8 @@
namespace App\Providers;
use App\Providers\LegacyUserProvider;
use App\Models\User;
use App\Policies\UserPolicy;
use App\Guards\ApiTokenGuard;
use Auth;
use Illuminate\Support\Facades\Gate;
@ -16,7 +17,7 @@ class AuthServiceProvider extends ServiceProvider
* @var array
*/
protected $policies = [
'App\Model' => 'App\Policies\ModelPolicy',
User::class => UserPolicy::class
];
/**

View File

@ -20,6 +20,7 @@ $factory->define(App\Models\User::class, function (Faker\Generator $faker) {
static $password;
return [
'auth_type' => 'mysql',
'username' => $faker->unique()->userName,
'realname' => $faker->name,
'email' => $faker->safeEmail,

View File

@ -131,6 +131,13 @@ $config['unflatten'] = "/usr/bin/unflatten";
$config['neato'] = "/usr/bin/neato";
$config['sfdp'] = "/usr/bin/sfdp";
```
### Authentication
Generic Authentication settings.
```php
$config['password']['min_length'] = 8; // password minimum length for auth that allows user creation
```
### Proxy support

View File

@ -19,7 +19,7 @@ if (! Auth::user()->hasGlobalAdmin()) {
$user = User::find($vars['user_id']);
$user_data = $user->toArray(); // for compatibility with current code
echo '<p><h2>'.$user_data['realname']."</h2><a href='edituser/'>Change...</a></p>";
echo '<p><h2>'.$user_data['realname']."</h2></p>";
// Perform actions if requested
if ($vars['action'] == 'deldevperm') {
if (dbFetchCell('SELECT COUNT(*) FROM devices_perms WHERE `device_id` = ? AND `user_id` = ?', array($vars['device_id'], $user_data['user_id']))) {
@ -246,238 +246,8 @@ if (! Auth::user()->hasGlobalAdmin()) {
<button type='submit' class='btn btn-default' name='Submit' value='Add'>Add</button>
</form>
</div>";
} elseif ($vars['user_id'] && $vars['edit']) {
if (Auth::user()->isDemo()) {
demo_account();
} else {
if (!empty($vars['new_level'])) {
if ($vars['can_modify_passwd'] == 'on') {
$vars['can_modify_passwd'] = '1';
}
LegacyAuth::get()->updateUser($vars['user_id'], $vars['new_realname'], $vars['new_level'], $vars['can_modify_passwd'], $vars['new_email']);
print_message('User has been updated');
if (!empty($vars['new_pass1']) && $vars['new_pass1'] == $vars['new_pass2'] && LegacyAuth::get()->canUpdatePasswords($vars['cur_username'])) {
if (LegacyAuth::get()->changePassword($vars['cur_username'], $vars['new_pass1']) == 1) {
print_message("User password has been updated");
} else {
print_error("Password couldn't be updated");
}
} elseif (!empty($vars['new_pass1']) && $vars['new_pass1'] != $vars['new_pass2']) {
print_error("The supplied passwords didn't match so weren't updated");
}
}
$users_details = User::find($vars['user_id'])->toArray();
if (!empty($users_details)) {
if (!empty($vars['dashboard']) && $vars['dashboard'] != $users_details['dashboard']) {
set_user_pref('dashboard', $vars['dashboard']);
print_message("User default dashboard updated");
}
echo "<form class='form-horizontal' role='form' method='post' action=''>
<input type='hidden' name='user_id' value='".$vars['user_id']."'>
<input type='hidden' name='cur_username' value='" . $users_details['username'] . "'>
<input type='hidden' name='edit' value='yes'>
";
if (LegacyAuth::get()->canUpdateUsers() == '1') {
if (empty($vars['new_realname'])) {
$vars['new_realname'] = $users_details['realname'];
}
if (empty($vars['new_level'])) {
$vars['new_level'] = $users_details['level'];
}
if (empty($vars['can_modify_passwd'])) {
$vars['can_modify_passwd'] = $users_details['can_modify_passwd'];
} elseif ($vars['can_modify_passwd'] == 'on') {
$vars['can_modify_passwd'] = '1';
}
if (empty($vars['new_email'])) {
$vars['new_email'] = $users_details['email'];
}
echo "
<div class='form-group'>
<label for='new_realname' class='col-sm-2 control-label'>Realname</label>
<div class='col-sm-4'>
<input name='new_realname' class='form-control input-sm' value='".$vars['new_realname']."'>
</div>
<div class='col-sm-6'>
</div>
</div>
<div class='form-group'>
<label for='new_email' class='col-sm-2 control-label'>Email</label>
<div class='col-sm-4'>
<input name='new_email' class='form-control input-sm' value='".$vars['new_email']."'>
</div>
<div class='col-sm-6'>
</div>
</div>
<div class='form-group'>
<label for='new_level' class='col-sm-2 control-label'>Level</label>
<div class='col-sm-4'>
<select name='new_level' class='form-control input-sm'>
<option value='1'";
if ($vars['new_level'] == '1') {
echo 'selected';
} echo ">Normal User</option>
<option value='5'";
if ($vars['new_level'] == '5') {
echo 'selected';
} echo ">Global Read</option>
<option value='10'";
if ($vars['new_level'] == '10') {
echo 'selected';
} echo ">Administrator</option>
<option value='11'";
if ($vars['new_level'] == '11') {
echo 'selected';
} echo ">Demo account</option>
</select>
</div>
<div class='col-sm-6'>
</div>
</div>";
if (LegacyAuth::get()->canUpdatePasswords($users_details['username'])) {
echo "
<div class='form-group'>
<label for='new_pass1' class='col-sm-2 control-label'>Password</label>
<div class='col-sm-4'>
<input type='password' name='new_pass1' class='form-control input-sm' value='". $vars['new_pass1'] ."'>
</div>
</div>
<div class='form-group'>
<label for='new_pass2' class='col-sm-2 control-label'>Confirm Password</label>
<div class='col-sm-4'>
<input type='password' name='new_pass2' class='form-control input-sm' value='". $vars['new_pass2'] ."'>
</div>
</div>
";
}
echo "<div class='form-group'>
<div class='col-sm-6'>
<div class='checkbox'>
<label>
<input type='checkbox' ";
if ($vars['can_modify_passwd'] == '1') {
echo "checked='checked'";
} echo " name='can_modify_passwd'> Allow the user to change their password.
</label>
</div>
</div>
<div class='col-sm-6'>
</div>
</div>
";
}
echo "
<div class='form-group'>
<label for='dashboard' class='col-sm-2 control-label'>Dashboard</label>
<div class='col-sm-4'><select class='form-control' name='dashboard'>";
foreach (get_dashboards($vars['user_id']) as $dash) {
echo "<option value='".$dash['dashboard_id']."'".($dash['default'] ? ' selected' : '').">".$dash['username'].':'.$dash['dashboard_name']."</option>";
}
echo "</select>
</div>
</div>
<button type='submit' class='btn btn-default'>Update User</button>
</form>";
if ($config['twofactor']) {
if ($vars['twofactorremove']) {
if (set_user_pref('twofactor', array(), $vars['user_id'])) {
echo "<div class='alert alert-success'>TwoFactor credentials removed.</div>";
} else {
echo "<div class='alert alert-danger'>Couldnt remove user's TwoFactor credentials.</div>";
}
}
if ($vars['twofactorunlock']) {
$twofactor = get_user_pref('twofactor', array(), $vars['user_id']);
$twofactor['fails'] = 0;
if (set_user_pref('twofactor', $twofactor, $vars['user_id'])) {
echo "<div class='alert alert-success'>User unlocked.</div>";
} else {
echo "<div class='alert alert-danger'>Couldnt reset user's TwoFactor failures.</div>";
}
}
echo "<br/><div class='well'><h3>Two-Factor Authentication</h3>";
$twofactor = get_user_pref('twofactor', array(), $vars['user_id']);
if ($twofactor['fails'] >= 3 && (!$config['twofactor_lock'] || (time() - $twofactor['last']) < $config['twofactor_lock'])) {
echo "<form class='form-horizontal' role='form' method='post' action=''>
<input type='hidden' name='user_id' value='".$vars['user_id']."'>
<input type='hidden' name='edit' value='yes'>
<div class='form-group'>
<label for='twofactorunlock' class='col-sm-2 control-label'>User exceeded failures</label>
<input type='hidden' name='twofactorunlock' value='1'>
<button type='submit' class='btn btn-default'>Unlock</button>
</div>
</form>";
}
if ($twofactor['key']) {
echo "<form class='form-horizontal' role='form' method='post' action=''>
<input type='hidden' name='user_id' value='".$vars['user_id']."'>
<input type='hidden' name='edit' value='yes'>
<input type='hidden' name='twofactorremove' value='1'>
<button type='submit' class='btn btn-danger'>Disable TwoFactor</button>
</form>
</div>";
} else {
echo '<p>No TwoFactor key generated for this user, Nothing to do.</p>';
}
}//end if
} else {
print_error('Error getting user details');
}//end if !empty($users_details)
}//end if
} else {
$userlist = User::thisAuth()->get();
echo '<h3>Select a user to edit</h3>';
echo "<form method='post' action='' class='form-horizontal' role='form'>
<input type='hidden' value='edituser' name='page'>
<div class='form-group'>
<label for='user_id' class='col-sm-2 control-label'>User</label>
<div class='col-sm-4'>
<select name='user_id' class='form-control input-sm'>";
foreach ($userlist as $userentry) {
switch ($userentry->level) {
case "10":
$userlevel = 'admin';
break;
case "11":
$userlevel = 'demo';
break;
default:
$userlevel = '';
}
if (empty($userlevel)) {
$userlevel=$userentry->auth_type;
} elseif (!empty($userentry->auth_type)) {
$userlevel.= ", ".$userentry->auth_type;
}
if (!empty($userlevel)) {
$userlevel=" ($userlevel)";
}
echo "<option value='".$userentry->user_id."'>".$userentry->username.$userlevel.'</option>';
}
echo "</select>
</div>
</div>
<div class='form-group'>
<div class='col-sm-offset-2 col-sm-3'>
<button type='submit' name='Submit' class='btn btn-default'>Edit Permissions</button> / <button type='submit' name='edit' value='user' class='btn btn-default'>Edit User</button>
</div>
</div>
</form>";
echo '<script>window.location.replace("' . url('users') . '");</script>';
}//end if
}//end if

View File

@ -124,15 +124,15 @@ if (LegacyAuth::user()->isDemoUser()) {
<div class="form-group">
<label for="twofactortype" class="col-sm-2 control-label">TwoFactor Type</label>
<div class="col-sm-4">
<select name="twofactortype" class="select">
<select name="twofactortype" class="select form-control">
<option value="time">Time Based (TOTP)</option>
<option value="counter">Counter Based (HOTP)</option>
</select>
</div>
</div>
<div class="form-group">
<div class="col-sm-4 col-sm-offset-1">
<button class="btn btn-default" type="submit">Generate TwoFactor Secret Key</button>
<div class="col-sm-4 col-sm-offset-2">
<button class="btn btn-default" type="submit" id="twofactor-generate">Generate TwoFactor Secret Key</button>
</div>
</div>
</form>';
@ -159,16 +159,16 @@ foreach (get_dashboards() as $dash) {
echo "
<option value='".$dash['dashboard_id']."'".($dash['default'] ? ' selected' : '').">".display($dash['username']).':'.display($dash['dashboard_name'])."</option>";
}
echo "
echo '
</select>
<br>
<center><button type='submit' class='btn btn-default'>Update Dashboard</button></center>
</div>
<div class='col-sm-6'></div>
</div>
<div class="form-group">
<div class="col-sm-4 col-sm-offset-2"><button type="submit" class="btn btn-default">Update Dashboard</button></div>
</div>
</div>
</form>
</div>";
</div>';
echo "<h3>Add schedule notes to devices notes</h3>

View File

@ -656,14 +656,8 @@ if (Auth::user()->hasGlobalAdmin()) {
<li role="presentation" class="divider"></li>
<?php if (Auth::user()->hasGlobalAdmin()) {
if (LegacyAuth::get()->canManageUsers()) {
echo('
<li><a href="adduser/"><i class="fa fa-user-plus fa-fw fa-lg" aria-hidden="true"></i> Add User</a></li>
<li><a href="deluser/"><i class="fa fa-user-times fa-fw fa-lg" aria-hidden="true"></i> Remove User</a></li>
');
}
echo('
<li><a href="edituser/"><i class="fa fa-user-circle-o fa-fw fa-lg" aria-hidden="true"></i> Edit User</a></li>
<li><a href="' . route('users.index') . '"><i class="fa fa-user-circle-o fa-fw fa-lg" aria-hidden="true"></i> Manage Users</a></li>
<li><a href="authlog/"><i class="fa fa-shield fa-fw fa-lg" aria-hidden="true"></i> Auth History</a></li>
<li role="presentation" class="divider"></li> ');
echo('

View File

@ -358,11 +358,7 @@
<li><a href="{{ url('settings') }}"><i class="fa fa-cogs fa-fw fa-lg" aria-hidden="true"></i> Global Settings</a></li>
<li><a href="{{ url('validate') }}"><i class="fa fa-check-circle fa-fw fa-lg" aria-hidden="true"></i> Validate Config</a></li>
<li role="presentation" class="divider"></li>
@if(\LibreNMS\Authentication\LegacyAuth::get()->canManageUsers())
<li><a href="{{ url('adduser') }}"><i class="fa fa-user-plus fa-fw fa-lg" aria-hidden="true"></i> Add User</a></li>
<li><a href="{{ url('deluser') }}"><i class="fa fa-user-times fa-fw fa-lg" aria-hidden="true"></i> Remove User</a></li>
@endif
<li><a href="{{ url('edituser') }}"><i class="fa fa-user-circle-o fa-fw fa-lg" aria-hidden="true"></i> Edit User</a></li>
<li><a href="{{ route('users.index') }}"><i class="fa fa-user-circle-o fa-fw fa-lg" aria-hidden="true"></i> Manage Users</a></li>
<li><a href="{{ url('authlog') }}"><i class="fa fa-shield fa-fw fa-lg" aria-hidden="true"></i> Auth History</a></li>
<li role="presentation" class="divider"></li>
<li class="dropdown-submenu">

View File

@ -0,0 +1,28 @@
@extends('layouts.librenmsv1')
@section('title', __('Create User'))
@section('content')
<div class="row">
<form action="users" method="POST" role="form" class="form-horizontal col-sm-offset-3 col-sm-6">
<legend>@lang('Create User')</legend>
<div class="form-group @if($errors->has('username')) has-error @endif">
<label for="realname" class="control-label col-sm-3">@lang('Username')</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="username" name="username" value="{{ old('username', $user->username) }}">
<span class="help-block">{{ $errors->first('username') }}</span>
</div>
</div>
@include('user.form')
<div class="form-group">
<div class="col-sm-9 col-sm-offset-3">
<button type="submit" class="btn btn-primary">@lang('Save')</button>
<a type="button" class="btn btn-danger" href="{{ route('users.index') }}">@lang('Cancel')</a>
</div>
</div>
</form>
</div>
@endsection

View File

@ -0,0 +1,91 @@
@extends('layouts.librenmsv1')
@section('title', __('Edit User'))
@section('content')
<div class="row">
<form action="users/{{ $user->user_id }}" method="POST" role="form" class="form-horizontal col-sm-offset-3 col-sm-6">
<legend>@lang('Edit User'): {{ $user->username }}</legend>
{{ method_field('PUT') }}
@include('user.form')
@config('twofactor')
<br/>
<div class="panel panel-default col-sm-offset-3">
<div class="panel-heading">
<h3 class="panel-title">Two-Factor Authentication</h3>
</div>
<div class="panel-body">
@if($twofactor_enabled)
@if($twofactor_locked)
<div class="form-group" id="twofactor-unlock-form">
<button type="button" id="twofactor-unlock" class="btn btn-default col-sm-4 col-sm-offset-1">@lang('Unlock')</button>
<label for="twofactor-unlock" class="col-sm-7 control-label">@lang('User exceeded failures')</label>
</div>
@endif
<div class="form-group">
<button type="button" id="twofactor-disable" class="btn btn-danger col-sm-offset-1">@lang('Disable TwoFactor')</button>
</div>
@else
<p>@lang('No TwoFactor key generated for this user, Nothing to do.')</p>
@endif
</div>
</div>
@endconfig
<div class="form-group">
<div class="col-sm-9 col-sm-offset-3">
<button type="submit" class="btn btn-primary">@lang('Save')</button>
<a type="button" class="btn btn-danger" href="{{ route('users.index') }}">@lang('Cancel')</a>
</div>
</div>
</form>
</div>
@endsection
@section('javascript')
<script type="application/javascript">
$(document).ready(function () {
$('#twofactor-unlock').click(function () {
console.log('unlock');
$.ajax({
type: 'POST',
url: '{{ route('2fa.unlock', ['user' => $user->user_id]) }}',
dataType: "json",
success: function(data){
if (data.status === 'ok') {
$('#twofactor-unlock-form').remove();
toastr.success('@lang('Unlocked Two Factor.')');
} else {
toastr.error('@lang('Failed to unlock Two Factor')<br />' + data.message);
}
},
error: function(){
toastr.error('@lang('Failed to unlock Two Factor')');
}
});
});
$('#twofactor-disable').click(function () {
$.ajax({
type: 'DELETE',
url: '{{ route('2fa.delete', ['user' => $user->user_id]) }}',
dataType: "json",
success: function(data){
if (data.status === 'ok') {
toastr.success('@lang('Removed Two Factor.')');
} else {
toastr.error('@lang('Failed to remove Two Factor')<br />' + data.message);
}
},
error: function(){
toastr.error('@lang('Failed to remove Two Factor')');
}
});
});
});
</script>
@endsection

View File

@ -0,0 +1,81 @@
<div class="form-group {{ $errors->has('realname') ? 'has-error' : '' }}">
<label for="realname" class="control-label col-sm-3 text-nowrap">@lang('Real Name')</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="realname" name="realname" value="{{ old('realname', $user->realname) }}">
<span class="help-block">{{ $errors->first('realname') }}</span>
</div>
</div>
<div class="form-group @if($errors->has('email')) has-error @endif">
<label for="email" class="control-label col-sm-3">@lang('Email')</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="email" name="email" value="{{ old('email', $user->email) }}">
<span class="help-block">{{ $errors->first('email') }}</span>
</div>
</div>
<div class="form-group @if($errors->has('descr')) has-error @endif">
<label for="descr" class="control-label col-sm-3">@lang('Description')</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="descr" name="descr" value="{{ old('descr', $user->descr) }}">
<span class="help-block">{{ $errors->first('descr') }}</span>
</div>
</div>
@can('admin')
<div class="form-group @if($errors->has('level')) has-error @endif">
<label for="level" class="control-label col-sm-3">@lang('Level')</label>
<div class="col-sm-9">
<select class="form-control" id="level" name="level">
<option value="1">@lang('Normal')</option>
<option value="5" @if(old('level', $user->level) == 5) selected @endif>@lang('Global Read')</option>
<option value="10" @if(old('level', $user->level) == 10) selected @endif>@lang('Admin')</option>
@if(old('level', $user->level) == 11)<option value="11" selected>@lang('Demo')</option>@endif
</select>
<span class="help-block">{{ $errors->first('level') }}</span>
</div>
</div>
@endcan
<div class="form-group @if($errors->has('dashboard')) has-error @endif">
<label for="dashboard" class="control-label col-sm-3">@lang('Dashboard')</label>
<div class="col-sm-9">
<select id="dashboard" name="dashboard" class="form-control">
@foreach($dashboards as $dash)
<option value="{{ $dash->dashboard_id }}" @if(old('dashboard', $dashboard) == $dash->dashboard_id) selected @endif>{{ $dash->dashboard_name }}</option>
@endforeach
</select>
<span class="help-block">{{ $errors->first('dashboard') }}</span>
</div>
</div>
@if($user->canSetPassword(auth()->user()))
<div class="form-group @if($errors->hasAny(['old_password', 'new_password', 'new_password_confirmation'])) has-error @endif">
<label for="password" class="control-label col-sm-3">@lang('Password')</label>
<div class="col-sm-9">
@cannot('admin')
<input type="password" class="form-control" id="old_password" name="old_password" placeholder="@lang('Current Password')">
@endcannot
<input type="password" autocomplete="off" class="form-control" id="new_password" name="new_password" placeholder="@lang('New Password')">
<input type="password" autocomplete="off" class="form-control" id="new_password_confirmation" name="new_password_confirmation" placeholder="@lang('Confirm Password')">
<span class="help-block">
@foreach($errors->get('*password*') as $error)
{{ implode(' ', $error) }}
@endforeach
</span>
</div>
</div>
@endif
@if(\LibreNMS\Authentication\LegacyAuth::get()->canUpdatePasswords())
<div class="form-group @if($errors->has('can_modify_passwd')) has-error @endif">
<div class="col-sm-9 col-sm-offset-3">
<div class="checkbox">
<label class="checkbox-inline">
<input type="checkbox" id="can_modify_passwd" name="can_modify_passwd" @if(old('can_modify_passwd', $user->can_modify_passwd)) checked @endif> @lang('Can Modify Password')
</label>
</div>
<span class="help-block">{{ $errors->first('can_modify_passwd') }}</span>
</div>
</div>
@endif

View File

@ -0,0 +1,123 @@
@extends('layouts.librenmsv1')
@section('title', __('Manage Users'))
@section('content')
<div id="manage-users-panel" class="panel panel-default">
<div class="panel-heading"><h4 class="panel-title"><i class="fa fa-user-circle-o fa-fw fa-lg" aria-hidden="true"></i> @lang('Manage Users')</h4></div>
<div class="panel-body">
<div class="table-responsive">
<table id="users" class="table table-bordered table-condensed" style="display: none;">
<thead>
<tr>
<th data-column-id="user_id" data-visible="false" data-identifier="true" data-type="numeric">@lang('ID')</th>
<th data-column-id="username">@lang('Username')</th>
<th data-column-id="realname">@lang('Real Name')</th>
<th data-column-id="level" data-formatter="level" data-type="numeric">@lang('Access')</th>
<th data-column-id="auth_type" data-visible="{{ $multiauth ? 'true' : 'false' }}">@lang('Auth')</th>
<th data-column-id="email">@lang('Email')</th>
<th data-column-id="descr">@lang('Description')</th>
<th data-column-id="action" data-formatter="actions" data-sortable="false" data-searchable="false">@lang('Actions')</th>
</tr>
</thead>
<tbody id="users_rows">
@foreach($users as $user)
<tr>
<td>{{ $user->user_id }}</td>
<td>{{ $user->username }}</td>
<td>{{ $user->realname }}</td>
<td>{{ $user->level }}</td>
<td>{{ $user->auth_type }}</td>
<td>{{ $user->email }}</td>
<td>{{ $user->descr }}</td>
<td></td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
@endsection
@section('javascript')
<script type="application/javascript">
$(document).ready(function(){
var user_grid = $("#users");
user_grid.bootgrid({
formatters: {
actions: function (column, row) {
var edit_button = '<form action="users/' + row['user_id'] + '/edit" method="GET">' +
'<button type="submit" title="@lang('Edit')" class="btn btn-sm btn-warning"><i class="fa fa-pencil"></i></button>' +
'</form> ';
var delete_button = '<button type="button" title="@lang('Delete')" class="btn btn-sm btn-danger" onclick="return delete_user(' + row['user_id'] + ', \'' + row['username'] + '\');">' +
'<i class="fa fa-trash"></i></button> ';
var manage_button = '<form action="edituser/" method="GET"';
if (row['level'] >= 5) {
manage_button += ' style="visibility:hidden;"'
}
manage_button += '><input type="hidden" name="user_id" value="' + row['user_id'] +
'"><button type="submit" title="@lang('Manage Access')" class="btn btn-sm btn-primary"><i class="fa fa-tasks"></i></button>' +
'</form> ';
var output = manage_button + edit_button;
if ('{{ Auth::id() }}' != row['user_id']) {
output += delete_button;
}
return output
},
level: function (column, row) {
var level = row[column.id];
if (level == 10) {
return '@lang('Admin')';
} else if (level == 5) {
return '@lang('Global Read')';
} else if (level == 11) {
return '@lang('Demo')';
}
return '@lang('Normal')';
}
}
});
@if(\LibreNMS\Authentication\LegacyAuth::get()->canManageUsers())
$('.actionBar').append('<div class="pull-left"><a href="users/create" type="button" class="btn btn-primary">@lang('Add User')</a></div>');
@endif
user_grid.css('display', 'table'); // done loading, show
});
function delete_user(user_id, username)
{
if (confirm('@lang('Are you sure you want to delete ')' + username + '?')) {
$.ajax({
url: 'users/' + user_id,
type: 'DELETE',
success: function (msg) {
$("#users").bootgrid("remove", [user_id]);
toastr.success(msg);
},
error: function () {
toastr.error('@lang('The user could not be deleted')');
}
});
}
return false;
}
</script>
@endsection
@section('css')
<style>
#manage-users-panel .panel-title { font-size: 18px; }
#users form { display:inline; }
</style>
@endsection

View File

@ -28,11 +28,18 @@ Route::group(['middleware' => ['auth', '2fa'], 'guard' => 'auth'], function () {
Route::permanentRedirect('poll-log', 'pollers/tab=log/');
// Two Factor Auth
Route::get('2fa', 'TwoFactorController@showTwoFactorForm')->name('2fa.form');
Route::post('2fa', 'TwoFactorController@verifyTwoFactor')->name('2fa.verify');
Route::post('2fa/add', 'TwoFactorController@create');
Route::post('2fa/cancel', 'TwoFactorController@cancelAdd')->name('2fa.cancel');
Route::post('2fa/remove', 'TwoFactorController@destroy');
Route::group(['prefix' => '2fa', 'namespace' => 'Auth'], function () {
Route::get('', 'TwoFactorController@showTwoFactorForm')->name('2fa.form');
Route::post('', 'TwoFactorController@verifyTwoFactor')->name('2fa.verify');
Route::post('add', 'TwoFactorController@create');
Route::post('cancel', 'TwoFactorController@cancelAdd')->name('2fa.cancel');
Route::post('remove', 'TwoFactorController@destroy')->name('2fa.remove');
Route::post('{user}/unlock', 'TwoFactorManagementController@unlock')->name('2fa.unlock');
Route::delete('{user}', 'TwoFactorManagementController@destroy')->name('2fa.delete');
});
Route::resource('users', 'UserController');
// Ajax routes
Route::group(['prefix' => 'ajax'], function () {

View File

@ -3,9 +3,12 @@
namespace LibreNMS\Tests\Browser;
use App\Models\User;
use App\Models\UserPref;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use LibreNMS\Config;
use LibreNMS\Tests\Browser\Pages\LoginPage;
use LibreNMS\Tests\Browser\Pages\TwoFactorPage;
use LibreNMS\Tests\DuskTestCase;
/**
@ -28,11 +31,50 @@ class LoginTest extends DuskTestCase
'password' => password_hash($password, PASSWORD_DEFAULT)
]);
$browser->visit(new LoginPage())
->type('username', $user->username)
->type('password', 'wrong_password')
->press('@login')
->assertPathIs('/login')
->type('username', $user->username)
->type('password', $password)
->press('@login')
->assertPathIs('/')
->logout();
$user->delete();
});
}
/**
* @throws \Throwable
*/
public function test2faLogin()
{
$this->browse(function (Browser $browser) {
$password = 'another_password';
$user = factory(User::class)->create([
'password' => password_hash($password, PASSWORD_DEFAULT)
]);
Config::set('twofactor', true, true); // set to db
UserPref::setPref($user, 'twofactor', [
'key' => '5P3FLXBX7NU3ZBFOTWZL2GL5MKFEWBOA', // known key: 634456, 613687, 064292
'fails' => 0,
'last' => 0,
'counter' => 1,
]);
$browser->visit(new LoginPage())
->type('username', $user->username)
->type('password', $password)
->press('#login')
->assertPathIs('/');
->on(new TwoFactorPage())
->assertFocused('@input')
->keys('@input', '999999', '{enter}') // try the wrong code first
->assertPathIs('/2fa')
->keys('@input', '634456', '{enter}')
->assertPathIs('/')
->logout();
$user->delete();
});

View File

@ -35,7 +35,7 @@ class LoginPage extends Page
public function elements()
{
return [
'@element' => '#selector',
'@login' => '#login',
];
}
}

View File

@ -0,0 +1,65 @@
<?php
/**
* PreferencesPage.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2019 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS\Tests\Browser\Pages;
use Laravel\Dusk\Browser;
class PreferencesPage extends Page
{
/**
* Get the URL for the page.
*
* @return string
*/
public function url()
{
return '/preferences';
}
/**
* Assert that the browser is on the page.
*
* @param Browser $browser
* @return void
*/
public function assert(Browser $browser)
{
$browser->assertPathIs($this->url());
}
/**
* Get the element shortcuts for the page.
*
* @return array
*/
public function elements()
{
return [
'@type' => 'select[name=twofactortype]',
'@generate' => '#twofactor-generate',
];
}
}

View File

@ -0,0 +1,64 @@
<?php
/**
* TwoFactorPage.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2019 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS\Tests\Browser\Pages;
use Laravel\Dusk\Browser;
class TwoFactorPage extends Page
{
/**
* Get the URL for the page.
*
* @return string
*/
public function url()
{
return '/2fa';
}
/**
* Assert that the browser is on the page.
*
* @param Browser $browser
* @return void
*/
public function assert(Browser $browser)
{
$browser->assertPathIs($this->url());
}
/**
* Get the element shortcuts for the page.
*
* @return array
*/
public function elements()
{
return [
'@input' => '#twofactor',
];
}
}

View File

@ -29,12 +29,17 @@ abstract class DuskTestCase extends BaseTestCase
*/
protected function driver()
{
$arguments = [
'--disable-gpu',
];
if (env('CHROME_HEADLESS')) {
$arguments[] = '--headless';
}
return RemoteWebDriver::create('http://localhost:9515', DesiredCapabilities::chrome()->setCapability(
ChromeOptions::CAPABILITY,
(new ChromeOptions)->addArguments([
'--disable-gpu',
'--headless'
])
(new ChromeOptions)->addArguments($arguments)
));
}
}