diff --git a/app/Activity.php b/app/Activity.php index ac7c1d749..d43419c17 100644 --- a/app/Activity.php +++ b/app/Activity.php @@ -2,8 +2,6 @@ namespace BookStack; -use Illuminate\Database\Eloquent\Model; - /** * @property string key * @property \User user @@ -28,7 +26,7 @@ class Activity extends Model */ public function user() { - return $this->belongsTo('BookStack\User'); + return $this->belongsTo(User::class); } /** diff --git a/app/Book.php b/app/Book.php index de1841459..919af80a5 100644 --- a/app/Book.php +++ b/app/Book.php @@ -1,35 +1,55 @@ -slug; } + /* + * Get the edit url for this book. + * @return string + */ public function getEditUrl() { return $this->getUrl() . '/edit'; } + /** + * Get all pages within this book. + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ public function pages() { - return $this->hasMany('BookStack\Page'); + return $this->hasMany(Page::class); } + /** + * Get all chapters within this book. + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ public function chapters() { - return $this->hasMany('BookStack\Chapter'); + return $this->hasMany(Chapter::class); } + /** + * Get an excerpt of this book's description to the specified length or less. + * @param int $length + * @return string + */ public function getExcerpt($length = 100) { - return strlen($this->description) > $length ? substr($this->description, 0, $length-3) . '...' : $this->description; + $description = $this->description; + return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description; } } diff --git a/app/Chapter.php b/app/Chapter.php index b6c8684a0..08faef68e 100644 --- a/app/Chapter.php +++ b/app/Chapter.php @@ -5,25 +5,43 @@ class Chapter extends Entity { protected $fillable = ['name', 'description', 'priority', 'book_id']; + /** + * Get the book this chapter is within. + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ public function book() { - return $this->belongsTo('BookStack\Book'); + return $this->belongsTo(Book::class); } + /** + * Get the pages that this chapter contains. + * @return mixed + */ public function pages() { - return $this->hasMany('BookStack\Page')->orderBy('priority', 'ASC'); + return $this->hasMany(Page::class)->orderBy('priority', 'ASC'); } + /** + * Get the url of this chapter. + * @return string + */ public function getUrl() { $bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug; return '/books/' . $bookSlug. '/chapter/' . $this->slug; } + /** + * Get an excerpt of this chapter's description to the specified length or less. + * @param int $length + * @return string + */ public function getExcerpt($length = 100) { - return strlen($this->description) > $length ? substr($this->description, 0, $length-3) . '...' : $this->description; + $description = $this->description; + return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description; } } diff --git a/app/Console/Commands/RegeneratePermissions.php b/app/Console/Commands/RegeneratePermissions.php new file mode 100644 index 000000000..60d5f4e45 --- /dev/null +++ b/app/Console/Commands/RegeneratePermissions.php @@ -0,0 +1,51 @@ +permissionService = $permissionService; + parent::__construct(); + } + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle() + { + $this->permissionService->buildJointPermissions(); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index e3a71bd14..b725c9e21 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -15,6 +15,7 @@ class Kernel extends ConsoleKernel protected $commands = [ \BookStack\Console\Commands\Inspire::class, \BookStack\Console\Commands\ResetViews::class, + \BookStack\Console\Commands\RegeneratePermissions::class, ]; /** diff --git a/app/EmailConfirmation.php b/app/EmailConfirmation.php index 46912e733..e77b754bb 100644 --- a/app/EmailConfirmation.php +++ b/app/EmailConfirmation.php @@ -1,15 +1,16 @@ -belongsTo('BookStack\User'); + return $this->belongsTo(User::class); } + } diff --git a/app/Entity.php b/app/Entity.php index 4f97c6bab..a0b25eba7 100644 --- a/app/Entity.php +++ b/app/Entity.php @@ -43,7 +43,7 @@ abstract class Entity extends Ownable */ public function activity() { - return $this->morphMany('BookStack\Activity', 'entity')->orderBy('created_at', 'desc'); + return $this->morphMany(Activity::class, 'entity')->orderBy('created_at', 'desc'); } /** @@ -51,15 +51,15 @@ abstract class Entity extends Ownable */ public function views() { - return $this->morphMany('BookStack\View', 'viewable'); + return $this->morphMany(View::class, 'viewable'); } /** * Get this entities restrictions. */ - public function restrictions() + public function permissions() { - return $this->morphMany('BookStack\Restriction', 'restrictable'); + return $this->morphMany(EntityPermission::class, 'restrictable'); } /** @@ -70,7 +70,28 @@ abstract class Entity extends Ownable */ public function hasRestriction($role_id, $action) { - return $this->restrictions->where('role_id', $role_id)->where('action', $action)->count() > 0; + return $this->permissions()->where('role_id', '=', $role_id) + ->where('action', '=', $action)->count() > 0; + } + + /** + * Check if this entity has live (active) restrictions in place. + * @param $role_id + * @param $action + * @return bool + */ + public function hasActiveRestriction($role_id, $action) + { + return $this->getRawAttribute('restricted') && $this->hasRestriction($role_id, $action); + } + + /** + * Get the entity jointPermissions this is connected to. + * @return \Illuminate\Database\Eloquent\Relations\MorphMany + */ + public function jointPermissions() + { + return $this->morphMany(JointPermission::class, 'entity'); } /** @@ -81,7 +102,16 @@ abstract class Entity extends Ownable */ public static function isA($type) { - return static::getClassName() === strtolower($type); + return static::getType() === strtolower($type); + } + + /** + * Get entity type. + * @return mixed + */ + public static function getType() + { + return strtolower(static::getClassName()); } /** diff --git a/app/Restriction.php b/app/EntityPermission.php similarity index 66% rename from app/Restriction.php rename to app/EntityPermission.php index 58d117997..eaf0a8951 100644 --- a/app/Restriction.php +++ b/app/EntityPermission.php @@ -1,10 +1,7 @@ -morphTo(); + return $this->morphTo('restrictable'); } } diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php index 91c965145..5b97fbdaf 100644 --- a/app/Http/Controllers/BookController.php +++ b/app/Http/Controllers/BookController.php @@ -71,11 +71,7 @@ class BookController extends Controller 'name' => 'required|string|max:255', 'description' => 'string|max:1000' ]); - $book = $this->bookRepo->newFromInput($request->all()); - $book->slug = $this->bookRepo->findSuitableSlug($book->name); - $book->created_by = Auth::user()->id; - $book->updated_by = Auth::user()->id; - $book->save(); + $book = $this->bookRepo->createFromInput($request->all()); Activity::add($book, 'book_create', $book->id); return redirect($book->getUrl()); } @@ -88,6 +84,7 @@ class BookController extends Controller public function show($slug) { $book = $this->bookRepo->getBySlug($slug); + $this->checkOwnablePermission('book-view', $book); $bookChildren = $this->bookRepo->getChildren($book); Views::add($book); $this->setPageTitle($book->getShortName()); @@ -121,10 +118,7 @@ class BookController extends Controller 'name' => 'required|string|max:255', 'description' => 'string|max:1000' ]); - $book->fill($request->all()); - $book->slug = $this->bookRepo->findSuitableSlug($book->name, $book->id); - $book->updated_by = Auth::user()->id; - $book->save(); + $book = $this->bookRepo->updateFromInput($book, $request->all()); Activity::add($book, 'book_update', $book->id); return redirect($book->getUrl()); } @@ -209,6 +203,7 @@ class BookController extends Controller // Add activity for books foreach ($sortedBooks as $bookId) { $updatedBook = $this->bookRepo->getById($bookId); + $this->bookRepo->updateBookPermissions($updatedBook); Activity::add($updatedBook, 'book_sort', $updatedBook->id); } @@ -226,7 +221,7 @@ class BookController extends Controller $this->checkOwnablePermission('book-delete', $book); Activity::addMessage('book_delete', 0, $book->name); Activity::removeEntity($book); - $this->bookRepo->destroyBySlug($bookSlug); + $this->bookRepo->destroy($book); return redirect('/books'); } @@ -257,7 +252,7 @@ class BookController extends Controller { $book = $this->bookRepo->getBySlug($bookSlug); $this->checkOwnablePermission('restrictions-manage', $book); - $this->bookRepo->updateRestrictionsFromRequest($request, $book); + $this->bookRepo->updateEntityPermissionsFromRequest($request, $book); session()->flash('success', 'Book Restrictions Updated'); return redirect($book->getUrl()); } diff --git a/app/Http/Controllers/ChapterController.php b/app/Http/Controllers/ChapterController.php index 4641ddbdb..69e9488b9 100644 --- a/app/Http/Controllers/ChapterController.php +++ b/app/Http/Controllers/ChapterController.php @@ -57,12 +57,9 @@ class ChapterController extends Controller $book = $this->bookRepo->getBySlug($bookSlug); $this->checkOwnablePermission('chapter-create', $book); - $chapter = $this->chapterRepo->newFromInput($request->all()); - $chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id); - $chapter->priority = $this->bookRepo->getNewPriority($book); - $chapter->created_by = auth()->user()->id; - $chapter->updated_by = auth()->user()->id; - $book->chapters()->save($chapter); + $input = $request->all(); + $input['priority'] = $this->bookRepo->getNewPriority($book); + $chapter = $this->chapterRepo->createFromInput($request->all(), $book); Activity::add($chapter, 'chapter_create', $book->id); return redirect($chapter->getUrl()); } @@ -77,6 +74,7 @@ class ChapterController extends Controller { $book = $this->bookRepo->getBySlug($bookSlug); $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); + $this->checkOwnablePermission('chapter-view', $chapter); $sidebarTree = $this->bookRepo->getChildren($book); Views::add($chapter); $this->setPageTitle($chapter->getShortName()); @@ -186,7 +184,7 @@ class ChapterController extends Controller $book = $this->bookRepo->getBySlug($bookSlug); $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); $this->checkOwnablePermission('restrictions-manage', $chapter); - $this->chapterRepo->updateRestrictionsFromRequest($request, $chapter); + $this->chapterRepo->updateEntityPermissionsFromRequest($request, $chapter); session()->flash('success', 'Chapter Restrictions Updated'); return redirect($chapter->getUrl()); } diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index a645ede02..19e5632a4 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -69,7 +69,7 @@ class PageController extends Controller { $book = $this->bookRepo->getBySlug($bookSlug); $draft = $this->pageRepo->getById($pageId, true); - $this->checkOwnablePermission('page-create', $draft); + $this->checkOwnablePermission('page-create', $book); $this->setPageTitle('Edit Page Draft'); return view('pages/create', ['draft' => $draft, 'book' => $book]); @@ -128,6 +128,8 @@ class PageController extends Controller return redirect($page->getUrl()); } + $this->checkOwnablePermission('page-view', $page); + $sidebarTree = $this->bookRepo->getChildren($book); Views::add($page); $this->setPageTitle($page->getShortName()); @@ -449,7 +451,7 @@ class PageController extends Controller } /** - * Set the restrictions for this page. + * Set the permissions for this page. * @param $bookSlug * @param $pageSlug * @param Request $request @@ -460,8 +462,8 @@ class PageController extends Controller $book = $this->bookRepo->getBySlug($bookSlug); $page = $this->pageRepo->getBySlug($pageSlug, $book->id); $this->checkOwnablePermission('restrictions-manage', $page); - $this->pageRepo->updateRestrictionsFromRequest($request, $page); - session()->flash('success', 'Page Restrictions Updated'); + $this->pageRepo->updateEntityPermissionsFromRequest($request, $page); + session()->flash('success', 'Page Permissions Updated'); return redirect($page->getUrl()); } diff --git a/app/Http/Controllers/PermissionController.php b/app/Http/Controllers/PermissionController.php index c565bb20a..ed430c0b7 100644 --- a/app/Http/Controllers/PermissionController.php +++ b/app/Http/Controllers/PermissionController.php @@ -2,6 +2,7 @@ use BookStack\Exceptions\PermissionsException; use BookStack\Repos\PermissionsRepo; +use BookStack\Services\PermissionService; use Illuminate\Http\Request; use BookStack\Http\Requests; @@ -62,11 +63,13 @@ class PermissionController extends Controller * Show the form for editing a user role. * @param $id * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + * @throws PermissionsException */ public function editRole($id) { $this->checkPermission('user-roles-manage'); $role = $this->permissionsRepo->getRoleById($id); + if ($role->hidden) throw new PermissionsException('This role cannot be edited'); return view('settings/roles/edit', ['role' => $role]); } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index d59931640..6956b8d18 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -49,7 +49,8 @@ class UserController extends Controller { $this->checkPermission('users-manage'); $authMethod = config('auth.method'); - return view('users/create', ['authMethod' => $authMethod]); + $roles = $this->userRepo->getAssignableRoles(); + return view('users/create', ['authMethod' => $authMethod, 'roles' => $roles]); } /** @@ -117,7 +118,8 @@ class UserController extends Controller $user = $this->user->findOrFail($id); $activeSocialDrivers = $socialAuthService->getActiveDrivers(); $this->setPageTitle('User Profile'); - return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod]); + $roles = $this->userRepo->getAssignableRoles(); + return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]); } /** diff --git a/app/JointPermission.php b/app/JointPermission.php new file mode 100644 index 000000000..6d0b0212e --- /dev/null +++ b/app/JointPermission.php @@ -0,0 +1,24 @@ +belongsTo(Role::class); + } + + /** + * Get the entity this points to. + * @return \Illuminate\Database\Eloquent\Relations\MorphOne + */ + public function entity() + { + return $this->morphOne(Entity::class, 'entity'); + } +} diff --git a/app/Model.php b/app/Model.php new file mode 100644 index 000000000..9ec2b7362 --- /dev/null +++ b/app/Model.php @@ -0,0 +1,19 @@ +belongsTo('BookStack\User', 'created_by'); + return $this->belongsTo(User::class, 'created_by'); } /** @@ -19,7 +18,7 @@ abstract class Ownable extends Model */ public function updatedBy() { - return $this->belongsTo('BookStack\User', 'updated_by'); + return $this->belongsTo(User::class, 'updated_by'); } /** diff --git a/app/Page.php b/app/Page.php index d2a303f61..c6978d34b 100644 --- a/app/Page.php +++ b/app/Page.php @@ -1,8 +1,5 @@ -toArray(), array_flip($this->simpleAttributes)); @@ -17,26 +18,46 @@ class Page extends Entity return $array; } + /** + * Get the book this page sits in. + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ public function book() { - return $this->belongsTo('BookStack\Book'); + return $this->belongsTo(Book::class); } + /** + * Get the chapter that this page is in, If applicable. + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ public function chapter() { - return $this->belongsTo('BookStack\Chapter'); + return $this->belongsTo(Chapter::class); } + /** + * Check if this page has a chapter. + * @return bool + */ public function hasChapter() { return $this->chapter()->count() > 0; } + /** + * Get the associated page revisions, ordered by created date. + * @return mixed + */ public function revisions() { - return $this->hasMany('BookStack\PageRevision')->where('type', '=', 'version')->orderBy('created_at', 'desc'); + return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc'); } + /** + * Get the url for this page. + * @return string + */ public function getUrl() { $bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug; @@ -45,6 +66,11 @@ class Page extends Entity return '/books/' . $bookSlug . $midText . $idComponent; } + /** + * Get an excerpt of this page's content to the specified length. + * @param int $length + * @return mixed + */ public function getExcerpt($length = 100) { $text = strlen($this->text) > $length ? substr($this->text, 0, $length-3) . '...' : $this->text; diff --git a/app/PageRevision.php b/app/PageRevision.php index c258913ff..dae74cd0f 100644 --- a/app/PageRevision.php +++ b/app/PageRevision.php @@ -1,6 +1,5 @@ belongsTo('BookStack\User', 'created_by'); + return $this->belongsTo(User::class, 'created_by'); } /** @@ -21,7 +20,7 @@ class PageRevision extends Model */ public function page() { - return $this->belongsTo('BookStack\Page'); + return $this->belongsTo(Page::class); } /** diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index c027578a7..509b86182 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -3,6 +3,7 @@ namespace BookStack\Providers; use Auth; +use BookStack\Services\LdapService; use Illuminate\Support\ServiceProvider; class AuthServiceProvider extends ServiceProvider @@ -25,7 +26,7 @@ class AuthServiceProvider extends ServiceProvider public function register() { Auth::provider('ldap', function($app, array $config) { - return new LdapUserProvider($config['model'], $app['BookStack\Services\LdapService']); + return new LdapUserProvider($config['model'], $app[LdapService::class]); }); } } diff --git a/app/Providers/CustomFacadeProvider.php b/app/Providers/CustomFacadeProvider.php index 9b290039c..b2c7acf5e 100644 --- a/app/Providers/CustomFacadeProvider.php +++ b/app/Providers/CustomFacadeProvider.php @@ -2,11 +2,18 @@ namespace BookStack\Providers; +use BookStack\Activity; use BookStack\Services\ImageService; +use BookStack\Services\PermissionService; use BookStack\Services\ViewService; +use BookStack\Setting; +use BookStack\View; +use Illuminate\Contracts\Cache\Repository; +use Illuminate\Contracts\Filesystem\Factory; use Illuminate\Support\ServiceProvider; use BookStack\Services\ActivityService; use BookStack\Services\SettingService; +use Intervention\Image\ImageManager; class CustomFacadeProvider extends ServiceProvider { @@ -29,30 +36,30 @@ class CustomFacadeProvider extends ServiceProvider { $this->app->bind('activity', function() { return new ActivityService( - $this->app->make('BookStack\Activity'), - $this->app->make('BookStack\Services\RestrictionService') + $this->app->make(Activity::class), + $this->app->make(PermissionService::class) ); }); $this->app->bind('views', function() { return new ViewService( - $this->app->make('BookStack\View'), - $this->app->make('BookStack\Services\RestrictionService') + $this->app->make(View::class), + $this->app->make(PermissionService::class) ); }); $this->app->bind('setting', function() { return new SettingService( - $this->app->make('BookStack\Setting'), - $this->app->make('Illuminate\Contracts\Cache\Repository') + $this->app->make(Setting::class), + $this->app->make(Repository::class) ); }); $this->app->bind('images', function() { return new ImageService( - $this->app->make('Intervention\Image\ImageManager'), - $this->app->make('Illuminate\Contracts\Filesystem\Factory'), - $this->app->make('Illuminate\Contracts\Cache\Repository') + $this->app->make(ImageManager::class), + $this->app->make(Factory::class), + $this->app->make(Repository::class) ); }); } diff --git a/app/Repos/BookRepo.php b/app/Repos/BookRepo.php index 1a56843ae..e62b101c5 100644 --- a/app/Repos/BookRepo.php +++ b/app/Repos/BookRepo.php @@ -1,5 +1,6 @@ restrictionService->enforceBookRestrictions($this->book, 'view'); + return $this->permissionService->enforceBookRestrictions($this->book, 'view'); } /** @@ -123,21 +124,43 @@ class BookRepo extends EntityRepo /** * Get a new book instance from request input. - * @param $input + * @param array $input * @return Book */ - public function newFromInput($input) + public function createFromInput($input) { - return $this->book->newInstance($input); + $book = $this->book->newInstance($input); + $book->slug = $this->findSuitableSlug($book->name); + $book->created_by = auth()->user()->id; + $book->updated_by = auth()->user()->id; + $book->save(); + $this->permissionService->buildJointPermissionsForEntity($book); + return $book; } /** - * Destroy a book identified by the given slug. - * @param $bookSlug + * Update the given book from user input. + * @param Book $book + * @param $input + * @return Book */ - public function destroyBySlug($bookSlug) + public function updateFromInput(Book $book, $input) + { + $book->fill($input); + $book->slug = $this->findSuitableSlug($book->name, $book->id); + $book->updated_by = auth()->user()->id; + $book->save(); + $this->permissionService->buildJointPermissionsForEntity($book); + return $book; + } + + /** + * Destroy the given book. + * @param Book $book + * @throws \Exception + */ + public function destroy(Book $book) { - $book = $this->getBySlug($bookSlug); foreach ($book->pages as $page) { $this->pageRepo->destroy($page); } @@ -145,10 +168,20 @@ class BookRepo extends EntityRepo $this->chapterRepo->destroy($chapter); } $book->views()->delete(); - $book->restrictions()->delete(); + $book->permissions()->delete(); + $this->permissionService->deleteJointPermissionsForEntity($book); $book->delete(); } + /** + * Alias method to update the book jointPermissions in the PermissionService. + * @param Book $book + */ + public function updateBookPermissions(Book $book) + { + $this->permissionService->buildJointPermissionsForEntity($book); + } + /** * Get the next child element priority. * @param Book $book @@ -204,7 +237,7 @@ class BookRepo extends EntityRepo public function getChildren(Book $book, $filterDrafts = false) { $pageQuery = $book->pages()->where('chapter_id', '=', 0); - $pageQuery = $this->restrictionService->enforcePageRestrictions($pageQuery, 'view'); + $pageQuery = $this->permissionService->enforcePageRestrictions($pageQuery, 'view'); if ($filterDrafts) { $pageQuery = $pageQuery->where('draft', '=', false); @@ -213,10 +246,10 @@ class BookRepo extends EntityRepo $pages = $pageQuery->get(); $chapterQuery = $book->chapters()->with(['pages' => function($query) use ($filterDrafts) { - $this->restrictionService->enforcePageRestrictions($query, 'view'); + $this->permissionService->enforcePageRestrictions($query, 'view'); if ($filterDrafts) $query->where('draft', '=', false); }]); - $chapterQuery = $this->restrictionService->enforceChapterRestrictions($chapterQuery, 'view'); + $chapterQuery = $this->permissionService->enforceChapterRestrictions($chapterQuery, 'view'); $chapters = $chapterQuery->get(); $children = $pages->merge($chapters); $bookSlug = $book->slug; @@ -253,7 +286,7 @@ class BookRepo extends EntityRepo public function getBySearch($term, $count = 20, $paginationAppends = []) { $terms = $this->prepareSearchTerms($term); - $books = $this->restrictionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms)) + $books = $this->permissionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms)) ->paginate($count)->appends($paginationAppends); $words = join('|', explode(' ', preg_quote(trim($term), '/'))); foreach ($books as $book) { diff --git a/app/Repos/ChapterRepo.php b/app/Repos/ChapterRepo.php index 530f550b1..0980e93a7 100644 --- a/app/Repos/ChapterRepo.php +++ b/app/Repos/ChapterRepo.php @@ -2,6 +2,7 @@ use Activity; +use BookStack\Book; use BookStack\Exceptions\NotFoundException; use Illuminate\Support\Str; use BookStack\Chapter; @@ -9,12 +10,12 @@ use BookStack\Chapter; class ChapterRepo extends EntityRepo { /** - * Base query for getting chapters, Takes restrictions into account. + * Base query for getting chapters, Takes permissions into account. * @return mixed */ private function chapterQuery() { - return $this->restrictionService->enforceChapterRestrictions($this->chapter, 'view'); + return $this->permissionService->enforceChapterRestrictions($this->chapter, 'view'); } /** @@ -66,7 +67,7 @@ class ChapterRepo extends EntityRepo */ public function getChildren(Chapter $chapter) { - $pages = $this->restrictionService->enforcePageRestrictions($chapter->pages())->get(); + $pages = $this->permissionService->enforcePageRestrictions($chapter->pages())->get(); // Sort items with drafts first then by priority. return $pages->sortBy(function($child, $key) { $score = $child->priority; @@ -78,11 +79,18 @@ class ChapterRepo extends EntityRepo /** * Create a new chapter from request input. * @param $input - * @return $this + * @param Book $book + * @return Chapter */ - public function newFromInput($input) + public function createFromInput($input, Book $book) { - return $this->chapter->fill($input); + $chapter = $this->chapter->newInstance($input); + $chapter->slug = $this->findSuitableSlug($chapter->name, $book->id); + $chapter->created_by = auth()->user()->id; + $chapter->updated_by = auth()->user()->id; + $chapter = $book->chapters()->save($chapter); + $this->permissionService->buildJointPermissionsForEntity($chapter); + return $chapter; } /** @@ -99,7 +107,8 @@ class ChapterRepo extends EntityRepo } Activity::removeEntity($chapter); $chapter->views()->delete(); - $chapter->restrictions()->delete(); + $chapter->permissions()->delete(); + $this->permissionService->deleteJointPermissionsForEntity($chapter); $chapter->delete(); } @@ -159,7 +168,7 @@ class ChapterRepo extends EntityRepo public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = []) { $terms = $this->prepareSearchTerms($term); - $chapters = $this->restrictionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms)) + $chapters = $this->permissionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms)) ->paginate($count)->appends($paginationAppends); $words = join('|', explode(' ', preg_quote(trim($term), '/'))); foreach ($chapters as $chapter) { diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php index cb3dd6674..6b4076e6e 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -4,7 +4,7 @@ use BookStack\Book; use BookStack\Chapter; use BookStack\Entity; use BookStack\Page; -use BookStack\Services\RestrictionService; +use BookStack\Services\PermissionService; use BookStack\User; class EntityRepo @@ -26,9 +26,9 @@ class EntityRepo public $page; /** - * @var RestrictionService + * @var PermissionService */ - protected $restrictionService; + protected $permissionService; /** * EntityService constructor. @@ -38,7 +38,7 @@ class EntityRepo $this->book = app(Book::class); $this->chapter = app(Chapter::class); $this->page = app(Page::class); - $this->restrictionService = app(RestrictionService::class); + $this->permissionService = app(PermissionService::class); } /** @@ -50,7 +50,7 @@ class EntityRepo */ public function getRecentlyCreatedBooks($count = 20, $page = 0, $additionalQuery = false) { - $query = $this->restrictionService->enforceBookRestrictions($this->book) + $query = $this->permissionService->enforceBookRestrictions($this->book) ->orderBy('created_at', 'desc'); if ($additionalQuery !== false && is_callable($additionalQuery)) { $additionalQuery($query); @@ -66,7 +66,7 @@ class EntityRepo */ public function getRecentlyUpdatedBooks($count = 20, $page = 0) { - return $this->restrictionService->enforceBookRestrictions($this->book) + return $this->permissionService->enforceBookRestrictions($this->book) ->orderBy('updated_at', 'desc')->skip($page * $count)->take($count)->get(); } @@ -79,7 +79,7 @@ class EntityRepo */ public function getRecentlyCreatedPages($count = 20, $page = 0, $additionalQuery = false) { - $query = $this->restrictionService->enforcePageRestrictions($this->page) + $query = $this->permissionService->enforcePageRestrictions($this->page) ->orderBy('created_at', 'desc')->where('draft', '=', false); if ($additionalQuery !== false && is_callable($additionalQuery)) { $additionalQuery($query); @@ -96,7 +96,7 @@ class EntityRepo */ public function getRecentlyCreatedChapters($count = 20, $page = 0, $additionalQuery = false) { - $query = $this->restrictionService->enforceChapterRestrictions($this->chapter) + $query = $this->permissionService->enforceChapterRestrictions($this->chapter) ->orderBy('created_at', 'desc'); if ($additionalQuery !== false && is_callable($additionalQuery)) { $additionalQuery($query); @@ -112,7 +112,7 @@ class EntityRepo */ public function getRecentlyUpdatedPages($count = 20, $page = 0) { - return $this->restrictionService->enforcePageRestrictions($this->page) + return $this->permissionService->enforcePageRestrictions($this->page) ->where('draft', '=', false) ->orderBy('updated_at', 'desc')->with('book')->skip($page * $count)->take($count)->get(); } @@ -136,14 +136,14 @@ class EntityRepo * @param $request * @param Entity $entity */ - public function updateRestrictionsFromRequest($request, Entity $entity) + public function updateEntityPermissionsFromRequest($request, Entity $entity) { $entity->restricted = $request->has('restricted') && $request->get('restricted') === 'true'; - $entity->restrictions()->delete(); + $entity->permissions()->delete(); if ($request->has('restrictions')) { foreach ($request->get('restrictions') as $roleId => $restrictions) { foreach ($restrictions as $action => $value) { - $entity->restrictions()->create([ + $entity->permissions()->create([ 'role_id' => $roleId, 'action' => strtolower($action) ]); @@ -151,6 +151,7 @@ class EntityRepo } } $entity->save(); + $this->permissionService->buildJointPermissionsForEntity($entity); } /** diff --git a/app/Repos/ImageRepo.php b/app/Repos/ImageRepo.php index 8dd4d346d..916ebd3e1 100644 --- a/app/Repos/ImageRepo.php +++ b/app/Repos/ImageRepo.php @@ -4,7 +4,7 @@ use BookStack\Image; use BookStack\Page; use BookStack\Services\ImageService; -use BookStack\Services\RestrictionService; +use BookStack\Services\PermissionService; use Setting; use Symfony\Component\HttpFoundation\File\UploadedFile; @@ -20,14 +20,14 @@ class ImageRepo * ImageRepo constructor. * @param Image $image * @param ImageService $imageService - * @param RestrictionService $restrictionService + * @param PermissionService $permissionService * @param Page $page */ - public function __construct(Image $image, ImageService $imageService, RestrictionService $restrictionService, Page $page) + public function __construct(Image $image, ImageService $imageService, PermissionService $permissionService, Page $page) { $this->image = $image; $this->imageService = $imageService; - $this->restictionService = $restrictionService; + $this->restictionService = $permissionService; $this->page = $page; } diff --git a/app/Repos/PageRepo.php b/app/Repos/PageRepo.php index ef470c01d..549ec98a7 100644 --- a/app/Repos/PageRepo.php +++ b/app/Repos/PageRepo.php @@ -32,7 +32,7 @@ class PageRepo extends EntityRepo */ private function pageQuery($allowDrafts = false) { - $query = $this->restrictionService->enforcePageRestrictions($this->page, 'view'); + $query = $this->permissionService->enforcePageRestrictions($this->page, 'view'); if (!$allowDrafts) { $query = $query->where('draft', '=', false); } @@ -76,7 +76,7 @@ class PageRepo extends EntityRepo { $revision = $this->pageRevision->where('slug', '=', $pageSlug) ->whereHas('page', function ($query) { - $this->restrictionService->enforcePageRestrictions($query); + $this->permissionService->enforcePageRestrictions($query); }) ->where('type', '=', 'version') ->where('book_slug', '=', $bookSlug)->orderBy('created_at', 'desc') @@ -168,6 +168,7 @@ class PageRepo extends EntityRepo if ($chapter) $page->chapter_id = $chapter->id; $book->pages()->save($page); + $this->permissionService->buildJointPermissionsForEntity($page); return $page; } @@ -241,7 +242,7 @@ class PageRepo extends EntityRepo public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = []) { $terms = $this->prepareSearchTerms($term); - $pages = $this->restrictionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms)) + $pages = $this->permissionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms)) ->paginate($count)->appends($paginationAppends); // Add highlights to page text. @@ -577,12 +578,13 @@ class PageRepo extends EntityRepo * Destroy a given page along with its dependencies. * @param $page */ - public function destroy($page) + public function destroy(Page $page) { Activity::removeEntity($page); $page->views()->delete(); $page->revisions()->delete(); - $page->restrictions()->delete(); + $page->permissions()->delete(); + $this->permissionService->deleteJointPermissionsForEntity($page); $page->delete(); } diff --git a/app/Repos/PermissionsRepo.php b/app/Repos/PermissionsRepo.php index 3c5efde23..e026d83e8 100644 --- a/app/Repos/PermissionsRepo.php +++ b/app/Repos/PermissionsRepo.php @@ -2,8 +2,9 @@ use BookStack\Exceptions\PermissionsException; -use BookStack\Permission; +use BookStack\RolePermission; use BookStack\Role; +use BookStack\Services\PermissionService; use Setting; class PermissionsRepo @@ -11,16 +12,21 @@ class PermissionsRepo protected $permission; protected $role; + protected $permissionService; + + protected $systemRoles = ['admin', 'public']; /** * PermissionsRepo constructor. - * @param $permission - * @param $role + * @param RolePermission $permission + * @param Role $role + * @param PermissionService $permissionService */ - public function __construct(Permission $permission, Role $role) + public function __construct(RolePermission $permission, Role $role, PermissionService $permissionService) { $this->permission = $permission; $this->role = $role; + $this->permissionService = $permissionService; } /** @@ -29,7 +35,7 @@ class PermissionsRepo */ public function getAllRoles() { - return $this->role->all(); + return $this->role->where('hidden', '=', false)->get(); } /** @@ -39,7 +45,7 @@ class PermissionsRepo */ public function getAllRolesExcept(Role $role) { - return $this->role->where('id', '!=', $role->id)->get(); + return $this->role->where('id', '!=', $role->id)->where('hidden', '=', false)->get(); } /** @@ -69,6 +75,7 @@ class PermissionsRepo $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : []; $this->assignRolePermissions($role, $permissions); + $this->permissionService->buildJointPermissionForRole($role); return $role; } @@ -77,10 +84,14 @@ class PermissionsRepo * Ensure Admin role always has all permissions. * @param $roleId * @param $roleData + * @throws PermissionsException */ public function updateRole($roleId, $roleData) { $role = $this->role->findOrFail($roleId); + + if ($role->hidden) throw new PermissionsException("Cannot update a hidden role"); + $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : []; $this->assignRolePermissions($role, $permissions); @@ -91,6 +102,7 @@ class PermissionsRepo $role->fill($roleData); $role->save(); + $this->permissionService->buildJointPermissionForRole($role); } /** @@ -122,8 +134,8 @@ class PermissionsRepo $role = $this->role->findOrFail($roleId); // Prevent deleting admin role or default registration role. - if ($role->name === 'admin') { - throw new PermissionsException('The admin role cannot be deleted'); + if ($role->system_name && in_array($role->system_name, $this->systemRoles)) { + throw new PermissionsException('This role is a system role and cannot be deleted'); } else if ($role->id == setting('registration-role')) { throw new PermissionsException('This role cannot be deleted while set as the default registration role.'); } @@ -136,6 +148,7 @@ class PermissionsRepo } } + $this->permissionService->deleteJointPermissionsForRole($role); $role->delete(); } diff --git a/app/Repos/UserRepo.php b/app/Repos/UserRepo.php index 9b5c8d7e7..b4931bdff 100644 --- a/app/Repos/UserRepo.php +++ b/app/Repos/UserRepo.php @@ -168,6 +168,15 @@ class UserRepo ]; } + /** + * Get the roles in the system that are assignable to a user. + * @return mixed + */ + public function getAssignableRoles() + { + return $this->role->visible(); + } + /** * Get all the roles which can be given restricted access to * other entities in the system. @@ -175,7 +184,7 @@ class UserRepo */ public function getRestrictableRoles() { - return $this->role->where('name', '!=', 'admin')->get(); + return $this->role->where('hidden', '=', false)->where('system_name', '=', '')->get(); } } \ No newline at end of file diff --git a/app/Role.php b/app/Role.php index 4e14db181..8d0a79e75 100644 --- a/app/Role.php +++ b/app/Role.php @@ -1,8 +1,5 @@ -belongsToMany('BookStack\User'); + return $this->belongsToMany(User::class); } /** - * The permissions that belong to the role. + * Get all related JointPermissions. + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function jointPermissions() + { + return $this->hasMany(JointPermission::class); + } + + /** + * The RolePermissions that belong to the role. */ public function permissions() { - return $this->belongsToMany('BookStack\Permission'); + return $this->belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id'); } /** * Check if this role has a permission. - * @param $permission + * @param $permissionName + * @return bool */ - public function hasPermission($permission) + public function hasPermission($permissionName) { - return $this->permissions->pluck('name')->contains($permission); + $permissions = $this->getRelationValue('permissions'); + foreach ($permissions as $permission) { + if ($permission->getRawAttribute('name') === $permissionName) return true; + } + return false; } /** * Add a permission to this role. - * @param Permission $permission + * @param RolePermission $permission */ - public function attachPermission(Permission $permission) + public function attachPermission(RolePermission $permission) { $this->permissions()->attach($permission->id); } /** * Detach a single permission from this role. - * @param Permission $permission + * @param RolePermission $permission */ - public function detachPermission(Permission $permission) + public function detachPermission(RolePermission $permission) { $this->permissions()->detach($permission->id); } @@ -61,4 +72,24 @@ class Role extends Model { return static::where('name', '=', $roleName)->first(); } + + /** + * Get the role object for the specified system role. + * @param $roleName + * @return mixed + */ + public static function getSystemRole($roleName) + { + return static::where('system_name', '=', $roleName)->first(); + } + + /** + * Get all visible roles + * @return mixed + */ + public static function visible() + { + return static::where('hidden', '=', false)->orderBy('name')->get(); + } + } diff --git a/app/Permission.php b/app/RolePermission.php similarity index 63% rename from app/Permission.php rename to app/RolePermission.php index a146dcf63..ded6f6394 100644 --- a/app/Permission.php +++ b/app/RolePermission.php @@ -1,22 +1,19 @@ -belongsToMany('BookStack\Role'); + return $this->belongsToMany(Role::class, 'permission_role','permission_id', 'role_id'); } /** * Get the permission object by name. - * @param $roleName + * @param $name * @return mixed */ public static function getByName($name) diff --git a/app/Services/ActivityService.php b/app/Services/ActivityService.php index d0029b6c4..90a3a6d82 100644 --- a/app/Services/ActivityService.php +++ b/app/Services/ActivityService.php @@ -8,17 +8,17 @@ class ActivityService { protected $activity; protected $user; - protected $restrictionService; + protected $permissionService; /** * ActivityService constructor. * @param Activity $activity - * @param RestrictionService $restrictionService + * @param PermissionService $permissionService */ - public function __construct(Activity $activity, RestrictionService $restrictionService) + public function __construct(Activity $activity, PermissionService $permissionService) { $this->activity = $activity; - $this->restrictionService = $restrictionService; + $this->permissionService = $permissionService; $this->user = auth()->user(); } @@ -88,7 +88,7 @@ class ActivityService */ public function latest($count = 20, $page = 0) { - $activityList = $this->restrictionService + $activityList = $this->permissionService ->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type') ->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get(); @@ -105,8 +105,16 @@ class ActivityService */ public function entityActivity($entity, $count = 20, $page = 0) { - $activity = $entity->hasMany('BookStack\Activity')->orderBy('created_at', 'desc') - ->skip($count * $page)->take($count)->get(); + if ($entity->isA('book')) { + $query = $this->activity->where('book_id', '=', $entity->id); + } else { + $query = $this->activity->where('entity_type', '=', get_class($entity)) + ->where('entity_id', '=', $entity->id); + } + + $activity = $this->permissionService + ->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type') + ->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get(); return $this->filterSimilar($activity); } @@ -121,7 +129,7 @@ class ActivityService */ public function userActivity($user, $count = 20, $page = 0) { - $activityList = $this->restrictionService + $activityList = $this->permissionService ->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type') ->orderBy('created_at', 'desc')->where('user_id', '=', $user->id)->skip($count * $page)->take($count)->get(); return $this->filterSimilar($activityList); diff --git a/app/Services/PermissionService.php b/app/Services/PermissionService.php new file mode 100644 index 000000000..2d5ee97a5 --- /dev/null +++ b/app/Services/PermissionService.php @@ -0,0 +1,498 @@ +currentUser = auth()->user(); + $userSet = $this->currentUser !== null; + $this->userRoles = false; + $this->isAdmin = $userSet ? $this->currentUser->hasRole('admin') : false; + if (!$userSet) $this->currentUser = new User(); + + $this->jointPermission = $jointPermission; + $this->role = $role; + $this->book = $book; + $this->chapter = $chapter; + $this->page = $page; + } + + /** + * Get the roles for the current user; + * @return array|bool + */ + protected function getRoles() + { + if ($this->userRoles !== false) return $this->userRoles; + + $roles = []; + + if (auth()->guest()) { + $roles[] = $this->role->getSystemRole('public')->id; + return $roles; + } + + + foreach ($this->currentUser->roles as $role) { + $roles[] = $role->id; + } + return $roles; + } + + /** + * Re-generate all entity permission from scratch. + */ + public function buildJointPermissions() + { + $this->jointPermission->truncate(); + + // Get all roles (Should be the most limited dimension) + $roles = $this->role->with('permissions')->get(); + + // Chunk through all books + $this->book->with('permissions')->chunk(500, function ($books) use ($roles) { + $this->createManyJointPermissions($books, $roles); + }); + + // Chunk through all chapters + $this->chapter->with('book', 'permissions')->chunk(500, function ($chapters) use ($roles) { + $this->createManyJointPermissions($chapters, $roles); + }); + + // Chunk through all pages + $this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($pages) use ($roles) { + $this->createManyJointPermissions($pages, $roles); + }); + } + + /** + * Create the entity jointPermissions for a particular entity. + * @param Entity $entity + */ + public function buildJointPermissionsForEntity(Entity $entity) + { + $roles = $this->role->with('jointPermissions')->get(); + $entities = collect([$entity]); + + if ($entity->isA('book')) { + $entities = $entities->merge($entity->chapters); + $entities = $entities->merge($entity->pages); + } elseif ($entity->isA('chapter')) { + $entities = $entities->merge($entity->pages); + } + + $this->deleteManyJointPermissionsForEntities($entities); + $this->createManyJointPermissions($entities, $roles); + } + + /** + * Build the entity jointPermissions for a particular role. + * @param Role $role + */ + public function buildJointPermissionForRole(Role $role) + { + $roles = collect([$role]); + + $this->deleteManyJointPermissionsForRoles($roles); + + // Chunk through all books + $this->book->with('permissions')->chunk(500, function ($books) use ($roles) { + $this->createManyJointPermissions($books, $roles); + }); + + // Chunk through all chapters + $this->chapter->with('book', 'permissions')->chunk(500, function ($books) use ($roles) { + $this->createManyJointPermissions($books, $roles); + }); + + // Chunk through all pages + $this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($books) use ($roles) { + $this->createManyJointPermissions($books, $roles); + }); + } + + /** + * Delete the entity jointPermissions attached to a particular role. + * @param Role $role + */ + public function deleteJointPermissionsForRole(Role $role) + { + $this->deleteManyJointPermissionsForRoles([$role]); + } + + /** + * Delete all of the entity jointPermissions for a list of entities. + * @param Role[] $roles + */ + protected function deleteManyJointPermissionsForRoles($roles) + { + foreach ($roles as $role) { + $role->jointPermissions()->delete(); + } + } + + /** + * Delete the entity jointPermissions for a particular entity. + * @param Entity $entity + */ + public function deleteJointPermissionsForEntity(Entity $entity) + { + $this->deleteManyJointPermissionsForEntities([$entity]); + } + + /** + * Delete all of the entity jointPermissions for a list of entities. + * @param Entity[] $entities + */ + protected function deleteManyJointPermissionsForEntities($entities) + { + foreach ($entities as $entity) { + $entity->jointPermissions()->delete(); + } + } + + /** + * Create & Save entity jointPermissions for many entities and jointPermissions. + * @param Collection $entities + * @param Collection $roles + */ + protected function createManyJointPermissions($entities, $roles) + { + $jointPermissions = []; + foreach ($entities as $entity) { + foreach ($roles as $role) { + foreach ($this->getActions($entity) as $action) { + $jointPermissions[] = $this->createJointPermissionData($entity, $role, $action); + } + } + } + $this->jointPermission->insert($jointPermissions); + } + + + /** + * Get the actions related to an entity. + * @param $entity + * @return array + */ + protected function getActions($entity) + { + $baseActions = ['view', 'update', 'delete']; + + if ($entity->isA('chapter')) { + $baseActions[] = 'page-create'; + } else if ($entity->isA('book')) { + $baseActions[] = 'page-create'; + $baseActions[] = 'chapter-create'; + } + + return $baseActions; + } + + /** + * Create entity permission data for an entity and role + * for a particular action. + * @param Entity $entity + * @param Role $role + * @param $action + * @return array + */ + protected function createJointPermissionData(Entity $entity, Role $role, $action) + { + $permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action; + $roleHasPermission = $role->hasPermission($permissionPrefix . '-all'); + $roleHasPermissionOwn = $role->hasPermission($permissionPrefix . '-own'); + $explodedAction = explode('-', $action); + $restrictionAction = end($explodedAction); + + if ($entity->isA('book')) { + + if (!$entity->restricted) { + return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn); + } else { + $hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction); + return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess); + } + + } elseif ($entity->isA('chapter')) { + + if (!$entity->restricted) { + $hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $restrictionAction); + $hasPermissiveAccessToBook = !$entity->book->restricted; + return $this->createJointPermissionDataArray($entity, $role, $action, + ($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)), + ($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook))); + } else { + $hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction); + return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess); + } + + } elseif ($entity->isA('page')) { + + if (!$entity->restricted) { + $hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $restrictionAction); + $hasPermissiveAccessToBook = !$entity->book->restricted; + $hasExplicitAccessToChapter = $entity->chapter && $entity->chapter->hasActiveRestriction($role->id, $restrictionAction); + $hasPermissiveAccessToChapter = $entity->chapter && !$entity->chapter->restricted; + $acknowledgeChapter = ($entity->chapter && $entity->chapter->restricted); + + $hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook; + $hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook; + + return $this->createJointPermissionDataArray($entity, $role, $action, + ($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)), + ($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents)) + ); + } else { + $hasAccess = $entity->hasRestriction($role->id, $action); + return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess); + } + + } + } + + /** + * Create an array of data with the information of an entity jointPermissions. + * Used to build data for bulk insertion. + * @param Entity $entity + * @param Role $role + * @param $action + * @param $permissionAll + * @param $permissionOwn + * @return array + */ + protected function createJointPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn) + { + $entityClass = get_class($entity); + return [ + 'role_id' => $role->getRawAttribute('id'), + 'entity_id' => $entity->getRawAttribute('id'), + 'entity_type' => $entityClass, + 'action' => $action, + 'has_permission' => $permissionAll, + 'has_permission_own' => $permissionOwn, + 'created_by' => $entity->getRawAttribute('created_by') + ]; + } + + /** + * Checks if an entity has a restriction set upon it. + * @param Entity $entity + * @param $permission + * @return bool + */ + public function checkEntityUserAccess(Entity $entity, $permission) + { + if ($this->isAdmin) return true; + $explodedPermission = explode('-', $permission); + + $baseQuery = $entity->where('id', '=', $entity->id); + $action = end($explodedPermission); + $this->currentAction = $action; + + $nonJointPermissions = ['restrictions']; + + // Handle non entity specific jointPermissions + if (in_array($explodedPermission[0], $nonJointPermissions)) { + $allPermission = $this->currentUser && $this->currentUser->can($permission . '-all'); + $ownPermission = $this->currentUser && $this->currentUser->can($permission . '-own'); + $this->currentAction = 'view'; + $isOwner = $this->currentUser && $this->currentUser->id === $entity->created_by; + return ($allPermission || ($isOwner && $ownPermission)); + } + + // Handle abnormal create jointPermissions + if ($action === 'create') { + $this->currentAction = $permission; + } + + + return $this->entityRestrictionQuery($baseQuery)->count() > 0; + } + + /** + * Check if an entity has restrictions set on itself or its + * parent tree. + * @param Entity $entity + * @param $action + * @return bool|mixed + */ + public function checkIfRestrictionsSet(Entity $entity, $action) + { + $this->currentAction = $action; + if ($entity->isA('page')) { + return $entity->restricted || ($entity->chapter && $entity->chapter->restricted) || $entity->book->restricted; + } elseif ($entity->isA('chapter')) { + return $entity->restricted || $entity->book->restricted; + } elseif ($entity->isA('book')) { + return $entity->restricted; + } + } + + /** + * The general query filter to remove all entities + * that the current user does not have access to. + * @param $query + * @return mixed + */ + protected function entityRestrictionQuery($query) + { + return $query->where(function ($parentQuery) { + $parentQuery->whereHas('jointPermissions', function ($permissionQuery) { + $permissionQuery->whereIn('role_id', $this->getRoles()) + ->where('action', '=', $this->currentAction) + ->where(function ($query) { + $query->where('has_permission', '=', true) + ->orWhere(function ($query) { + $query->where('has_permission_own', '=', true) + ->where('created_by', '=', $this->currentUser->id); + }); + }); + }); + }); + } + + /** + * Add restrictions for a page query + * @param $query + * @param string $action + * @return mixed + */ + public function enforcePageRestrictions($query, $action = 'view') + { + // Prevent drafts being visible to others. + $query = $query->where(function ($query) { + $query->where('draft', '=', false); + if ($this->currentUser) { + $query->orWhere(function ($query) { + $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser->id); + }); + } + }); + + if ($this->isAdmin) return $query; + $this->currentAction = $action; + return $this->entityRestrictionQuery($query); + } + + /** + * Add on permission restrictions to a chapter query. + * @param $query + * @param string $action + * @return mixed + */ + public function enforceChapterRestrictions($query, $action = 'view') + { + if ($this->isAdmin) return $query; + $this->currentAction = $action; + return $this->entityRestrictionQuery($query); + } + + /** + * Add restrictions to a book query. + * @param $query + * @param string $action + * @return mixed + */ + public function enforceBookRestrictions($query, $action = 'view') + { + if ($this->isAdmin) return $query; + $this->currentAction = $action; + return $this->entityRestrictionQuery($query); + } + + /** + * Filter items that have entities set a a polymorphic relation. + * @param $query + * @param string $tableName + * @param string $entityIdColumn + * @param string $entityTypeColumn + * @return mixed + */ + public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn) + { + if ($this->isAdmin) return $query; + $this->currentAction = 'view'; + $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn]; + + return $query->where(function ($query) use ($tableDetails) { + $query->whereExists(function ($permissionQuery) use (&$tableDetails) { + $permissionQuery->select('id')->from('joint_permissions') + ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) + ->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn']) + ->where('action', '=', $this->currentAction) + ->whereIn('role_id', $this->getRoles()) + ->where(function ($query) { + $query->where('has_permission', '=', true)->orWhere(function ($query) { + $query->where('has_permission_own', '=', true) + ->where('created_by', '=', $this->currentUser->id); + }); + }); + }); + }); + + } + + /** + * Filters pages that are a direct relation to another item. + * @param $query + * @param $tableName + * @param $entityIdColumn + * @return mixed + */ + public function filterRelatedPages($query, $tableName, $entityIdColumn) + { + if ($this->isAdmin) return $query; + $this->currentAction = 'view'; + $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn]; + + return $query->where(function ($query) use ($tableDetails) { + $query->where(function ($query) use (&$tableDetails) { + $query->whereExists(function ($permissionQuery) use (&$tableDetails) { + $permissionQuery->select('id')->from('joint_permissions') + ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) + ->where('entity_type', '=', 'Bookstack\\Page') + ->where('action', '=', $this->currentAction) + ->whereIn('role_id', $this->getRoles()) + ->where(function ($query) { + $query->where('has_permission', '=', true)->orWhere(function ($query) { + $query->where('has_permission_own', '=', true) + ->where('created_by', '=', $this->currentUser->id); + }); + }); + }); + })->orWhere($tableDetails['entityIdColumn'], '=', 0); + }); + } + +} \ No newline at end of file diff --git a/app/Services/RestrictionService.php b/app/Services/RestrictionService.php deleted file mode 100644 index 50cbe4a51..000000000 --- a/app/Services/RestrictionService.php +++ /dev/null @@ -1,326 +0,0 @@ -currentUser = auth()->user(); - $this->userRoles = $this->currentUser ? $this->currentUser->roles->pluck('id') : []; - $this->isAdmin = $this->currentUser ? $this->currentUser->hasRole('admin') : false; - } - - /** - * Checks if an entity has a restriction set upon it. - * @param Entity $entity - * @param $action - * @return bool - */ - public function checkIfEntityRestricted(Entity $entity, $action) - { - if ($this->isAdmin) return true; - $this->currentAction = $action; - $baseQuery = $entity->where('id', '=', $entity->id); - if ($entity->isA('page')) { - return $this->pageRestrictionQuery($baseQuery)->count() > 0; - } elseif ($entity->isA('chapter')) { - return $this->chapterRestrictionQuery($baseQuery)->count() > 0; - } elseif ($entity->isA('book')) { - return $this->bookRestrictionQuery($baseQuery)->count() > 0; - } - return false; - } - - /** - * Check if an entity has restrictions set on itself or its - * parent tree. - * @param Entity $entity - * @param $action - * @return bool|mixed - */ - public function checkIfRestrictionsSet(Entity $entity, $action) - { - $this->currentAction = $action; - if ($entity->isA('page')) { - return $entity->restricted || ($entity->chapter && $entity->chapter->restricted) || $entity->book->restricted; - } elseif ($entity->isA('chapter')) { - return $entity->restricted || $entity->book->restricted; - } elseif ($entity->isA('book')) { - return $entity->restricted; - } - } - - /** - * Add restrictions for a page query - * @param $query - * @param string $action - * @return mixed - */ - public function enforcePageRestrictions($query, $action = 'view') - { - // Prevent drafts being visible to others. - $query = $query->where(function ($query) { - $query->where('draft', '=', false); - if ($this->currentUser) { - $query->orWhere(function ($query) { - $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser->id); - }); - } - }); - - if ($this->isAdmin) return $query; - $this->currentAction = $action; - return $this->pageRestrictionQuery($query); - } - - /** - * The base query for restricting pages. - * @param $query - * @return mixed - */ - private function pageRestrictionQuery($query) - { - return $query->where(function ($parentWhereQuery) { - - $parentWhereQuery - // (Book & chapter & page) or (Book & page & NO CHAPTER) unrestricted - ->where(function ($query) { - $query->where(function ($query) { - $query->whereExists(function ($query) { - $query->select('*')->from('chapters') - ->whereRaw('chapters.id=pages.chapter_id') - ->where('restricted', '=', false); - })->whereExists(function ($query) { - $query->select('*')->from('books') - ->whereRaw('books.id=pages.book_id') - ->where('restricted', '=', false); - })->where('restricted', '=', false); - })->orWhere(function ($query) { - $query->where('restricted', '=', false)->where('chapter_id', '=', 0) - ->whereExists(function ($query) { - $query->select('*')->from('books') - ->whereRaw('books.id=pages.book_id') - ->where('restricted', '=', false); - }); - }); - }) - // Page unrestricted, Has no chapter & book has accepted restrictions - ->orWhere(function ($query) { - $query->where('restricted', '=', false) - ->whereExists(function ($query) { - $query->select('*')->from('chapters') - ->whereRaw('chapters.id=pages.chapter_id'); - }, 'and', true) - ->whereExists(function ($query) { - $query->select('*')->from('books') - ->whereRaw('books.id=pages.book_id') - ->whereExists(function ($query) { - $this->checkRestrictionsQuery($query, 'books', 'Book'); - }); - }); - }) - // Page unrestricted, Has an unrestricted chapter & book has accepted restrictions - ->orWhere(function ($query) { - $query->where('restricted', '=', false) - ->whereExists(function ($query) { - $query->select('*')->from('chapters') - ->whereRaw('chapters.id=pages.chapter_id')->where('restricted', '=', false); - }) - ->whereExists(function ($query) { - $query->select('*')->from('books') - ->whereRaw('books.id=pages.book_id') - ->whereExists(function ($query) { - $this->checkRestrictionsQuery($query, 'books', 'Book'); - }); - }); - }) - // Page unrestricted, Has a chapter with accepted permissions - ->orWhere(function ($query) { - $query->where('restricted', '=', false) - ->whereExists(function ($query) { - $query->select('*')->from('chapters') - ->whereRaw('chapters.id=pages.chapter_id') - ->where('restricted', '=', true) - ->whereExists(function ($query) { - $this->checkRestrictionsQuery($query, 'chapters', 'Chapter'); - }); - }); - }) - // Page has accepted permissions - ->orWhereExists(function ($query) { - $this->checkRestrictionsQuery($query, 'pages', 'Page'); - }); - }); - } - - /** - * Add on permission restrictions to a chapter query. - * @param $query - * @param string $action - * @return mixed - */ - public function enforceChapterRestrictions($query, $action = 'view') - { - if ($this->isAdmin) return $query; - $this->currentAction = $action; - return $this->chapterRestrictionQuery($query); - } - - /** - * The base query for restricting chapters. - * @param $query - * @return mixed - */ - private function chapterRestrictionQuery($query) - { - return $query->where(function ($parentWhereQuery) { - - $parentWhereQuery - // Book & chapter unrestricted - ->where(function ($query) { - $query->where('restricted', '=', false)->whereExists(function ($query) { - $query->select('*')->from('books') - ->whereRaw('books.id=chapters.book_id') - ->where('restricted', '=', false); - }); - }) - // Chapter unrestricted & book has accepted restrictions - ->orWhere(function ($query) { - $query->where('restricted', '=', false) - ->whereExists(function ($query) { - $query->select('*')->from('books') - ->whereRaw('books.id=chapters.book_id') - ->whereExists(function ($query) { - $this->checkRestrictionsQuery($query, 'books', 'Book'); - }); - }); - }) - // Chapter has accepted permissions - ->orWhereExists(function ($query) { - $this->checkRestrictionsQuery($query, 'chapters', 'Chapter'); - }); - }); - } - - /** - * Add restrictions to a book query. - * @param $query - * @param string $action - * @return mixed - */ - public function enforceBookRestrictions($query, $action = 'view') - { - if ($this->isAdmin) return $query; - $this->currentAction = $action; - return $this->bookRestrictionQuery($query); - } - - /** - * The base query for restricting books. - * @param $query - * @return mixed - */ - private function bookRestrictionQuery($query) - { - return $query->where(function ($parentWhereQuery) { - $parentWhereQuery - ->where('restricted', '=', false) - ->orWhere(function ($query) { - $query->where('restricted', '=', true)->whereExists(function ($query) { - $this->checkRestrictionsQuery($query, 'books', 'Book'); - }); - }); - }); - } - - /** - * Filter items that have entities set a a polymorphic relation. - * @param $query - * @param string $tableName - * @param string $entityIdColumn - * @param string $entityTypeColumn - * @return mixed - */ - public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn) - { - if ($this->isAdmin) return $query; - $this->currentAction = 'view'; - $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn]; - return $query->where(function ($query) use ($tableDetails) { - $query->where(function ($query) use (&$tableDetails) { - $query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Page') - ->whereExists(function ($query) use (&$tableDetails) { - $query->select('*')->from('pages')->whereRaw('pages.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) - ->where(function ($query) { - $this->pageRestrictionQuery($query); - }); - }); - })->orWhere(function ($query) use (&$tableDetails) { - $query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Book')->whereExists(function ($query) use (&$tableDetails) { - $query->select('*')->from('books')->whereRaw('books.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) - ->where(function ($query) { - $this->bookRestrictionQuery($query); - }); - }); - })->orWhere(function ($query) use (&$tableDetails) { - $query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Chapter')->whereExists(function ($query) use (&$tableDetails) { - $query->select('*')->from('chapters')->whereRaw('chapters.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) - ->where(function ($query) { - $this->chapterRestrictionQuery($query); - }); - }); - }); - }); - } - - /** - * Filters pages that are a direct relation to another item. - * @param $query - * @param $tableName - * @param $entityIdColumn - * @return mixed - */ - public function filterRelatedPages($query, $tableName, $entityIdColumn) - { - if ($this->isAdmin) return $query; - $this->currentAction = 'view'; - $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn]; - return $query->where(function ($query) use (&$tableDetails) { - $query->where(function ($query) use (&$tableDetails) { - $query->whereExists(function ($query) use (&$tableDetails) { - $query->select('*')->from('pages')->whereRaw('pages.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) - ->where(function ($query) { - $this->pageRestrictionQuery($query); - }); - })->orWhere($tableDetails['entityIdColumn'], '=', 0); - }); - }); - } - - /** - * The query to check the restrictions on an entity. - * @param $query - * @param $tableName - * @param $modelName - */ - private function checkRestrictionsQuery($query, $tableName, $modelName) - { - $query->select('*')->from('restrictions') - ->whereRaw('restrictions.restrictable_id=' . $tableName . '.id') - ->where('restrictions.restrictable_type', '=', 'BookStack\\' . $modelName) - ->where('restrictions.action', '=', $this->currentAction) - ->whereIn('restrictions.role_id', $this->userRoles); - } - - -} \ No newline at end of file diff --git a/app/Services/ViewService.php b/app/Services/ViewService.php index 6b50e90de..849a164cf 100644 --- a/app/Services/ViewService.php +++ b/app/Services/ViewService.php @@ -8,18 +8,18 @@ class ViewService protected $view; protected $user; - protected $restrictionService; + protected $permissionService; /** * ViewService constructor. * @param View $view - * @param RestrictionService $restrictionService + * @param PermissionService $permissionService */ - public function __construct(View $view, RestrictionService $restrictionService) + public function __construct(View $view, PermissionService $permissionService) { $this->view = $view; $this->user = auth()->user(); - $this->restrictionService = $restrictionService; + $this->permissionService = $permissionService; } /** @@ -55,7 +55,7 @@ class ViewService public function getPopular($count = 10, $page = 0, $filterModel = false) { $skipCount = $count * $page; - $query = $this->restrictionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type') + $query = $this->permissionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type') ->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count')) ->groupBy('viewable_id', 'viewable_type') ->orderBy('view_count', 'desc'); @@ -76,7 +76,7 @@ class ViewService { if ($this->user === null) return collect(); - $query = $this->restrictionService + $query = $this->permissionService ->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type'); if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel)); diff --git a/app/Setting.php b/app/Setting.php index 05bd2c226..0af3652db 100644 --- a/app/Setting.php +++ b/app/Setting.php @@ -1,8 +1,4 @@ -belongsTo('BookStack\User'); + return $this->belongsTo(User::class); } } diff --git a/app/User.php b/app/User.php index a16eab972..74aec7e3a 100644 --- a/app/User.php +++ b/app/User.php @@ -1,9 +1,6 @@ -belongsToMany('BookStack\Role'); + return $this->belongsToMany(Role::class); } /** @@ -116,7 +113,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon */ public function socialAccounts() { - return $this->hasMany('BookStack\SocialAccount'); + return $this->hasMany(SocialAccount::class); } /** @@ -151,7 +148,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon */ public function avatar() { - return $this->belongsTo('BookStack\Image', 'image_id'); + return $this->belongsTo(Image::class, 'image_id'); } /** diff --git a/app/View.php b/app/View.php index 50dd06012..c02550c7c 100644 --- a/app/View.php +++ b/app/View.php @@ -1,8 +1,4 @@ -check()) return false; if ($ownable === null) { return auth()->user() && auth()->user()->can($permission); } // Check permission on ownable item - $permissionBaseName = strtolower($permission) . '-'; - $hasPermission = false; - if (auth()->user()->can($permissionBaseName . 'all')) $hasPermission = true; - if (auth()->user()->can($permissionBaseName . 'own') && $ownable->createdBy && $ownable->createdBy->id === auth()->user()->id) $hasPermission = true; - - if (!$ownable instanceof \BookStack\Entity) return $hasPermission; - - // Check restrictions on the entity - $restrictionService = app('BookStack\Services\RestrictionService'); - $explodedPermission = explode('-', $permission); - $action = end($explodedPermission); - $hasAccess = $restrictionService->checkIfEntityRestricted($ownable, $action); - $restrictionsSet = $restrictionService->checkIfRestrictionsSet($ownable, $action); - return ($hasAccess && $restrictionsSet) || (!$restrictionsSet && $hasPermission); + $permissionService = app('BookStack\Services\PermissionService'); + return $permissionService->checkEntityUserAccess($ownable, $permission); } /** diff --git a/database/migrations/2014_10_12_000000_create_users_table.php b/database/migrations/2014_10_12_000000_create_users_table.php index 5e060006e..17e71de5f 100644 --- a/database/migrations/2014_10_12_000000_create_users_table.php +++ b/database/migrations/2014_10_12_000000_create_users_table.php @@ -21,10 +21,13 @@ class CreateUsersTable extends Migration $table->nullableTimestamps(); }); - \BookStack\User::forceCreate([ + // Create the initial admin user + DB::table('users')->insert([ 'name' => 'Admin', 'email' => 'admin@admin.com', - 'password' => bcrypt('password') + 'password' => bcrypt('password'), + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() ]); } diff --git a/database/migrations/2015_08_29_105422_add_roles_and_permissions.php b/database/migrations/2015_08_29_105422_add_roles_and_permissions.php index 4389dc32e..763a33fec 100644 --- a/database/migrations/2015_08_29_105422_add_roles_and_permissions.php +++ b/database/migrations/2015_08_29_105422_add_roles_and_permissions.php @@ -68,35 +68,44 @@ class AddRolesAndPermissions extends Migration // Create default roles - $admin = new \BookStack\Role(); - $admin->name = 'admin'; - $admin->display_name = 'Admin'; - $admin->description = 'Administrator of the whole application'; - $admin->save(); + $adminId = DB::table('roles')->insertGetId([ + 'name' => 'admin', + 'display_name' => 'Admin', + 'description' => 'Administrator of the whole application', + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + ]); + $editorId = DB::table('roles')->insertGetId([ + 'name' => 'editor', + 'display_name' => 'Editor', + 'description' => 'User can edit Books, Chapters & Pages', + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + ]); + $viewerId = DB::table('roles')->insertGetId([ + 'name' => 'viewer', + 'display_name' => 'Viewer', + 'description' => 'User can view books & their content behind authentication', + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + ]); - $editor = new \BookStack\Role(); - $editor->name = 'editor'; - $editor->display_name = 'Editor'; - $editor->description = 'User can edit Books, Chapters & Pages'; - $editor->save(); - - $viewer = new \BookStack\Role(); - $viewer->name = 'viewer'; - $viewer->display_name = 'Viewer'; - $viewer->description = 'User can view books & their content behind authentication'; - $viewer->save(); // Create default CRUD permissions and allocate to admins and editors $entities = ['Book', 'Page', 'Chapter', 'Image']; $ops = ['Create', 'Update', 'Delete']; foreach ($entities as $entity) { foreach ($ops as $op) { - $newPermission = new \BookStack\Permission(); - $newPermission->name = strtolower($entity) . '-' . strtolower($op); - $newPermission->display_name = $op . ' ' . $entity . 's'; - $newPermission->save(); - $admin->attachPermission($newPermission); - $editor->attachPermission($newPermission); + $newPermId = DB::table('permissions')->insertGetId([ + 'name' => strtolower($entity) . '-' . strtolower($op), + 'display_name' => $op . ' ' . $entity . 's', + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + ]); + DB::table('permission_role')->insert([ + ['permission_id' => $newPermId, 'role_id' => $adminId], + ['permission_id' => $newPermId, 'role_id' => $editorId] + ]); } } @@ -105,19 +114,27 @@ class AddRolesAndPermissions extends Migration $ops = ['Create', 'Update', 'Delete']; foreach ($entities as $entity) { foreach ($ops as $op) { - $newPermission = new \BookStack\Permission(); - $newPermission->name = strtolower($entity) . '-' . strtolower($op); - $newPermission->display_name = $op . ' ' . $entity; - $newPermission->save(); - $admin->attachPermission($newPermission); + $newPermId = DB::table('permissions')->insertGetId([ + 'name' => strtolower($entity) . '-' . strtolower($op), + 'display_name' => $op . ' ' . $entity, + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + ]); + DB::table('permission_role')->insert([ + 'permission_id' => $newPermId, + 'role_id' => $adminId + ]); } } // Set all current users as admins // (At this point only the initially create user should be an admin) - $users = \BookStack\User::all(); + $users = DB::table('users')->get(); foreach ($users as $user) { - $user->attachRole($admin); + DB::table('role_user')->insert([ + 'role_id' => $adminId, + 'user_id' => $user->id + ]); } } diff --git a/database/migrations/2016_02_27_120329_update_permissions_and_roles.php b/database/migrations/2016_02_27_120329_update_permissions_and_roles.php index ea3735d9e..af6bb1232 100644 --- a/database/migrations/2016_02_27_120329_update_permissions_and_roles.php +++ b/database/migrations/2016_02_27_120329_update_permissions_and_roles.php @@ -13,29 +13,31 @@ class UpdatePermissionsAndRoles extends Migration public function up() { // Get roles with permissions we need to change - $adminRole = \BookStack\Role::getRole('admin'); - $editorRole = \BookStack\Role::getRole('editor'); + $adminRoleId = DB::table('roles')->where('name', '=', 'admin')->first()->id; + $editorRole = DB::table('roles')->where('name', '=', 'editor')->first(); // Delete old permissions - $permissions = \BookStack\Permission::all(); - $permissions->each(function ($permission) { - $permission->delete(); - }); + $permissions = DB::table('permissions')->delete(); // Create & attach new admin permissions $permissionsToCreate = [ 'settings-manage' => 'Manage Settings', 'users-manage' => 'Manage Users', 'user-roles-manage' => 'Manage Roles & Permissions', - 'restrictions-manage-all' => 'Manage All Entity Restrictions', - 'restrictions-manage-own' => 'Manage Entity Restrictions On Own Content' + 'restrictions-manage-all' => 'Manage All Entity Permissions', + 'restrictions-manage-own' => 'Manage Entity Permissions On Own Content' ]; foreach ($permissionsToCreate as $name => $displayName) { - $newPermission = new \BookStack\Permission(); - $newPermission->name = $name; - $newPermission->display_name = $displayName; - $newPermission->save(); - $adminRole->attachPermission($newPermission); + $permissionId = DB::table('permissions')->insertGetId([ + 'name' => $name, + 'display_name' => $displayName, + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + ]); + DB::table('permission_role')->insert([ + 'role_id' => $adminRoleId, + 'permission_id' => $permissionId + ]); } // Create & attach new entity permissions @@ -43,12 +45,22 @@ class UpdatePermissionsAndRoles extends Migration $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own']; foreach ($entities as $entity) { foreach ($ops as $op) { - $newPermission = new \BookStack\Permission(); - $newPermission->name = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)); - $newPermission->display_name = $op . ' ' . $entity . 's'; - $newPermission->save(); - $adminRole->attachPermission($newPermission); - if ($editorRole !== null) $editorRole->attachPermission($newPermission); + $permissionId = DB::table('permissions')->insertGetId([ + 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)), + 'display_name' => $op . ' ' . $entity . 's', + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + ]); + DB::table('permission_role')->insert([ + 'role_id' => $adminRoleId, + 'permission_id' => $permissionId + ]); + if ($editorRole !== null) { + DB::table('permission_role')->insert([ + 'role_id' => $editorRole->id, + 'permission_id' => $permissionId + ]); + } } } @@ -62,24 +74,26 @@ class UpdatePermissionsAndRoles extends Migration public function down() { // Get roles with permissions we need to change - $adminRole = \BookStack\Role::getRole('admin'); + $adminRoleId = DB::table('roles')->where('name', '=', 'admin')->first()->id; // Delete old permissions - $permissions = \BookStack\Permission::all(); - $permissions->each(function ($permission) { - $permission->delete(); - }); + $permissions = DB::table('permissions')->delete(); // Create default CRUD permissions and allocate to admins and editors $entities = ['Book', 'Page', 'Chapter', 'Image']; $ops = ['Create', 'Update', 'Delete']; foreach ($entities as $entity) { foreach ($ops as $op) { - $newPermission = new \BookStack\Permission(); - $newPermission->name = strtolower($entity) . '-' . strtolower($op); - $newPermission->display_name = $op . ' ' . $entity . 's'; - $newPermission->save(); - $adminRole->attachPermission($newPermission); + $permissionId = DB::table('permissions')->insertGetId([ + 'name' => strtolower($entity) . '-' . strtolower($op), + 'display_name' => $op . ' ' . $entity . 's', + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + ]); + DB::table('permission_role')->insert([ + 'role_id' => $adminRoleId, + 'permission_id' => $permissionId + ]); } } @@ -88,11 +102,16 @@ class UpdatePermissionsAndRoles extends Migration $ops = ['Create', 'Update', 'Delete']; foreach ($entities as $entity) { foreach ($ops as $op) { - $newPermission = new \BookStack\Permission(); - $newPermission->name = strtolower($entity) . '-' . strtolower($op); - $newPermission->display_name = $op . ' ' . $entity; - $newPermission->save(); - $adminRole->attachPermission($newPermission); + $permissionId = DB::table('permissions')->insertGetId([ + 'name' => strtolower($entity) . '-' . strtolower($op), + 'display_name' => $op . ' ' . $entity, + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + ]); + DB::table('permission_role')->insert([ + 'role_id' => $adminRoleId, + 'permission_id' => $permissionId + ]); } } } diff --git a/database/migrations/2016_04_09_100730_add_view_permissions_to_roles.php b/database/migrations/2016_04_09_100730_add_view_permissions_to_roles.php new file mode 100644 index 000000000..9bdf4397f --- /dev/null +++ b/database/migrations/2016_04_09_100730_add_view_permissions_to_roles.php @@ -0,0 +1,58 @@ +get(); + + // Create new view permission + $entities = ['Book', 'Page', 'Chapter']; + $ops = ['View All', 'View Own']; + foreach ($entities as $entity) { + foreach ($ops as $op) { + $permId = DB::table('permissions')->insertGetId([ + 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)), + 'display_name' => $op . ' ' . $entity . 's', + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + ]); + // Assign view permission to all current roles + foreach ($currentRoles as $role) { + DB::table('permission_role')->insert([ + 'role_id' => $role->id, + 'permission_id' => $permId + ]); + } + } + } + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // Delete the new view permission + $entities = ['Book', 'Page', 'Chapter']; + $ops = ['View All', 'View Own']; + foreach ($entities as $entity) { + foreach ($ops as $op) { + $permissionName = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)); + $permission = DB::table('permissions')->where('name', '=', $permissionName)->first(); + DB::table('permission_role')->where('permission_id', '=', $permission->id)->delete(); + DB::table('permissions')->where('name', '=', $permissionName)->delete(); + } + } + } +} diff --git a/database/migrations/2016_04_20_192649_create_joint_permissions_table.php b/database/migrations/2016_04_20_192649_create_joint_permissions_table.php new file mode 100644 index 000000000..db941f9de --- /dev/null +++ b/database/migrations/2016_04_20_192649_create_joint_permissions_table.php @@ -0,0 +1,102 @@ +increments('id'); + $table->integer('role_id'); + $table->string('entity_type'); + $table->integer('entity_id'); + $table->string('action'); + $table->boolean('has_permission')->default(false); + $table->boolean('has_permission_own')->default(false); + $table->integer('created_by'); + // Create indexes + $table->index(['entity_id', 'entity_type']); + $table->index('has_permission'); + $table->index('has_permission_own'); + $table->index('role_id'); + $table->index('action'); + $table->index('created_by'); + }); + + Schema::table('roles', function (Blueprint $table) { + $table->string('system_name'); + $table->boolean('hidden')->default(false); + $table->index('hidden'); + $table->index('system_name'); + }); + + Schema::rename('permissions', 'role_permissions'); + Schema::rename('restrictions', 'entity_permissions'); + + // Create the new public role + $publicRoleData = [ + 'name' => 'public', + 'display_name' => 'Public', + 'description' => 'The role given to public visitors if allowed', + 'system_name' => 'public', + 'hidden' => true, + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + ]; + + // Ensure unique name + while (DB::table('roles')->where('name', '=', $publicRoleData['display_name'])->count() > 0) { + $publicRoleData['display_name'] = $publicRoleData['display_name'] . str_random(2); + } + $publicRoleId = DB::table('roles')->insertGetId($publicRoleData); + + // Add new view permissions to public role + $entities = ['Book', 'Page', 'Chapter']; + $ops = ['View All', 'View Own']; + foreach ($entities as $entity) { + foreach ($ops as $op) { + $name = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)); + $permission = DB::table('role_permissions')->where('name', '=', $name)->first(); + // Assign view permission to public + DB::table('permission_role')->insert([ + 'permission_id' => $permission->id, + 'role_id' => $publicRoleId + ]); + } + } + + // Update admin role with system name + DB::table('roles')->where('name', '=', 'admin')->update(['system_name' => 'admin']); + + // Generate the new entity jointPermissions + $restrictionService = app(\BookStack\Services\PermissionService::class); + $restrictionService->buildJointPermissions(); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('joint_permissions'); + + Schema::rename('role_permissions', 'permissions'); + Schema::rename('entity_permissions', 'restrictions'); + + // Delete the public role + DB::table('roles')->where('system_name', '=', 'public')->delete(); + + Schema::table('roles', function (Blueprint $table) { + $table->dropColumn('system_name'); + }); + } +} diff --git a/database/seeds/DummyContentSeeder.php b/database/seeds/DummyContentSeeder.php index 328971f26..c64ca2a8c 100644 --- a/database/seeds/DummyContentSeeder.php +++ b/database/seeds/DummyContentSeeder.php @@ -20,12 +20,15 @@ class DummyContentSeeder extends Seeder ->each(function($book) use ($user) { $chapters = factory(BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id]) ->each(function($chapter) use ($user, $book){ - $pages = factory(\BookStack\Page::class, 10)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id]); + $pages = factory(\BookStack\Page::class, 5)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id]); $chapter->pages()->saveMany($pages); }); $pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $user->id, 'updated_by' => $user->id]); $book->chapters()->saveMany($chapters); $book->pages()->saveMany($pages); }); + + $restrictionService = app(\BookStack\Services\PermissionService::class); + $restrictionService->buildJointPermissions(); } } diff --git a/resources/views/chapters/show.blade.php b/resources/views/chapters/show.blade.php index b6b2d5c97..0bb61cebc 100644 --- a/resources/views/chapters/show.blade.php +++ b/resources/views/chapters/show.blade.php @@ -49,9 +49,15 @@

No pages are currently in this chapter.

- Create a new page -   -or-    - Sort the current book + @if(userCan('page-create', $chapter)) + Create a new page + @endif + @if(userCan('page-create', $chapter) && userCan('book-update', $book)) +   -or-    + @endif + @if(userCan('book-update', $book)) + Sort the current book + @endif


@endif diff --git a/resources/views/settings/index.blade.php b/resources/views/settings/index.blade.php index 7c1ec59bf..ce7a8d5d1 100644 --- a/resources/views/settings/index.blade.php +++ b/resources/views/settings/index.blade.php @@ -66,8 +66,8 @@
hasPermission($permission)))) checked="checked" @endif + @if(old('permissions'.$permission, false)|| (!old('display_name', false) && (isset($role) && $role->hasPermission($permission)))) checked="checked" @endif value="true"> \ No newline at end of file diff --git a/resources/views/settings/roles/form.blade.php b/resources/views/settings/roles/form.blade.php index 9b0b35d3e..6181acaea 100644 --- a/resources/views/settings/roles/form.blade.php +++ b/resources/views/settings/roles/form.blade.php @@ -18,7 +18,7 @@ - +
@@ -31,16 +31,21 @@

