Big picture:
user <-> something <-> ... <-> something <-> action
You give the permission system the starting point and the ending point, asking it whether this user can perform this action. The system loads or generates the graph in the middle and tries to find a path from one side to the other....
More details follow.
API for checking permissions was implemented as a boolean function of the following form:
CanDo(user, action)
Action was represented by a string of a particular format, e.g. "articles.edit.987". This is semi-arbitrary, as long as you stick with your convention.The function expanded user into a list of appropriate "roles".
Roles were handled in such a way that they could represent real users (user.12434) or groups (group.admins) or virtual groups (virtual.loggedIn).
The database stored information about what various roles could do in a table that had two essential columns: role id and permission pattern. This can be done in a variety of ways, depending on what storage mechanisms you use.
The resolution was as follows.
1. Find all rules for all the roles the user is in.
2. Take each pattern and match it against action supplied to canDo() function.
3. If there is a match, return true. If you exhaust the list, return false.
This setup is surprisingly flexible, as long as you understand how to use it. The trick is that you can add more entities between "user" and "action" and that will not change the API, just the resolution process. Moreover, since you effectively searching for a path in an acyclic graph between user and actions, you can search in any order and in any direction. And you can store permissions in all kinds of formats, again, without changing the application API.
All the business logic unrelated to authentication is handled in user code. E.g., if you want to allow someone to post articles on Tuesdays only, you write something like the following code:
if (TodayIsTuesday() && CanDo(currentUser, "articles.editOnTuesdays")){
//do stuff
} else {
//error
}
Technically, "pattern" for permissions could be anything. You could use a regex, but I would advice against it. If you use a simple hierarchy, you could expand it and run the match in a single DB query.