Skip to content

Commit

Permalink
Creating a configuration template for extensions and corresponding cl…
Browse files Browse the repository at this point in the history
…asses for config loading and extension management.
  • Loading branch information
krulis-martin committed Dec 17, 2024
1 parent 45fb957 commit 3c490a5
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 3 deletions.
14 changes: 14 additions & 0 deletions app/config/config.local.neon.example
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,20 @@ parameters:
# Please note that the length of the auth. token expiration should be considered (readonly tokens may expire after 1 year).
threshold: "2 years"

extensions: # 3rd party tools which are linked from UI and can cooperate with ReCodEx
- id: "ext-identifier"
caption: # to be displayed in UI; could be also single string (for all localizations)
cs: "Český popisek"
en: "English Caption"
url: "https://extetrnal.domain.com/recodex/extension?token={token}&locale={locale}" # '{token}' and '{locale}' are placeholders
token: # generated from tmp tokens passed via URL so the ext. tool can access ReCodEx API
scope: master # scope of generated tokens (to be used by the extension)
user: null # user override (ID) for generating tokens (if null, the token will be generated for logged-in user)
instances: [] # array of instances where this extension is enabled (empty array = all)
user: # filters applied to determine, whether logged-in user can access the extension
roles: [] # array of enabled user roles (empty array = all)
externalLogins: [] # list of external_login.auth_service IDs (at least one is required, empty array = nothing is required)


# The most important part - a database system connection
nettrine.dbal:
Expand Down
3 changes: 3 additions & 0 deletions app/config/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ parameters:
roles: # restrict the cleanup to the following roles
- "student"

extensions: []


application:
errorPresenter: V1:ApiError
Expand Down Expand Up @@ -389,6 +391,7 @@ services:
- App\Helpers\ExerciseConfig\Compilation\DirectoriesResolver
- App\Helpers\ExerciseConfig\Helper
- App\Helpers\ExerciseConfig\PipelinesCache
- App\Helpers\Extensions(%extensions%)
- App\Helpers\SisHelper(%sis.apiBase%, %sis.faculty%, %sis.secret%)
- App\Helpers\UserActions
- App\Helpers\ExerciseConfig\ExerciseConfigChecker
Expand Down
1 change: 0 additions & 1 deletion app/config/permissions.neon
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,6 @@ permissions:
- setIsAllowed
- createLocalAccount
- invalidateTokens
- delete

- allow: true
role: student
Expand Down
34 changes: 34 additions & 0 deletions app/exceptions/ConfigException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace App\Exceptions;

use Exception;
use Nette\Http\IResponse;

/**
* Exception concerning core module confugration.
*/
class ConfigException extends ApiException
{
/**
* Creates instance with further description.
* @param string $msg description
* @param Exception|null $previous
* @param string $frontendErrorCode
* @param array|null $frontendErrorParams
*/
public function __construct(
$msg,
$previous = null,
string $frontendErrorCode = FrontendErrorMappings::E500_000__INTERNAL_SERVER_ERROR,
$frontendErrorParams = null
) {
parent::__construct(
"Internal configuration error - $msg",
IResponse::S500_InternalServerError,
$frontendErrorCode,
$frontendErrorParams,
$previous
);
}
}
141 changes: 141 additions & 0 deletions app/helpers/Extensions/ExtensionConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php

namespace App\Helpers;

use App\Model\Entity\Instance;
use App\Model\Entity\User;
use App\Exceptions\ConfigException;
use Nette;
use Nette\Utils\Arrays;

