Skip to content

Commit 3ba2eb7

Browse files
committed
feat(accounts): add admin-configurable per-property scope ceiling
Admins can now set a maximum allowed visibility scope per account property via the system config key `account_manager.max_property_scope` (array of property name => max scope, e.g. `['email' => 'v2-local', 'website' => 'v2-local']`). Backend (`AccountManager::testPropertyScope`) rejects any scope that exceeds the configured ceiling, returning `Invalid scope` via the API. The frontend (`FederationControl.vue`) also filters out disallowed scope options so users only see choices their admin permits. `PersonalInfo.php` sanitises and passes the config to the frontend via initial state. `IAccountManager` adds `PROPERTY_SCOPE_ORDER` documenting the visibility ordering used for ceiling comparisons. Also fixes a latent mutation bug in `FederationControl.vue` where `supportedScopes` was calling `.push()` on the inner arrays of the `Object.freeze`d `PROPERTY_READABLE_SUPPORTED_SCOPES_ENUM` constant, causing duplicate entries to accumulate across re-renders. Signed-off-by: Anna Larch <anna@nextcloud.com> AI-Assisted-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f55b3fb commit 3ba2eb7

294 files changed

Lines changed: 375 additions & 260 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/settings/lib/Settings/Personal/PersonalInfo.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,19 @@ public function getForm(): TemplateResponse {
115115
'pronouns' => $this->getProperty($account, IAccountManager::PROPERTY_PRONOUNS),
116116
];
117117

118+
$maxPropertyScopes = array_filter(
119+
$this->config->getSystemValue('account_manager.max_property_scope', []),
120+
static fn (string $scope, string $property): bool => in_array($property, IAccountManager::ALLOWED_PROPERTIES, true) && in_array($scope, IAccountManager::ALLOWED_SCOPES, true),
121+
ARRAY_FILTER_USE_BOTH,
122+
);
123+
118124
$accountParameters = [
119125
'avatarChangeSupported' => $user->canChangeAvatar(),
120126
'displayNameChangeSupported' => $user->canChangeDisplayName(),
121127
'emailChangeSupported' => $user->canChangeEmail(),
122128
'federationEnabled' => $federationEnabled,
123129
'lookupServerUploadEnabled' => $lookupServerUploadEnabled,
130+
'maxPropertyScopes' => $maxPropertyScopes,
124131
];
125132

