diff --git a/src/XrdHttp.cmake b/src/XrdHttp.cmake index bb42450270c..cf9745f3735 100644 --- a/src/XrdHttp.cmake +++ b/src/XrdHttp.cmake @@ -17,12 +17,18 @@ if( BUILD_HTTP ) # The XrdHttp library #----------------------------------------------------------------------------- include_directories( ${OPENSSL_INCLUDE_DIR} ) - + +if( BUILD_MACAROONS ) + set( MACAROON_FILES XrdMacaroons/XrdMacaroonsAuthz.cc XrdMacaroons/XrdMacaroonsAuthz.hh + XrdMacaroons/XrdMacaroonsConfigure.cc ) +endif() + # Note this is marked as a shared library as XrdHttp plugins are expected to # link against this for the XrdHttpExt class implementations. add_library( ${LIB_XRD_HTTP_UTILS} SHARED + ${MACAROON_FILES} XrdHttp/XrdHttpProtocol.cc XrdHttp/XrdHttpProtocol.hh XrdHttp/XrdHttpReq.cc XrdHttp/XrdHttpReq.hh XrdHttp/XrdHttpSecXtractor.hh @@ -36,12 +42,18 @@ if( BUILD_HTTP ) MODULE XrdHttp/XrdHttpModule.cc ) +if( BUILD_MACAROONS ) + include_directories(${MACAROONS_INCLUDES} ${UUID_INCLUDE_DIRS}) +endif() + target_link_libraries( ${LIB_XRD_HTTP_UTILS} XrdServer XrdUtils ${CMAKE_DL_LIBS} pthread + ${MACAROONS_LIB} + ${UUID_LIBRARIES} ${OPENSSL_LIBRARIES} ${OPENSSL_CRYPTO_LIBRARY} ) diff --git a/src/XrdHttp/XrdHttpProtocol.cc b/src/XrdHttp/XrdHttpProtocol.cc index 27fc59dbd8a..dda44c09c65 100644 --- a/src/XrdHttp/XrdHttpProtocol.cc +++ b/src/XrdHttp/XrdHttpProtocol.cc @@ -43,6 +43,11 @@ #include "XrdHttpSecXtractor.hh" #include "XrdHttpExtHandler.hh" +// Includes only needed for macaroon signing. +#include "XrdMacaroons/XrdMacaroonsAuthz.hh" +#include "XrdOuc/XrdOucPinPath.hh" +#include + #include "XrdTls/XrdTlsContext.hh" #include @@ -62,6 +67,14 @@ /* G l o b a l s */ /******************************************************************************/ +// Trick to access compiled version and directly call for the default object +// is taken from xrootd-scitokens. +static XrdVERSIONINFODEF(compiledVerAcc, XrdAccTest, XrdVNUMBER, XrdVERSION); +extern XrdAccAuthorize *XrdAccDefaultAuthorizeObject(XrdSysLogger *lp, + const char *cfn, + const char *parm, + XrdVersionInfo &myVer); + // It seems that eos needs this to be present const char *XrdHttpSecEntityTident = "http"; @@ -113,6 +126,8 @@ XrdSecService *XrdHttpProtocol::CIA = 0; // Authentication Server int XrdHttpProtocol::m_bio_type = 0; // BIO type identifier for our custom BIO. BIO_METHOD *XrdHttpProtocol::m_bio_method = NULL; // BIO method constructor. +void *XrdHttpProtocol::m_macaroon_authz = NULL; + /******************************************************************************/ /* P r o t o c o l M a n a g e m e n t S t a c k s */ /******************************************************************************/ @@ -1082,6 +1097,7 @@ int XrdHttpProtocol::Config(const char *ConfigFN, XrdOucEnv *myEnv) { else if TS_Xeq("desthttps", xdesthttps); else if TS_Xeq("secxtractor", xsecxtractor); else if TS_Xeq3("exthandler", xexthandler); + else if TS_Xeq3("proxymacaroons", xproxymacaroon); else if TS_Xeq("selfhttps2http", xselfhttps2http); else if TS_Xeq("embeddedstatic", xembeddedstatic); else if TS_Xeq("listingredir", xlistredir); @@ -2412,10 +2428,123 @@ int XrdHttpProtocol::xsecxtractor(XrdOucStream & Config) { +/******************************************************************************/ +/* x p r o x y m a c a r o o n */ +/******************************************************************************/ + +/* Function: xproxymacaroon + * + * Purpose: To parse the directive: proxymacaroon {true|false} + * + * Set to `true` to have the HTTP plugin to sign a macaroon internally + * for each request; useful when an internal filesystem (such as XrdPss) + * does not have the ability to forward an XrdSecEntity but might understand + * macaroons. + * + * Output: 0 upon success or !0 upon failure. + */ + +int XrdHttpProtocol::xproxymacaroon(XrdOucStream & Config, const char *ConfigFN, + XrdOucEnv *myEnv) +{ + char *val = Config.GetWord(); + if (!val || !val[0]) { + eDest.Emsg("Config", "http.proxymacaroon must be set to true or false."); + return 1; + } + + if (!strcasecmp(val, "false")) { + return 0; + } else if (strcasecmp(val, "true")) { + eDest.Emsg("Config", "http.proxymacaroon must be set to true or false; invalid value:", val); + return 1; + } + XrdOucStream config_obj(&eDest, getenv("XRDINSTANCE"), myEnv, "=====> "); + // Open and attach the config file + // + int cfg_fd; + if ((cfg_fd = open(ConfigFN, O_RDONLY, 0)) < 0) { + return eDest.Emsg("Config", errno, "open config file", ConfigFN); + } + config_obj.Attach(cfg_fd); + static const char *cvec[] = { "*** HTTP request handler config:", 0 }; + config_obj.Capture(cvec); + XrdAccAuthorize *chain_authz = NULL; + // Process items + // + char *var; + while ((var = config_obj.GetMyFirstWord())) { + if (strcmp("ofs.authlib", var)) {continue;} + + if (!config_obj.GetWord()) { + eDest.Emsg("Config", "Invalid ofs.authlib line found."); + break; + } + + char parms[2048]; parms[0] = '\0'; + if (!config_obj.GetRest(parms, sizeof(parms))) { + eDest.Emsg("Config", "Failed to get remaining parameters"); + } + + if (parms[0]) { + XrdOucString parms_str(parms); + XrdOucString chained_lib; + int from = parms_str.tokenize(chained_lib, 0, ' '); + const char *chained_parms = NULL; + eDest.Emsg("Config", "Will chain library", chained_lib.c_str()); + if (from > 0) + { + parms_str.erasefromstart(from); + if (parms_str.length()) + { + eDest.Emsg("Config", "Will chain parameters", parms_str.c_str()); + chained_parms = parms_str.c_str(); + } + } + char resolvePath[2048]; + bool usedAltPath{true}; + if (!XrdOucPinPath(chained_lib.c_str(), usedAltPath, resolvePath, 2048)) { + eDest.Emsg("Config", "Failed to locate appropriately versioned chained auth library:", parms); + return 1; + } + void *handle_base = dlopen(resolvePath, RTLD_LOCAL|RTLD_NOW); + if (handle_base == NULL) { + eDest.Emsg("Config", "Failed to base plugin ", resolvePath, dlerror()); + return 1; + } + + XrdAccAuthorize *(*ep)(XrdSysLogger *, const char *, const char *); + ep = (XrdAccAuthorize *(*)(XrdSysLogger *, const char *, const char *)) + (dlsym(handle_base, "XrdAccAuthorizeObject")); + if (!ep) + { + eDest.Emsg("Config", "Unable to chain second authlib after macaroons", parms); + return 1; + } + chain_authz = (*ep)(eDest.logger(), ConfigFN, chained_parms); + } + break; + } + if (!chain_authz) { + chain_authz = XrdAccDefaultAuthorizeObject(eDest.logger(), ConfigFN, "", compiledVerAcc); + } + + try + { + m_macaroon_authz = new Macaroons::Authz(eDest.logger(), ConfigFN, chain_authz); + return 0; + } + catch (std::runtime_error &e) + { + eDest.Emsg("Config", "Configuration of Macaroon authorization handler failed", e.what()); + m_macaroon_authz = NULL; + } + return 1; +} /******************************************************************************/ diff --git a/src/XrdHttp/XrdHttpProtocol.hh b/src/XrdHttp/XrdHttpProtocol.hh index bb850b2d0aa..6faa73e1e9f 100644 --- a/src/XrdHttp/XrdHttpProtocol.hh +++ b/src/XrdHttp/XrdHttpProtocol.hh @@ -166,6 +166,7 @@ private: static int xsslkey(XrdOucStream &Config); static int xsecxtractor(XrdOucStream &Config); static int xexthandler(XrdOucStream & Config, const char *ConfigFN, XrdOucEnv *myEnv); + static int xproxymacaroon(XrdOucStream & Config, const char *ConfigFN, XrdOucEnv *myEnv); static int xsslcadir(XrdOucStream &Config); static int xsslcipherfilter(XrdOucStream &Config); static int xdesthttps(XrdOucStream &Config); @@ -318,6 +319,10 @@ protected: // Processing configuration values // + /// XrdMacaroonAuthz handler (type-erased) for signing + /// tokens. + static void *m_macaroon_authz; + /// Timeout for reading the handshake static int hailWait; diff --git a/src/XrdHttp/XrdHttpReq.cc b/src/XrdHttp/XrdHttpReq.cc index 92dc45d2667..88dd034fafd 100644 --- a/src/XrdHttp/XrdHttpReq.cc +++ b/src/XrdHttp/XrdHttpReq.cc @@ -66,6 +66,8 @@ // This is to fix the trace macros #define TRACELINK prot->Link +// Required for Macaroon-based proxying +#include "XrdMacaroons/XrdMacaroonsAuthz.hh" static std::string convert_digest_rfc_name(const std::string &rfc_name_multiple) { @@ -284,6 +286,9 @@ int XrdHttpReq::parseLine(char *line, int len) { hdr2cgistr.append(it->second); hdr2cgistr.append("="); hdr2cgistr.append(s); + if (it->second == "authz") { + m_authz_header = true; + } } } @@ -976,6 +981,18 @@ int XrdHttpReq::ProcessHTTPReq() { m_appended_hdr2cgistr = true; } + if (!m_authz_header && prot->m_macaroon_authz && (!opaque || !opaque->Get("authz"))) { + auto authz = static_cast(prot->m_macaroon_authz); + std::string result; + if (authz->Sign(&(prot->SecEntity), resource.c_str(), opaque, result)) + { + const char *p = strchr(resourceplusopaque.c_str(), '?'); + resourceplusopaque.append(p ? "&" : "?"); + resourceplusopaque.append("authz=Bearer%20"); + resourceplusopaque.append(result.c_str()); + } + } + // Verify if we have an external handler for this request if (reqstate == 0) { XrdHttpExtHandler *exthandler = prot->FindMatchingExtHandler(*this); @@ -2724,6 +2741,7 @@ void XrdHttpReq::reset() { destination = ""; hdr2cgistr = ""; m_appended_hdr2cgistr = false; + m_authz_header = false; iovP = 0; iovN = 0; diff --git a/src/XrdHttp/XrdHttpReq.hh b/src/XrdHttp/XrdHttpReq.hh index 4d470d2a444..8004d5f1d0c 100644 --- a/src/XrdHttp/XrdHttpReq.hh +++ b/src/XrdHttp/XrdHttpReq.hh @@ -227,7 +227,8 @@ public: /// Additional opaque info that may come from the hdr2cgi directive std::string hdr2cgistr; bool m_appended_hdr2cgistr; - + bool m_authz_header; + // // Area for coordinating request and responses to/from the bridge // diff --git a/src/XrdMacaroons/XrdMacaroonsAuthz.cc b/src/XrdMacaroons/XrdMacaroonsAuthz.cc index 081a04ab876..1974bb7408b 100644 --- a/src/XrdMacaroons/XrdMacaroonsAuthz.cc +++ b/src/XrdMacaroons/XrdMacaroonsAuthz.cc @@ -4,6 +4,7 @@ #include +#include "uuid.h" #include "macaroons.h" #include "XrdOuc/XrdOucEnv.hh" @@ -55,6 +56,50 @@ class AuthzCheck time_t m_now; }; +// Given a list of privileges, generate a list of +// corresponding allowed activities. +static std::string GenerateActivities(XrdAccPrivs privs) +{ + std::stringstream result; + bool has_activities = false; + if ((privs & XrdAccPriv_Chmod) && (privs & XrdAccPriv_Chown)) { + result << "UPDATE_METADATA"; + has_activities = true; + } + if ((privs & XrdAccPriv_Insert) && (privs & XrdAccPriv_Lock) && (privs & XrdAccPriv_Mkdir) && + (privs & XrdAccPriv_Rename) && (privs & XrdAccPriv_Update)) + { + if (has_activities) result << ","; + result << "MANAGE"; + has_activities = true; + } + if (privs & XrdAccPriv_Create) { + if (has_activities) result << ","; + result << "UPLOAD"; + has_activities = true; + } + if (privs & XrdAccPriv_Delete) { + if (has_activities) result << ","; + result << "DELETE"; + has_activities = true; + } + if (privs & XrdAccPriv_Read) { + if (has_activities) result << ","; + result << "DOWNLOAD"; + has_activities = true; + } + if (privs & XrdAccPriv_Readdir) { + if (has_activities) result << ","; + result << "LIST"; + has_activities = true; + } + if (privs & XrdAccPriv_Lookup) { + if (has_activities) result << ","; + result << "READ_METADATA"; + has_activities = true; + } + return "activity:" + result.str(); +} static XrdAccPrivs AddPriv(Access_Operation op, XrdAccPrivs privs) { @@ -104,6 +149,36 @@ static XrdAccPrivs AddPriv(Access_Operation op, XrdAccPrivs privs) } +std::string +Authz::GenerateID(const std::string &resource, + const XrdSecEntity &entity, + const std::string &activities, + const std::string &before) +{ + uuid_t uu; + uuid_generate_random(uu); + char uuid_buf[37]; + uuid_unparse(uu, uuid_buf); + std::string result(uuid_buf); + + std::stringstream ss; + ss << "ID=" << result << ", "; + ss << "resource=" << resource << ", "; + if (entity.prot[0] != '\0') {ss << "protocol=" << entity.prot << ", ";} + if (entity.name) {ss << "name=" << entity.name << ", ";} + if (entity.host) {ss << "host=" << entity.host << ", ";} + if (entity.vorg) {ss << "vorg=" << entity.vorg << ", ";} + if (entity.role) {ss << "role=" << entity.role << ", ";} + if (entity.grps) {ss << "groups=" << entity.grps << ", ";} + if (entity.endorsements) {ss << "endorsements=" << entity.endorsements << ", ";} + if (activities.size()) {ss << "base_activities=" << activities << ", ";} + + ss << "expires=" << before; + + m_log.Emsg("MacaroonGen", ss.str().c_str()); + return result; +} + Authz::Authz(XrdSysLogger *log, char const *config, XrdAccAuthorize *chain) : m_max_duration(86400), @@ -137,6 +212,107 @@ Authz::OnMissing(const XrdSecEntity *Entity, const char *path, return XrdAccPriv_None; } +bool Authz::Sign(const XrdSecEntity *Entity, const char *path, + XrdOucEnv *env, std::string &result) +{ + XrdAccPrivs privs = Access(Entity, path, AOP_Any, env); + if (privs == XrdAccPriv_None) {return false;} + + auto activities = GenerateActivities(privs); + + time_t now; + time(&now); + now += 60; + + char utc_time_buf[21]; + if (!strftime(utc_time_buf, 21, "%FT%TZ", gmtime(&now))) + { + return false; + } + std::string utc_time_str(utc_time_buf); + std::stringstream ss; + ss << "before:" << utc_time_str; + std::string utc_time_caveat = ss.str(); + + + std::string macaroon_id = GenerateID(path, *Entity, activities, utc_time_str); + enum macaroon_returncode mac_err; + + struct macaroon *mac = macaroon_create(reinterpret_cast(m_location.c_str()), + m_location.size(), + reinterpret_cast(m_secret.c_str()), + m_secret.size(), + reinterpret_cast(macaroon_id.c_str()), + macaroon_id.size(), &mac_err); + if (!mac) { + return false; + } + + // Embed the SecEntity name, if present. + struct macaroon *mac_with_name; + const char * sec_name = Entity->name; + if (sec_name) { + std::stringstream name_caveat_ss; + name_caveat_ss << "name:" << sec_name; + std::string name_caveat = name_caveat_ss.str(); + mac_with_name = macaroon_add_first_party_caveat(mac, + reinterpret_cast(name_caveat.c_str()), + name_caveat.size(), + &mac_err); + macaroon_destroy(mac); + } else { + mac_with_name = mac; + } + if (!mac_with_name) + { + return false; + } + + struct macaroon *mac_with_activities = macaroon_add_first_party_caveat(mac_with_name, + reinterpret_cast(activities.c_str()), + activities.size(), + &mac_err); + macaroon_destroy(mac_with_name); + if (!mac_with_activities) + { + return false; + } + + std::string path_caveat = "path:"; + path_caveat += path; + struct macaroon *mac_with_path = macaroon_add_first_party_caveat(mac_with_activities, + reinterpret_cast(path_caveat.c_str()), + path_caveat.size(), + &mac_err); + macaroon_destroy(mac_with_activities); + if (!mac_with_path) { + return false; + } + + struct macaroon *mac_with_date = macaroon_add_first_party_caveat(mac_with_path, + reinterpret_cast(utc_time_caveat.c_str()), + utc_time_caveat.size(), + &mac_err); + macaroon_destroy(mac_with_path); + if (!mac_with_date) { + return false; + } + + size_t size_hint = macaroon_serialize_size_hint(mac_with_date); + + std::vector macaroon_resp; macaroon_resp.reserve(size_hint); + if (macaroon_serialize(mac_with_date, &macaroon_resp[0], size_hint, &mac_err)) + { + return false; + } + macaroon_destroy(mac_with_date); + + result = &macaroon_resp[0]; + + return true; +} + + XrdAccPrivs Authz::Access(const XrdSecEntity *Entity, const char *path, const Access_Operation oper, XrdOucEnv *env) diff --git a/src/XrdMacaroons/XrdMacaroonsAuthz.hh b/src/XrdMacaroons/XrdMacaroonsAuthz.hh index 4a9ac3d251d..aff74c1206a 100644 --- a/src/XrdMacaroons/XrdMacaroonsAuthz.hh +++ b/src/XrdMacaroons/XrdMacaroonsAuthz.hh @@ -20,6 +20,11 @@ public: const Access_Operation oper, XrdOucEnv *env); + bool Sign(const XrdSecEntity *Entity, + const char *path, + XrdOucEnv *env, + std::string &result); + virtual int Audit(const int accok, const XrdSecEntity *Entity, const char *path, const Access_Operation oper, XrdOucEnv *Env) @@ -39,6 +44,12 @@ private: const Access_Operation oper, XrdOucEnv *env); + std::string + GenerateID(const std::string &resource, + const XrdSecEntity &entity, + const std::string &activities, + const std::string &before); + ssize_t m_max_duration; XrdAccAuthorize *m_chain; XrdSysError m_log; diff --git a/src/XrdTpc/XrdTpcTPC.cc b/src/XrdTpc/XrdTpcTPC.cc index 988c9b8ebbf..1ec804c8593 100644 --- a/src/XrdTpc/XrdTpcTPC.cc +++ b/src/XrdTpc/XrdTpcTPC.cc @@ -39,12 +39,16 @@ XrdVERSIONINFO(XrdHttpGetExtHandler, HttpTPC); // header (which will later be used for XrdSecEntity). The latter copy is only done if // the Authorization header is not already present. static std::string prepareURL(XrdHttpExtReq &req) { - std::map::const_iterator iter = req.headers.find("xrd-http-query"); + std::map::const_iterator iter = req.headers.find("xrd-http-fullresource"); if (iter == req.headers.end() || iter->second.empty()) {return req.resource;} auto has_authz_header = req.headers.find("Authorization") != req.headers.end(); - std::istringstream requestStream(iter->second); + const char *query_start = strchr(iter->second.c_str(), '?'); + if (!query_start) {query_start = "";} + else {query_start++;} + + std::istringstream requestStream(query_start); std::string token; std::stringstream result; bool found_first_header = false; @@ -184,7 +188,9 @@ std::string TPCHandler::GetAuthz(XrdHttpExtReq &req) { ss << "authz=" << quoted_url; free(quoted_url); authz = ss.str(); + return authz; } + return authz; }