Skip to content

Commit 3c490a5

Browse files
committed
Creating a configuration template for extensions and corresponding classes for config loading and extension management.
1 parent 45fb957 commit 3c490a5

File tree

8 files changed

+248
-3
lines changed

8 files changed

+248
-3
lines changed

app/config/config.local.neon.example

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,20 @@ parameters:
7575
# Please note that the length of the auth. token expiration should be considered (readonly tokens may expire after 1 year).
7676
threshold: "2 years"
7777

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

7993
# The most important part - a database system connection
8094
nettrine.dbal:

app/config/config.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ parameters:
205205
roles: # restrict the cleanup to the following roles
206206
- "student"
207207

208+
extensions: []
209+
208210

209211
application:
210212
errorPresenter: V1:ApiError
@@ -389,6 +391,7 @@ services:
389391
- App\Helpers\ExerciseConfig\Compilation\DirectoriesResolver
390392
- App\Helpers\ExerciseConfig\Helper
391393
- App\Helpers\ExerciseConfig\PipelinesCache
394+
- App\Helpers\Extensions(%extensions%)
392395
- App\Helpers\SisHelper(%sis.apiBase%, %sis.faculty%, %sis.secret%)
393396
- App\Helpers\UserActions
394397
- App\Helpers\ExerciseConfig\ExerciseConfigChecker

app/config/permissions.neon

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,6 @@ permissions:
402402
- setIsAllowed
403403
- createLocalAccount
404404
- invalidateTokens
405-
- delete
406405

407406
- allow: true
408407
role: student

