Re: Restricting functionality on objects: "remote access proxy" (pattern)
- From: "H. S. Lahman" <h.lahman@xxxxxxxxxxx>
- Date: Wed, 22 Aug 2007 20:59:25 GMT
Responding to Wutzke...
2. The list of commands generated via some CommandFactory (the objects
that would do something when a button gets pressed) is limited by the
user's (or some other) level. A normal user can't instantiate a
"BanCommand" for example. The command objects act as messenger
objects, but nevertheless those objects would still call the public
unfiltered methods on that interface.
You could use the Command pattern to check privileges, but that is a
pretty complicated way to do a simple task.
I wonder what you had in mind by saying complicated. The client knows
about the current user's access rights in a chat. This "access state"
is dynamic, that is it can change while chatting, e.g. when an
operator up- or degrades your level (admin, superop, owner, op,
tempop, normal). The state of the current users chat (one of many
active chats) can be set with something like setLevel(...). When that
is done, the chat will appear do be a little different, either some
functionality was added or some was removed.
I mean that the Command pattern requires abstracting the notion of a 'command' explicitly as an object rather than just responsibilities of some already abstracted object. In the solution I suggested one captures the entire privileges in a single attribute value. (I am assuming there are other reasons for having a UserSurrogate (or equivalent) object, such as keeping track of user identity.)
If the level of privilege can change during the chat, then whoever understands that change context can simply reset the privilege attribute value.
What I had in mind then was to provide Commmand objects depending on
the user's level in the chat. These commands would be called from menu
items or buttons. If the level of a user doesn't allow a command, the
respective GUI component wouldn't be created (either Command object
creation throws an exception or null is returned).
Usually UI buttons, menus, etc. are mapped into messages sent back to the subsystem/layer where the customer problem is solved. The simplest implementation is for the subsystem interface to re-dispatch those messages to objects that have the responsibilities needed to to service the user's request. I still don't see a need to insert a separate Command object between the external message and the object responsibility that services it.
With a simple attribute to describe <current> privileges, each responsibility that might care can decide what to do by simply checking that attribute value.
This is not complicated IMHO, I just wonder why I'd need a Proxy then.
Maybe only a "remote proxy", but not "remote access/protection proxy"
since the access restriction was/is already handled by the command
factory (or such).
I still don't see a need for a Proxy pattern either. However, within the subsystem that solves the main application problem, it might be convenient to have a simple minded surrogate for the software user to hold knowledge like its privileges and identity. That object would be a proxy for the real user in some sense.
3. Somehow provide different implementations of that interface by
subclassing (or else) and throwing some exception for those operations/
method calls that are not allowed with the current user's level. A
normal user calling myChat.ban(otherUser) would get such an exception.
Disadvantage of subclassing here would be, that I'd need one subclass
per user level (admin, moderator, owner, operator, normal). In case
the user's level gets upgraded, the instance/s of such chats would
have to be replaced. Looks like bad design to me.
This is close but a better idea is to provide different interfaces to
the object and only expose the less privileged clients to the interface
elements they have a right to access. That, though, becomes difficult
when the client is external to the subsystem, like a GUI. One would need
to provide the right subsystem interface to the client, but which one is
right will depend on who is logged in.
I have two problems with providing different interfaces to clients:
As I indicated, I think that solution is not very good when the clients are external to the subsystem/layer where the privileges are relevant.
1. How does the client know which Proxy to expect/use? Isn't the
purpose of the Proxy to provide a placeholder so that clients *don't*
need to know which interface to expect? Furthermore, which variable
would I assign a concrete Proxy to after it got instantiated (via some
Factory maybe)?
Proxy classes are: AdminChatContext, SuperOpChatContext,
OwnerChatContext, OpChatContext, TempOpCHatContext, NormalChatContext.
Each class going from NormalChatContext up to AdminChatContext would
add 1-n new methods.
In the client of the proxy, what local variable would I assign the
concrete proxies to to make use of their extended interfaces?
Assigning them to the base
Chat cht = SomeFactory.createOpChatFactory();
seems right at first, but I can't access the concrete class' methods
without downcasting.
Downcasting would be a Big Clue that something was wrong somewhere. B-) Despite what languages like C++ allow, generalization relations are not navigable. Like Highlanders, There Can Be Only One <object> so there is nothing to navigate.
2. Isn't adding/withdrawing functionality dynamically rather a
decorator, providing different subclasses and methods? Even when using
decorators, clients are still left with problem 1.
My own summary would (currently) be:
Using subclassing for user rights not being able to use a common
interface to all seems not to be the right approach. Maybe I missed
something, so please correct me if I'm wrong.
I agree. Again, a problem exists when the client is external. If one needs to access specialized properties of a subclass, one needs to navigate a direct relationship to the specialized subclass. But an external client would not even know the generalization existed, much less which relationship to navigate.
So that would mean one has to put enough intelligence in the subsystem interface implementation to know that certain messages can't go to certain subclasses. Then one is back to the downcasting from (1) again; it has just been moved to the subsystem interface. Worse, it is not good practice to "hide" business rules and policies (i.e., the privilege decision) in subsystem interfaces. They should only be used to resolve syntactic mismatches and provide identity mapping, not problem semantics.
The simplest way is to allow each user to identify themselves and
provide a privilege bitmap for each user from a separate <secure>
configuration source. There are many ways to do that, so the following
is only one possibility:
* creates R1 1
[UserSurrogate] ---------------- [UserFactory]
+ privilegeBitmap
| *
| accessed by
|
| R2
|
| *
[Chat]
+ getName()
+ setName()
+ getOwner()
+ setOwnder().
When a UserSurrogate is instantiated by the factory object (presumably
when someone logs into a chat session), the factory object goes to a DB
and looks up the privilegeBitmap for that particular user identifier to
initialize it. For accessors like getName and getOwner the privilege
doesn't matter so they don't do anything special. However, the setName
and setOwner accessors will access the privilegeBitmap and check if
their bit in it is set of not. If not, they generate and exception.
Right.
So what's wrong with this picture? The R2 relationship is *:*, so how
does a Chat know which UserSurrogate to access to get the bitmap? The
obvious and simplest way is for UserSurrogate to pass its bitmap value
in the message that it sends to Chat (e.g., setName(name, bitmap)). Note
that passing the bitmap is fine because that is under the control of the
subsystem in hand and it forces the mapping of privileges to Just Work
because it controls the initialization of privilegeBitmap.
[Caveat: passing the bitmap value is not the most robust solution from a
maintenance viewpoint, but I wanted to keep the example simple.]
I understood that, ok. I must pass the user's level/permissions s/he
has for the chat (the State pattern variable) to the surrogate/proxy.
Not quite. I was arguing that it is generally not a good idea for behavior messages to have data packets in OO applications. The behavior method should navigate directly to whoever owns the knowledge on an as-needed basis. That makes things much easier for managing data integrity if one needs to implement in concurrent, asynchronous, distributed, etc. environments. That's the main reason why in OOA/D knowledge access is assumed to be synchronous while behavior access is assumed to be asynchronous. One can then unambiguously implement the OOA/D model in any computing environment during OOP without change.
The issue I think you are referring to is about who sets the bitmap value in UserSurrogate. There are three choices:
(1) If the value comes from a database, the factory creating UserSurrogate can set it after reading the value from the DB.
(2) If it is set dynamically by someone in the same subsystem scope that understands the context, then a simple knowledge setter can be used when the context changes.
(3) If an external client understands the context for changing privilege, that client can send a synchronizing message to this subsystem to set the bitmap with the new value. However, that places some constraints on the client, namely to ensure the privilege update message is sent before other messages that might critically depend on the update. That may require a handshaking protocol and/or blocking in the client to ensure the service is ready to process more requests. (Fortunately such synchronization is usually trivial to enforce, but one needs to at least think about it when designing subsystem interactions.)
Instead of subclassing one could also implement this via several
Strategies, one per method. I believe the "Bridge" pattern could be
applied as well, for dynamic method implementations.
In any way, I started creating a concrete subclass of a Chat interface
called "ChatProxy" which is supposed to
handle the communication with the actual chat server (no
implementation yet). Maybe that is supposed to be the class where the
restrictions belong. Such a proxy might act as an "access proxy" but
also as a "remote proxy".
Note that UserSurrogate is essentially a proxy for the external user. It
is just less complicated than the GoF pattern because there is no exotic
dynamic substitution required here.
Hmm I don't understand that really. Can you explain a little more what
you mean why there's no dynamic substitution here? The only thing that
changes (and must change) is the user's level *per chat*. So...
You're right, I was making assumptions about the chat. I assumed that a user's privileges were applied across all chats. If the privilege is per chat, then we have a different situation. However, I still think we can provide a simpler solution:
[UserSurrogate]
+ loginName
....
| *
| accessed by
|
R2 |------------------- [Privilege]
| + privilegeBitmap
| *
[Chat]
+ chatName
+ setName()
...
You will note that I glossed over the *:* relationship in my original model because the necessary associative object was not very interesting. But when privilege depends upon specific relationships, the associative object is ideally suited to handling that.
If one uses alternative identity, such as consecutive integers, within the subsystem then the [Privilege] just becomes a lookup table with two indices:
[UserSurrogate]
+ loginName
+ userIndex
....
| *
| accessed by
|
R2 |------------------- [Privilege]
| + userIndex [R2]
| * + chatIndex [R2]
[Chat] + privilegeBitmap
+ chatName
+ chatIndex
+ setName()
...
That is, we can reify to:
[UserSurrogate]
+ loginName
+ userIndex
....
| *
| defines privileges for
|
| R2
|
| 1
[PrivilegeTable]
- privilegeBitmapArray
+ setPrivilege(chatIndex, userIndex, privilegeBitmap)
+ getPrivilege(chatIndex, userIndex)
| 1
|
| R3
|
| limits access of
| *
[Chat]
+ chatName
+ chatIndex
+ setName(userIndex, name)
Presumably the subsystem receives a request like setName(userHandle, chatHandle, name). The xxxHandles can be any identity that the interface can map into userIndex and a Chat reference via table lookup. The interface re-dispatches the request to Chat::setName for the right Chat object while providing the userIndex value of user identity. The setName() behavior then gets the privilegeBitmap from PrivilegeTable using the provided userIndex and its own chatIndex to check the privilege. The privilegeBitmapArray can be initialized to NO_ACCESS or defaults from a DB at startup.
How one handles xxxHandle is really a performance issue. There will have to be a conversion from loginName to userIndex somewhere and deciding where is a low level implementation issue. For example, one might handle it in the client UI by passing back an address or computed userIndex whenever a user logs in through the UI, much like GUI window handles are provided. The key is that the client UI doesn't need to know what it is and can't do anything with it; it just saves it for later inclusion in subsequent messages.
When a message comes from GUI (or
whatever) is will contain an identifier for the actual user. The
subsystem interface will then redirect that message to the right
UserSurrogate object who will then talk to other objects as needed.
I wonder how it's possible to use concrete interfaces from the
client... Conceptually and practically I'm reverting to implementing a
*common* interface, that is, have n subclasses for the n possible user
levels/states. Methods not allowed for the chat's user level will
(probably) throw some exception. Like this a common interface can be
accessed and depending on the instance's class will do something
meaningful or report "invalid". When the user's chat level is changed,
the proxy instance provided will be different and consequently changes
what the user can do/access. The GUI will reflect this of course.
The short answer is that you can't use concrete interfaces from the client if the client is external to the subsystem in hand. Neither subsystem's implementation should know that objects in the other subsystem even exist, much less how to access them. OO subsystem interfaces are designed to completely decouple the implementations so that they have no knowledge of each other. (A common view of OO subsystems is that they are objects on steroids and all the same good OO practices for encapsulation, implementation hiding, and whatnot still apply.) To that end OO subsystem interfaces are usually pure message interfaces (message ID, <data packet>}, commonly event-based.
However, the reality is that objects in each subsystem do actually have extended collaborations with one another, however well the messages decouple them. IOW, when a specific object in one subsystem sends a message, it must get to a particular object in the other subsystem. So one does need some sort of identity mapping between the subsystems. Hence OO subsystem interfaces provide infrastructure for the notion of identity "handles" I mentioned above.
For example, presumably you have a subsystem for managing networking that formats message in TCP/IP or whatever and provides handshaking for multiple message packets and whatnot. Embedded in each message data packet is a sender identifier and a receiver identifier. The networking subsystem has no idea what those identifiers identity. It just compares them and dutifully passes them on to another subsystem that will process the whole message once the data packets have been assembled and converted into text.
The interface to the receiving subsystem will understand the receiver identifier and perform a table lookup to get an object address. (One can obviously get clever about using consecutive integers to identify objects.) The interface then re-dispatches the message to the referenced object. If need be, another table lookup can be used to determine which method should be mapped to the message identifier. (Event-based interfaces are very efficient for this sort of mapping because the method mapping is done through an STT lookup by the receiving object.)
So essentially subsystem interfaces provide an equivalent mechanism to concrete interfaces through identity mapping infrastructure.
The command pattern also seems appropriate to use since I've written a
small framework which supports Command objects. I can also create
commands that are made up of several smaller commands. An example
would be a user to "Kick & ban" another user, effectively this could
be a KickCommmand + BanCommand.
If you already have a Command infrastructure for other reasons, then use it.
*************
There is nothing wrong with me that could
not be cured by a capful of Drano.
H. S. Lahman
hsl@xxxxxxxxxxxxxxxxx
Pathfinder Solutions
http://www.pathfindermda.com
blog: http://pathfinderpeople.blogs.com/hslahman
"Model-Based Translation: The Next Step in Agile Development". Email
info@xxxxxxxxxxxxxxxxx for your copy.
Pathfinder is hiring: http://www.pathfindermda.com/about_us/careers_pos3.php.
(888)OOA-PATH
.
- References:
- Restricting functionality on objects: "remote access proxy" (pattern)
- From: Karsten Wutzke
- Re: Restricting functionality on objects: "remote access proxy" (pattern)
- From: H. S. Lahman
- Re: Restricting functionality on objects: "remote access proxy" (pattern)
- From: Karsten Wutzke
- Restricting functionality on objects: "remote access proxy" (pattern)
- Prev by Date: Re: Confused betwen user cases and user stories
- Next by Date: Re: Shortcuts in UML activity diagrams
- Previous by thread: Re: Restricting functionality on objects: "remote access proxy" (pattern)
- Next by thread: Re: Restricting functionality on objects: "remote access proxy" (pattern)
- Index(es):