1/* 2 Copyright (C) 2002-2005 SKYRIX Software AG 3 4 This file is part of SOPE. 5 6 SOPE is free software; you can redistribute it and/or modify it under 7 the terms of the GNU Lesser General Public License as published by the 8 Free Software Foundation; either version 2, or (at your option) any 9 later version. 10 11 SOPE is distributed in the hope that it will be useful, but WITHOUT ANY 12 WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 14 License for more details. 15 16 You should have received a copy of the GNU Lesser General Public 17 License along with SOPE; see the file COPYING. If not, write to the 18 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 19 02111-1307, USA. 20*/ 21 22#include "SoSecurityManager.h" 23#include "SoObject.h" 24#include "SoClass.h" 25#include "SoClassSecurityInfo.h" 26#include "SoPermissions.h" 27#include "SoUser.h" 28#include "SoSecurityException.h" 29#include "WOContext+SoObjects.h" 30#include <NGObjWeb/WOResponse.h> 31#include <NGObjWeb/WOApplication.h> 32#include "common.h" 33 34@interface NSObject(WOAppAuth) 35- (BOOL)isPublicInContext:(id)_ctx; 36@end 37 38@interface NSObject(UserDB) 39- (id)userInContext:(WOContext *)_ctx; 40@end 41 42@interface NSString(SpecialPermissionChecks) 43- (BOOL)isSoPublicPermission; 44- (BOOL)isSoAnonymousUserLogin; 45@end 46 47#if USE_PERM_CACHE 48static NSString *SoPermCache = @"__validatedperms"; 49#endif 50 51@implementation SoSecurityManager 52 53static int debugOn = -1; 54 55+ (void)initialize { 56 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; 57 58 debugOn = [ud boolForKey:@"SoSecurityManagerDebugEnabled"] ? 1 : 0; 59} 60 61+ (id)sharedSecurityManager { 62 static SoSecurityManager *sharedManager = nil; // THREAD 63 64 if (sharedManager == nil) 65 sharedManager = [[SoSecurityManager alloc] init]; 66 return sharedManager; 67} 68 69- (void)dealloc { 70 [super dealloc]; 71} 72 73/* exceptions */ 74 75- (NSException *)makeExceptionForObject:(id)_obj reason:(NSString *)_r { 76 NSException *e; 77 NSString *r; 78 if (_obj == nil) return nil; 79 if ([_r length] <= 0) 80 r = nil; 81 else 82 r = _r; 83 e = [SoAccessDeniedException securityExceptionOnObject:_obj 84 withAuthenticator:nil 85 andManager:self 86 reason:_r]; 87 return e; 88} 89 90- (NSException *)isPrivateExceptionForObject:(id)_object { 91 NSString *r; 92 93 r = [NSString stringWithFormat:@"tried to access private object " 94 @"(0x%p, SoClass=%@)", 95 _object, [[_object soClass] className]]; 96 return [self makeExceptionForObject:_object reason:r]; 97} 98- (NSException *)missingPermissionException:(NSString *)_perm 99 forObject:(id)_object 100{ 101 NSString *r; 102 103 r = [NSString stringWithFormat:@"missing permission '%@' on object " 104 @"(0x%p, SoClass=%@)", 105 _perm, _object, [[_object soClass] className]]; 106 return [self makeExceptionForObject:_object reason:r]; 107} 108 109- (NSException *)isPrivateKeyException:(NSString *)_key ofObject:(id)_object { 110 NSException *e; 111 NSString *s; 112 113 s = [[NSString alloc] initWithFormat: 114 @"tried to access private key '%@' of object: %@", 115 _key, _object]; 116 e = [self makeExceptionForObject:_object reason:s]; 117 [s release]; 118 return e; 119} 120 121/* secinfo lookup */ 122 123- (SoClassSecurityInfo *)lookupInfoOfClass:(SoClass *)_soClass 124 condition:(SEL)_sel 125 object:(id)_object 126{ 127 SoClass *soClass; 128 129 for (soClass = _soClass; soClass; soClass = [soClass soSuperClass]) { 130 SoClassSecurityInfo *sinfo; 131 IMP m; 132 133 // [self logWithFormat:@"CHECK CLASS: %@", soClass]; 134 135 if ((sinfo = [soClass soClassSecurityInfo]) == nil) continue; 136 if ((m = [sinfo methodForSelector:_sel])) { 137 BOOL ok; 138 139 ok = (_object) 140 ? ((BOOL (*)(id, SEL, id))m)(sinfo, _sel, _object) 141 : ((BOOL (*)(id, SEL))m)(sinfo, _sel); 142 if (ok) return sinfo; 143 } 144 } 145 return nil; 146} 147 148/* validation */ 149 150- (id)authenticatorInContext:(id)_ctx object:(id)_object { 151 id authenticator; 152 153 if ((authenticator = [_object authenticatorInContext:_ctx]) == nil) 154 authenticator = [[WOApplication application] authenticatorInContext:_ctx]; 155 return authenticator; 156} 157- (id<SoUser>)userInContext:(id)_ctx object:(id)_object { 158 id user, authenticator; 159 160 if ((user = [(WOContext *)_ctx activeUser]) != nil) 161 return [user isNotNull] ? user : nil; 162 163 authenticator = [self authenticatorInContext:_ctx object:_object]; 164 165 if ((user = [authenticator userInContext:_ctx]) != nil) 166 [(WOContext *)_ctx setActiveUser:user]; 167 168 return [user isNotNull] ? user : nil; 169} 170 171- (BOOL)isUser:(id<SoUser>)_user ownerOfObject:(id)_obj inContext:(id)_ctx { 172 NSString *objectOwner; 173 174 if ((objectOwner = [_obj ownerInContext:_ctx]) == nil) 175 return NO; 176 177 if ([[_user login] isEqualToString:objectOwner]) 178 return YES; 179 180 return NO; 181} 182 183- (NSException *)validatePermission:(NSString *)_perm 184 onObject:(id)_object 185 inContext:(id)_ctx 186{ 187 NSMutableDictionary *validatedPerms; 188 NSArray *rolesHavingPermission; 189 SoClassSecurityInfo *sinfo; 190 id<SoUser> user; 191 NSArray *userRoles; 192 NSEnumerator *e; 193 NSString *role; 194 195 if (_perm == nil) 196 return [self missingPermissionException:_perm forObject:_object]; 197 198#if !USE_PERM_CACHE 199 validatedPerms = nil; 200#else 201 // TODO: Bug !! The cache must go on Permission+ObjectID since the 202 // permission can be set without the ID ! 203 204 /* check the cache */ 205 if ((validatedPerms = [_ctx objectForKey:SoPermCache])) { 206 NSException *o; 207 208 if ((o = [validatedPerms objectForKey:_perm])) { 209 if (debugOn) 210 [self debugWithFormat:@"permission '%@' cached as valid ...", _perm]; 211 212 if ([o isNotNull]) 213 /* an exception */ 214 return o; 215 return nil; 216 } 217 } 218 else { 219 /* setup cache */ 220 validatedPerms = [[NSMutableDictionary alloc] init]; 221 [_ctx setObject:validatedPerms forKey:SoPermCache]; 222 [validatedPerms autorelease]; 223 } 224#endif 225 226 if (debugOn) { 227 [self debugWithFormat:@"validate permission '%@' on object: %@", 228 _perm, _object]; 229 } 230 231 if ([_perm isSoPublicPermission]) 232 /* the object is public */ 233 goto found; 234 235 /* determine the possible roles for the permission */ 236 237 // TODO: check object for policy (currently only default roles are checked) 238 239 sinfo = [self lookupInfoOfClass:[_object soClass] 240 condition:@selector(hasDefaultRoleForPermission:) 241 object:_perm]; 242 243 if (sinfo == nil) 244 sinfo = [[_object soClass] soClassSecurityInfo]; 245 246 rolesHavingPermission = [sinfo defaultRolesForPermission:_perm]; 247 if (debugOn) { 248 [self debugWithFormat:@" possible roles for permission '%@': %@", 249 _perm, [rolesHavingPermission componentsJoinedByString:@", "]]; 250 } 251 252 if ([rolesHavingPermission containsObject:SoRole_Anonymous]) { 253 /* is public */ 254 [self debugWithFormat:@" allowed because of anonymous roles."]; 255 goto found; 256 } 257 if ([rolesHavingPermission count] == 0) { 258 /* is public */ 259 [self debugWithFormat:@" allowed because no roles are required."]; 260 goto found; 261 } 262 263 /* now retrieve the user that is logged in */ 264 265 if ((user = [self userInContext:_ctx object:_object]) == nil) { 266 /* no user, anonymous */ 267 [self debugWithFormat:@" got no user (=> auth required)."]; 268 return [SoAuthRequiredException securityExceptionOnObject:_object 269 withAuthenticator: 270 [self authenticatorInContext:_ctx 271 object:_object] 272 andManager:self 273 reason:nil]; 274 } 275 276 [self debugWithFormat:@" got user: %@)", user]; 277 278 /* process user */ 279 280 userRoles = [user rolesForObject:_object inContext:_ctx]; 281 [self debugWithFormat:@" user roles: %@", 282 [userRoles componentsJoinedByString:@","]]; 283 if ([userRoles count] == 0) 284 return [self isPrivateExceptionForObject:_object]; 285 286 /* now check whether the roles subset */ 287 288 e = [userRoles objectEnumerator]; 289 while ((role = [e nextObject])) { 290 if ([rolesHavingPermission containsObject:role]) { 291 /* found role ! */ 292 break; 293 } 294 } 295 296 /* if no role was found, check whether the user is the owner */ 297 298 if (role == nil) { 299 if ([rolesHavingPermission containsObject:SoRole_Owner]) { 300 if ([self isUser:user ownerOfObject:_object inContext:_ctx]) { 301 role = SoRole_Owner; 302 [self debugWithFormat:@" user is owner of object."]; 303 } 304 else if ([_object ownerInContext:_ctx] == nil) { 305 role = SoRole_Owner; 306 [self debugWithFormat:@" object is not owned, grant access."]; 307 } 308 else { 309 role = nil; 310 [self debugWithFormat: 311 @" user is not the owner of object (owner=%@).", 312 [_object ownerInContext:_ctx]]; 313 } 314 } 315 } 316 317 /* check whether a role was finally found */ 318 319 if (role == nil) { 320 [self debugWithFormat:@" found no matching role."]; 321 322 if ([[user login] isSoAnonymousUserLogin]) { 323 [self debugWithFormat:@"still anonymous, requesting login ..."]; 324 return [SoAuthRequiredException securityExceptionOnObject:_object 325 withAuthenticator: 326 [self authenticatorInContext:_ctx 327 object:_object] 328 andManager:self 329 reason:nil]; 330 } 331 else { 332 /* 333 Note: AFAIK Zope will present the user a login panel in any 334 case. IMHO this is not good in practice (you don't change 335 identities very often ;-), and the 403 code has it's value too. 336 */ 337 [self debugWithFormat:@"valid user, denying access ..."]; 338 return [self isPrivateExceptionForObject:_object]; 339 } 340 } 341 342 [self debugWithFormat:@" found a valid role: '%@'.", role]; 343 344 found: 345 [self debugWithFormat:@" successfully validated permission '%@'.", _perm]; 346 [validatedPerms setObject:[NSNull null] forKey:_perm]; 347 return nil; 348} 349 350- (NSException *)validateObject:(id)_object inContext:(id)_ctx { 351 /* This methods check how the object itself is protected. */ 352 NSMutableArray *validatedObjects; 353 SoClassSecurityInfo *sinfo; 354 NSString *perm; 355 NSException *e; 356 357 if (_object == nil) return nil; 358 359 /* some objects are always public */ 360 if ([_object isPublicInContext:_ctx]) 361 return nil; 362 363 /* check the cache */ 364 if ((validatedObjects = [(WOContext *)_ctx objectPermissionCache])) { 365 if ([validatedObjects indexOfObjectIdenticalTo:_object] != NSNotFound) 366 return nil; 367 } 368 else { 369 /* setup cache */ 370 validatedObjects = [[NSMutableArray alloc] init]; 371 [(WOContext *)_ctx setObjectPermissionCache:validatedObjects]; 372 [validatedObjects autorelease]; 373 } 374 375 [self debugWithFormat:@"validate object: %@", _object]; 376 377 /* find the security info which has object protections */ 378 sinfo = [self lookupInfoOfClass:[_object soClass] 379 condition:@selector(hasObjectProtections) 380 object:nil]; 381 if (sinfo == nil) { 382 [self debugWithFormat: 383 @"found no security info with object protection for object " 384 @"(rejecting access):\n object: %@\n class: %@\n soclass: %@)", 385 _object, NSStringFromClass([_object class]), [_object soClass]]; 386 return [self isPrivateExceptionForObject:_object]; 387 } 388 389 if ([sinfo isObjectPublic]) { 390 /* object is public ... */ 391 [self debugWithFormat:@" object is public."]; 392 [validatedObjects addObject:_object]; 393 return nil; 394 } 395 396 if ([sinfo isObjectPrivate]) { 397 /* object is private ... */ 398 [self debugWithFormat:@" object is private."]; 399 return [self isPrivateExceptionForObject:_object]; 400 } 401 402 perm = [sinfo permissionRequiredForObject]; 403 if ((e = [self validatePermission:perm onObject:_object inContext:_ctx])) 404 return e; 405 406 [self debugWithFormat:@" successfully validated object (perm=%@).", perm]; 407 [validatedObjects addObject:_object]; 408 return nil; 409} 410 411- (NSException *)validateName:(NSString *)_key 412 ofObject:(id)_object 413 inContext:(id)_ctx 414{ 415 /* note: this does not check object-value restrictions */ 416 SoClassSecurityInfo *sinfo; 417 NSException *e; 418 NSString *perm; 419 420 /* step a: find out permission required for object */ 421 422 if ((e = [self validateObject:_object inContext:_ctx])) { 423 [self debugWithFormat:@" object did not validate (tried lookup on %@).", 424 _key]; 425 return e; 426 } 427 428 /* step b: find out permission required for key */ 429 430 [self debugWithFormat:@"validate key %@ of object: %@", _key, _object]; 431 432 /* find the security info which has protections for the key */ 433 sinfo = [self lookupInfoOfClass:[_object soClass] 434 condition:@selector(hasProtectionsForKey:) 435 object:_key]; 436 437 if (sinfo == nil) { 438 /* found no security for key, so we take the defaults */ 439 [self debugWithFormat:@" found no security info for key (class %@): %@", 440 NSStringFromClass([_object class]), _key]; 441 442 sinfo = [self lookupInfoOfClass:[_object soClass] 443 condition:@selector(hasDefaultAccessDeclaration) 444 object:nil]; 445 446 // TODO: search superclasses for one with declared default-access 447 if ([[sinfo defaultAccess] isEqualToString:@"allow"]) { 448 [self debugWithFormat:@" default is allow ..."]; 449 return nil; 450 } 451 return [self isPrivateKeyException:_key ofObject:_object]; 452 } 453 454 if ([sinfo isKeyPublic:_key]) 455 return nil; 456 457 if ([sinfo isKeyPrivate:_key]) 458 /* key is private ... */ 459 return [self isPrivateKeyException:_key ofObject:_object]; 460 461 perm = [sinfo permissionRequiredForKey:_key]; 462 if ((e = [self validatePermission:perm onObject:_object inContext:_ctx])) 463 return e; 464 465 [self debugWithFormat:@" successfully validated key (%@).", _key]; 466 return nil; 467} 468 469- (NSException *)validateValue:(id)_value 470 forName:(NSString *)_key 471 ofObject:(id)_object 472 inContext:(id)_ctx 473{ 474 /* this additionally checks object restrictions of the value */ 475 if (_value) { 476 NSException *e; 477 478 if ((e = [self validateObject:_value inContext:_ctx])) { 479 [self debugWithFormat:@"value (0x%p,%@) of key %@ didn't validate", 480 _value, NSStringFromClass([_value class]), _key]; 481 return e; 482 } 483 } 484 return [self validateName:_key ofObject:_object inContext:_ctx]; 485} 486 487@end /* SoSecurityManager */ 488 489@implementation SoSecurityManager(Logging) 490// Note: this is a category, so that its more difficult to override (of course 491// still not impossible ... 492 493- (NSString *)loggingPrefix { 494 return @"[so-security]"; 495} 496- (BOOL)isDebuggingEnabled { 497 return debugOn ? YES : NO; 498} 499 500@end /* SoSecurityManager(Logging) */ 501 502 503/* public objects */ 504 505@implementation NSObject(Pub) 506- (BOOL)isPublicInContext:(id)_ctx { return NO; } 507@end 508 509@implementation NSArray(Pub) 510- (BOOL)isPublicInContext:(id)_ctx { return YES; } 511@end 512 513@implementation NSString(Pub) 514- (BOOL)isPublicInContext:(id)_ctx { return YES; } 515@end 516 517@implementation NSDictionary(Pub) 518- (BOOL)isPublicInContext:(id)_ctx { return YES; } 519@end 520 521@implementation NSException(Pub) 522- (BOOL)isPublicInContext:(id)_ctx { return YES; } 523@end 524 525@implementation NSString(SpecialPermissionChecks) 526 527- (BOOL)isSoPublicPermission { 528 return [@"<public>" isEqualToString:self]; 529} 530- (BOOL)isSoAnonymousUserLogin { 531 return [@"anonymous" isEqualToString:self]; 532} 533 534@end /* NSString(SpecialPermissionChecks) */ 535