On authorization proxy objects

Authorization Services is quite a nice way to build in discretionary access controls to a Mac application. There’s a whole chapter in Professional Cocoa Application Security (Chapter 6) dedicated to the topic, if you’re interested in how it works.

The thing is, it’s quite verbose. If you’ve got a number of privileged operations (like, one or more) in an app, then the Auth Services code can get in the way of the real code, making it harder to unpick what a method is up to when you read it again a few months later.

Let’s use some of the nicer features of the Objective-C runtime to solve that problem. Assuming we’ve got an object that actually does the privileged work, we’ll create a façade object GLPrivilegedPerformer that handles the authorization for us. It can distinguish between methods that do or don’t require privileges, and will attempt to gain different rights for different methods on different classes. That allows administrators to configure privileges for the whole app, for a particular class or even for individual tasks. If it can’t get the privilege, it will throw an exception. OK, enough rabbiting. The code:

@interface GLPrivilegedPerformer : NSObject {
    id actual;
    AuthorizationRef auth;
}
- (id)initWithClass: (Class)cls;
@end

@implementation GLPrivilegedPerformer

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *sig = [super methodSignatureForSelector: aSelector];
    if (!sig) {
        sig = [actual methodSignatureForSelector: aSelector];
    }
    return sig;
}

- (BOOL)respondsToSelector:(SEL)aSelector {
    if (![super respondsToSelector: aSelector]) {
        return [actual respondsToSelector: aSelector];
    }
    return YES;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if ([actual respondsToSelector: [anInvocation selector]]) {
        NSString *selName = NSStringFromSelector([anInvocation selector]);
        if ([selName length] > 3 && [[selName substringToIndex: 4] isEqualToString: @"priv"]) {
            NSString *rightName = [NSString stringWithFormat: @"%@.%@.%@",
                                   [[NSBundle mainBundle] bundleIdentifier],
                                   NSStringFromClass([actual class]),
                                   selName];
            AuthorizationItem item = {0};
            item.name = [rightName UTF8String];
            AuthorizationRights requested = {
                .count = 1,
                .items = &item,
            };
            OSStatus authResult = AuthorizationCopyRights(auth,
                                                          &requested,
                                                          kAuthorizationEmptyEnvironment,
                                                          kAuthorizationFlagDefaults |
                                                          kAuthorizationFlagExtendRights |
                                                          kAuthorizationFlagInteractionAllowed,
                                                          NULL);
            
            if (errAuthorizationSuccess != authResult) {
                [self doesNotRecognizeSelector: [anInvocation selector]];
            }
        }
        [anInvocation invokeWithTarget: actual];
    }
    else {
        [super forwardInvocation: anInvocation];
    }
}

- (id)initWithClass: (Class)cls {
    self = [super init];
    if (self) {
        OSStatus authResult = AuthorizationCreate(NULL,
                                                  kAuthorizationEmptyEnvironment,
                                                  kAuthorizationFlagDefaults,
                                                  &auth);
        if (errAuthorizationSuccess != authResult) {
            NSLog(@"couldn't create auth ref");
            return nil;
        }
        actual = [[cls alloc] init];
    }
    return self;
}

- (void)dealloc {
    AuthorizationFree(auth, kAuthorizationFlagDefaults);
    [actual release];
    [super dealloc];
}
@end

Some notes:

  • You may want to raise a custom exception rather than using -doesNotRecognizeSelector: on failure. But you’re going to have to @catch something on failure. That’s consistent with the way Distributed Objects handles authentication failures.
  • The rights it generates will have names of the form com.example.MyApplication.GLActualPerformer.privilegedTask, where GLActualPerformer is the name of the target class and privilegedTask is the method name.
  • There’s an argument for the Objective-C proxying mechanism making code harder to read than putting the code inline. As discussed in Chapter 9, using object-oriented tricks to make code non-linear has been found to make it harder to review the code. However, this proxy object is small enough to be easily-understandable, and just removes authorization as a cross-cutting concern in the style of aspect-oriented programming (AOP). If you think this will make your code too hard to understand, don’t use it. I won’t mind.
  • As mentioned elsewhere, Authorization Services is discretionary. This proxy pattern doesn’t make it impossible for injected code to bypass the authorization request by using the target class directly. Even if the target class has the “hidden” visibility attribute, class-dump can find it and NSClassFromString() can get the Class object.

About Graham

I make it faster and easier for you to create high-quality code.
This entry was posted in Authorization, code-level, Mac, PCAS, software-engineering. Bookmark the permalink.