Skip to content

Commit d801de2

Browse files
authored
Enable testing entity collection in admin area (#336)
1 parent 06bebe8 commit d801de2

15 files changed

Lines changed: 422 additions & 19 deletions

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"psr/container": "^2.0",
3232
"psr/log": "^3",
3333
"simplesamlphp/composer-module-installer": "^1.3",
34-
"simplesamlphp/openid": "~v0.1.1",
34+
"simplesamlphp/openid": "~0.2.0",
3535
"spomky-labs/base64url": "^2.0",
3636
"symfony/expression-language": "^7.4",
3737
"symfony/psr-http-message-bridge": "^7.4",

public/assets/css/src/default.css

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,31 @@ table.client-table {
104104
font-weight: bolder;
105105
}
106106

107-
.confirm-action {}
107+
108108

109109
form.pure-form-stacked .full-width {
110110
width: 100%;
111111
}
112+
113+
/* Form loading state */
114+
.form-loading-spinner {
115+
display: inline-block;
116+
width: 0.85em;
117+
height: 0.85em;
118+
border: 2px solid currentColor;
119+
border-top-color: transparent;
120+
border-radius: 50%;
121+
animation: form-spinner-rotate 0.7s linear infinite;
122+
vertical-align: middle;
123+
margin-right: 0.4em;
124+
opacity: 0.8;
125+
}
126+
127+
@keyframes form-spinner-rotate {
128+
to { transform: rotate(360deg); }
129+
}
130+
131+
button[disabled].loading {
132+
opacity: 0.7;
133+
cursor: not-allowed;
134+
}

public/assets/js/src/default.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
(function() {
32

43
// Attach `confirm-action` click event to all elements with the `confirm-action` class.
@@ -19,4 +18,17 @@
1918
}
2019
});
2120
});
21+
22+
// Handle forms with loading state
23+
document.querySelectorAll('form.form-with-loading-state').forEach(form => {
24+
form.addEventListener('submit', function (event) {
25+
const submitter = event.submitter || this.querySelector('button[type="submit"]');
26+
if (submitter) {
27+
const loadingText = submitter.getAttribute('data-loading-text') || 'Processing...';
28+
submitter.disabled = true;
29+
submitter.classList.add('loading');
30+
submitter.innerHTML = `<span class="form-loading-spinner"></span> ${loadingText}`;
31+
}
32+
});
33+
});
2234
})();

