What is the distinction between roles and groups in your software? Very frequently the two are interchangeable, but without knowing how you handle them, I can't say whether you can merge them.
Now it sounds to me like you have already decided on an ACL based system. (If you weren't aware that this was a decision, read up on capability systems for a very different approach to security.) Your question is how to handle permissions.
Some years ago I built an interesting permissioning system that had some ideas that you may find interesting. It had some downsides as well. I'll describe it, and you can decide whether you want to borrow anything from it.
My central thinking was the following. Groups can belong to roles. Roles (and users) can have permissions. All of these permissions can be handed out on either a temporary or permanent basis. If you got the permission in multiple ways, you'd get the permission for the longest possible period of time.
First the positive. The users found it really easy to manage permissions. For instance a common activity would be to hand out a free trial to a company. To do that you took the accounts for that company, said that they belonged to the company, and then gave the company temporary membership in the free trial role. Voila! Later when we came out with a new product and we wanted to add it to what people on free trial got, they just added the new permission to the free trial and everyone got it.
Now the negative. Conceptually it was really clean. But whenever a permission was changed, I'd have to follow the logic to figure out all of the cascading permission changes. This piece of logic was slow. The users didn't care much - they just handed out permissions then did something else while the system chugged away. But it caused me headaches. Mostly it was my implementation, and partially it was inherent in how the system worked. In our system we had thousands of users with thousands of permissions each. Tracing through that mess of permissions to figure out everything that just changed was a challenge. If you have fewer users (hopefully not) or fewer permissions per user (quite likely), this will hopefully be less of an issue.
I actually implemented it in a database. Here are the necessary tables for the approach that I took:
// Only one of the following two is non-zero, and
// there is a unique index on the pair.
Now how did it work? Each user had a corresponding user role. At run-time
user joins to
role, joins to
role_permission (peeking at whether it has expired), joins to
permission to get all of the user's permissions. (In practice on the website for a permission check we had both the user and the permission, and this lookup was quite fast.)
In the permission panel you could look at a user or role, and it would show all of the permissions that that user directly had, when they expired (if ever), and which role they were granted through (if not granted directly). You could also add permissions, possibly with a time period set. Many of those possible permissions tied to a permission role. (For instance "free trial".)
The fun part was the logic to track through the cascade of possible changes when a permission was changed. The idea there was that I would run a query looking for permissions that needed revoking and ones that needed adding. As long as I was changing permissions, I'd do it again. That's where I ran into performance problems.
I think that if I had to do it over again with what I know now, I could solve those performance problems much better than I did then. But that logic was surprisingly tricky to get right.
Good luck on your project. Hopefully seeing how someone else tackled a problem like this can give you ideas for how you might want to do it as well.