Object-oriented – Filesystem like permissions for C++ type-members

cidiomsmaintainabilityobject-orientedpermissions

Abstract (tl;dr)

Please read the full question, this is awfully simplified:
How can unix file permission style restrictions be applied to inter-type data/control flows, allowing fine-grained access to some class-members for some groups of classes?

Background information

If you think about unix file systems and permissions, there is a diverse way of encoding file-access privileges of users (especially if you also consider FACL). For example, if a directory contains 3 files, they could belong to several users, and different other users may have restricted permissions:

-rwxr-xr--  jean-luc  staff    engage.sh
-rw-r-----  william   crew     roster.txt
-rw-------  beverly   beverly  patients.txt

Core idea

As you can see, depending on the groups a particular user is in, different access levels are allowed. For example, crewmembers are allowed to read roster.txt, which belongs to william, but guests who presumably do not belong to crew cannot. More importantly, the group crew can contain many people.

So I was thinking that there is some similarity to access permissions inside object oriented languages like C++ if you think of types (classes) as users. Although a function can only be executed, but not read, the rwx flags represent meaningful descriptions for class members. A data member can be read (r) and written (w) to, perhaps via accessors, while member functions may be executed (x) or not.

However, in C++ and other object oriented languages (I know of), this is more or less an all or nothing thing, if we leave out inheritance for a second; If class William makes his member Txt roster; public, everybody will see it. If he makes it private, nobody except himself will see it. He may add one or more friends, friend JeanLuc; but then they will see all his private members (the equivalent of granting user:jean-luc:rwx to all his files, in FACL lingo).
This is entirely orthogonal to inheritance — JeanLuc and William are not part of the same hierarchy, they are not related.

So the main idea would be to allow group-based access restrictions, as a generalisation of private/public. Allowing finer grained inter-class access to member functions and member data.

I believe this idiom could help maintainability/readability, as it adds additional facets to restrict interaction permissions.
As with operating systems, where this adds an important layer of security to the system, the same familiar pattern could add safety to a C++ project.

Thoughts about representation in C++

However, I'm at a loss of thinking of a good way to represent this. You could decompose William objects into several objects of subtypes: William_Crew, William_William and so forth, representing the respective groups. This seems to be horribly ugly. Another idea could be dedicated types with forwarder functions, representing the individual groups, like this:

class Crew { // group class
  // in this group are:
  friend JeanLuc;
  friend Geordi;
  friend Beverly;
  // ...
  static Txt getRoster(William*);
};
class William {
  friend Crew; // Problem: Crew has full access (rwx)
  Txt roster;
};

But each group would have to be tailored to a particular class to be used with, which would seem to be massively redundant, if the group is used by several users/classes.

Question

The approaches I provided are not great (to put it mildly), and I'm sure they wouldn't work as intended. I'm not sure if this is a novel/stupid/well-known idea, but I wonder how you could implement this with the features provided by the C++ language. Are there objective arguments why this would or would not be useful/helpful?

Best Answer

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.