Skip to content
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

feat: CLI generate token from JVS server #70

Merged
merged 18 commits into from
Jun 17, 2022
Merged
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
4 changes: 3 additions & 1 deletion pkg/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ func init() {
cobra.OnInitialize(initCfg)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.jvsctl/config.yaml)")
rootCmd.PersistentFlags().String("server", "", "overwrite the JVS server address")
viper.BindPFlag("server", rootCmd.PersistentFlags().Lookup("server")) //nolint // not expect err
rootCmd.PersistentFlags().Bool("insecure", false, "use insecure connection to JVS server")
viper.BindPFlag("server", rootCmd.PersistentFlags().Lookup("server")) //nolint // not expect err
viper.BindPFlag("insecure", rootCmd.PersistentFlags().Lookup("insecure")) //nolint // not expect err

rootCmd.AddCommand(tokenCmd)
}
Expand Down
5 changes: 3 additions & 2 deletions pkg/cli/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ func TestInitCfg(t *testing.T) {
initCfg()

wantCfg := &config.CLIConfig{
Version: 1,
Server: "https://example.com",
Version: 1,
Server: "https://example.com",
Authentication: &config.CLIAuthentication{},
}
if diff := cmp.Diff(wantCfg, cfg); diff != "" {
t.Errorf("CLI config loaded (-want,+got):\n%s", diff)
Expand Down
78 changes: 77 additions & 1 deletion pkg/cli/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,21 @@
package cli

import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"time"

"github.com/spf13/cobra"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/credentials/oauth"
"google.golang.org/protobuf/types/known/durationpb"

jvsapis "github.com/abcxyz/jvs/apis/v0"
"github.com/abcxyz/jvs/pkg/idtoken"
)

var (
Expand All @@ -35,7 +46,38 @@ var tokenCmd = &cobra.Command{
}

func runTokenCmd(cmd *cobra.Command, args []string) error {
return fmt.Errorf("not implemented")
ctx := context.Background()
dialOpts, err := dialOpts()
if err != nil {
return err
}
callOpts, err := callOpts(ctx)
if err != nil {
return err
}

// TODO(#69): Generate breakglass token w/o JVS server.

conn, err := grpc.Dial(cfg.Server, dialOpts...)
if err != nil {
return fmt.Errorf("failed to connect to JVS service: %w", err)
}
jvsclient := jvsapis.NewJVSServiceClient(conn)

req := &jvsapis.CreateJustificationRequest{
Justifications: []*jvsapis.Justification{{
Category: "explanation",
Value: tokenExplanation,
}},
Ttl: durationpb.New(ttl),
raserva marked this conversation as resolved.
Show resolved Hide resolved
}
resp, err := jvsclient.CreateJustification(ctx, req, callOpts...)
if err != nil {
return err
}

_, err = cmd.OutOrStdout().Write([]byte(resp.Token))
return err
}

func init() {
Expand All @@ -44,3 +86,37 @@ func init() {
tokenCmd.Flags().BoolVar(&breakglass, "breakglass", false, "Whether it will be a breakglass action")
tokenCmd.Flags().DurationVar(&ttl, "ttl", time.Hour, "The token time-to-live duration")
}

func dialOpts() ([]grpc.DialOption, error) {
if cfg.Authentication.Insecure {
return []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}, nil
}

// The default.
systemRoots, err := x509.SystemCertPool()
if err != nil {
return nil, fmt.Errorf("failed to load system cert pool: %w", err)
}
//nolint:gosec // We need to support TLS 1.2 for now (G402).
cred := credentials.NewTLS(&tls.Config{
RootCAs: systemRoots,
})
return []grpc.DialOption{grpc.WithTransportCredentials(cred)}, nil
}

func callOpts(ctx context.Context) ([]grpc.CallOption, error) {
if cfg.Authentication.Insecure {
return nil, nil
}

ts, err := idtoken.FromDefaultCredentials(ctx)
if err != nil {
return nil, err
}

token, err := ts.Token()
if err != nil {
return nil, err
}
return []grpc.CallOption{grpc.PerRPCCredentials(oauth.NewOauthAccess(token))}, nil
}
110 changes: 106 additions & 4 deletions pkg/cli/token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,117 @@
package cli

import (
"context"
"fmt"
"net"
"strings"
"testing"
"time"

"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"

jvsapis "github.com/abcxyz/jvs/apis/v0"
"github.com/abcxyz/jvs/pkg/config"
"github.com/abcxyz/pkg/testutil"
"github.com/spf13/cobra"
)

type fakeJVS struct {
jvsapis.UnimplementedJVSServiceServer
returnErr error
}

func (j *fakeJVS) CreateJustification(_ context.Context, req *jvsapis.CreateJustificationRequest) (*jvsapis.CreateJustificationResponse, error) {
if j.returnErr != nil {
return nil, j.returnErr
}

if req.Justifications[0].Category != "explanation" {
return nil, fmt.Errorf("unexpected category: %q", req.Justifications[0].Category)
}

return &jvsapis.CreateJustificationResponse{
Token: fmt.Sprintf("tokenized(%s);ttl=%v", req.Justifications[0].Value, req.Ttl.AsDuration()),
}, nil
}

func TestRunTokenCmd(t *testing.T) {
err := runTokenCmd(nil, nil)
wantErrStr := "not implemented"
if diff := testutil.DiffErrString(err, wantErrStr); diff != "" {
t.Errorf("unexpected error: %s", diff)
t.Parallel()

tests := []struct {
name string
jvs *fakeJVS
explanation string
wantToken string
wantErr string
}{{
name: "success",
jvs: &fakeJVS{},
explanation: "i-have-reason",
wantToken: fmt.Sprintf("tokenized(i-have-reason);ttl=%v", time.Minute),
}, {
name: "error",
jvs: &fakeJVS{returnErr: fmt.Errorf("server err")},
explanation: "i-have-reason",
wantErr: "server err",
}}

for _, tc := range tests {
// Cannot parallel because the global CLI config.
t.Run(tc.name, func(t *testing.T) {
server, _ := testFakeGRPCServer(t, func(s *grpc.Server) { jvsapis.RegisterJVSServiceServer(s, tc.jvs) })

// These are global flags.
cfg = &config.CLIConfig{
Server: server,
Authentication: &config.CLIAuthentication{
Insecure: true,
},
}
tokenExplanation = tc.explanation
ttl = time.Minute

buf := &strings.Builder{}
cmd := &cobra.Command{}
cmd.SetOut(buf)

err := runTokenCmd(cmd, nil)
if diff := testutil.DiffErrString(err, tc.wantErr); diff != "" {
t.Errorf("unexpected err: %s", diff)
}

if gotToken := buf.String(); gotToken != tc.wantToken {
t.Errorf("justification token got=%q, want=%q", gotToken, tc.wantToken)
}
})
}
}

// Copied over from Lumberjack. TODO: share it in pkg.
func testFakeGRPCServer(tb testing.TB, registerFunc func(*grpc.Server)) (string, *grpc.ClientConn) {
tb.Helper()

s := grpc.NewServer()
tb.Cleanup(func() { s.GracefulStop() })

registerFunc(s)

lis, err := net.Listen("tcp", "localhost:0")
if err != nil {
tb.Fatalf("net.Listen(tcp, localhost:0) failed: %v", err)
}

go func() {
if err := s.Serve(lis); err != nil {
tb.Logf("net.Listen(tcp, localhost:0) serve failed: %v", err)
}
}()

addr := lis.Addr().String()
conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
tb.Fatalf("failed to dail %q: %s", addr, err)
}
return addr, conn
}
12 changes: 12 additions & 0 deletions pkg/config/cli_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ type CLIConfig struct {

// Server is the JVS server address.
Server string `yaml:"server,omitempty" env:"SERVER,overwrite"`

// Authentication is the authentication config.
Authentication *CLIAuthentication `yaml:"authentication,omitempty"`
}

// CLIAuthentication is the CLI authentication config.
type CLIAuthentication struct {
// Insecure indiates whether to use insecured connection to the JVS server.
Insecure bool `yaml:"insecure,omitempty"`
}

// Validate checks if the config is valid.
Expand All @@ -50,4 +59,7 @@ func (cfg *CLIConfig) SetDefault() {
if cfg.Version == 0 {
cfg.Version = DefaultCLIConfigVersion
}
if cfg.Authentication == nil {
cfg.Authentication = &CLIAuthentication{}
}
}
29 changes: 29 additions & 0 deletions pkg/config/cli_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"testing"

"github.com/abcxyz/pkg/testutil"
"github.com/google/go-cmp/cmp"
)

func TestValidateCLIConfig(t *testing.T) {
Expand Down Expand Up @@ -47,3 +48,31 @@ func TestValidateCLIConfig(t *testing.T) {
})
}
}

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

tests := []struct {
name string
cfg *CLIConfig
wantCfg *CLIConfig
}{{
name: "default_empty_authentication",
cfg: &CLIConfig{},
wantCfg: &CLIConfig{
Version: 1,
Authentication: &CLIAuthentication{},
},
}}

for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
tc.cfg.SetDefault()
if diff := cmp.Diff(tc.wantCfg, tc.cfg); diff != "" {
t.Errorf("config with defaults (-want,+got):\n%s", diff)
}
})
}
}