Skip to content

Commit b44f156

Browse files
authored
Merge pull request #51288 from nextcloud/fix/admin-tag-color-prevent
fix(systemtags): unify restrict_creation_to_admin handling
2 parents 4fe518a + ad7afc6 commit b44f156

28 files changed

+186
-40
lines changed

apps/dav/lib/SystemTag/SystemTagPlugin.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use OCP\SystemTag\ISystemTagObjectMapper;
2020
use OCP\SystemTag\TagAlreadyExistsException;
2121
use OCP\SystemTag\TagCreationForbiddenException;
22+
use OCP\SystemTag\TagUpdateForbiddenException;
2223
use OCP\Util;
2324
use Sabre\DAV\Exception\BadRequest;
2425
use Sabre\DAV\Exception\Conflict;
@@ -191,7 +192,7 @@ private function createTag($data, $contentType = 'application/json') {
191192
} catch (TagAlreadyExistsException $e) {
192193
throw new Conflict('Tag already exists', 0, $e);
193194
} catch (TagCreationForbiddenException $e) {
194-
throw new Forbidden('You don’t have right to create tags', 0, $e);
195+
throw new Forbidden('You don’t have permissions to create tags', 0, $e);
195196
}
196197
}
197198

@@ -472,7 +473,11 @@ public function handleUpdateProperties($path, PropPatch $propPatch) {
472473
}
473474

474475
if ($updateTag) {
475-
$node->update($name, $userVisible, $userAssignable, $color);
476+
try {
477+
$node->update($name, $userVisible, $userAssignable, $color);
478+
} catch (TagUpdateForbiddenException $e) {
479+
throw new Forbidden('You don’t have permissions to update tags', 0, $e);
480+
}
476481
}
477482

478483
return true;

