Rephrasing the problem a bit:
- I have an instance of
MySpecialObject
- an instance of
JLP
wants to call the methods of MySpecialObject
- an instance of
BEV
wants to read the public data of MySpecialObject
- the instance of JLP shouldn't be able to read public data and the
BEV
shouldn't be able to call member functions
This should be as type-safe as possible.
A solution I see involves wrappers :)
- you write the code for
MySpecialObject
as you would normally, ignoring this extra requirement
- you write a wrapper for each aspect of your class you wish to restrict. Say,
MySpecialObject_Read
and MySpecialObject_Execute
.
- these wrappers forward the requests (method calls, getter/setters) to an underlying
shared_ptr
of MySpecialObject
- your
MySpecialObject
, MySpecialObject_Read
and MySpecialObject_Execute
classes each have (as needed) a "convert to ..." method. This method returns an appropriate wrapper over the underlying MySpecialObject
This solution provides a type-safe accessor with the desired limitations.
Each client class can choose what kind of access it needs.
(And since you write these classes, you know what access you need.)
If that is not acceptable, you could add factories that only create wrappers with certain limitations depending on a "token". That token might be the RTTI information of the calling class instance, although this could be abused.
Does this solve the original problem?
EDIT:
Remember that since you are the programmer you can instantiate an A
whenever you want. By adding your class to a friend
list or whatever...
What this solution provides is a clearer interface. Removing the explicit conversions probably makes it safer but less flexible. But, since they are explicit, you can easily look for them in the code and treat them as signs your architecture has a flaw somewhere.
Specifically, you can have code like this:
shared_ptr<A> * a = new A(parameters);
A_read aRead(a);
A_execute aExec(a);
A_write aWrite(aExec);
logger->Log(aRead);
view->SetUserData(aWrite);
controller->Execute(aExec);
Here there is an explicit conversion between an execute wrapper and a write wrapper, but you can decide on this based on your specific requirements.
But, with little effort (knowing which conversions are valid), just by looking at the call locations you can see that (with confidence!):
- the
logger
will not change the state of your A
- the
view
will not call methods on your A
(other than setters)
This is true even if those particular method calls end up calling hundreds of other methods, more than you'd like to examine by hand.
At the cost of a few thin wrappers you gain the ability to see at a glance what a particular function call will do with the parameters you send. This may help a lot during debugging by helping you eliminate some branches from your investigation and, in general, would help people trying to understand the program.
I couldn't really find other reasons for using this ACL idea, at least ones where the costs don't outweigh the benefits. However, it does seem more intuitive than the visitor solution mentioned in the other answer.
The guardian shold have a
public boolean checkPermission(String permissionTag, int userID);
method.
That way the guardian doesn't have to change when new permissions come to exist.
Usage
if (guardian.checkPermission("PLACE_ORDER",loggedUser)) {
// place order
}
Best Answer
1.Organize the permissions and their control
Indeed, if you define individual feature access permissions, you will have, as in feature toggles, an
if
-clause in each feature to check if the user has the right permission.If you abstract the feature from the permission, for example by using a level of indirection, such as a feature group (e.g. one feature belongs to a configurable group) or an autorisation profile (e.g. several user belongs to one profile, which has several feature permissions), you will still need an if clause for each feature.
In order to make this easy to handle, you should in anyway encapsulate the permission check in a permission object. This would allow you to implement the link between the feature and the user with any of the above mentioned strategy, without having to change the code:
or
2.Control features access upfront
If the features are independent functions, you could control access upfront.
This means that the UI shall ensure that user can only invoke fonctions and commands for which the user has permission. This allows to isolate permission controls in your architecture. The various feature will then not have to implement additional checks.
Unfortunately, this will not work if the feature is optional, i.e. the user can invoke a function, and the function is executed differently if the feature is there or not.
Manage features as a strategy
This approach uses runtime strategies. The strategy either invokes a feature or nothing. The
Context
would in this case be a kind of user profile object that is initialized at login. For each feature there would be a strategy (this could remain flexible, by using an associative container such as a feature map). The strategies would be initialized depending on the user permissions.Later on, when the different functions of the software are executed, they would use the relevant strategy (e.g. either an object that will execute the right feature, or an empty strategy object that does nothing).
This approach seems rather elegant. However, instead of adding an
if
in the code of each feature, it would require to implement each feature as a self-contained strategy, which might be extremely challenging.So what ?
Personally, even if the second seems more elegant, I'd go for option 1: this is much more flexible. Proper encapsulation of the permission can reduce the permission control in the feature to a single statement.