Skip to content

Commit

Permalink
pkp#10571 WIP: Add checks to limit email template access by usergroups
Browse files Browse the repository at this point in the history
  • Loading branch information
taslangraham committed Nov 25, 2024
1 parent 43eced2 commit 45a86ac
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 27 deletions.
8 changes: 7 additions & 1 deletion api/v1/emailTemplates/PKPEmailTemplateController.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ public function getMany(Request $illuminateRequest): JsonResponse

Hook::call('API::emailTemplates::params', [$collector, $illuminateRequest]);

$emailTemplates = $collector->getMany();
$emailTemplates = Repo::emailTemplate()->filterTemplatesByUserAccess($collector->getMany()->all(), $request->getUser(), $request->getContext()->getId());

return response()->json([
'itemsMax' => $collector->getCount(),
Expand All @@ -176,6 +176,12 @@ public function get(Request $illuminateRequest): JsonResponse
], Response::HTTP_NOT_FOUND);
}

if (!Repo::emailTemplate()->isTemplateAccessibleToUser($request->getUser(), $emailTemplate, $request->getContext()->getId())) {
return response()->json([
'error' => __('api.emailTemplates.404.templateNotFound')
], Response::HTTP_NOT_FOUND);
}

return response()->json(Repo::emailTemplate()->getSchemaMap()->map($emailTemplate), Response::HTTP_OK);
}

Expand Down
9 changes: 7 additions & 2 deletions classes/decision/steps/Email.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,16 +128,21 @@ protected function getEmailTemplates(): array
$emailTemplates = collect();
if ($this->mailable::getEmailTemplateKey()) {
$emailTemplate = Repo::emailTemplate()->getByKey($context->getId(), $this->mailable::getEmailTemplateKey());
if ($emailTemplate) {
if (Repo::emailTemplate()->isTemplateAccessibleToUser($request->getUser(), $emailTemplate, $context->getId())) {
$emailTemplates->add($emailTemplate);
}
Repo::emailTemplate()
->getCollector($context->getId())
->alternateTo([$this->mailable::getEmailTemplateKey()])
->getMany()
->each(fn (EmailTemplate $e) => $emailTemplates->add($e));
->each(function (EmailTemplate $e) use ($context, $request, $emailTemplates) {
if (Repo::emailTemplate()->isTemplateAccessibleToUser($request->getUser(), $e, $context->getId())) {
$emailTemplates->add($e);
}
});
}


return Repo::emailTemplate()->getSchemaMap()->mapMany($emailTemplates)->toArray();
}

Expand Down
49 changes: 49 additions & 0 deletions classes/emailTemplate/Repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@

use APP\emailTemplate\DAO;
use APP\facades\Repo;
use Illuminate\Support\Facades\DB;
use PKP\context\Context;
use PKP\core\PKPRequest;
use PKP\plugins\Hook;
use PKP\security\Role;
use PKP\services\PKPSchemaService;
use PKP\user\User;
use PKP\validation\ValidatorFactory;

class Repository
Expand Down Expand Up @@ -208,4 +211,50 @@ public function restoreDefaults($contextId): array
Hook::call('EmailTemplate::restoreDefaults', [&$deletedKeys, $contextId]);
return $deletedKeys;
}


public function getGroupsAssignedToTemplate(string $key, int $contextId): array
{
// FIXME - can this be replaced with eloquent?
return DB::table('email_template_role_access')
->where('email_key', $key)
->where('context_id', $contextId)
->pluck('user_group_id')
->toArray();
}


public function isTemplateAccessibleToUser(User $user, EmailTemplate $template, int $contextId): bool
{
if ($user->hasRole([Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER,], $contextId)) {
return true;
}

$userUserGroups = Repo::userGroup()->userUserGroups($user->getId(), $contextId)->all();
$templateUserGroups = $this->getGroupsAssignedToTemplate($template->getData('key'), $contextId);
$userHasAccess = false;

foreach ($userUserGroups as $userGroup) {
if (in_array($userGroup->getId(), $templateUserGroups) || $template->getData('isUnrestricted')) {
$userHasAccess = true;
break;
}
}

return $userHasAccess;
}

/**
* Filters a list of EmailTemplates to return only those accessible by a specified user.
*
* @param array $templates List of EmailTemplate objects to filter.
* @param User $user The user whose access level is used for filtering.
*
* @return \Illuminate\Support\Collection Filtered list of EmailTemplate objects accessible to the user.
*/
public function filterTemplatesByUserAccess(array $templates, User $user, int $contextId): \Illuminate\Support\Collection
{
return collect(array_filter($templates, fn (EmailTemplate $template) => $this->isTemplateAccessibleToUser($user, $template, $contextId)));
}

}
55 changes: 49 additions & 6 deletions classes/emailTemplate/maps/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

namespace PKP\emailTemplate\maps;

