Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
36 changes: 25 additions & 11 deletions command/ca/provisioner/provisioner.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net"
"os"
"time"

"github.com/pkg/errors"
Expand Down Expand Up @@ -105,6 +106,15 @@ type crudClient interface {
func newCRUDClient(cliCtx *cli.Context, cfgFile string) (crudClient, error) {
unauthAdminClient, err := cautils.NewUnauthenticatedAdminClient(cliCtx)
if err != nil {
// Documented offline workflow: `step ca provisioner add ... --ca-config
// ca.json` edits the file locally and must not require --ca-url/--root
// when those flags are absent. Fall back only when the config file
// exists; otherwise keep the original admin-client error.
if cfgFile != "" {
if _, statErr := os.Stat(cfgFile); statErr == nil {
return newLocalCaConfigClient(cfgFile)
}
}
return nil, fmt.Errorf("error generating admin client: %w", err)
}

Expand All @@ -113,24 +123,28 @@ func newCRUDClient(cliCtx *cli.Context, cfgFile string) (crudClient, error) {
err = unauthAdminClient.IsEnabled()
switch {
case errors.As(err, &netErr) || errors.Is(err, ca.ErrAdminAPINotImplemented):
ui.PrintSelected("CA Configuration", cfgFile)
cfg, err := config.LoadConfiguration(cfgFile)
if err != nil {
return nil, fmt.Errorf("error loading configuration: %w", err)
}
// Assume the ca.json is already valid to avoid enabling all the
// features present in step-ca just to modify the provisioners.
cfg.SkipValidation = true

ui.Println()
return newCaConfigClient(context.Background(), cfg, cfgFile)
return newLocalCaConfigClient(cfgFile)
case errors.Is(err, ca.ErrAdminAPINotAuthorized):
return cautils.NewAdminClient(cliCtx)
default:
return nil, err
}
}

func newLocalCaConfigClient(cfgFile string) (crudClient, error) {
ui.PrintSelected("CA Configuration", cfgFile)
cfg, err := config.LoadConfiguration(cfgFile)
if err != nil {
return nil, fmt.Errorf("error loading configuration: %w", err)
}
// Assume the ca.json is already valid to avoid enabling all the
// features present in step-ca just to modify the provisioners.
cfg.SkipValidation = true

ui.Println()
return newCaConfigClient(context.Background(), cfg, cfgFile)
}

func parseInstanceAge(ctx *cli.Context) (age string, err error) {
if !ctx.IsSet("instance-age") {
return
Expand Down
52 changes: 52 additions & 0 deletions command/ca/provisioner/provisioner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@ package provisioner
import (
"crypto/ed25519"
"crypto/rand"
"flag"
"fmt"
"net/netip"
"os"
"path/filepath"
"testing"
"time"

nebula "github.com/slackhq/nebula/cert"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli"
)

func TestReadNebulaRoots(t *testing.T) {
Expand Down Expand Up @@ -95,3 +99,51 @@ func serializeAndWriteNebulaCert(t *testing.T, tempDir string, cert nebula.Certi

return file.Name(), data
}

func TestNewCRUDClient_CaConfigWithoutCaURL(t *testing.T) {
t.Parallel()

dir := t.TempDir()
cfgFile := filepath.Join(dir, "ca.json")
// Paths need not exist: SkipValidation is set before authority.New.
cfg := fmt.Sprintf(`{
"root": %q,
"crt": %q,
"key": %q,
"address": ":9000",
"dnsNames": ["localhost"],
"authority": {
"provisioners": []
}
}
`, filepath.Join(dir, "root.crt"), filepath.Join(dir, "intermediate.crt"), filepath.Join(dir, "intermediate.key"))
require.NoError(t, os.WriteFile(cfgFile, []byte(cfg), 0o600))

app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
_ = set.String("ca-url", "", "")
_ = set.String("root", "", "")
_ = set.String("ca-config", cfgFile, "")
ctx := cli.NewContext(app, set, nil)

client, err := newCRUDClient(ctx, cfgFile)
require.NoError(t, err)
require.NotNil(t, client)
_, ok := client.(*caConfigClient)
require.True(t, ok, "expected local caConfigClient when --ca-config exists and --ca-url is unset")
}

func TestNewCRUDClient_MissingCaURLWithoutConfig(t *testing.T) {
t.Parallel()

app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
_ = set.String("ca-url", "", "")
_ = set.String("root", "", "")
ctx := cli.NewContext(app, set, nil)

client, err := newCRUDClient(ctx, "")
require.Error(t, err)
require.Nil(t, client)
require.Contains(t, err.Error(), "ca-url")
}