diff --git a/src/XrdHttp/XrdHttpSecurity.cc b/src/XrdHttp/XrdHttpSecurity.cc index af1c04c1530..638045eb659 100644 --- a/src/XrdHttp/XrdHttpSecurity.cc +++ b/src/XrdHttp/XrdHttpSecurity.cc @@ -26,6 +26,7 @@ #include "XrdCrypto/XrdCryptoX509Chain.hh" #include "XrdCrypto/XrdCryptosslAux.hh" #include "XrdCrypto/XrdCryptoFactory.hh" +#include "XrdSec/XrdSecEntityAttr.hh" #include "XrdTls/XrdTlsPeerCerts.hh" #include "XrdTls/XrdTlsContext.hh" #include "XrdOuc/XrdOucGMap.hh" @@ -164,6 +165,7 @@ XrdHttpProtocol::HandleGridMap(XrdLink* lp) TRACEI(DEBUG, " Mapping name: '" << SecEntity.moninfo << "' --> " << bufname); if (SecEntity.name) free(SecEntity.name); SecEntity.name = strdup(bufname); + SecEntity.eaAPI->Add("gridmap.name", "1", true); } else { TRACEI(ALL, " Mapping name: " << SecEntity.moninfo << " Failed. err: " << mape); diff --git a/src/XrdSecgsi/XrdSecProtocolgsi.cc b/src/XrdSecgsi/XrdSecProtocolgsi.cc index b4b704b4505..c7bcb1d7e98 100644 --- a/src/XrdSecgsi/XrdSecProtocolgsi.cc +++ b/src/XrdSecgsi/XrdSecProtocolgsi.cc @@ -43,6 +43,7 @@ #include "XrdVersion.hh" #include "XrdNet/XrdNetAddr.hh" +#include "XrdSec/XrdSecEntityAttr.hh" #include "XrdSys/XrdSysHeaders.hh" #include "XrdSys/XrdSysLogger.hh" #include "XrdSys/XrdSysError.hh" @@ -1953,6 +1954,7 @@ int XrdSecProtocolgsi::Authenticate(XrdSecCredentials *cred, DEBUG("user mapping lookup successful: name is '"<Add("gridmap.name", "1", true); } } // If not set, use DN diff --git a/src/XrdVoms.cmake b/src/XrdVoms.cmake index bc4a4229c7d..1951a376eb0 100644 --- a/src/XrdVoms.cmake +++ b/src/XrdVoms.cmake @@ -13,6 +13,7 @@ add_library( ${LIB_XRD_VOMS} MODULE ${CMAKE_SOURCE_DIR}/src/XrdVoms/XrdVomsFun.cc + ${CMAKE_SOURCE_DIR}/src/XrdVoms/XrdVomsMapfile.cc ${CMAKE_SOURCE_DIR}/src/XrdVoms/XrdVomsgsi.cc ${CMAKE_SOURCE_DIR}/src/XrdVoms/XrdVomsHttp.cc ) diff --git a/src/XrdVoms/README.md b/src/XrdVoms/README.md new file mode 100644 index 00000000000..e4a15d77240 --- /dev/null +++ b/src/XrdVoms/README.md @@ -0,0 +1,91 @@ + +VOMS Mapping +============ + +The VOMS plugin can now populate the XRootD session's `name` attribute from a +mapping file (the "voms-mapfile"). Filesystems which rely on the username +in addition to the XRootD authorization can utilize this name to make authorization +and file ownership decisions. + +Note the plugins have the following precedence for the `name` attribute: + +- Explicit entry in the grid-mapfile. +- Entry in the voms-mapfile. +- Default auto-generated name for the grid mapfile. + +Administrators may desire to disable the auto-generated name as it likely does +not match any Unix username on the system. + +Configuration +------------- + +There are two configuration options that control the plugin: + +``` +voms.mapfile FILENAME +``` + +Enables the mapping functionality and uses the file at FILENAME as the voms-mapfile. +The mapfile is reloaded every 30 seconds; the daemon does not need to be restarted +to pick up changes. + +``` +voms.trace [none|all|debug|info|warning|error]+ +``` + +Enable debugging of the VOMS mapfile logic. Options are additive and multiple can be +given. + +Format and Matching Details +--------------------------- + +The file format ignores empty lines; a line beginning with the hash (`#`) are considered +comments and ignored. + +Otherwise, each line specifies a mapping from an expression to a Unix username in the +following form: + +``` +"EXPRESSION" USERNAME +``` + +If the session has a VOMS FQAN matching EXPRESSION then the session's name will be set +to USERNAME. + +Examples of the EXPRESSION include: + +``` +/cms/Role=production/Capability=NULL +/atlas/usatlas/Role=pilot/Capability=NULL +``` + +Expressions may also have wildcards (`*`) present. The wildcard can serve as +two roles: + +- If the expression ends with `/*`, then any remaining portion of the attribute + is matched. For example, `/cms/*` matches `/cms/Role=NULL/Capability=NULL` and + `/cms/uscms/Role=pilot/Capability=NULL`. +- If the wildcard is inside a path hierarchy, it allows any character inisde the + path. For example, `/fermilab/*/Role=pilot/Capability=NULL` matches both + `/fermilab/dune/Role=pilot/Capability=NULL` and `/fermilab/des/Role=pilot/Capability=NULL` + but not `/fermilab/Role=pilot/Capability=NULL`. + +Several escape sequences are supported within the expression: + +- `\'`: a single quote character (`'`). +- `\"`: a double quote character (`"`). +- `\\`: a backwards slash (`\`). +- `\/`: a forward slash that is not a path separator (`/`) +- `\f`: a formfeed +- `\n`: a newline +- `\r`: a carriage return +- `\t`: a tab character. + +The use of these escape sequences are discouraged as it's unclear whether other software +is able to safely handle them. Unicode and extended 8-bit ASCII are not supported at this +time. + +Note, as is tradition, the name of the VO in the VOMS FQAN must match the first group name. +That is, if the `cms` VO issues a FQAN of the form `/atlas/Role=pilot/Capability=NULL` then +the FQAN is ignored. + diff --git a/src/XrdVoms/XrdVomsFun.cc b/src/XrdVoms/XrdVomsFun.cc index 1c7fd5417c7..a4ced43ba5c 100644 --- a/src/XrdVoms/XrdVomsFun.cc +++ b/src/XrdVoms/XrdVomsFun.cc @@ -41,6 +41,7 @@ #include "XrdVoms.hh" #include "XrdVomsFun.hh" #include "XrdVomsTrace.hh" +#include "XrdVomsMapfile.hh" #ifdef HAVE_XRDCRYPTO #include "XrdCrypto/XrdCryptoX509.hh" @@ -380,7 +381,13 @@ int XrdVomsFun::VOMSFun(XrdSecEntity &ent) // Success or failure? int rc = !ent.vorg ? -1 : 0; if (rc == 0 && gGrps.Num() && !ent.grps) rc = -1; - + + // If we have a mapfile object, apply the mapping now. + if (m_mapfile) { + auto mapfile_rc = m_mapfile->Apply(ent); + rc = rc ? rc : mapfile_rc; + } + // Done return rc; } @@ -592,6 +599,13 @@ int XrdVomsFun::VOMSInit(const char *cfg) if (gVOs.Num() > 0) {PRINT("+++ VO(s): "<< voss);} else {PRINT("+++ VO(s): all");} PRINT("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"); + + m_mapfile = XrdVomsMapfile::Configure(&gDest); + if (m_mapfile == VOMS_MAP_FAILED) { + aOK = false; + PRINT("VOMS mapfile requested but initialization failed; failing VOMS plugin config."); + } + // Done return (aOK ? gCertFmt : -1); } diff --git a/src/XrdVoms/XrdVomsFun.hh b/src/XrdVoms/XrdVomsFun.hh index 41a9251c9fd..ff5df58efc8 100644 --- a/src/XrdVoms/XrdVomsFun.hh +++ b/src/XrdVoms/XrdVomsFun.hh @@ -38,6 +38,7 @@ class XrdSecEntity; class XrdSysError; class XrdSysLogger; +class XrdVomsMapfile; class XrdVomsFun { @@ -82,5 +83,7 @@ XrdOucString gVoFmt; // format contents of XrdSecEntity::vorg XrdSysError &gDest; XrdSysLogger *gLogger; + +XrdVomsMapfile *m_mapfile{nullptr}; }; #endif diff --git a/src/XrdVoms/XrdVomsMapfile.cc b/src/XrdVoms/XrdVomsMapfile.cc new file mode 100644 index 00000000000..824edd94a49 --- /dev/null +++ b/src/XrdVoms/XrdVomsMapfile.cc @@ -0,0 +1,424 @@ +/******************************************************************************/ +/* */ +/* X r d V o m s M a p f i l e . c c */ +/* */ +/* This file is part of the XRootD software suite. */ +/* */ +/* XRootD is free software: you can redistribute it and/or modify it under */ +/* the terms of the GNU Lesser General Public License as published by the */ +/* Free Software Foundation, either version 3 of the License, or (at your */ +/* option) any later version. */ +/* */ +/* XRootD 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 Lesser General Public */ +/* License for more details. */ +/* */ +/* You should have received a copy of the GNU Lesser General Public License */ +/* along with XRootD in a file called COPYING.LESSER (LGPL license) and file */ +/* COPYING (GPL license). If not, see . */ +/* */ +/* The copyright holder's institutional names and contributor's names may not */ +/* be used to endorse or promote products derived from this software without */ +/* specific prior written permission of the institution or contributor. */ +/******************************************************************************/ + +#include "XrdVoms/XrdVomsMapfile.hh" + +#include "XrdOuc/XrdOucEnv.hh" +#include "XrdOuc/XrdOucString.hh" +#include "XrdOuc/XrdOucStream.hh" +#include "XrdSec/XrdSecEntity.hh" +#include "XrdSec/XrdSecEntityAttr.hh" +#include "XrdSys/XrdSysError.hh" +#include "XrdSys/XrdSysFD.hh" +#include "XrdSys/XrdSysPthread.hh" + +#include +#include +#include +#include +#include +#include +#include + +bool XrdVomsMapfile::tried_configure = false; +std::unique_ptr XrdVomsMapfile::mapper; + +namespace { + +std::string +PathToString(const std::vector &path) +{ + if (path.empty()) {return "/";} + std::stringstream ss; + for (const auto &entry : path) { + ss << "/" << entry; + } + + return ss.str(); +} + +uint64_t monotonic_time_s() { + struct timespec tp; + clock_gettime(CLOCK_MONOTONIC, &tp); + return tp.tv_sec + (tp.tv_nsec >= 500000000); +} + +} + + +XrdVomsMapfile::XrdVomsMapfile(XrdSysError *erp, + const std::string &mapfile) + : m_mapfile(mapfile), m_edest(erp) +{ + struct stat statbuf; + if (-1 == stat(m_mapfile.c_str(), &statbuf)) { + m_edest->Emsg("XrdVomsMapfile", errno, "Error checking the mapfile", m_mapfile.c_str()); + return; + } + memcpy(&m_mapfile_ctime, &statbuf.st_ctim, sizeof(decltype(m_mapfile_ctime))); + + if (!ParseMapfile(m_mapfile)) {return;} + + pthread_t tid; + auto rc = XrdSysThread::Run(&tid, XrdVomsMapfile::MaintenanceThread, + static_cast(this), 0, "VOMS Mapfile refresh"); + if (rc) { + m_edest->Emsg("XrdVomsMapfile", "Failed to launch VOMS mapfile monitoring thread"); + return; + } + m_is_valid = true; +} + + +XrdVomsMapfile::~XrdVomsMapfile() +{} + + +bool +XrdVomsMapfile::ParseMapfile(const std::string &mapfile) +{ + std::ifstream fstr(mapfile); + if (!fstr.is_open()) { + m_edest->Emsg("ParseMapfile", "Failed to open file", mapfile.c_str(), strerror(errno)); + return false; + } + std::shared_ptr> entries(new std::vector()); + for (std::string line; std::getline(fstr, line); ) { + MapfileEntry entry; + if (ParseLine(line, entry.m_path, entry.m_target) && !entry.m_path.empty()) { + if (m_edest->getMsgMask() & LogMask::Debug) { + m_edest->Log(LogMask::Debug, "ParseMapfile", PathToString(entry.m_path).c_str(), "->", entry.m_target.c_str()); + } + entries->emplace_back(entry); + } + } + m_entries = entries; + return true; +} + + +bool +XrdVomsMapfile::ParseLine(const std::string &line, std::vector &entry, std::string &target) +{ + bool began_entry = false; + bool finish_entry = false; + bool began_target = false; + std::string element; + element.reserve(16); + for (size_t idx=0; idx &fqan) +{ + decltype(m_entries) entries = m_entries; + if (!entries) {return "";} + + if (m_edest && (m_edest->getMsgMask() & LogMask::Debug)) { + m_edest->Log(LogMask::Debug, "VOMSMapfile", "Mapping VOMS FQAN", PathToString(fqan).c_str()); + } + + for (const auto &entry : *entries) { + if (Compare(entry, fqan)) { + if (m_edest && (m_edest->getMsgMask() & LogMask::Debug)) { + m_edest->Log(LogMask::Debug, "VOMSMapfile", "Mapped FQAN to target", entry.m_target.c_str()); + } + return entry.m_target; + } + } + return ""; +} + + +bool +XrdVomsMapfile::Compare(const MapfileEntry &entry, const std::vector &fqan) +{ + if (entry.m_path.empty()) {return false;} + + // A more specific mapfile entry cannot match a generic FQAN + if (fqan.size() < entry.m_path.size()) {return false;} + + XrdOucString fqan_element; + for (size_t idx=0; idx +XrdVomsMapfile::MakePath(const XrdOucString &group) +{ + int from = 0; + XrdOucString entry; + std::vector path; + path.reserve(4); + // The const'ness of the tokenize method as declared is incorrect; we use + // const_cast here to avoid fixing the XrdOucString header (which would break + // the ABI). + while ((from = const_cast(group).tokenize(entry, from, '/')) != -1) { + if (entry.length() == 0) continue; + path.emplace_back(entry.c_str()); + } + return path; +} + + +int +XrdVomsMapfile::Apply(XrdSecEntity &entity) +{ + // In current use cases, the gridmap results take precedence over the voms-mapfile + // results. However, the grid mapfile plugins often will populate the name attribute + // with a reasonable default (DN or DN hash) if the mapping fails, meaning we can't + // simply look at entity.name; instead, we look at an extended attribute that is only + // set when the mapfile is used to generate the name. + std::string gridmap_name; + auto gridmap_success = entity.eaAPI->Get("gridmap.name", gridmap_name); + if (gridmap_success && gridmap_name == "1") { + return 0; + } + + int from_vorg = 0, from_role = 0, from_grps = 0; + XrdOucString vorg = entity.vorg, entry_vorg; + XrdOucString role = entity.role, entry_role; + XrdOucString grps = entity.grps, entry_grps; + if (m_edest) m_edest->Log(LogMask::Debug, "VOMSMapfile", "Applying VOMS mapfile to incoming credential"); + while (((from_vorg = vorg.tokenize(entry_vorg, from_vorg, ' ')) != -1) && + ((from_role = role.tokenize(entry_role, from_role, ' ')) != -1) && + ((from_grps = grps.tokenize(entry_grps, from_grps, ' ')) != -1)) + { + auto fqan = MakePath(entry_grps); + if (fqan.empty()) {continue;} + + // By convention, the root group should be the same as the VO name; however, + // the VOMS mapfile makes this assumption. To be secure, enforce it. + if (strcmp(fqan[0].c_str(), entry_vorg.c_str())) {continue;} + + fqan.emplace_back(std::string("Role=") + entry_role.c_str()); + fqan.emplace_back("Capability=NULL"); + std::string username; + if (!(username = Map(fqan)).empty()) { + if (entity.name) {free(entity.name);} + entity.name = strdup(username.c_str()); + break; + } + } + + return 0; +} + + +XrdVomsMapfile * +XrdVomsMapfile::Get() +{ + return mapper.get(); +} + + +XrdVomsMapfile * +XrdVomsMapfile::Configure(XrdSysError *erp) +{ + if (tried_configure) { + auto result = mapper.get(); + if (result) { + result->SetErrorStream(erp); + } + return result; + } + + tried_configure = true; + + // Set default mask for logging. + if (erp) erp->setMsgMask(LogMask::Error | LogMask::Warning); + + char *config_filename = nullptr; + if (!XrdOucEnv::Import("XRDCONFIGFN", config_filename)) { + return VOMS_MAP_FAILED; + } + XrdOucStream stream(erp, getenv("XRDINSTANCE")); + + int cfg_fd; + if ((cfg_fd = open(config_filename, O_RDONLY, 0)) < 0) { + if (erp) erp->Emsg("Config", errno, "open config file", config_filename); + return VOMS_MAP_FAILED; + } + stream.Attach(cfg_fd); + char *var; + std::string map_filename; + while ((var = stream.GetMyFirstWord())) { + if (!strcmp(var, "voms.mapfile")) { + auto val = stream.GetWord(); + if (!val || !val[0]) { + if (erp) erp->Emsg("Config", "VOMS mapfile not specified"); + return VOMS_MAP_FAILED; + } + map_filename = val; + } else if (!strcmp(var, "voms.trace")) { + auto val = stream.GetWord(); + if (!val || !val[0]) { + if (erp) erp->Emsg("Config", "VOMS logging level not specified"); + return VOMS_MAP_FAILED; + } + if (erp) erp->setMsgMask(0); + if (erp) do { + if (!strcmp(val, "all")) {erp->setMsgMask(erp->getMsgMask() | LogMask::All);} + else if (!strcmp(val, "error")) {erp->setMsgMask(erp->getMsgMask() | LogMask::Error);} + else if (!strcmp(val, "warning")) {erp->setMsgMask(erp->getMsgMask() | LogMask::Warning);} + else if (!strcmp(val, "info")) {erp->setMsgMask(erp->getMsgMask() | LogMask::Info);} + else if (!strcmp(val, "debug")) {erp->setMsgMask(erp->getMsgMask() | LogMask::Debug);} + else if (!strcmp(val, "none")) {erp->setMsgMask(0);} + else {erp->Emsg("Config", "voms.trace encountered an unknown directive:", val);} + val = stream.GetWord(); + } while (val); + } + } + + if (!map_filename.empty()) { + if (erp) erp->Emsg("Config", "Will initialize VOMS mapfile", map_filename.c_str()); + mapper.reset(new XrdVomsMapfile(erp, map_filename)); + if (!mapper->IsValid()) { + mapper.reset(nullptr); + return VOMS_MAP_FAILED; + } + } + + return mapper.get(); +} + + +void * +XrdVomsMapfile::MaintenanceThread(void *myself_raw) +{ + auto myself = static_cast(myself_raw); + + auto now = monotonic_time_s(); + auto next_update = now + m_update_interval; + while (true) { + now = monotonic_time_s(); + auto remaining = next_update - now; + auto rval = sleep(remaining); + if (rval > 0) { + // Woke up early due to a signal; re-run prior logic. + continue; + } + next_update = monotonic_time_s() + m_update_interval; + struct stat statbuf; + if (-1 == stat(myself->m_mapfile.c_str(), &statbuf)) { + myself->m_edest->Emsg("XrdVomsMapfile", errno, "Error checking the mapfile", + myself->m_mapfile.c_str()); + myself->m_mapfile_ctime.tv_sec = 0; + myself->m_mapfile_ctime.tv_nsec = 0; + myself->m_is_valid = false; + continue; + } + // Use ctime here as it is solely controlled by the OS (unlike mtime, + // which can be manipulated by userspace and potentially not change + // when updated - rsync, tar, and rpm, for example, all preserve mtime). + // ctime also will also be updated appropriately for overwrites/renames, + // allowing us to detect those changes as well. + // + if ((myself->m_mapfile_ctime.tv_sec == statbuf.st_ctim.tv_sec) && + (myself->m_mapfile_ctime.tv_nsec == statbuf.st_ctim.tv_nsec)) + { + myself->m_edest->Log(LogMask::Debug, "Maintenance", "Not reloading VOMS mapfile; " + "no changes detected."); + continue; + } + memcpy(&myself->m_mapfile_ctime, &statbuf.st_ctim, sizeof(decltype(statbuf.st_ctim))); + + myself->m_edest->Log(LogMask::Debug, "Maintenance", "Reloading VOMS mapfile now"); + if ( !(myself->m_is_valid = myself->ParseMapfile(myself->m_mapfile)) ) { + myself->m_edest->Log(LogMask::Error, "Maintenance", "Failed to reload VOMS mapfile"); + } + } + return nullptr; +} diff --git a/src/XrdVoms/XrdVomsMapfile.hh b/src/XrdVoms/XrdVomsMapfile.hh new file mode 100644 index 00000000000..8119411cb6d --- /dev/null +++ b/src/XrdVoms/XrdVomsMapfile.hh @@ -0,0 +1,99 @@ +/******************************************************************************/ +/* */ +/* X r d V o m s M a p f i l e . h h */ +/* */ +/* This file is part of the XRootD software suite. */ +/* */ +/* XRootD is free software: you can redistribute it and/or modify it under */ +/* the terms of the GNU Lesser General Public License as published by the */ +/* Free Software Foundation, either version 3 of the License, or (at your */ +/* option) any later version. */ +/* */ +/* XRootD 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 Lesser General Public */ +/* License for more details. */ +/* */ +/* You should have received a copy of the GNU Lesser General Public License */ +/* along with XRootD in a file called COPYING.LESSER (LGPL license) and file */ +/* COPYING (GPL license). If not, see . */ +/* */ +/* The copyright holder's institutional names and contributor's names may not */ +/* be used to endorse or promote products derived from this software without */ +/* specific prior written permission of the institution or contributor. */ +/******************************************************************************/ + +#include "XrdOuc/XrdOucString.hh" +#include "XrdSys/XrdSysError.hh" +#include "XrdSec/XrdSecEntity.hh" + +#include +#include +#include +#include + +#define VOMS_MAP_FAILED ((XrdVomsMapfile *)-1) + +class XrdVomsMapfile { + +public: + virtual ~XrdVomsMapfile(); + + // Returns `nullptr` if the mapfile was not configured; returns + // VOMS_MAP_FAILED (`(void*)-1`) if the mapfile was configured but it + // was unable to be parsed (or other error occurred). + static XrdVomsMapfile *Configure(XrdSysError *); + static XrdVomsMapfile *Get(); + + int Apply(XrdSecEntity &); + + bool IsValid() const {return m_is_valid;} + +private: + bool Reconfigure(); + void SetErrorStream(XrdSysError *erp) {if (erp) {m_edest = erp;}} + + XrdVomsMapfile(XrdSysError *erp, const std::string &mapfile); + + enum LogMask { + Debug = 0x01, + Info = 0x02, + Warning = 0x04, + Error = 0x08, + All = 0xff + }; + + struct MapfileEntry { + std::vector m_path; + std::string m_target; + }; + + bool ParseMapfile(const std::string &mapfile); + bool ParseLine(const std::string &line, std::vector &entry, std::string &target); + + std::string Map(const std::vector &fqan); + bool Compare(const MapfileEntry &entry, const std::vector &fqan); + std::vector MakePath(const XrdOucString &group); + + // A continuously-running thread for maintenance tasks (reloading the mapfile) + static void *MaintenanceThread(void *myself_raw); + + // Set to true if the last maintenance attempt succeeded. + bool m_is_valid = false; + // Time of the last observed status change of file. + struct timespec m_mapfile_ctime{0, 0}; + + std::string m_mapfile; + std::shared_ptr> m_entries; + XrdSysError *m_edest{nullptr}; + + // After success, how long to wait until the next mapfile check. + static constexpr unsigned m_update_interval = 30; + + // Singleton + static std::unique_ptr mapper; + // There are multiple protocol objects that may need the mapfile object; + // if we already tried-and-failed configuration once, this singleton will + // help us avoid failing again. + static bool tried_configure; +};