apps/settings/lib/Settings/Admin/Server.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public function getForm() {
5555
$this->initialStateService->provideInitialState('profileEnabledByDefault', $this->isProfileEnabledByDefault($this->config));
5656

5757
// Basic settings
58-
$this->initialStateService->provideInitialState('restrictSystemTagsCreationToAdmin', $this->appConfig->getValueString('systemtags', 'restrict_creation_to_admin', 'true'));
58+
$this->initialStateService->provideInitialState('restrictSystemTagsCreationToAdmin', $this->appConfig->getValueBool('systemtags', 'restrict_creation_to_admin', false));
5959

6060
return new TemplateResponse('settings', 'settings/admin/server', [
6161
'profileEnabledGlobally' => $this->profileManager->isProfileEnabled(),

apps/settings/tests/Settings/Admin/ServerTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ public function testGetForm(): void {
8585
->expects($this->any())
8686
->method('getValueString')
8787
->willReturnCallback(fn ($a, $b, $default) => $default);
88+
$this->appConfig
89+
->expects($this->any())
90+
->method('getValueBool')
91+
->willReturnCallback(fn ($a, $b, $default) => $default);
8892
$this->profileManager
8993
->expects($this->exactly(2))
9094
->method('isProfileEnabled')

apps/systemtags/lib/Listeners/BeforeTemplateRenderedListener.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,29 @@
99

1010
use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent;
1111
use OCA\SystemTags\AppInfo\Application;
12+
use OCP\AppFramework\Services\IInitialState;
1213
use OCP\EventDispatcher\Event;
1314
use OCP\EventDispatcher\IEventListener;
15+
use OCP\IAppConfig;
1416
use OCP\Util;
1517

1618
/**
1719
* @template-implements IEventListener<BeforeTemplateRenderedEvent>
1820
*/
1921
class BeforeTemplateRenderedListener implements IEventListener {
22+
public function __construct(
23+
private IAppConfig $appConfig,
24+
private IInitialState $initialState,
25+
) {
26+
}
27+
2028
public function handle(Event $event): void {
2129
if (!$event instanceof BeforeTemplateRenderedEvent) {
2230
return;
2331
}
2432
Util::addInitScript(Application::APP_ID, 'init');
33+
34+
$restrictSystemTagsCreationToAdmin = $this->appConfig->getValueBool(Application::APP_ID, 'restrict_creation_to_admin', false);
35+
$this->initialState->provideInitialState('restrictSystemTagsCreationToAdmin', $restrictSystemTagsCreationToAdmin);
2536
}
2637
}

apps/systemtags/lib/Listeners/LoadAdditionalScriptsListener.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,29 @@
99

1010
use OCA\Files\Event\LoadAdditionalScriptsEvent;
1111
use OCA\SystemTags\AppInfo\Application;
12+
use OCP\AppFramework\Services\IInitialState;
1213
use OCP\EventDispatcher\Event;
1314
use OCP\EventDispatcher\IEventListener;
15+
use OCP\IAppConfig;
1416
use OCP\Util;
1517

1618
/**
1719
* @template-implements IEventListener<LoadAdditionalScriptsEvent>
1820
*/
1921
class LoadAdditionalScriptsListener implements IEventListener {
22+
public function __construct(
23+
private IAppConfig $appConfig,
24+
private IInitialState $initialState,
25+
) {
26+
}
27+
2028
public function handle(Event $event): void {
2129
if (!$event instanceof LoadAdditionalScriptsEvent) {
2230
return;
2331
}
2432
Util::addInitScript(Application::APP_ID, 'init');
33+
34+
$restrictSystemTagsCreationToAdmin = $this->appConfig->getValueBool(Application::APP_ID, 'restrict_creation_to_admin', false);
35+
$this->initialState->provideInitialState('restrictSystemTagsCreationToAdmin', $restrictSystemTagsCreationToAdmin);
2536
}
2637
}

apps/systemtags/src/components/SystemTagPicker.vue

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
<!-- Search or create input -->
2626
<div class="systemtags-picker__input">
2727
<NcTextField :value.sync="input"
28-
:label="t('systemtags', 'Search or create tag')"
28+
:label="canEditOrCreateTag ? t('systemtags', 'Search or create tag') : t('systemtags', 'Search tag')"
2929
data-cy-systemtags-picker-input>
3030
<TagIcon :size="20" />
3131
</NcTextField>
@@ -49,7 +49,8 @@
4949
</NcCheckboxRadioSwitch>
5050

5151
<!-- Color picker -->
52-
<NcColorPicker :data-cy-systemtags-picker-tag-color="tag.id"
52+
<NcColorPicker v-if="canEditOrCreateTag"
53+
:data-cy-systemtags-picker-tag-color="tag.id"
5354
:value="`#${tag.color}`"
5455
:shown="openedPicker === tag.id"
5556
class="systemtags-picker__tag-color"
@@ -68,7 +69,7 @@
6869

6970
<!-- Create new tag -->
7071
<li>
71-
<NcButton v-if="canCreateTag"
72+
<NcButton v-if="canEditOrCreateTag && canCreateTag"
7273
:disabled="status === Status.CREATING_TAG"
7374
alignment="start"
7475
class="systemtags-picker__tag-create"
@@ -88,7 +89,7 @@
8889
<!-- Note -->
8990
<div class="systemtags-picker__note">
9091
<NcNoteCard v-if="!hasChanges" type="info">
91-
{{ t('systemtags', 'Select or create tags to apply to all selected files') }}
92+
{{ canEditOrCreateTag ? t('systemtags', 'Select or create tags to apply to all selected files'): t('systemtags', 'Select tags to apply to all selected files') }}
9293
</NcNoteCard>
9394
<NcNoteCard v-else type="info">
9495
<span v-html="statusMessage" />
@@ -127,7 +128,9 @@ import type { Tag, TagWithId } from '../types'
127128
128129
import { defineComponent } from 'vue'
129130
import { emit } from '@nextcloud/event-bus'
131+
import { getCurrentUser } from '@nextcloud/auth'
130132
import { getLanguage, n, t } from '@nextcloud/l10n'
133+
import { loadState } from '@nextcloud/initial-state'
131134
import { showError, showInfo } from '@nextcloud/dialogs'
132135
import debounce from 'debounce'
133136
import domPurify from 'dompurify'
@@ -149,9 +152,9 @@ import PencilIcon from 'vue-material-design-icons/Pencil.vue'
149152
import PlusIcon from 'vue-material-design-icons/Plus.vue'
150153
import TagIcon from 'vue-material-design-icons/Tag.vue'
151154
152-
import { createTag, fetchTag, fetchTags, getTagObjects, setTagObjects, updateTag } from '../services/api'
153-
import { getNodeSystemTags, setNodeSystemTags } from '../utils'
154-
import { elementColor, invertTextColor, isDarkModeEnabled } from '../utils/colorUtils'
155+
import { createTag, fetchTag, fetchTags, getTagObjects, setTagObjects, updateTag } from '../services/api.ts'
156+
import { elementColor, invertTextColor, isDarkModeEnabled } from '../utils/colorUtils.ts'
157+
import { getNodeSystemTags, setNodeSystemTags } from '../utils.ts'
155158
import logger from '../logger.ts'
156159
157160
const debounceUpdateTag = debounce(updateTag, 500)
@@ -170,6 +173,8 @@ enum Status {
170173
DONE = 'done',
171174
}
172175
176+
const restrictSystemTagsCreationToAdmin = loadState('systemtags', 'restrictSystemTagsCreationToAdmin', false)
177+
173178
export default defineComponent({
174179
name: 'SystemTagPicker',
175180
@@ -204,6 +209,8 @@ export default defineComponent({
204209
emit,
205210
Status,
206211
t,
212+
// Either tag creation is not restricted to admins or the current user is an admin
213+
canEditOrCreateTag: !restrictSystemTagsCreationToAdmin || getCurrentUser()?.isAdmin,
207214
}
208215
},
209216
@@ -364,6 +371,10 @@ export default defineComponent({
364371
})
365372
return acc
366373
}, {} as TagListCount) as TagListCount
374+
375+
if (!this.canEditOrCreateTag) {
376+
logger.debug('System tag creation is restricted to admins and the current user is not an admin')
377+
}
367378
},
368379
369380
methods: {
@@ -422,6 +433,12 @@ export default defineComponent({
422433
},
423434
424435
async onNewTag() {
436+
if (!this.canEditOrCreateTag) {
437+
// Should not happen ™
438+
showError(t('systemtags', 'Only admins can create new tags'))
439+
return
440+
}
441+
425442
this.status = Status.CREATING_TAG
426443
try {
427444
const payload: Tag = {

apps/systemtags/src/components/SystemTags.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,8 @@ export default Vue.extend({
189189
this.sortedTags.unshift(createdTag)
190190
this.selectedTags.push(createdTag)
191191
} catch (error) {
192-
if(loadState('settings', 'restrictSystemTagsCreationToAdmin', '0') === '1') {
192+
const systemTagsCreationRestrictedToAdmin = loadState<true|false>('settings', 'restrictSystemTagsCreationToAdmin', false) === true
193+
if (systemTagsCreationRestrictedToAdmin) {
193194
showError(t('systemtags', 'System admin disabled tag creation. You can only use existing ones.'))
194195
return
195196
}

apps/systemtags/src/components/SystemTagsCreationControl.vue

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,17 @@
66
<template>
77
<div id="system-tags-creation-control">
88
<h4 class="inlineblock">
9-
{{ t('settings', 'System tag creation') }}
9+
{{ t('settings', 'System tag management') }}
1010
</h4>
1111

1212
<p class="settings-hint">
13-
{{ t('settings', 'If enabled, regular accounts will be restricted from creating new tags but will still be able to assign and remove them from their files.') }}
13+
{{ t('settings', 'If enabled, only administrators can create and edit tags. Accounts can still assign and remove them from files.') }}
1414
</p>
1515

1616
<NcCheckboxRadioSwitch type="switch"
1717
:checked.sync="systemTagsCreationRestrictedToAdmin"
1818
@update:checked="updateSystemTagsDefault">
19-
{{ t('settings', 'Restrict tag creation to admins only') }}
19+
{{ t('settings', 'Restrict tag creation and editing to administrators') }}
2020
</NcCheckboxRadioSwitch>
2121
</div>
2222
</template>
@@ -25,8 +25,9 @@
2525
import { loadState } from '@nextcloud/initial-state'
2626
import { showError, showSuccess } from '@nextcloud/dialogs'
2727
import { t } from '@nextcloud/l10n'
28-
import logger from '../logger.ts'
28+
2929
import { updateSystemTagsAdminRestriction } from '../services/api.js'
30+
import logger from '../logger.ts'
3031
3132
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
3233
@@ -37,14 +38,19 @@ export default {
3738
NcCheckboxRadioSwitch,
3839
},
3940
41+
setup() {
42+
return {
43+
t,
44+
}
45+
},
46+
4047
data() {
4148
return {
4249
// By default, system tags creation is not restricted to admins
43-
systemTagsCreationRestrictedToAdmin: loadState('settings', 'restrictSystemTagsCreationToAdmin', '0') === '1',
50+
systemTagsCreationRestrictedToAdmin: loadState('settings', 'restrictSystemTagsCreationToAdmin', false),
4451
}
4552
},
4653
methods: {
47-
t,
4854
async updateSystemTagsDefault(isRestricted: boolean) {
4955
try {
5056
const responseData = await updateSystemTagsAdminRestriction(isRestricted)

apps/systemtags/src/files_actions/bulkSystemTagsAction.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,9 @@ import { FileAction } from '@nextcloud/files'
99
import { isPublicShare } from '@nextcloud/sharing/public'
1010
import { spawnDialog } from '@nextcloud/dialogs'
1111
import { t } from '@nextcloud/l10n'
12-
import { getCurrentUser } from '@nextcloud/auth'
13-
import { loadState } from '@nextcloud/initial-state'
1412

1513
import TagMultipleSvg from '@mdi/svg/svg/tag-multiple.svg?raw'
1614

17-
const restrictSystemTagsCreationToAdmin = loadState<'0'|'1'>('settings', 'restrictSystemTagsCreationToAdmin', '0') === '1'
18-
1915
/**
2016
* Spawn a dialog to add or remove tags from multiple nodes.
2117
* @param nodes Nodes to modify tags for
@@ -38,11 +34,6 @@ export const action = new FileAction({
3834

3935
// If the app is disabled, the action is not available anyway
4036
enabled(nodes) {
41-
// By default, everyone can create system tags
42-
if (restrictSystemTagsCreationToAdmin && getCurrentUser()?.isAdmin !== true) {
43-
return false
44-
}
45-
4637
if (isPublicShare()) {
4738
return false
4839
}

cypress/e2e/systemtags/files-bulk-action.cy.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ describe('Systemtags: Files bulk action', { testIsolation: false }, () => {
7575
resetTags()
7676
})
7777

78+
after(() => {
79+
resetTags()
80+
cy.runOccCommand('config:app:set systemtags restrict_creation_to_admin --value 0')
81+
})
82+
7883
it('Can assign tag to selection', () => {
7984
cy.login(user1)
8085
cy.visit('/apps/files')
@@ -87,6 +92,7 @@ describe('Systemtags: Files bulk action', { testIsolation: false }, () => {
8792

8893
triggerTagManagementDialogAction()
8994
cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 5)
95+
cy.get('[data-cy-systemtags-picker-tag-color]').should('have.length', 5)
9096

9197
cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData')
9298
cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData')
@@ -115,6 +121,7 @@ describe('Systemtags: Files bulk action', { testIsolation: false }, () => {
115121

116122
triggerTagManagementDialogAction()
117123
cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 5)
124+
cy.get('[data-cy-systemtags-picker-tag-color]').should('have.length', 5)
118125

119126
cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData')
120127
cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData')
@@ -353,4 +360,55 @@ describe('Systemtags: Files bulk action', { testIsolation: false }, () => {
353360
expectInlineTagForFile('file5.txt', [newTag])
354361
})
355362
})
363+
364+
it('Cannot create tag if restriction is in place', () => {
365+
let tagId: string
366+
367+
cy.runOccCommand('config:app:set systemtags restrict_creation_to_admin --value 1')
368+
cy.runOccCommand('tag:add testTag public --output json').then(({ stdout }) => {
369+
const tag = JSON.parse(stdout)
370+
tagId = tag.id
371+
})
372+
373+
cy.createRandomUser().then((user1) => {
374+
files.forEach((file) => {
375+
cy.uploadContent(user1, new Blob([]), 'text/plain', '/' + file)
376+
})
377+
378+
cy.login(user1)
379+
cy.visit('/apps/files')
380+
381+
files.forEach((file) => {
382+
getRowForFile(file).should('be.visible')
383+
})
384+
selectAllFiles()
385+
386+
triggerTagManagementDialogAction()
387+
388+
cy.findByRole('textbox', { name: 'Search or create tag' }).should('not.exist')
389+
cy.findByRole('textbox', { name: 'Search tag' }).should('be.visible')
390+
391+
cy.get('[data-cy-systemtags-picker-input]').type('testTag')
392+
393+
cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 1)
394+
cy.get('[data-cy-systemtags-picker-button-create]').should('not.exist')
395+
cy.get('[data-cy-systemtags-picker-tag-color]').should('not.exist')
396+
397+
// Assign the tag
398+
cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData')
399+
cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData')
400+
401+
cy.get(`[data-cy-systemtags-picker-tag="${tagId}"]`).should('be.visible')
402+
.findByRole('checkbox').click({ force: true })
403+
cy.get('[data-cy-systemtags-picker-button-submit]').click()
404+
405+
cy.wait('@getTagData')
406+
cy.wait('@assignTagData')
407+
408+
cy.get('[data-cy-systemtags-picker]').should('not.exist')
409+
410+
// Finally, reset the restriction
411+
cy.runOccCommand('config:app:set systemtags restrict_creation_to_admin --value 0')
412+
})
413+
})
356414
})

0 commit comments

Comments
 (0)