Skip to content
Merged
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
14 changes: 14 additions & 0 deletions docs/features.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
[
{
"slug": "admin-api",
"title": "Admin API",
"summary": "Everything an administrator does on the Versioniq page can also be done by a script or another system.",
"status": "stable",
"docsUrl": "openspec/specs/admin-api/spec.md"
},
{
"slug": "app-discovery",
"title": "App Discovery",
Expand Down Expand Up @@ -55,6 +62,13 @@
"status": "stable",
"docsUrl": "openspec/specs/pat-management/spec.md"
},
{
"slug": "pending-updates",
"title": "Pending Updates",
"summary": "An admin sees on one screen which apps run which version, which have a newer one, and which fell behind the agreed limit.",
"status": "stable",
"docsUrl": "openspec/specs/pending-updates/spec.md"
},
{
"slug": "version-management",
"title": "Version Management",
Expand Down
2 changes: 1 addition & 1 deletion lib/Controller/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ public function advisories(): DataResponse {
* Like /api/advisories it reads what AvailabilityRefreshJob stored and
* never calls a source (issue #160).
*
* @return DataResponse<Http::STATUS_OK, array{updates: array<array-key, mixed>, checkedAt: ?int, maxLinesBehind: ?int}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array{message: string}, array{}>
* @return DataResponse<Http::STATUS_OK, array{updates: array<string, mixed>, checkedAt: ?int, maxLinesBehind: ?int}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array{message: string}, array{}>
*
* 200: Stored pending updates returned
* 403: Caller is not an administrator
Expand Down
8 changes: 6 additions & 2 deletions lib/Service/Availability/AvailabilityResultStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public function save(array $updates, int $checkedAt): void {

/**
* @spec openspec/specs/pending-updates/spec.md#requirement-the-installed-version-and-pending-updates-are-swept-into-a-snapshot
* @return array{updates: array<array-key, mixed>, checkedAt: ?int}
* @return array{updates: array<string, mixed>, checkedAt: ?int}
*/
public function read(): array {
$raw = $this->config->getValueString(Application::APP_ID, self::KEY, '');
Expand All @@ -83,6 +83,10 @@ public function read(): array {

$checkedAt = $this->config->getValueInt(Application::APP_ID, self::KEY_CHECKED_AT, 0);

return ['updates' => $decoded, 'checkedAt' => $checkedAt > 0 ? $checkedAt : null];
// json_decode yields array-key keys; the keys we wrote are app ids.
/** @var array<string, mixed> $updates */
$updates = $decoded;

return ['updates' => $updates, 'checkedAt' => $checkedAt > 0 ? $checkedAt : null];
}
}
24 changes: 21 additions & 3 deletions lib/Service/SelectedReleaseInstallerService.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
use OCP\IUserSession;
use OCP\L10N\IFactory;
use OCP\Server;
use phpseclib\File\X509;
use phpseclib\File\X509 as X509V2;
use phpseclib3\File\X509 as X509V3;

/**
* @psalm-api
Expand Down Expand Up @@ -123,6 +124,23 @@ private function splitCerts(string $cert): array {
return $matches[0];
}

/**
* Returns an X509 of the phpseclib the server bundles: phpseclib 3 on
* Nextcloud 35 and later, phpseclib 2 on Nextcloud 32-34. Both offer the
* loadCA, loadX509, loadCRL, validateSignature and getRevoked calls that
* verifyCertificate() makes, the same calls core's Installer makes.
*
* @return X509V3|X509V2
* @spec openspec/specs/migration-safety/spec.md#requirement-an-app-store-package-is-verified-against-nextcloud-code-signing-before-install
*/
private function newX509(): X509V3|X509V2 {
if (class_exists(X509V3::class)) {
return new X509V3();
}

return new X509V2();
}

/**
* Verifies app certificate against Nextcloud signing chain and CRL.
*
Expand All @@ -141,7 +159,7 @@ private function verifyCertificate(string $appId, string $certificate): void {
throw new Exception('Unable to load Nextcloud certificate revocation list.');
}

$x509 = new X509();
$x509 = $this->newX509();
$rootCrtList = $this->splitCerts($rootCrt);
foreach ($rootCrtList as $rootCertificate) {
$x509->loadCA($rootCertificate);
Expand All @@ -152,7 +170,7 @@ private function verifyCertificate(string $appId, string $certificate): void {
throw new Exception('Could not parse app certificate.');
}

$crl = new X509();
$crl = $this->newX509();
foreach ($rootCrtList as $rootCertificate) {
$crl->loadCA($rootCertificate);
}
Expand Down
184 changes: 184 additions & 0 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,182 @@
}
}
},
"/ocs/v2.php/apps/versioniq/api/updates": {
"get": {
"operationId": "api-updates",
"summary": "Returns the stored pending-updates snapshot (admin-only, read-only): per app the installed version, the newest version, the newest one this server can run, the release lines behind, the pin and the source error. `checkedAt` is the unix time of the last sweep, null when none has run; `maxLinesBehind` is the admin's lag limit, null when the check is off. Like /api/advisories it reads what AvailabilityRefreshJob stored and never calls a source (issue #160).",
"description": "openspec/specs/pending-updates/spec.md#requirement-an-admin-sets-how-far-an-app-may-fall-behind\nThis endpoint requires admin access",
"tags": [
"api"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Stored pending updates returned",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"updates",
"checkedAt",
"maxLinesBehind"
],
"properties": {
"updates": {
"type": "object",
"additionalProperties": {
"type": "object"
}
},
"checkedAt": {
"type": "integer",
"format": "int64",
"nullable": true
},
"maxLinesBehind": {
"type": "integer",
"format": "int64",
"nullable": true
}
}
}
}
}
}
}
}
}
},
"403": {
"description": "Caller is not an administrator",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"message"
],
"properties": {
"message": {
"type": "string"
}
}
}
}
}
}
},
{
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {}
}
}
}
}
]
}
}
}
},
"401": {
"description": "Current user is not logged in",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {}
}
}
}
}
}
}
}
}
}
},
"/ocs/v2.php/apps/versioniq/api/update-channel": {
"get": {
"operationId": "api-update-channel",
Expand Down Expand Up @@ -6195,6 +6371,7 @@
"data": {
"type": "object",
"additionalProperties": {
"nullable": true,
"oneOf": [
{
"type": "integer",
Expand Down Expand Up @@ -6363,6 +6540,12 @@
"nullable": true,
"default": null,
"description": "Advisory feed URL; blank uses the published Nextcloud feed"
},
"maxLinesBehind": {
"type": "string",
"nullable": true,
"default": null,
"description": "Release lines an app may fall behind, 0 to 10; blank turns the check off"
}
}
}
Expand Down Expand Up @@ -6405,6 +6588,7 @@
"data": {
"type": "object",
"additionalProperties": {
"nullable": true,
"oneOf": [
{
"type": "integer",
Expand Down
23 changes: 23 additions & 0 deletions tests/stubs/server-internals.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,29 @@ public function getRevoked(string $serial): mixed {
}
}

// Nextcloud 35 bundles phpseclib 3 instead of 2; same calls, new namespace.
namespace phpseclib3\File {
class X509 {
public function loadCA(string $cert): mixed {
}

/**
* @return array<string, mixed>|false
*/
public function loadX509(string $cert): array|false {
}

public function loadCRL(string $crl): mixed {
}

public function validateSignature(): bool {
}

public function getRevoked(string $serial): mixed {
}
}
}

namespace Doctrine\DBAL\Schema {
class Table {
/**
Expand Down
Loading