/* Copyright (C) 2002-2004 SKYRIX Software AG This file is part of OpenGroupware.org. OGo is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. OGo is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with OGo; see the file COPYING. If not, write to the Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "SoSecurityManager.h" #include "SoObject.h" #include "SoClass.h" #include "SoClassSecurityInfo.h" #include "SoPermissions.h" #include "SoUser.h" #include "SoSecurityException.h" #include "WOContext+SoObjects.h" #include #include #include "common.h" @interface NSObject(WOAppAuth) - (BOOL)isPublicInContext:(id)_ctx; @end @interface NSObject(UserDB) - (id)userInContext:(WOContext *)_ctx; @end @interface NSString(SpecialPermissionChecks) - (BOOL)isSoPublicPermission; - (BOOL)isSoAnonymousUserLogin; @end #if USE_PERM_CACHE static NSString *SoPermCache = @"__validatedperms"; #endif @implementation SoSecurityManager static int debugOn = -1; + (void)initialize { NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; debugOn = [ud boolForKey:@"SoSecurityManagerDebugEnabled"] ? 1 : 0; } + (id)sharedSecurityManager { static SoSecurityManager *sharedManager = nil; // THREAD if (sharedManager == nil) sharedManager = [[SoSecurityManager alloc] init]; return sharedManager; } - (void)dealloc { [super dealloc]; } /* exceptions */ - (NSException *)makeExceptionForObject:(id)_obj reason:(NSString *)_r { NSException *e; if (_obj == nil) return nil; e = [SoAccessDeniedException securityExceptionOnObject:_obj withAuthenticator:nil andManager:self]; if ([_r length] > 0) [e setReason:_r]; return e; } - (NSException *)isPrivateExceptionForObject:(id)_object { NSString *r; r = [NSString stringWithFormat:@"tried to access private object " @"(0x%08X, SoClass=%@)", _object, [[_object soClass] className]]; return [self makeExceptionForObject:_object reason:r]; } - (NSException *)missingPermissionException:(NSString *)_perm forObject:(id)_object { NSString *r; r = [NSString stringWithFormat:@"missing permission '%@' on object " @"(0x%08X, SoClass=%@)", _perm, _object, [[_object soClass] className]]; return [self makeExceptionForObject:_object reason:r]; } - (NSException *)isPrivateKeyException:(NSString *)_key ofObject:(id)_object { NSException *e; NSString *s; s = [[NSString alloc] initWithFormat: @"tried to access private key '%@' of object: %@", _key, _object]; e = [self makeExceptionForObject:_object reason:s]; [s release]; return e; } /* secinfo lookup */ - (SoClassSecurityInfo *)lookupInfoOfClass:(SoClass *)_soClass condition:(SEL)_sel object:(id)_object { SoClass *soClass; for (soClass = _soClass; soClass; soClass = [soClass soSuperClass]) { SoClassSecurityInfo *sinfo; IMP m; if ((sinfo = [soClass soClassSecurityInfo]) == nil) continue; if ((m = [sinfo methodForSelector:_sel])) { BOOL ok; ok = (_object) ? ((BOOL (*)(id, SEL, id))m)(sinfo, _sel, _object) : ((BOOL (*)(id, SEL))m)(sinfo, _sel); if (ok) return sinfo; } } return nil; } /* validation */ - (id)authenticatorInContext:(id)_ctx object:(id)_object { id authenticator; if ((authenticator = [_object authenticatorInContext:_ctx]) == nil) authenticator = [[WOApplication application] authenticatorInContext:_ctx]; return authenticator; } - (id)userInContext:(id)_ctx object:(id)_object { id user, authenticator; if ((user = [(WOContext *)_ctx activeUser]) != nil) return [user isNotNull] ? user : nil; authenticator = [self authenticatorInContext:_ctx object:_object]; if ((user = [authenticator userInContext:_ctx]) != nil) [(WOContext *)_ctx setActiveUser:user]; return [user isNotNull] ? user : nil; } - (BOOL)isUser:(id)_user ownerOfObject:(id)_obj inContext:(id)_ctx { NSString *objectOwner; if ((objectOwner = [_obj ownerInContext:_ctx]) == nil) return NO; if ([[_user login] isEqualToString:objectOwner]) return YES; return NO; } - (NSException *)validatePermission:(NSString *)_perm onObject:(id)_object inContext:(id)_ctx { NSMutableDictionary *validatedPerms; NSArray *rolesHavingPermission; SoClassSecurityInfo *sinfo; id user; NSArray *userRoles; NSEnumerator *e; NSString *role; if (_perm == nil) return [self missingPermissionException:_perm forObject:_object]; #if !USE_PERM_CACHE validatedPerms = nil; #else // TODO: Bug !! The cache must go on Permission+ObjectID since the // permission can be set without the ID ! /* check the cache */ if ((validatedPerms = [_ctx objectForKey:SoPermCache])) { NSException *o; if ((o = [validatedPerms objectForKey:_perm])) { if (debugOn) [self debugWithFormat:@"permission '%@' cached as valid ...", _perm]; if ([o isNotNull]) /* an exception */ return o; return nil; } } else { /* setup cache */ validatedPerms = [[NSMutableDictionary alloc] init]; [_ctx setObject:validatedPerms forKey:SoPermCache]; [validatedPerms autorelease]; } #endif if (debugOn) { [self debugWithFormat:@"validate permission '%@' on object: %@", _perm, _object]; } if ([_perm isSoPublicPermission]) /* the object is public */ goto found; /* determine the possible roles for the permission */ // TODO: check object for policy (currently only default roles are checked) sinfo = [self lookupInfoOfClass:[_object soClass] condition:@selector(hasDefaultRoleForPermission:) object:_perm]; if (sinfo == nil) sinfo = [[_object soClass] soClassSecurityInfo]; rolesHavingPermission = [sinfo defaultRolesForPermission:_perm]; if (debugOn) { [self debugWithFormat:@" possible roles for permission '%@': %@", _perm, [rolesHavingPermission componentsJoinedByString:@", "]]; } if ([rolesHavingPermission containsObject:SoRole_Anonymous]) { /* is public */ [self debugWithFormat:@" allowed because of anonymous roles."]; goto found; } if ([rolesHavingPermission count] == 0) { /* is public */ [self debugWithFormat:@" allowed because no roles are required."]; goto found; } /* now retrieve the user that is logged in */ if ((user = [self userInContext:_ctx object:_object]) == nil) { /* no user, anonymous */ [self debugWithFormat:@" got no user (=> auth required)."]; return [SoAuthRequiredException securityExceptionOnObject:_object withAuthenticator: [self authenticatorInContext:_ctx object:_object] andManager:self]; } [self debugWithFormat:@" got user: %@)", user]; /* process user */ userRoles = [user rolesForObject:_object inContext:_ctx]; [self debugWithFormat:@" user roles: %@", [userRoles componentsJoinedByString:@","]]; if ([userRoles count] == 0) return [self isPrivateExceptionForObject:_object]; /* now check whether the roles subset */ e = [userRoles objectEnumerator]; while ((role = [e nextObject])) { if ([rolesHavingPermission containsObject:role]) { /* found role ! */ break; } } /* if no role was found, check whether the user is the owner */ if (role == nil) { if ([rolesHavingPermission containsObject:SoRole_Owner]) { if ([self isUser:user ownerOfObject:_object inContext:_ctx]) { role = SoRole_Owner; [self debugWithFormat:@" user is owner of object."]; } else if ([_object ownerInContext:_ctx] == nil) { role = SoRole_Owner; [self debugWithFormat:@" object is not owned, grant access."]; } else { role = nil; [self debugWithFormat: @" user is not the owner of object (owner=%@).", [_object ownerInContext:_ctx]]; } } } /* check whether a role was finally found */ if (role == nil) { [self debugWithFormat:@" found no matching role."]; if ([[user login] isSoAnonymousUserLogin]) { [self debugWithFormat:@"still anonymous, requesting login ..."]; return [SoAuthRequiredException securityExceptionOnObject:_object withAuthenticator: [self authenticatorInContext:_ctx object:_object] andManager:self]; } else { /* Note: AFAIK Zope will present the user a login panel in any case. IMHO this is not good in practice (you don't change identities very often ;-), and the 403 code has it's value too. */ [self debugWithFormat:@"valid user, denying access ..."]; return [self isPrivateExceptionForObject:_object]; } } [self debugWithFormat:@" found a valid role: '%@'.", role]; found: [self debugWithFormat:@" successfully validated permission '%@'.", _perm]; [validatedPerms setObject:[NSNull null] forKey:_perm]; return nil; } - (NSException *)validateObject:(id)_object inContext:(id)_ctx { /* This methods check how the object itself is protected. */ NSMutableArray *validatedObjects; SoClassSecurityInfo *sinfo; NSString *perm; NSException *e; if (_object == nil) return nil; /* some objects are always public */ if ([_object isPublicInContext:_ctx]) return nil; /* check the cache */ if ((validatedObjects = [(WOContext *)_ctx objectPermissionCache])) { if ([validatedObjects indexOfObjectIdenticalTo:_object] != NSNotFound) return nil; } else { /* setup cache */ validatedObjects = [[NSMutableArray alloc] init]; [(WOContext *)_ctx setObjectPermissionCache:validatedObjects]; [validatedObjects autorelease]; } [self debugWithFormat:@"validate object: %@", _object]; /* find the security info which has object protections */ sinfo = [self lookupInfoOfClass:[_object soClass] condition:@selector(hasObjectProtections) object:nil]; if (sinfo == nil) { [self debugWithFormat: @"found no security info with object protection for object " @"(rejecting access):\n object: %@\n class: %@\n soclass: %@)", _object, NSStringFromClass([_object class]), [_object soClass]]; return [self isPrivateExceptionForObject:_object]; } if ([sinfo isObjectPublic]) { /* object is public ... */ [self debugWithFormat:@" object is public."]; [validatedObjects addObject:_object]; return nil; } if ([sinfo isObjectPrivate]) { /* object is private ... */ [self debugWithFormat:@" object is private."]; return [self isPrivateExceptionForObject:_object]; } perm = [sinfo permissionRequiredForObject]; if ((e = [self validatePermission:perm onObject:_object inContext:_ctx])) return e; [self debugWithFormat:@" successfully validated object (perm=%@).", perm]; [validatedObjects addObject:_object]; return nil; } - (NSException *)validateName:(NSString *)_key ofObject:(id)_object inContext:(id)_ctx { /* note: this does not check object-value restrictions */ SoClassSecurityInfo *sinfo; NSException *e; NSString *perm; /* step a: find out permission required for object */ if ((e = [self validateObject:_object inContext:_ctx])) { [self debugWithFormat:@" object did not validate (tried lookup on %@).", _key]; return e; } /* step b: find out permission required for key */ [self debugWithFormat:@"validate key %@ of object: %@", _key, _object]; /* find the security info which has protections for the key */ sinfo = [self lookupInfoOfClass:[_object soClass] condition:@selector(hasProtectionsForKey:) object:_key]; if (sinfo == nil) { /* found no security for key, so we take the defaults */ [self debugWithFormat:@" found no security info for key (class %@): %@", NSStringFromClass([_object class]), _key]; sinfo = [self lookupInfoOfClass:[_object soClass] condition:@selector(hasDefaultAccessDeclaration) object:nil]; // TODO: search superclasses for one with declared default-access if ([[sinfo defaultAccess] isEqualToString:@"allow"]) { [self debugWithFormat:@" default is allow ..."]; return nil; } return [self isPrivateKeyException:_key ofObject:_object]; } if ([sinfo isKeyPublic:_key]) return nil; if ([sinfo isKeyPrivate:_key]) /* key is private ... */ return [self isPrivateKeyException:_key ofObject:_object]; perm = [sinfo permissionRequiredForKey:_key]; if ((e = [self validatePermission:perm onObject:_object inContext:_ctx])) return e; [self debugWithFormat:@" successfully validated key (%@).", _key]; return nil; } - (NSException *)validateValue:(id)_value forName:(NSString *)_key ofObject:(id)_object inContext:(id)_ctx { /* this additionally checks object restrictions of the value */ if (_value) { NSException *e; if ((e = [self validateObject:_value inContext:_ctx])) { [self debugWithFormat:@"value (0x%08X,%@) of key %@ didn't validate", _value, NSStringFromClass([_value class]), _key]; return e; } } return [self validateName:_key ofObject:_object inContext:_ctx]; } @end /* SoSecurityManager */ @implementation SoSecurityManager(Logging) // Note: this is a category, so that its more difficult to override (of course // still not impossible ... - (NSString *)loggingPrefix { return @"[so-security]"; } - (BOOL)isDebuggingEnabled { return debugOn ? YES : NO; } @end /* SoSecurityManager(Logging) */ /* public objects */ @implementation NSObject(Pub) - (BOOL)isPublicInContext:(id)_ctx { return NO; } @end @implementation NSArray(Pub) - (BOOL)isPublicInContext:(id)_ctx { return YES; } @end @implementation NSString(Pub) - (BOOL)isPublicInContext:(id)_ctx { return YES; } @end @implementation NSDictionary(Pub) - (BOOL)isPublicInContext:(id)_ctx { return YES; } @end @implementation NSException(Pub) - (BOOL)isPublicInContext:(id)_ctx { return YES; } @end @implementation NSString(SpecialPermissionChecks) - (BOOL)isSoPublicPermission { return [@"" isEqualToString:self]; } - (BOOL)isSoAnonymousUserLogin { return [@"anonymous" isEqualToString:self]; } @end /* NSString(SpecialPermissionChecks) */