nix-community / bundix

Generates a Nix expression for your Bundler-managed application. [maintainer=@manveru]
163 stars 55 forks source link

Fetching private gems using bundler's credentials #61

Open burke opened 5 years ago

burke commented 5 years ago

So a few months back, I added support for bundix to look up bundler credentials to fetch gems requiring authentication. Today I took a run at teaching nix to do this without the runtime assistance of bundix, and this is what I came up with. It's pretty rough but Technically Works™.

Anyone know how I could do this better? Is there a form I could shape this into that would make sense to merge into bundix?

I'm happy to pour a bunch of work into this if someone can point me in the right direction.

I'm going to want to come up with similar solutions for gems fetched from private git remotes.

# gemset.nix
let

  hostnameFromURL = url:
    builtins.elemAt (
      builtins.match "https?://([^/]+)/.*" url # [ "packages.shopify.io" ]
    ) 0; # "packages.shopify.io"

  bundlerVarFromHostname = hostname:
    with import <nixpkgs> { }; "BUNDLE_" + lib.toUpper (
      builtins.replaceStrings ["."] ["__"] hostname # "packages__shopify__io"
    ); # "BUNDLE_PACKAGES__SHOPIFY__IO"

  tokenFromHomeBundlerConfig = var:
    if   builtins.getEnv var != ""
    then builtins.getEnv var
    else builtins.elemAt (
      builtins.match ".*\n${var}: ([^\n]+)\n.*" (
        builtins.readFile ((builtins.getEnv "HOME") + "/.bundle/config")
      )
    ) 0;

  netrcEntryForGemURL = url:
    let
      hostname = hostnameFromURL url;
      var = bundlerVarFromHostname hostname;
      token = tokenFromHomeBundlerConfig var;
    in
      "machine ${hostname}\n\tlogin ${token}\n";

  fetchurlWithBundlerAuthentication = { url, sha256 }:
    let
      entry = netrcEntryForGemURL url;
    in
      with import <nixpkgs> {}; fetchurl {
        url = url;
        sha256 = sha256;

        netrcPhase = ''
          cat > netrc <<EOF
          ${entry}
          EOF
        '';
      };

in

{
  abc = {
    dependencies = ["def"];
    groups = ["development" "test"];
    platforms = [];
    source = {
      remotes = ["https://packages.shopify.io/shopify/gems"];
      sha256 = "0000000000000000000000000000000000000000000000000001";
      type = "gem";
    };
    src = fetchurlWithBundlerAuthentication {
      url = "https://packages.shopify.io/shopify/gems/gems/abc-0.0.1.gem";
      sha256 = "0000000000000000000000000000000000000000000000000001";
    };
    version = "0.0.1";
  };
}
burke commented 5 years ago

Hm, I refined this to:

let

  hostnameFromURL = url: # String -> String
    builtins.elemAt (
      builtins.match "https?://([^/]+)/.*" url # [ "packages.shopify.io" ]
    ) 0; # "packages.shopify.io"

  bundlerVarFromHostname = hostname: # String -> String
    with import <nixpkgs> { }; "BUNDLE_" + lib.toUpper (
      builtins.replaceStrings ["."] ["__"] hostname # "packages__shopify__io"
    ); # "BUNDLE_PACKAGES__SHOPIFY__IO"

  lookUpBundlerConfig = var: # String -> String
    if   builtins.getEnv var != ""
    then builtins.getEnv var
    else builtins.elemAt (
      builtins.match ".*\n${var}: ([^\n]+)\n.*" (
        builtins.readFile ((builtins.getEnv "HOME") + "/.bundle/config")
      )
    ) 0;

  injectAuth = url: # String -> String
    let
      hostname = hostnameFromURL url;
      var = bundlerVarFromHostname hostname;
      token = lookUpBundlerConfig var;
    in
      builtins.replaceStrings ["://"] ["://${token}@"] url;

in

{
  abc = {
    dependencies = ["def"];
    groups = ["development" "test"];
    platforms = [];
    source = {
      remotes = [(injectAuth "https://packages.shopify.io/shopify/gems")];
      sha256 = "0000000000000000000000000000000000000000000000000001";
      type = "gem";
    };
    version = "0.0.1";
  };
}

This is probably progress in the right direction, but this feels like a weird place for this code to live.

zimbatm commented 5 years ago

One issue with this approach is that the secrets will be written to the /nix/store which is readable by all the users on the machine. What some people do is rotate the token frequently to limit the window of attack.

Another approach would be to change the fetchers to use buitlins.fetchurl and builtins.fetchgit instead. Those are executed at evaluation time and also read from the user's ~/.netrc by default. To do that you will probably have to introduce an extension to <nixpkgs/pkgs/development/ruby-modules/gem/default.nix> to allow to pass custom fetchers.

burke commented 5 years ago

Thanks. I like that idea. I'll play with that when I get a chance.

bugeats commented 2 years ago

I've got a Gemfile with private repos accessed over https. In a normal flow, I would just set bundler to use a personal access token like so:

bundle config GITHUB__COM <token>:x-oauth-basic

This of course doesn't work with bundix. I'm highly interested in finding a solution.