From fdfb67b8a1a422787039ffa44adf02f3bccfce22 Mon Sep 17 00:00:00 2001
From: Kartal Kaan Bozdogan <kartalkaanbozdogan@gmail.com>
Date: Fri, 6 Sep 2024 11:28:37 +0200
Subject: [PATCH 1/7] Cloud code and triggers: Pass the set of roles the user
 has

---
 spec/CloudCode.spec.js         | 78 ++++++++++++++++++++++++++++++++++
 src/Routers/FunctionsRouter.js |  6 +++
 src/triggers.js                |  3 ++
 3 files changed, 87 insertions(+)

diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js
index 99ec4910d1..3062165f20 100644
--- a/spec/CloudCode.spec.js
+++ b/spec/CloudCode.spec.js
@@ -1163,6 +1163,53 @@ describe('Cloud Code', () => {
     );
   });
 
+  it('test save triggers get user roles', async () => {
+    let beforeSaveFlag = false,
+      afterSaveFlag = false;
+    Parse.Cloud.beforeSave('SaveTriggerUserRoles', async function (req) {
+      expect(await req.getRoles()).toEqual(['TestRole']);
+      beforeSaveFlag = true;
+    });
+
+    Parse.Cloud.afterSave('SaveTriggerUserRoles', async function (req) {
+      expect(await req.getRoles()).toEqual(['TestRole']);
+      afterSaveFlag = true;
+    });
+
+    const user = new Parse.User();
+    user.set('password', 'asdf');
+    user.set('email', 'asdf@example.com');
+    user.set('username', 'zxcv');
+    await user.signUp();
+    const role = new Parse.Role('TestRole', new Parse.ACL({ '*': { read: true, write: true } }));
+    role.getUsers().add(user);
+    await role.save();
+
+    const obj = new Parse.Object('SaveTriggerUserRoles');
+    await obj.save();
+    expect(beforeSaveFlag).toBeTrue();
+    expect(afterSaveFlag).toBeTrue();
+  });
+
+  it('should not have user roles for anonymous calls', async () => {
+    let beforeSaveFlag = false,
+      afterSaveFlag = false;
+    Parse.Cloud.beforeSave('SaveTriggerUserRoles', async function (req) {
+      expect(req.getRoles).toBeUndefined();
+      beforeSaveFlag = true;
+    });
+
+    Parse.Cloud.afterSave('SaveTriggerUserRoles', async function (req) {
+      expect(req.getRoles).toBeUndefined();
+      afterSaveFlag = true;
+    });
+
+    const obj = new Parse.Object('SaveTriggerUserRoles');
+    await obj.save();
+    expect(beforeSaveFlag).toBeTrue();
+    expect(afterSaveFlag).toBeTrue();
+  });
+
   it('beforeSave change propagates through the save response', done => {
     Parse.Cloud.beforeSave('ChangingObject', function (request) {
       request.object.set('foo', 'baz');
@@ -2014,6 +2061,37 @@ describe('cloud functions', () => {
 
     Parse.Cloud.run('myFunction', {}).then(() => done());
   });
+
+  it('should have user roles', async () => {
+    let flag = false;
+    Parse.Cloud.define('myFunction', async function (req) {
+      expect(await req.getRoles()).toEqual(['TestRole']);
+      flag = true;
+    });
+
+    const user = new Parse.User();
+    user.set('password', 'asdf');
+    user.set('email', 'asdf@example.com');
+    user.set('username', 'zxcv');
+    await user.signUp();
+    const role = new Parse.Role('TestRole', new Parse.ACL({ '*': { read: true, write: true } }));
+    role.getUsers().add(user);
+    await role.save();
+
+    await Parse.Cloud.run('myFunction', { sessionToken: user.getSessionToken() });
+    expect(flag).toBeTrue();
+  });
+
+  it('should not have user roles for anonymous calls', async () => {
+    let flag = false;
+    Parse.Cloud.define('myFunction', async function (req) {
+      expect(req.getRoles).toBeUndefined();
+      flag = true;
+    });
+
+    await Parse.Cloud.run('myFunction');
+    expect(flag).toBeTrue();
+  });
 });
 
 describe('beforeSave hooks', () => {
diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js
index 77c0dff7cc..d02d983fa3 100644
--- a/src/Routers/FunctionsRouter.js
+++ b/src/Routers/FunctionsRouter.js
@@ -131,6 +131,12 @@ export class FunctionsRouter extends PromiseRouter {
       params: params,
       master: req.auth && req.auth.isMaster,
       user: req.auth && req.auth.user,
+      getRoles:
+        req.auth && req.auth.user
+          ? async () => {
+            return (await req.auth.getUserRoles()).map(r => r.substr('role:'.length));
+          }
+          : undefined,
       installationId: req.info.installationId,
       log: req.config.loggerController,
       headers: req.config.headers,
diff --git a/src/triggers.js b/src/triggers.js
index e34c5fd3a8..888f6b4361 100644
--- a/src/triggers.js
+++ b/src/triggers.js
@@ -292,6 +292,9 @@ export function getRequestObject(
   }
   if (auth.user) {
     request['user'] = auth.user;
+    request['getRoles'] = async () => {
+      return (await auth.getUserRoles()).map(r => r.substr('role:'.length));
+    };
   }
   if (auth.installationId) {
     request['installationId'] = auth.installationId;

From 105838f5413800f15b8bbd87717abb0b2d2263b3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kartal=20Kaan=20Bozdo=C4=9Fan?=
 <kartalkaanbozdogan@gmail.com>
Date: Tue, 10 Sep 2024 10:21:26 +0200
Subject: [PATCH 2/7] Update spec/CloudCode.spec.js
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: Manuel <5673677+mtrezza@users.noreply.github.com>
Signed-off-by: Kartal Kaan Bozdoğan <kartalkaanbozdogan@gmail.com>
---
 spec/CloudCode.spec.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js
index 3062165f20..d350d05c9f 100644
--- a/spec/CloudCode.spec.js
+++ b/spec/CloudCode.spec.js
@@ -1164,8 +1164,8 @@ describe('Cloud Code', () => {
   });
 
   it('test save triggers get user roles', async () => {
-    let beforeSaveFlag = false,
-      afterSaveFlag = false;
+    let beforeSaveFlag = false;
+    let afterSaveFlag = false;
     Parse.Cloud.beforeSave('SaveTriggerUserRoles', async function (req) {
       expect(await req.getRoles()).toEqual(['TestRole']);
       beforeSaveFlag = true;

From 147060d077dfd2c021398f88951c75bd165f4961 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kartal=20Kaan=20Bozdo=C4=9Fan?=
 <kartalkaanbozdogan@gmail.com>
Date: Tue, 10 Sep 2024 10:21:43 +0200
Subject: [PATCH 3/7] Update spec/CloudCode.spec.js
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: Manuel <5673677+mtrezza@users.noreply.github.com>
Signed-off-by: Kartal Kaan Bozdoğan <kartalkaanbozdogan@gmail.com>
---
 spec/CloudCode.spec.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js
index d350d05c9f..67c4211f7b 100644
--- a/spec/CloudCode.spec.js
+++ b/spec/CloudCode.spec.js
@@ -1192,8 +1192,8 @@ describe('Cloud Code', () => {
   });
 
   it('should not have user roles for anonymous calls', async () => {
-    let beforeSaveFlag = false,
-      afterSaveFlag = false;
+    let beforeSaveFlag = false;
+    let afterSaveFlag = false;
     Parse.Cloud.beforeSave('SaveTriggerUserRoles', async function (req) {
       expect(req.getRoles).toBeUndefined();
       beforeSaveFlag = true;

From 2317a95c434eff07397bcbeda5d478b9c0fdc354 Mon Sep 17 00:00:00 2001
From: Kartal Kaan Bozdogan <kartalkaanbozdogan@gmail.com>
Date: Tue, 10 Sep 2024 10:27:26 +0200
Subject: [PATCH 4/7] Use arrow functions

---
 spec/CloudCode.spec.js | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js
index 67c4211f7b..291421232c 100644
--- a/spec/CloudCode.spec.js
+++ b/spec/CloudCode.spec.js
@@ -1166,12 +1166,12 @@ describe('Cloud Code', () => {
   it('test save triggers get user roles', async () => {
     let beforeSaveFlag = false;
     let afterSaveFlag = false;
-    Parse.Cloud.beforeSave('SaveTriggerUserRoles', async function (req) {
+    Parse.Cloud.beforeSave('SaveTriggerUserRoles', async req => {
       expect(await req.getRoles()).toEqual(['TestRole']);
       beforeSaveFlag = true;
     });
 
-    Parse.Cloud.afterSave('SaveTriggerUserRoles', async function (req) {
+    Parse.Cloud.afterSave('SaveTriggerUserRoles', async req => {
       expect(await req.getRoles()).toEqual(['TestRole']);
       afterSaveFlag = true;
     });
@@ -1194,12 +1194,12 @@ describe('Cloud Code', () => {
   it('should not have user roles for anonymous calls', async () => {
     let beforeSaveFlag = false;
     let afterSaveFlag = false;
-    Parse.Cloud.beforeSave('SaveTriggerUserRoles', async function (req) {
+    Parse.Cloud.beforeSave('SaveTriggerUserRoles', async req => {
       expect(req.getRoles).toBeUndefined();
       beforeSaveFlag = true;
     });
 
-    Parse.Cloud.afterSave('SaveTriggerUserRoles', async function (req) {
+    Parse.Cloud.afterSave('SaveTriggerUserRoles', async req => {
       expect(req.getRoles).toBeUndefined();
       afterSaveFlag = true;
     });
@@ -2064,7 +2064,7 @@ describe('cloud functions', () => {
 
   it('should have user roles', async () => {
     let flag = false;
-    Parse.Cloud.define('myFunction', async function (req) {
+    Parse.Cloud.define('myFunction', async req => {
       expect(await req.getRoles()).toEqual(['TestRole']);
       flag = true;
     });
@@ -2084,7 +2084,7 @@ describe('cloud functions', () => {
 
   it('should not have user roles for anonymous calls', async () => {
     let flag = false;
-    Parse.Cloud.define('myFunction', async function (req) {
+    Parse.Cloud.define('myFunction', async req => {
       expect(req.getRoles).toBeUndefined();
       flag = true;
     });

From cbf3eb14fe6ff7631f29110ef75857695c1b5af2 Mon Sep 17 00:00:00 2001
From: Kartal Kaan Bozdogan <kartalkaanbozdogan@gmail.com>
Date: Wed, 25 Sep 2024 15:39:33 +0200
Subject: [PATCH 5/7] Renamed getRoles() -> getUserRoles()

---
 spec/CloudCode.spec.js         | 12 ++++++------
 src/Routers/FunctionsRouter.js |  6 +++---
 src/triggers.js                |  2 +-
 3 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js
index 291421232c..f06391d818 100644
--- a/spec/CloudCode.spec.js
+++ b/spec/CloudCode.spec.js
@@ -1167,12 +1167,12 @@ describe('Cloud Code', () => {
     let beforeSaveFlag = false;
     let afterSaveFlag = false;
     Parse.Cloud.beforeSave('SaveTriggerUserRoles', async req => {
-      expect(await req.getRoles()).toEqual(['TestRole']);
+      expect(await req.getUserRoles()).toEqual(['TestRole']);
       beforeSaveFlag = true;
     });
 
     Parse.Cloud.afterSave('SaveTriggerUserRoles', async req => {
-      expect(await req.getRoles()).toEqual(['TestRole']);
+      expect(await req.getUserRoles()).toEqual(['TestRole']);
       afterSaveFlag = true;
     });
 
@@ -1195,12 +1195,12 @@ describe('Cloud Code', () => {
     let beforeSaveFlag = false;
     let afterSaveFlag = false;
     Parse.Cloud.beforeSave('SaveTriggerUserRoles', async req => {
-      expect(req.getRoles).toBeUndefined();
+      expect(req.getUserRoles).toBeUndefined();
       beforeSaveFlag = true;
     });
 
     Parse.Cloud.afterSave('SaveTriggerUserRoles', async req => {
-      expect(req.getRoles).toBeUndefined();
+      expect(req.getUserRoles).toBeUndefined();
       afterSaveFlag = true;
     });
 
@@ -2065,7 +2065,7 @@ describe('cloud functions', () => {
   it('should have user roles', async () => {
     let flag = false;
     Parse.Cloud.define('myFunction', async req => {
-      expect(await req.getRoles()).toEqual(['TestRole']);
+      expect(await req.getUserRoles()).toEqual(['TestRole']);
       flag = true;
     });
 
@@ -2085,7 +2085,7 @@ describe('cloud functions', () => {
   it('should not have user roles for anonymous calls', async () => {
     let flag = false;
     Parse.Cloud.define('myFunction', async req => {
-      expect(req.getRoles).toBeUndefined();
+      expect(req.getUserRoles).toBeUndefined();
       flag = true;
     });
 
diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js
index d02d983fa3..3c80ccb98a 100644
--- a/src/Routers/FunctionsRouter.js
+++ b/src/Routers/FunctionsRouter.js
@@ -131,11 +131,11 @@ export class FunctionsRouter extends PromiseRouter {
       params: params,
       master: req.auth && req.auth.isMaster,
       user: req.auth && req.auth.user,
-      getRoles:
+      getUserRoles:
         req.auth && req.auth.user
           ? async () => {
-            return (await req.auth.getUserRoles()).map(r => r.substr('role:'.length));
-          }
+              return (await req.auth.getUserRoles()).map(r => r.substr('role:'.length));
+            }
           : undefined,
       installationId: req.info.installationId,
       log: req.config.loggerController,
diff --git a/src/triggers.js b/src/triggers.js
index 888f6b4361..e16b645de1 100644
--- a/src/triggers.js
+++ b/src/triggers.js
@@ -292,7 +292,7 @@ export function getRequestObject(
   }
   if (auth.user) {
     request['user'] = auth.user;
-    request['getRoles'] = async () => {
+    request['getUserRoles'] = async () => {
       return (await auth.getUserRoles()).map(r => r.substr('role:'.length));
     };
   }

From 0cbeba614fb1fbd14364c3cd7322ad4e145c84f1 Mon Sep 17 00:00:00 2001
From: Kartal Kaan Bozdogan <kartalkaanbozdogan@gmail.com>
Date: Wed, 25 Sep 2024 16:50:27 +0200
Subject: [PATCH 6/7] Also test that getUserRoles is recursive

---
 spec/CloudCode.spec.js | 24 +++++++++++++++---------
 1 file changed, 15 insertions(+), 9 deletions(-)

diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js
index f06391d818..2de059f38a 100644
--- a/spec/CloudCode.spec.js
+++ b/spec/CloudCode.spec.js
@@ -1167,12 +1167,12 @@ describe('Cloud Code', () => {
     let beforeSaveFlag = false;
     let afterSaveFlag = false;
     Parse.Cloud.beforeSave('SaveTriggerUserRoles', async req => {
-      expect(await req.getUserRoles()).toEqual(['TestRole']);
+      expect(new Set(await req.getUserRoles())).toEqual(new Set(['TestRole1', 'TestRole2']));
       beforeSaveFlag = true;
     });
 
     Parse.Cloud.afterSave('SaveTriggerUserRoles', async req => {
-      expect(await req.getUserRoles()).toEqual(['TestRole']);
+      expect(new Set(await req.getUserRoles())).toEqual(new Set(['TestRole1', 'TestRole2']));
       afterSaveFlag = true;
     });
 
@@ -1181,9 +1181,12 @@ describe('Cloud Code', () => {
     user.set('email', 'asdf@example.com');
     user.set('username', 'zxcv');
     await user.signUp();
-    const role = new Parse.Role('TestRole', new Parse.ACL({ '*': { read: true, write: true } }));
-    role.getUsers().add(user);
-    await role.save();
+    const role1 = new Parse.Role('TestRole1', new Parse.ACL({ '*': { read: true, write: true } }));
+    const role2 = new Parse.Role('TestRole2', new Parse.ACL({ '*': { read: true, write: true } }));
+    await role1.save();
+    role2.getRoles().add(role1);
+    role1.getUsers().add(user);
+    await Parse.Object.saveAll([role1, role2]);
 
     const obj = new Parse.Object('SaveTriggerUserRoles');
     await obj.save();
@@ -2065,7 +2068,7 @@ describe('cloud functions', () => {
   it('should have user roles', async () => {
     let flag = false;
     Parse.Cloud.define('myFunction', async req => {
-      expect(await req.getUserRoles()).toEqual(['TestRole']);
+      expect(new Set(await req.getUserRoles())).toEqual(new Set(['TestRole1', 'TestRole2']));
       flag = true;
     });
 
@@ -2074,9 +2077,12 @@ describe('cloud functions', () => {
     user.set('email', 'asdf@example.com');
     user.set('username', 'zxcv');
     await user.signUp();
-    const role = new Parse.Role('TestRole', new Parse.ACL({ '*': { read: true, write: true } }));
-    role.getUsers().add(user);
-    await role.save();
+    const role1 = new Parse.Role('TestRole1', new Parse.ACL({ '*': { read: true, write: true } }));
+    const role2 = new Parse.Role('TestRole2', new Parse.ACL({ '*': { read: true, write: true } }));
+    await role1.save();
+    role2.getRoles().add(role1);
+    role1.getUsers().add(user);
+    await Parse.Object.saveAll([role1, role2]);
 
     await Parse.Cloud.run('myFunction', { sessionToken: user.getSessionToken() });
     expect(flag).toBeTrue();

From dee0d1fbc3cbc3e95bb359f8a8d7715c9c8ef68e Mon Sep 17 00:00:00 2001
From: Kartal Kaan Bozdogan <kartalkaanbozdogan@gmail.com>
Date: Wed, 25 Sep 2024 17:03:38 +0200
Subject: [PATCH 7/7] Added a new util, stripACLRolePrefix

---
 src/Routers/FunctionsRouter.js |  3 ++-
 src/Utils.js                   | 12 ++++++++++++
 src/triggers.js                |  3 ++-
 3 files changed, 16 insertions(+), 2 deletions(-)

diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js
index 3c80ccb98a..301b3cb123 100644
--- a/src/Routers/FunctionsRouter.js
+++ b/src/Routers/FunctionsRouter.js
@@ -8,6 +8,7 @@ import { promiseEnforceMasterKeyAccess, promiseEnsureIdempotency } from '../midd
 import { jobStatusHandler } from '../StatusHandler';
 import _ from 'lodash';
 import { logger } from '../logger';
+import Utils from '../Utils';
 
 function parseObject(obj, config) {
   if (Array.isArray(obj)) {
@@ -134,7 +135,7 @@ export class FunctionsRouter extends PromiseRouter {
       getUserRoles:
         req.auth && req.auth.user
           ? async () => {
-              return (await req.auth.getUserRoles()).map(r => r.substr('role:'.length));
+              return (await req.auth.getUserRoles()).map(Utils.stripACLRolePrefix);
             }
           : undefined,
       installationId: req.info.installationId,
diff --git a/src/Utils.js b/src/Utils.js
index b77a3d85d7..1466cc1449 100644
--- a/src/Utils.js
+++ b/src/Utils.js
@@ -399,6 +399,18 @@ class Utils {
     }
     return obj;
   }
+
+  /**
+   * Strips the "role:" prefix from the role name as it appears in the ACL.
+   *
+   * @param {String} entry The role name prefixed with the string "role:".
+   * @returns {String} The role name, without the "role": prefix.
+   * @example
+   * stripACLRolePrefix("role:myrole") // Returns "myrole"
+   */
+  static stripACLRolePrefix(entry) {
+    return entry.substr(5 /* 'role:'.length */);
+  }
 }
 
 module.exports = Utils;
diff --git a/src/triggers.js b/src/triggers.js
index e16b645de1..3e3db04a7c 100644
--- a/src/triggers.js
+++ b/src/triggers.js
@@ -1,6 +1,7 @@
 // triggers.js
 import Parse from 'parse/node';
 import { logger } from './logger';
+import Utils from './Utils';
 
 export const Types = {
   beforeLogin: 'beforeLogin',
@@ -293,7 +294,7 @@ export function getRequestObject(
   if (auth.user) {
     request['user'] = auth.user;
     request['getUserRoles'] = async () => {
-      return (await auth.getUserRoles()).map(r => r.substr('role:'.length));
+      return (await auth.getUserRoles()).map(Utils.stripACLRolePrefix);
     };
   }
   if (auth.installationId) {