Skip to content

Add ability to deactivate a role (Enable/Disable) #4821

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
316 changes: 316 additions & 0 deletions spec/ParseRole.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -527,4 +527,320 @@ describe('Parse Role testing', () => {
});
});
});

it('should be able to create an enabled role (#4591)', (done) => {
const roleACL = new Parse.ACL();
roleACL.setPublicReadAccess(true);
const role = new Parse.Role('some_active_role', roleACL);
role.set('enabled', true);
role.save({}, {useMasterKey : true})
.then((savedRole)=>{
expect(savedRole.get('enabled')).toEqual(true);
const query = new Parse.Query('_Role');
return query.find({ useMasterKey: true });
}).then((roles) => {
expect(roles.length).toEqual(1);
const role = roles[0];
expect(role.get('enabled')).toEqual(true);
done();
});
});

it('should be able to create a disabled role (#4591)', (done) => {
const roleACL = new Parse.ACL();
roleACL.setPublicReadAccess(true);
const role = new Parse.Role('some_disabled_role', roleACL);
role.set('enabled', false);
role.save({}, {useMasterKey : true})
.then(() => {
expect(role.get('enabled')).toEqual(false);

const query = new Parse.Query('_Role');
return query.find({ useMasterKey: true });
}).then((roles) => {
expect(roles.length).toEqual(1);
const role = roles[0];
expect(role.get('enabled')).toEqual(false);
done();
});
});

it('should create an enabled role by default (#4591)', (done) => {
const roleACL = new Parse.ACL();
roleACL.setPublicReadAccess(true);
const role = new Parse.Role('some_active_role', roleACL);
role.save({}, {useMasterKey : true})
.then(() => {
expect(role.get('enabled')).toEqual(true);

const query = new Parse.Query('_Role');
return query.find({ useMasterKey: true });
}).then((roles) => {
expect(roles.length).toEqual(1);
const role = roles[0];
expect(role.get('enabled')).toEqual(true);
done();
});
});