use APP\core\Application;
use APP\facades\Repo;
use Illuminate\Support\Enumerable;
use PKP\core\PKPApplication;
use PKP\emailTemplate\EmailTemplate;
Expand Down Expand Up @@ -40,10 +42,12 @@ public function map(EmailTemplate $item): array
* Summarize an email template
*
* Includes properties with the apiSummary flag in the email template schema.
*
* @param null|mixed $mailableClass
*/
public function summarize(EmailTemplate $item): array
public function summarize(EmailTemplate $item, $mailableClass = null): array
{
return $this->mapByProperties($this->getSummaryProps(), $item);
return $this->mapByProperties($this->getSummaryProps(), $item, $mailableClass);
}

/**
Expand All @@ -64,20 +68,59 @@ public function mapMany(Enumerable $collection): Enumerable
*
* @see self::summarize
*/
public function summarizeMany(Enumerable $collection): Enumerable
public function summarizeMany(Enumerable $collection, string $mailableClass = null): Enumerable
{
$this->collection = $collection;
return $collection->map(function ($item) {
return $this->summarize($item);
return $collection->map(function ($item) use ($mailableClass) {
return $this->summarize($item, $mailableClass);
});
}

/**
* Map schema properties of an Announcement to an assoc array
*/
protected function mapByProperties(array $props, EmailTemplate $item): array
protected function mapByProperties(array $props, EmailTemplate $item, string $mailableClass = null): array
{
$output = [];

$mailableClass = $mailableClass ?? Repo::mailable()->getMailableByEmailTemplate($item);

if(!$mailableClass) {
error_log('TEMPLATE NAME ' . $item->getData('key'));
error_log('TEMPLATE ALTERNATE TO ' . $item->getData('alternateTo') ?? '');
}


// some mailable are not found during some operations such as performing a search for templates. So ensure mailable exist before using
if($mailableClass) {
$isUserGroupsAssignable = Repo::mailable()->isGroupsAssignableToTemplates($mailableClass);

if ($isUserGroupsAssignable) {
$output['assignableUserGroups'] = [];
$output['assignedUserGroupIds'] = [];
} else {
// get roles for mailable
$roles = $mailableClass::getFromRoleIds();
// Get the groups for each role
$userGroups = [];
$roleNames = Application::get()->getRoleNames();

foreach (Repo::userGroup()->getByRoleIds($roles, $this->context->getId())->all() as $group) {
$roleId = $group->getRoleId();
$userGroups[] = [
'id' => $group->getId(),
'name' => $group->getLocalizedName(),
'roleId' => $roleId,
'roleName' => $roleNames[$roleId]];
}

$output['assignableUserGroups'] = $userGroups;
// Get the current user groups assigned to the template
$output['assignedUserGroupIds'] = Repo::emailTemplate()->getGroupsAssignedToTemplate($item->getData('key'), Application::get()->getRequest()->getContext()->getId());
}
}


foreach ($props as $prop) {
switch ($prop) {
case '_href':
Expand Down
48 changes: 39 additions & 9 deletions classes/mail/Repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use PKP\context\Context;
use PKP\emailTemplate\EmailTemplate;
use PKP\mail\mailables\DecisionNotifyOtherAuthors;
use PKP\mail\mailables\EditReviewNotify;
use PKP\mail\mailables\ReviewCompleteNotifyEditors;
Expand Down Expand Up @@ -116,12 +117,13 @@ public function summarizeMailable(string $class): array
'name' => $class::getName(),
'supportsTemplates' => $class::getSupportsTemplates(),
'toRoleIds' => $class::getToRoleIds(),
'canAssignUserGroupToTemplates' => $this->isGroupsAssignableToTemplates($class),
];
}

/**
* Get a full description of a mailable's properties, including any
* assigned email templates
* assigned email templates that are accessible to user
*/
public function describeMailable(string $class, int $contextId): array
{
Expand All @@ -137,16 +139,14 @@ public function describeMailable(string $class, int $contextId): array

$defaultTemplate = Repo::emailTemplate()->getByKey($contextId, $class::getEmailTemplateKey());

$request = Application::get()->getRequest();
$user = $request->getUser();

// Limit templates to only those accessible to the user's user group(s)
$userAccessibleTemplates = Repo::emailTemplate()->filterTemplatesByUserAccess(array_merge([$defaultTemplate], $templates->values()->toArray()), $user, $contextId);
$data['emailTemplates'] = Repo::emailTemplate()
->getSchemaMap()
->summarizeMany(
collect(
array_merge(
[$defaultTemplate],
$templates->values()->toArray()
)
)
)
->summarizeMany($userAccessibleTemplates, $class)
->values();
}

Expand Down Expand Up @@ -206,6 +206,21 @@ protected function isMailableConfigurable(string $class, Context $context): bool
return true;
}


// Check if the templates of a given mailable can be assigned to specific groups
/**
* @param Mailable|string $mailable - Mailable class or qualified string referencing the class
*/
public function isGroupsAssignableToTemplates(Mailable|string $mailable): bool
{
return !empty(array_intersect($mailable::getGroupIds(), [
Mailable::GROUP_SUBMISSION,
Mailable::GROUP_REVIEW,
Mailable::GROUP_COPYEDITING,
Mailable::GROUP_PRODUCTION,
]));
}

/**
* Get the mailables used in this app
*/
Expand Down Expand Up @@ -265,4 +280,19 @@ public function map(): Collection
mailables\ValidateEmailSite::class,
]);
}

