You're correct that reduce() will probably give the cleanest approach.
You can concat() together the arrays:
const allPermissions = user.roles.reduce((r, role) =>
r.concat(role.permissionGroups.reduce((r, group) =>
r.concat(group.permission), []),
[]);
Note that will actually create a new array every single time it returns, so if there are lots of roles, it could be a bit inefficient. To do it without creating a new array every time, you could use push.apply() instead:
const allPermissions = user.roles.reduce((r, role) =>
r.push.apply(r, role.permissisionGroups.reduce((r, group) =>
r.push.apply(r, group.permissions), []),
[]);
Using apply() will let you pass in the permissions as the arguments (basically the same as calling push() a lot). If you try to push an array, it'll push it as an array instead of the individual values.
If you want to take it a step further and keep one array the whole time, you can use:
const allPermissions = [];
user.roles.forEach(role =>
role.permissionGroups.forEach(group =>
allPermissions.push.apply(allPermissions, group.permissions)
)
)
This is still basically a reduce() pattern, but doesn't create lots of extra values. If you wanted to use reduce() with it, it could look like this:
const allPermissions = user.roles.reduce((r, role) =>
r.push.apply(r, role.permissionGroups.reduce((r, group) =>
r.push.apply(r, group.permissions)), r),
[]
);