Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use OCA\User_SAML\Middleware\OnlyLoggedInMiddleware;
use OCA\User_SAML\SAMLSettings;
use OCA\User_SAML\Service\SessionService;
use OCA\User_SAML\Support\SystemReportSection;
use OCA\User_SAML\UserBackend;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
Expand Down Expand Up @@ -77,6 +78,14 @@ public function register(IRegistrationContext $context): void {
*/
$context->registerAlternativeLoginProvider(AlternativeLoginProvider::class);
}

if (method_exists($context, 'registerSystemReportSection')) {
/**
* @psalm-suppress UndefinedInterfaceMethod
* @psalm-suppress MissingDependency
*/
$context->registerSystemReportSection(SystemReportSection::class);
}
}

#[\Override]
Expand Down
91 changes: 91 additions & 0 deletions lib/Support/SystemReportSection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\User_SAML\Support;

use OCA\User_SAML\SAMLSettings;
use OCP\IL10N;
use OCP\SystemReport\ISystemReportSection;
use OCP\SystemReport\SystemReportDetail;
use OCP\SystemReport\SystemReportDetailFormat;

/**
* Contributes the configuration of every configured SAML provider to the
* system report generated by the support app.
*/
class SystemReportSection implements ISystemReportSection {
/**
* Keys from SAMLSettings::IDP_CONFIG_KEYS that must never end up in a
* system report, since it is commonly shared with third parties.
*/
private const SECRET_KEYS = [
'sp-privateKey',
];

/**
* Certificates are not secret, but reporting them in full only adds
* noise, so only whether one is configured is reported.
*/
private const PRESENCE_ONLY_KEYS = [
'idp-x509cert',
'sp-x509cert',
];

public function __construct(
private readonly SAMLSettings $samlSettings,
private readonly IL10N $l10n,
) {
}

#[\Override]
public function getId(): string {
return 'saml';
}

#[\Override]
public function getTitle(): string {
return $this->l10n->t('SAML');
}

#[\Override]
public function getDetails(): array {
$details = [];

foreach ($this->samlSettings->getListOfIdps() as $id => $displayName) {
$title = $displayName !== '' ? $displayName : $this->l10n->t('SAML provider #%s', [(string)$id]);
$details[] = new SystemReportDetail(
$title,
$this->renderConfig($this->samlSettings->get($id)),
SystemReportDetailFormat::Preformatted,
);
}

return $details;
}

private function renderConfig(array $config): string {
$lines = [];
foreach (SAMLSettings::IDP_CONFIG_KEYS as $key) {
if (in_array($key, self::SECRET_KEYS, true)) {
continue;
}

$value = $config[$key] ?? '';
if (in_array($key, self::PRESENCE_ONLY_KEYS, true)) {
$value = $value !== '' ? 'configured' : 'not configured';
} elseif (is_array($value)) {
$value = implode(';', $value);
}

$lines[] = $key . ': ' . $value;
}

return implode("\n", $lines);
}
}
3 changes: 3 additions & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
<file name="tests/stubs/oc_hooks_emitter.php" />
<file name="tests/stubs/oc_core_controller_clientflowloginv2controller.php" />
<file name="tests/stubs/oc_core_controller_clientflowlogincontroller.php" />
<file name="tests/stubs/ocp_systemreport_isystemreportsection.php" />
<file name="tests/stubs/ocp_systemreport_systemreportdetail.php" />
<file name="tests/stubs/ocp_systemreport_systemreportdetailformat.php" />
</stubs>
<projectFiles>
<directory name="lib" />
Expand Down
41 changes: 41 additions & 0 deletions tests/stubs/ocp_systemreport_isystemreportsection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCP\SystemReport;

/**
* This interface needs to be implemented if you want to contribute a custom
* section to the system report that administrators can generate for support
* purposes. Register your implementation with
* \OCP\AppFramework\Bootstrap\IRegistrationContext::registerSystemReportSection().
*
* @since 36.0.0
*/
interface ISystemReportSection {
/**
* Unique, stable identifier of this section, e.g. "saml".
* Sections registered under the same id are merged.
*
* @since 36.0.0
*/
public function getId(): string;

/**
* Translated title shown as heading for this section in the report.
*
* @since 36.0.0
*/
public function getTitle(): string;

/**
* @return SystemReportDetail[]
* @since 36.0.0
*/
public function getDetails(): array;
}
47 changes: 47 additions & 0 deletions tests/stubs/ocp_systemreport_systemreportdetail.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCP\SystemReport;

