From ec684bbd1629803bee4c15faf78c9997aaf7daf5 Mon Sep 17 00:00:00 2001
From: Ron Lucke <lucke@elan-ev.de>
Date: Mon, 26 Sep 2022 08:11:22 +0000
Subject: [PATCH] =?UTF-8?q?StEP00362:=20Rechte-=20und=20Zugriffsverwaltung?=
 =?UTF-8?q?=20f=C3=BCr=20Arbeitsplatz=20>=20Lernmaterialien?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Closes #919

Merge request studip/studip!639
---
 app/controllers/contents/courseware.php       |  37 ++
 app/controllers/multipersonsearch.php         |  40 +-
 .../courseware/shared_content_courseware.php  |  10 +
 lib/classes/JsonApi/RouteMap.php              |   6 +-
 .../JsonApi/Routes/Courseware/Authority.php   |  61 ++-
 .../Courseware/CoursewareInstancesHelper.php  |   1 +
 .../StructuralElementsReleasedIndex.php       |  59 ++
 .../StructuralElementsSharedIndex.php         |  77 +++
 lib/models/Courseware/StructuralElement.php   | 166 ++++--
 lib/navigation/ContentsNavigation.php         |  54 +-
 .../javascripts/bootstrap/courseware.js       |   2 +-
 .../assets/stylesheets/scss/courseware.scss   |  73 ++-
 .../stylesheets/scss/multi_person_search.scss |   8 +
 resources/assets/stylesheets/studip.scss      |   1 +
 resources/vue/base-components.js              |   4 +-
 resources/vue/components/StudipIcon.vue       |   3 +-
 .../components/StudipMultiPersonSearch.vue    | 190 +++++++
 .../courseware/ContentOverviewApp.vue         |   4 +-
 .../courseware/ContentReleasesApp.vue         |   6 +-
 .../CoursewareContentOverviewElements.vue     | 504 ++++++++++--------
 .../CoursewareContentOverviewFilterWidget.vue | 106 +++-
 .../CoursewareContentPermissions.vue          | 391 ++++++++++++++
 .../courseware/CoursewareContentShared.vue    | 173 ++++++
 .../CoursewareStructuralElement.vue           |  24 +-
 .../courseware/CoursewareTreeItem.vue         |   3 +
 .../vue/courseware-content-overview-app.js    |   3 +
 .../vue/courseware-content-releases-app.js    |   5 +-
 .../vue/store/courseware/courseware.module.js |  20 +
 28 files changed, 1704 insertions(+), 327 deletions(-)
 create mode 100755 app/views/contents/courseware/shared_content_courseware.php
 create mode 100644 lib/classes/JsonApi/Routes/Courseware/StructuralElementsReleasedIndex.php
 create mode 100644 lib/classes/JsonApi/Routes/Courseware/StructuralElementsSharedIndex.php
 create mode 100755 resources/assets/stylesheets/scss/multi_person_search.scss
 create mode 100644 resources/vue/components/StudipMultiPersonSearch.vue
 create mode 100755 resources/vue/components/courseware/CoursewareContentPermissions.vue
 create mode 100644 resources/vue/components/courseware/CoursewareContentShared.vue

diff --git a/app/controllers/contents/courseware.php b/app/controllers/contents/courseware.php
index c7edcaced56..860fdc64d06 100644
--- a/app/controllers/contents/courseware.php
+++ b/app/controllers/contents/courseware.php
@@ -432,4 +432,41 @@ class Contents_CoursewareController extends AuthenticatedController
 
         $this->render_pdf($element->pdfExport($this->user, $with_children), trim($element->title).'.pdf');
     }
+
+    /**
+     * To display the shared courseware
+     *
+     * @param string $entry_element_id the shared struct element id
+     */
+    public function shared_content_courseware_action($entry_element_id)
+    {
+        global $perm, $user;
+
+        $navigation = new Navigation(_('Geteiltes Lernmaterial'), 'dispatch.php/contents/courseware/shared_content_courseware/' . $entry_element_id);
+        Navigation::addItem('/contents/courseware/shared_content_courseware', $navigation);
+        Navigation::activateItem('/contents/courseware/shared_content_courseware');
+
+        $this->entry_element_id = $entry_element_id;
+
+        $struct = \Courseware\StructuralElement::findOneBySQL(
+            "id = ? AND range_type = 'user'",
+            [$this->entry_element_id]
+        );
+
+        if (!$struct) {
+            throw new Trails_Exception(404, _('Der geteilte Inhalt kann nicht gefunden werden.'));
+        }
+
+        if (!$struct->canRead($user) && !$struct->canEdit($user)) {
+            throw new AccessDeniedException();
+        }
+
+        $this->user_id = $struct->owner_id;
+
+        $this->licenses = $this->getLicences();
+
+        $this->oer_enabled = Config::get()->OERCAMPUS_ENABLED && $perm->have_perm(Config::get()->OER_PUBLIC_STATUS);
+
+        $this->setCoursewareSidebar();
+    }
 }
diff --git a/app/controllers/multipersonsearch.php b/app/controllers/multipersonsearch.php
index d036315841f..01733cb3791 100644
--- a/app/controllers/multipersonsearch.php
+++ b/app/controllers/multipersonsearch.php
@@ -110,7 +110,8 @@ class MultipersonsearchController extends AuthenticatedController
      * This needs to be done in one single action to provider a similar
      * usability for no-JavaScript users as for JavaScript users.
      */