app/exceptions/ConfigException.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace App\Exceptions;
4+
5+
use Exception;
6+
use Nette\Http\IResponse;
7+
8+
/**
9+
* Exception concerning core module confugration.
10+
*/
11+
class ConfigException extends ApiException
12+
{
13+
/**
14+
* Creates instance with further description.
15+
* @param string $msg description
16+
* @param Exception|null $previous
17+
* @param string $frontendErrorCode
18+
* @param array|null $frontendErrorParams
19+
*/
20+
public function __construct(
21+
$msg,
22+
$previous = null,
23+
string $frontendErrorCode = FrontendErrorMappings::E500_000__INTERNAL_SERVER_ERROR,
24+
$frontendErrorParams = null
25+
) {
26+
parent::__construct(
27+
"Internal configuration error - $msg",
28+
IResponse::S500_InternalServerError,
29+
$frontendErrorCode,
30+
$frontendErrorParams,
31+
$previous
32+
);
33+
}
34+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<?php
2+
3+
namespace App\Helpers;
4+
5+
use App\Model\Entity\Instance;
6+
use App\Model\Entity\User;
7+
use App\Exceptions\ConfigException;
8+
use Nette;
9+
use Nette\Utils\Arrays;
10+
11+
/**
12+
* This holds a configuration and help handle tokens for a single extension.
13+
*/
14+
class ExtensionConfig
15+
{
16+
use Nette\SmartObject;
17+
18+
/**
19+
* Internal identifier.
20+
*/
21+
private string $id;
22+
23+
/**
24+
* Caption as a string or localized strings (array locale => caption).
25+
* @var string|string[]
26+
*/
27+
private string|array $caption;
28+
29+
/**
30+
* URL template for the external service. The template may hold the following placeholders:
31+
* - {token} - will be replaced with URL-encoded temporary token
32+
* - {locale} - will be replaced with a language identifier ('en', 'cs', ...) based on currently selected language
33+
*/
34+
private string $url;
35+
36+
/**
37+
* A scope that will be set to (full) access tokens generated after tmp-token verification.
38+
*/
39+
private string $tokenScope;
40+
41+
/**
42+
* User override for (full) access tokens. This user will be used instead of user ID passed in tmp token.
43+
* This is a way how to safely provide more powerful full tokens (without compromising tmp tokens).
44+
* If null, the (logged in) user from tmp token is passed to the full token.
45+
*/
46+
private string|null $tokenUserId = null;
47+
48+
/**
49+
* List of instances in which the extension should appear.
50+
* Empty list = all instances.
51+
* @var string[]
52+
*/
53+
private array $instances = [];
54+
55+
/**
56+
* List of user roles for which this extensions should appear.
57+
* Empty list = all roles.
58+
* @var string[]
59+
*/
60+
private array $userRoles = [];
61+
62+
/**
63+
* List of eligible user external login types. A user must hava at least one of these logins to see the extension.
64+
* Empty list = no external logins are required.
65+
*/
66+
private array $userExternalLogins = [];
67+
68+
public function __construct(array $config)
69+
{
70+
$this->id = (string)Arrays::get($config, "id");
71+
72+
$this->caption = Arrays::get($config, "caption");
73+
if (is_array($this->caption)) {
74+
foreach ($this->caption as $locale => $caption) {
75+
if (!is_string($locale) || !is_string($caption)) {
76+
throw new ConfigException("Invalid extension caption format.");
77+
}
78+
}
79+
}
80+
81+
$this->url = Arrays::get($config, "url");
82+
$this->tokenScope = Arrays::get($config, ["token", "scope"], "master");
83+
$this->tokenUserId = Arrays::get($config, ["token", "user"], null);
84+
$this->instances = Arrays::get($config, "instances", []);
85+
$this->userRoles = Arrays::get($config, ["user", "roles"], []);
86+
$this->userExternalLogins = Arrays::get($config, ["user", "externalLogins"], []);
87+
}
88+
89+
public function getId(): string
90+
{
91+
return $this->id;
92+
}
93+
94+
public function getCaption(): string|array
95+
{
96+
return $this->caption;
97+
}
98+
99+
/**
100+
* Get formatted URL. A template is injected a token and current locale.
101+
* @param string $token already serialized JWT
102+
* @param string $locale language identification ('en', 'cs', ...)
103+
* @return string an instantiated URL template
104+
*/
105+
public function getUrl(string $token, string $locale): string
106+
{
107+
$url = $this->url;
108+
$url = str_replace('{token}', urlencode($token), $url);
109+
$url = str_replace('{locale}', urlencode($locale), $url);
110+
return $url;
111+
}
112+
113+
/**
114+
* Check whether this extension is accessible by given user in given instance.
115+
* @param Instance $instance
116+
* @param User $user
117+
* @return bool true if the extension is accessible
118+
*/
119+
public function isAccessible(Instance $instance, User $user): bool
120+
{
121+
if ($this->instances && !in_array($instance->getId(), $this->instances)) {
122+
return false;
123+
}
124+
125+
if ($this->userRoles && !in_array($user->getRole(), $this->userRoles)) {
126+
return false;
127+
}
128+
129+
if ($this->userExternalLogins) {
130+
$logins = $user->getConsolidatedExternalLogins();
131+
foreach ($this->userExternalLogins as $service) {
132+
if (array_key_exists($service, $logins)) {
133+
return true;
134+
}
135+
}
136+
return false;
137+
}
138+
139+
return true;
140+
}
141+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
namespace App\Helpers;
4+
5+
use App\Model\Entity\Instance;
6+
use App\Model\Entity\User;
7+
use Nette;
8+
9+
/**
10+
* Configuration and related management of ReCodEx extensions. An extension is a 3rd party webapp that can be used
11+
* to cooperate with ReCodEx (e.g., for user-membership management based on external university system).
12+
* An extension has a URL which is injected with tmp token (holding the ID of currently logged user).
13+
* The tmp token can be used by the extension to fetch a full token which can be used to access the API on behalf
14+
* of the logged in user.
15+
*/
16+
class Extensions
17+
{
18+
use Nette\SmartObject;
19+
20+
protected array $extensions = [];
21+
22+
public function __construct(array $extensions)
23+
{
24+
foreach ($extensions as $config) {
25+
$extension = new ExtensionConfig($config);
26+
$this->extensions[$extension->getId()] = $extension;
27+
}
28+
}
29+
30+
/**
31+
* Retrieve the extension by its ID.
32+
* @param string $id
33+
* @return ExtensionConfig|null null is returned if no such extension exists
34+
*/
35+
public function getExtension(string $id): ?ExtensionConfig
36+
{
37+
return $this->extensions[$id] ?? null;
38+
}
39+
40+
/**
41+
* Filter out extensions that are accessible by given user in given instance.
42+
* @param Instance $instance
43+
* @param User $user
44+
* @return ExtensionConfig[] array indexed by extension IDs
45+
*/
46+
public function getAccessibleExtensions(Instance $instance, User $user): array
47+
{
48+
$res = [];
49+
foreach ($this->extensions as $id => $extension) {
50+
if ($extension->isAccessible($instance, $user)) {
51+
$res[$id] = $extension;
52+
}
53+
}
54+
return $res;
55+
}
56+
}

app/model/entity/User.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
use Doctrine\Common\Collections\ArrayCollection;
1010
use Doctrine\Common\Collections\Criteria;
1111
use Gravatar\Gravatar;
12-
use App\Exceptions\ApiException;
1312
use InvalidArgumentException;
1413
use DateTime;
1514
use DateTimeInterface;

app/model/view/InstanceViewFactory.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
*/
1313
class InstanceViewFactory
1414
{
15-
1615
/** @var GroupViewFactory */
1716
private $groupViewFactory;
1817

0 commit comments

Comments
 (0)