/**
* A single piece of information contributed to a system report section.
*
* @since 36.0.0
*/
final readonly class SystemReportDetail {
/**
* @param string $title Translated label of this detail
* @param string $content Value of this detail. Must not contain secrets such as
* passwords, private keys, or tokens - system reports are
* commonly shared with third parties for support purposes.
* @param SystemReportDetailFormat $format How to render $content
* @since 36.0.0
*/
public function __construct(
private string $title,
private string $content,
private SystemReportDetailFormat $format = SystemReportDetailFormat::MultiLine,
) {
}

/** @since 36.0.0 */
public function getTitle(): string {
return $this->title;
}

/** @since 36.0.0 */
public function getContent(): string {
return $this->content;
}

/** @since 36.0.0 */
public function getFormat(): SystemReportDetailFormat {
return $this->format;
}
}
24 changes: 24 additions & 0 deletions tests/stubs/ocp_systemreport_systemreportdetailformat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCP\SystemReport;

/**
* How a \OCP\SystemReport\SystemReportDetail should be rendered.
*
* @since 36.0.0
*/
enum SystemReportDetailFormat {
/** @since 36.0.0 */
case SingleLine;
/** @since 36.0.0 */
case MultiLine;
/** @since 36.0.0 */
case Preformatted;
}
107 changes: 107 additions & 0 deletions tests/unit/Support/SystemReportSectionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\User_SAML\Tests\Support;

use OCA\User_SAML\SAMLSettings;
use OCA\User_SAML\Support\SystemReportSection;
use OCP\IL10N;
use OCP\SystemReport\ISystemReportSection;
use OCP\SystemReport\SystemReportDetailFormat;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;

class SystemReportSectionTest extends TestCase {
private SAMLSettings&MockObject $samlSettings;
private IL10N&MockObject $l10n;
private SystemReportSection $section;

#[\Override]
protected function setUp(): void {
parent::setUp();

if (!interface_exists(ISystemReportSection::class)) {
$this->markTestSkipped('System report sections require Nextcloud 36 or later');
}

$this->samlSettings = $this->createMock(SAMLSettings::class);
$this->l10n = $this->createMock(IL10N::class);
$this->l10n->method('t')
->willReturnCallback(fn (string $text, array $parameters = []) => vsprintf($text, $parameters));

$this->section = new SystemReportSection(
$this->samlSettings,
$this->l10n,
);
}

public function testGetIdAndTitle(): void {
$this->assertSame('saml', $this->section->getId());
$this->assertSame('SAML', $this->section->getTitle());
}

public function testGetDetailsReturnsNothingWithoutConfiguredIdps(): void {
$this->samlSettings->method('getListOfIdps')
->willReturn([]);

$this->assertSame([], $this->section->getDetails());
}

public function testGetDetailsNeverIncludesThePrivateKey(): void {
$this->samlSettings->method('getListOfIdps')
->willReturn([1 => 'My IdP']);
$this->samlSettings->method('get')
->with(1)
->willReturn([
'idp-entityId' => 'https://idp.example.com',
'sp-privateKey' => 'super-secret-private-key',
]);

$details = $this->section->getDetails();
$this->assertCount(1, $details);
$this->assertStringNotContainsString('super-secret-private-key', $details[0]->getContent());
$this->assertStringContainsString('idp-entityId: https://idp.example.com', $details[0]->getContent());
}

public function testGetDetailsOnlyReportsCertificatePresence(): void {
$this->samlSettings->method('getListOfIdps')
->willReturn([1 => 'My IdP']);
$this->samlSettings->method('get')
->with(1)
->willReturn([
'idp-x509cert' => '-----BEGIN CERTIFICATE-----MIIB...-----END CERTIFICATE-----',
'sp-x509cert' => '',
]);

$content = $this->section->getDetails()[0]->getContent();
$this->assertStringNotContainsString('BEGIN CERTIFICATE', $content);
$this->assertStringContainsString('idp-x509cert: configured', $content);
$this->assertStringContainsString('sp-x509cert: not configured', $content);
}

public function testGetDetailsUsesFallbackTitleWhenDisplayNameIsEmpty(): void {
$this->samlSettings->method('getListOfIdps')
->willReturn([3 => '']);
$this->samlSettings->method('get')
->with(3)
->willReturn([]);

$this->assertSame('SAML provider #3', $this->section->getDetails()[0]->getTitle());
}

public function testGetDetailsUsesPreformattedFormat(): void {
$this->samlSettings->method('getListOfIdps')
->willReturn([1 => 'My IdP']);
$this->samlSettings->method('get')
->with(1)
->willReturn([]);

$this->assertSame(SystemReportDetailFormat::Preformatted, $this->section->getDetails()[0]->getFormat());
}
}
8 changes: 4 additions & 4 deletions vendor-bin/psalm/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading