Skip to content

Commit d5213f8

Browse files
authored
feat: Add protectedFieldsOwnerExempt option to control _User class owner exemption for protectedFields (#10280)
1 parent 564c845 commit d5213f8

File tree

7 files changed

+165
-10
lines changed

7 files changed

+165
-10
lines changed

spec/ProtectedFields.spec.js

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1837,4 +1837,140 @@ describe('ProtectedFields', function () {
18371837
await expectAsync(restQuery.denyProtectedFields()).toBeResolved();
18381838
});
18391839
});
1840+
1841+
describe('protectedFieldsOwnerExempt', function () {
1842+
it('owner sees own protectedFields when protectedFieldsOwnerExempt is true', async function () {
1843+
const protectedFields = {
1844+
_User: {
1845+
'*': ['phone'],
1846+
},
1847+
};
1848+
await reconfigureServer({ protectedFields, protectedFieldsOwnerExempt: true });
1849+
const user1 = new Parse.User();
1850+
user1.setUsername('user1');
1851+
user1.setPassword('password');
1852+
user1.set('phone', '555-1234');
1853+
const acl = new Parse.ACL();
1854+
acl.setPublicReadAccess(true);
1855+
user1.setACL(acl);
1856+
await user1.signUp();
1857+
const sessionToken1 = user1.getSessionToken();
1858+
1859+
// Owner fetches own object — phone should be visible
1860+
const response = await request({
1861+
url: `http://localhost:8378/1/users/${user1.id}`,
1862+
headers: {
1863+
'X-Parse-Application-Id': 'test',
1864+
'X-Parse-REST-API-Key': 'rest',
1865+
'X-Parse-Session-Token': sessionToken1,
1866+
},
1867+
});
1868+
expect(response.data.phone).toBe('555-1234');
1869+
1870+
// Another user fetches the first user — phone should be hidden
1871+
const user2 = new Parse.User();
1872+
user2.setUsername('user2');
1873+
user2.setPassword('password');
1874+
await user2.signUp();
1875+
const response2 = await request({
1876+
url: `http://localhost:8378/1/users/${user1.id}`,
1877+
headers: {
1878+
'X-Parse-Application-Id': 'test',
1879+
'X-Parse-REST-API-Key': 'rest',
1880+
'X-Parse-Session-Token': user2.getSessionToken(),
1881+
},
1882+
});
1883+
expect(response2.data.phone).toBeUndefined();
1884+
});
1885+
1886+
it('owner does NOT see own protectedFields when protectedFieldsOwnerExempt is false', async function () {
1887+
await reconfigureServer({
1888+
protectedFields: {
1889+
_User: {
1890+
'*': ['phone'],
1891+
},
1892+
},
1893+
protectedFieldsOwnerExempt: false,
1894+
});
1895+
const user = await Parse.User.signUp('user1', 'password');
1896+
const sessionToken = user.getSessionToken();
1897+
user.set('phone', '555-1234');
1898+
await user.save(null, { sessionToken });
1899+
1900+
// Owner fetches own object — phone should be hidden
1901+
const response = await request({
1902+
url: `http://localhost:8378/1/users/${user.id}`,
1903+
headers: {
1904+
'X-Parse-Application-Id': 'test',
1905+
'X-Parse-REST-API-Key': 'rest',
1906+
'X-Parse-Session-Token': sessionToken,
1907+
},
1908+
});
1909+
expect(response.data.phone).toBeUndefined();
1910+
1911+
// Master key — phone should be visible
1912+
const masterResponse = await request({
1913+
url: `http://localhost:8378/1/users/${user.id}`,
1914+
headers: {
1915+
'X-Parse-Application-Id': 'test',
1916+
'X-Parse-Master-Key': 'test',
1917+
},
1918+
});
1919+
expect(masterResponse.data.phone).toBe('555-1234');
1920+
});
1921+
1922+
it('non-_User classes unaffected by protectedFieldsOwnerExempt', async function () {
1923+
await reconfigureServer({
1924+
protectedFields: {
1925+
TestClass: {
1926+
'*': ['secret'],
1927+
},
1928+
},
1929+
protectedFieldsOwnerExempt: true,
1930+
});
1931+
const user = await Parse.User.signUp('user1', 'password');
1932+
const obj = new Parse.Object('TestClass');
1933+
obj.set('secret', 'hidden-value');
1934+
obj.setACL(new Parse.ACL(user));
1935+
await obj.save(null, { sessionToken: user.getSessionToken() });
1936+
1937+
// Owner fetches own object — secret should still be hidden (non-_User class)
1938+
const response = await request({
1939+
url: `http://localhost:8378/1/classes/TestClass/${obj.id}`,
1940+
headers: {
1941+
'X-Parse-Application-Id': 'test',
1942+
'X-Parse-REST-API-Key': 'rest',
1943+
'X-Parse-Session-Token': user.getSessionToken(),
1944+
},
1945+
});
1946+
expect(response.data.secret).toBeUndefined();
1947+
});
1948+
1949+
it('/users/me respects protectedFieldsOwnerExempt: false', async function () {
1950+
await reconfigureServer({
1951+
protectedFields: {
1952+
_User: {
1953+
'*': ['phone'],
1954+
},
1955+
},
1956+
protectedFieldsOwnerExempt: false,
1957+
});
1958+
const user = await Parse.User.signUp('user1', 'password');
1959+
const sessionToken = user.getSessionToken();
1960+
user.set('phone', '555-1234');
1961+
await user.save(null, { sessionToken });
1962+
1963+
// GET /users/me — phone should be hidden
1964+
const response = await request({
1965+
url: 'http://localhost:8378/1/users/me',
1966+
headers: {
1967+
'X-Parse-Application-Id': 'test',
1968+
'X-Parse-REST-API-Key': 'rest',
1969+
'X-Parse-Session-Token': sessionToken,
1970+
},
1971+
});
1972+
expect(response.data.phone).toBeUndefined();
1973+
expect(response.data.objectId).toBe(user.id);
1974+
});
1975+
});
18401976
});

src/Controllers/DatabaseController.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,8 @@ const filterSensitiveData = (
195195
schema: SchemaController.SchemaController | any,
196196
className: string,
197197
protectedFields: null | Array<any>,
198-
object: any
198+
object: any,
199+
protectedFieldsOwnerExempt: ?boolean
199200
) => {
200201
let userId = null;
201202
if (auth && auth.user) { userId = auth.user.id; }
@@ -271,8 +272,9 @@ const filterSensitiveData = (
271272
}
272273

273274
/* special treat for the user class: don't filter protectedFields if currently loggedin user is
274-
the retrieved user */
275-
if (!(isUserClass && userId && object.objectId === userId)) {
275+
the retrieved user, unless protectedFieldsOwnerExempt is false */
276+
const isOwnerExempt = protectedFieldsOwnerExempt !== false && isUserClass && userId && object.objectId === userId;
277+
if (!isOwnerExempt) {
276278
protectedFields && protectedFields.forEach(k => delete object[k]);
277279

278280
// fields not requested by client (excluded),
@@ -1407,7 +1409,8 @@ class DatabaseController {
14071409
schemaController,
14081410
className,
14091411
protectedFields,
1410-
object
1412+
object,
1413+
this.options.protectedFieldsOwnerExempt
14111414
);
14121415
})
14131416
)
@@ -1667,7 +1670,7 @@ class DatabaseController {
16671670
const protectedFields = perms.protectedFields;
16681671
if (!protectedFields) { return null; }
16691672

1670-
if (aclGroup.indexOf(query.objectId) > -1) { return null; }
1673+
if (className === '_User' && this.options.protectedFieldsOwnerExempt !== false && aclGroup.indexOf(query.objectId) > -1) { return null; }
16711674

16721675
// for queries where "keys" are set and do not include all 'userField':{field},
16731676
// we have to transparently include it, and then remove before returning to client
@@ -1989,7 +1992,7 @@ class DatabaseController {
19891992
}
19901993

19911994
static _validateQuery: (any, boolean, boolean, boolean) => void;
1992-
static filterSensitiveData: (boolean, boolean, any[], any, any, any, string, any[], any) => void;
1995+
static filterSensitiveData: (boolean, boolean, any[], any, any, any, string, any[], any, ?boolean) => void;
19931996
}
19941997

19951998
module.exports = DatabaseController;

src/Deprecator/Deprecations.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,9 @@ module.exports = [
8686
changeNewKey: '',
8787
solution: "Auth providers are always validated on login regardless of this setting. Set 'allowExpiredAuthDataToken' to 'false' or remove the option to accept the future removal.",
8888
},
89+
{
90+
optionKey: 'protectedFieldsOwnerExempt',
91+
changeNewDefault: 'false',
92+
solution: "Set 'protectedFieldsOwnerExempt' to 'false' to apply protectedFields consistently to the user's own _User object (same as all other classes), or to 'true' to keep the current behavior where a user can see all their own fields.",
93+
},
8994
];

src/LiveQuery/ParseLiveQueryServer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -783,7 +783,7 @@ class ParseLiveQueryServer {
783783
res.object.className,
784784
protectedFields,
785785
obj,
786-
query
786+
this.config.protectedFieldsOwnerExempt
787787
);
788788
};
789789
res.object = filter(res.object);

src/Options/Definitions.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,14 +470,20 @@ module.exports.ParseServerOptions = {
470470
},
471471
protectedFields: {
472472
env: 'PARSE_SERVER_PROTECTED_FIELDS',
473-
help: 'Protected fields that should be treated with extra security when fetching details.',
473+
help: "Fields per class that are hidden from query results for specific user groups. Protected fields are stripped from the server response, but can still be used internally (e.g. in Cloud Code triggers). Configure as `{ 'ClassName': { 'UserGroup': ['field1', 'field2'] } }` where `UserGroup` is one of: `'*'` (all users), `'authenticated'` (authenticated users), `'role:RoleName'` (users with a specific role), `'userField:FieldName'` (users referenced by a pointer field), or a user `objectId` to target a specific user. When multiple groups apply, the intersection of their protected fields is used. By default, `email` is protected on the `_User` class for all users. On the `_User` class, the object owner is exempt from protected fields by default; see `protectedFieldsOwnerExempt` to change this.",
474474
action: parsers.objectParser,
475475
default: {
476476
_User: {
477477
'*': ['email'],
478478
},
479479
},
480480
},
481+
protectedFieldsOwnerExempt: {
482+
env: 'PARSE_SERVER_PROTECTED_FIELDS_OWNER_EXEMPT',
483+
help: "Whether the `_User` class is exempt from `protectedFields` when the logged-in user queries their own user object. If `true` (default), a user can see all their own fields regardless of `protectedFields` configuration. If `false`, `protectedFields` applies equally to the user's own object, consistent with all other classes. Defaults to `true`.",
484+
action: parsers.booleanParser,
485+
default: true,
486+
},
481487
publicServerURL: {
482488
env: 'PARSE_PUBLIC_SERVER_URL',
483489
help: 'Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL string must start with `http://` or `https://`.',

src/Options/docs.js

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

src/Options/index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,13 @@ export interface ParseServerOptions {
168168
preserveFileName: ?boolean;
169169
/* Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields */
170170
userSensitiveFields: ?(string[]);
171-
/* Protected fields that should be treated with extra security when fetching details.
171+
/* Fields per class that are hidden from query results for specific user groups. Protected fields are stripped from the server response, but can still be used internally (e.g. in Cloud Code triggers). Configure as `{ 'ClassName': { 'UserGroup': ['field1', 'field2'] } }` where `UserGroup` is one of: `'*'` (all users), `'authenticated'` (authenticated users), `'role:RoleName'` (users with a specific role), `'userField:FieldName'` (users referenced by a pointer field), or a user `objectId` to target a specific user. When multiple groups apply, the intersection of their protected fields is used. By default, `email` is protected on the `_User` class for all users. On the `_User` class, the object owner is exempt from protected fields by default; see `protectedFieldsOwnerExempt` to change this.
172172
:DEFAULT: {"_User": {"*": ["email"]}} */
173173
protectedFields: ?ProtectedFields;
174+
/* Whether the `_User` class is exempt from `protectedFields` when the logged-in user queries their own user object. If `true` (default), a user can see all their own fields regardless of `protectedFields` configuration. If `false`, `protectedFields` applies equally to the user's own object, consistent with all other classes. Defaults to `true`.
175+
:ENV: PARSE_SERVER_PROTECTED_FIELDS_OWNER_EXEMPT
176+
:DEFAULT: true */
177+
protectedFieldsOwnerExempt: ?boolean;
174178
/* Enable (or disable) anonymous users, defaults to true
175179
:ENV: PARSE_SERVER_ENABLE_ANON_USERS
176180
:DEFAULT: true */

0 commit comments

Comments
 (0)