//------------------------------------------------------------------------------
//! @file Acl.cc
//------------------------------------------------------------------------------
/************************************************************************
* EOS - the CERN Disk Storage System *
* Copyright (C) 2017 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 .*
************************************************************************/
#include "mgm/Acl.hh"
#include "mgm/Egroup.hh"
#include "mgm/XrdMgmOfs.hh"
#include "common/StringConversion.hh"
#include
EOSMGMNAMESPACE_BEGIN
//------------------------------------------------------------------------------
// Constructor
//------------------------------------------------------------------------------
Acl::Acl(std::string sysacl, std::string useracl,
const eos::common::VirtualIdentity& vid, bool allowUserAcl)
{
std::string tokenacl = TokenAcl(vid);
Set(sysacl, useracl, tokenacl, vid, allowUserAcl);
}
//------------------------------------------------------------------------------
//!
//! Constructor
//!
//! @param attr map containing all extended attributes
//! @param vid virtual id to match ACL
//!
//------------------------------------------------------------------------------
Acl::Acl(const eos::IContainerMD::XAttrMap& attrmap,
const eos::common::VirtualIdentity& vid)
{
// define the acl rules from the attributes
SetFromAttrMap(attrmap, vid);
}
//------------------------------------------------------------------------------
// Constructor by path
//------------------------------------------------------------------------------
Acl::Acl(const char* path, XrdOucErrInfo& error,
const eos::common::VirtualIdentity& vid,
eos::IContainerMD::XAttrMap& attrmap, bool lockNs)
{
if (path && strlen(path)) {
int rc = gOFS->_attr_ls(path, error, vid, 0, attrmap);
if (rc) {
eos_static_info("attr-ls failed: path=%s errno=%d", path, errno);
}
}
// Set the acl rules from the attributes
SetFromAttrMap(attrmap, vid);
}
//------------------------------------------------------------------------------
// Set Acls by interpreting the attribute map
//------------------------------------------------------------------------------
void
Acl::SetFromAttrMap(const eos::IContainerMD::XAttrMap& attrmap,
const eos::common::VirtualIdentity& vid,
eos::IFileMD::XAttrMap* attrmapF, bool sysaclOnly)
{
std::string usr_acl;
bool eval_usr_acl = false;
mEvalFileUserAcl = false;
if (!sysaclOnly) {
if (attrmapF && (attrmapF->count("user.acl") > 0)) {
eval_usr_acl = (attrmapF->count("sys.eval.useracl") > 0);
if (eval_usr_acl) {
usr_acl = (*attrmapF)["user.acl"];
mFileUserAcl = usr_acl;
mEvalFileUserAcl = true;
}
} else {
eval_usr_acl = (attrmap.count("sys.eval.useracl") > 0);
if (eval_usr_acl) {
auto it = attrmap.find("user.acl");
if (it != attrmap.end()) {
usr_acl = it->second;
}
}
}
}
std::string sys_acl;
auto itc = attrmap.find("sys.acl");
if (itc != attrmap.end()) {
sys_acl = itc->second;
}
if (attrmapF) {
auto itf = attrmapF->find("sys.acl");
if (itf != attrmapF->end()) {
if (!sys_acl.empty()) {
sys_acl += ',';
}
sys_acl += itf->second;
}
}
std::string token_acl = TokenAcl(vid);
eos_static_debug("sysacl=\"%s\" useracl=\"%s\" token_acl=\"%s\" "
"eval_usr_acl=%d", sys_acl.c_str(), usr_acl.c_str(),
token_acl.c_str(), eval_usr_acl);
Set(sys_acl, usr_acl, token_acl, vid, eval_usr_acl);
}
//------------------------------------------------------------------------------
// Set the contents of an ACL and compute the canXX and hasXX booleans.
//------------------------------------------------------------------------------
void
Acl::Set(std::string sysacl, std::string useracl, std::string tokenacl,
const eos::common::VirtualIdentity& vid, bool allowUserAcl)
{
std::string acl = "";
sysattr = "";
userattr = "";
mEvalDirUserAcl = false;
if (sysacl.length()) {
acl += sysacl;
sysattr = sysacl;
}
if (allowUserAcl) {
mEvalDirUserAcl = true;
if (useracl.length()) {
if (sysacl.length()) {
acl += ",";
}
acl += useracl;
userattr = useracl;
}
}
if (tokenacl.length()) {
// overwrite all other ACLs with a token
acl = tokenacl;
sysacl = tokenacl;
allowUserAcl = false;
}
// By default nothing is granted
mHasAcl = false;
mCanRead = false;
mCanNotRead = false;
mCanWrite = false;
mCanNotWrite = false;
mCanWriteOnce = false;
mCanUpdate = false;
mCanNotUpdate = false;
mCanBrowse = false;
mCanNotBrowse = false;
mCanChmod = false;
mCanNotChmod = false;
mCanChown = false;
mCanNotDelete = false;
mCanDelete = false;
mCanSetQuota = false;
mHasEgroup = false;
mIsMutable = true;
mCanArchive = false;
mCanPrepare = false;
// no acl definition
if (!acl.length()) {
return;
}
int errc = 0;
std::vector rules;
std::string delimiter = ",";
eos::common::StringConversion::Tokenize(sysacl, rules, delimiter);
int num_sysacl_rules =
rules.size(); /* number of entries in sysacl, used to limit "+" (reallow) */
if (allowUserAcl) {
eos::common::StringConversion::Tokenize(useracl, rules,
delimiter); /* append to rules */
}
if (EOS_LOGS_DEBUG) {
eos_static_debug("sysacl '%s' (%d entries), useracl '%s', total %d entries",
sysacl.c_str(), num_sysacl_rules, useracl.c_str(), rules.size());
}
std::vector::const_iterator it;
XrdOucString sizestring1;
XrdOucString sizestring2;
char denials[256], reallows[256];
memset(denials, 0, sizeof(denials)); /* start with no denials */
memset(reallows, 0, sizeof(reallows)); /* nor reallows */
std::set gids;
if (eos::common::Mapping::gSecondaryGroups) {
gids=vid.allowed_gids;
} else {
gids.insert(vid.gid);
}
for (const auto& chk_gid : gids) {
// Only check non-system groups
if (chk_gid == 0) {
continue;
}
std::string userid = eos::common::StringConversion::GetSizeString(sizestring1,
(unsigned long long) vid.uid);
std::string groupid = eos::common::StringConversion::GetSizeString(sizestring2,
(unsigned long long) chk_gid);
std::string usertag = "u:";
usertag += userid;
usertag += ":";
std::string grouptag = "g:";
grouptag += groupid;
grouptag += ":";
std::string username = eos::common::Mapping::UidToUserName(vid.uid, errc);
if (errc) {
username = "_INVAL_";
}
std::string groupname = eos::common::Mapping::GidToGroupName(chk_gid, errc);
if (errc) {
groupname = "_INVAL_";
}
if (EOS_LOGS_DEBUG) {
eos_static_debug("username '%s' groupname '%s'", username.c_str(),
groupname.c_str());
}
std::string usr_name_tag = "u:";
usr_name_tag += username;
usr_name_tag += ":";
std::string grp_name_tag = "g:";
grp_name_tag += groupname;
grp_name_tag += ":";
std::string ztag = "z:";
std::string keytag = "k:";
keytag += vid.key;
keytag += ":";;
if (EOS_LOGS_DEBUG) eos_static_debug("%s %s %s %s %s", usertag.c_str(),
grouptag.c_str(),
usr_name_tag.c_str(), grp_name_tag.c_str(), keytag.c_str());
// Rule interpretation logic
int sysacl_rules_remaining = num_sysacl_rules;
for (it = rules.begin(); it != rules.end(); it++) {
bool egroupmatch = false;
/* when negative, we're in user.acl */
sysacl_rules_remaining -= 1;
// Check for e-group membership
if (!it->compare(0, strlen("egroup:"), "egroup:")) {
std::vector entry;
std::string delimiter = ":";
eos::common::StringConversion::Tokenize(*it, entry, delimiter);
if (entry.size() < 3) {
continue;
}
egroupmatch = gOFS->EgroupRefresh->Member(username, entry[1]);
mHasEgroup = egroupmatch;
}
// Match 'our' rule
if ((!it->compare(0, usertag.length(), usertag)) ||
(!it->compare(0, grouptag.length(), grouptag)) ||
(!it->compare(0, ztag.length(), ztag)) ||
(egroupmatch) ||
(!it->compare(0, keytag.length(), keytag)) ||
(!it->compare(0, usr_name_tag.length(), usr_name_tag)) ||
(!it->compare(0, grp_name_tag.length(), grp_name_tag))) {
std::vector entry;
std::string delimiter = ":";
eos::common::StringConversion::Tokenize(*it, entry, delimiter);
if (entry.size() < 3) {
// z tag entries have only two fields
if (it->compare(0, ztag.length(), ztag) || (entry.size() < 2)) {
continue;
}
// add an empty entry field
entry.resize(3);
entry[2] = entry[1];
}
if (EOS_LOGS_DEBUG) {
eos_static_debug("parsing permissions '%s'", entry[2].c_str());
}
bool deny = false, reallow = false;
for (char* s = (char*) entry[2].c_str(); s[0] != 0; s++) {
int c = s[0]; /* need a copy in case of an "s++" later */
if (EOS_LOGS_DEBUG) {
eos_static_debug("c=%c deny=%d reallow=%d", c, deny, reallow);
}
if (reallow && !(c == 'u' || c == 'd')) {
eos_static_info("'+' Acl flag ignored for '%c'", c);
}
switch (c) {
case '!':
deny = true;
continue;
case '+':
reallow = true;
continue;
case 'a': // 'a' defines archiving permission
mCanArchive = !deny;
break;
case 'r': // 'r' defines read permission
mCanRead = !deny;
break;
case 'x': // 'x' defines browsing permission
mCanBrowse = !deny;
break;
case 'p': // 'p' defines workflow permission
mCanPrepare = !deny;
break;
case 'm': // 'm' defines mode change permission
if (deny) {
mCanNotChmod = true;
} else {
mCanChmod = true;
}
break;
case 'c': // 'c' defines owner change permission (for directories)
/* pass here; but chown imposes further restrictions, like limited to sys.acl */
mCanChown = true;
break;
case 'd': // '!d' forbids deletion
if (deny && !mCanDelete) {
mCanNotDelete = true;
} else if (reallow) {
if (sysacl_rules_remaining < 0) {
eos_static_info("'+d' ignored in user acl '%s'", entry[2].c_str());
reallow = 0; /* ignore the reallow */
break;
}
mCanDelete = true;
mCanNotDelete = false;
mCanWriteOnce = false;
denials['d'] = 0; /* drop denial, 'd' and 'u' are "odd" */
}
break;
case 'u':// '!u' denies update, 'u' and '+u' add update. '!+u' and '+!u' would *deny* updates
mCanUpdate = !deny;
if (mCanUpdate && reallow) {
denials['u'] = 0; /* drop denial, 'd' and 'u' are "odd" */
}
break;
case 'w': // 'wo' defines write once permissions, 'w' defines write permissions if 'wo' is not granted
if ((s + 1)[0] == 'o') { /* this is a 'wo' */
s++;
c = 'W'; /* for the denial entry */
mCanWriteOnce = !deny;
} else {
if (!mCanWriteOnce) {
mCanWrite = !deny;
mCanUpdate = !deny; // by default 'w' adds update rights
}
}
break;
case 'q':
if (sysacl_rules_remaining >=
0) { // this is only valid if specified as a sysacl
mCanSetQuota = !deny;
}
break;
case 'i': // 'i' makes directories immutable
mIsMutable = deny;
break;
}
mHasAcl = true;
if (reallow) {
reallows[c] = 1; /* remember reallows */
} else if (deny) {
denials[c] = 1; /* remember denials */
}
deny = reallow = false; /* reset for next permission char */
}
}
}
}
/* Now that all ACLs have been parsed, handle re-allows and denials */
char rights[] = "arxpmcWwdui";
unsigned char r;
for (int i = 0; (r = rights[i]); i++) {
bool is_allowed;
if (reallows[r]) {
denials[r] = 0;
is_allowed = true;
eos_static_debug("reallow %c", r);
} else if (denials[r]) { /* re-allows beat denials */
is_allowed = false;
if (r != 'W') {
eos_static_debug("deny %c", r);
}
} else {
continue;
}
switch (r) {
case 'a':
mCanArchive = is_allowed;
break;
case 'r':
mCanRead = is_allowed;
mCanNotRead = !is_allowed;
break;
case 'x':
mCanBrowse = is_allowed;
mCanNotBrowse = !is_allowed;
break;
case 'p':
mCanPrepare = is_allowed;
break;
case 'm':
mCanNotChmod = !is_allowed;
break;
case 'c':
mCanChown = is_allowed;
break;
case 'W':
mCanWriteOnce = is_allowed;
eos_static_debug("writeonce %d", mCanWriteOnce);
break;
case 'w':
mCanWrite = is_allowed;
mCanNotWrite = !is_allowed;
/* if mCanWrite, grant mCanUpdate implicitely unless 'u' explicitely denied */
if (mCanWrite) {
mCanUpdate = true; /* 'u' is checked after 'w', this could be reverted */
}
break;
case 'd':
mCanNotDelete = !is_allowed;
break;
case 'u':
mCanUpdate = is_allowed;
mCanNotUpdate = !is_allowed;
break;
case 'i':
mIsMutable = !is_allowed;
break;
}
}
if (EOS_LOGS_DEBUG) {
eos_static_debug(
"mCanRead %d mCanNotRead %d mCanWrite %d mCanNotWrite %d mCanWriteOnce %d mCanUpdate %d mCanNotUpdate %d "
"mCanBrowse %d mCanNotBrowse %d mCanChmod %d mCanChown %d mCanNotDelete %d mCanNotChmod %d "
"mCanDelete %d mCanSetQuota %d mHasAcl %d mHasEgroup %d mIsMutable %d mCanArchive %d mCanPrepare %d",
mCanRead, mCanNotRead, mCanWrite, mCanNotWrite, mCanWriteOnce, mCanUpdate,
mCanNotUpdate,
mCanBrowse, mCanNotBrowse, mCanChmod, mCanChown, mCanNotDelete, mCanNotChmod,
mCanDelete, mCanSetQuota, mHasAcl, mHasEgroup, mIsMutable, mCanArchive,
mCanPrepare);
}
}
//------------------------------------------------------------------------------
// Check whether ACL has a valid format / syntax.
//------------------------------------------------------------------------------
bool
Acl::IsValid(const std::string& value, XrdOucErrInfo& error, bool is_sys_acl,
bool check_numeric)
{
// Empty is valid
if (!value.length()) {
return true;
}
int regexErrorCode;
int result;
regex_t regex;
std::string regexString;
if (is_sys_acl) {
if (check_numeric) {
regexString = sRegexSysNumericAcl;
} else {
regexString = sRegexSysGenericAcl;
}
} else {
if (check_numeric) {
regexString = sRegexUsrNumericAcl;
} else {
regexString = sRegexUsrGenericAcl;
}
}
// Compile regex
regexErrorCode = regcomp(®ex, regexString.c_str(), REG_EXTENDED);
if (regexErrorCode) {
eos_static_debug("regcomp regexErrorCode=%d regex '%s'", regexErrorCode,
regexString.c_str()); // the setErrInfo below does not always produce a visible result
error.setErrInfo(2, "failed to compile regex");
regfree(®ex);
return false;
}
// Execute regex
result = regexec(®ex, value.c_str(), 0, NULL, 0);
regfree(®ex);
// Check the result
if (result == 0) {
return true;
} else if (result == REG_NOMATCH) {
error.setErrInfo(1, "invalid acl syntax");
return false;
} else { // REG_BADPAT, REG_ESPACE, etc...
error.setErrInfo(2, "invalid regex or out of memory");
return false;
}
}
//------------------------------------------------------------------------------
// Convert acl rules to numeric uid/gid if needed
//------------------------------------------------------------------------------
int
Acl::ConvertIds(std::string& acl_val, bool to_string)
{
if (acl_val.empty()) {
return 0;
}
bool is_uid, is_gid;
std::string sid;
std::ostringstream oss;
using eos::common::StringConversion;
std::vector rules;
StringConversion::Tokenize(acl_val, rules, ",");
if (!rules.size() && acl_val.length()) {
rules.push_back(acl_val);
}
for (auto& rule : rules) {
is_uid = is_gid = false;
std::vector tokens;
StringConversion::Tokenize(rule, tokens, ":");
eos_static_debug("rule=%s, tokens.size=%i", rule.c_str(), tokens.size());
if (tokens.size() != 3) {
oss << rule << ',';
continue;
}
is_uid = (tokens[0] == "u");
is_gid = (tokens[0] == "g");
if (!is_uid && !is_gid) {
oss << rule << ',';
continue;
}
sid = tokens[1];
bool needs_conversion = false;
if (to_string) {
// Convert to string representation if needed
needs_conversion =
(std::find_if(sid.begin(), sid.end(),
[](const char& c) {
return std::isalpha(c);
}) == sid.end());
} else {
// Convert to numeric representation if needed
needs_conversion =
(std::find_if(sid.begin(), sid.end(),
[](const char& c) {
return std::isalpha(c);
}) != sid.end());
}
if (needs_conversion) {
int errc = 0;
std::uint32_t numeric_id {0};
std::string string_id {""};
if (is_uid) {
if (!to_string) {
numeric_id = eos::common::Mapping::UserNameToUid(sid, errc);
string_id = std::to_string(numeric_id);
} else {
numeric_id = atoi(sid.c_str());
string_id = eos::common::Mapping::UidToUserName(numeric_id, errc);
}
} else {
if (!to_string) {
numeric_id = eos::common::Mapping::GroupNameToGid(sid, errc);
string_id = std::to_string(numeric_id);
} else {
numeric_id = atoi(sid.c_str());
string_id = eos::common::Mapping::GidToGroupName(numeric_id, errc);
}
}
if (errc) {
oss.str("");
if (to_string) {
oss << "failed to convert id: \"" << sid << "\" to string format";
} else {
oss << "failed to convert id: \"" << sid << "\" to numeric format";
}
// Print error message but still return the original value that we have
eos_static_err(oss.str().c_str());
string_id = sid;
return 1;
}
oss << tokens[0] << ':' << string_id << ':' << tokens[2] << ',';
} else {
oss << rule << ',';
}
}
acl_val = oss.str();
if (*acl_val.rbegin() == ',') {
acl_val.pop_back();
}
return 0;
}
//------------------------------------------------------------------------------
// Extract an ACL rule from a token
//------------------------------------------------------------------------------
std::string
Acl::TokenAcl(const eos::common::VirtualIdentity& vid) const
{
if (vid.token) {
if (vid.token->Valid()) {
if (!vid.token->ValidatePath(vid.scope)) {
std::string tokenacl;
tokenacl = "u:";
tokenacl += vid.uid_string;
tokenacl += ":";
tokenacl += vid.token->Permission();
return tokenacl;
} else {
eos_static_err("%s", "msg=\"invald path token\"");
}
} else {
eos_static_err("%s", "msg=\"invalid token\"");
}
} else {
eos_static_debug("%s", "msg=\"no token\"");
}
return "";
}
EOSMGMNAMESPACE_END