it('should properly handle enabled/disabled role states permissions across multiple role levels properly (#4591)', (done) => {
// Owners inherit from Collaborators
// Collaborators inherit from members
// Members does not inherit from any role
// Owner -> Collaborator -> member -> [protected objects]
// If any role is disabled, the remaining role link tree is broken.
const owner = new Parse.User();
const collaborator = new Parse.User();
const member = new Parse.User();
let ownerRole, collaboratorRole, memberRole;
let objectOnlyForOwners; // Acl access by owners only
let objectOnlyForCollaborators; // Acl access by collaborators only
let objectOnlyForMembers; // Acl access by members only
let ownerACL, collaboratorACL, memberACL;

return owner.save({ username: 'owner', password: 'pass' })
.then(() => collaborator.save({ username: 'collaborator', password: 'pass' }))
.then(() => member.save({ username: 'member', password: 'pass' }))
.then(() => {
ownerACL = new Parse.ACL();
ownerACL.setRoleReadAccess("ownerRole", true);
ownerACL.setRoleWriteAccess("ownerRole", true);
ownerRole = new Parse.Role('ownerRole', ownerACL);
ownerRole.getUsers().add(owner);
return ownerRole.save({}, { useMasterKey: true });
}).then(() => {
collaboratorACL = new Parse.ACL();
collaboratorACL.setRoleReadAccess('collaboratorRole', true);
collaboratorACL.setRoleWriteAccess('collaboratorRole', true);
collaboratorRole = new Parse.Role('collaboratorRole', collaboratorACL);
collaboratorRole.getUsers().add(collaborator);
// owners inherit from collaborators
collaboratorRole.getRoles().add(ownerRole);
return collaboratorRole.save({}, { useMasterKey: true });
}).then(() => {
memberACL = new Parse.ACL();
memberACL.setRoleReadAccess('memberRole', true);
memberRole = new Parse.Role('memberRole', memberACL);
memberRole.set('enabled', false); // Disabled!!
memberRole.getUsers().add(member);
// collaborators inherit from members
memberRole.getRoles().add(collaboratorRole);
return memberRole.save({}, { useMasterKey: true });
}).then(() => {
// routine check
const query = new Parse.Query('_Role');
return query.find({ useMasterKey: true });
}).then((x) => {
expect(x.length).toEqual(3);
x.forEach(role => {
if(role.name === "ownerRole") expect(role.get('enabled').toBeEqual(true));
if(role.name === "collaboratorRole") expect(role.get('enabled').toBeEqual(true));
if(role.name === "memberRole") expect(role.get('enabled').toBeEqual(false));
});

const acl = new Parse.ACL();
acl.setRoleReadAccess("memberRole", true);
acl.setRoleWriteAccess("memberRole", true);
objectOnlyForMembers = new Parse.Object('TestObjectRoles');
objectOnlyForMembers.setACL(acl);
return objectOnlyForMembers.save(null, { useMasterKey: true });
}).then(() => {
const acl = new Parse.ACL();
acl.setRoleReadAccess("collaboratorRole", true);
acl.setRoleWriteAccess("collaboratorRole", true);
objectOnlyForCollaborators = new Parse.Object('TestObjectRoles');
objectOnlyForCollaborators.setACL(acl);
return objectOnlyForCollaborators.save(null, { useMasterKey: true });
}).then(() => {
const acl = new Parse.ACL();
acl.setRoleReadAccess("ownerRole", true);
acl.setRoleWriteAccess("ownerRole", true);
objectOnlyForOwners = new Parse.Object('TestObjectRoles');
objectOnlyForOwners.setACL(acl);
return objectOnlyForOwners.save(null, { useMasterKey: true });
})

.then(() => {
// First level role - members should not be able to edit object when their role is disabled
objectOnlyForMembers.set('hello', 'hello');
return objectOnlyForMembers.save(null, { sessionToken: member.getSessionToken() });
}).then(() => {
fail('A disabled role cannot grant permission to its users. (Level-0)');
done()
}, (error) => {
expect(error.code).toEqual(101);
return Promise.resolve()
})

.then(() => {
// Second level role - collaborators should not be able to edit object when member role is disabled
objectOnlyForMembers.set('hello', 'hello');
return objectOnlyForMembers.save(null, { sessionToken: collaborator.getSessionToken() });
}).then(() => {
fail('A disabled role cannot grant permission to its child roles. (Level-1)');
done()
}, (error) => {
expect(error.code).toEqual(101);
return Promise.resolve()
})

.then(() => {
// Third level role - admins should not be able to edit object when member role is disabled
return objectOnlyForMembers.save(null, { sessionToken: owner.getSessionToken() });
}).then(() => {
fail('A disabled role cannot grant permission to its child roles. (Level-2)');
done()
}, (error) => {
expect(error.code).toEqual(101);
return Promise.resolve()
})

.then(() => {
// Owners should be able to inherit form collaborator role and edit object
objectOnlyForCollaborators.set('hello', 'hello');
return objectOnlyForCollaborators.save(null, { sessionToken: owner.getSessionToken() });
}).then(() => {
return Promise.resolve()
}, () => {
fail('Enabled roles should grant permissions to child roles normally.');
done()
})

.then(() => {
// Set members enabled and collaborators to disabled
// Members should be able to edit. Collaborators and Owners should not.
memberRole.set('enabled', true);
collaboratorRole.set('enabled', false);
return memberRole.save({}, {useMasterKey: true}).then(() => collaboratorRole.save({}, {useMasterKey: true}));
}).then(() => {
// this should succeed
objectOnlyForMembers.set('hello', 'hello');
return objectOnlyForMembers.save(null, { sessionToken: member.getSessionToken() });
}, () => {
fail('Enabled roles should grant permissions to its users.');
done()
})
.then(() => {
expect(objectOnlyForMembers.get('hello')).toEqual('hello');
// this should fail, collaborator should not be able to edit, since their role is disabled
objectOnlyForMembers.unset('hello');
return objectOnlyForMembers.save(null, { sessionToken: collaborator.getSessionToken() });
})
.then(() => {
fail('Disabled roles cannot not grant permission ot its users');
done();
}, (error) => {
expect(error.code).toEqual(101);
return Promise.resolve()
})
.then(() => {
// this should fail
return objectOnlyForMembers.save(null, { sessionToken: owner.getSessionToken() });
}).then(() => {
fail('Disabled roles cannot not grant permission to its children roles');
done()
}, (error) => {
expect(error.code).toEqual(101);
return Promise.resolve()
})

// Extra uneeded check
.then(() => {
// Check that role tree operate normally in enabled/disabled state.
// Collaborators should not be able to edit admin role protected objects.
collaboratorRole.set('enabled', true);
ownerRole.set('enabled', false);
return ownerRole.save({}, {useMasterKey: true}).then(() => collaboratorRole.save({}, {useMasterKey: true}));
}).then(() => {
objectOnlyForOwners.unset('hello');
return objectOnlyForOwners.save(null, { sessionToken: collaborator.getSessionToken() });
}).then(() => {
fail('Roles do not work this way. Child inherits from parent, not the other way around');
done()
}, (error) => {
expect(error.code).toEqual(101);
return Promise.resolve()
})

.then(() => {
done();
});
});

