Skip to content

feat: Add Git auth for GitHub, GitLab, Azure DevOps, and BitBucket #4670

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Oct 25, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add configuration files and tests!
  • Loading branch information
kylecarbs committed Oct 20, 2022
commit 4c37c34abe46bf1b3553e32033f566bd5fcfba74
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@
"derphttp",
"derpmap",
"devel",
"devtunnel",
"dflags",
"drpc",
"drpcconn",
"drpcmux",
"drpcserver",
"Dsts",
"embeddedpostgres",
"enablements",
"errgroup",
"eventsourcemock",
"fatih",
"Formik",
Expand Down Expand Up @@ -80,6 +83,7 @@
"parameterscopeid",
"pqtype",
"prometheusmetrics",
"promhttp",
"promptui",
"protobuf",
"provisionerd",
Expand Down
3 changes: 2 additions & 1 deletion cli/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,9 @@ func workspaceAgent() *cobra.Command {
Logger: logger,
EnvironmentVariables: map[string]string{
// Override the "CODER_AGENT_TOKEN" variable in all
// shells so "gitssh" works!
// shells so "gitssh" and "gitaskpass" works!
"CODER_AGENT_TOKEN": client.SessionToken,
"GIT_ASKPASS": executablePath,
},
CoordinatorDialer: client.ListenWorkspaceAgentTailnet,
StatsReporter: client.AgentReportStats,
Expand Down
4 changes: 4 additions & 0 deletions cli/config/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ func (r Root) PostgresPort() File {
return File(filepath.Join(r.PostgresPath(), "port"))
}

func (r Root) ServerConfig() File {
return File(filepath.Join(string(r), "server.yaml"))
}

// File provides convenience methods for interacting with *os.File.
type File string

Expand Down
56 changes: 56 additions & 0 deletions cli/config/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package config

import (
"errors"
"net/url"
"os"

"github.com/spf13/cobra"
"gopkg.in/yaml.v3"

"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/coderd/gitauth"

_ "embed"
)

//go:embed server.yaml
var defaultServer string

// Server represents a parsed server configuration.
type Server struct {
GitAuth []*gitauth.Config
}

// ParseServer creates or consumes a server config by path.
// If one does not exist, it will create one. If it fails to create,
// a warning will appear but the server will not fail to start.
// This is to prevent blocking execution on readonly file-systems
// that didn't provide a default config.
func ParseServer(cmd *cobra.Command, accessURL *url.URL, path string) (*Server, error) {
_, err := os.Stat(path)
if errors.Is(err, os.ErrNotExist) {
err = os.WriteFile(path, []byte(defaultServer), 0600)
if err != nil {
cmd.Printf("%s Unable to write the default config file: %s", cliui.Styles.Warn.Render("Warning:"), err)
}
}
data, err := os.ReadFile(path)
if err != nil {
data = []byte(defaultServer)
}
var server struct {
GitAuth []*gitauth.YAML `yaml:"gitauth"`
}
err = yaml.Unmarshal(data, &server)
if err != nil {
return nil, err
}
configs, err := gitauth.ConvertYAML(server.GitAuth, accessURL)
if err != nil {
return nil, err
}
return &Server{
GitAuth: configs,
}, nil
}
25 changes: 25 additions & 0 deletions cli/config/server.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Coder Server Configuration

# Automatically authenticate HTTP(s) Git requests.
gitauth:
# Supported: azure-devops, bitbucket, github, gitlab
# - type: github
# client_id: xxxxxx
# client_secret: xxxxxx

# Multiple providers are an Enterprise feature.
# Contact sales@coder.com for a license.
#
# If multiple providers are used, a unique "id"
# must be provided for each one.
# - id: example
# type: azure-devops
# client_id: xxxxxxx
# client_secret: xxxxxxx
# A custom regex can be used to match a specific
# repository or organization to limit auth scope.
# regex: github.com/coder
# Custom authentication and token URLs should be
# used for self-managed Git provider deployments.
# auth_url: https://example.com/oauth/authorize
# token_url: https://example.com/oauth/token
40 changes: 40 additions & 0 deletions cli/config/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package config_test

import (
"net/url"
"os"
"path/filepath"
"testing"

"github.com/spf13/cobra"
"github.com/stretchr/testify/require"

"github.com/coder/coder/cli/config"
)

func TestServer(t *testing.T) {
t.Parallel()
t.Run("WritesDefault", func(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "server.yaml")
_, err := config.ParseServer(&cobra.Command{}, &url.URL{}, path)
require.NoError(t, err)
data, err := os.ReadFile(path)
require.NoError(t, err)
require.Greater(t, len(data), 0)
})
t.Run("Filled", func(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "server.yaml")
err := os.WriteFile(path, []byte(`
gitauth:
- type: github
client_id: xxx
client_secret: xxx
`), 0600)
require.NoError(t, err)
cfg, err := config.ParseServer(&cobra.Command{}, &url.URL{}, path)
require.NoError(t, err)
require.Len(t, cfg.GitAuth, 1)
})
}
6 changes: 6 additions & 0 deletions cli/deployment/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ func Flags() *codersdk.DeploymentFlags {
Hidden: true,
Default: time.Minute,
},
ConfigPath: &codersdk.StringFlag{
Name: "Configuration Path",
Flag: "config",
EnvVar: "CODER_SERVER_CONFIG",
Description: "Path to the Coder configuration file.",
},
DerpServerEnable: &codersdk.BoolFlag{
Name: "DERP Server Enabled",
Flag: "derp-server-enable",
Expand Down
71 changes: 71 additions & 0 deletions cli/gitaskpass.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package cli

import (
"fmt"
"os/signal"
"time"

"github.com/spf13/cobra"
"golang.org/x/xerrors"

"github.com/coder/coder/coderd/gitauth"
"github.com/coder/retry"
)

func gitAskpass() *cobra.Command {
return &cobra.Command{
Use: "gitaskpass",
Hidden: true,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) (err error) {
ctx := cmd.Context()

ctx, stop := signal.NotifyContext(ctx, interruptSignals...)
defer stop()

defer func() {
if ctx.Err() != nil {
err = ctx.Err()
}
}()

user, host, err := gitauth.ParseAskpass(args[0])
if err != nil {
return xerrors.Errorf("parse host: %w", err)
}

client, err := createAgentClient(cmd)
if err != nil {
return xerrors.Errorf("create agent client: %w", err)
}

token, err := client.WorkspaceAgentGitAuth(ctx, host, false)
if err != nil {
return xerrors.Errorf("get git token: %w", err)
}
if token.URL != "" {
cmd.Printf("Visit the following URL to authenticate with Git:\n%s\n", token.URL)
for r := retry.New(time.Second, 10*time.Second); r.Wait(ctx); {
token, err = client.WorkspaceAgentGitAuth(ctx, host, true)
if err != nil {
continue
}
cmd.Printf("\nYou've been authenticated with Git!\n")
break
}
}

if token.Password != "" {
if user == "" {
fmt.Fprintln(cmd.OutOrStdout(), token.Username)
} else {
fmt.Fprintln(cmd.OutOrStdout(), token.Password)
}
} else {
fmt.Fprintln(cmd.OutOrStdout(), token.Username)
}

return nil
},
}
}
1 change: 1 addition & 0 deletions cli/gitaskpass_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package cli_test
25 changes: 24 additions & 1 deletion cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/coder/coder/cli/config"
"github.com/coder/coder/cli/deployment"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/gitauth"
"github.com/coder/coder/codersdk"
)

