There are a lot of different ways to do permissions, but here are some pointers.
Bear in mind there are two programs here: the administrator interface, which lets an admin set up users, groups, and permissions; and the main program, which needs to know who has which permissions.
Start with the main program
Permissions get set up only occasionally. But they get read over and over. Reading is far more important in this regard.
You will end up adding this sort of code to nearly every page/form:
bool ok = HasPermission(user, permissionCode);
AssertHasPermission(user, permissionCode, Action callbackIfFailed);
the latter could be useful if called like this:
AssertHasPermission(context.User, "108", () => context.Redirect("~/Error.html"));
You want this code to run very efficiently. Whatever data structure you use, it must be simple; take a user and permission and see if it exists. I suggest a flat/denormalized structure. At run-time, it should not be necessary to walk a tree, for example, or apply cascading rules.
If your requirements include group membership and group permissions, implement those later... see below.
A feature might not be a feature
To an end user, a "feature" might be composed of several functions in code. For this reason, you need at least one layer of indirection between a permission and a code base. That is why in the example above I am passing a permission code and not, say, a page name. You will need this abstraction in case you need to add more pages to a feature, or if a page has to be split up into several features (e.g. read and write for the same domain object).
Groups, cascading permissions, and add/subtract rules
Sounds like your requirements include group permissions. This sort of thing is almost cosmetic-- it is mainly to make things easier for an end user, i.e. manage several users in lockstep by putting them in a group. But then there are exceptions; maybe you need the ability to override group permissions for certain members, for example, or you need two different permissions to access a single feature. You can drive yourself mad with the options.
The important thing is-- keep these options out of the main code base. Your main code shouldn't care if a user is in a group; it only matters if he has the right permission code after all group rules are applied.
I would suggest that, after accepting input from the administrator, the admin program should maintain a separate structure for groups and users, then walk the structure and compose the flat structure that will be used by the main program. You can think of this as "compiling" or denormalizing.
If you do this correctly, you will save yourself a lot of work. Later on, if your concepts of group membership and group permissions change (e.g. you add blacklisting as well as whitelisting, or you integrate with Active Directory groups, etc.), the computed permissions list should still remain the same old flat structure in the end. That way you can tweak the admin UI to your heart's content and your main code base won't require any modification.
In my experience, people get very creative when coming up with possibilities for managing permissions. In almost every single case I have seen this, the resulting implementation is way more complex than is actually needed. Keep things simple. Don't try to include every imaginable use case. Instead, focus on extensibility, and remember you can always improve the group structures without impacting the main program.