Skip to content

Commit

Permalink
add support for tokens (#35)
Browse files Browse the repository at this point in the history
Allow users to provide tokens for authentication with the remote image registry.

* add `--token` and `--username` flags to `uenv image pull`
* use the current user's username as the default if `--token` is set, but `--username` is not
* add support for optional credentials to all oras calls

fixes #34
  • Loading branch information
bcumming authored Dec 16, 2024
1 parent 10e7781 commit e8161c0
Show file tree
Hide file tree
Showing 10 changed files with 210 additions and 79 deletions.
77 changes: 72 additions & 5 deletions src/cli/pull.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ void image_pull_args::add_cli(CLI::App& cli,
->add_option("uenv", uenv_description,
"the uenv to pull, either name/version:tag, sha256 or id")
->required();
pull_cli->add_option(
"--token", token,
"a path that contains a TOKEN file for accessing restricted uenv");
pull_cli->add_option("--username", username,
"user name for accessing restricted uenv.");
pull_cli->add_flag("--only-meta", only_meta, "only download meta data");
pull_cli->add_flag("--force", force,
"download and overwrite existing images");
Expand All @@ -60,6 +65,59 @@ int image_pull([[maybe_unused]] const image_pull_args& args,
return 1;
}

// check whether a token has been provided via --token
// if it has been provided:
// - check that it exists
// - check that the current user has read access
std::optional<fs::path> token_path;
if (args.token) {
fs::path tp{args.token.value()};
if (!fs::exists(tp)) {
term::error("the token '{}' is not a path or file.", tp.string());
return 1;
}

if (fs::is_directory(tp)) {
tp = tp / "TOKEN";
if (!fs::exists(tp)) {
term::error("the token file '{}' does not exist.", tp.string());
return 1;
}
}

if (util::file_access_level(tp) < util::file_level::readonly) {
term::error(
"you do not have permission to read the token file '{}'",
tp.string());
return 1;
}

token_path = tp;
} else if (args.username) {
term::warn("ignoring the --username flag, which is only required "
"when used with the --token flag.");
}

std::optional<oras::credentials> credentials;
if (token_path) {
std::ifstream fid(*token_path);
std::string token{};
if (fid) {
std::getline(fid, token);
} else {
term::error("unable to read a valid token from '{}'",
token_path.value());
return 1;
}
auto username = args.username ? args.username : site::get_username();
if (!username) {
term::error("provide a username with --username for the --token.");
return 1;
}
credentials = {.username = username.value(), .token = token};
spdlog::info("using credentials {}", credentials);
}

// pull the search term that was provided by the user
uenv_label label{};
std::string nspace{site::default_namespace()};
Expand Down Expand Up @@ -95,7 +153,6 @@ int image_pull([[maybe_unused]] const image_pull_args& args,
term::error("invalid search term: {}", registry.error());
return 1;
}

// check that there is one record with a unique sha
if (remote_matches->empty()) {
using enum help::block::admonition;
Expand Down Expand Up @@ -159,7 +216,8 @@ int image_pull([[maybe_unused]] const image_pull_args& args,
auto rego_url = site::registry_url();
spdlog::debug("registry url: {}", rego_url);

auto digests = oras::discover(rego_url, nspace, record);
auto digests =
oras::discover(rego_url, nspace, record, credentials);
if (!digests || digests->empty()) {
term::error(
"unable to pull image - rerun with -vvv flag and send "
Expand All @@ -171,16 +229,16 @@ int image_pull([[maybe_unused]] const image_pull_args& args,
const auto digest = *(digests->begin());

if (auto okay = oras::pull_digest(rego_url, nspace, record, digest,
paths.store);
paths.store, credentials);
!okay) {
term::error(
"unable to pull image - rerun with -vvv flag and send "
"error report to service desk.\n");
return 1;
}

auto tag_result =
oras::pull_tag(rego_url, nspace, record, paths.store);
auto tag_result = oras::pull_tag(rego_url, nspace, record,
paths.store, credentials);
if (!tag_result) {
return 1;
}
Expand Down Expand Up @@ -221,6 +279,15 @@ std::string image_pull_footer() {
// clang-format off
help::block{none, "Download a uenv from a registry." },
help::linebreak{},
help::linebreak{},
help::block{xmpl, "pull a uenv"},
help::block{code, "uenv image pull prgenv-gnu"},
help::block{code, "uenv image pull prgenv-gnu/24.11:v1@todi"},
help::linebreak{},
help::block{xmpl, "use a token for the registry"},
help::block{code, "uenv image pull --token=/opt/cscs/uenv/tokens/vasp6 vasp/6.4.2:v1"},
help::block{note, "this is only required when accessing uenv that require special" },
help::block{none, "permission or a license to access." },
// clang-format on
};

Expand Down
7 changes: 5 additions & 2 deletions src/cli/pull.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once
// vim: ts=4 sts=4 sw=4 et

#include <optional>
#include <string>

#include <CLI/CLI.hpp>
Expand All @@ -14,6 +15,8 @@ namespace uenv {

struct image_pull_args {
std::string uenv_description;
std::optional<std::string> token;
std::optional<std::string> username;
bool only_meta = false;
bool force = false;
bool build = false;
Expand All @@ -35,7 +38,7 @@ template <> class fmt::formatter<uenv::image_pull_args> {
constexpr auto format(uenv::image_pull_args const& opts,
FmtContext& ctx) const {
return fmt::format_to(
ctx.out(), "(image pull {} .only_meta={} .force={})",
opts.uenv_description, opts.only_meta, opts.force);
ctx.out(), "(image pull {} .only_meta={} .force={} .token={})",
opts.uenv_description, opts.only_meta, opts.force, opts.token);
}
};
8 changes: 3 additions & 5 deletions src/cli/uenv.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ int main(int argc, char** argv) {
uenv::global_settings settings;
bool print_version = false;

// enable/disable color depending on NOCOLOR env. var
// and tty terminal status.
// enable/disable color depending on NOCOLOR env. var and tty terminal
// status.
color::default_color();

CLI::App cli(fmt::format("uenv {}", UENV_VERSION));
Expand Down Expand Up @@ -63,10 +63,8 @@ int main(int argc, char** argv) {

CLI11_PARSE(cli, argc, argv);

// color::set_color(!settings.no_color);

// By default there is no logging to the console
// user-friendly logging off errors and warnings is handled using
// user-friendly logging of errors and warnings is handled using
// term::error and term::warn
// The level of logging is increased by adding --verbose
spdlog::level::level_enum console_log_level = spdlog::level::off;
Expand Down
9 changes: 9 additions & 0 deletions src/site/site.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#include <unistd.h>

#include <optional>
#include <string>

Expand All @@ -13,6 +15,13 @@

namespace site {

std::optional<std::string> get_username() {
if (const auto name = getlogin()) {
return std::string{name};
}
return std::nullopt;
}

std::optional<std::string> get_system_name(std::optional<std::string> value) {
if (value) {
if (value == "*") {
Expand Down
2 changes: 2 additions & 0 deletions src/site/site.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ namespace site {
// on CSCS systems this is derived from the CLUSTER_NAME environment variable
std::optional<std::string> get_system_name(std::optional<std::string>);

std::optional<std::string> get_username();

// default namespace for image deployment
std::string default_namespace();

Expand Down
42 changes: 32 additions & 10 deletions src/uenv/oras.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
namespace uenv {
namespace oras {

// namespace fs = std::filesystem;
using opt_creds = std::optional<credentials>;

struct oras_output {
int rcode = -1;
Expand Down Expand Up @@ -60,13 +60,20 @@ run_oras_async(std::vector<std::string> args) {

util::expected<std::vector<std::string>, std::string>
discover(const std::string& registry, const std::string& nspace,
const uenv_record& uenv) {
const uenv_record& uenv, const opt_creds token) {
auto address =
fmt::format("{}/{}/{}/{}/{}/{}:{}", registry, nspace, uenv.system,
uenv.uarch, uenv.name, uenv.version, uenv.tag);

auto result = run_oras({"discover", "--format", "json", "--artifact-type",
"uenv/meta", address});
std::vector<std::string> args = {"discover", "--format", "json",
"--artifact-type", "uenv/meta", address};
if (token) {
args.push_back("--password");
args.push_back(token->token);
args.push_back("--username");
args.push_back(token->username);
}
auto result = run_oras(args);

if (result.rcode) {
spdlog::error("oras discover {}: {}", result.rcode, result.stderr);
Expand All @@ -91,14 +98,22 @@ discover(const std::string& registry, const std::string& nspace,
util::expected<void, int>
pull_digest(const std::string& registry, const std::string& nspace,
const uenv_record& uenv, const std::string& digest,
const std::filesystem::path& destination) {
const std::filesystem::path& destination, const opt_creds token) {
auto address =
fmt::format("{}/{}/{}/{}/{}/{}@{}", registry, nspace, uenv.system,
uenv.uarch, uenv.name, uenv.version, digest);

spdlog::debug("oras::pull_digest: {}", address);

auto proc = run_oras({"pull", "--output", destination.string(), address});
std::vector<std::string> args{"pull", "--output", destination.string(),
address};
if (token) {
args.push_back("--password");
args.push_back(token->token);
args.push_back("--username");
args.push_back(token->username);
}
auto proc = run_oras(args);

if (proc.rcode) {
spdlog::error("unable to pull digest with oras: {}", proc.stderr);
Expand All @@ -111,7 +126,8 @@ pull_digest(const std::string& registry, const std::string& nspace,
util::expected<void, int> pull_tag(const std::string& registry,
const std::string& nspace,
const uenv_record& uenv,
const std::filesystem::path& destination) {
const std::filesystem::path& destination,
const opt_creds token) {
using namespace std::chrono_literals;
namespace fs = std::filesystem;
namespace bk = barkeep;
Expand All @@ -121,9 +137,15 @@ util::expected<void, int> pull_tag(const std::string& registry,
uenv.uarch, uenv.name, uenv.version, uenv.tag);

spdlog::debug("oras::pull_tag: {}", address);

auto proc =
run_oras_async({"pull", "--output", destination.string(), address});
std::vector<std::string> args{"pull", "--output", destination.string(),
address};
if (token) {
args.push_back("--password");
args.push_back(token->token);
args.push_back("--username");
args.push_back(token->username);
}
auto proc = run_oras_async(args);

if (!proc) {
spdlog::error("unable to pull tag with oras: {}", proc.error());
Expand Down
45 changes: 35 additions & 10 deletions src/uenv/oras.h
Original file line number Diff line number Diff line change
@@ -1,29 +1,54 @@
#pragma once

#include <filesystem>
#include <string>
#include <vector>

#include <fmt/core.h>

#include <uenv/uenv.h>

namespace uenv {
namespace oras {

struct credentials {
std::string username;
std::string token;
};

bool pull(std::string rego, std::string nspace);

util::expected<std::vector<std::string>, std::string>
discover(const std::string& registry, const std::string& nspace,
const uenv_record& uenv);
const uenv_record& uenv,
const std::optional<credentials> token = std::nullopt);

util::expected<void, int> pull_digest(const std::string& registry,
const std::string& nspace,
const uenv_record& uenv,
const std::string& digest,
const std::filesystem::path& destination);
util::expected<void, int>
pull_digest(const std::string& registry, const std::string& nspace,
const uenv_record& uenv, const std::string& digest,
const std::filesystem::path& destination,
const std::optional<credentials> token = std::nullopt);

util::expected<void, int> pull_tag(const std::string& registry,
const std::string& nspace,
const uenv_record& uenv,
const std::filesystem::path& destination);
util::expected<void, int>
pull_tag(const std::string& registry, const std::string& nspace,
const uenv_record& uenv, const std::filesystem::path& destination,
const std::optional<credentials> token = std::nullopt);

} // namespace oras
} // namespace uenv

template <> class fmt::formatter<uenv::oras::credentials> {
public:
// parse format specification and store it:
constexpr auto parse(format_parse_context& ctx) {
return ctx.end();
}
// format a value using stored specification:
template <typename FmtContext>
constexpr auto format(uenv::oras::credentials const& c,
FmtContext& ctx) const {
// replace token characters with 'X'
return fmt::format_to(ctx.out(), "{{username: {}, token: {:X>{}}}}",
c.username, "", c.token.size());
}
};
Loading

0 comments on commit e8161c0

Please # to comment.