/**
* This holds a configuration and help handle tokens for a single extension.
*/
class ExtensionConfig
{
use Nette\SmartObject;

/**
* Internal identifier.
*/
private string $id;

/**
* Caption as a string or localized strings (array locale => caption).
* @var string|string[]
*/
private string|array $caption;

/**
* URL template for the external service. The template may hold the following placeholders:
* - {token} - will be replaced with URL-encoded temporary token
* - {locale} - will be replaced with a language identifier ('en', 'cs', ...) based on currently selected language
*/
private string $url;

/**
* A scope that will be set to (full) access tokens generated after tmp-token verification.
*/
private string $tokenScope;

Check failure on line 39 in app/helpers/Extensions/ExtensionConfig.php

View workflow job for this annotation

GitHub Actions / phpstan (8.2)

Property App\Helpers\ExtensionConfig::$tokenScope is never read, only written.

Check failure on line 39 in app/helpers/Extensions/ExtensionConfig.php

View workflow job for this annotation

GitHub Actions / phpstan (8.3)

Property App\Helpers\ExtensionConfig::$tokenScope is never read, only written.

/**
* User override for (full) access tokens. This user will be used instead of user ID passed in tmp token.
* This is a way how to safely provide more powerful full tokens (without compromising tmp tokens).
* If null, the (logged in) user from tmp token is passed to the full token.
*/
private string|null $tokenUserId = null;

Check failure on line 46 in app/helpers/Extensions/ExtensionConfig.php

View workflow job for this annotation

GitHub Actions / phpstan (8.2)

Property App\Helpers\ExtensionConfig::$tokenUserId is never read, only written.

Check failure on line 46 in app/helpers/Extensions/ExtensionConfig.php

View workflow job for this annotation

GitHub Actions / phpstan (8.3)

Property App\Helpers\ExtensionConfig::$tokenUserId is never read, only written.

/**
* List of instances in which the extension should appear.
* Empty list = all instances.
* @var string[]
*/
private array $instances = [];

/**
* List of user roles for which this extensions should appear.
* Empty list = all roles.
* @var string[]
*/
private array $userRoles = [];

/**
* List of eligible user external login types. A user must hava at least one of these logins to see the extension.
* Empty list = no external logins are required.
*/
private array $userExternalLogins = [];

public function __construct(array $config)
{
$this->id = (string)Arrays::get($config, "id");

$this->caption = Arrays::get($config, "caption");
if (is_array($this->caption)) {
foreach ($this->caption as $locale => $caption) {
if (!is_string($locale) || !is_string($caption)) {
throw new ConfigException("Invalid extension caption format.");
}
}
}

$this->url = Arrays::get($config, "url");
$this->tokenScope = Arrays::get($config, ["token", "scope"], "master");
$this->tokenUserId = Arrays::get($config, ["token", "user"], null);
$this->instances = Arrays::get($config, "instances", []);
$this->userRoles = Arrays::get($config, ["user", "roles"], []);
$this->userExternalLogins = Arrays::get($config, ["user", "externalLogins"], []);
}

public function getId(): string
{
return $this->id;
}

public function getCaption(): string|array
{
return $this->caption;
}

/**
* Get formatted URL. A template is injected a token and current locale.
* @param string $token already serialized JWT
* @param string $locale language identification ('en', 'cs', ...)
* @return string an instantiated URL template
*/
public function getUrl(string $token, string $locale): string
{
$url = $this->url;
$url = str_replace('{token}', urlencode($token), $url);
$url = str_replace('{locale}', urlencode($locale), $url);
return $url;
}

/**
* Check whether this extension is accessible by given user in given instance.
* @param Instance $instance
* @param User $user
* @return bool true if the extension is accessible
*/
public function isAccessible(Instance $instance, User $user): bool
{
if ($this->instances && !in_array($instance->getId(), $this->instances)) {
return false;
}

if ($this->userRoles && !in_array($user->getRole(), $this->userRoles)) {
return false;
}

if ($this->userExternalLogins) {
$logins = $user->getConsolidatedExternalLogins();
foreach ($this->userExternalLogins as $service) {
if (array_key_exists($service, $logins)) {
return true;
}
}
return false;
}

return true;
}
}
56 changes: 56 additions & 0 deletions app/helpers/Extensions/Extensions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace App\Helpers;

use App\Model\Entity\Instance;
use App\Model\Entity\User;
use Nette;

/**
* Configuration and related management of ReCodEx extensions. An extension is a 3rd party webapp that can be used
* to cooperate with ReCodEx (e.g., for user-membership management based on external university system).
* An extension has a URL which is injected with tmp token (holding the ID of currently logged user).
* The tmp token can be used by the extension to fetch a full token which can be used to access the API on behalf
* of the logged in user.
*/
class Extensions
{
use Nette\SmartObject;

protected array $extensions = [];

public function __construct(array $extensions)
{
foreach ($extensions as $config) {
$extension = new ExtensionConfig($config);
$this->extensions[$extension->getId()] = $extension;
}
}

/**
* Retrieve the extension by its ID.
* @param string $id
* @return ExtensionConfig|null null is returned if no such extension exists
*/
public function getExtension(string $id): ?ExtensionConfig
{
return $this->extensions[$id] ?? null;
}

/**
* Filter out extensions that are accessible by given user in given instance.
* @param Instance $instance
* @param User $user
* @return ExtensionConfig[] array indexed by extension IDs
*/
public function getAccessibleExtensions(Instance $instance, User $user): array
{
$res = [];
foreach ($this->extensions as $id => $extension) {
if ($extension->isAccessible($instance, $user)) {
$res[$id] = $extension;
}
}
return $res;
}
}
1 change: 0 additions & 1 deletion app/model/entity/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
use Gravatar\Gravatar;
use App\Exceptions\ApiException;
use InvalidArgumentException;
use DateTime;
use DateTimeInterface;
Expand Down
1 change: 0 additions & 1 deletion app/model/view/InstanceViewFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
*/
class InstanceViewFactory
{

/** @var GroupViewFactory */
private $groupViewFactory;

Expand Down

0 comments on commit 3c490a5

Please sign in to comment.