Expand Down Expand Up @@ -109,12 +110,31 @@ func AGPL() []*cobra.Command {
}

func Root(subcommands []*cobra.Command) *cobra.Command {
// The GIT_ASKPASS environment variable must point at
// a binary with no arguments. To prevent writing
// cross-platform scripts to invoke the Coder binary
// with a `gitaskpass` subcommand, we override the entrypoint
// to check if the command was invoked.
isGitAskpass := false

cmd := &cobra.Command{
Use: "coder",
SilenceErrors: true,
SilenceUsage: true,
Long: `Coder — A tool for provisioning self-hosted development environments with Terraform.
`,
`, Args: func(cmd *cobra.Command, args []string) error {
if gitauth.CheckCommand(args, os.Environ()) {
isGitAskpass = true
return nil
}
return cobra.NoArgs(cmd, args)
},
RunE: func(cmd *cobra.Command, args []string) error {
if isGitAskpass {
return gitAskpass().RunE(cmd, args)
}
return cmd.Help()
},
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if cliflag.IsSetBool(cmd, varNoVersionCheck) &&
cliflag.IsSetBool(cmd, varNoFeatureWarning) {
Expand All @@ -134,6 +154,9 @@ func Root(subcommands []*cobra.Command) *cobra.Command {
if cmd.Name() == "login" || cmd.Name() == "server" || cmd.Name() == "agent" || cmd.Name() == "gitssh" {
return
}
if isGitAskpass {
return
}

client, err := CreateClient(cmd)
// If we are unable to create a client, presumably the subcommand will fail as well
Expand Down
19 changes: 14 additions & 5 deletions cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
// be interrupted by additional signals. Note that we avoid
// shadowing cancel() (from above) here because notifyStop()
// restores default behavior for the signals. This protects
// the shutdown sequence from abrubtly terminating things
// the shutdown sequence from abruptly terminating things
// like: database migrations, provisioner work, workspace
// cleanup in dev-mode, etc.
//
Expand Down Expand Up @@ -143,13 +143,13 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
}
}

config := createConfig(cmd)
cfg := createConfig(cmd)
builtinPostgres := false
// Only use built-in if PostgreSQL URL isn't specified!
if !dflags.InMemoryDatabase.Value && dflags.PostgresURL.Value == "" {
var closeFunc func() error
cmd.Printf("Using built-in PostgreSQL (%s)\n", config.PostgresPath())
dflags.PostgresURL.Value, closeFunc, err = startBuiltinPostgres(ctx, config, logger)
cmd.Printf("Using built-in PostgreSQL (%s)\n", cfg.PostgresPath())
dflags.PostgresURL.Value, closeFunc, err = startBuiltinPostgres(ctx, cfg, logger)
if err != nil {
return err
}
Expand Down Expand Up @@ -311,6 +311,14 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
}
}

if dflags.ConfigPath.Value == "" {
dflags.ConfigPath.Value = string(cfg.ServerConfig())
}
serverConfig, err := config.ParseServer(cmd, accessURLParsed, dflags.ConfigPath.Value)
if err != nil {
return xerrors.Errorf("parse server config: %w", err)
}

options := &coderd.Options{
AccessURL: accessURLParsed,
AppHostname: appHostname,
Expand All @@ -321,6 +329,7 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
Pubsub: database.NewPubsubInMemory(),
CacheDir: dflags.CacheDir.Value,
GoogleTokenValidator: googleTokenValidator,
GitAuthConfigs: serverConfig.GitAuth,
SecureAuthCookie: dflags.SecureAuthCookie.Value,
SSHKeygenAlgorithm: sshKeygenAlgorithm,
TracerProvider: tracerProvider,
Expand Down Expand Up @@ -602,7 +611,7 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
// This is helpful for tests, but can be silently ignored.
// Coder may be ran as users that don't have permission to write in the homedir,
// such as via the systemd service.
_ = config.URL().Write(client.URL.String())
_ = cfg.URL().Write(client.URL.String())

// Currently there is no way to ask the server to shut
// itself down, so any exit signal will result in a non-zero
Expand Down
3 changes: 2 additions & 1 deletion coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/awsidentity"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/gitauth"
"github.com/coder/coder/coderd/gitsshkey"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
Expand Down Expand Up @@ -83,7 +84,7 @@ type Options struct {
Telemetry telemetry.Reporter
TracerProvider trace.TracerProvider
AutoImportTemplates []AutoImportTemplate
GitAuthConfigs []*GitAuthConfig
GitAuthConfigs []*gitauth.Config

// TLSCertificates is used to mesh DERP servers securely.
TLSCertificates []tls.Certificate
Expand Down
Loading