diff --git a/.env.example b/.env.example
index 00d230bff..d32d96c0d 100644
--- a/.env.example
+++ b/.env.example
@@ -36,6 +36,14 @@ APP_URL=http://bookstack.dev
# External services such as Gravatar
DISABLE_EXTERNAL_SERVICES=false
+# LDAP Settings
+LDAP_SERVER=false
+LDAP_BASE_DN=false
+LDAP_DN=false
+LDAP_PASS=false
+LDAP_USER_FILTER=false
+LDAP_VERSION=false
+
# Mail settings
MAIL_DRIVER=smtp
MAIL_HOST=localhost
diff --git a/app/Http/Controllers/Auth/AuthController.php b/app/Http/Controllers/Auth/AuthController.php
index 21abfb24c..d601b4985 100644
--- a/app/Http/Controllers/Auth/AuthController.php
+++ b/app/Http/Controllers/Auth/AuthController.php
@@ -118,17 +118,20 @@ class AuthController extends Controller
*/
protected function authenticated(Request $request, Authenticatable $user)
{
- if(!$user->exists && $user->email === null && !$request->has('email')) {
+ // Explicitly log them out for now if they do no exist.
+ if (!$user->exists) auth()->logout($user);
+
+ if (!$user->exists && $user->email === null && !$request->has('email')) {
$request->flash();
session()->flash('request-email', true);
return redirect('/login');
}
- if(!$user->exists && $user->email === null && $request->has('email')) {
+ if (!$user->exists && $user->email === null && $request->has('email')) {
$user->email = $request->get('email');
}
- if(!$user->exists) {
+ if (!$user->exists) {
$user->save();
$this->userRepo->attachDefaultRole($user);
auth()->login($user);
diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php
index ebd830ffe..ad804d0d8 100644
--- a/app/Http/Middleware/Authenticate.php
+++ b/app/Http/Middleware/Authenticate.php
@@ -38,6 +38,7 @@ class Authenticate
if(auth()->check() && auth()->user()->email_confirmed == false) {
return redirect()->guest('/register/confirm/awaiting');
}
+
if ($this->auth->guest() && !Setting::get('app-public')) {
if ($request->ajax()) {
return response('Unauthorized.', 401);
diff --git a/app/Http/routes.php b/app/Http/routes.php
index aedfca9bc..23d4c33ab 100644
--- a/app/Http/routes.php
+++ b/app/Http/routes.php
@@ -1,11 +1,5 @@
getUserDetails('ksmith'));
-});
-
// Authenticated routes...
Route::group(['middleware' => 'auth'], function () {
diff --git a/app/Providers/LdapUserProvider.php b/app/Providers/LdapUserProvider.php
index 98cfc8340..30fa739c2 100644
--- a/app/Providers/LdapUserProvider.php
+++ b/app/Providers/LdapUserProvider.php
@@ -86,8 +86,10 @@ class LdapUserProvider implements UserProvider
*/
public function updateRememberToken(Authenticatable $user, $token)
{
- $user->setRememberToken($token);
- $user->save();
+ if ($user->exists) {
+ $user->setRememberToken($token);
+ $user->save();
+ }
}
/**
@@ -113,6 +115,7 @@ class LdapUserProvider implements UserProvider
$model->name = $userDetails['name'];
$model->external_auth_id = $userDetails['uid'];
$model->email = $userDetails['email'];
+ $model->email_confirmed = true;
return $model;
}
diff --git a/app/Services/Ldap.php b/app/Services/Ldap.php
new file mode 100644
index 000000000..cfefbb4b6
--- /dev/null
+++ b/app/Services/Ldap.php
@@ -0,0 +1,86 @@
+search($ldapConnection, $baseDn, $filter, $attributes);
+ return $this->getEntries($ldapConnection, $search);
+ }
+
+ /**
+ * Bind to LDAP directory.
+ * @param resource $ldapConnection
+ * @param string $bindRdn
+ * @param string $bindPassword
+ * @return bool
+ */
+ public function bind($ldapConnection, $bindRdn = null, $bindPassword = null)
+ {
+ return ldap_bind($ldapConnection, $bindRdn, $bindPassword);
+ }
+
+}
\ No newline at end of file
diff --git a/app/Services/LdapService.php b/app/Services/LdapService.php
index cd80290e4..d33f8c378 100644
--- a/app/Services/LdapService.php
+++ b/app/Services/LdapService.php
@@ -4,10 +4,27 @@
use BookStack\Exceptions\LdapException;
use Illuminate\Contracts\Auth\Authenticatable;
+/**
+ * Class LdapService
+ * Handles any app-specific LDAP tasks.
+ * @package BookStack\Services
+ */
class LdapService
{
+ protected $ldap;
protected $ldapConnection;
+ protected $config;
+
+ /**
+ * LdapService constructor.
+ * @param Ldap $ldap
+ */
+ public function __construct(Ldap $ldap)
+ {
+ $this->ldap = $ldap;
+ $this->config = config('services.ldap');
+ }
/**
* Get the details of a user from LDAP using the given username.
@@ -21,17 +38,16 @@ class LdapService
$ldapConnection = $this->getConnection();
// Find user
- $userFilter = $this->buildFilter(config('services.ldap.user_filter'), ['user' => $userName]);
- $baseDn = config('services.ldap.base_dn');
- $ldapSearch = ldap_search($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', 'mail']);
- $users = ldap_get_entries($ldapConnection, $ldapSearch);
+ $userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]);
+ $baseDn = $this->config['base_dn'];
+ $users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', 'mail']);
if ($users['count'] === 0) return null;
$user = $users[0];
return [
- 'uid' => $user['uid'][0],
- 'name' => $user['cn'][0],
- 'dn' => $user['dn'],
+ 'uid' => $user['uid'][0],
+ 'name' => $user['cn'][0],
+ 'dn' => $user['dn'],
'email' => (isset($user['mail'])) ? $user['mail'][0] : null
];
}
@@ -50,7 +66,12 @@ class LdapService
if ($ldapUser['uid'] !== $user->external_auth_id) return false;
$ldapConnection = $this->getConnection();
- $ldapBind = @ldap_bind($ldapConnection, $ldapUser['dn'], $password);
+ try {
+ $ldapBind = $this->ldap->bind($ldapConnection, $ldapUser['dn'], $password);
+ } catch (\ErrorException $e) {
+ $ldapBind = false;
+ }
+
return $ldapBind;
}
@@ -62,14 +83,14 @@ class LdapService
*/
protected function bindSystemUser($connection)
{
- $ldapDn = config('services.ldap.dn');
- $ldapPass = config('services.ldap.pass');
+ $ldapDn = $this->config['dn'];
+ $ldapPass = $this->config['pass'];
$isAnonymous = ($ldapDn === false || $ldapPass === false);
if ($isAnonymous) {
- $ldapBind = ldap_bind($connection);
+ $ldapBind = $this->ldap->bind($connection);
} else {
- $ldapBind = ldap_bind($connection, $ldapDn, $ldapPass);
+ $ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass);
}
if (!$ldapBind) throw new LdapException('LDAP access failed using ' . $isAnonymous ? ' anonymous bind.' : ' given dn & pass details');
@@ -86,20 +107,22 @@ class LdapService
if ($this->ldapConnection !== null) return $this->ldapConnection;
// Check LDAP extension in installed
- if (!function_exists('ldap_connect')) {
+ if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {
throw new LdapException('LDAP PHP extension not installed');
}
// Get port from server string if specified.
- $ldapServer = explode(':', config('services.ldap.server'));
- $ldapConnection = ldap_connect($ldapServer[0], count($ldapServer) > 1 ? $ldapServer[1] : 389);
+ $ldapServer = explode(':', $this->config['server']);
+ $ldapConnection = $this->ldap->connect($ldapServer[0], count($ldapServer) > 1 ? $ldapServer[1] : 389);
if ($ldapConnection === false) {
throw new LdapException('Cannot connect to ldap server, Initial connection failed');
}
// Set any required options
- ldap_set_option($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, 3); // TODO - make configurable
+ if ($this->config['version']) {
+ $this->ldap->setOption($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, $this->config['version']);
+ }
$this->ldapConnection = $ldapConnection;
return $this->ldapConnection;
@@ -107,7 +130,7 @@ class LdapService
/**
* Build a filter string by injecting common variables.
- * @param $filterString
+ * @param string $filterString
* @param array $attrs
* @return string
*/
diff --git a/config/services.php b/config/services.php
index 08c2f3217..d71ff4a26 100644
--- a/config/services.php
+++ b/config/services.php
@@ -54,7 +54,8 @@ return [
'dn' => env('LDAP_DN', false),
'pass' => env('LDAP_PASS', false),
'base_dn' => env('LDAP_BASE_DN', false),
- 'user_filter' => env('LDAP_USER_FILTER', '(&(uid=${user}))')
+ 'user_filter' => env('LDAP_USER_FILTER', '(&(uid=${user}))'),
+ 'version' => env('LDAP_VERSION', false)
]
];
diff --git a/resources/views/public.blade.php b/resources/views/public.blade.php
index 52c287987..d34c490a7 100644
--- a/resources/views/public.blade.php
+++ b/resources/views/public.blade.php
@@ -5,19 +5,19 @@
+
-
-
+
-
+
@include('partials/notifications')
@@ -37,12 +37,15 @@
@yield('header-buttons')
@if(isset($signedIn) && $signedIn)
-
-
-
- {{ $currentUser->name }}
+
+
+
+ {{ $currentUser->name }}
+ -
+ Edit Profile
+
-
Logout
diff --git a/tests/ActivityTrackingTest.php b/tests/ActivityTrackingTest.php
index 8a237f880..8ee20ab6b 100644
--- a/tests/ActivityTrackingTest.php
+++ b/tests/ActivityTrackingTest.php
@@ -7,7 +7,7 @@ use Illuminate\Foundation\Testing\DatabaseTransactions;
class ActivityTrackingTest extends TestCase
{
- public function testRecentlyViewedBooks()
+ public function test_recently_viewed_books()
{
$books = \BookStack\Book::all()->take(10);
@@ -21,7 +21,7 @@ class ActivityTrackingTest extends TestCase
->seeInElement('#recents', $books[1]->name);
}
- public function testPopularBooks()
+ public function test_popular_books()
{
$books = \BookStack\Book::all()->take(10);
diff --git a/tests/AuthTest.php b/tests/Auth/AuthTest.php
similarity index 78%
rename from tests/AuthTest.php
rename to tests/Auth/AuthTest.php
index 3faae6506..022dc14fb 100644
--- a/tests/AuthTest.php
+++ b/tests/Auth/AuthTest.php
@@ -5,23 +5,19 @@ use BookStack\EmailConfirmation;
class AuthTest extends TestCase
{
- public function testAuthWorking()
+ public function test_auth_working()
{
$this->visit('/')
->seePageIs('/login');
}
- public function testLogin()
+ public function test_login()
{
- $this->visit('/')
- ->seePageIs('/login');
-
$this->login('admin@admin.com', 'password')
- ->seePageIs('/')
- ->see('BookStack');
+ ->seePageIs('/');
}
- public function testPublicViewing()
+ public function test_public_viewing()
{
$settings = app('BookStack\Services\SettingService');
$settings->put('app-public', 'true');
@@ -30,7 +26,7 @@ class AuthTest extends TestCase
->see('Sign In');
}
- public function testRegistrationShowing()
+ public function test_registration_showing()
{
// Ensure registration form is showing
$this->setSettings(['registration-enabled' => 'true']);
@@ -40,7 +36,7 @@ class AuthTest extends TestCase
->seePageIs('/register');
}
- public function testNormalRegistration()
+ public function test_normal_registration()
{
// Set settings and get user instance
$this->setSettings(['registration-enabled' => 'true']);
@@ -58,7 +54,8 @@ class AuthTest extends TestCase
->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email]);
}
- public function testConfirmedRegistration()
+
+ public function test_confirmed_registration()
{
// Set settings and get user instance
$this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']);
@@ -102,7 +99,32 @@ class AuthTest extends TestCase
->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => true]);
}
- public function testUserCreation()
+ public function test_restricted_registration()
+ {
+ $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true', 'registration-restrict' => 'example.com']);
+ $user = factory(\BookStack\User::class)->make();
+ // Go through registration process
+ $this->visit('/register')
+ ->type($user->name, '#name')
+ ->type($user->email, '#email')
+ ->type($user->password, '#password')
+ ->press('Create Account')
+ ->seePageIs('/register')
+ ->dontSeeInDatabase('users', ['email' => $user->email])
+ ->see('That email domain does not have access to this application');
+
+ $user->email = 'barry@example.com';
+
+ $this->visit('/register')
+ ->type($user->name, '#name')
+ ->type($user->email, '#email')
+ ->type($user->password, '#password')
+ ->press('Create Account')
+ ->seePageIs('/register/confirm')
+ ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
+ }
+
+ public function test_user_creation()
{
$user = factory(\BookStack\User::class)->make();
@@ -120,7 +142,7 @@ class AuthTest extends TestCase
->see($user->name);
}
- public function testUserUpdating()
+ public function test_user_updating()
{
$user = \BookStack\User::all()->last();
$password = $user->password;
@@ -136,7 +158,7 @@ class AuthTest extends TestCase
->notSeeInDatabase('users', ['name' => $user->name]);
}
- public function testUserPasswordUpdate()
+ public function test_user_password_update()
{
$user = \BookStack\User::all()->last();
$userProfilePage = '/users/' . $user->id;
@@ -156,7 +178,7 @@ class AuthTest extends TestCase
$this->assertTrue(Hash::check('newpassword', $userPassword));
}
- public function testUserDeletion()
+ public function test_user_deletion()
{
$userDetails = factory(\BookStack\User::class)->make();
$user = $this->getNewUser($userDetails->toArray());
@@ -170,7 +192,7 @@ class AuthTest extends TestCase
->notSeeInDatabase('users', ['name' => $user->name]);
}
- public function testUserCannotBeDeletedIfLastAdmin()
+ public function test_user_cannot_be_deleted_if_last_admin()
{
$adminRole = \BookStack\Role::getRole('admin');
// Ensure we currently only have 1 admin user
@@ -184,7 +206,7 @@ class AuthTest extends TestCase
->see('You cannot delete the only admin');
}
- public function testLogout()
+ public function test_logout()
{
$this->asAdmin()
->visit('/')
@@ -200,7 +222,7 @@ class AuthTest extends TestCase
* @param string $password
* @return $this
*/
- private function login($email, $password)
+ protected function login($email, $password)
{
return $this->visit('/login')
->type($email, '#email')
diff --git a/tests/Auth/LdapTest.php b/tests/Auth/LdapTest.php
new file mode 100644
index 000000000..6b026dab9
--- /dev/null
+++ b/tests/Auth/LdapTest.php
@@ -0,0 +1,43 @@
+set(['auth.method' => 'ldap', 'services.ldap.base_dn' => 'dc=ldap,dc=local', 'auth.providers.users.driver' => 'ldap']);
+ $this->mockLdap = Mockery::mock(BookStack\Services\Ldap::class);
+ $this->app['BookStack\Services\Ldap'] = $this->mockLdap;
+ $this->mockUser = factory(User::class)->make();
+ }
+
+ public function test_ldap_login()
+ {
+ $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
+ $this->mockLdap->shouldReceive('setOption')->once();
+ $this->mockLdap->shouldReceive('searchAndGetEntries')->twice()
+ ->with($this->resourceId, config('services.ldap.base_dn'), Mockery::type('string'), Mockery::type('array'))
+ ->andReturn(['count' => 1, 0 => [
+ 'uid' => [$this->mockUser->name],
+ 'cn' => [$this->mockUser->name],
+ 'dn' => ['dc=test'.config('services.ldap.base_dn')]
+ ]]);
+ $this->mockLdap->shouldReceive('bind')->times(1)->andReturn(true);
+
+ $this->visit('/login')
+ ->see('Username')
+ ->type($this->mockUser->name, '#username')
+ ->type($this->mockUser->password, '#password')
+ ->press('Sign In')
+ ->seePageIs('/login')->see('Please enter an email to use for this account.');
+ }
+
+}
\ No newline at end of file
diff --git a/tests/SocialAuthTest.php b/tests/Auth/SocialAuthTest.php
similarity index 82%
rename from tests/SocialAuthTest.php
rename to tests/Auth/SocialAuthTest.php
index 820ecd4dd..d5a7e6921 100644
--- a/tests/SocialAuthTest.php
+++ b/tests/Auth/SocialAuthTest.php
@@ -3,13 +3,13 @@
class SocialAuthTest extends TestCase
{
- public function testSocialRegistration()
+ public function test_social_registration()
{
// http://docs.mockery.io/en/latest/reference/startup_methods.html
$user = factory(\BookStack\User::class)->make();
$this->setSettings(['registration-enabled' => 'true']);
- $this->setEnvironment(['GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc', 'APP_URL' => 'http://localhost']);
+ config(['GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc', 'APP_URL' => 'http://localhost']);
$mockSocialite = Mockery::mock('Laravel\Socialite\Contracts\Factory');
$this->app['Laravel\Socialite\Contracts\Factory'] = $mockSocialite;
@@ -32,11 +32,4 @@ class SocialAuthTest extends TestCase
$this->seeInDatabase('social_accounts', ['user_id' => $user->id]);
}
- protected function setEnvironment($array)
- {
- foreach ($array as $key => $value) {
- putenv("$key=$value");
- }
- }
-
}
diff --git a/tests/EntityTest.php b/tests/EntityTest.php
index 1eda7c73c..5bfedb535 100644
--- a/tests/EntityTest.php
+++ b/tests/EntityTest.php
@@ -5,7 +5,7 @@ use Illuminate\Support\Facades\DB;
class EntityTest extends TestCase
{
- public function testEntityCreation()
+ public function test_entity_creation()
{
// Test Creation
@@ -51,7 +51,7 @@ class EntityTest extends TestCase
return \BookStack\Book::find($book->id);
}
- public function testBookSortPageShows()
+ public function test_book_sort_page_shows()
{
$books = \BookStack\Book::all();
$bookToSort = $books[0];
@@ -65,7 +65,7 @@ class EntityTest extends TestCase
->see($books[1]->name);
}
- public function testBookSortItemReturnsBookContent()
+ public function test_book_sort_item_returns_book_content()
{
$books = \BookStack\Book::all();
$bookToSort = $books[0];
@@ -155,7 +155,7 @@ class EntityTest extends TestCase
return $book;
}
- public function testPageSearch()
+ public function test_page_search()
{
$book = \BookStack\Book::all()->first();
$page = $book->pages->first();
@@ -170,7 +170,7 @@ class EntityTest extends TestCase
->seePageIs($page->getUrl());
}
- public function testInvalidPageSearch()
+ public function test_invalid_page_search()
{
$this->asAdmin()
->visit('/')
@@ -180,7 +180,7 @@ class EntityTest extends TestCase
->seeStatusCode(200);
}
- public function testEmptySearchRedirectsBack()
+ public function test_empty_search_redirects_back()
{
$this->asAdmin()
->visit('/')
@@ -188,7 +188,7 @@ class EntityTest extends TestCase
->seePageIs('/');
}
- public function testBookSearch()
+ public function test_book_search()
{
$book = \BookStack\Book::all()->first();
$page = $book->pages->last();
@@ -202,7 +202,7 @@ class EntityTest extends TestCase
->see($chapter->name);
}
- public function testEmptyBookSearchRedirectsBack()
+ public function test_empty_book_search_redirects_back()
{
$book = \BookStack\Book::all()->first();
$this->asAdmin()
@@ -212,7 +212,7 @@ class EntityTest extends TestCase
}
- public function testEntitiesViewableAfterCreatorDeletion()
+ public function test_entities_viewable_after_creator_deletion()
{
// Create required assets and revisions
$creator = $this->getNewUser();
@@ -225,7 +225,7 @@ class EntityTest extends TestCase
$this->checkEntitiesViewable($entities);
}
- public function testEntitiesViewableAfterUpdaterDeletion()
+ public function test_entities_viewable_after_updater_deletion()
{
// Create required assets and revisions
$creator = $this->getNewUser();
diff --git a/tests/PublicViewTest.php b/tests/PublicViewTest.php
index b5141f0f6..58e39dfd9 100644
--- a/tests/PublicViewTest.php
+++ b/tests/PublicViewTest.php
@@ -3,7 +3,7 @@
class PublicViewTest extends TestCase
{
- public function testBooksViewable()
+ public function test_books_viewable()
{
$this->setSettings(['app-public' => 'true']);
$books = \BookStack\Book::orderBy('name', 'asc')->take(10)->get();
@@ -13,14 +13,14 @@ class PublicViewTest extends TestCase
$this->visit('/books')
->seeStatusCode(200)
->see($books[0]->name)
- // Check indavidual book page is showing and it's child contents are visible.
+ // Check individual book page is showing and it's child contents are visible.
->click($bookToVisit->name)
->seePageIs($bookToVisit->getUrl())
->see($bookToVisit->name)
->see($bookToVisit->chapters()->first()->name);
}
- public function testChaptersViewable()
+ public function test_chapters_viewable()
{
$this->setSettings(['app-public' => 'true']);
$chapterToVisit = \BookStack\Chapter::first();
@@ -30,7 +30,7 @@ class PublicViewTest extends TestCase
$this->visit($chapterToVisit->getUrl())
->seeStatusCode(200)
->see($chapterToVisit->name)
- // Check indavidual chapter page is showing and it's child contents are visible.
+ // Check individual chapter page is showing and it's child contents are visible.
->see($pageToVisit->name)
->click($pageToVisit->name)
->see($chapterToVisit->book->name)