Archived

This forum has been archived. Please start a new discussion on GitHub.

Servant Locator implementation

Hi.

I want fine-grained permission-based access to operations, such that if a client does not have permission to use a particular operation a NoPermissionException will be thrown.

It looks as though I can do almost everything I need with a servant locator (determine which operation is being requested from the the Current parameter to locate, check a permissions token that would be passed as part of the context, and determine if the caller is allowed to invoke that operation), except that according to the manual the only exceptions that will be propagated are ObjectNotExist, FacetNotExist, and OperationNotExist; any other exception will be propagated as UnknownLocalException. I guess the caller could handle ULE, but it's not very pretty semantically. OTOH, if I'm careful in my locate() implementation to throw only ULE, it would be a reliable indication to the caller that permission was denied.

Opinion?

Thanks,
Mark

Comments

  • Hi Mark,

    there are several ways you could do this:

    One way is to throw an OperationNotExistException from locate(), and to (ab)use the operation name to indicate the reason. You could put something like "NoPermission <operation name>" in there. Not exactly pretty, but works.

    Another way is to define a user exception NoPermission, and add an exception specification to the operations that require a permission check. locate() then delegates the request to the appropriate servant as usual and, on entry to each operation with a permission check, you do the check and throw if necessary.

    This way is clean, in the sense that it doesn't play tricks with run-time exceptions. However, it's intrusive to the code, and not all that secure: as the code base gets modified over time, it's easy to forget to add a permission check to a new operation.

    The third way is probably cleanest: Make a session object that implements the permission check and then, if permission is granted, provides the client access to only those objects the client is allowed to use. The session can also act as a facade, so it can present a unified interface to clients for all those objects.

    The session approach is probably best because it centralizes the permission check in a single place, and it doesn't abuse run-time exceptions to indicate things they weren't meant for.

    Cheers,

    Michi.
  • Thanks, Michi.

    I thought of another way using the Servant Locator that isn't intrusive; provide a "Dummy" servant that throws "NoPermissionException" for every call. My servant locator implementation could return the dummy if the caller doesn't have permission for that call. The caller will then get the semantically correct exception.

    My servant locator implementation would be generic, my users would only have to provide a mapping of operations to permissions and the dummy implementation (most of which would be easily generated by the --slice-impl option).
  • You dummy servant would have to have as many operations as there are real operations that can throw a NoPermission exception. That's because, otherwise, you'll get an OperationNotExistException.

    If you go that way, I think that would work. (The operations could all have void return type and no parameters--the Ice run time doesn't check whether, after parameters have been unmarshaled, there is are any extra bytes remaining in the request, as far as I can remember.)

    It's a bit of hack though, IMO...

    Cheers,

    Michi.
  • michi wrote:
    Hi Mark,

    The third way is probably cleanest: Make a session object that implements the permission check and then, if permission is granted, provides the client access to only those objects the client is allowed to use. The session can also act as a facade, so it can present a unified interface to clients for all those objects.

    My goal is not to just control access to objects (I could just use an IceGrid session for that), but to control access to individual operations on an object. So, someone with read-only access can always call get operations, but not set operations, an expert can call set operations, etc. I also want to deny access to certain operations based on what side of the firewall a caller is on, so that we don't accidentally connect to running software in the real system and cause problems. And, finally, I want a master client to be able to override any other client and take control of the object. The servant locator seemed like a good place to intercept operations and block invocation; I'm pretty sure I can meet the above requirements with a generic servant locator, with what seems a relatively harmless and simple hack with the dummy object.
  • michi wrote:
    You dummy servant would have to have as many operations as there are real operations that can throw a NoPermission exception. That's because, otherwise, you'll get an OperationNotExistException.
    ...
    It seems that dynamic invocation can be used here and therefore the dummy servant need only to implement an ice_invoke() operation instead of implementing many other application-level operations.
  • Eric's suggestion would work I think. Use a blobject to throw the user exception, and have the servant locator redirect the request to the blobject if the client doesn't have permissions. That might be cleaner than making a dummy Slice interface with the union of all the operations.

    Cheers,

    Michi.
  • Note that you would have to marshal the exception, you couldn't just throw it (marshal using Dynamic Ice).

    I do not recommend this approach in general. I would use a facade, which exposes different interfaces to the server back-end, depending on permissions. The facade object could also be the session object. The facade pattern has also other advantages, such as that if the server back-end changes, you can still keep the facade, which makes your clients more immune against changes in the implementation detail of the server back-end.
  • rc_hz wrote:
    It seems that dynamic invocation can be used here and therefore the dummy servant need only to implement an ice_invoke() operation instead of implementing many other application-level operations.

    Thanks, Eric. Yes, I think you're right - I fell victim to a lack of understanding of how the object adapter actually calls onto a servant, but I've cleared that up.
  • marc wrote:
    Note that you would have to marshal the exception, you couldn't just throw it (marshal using Dynamic Ice).

    I do not recommend this approach in general. I would use a facade, which exposes different interfaces to the server back-end, depending on permissions. The facade object could also be the session object. The facade pattern has also other advantages, such as that if the server back-end changes, you can still keep the facade, which makes your clients more immune against changes in the implementation detail of the server back-end.

    I could do what I want with the facade, I agree. The issue I have with the facade idea is that it puts a burden on programmers to actually create the facade; they would not be happy. With the design I'm thinking of, I can do all the permission checks and so on with one implementation and save everybody the trouble of creating a facade.

    It seems that the servant locator concept is perfect for doing something like this;it's kind of an invocation interceptor. I know you can't predict exactly, but is there really a possibility that the invocation mechnisms would change that drastically?
  • If you use the approach outlined above, i.e., divert all calls to a dummy servant (a Blobject servant), and always marshal (not throw) a NoPermissionException in ice_invoke(), then you are well within the spec, provided that all your operations declare that they can throw such a NoPermissionException (even if the implementation doesn't throw directly). If you do not declare the NoPermissionException, then you are outside the spec. The client will not recognize such exceptions, but instead will raise an UnknownUserException.

    This is how you can do it technically. I do still not recommend it though :)

    This approach doesn't scale well. Every time one of your developers adds an operation, you have to change the implementation of your servant locator. In other words, your servant locator must know about all interfaces and all operations in your server back-end. It might appear simpler at first, but over time, it will be much harder to maintain than a facade.

    If you don't want the facade approach, then splitting the operations into different Ice objects (with different categories) would be the second best approach. The disadvantage of this is that you have to model your back-end server for access levels. A facade avoids this.

    As an analogy, think of a facade as an API for an operating system. An operating system has many internal functions, but they are exposed only through an API (which is a facade). Different categories of applications see different APIs. For example, a device driver sees a different API than a regular user application. Regardless, if the OS internals change, your APIs can still be the same.
  • marc wrote:
    The client will not recognize such exceptions, but instead will raise an UnknownUserException.
    Yes, the client will instead get an UnknownUserException. However, it is not really a problem because the server can put the exception reason like "permission denied" into the UnknownUserException.unknown member.
    marc wrote:
    This approach doesn't scale well. Every time one of your developers adds an operation, you have to change the implementation of your servant locator. In other words, your servant locator must know about all interfaces and all operations in your server back-end. It might appear simpler at first, but over time, it will be much harder to maintain than a facade.
    No, the implementation of the servant locator does not need to change when developers add new operations. In practice, we can put the relationships of user_id/object_id/operation_name/permission(get or set) in a config file.


    Of course, It seems that facade design pattern may be another good solution and that Marc is an advocator of facade:)
    http://www.zeroc.com/vbulletin/showthread.php?t=1370&highlight=facade
    Hope it can be one topic in the next newsletter:)
  • marc wrote:
    If you use the approach outlined above, i.e., divert all calls to a dummy servant (a Blobject servant), and always marshal (not throw) a NoPermissionException in ice_invoke(), then you are well within the spec, provided that all your operations declare that they can throw such a NoPermissionException (even if the implementation doesn't throw directly). If you do not declare the NoPermissionException, then you are outside the spec. The client will not recognize such exceptions, but instead will raise an UnknownUserException.

    This is how you can do it technically. I do still not recommend it though :)

    This approach doesn't scale well. Every time one of your developers adds an operation, you have to change the implementation of your servant locator. In other words, your servant locator must know about all interfaces and all operations in your server back-end. It might appear simpler at first, but over time, it will be much harder to maintain than a facade.

    If you don't want the facade approach, then splitting the operations into different Ice objects (with different categories) would be the second best approach. The disadvantage of this is that you have to model your back-end server for access levels. A facade avoids this.

    As an analogy, think of a facade as an API for an operating system. An operating system has many internal functions, but they are exposed only through an API (which is a facade). Different categories of applications see different APIs. For example, a device driver sees a different API than a regular user application. Regardless, if the OS internals change, your APIs can still be the same.


    I plan on having a method like "addProtectedOperation(string opName)". When locate is called, I'll look at the operation, if it's in the map and the context does not contain a valid token, locate will return a default Blobject. When the OA calls ice_invoke on the Blobject, I'll just return a NoPermissionException. The Blobject doesn't care what the operation actually is, it just knows that it has to throw the exception. This way, no one has to write a facade object or change their servant implementations. I guess it's a "trick", but it seems fairly clean and straightforward...
  • Okay, I'm going with a session facade. I've created a Blobject that contains a proxy to the protected servant and intercepts all calls to the protected servant in ice_invoke(). The ice_invoke() implementation checks the operation name and marshals an exception if the operation is not allowed.

    I have a "PermissionSessionFactory" servant that returns a proxy to the Blobject; I do an uncheckedCast to the protected servant type and make a call. The call makes it all the way to my ice_invoke() method in the Blobject, but when I call ice_invoke on the protected proxy, nothing happens - the protected servant never gets called, and communications just hangs.

    I'm adding the protected servant to the OA with addWithUUID. If I simply return that proxy in the factory call, the client talks to it just fine.

    Here's my session factory:
    ::Ice::ObjectPrx Permissions::PermSessionFactoryImpl::
    createSession(const Ice::Current& current)
    {
    
        //
        // Create a Protected servant (has methods op1, op2, op3, op4).
        //
        protectThisPrx_ = Permissions::ProtectedPrx::uncheckedCast(
            current.adapter->addWithUUID(new Permissions::ProtectedImpl()));
    
        //
        // Create a PermissionSession servant, which is a Blobject that implements
        // ice_invoke to check the operation name and throw an exception if the
        // client does not have permission to call that method.
        //
        Ice::ObjectPrx proxy_ = Ice::ObjectPrx::uncheckedCast(
            current.adapter->addWithUUID(new PermissionSessionImpl(protectThisPrx_)));
    
        return proxy_;
    
    }
    
    

    Not sure what I'm doing wrong...