Skip to content

Commit

Permalink
add "setup-keyring" subcommand
Browse files Browse the repository at this point in the history
This command can be used to store secrets in the system keyring.

Examples:

  $ echo -ne "user:pass" | credential-helper setup-keyring tweag-credential-helper:remoteapis
  $ credential-helper setup-keyring --file github_secret.txt gh:github.com
  • Loading branch information
malt3 committed Feb 22, 2025
1 parent ce71a1f commit ab7af50
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 2 deletions.
16 changes: 16 additions & 0 deletions agent/locate/locate.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ import (
// environment variables to ensure
// a consistent working environment.
func SetupEnvironment() error {
originalWorkingDirectory, err := os.Getwd()
if err != nil {
return err
}
if err := os.Setenv(api.OriginalWorkingDirectoryEnv, originalWorkingDirectory); err != nil {
return err
}

workspacePath, err := setupWorkspaceDirectory()
if err != nil {
return err
Expand Down Expand Up @@ -111,6 +119,14 @@ func AgentPaths() (string, string) {
return socketPath, pidPath
}

func RemapToOriginalWorkingDirectory(p string) string {
if filepath.IsAbs(p) {
return p
}
prefix := os.Getenv(api.OriginalWorkingDirectoryEnv)
return filepath.Join(prefix, p)
}

func tmpDir() string {
// In Bazel integration tests we can't (and shouldn't)
// touch /tmp
Expand Down
8 changes: 8 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ type Cache interface {
Prune(context.Context) error
}

// URISetupper is an optional interface that can be implemented by helpers to perform setup for a given URI.
type URISetupper interface {
SetupInstructionsForURI(uri string) string
}

var CacheMiss = errors.New("cache miss")

// Environment variable names used by the credential helper.
Expand All @@ -90,6 +95,9 @@ const (
WorkdirEnv = "CREDENTIAL_HELPER_WORKDIR"
// The working directory of Bazel (path containing root module).
WorkspaceEnv = "CREDENTIAL_HELPER_WORKSPACE_DIRECTORY"
// The cwd of the helper process before it is chdir'd into the workdir.
// This is used to resolve relative paths for some CLI commands.
OriginalWorkingDirectoryEnv = "CREDENTIAL_HELPER_ORIGINAL_WORKING_DIRECTORY"
)

// Placeholders in configuration that is expanded automatically.
Expand Down
1 change: 1 addition & 0 deletions cmd/root/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ go_library(
"//api",
"//cache",
"//cmd/installer",
"//cmd/setup",
"//config",
"//logging",
],
Expand Down
10 changes: 8 additions & 2 deletions cmd/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@ import (
"github.com/tweag/credential-helper/api"
"github.com/tweag/credential-helper/cache"
"github.com/tweag/credential-helper/cmd/installer"
"github.com/tweag/credential-helper/cmd/setup"
"github.com/tweag/credential-helper/config"
"github.com/tweag/credential-helper/logging"
)

const usage = `Usage:
credential-helper get`
const usage = `Usage: credential-helper [COMMAND] [ARGS...]
Commands:
get get credentials in the form of http headers for the uri provided on stdin and print result to stdout (see https://github.com/EngFlow/credential-helper-spec for more information)
setup-keyring stores a secret in the system keyring`

func Run(ctx context.Context, helperFactory api.HelperFactory, newCache api.NewCache, args []string) {
setLogLevel()
Expand All @@ -40,6 +44,8 @@ func Run(ctx context.Context, helperFactory api.HelperFactory, newCache api.NewC
switch command {
case "get":
clientProcess(ctx, helperFactory)
case "setup-keyring":
setup.KeyringProcess(args[2:])
case "agent-launch":
agentProcess(ctx, newCache)
case "agent-shutdown":
Expand Down
12 changes: 12 additions & 0 deletions cmd/setup/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
load("@rules_go//go:def.bzl", "go_library")

go_library(
name = "setup",
srcs = ["keyring.go"],
importpath = "github.com/tweag/credential-helper/cmd/setup",
visibility = ["//visibility:public"],
deps = [
"//agent/locate",
"@com_github_zalando_go_keyring//:go-keyring",
],
)
111 changes: 111 additions & 0 deletions cmd/setup/keyring.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package setup

import (
"flag"
"fmt"
"io"
"os"
"strings"

"github.com/tweag/credential-helper/agent/locate"
keyring "github.com/zalando/go-keyring"
)

// KeyringProcess is the entry point for the setup-keyring command.
// It takes a string key (name of the secret) and a string value (the secret itself) and stores it in the keyring.
func KeyringProcess(args []string) {
var sourceFilePath string
var read bool
var clear bool

flagSet := flag.NewFlagSet("setup-keyring", flag.ExitOnError)
flagSet.Usage = func() {
fmt.Fprintf(flagSet.Output(), "Stores a secret read from a file or stdin in the system keyring under the name specified by service.\n\n")
fmt.Fprintf(flagSet.Output(), "Usage: credential-helper setup-keyring [--file file] [--read] [service]\n")
flagSet.PrintDefaults()
examples := []string{
"credential-helper setup-keyring gh:github.com < secret.txt",
"credential-helper setup-keyring --clear gh:github.com",

"credential-helper setup-keyring --file secret.txt tweag-credential-helper:remoteapis",
"credential-helper setup-keyring --read tweag-credential-helper:buildbuddy_api_key",
}
fmt.Fprintf(flagSet.Output(), "\nExamples:\n")
for _, example := range examples {
fmt.Fprintf(flagSet.Output(), " $ %s\n", example)
}
os.Exit(1)
}
flagSet.StringVar(&sourceFilePath, "file", "", "File to read the secret from")
flagSet.BoolVar(&read, "read", false, "Print the current secret stored in the keyring for this service to stdout and exit")
flagSet.BoolVar(&clear, "clear", false, "Clear the secret stored in the keyring for this service and exit")

if err := flagSet.Parse(args); err != nil {
fatalFmt("parsing flags for setup-keyring: %v", err)
}

if flagSet.NArg() != 1 {
flagSet.Usage()
}

service := flagSet.Arg(0)

if read && clear {
fatalFmt("cannot specify both --read and --clear")
}
if read {
secret, err := keyring.Get(service, "")
if err != nil {
fatalFmt("reading secret from keyring: %v", err)
}
fmt.Print(secret)
return
}
if clear {
if err := keyring.Delete(service, ""); err != nil {
fatalFmt("deleting secret from keyring: %v", err)
}
fmt.Printf("Cleared secret %s\n", service)
return
}

var sourceFile io.Reader
if len(sourceFilePath) > 0 {
// the credential-helper process changes it's own working directory
// during setup.
// This remapping is necessary to find original, relative paths.
sourceFilePath = locate.RemapToOriginalWorkingDirectory(sourceFilePath)
var openErr error
sourceFile, openErr = os.OpenFile(sourceFilePath, os.O_RDONLY, 0)
if openErr != nil {
fatalFmt("opening source file %s: %v", sourceFilePath, openErr)
}
} else {
// use stdin as source
sourceFile = os.Stdin
}

secret, err := io.ReadAll(sourceFile)
if err != nil {
sourceName := sourceFilePath
if sourceName == "" {
sourceName = "stdin"
fmt.Fprintf(os.Stderr, "Reading secret from stdin.\n")
}
fatalFmt("reading secret from %s: %v", sourceName, err)
}

if err := keyring.Set(service, "", string(secret)); err != nil {
fatalFmt("storing secret in keyring: %v", err)
}

fmt.Printf("Stored secret %s\n", service)
}

func fatalFmt(format string, args ...any) {
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
fmt.Fprintf(os.Stderr, format, args...)
os.Exit(1)
}

0 comments on commit ab7af50

Please sign in to comment.