/**
* Gets the mailable for a given email template
*
* @param EmailTemplate $template
*
* Note: This does not discover/find mailbles defined within plugins
*
* @return string|null - Fully Qualified Class Name of a mailable
*/
public function getMailableByEmailTemplate(EmailTemplate $template): ?string
{
$emailKey = $template->getData('alternateTo') ?? $template->getData('key');
return $this->map()->first(fn ($class) => $class::getEmailTemplateKey() === $emailKey);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace PKP\migration\upgrade\v3_5_0;

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use PKP\migration\Migration;

class I10403_EmailTemplateRoleAccess extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{

Schema::create('email_template_role_access', function (Blueprint $table) {
$table->bigInteger('email_template_role_access_id')->autoIncrement();
$table->string('email_key', 255);
$table->bigInteger('context_id');
$table->bigInteger('user_group_id');

$table->foreign('context_id')->references('journal_id')->on('journals')->onDelete('cascade');
$table->foreign('user_group_id')->references('user_group_id')->on('user_groups')->onDelete('cascade');
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::drop('email_template_role_access');
}
}
13 changes: 10 additions & 3 deletions controllers/grid/queries/form/QueryForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public function __construct($request, $assocType, $assocId, $stageId, $queryId =
]);

Note::create([
'userId' => $request->getUser()->getId(),
'userId' => $request->getUser()->getId(),
'assocType' => Application::ASSOC_TYPE_QUERY,
'assocId' => $query->id,
]);
Expand Down Expand Up @@ -273,18 +273,25 @@ public function fetch($request, $template = null, $display = false, $actionArgs
$mailable = $this->getStageMailable($context, $submission);
$data = $mailable->getData();
$defaultTemplate = Repo::emailTemplate()->getByKey($context->getId(), $mailable::getEmailTemplateKey());
$templateKeySubjectPairs = [$mailable::getEmailTemplateKey() => $defaultTemplate->getLocalizedData('name')];

// check to ensure user's user group has access to the templates for the mailable
if(Repo::emailTemplate()->isTemplateAccessibleToUser($user, $defaultTemplate, $context->getId())) {
$templateKeySubjectPairs[$mailable::getEmailTemplateKey()] = $defaultTemplate->getLocalizedData('name');
}

$alternateTemplates = Repo::emailTemplate()->getCollector($context->getId())
->alternateTo([$mailable::getEmailTemplateKey()])
->getMany();
foreach ($alternateTemplates as $alternateTemplate) {

foreach (Repo::emailTemplate()->filterTemplatesByUserAccess($alternateTemplates->all(), $user, $context->getId()) as $alternateTemplate) {
$templateKeySubjectPairs[$alternateTemplate->getData('key')] = Mail::compileParams(
$alternateTemplate->getLocalizedData('name'),
$data
);
}
}


$templateMgr->assign('templates', $templateKeySubjectPairs);

// Get currently selected participants in the query
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,22 @@ public function fetch($request, $template = null, $display = false)
$mailable = $this->getStageMailable($context, $submission);
$data = $mailable->getData();
$defaultTemplate = Repo::emailTemplate()->getByKey($context->getId(), $mailable::getEmailTemplateKey());
$templates = [$mailable::getEmailTemplateKey() => $defaultTemplate->getLocalizedData('name')];

$templates = [];
if (Repo::emailTemplate()->isTemplateAccessibleToUser($user, $defaultTemplate, $context->getId())) {
$templates[$mailable::getEmailTemplateKey()] = $defaultTemplate->getLocalizedData('name');
}
$alternateTemplates = Repo::emailTemplate()->getCollector($context->getId())
->alternateTo([$mailable::getEmailTemplateKey()])
->getMany();

foreach ($alternateTemplates as $alternateTemplate) {
$templates[$alternateTemplate->getData('key')] = Mail::compileParams(
$alternateTemplate->getLocalizedData('name'),
$data
);
if (Repo::emailTemplate()->isTemplateAccessibleToUser($user, $alternateTemplate, $context->getId())) {
$templates[$alternateTemplate->getData('key')] = Mail::compileParams(
$alternateTemplate->getLocalizedData('name'),
$data
);
}
}
}

Expand Down Expand Up @@ -212,7 +219,7 @@ public function sendMessage(int $userId, Submission $submission, Request $reques

// Create a head note
$headNote = Note::create([
'userId' => $request->getUser()->getId(),
'userId' => $request->getUser()->getId(),
'assocType' => PKPApplication::ASSOC_TYPE_QUERY,
'assocId' => $query->id,
'title' => Mail::compileParams(
Expand Down

0 comments on commit 45a86ac

Please sign in to comment.