1/* SOGoGroup.m - this file is part of SOGo
2 *
3 * Copyright (C) 2009-2014 Inverse inc.
4 *
5 * This file is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2, or (at your option)
8 * any later version.
9 *
10 * This file is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; see the file COPYING.  If not, write to
17 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
18 * Boston, MA 02111-1307, USA.
19 */
20
21/*
22  Here are some group samples:
23
24  [ POSIX group ]
25
26  dn: cn=it-staff,ou=Group,dc=zzz,dc=xxx,dc=yyy
27  objectClass: posixGroup
28  objectClass: top
29  cn: it-staff
30  userPassword: {crypt}x
31  gidNumber: 8000
32  memberUid: lsa
33  memberUid: mrm
34  memberUid: ij
35  memberUid: no
36  memberUid: ld
37  memberUid: db
38  memberUid: rgl
39  memberUid: ja
40  memberUid: hbt
41  memberUid: hossein
42
43  dn: cn=inverse,ou=groups,dc=inverse,dc=ca
44  objectClass: groupOfUniqueNames
45  objectClass: top
46  objectClass: extensibleObject
47  uniqueMember: uid=flachapelle,ou=users,dc=inverse,dc=ca
48  uniqueMember: uid=lmarcotte,ou=users,dc=inverse,dc=ca
49  uniqueMember: uid=wsourdeau,ou=users,dc=inverse,dc=ca
50  cn: inverse
51  mail: inverse@inverse.ca
52
53 */
54
55#include "SOGoGroup.h"
56
57#import <Foundation/NSArray.h>
58#import <Foundation/NSAutoreleasePool.h>
59#import <Foundation/NSDictionary.h>
60#import <Foundation/NSString.h>
61
62#include "SOGoCache.h"
63#include "SOGoSource.h"
64#include "SOGoSystemDefaults.h"
65#include "SOGoUserManager.h"
66#include "SOGoUser.h"
67
68#import <NGLdap/NGLdapConnection.h>
69#import <NGLdap/NGLdapAttribute.h>
70#import <NGLdap/NGLdapEntry.h>
71
72#define CHECK_CLASS(o) ({ \
73  if ([o isKindOfClass: [NSString class]]) \
74    o = [NSArray arrayWithObject: o]; \
75})
76
77@implementation SOGoGroup
78
79- (id) initWithIdentifier: (NSString *) theID
80		   domain: (NSString *) theDomain
81		   source: (NSObject <SOGoSource> *) theSource
82		    entry: (NGLdapEntry *) theEntry
83{
84  self = [super init];
85
86  if (self)
87    {
88      ASSIGN(_identifier, theID);
89      ASSIGN(_domain, theDomain);
90      ASSIGN(_source, theSource);
91      ASSIGN(_entry, theEntry);
92      _members = nil;
93    }
94
95  return self;
96}
97
98- (void) dealloc
99{
100  RELEASE(_identifier);
101  RELEASE(_domain);
102  RELEASE(_source);
103  RELEASE(_entry);
104  RELEASE(_members);
105
106  [super dealloc];
107}
108
109+ (id) groupWithIdentifier: (NSString *) theID
110                  inDomain: (NSString *) domain
111{
112  NSRange r;
113  NSString *uid, *inDomain;
114  SOGoSystemDefaults *sd;
115
116  uid = [theID hasPrefix: @"@"] ? [theID substringFromIndex: 1] : theID;
117  inDomain = domain;
118
119  sd = [SOGoSystemDefaults sharedSystemDefaults];
120  if ([sd enableDomainBasedUID])
121    {
122      /* Split domain from uid */
123      r = [uid rangeOfString: @"@" options: NSBackwardsSearch];
124      if (r.location != NSNotFound)
125        {
126          if (!domain)
127            inDomain = [uid substringFromIndex: r.location + 1];
128          uid = [uid substringToIndex: r.location];
129        }
130    }
131
132  return [SOGoGroup groupWithValue: uid
133                 andSourceSelector: @selector (lookupGroupEntryByUID:inDomain:)
134                          inDomain: inDomain];
135}
136
137+ (id) groupWithEmail: (NSString *) theEmail
138             inDomain: (NSString *) domain
139{
140  return [SOGoGroup groupWithValue: theEmail
141                 andSourceSelector: @selector (lookupGroupEntryByEmail:inDomain:)
142                          inDomain: domain];
143}
144
145//
146// Returns nil if theValue doesn't match to a group
147//  (so its objectClass isn't a group)
148//
149+ (id) groupWithValue: (NSString *) theValue
150    andSourceSelector: (SEL) theSelector
151             inDomain: (NSString *) domain
152{
153  NSArray *allSources;
154  NGLdapEntry *entry;
155  NSObject <SOGoSource, SOGoDNSource> *source;
156  id o;
157  NSEnumerator *gclasses;
158  NSString *gclass;
159
160  int i;
161
162  // Don't bother looking in all sources if the
163  // supplied value is nil.
164  if (!theValue)
165    return nil;
166
167  allSources = [[SOGoUserManager sharedUserManager]
168                 sourceIDsInDomain: domain];
169  entry = nil;
170  o = nil;
171
172  for (i = 0; i < [allSources count]; i++)
173    {
174      source = (NSObject <SOGoSource, SOGoDNSource> *) [[SOGoUserManager sharedUserManager] sourceWithID: [allSources objectAtIndex: i]];
175
176      // Our different sources might not all implements groups support
177      if ([source respondsToSelector: theSelector])
178        entry = [source performSelector: theSelector
179                             withObject: theValue
180                             withObject: domain];
181      if (entry)
182	break;
183
184      entry = nil;
185    }
186
187  if (entry)
188    {
189      NSArray *classes;
190
191      // We check to see if it's a group
192      classes = [[entry asDictionary] objectForKey: @"objectclass"];
193
194      if (classes)
195	{
196          /* LDAP records returned as dictionaries may contain NSString or
197             NSArray values, depending on whether the amount of values
198             assigned to a key is 1 or more. Since this can occur with
199             "objectclass" too, we need to check whether "classes" is actually
200             an NSString instance... */
201          if ([classes isKindOfClass: [NSString class]])
202            classes = [NSArray arrayWithObject:
203                                 [(NSString *) classes lowercaseString]];
204          else
205            {
206              int i, c;
207
208              classes = [NSMutableArray arrayWithArray: classes];
209              c = [classes count];
210              for (i = 0; i < c; i++)
211                [(id)classes replaceObjectAtIndex: i
212                     withObject: [[classes objectAtIndex: i] lowercaseString]];
213            }
214	}
215
216        gclasses = [[source groupObjectClasses] objectEnumerator];
217        while (gclass = [gclasses nextObject])
218          if ([classes containsObject: gclass])
219           {
220          // Found a group, let's return it.
221	  o = [[self alloc] initWithIdentifier: theValue
222			                domain: domain
223                                        source: source
224                                         entry: entry];
225	  AUTORELEASE(o);
226	}
227    }
228
229  return o;
230}
231
232//
233// This method actually try to obtain all members
234// from either dynamic of static groups.
235//
236- (NSArray *) members
237{
238  NSMutableArray *dns, *uids, *logins;
239  NSString *dn, *login;
240  SOGoUserManager *um;
241  NSDictionary *d;
242  SOGoUser *user;
243  NSArray *o;
244  NSAutoreleasePool *pool;
245  int i, c;
246
247  if (!_members)
248    {
249      _members = [NSMutableArray new];
250      uids = [NSMutableArray array];
251      dns = [NSMutableArray array];
252      logins = [NSMutableArray array];
253
254      // We check if it's a static group
255      // Fetch "members" - we get DNs
256      d = [_entry asDictionary];
257      o = [d objectForKey: @"member"];
258      CHECK_CLASS(o);
259      if (o) [dns addObjectsFromArray: o];
260
261      // Fetch "uniqueMembers" - we get DNs
262      o = [d objectForKey: @"uniquemember"];
263      CHECK_CLASS(o);
264      if (o) [dns addObjectsFromArray: o];
265
266      // Fetch "memberUid" - we get UID (like login names)
267      o = [d objectForKey: @"memberuid"];
268      CHECK_CLASS(o);
269      if (o) [uids addObjectsFromArray: o];
270
271      c = [dns count] + [uids count];
272
273      // We deal with a static group, let's add the members
274      if (c)
275        {
276          um = [SOGoUserManager sharedUserManager];
277
278          // We add members for whom we have their associated DN
279          for (i = 0; i < [dns count]; i++)
280            {
281              pool = [NSAutoreleasePool new];
282              dn = [dns objectAtIndex: i];
283              login = [um getLoginForDN: [dn lowercaseString]];
284              user = [SOGoUser userWithLogin: login  roles: nil];
285              if (user)
286                {
287                  [logins addObject: login];
288                  [_members addObject: user];
289                }
290              [pool release];
291            }
292
293          // We add members for whom we have their associated login name
294          for (i = 0; i < [uids count]; i++)
295            {
296              pool = [NSAutoreleasePool new];
297              login = [uids objectAtIndex: i];
298              user = [SOGoUser userWithLogin: login  roles: nil];
299
300              if (user)
301                {
302                  [logins addObject: login];
303                  [_members addObject: user];
304                }
305              [pool release];
306            }
307
308
309          // We are done fetching members, let's cache the members of the group
310          // (ie., their UIDs) in memcached to speed up -hasMemberWithUID.
311          [[SOGoCache sharedCache] setValue: [logins componentsJoinedByString: @","]
312            forKey: [NSString stringWithFormat: @"%@+%@", _identifier, _domain]];
313        }
314      else
315        {
316          // We deal with a dynamic group, let's search all users for whom
317          // memberOf is equal to our group's DN.
318          // We also need to look for labelelURI?
319        }
320    }
321
322  return _members;
323}
324
325//
326//
327//
328- (BOOL) hasMemberWithUID: (NSString *) memberUID
329{
330
331  BOOL rc;
332
333  rc = NO;
334
335  // If _members is initialized, we use it as it's very accurate.
336  // Otherwise, we fallback on memcached in order to avoid
337  // decomposing the group all the time just to see if a user
338  // is a member of it.
339  if (_members)
340    {
341      NSString *currentUID;
342
343      int count, max;
344      max = [_members count];
345      for (count = 0; !rc && count < max; count++)
346	{
347	  currentUID = [[_members objectAtIndex: count] login];
348	  rc = [memberUID isEqualToString: currentUID];
349	}
350
351    }
352  else
353    {
354      NSString *key, *value;;
355      NSArray *a;
356
357      key = [NSString stringWithFormat: @"%@+%@", _identifier, _domain];
358      value = [[SOGoCache sharedCache] valueForKey: key];
359
360
361      // If the value isn't in memcached, that probably means -members was never called.
362      // We call it only once here.
363      if (!value)
364	{
365	  [self members];
366	  value = [[SOGoCache sharedCache] valueForKey: key];
367	}
368
369      a = [value componentsSeparatedByString: @","];
370      rc = [a containsObject: memberUID];
371    }
372
373  return rc;
374}
375
376@end
377