-    public function no_js_form_action() {
+    public function no_js_form_action()
+    {
 
         if (!empty($_POST)) {
             CSRFProtection::verifyUnsafeRequest();
@@ -243,4 +244,41 @@ class MultipersonsearchController extends AuthenticatedController
 
     }
 
+
+    public function ajax_search_vue_action($name)
+    {
+        $searchterm = Request::get('s');
+        $searchterm = str_replace(',', ' ', $searchterm);
+        $searchterm = preg_replace('/\s+/u', ' ', $searchterm);
+
+        $result = [];
+        // execute searchobject if searchterm is at least 3 chars long
+        if (mb_strlen($searchterm) >= 3) {
+            $mp = MultiPersonSearch::load($name);
+            $mp->setSearchObject(new StandardSearch('user_id'));
+            $searchObject = $mp->getSearchObject();
+            $result = array_map(function ($r) {
+                return $r['user_id'];
+            }, $searchObject->getResults($searchterm, [], 50));
+            $result = User::findFullMany($result, 'ORDER BY Nachname ASC, Vorname ASC');
+            $alreadyMember = $mp->getDefaultSelectedUsersIDs();
+        }
+
+        $output = [];
+        foreach ($result as $user) {
+            $output[] = [
+                'id' => $user->id,
+                'avatar'  => Avatar::getAvatar($user->id)->getURL(Avatar::SMALL),
+                'text'    => "{$user->nachname}, {$user->vorname} -- {$user->perms} ({$user->username})",
+                'selected'  => $alreadyMember === null ? false : in_array($user->id, $alreadyMember),
+                'nachname' => $user->nachname,
+                'vorname' => $user->vorname,
+                'username' => $user->username,
+                'formatted-name' => trim($user->getFullName())
+            ];
+        }
+        $this->render_json($output);
+    }
+
+
 }
diff --git a/app/views/contents/courseware/shared_content_courseware.php b/app/views/contents/courseware/shared_content_courseware.php
new file mode 100755
index 00000000000..f9590593b15
--- /dev/null
+++ b/app/views/contents/courseware/shared_content_courseware.php
@@ -0,0 +1,10 @@
+<div
+    id="courseware-index-app"
+    entry-element-id="<?= $entry_element_id ?>"
+    entry-type="sharedusers"
+    entry-id="<?= $entry_element_id ?>"
+    oer-enabled='<?= $oer_enabled ?>'
+    oer-title="<?= Config::get()->OER_TITLE ?>"
+    licenses='<?= $licenses ?>'
+    >
+</div>
diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php
index 478e319b97d..312a90ab4ef 100644
--- a/lib/classes/JsonApi/RouteMap.php
+++ b/lib/classes/JsonApi/RouteMap.php
@@ -308,7 +308,7 @@ class RouteMap
 
     private function addAuthenticatedCoursewareRoutes(RouteCollectorProxy $group): void
     {
-        $group->get('/{type:courses|users}/{id}/courseware', Routes\Courseware\CoursewareInstancesShow::class);
+        $group->get('/{type:courses|users|sharedusers}/{id}/courseware', Routes\Courseware\CoursewareInstancesShow::class);
         $group->patch('/courseware-instances/{id}', Routes\Courseware\CoursewareInstancesUpdate::class);
         $this->addRelationship(
             $group,
@@ -420,6 +420,10 @@ class RouteMap
         $group->patch('/courseware-structural-element-feedback/{id}', Routes\Courseware\StructuralElementFeedbackUpdate::class);
         $group->delete('/courseware-structural-element-feedback/{id}', Routes\Courseware\StructuralElementFeedbackDelete::class);
 
+        $group->get('/courseware-structural-elements-shared', Routes\Courseware\StructuralElementsSharedIndex::class);
+        $group->get('/courseware-structural-elements-released', Routes\Courseware\StructuralElementsReleasedIndex::class);
+
+
         $group->get('/courseware-blocks/{id}/user-data-field', Routes\Courseware\UserDataFieldOfBlocksShow::class);
         $group->get('/courseware-user-data-fields/{id}', Routes\Courseware\UserDataFieldsShow::class);
         $group->patch('/courseware-user-data-fields/{id}', Routes\Courseware\UserDataFieldsUpdate::class);
diff --git a/lib/classes/JsonApi/Routes/Courseware/Authority.php b/lib/classes/JsonApi/Routes/Courseware/Authority.php
index 5e30a415ceb..29bde20557e 100644
--- a/lib/classes/JsonApi/Routes/Courseware/Authority.php
+++ b/lib/classes/JsonApi/Routes/Courseware/Authority.php
@@ -59,7 +59,23 @@ class Authority
     public static function canUpdateBlock(User $user, Block $resource)
     {
         if ($resource->isBlocked()) {
-            return $resource->getBlockerUserId() == $user->id;
+            $structural_element = $resource->container->structural_element;
+
+            if ($structural_element->range_type === 'user') {
+                if ($structural_element->range_id === $user->id) {
+                    return true;
+                }
+
+                return $structural_element->canEdit($user);
+            }
+
+            $perm = $GLOBALS['perm']->have_studip_perm(
+                $structural_element->course->config->COURSEWARE_EDITING_PERMISSION,
+                $structural_element->course->id,
+                $user->id
+            );
+
+            return $resource->getBlockerUserId() === $user->id || $perm;
         }
 
         return self::canUpdateContainer($user, $resource->container);
@@ -72,7 +88,36 @@ class Authority
 
     public static function canUpdateEditBlocker(User $user, $resource)
     {
-        return $resource->edit_blocker_id == '' || $resource->edit_blocker_id === $user->id;
+        $structural_element = null;
+        if ($resource instanceof Block) {
+            $structural_element = $resource->container->structural_element;
+        }
+        if ($resource instanceof Container) {
+            $structural_element = $resource->structural_element;
+        }
+        if ($resource instanceof StructuralElement) {
+            $structural_element = $resource;
+        }
+
+        if ($structural_element === null) {
+            return false;
+        }
+
+        if ($structural_element->range_type === 'user') {
+            if ($structural_element->range_id === $user->id) {
+                return true;
+            }
+
+            return $structural_element->canEdit($user);
+        }
+
+        $perm = $GLOBALS['perm']->have_studip_perm(
+            $structural_element->course->config->COURSEWARE_EDITING_PERMISSION,
+            $structural_element->course->id,
+            $user->id
+        );
+
+        return $resource->edit_blocker_id == '' || $resource->edit_blocker_id === $user->id || $perm;
     }
 
     public static function canShowContainer(User $user, Container $resource)
@@ -163,6 +208,18 @@ class Authority
         return $GLOBALS['perm']->have_perm('root', $user->id);
     }
 
+    public static function canIndexStructuralElementsShared(User $user)
+    {
+        //TODO ?
+        return true;
+    }
+
+    public static function canIndexStructuralElementsReleased(User $user)
+    {
+        //TODO ?
+        return true;
+    }
+
     public static function canReorderStructuralElements(User $user, $resource)
     {
         return self::canUpdateStructuralElement($user, $resource);
diff --git a/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesHelper.php b/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesHelper.php
index 843f7c2a902..46a2e689d67 100644
--- a/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesHelper.php
+++ b/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesHelper.php
@@ -26,6 +26,7 @@ trait CoursewareInstancesHelper
             'courses' => 'getCoursewareCourse',
             'user' => 'getCoursewareUser',
             'users' => 'getCoursewareUser',
+            'sharedusers' => 'getSharedCoursewareUser',
         ];
         if (!($method = $methods[$rangeType])) {
             throw new BadRequestException('Invalid range type: "' . $rangeType . '".');
diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsReleasedIndex.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsReleasedIndex.php
new file mode 100644
index 00000000000..b4a8e1c1108
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsReleasedIndex.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\StructuralElement;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Class StructuralElementsReleasedIndex.
+ */
+class StructuralElementsReleasedIndex extends JsonApiController
+{
+    protected $allowedPagingParameters = ['offset', 'limit'];
+
+    protected $allowedIncludePaths = [
+        'ancestors',
+        'children',
+        'containers',
+        'containers.blocks',
+        'containers.blocks.edit-blocker',
+        'containers.blocks.editor',
+        'containers.blocks.owner',
+        'containers.blocks.user-data-field',
+        'containers.blocks.user-progress',
+        'course',
+        'editor',
+        'owner',
+        'parent',
+    ];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $user = $this->getUser($request);
+        if (!Authority::canIndexStructuralElementsReleased($user)) {
+            throw new AuthorizationFailedException();
+        }
+
+        list($offset, $limit) = $this->getOffsetAndLimit();
+        $resources = [];
+        $contents = StructuralElement::findBySQL(
+            'range_id = ? AND range_type = ? ORDER BY mkdate DESC',
+            [$user->id, 'user']
+        );
+
+        foreach ($contents as $content) {
+            if ((count($content->read_approval) && count($content->read_approval['users']) > 0) || (count($content->write_approval) && count($content->write_approval['users']) > 0)) {
+                $resources[] = $content;
+            }
+        }
+
+        return $this->getPaginatedContentResponse($resources, count($resources));
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsSharedIndex.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsSharedIndex.php
new file mode 100644
index 00000000000..0dc7c0d447c
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsSharedIndex.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\StructuralElement;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Class StructuralElementsSharedIndex.
+ */
+class StructuralElementsSharedIndex extends JsonApiController
+{
+    protected $allowedPagingParameters = ['offset', 'limit'];
+
+    protected $allowedIncludePaths = [
+        'ancestors',
+        'children',
+        'containers',
+        'containers.blocks',
+        'containers.blocks.edit-blocker',
+        'containers.blocks.editor',
+        'containers.blocks.owner',
+        'containers.blocks.user-data-field',
+        'containers.blocks.user-progress',
+        'course',
+        'editor',
+        'owner',
+        'parent',
+    ];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $user = $this->getUser($request);
+        if (!Authority::canIndexStructuralElementsShared($user)) {
+            throw new AuthorizationFailedException();
+        }
+
+        list($offset, $limit) = $this->getOffsetAndLimit();
+        $resources = [];
+        $contents = StructuralElement::findBySQL(
+            'range_id != ? AND range_type = ? ORDER BY mkdate DESC',
+            [$user->id, 'user']
+        );
+
+        foreach ($contents as $content) {
+            if (!count($content->read_approval) || !count($content->write_approval)) {
+                continue;
+            }
+
+            $add_content = false;
+
+            foreach ($content->read_approval['users'] as $listedUserPerm) {
+                if ($listedUserPerm['id'] == $user->id && $listedUserPerm['read']) {
+                    $add_content = true;
+                }
+            }
+
+            foreach ($content->write_approval['users'] as $listedUserPerm) {
+                if ($listedUserPerm['id'] == $user->id && $listedUserPerm['read']) {
+                    $add_content = true;
+                }
+            }
+
+            if ($add_content) {
+                $resources[] = $content;
+            }
+        }
+
+        return $this->getPaginatedContentResponse($resources, count($resources));
+    }
+}
diff --git a/lib/models/Courseware/StructuralElement.php b/lib/models/Courseware/StructuralElement.php
index 8edabadb5fc..c242870002e 100644
--- a/lib/models/Courseware/StructuralElement.php
+++ b/lib/models/Courseware/StructuralElement.php
@@ -169,14 +169,22 @@ class StructuralElement extends \SimpleORMap
         return self::getCourseware($courseId, 'course');
     }
 
-    private static function getCourseware(string $rangeId, string $rangeType): ?StructuralElement
+    public static function getSharedCoursewareUser(string $root_id): ?StructuralElement
     {
-        /** @var ?StructuralElement $result */
-        $result = self::findOneBySQL(
-            'range_id = ?
-            AND range_type = ? AND parent_id IS NULL',
-            [$rangeId, $rangeType]
-        );
+        return self::getCourseware('', '', $root_id);
+    }
+
+    private static function getCourseware(string $rangeId, string $rangeType, string $root_id = null): ?StructuralElement
+    {
+        if ($root_id) {
+            $result = self::find($root_id);
+        } else {
+            $result = self::findOneBySQL(
+                'range_id = ?
+                AND range_type = ? AND parent_id IS NULL',
+                [$rangeId, $rangeType]
+            );
+        }
 
         return $result;
     }
@@ -222,7 +230,11 @@ class StructuralElement extends \SimpleORMap
 
         switch ($this->range_type) {
             case 'user':
-                return $this->range_id === $user->id;
+                if ($this->range_id === $user->id) {
+                    return true;
+                }
+
+                return $this->hasWriteApproval($user);
 
             case 'course':
                 $hasEditingPermission = $this->hasEditingPermission($user);
@@ -273,11 +285,12 @@ class StructuralElement extends \SimpleORMap
 
         switch ($this->range_type) {
             case 'user':
-                // Kontext "user": Nutzende können nur ihre eigenen Strukturknoten sehen.
-                if  ($this->range_id === $user->id) {
+                if ($this->range_id === $user->id) {
                     return true;
                 }
 
+                return $this->hasReadApproval($user);
+
                 $link = StructuralElement::findOneBySQL('target_id = ?', [$this->id]);
                 if ($link) {
                     return true;
@@ -313,8 +326,11 @@ class StructuralElement extends \SimpleORMap
 
         switch ($this->range_type) {
             case 'user':
-                // Kontext "user": Nutzende können nur ihre eigenen Strukturknoten sehen.
-                return $this->range_id === $user->id;
+                if ($this->range_id === $user->id) {
+                    return true;
+                }
+
+                return $this->hasReadApproval($user);
 
             case 'course':
                 if (!$GLOBALS['perm']->have_studip_perm('user', $this->range_id, $user->id)) {
@@ -367,59 +383,133 @@ class StructuralElement extends \SimpleORMap
 
     private function hasReadApproval($user): bool
     {
-        if (!count($this->read_approval)) {
+        // this property is shared between all range types.
+        if ($this->read_approval['all']) {
             return true;
         }
 
-        if ($this->read_approval['all']) {
-            return true;
+        // now we also check against the perms for contents.
+        if ($this->range_type === 'user') {
+            return $this->hasUserReadApproval($user);
+        } else {
+            if (!count($this->read_approval)) {
+                return true;
+            }
+
+            // find user in users
+            $users = $this->read_approval['users'];
+            foreach ($users as $approvedUserId) {
+                if ($approvedUserId === $user->id) {
+                    return true;
+                }
+            }
+
+            // find user in groups
+            $groups = $this->read_approval['groups'];
+            foreach ($groups as $groupId) {
+                /** @var ?\Statusgruppen $group */
+                $group = \Statusgruppen::find($groupId);
+                if ($group && $group->isMember($user->id)) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    private function hasUserReadApproval($user): bool
+    {
+        if (!count($this->read_approval)) {
+            if ($this->isRootNode()) {
+                return false;
+            }
+            return $this->parent->hasUserReadApproval($user);
         }
 
         // find user in users
         $users = $this->read_approval['users'];
-        foreach ($users as $approvedUserId) {
-            if ($approvedUserId == $user->id) {
+        foreach ($users as $listedUserPerm) {
+            // now for contents, there is an expiry date defined.
+            if (!empty($listedUserPerm['expiry']) && strtotime($listedUserPerm['expiry']) < strtotime('today')) {
+                if ($this->isRootNode()) {
+                    return false;
+                }
+                return $this->parent->hasUserReadApproval($user);
+            }
+            // In order to have a record of the users in the perms list of contents,
+            // we keep a full perm record in read_approval column, and set read property to true or false,
+            // this won't apply to write_approval column.
+            if ($listedUserPerm['id'] == $user->id && $listedUserPerm['read'] == true) {
                 return true;
             }
         }
+    }
+
+    private function hasWriteApproval($user): bool
+    {
+        // this property is shared between all range types.
+        if ($this->write_approval['all']) {
+            return true;
+        }
 
-        // find user in groups
-        $groups = $this->read_approval['groups'];
-        foreach ($groups as $groupId) {
-            /** @var ?\Statusgruppen $group */
-            $group = \Statusgruppen::find($groupId);
-            if ($group && $group->isMember($user->id)) {
+        // now we also check against the perms for contents.
+        if ($this->range_type === 'user') {
+            return $this->hasUserWriteApproval($user);
+        } else {
+            if (!count($this->write_approval)) {
+                return false;
+            }
+
+            if ($this->write_approval['all']) {
+                return true;
+            }
+
+            // find user in users
+            $users = $this->write_approval['users']->getArrayCopy();
+            if (in_array($user->id, $users)) {
                 return true;
             }
+
+            // find user in groups
+            foreach (\Statusgruppen::findMany($this->write_approval['groups']->getArrayCopy()) as $group) {
+                if ($group->isMember($user->id)) {
+                    return true;
+                }
+            }
         }
 
         return false;
     }
 
-    private function hasWriteApproval($user): bool
+    private function hasUserWriteApproval($user): bool
     {
         if (!count($this->write_approval)) {
-            return false;
-        }
-
-        if ($this->write_approval['all']) {
-            return true;
+            if ($this->isRootNode()) {
+                return false;
+            }
+            return $this->parent->hasUserWriteApproval($user);
         }
 
         // find user in users
-        $users = $this->write_approval['users']->getArrayCopy();
-        if (in_array($user->id, $users)) {
-            return true;
-        }
-
-        // find user in groups
-        foreach (\Statusgruppen::findMany($this->write_approval['groups']->getArrayCopy()) as $group) {
-            if ($group->isMember($user->id)) {
+        $users = $this->write_approval['users'];
+        foreach ($users as $listedUserPerm) {
+            // now for contents, there is an expiry date defined.
+            if (!empty($listedUserPerm['expiry']) && strtotime($listedUserPerm['expiry']) < strtotime('today')) {
+                if ($this->isRootNode()) {
+                    return false;
+                }
+                return $this->parent->hasUserWriteApproval($user);
+            }
+            if ($listedUserPerm['id'] == $user->id) {
                 return true;
             }
         }
 
-        return false;
+        if ($this->isRootNode()) {
+            return false;
+        }
+        return $this->parent->hasUserWriteApproval($user);
     }
 
     /**
diff --git a/lib/navigation/ContentsNavigation.php b/lib/navigation/ContentsNavigation.php
index 32cefa56fd9..8e814ef4728 100644
--- a/lib/navigation/ContentsNavigation.php
+++ b/lib/navigation/ContentsNavigation.php
@@ -47,30 +47,36 @@ class ContentsNavigation extends Navigation
             $courseware->setDescription(_('Erstellen und Sammeln von Lernmaterialien'));
             $courseware->setImage(Icon::create('courseware'));
 
-            $courseware->addSubNavigation(
-                'overview',
-                new Navigation(_('Übersicht'), 'dispatch.php/contents/courseware/index')
-            );
-            $courseware->addSubNavigation(
-                'courseware',
-                new Navigation(_('Persönliche Lernmaterialien'), 'dispatch.php/contents/courseware/courseware')
-            );
-            $courseware->addSubNavigation(
-                'courseware_manager',
-                new Navigation(_('Verwaltung persönlicher Lernmaterialien'), 'dispatch.php/contents/courseware/courseware_manager')
-            );
-            $courseware->addSubNavigation(
-                'releases',
-                new Navigation(_('Freigaben'), 'dispatch.php/contents/courseware/releases')
-            );
-            $courseware->addSubNavigation(
-                'bookmarks',
-                new Navigation(_('Lesezeichen'), 'dispatch.php/contents/courseware/bookmarks')
-            );
-            $courseware->addSubNavigation(
-                'courses_overview',
-                new Navigation(_('Meine Veranstaltungen'), 'dispatch.php/contents/courseware/courses_overview')
-            );
+        $courseware = new Navigation(_('Courseware'));
+        $courseware->setDescription(_('Erstellen und Sammeln von Lernmaterialien'));
+        $courseware->setImage(Icon::create('courseware'));
+
+        $courseware->addSubNavigation(
+            'overview',
+            new Navigation(_('Übersicht'), 'dispatch.php/contents/courseware/index')
+        );
+        $courseware->addSubNavigation(
+            'courseware',
+            new Navigation(_('Persönliche Lernmaterialien'), 'dispatch.php/contents/courseware/courseware')
+        );
+        $courseware->addSubNavigation(
+            'courseware_manager',
+            new Navigation(_('Verwaltung persönlicher Lernmaterialien'), 'dispatch.php/contents/courseware/courseware_manager')
+        );
+        $courseware->addSubNavigation(
+            'releases',
+            new Navigation(_('Freigaben'), 'dispatch.php/contents/courseware/releases')
+        );
+        $courseware->addSubNavigation(
+            'bookmarks',
+            new Navigation(_('Lesezeichen'), 'dispatch.php/contents/courseware/bookmarks')
+        );
+        $courseware->addSubNavigation(
+            'courses_overview',
+            new Navigation(_('Meine Veranstaltungen'), 'dispatch.php/contents/courseware/courses_overview')
+        );
+
+        $this->addSubNavigation('courseware', $courseware);
 
             $this->addSubNavigation('courseware', $courseware);
         }
diff --git a/resources/assets/javascripts/bootstrap/courseware.js b/resources/assets/javascripts/bootstrap/courseware.js
index 87101506622..7124ac9a8cc 100644
--- a/resources/assets/javascripts/bootstrap/courseware.js
+++ b/resources/assets/javascripts/bootstrap/courseware.js
@@ -79,7 +79,7 @@ STUDIP.domReady(() => {
     if (document.getElementById('courseware-content-releases-app')) {
         STUDIP.Vue.load().then(({ createApp }) => {
             import(
-                /* webpackChunkName: "courseware-content-links-app" */
+                /* webpackChunkName: "courseware-content-releases-app" */
                 '@/vue/courseware-content-releases-app.js'
             ).then(({ default: mountApp }) => {
                 return mountApp(STUDIP, createApp, '#courseware-content-releases-app');
diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss
index f7ee74ea7f6..ab114ad4097 100644
--- a/resources/assets/stylesheets/scss/courseware.scss
+++ b/resources/assets/stylesheets/scss/courseware.scss
@@ -94,6 +94,12 @@ c o n t e n t s
 * * * * * * * * */
 .cw-content-overview {
     max-width: 1100px;
+    h2 {
+        margin: 0;
+        font-weight: 400;
+        padding: 5px 0;
+        font-size: 1.4em;
+    }
 }
 
 .cw-contents-overview-teaser {
@@ -193,6 +199,22 @@ c o n t e n t s
     }
 }
 
+.cw-content-courses {
+    h2 {
+        margin: 0;
+        font-weight: 400;
+        padding: 5px 0;
+        font-size: 1.4em;
+    }
+    ul.cw-tiles {
+        margin-bottom: 20px;
+    }
+}
+
+.cw-contents-overview-personal {
+    margin-bottom: 2em;
+}
+
 /* * * * * * * * * * *
 c o n t e n t s  e n d
 * * * * * * * * * * */
@@ -202,7 +224,8 @@ r i b b o n
 * * * * * */
 $consum_ribbon_width: calc(100% - 58px);
 #course-courseware-index,
-#contents-courseware-courseware {
+#contents-courseware-courseware,
+#contents-courseware-shared_content_courseware  {
     &.consume {
         overflow: hidden;
     }
@@ -725,6 +748,23 @@ ribbon end
             padding: 0;
             font-size: 1.25em;
         }
+        td.perm {
+            input.right, input.date {
+                cursor: pointer !important;
+            }
+        }
+    }
+    button.cw-add-persons {
+        margin-left: 4px;
+    }
+    button.cw-permission-delete {
+        width: 24px;
+        height: 24px;
+        border: none;
+        background-color: transparent;
+        @include background-icon(trash, clickable);
+        background-repeat: no-repeat;
+        cursor: pointer;
     }
 }
 
@@ -3117,6 +3157,26 @@ a u d i o  b l o c k
 a u d i o  b l o c k  e n d
 * * * * * * * * * * * * * */
 
+/* * * * * * * * * * * * * * * * * * * *
+f o r  m u l t i m e d i a  b l o c k s
+* * * * * * * * * * * * * * * * * * * */
+.cw-file-empty {
+    @include background-icon(file, info, 96);
+    border: solid thin $content-color-40;
+    background-position: center 1em;
+    background-repeat: no-repeat;
+    min-height: 140px;
+    padding: 1em;
+    p {
+        text-align: center;
+        padding-top: 106px;
+    }
+}
+
+/* * * * * * * * * * * * * * * * * * * * * * * *
+f o r  m u l t i m e d i a  b l o c k s  e n d
+* * * * * * * * * * * * * * * * * * * * * * * */
+
 /* * * * * * * * * *
 v i d e o  b l o c k
 * * * * * * * * * * */
@@ -4808,17 +4868,6 @@ cw tiles end
 }
 /* courseware template preview end*/
 
-/* contents courseware courses */
-.cw-content-courses {
-    h2 {
-        margin-top: 0;
-    }
-    ul.cw-tiles {
-        margin-bottom: 20px;
-    }
-}
-/* contents courseware courses end*/
-
 /* * * * * * * * * *
  i n p u t  f i l e
 * * * * * * * * * */
diff --git a/resources/assets/stylesheets/scss/multi_person_search.scss b/resources/assets/stylesheets/scss/multi_person_search.scss
new file mode 100755
index 00000000000..e1e6270f150
--- /dev/null
+++ b/resources/assets/stylesheets/scss/multi_person_search.scss
@@ -0,0 +1,8 @@
+.studip-msp-vue {
+    a.msp-btn {
+        margin-left: 5px;
+        img {
+            vertical-align: middle;
+        }
+    }
+}
diff --git a/resources/assets/stylesheets/studip.scss b/resources/assets/stylesheets/studip.scss
index deca8381a99..1b0d2e0cb34 100644
--- a/resources/assets/stylesheets/studip.scss
+++ b/resources/assets/stylesheets/studip.scss
@@ -99,6 +99,7 @@
 @import "scss/typography";
 @import "scss/user-administration";
 @import "scss/wiki";
+@import "scss/multi_person_search";
 
 
 // Class for DOM elements that should only be visible to Screen readers
diff --git a/resources/vue/base-components.js b/resources/vue/base-components.js
index 09dbac3fb54..ccff7c5652e 100644
--- a/resources/vue/base-components.js
+++ b/resources/vue/base-components.js
@@ -19,6 +19,7 @@ import StudipProxyCheckbox from './components/StudipProxyCheckbox.vue';
 import StudipProxiedCheckbox from './components/StudipProxiedCheckbox.vue';
 import StudipTooltipIcon from './components/StudipTooltipIcon.vue';
 import StudipSelect from './components/StudipSelect.vue';
+import StudipMultiPersonSearch from './components/StudipMultiPersonSearch.vue';
 
 const BaseComponents = {
     Multiselect,
@@ -41,7 +42,8 @@ const BaseComponents = {
     StudipProxiedCheckbox,
     StudipTooltipIcon,
     StudipSelect,
-    TextareaWithToolbar
+    TextareaWithToolbar,
+    StudipMultiPersonSearch
 };
 
 export default BaseComponents;
diff --git a/resources/vue/components/StudipIcon.vue b/resources/vue/components/StudipIcon.vue
index 21b37a2616e..3a2ac79889b 100644
--- a/resources/vue/components/StudipIcon.vue
+++ b/resources/vue/components/StudipIcon.vue
@@ -28,7 +28,8 @@
                 if (this.shape.indexOf("http") === 0) {
                     return this.shape;
                 }
-                return `${STUDIP.ASSETS_URL}images/icons/${this.color}/${this.shape}.svg`;
+                var path = this.shape.split('+').reverse().join('/');
+                return `${STUDIP.ASSETS_URL}images/icons/${this.color}/${path}.svg`;
             },
             color: function () {
                 switch (this.role) {
diff --git a/resources/vue/components/StudipMultiPersonSearch.vue b/resources/vue/components/StudipMultiPersonSearch.vue
new file mode 100644
index 00000000000..17a70cfc713
--- /dev/null
+++ b/resources/vue/components/StudipMultiPersonSearch.vue
@@ -0,0 +1,190 @@
+<template>
+    <div class="mpscontainer studip-msp-vue">
+        <form method="post" class="default" @submit.prevent="search">
+            <label class="with-action">
+                <input type="text" ref="searchInputField" v-model="searchTerm" :placeholder="$gettext('Suchen')" style="width: 260px;">
+                <a href="#" class="msp-btn" @click.prevent="search" :title="$gettext('Suche starten')">
+                    <studip-icon shape="search" role="clickable" size="16"></studip-icon>
+                </a>
+                <a href="#" class="msp-btn" @click.prevent="resetSearch" :title="$gettext('Suche zurücksetzen')">
+                    <studip-icon shape="decline" role="clickable" size="16"></studip-icon>
+                </a>
+            </label>
+            <select multiple="multiple" :id="select_box_id" name="selectbox[]"></select>
+        </form>
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'studip-multi-person-search',
+    props: {
+        name: String,
+        withDetail: {
+            type: Boolean,
+            default: true
+        }
+    },
+    data() {
+        return {
+            searchTerm: '',
+            count: 0,
+            users: []
+        }
+    },
+    mounted () {
+        this.$nextTick(() => {
+            this.init();
+            setTimeout(() => {
+                this.$refs.searchInputField.focus();
+            }, 100);
+        });
+    },
+    computed: {
+        id() {
+            return this._uid;
+        },
+        count_text_id() {
+            return this.id + '_count';
+        },
+        select_box_id() {
+            return this.id + '_selectbox';
+        },
+    },
+    methods: {
+        init() {
+            let select_all_btn = document.createElement('a');
+            select_all_btn.setAttribute('id', `${this.id}-select-all`);
+            select_all_btn.setAttribute('href', '#');
+            select_all_btn.innerText = this.$gettext('Alle hinzufügen');
+            select_all_btn.addEventListener('click', (e) => {
+                e.preventDefault();
+                this.selectAll();
+            });
+            let unselect_all_btn = document.createElement('a');
+            unselect_all_btn.setAttribute('id', `${this.id}-unselect-all`);
+            unselect_all_btn.setAttribute('href', '#');
+            unselect_all_btn.innerText = this.$gettext('Alle entfernen');
+            unselect_all_btn.addEventListener('click', (e) => {
+                e.preventDefault();
+                this.unselectAll();
+            });
+            let selection_header = document.createElement('div');
+            selection_header.setAttribute('id', this.count_text_id);
+            selection_header.innerText = this.$gettextInterpolate('Sie haben %{ count } Personen ausgewählt', {count: this.count});
+
+            $('#' + this.select_box_id).multiSelect({
+                selectableHeader: '<div>' + this.$gettext('Suchergebnisse') + '</div>',
+                selectionHeader: selection_header,
+                selectableFooter: select_all_btn,
+                selectionFooter: unselect_all_btn,
+                afterSelect: () => this.updateSelection(),
+                afterDeselect: () => this.updateSelection()
+            });
+        },
+
+        search() {
+            this.users = [];
+            let view = this;
+            $.getJSON(
+                STUDIP.URLHelper.getURL('dispatch.php/multipersonsearch/ajax_search_vue/' + this.name, { s: this.searchTerm }),
+                function(data) {
+                    view.removeAllNotSelected();
+                    var searchcount = 0;
+                    $.each(data, function(i, item) {
+                        searchcount += view.append(
+                            item.id,
+                            item.avatar + ' -- ' + item.text,
+                            item.selected
+                        );
+                        delete item.selected;
+                        view.users.push(item);
+                    });
+                    view.refresh();
+
+                    if (searchcount === 0) {
+                        view.append(
+                            '--',
+                            view.$gettextInterpolate('Es wurden keine neuen Ergebnisse für "%{ needle }" gefunden.', {needle: view.searchTerm}),
+                            true
+                        );
+                        view.refresh();
+                    }
+                }
+            );
+        },
+
+        selectAll: function() {
+            $('#' + this.select_box_id).multiSelect('select_all');
+            this.updateSelection();
+        },
+
+        unselectAll: function() {
+            $('#' + this.select_box_id).multiSelect('deselect_all');
+            this.updateSelection();
+        },
+
+        removeAll: function() {
+            $('#' + this.select_box_id + ' option').remove();
+            this.refresh();
+        },
+
+        removeAllNotSelected() {
+            $('#' + this.select_box_id + ' option:not(:selected)').remove();
+            this.refresh();
+        },
+
+        resetSearch() {
+            this.searchTerm = '';
+            this.removeAllNotSelected();
+        },
+
+        append(id, text, selected = false) {
+            if ($('#' + this.select_box_id + ' option[value=' + id + ']').length === 0) {
+                $('#' + this.select_box_id).multiSelect('addOption', {
+                    value: id,
+                    text: text,
+                    disabled: selected
+                });
+                return 1;
+            }
+            return 0;
+        },
+
+        refresh() {
+            $('#' + this.select_box_id).multiSelect('refresh');
+            this.updateSelection();
+        },
+
+        updateCount(){
+            this.count = $('#' + this.select_box_id + ' option:enabled:selected').length;
+            $('#' + this.count_text_id).text(this.$gettextInterpolate('Sie haben %{ count } Personen ausgewählt', {count: this.count}));
+        },
+
+        async updateSelection() {
+            this.updateCount();
+            let selected_options = $('#' + this.select_box_id + ' option:enabled:selected');
+            let user_ids = [];
+            if (selected_options.length) {
+                for (const option of selected_options) {
+                    user_ids.push(option.value);
+                }
+            }
+            let return_value = [];
+            if (this.withDetail && this.users.length) {
+                for (const user_id of user_ids) {
+                    let existing_index = this.users.findIndex(user => {
+                        return user.id === user_id;
+                    });
+                    if (existing_index !== -1) {
+                        return_value.push(this.users[existing_index]);
+                    }
+                }
+            } else {
+                return_value = user_ids;
+            }
+            this.$emit('input', return_value);
+        }
+    },
+}
+</script>
diff --git a/resources/vue/components/courseware/ContentOverviewApp.vue b/resources/vue/components/courseware/ContentOverviewApp.vue
index cad3fc0076a..831a2e76cad 100644
--- a/resources/vue/components/courseware/ContentOverviewApp.vue
+++ b/resources/vue/components/courseware/ContentOverviewApp.vue
@@ -1,10 +1,10 @@
 <template>
   <div class="cw-content-overview">
         <courseware-content-overview-elements />
-        <MountingPortal mountTo="#courseware-content-overview-action-widget" name="sidebar-views">
+        <MountingPortal mountTo="#courseware-content-overview-action-widget" name="sidebar-actions">
             <courseware-content-overview-action-widget />
         </MountingPortal>
-        <MountingPortal mountTo="#courseware-content-overview-filter-widget" name="sidebar-views">
+        <MountingPortal mountTo="#courseware-content-overview-filter-widget" name="sidebar-filters">
             <courseware-content-overview-filter-widget />
         </MountingPortal>
   </div>
diff --git a/resources/vue/components/courseware/ContentReleasesApp.vue b/resources/vue/components/courseware/ContentReleasesApp.vue
index 7dd550079eb..1a2ed6fcc9c 100644
--- a/resources/vue/components/courseware/ContentReleasesApp.vue
+++ b/resources/vue/components/courseware/ContentReleasesApp.vue
@@ -1,18 +1,22 @@
 <template>
   <div class="cw-content-releases">
         <courseware-content-links />
+        <courseware-content-shared />
         <courseware-companion-overlay />
   </div>
 </template>
 
 <script>
 import CoursewareContentLinks from './CoursewareContentLinks.vue';
+import CoursewareContentShared from './CoursewareContentShared.vue';
 import CoursewareCompanionOverlay from './CoursewareCompanionOverlay.vue';
 
 export default {
     components: {
         CoursewareContentLinks,
+        CoursewareContentShared,
         CoursewareCompanionOverlay
-    }
+    },
+
 }
 </script>
diff --git a/resources/vue/components/courseware/CoursewareContentOverviewElements.vue b/resources/vue/components/courseware/CoursewareContentOverviewElements.vue
index 09584757482..c23e6d621b1 100644
--- a/resources/vue/components/courseware/CoursewareContentOverviewElements.vue
+++ b/resources/vue/components/courseware/CoursewareContentOverviewElements.vue
@@ -1,214 +1,267 @@
 <template>
-<div v-if="root">
-    <ul class="cw-tiles">
-        <li
-            v-for="child in filteredChildren"
-            :key="child.id"
-            class="tile"
-            :class="[child.attributes.payload.color, filteredChildren.length > 3 ? '':  'cw-tile-margin']"
-        >
-            <a :href="getElementUrl(child.id)" :title="child.attributes.title">
-                <div
-                    class="preview-image"
-                    :class="[hasImage(child) ? '' : 'default-image']"
-                    :style="getChildStyle(child)"
-                ></div>
-                <div class="description">
-                    <header
-                        :class="[child.attributes.purpose !== '' ? 'description-icon-' + child.attributes.purpose : '']"
-                    >
-                        {{ child.attributes.title }}
-                    </header>
-                    <div class="description-text-wrapper">
-                        <p>{{ child.attributes.payload.description }}</p>
-                    </div>
-                    <footer>
-                        {{ countChildren(child) + 1 }}
-                        <translate
-                            :translate-n="countChildren(child) + 1"
-                            translate-plural="Seiten"
-                        >
-                            Seite
-                        </translate>
-                    </footer>
-                </div>
-            </a>
-        </li>
-    </ul>
-    <courseware-companion-box v-if="children.length !== 0 && filteredChildren.length === 0 && purposeFilter !== 'all'" :msgCompanion="text.emptyFilter" mood="pointing"/>
-    <div v-if="children.length === 0" class="cw-contents-overview-teaser">
-        <div class="cw-contents-overview-teaser-content">
-            <header><translate>Ihre persönlichen Lernmaterialien</translate></header>
-            <p><translate>Erstellen und Verwalten Sie hier ihre eigenen persönlichen Lernmaterialien in Form von ePorfolios,
-                        Vorlagen für Veranstaltungen oder einfach nur persönliche Inhalte für das Studium.
-                        Entwickeln Sie ihre eigenen (Lehr-)Materialien für Studium oder die Lehre und teilen diese mit anderen Nutzenden.</translate></p>
-            <button class="button" @click="addElement">
-                <translate>Neues Lernmaterial anlegen</translate>
-            </button>
-        </div>
-    </div>
-    <studip-dialog
-        v-if="showOverviewElementAddDialog"
-        :title="$gettext('Neues Lernmaterial anlegen')"
-        height="600"
-        width="500"
-        :confirmText="$gettext('Erstellen')"
-        confirmClass="accept"
-        :closeText="$gettext('Schließen')"
-        closeClass="cancel"
-        class="cw-structural-element-dialog"
-        @close="closeAddDialog"
-        @confirm="createElement"
-    >
-        <template v-slot:dialogContent>
-
-                <courseware-collapsible-box
-                :title="$gettext('Grundeinstellungen')"
-                :open="true"
+    <div class="cw-contents-overview-wrapper">
+        <div v-if="root && filteredChildren.length > 0" class="cw-contents-overview-personal">
+            <h2>
+                <translate>Persönliche Lernmaterialien</translate>
+            </h2>
+            <ul class="cw-tiles">
+                <li
+                    v-for="child in filteredChildren"
+                    :key="child.id"
+                    class="tile"
+                    :class="[child.attributes.payload.color, filteredChildren.length > 3 ? '':  'cw-tile-margin']"
                 >
-                    <form class="default" @submit.prevent="">
-                        <label>
-                            <translate>Titel des Lernmaterials</translate><br />
-                            <input v-model="newElement.attributes.title" type="text" />
-                        </label>
-                        <label>
-                            <translate>Zusammenfassung</translate><br />
-                            <textarea v-model="newElement.attributes.payload.description"></textarea>
-                        </label>
-                        <label>
-                            <translate>Bild</translate>
-                            <br>
-                            <input ref="upload_image" type="file" accept="image/*" @change="checkUploadFile" />
-                            <courseware-companion-box
-                                v-if="uploadFileError"
-                                :msgCompanion="uploadFileError"
-                                mood="sad"
-                                class="cw-companion-box-in-form"
-                            />
-                        </label>
-                        <label>
-                            <translate>Art des Lernmaterials</translate>
-                            <select v-model="newElementPurpose">
-                                <option value="content"><translate>Inhalt</translate></option>
-                                <option value="template"><translate>Aufgabenvorlage</translate></option>
-                                <option value="oer"><translate>OER-Material</translate></option>
-                                <option value="portfolio"><translate>ePortfolio</translate></option>
-                                <option value="draft"><translate>Entwurf</translate></option>
-                                <option value="other"><translate>Sonstiges</translate></option>
-                            </select>
-                        </label>
-                        <label>
-                            <translate>Lernmaterialvorlage</translate>
-                            <select v-model="newElementTemplate">
-                                <option :value="null"><translate>ohne Vorlage</translate></option>
-                                <option
-                                    v-for="template in selectableTemplates"
-                                    :key="template.id"
-                                    :value="template"
-                                >
-                                    {{ template.attributes.name }}
-                                </option>
-                            </select>
-                        </label>
-                    </form>
-                </courseware-collapsible-box>
-                <courseware-collapsible-box :title="$gettext('Vorschau')">
-                    <div v-if="currentTemplateStructure" class="cw-template-preview">
+                    <a :href="getElementUrl(child.id)" :title="child.attributes.title">
                         <div
-                            class="cw-template-preview-container-wrapper"
-                            v-for="container in currentTemplateStructure.containers"
-                            :key="container.id"
-                            :class="['cw-template-preview-container-' + container.attributes.payload.colspan]"
-                        >
-                            <div class="cw-template-preview-container-content">
-                                <header class="cw-template-preview-container-title">
-                                    {{ container.attributes.title }} | {{ container.attributes.width }}
-                                </header>
-                                <div class="cw-template-preview-blocks" v-for="block in container.blocks" :key="block.id">
-                                    <header class="cw-template-preview-blocks-title">
-                                        {{ block.attributes.title }}
+                            class="preview-image"
+                            :class="[hasImage(child) ? '' : 'default-image']"
+                            :style="getChildStyle(child)"
+                        ></div>
+                        <div class="description">
+                            <header
+                                :class="[child.attributes.purpose !== '' ? 'description-icon-' + child.attributes.purpose : '']"
+                            >
+                                {{ child.attributes.title }}
+                            </header>
+                            <div class="description-text-wrapper">
+                                <p>{{ child.attributes.payload.description }}</p>
+                            </div>
+                            <footer>
+                                {{ countChildren(child) + 1 }}
+                                <translate
+                                    :translate-n="countChildren(child) + 1"
+                                    translate-plural="Seiten"
+                                >
+                                    Seite
+                                </translate>
+                            </footer>
+                        </div>
+                    </a>
+                </li>
+            </ul>
+        </div>
+        <div v-if="children.length === 0" class="cw-contents-overview-teaser">
+            <div class="cw-contents-overview-teaser-content">
+                <header><translate>Ihre persönlichen Lernmaterialien</translate></header>
+                <p><translate>Erstellen und Verwalten Sie hier ihre eigenen persönlichen Lernmaterialien in Form von ePorfolios,
+                            Vorlagen für Veranstaltungen oder einfach nur persönliche Inhalte für das Studium.
+                            Entwickeln Sie ihre eigenen (Lehr-)Materialien für Studium oder die Lehre und teilen diese mit anderen Nutzenden.</translate></p>
+                <button class="button" @click="addElement">
+                    <translate>Neues Lernmaterial anlegen</translate>
+                </button>
+            </div>
+        </div>
+        <studip-dialog
+                v-if="showOverviewElementAddDialog"
+                :title="$gettext('Neues Lernmaterial anlegen')"
+                height="600"
+                width="500"
+                :confirmText="$gettext('Erstellen')"
+                confirmClass="accept"
+                :closeText="$gettext('Schließen')"
+                closeClass="cancel"
+                class="cw-structural-element-dialog"
+                @close="closeAddDialog"
+                @confirm="createElement"
+        >
+            <template v-slot:dialogContent>
+
+                    <courseware-collapsible-box
+                    :title="$gettext('Grundeinstellungen')"
+                    :open="true"
+                    >
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                <translate>Titel des Lernmaterials</translate><br />
+                                <input v-model="newElement.attributes.title" type="text" />
+                            </label>
+                            <label>
+                                <translate>Zusammenfassung</translate><br />
+                                <textarea v-model="newElement.attributes.payload.description"></textarea>
+                            </label>
+                            <label>
+                                <translate>Bild</translate>
+                                <br>
+                                <input ref="upload_image" type="file" accept="image/*" @change="checkUploadFile" />
+                                <courseware-companion-box
+                                    v-if="uploadFileError"
+                                    :msgCompanion="uploadFileError"
+                                    mood="sad"
+                                    class="cw-companion-box-in-form"
+                                />
+                            </label>
+                            <label>
+                                <translate>Art des Lernmaterials</translate>
+                                <select v-model="newElementPurpose">
+                                    <option value="content"><translate>Inhalt</translate></option>
+                                    <option value="template"><translate>Aufgabenvorlage</translate></option>
+                                    <option value="oer"><translate>OER-Material</translate></option>
+                                    <option value="portfolio"><translate>ePortfolio</translate></option>
+                                    <option value="draft"><translate>Entwurf</translate></option>
+                                    <option value="other"><translate>Sonstiges</translate></option>
+                                </select>
+                            </label>
+                            <label>
+                                <translate>Lernmaterialvorlage</translate>
+                                <select v-model="newElementTemplate">
+                                    <option :value="null"><translate>ohne Vorlage</translate></option>
+                                    <option
+                                        v-for="template in selectableTemplates"
+                                        :key="template.id"
+                                        :value="template"
+                                    >
+                                        {{ template.attributes.name }}
+                                    </option>
+                                </select>
+                            </label>
+                        </form>
+                    </courseware-collapsible-box>
+                    <courseware-collapsible-box :title="$gettext('Vorschau')">
+                        <div v-if="currentTemplateStructure" class="cw-template-preview">
+                            <div
+                                class="cw-template-preview-container-wrapper"
+                                v-for="container in currentTemplateStructure.containers"
+                                :key="container.id"
+                                :class="['cw-template-preview-container-' + container.attributes.payload.colspan]"
+                            >
+                                <div class="cw-template-preview-container-content">
+                                    <header class="cw-template-preview-container-title">
+                                        {{ container.attributes.title }} | {{ container.attributes.width }}
                                     </header>
+                                    <div class="cw-template-preview-blocks" v-for="block in container.blocks" :key="block.id">
+                                        <header class="cw-template-preview-blocks-title">
+                                            {{ block.attributes.title }}
+                                        </header>
+                                    </div>
                                 </div>
                             </div>
                         </div>
-                    </div>
-                    <courseware-companion-box
-                        v-else
-                        :msgCompanion="$gettext('Sie können eine Lernmaterialvorlage auswählen und hier eine Vorschau betrachten. Ohne Vorlage wird eine leere Seite erzeugt.')"
-                    />
-                </courseware-collapsible-box>
-                <courseware-collapsible-box
-                    :title="$gettext('Zusatzangaben')"
-                >
-                    <form class="default" @submit.prevent="">
-                        <label>
-                            <translate>Lizenztyp</translate>
-                            <select v-model="newElement.attributes.payload.license_type">
-                                <option v-for="license in licenses" :key="license.id" :value="license.id">
-                                    {{ license.name }}
-                                </option>
-                            </select>
-                        </label>
-                        <label>
-                            <translate>Geschätzter zeitlicher Aufwand</translate>
-                            <input type="text" v-model="newElement.attributes.payload.required_time" />
-                        </label>
-                        <label>
-                            <translate>Niveau</translate><br />
-                            <translate>von</translate>
-                            <select v-model="newElement.attributes.payload.difficulty_start">
-                                <option
-                                    v-for="difficulty_start in 12"
-                                    :key="difficulty_start"
-                                    :value="difficulty_start"
-                                >
-                                    {{ difficulty_start }}
-                                </option>
-                            </select>
-                            <translate>bis</translate>
-                            <select v-model="newElement.attributes.payload.difficulty_end">
-                                <option
-                                    v-for="difficulty_end in 12"
-                                    :key="difficulty_end"
-                                    :value="difficulty_end"
+                        <courseware-companion-box
+                            v-else
+                            :msgCompanion="$gettext('Sie können eine Lernmaterialvorlage auswählen und hier eine Vorschau betrachten. Ohne Vorlage wird eine leere Seite erzeugt.')"
+                        />
+                    </courseware-collapsible-box>
+                    <courseware-collapsible-box
+                        :title="$gettext('Zusatzangaben')"
+                    >
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                <translate>Lizenztyp</translate>
+                                <select v-model="newElement.attributes.payload.license_type">
+                                    <option v-for="license in licenses" :key="license.id" :value="license.id">
+                                        {{ license.name }}
+                                    </option>
+                                </select>
+                            </label>
+                            <label>
+                                <translate>Geschätzter zeitlicher Aufwand</translate>
+                                <input type="text" v-model="newElement.attributes.payload.required_time" />
+                            </label>
+                            <label>
+                                <translate>Niveau</translate><br />
+                                <translate>von</translate>
+                                <select v-model="newElement.attributes.payload.difficulty_start">
+                                    <option
+                                        v-for="difficulty_start in 12"
+                                        :key="difficulty_start"
+                                        :value="difficulty_start"
+                                    >
+                                        {{ difficulty_start }}
+                                    </option>
+                                </select>
+                                <translate>bis</translate>
+                                <select v-model="newElement.attributes.payload.difficulty_end">
+                                    <option
+                                        v-for="difficulty_end in 12"
+                                        :key="difficulty_end"
+                                        :value="difficulty_end"
+                                    >
+                                        {{ difficulty_end }}
+                                    </option>
+                                </select>
+                            </label>
+                            <label>
+                                <translate>Farbe</translate>
+                                <studip-select
+                                    v-model="newElement.attributes.payload.color"
+                                    :options="colors"
+                                    :reduce="(color) => color.class"
+                                    label="class"
                                 >
-                                    {{ difficulty_end }}
-                                </option>
-                            </select>
-                        </label>
-                        <label>
-                            <translate>Farbe</translate>
-                            <v-select
-                                v-model="newElement.attributes.payload.color"
-                                :options="colors"
-                                :reduce="(color) => color.class"
-                                label="class"
+                                    <template #open-indicator="selectAttributes">
+                                        <span v-bind="selectAttributes"
+                                            ><studip-icon shape="arr_1down" size="10"
+                                        /></span>
+                                    </template>
+                                    <template #no-options="{ search, searching, loading }">
+                                        <translate>Es steht keine Auswahl zur Verfügung.</translate>
+                                    </template>
+                                    <template #selected-option="{ name, hex }">
+                                        <span class="vs__option-color" :style="{ 'background-color': hex }"></span
+                                        ><span>{{ name }}</span>
+                                    </template>
+                                    <template #option="{ name, hex }">
+                                        <span class="vs__option-color" :style="{ 'background-color': hex }"></span
+                                        ><span>{{ name }}</span>
+                                    </template>
+                                </studip-select>
+                            </label>
+                        </form>
+                    </courseware-collapsible-box>
+
+            </template>
+        </studip-dialog>
+
+        <div v-if="filteredShared.length > 0" class="cw-contents-overview-shared">
+            <h2>
+                <translate>Geteilte Lernmaterialien</translate>
+            </h2>
+            <ul class="cw-tiles">
+                <li
+                    v-for="element in filteredShared"
+                    :key="element.id"
+                    class="tile"
+                    :class="[element.attributes.payload.color, sharedElements.length > 3 ? '':  'cw-tile-margin']"
+                >
+                    <a :href="getSharedElementUrl(element.id)" :title="element.attributes.title">
+                        <div
+                            class="preview-image"
+                            :class="[hasImage(element) ? '' : 'default-image']"
+                            :style="getChildStyle(element)"
+                        >
+                            <div class="overlay-text">{{ getOwnerName(element) }}</div>
+                        </div>
+                        <div class="description">
+                            <header
+                                :class="[element.attributes.purpose !== '' ? 'description-icon-' + element.attributes.purpose : '']"
                             >
-                                <template #open-indicator="selectAttributes">
-                                    <span v-bind="selectAttributes"
-                                        ><studip-icon shape="arr_1down" size="10"
-                                    /></span>
-                                </template>
-                                <template #no-options="{ search, searching, loading }">
-                                    <translate>Es steht keine Auswahl zur Verfügung.</translate>
-                                </template>
-                                <template #selected-option="{ name, hex }">
-                                    <span class="vs__option-color" :style="{ 'background-color': hex }"></span
-                                    ><span>{{ name }}</span>
-                                </template>
-                                <template #option="{ name, hex }">
-                                    <span class="vs__option-color" :style="{ 'background-color': hex }"></span
-                                    ><span>{{ name }}</span>
-                                </template>
-                            </v-select>
-                        </label>
-                    </form>
-                </courseware-collapsible-box>
+                                {{ element.attributes.title }}
+                            </header>
+                            <div class="description-text-wrapper">
+                                <p>{{ element.attributes.payload.description }}</p>
+                            </div>
+                            <footer>
+                                {{ countChildren(element) + 1 }}
+                                <translate
+                                    :translate-n="countChildren(element) + 1"
+                                    translate-plural="Seiten"
+                                >
+                                    Seite
+                                </translate>
+                            </footer>
+                        </div>
+                    </a>
+                </li>
+            </ul>
+        </div>
+        <courseware-companion-box
+            v-if="children.length !== 0 && filteredChildren.length === 0 && sharedElements.length !== 0 && filteredShared.length === 0"
+            :msgCompanion="$gettext('Für diese Auswahl wurden keine Lernmaterialien gefunden.')"
+            mood="pointing"
+        />
 
-        </template>
-    </studip-dialog>
-    <courseware-companion-overlay />
-</div>
+        <courseware-companion-overlay />
+    </div>
 </template>
 
 <script>
@@ -228,10 +281,6 @@ export default {
     },
     data() {
         return {
-            text: {
-                emptyFilter: this.$gettext('Für diese Auswahl wurden keine Lernmaterialien gefunden.'),
-                empty: this.$gettext('Es wurden keine Lernmaterialien gefunden.'),
-            },
             newElement: {
                 attributes: {
                     payload: {},
@@ -246,9 +295,13 @@ export default {
         ...mapGetters({
             getElement: 'courseware-structural-elements/byId',
             licenses: 'licenses',
+            permissionFilter: 'permissionFilter',
             purposeFilter: 'purposeFilter',
+            sourceFilter: 'sourceFilter',
             showOverviewElementAddDialog: 'showOverviewElementAddDialog',
             templates: 'courseware-templates/all',
+            sharedElements: 'courseware-structural-elements-shared/all',
+            userById: 'users/byId',
         }),
         root() {
             return this.getElement({id: STUDIP.COURSEWARE_USERS_ROOT_ID});
@@ -266,10 +319,32 @@ export default {
             return children;
         },
         filteredChildren() {
+            if (!['all', 'personal'].includes(this.sourceFilter)) {
+                return [];
+            }
+            let children = this.children;
+            if (this.purposeFilter !== 'all') {
+                children = children.filter(child => { return child.attributes.purpose === this.purposeFilter});
+            }
+            if (this.permissionFilter !== 'read') {
+                children = children.filter(child => { return child.attributes['can-edit'] });
+            }
+
+            return children;
+        },
+        filteredShared() {
+            if (!['all', 'shared'].includes(this.sourceFilter)) {
+                return [];
+            }
+            let elements = this.sharedElements;
             if (this.purposeFilter !== 'all') {
-                return this.children.filter(child => { return child.attributes.purpose === this.purposeFilter});
+                elements =  elements.filter(element => { return element.attributes.purpose === this.purposeFilter});
+            }
+            if (this.permissionFilter !== 'read') {
+                elements = elements.filter(element => { return element.attributes['can-edit'] });
             }
-            return this.children;
+
+            return elements;
         },
         colors() {
             const colors = [
@@ -455,8 +530,17 @@ export default {
         hasImage(child) {
             return child.relationships?.image?.data !== null;
         },
-        getElementUrl(element_id) {
-            return STUDIP.URLHelper.base_url + 'dispatch.php/contents/courseware/courseware#/structural_element/' + element_id;
+        getElementUrl(elementId) {
+            return STUDIP.URLHelper.base_url + 'dispatch.php/contents/courseware/courseware#/structural_element/' + elementId;
+        },
+        getSharedElementUrl(elementId) {
+            return STUDIP.URLHelper.base_url + 'dispatch.php/contents/courseware/shared_content_courseware/' + elementId;
+        },
+        getOwnerName(element) {
+            const ownerId = element.relationships.owner.data.id;
+            const owner = this.userById({ id: ownerId });
+
+            return owner.attributes['formatted-name']; 
         },
         addElement() {
             this.setShowOverviewElementAddDialog(true);
@@ -519,7 +603,7 @@ export default {
             } else {
                 this.uploadFileError = '';
             }
-        },
+        }
     },
     watch: {
         root(newRootObject) {
diff --git a/resources/vue/components/courseware/CoursewareContentOverviewFilterWidget.vue b/resources/vue/components/courseware/CoursewareContentOverviewFilterWidget.vue
index 3fd93ab5067..43fbd1e0c9c 100644
--- a/resources/vue/components/courseware/CoursewareContentOverviewFilterWidget.vue
+++ b/resources/vue/components/courseware/CoursewareContentOverviewFilterWidget.vue
@@ -1,51 +1,99 @@
 <template>
-    <select v-model="purposeFilter" class="sidebar-selectlist">
-        <option value="all">
-            <translate>alle</translate>
-        </option>
-        <option value="content">
-            <translate>Inhalt</translate>
-        </option>
-        <option value="template">
-            <translate>Aufgabenvorlage</translate>
-        </option>
-        <option value="oer">
-            <translate>OER-Material</translate>
-        </option>
-        <option value="portfolio">
-            <translate>ePortfolio</translate>
-        </option>
-        <option value="draft">
-            <translate>Entwurf</translate>
-        </option>
-        <option value="other">
-            <translate>Sonstiges</translate>
-        </option>
-    </select>
+    <div class="cw-filter-widget">
+        <form class="default" @submit.prevent="">
+            <label>
+                <translate>Lernmaterialien</translate>
+                <select v-model="sourceFilter">
+                    <option value="all">
+                        <translate>Alle</translate>
+                    </option>
+                    <option value="personal">
+                        <translate>Persönliche</translate>
+                    </option>
+                    <option value="shared">
+                        <translate>Geteilte</translate>
+                    </option>
+                </select>
+            </label>
+            <label>
+                <translate>Zweck</translate>
+                <select v-model="purposeFilter">
+                    <option value="all">
+                        <translate>Alle</translate>
+                    </option>
+                    <option value="content">
+                        <translate>Inhalt</translate>
+                    </option>
+                    <option value="template">
+                        <translate>Aufgabenvorlage</translate>
+                    </option>
+                    <option value="oer">
+                        <translate>OER-Material</translate>
+                    </option>
+                    <option value="portfolio">
+                        <translate>ePortfolio</translate>
+                    </option>
+                    <option value="draft">
+                        <translate>Entwurf</translate>
+                    </option>
+                    <option value="other">
+                        <translate>Sonstiges</translate>
+                    </option>
+                </select>
+            </label>
+            <label>
+                <translate>Rechte</translate>
+                <select v-model="permissionFilter">
+                    <option value="read">
+                        <translate>Lesen</translate>
+                    </option>
+                    <option value="write">
+                        <translate>Lesen und schreiben</translate>
+                    </option>
+                </select>
+            </label>
+        </form>
+    </div>
 </template>
 
 <script>
-import { mapActions, mapGetters } from 'vuex';
+import { mapActions } from 'vuex';
 
 export default {
     name: 'courseware-content-overview-filter-widget',
     data() {
         return {
-            purposeFilter: 'all'
+            permissionFilter: 'read',
+            purposeFilter: 'all',
+            sourceFilter: 'all',
         };
     },
     methods: {
         ...mapActions({
-            setPurposeFilter: 'setPurposeFilter'
+            setPermissionFilter: 'setPermissionFilter',
+            setPurposeFilter: 'setPurposeFilter',
+            setSourceFilter: 'setSourceFilter',
         }),
+        filterPermission() {
+            this.setPermissionFilter(this.permissionFilter);
+        },
         filterPurpose() {
             this.setPurposeFilter(this.purposeFilter);
-        }
+        },
+        filterSource() {
+            this.setSourceFilter(this.sourceFilter);
+        },
     },
     watch: {
+        permissionFilter() {
+            this.filterPermission();
+        },
         purposeFilter() {
             this.filterPurpose();
-        }
+        },
+        sourceFilter() {
+            this.filterSource();
+        },
     }
 }
-</script>
\ No newline at end of file
+</script>
diff --git a/resources/vue/components/courseware/CoursewareContentPermissions.vue b/resources/vue/components/courseware/CoursewareContentPermissions.vue
new file mode 100755
index 00000000000..22640721aa2
--- /dev/null
+++ b/resources/vue/components/courseware/CoursewareContentPermissions.vue
@@ -0,0 +1,391 @@
+<template>
+    <div class="cw-element-permissions">
+        <studip-message-box v-if="message != false"
+            :type="message.type ? message.type : 'info'"
+            :details="message.details ? message.details : []"
+            :hideClose="false">
+            {{ $gettext(message.text) }}
+        </studip-message-box>
+        <table class="default">
+            <caption>
+                <translate>Personen</translate>
+            </caption>
+            <colgroup>
+                <col style="width:35%">
+                <col style="width:15%">
+                <col style="width:25%">
+                <col style="width:15%">
+                <col style="width:10%">
+            </colgroup>
+            <thead>
+                <tr>
+                    <th><translate>Name</translate></th>
+                    <th><translate>Leserechte</translate></th>
+                    <th><translate>Lese- und Schreibrechte</translate></th>
+                    <th><translate>Ablaufdatum</translate></th>
+                    <th class="actions"><translate>Aktion</translate></th>
+                </tr>
+            </thead>
+            <tbody>
+                <tr v-if="listEmpty" class="empty">
+                    <td colspan="5">
+                        <translate>Es wurden noch keine Freigaben erteilt</translate>
+                    </td>
+                </tr>
+                <tr v-for="(user_perm, index) of userPermsList" :key="index">
+                    <td>
+                        <label>
+                            {{ user_perm['formatted-name'] }}
+                            <i>{{ user_perm.username }}</i>
+                        </label>
+                    </td>
+                    <td class="perm">
+                        <input
+                            class="right"
+                            :title="$gettextInterpolate('Leserechte für %{ userName }', { userName: user_perm.username })"
+                            type="radio"
+                            :name="`${user_perm.id}_right`"
+                            value="read"
+                            :checked="userPermsList[index]['read'] && !userPermsList[index]['write']"
+                            @change="updateReadWritePerm(index, $event.target.value)"
+                        />
+                    </td>
+                    <td class="perm">
+                        <input
+                            class="right"
+                            :title="$gettextInterpolate('Lese- und Schreibrechte für %{ userName }', { userName: user_perm.username })"
+                            type="radio"
+                            :name="`${user_perm.id}_right`"
+                            value="write"
+                            :checked="userPermsList[index]['read'] && userPermsList[index]['write']"
+                            @change="updateReadWritePerm(index, $event.target.value)"
+                        />
+                    </td>
+                    <td>
+                        <input
+                            style="cursor: pointer !important;"
+                            :title="getExpiryTitle(user_perm.username, userPermsList[index]['expiry'])"
+                            type="date"
+                            :min="minDate"
+                            :id="`${user_perm.id}_expiry`"
+                            v-model="userPermsList[index]['expiry']"
+                            @change="refreshReadWriteApproval"
+                        />
+                    </td>
+                    <td class="actions">
+                        <button
+                            class="cw-permission-delete"
+                            :title="$gettextInterpolate('Entfernen der Rechte von %{ userName }', { userName: user_perm.username })"
+                            @click.prevent="confirmDeleteUserPerm(index)"
+                        >
+                        </button>
+                    </td>
+                </tr>
+            </tbody>
+            <tfoot>
+                <tr>
+                    <td colspan="5">
+                        <span class="multibuttons">
+                            <button class="button add cw-add-persons" @click.prevent="showAddMultiPersonDialog = true">
+                                <translate>Personen hinzufügen</translate>
+                            </button>
+                            <button
+                                class="button"
+                                :class="{disabled: listEmpty}"
+                                :disabled="listEmpty"
+                                @click.prevent="setAllPerms('read')"
+                            >
+                                <translate>Allen Leserechte geben</translate>
+                            </button>
+                            <button
+                                class="button"
+                                :class="{disabled: listEmpty}"
+                                :disabled="listEmpty"
+                                @click.prevent="setAllPerms('write')"
+                            >
+                                <translate>Allen Lese- und Schreibrechte geben</translate>
+                            </button>
+                        </span>
+                    </td>
+                </tr>
+            </tfoot>
+        </table>
+        <studip-dialog
+            v-if="showAddMultiPersonDialog"
+            :title="$gettext('Personen hinzufügen')"
+            :confirmText="$gettext('Speichern')"
+            confirmClass="accept"
+            :closeText="$gettext('Schließen')"
+            closeClass="cancel"
+            @close="clearSelectedUsers"
+            @confirm="getSelectedUsers"
+            height="500"
+            width="750"
+        >
+            <template v-slot:dialogContent>
+                <studip-multi-person-search v-model="selectedUsers" name="content-persons"/>
+            </template>
+        </studip-dialog>
+        <studip-dialog
+            v-if="showDeleteDialog"
+            :title="$gettext('Personen löschen')"
+            :question="$gettext('Möchten Sie diese Person wirklich löschen?')"
+            height="180"
+            @confirm="performDeleteUserPerm"
+            @close="clearDeleteUserPerm"
+        ></studip-dialog>
+    </div>
+</template>
+<script>
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-content-permissions',
+    props: {
+        element: Object,
+    },
+    data() {
+        return {
+            showAddMultiPersonDialog: null,
+            userPermsList: [],
+            selectedUsers:[],
+            userPermsReadAll: false,
+            userPermsWriteAll: false,
+            userPermsReadUsers: [],
+            userPermsWriteUsers: [],
+            message: false,
+            showDeleteDialog: false,
+            deleteUserPermIndex: -1
+        };
+    },
+
+    mounted() {
+        if (this.element.attributes['read-approval'].all !== undefined) {
+            this.userPermsReadAll = this.element.attributes['read-approval'].all;
+        } else {
+            this.userPermsReadAll = false;
+        }
+        if (this.element.attributes['write-approval'].all !== undefined) {
+            this.userPermsWriteAll = this.element.attributes['write-approval'].all;
+        } else {
+            this.userPermsWriteAll = false;
+        }
+        this.initUserPermsList();
+    },
+
+    computed: {
+        ...mapGetters({
+            userById: 'users/byId',
+        }),
+
+        listEmpty() {
+            return this.userPermsList.length === 0;
+        },
+
+        readApproval() {
+            return {
+                all: this.userPermsReadAll,
+                users: this.userPermsReadUsers,
+                groups: []
+            };
+        },
+
+        writeApproval() {
+            return {
+                all: this.userPermsWriteAll,
+                users: this.userPermsWriteUsers,
+                groups: []
+            };
+        },
+
+        minDate() {
+            let today = new Date();
+            return today.toISOString().split('T')[0];
+        }
+    },
+
+    methods: {
+        ...mapActions({
+            loadUser: 'users/loadById',
+        }),
+
+        getExpiryTitle(userName, date) {
+            if (date) {
+                return this.$gettextInterpolate('Die Berechtigungen für %{ userName } laufen am folgendem Datum ab: %{ dateStr }', { userName: userName, dateStr: new Date(date).toLocaleDateString() })
+            } else {
+                return this.$gettextInterpolate('Das Ablaufdatum der Berechtigungen für %{ userName }', { userName: userName });
+            }
+        },
+
+        async getUser(userId) {
+            await this.loadUser({id: userId});
+            const user = this.userById({id: userId});
+            return user;
+        },
+
+        async initUserPermsList() {
+
+            if (this.element.attributes['read-approval'].users !== undefined) {
+                this.userPermsReadUsers = this.element.attributes['read-approval'].users;
+            }
+
+            if (this.element.attributes['write-approval'].users !== undefined) {
+                this.userPermsWriteUsers = this.element.attributes['write-approval'].users;
+            }
+
+            for (const user_perm_obj of this.userPermsReadUsers) {
+                let userObj = await this.getUser(user_perm_obj.id);
+                let writePerm = this.userPermsWriteUsers.some(user_write_perm => user_write_perm.id === user_perm_obj.id) ? true : false;
+                this.userPermsList.push({
+                    'id' : user_perm_obj.id,
+                    'read': user_perm_obj.read,
+                    'write': writePerm,
+                    'expiry': user_perm_obj.expiry ? new Date(user_perm_obj.expiry).toISOString().split('T')[0] : '',
+                    'formatted-name': userObj.attributes['formatted-name'],
+                    'username': userObj.attributes['username'],
+                });
+            }
+        },
+
+        async getSelectedUsers() {
+            this.message = false;
+            let duplicatedUsers = [];
+            if (this.selectedUsers.length) {
+                for (const selected_user of this.selectedUsers) {
+                    let exists = this.userPermsList.some(user => {
+                        return user.id === selected_user.id;
+                    });
+                    if (!exists) {
+                        let newUserPerm = {
+                            'id': selected_user.id,
+                            'read': true,
+                            'write': false,
+                            'expiry': '',
+                            'formatted-name': selected_user['formatted-name'],
+                            'username': selected_user.username,
+                        };
+                        this.userPermsList.push(newUserPerm);
+                        this.refreshReadWriteApproval();
+                    } else {
+                        duplicatedUsers.push(selected_user);
+                    }
+                }
+                this.selectedUsers = [];
+            }
+            this.showAddMultiPersonDialog = false;
+
+            if (duplicatedUsers.length > 0) {
+                this.message = {};
+                this.message.text = this.$gettext('Die folgenden ausgewählten Personen existierten bereits:');
+                this.message.type = 'info';
+                this.message.details = [];
+                for (const duplicated of duplicatedUsers) {
+                    this.message.details.push(duplicated['formatted-name']);
+                }
+            }
+        },
+
+        clearSelectedUsers() {
+            this.selectedUsers = [];
+            this.showAddMultiPersonDialog = false;
+        },
+
+        confirmDeleteUserPerm(index) {
+            this.deleteUserPermIndex = index;
+            this.showDeleteDialog = true;
+        },
+
+        performDeleteUserPerm() {
+            if (this.deleteUserPermIndex !== -1) {
+                this.userPermsList.splice(this.deleteUserPermIndex, 1);
+                this.refreshReadWriteApproval();
+            }
+            this.clearDeleteUserPerm();
+        },
+
+        clearDeleteUserPerm() {
+            this.deleteUserPermIndex = -1;
+            this.showDeleteDialog = false;
+        },
+
+        updateReadWritePerm(index, value) {
+            let read = false;
+            let write = false;
+
+            if (value === 'read') {
+                read = true;
+            } else if (value === 'write') {
+                read = true;
+                write = true;
+            }
+
+            this.userPermsList[index]['read'] = read;
+            this.userPermsList[index]['write'] = write;
+            this.refreshReadWriteApproval();
+        },
+
+        setAllPerms(permtype) {
+            if (this.listEmpty) {
+                return false;
+            }
+            let read = true;
+            let write = permtype === 'write';
+            this.userPermsList.every(item => {
+                item['read'] = read;
+                item['write'] = write;
+
+                return true;
+            });
+
+            this.refreshReadWriteApproval();
+        },
+
+        refreshReadWriteApproval() {
+            this.refreshReadApproval();
+            this.refreshWriteApproval();
+        },
+
+        refreshReadApproval() {
+            this.userPermsReadUsers = [];
+            for (const user_perm_obj of this.userPermsList) {
+                let readRight = user_perm_obj.write ? true : user_perm_obj.read;
+                this.userPermsReadUsers.push({
+                    'id': user_perm_obj.id,
+                    'read': readRight,
+                    'write': user_perm_obj.write,
+                    'expiry': user_perm_obj.expiry ? new Date(user_perm_obj.expiry).toISOString() : ''
+                });
+            }
+            this.$emit('updateReadApproval', this.readApproval);
+        },
+
+        refreshWriteApproval() {
+            this.userPermsWriteUsers = [];
+            for (const user_perm_obj of this.userPermsList) {
+                if (user_perm_obj.write) {
+                    this.userPermsWriteUsers.push({
+                        'id': user_perm_obj.id,
+                        'expiry': user_perm_obj.expiry ? new Date(user_perm_obj.expiry).toISOString() : ''
+                    });
+                }
+            }
+            this.$emit('updateWriteApproval', this.writeApproval);
+        }
+    },
+
+    watch: {
+        userPermsReadAll(newVal, oldVal) {
+            this.$emit('updateReadApproval', this.readApproval);
+            if (newVal === true) {
+                this.userPermsWriteAll = false;
+            }
+        },
+        userPermsWriteAll(newVal, oldVal) {
+            this.$emit('updateWriteApproval', this.writeApproval);
+            if (newVal === true) {
+                this.userPermsReadAll = false;
+            }
+        },
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/CoursewareContentShared.vue b/resources/vue/components/courseware/CoursewareContentShared.vue
new file mode 100644
index 00000000000..19594d00302
--- /dev/null
+++ b/resources/vue/components/courseware/CoursewareContentShared.vue
@@ -0,0 +1,173 @@
+<template>
+   <div>
+        <table class="default">
+            <caption>
+                <translate>Von mir geteilte Lerninhalte</translate>
+            </caption>
+            <thead>
+                <tr>
+                    <th><translate>Seite</translate></th>
+                    <th><translate>Lesen</translate></th>
+                    <th><translate>Lesen & Schreiben</translate></th>
+                    <th class="actions"><translate>Aktionen</translate></th>
+                </tr>
+            </thead>
+             <tbody>
+                <tr v-for="element in releasedElements" :key="element.id">
+                    <td>
+                        <a :href="getElementUrl(element)">
+                            {{ element.attributes.title }}
+                        </a>
+                    </td>
+                    <td>
+                        <span
+                            v-if="element.attributes['read-approval'].users.length > 0"
+                            role="checkbox"
+                            aria-checked="true"
+                            aria-disabled="true"
+                        >
+                            <studip-icon shape="accept" role="info" />
+                        </span>
+                    </td>
+                    <td>
+                        <span
+                            v-if="element.attributes['write-approval'].users.length > 0"
+                            role="checkbox"
+                            aria-checked="true"
+                            aria-disabled="true"
+                        >
+                            <studip-icon shape="accept" role="info" />
+                        </span>
+                    </td>
+                    <td class="actions">
+                        <studip-action-menu
+                            :items="menuItems"
+                            @editReleases="displayEditReleases(element)"
+                            @clearReleases="displayClearReleases(element)"
+                        />
+                    </td>
+                </tr>
+             </tbody>
+        </table>
+
+        <studip-dialog
+            v-if="showEditReleases"
+            :title="$gettext('Freigabe bearbeiten')"
+            :confirmText="$gettext('Speichern')"
+            confirmClass="accept"
+            :closeText="$gettext('Schließen')"
+            closeClass="cancel"
+            height="480"
+            width="720"
+            @confirm="storeReleases"
+            @close="closeEditReleases"
+        >
+            <template v-slot:dialogContent>
+                <courseware-content-permissions
+                    :element="selectedElement"
+                    @updateReadApproval="updateReadApproval"
+                    @updateWriteApproval="updateWriteApproval"
+                />
+            </template>
+        </studip-dialog>
+
+        <studip-dialog
+            v-if="showClearReleases"
+            :title="$gettext('Löschen der Freigabe')"
+            :question="$gettextInterpolate('Möchten Sie die Freigabe für %{ pageTitle} wirklich löschen?', {pageTitle: this.selectedElement.attributes.title})"
+            height="220"
+            @confirm="clearReleases"
+            @close="closeClearReleases"
+        ></studip-dialog>
+   </div>
+</template>
+
+<script>
+import CoursewareContentPermissions from './CoursewareContentPermissions.vue';
+import { mapActions, mapGetters } from 'vuex';
+import StudipActionMenu from './../StudipActionMenu.vue';
+import StudipDialog from '../StudipDialog.vue';
+import StudipIcon from '../StudipIcon.vue';
+
+export default {
+    name: 'courseware-content-shared',
+    components: {
+        CoursewareContentPermissions,
+        StudipActionMenu,
+        StudipDialog,
+        StudipIcon,
+    },
+    data() {
+        return {
+            menuItems: [
+                { id: 1, label: this.$gettext('Freigabe bearbeiten'), icon: 'edit', emit: 'editReleases' },
+                { id: 2, label: this.$gettext('Freigabe löschen'), icon: 'trash', emit: 'clearReleases' }
+            ],
+            showClearReleases: false,
+            showEditReleases: false,
+            selectedElement: null
+        }
+    },
+    computed: {
+        ...mapGetters({
+            releasedElements: 'courseware-structural-elements-released/all',
+        }),
+     },
+      methods: {
+        ...mapActions({
+            updateStructuralElement: 'updateStructuralElement',
+            lockObject: 'lockObject',
+            unlockObject: 'unlockObject',
+            relaodSharedElements: 'courseware-structural-elements-released/loadAll'
+        }),
+        getElementUrl(element) {
+            return STUDIP.URLHelper.base_url + 'dispatch.php/contents/courseware/courseware#/structural_element/' + element.id;
+        },
+        updateReadApproval(approval) {
+            this.selectedElement.attributes['read-approval'] = approval;
+        },
+        updateWriteApproval(approval) {
+            this.selectedElement.attributes['write-approval'] = approval;
+        },
+        displayEditReleases(element) {
+            this.selectedElement = element;
+            this.showEditReleases = true;
+        },
+        async storeReleases() {
+            const currentId = this.selectedElement.id;
+            await this.lockObject({ id: currentId, type: 'courseware-structural-elements' });
+            await this.updateStructuralElement({
+                element: this.selectedElement,
+                id: currentId,
+            });
+            await this.unlockObject({ id: currentId, type: 'courseware-structural-elements' });
+            this.closeEditReleases();
+        },
+        closeEditReleases() {
+            this.showEditReleases = false;
+            this.selectedElement = null;
+        },
+        displayClearReleases(element) {
+            this.selectedElement = element;
+            this.showClearReleases = true;
+        },
+        async clearReleases() {
+            const currentId = this.selectedElement.id;
+            this.selectedElement.attributes['read-approval'].users = [];
+            this.selectedElement.attributes['write-approval'].users = [];
+            await this.lockObject({ id: currentId, type: 'courseware-structural-elements' });
+            await this.updateStructuralElement({
+                element: this.selectedElement,
+                id: currentId,
+            });
+            await this.unlockObject({ id: currentId, type: 'courseware-structural-elements' });
+            this.closeClearReleases();
+            this.relaodSharedElements();
+        },
+        closeClearReleases() {
+            this.showClearReleases = false;
+            this.selectedElement = null;
+        },
+      },
+}
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/CoursewareStructuralElement.vue b/resources/vue/components/courseware/CoursewareStructuralElement.vue
index 55dd41637d8..bdd01965f3c 100644
--- a/resources/vue/components/courseware/CoursewareStructuralElement.vue
+++ b/resources/vue/components/courseware/CoursewareStructuralElement.vue
@@ -179,7 +179,7 @@
                     :closeText="textEdit.close"
                     closeClass="cancel"
                     height="500"
-                    width="500"
+                    :width="inContent ? '720' : '500'"
                     class="studip-dialog-with-tab"
                     @close="closeEditDialog"
                     @confirm="storeCurrentElement"
@@ -305,6 +305,12 @@
                                     @updateReadApproval="updateReadApproval"
                                     @updateWriteApproval="updateWriteApproval"
                                 />
+                                <courseware-content-permissions
+                                    v-if="inContent"
+                                    :element="currentElement"
+                                    @updateReadApproval="updateReadApproval"
+                                    @updateWriteApproval="updateWriteApproval"
+                                />
                             </courseware-tab>
                             <courseware-tab v-if="inCourse" :name="textEdit.visible" :index="4">
                                 <form class="default" @submit.prevent="">
@@ -599,6 +605,7 @@
 import ContainerComponents from './container-components.js';
 import CoursewarePluginComponents from './plugin-components.js';
 import CoursewareStructuralElementPermissions from './CoursewareStructuralElementPermissions.vue';
+import CoursewareContentPermissions from './CoursewareContentPermissions.vue';
 import CoursewareStructuralElementDiscussion from './CoursewareStructuralElementDiscussion.vue';
 import CoursewareAccordionContainer from './CoursewareAccordionContainer.vue';
 import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
@@ -623,6 +630,7 @@ export default {
     components: {
         CoursewareStructuralElementDiscussion,
         CoursewareStructuralElementPermissions,
+        CoursewareContentPermissions,
         CoursewareRibbon,
         CoursewareListContainer,
         CoursewareAccordionContainer,
@@ -758,6 +766,11 @@ export default {
             return this.$store.getters.context.type === 'courses';
         },
 
+        inContent() {
+            // The rights tab in contents will be only visible to the owner.
+            return this.$store.getters.context.type === 'users' && this.userId === this.currentElement.relationships.user.data.id;
+        },
+
         textDelete() {
             let textDelete = {};
             textDelete.title = this.$gettext('Seite unwiderruflich löschen');
@@ -790,6 +803,11 @@ export default {
                     valid = true;
                 }
             }
+            if (context.type === 'sharedusers') {
+                if (context.id === this.courseware.relationships.root.data.id) {
+                    valid = true;
+                }
+            }
 
             if (context.type === 'public') {
                 valid = true;
@@ -925,11 +943,11 @@ export default {
                 menu.push({ id: 3, label: this.$gettext('Seite hinzufügen'), icon: 'add', emit: 'addElement' });
             }
             if (this.context.type === 'users') {
-                menu.push({ id: 6, label: this.$gettext('Öffentlichen Link erzeugen'), icon: 'group', emit: 'linkElement' });
+                menu.push({ id: 7, label: this.$gettext('Öffentlichen Link erzeugen'), icon: 'group', emit: 'linkElement' });
             }
             if (!this.isRoot && this.canEdit && !this.isTask) {
                 menu.push({
-                    id: 9,
+                    id: 8,
                     label: this.$gettext('Seite löschen'),
                     icon: 'trash',
                     emit: 'deleteCurrentElement',
diff --git a/resources/vue/components/courseware/CoursewareTreeItem.vue b/resources/vue/components/courseware/CoursewareTreeItem.vue
index b372b17cc4a..4e359778a7e 100644
--- a/resources/vue/components/courseware/CoursewareTreeItem.vue
+++ b/resources/vue/components/courseware/CoursewareTreeItem.vue
@@ -112,6 +112,9 @@ export default {
             return writeApproval.all || writeApproval.groups.length > 0 || writeApproval.users.length > 0;
         },
         hasNoReadApproval() {
+            if (this.context.type === 'users') {
+                return false;
+            }
             const readApproval = this.element.attributes['read-approval'];
 
             if (Object.keys(readApproval).length === 0 || this.hasWriteApproval) {
diff --git a/resources/vue/courseware-content-overview-app.js b/resources/vue/courseware-content-overview-app.js
index e1f15ef2450..ccacce86174 100644
--- a/resources/vue/courseware-content-overview-app.js
+++ b/resources/vue/courseware-content-overview-app.js
@@ -37,6 +37,7 @@ const mountApp = (STUDIP, createApp, element) => {
                     'courseware-containers',
                     'courseware-instances',
                     'courseware-structural-elements',
+                    'courseware-structural-elements-shared',
                     'courseware-templates',
                     'courseware-user-data-fields',
                     'courseware-user-progresses',
@@ -84,6 +85,8 @@ const mountApp = (STUDIP, createApp, element) => {
         type: entry_type,
     });
 
+    store.dispatch('courseware-structural-elements-shared/loadAll', { options: { include: 'owner' } });
+
     const app = createApp({
         render: (h) => h(ContentOverviewApp),
         store
diff --git a/resources/vue/courseware-content-releases-app.js b/resources/vue/courseware-content-releases-app.js
index f99744b34a8..79d56cced2a 100644
--- a/resources/vue/courseware-content-releases-app.js
+++ b/resources/vue/courseware-content-releases-app.js
@@ -23,6 +23,7 @@ const mountApp = (STUDIP, createApp, element) => {
                     'courseware-containers',
                     'courseware-public-links',
                     'courseware-structural-elements',
+                    'courseware-structural-elements-released',
                     'file-refs',
                     'users',
                 ],
@@ -46,6 +47,7 @@ const mountApp = (STUDIP, createApp, element) => {
         }
     }
 
+    store.dispatch('setUserId', STUDIP.USER_ID);
     store.dispatch('coursewareContext', {
         id: entry_id,
         type: entry_type,
@@ -56,6 +58,7 @@ const mountApp = (STUDIP, createApp, element) => {
             include: 'structural-element',
         },
     });
+    store.dispatch('courseware-structural-elements-released/loadAll', {});
 
     const app = createApp({
         render: (h) => h(ContentReleasesApp),
@@ -67,4 +70,4 @@ const mountApp = (STUDIP, createApp, element) => {
     return app;
 }
 
-export default mountApp;
\ No newline at end of file
+export default mountApp;
diff --git a/resources/vue/store/courseware/courseware.module.js b/resources/vue/store/courseware/courseware.module.js
index f8387604d60..0c4811e28c3 100644
--- a/resources/vue/store/courseware/courseware.module.js
+++ b/resources/vue/store/courseware/courseware.module.js
@@ -50,7 +50,9 @@ const getDefaultState = () => {
         exportState: '',
         exportProgress: 0,
 
+        permissionFilter: 'read',
         purposeFilter: 'all',
+        sourceFilter: 'all',
         showOverviewElementAddDialog: false,
 
         bookmarkFilter: 'all',
@@ -201,9 +203,15 @@ const getters = {
     exportProgress(state) {
         return state.exportProgress;
     },
+    permissionFilter(state) {
+        return state.permissionFilter;
+    },
     purposeFilter(state) {
         return state.purposeFilter;
     },
+    sourceFilter(state) {
+        return state.sourceFilter;
+    },
     bookmarkFilter(state) {
         return state.bookmarkFilter;
     },
@@ -1226,9 +1234,15 @@ export const actions = {
         await dispatch('courseware-task-feedback/delete', data, { root: true });
     },
 
+    setPermissionFilter({ commit }, permission) {
+        commit('setPermissionFilter', permission);
+    },
     setPurposeFilter({ commit }, purpose) {
         commit('setPurposeFilter', purpose);
     },
+    setSourceFilter({ commit }, source) {
+        commit('setSourceFilter', source);
+    },
     setBookmarkFilter({ commit }, course) {
         commit('setBookmarkFilter', course);
     },
@@ -1413,9 +1427,15 @@ export const mutations = {
     setExportProgress(state, exportProgress) {
         state.exportProgress = exportProgress;
     },
+    setPermissionFilter(state, permission) {
+        state.permissionFilter = permission;
+    },
     setPurposeFilter(state, purpose) {
         state.purposeFilter = purpose;
     },
+    setSourceFilter(state, source) {
+        state.sourceFilter = source;
+    },
     setBookmarkFilter(state, course) {
         state.bookmarkFilter = course;
     },
-- 
GitLab