126133
$profileParameters = [

apps/settings/src/components/PersonalInfo/shared/FederationControl.vue

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import { handleError } from '../../../utils/handlers.ts'
5252
const {
5353
federationEnabled,
5454
lookupServerUploadEnabled,
55+
maxPropertyScopes,
5556
} = loadState('settings', 'accountParameters', {})
5657
5758
export default {
@@ -123,18 +124,24 @@ export default {
123124
},
124125
125126
supportedScopes() {
126-
const scopes = PROPERTY_READABLE_SUPPORTED_SCOPES_ENUM[this.readable]
127-
128-
if (UNPUBLISHED_READABLE_PROPERTIES.includes(this.readable)) {
129-
return scopes
130-
}
131-
132-
if (federationEnabled) {
133-
scopes.push(SCOPE_ENUM.FEDERATED)
127+
const scopes = [...PROPERTY_READABLE_SUPPORTED_SCOPES_ENUM[this.readable]]
128+
129+
if (!UNPUBLISHED_READABLE_PROPERTIES.includes(this.readable)) {
130+
if (federationEnabled) {
131+
scopes.push(SCOPE_ENUM.FEDERATED)
132+
}
133+
if (lookupServerUploadEnabled) {
134+
scopes.push(SCOPE_ENUM.PUBLISHED)
135+
}
134136
}
135137
136-
if (lookupServerUploadEnabled) {
137-
scopes.push(SCOPE_ENUM.PUBLISHED)
138+
// Apply admin-configured scope ceiling for this property.
139+
const propertyKey = PROPERTY_READABLE_KEYS_ENUM[this.readable]
140+
const maxScope = propertyKey && maxPropertyScopes?.[propertyKey]
141+
if (maxScope) {
142+
const order = [SCOPE_ENUM.PRIVATE, SCOPE_ENUM.LOCAL, SCOPE_ENUM.FEDERATED, SCOPE_ENUM.PUBLISHED]
143+
const maxIndex = order.indexOf(maxScope)
144+
return scopes.filter((scope) => order.indexOf(scope) <= maxIndex)
138145
}
139146
140147
return scopes

config/config.sample.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2886,6 +2886,29 @@
28862886
*/
28872887
'account_manager.default_property_scope' => [],
28882888

2889+
/**
2890+
* Set a maximum allowed visibility scope for individual account properties.
2891+
* Users cannot set a property to a scope more visible than the configured
2892+
* ceiling, neither through the UI nor the API.
2893+
*
2894+
* Valid property names and scope values are defined in
2895+
* ``OCP\Accounts\IAccountManager``.
2896+
*
2897+
* Example: Prevent users from making their email or website visible beyond
2898+
* the local instance:
2899+
* ``[
2900+
* \OCP\Accounts\IAccountManager::PROPERTY_EMAIL => \OCP\Accounts\IAccountManager::SCOPE_LOCAL,
2901+
* \OCP\Accounts\IAccountManager::PROPERTY_WEBSITE => \OCP\Accounts\IAccountManager::SCOPE_LOCAL,
2902+
* ]``
2903+
*
2904+
* WARNING: Restricting the scope of properties that are required for
2905+
* federation (``displayname``, ``email``, ``avatar``, ``pronouns``) below
2906+
* ``SCOPE_FEDERATED`` will break federated sharing and other cross-instance
2907+
* features that depend on those fields being visible to trusted remote
2908+
* servers.
2909+
*/
2910+
'account_manager.max_property_scope' => [],
2911+
28892912
/**
28902913
* Enable the deprecated Projects feature, superseded by Related Resources since
28912914
* Nextcloud 25.

dist/ActivityCommentAction-4Z_gsB_Z.chunk.mjs

Lines changed: 0 additions & 2 deletions
This file was deleted.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import{a as t}from"./index-C1xmmKTZ-BdqLiU2K.chunk.mjs";import{t as e}from"./translation-DoG5ZELJ-CPJIGC2H.chunk.mjs";import{C as m,a}from"./CommentView-bIFQcpqr.chunk.mjs";import{l as p}from"./activity-CS_yDSTQ.chunk.mjs";import{b as i,r as s,o as n,c,m as u}from"./preload-helper-CX9gtE7n.chunk.mjs";import{_ as l}from"./public-C1mLBHT3.chunk.mjs";import"./index-C6zIcU-d.chunk.mjs";import"./NcModal-kyWZ3UFC-CV6Hvf6d.chunk.mjs";import"./ArrowRight-JDdFcric.chunk.mjs";import"./Web-lLWc6zap.chunk.mjs";import"./index-CziSTDUD.chunk.mjs";import"./TrashCanOutline-B-GxU5E3.chunk.mjs";import"./mdi-BjKyjJ9m.chunk.mjs";import"./pinia-Bx2CoJV6.chunk.mjs";import"./PencilOutline-Bexowggr.chunk.mjs";/* empty css */import"./NcAvatar-ruClKRzS-DbbQH8Qz.chunk.mjs";import"./index-BpWtOFbq.chunk.mjs";import"./util-BUyb4W9M.chunk.mjs";import"./colors-BfjxNgsx-CknPG731.chunk.mjs";import"./NcUserStatusIcon-JWiuiAXe-FPvdKWWr.chunk.mjs";import"./NcDateTime.vue_vue_type_script_setup_true_lang-B4upiZjL-CDStkW4e.chunk.mjs";import"./NcUserBubble-BE6yD-R0-BI6ZwX-N.chunk.mjs";import"./GetComments-B2KQKdVF.chunk.mjs";import"./index-COu4RIcm.chunk.mjs";const d=i({components:{Comment:a},mixins:[m],props:{reloadCallback:{type:Function,required:!0}},methods:{onNewComment(){try{this.reloadCallback()}catch(o){t(e("comments","Could not reload comments")),p.error("Could not reload comments",{error:o})}}}});function C(o,f,y,w,D,N){const r=s("Comment");return n(),c(r,u(o.editorData,{autoComplete:o.autoComplete,resourceType:o.resourceType,editor:!0,userData:o.userData,resourceId:o.resourceId,class:"comments-action",onNew:o.onNewComment}),null,16,["autoComplete","resourceType","userData","resourceId","onNew"])}const R=l(d,[["render",C],["__scopeId","data-v-29a1e244"]]);export{R as default};
2+
//# sourceMappingURL=ActivityCommentAction-CaBMEWuf.chunk.mjs.map
File renamed without changes.

dist/ActivityCommentAction-4Z_gsB_Z.chunk.mjs.map renamed to dist/ActivityCommentAction-CaBMEWuf.chunk.mjs.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/ActivityCommentAction-4Z_gsB_Z.chunk.mjs.map.license renamed to dist/ActivityCommentAction-CaBMEWuf.chunk.mjs.map.license

File renamed without changes.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import{t as s}from"./translation-DoG5ZELJ-CPJIGC2H.chunk.mjs";import{C as p,a}from"./CommentView-bIFQcpqr.chunk.mjs";import{_ as i}from"./public-C1mLBHT3.chunk.mjs";import{r as n,o as c,c as u,m as l}from"./preload-helper-CX9gtE7n.chunk.mjs";import"./index-CziSTDUD.chunk.mjs";import"./pinia-Bx2CoJV6.chunk.mjs";import"./PencilOutline-Bexowggr.chunk.mjs";import"./ArrowRight-JDdFcric.chunk.mjs";import"./Web-lLWc6zap.chunk.mjs";import"./NcModal-kyWZ3UFC-CV6Hvf6d.chunk.mjs";/* empty css */import"./NcAvatar-ruClKRzS-DbbQH8Qz.chunk.mjs";import"./index-BpWtOFbq.chunk.mjs";import"./util-BUyb4W9M.chunk.mjs";import"./colors-BfjxNgsx-CknPG731.chunk.mjs";import"./NcUserStatusIcon-JWiuiAXe-FPvdKWWr.chunk.mjs";import"./NcDateTime.vue_vue_type_script_setup_true_lang-B4upiZjL-CDStkW4e.chunk.mjs";import"./TrashCanOutline-B-GxU5E3.chunk.mjs";import"./NcUserBubble-BE6yD-R0-BI6ZwX-N.chunk.mjs";import"./index-C1xmmKTZ-BdqLiU2K.chunk.mjs";import"./index-C6zIcU-d.chunk.mjs";import"./mdi-BjKyjJ9m.chunk.mjs";import"./activity-CS_yDSTQ.chunk.mjs";import"./GetComments-B2KQKdVF.chunk.mjs";import"./index-COu4RIcm.chunk.mjs";const d={name:"ActivityCommentEntry",components:{Comment:a},mixins:[p],props:{comment:{type:Object,required:!0},reloadCallback:{type:Function,required:!0}},data(){return{commentMessage:""}},watch:{comment(){this.commentMessage=this.comment.props.message}},mounted(){this.commentMessage=this.comment.props.message},methods:{t:s}};function g(t,o,e,f,m,C){const r=n("Comment");return c(),u(r,l({ref:"comment",tag:"li"},e.comment.props,{autoComplete:t.autoComplete,resourceType:t.resourceType,message:m.commentMessage,resourceId:t.resourceId,userData:t.genMentionsData(e.comment.props.mentions),class:"comments-activity",onDelete:o[0]||(o[0]=y=>e.reloadCallback())}),null,16,["autoComplete","resourceType","message","resourceId","userData"])}const P=i(d,[["render",g],["__scopeId","data-v-afc310f1"]]);export{P as default};
2+
//# sourceMappingURL=ActivityCommentEntry-B_giHO2y.chunk.mjs.map
File renamed without changes.

0 commit comments

Comments
 (0)