- - - - + + + + + + + + +
CreateEditDeleteCreateViewEditDelete
Books + + + @@ -56,6 +61,10 @@ + + + @@ -71,6 +80,10 @@ + + + @@ -83,6 +96,7 @@
Images @include('settings/roles/checkbox', ['permission' => 'image-create-all'])Controlled by the asset they are uploaded to diff --git a/resources/views/users/forms/ldap.blade.php b/resources/views/users/forms/ldap.blade.php index 47edb211b..5fc8ce397 100644 --- a/resources/views/users/forms/ldap.blade.php +++ b/resources/views/users/forms/ldap.blade.php @@ -13,7 +13,7 @@ @if(userCan('users-manage'))
- @include('form/role-checkboxes', ['name' => 'roles', 'roles' => \BookStack\Role::all()]) + @include('form/role-checkboxes', ['name' => 'roles', 'roles' => $roles])
@endif diff --git a/resources/views/users/forms/standard.blade.php b/resources/views/users/forms/standard.blade.php index 9bd70b43c..52ebac976 100644 --- a/resources/views/users/forms/standard.blade.php +++ b/resources/views/users/forms/standard.blade.php @@ -11,7 +11,7 @@ @if(userCan('users-manage'))
- @include('form/role-checkboxes', ['name' => 'roles', 'roles' => \BookStack\Role::all()]) + @include('form/role-checkboxes', ['name' => 'roles', 'roles' => $roles])
@endif diff --git a/tests/Permissions/RestrictionsTest.php b/tests/Permissions/RestrictionsTest.php index 4ecf5fb20..75d83cbfc 100644 --- a/tests/Permissions/RestrictionsTest.php +++ b/tests/Permissions/RestrictionsTest.php @@ -4,12 +4,14 @@ class RestrictionsTest extends TestCase { protected $user; protected $viewer; + protected $restrictionService; public function setUp() { parent::setUp(); $this->user = $this->getNewUser(); $this->viewer = $this->getViewer(); + $this->restrictionService = $this->app[\BookStack\Services\PermissionService::class]; } protected function getViewer() @@ -21,28 +23,30 @@ class RestrictionsTest extends TestCase } /** - * Manually set some restrictions on an entity. + * Manually set some permissions on an entity. * @param \BookStack\Entity $entity * @param $actions */ protected function setEntityRestrictions(\BookStack\Entity $entity, $actions) { $entity->restricted = true; - $entity->restrictions()->delete(); + $entity->permissions()->delete(); $role = $this->user->roles->first(); $viewerRole = $this->viewer->roles->first(); foreach ($actions as $action) { - $entity->restrictions()->create([ + $entity->permissions()->create([ 'role_id' => $role->id, 'action' => strtolower($action) ]); - $entity->restrictions()->create([ + $entity->permissions()->create([ 'role_id' => $viewerRole->id, 'action' => strtolower($action) ]); } $entity->save(); - $entity->load('restrictions'); + $entity->load('permissions'); + $this->restrictionService->buildJointPermissionsForEntity($entity); + $entity->load('jointPermissions'); } public function test_book_view_restriction() @@ -344,7 +348,7 @@ class RestrictionsTest extends TestCase ->check('restrictions[2][view]') ->press('Save Permissions') ->seeInDatabase('books', ['id' => $book->id, 'restricted' => true]) - ->seeInDatabase('restrictions', [ + ->seeInDatabase('entity_permissions', [ 'restrictable_id' => $book->id, 'restrictable_type' => 'BookStack\Book', 'role_id' => '2', @@ -361,7 +365,7 @@ class RestrictionsTest extends TestCase ->check('restrictions[2][update]') ->press('Save Permissions') ->seeInDatabase('chapters', ['id' => $chapter->id, 'restricted' => true]) - ->seeInDatabase('restrictions', [ + ->seeInDatabase('entity_permissions', [ 'restrictable_id' => $chapter->id, 'restrictable_type' => 'BookStack\Chapter', 'role_id' => '2', @@ -378,7 +382,7 @@ class RestrictionsTest extends TestCase ->check('restrictions[2][delete]') ->press('Save Permissions') ->seeInDatabase('pages', ['id' => $page->id, 'restricted' => true]) - ->seeInDatabase('restrictions', [ + ->seeInDatabase('entity_permissions', [ 'restrictable_id' => $page->id, 'restrictable_type' => 'BookStack\Page', 'role_id' => '2', diff --git a/tests/Permissions/RolesTest.php b/tests/Permissions/RolesTest.php index 8ecdb37a3..b64f40dc6 100644 --- a/tests/Permissions/RolesTest.php +++ b/tests/Permissions/RolesTest.php @@ -7,7 +7,15 @@ class RolesTest extends TestCase public function setUp() { parent::setUp(); - $this->user = $this->getNewBlankUser(); + $this->user = $this->getViewer(); + } + + protected function getViewer() + { + $role = \BookStack\Role::getRole('viewer'); + $viewer = $this->getNewBlankUser(); + $viewer->attachRole($role);; + return $viewer; } /** @@ -141,7 +149,7 @@ class RolesTest extends TestCase public function test_restrictions_manage_own_permission() { - $otherUsersPage = \BookStack\Page::take(1)->get()->first(); + $otherUsersPage = \BookStack\Page::first(); $content = $this->createEntityChainBelongingToUser($this->user); // Check can't restrict other's content $this->actingAs($this->user)->visit($otherUsersPage->getUrl()) @@ -536,4 +544,27 @@ class RolesTest extends TestCase ->dontSeeInElement('.book-content', $otherPage->name); } + public function test_public_role_not_visible_in_user_edit_screen() + { + $user = \BookStack\User::first(); + $this->asAdmin()->visit('/settings/users/' . $user->id) + ->seeElement('#roles-admin') + ->dontSeeElement('#roles-public'); + } + + public function test_public_role_not_visible_in_role_listing() + { + $this->asAdmin()->visit('/settings/roles') + ->see('Admin') + ->dontSee('Public'); + } + + public function test_public_role_not_visible_in_default_role_setting() + { + $this->asAdmin()->visit('/settings') + ->seeElement('[data-role-name="admin"]') + ->dontSeeElement('[data-role-name="public"]'); + + } + } diff --git a/tests/TestCase.php b/tests/TestCase.php index f46d73e04..5d0545b66 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -65,6 +65,8 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase $page = factory(BookStack\Page::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id, 'book_id' => $book->id]); $book->chapters()->saveMany([$chapter]); $chapter->pages()->saveMany([$page]); + $restrictionService = $this->app[\BookStack\Services\PermissionService::class]; + $restrictionService->buildJointPermissionsForEntity($book); return [ 'book' => $book, 'chapter' => $chapter,