Skip to content

Commit

Permalink
feat: Add OpenAPI
Browse files Browse the repository at this point in the history
Signed-off-by: provokateurin <kate@provokateurin.de>
  • Loading branch information
provokateurin committed Oct 7, 2024
1 parent ffcd9c0 commit ac3b696
Show file tree
Hide file tree
Showing 37 changed files with 4,071 additions and 342 deletions.
11 changes: 11 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,15 @@ module.exports = {
extends: [
'@nextcloud/eslint-config/typescript',
],
overrides: [
{
files: ['src/types/openapi/*.ts'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
quotes: 'off',
'no-multiple-empty-lines': 'off',
'no-use-before-define': 'off',
},
},
],
}
94 changes: 94 additions & 0 deletions .github/workflows/openapi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
#
# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-FileCopyrightText: 2024 Arthur Schiwon <blizzz@arthur-schiwon.de>
# SPDX-License-Identifier: MIT

name: OpenAPI

on: pull_request

permissions:
contents: read

concurrency:
group: openapi-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
openapi:
runs-on: ubuntu-latest

if: ${{ github.repository_owner != 'nextcloud-gmbh' }}

steps:
- name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7

- name: Get php version
id: php_versions
uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1

- name: Set up php
uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 # v2.31.1
with:
php-version: ${{ steps.php_versions.outputs.php-available }}
extensions: xml
coverage: none
ini-file: development
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Check Typescript OpenApi types
id: check_typescript_openapi
uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0
with:
files: "src/types/openapi/openapi*.ts"

- name: Read package.json node and npm engines version
if: steps.check_typescript_openapi.outputs.files_exists == 'true'
uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3
id: node_versions
# Continue if no package.json
continue-on-error: true
with:
fallbackNode: '^20'
fallbackNpm: '^10'

- name: Set up node ${{ steps.node_versions.outputs.nodeVersion }}
if: ${{ steps.node_versions.outputs.nodeVersion }}
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
with:
node-version: ${{ steps.node_versions.outputs.nodeVersion }}

- name: Set up npm ${{ steps.node_versions.outputs.npmVersion }}
if: ${{ steps.node_versions.outputs.nodeVersion }}
run: npm i -g 'npm@${{ steps.node_versions.outputs.npmVersion }}'

- name: Install dependencies & build
if: ${{ steps.node_versions.outputs.nodeVersion }}
env:
CYPRESS_INSTALL_BINARY: 0
PUPPETEER_SKIP_DOWNLOAD: true
run: |
npm ci
- name: Set up dependencies
run: composer i

- name: Regenerate OpenAPI
run: composer run openapi

