//------------------------------------------------------------------------------
//! @file richacl.hh
//! @author Rainer Toebikke CERN
//! @brief richacls<=>eosacls translation functions
//------------------------------------------------------------------------------
/************************************************************************
* EOS - the CERN Disk Storage System *
* Copyright (C) 2018 CERN/Switzerland *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see .*
************************************************************************/
struct {
int raclBits;
char eosChr;
} raclEosPerms[] = {
{RICHACE_READ_DATA, 'r'},
{RICHACE_WRITE_DATA, 'w'},
{RICHACE_EXECUTE, 'x'},
{RICHACE_WRITE_ACL|RICHACE_WRITE_ATTRIBUTES|RICHACE_WRITE_NAMED_ATTRS, 'm'},
{RICHACE_APPEND_DATA, 'u'},
{RICHACE_DELETE_CHILD, 'd'},
{RICHACE_WRITE_OWNER, 'c'}
};
const unsigned int raclEosPermsLen = sizeof(raclEosPerms) / sizeof(
raclEosPerms[0]);
int
static racl2eos(struct richacl* acl, char* buf, int bufsz, metad::shared_md md)
{
char* end_buf = buf + bufsz - 1;
bool add_comma = false;
int rc = 0;
if (bufsz < 1) {
return (EINVAL);
}
buf[0] = '\0'; /* in case there are no eos-compatible ACLs */
/* An attempt is made to "merge" allow and deny entries into one eos ACL.
* Here, this (silently) supposes that there is only one entry to be merged and the other one is
* the opposite in allow/deny terms
*/
struct masks { unsigned int allow; unsigned int deny; };
struct masks zeromasks = {0, 0};
std::map ace_mask;
struct richace* ace;
richacl_for_each_entry(ace, acl) {
std::string who;
int rc2 = 0;
const char* ug = (ace->e_flags & RICHACE_IDENTIFIER_GROUP) ? "g" : "u";
if (ace->e_flags & RICHACE_UNMAPPED_WHO) {
who = ace->e_who;
if (*ug == 'g') {
ug = "egroup";
}
} else if (!(ace->e_flags & RICHACE_SPECIAL_WHO)) {
if (*ug == 'g') {
who = eos::common::Mapping::GidToGroupName(ace->e_id, rc2);
} else {
who = eos::common::Mapping::UidToUserName(ace->e_id, rc2);
}
} else { /* this is a SPECIAL_WHO */
if (ace->e_id == RICHACE_EVERYONE_SPECIAL_ID) { /* everyone@ */
who = "";
ug = "z";
} else if (ace->e_id == RICHACE_OWNER_SPECIAL_ID) { /* owner@ */
who = eos::common::Mapping::UidToUserName((*md)()->uid(), rc2);
if (EOS_LOGS_DEBUG)
eos_static_debug("racl2eos special user id %d '%s' ug '%s'", ace->e_id, who.c_str(), ug);
} else if (ace->e_id == RICHACE_GROUP_SPECIAL_ID) { /* group@ */
who = eos::common::Mapping::GidToGroupName((*md)()->gid(), rc2);
ug = "g"; /* RICHACE_IDENTIFIER_GROUP not necessarily set */
if (EOS_LOGS_DEBUG)
eos_static_debug("racl2eos special group id %d '%s' ug '%s'", ace->e_id, who.c_str(), ug);
}
else {
if (EOS_LOGS_DEBUG)
eos_static_debug("racl2eos special who %d ignored", ace->e_id);
continue; /* silently ignored */
}
}
if (rc2) {
who = "_unknown_";
}
std::string id_s(ug);
if (!who.empty()) id_s += ":" + who;
auto a = ace_mask.find(id_s);
if (a == ace_mask.end()) {
ace_mask[id_s] = zeromasks;
if (EOS_LOGS_DEBUG) eos_static_debug("racl2eos ace_mask for %s initialised to (%#x, %#x)",
id_s.c_str(), ace_mask[id_s].allow, ace_mask[id_s].deny);
}
if (richace_is_deny(ace)) {
ace_mask[id_s].deny = ace->e_mask;
} else {
ace_mask[id_s].allow = ace->e_mask;
}
}
for (auto map = ace_mask.begin(); map != ace_mask.end(); ++map) {
char perms[64];
int pos = 0;
struct masks allowdeny = map->second;
unsigned int e_mask = allowdeny.allow & ~allowdeny.deny;
if (EOS_LOGS_DEBUG) eos_static_debug("racl2eos ace_mask %s allow %#x deny %#x mask %#x", map->first.c_str(),
allowdeny.allow, allowdeny.deny, e_mask);
for (unsigned int j = 0; j < raclEosPermsLen; j++) {
if (allowdeny.deny & raclEosPerms[j].raclBits) {
perms[pos++] = '!';
}
else if (!(e_mask & raclEosPerms[j].raclBits)) {
continue; /* not allowed, no need to mention it */
}
// with denials, 'u' should no longer automatically disable them depending on position!
// if (raclEosPerms[j].eosChr == 'u' && !allowdeny.deny) perms[pos++] = '+';
eos_static_debug("racl2eos %c for %#x @%d", raclEosPerms[j].eosChr, raclEosPerms[j].raclBits, pos);
perms[pos++] = raclEosPerms[j].eosChr;
}
perms[pos] = '\0'; /* terminate string */
char* b = buf;
if ((end_buf - b) < 10) { /* don't take chances */
rc = ENOSPC;
break;
}
if (add_comma) {
*b++ = ',';
}
if (EOS_LOGS_DEBUG) eos_static_debug("racl2eos %s perm %s", map->first.c_str(), perms);
int l = snprintf(b, end_buf - b, "%s:%s", map->first.c_str(), perms);
if (b + l >= end_buf) {
*b = '\0';
return E2BIG;
}
buf = b + l;
add_comma = true;
}
return rc;
}
static struct richacl*
eos2racl(const char* eosacl, metad::shared_md md)
{
char* tacl = (char*) alloca(strlen(eosacl) + 1);
strcpy(tacl, eosacl); /* copy, strtok_r is going to destroy this string */
char* curr = tacl, *lasts;
int numace = 1;
while ((curr = strchr(curr + 1, ','))) {
numace += 1; /* a leading ',' would not do real harm */
}
curr = strtok_r(tacl, ",", &lasts);
if (curr == NULL) {
return richacl_from_mode((*md)()->mode()); /* return ACL from mode bits, NULL means invalid ACL */
}
struct richacl* acl = richacl_alloc(numace);
if (acl == NULL) return NULL;
acl->a_count = 0;
struct richace *ace;
int idx_everyone=-1, idx_owner=-1, idx_group=-1;
if (EOS_LOGS_DEBUG) eos_static_debug("eos2racl curr='%s' next='%s'", curr, lasts);
int rc = 0;
std::map ace_mask_deny;
struct richacl *denials = richacl_alloc(numace);
denials->a_count = 0;
do { /* one entry, e.g. u:username:rx or egroup:groupname:rw or z:wx */
ace = &acl->a_entries[acl->a_count];
memset(ace, 0, sizeof(
*ace)); /* makes the ace an "allow" type entry in passing */
char* l2;
char* uge = strtok_r(curr, ":", &l2);
char* qlf = strtok_r(NULL, ":", &l2);
char* perm = strtok_r(NULL, ":", &l2);
if (strlen(uge) == 0 || qlf == NULL) { /* badly formatted entry */
continue;
}
if (perm == NULL) { /* would be the case for z:wx */
if (uge[0] != 'z')
continue; /* invalid entry */
perm = qlf;
qlf = NULL;
}
/* verify all eos chars have an entry in raclEosPerms! */
for (char* s = perm; *s != '\0'; s++) {
if (s[0] == '+' || s[0] == '!') {
continue;
}
unsigned int j;
for (j = 0; j < raclEosPermsLen && raclEosPerms[j].eosChr != s[0]; j++);
if (j >= raclEosPermsLen) {
eos_static_err("eos2racl eos permission '%c' not supported in '%s'", s[0],
perm);
rc = EINVAL;
break;
}
}
if (rc != 0) {
richacl_free(acl);
return NULL;
}
int qlf_num = 0;
if (qlf != NULL) {
char* qlf_end;
qlf_num = strtoll((const char*) qlf, &qlf_end, 10);
if (qlf_end[0] != '\0') {
qlf_num = -1; /* not a number */
}
/* if (EOS_LOGS_DEBUG) eos_static_debug("qlf='%s' qlf_num=%d qlf_end='%s'", qlf, qlf_num, qlf_end); */
}
switch (uge[0]) { /* qualifier type, just check first char */
case 'u': /* u in eos, meaning 'user' */
if (qlf_num < 0) {
ace->e_id = eos::common::Mapping::UserNameToUid(std::string(qlf), rc);
} else {
ace->e_id = qlf_num;
}
if (EOS_LOGS_DEBUG) eos_static_debug("qge=%s qlf=%s qlf_num=%d rc=%d e_id=%d", uge, qlf, qlf_num,
rc, ace->e_id);
if (ace->e_id == (id_t) (*md)()->uid()) { /* owner */
if (idx_owner >= 0) { /* already have an entry */
acl->a_owner_mask = ace->e_mask;
ace = &acl->a_entries[idx_owner];
ace->e_mask = acl->a_owner_mask;
acl->a_count--; /* it'll be re-incremented shortly */
}
ace->e_id = RICHACE_OWNER_SPECIAL_ID;
ace->e_flags |= RICHACE_SPECIAL_WHO;
}
break;
case 'g': /* g in eos, meaning 'group' */
if (qlf_num < 0) {
ace->e_id = eos::common::Mapping::GroupNameToGid(std::string(qlf), rc);
} else {
ace->e_id = qlf_num;
}
if (EOS_LOGS_DEBUG) eos_static_debug("qge=%s qlf=%s qlf_num=%d rc=%d e_id=%d", uge, qlf, qlf_num,
rc, ace->e_id);
if (ace->e_id != (id_t) (*md)()->gid()) { /* group other than file's group*/
ace->e_flags |= RICHACE_IDENTIFIER_GROUP;
// I had assumed that setting RICHACE_IDENTIFIER_GROUP also in the case e_id == gid below would be appropriate,
// however that causes problems with "setrichacl -m" finding the right entry. Response from
// Andreas Gruenbacher (agruenba@redhat.com) pending (Dec 2018)
} else {
if (idx_group >= 0) { /* already have an entry */
acl->a_group_mask = ace->e_mask;
ace = &acl->a_entries[idx_group];
ace->e_mask = acl->a_group_mask;
acl->a_count--; /* it'll be re-incremented shortly */
}
ace->e_id = RICHACE_GROUP_SPECIAL_ID;
ace->e_flags |= RICHACE_SPECIAL_WHO;
}
break;
case 'e': /* egroup in eos */
ace->e_flags |= RICHACE_IDENTIFIER_GROUP;
rc = richace_set_unmapped_who(ace, qlf,
ace->e_flags); /* This will set RICHACE_UNMAPPED_WHO, it mustn't be set before! */
break;
case 'z': /* everyone@ */
if (idx_everyone >= 0) {
acl->a_other_mask = ace->e_mask;
ace = &acl->a_entries[idx_everyone];
ace->e_mask = acl->a_other_mask;
acl->a_count--; /* it'll be re-incremented shortly */
}
ace->e_id = RICHACE_EVERYONE_SPECIAL_ID;
ace->e_flags |= RICHACE_SPECIAL_WHO;
break;
default:
eos_static_err("eos2racl invalid qualifier type: %s", uge);
}
if (rc) {
eos_static_err("eos2racl parsing failed: ", strerror(rc));
break;
}
/* interpret mask characters */
unsigned int deny = 0;
if (EOS_LOGS_DEBUG) eos_static_debug("eos2racl perm=%s", perm);
for (unsigned int j = 0; j < raclEosPermsLen; j++) {
char* s = strchr(perm, raclEosPerms[j].eosChr);
if (s != NULL) {
bool has_not = (s > perm) && (s[-1] == '!');
if (has_not) {
if (EOS_LOGS_DEBUG) eos_static_debug("eos2racl need a deny entry for '%c' in '%s'", s[0], perm);
deny |= raclEosPerms[j].raclBits;
ace->e_mask &= ~raclEosPerms[j].raclBits;
} else {
ace->e_mask |= raclEosPerms[j].raclBits;
}
} else if (acl->a_other_mask & raclEosPerms[j].raclBits) {
eos_static_err("eos2racl need a deny entry for '%c' in '%s'",
raclEosPerms[j].eosChr, perm);
deny |= raclEosPerms[j].raclBits;
}
}
if (deny != 0) {
struct richace *dace = &denials->a_entries[denials->a_count];
richace_copy(dace, ace); /* denials has been cleared upon alloc, good for richace_copy */
dace->e_mask = deny;
dace->e_type = RICHACE_ACCESS_DENIED_ACE_TYPE;
denials->a_count++;
}
if (ace->e_mask) {
acl->a_count++;
if (EOS_LOGS_DEBUG) eos_static_debug("eos2racl a_count=%d numace=%d", acl->a_count, numace);
} else if (EOS_LOGS_DEBUG)
eos_static_debug("eos2racl skipped entry e_mask=%#x a_count=%d numace=%d", ace->e_mask, acl->a_count, numace);
} while ((curr = strtok_r(NULL, ",", &lasts)));
/* if needed, create a new ACL with denials first */
if (denials->a_count > 0) {
struct richacl *acl2 = richacl_alloc(denials->a_count + acl->a_count); /* alloc and clear */
if (EOS_LOGS_DEBUG) eos_static_debug("allocated new acl for %d entries", acl2->a_count);
/*int sz = (void *) (acl->a_entries) - (void *) acl; ...this does not compile ?? */
int sz = (char *) (acl->a_entries) - (char *) acl;
memcpy(acl2, acl, sz); /* copy header */
acl2->a_count = 0;
/* copy all useful deny entries */
struct richace *ace;
richacl_for_each_entry(ace, denials) {
richace_copy(&acl2->a_entries[acl2->a_count++], ace); /* acl2 had been cleared upon alloc */
if (EOS_LOGS_DEBUG) eos_static_debug("copied mask %#x for id %d count %d", ace->e_mask, ace->e_id, acl2->a_count);
}
/* copy all allow entries */
richacl_for_each_entry(ace, acl) {
richace_copy(&acl2->a_entries[acl2->a_count++], ace);
}
richacl_free(acl);
acl = acl2;
}
richacl_free(denials); /* the updated a_count does not kmetter */
if (rc == 0) {
return acl;
}
richacl_free(acl);
return NULL;
}
/* normalize e_id to (idType, id) for easy comparisons */
id_t
richacl_normalize_id(const struct richace *ace, metad::shared_md md, int *idType) {
id_t id = 0;
*idType = 0;
if (ace->e_flags & RICHACE_SPECIAL_WHO) {
*idType = (int) ace->e_id; /* one of owner, group, everyone */
if (*idType == RICHACE_OWNER_SPECIAL_ID) {
id = (*md)()->uid();
} else if (*idType == RICHACE_GROUP_SPECIAL_ID) {
id = (*md)()->gid();
}
} else {
id = ace->e_id;
if (ace->e_flags & RICHACE_IDENTIFIER_GROUP) {
*idType = RICHACE_GROUP_SPECIAL_ID;
} else {
*idType = RICHACE_OWNER_SPECIAL_ID;
}
}
return id;
}
static richace *
richacl_find_matching_ace(struct richace *e, metad::shared_md pmd,
struct richacl *acl, metad::shared_md md) {
struct richace *ace;
int idType1;
id_t id1 = richacl_normalize_id(e, pmd, &idType1);
richacl_for_each_entry(ace, acl) {
if (ace->e_type==RICHACE_ACCESS_ALLOWED_ACE_TYPE) {
if (richace_is_same_identifier(e, ace)) return ace;
int idType2;
id_t id2 = richacl_normalize_id(ace, md, &idType2);
if ((idType1 != idType2) || (id1 != id2)) continue;
return ace;
}
}
return NULL;
}
// merge parent ACL as defined:
// for non-Dir (dynamically) inherits parent ACL
// inherits RICHACL_DELETE_CHILD as RICHACL_DELETE for all
struct richacl *
richacl_merge_parent(struct richacl *acl, metad::shared_md md, /* subject */
struct richacl *pacl, metad::shared_md pmd /* parent */) {
bool isDir = S_ISDIR((*md)()->mode()); /* non-Dir inherits from parent if acl == NULL */
struct richace *ace, *pace;
if (acl == NULL && !isDir) { /* inherits ACL from parent */
acl = richacl_clone(pacl);
eos_static_debug("richacl cloned %d entries from parent for non-dir", pacl->a_count);
richacl_for_each_entry(ace, acl) {
if ( (ace->e_mask & RICHACE_DELETE_CHILD) ) {
ace->e_mask |= RICHACE_DELETE; /* works for both deny/allow */
}
ace->e_mask &= ~RICHACE_DELETE_CHILD; /* not meaningful */
}
} else { /* inherits only RICHACL_DELETE_CHILD */
if (acl == NULL) /* Container without ACL, use mode bits, no inheritance */
acl = richacl_from_mode((*md)()->mode());
/* Loop over all entries in parent ACL and merge into child */
richacl_for_each_entry(pace, pacl) {
if ( (pace->e_mask & RICHACE_DELETE_CHILD) == 0) continue; /* only inherits RICHACL_DELETE from RICHACL_DELETE_CHILD */
ace = richacl_find_matching_ace(pace, pmd, acl, md);
if (ace == NULL) { /* need a new entry */
size_t newsz = sizeof(struct richacl) + (acl->a_count + 1) * sizeof(struct richace);
struct richacl *newacl = (struct richacl *) realloc(acl, newsz);
eos_static_debug("richacl realloced %d bytes for parent DELETE_CHILD old=%#p new=%#p, e_id=%d", newsz, acl, newacl, pace->e_id);
if (newacl == NULL) { /* running out of memory */
richacl_free(acl); /* complete, high-level free */
return NULL;
}
acl = newacl;
ace = &(acl->a_entries[acl->a_count]);
memset(ace, 0, sizeof(*ace)); /* richace_copy needs a clean entry */
richace_copy(ace, pace);
ace->e_mask = 0; /* no mask bits yet */
acl->a_count++;
}
ace->e_mask |= RICHACE_DELETE; /* works for both deny/allow */
eos_static_debug("richacl allowing DELETE for %d, mask %#x", ace->e_id, ace->e_mask);
}
}
return acl;
}
std::string escape(std::string src)
{
std::string s = "";
for (unsigned char c : src) {
if (isprint(c)) {
s += c;
} else {
char buf[64];
sprintf(buf, "\\x%02x", c);
s += buf;
}
}
return s;
}