Below is what I use for permissions
All the actions that need to be restricted...
actions
id varchar(50)
description varchar(255)
+-------------+--------------------------------------------------------------+
| id | description |
+-------------+--------------------------------------------------------------+
| VIEW_SSN | Allow a user to view a complete Social Security Number (SSN) |
| DELETE_USER | Admin: Allow a user to delete another user |
| ........... | ............................................................ |
+-------------+--------------------------------------------------------------+
The roles a user can be associated with. Generally speaking these are usually (but not always) strongly associated with the department a user is in.
roles
id unsigned int(P)
description varchar(50)
+----+-------------------+
| id | description |
+----+-------------------+
| 1 | HR: Clerk |
| 2 | HR: Manager |
| 3 | HR: Director |
| 4 | IT: Administrator |
| .. | ................. |
+----+-------------------+
Associate actions with roles. Both columns are foreign keys to their respective table and together they form the primary key. In my example data you see that the HR Clerk, HR Manager and HR Director roles can all view social security numbers while the IT Administrator can delete user accounts.
roles_actions
role_id unsigned int(F roles.id)----\__(P)
action_id varchar(255)(F actions.id)--/
+---------+-------------+
| role_id | action_id |
+---------+-------------+
| 1 | VIEW_SSN |
| 2 | VIEW_SSN |
| 3 | VIEW_SSN |
| 4 | DELETE_USER |
| ....... | ........... |
+---------+-------------+
Of course you've got your users...
users
id unsigned int(P)
username varchar(32)(U)
...
+----+----------+-----+
| id | username | ... |
+----+----------+-----+
| 1 | bob | ... |
| 2 | mary | ... |
| 3 | john | ... |
| .. | ........ | ... |
+----+----------+-----+
And finally we have to associate users with roles. Like the roles_actions table each column is a foreign key to it's respective table and together they form the primary key.
In my example data we see that bob is an HR: Manager, mary is an HR: Director and john has both HR: Director and IT: Administrator roles (so he could view social security numbers as well as delete user accounts).
users_roles
user_id unsigned int(F users.id)--\__(P)
role_id unsigned int(F roles.id)--/
+---------+---------+
| user_id | role_id |
+---------+---------+
| 1 | 2 |
| 2 | 3 |
| 3 | 3 |
| 3 | 4 |
| ....... | ....... |
+---------+---------+
To find out if a user has a particular permission do something like:
SELECT COUNT(r.action_id)
FROM users_roles u
LEFT JOIN roles_actions r ON u.role_id = r.role_id
WHERE u.user_id = :user_id and r.action_id = :action_id
Having 10K active users shouldn't be a problem. Managing 5K roles could be a huge pain but my guess is you'd never have anywhere near that many roles. I'm currently logged into a client's system and they have 2,500+ users and are only using ~35 roles. That's lower than I expected but even for an organization of 10K users I wouldn't think you'd need more than a few hundred roles.
I chose to implement permissions this way rather than through inheriting groups like you're considering because I felt this gave me more flexibility. Every role can have an unlimited number of actions and every user can have an unlimited number of roles. For the clients I've worked with the idea of a role has always coincided very closely with a job function like "Payroll Clerk" or "Accounts Receivable Manager". So if you have "Payroll Clerk", "Accountant" and "Tax Specialist" roles in the accounting department and you have a new Director of Accounting, you just give him/her those three roles and they have permission to do everything his/her employees can do - takes about 10 seconds using a good jQuery interface.
If you really do end up with too many roles to manage effectively you could add another column, group_id, and associate individual roles with particular groups. This would make it easier to find all the roles associated with the "Accounting" group for instance.
Obviously this was far too much to put in a comment :-) This kind of design may or may not work for you in your current environment - it's worked well for me in about a dozen very different businesses.