it('parent role should still be able to edit roles that it has disabled and have R/W access to (#4591)', (done) => {
const admin = new Parse.User();
const member = new Parse.User();
let adminRole, membersRole;
let adminACL, memberACL;

return admin.save({ username: 'admin', password: 'pass' })
.then(() => member.save({ username: 'member', password: 'pass' }))
.then(() => {
adminACL = new Parse.ACL();
adminACL.setRoleReadAccess("ownerRole", true);
adminACL.setRoleWriteAccess("ownerRole", true);
adminRole = new Parse.Role('ownerRole', adminACL);
adminRole.getUsers().add(admin);
return adminRole.save({}, { useMasterKey: true });
}).then(() => {
memberACL = new Parse.ACL();
memberACL.setRoleReadAccess('collaboratorRole', true);
// admin can write on this role
memberACL.setRoleWriteAccess('ownerRole', true);
membersRole = new Parse.Role('collaboratorRole', memberACL);
membersRole.getUsers().add(member);
// admins inherit from members
membersRole.getRoles().add(adminRole);
return membersRole.save({}, { useMasterKey: true });
}).then(() => {
// admins should be able to edit members when members are enabled
membersRole.set('enabled', false)
return membersRole.save(null, { sessionToken: admin.getSessionToken() });
}).then(() => {
return Promise.resolve()
}, () => {
fail('parent role should be able to edit child roles when enabled child roles are enabled');
return Promise.resolve()
})
.then(() => {
// admins should be able to edit members even when members role is disabled
membersRole.set('enabled', true)
return membersRole.save(null, { sessionToken: admin.getSessionToken() });
}).then(() => {
return Promise.resolve()
}, () => {
fail('parent role should be able to edit child roles when enabled child roles are disabled');
return Promise.resolve()
})
.then(() => {
done();
});
});

it('disabled roles cannot edit themselves even with R/W access (#4591)', (done) => {
const member = new Parse.User();
let role;
let roleACL;

return member.save({ username: 'member', password: 'pass' })
.then(() => {
roleACL = new Parse.ACL();
roleACL.setRoleReadAccess("ownerRole", true);
roleACL.setRoleWriteAccess("ownerRole", true);
role = new Parse.Role('ownerRole', roleACL);
role.getUsers().add(member);
role.set('enabled', false);
return role.save({}, { useMasterKey: true });
}).then(() => {
role.set('enabled', true)
return role.save(null, { sessionToken: member.getSessionToken() });
}).then(() => {
fail('disabled role should not grand permission to its users, even for itself');
done();
}, (error) => {
expect(error.code).toEqual(101);
done()
})
});

});
8 changes: 8 additions & 0 deletions src/Auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ Auth.prototype._loadRoles = function() {
__type: 'Pointer',
className: '_User',
objectId: this.user.id
},
// By using $ne instead of $eq=true we support earlier versions of parse where the 'enabled' field might be undefined
// $ne should not affect performance since Roles are often fetched from cache
'enabled': {
'$ne' : false
}
};
// First get the role ids this user is directly a member of
Expand Down Expand Up @@ -191,6 +196,9 @@ Auth.prototype._getAllRolesNamesForRoleIds = function(roleIDs, names = [], queri
} else {
restWhere = { 'roles': { '$in': ins }}
}
// Always make sure roles are enabled
restWhere.enabled = { '$ne' : false }

const query = new RestQuery(this.config, master(this.config), '_Role', restWhere, {});
return query.execute().then((response) => {
var results = response.results;
Expand Down
3 changes: 2 additions & 1 deletion src/Controllers/SchemaController.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ const defaultColumns: {[string]: SchemaFields} = Object.freeze({
_Role: {
"name": {type:'String'},
"users": {type:'Relation', targetClass:'_User'},
"roles": {type:'Relation', targetClass:'_Role'}
"roles": {type:'Relation', targetClass:'_Role'},
"enabled": {type:'Boolean'}
},
// The additional default columns for the _Session collection (in addition to DefaultCols)
_Session: {
Expand Down
17 changes: 15 additions & 2 deletions src/RestWrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,16 @@ RestWrite.prototype.setRequiredFieldsIfNeeded = function() {
if (!this.query) {
this.data.createdAt = this.updatedAt;

// Only assign new objectId if we are creating new object
if (!this.data.objectId) {
const creatingObject = !this.data.objectId;
if (creatingObject) {
// Only assign new objectId if we are creating new object
this.data.objectId = cryptoUtils.newObjectId(this.config.objectIdSize);
// On Role, assume is not disabled, and assign 'enabled' to true
if(this.className === '_Role'){
if(this.data.enabled === undefined){
this.data.enabled = true;
}
}
}
}
}
Expand Down Expand Up @@ -1086,6 +1093,12 @@ RestWrite.prototype.runDatabaseOperation = function() {
response.objectId = this.data.objectId;
response.createdAt = this.data.createdAt;

// (#4591) Role "enabled" key is generated with on create, and SDKs might not support this change yet.
// removing this line will brake RoleTest ("should create an enabled role by default")
if(this.className === "_Role" && !this.query){
response.enabled = this.data.enabled;
}

if (this.responseShouldHaveUsername) {
response.username = this.data.username;
}
Expand Down