]> andersk Git - moira.git/blame - afssync/ptutils.c
Forgot to include ptutils.o
[moira.git] / afssync / ptutils.c
CommitLineData
da0b4fd5 1/* Copyright (C) 1990, 1989 Transarc Corporation - All rights reserved */
e1f001e5 2/*
3 * P_R_P_Q_# (C) COPYRIGHT IBM CORPORATION 1988
4 * LICENSED MATERIALS - PROPERTY OF IBM
5 * REFER TO COPYRIGHT INSTRUCTIONS FORM NUMBER G120-2083
6 */
7
dba0cf81 8
e1f001e5 9/*
10 Sherri Nichols
11 Information Technology Center
12 November, 1988
13
14 Modified May, 1989 by Jeff Schiller to keep disk file in
15 network byte order
16
17*/
18
dba0cf81 19#include <afs/param.h>
da0b4fd5 20#include <afs/stds.h>
dba0cf81 21#include <sys/types.h>
e1f001e5 22#include <stdio.h>
da0b4fd5 23#ifdef AFS_HPUX_ENV
24#include <string.h>
25#else
26#include <strings.h>
27#endif
e1f001e5 28#include <lock.h>
dba0cf81 29#include <netinet/in.h>
e1f001e5 30#include <ubik.h>
31#include <rx/xdr.h>
dba0cf81 32#include <afs/com_err.h>
33#include "ptserver.h"
34#include "pterror.h"
da0b4fd5 35
36RCSID ("$Header$")
e1f001e5 37
e1f001e5 38extern struct ubik_dbase *dbase;
39extern struct afsconf_dir *prdir;
dba0cf81 40extern int pr_noAuth;
e1f001e5 41
dba0cf81 42static char *whoami = "ptserver";
43
44/* CorrectUserName - Check to make sure a user name is OK. It must not include
45 * either a colon (or it would look like a group) or an atsign (or it would
46 * look like a foreign user). The length is checked as well to make sure
47 * that the user name, an atsign, and the local cell name will fit in
48 * PR_MAXNAMELEN. This is so this user can fit in another cells database as
49 * a foreign user with our cell name tacked on. This is a predicate, so it
50 * return one if name is OK and zero if name is bogus. */
51
52static int CorrectUserName (name)
53 char *name;
54{
55 extern int pr_realmNameLen;
56
da0b4fd5 57#ifdef CROSS_CELL
58 if (index (name, ':') || index(name, '\n')) return 0;
59#else
dba0cf81 60 if (index (name, ':') || index(name, '@') || index(name, '\n')) return 0;
da0b4fd5 61#endif
dba0cf81 62 if (strlen (name) >= PR_MAXNAMELEN - pr_realmNameLen - 1) return 0;
63 return 1;
64}
65
da0b4fd5 66/* CorrectGroupName - Like the above but handles more complicated cases caused
67 * by including the ownership in the name. The interface works by calculating
68 * the correct name based on a given name and owner. This allows easy use by
69 * rename, which then compares the correct name with the requested new name. */
70
dba0cf81 71static long CorrectGroupName (ut, aname, cid, oid, cname)
72 struct ubik_trans *ut;
73 char aname[PR_MAXNAMELEN]; /* name for group */
74 long cid; /* caller id */
75 long oid; /* owner of group */
76 char cname[PR_MAXNAMELEN]; /* correct name for group */
77{
78 long code;
79 int admin;
80 char *prefix; /* ptr to group owner part */
81 char *suffix; /* ptr to group name part */
82 char name[PR_MAXNAMELEN]; /* correct name for group */
83 struct prentry tentry;
84
85 if (strlen (aname) >= PR_MAXNAMELEN) return PRBADNAM;
86 admin = pr_noAuth || IsAMemberOf (ut, cid, SYSADMINID);
87
88 if (oid == 0) oid = cid;
89
90 /* Determine the correct prefix for the name. */
91 if (oid == SYSADMINID) prefix = "system";
92 else {
93 long loc = FindByID (ut, oid);
94 if (loc == 0) {
95 /* let admin create groups owned by non-existent ids (probably
96 * setting a group to own itself). Check that they look like
97 * groups (with a colon) or otherwise are good user names. */
98 if (admin) {
99 strcpy (cname, aname);
100 goto done;
101 }
102 return PRNOENT;
103 }
104 code = pr_Read (ut, 0, loc, &tentry, sizeof(tentry));
105 if (code) return code;
106 if (ntohl(tentry.flags) & PRGRP) {
107 if ((tentry.count == 0) && !admin) return PRGROUPEMPTY;
108 /* terminate prefix at colon if there is one */
109 if (prefix = index(tentry.name, ':')) *prefix = 0;
110 }
111 prefix = tentry.name;
112 }
113 /* only sysadmin allow to use 'system:' prefix */
114 if ((strcmp (prefix, "system") == 0) && !admin) return PRPERM;
115
116 strcpy (name, aname); /* in case aname & cname are same */
117 suffix = index(name, ':');
118 if (suffix == 0) {
119 /* sysadmin can make groups w/o ':', but they must still look like
120 * legal user names. */
121 if (!admin) return PRBADNAM;
122 strcpy (cname, name);
123 }
124 else {
125 if (strlen(prefix)+strlen(suffix) >= PR_MAXNAMELEN) return PRBADNAM;
126 strcpy (cname, prefix);
127 strcat (cname, suffix);
128 }
129 done:
130 /* check for legal name with either group rules or user rules */
131 if (suffix = index(cname, ':')) {
132 /* check for confusing characters */
da0b4fd5 133#ifdef CROSS_CELL
134 if (index(cname, '\n') || /* restrict so recreate can work */
135 index(suffix+1, ':')) /* avoid multiple colons */
136 return PRBADNAM;
137#else
dba0cf81 138 if (index(cname, '@') || /* avoid confusion w/ foreign users */
139 index(cname, '\n') || /* restrict so recreate can work */
140 index(suffix+1, ':')) /* avoid multiple colons */
141 return PRBADNAM;
da0b4fd5 142#endif
dba0cf81 143 } else {
144 if (!CorrectUserName (cname)) return PRBADNAM;
145 }
146 return 0;
147}
148
149int AccessOK (ut, cid, tentry, mem, any)
150 struct ubik_trans *ut;
151 long cid; /* caller id */
152 struct prentry *tentry; /* object being accessed */
153 int mem; /* check membership in aid, if group */
154 int any; /* if set return true */
155{ long flags;
156 long oid;
157 long aid;
158
159 if (pr_noAuth) return 1;
da0b4fd5 160 if (cid == SYSADMINID) return 1; /* special case fileserver */
dba0cf81 161 if (tentry) {
162 flags = tentry->flags;
163 oid = tentry->owner;
164 aid = tentry->id;
165 } else {
166 flags = oid = aid = 0;
167 }
168 if (!(flags & PRACCESS)) /* provide default access */
169 if (flags & PRGRP)
170 flags |= PRP_GROUP_DEFAULT;
171 else
172 flags |= PRP_USER_DEFAULT;
173
174 if (flags & any) return 1;
175 if (oid) {
176 if ((cid == oid) ||
177 IsAMemberOf (ut, cid, oid)) return 1;
178 }
179 if (aid > 0) { /* checking on a user */
180 if (aid == cid) return 1;
181 } else if (aid < 0) { /* checking on group */
182 if ((flags & mem) && IsAMemberOf (ut, cid, aid)) return 1;
183 }
184 if (IsAMemberOf (ut, cid, SYSADMINID)) return 1;
185 return 0; /* no access */
186}
187
188long CreateEntry (at, aname, aid, idflag, flag, oid, creator)
189 register struct ubik_trans *at;
190 char aname[PR_MAXNAMELEN];
191 long *aid;
192 long idflag;
193 long flag;
194 long oid;
195 long creator;
e1f001e5 196{
197 /* get and init a new entry */
198 register long code;
199 long newEntry;
e1f001e5 200 struct prentry tentry;
201
202 bzero(&tentry, sizeof(tentry));
dba0cf81 203
204 if ((oid == 0) || (oid == ANONYMOUSID)) oid = creator;
205
206 if (flag & PRGRP) {
207 code = CorrectGroupName (at, aname, creator, oid, tentry.name);
208 if (code) return code;
209 if (strcmp (aname, tentry.name) != 0) return PRBADNAM;
210 } else { /* non-group must not have colon */
211 if (!CorrectUserName(aname)) return PRBADNAM;
212 strcpy (tentry.name, aname);
213 }
214
215 if (FindByName(at,aname)) return PREXIST;
216
e1f001e5 217 newEntry = AllocBlock(at);
218 if (!newEntry) return PRDBFAIL;
dba0cf81 219#ifdef PR_REMEMBER_TIMES
220 tentry.createTime = time(0);
221#endif
e1f001e5 222 if (flag & PRGRP) {
223 tentry.flags |= PRGRP;
224 tentry.owner = oid;
225 }
226 else if (flag & PRFOREIGN) {
227 tentry.flags |= PRFOREIGN;
228 tentry.owner = oid;
229 }
230 else tentry.owner = SYSADMINID;
da0b4fd5 231
232#ifdef CROSS_CELL
233#define ADD_TO_AUTHUSER_GROUP 1
234#define AUTHUSER_GROUP "system:authuser"
235 {
236 char * atsign;
237
238 if (!(atsign= index(aname,'@'))) { /* No @ so local cell*/
239 if (idflag)
240 tentry.id = *aid;
241 else {
242 code= AllocID(at,flag,&tentry.id);
243 if (code != PRSUCCESS) return code;
244 }
245 } else {
246 /*foreign cells are represented by the group system:authuser@cell*/
247 if (flag & PRGRP) {
248 /* it's a new foreign cell so the format
249 * must be AUTHUSER_GROUP@cellname */
250 int badFormat;
251
252 *atsign = '\0';
253 badFormat = strcmp(AUTHUSER_GROUP, aname);
254 *atsign = '@';
255 if (badFormat) return PRBADNAM;
256 if (idflag)
257 tentry.id = *aid;
258 else {
259 code= AllocID(at,flag,&tentry.id);
260 if (code != PRSUCCESS) return code;
261 }
262 } else {
263 /* it's a foreign cell entry */
264 char *cellGroup;
265 long pos;
266 struct prentry centry;
267 extern long allocNextId();
268 extern long AddToEntry();
269
270 cellGroup = (char *) malloc (strlen(AUTHUSER_GROUP) +
271 strlen(atsign) +1);
272 strcpy(cellGroup, AUTHUSER_GROUP);
273 strcat(cellGroup, atsign);
274 pos = FindByName(at,cellGroup);
275
276 /* if the group doesn't exist don't allow user creation */
277 if (!pos) return PRBADNAM;
278
279 code = pr_Read (at, 0, pos, &centry, sizeof(centry));
280 if (code) return code;
281 tentry.cellid = ntohl(centry.id);
282 /* cellid is the id of the group representing the cell */
283
284 if (idflag) {
285 if (!inRange(&centry,*aid))
286 return PRBADARG; /* the id specified is not in
287 * the id space of the group */
288 tentry.id = *aid;
289 } else
290 /* allocNextID() will allocate the next id
291 * in that cell's space */
292 tentry.id = allocNextId(&centry);
293
294 /* charge the cell group for the new user and test quota */
295 if (!(ntohl(centry.flags) & PRQUOTA)) {
296 /* quota uninitialized, so initialize it now */
297 centry.flags = htonl (ntohl(centry.flags) | PRQUOTA);
298 centry.ngroups = htonl(30);
299 }
300
301 centry.ngroups = htonl(ntohl(centry.ngroups) - 1);
302 if ( centry.ngroups < 0)
303 if (!pr_noAuth) return PRNOMORE;
304
305#if !ADD_TO_AUTHUSER_GROUP
306 centry.count = htonl(ntohl(centry.ngroups) +1);
307 /* keep count of how many people are in the group. */
308#endif
309
310 code = pr_Write (at, 0, pos, &centry, sizeof(centry));
311 /* write updated entry for group */
312
313 /* Now add the new user entry to the database */
314
315 tentry.creator = creator;
316 *aid = tentry.id;
317 code = pr_WriteEntry(at, 0, newEntry, &tentry);
318 if (code) return PRDBFAIL;
319 code = AddToIDHash(at,*aid,newEntry);
320 if (code != PRSUCCESS) return code;
321 code = AddToNameHash(at,aname,newEntry);
322 if (code != PRSUCCESS) return code;
323 if (inc_header_word (at, foreigncount, 1)) return PRDBFAIL;
324
325#if ADD_TO_AUTHUSER_GROUP
326
327 /* Now add the entry to the authuser group for this cell.
328 * We will reread the entries for the user and the group
329 * instead of modifying them before writing them in the
330 * previous steps. Although not very efficient, much simpler */
331
332 /* First update the group entry */
333 pos = FindByID(at,tentry.cellid);
334 if (!pos) return PRBADNAM;
335 code = pr_ReadEntry (at, 0, pos, &centry);
336 if (code) return code;
337 code = AddToEntry(at, &centry,pos,*aid);
338 if (code) return code;
339 /* and now the user entry */
340 pos = FindByID(at,*aid);
341 if (!pos) return PRBADNAM;
342 code = pr_ReadEntry(at, 0, pos, &tentry);
343 if (code) return code;
344 code = AddToEntry(at, &tentry,pos,tentry.cellid);
345 if (code) return code;
346
347#endif
348
349 /* Ok we're done */
350 return PRSUCCESS;
351 }
352 }
353 }
354
355#else /* !CROSS_CELL */
356
e1f001e5 357 if (idflag)
358 tentry.id = *aid;
359 else {
360 code= AllocID(at,flag,&tentry.id);
361 if (code != PRSUCCESS) return code;
362 }
da0b4fd5 363#endif /* !CROSS_CELL */
364
e1f001e5 365 if (flag & PRGRP) {
dba0cf81 366 /* group ids are negative */
367 if (tentry.id < (long)ntohl(cheader.maxGroup)) {
368 code = set_header_word (at, maxGroup, htonl(tentry.id));
e1f001e5 369 if (code) return PRDBFAIL;
370 }
371 }
372 else if (flag & PRFOREIGN) {
dba0cf81 373 if (tentry.id > (long)ntohl(cheader.maxForeign)) {
374 code = set_header_word (at, maxForeign, htonl(tentry.id));
e1f001e5 375 if (code) return PRDBFAIL;
376 }
377 }
378 else {
dba0cf81 379 if (tentry.id > (long)ntohl(cheader.maxID)) {
380 code = set_header_word (at, maxID, htonl(tentry.id));
e1f001e5 381 if (code) return PRDBFAIL;
382 }
383 }
dba0cf81 384 /* PRACCESS is off until set, defaults provided in AccessOK */
385 if (flag == 0) { /* only normal users get quota */
386 tentry.flags |= PRQUOTA;
387 tentry.ngroups = tentry.nusers = 20;
388 }
389
390 if (flag & (PRGRP | PRFOREIGN)) {
391 long loc = FindByID (at, creator);
392 struct prentry centry;
393 long *nP; /* ptr to entry to be decremented */
394 long n; /* quota to check */
395
396 if (loc) { /* this should only fail during initialization */
397 code = pr_Read (at, 0, loc, &centry, sizeof(centry));
398 if (code) return code;
399
400 if (flag & PRGRP) nP = &centry.ngroups;
401 else if (flag & PRFOREIGN) nP = &centry.nusers;
402 else nP = 0;
403
404 if (nP) {
405 if (!(ntohl(centry.flags) & PRQUOTA)) {
406 /* quota uninitialized, so do it now */
407 centry.flags = htonl (ntohl(centry.flags) | PRQUOTA);
408 centry.ngroups = centry.nusers = htonl(20);
409 }
410 n = ntohl(*nP);
411 if (n <= 0) {
412 if (!pr_noAuth &&
413 !IsAMemberOf (at, creator, SYSADMINID))
414 return PRNOMORE;
415 }
da0b4fd5 416 else { /* don't use up admin user's quota */
417 int admin = ((creator == SYSADMINID) ||
418 IsAMemberOf (at, creator, SYSADMINID));
419 if (!admin) *nP = htonl(n-1);
420 }
dba0cf81 421 }
422 code = pr_Write (at, 0, loc, &centry, sizeof(centry));
423 if (code) return code;
424 } /* if (loc) */
425 } /* need to check creation quota */
e1f001e5 426 tentry.creator = creator;
427 *aid = tentry.id;
e1f001e5 428 code = pr_WriteEntry(at, 0, newEntry, &tentry);
429 if (code) return PRDBFAIL;
430 code = AddToIDHash(at,*aid,newEntry);
431 if (code != PRSUCCESS) return code;
432 code = AddToNameHash(at,aname,newEntry);
433 if (code != PRSUCCESS) return code;
434 if (tentry.flags & PRGRP) {
435 code = AddToOwnerChain(at,tentry.id,oid);
436 if (code) return code;
437 }
438 if (tentry.flags & PRGRP) {
dba0cf81 439 if (inc_header_word (at, groupcount, 1)) return PRDBFAIL;
e1f001e5 440 }
441 else if (tentry.flags & PRFOREIGN) {
dba0cf81 442 if (inc_header_word (at, foreigncount, 1)) return PRDBFAIL;
e1f001e5 443 }
444 else if (tentry.flags & PRINST) {
dba0cf81 445 if (inc_header_word (at, instcount, 1)) return PRDBFAIL;
e1f001e5 446 }
447 else {
dba0cf81 448 if (inc_header_word (at, usercount, 1)) return PRDBFAIL;
e1f001e5 449 }
450 return PRSUCCESS;
451}
452
453
dba0cf81 454/* RemoveFromEntry - remove aid from bid's entries list, freeing a continuation
455 * entry if appropriate */
e1f001e5 456
dba0cf81 457long RemoveFromEntry (at, aid, bid)
458 register struct ubik_trans *at;
459 register long aid;
460 register long bid;
e1f001e5 461{
e1f001e5 462 register long code;
463 struct prentry tentry;
464 struct contentry centry;
465 struct contentry hentry;
466 long temp;
e1f001e5 467 long i,j;
468 long nptr;
dba0cf81 469 long hloc;
e1f001e5 470
dba0cf81 471 if (aid == bid) return PRINCONSISTENT;
e1f001e5 472 bzero(&hentry,sizeof(hentry));
473 temp = FindByID(at,bid);
dba0cf81 474 if (temp == 0) return PRNOENT;
e1f001e5 475 code = pr_ReadEntry(at, 0, temp, &tentry);
476 if (code != 0) return code;
dba0cf81 477#ifdef PR_REMEMBER_TIMES
478 tentry.removeTime = time(0);
479#endif
e1f001e5 480 for (i=0;i<PRSIZE;i++) {
481 if (tentry.entries[i] == aid) {
482 tentry.entries[i] = PRBADID;
483 tentry.count--;
484 code = pr_WriteEntry(at,0,temp,&tentry);
485 if (code != 0) return code;
486 return PRSUCCESS;
487 }
488 if (tentry.entries[i] == 0) /* found end of list */
489 return PRNOENT;
490 }
dba0cf81 491 hloc = 0;
492 nptr = tentry.next;
493 while (nptr != NULL) {
494 code = pr_ReadCoEntry(at,0,nptr,&centry);
495 if (code != 0) return code;
496 if ((centry.id != bid) || !(centry.flags & PRCONT)) return PRDBBAD;
497 for (i=0;i<COSIZE;i++) {
498 if (centry.entries[i] == aid) {
499 centry.entries[i] = PRBADID;
500 for (j=0;j<COSIZE;j++)
501 if (centry.entries[j] != PRBADID &&
502 centry.entries[j] != 0) break;
503 if (j == COSIZE) { /* can free this block */
504 if (hloc == 0) {
505 tentry.next = centry.next;
e1f001e5 506 }
dba0cf81 507 else {
508 hentry.next = centry.next;
509 code = pr_WriteCoEntry (at, 0, hloc, &hentry);
e1f001e5 510 if (code != 0) return code;
e1f001e5 511 }
dba0cf81 512 code = FreeBlock (at, nptr);
513 if (code) return code;
e1f001e5 514 }
dba0cf81 515 else { /* can't free it yet */
516 code = pr_WriteCoEntry(at,0,nptr,&centry);
517 if (code != 0) return code;
518 }
519 tentry.count--;
520 code = pr_WriteEntry(at,0,temp,&tentry);
521 if (code) return PRDBFAIL;
522 return 0;
e1f001e5 523 }
dba0cf81 524 if (centry.entries[i] == 0) return PRNOENT;
525 } /* for all coentry slots */
526 hloc = nptr;
527 nptr = centry.next;
528 bcopy(&centry,&hentry,sizeof(centry));
529 } /* while there are coentries */
530 return PRNOENT;
e1f001e5 531}
532
dba0cf81 533/* DeleteEntry - delete the entry in tentry at loc, removing it from all
534 * groups, putting groups owned by it on orphan chain, and freeing the space */
535
536long DeleteEntry (at, tentry, loc)
537 register struct ubik_trans *at;
538 struct prentry *tentry;
539 long loc;
e1f001e5 540{
e1f001e5 541 register long code;
e1f001e5 542 struct contentry centry;
e1f001e5 543 register long i;
544 long nptr;
dba0cf81 545
da0b4fd5 546#ifdef CROSS_CELL
547 if (index(tentry->name,'@')) {
548 if (tentry->flags & PRGRP) {
549 /* If there are still foreign user accounts from that cell
550 * don't delete the group */
551 if (tentry->count) return PRBADARG;
552 } else {
553 /* It's a user adjust the group quota upwards */
554 long loc = FindByID (at, tentry->cellid);
555 struct prentry centry;
556 if (loc) {
557 code = pr_Read (at, 0, loc, &centry, sizeof(centry));
558 if (code) return code;
559 if (ntohl(centry.flags) & PRQUOTA) {
560 centry.ngroups = htonl(ntohl(centry.ngroups) + 1);
561 }
562#if !ADD_TO_AUTHUSER_GROUP
563 /* if this is a foreign cell entry then decrement the number of
564 * existing users in the prentry of the authuser group for that
565 * cell
566 */
567 centry.count = htonl(ntohl(centry.count) - 1);
568#endif
569 code = pr_Write (at, 0, loc, &centry, sizeof(centry));
570 if (code) return code;
571 }
572 }
573 }
574#endif /* CROSS_CELL */
575
dba0cf81 576 /* First remove the entire membership list */
e1f001e5 577 for (i=0;i<PRSIZE;i++) {
dba0cf81 578 if (tentry->entries[i] == PRBADID) continue;
579 if (tentry->entries[i] == 0) break;
580 code = RemoveFromEntry (at, tentry->id, tentry->entries[i]);
581 if (code) return code;
e1f001e5 582 }
dba0cf81 583 nptr = tentry->next;
e1f001e5 584 while (nptr != NULL) {
585 code = pr_ReadCoEntry(at,0,nptr,&centry);
586 if (code != 0) return PRDBFAIL;
587 for (i=0;i<COSIZE;i++) {
dba0cf81 588 if (centry.entries[i] == PRBADID) continue;
e1f001e5 589 if (centry.entries[i] == 0) break;
dba0cf81 590 code = RemoveFromEntry (at, tentry->id, centry.entries[i]);
591 if (code) return code;
e1f001e5 592 }
dba0cf81 593 code = FreeBlock (at, nptr); /* free continuation block */
594 if (code) return code;
e1f001e5 595 nptr = centry.next;
596 }
dba0cf81 597
598 /* Remove us from other's owned chain. Note that this will zero our owned
599 * field (on disk) so this step must follow the above step in case we are
600 * on our own owned list. */
601 if (tentry->flags & PRGRP) {
602 if (tentry->owner) {
603 code = RemoveFromOwnerChain (at, tentry->id, tentry->owner);
e1f001e5 604 if (code) return code;
605 }
606 else {
dba0cf81 607 code = RemoveFromOrphan (at, tentry->id);
e1f001e5 608 if (code) return code;
609 }
610 }
dba0cf81 611
612 code = RemoveFromIDHash(at,tentry->id,&loc);
e1f001e5 613 if (code != PRSUCCESS) return code;
dba0cf81 614 code = RemoveFromNameHash(at,tentry->name,&loc);
e1f001e5 615 if (code != PRSUCCESS) return code;
dba0cf81 616
617 if (tentry->flags & (PRGRP | PRFOREIGN)) {
618 long loc = FindByID (at, tentry->creator);
619 struct prentry centry;
da0b4fd5 620 int admin;
dba0cf81 621 if (loc) {
622 code = pr_Read (at, 0, loc, &centry, sizeof(centry));
623 if (code) return code;
da0b4fd5 624 admin = ((tentry->creator == SYSADMINID) ||
625 IsAMemberOf (at, tentry->creator, SYSADMINID));
dba0cf81 626 if (ntohl(centry.flags) & PRQUOTA) {
da0b4fd5 627 if ((tentry->flags & PRGRP) &&
628 !(admin && (ntohl(centry.ngroups) >= 20))) {
dba0cf81 629 centry.ngroups = htonl(ntohl(centry.ngroups) + 1);
da0b4fd5 630 } else if ((tentry->flags & PRFOREIGN) &&
631 !(admin && (ntohl(centry.nusers) >= 20))) {
dba0cf81 632 centry.nusers = htonl(ntohl(centry.nusers) + 1);
633 }
634 }
635 code = pr_Write (at, 0, loc, &centry, sizeof(centry));
636 if (code) return code;
637 }
e1f001e5 638 }
dba0cf81 639
640 if (tentry->flags & PRGRP) {
641 if (inc_header_word (at, groupcount, -1)) return PRDBFAIL;
e1f001e5 642 }
dba0cf81 643 else if (tentry->flags & PRFOREIGN) {
644 if (inc_header_word (at, foreigncount, -1)) return PRDBFAIL;
645 }
646 else if (tentry->flags & PRINST) {
647 if (inc_header_word (at, instcount, -1)) return PRDBFAIL;
e1f001e5 648 }
649 else {
dba0cf81 650 if (inc_header_word (at, usercount, -1)) return PRDBFAIL;
e1f001e5 651 }
dba0cf81 652 code = FreeBlock(at, loc);
653 return code;
e1f001e5 654}
655
dba0cf81 656/* AddToEntry - add aid to entry's entries list, alloc'ing a continuation block
657 * if needed.
658 *
659 * Note the entry is written out by this routine. */
e1f001e5 660
dba0cf81 661long AddToEntry (tt, entry, loc, aid)
662 struct ubik_trans *tt;
663 struct prentry *entry;
664 long loc;
665 long aid;
e1f001e5 666{
e1f001e5 667 register long code;
668 long i;
669 struct contentry nentry;
670 struct contentry aentry;
671 long nptr;
dba0cf81 672 long last; /* addr of last cont. block */
e1f001e5 673 long first = 0;
674 long cloc;
675 long slot = -1;
676
dba0cf81 677 if (entry->id == aid) return PRINCONSISTENT;
678#ifdef PR_REMEMBER_TIMES
679 entry->addTime = time(0);
680#endif
e1f001e5 681 for (i=0;i<PRSIZE;i++) {
dba0cf81 682 if (entry->entries[i] == aid)
e1f001e5 683 return PRIDEXIST;
dba0cf81 684 if (entry->entries[i] == PRBADID) { /* remember this spot */
e1f001e5 685 first = 1;
686 slot = i;
687 }
dba0cf81 688 else if (entry->entries[i] == 0) { /* end of the line */
e1f001e5 689 if (slot == -1) {
690 first = 1;
691 slot = i;
692 }
693 break;
694 }
695 }
dba0cf81 696 last = 0;
697 nptr = entry->next;
e1f001e5 698 while (nptr != NULL) {
dba0cf81 699 code = pr_ReadCoEntry(tt,0,nptr,&nentry);
e1f001e5 700 if (code != 0) return code;
dba0cf81 701 last = nptr;
e1f001e5 702 if (!(nentry.flags & PRCONT)) return PRDBFAIL;
703 for (i=0;i<COSIZE;i++) {
704 if (nentry.entries[i] == aid)
705 return PRIDEXIST;
706 if (nentry.entries[i] == PRBADID) {
707 if (slot == -1) {
708 slot = i;
709 cloc = nptr;
710 }
711 }
dba0cf81 712 else if (nentry.entries[i] == 0) {
e1f001e5 713 if (slot == -1) {
714 slot = i;
715 cloc = nptr;
716 }
717 break;
718 }
719 }
e1f001e5 720 nptr = nentry.next;
721 }
dba0cf81 722 if (slot != -1) { /* we found a place */
723 entry->count++;
e1f001e5 724 if (first) { /* place is in first block */
dba0cf81 725 entry->entries[slot] = aid;
726 code = pr_WriteEntry (tt, 0, loc, entry);
e1f001e5 727 if (code != 0) return code;
728 return PRSUCCESS;
729 }
dba0cf81 730 code = pr_WriteEntry (tt, 0, loc, entry);
731 if (code) return code;
e1f001e5 732 code = pr_ReadCoEntry(tt,0,cloc,&aentry);
733 if (code != 0) return code;
734 aentry.entries[slot] = aid;
735 code = pr_WriteCoEntry(tt,0,cloc,&aentry);
736 if (code != 0) return code;
737 return PRSUCCESS;
738 }
739 /* have to allocate a continuation block if we got here */
740 nptr = AllocBlock(tt);
dba0cf81 741 if (last) {
742 /* then we should tack new block after last block in cont. chain */
e1f001e5 743 nentry.next = nptr;
744 code = pr_WriteCoEntry(tt,0,last,&nentry);
745 if (code != 0) return code;
746 }
747 else {
dba0cf81 748 entry->next = nptr;
e1f001e5 749 }
dba0cf81 750 bzero(&aentry,sizeof(aentry));
e1f001e5 751 aentry.flags |= PRCONT;
dba0cf81 752 aentry.id = entry->id;
e1f001e5 753 aentry.next = NULL;
754 aentry.entries[0] = aid;
755 code = pr_WriteCoEntry(tt,0,nptr,&aentry);
756 if (code != 0) return code;
757 /* don't forget to update count, here! */
dba0cf81 758 entry->count++;
759 code = pr_WriteEntry (tt, 0, loc, entry);
760 return code;
e1f001e5 761
762}
763
dba0cf81 764long AddToPRList (alist, sizeP, id)
765 prlist *alist;
766 int *sizeP;
767 long id;
768{
769 if (alist->prlist_len >= PR_MAXGROUPS) return PRTOOMANY;
770 if (alist->prlist_len >= *sizeP) {
771 *sizeP = *sizeP + 100;
772 if (*sizeP > PR_MAXGROUPS) *sizeP = PR_MAXGROUPS;
773 alist->prlist_val =
774 (long *) ((alist->prlist_val) ?
775 realloc (alist->prlist_val, (*sizeP)*sizeof(long)) :
776 malloc ((*sizeP)*sizeof(long)));
777 }
778 alist->prlist_val[alist->prlist_len++] = id;
779 return 0;
780}
781
782long GetList (at, tentry, alist, add)
783 struct ubik_trans *at;
784 struct prentry *tentry;
785 prlist *alist;
786 long add;
e1f001e5 787{
788 register long code;
e1f001e5 789 long i;
e1f001e5 790 struct contentry centry;
791 long nptr;
dba0cf81 792 int size;
793 int count = 0;
e1f001e5 794 extern long IDCmp();
795
dba0cf81 796 size = 0;
797 alist->prlist_val = 0;
e1f001e5 798 alist->prlist_len = 0;
dba0cf81 799
e1f001e5 800 for (i=0;i<PRSIZE;i++) {
dba0cf81 801 if (tentry->entries[i] == PRBADID) continue;
802 if (tentry->entries[i] == 0) break;
803 code = AddToPRList (alist, &size, tentry->entries[i]);
804 if (code) return code;
e1f001e5 805 }
dba0cf81 806
807 nptr = tentry->next;
e1f001e5 808 while (nptr != NULL) {
809 /* look through cont entries */
810 code = pr_ReadCoEntry(at,0,nptr,&centry);
811 if (code != 0) return code;
812 for (i=0;i<COSIZE;i++) {
813 if (centry.entries[i] == PRBADID) continue;
814 if (centry.entries[i] == 0) break;
dba0cf81 815 code = AddToPRList (alist, &size, centry.entries[i]);
816 if (code) return code;
e1f001e5 817 }
818 nptr = centry.next;
dba0cf81 819 if (count++ > 50) IOMGR_Poll(), count = 0;
e1f001e5 820 }
dba0cf81 821
e1f001e5 822 if (add) { /* this is for a CPS, so tack on appropriate stuff */
dba0cf81 823 if (tentry->id != ANONYMOUSID && tentry->id != ANYUSERID) {
da0b4fd5 824#ifdef CROSS_CELL
825 if ((code = AddToPRList (alist, &size, ANYUSERID)) ||
826 (code = AddAuthGroup(tentry, alist, &size)) ||
827 (code = AddToPRList (alist, &size, tentry->id))) return code;
828#else
dba0cf81 829 if ((code = AddToPRList (alist, &size, ANYUSERID)) ||
830 (code = AddToPRList (alist, &size, AUTHUSERID)) ||
831 (code = AddToPRList (alist, &size, tentry->id))) return code;
da0b4fd5 832#endif
e1f001e5 833 }
834 else {
dba0cf81 835 if ((code = AddToPRList (alist, &size, ANYUSERID)) ||
836 (code = AddToPRList (alist, &size, tentry->id))) return code;
e1f001e5 837 }
838 }
dba0cf81 839 if (alist->prlist_len > 100) IOMGR_Poll();
840 qsort(alist->prlist_val,alist->prlist_len,sizeof(long),IDCmp);
841 return PRSUCCESS;
842}
843
844long GetOwnedChain (ut, next, alist)
845 struct ubik_trans *ut;
846 long next;
847 prlist *alist;
848{ register long code;
849 struct prentry tentry;
850 int size;
851 int count = 0;
da0b4fd5 852 extern long IDCmp();
dba0cf81 853
854 size = 0;
855 alist->prlist_val = 0;
856 alist->prlist_len = 0;
857
858 while (next) {
859 code = pr_Read (ut, 0, next, &tentry, sizeof(tentry));
860 if (code) return code;
861 code = AddToPRList (alist, &size, ntohl(tentry.id));
862 if (code) return code;
863 next = ntohl(tentry.nextOwned);
864 if (count++ > 50) IOMGR_Poll(), count = 0;
865 }
866 if (alist->prlist_len > 100) IOMGR_Poll();
e1f001e5 867 qsort(alist->prlist_val,alist->prlist_len,sizeof(long),IDCmp);
868 return PRSUCCESS;
869}
870
871long GetMax(at,uid,gid)
872register struct ubik_trans *at;
873long *uid;
874long *gid;
875{
876 *uid = ntohl(cheader.maxID);
877 *gid = ntohl(cheader.maxGroup);
878 return PRSUCCESS;
879}
880
881long SetMax(at,id,flag)
882register struct ubik_trans *at;
883long id;
884long flag;
885{
886 register long code;
887 if (flag & PRGRP) {
888 cheader.maxGroup = htonl(id);
889 code = pr_Write(at,0,16,(char *)&cheader.maxGroup,sizeof(cheader.maxGroup));
890 if (code != 0) return code;
891 }
892 else {
893 cheader.maxID = htonl(id);
894 code = pr_Write(at,0,20,(char *)&cheader.maxID,sizeof(cheader.maxID));
895 if (code != 0) return code;
896 }
897 return PRSUCCESS;
898}
899
dba0cf81 900int pr_noAuth;
901
902long Initdb()
e1f001e5 903{
904 long code;
905 struct ubik_trans *tt;
906 long len;
e1f001e5 907 static long initd=0;
dba0cf81 908#if 0
e1f001e5 909 static struct ubik_version curver;
910 struct ubik_version newver;
dba0cf81 911#endif
e1f001e5 912
913 /* init the database. We'll try reading it, but if we're starting from scratch, we'll have to do a write transaction. */
914
dba0cf81 915 pr_noAuth = afsconf_GetNoAuthFlag(prdir);
916
da0b4fd5 917 code = ubik_BeginTransReadAny(dbase,UBIK_READTRANS, &tt);
e1f001e5 918 if (code) return code;
919 code = ubik_SetLock(tt,1,1,LOCKREAD);
920 if (code) {
921 ubik_AbortTrans(tt);
922 return code;
923 }
924 if (!initd) {
925 initd = 1;
dba0cf81 926#if 0
e1f001e5 927 bzero(&curver,sizeof(curver));
dba0cf81 928#endif
929 } else if (!ubik_CacheUpdate (tt)) {
930 code = ubik_EndTrans(tt);
931 return code;
932 }
933#if 0
934 code = ubik_GetVersion(tt,&newver);
935 if (vcmp(curver,newver) == 0) {
936 /* same version */
937 code = ubik_EndTrans(tt);
938 if (code) return code;
939 return PRSUCCESS;
e1f001e5 940 }
dba0cf81 941 bcopy(&newver,&curver,sizeof(struct ubik_version));
942#endif
943
e1f001e5 944 len = sizeof(cheader);
945 code = pr_Read(tt, 0, 0, (char *) &cheader, len);
946 if (code != 0) {
dba0cf81 947 com_err (whoami, code, "couldn't read header");
e1f001e5 948 ubik_AbortTrans(tt);
949 return code;
950 }
dba0cf81 951 if ((ntohl(cheader.version) == PRDBVERSION) &&
952 ntohl(cheader.headerSize) == sizeof(cheader) &&
953 ntohl(cheader.eofPtr) != NULL &&
954 FindByID(tt,ANONYMOUSID) != 0){
e1f001e5 955 /* database exists, so we don't have to build it */
956 code = ubik_EndTrans(tt);
957 if (code) return code;
958 return PRSUCCESS;
959 }
960 /* else we need to build a database */
961 code = ubik_EndTrans(tt);
962 if (code) return code;
dba0cf81 963
964 /* Only rebuild database if the db was deleted (the header is zero) and we
965 are running noAuth. */
966 { char *bp = (char *)&cheader;
967 int i;
968 for (i=0; i<sizeof(cheader); i++)
969 if (bp[i]) {
970 code = PRDBBAD;
971 com_err (whoami, code,
972 "Can't rebuild database because it is not empty");
973 return code;
974 }
975 }
976 if (!pr_noAuth) {
977 code = PRDBBAD;
978 com_err (whoami, code,
979 "Can't rebuild database because not running NoAuth");
980 return code;
981 }
982
e1f001e5 983 code = ubik_BeginTrans(dbase,UBIK_WRITETRANS, &tt);
984 if (code) return code;
da0b4fd5 985
e1f001e5 986 code = ubik_SetLock(tt,1,1,LOCKWRITE);
987 if (code) {
988 ubik_AbortTrans(tt);
989 return code;
990 }
dba0cf81 991
da0b4fd5 992 /* before doing a rebuild, check again that the dbase looks bad, because
993 * the previous check was only under a ReadAny transaction, and there could
994 * actually have been a good database out there. Now that we have a
995 * real write transaction, make sure things are still bad.
996 */
997 if ((ntohl(cheader.version) == PRDBVERSION) &&
998 ntohl(cheader.headerSize) == sizeof(cheader) &&
999 ntohl(cheader.eofPtr) != NULL &&
1000 FindByID(tt,ANONYMOUSID) != 0){
1001 /* database exists, so we don't have to build it */
1002 code = ubik_EndTrans(tt);
1003 if (code) return code;
1004 return PRSUCCESS;
1005 }
dba0cf81 1006
da0b4fd5 1007 /* Initialize the database header */
dba0cf81 1008 if ((code = set_header_word (tt, version, htonl(PRDBVERSION))) ||
1009 (code = set_header_word (tt, headerSize, htonl(sizeof(cheader)))) ||
1010 (code = set_header_word (tt, eofPtr, cheader.headerSize))) {
1011 com_err (whoami, code, "couldn't write header words");
e1f001e5 1012 ubik_AbortTrans(tt);
1013 return code;
1014 }
dba0cf81 1015
1016#define InitialGroup(id,name) do { \
1017 long temp = (id); \
1018 long flag = (id) < 0 ? PRGRP : 0; \
1019 code = CreateEntry \
1020 (tt, (name), &temp, /*idflag*/1, flag, SYSADMINID, SYSADMINID); \
1021 if (code) { \
1022 com_err (whoami, code, "couldn't create %s with id %di.", \
1023 (name), (id)); \
1024 ubik_AbortTrans(tt); \
1025 return code; \
1026 } \
1027} while (0)
1028
1029 InitialGroup (SYSADMINID, "system:administrators");
1030 InitialGroup (ANYUSERID, "system:anyuser");
1031 InitialGroup (AUTHUSERID, "system:authuser");
1032 InitialGroup (ANONYMOUSID, "anonymous");
1033
1034 /* Well, we don't really want the max id set to anonymousid, so we'll set
1035 * it back to 0 */
1036 code = set_header_word (tt, maxID, 0); /* correct in any byte order */
1037 if (code) {
1038 com_err (whoami, code, "couldn't reset max id");
e1f001e5 1039 ubik_AbortTrans(tt);
1040 return code;
1041 }
dba0cf81 1042
e1f001e5 1043 code = ubik_EndTrans(tt);
1044 if (code) return code;
1045 return PRSUCCESS;
1046}
1047
dba0cf81 1048long ChangeEntry (at, aid, cid, name, oid, newid)
1049 struct ubik_trans *at;
1050 long aid;
1051 long cid;
1052 char *name;
1053 long oid;
1054 long newid;
e1f001e5 1055{
1056 register long code;
1057 long pos;
1058 struct prentry tentry;
1059 long loc;
dba0cf81 1060 long oldowner;
e1f001e5 1061 char holder[PR_MAXNAMELEN];
1062 char temp[PR_MAXNAMELEN];
dba0cf81 1063 char oldname[PR_MAXNAMELEN];
da0b4fd5 1064#if CROSS_CELL
1065 char *atsign;
1066#endif
e1f001e5 1067
e1f001e5 1068 bzero(holder,PR_MAXNAMELEN);
1069 bzero(temp,PR_MAXNAMELEN);
1070 loc = FindByID(at,aid);
1071 if (!loc) return PRNOENT;
1072 code = pr_ReadEntry(at,0,loc,&tentry);
1073 if (code) return PRDBFAIL;
dba0cf81 1074 if (tentry.owner != cid &&
1075 !IsAMemberOf(at,cid,SYSADMINID) &&
1076 !IsAMemberOf(at,cid,tentry.owner) &&
1077 !pr_noAuth) return PRPERM;
1078#ifdef PR_REMEMBER_TIMES
1079 tentry.changeTime = time(0);
1080#endif
1081
1082 /* we're actually trying to change the id */
1083 if (aid != newid && newid != 0) {
1084 if (!IsAMemberOf(at,cid,SYSADMINID) && !pr_noAuth) return PRPERM;
e1f001e5 1085 pos = FindByID(at,newid);
1086 if (pos) return PRIDEXIST; /* new id already in use! */
da0b4fd5 1087 if ((aid < 0 && newid > 0) || (aid > 0 && newid < 0)) return PRPERM;
e1f001e5 1088 /* if new id is not in use, rehash things */
1089 code = RemoveFromIDHash(at,aid,&loc);
1090 if (code != PRSUCCESS) return code;
1091 tentry.id = newid;
1092 code = pr_WriteEntry(at,0,loc,&tentry);
dba0cf81 1093 if (code) return code;
e1f001e5 1094 code = AddToIDHash(at,tentry.id,loc);
1095 if (code) return code;
dba0cf81 1096 /* get current data */
1097 code = pr_ReadEntry(at,0,loc,&tentry);
1098 if (code) return PRDBFAIL;
e1f001e5 1099 }
dba0cf81 1100
da0b4fd5 1101#ifdef CROSS_CELL
1102 atsign = index(tentry.name, '@'); /* check for foreign entry */
1103#endif
1104
dba0cf81 1105 /* Change the owner */
e1f001e5 1106 if (tentry.owner != oid && oid) {
dba0cf81 1107 /* only groups can have their owner's changed */
1108 if (!(tentry.flags & PRGRP)) return PRPERM;
da0b4fd5 1109#ifdef CROSS_CELL
1110 /* don't allow modifications to foreign cell group owners */
1111 if (atsign) return PRPERM;
1112#endif
dba0cf81 1113 oldowner = tentry.owner;
1114 tentry.owner = oid;
1115 /* The entry must be written through first so Remove and Add routines
1116 * can operate on disk data */
1117 code = pr_WriteEntry(at,0,loc,(char *)&tentry);
1118 if (code) return PRDBFAIL;
e1f001e5 1119 if (tentry.flags & PRGRP) {
dba0cf81 1120 /* switch owner chains */
1121 if (oldowner) /* if it has an owner */
1122 code = RemoveFromOwnerChain(at,tentry.id,oldowner);
1123 else /* must be an orphan */
e1f001e5 1124 code = RemoveFromOrphan(at,tentry.id);
1125 if (code) return code;
dba0cf81 1126 code = AddToOwnerChain(at,tentry.id,tentry.owner);
e1f001e5 1127 if (code) return code;
1128 }
dba0cf81 1129 /* fix up the name */
1130 if ((tentry.flags & PRGRP) && (strlen(name) == 0)) name = tentry.name;
1131 /* get current data */
1132 code = pr_ReadEntry(at,0,loc,&tentry);
e1f001e5 1133 if (code) return PRDBFAIL;
1134 }
dba0cf81 1135
1136 /* Change the name, if name is a ptr to tentry.name then this name change
1137 * is due to a chown, otherwise caller has specified a new name */
1138 if ((name == tentry.name) ||
1139 (*name && (strcmp (tentry.name, name) != 0))) {
1140 strncpy (oldname, tentry.name, PR_MAXNAMELEN);
e1f001e5 1141 if (tentry.flags & PRGRP) {
da0b4fd5 1142#ifdef CROSS_CELL
1143 if (atsign) return PRPERM;
1144#endif
dba0cf81 1145 code = CorrectGroupName (at, name, cid, tentry.owner, tentry.name);
1146 if (code) return code;
1147
1148 if (name == tentry.name) { /* owner fixup */
1149 if (strcmp (oldname, tentry.name) == 0) goto nameOK;
1150 } else { /* new name, caller must be correct */
1151 if (strcmp (name, tentry.name) != 0) return PRBADNAM;
1152 }
da0b4fd5 1153 } else {
1154#ifdef CROSS_CELL
1155 /* Allow a foreign name change only if the cellname part is
1156 * the same */
1157 char *newatsign;
1158
1159 newatsign = index (name, '@');
1160 if (newatsign != atsign){ /* if they are the same no problem*/
1161 /* if the pointers are not equal the strings better be */
1162 if ((atsign == 0) || (newatsign == 0) ||
1163 strcmp (atsign,newatsign)) return PRPERM;
1164 }
1165#endif
1166 if (!CorrectUserName(name)) return PRBADNAM;
1167 }
dba0cf81 1168
e1f001e5 1169 pos = FindByName(at,name);
1170 if (pos) return PREXIST;
dba0cf81 1171 code = RemoveFromNameHash (at, oldname, &loc);
e1f001e5 1172 if (code != PRSUCCESS) return code;
dba0cf81 1173 strncpy (tentry.name, name, PR_MAXNAMELEN);
1174 code = pr_WriteEntry(at,0,loc,(char *)&tentry);
e1f001e5 1175 if (code) return PRDBFAIL;
1176 code = AddToNameHash(at,tentry.name,loc);
1177 if (code != PRSUCCESS) return code;
dba0cf81 1178nameOK:;
e1f001e5 1179 }
1180 return PRSUCCESS;
1181}
da0b4fd5 1182
1183#ifdef CROSS_CELL
1184long allocNextId(cellEntry)
1185struct prentry *cellEntry;
1186{
1187 /* Id's for foreign cell entries are constructed as follows:
1188 * The 16 low order bits are the group id of the cell and the
1189 * top 16 bits identify the particular users in that cell */
1190
1191 long id;
1192
1193 id = (ntohl(cellEntry -> nusers) +1);
1194 cellEntry -> nusers = htonl(id);
1195 /* use the field nusers to keep the last used id in that
1196 * foreign cell's group.
1197 *
1198 * Note: It would seem more appropriate to use ngroup for
1199 * that and nusers to enforce the quota, however pts does not
1200 * have an option to change foreign users quota yet. */
1201 id = (id << 16) | ((ntohl(cellEntry-> id)) & 0x0000ffff);
1202 return id;
1203}
1204
1205int inRange(cellEntry,aid)
1206struct prentry *cellEntry;
1207long aid;
1208{
1209 unsigned long id,cellid,groupid;
1210
1211 /* The only thing that we want to make sure here is that the id
1212 * is in the legal range of this group. If it is a duplicate we
1213 * don't care since it will get in a different check. */
1214 cellid = aid & 0x0000ffff;
1215 groupid = (ntohl(cellEntry-> id)) & 0x0000ffff;
1216 if (cellid != groupid) return 0; /* not in range */
1217
1218 /* if we got here we're ok but we need to update the nusers field
1219 * in order to get the id correct the next time that we try to
1220 * allocate it automatically. */
1221 id = aid >> 16;
1222 if (id > ntohl(cellEntry -> nusers))
1223 cellEntry -> nusers = htonl(id);
1224 return 1;
1225}
1226
1227AddAuthGroup(tentry, alist, size)
1228 struct prentry *tentry;
1229 prlist *alist;
1230 long *size;
1231{
1232 if (!(index(tentry->name, '@')))
1233 return (AddToPRList (alist, size, AUTHUSERID));
1234#if ADD_TO_AUTHUSER_GROUP
1235 return PRSUCCESS;
1236#else
1237 return (AddToPRList (alist, size, tentry->cellid));
1238#endif
1239}
1240#endif /* CROSS_CELL */
This page took 0.245916 seconds and 5 git commands to generate.