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