1997-09-04 06:04:33 +00:00
|
|
|
/*
|
|
|
|
Copyright (C) 1989 by the Massachusetts Institute of Technology
|
|
|
|
|
|
|
|
Export of this software from the United States of America is assumed
|
|
|
|
to require a specific license from the United States Government.
|
|
|
|
It is the responsibility of any person or organization contemplating
|
|
|
|
export to obtain such a license before exporting.
|
|
|
|
|
|
|
|
WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
|
|
|
|
distribute this software and its documentation for any purpose and
|
|
|
|
without fee is hereby granted, provided that the above copyright
|
|
|
|
notice appear in all copies and that both that copyright notice and
|
|
|
|
this permission notice appear in supporting documentation, and that
|
|
|
|
the name of M.I.T. not be used in advertising or publicity pertaining
|
|
|
|
to distribution of the software without specific, written prior
|
|
|
|
permission. M.I.T. makes no representations about the suitability of
|
|
|
|
this software for any purpose. It is provided "as is" without express
|
|
|
|
or implied warranty.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "config.h"
|
|
|
|
#include "protos.h"
|
|
|
|
|
2000-01-09 08:31:47 +00:00
|
|
|
RCSID("$Id: acl_files.c,v 1.14 1999/09/16 20:41:43 assar Exp $");
|
1997-09-04 06:04:33 +00:00
|
|
|
|
|
|
|
#include <stdio.h>
|
|
|
|
#include <stdlib.h>
|
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
#ifdef HAVE_UNISTD_H
|
|
|
|
#include <unistd.h>
|
|
|
|
#endif
|
|
|
|
#ifdef HAVE_SYS_TYPES_H
|
|
|
|
#include <sys/types.h>
|
|
|
|
#endif
|
|
|
|
#include <time.h>
|
|
|
|
#ifdef HAVE_FCNTL_H
|
|
|
|
#include <fcntl.h>
|
|
|
|
#endif
|
|
|
|
#ifdef HAVE_SYS_FILE_H
|
|
|
|
#include <sys/file.h>
|
|
|
|
#endif
|
|
|
|
#ifdef HAVE_SYS_STAT_H
|
|
|
|
#include <sys/stat.h>
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#include <errno.h>
|
|
|
|
#include <ctype.h>
|
|
|
|
|
|
|
|
#include <roken.h>
|
|
|
|
|
|
|
|
#include <krb.h>
|
|
|
|
#include <acl.h>
|
|
|
|
|
|
|
|
/*** Routines for manipulating access control list files ***/
|
|
|
|
|
|
|
|
/* "aname.inst@realm" */
|
|
|
|
#define MAX_PRINCIPAL_SIZE (ANAME_SZ + INST_SZ + REALM_SZ + 3)
|
|
|
|
#define INST_SEP '.'
|
|
|
|
#define REALM_SEP '@'
|
|
|
|
|
|
|
|
#define LINESIZE 2048 /* Maximum line length in an acl file */
|
|
|
|
|
|
|
|
#define NEW_FILE "%s.~NEWACL~" /* Format for name of altered acl file */
|
|
|
|
#define WAIT_TIME 300 /* Maximum time allowed write acl file */
|
|
|
|
|
|
|
|
#define CACHED_ACLS 8 /* How many acls to cache */
|
|
|
|
/* Each acl costs 1 open file descriptor */
|
|
|
|
#define ACL_LEN 16 /* Twice a reasonable acl length */
|
|
|
|
|
|
|
|
#define COR(a,b) ((a!=NULL)?(a):(b))
|
|
|
|
|
1999-09-19 14:19:32 +00:00
|
|
|
/*
|
|
|
|
* Canonicalize a principal name.
|
|
|
|
* If instance is missing, it becomes ""
|
|
|
|
* If realm is missing, it becomes the local realm
|
|
|
|
* Canonicalized form is put in canon, which must be big enough to
|
|
|
|
* hold MAX_PRINCIPAL_SIZE characters
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
1997-09-04 06:04:33 +00:00
|
|
|
void
|
|
|
|
acl_canonicalize_principal(char *principal, char *canon)
|
|
|
|
{
|
1999-09-19 14:19:32 +00:00
|
|
|
krb_principal princ;
|
|
|
|
int ret;
|
|
|
|
ret = krb_parse_name(principal, &princ);
|
|
|
|
if(ret) { /* ? */
|
|
|
|
*canon = '\0';
|
|
|
|
return;
|
1997-09-04 06:04:33 +00:00
|
|
|
}
|
1999-09-19 14:19:32 +00:00
|
|
|
if(princ.realm[0] == '\0')
|
|
|
|
krb_get_lrealm(princ.realm, 1);
|
|
|
|
krb_unparse_name_r(&princ, canon);
|
1997-09-04 06:04:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/* Get a lock to modify acl_file */
|
|
|
|
/* Return new FILE pointer */
|
|
|
|
/* or NULL if file cannot be modified */
|
|
|
|
/* REQUIRES WRITE PERMISSION TO CONTAINING DIRECTORY */
|
|
|
|
static
|
|
|
|
FILE *acl_lock_file(char *acl_file)
|
|
|
|
{
|
|
|
|
struct stat s;
|
|
|
|
char new[LINESIZE];
|
|
|
|
int nfd;
|
|
|
|
FILE *nf;
|
|
|
|
int mode;
|
|
|
|
|
|
|
|
if(stat(acl_file, &s) < 0) return(NULL);
|
|
|
|
mode = s.st_mode;
|
|
|
|
snprintf(new, sizeof(new), NEW_FILE, acl_file);
|
|
|
|
for(;;) {
|
|
|
|
/* Open the new file */
|
|
|
|
if((nfd = open(new, O_WRONLY|O_CREAT|O_EXCL, mode)) < 0) {
|
|
|
|
if(errno == EEXIST) {
|
|
|
|
/* Maybe somebody got here already, maybe it's just old */
|
|
|
|
if(stat(new, &s) < 0) return(NULL);
|
|
|
|
if(time(0) - s.st_ctime > WAIT_TIME) {
|
|
|
|
/* File is stale, kill it */
|
|
|
|
unlink(new);
|
|
|
|
continue;
|
|
|
|
} else {
|
|
|
|
/* Wait and try again */
|
|
|
|
sleep(1);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
/* Some other error, we lose */
|
|
|
|
return(NULL);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* If we got to here, the lock file is ours and ok */
|
|
|
|
/* Reopen it under stdio */
|
|
|
|
if((nf = fdopen(nfd, "w")) == NULL) {
|
|
|
|
/* Oops, clean up */
|
|
|
|
unlink(new);
|
|
|
|
}
|
|
|
|
return(nf);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Abort changes to acl_file written onto FILE *f */
|
|
|
|
/* Returns 0 if successful, < 0 otherwise */
|
|
|
|
/* Closes f */
|
|
|
|
static int
|
|
|
|
acl_abort(char *acl_file, FILE *f)
|
|
|
|
{
|
|
|
|
char new[LINESIZE];
|
|
|
|
int ret;
|
|
|
|
struct stat s;
|
|
|
|
|
|
|
|
/* make sure we aren't nuking someone else's file */
|
|
|
|
if(fstat(fileno(f), &s) < 0
|
|
|
|
|| s.st_nlink == 0) {
|
|
|
|
fclose(f);
|
|
|
|
return(-1);
|
|
|
|
} else {
|
|
|
|
snprintf(new, sizeof(new), NEW_FILE, acl_file);
|
|
|
|
ret = unlink(new);
|
|
|
|
fclose(f);
|
|
|
|
return(ret);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Commit changes to acl_file written onto FILE *f */
|
|
|
|
/* Returns zero if successful */
|
|
|
|
/* Returns > 0 if lock was broken */
|
|
|
|
/* Returns < 0 if some other error occurs */
|
|
|
|
/* Closes f */
|
|
|
|
static int
|
|
|
|
acl_commit(char *acl_file, FILE *f)
|
|
|
|
{
|
|
|
|
char new[LINESIZE];
|
|
|
|
int ret;
|
|
|
|
struct stat s;
|
|
|
|
|
|
|
|
snprintf(new, sizeof(new), NEW_FILE, acl_file);
|
|
|
|
if(fflush(f) < 0
|
|
|
|
|| fstat(fileno(f), &s) < 0
|
|
|
|
|| s.st_nlink == 0) {
|
|
|
|
acl_abort(acl_file, f);
|
|
|
|
return(-1);
|
|
|
|
}
|
|
|
|
|
|
|
|
ret = rename(new, acl_file);
|
|
|
|
fclose(f);
|
|
|
|
return(ret);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Initialize an acl_file */
|
|
|
|
/* Creates the file with permissions perm if it does not exist */
|
|
|
|
/* Erases it if it does */
|
|
|
|
/* Returns return value of acl_commit */
|
|
|
|
int
|
|
|
|
acl_initialize(char *acl_file, int perm)
|
|
|
|
{
|
|
|
|
FILE *new;
|
|
|
|
int fd;
|
|
|
|
|
|
|
|
/* Check if the file exists already */
|
|
|
|
if((new = acl_lock_file(acl_file)) != NULL) {
|
|
|
|
return(acl_commit(acl_file, new));
|
|
|
|
} else {
|
|
|
|
/* File must be readable and writable by owner */
|
|
|
|
if((fd = open(acl_file, O_CREAT|O_EXCL, perm|0600)) < 0) {
|
|
|
|
return(-1);
|
|
|
|
} else {
|
|
|
|
close(fd);
|
|
|
|
return(0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Eliminate all whitespace character in buf */
|
|
|
|
/* Modifies its argument */
|
|
|
|
static void
|
1999-09-19 14:19:32 +00:00
|
|
|
nuke_whitespace(char *buf)
|
1997-09-04 06:04:33 +00:00
|
|
|
{
|
1999-09-19 14:19:32 +00:00
|
|
|
unsigned char *pin, *pout;
|
1997-09-04 06:04:33 +00:00
|
|
|
|
1999-09-19 14:19:32 +00:00
|
|
|
for(pin = pout = (unsigned char *)buf; *pin != '\0'; pin++)
|
|
|
|
if(!isspace(*pin))
|
|
|
|
*pout++ = *pin;
|
1997-09-04 06:04:33 +00:00
|
|
|
*pout = '\0'; /* Terminate the string */
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Hash table stuff */
|
|
|
|
|
|
|
|
struct hashtbl {
|
|
|
|
int size; /* Max number of entries */
|
|
|
|
int entries; /* Actual number of entries */
|
|
|
|
char **tbl; /* Pointer to start of table */
|
|
|
|
};
|
|
|
|
|
|
|
|
/* Make an empty hash table of size s */
|
|
|
|
static struct hashtbl *
|
|
|
|
make_hash(int size)
|
|
|
|
{
|
|
|
|
struct hashtbl *h;
|
|
|
|
|
|
|
|
if(size < 1) size = 1;
|
|
|
|
h = (struct hashtbl *) malloc(sizeof(struct hashtbl));
|
1999-09-19 14:19:32 +00:00
|
|
|
if (h == NULL)
|
|
|
|
return NULL;
|
1997-09-04 06:04:33 +00:00
|
|
|
h->size = size;
|
|
|
|
h->entries = 0;
|
|
|
|
h->tbl = (char **) calloc(size, sizeof(char *));
|
1999-09-19 14:19:32 +00:00
|
|
|
if (h->tbl == NULL) {
|
|
|
|
free (h);
|
|
|
|
return NULL;
|
|
|
|
}
|
1997-09-04 06:04:33 +00:00
|
|
|
return(h);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Destroy a hash table */
|
|
|
|
static void
|
|
|
|
destroy_hash(struct hashtbl *h)
|
|
|
|
{
|
|
|
|
int i;
|
|
|
|
|
|
|
|
for(i = 0; i < h->size; i++) {
|
|
|
|
if(h->tbl[i] != NULL) free(h->tbl[i]);
|
|
|
|
}
|
|
|
|
free(h->tbl);
|
|
|
|
free(h);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Compute hash value for a string */
|
|
|
|
static unsigned int
|
|
|
|
hashval(char *s)
|
|
|
|
{
|
|
|
|
unsigned hv;
|
|
|
|
|
|
|
|
for(hv = 0; *s != '\0'; s++) {
|
|
|
|
hv ^= ((hv << 3) ^ *s);
|
|
|
|
}
|
|
|
|
return(hv);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Add an element to a hash table */
|
|
|
|
static void
|
|
|
|
add_hash(struct hashtbl *h, char *el)
|
|
|
|
{
|
|
|
|
unsigned hv;
|
|
|
|
char *s;
|
|
|
|
char **old;
|
|
|
|
int i;
|
|
|
|
|
|
|
|
/* Make space if it isn't there already */
|
|
|
|
if(h->entries + 1 > (h->size >> 1)) {
|
|
|
|
old = h->tbl;
|
|
|
|
h->tbl = (char **) calloc(h->size << 1, sizeof(char *));
|
|
|
|
for(i = 0; i < h->size; i++) {
|
|
|
|
if(old[i] != NULL) {
|
|
|
|
hv = hashval(old[i]) % (h->size << 1);
|
|
|
|
while(h->tbl[hv] != NULL) hv = (hv+1) % (h->size << 1);
|
|
|
|
h->tbl[hv] = old[i];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
h->size = h->size << 1;
|
|
|
|
free(old);
|
|
|
|
}
|
|
|
|
|
|
|
|
hv = hashval(el) % h->size;
|
|
|
|
while(h->tbl[hv] != NULL && strcmp(h->tbl[hv], el)) hv = (hv+1) % h->size;
|
|
|
|
s = strdup(el);
|
1999-09-19 14:19:32 +00:00
|
|
|
if (s != NULL) {
|
|
|
|
h->tbl[hv] = s;
|
|
|
|
h->entries++;
|
|
|
|
}
|
1997-09-04 06:04:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/* Returns nonzero if el is in h */
|
|
|
|
static int
|
|
|
|
check_hash(struct hashtbl *h, char *el)
|
|
|
|
{
|
|
|
|
unsigned hv;
|
|
|
|
|
|
|
|
for(hv = hashval(el) % h->size;
|
|
|
|
h->tbl[hv] != NULL;
|
|
|
|
hv = (hv + 1) % h->size) {
|
|
|
|
if(!strcmp(h->tbl[hv], el)) return(1);
|
|
|
|
}
|
|
|
|
return(0);
|
|
|
|
}
|
|
|
|
|
|
|
|
struct acl {
|
|
|
|
char filename[LINESIZE]; /* Name of acl file */
|
|
|
|
int fd; /* File descriptor for acl file */
|
|
|
|
struct stat status; /* File status at last read */
|
|
|
|
struct hashtbl *acl; /* Acl entries */
|
|
|
|
};
|
|
|
|
|
|
|
|
static struct acl acl_cache[CACHED_ACLS];
|
|
|
|
|
|
|
|
static int acl_cache_count = 0;
|
|
|
|
static int acl_cache_next = 0;
|
|
|
|
|
|
|
|
/* Returns < 0 if unsuccessful in loading acl */
|
|
|
|
/* Returns index into acl_cache otherwise */
|
|
|
|
/* Note that if acl is already loaded, this is just a lookup */
|
|
|
|
static int
|
|
|
|
acl_load(char *name)
|
|
|
|
{
|
|
|
|
int i;
|
|
|
|
FILE *f;
|
|
|
|
struct stat s;
|
|
|
|
char buf[MAX_PRINCIPAL_SIZE];
|
|
|
|
char canon[MAX_PRINCIPAL_SIZE];
|
|
|
|
|
|
|
|
/* See if it's there already */
|
|
|
|
for(i = 0; i < acl_cache_count; i++) {
|
|
|
|
if(!strcmp(acl_cache[i].filename, name)
|
|
|
|
&& acl_cache[i].fd >= 0) goto got_it;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* It isn't, load it in */
|
|
|
|
/* maybe there's still room */
|
|
|
|
if(acl_cache_count < CACHED_ACLS) {
|
|
|
|
i = acl_cache_count++;
|
|
|
|
} else {
|
|
|
|
/* No room, clean one out */
|
|
|
|
i = acl_cache_next;
|
|
|
|
acl_cache_next = (acl_cache_next + 1) % CACHED_ACLS;
|
|
|
|
close(acl_cache[i].fd);
|
|
|
|
if(acl_cache[i].acl) {
|
|
|
|
destroy_hash(acl_cache[i].acl);
|
|
|
|
acl_cache[i].acl = (struct hashtbl *) 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Set up the acl */
|
2000-01-09 08:31:47 +00:00
|
|
|
strlcpy(acl_cache[i].filename, name, LINESIZE);
|
1997-09-04 06:04:33 +00:00
|
|
|
if((acl_cache[i].fd = open(name, O_RDONLY, 0)) < 0) return(-1);
|
|
|
|
/* Force reload */
|
|
|
|
acl_cache[i].acl = (struct hashtbl *) 0;
|
|
|
|
|
|
|
|
got_it:
|
|
|
|
/*
|
|
|
|
* See if the stat matches
|
|
|
|
*
|
|
|
|
* Use stat(), not fstat(), as the file may have been re-created by
|
|
|
|
* acl_add or acl_delete. If this happens, the old inode will have
|
|
|
|
* no changes in the mod-time and the following test will fail.
|
|
|
|
*/
|
|
|
|
if(stat(acl_cache[i].filename, &s) < 0) return(-1);
|
|
|
|
if(acl_cache[i].acl == (struct hashtbl *) 0
|
|
|
|
|| s.st_nlink != acl_cache[i].status.st_nlink
|
|
|
|
|| s.st_mtime != acl_cache[i].status.st_mtime
|
|
|
|
|| s.st_ctime != acl_cache[i].status.st_ctime) {
|
|
|
|
/* Gotta reload */
|
|
|
|
if(acl_cache[i].fd >= 0) close(acl_cache[i].fd);
|
|
|
|
if((acl_cache[i].fd = open(name, O_RDONLY, 0)) < 0) return(-1);
|
|
|
|
if((f = fdopen(acl_cache[i].fd, "r")) == NULL) return(-1);
|
|
|
|
if(acl_cache[i].acl) destroy_hash(acl_cache[i].acl);
|
|
|
|
acl_cache[i].acl = make_hash(ACL_LEN);
|
|
|
|
while(fgets(buf, sizeof(buf), f) != NULL) {
|
|
|
|
nuke_whitespace(buf);
|
|
|
|
acl_canonicalize_principal(buf, canon);
|
|
|
|
add_hash(acl_cache[i].acl, canon);
|
|
|
|
}
|
|
|
|
fclose(f);
|
|
|
|
acl_cache[i].status = s;
|
|
|
|
}
|
|
|
|
return(i);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Returns nonzero if it can be determined that acl contains principal */
|
|
|
|
/* Principal is not canonicalized, and no wildcarding is done */
|
|
|
|
int
|
|
|
|
acl_exact_match(char *acl, char *principal)
|
|
|
|
{
|
|
|
|
int idx;
|
|
|
|
|
|
|
|
return((idx = acl_load(acl)) >= 0
|
|
|
|
&& check_hash(acl_cache[idx].acl, principal));
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Returns nonzero if it can be determined that acl contains principal */
|
|
|
|
/* Recognizes wildcards in acl of the form
|
|
|
|
name.*@realm, *.*@realm, and *.*@* */
|
|
|
|
int
|
|
|
|
acl_check(char *acl, char *principal)
|
|
|
|
{
|
|
|
|
char buf[MAX_PRINCIPAL_SIZE];
|
|
|
|
char canon[MAX_PRINCIPAL_SIZE];
|
|
|
|
char *realm;
|
|
|
|
|
|
|
|
acl_canonicalize_principal(principal, canon);
|
|
|
|
|
|
|
|
/* Is it there? */
|
|
|
|
if(acl_exact_match(acl, canon)) return(1);
|
|
|
|
|
|
|
|
/* Try the wildcards */
|
|
|
|
realm = strchr(canon, REALM_SEP);
|
|
|
|
*strchr(canon, INST_SEP) = '\0'; /* Chuck the instance */
|
|
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "%s.*%s", canon, realm);
|
|
|
|
if(acl_exact_match(acl, buf)) return(1);
|
|
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "*.*%s", realm);
|
|
|
|
if(acl_exact_match(acl, buf) || acl_exact_match(acl, "*.*@*")) return(1);
|
|
|
|
|
|
|
|
return(0);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Adds principal to acl */
|
|
|
|
/* Wildcards are interpreted literally */
|
|
|
|
int
|
|
|
|
acl_add(char *acl, char *principal)
|
|
|
|
{
|
|
|
|
int idx;
|
|
|
|
int i;
|
|
|
|
FILE *new;
|
|
|
|
char canon[MAX_PRINCIPAL_SIZE];
|
|
|
|
|
|
|
|
acl_canonicalize_principal(principal, canon);
|
|
|
|
|
|
|
|
if((new = acl_lock_file(acl)) == NULL) return(-1);
|
|
|
|
if((acl_exact_match(acl, canon))
|
|
|
|
|| (idx = acl_load(acl)) < 0) {
|
|
|
|
acl_abort(acl, new);
|
|
|
|
return(-1);
|
|
|
|
}
|
|
|
|
/* It isn't there yet, copy the file and put it in */
|
|
|
|
for(i = 0; i < acl_cache[idx].acl->size; i++) {
|
|
|
|
if(acl_cache[idx].acl->tbl[i] != NULL) {
|
|
|
|
if(fputs(acl_cache[idx].acl->tbl[i], new) == EOF
|
|
|
|
|| putc('\n', new) != '\n') {
|
|
|
|
acl_abort(acl, new);
|
|
|
|
return(-1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fputs(canon, new);
|
|
|
|
putc('\n', new);
|
|
|
|
return(acl_commit(acl, new));
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Removes principal from acl */
|
|
|
|
/* Wildcards are interpreted literally */
|
|
|
|
int
|
|
|
|
acl_delete(char *acl, char *principal)
|
|
|
|
{
|
|
|
|
int idx;
|
|
|
|
int i;
|
|
|
|
FILE *new;
|
|
|
|
char canon[MAX_PRINCIPAL_SIZE];
|
|
|
|
|
|
|
|
acl_canonicalize_principal(principal, canon);
|
|
|
|
|
|
|
|
if((new = acl_lock_file(acl)) == NULL) return(-1);
|
|
|
|
if((!acl_exact_match(acl, canon))
|
|
|
|
|| (idx = acl_load(acl)) < 0) {
|
|
|
|
acl_abort(acl, new);
|
|
|
|
return(-1);
|
|
|
|
}
|
|
|
|
/* It isn't there yet, copy the file and put it in */
|
|
|
|
for(i = 0; i < acl_cache[idx].acl->size; i++) {
|
|
|
|
if(acl_cache[idx].acl->tbl[i] != NULL
|
|
|
|
&& strcmp(acl_cache[idx].acl->tbl[i], canon)) {
|
|
|
|
fputs(acl_cache[idx].acl->tbl[i], new);
|
|
|
|
putc('\n', new);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return(acl_commit(acl, new));
|
|
|
|
}
|