routing/routes/routes.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@
7777
$routes->add(RoutesEnum::AdminTestTrustMarkValidation->name, RoutesEnum::AdminTestTrustMarkValidation->value)
7878
->controller([FederationTestController::class, 'trustMarkValidation'])
7979
->methods([HttpMethodsEnum::GET->value, HttpMethodsEnum::POST->value]);
80+
$routes->add(RoutesEnum::AdminTestFederationDiscovery->name, RoutesEnum::AdminTestFederationDiscovery->value)
81+
->controller([FederationTestController::class, 'federationDiscovery'])
82+
->methods([HttpMethodsEnum::GET->value, HttpMethodsEnum::POST->value]);
8083
$routes->add(
8184
RoutesEnum::AdminTestVerifiableCredentialIssuance->name,
8285
RoutesEnum::AdminTestVerifiableCredentialIssuance->value,

src/Codebooks/RoutesEnum.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ enum RoutesEnum: string
2828
// Testing
2929
case AdminTestTrustChainResolution = 'admin/test/trust-chain-resolution';
3030
case AdminTestTrustMarkValidation = 'admin/test/trust-mark-validation';
31+
case AdminTestFederationDiscovery = 'admin/test/federation-discovery';
3132
case AdminTestVerifiableCredentialIssuance = 'admin/test/verifiable-credential-issuance';
3233

3334

src/Controllers/Admin/FederationTestController.php

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,135 @@ public function trustMarkValidation(Request $request): Response
169169
RoutesEnum::AdminTestTrustMarkValidation->value,
170170
);
171171
}
172+
173+
174+
/**
175+
* @throws \SimpleSAML\Error\ConfigurationError
176+
* @throws \SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException
177+
* @throws \SimpleSAML\Module\oidc\Exceptions\OidcException
178+
*/
179+
public function federationDiscovery(Request $request): Response
180+
{
181+
$trustAnchorId = null;
182+
$isFormSubmitted = false;
183+
$entities = [];
184+
$forceRefresh = false;
185+
$filterEntityTypes = [];
186+
$filterTrustMarkTypes = '';
187+
$filterQuery = '';
188+
$sortBy = 'entity_id';
189+
$sortOrder = 'asc';
190+
$pageLimit = 50;
191+
$pageFrom = null;
192+
$nextPageToken = null;
193+
$totalCount = 0;
194+
195+
if ($request->isMethod(Request::METHOD_POST)) {
196+
$isFormSubmitted = true;
197+
198+
!empty($trustAnchorId = $request->request->getString('trustAnchorId')) ||
199+
throw new OidcException('Empty Trust Anchor ID.');
200+
201+
$forceRefresh = $request->request->getBoolean('forceRefresh');
202+
/** @var string[] $filterEntityTypes */
203+
$filterEntityTypes = $request->request->all('filterEntityTypes');
204+
$filterTrustMarkTypes = $request->request->getString('filterTrustMarkTypes');
205+
$filterQuery = $request->request->getString('filterQuery');
206+
$sortBy = $request->request->getString('sortBy', 'entity_id');
207+
$sortOrder = $request->request->getString('sortOrder', 'asc');
208+
/** @var 'asc'|'desc' $sortOrder */
209+
$sortOrder = in_array($sortOrder, ['asc', 'desc']) ? $sortOrder : 'asc';
210+
$pageLimit = $request->request->getInt('pageLimit', 50);
211+
$pageFrom = $request->request->get('pageFrom');
212+
$pageFrom = is_string($pageFrom) ? $pageFrom : null;
213+
214+
try {
215+
$entityCollection = $this->federationWithArrayLogger->federationDiscovery()->discover(
216+
trustAnchorId: $trustAnchorId,
217+
forceRefresh: $forceRefresh,
218+
);
219+
220+
// 1. Filtering
221+
$criteria = array_filter([
222+
'entity_type' => $filterEntityTypes,
223+
'trust_mark_type' => $this->helpers->str()->convertTextToArray($filterTrustMarkTypes),
224+
'query' => $filterQuery,
225+
]);
226+
if (!empty($criteria)) {
227+
$entityCollection->filter($criteria);
228+
}
229+
230+
$totalCount = count($entityCollection->getEntities());
231+
232+
// 2. Sorting
233+
$claimPaths = match ($sortBy) {
234+
'display_name' => [
235+
['metadata', EntityTypesEnum::OpenIdProvider->value, 'display_name'],
236+
['metadata', EntityTypesEnum::FederationEntity->value, 'display_name'],
237+
['metadata', EntityTypesEnum::OpenIdRelyingParty->value, 'display_name'],
238+
],
239+
'organization_name' => [
240+
['metadata', EntityTypesEnum::OpenIdProvider->value, 'organization_name'],
241+
['metadata', EntityTypesEnum::FederationEntity->value, 'organization_name'],
242+
['metadata', EntityTypesEnum::OpenIdRelyingParty->value, 'organization_name'],
243+
],
244+
default => [['sub']],
245+
};
246+
$entityCollection->sort($claimPaths, $sortOrder);
247+
248+
// 3. Pagination
249+
/** @var positive-int $pageLimit */
250+
$entityCollection->paginate($pageLimit, $pageFrom);
251+
252+
$nextPageToken = $entityCollection->getNextPageToken();
253+
254+
foreach ($entityCollection->getEntities() as $id => $payload) {
255+
$entities[] = [
256+
'id' => $id,
257+
'payload' => $payload,
258+
];
259+
}
260+
} catch (\Throwable $exception) {
261+
$this->arrayLogger->error(sprintf(
262+
'Error during entity discovery under Trust Anchor %s. Error was %s',
263+
$trustAnchorId,
264+
$exception->getMessage(),
265+
));
266+
}
267+
}
268+
269+
$logMessages = $this->arrayLogger->getEntries();
270+
271+
try {
272+
$trustAnchorIds = $this->moduleConfig->getFederationTrustAnchorIds();
273+
} catch (\Throwable $exception) {
274+
$this->arrayLogger->error('Module config error: ' . $exception->getMessage());
275+
$trustAnchorIds = [];
276+
}
277+
278+
$entityTypeOptions = array_map(fn (EntityTypesEnum $enum) => $enum->value, EntityTypesEnum::cases());
279+
280+
return $this->templateFactory->build(
281+
'oidc:tests/federation-discovery.twig',
282+
compact(
283+
'trustAnchorId',
284+
'logMessages',
285+
'isFormSubmitted',
286+
'entities',
287+
'trustAnchorIds',
288+
'forceRefresh',
289+
'filterEntityTypes',
290+
'filterTrustMarkTypes',
291+
'filterQuery',
292+
'sortBy',
293+
'sortOrder',
294+
'pageLimit',
295+
'pageFrom',
296+
'nextPageToken',
297+
'totalCount',
298+
'entityTypeOptions',
299+
),
300+
RoutesEnum::AdminTestFederationDiscovery->value,
301+
);
302+
}
172303
}