- name: Check openapi*.json and typescript changes
run: |
bash -c "[[ ! \"`git status --porcelain `\" ]] || (echo 'Please run \"composer run openapi\" and commit the openapi*.json files and (if applicable) src/types/openapi/openapi*.ts, see the section \"Show changes on failure\" for details' && exit 1)"
- name: Show changes on failure
if: failure()
run: |
git status
git --no-pager diff
exit 1 # make it red to grab attention
30 changes: 1 addition & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,35 +126,7 @@ To disable the advanced permissions feature for a group folder, use `occ groupfo

### REST API

Group folders can be configured externally through REST APIs.

The following REST API's are supported:

- `GET apps/groupfolders/folders`: Returns a list of all configured folders and their settings
- `POST apps/groupfolders/folders`: Create a new group folder
- `mountpoint`: The name for the new folder
- `GET apps/groupfolders/folders/$folderId`: Return a specific configured folder and its settings
- `DELETE apps/groupfolders/folders/$folderId`: Delete a group folder
- `POST apps/groupfolders/folders/$folderId/groups`: Give a group access to a folder
- `group`: The id of the group to be given access to the folder
- `DELETE apps/groupfolders/folders/$folderId/groups/$groupId`: Remove access from a group to a folder
- `POST apps/groupfolders/folders/$folderId/acl`: Enable/Disable folder advanced permissions
- `acl` 1 for enable, 0 for disable.
- `POST apps/groupfolders/folders/$folderId/manageACL`: Grants/Removes a group or user the ability to manage a groupfolders' advanced permissions
- `$mappingId`: the id of the group/user to be granted/removed access to/from the folder
- `$mappingType`: 'group' or 'user'
- `$manageAcl`: true to grants ability to manage a groupfolders' advanced permissions, false to remove
- `POST apps/groupfolders/folders/$folderId/groups/$groupId`: Set the permissions a group has in a folder
- `permissions` The new permissions for the group as bitmask of [permissions constants](https://github.com/nextcloud/server/blob/b4f36d44c43aac0efdc6c70ff8e46473341a9bfe/lib/public/Constants.php#L65)
- `POST apps/groupfolders/folders/$folderId/quota`: Set the quota for a folder
- `quota`: The new quota for the folder in bytes, user `-3` for unlimited
- `POST apps/groupfolders/folders/$folderId/mountpoint`: Change the name of a folder
- `mountpoint`: The new name for the folder

For all `POST` calls the required parameters are listed.

Non-admins can access the `GET` requests to retrieve info about group folders they have access to.
Admins can add `applicable=1` as a parameter to the group folder list request to get the same filtered results of only folders they have direct access to.
See the [OpenAPI specification](openapi.json) to learn about all available API endpoints.

### WebDAV API

Expand Down
6 changes: 6 additions & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,9 @@ path = ["tests/stubs/icewind_streams_**"]
precedence = "aggregate"
SPDX-FileCopyrightText = "none"
SPDX-License-Identifier = "MIT"

[[annotations]]
path = ["openapi.json", "src/types/openapi/openapi.ts"]
precedence = "aggregate"
SPDX-FileCopyrightText = "2024 Nextcloud GmbH and Nextcloud contributors"
SPDX-License-Identifier = "AGPL-3.0-or-later"
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"psalm:fix": "psalm --no-cache --alter --issues=InvalidReturnType,InvalidNullableReturnType,MissingParamType,InvalidFalsableReturnType",
"test:unit": "phpunit -c tests/phpunit.xml",
"test:unit:coverage": "XDEBUG_MODE=coverage phpunit -c tests/phpunit.xml",
"rector": "rector && composer cs:fix"
"rector": "rector && composer cs:fix",
"openapi": "generate-spec && npm run typescript:generate"
},
"config": {
"allow-plugins": {
Expand Down
8 changes: 8 additions & 0 deletions lib/AppInfo/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ public function __construct(
) {
}

/**
* @return array{
* groupfolders?: array{
* appVersion: string,
* hasGroupFolders: bool,
* },
* }
*/
public function getCapabilities(): array {
$user = $this->userSession->getUser();
if (!$user) {
Expand Down
5 changes: 4 additions & 1 deletion lib/Command/FolderCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@
use OC\Core\Command\Base;
use OCA\GroupFolders\Folder\FolderManager;
use OCA\GroupFolders\Mount\MountProvider;
use OCA\GroupFolders\ResponseDefinitions;
use OCP\Files\IRootFolder;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

/**
* Base command for commands asking the user for a folder id.
*
* @psalm-import-type GroupFoldersFolder from ResponseDefinitions
*/
abstract class FolderCommand extends Base {

Expand All @@ -29,7 +32,7 @@ public function __construct(
}

/**
* @psalm-return ?array{id: mixed, mount_point: string, groups: array<empty, empty>|mixed, quota: int, size: int|mixed, acl: bool}
* @return ?GroupFoldersFolder
*/
protected function getFolder(InputInterface $input, OutputInterface $output): ?array {
$folderId = (int)$input->getArgument('folder_id');
Expand Down
5 changes: 4 additions & 1 deletion lib/Command/Group.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$permissionsString = $input->getArgument('permissions');
$permissions = $this->getNewPermissions($permissionsString);
if ($permissions) {
if (!isset($folder['groups'][$groupString])) {
if (!is_array($folder['groups'])) {
$folder['groups'] = [];
}
if (empty(array_filter($folder['groups'], static fn (array $group) => $group['id'] === $groupString))) {
$this->folderManager->addApplicableGroup($folder['id'], $groupString);
}

Expand Down
25 changes: 16 additions & 9 deletions lib/Command/ListCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int

if ($outputType === self::OUTPUT_FORMAT_JSON || $outputType === self::OUTPUT_FORMAT_JSON_PRETTY) {
foreach ($folders as &$folder) {
$folder['group_details'] = $folder['groups'];
$folder['groups'] = array_map(fn (array $group): int => $group['permissions'], $folder['groups']);
if (is_array($folder['groups'])) {
$folder['groups'] = array_map(fn (array $group): int => $group['permissions'], $folder['groups']);
} else {
$folder['groups'] = [];
}
}

$this->writeArrayInOutputFormat($input, $output, $folders);
Expand All @@ -98,13 +101,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$table->setRows(array_map(function (array $folder) use ($groupNames): array {
$folder['size'] = \OCP\Util::humanFileSize($folder['size']);
$folder['quota'] = ($folder['quota'] > 0) ? \OCP\Util::humanFileSize($folder['quota']) : 'Unlimited';
$groupStrings = array_map(function (string $groupId, array $entry) use ($groupNames): string {
[$permissions, $displayName] = [$entry['permissions'], $entry['displayName']];
$groupName = array_key_exists($groupId, $groupNames) && ($groupNames[$groupId] !== $groupId) ? $groupNames[$groupId] . ' (' . $groupId . ')' : $displayName;

return $groupName . ': ' . $this->permissionsToString($permissions);
}, array_keys($folder['groups']), array_values($folder['groups']));
$folder['groups'] = implode("\n", $groupStrings);
if (is_array($folder['groups'])) {
$groupStrings = array_map(function (string $groupId, array $entry) use ($groupNames): string {
[$permissions, $displayName] = [$entry['permissions'], $entry['displayName']];
$groupName = array_key_exists($groupId, $groupNames) && ($groupNames[$groupId] !== $groupId) ? $groupNames[$groupId] . ' (' . $groupId . ')' : $displayName;

return $groupName . ': ' . $this->permissionsToString($permissions);
}, array_keys($folder['groups']), array_values($folder['groups']));
$folder['groups'] = implode("\n", $groupStrings);
} else {
$folder['groups'] = [];
}
$folder['acl'] = $folder['acl'] ? 'Enabled' : 'Disabled';
$manageStrings = array_map(fn (array $manage): string => $manage['id'] . ' (' . $manage['type'] . ')', $folder['manage']);
$folder['manage'] = implode("\n", $manageStrings);
Expand Down
2 changes: 1 addition & 1 deletion lib/Command/Scan.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return -1;
}

$folders = [$folder['id'] => $folder];
$folders = [$folder];
}

$inputPath = $input->getOption('path');
Expand Down
20 changes: 19 additions & 1 deletion lib/Controller/DelegationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@

use OCA\Circles\CirclesManager;
use OCA\GroupFolders\Attribute\RequireGroupFolderAdmin;
use OCA\GroupFolders\ResponseDefinitions;
use OCA\GroupFolders\Service\DelegationService;
use OCA\Settings\Service\AuthorizedGroupService;
use OCP\App\IAppManager;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\ApiRoute;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
Expand All @@ -23,6 +25,10 @@
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;

/**
* @psalm-import-type GroupFoldersGroup from ResponseDefinitions
* @psalm-import-type GroupFoldersCircle from ResponseDefinitions
*/
class DelegationController extends OCSController {
public function __construct(
string $appName,
Expand All @@ -39,6 +45,10 @@ public function __construct(

/**
* Returns the list of all groups
*
* @return DataResponse<Http::STATUS_OK, list<GroupFoldersGroup>, array{}>
*
* 200: All groups returned
*/
#[RequireGroupFolderAdmin]
#[NoAdminRequired]
Expand All @@ -61,6 +71,10 @@ public function getAllGroups(): DataResponse {

/**
* Returns the list of all visible circles
*
* @return DataResponse<Http::STATUS_OK, list<GroupFoldersCircle>, array{}>
*
* 200: All circles returned
*/
#[RequireGroupFolderAdmin]
#[NoAdminRequired]
Expand Down Expand Up @@ -97,10 +111,14 @@ public function getAllCircles(): DataResponse {

/**
* Get the list Groups related to classname.
* If the classname is
* @param string $classname If the classname is
* - OCA\GroupFolders\Settings\Admin : It's reference to fields in Admin Priveleges.
* - OCA\GroupFolders\Controller\DelegationController : It's just to specific the subadmins.
* They can only manage groupfolders in which they are added in the Advanced Permissions (groups only)
*
* @return DataResponse<Http::STATUS_OK, list<GroupFoldersGroup>, array{}>
*
* 200: Authorized groups returned
*/
#[RequireGroupFolderAdmin]
#[NoAdminRequired]
Expand Down
Loading

0 comments on commit ac3b696

Please sign in to comment.