src/Controllers/Federation/EntityStatementController.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,6 @@ public function configuration(): Response
9595
ClaimsEnum::OrganizationUri->value => $this->moduleConfig->getOrganizationUri(),
9696
],
9797
)),
98-
ClaimsEnum::FederationFetchEndpoint->value => $this->routes->urlFederationFetch(),
99-
ClaimsEnum::FederationListEndpoint->value => $this->routes->urlFederationList(),
10098
// TODO v7 mivanci Add when ready. Use ClaimsEnum for keys.
10199
// https://openid.net/specs/openid-federation-1_0.html#name-federation-entity
102100
//'federation_resolve_endpoint',

src/Controllers/VerifiableCredentials/CredentialIssuerCredentialController.php

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -524,13 +524,20 @@ public function credential(Request $request): Response
524524
// Get valid claim paths so we can check if the user attribute is allowed to be included in the credential,
525525
// as per the credential configuration supported configuration.
526526
$validClaimPaths = $this->moduleConfig->getVciValidCredentialClaimPathsFor($resolvedCredentialIdentifier);
527-
527+
$this->loggerService->debug(
528+
'CredentialIssuerCredentialController::credential: Valid claim paths for credential configuration ',
529+
['validClaimPaths' => $validClaimPaths],
530+
);
528531
// Map user attributes to credential claims
529532
$credentialSubject = []; // For JwtVcJson
530533
$disclosureBag = $this->verifiableCredentials->disclosureBagFactory()->build(); // For DcSdJwt
531534
$attributeToCredentialClaimPathMap = $this->moduleConfig->getVciUserAttributeToCredentialClaimPathMapFor(
532535
$resolvedCredentialIdentifier,
533536
);
537+
$this->loggerService->debug(
538+
'CredentialIssuerCredentialController::credential: Attribute to credential claim path map',
539+
['attributeToCredentialClaimPathMap' => $attributeToCredentialClaimPathMap],
540+
);
534541
foreach ($attributeToCredentialClaimPathMap as $mapEntry) {
535542
if (!is_array($mapEntry)) {
536543
$this->loggerService->warning(
@@ -542,6 +549,11 @@ public function credential(Request $request): Response
542549
continue;
543550
}
544551

552+
$this->loggerService->debug(
553+
'Map entry: ',
554+
['mapEntry' => $mapEntry],
555+
);
556+
545557
$userAttributeName = key($mapEntry);
546558
if (!is_string($userAttributeName)) {
547559
$this->loggerService->warning(
@@ -553,6 +565,10 @@ public function credential(Request $request): Response
553565
continue;
554566
}
555567

568+
$this->loggerService->debug(
569+
'User attribute name: ' . $userAttributeName,
570+
);
571+
556572
/** @psalm-suppress MixedAssignment */
557573
$credentialClaimPath = current($mapEntry);
558574
if (!is_array($credentialClaimPath)) {
@@ -574,6 +590,11 @@ public function credential(Request $request): Response
574590
continue;
575591
}
576592

593+
$this->loggerService->debug(
594+
'Credential claim path',
595+
['credentialClaimPath' => $credentialClaimPath],
596+
);
597+
577598
if (!isset($userAttributes[$userAttributeName])) {
578599
$this->loggerService->warning(
579600
'Attribute "%s" does not exist in user attributes.',
@@ -590,6 +611,7 @@ public function credential(Request $request): Response
590611
$userAttributes[$userAttributeName];
591612

592613
if ($credentialFormatId === CredentialFormatIdentifiersEnum::JwtVcJson->value) {
614+
$this->loggerService->debug('JwtVcJson format detected, adding user attribute to credential subject.');
593615
$this->verifiableCredentials->helpers()->arr()->setNestedValue(
594616
$credentialSubject,
595617
$attributeValue,
@@ -598,6 +620,11 @@ public function credential(Request $request): Response
598620
}
599621

600622
if (in_array($credentialFormatId, self::SD_JWT_FORMAT_IDS, true)) {
623+
$this->loggerService->debug(
624+
'CredentialIssuerCredentialController::credential: Processing SD JWT credential format ID '
625+
. $credentialFormatId,
626+
);
627+
601628
// For now, we will only support disclosures for object properties.
602629
$claimName = array_pop($credentialClaimPath);
603630
if (!is_string($claimName)) {
@@ -611,8 +638,17 @@ public function credential(Request $request): Response
611638
continue;
612639
}
613640

614-
if ($credentialFormatId === CredentialFormatIdentifiersEnum::VcSdJwt->value) {
641+
$this->loggerService->debug('Claim name: ' . $claimName);
642+
643+
if (
644+
$credentialFormatId === CredentialFormatIdentifiersEnum::VcSdJwt->value &&
645+
!in_array(ClaimsEnum::Credential_Subject->value, $credentialClaimPath, true)
646+
) {
647+
$this->loggerService->debug('VC SD JWT - adding credential subject to claim path for claim "%s".');
615648
array_unshift($credentialClaimPath, ClaimsEnum::Credential_Subject->value);
649+
$this->loggerService->debug(
650+
'Credential claim path for credential subject: ' . print_r($credentialClaimPath, true),
651+
);
616652
}
617653

618654
/** @psalm-suppress ArgumentTypeCoercion */
@@ -722,14 +758,16 @@ public function credential(Request $request): Response
722758
// Always start with the VCDM 2.0 base context URL (mandatory).
723759
$atContext = [AtContextsEnum::W3OrgNsCredentialsV2->value];
724760

725-
// If a JSON-LD context document is configured for this credential, append the module-hosted
726-
// context URL so that verifiers can resolve the custom credential subject terms.
761+
// If a JSON-LD context document is configured for this credential,
762+
// append the module-hosted context URL so that verifiers can
763+
// resolve the custom credential subject terms.
727764
if ($this->moduleConfig->getVciCredentialJsonLdContextFor($resolvedCredentialIdentifier) !== null) {
728765
$atContext[] = $this->routes->urlCredentialJsonLdContext($resolvedCredentialIdentifier);
729766
}
730767

731-
// Append any additional context URLs declared in the credential configuration's @context field
732-
// (skipping the base W3C URL, which is already first in the list).
768+
// Append any additional context URLs declared in the credential
769+
// configuration's @context field (skipping the base W3C URL,
770+
// which is already first in the list).
733771
/** @psalm-suppress MixedAssignment */
734772
$configuredContexts = $resolvedCredentialConfiguration[ClaimsEnum::AtContext->value] ?? [];
735773
if (is_array($configuredContexts)) {
@@ -776,6 +814,7 @@ public function credential(Request $request): Response
776814
[
777815
ClaimsEnum::Kid->value => $issuerDid . '#0',
778816
],
817+
disclosureBag: $disclosureBag,
779818
);
780819
}
781820

src/Controllers/VerifiableCredentials/CredentialJsonLdContextController.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
2828
use SimpleSAML\Module\oidc\Services\LoggerService;
2929
use SimpleSAML\Module\oidc\Utils\Routes;
30-
use Symfony\Component\HttpFoundation\JsonResponse;
3130
use Symfony\Component\HttpFoundation\Response;
3231

3332
/**
@@ -81,7 +80,7 @@ public function context(string $credentialConfigurationId): Response
8180
return $this->routes->newResponse(null, Response::HTTP_NOT_FOUND);
8281
}
8382

84-
return new JsonResponse(
83+
return $this->routes->newJsonResponse(
8584
$contextDocument,
8685
Response::HTTP_OK,
8786
['Content-Type' => 'application/ld+json'],

src/Factories/TemplateFactory.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,13 @@ protected function includeDefaultMenuItems(): void
142142
),
143143
);
144144

145+
$this->oidcMenu->addItem(
146+
$this->oidcMenu->buildItem(
147+
$this->moduleConfig->getModuleUrl(RoutesEnum::AdminTestFederationDiscovery->value),
148+
Translate::noop('Test Federation Discovery'),
149+
),
150+
);
151+
145152
$this->oidcMenu->addItem(
146153
$this->oidcMenu->buildItem(
147154
$this->moduleConfig->getModuleUrl(RoutesEnum::AdminConfigVerifiableCredential->value),

0 commit comments

Comments
 (0)