diff --git a/cmd/notation/main.go b/cmd/notation/main.go index bddc03568..4195d958e 100644 --- a/cmd/notation/main.go +++ b/cmd/notation/main.go @@ -22,6 +22,7 @@ func main() { listCommand(nil), certCommand(), keyCommand(), + policyCommand(), cacheCommand(), pluginCommand(), loginCommand(nil), diff --git a/cmd/notation/policy.go b/cmd/notation/policy.go new file mode 100644 index 000000000..ea244cf8e --- /dev/null +++ b/cmd/notation/policy.go @@ -0,0 +1,379 @@ +package main + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/notaryproject/notation-go/dir" + "github.com/notaryproject/notation-go/verification" + "github.com/notaryproject/notation/internal/certificate" + "github.com/notaryproject/notation/internal/cmd" + "github.com/notaryproject/notation/internal/ioutil" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +type policyOpts struct { + configPath string + name string + scopes []string + level string + override string + stores []string + identities []string + certPath string +} + +type deleteOpts struct { + names []string + confirmed bool +} + +func policyCommand() *cobra.Command { + command := &cobra.Command{ + Use: "policy", + Short: "Manage trust policy for verification", + } + command.AddCommand( + policyListCommand(), + policyShowCommand(), + policyResolveCommand(), + policyDeleteCommand(nil), + policyUpdateCommand(nil), + policyAddCommand(nil), + ) + return command +} + +func policyListCommand() *cobra.Command { + return &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List all configured trust policies", + RunE: func(cmd *cobra.Command, args []string) error { + return listPolicies() + }, + } +} + +func policyShowCommand() *cobra.Command { + var policyName string + return &cobra.Command{ + Use: "show ", + Short: "Inspect a policy by name", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("no policy name specified") + } + policyName = args[0] + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return runShowCommand(policyName) + }, + } +} + +func policyResolveCommand() *cobra.Command { + var scope string + return &cobra.Command{ + Use: "resolve ", + Short: "Present policies under the given scope", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("no scope specified") + } + scope = args[0] + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return runResolveCommand(scope) + }, + } +} + +func policyDeleteCommand(opts *deleteOpts) *cobra.Command { + if opts == nil { + opts = &deleteOpts{} + } + command := &cobra.Command{ + Use: "delete ...", + Short: "Delete specified policies", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("no policy specified") + } + opts.names = append(opts.names, args...) + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return runDeleteCommand(opts) + }, + } + command.Flags().BoolVarP(&opts.confirmed, "confirm", "y", false, "if yes, do not prompt for confirmation") + + return command +} + +func policyUpdateCommand(opts *policyOpts) *cobra.Command { + if opts == nil { + opts = &policyOpts{} + } + command := &cobra.Command{ + Use: "update [] [|]", + Short: "Update existing policies", + Long: `Update existing policies +notation policy update path/to/config/file +notation policy update [--scope /] [--level-override =] [--trust-store :] [--identity ] + +Example - Update policies from a config file + notation policy update "/home/user/trustpolicy.json" + +Example - Update policy from options + notation policy update wabbit-networks-images \ + --scope registry.acme-rockets.io/software/net-monitor \ + --scope registry.acme-rockets.io/software/net-logger \ + --level strict \ + --trust-store ca:wabbit-networks \ + --identity "x509.subject: C=US, ST=WA, L=Seattle, O=wabbit-networks.io, OU=Security Tools"`, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("no policy name or file path provided") + } + opts.configPath = args[0] + return nil + }, + PreRun: func(cmd *cobra.Command, args []string) { + trimSpace(opts) + }, + RunE: func(cmd *cobra.Command, args []string) error { + return updatePolicy(opts) + }, + } + + opts.applyFlags(command.Flags()) + return command +} + +func policyAddCommand(opts *policyOpts) *cobra.Command { + if opts == nil { + opts = &policyOpts{} + } + command := &cobra.Command{ + Use: "add []", + Short: "Add new policies", + Long: `Add new policies +notation policy add path/to/config/file +notation policy add --name --scope / --level --level-override = --trust-store : --identity [--identity-cert ] + +Example - Add policies from a config file + notation policy add "/home/user/trustpolicy.json" + +Example - Add policy from options + notation policy add --name wabbit-networks-images \ + --scope registry.acme-rockets.io/software/net-monitor \ + --scope registry.acme-rockets.io/software/net-logger \ + --level strict \ + --trust-store ca:wabbit-networks \ + --identity "x509.subject: C=US, ST=WA, L=Seattle, O=wabbit-networks.io, OU=Security Tools" + --identity-cert acme_root.crt`, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 1 { + opts.configPath = args[0] + } else if len(args) > 1 { + fmt.Println("multiple files provided, only the first one will be honored") + } + return nil + }, + PreRun: func(cmd *cobra.Command, args []string) { + trimSpace(opts) + }, + RunE: func(cmd *cobra.Command, args []string) error { + return addPolicy(opts) + }, + } + + opts.applyFlags(command.Flags()) + command.Flags().StringVar(&opts.name, "name", "", "policy name") + command.Flags().StringVar(&opts.certPath, "identity-cert", "", "path to the certificate file") + return command +} + +func listPolicies() error { + policyDocument, err := verification.LoadDefaultPolicyDocument() + if err != nil { + return err + } + policies := policyDocument.ListPolicies() + + return ioutil.PrintPolicyMap(os.Stdout, policies) +} + +func runShowCommand(policyName string) error { + policyDocument, err := verification.LoadDefaultPolicyDocument() + if err != nil { + return err + } + + policy, err := policyDocument.GetPolicy(policyName) + if err != nil { + return err + } + + return ioutil.PrintPolicyMap(os.Stdout, []*verification.TrustPolicy{policy}) +} + +func runResolveCommand(scope string) error { + policyDocument, err := verification.LoadDefaultPolicyDocument() + if err != nil { + return err + } + + policies := policyDocument.ListPoliciesWithinScope(scope) + + return ioutil.PrintPolicyMap(os.Stdout, policies) +} + +func runDeleteCommand(opts *deleteOpts) error { + prompt := fmt.Sprintf("Are you sure you want to delete policies: %s ?", strings.Join(opts.names, ", ")) + confirmed, err := ioutil.AskForConfirmation(os.Stdin, prompt, opts.confirmed) + if err != nil { + return err + } + if !confirmed { + return nil + } + + policyDocument, err := verification.LoadDefaultPolicyDocument() + if err != nil { + return err + } + + if err = policyDocument.DeletePolicies(opts.names, dir.UserLevel); err != nil { + return err + } + + ioutil.PrintDeletedPolicyNames(os.Stdout, opts.names) + return nil +} + +func updatePolicy(opts *policyOpts) error { + opts.prepareForUpdate() + newPolicies, err := loadPoliciesFromOptions(opts) + if err != nil { + return err + } + + policyDocument, err := verification.LoadDefaultPolicyDocument() + if err != nil { + return err + } + + if err = policyDocument.UpdatePolicies(newPolicies, dir.UserLevel); err != nil { + return err + } + + ioutil.PrintPolicyNames(os.Stdout, newPolicies, "Updated Policies:") + return nil +} + +func addPolicy(opts *policyOpts) error { + policies, err := loadPoliciesFromOptions(opts) + if err != nil { + return err + } + + policyDocument, err := verification.LoadDefaultPolicyDocument() + if err != nil { + return err + } + + if err = policyDocument.AddPolicies(policies, dir.UserLevel); err != nil { + return err + } + + if err = addCertificate(opts.certPath, opts.stores); err != nil { + return err + } + + ioutil.PrintPolicyNames(os.Stdout, policies, "Added Policies:") + return nil +} + +func loadPoliciesFromOptions(opts *policyOpts) ([]*verification.TrustPolicy, error) { + if len(opts.configPath) == 0 { + // create a policy from options. + policy, err := newPolicyFromOptions(opts) + if err != nil { + return nil, err + } + return []*verification.TrustPolicy{policy}, nil + } + // load policies from provided JSON file. + return verification.LoadTrustPolicies(opts.configPath) +} + +func newPolicyFromOptions(opts *policyOpts) (*verification.TrustPolicy, error) { + if len(opts.name) == 0 { + return nil, errors.New("no name specified") + } + override, err := cmd.ParseFlagPluginConfig(opts.override) + if err != nil { + return nil, err + } + + // TODO: check the default values of each fields once spec is available. + + return &verification.TrustPolicy{ + Name: opts.name, + RegistryScopes: opts.scopes, + SignatureVerification: verification.SignatureVerification{ + Level: opts.level, + Override: override, + }, + TrustStores: opts.stores, + TrustedIdentities: opts.identities, + }, nil +} + +func (opts *policyOpts) applyFlags(fs *pflag.FlagSet) { + fs.StringSliceVar(&opts.scopes, "scope", nil, "registry scopes") + fs.StringVar(&opts.level, "level", "", "verification level") + fs.StringVar(&opts.override, "level-override", "", "list of comma-separated {key}={value} pairs that override the behavior of the verification level") + fs.StringSliceVar(&opts.stores, "trust-store", nil, "trust stores containing trusted roots") + fs.StringArrayVar(&opts.identities, "identity", nil, "trusted identities") +} + +// prepareForUpdate checks if the update works on a file or via typed options. +func (opts *policyOpts) prepareForUpdate() { + if len(opts.scopes) != 0 || opts.level != "" || len(opts.override) != 0 || len(opts.stores) != 0 || len(opts.identities) != 0 { + opts.name = opts.configPath + opts.configPath = "" + } +} + +func trimSpace(opts *policyOpts) { + opts.configPath = strings.TrimSpace(opts.configPath) + opts.name = strings.TrimSpace(opts.name) + opts.certPath = strings.TrimSpace(opts.certPath) +} + +func addCertificate(certPath string, stores []string) error { + if len(certPath) == 0 { + return nil + } + + for _, store := range stores { + parts := strings.Split(store, ":") + if len(parts) != 2 { + return fmt.Errorf("trust store: %s is invalid, the valid format is: type:name", store) + } + if err := certificate.AddCertCore(certPath, parts[0], parts[1], false); err != nil { + return err + } + } + + return nil +} \ No newline at end of file diff --git a/cmd/notation/policy_test.go b/cmd/notation/policy_test.go new file mode 100644 index 000000000..994f4340c --- /dev/null +++ b/cmd/notation/policy_test.go @@ -0,0 +1,197 @@ +package main + +import ( + "reflect" + "testing" +) + +const ( + testPolicyName = "test_policy_name" + testScope = "test_scope" + testPath = "test_path" + testLevel = "test_level" + testOverride = "key=val" + testTrustStore = "ca:test" + testIdentity = "testIdentity" +) + +func TestPolicyListCommand_BasicArgs(t *testing.T) { + cmd := policyListCommand() + if err := cmd.ParseFlags([]string{}); err != nil { + t.Fatalf("Parse flag failed: %v", err) + } +} + +func TestPolicyShowCommand_ValidArgs(t *testing.T) { + cmd := policyShowCommand() + if err := cmd.ParseFlags([]string{testPolicyName}); err != nil { + t.Fatalf("Parse flag failed: %v", err) + } + if err := cmd.Args(cmd, cmd.Flags().Args()); err != nil { + t.Fatalf("Parse args failed: %v", err) + } +} + +func TestPolicyShowCommand_NoArg(t *testing.T) { + cmd := policyShowCommand() + if err := cmd.ParseFlags([]string{}); err != nil { + t.Fatalf("Parse flag failed: %v", err) + } + if err := cmd.Args(cmd, cmd.Flags().Args()); err == nil { + t.Fatal("Error should be returned but got nil") + } +} + +func TestPolicyResolveCommand_ValidArgs(t *testing.T) { + cmd := policyResolveCommand() + if err := cmd.ParseFlags([]string{testScope}); err != nil { + t.Fatalf("Parse flag failed: %v", err) + } + if err := cmd.Args(cmd, cmd.Flags().Args()); err != nil { + t.Fatalf("Parse args failed: %v", err) + } +} + +func TestPolicyResolveCommand_NoArgs(t *testing.T) { + cmd := policyResolveCommand() + if err := cmd.ParseFlags([]string{}); err != nil { + t.Fatalf("Parse flag failed: %v", err) + } + if err := cmd.Args(cmd, cmd.Flags().Args()); err == nil { + t.Fatal("Error should be returned but got nil") + } +} + +func TestPolicyDeleteCommand_ValidArgs(t *testing.T) { + opts := &deleteOpts{} + cmd := policyDeleteCommand(opts) + expected := &deleteOpts{ + names: []string{testPolicyName}, + confirmed: false, + } + if err := cmd.ParseFlags(expected.names); err != nil { + t.Fatalf("Parse flag failed: %v", err) + } + if err := cmd.Args(cmd, cmd.Flags().Args()); err != nil { + t.Fatalf("Parse args failed: %v", err) + } + if !reflect.DeepEqual(opts, expected) { + t.Fatalf("Expect opts: %+v, got: %+v", expected, opts) + } +} + +func TestPolicyDeleteCommand_NoArgs(t *testing.T) { + opts := &deleteOpts{} + cmd := policyDeleteCommand(opts) + if err := cmd.ParseFlags([]string{}); err != nil { + t.Fatalf("Parse flag failed: %v", err) + } + if err := cmd.Args(cmd, cmd.Flags().Args()); err == nil { + t.Fatal("Error should be returned but got nil") + } +} + +func TestPolicyUpdateCommand_NoArgs(t *testing.T) { + opts := &policyOpts{} + cmd := policyUpdateCommand(opts) + if err := cmd.ParseFlags([]string{}); err != nil { + t.Fatalf("Parse flag failed: %v", err) + } + if err := cmd.Args(cmd, cmd.Flags().Args()); err == nil { + t.Fatal("Error should be returned but got nil") + } +} + +func TestPolicyUpdateCommand_FilePath(t *testing.T) { + opts := &policyOpts{} + cmd := policyUpdateCommand(opts) + expected := &policyOpts{ + configPath: testPath, + } + if err := cmd.ParseFlags([]string{testPath}); err != nil { + t.Fatalf("Parse flag failed: %v", err) + } + if err := cmd.Args(cmd, cmd.Flags().Args()); err != nil { + t.Fatalf("Parse args failed: %v", err) + } + if !reflect.DeepEqual(opts, expected) { + t.Fatalf("Expect opts: %+v, got: %+v", expected, opts) + } +} + +func TestPolicyUpdateCommand_Flags(t *testing.T) { + opts := &policyOpts{} + cmd := policyUpdateCommand(opts) + expected := &policyOpts{ + configPath: testPolicyName, + scopes: []string{testScope}, + level: testLevel, + override: testOverride, + stores: []string{testTrustStore}, + identities: []string{testIdentity}, + } + if err := cmd.ParseFlags([]string{ + "--scope", testScope, + "--level", testLevel, + "--level-override", testOverride, + "--trust-store", testTrustStore, + "--identity", testIdentity, + testPolicyName, + }); err != nil { + t.Fatalf("Parse flag failed: %v", err) + } + if err := cmd.Args(cmd, cmd.Flags().Args()); err != nil { + t.Fatalf("Parse args failed: %v", err) + } + if !reflect.DeepEqual(opts, expected) { + t.Fatalf("Expect opts: %+v, got: %+v", expected, opts) + } +} + +func TestPolicyAddCommand_FilePath(t *testing.T) { + opts := &policyOpts{} + cmd := policyAddCommand(opts) + expected := &policyOpts{ + configPath: testPath, + } + if err := cmd.ParseFlags([]string{testPath}); err != nil { + t.Fatalf("Parse flag failed: %v", err) + } + if err := cmd.Args(cmd, cmd.Flags().Args()); err != nil { + t.Fatalf("Parse args failed: %v", err) + } + if !reflect.DeepEqual(opts, expected) { + t.Fatalf("Expect opts: %+v, got: %+v", expected, opts) + } +} + +func TestPolicyAddCommand_Flags(t *testing.T) { + opts := &policyOpts{} + cmd := policyAddCommand(opts) + expected := &policyOpts{ + configPath: testPolicyName, + scopes: []string{testScope}, + level: testLevel, + override: testOverride, + stores: []string{testTrustStore}, + identities: []string{testIdentity}, + certPath: testPath, + } + if err := cmd.ParseFlags([]string{ + "--scope", testScope, + "--level", testLevel, + "--level-override", testOverride, + "--trust-store", testTrustStore, + "--identity", testIdentity, + "--identity-cert", testPath, + testPolicyName, + }); err != nil { + t.Fatalf("Parse flag failed: %v", err) + } + if err := cmd.Args(cmd, cmd.Flags().Args()); err != nil { + t.Fatalf("Parse args failed: %v", err) + } + if !reflect.DeepEqual(opts, expected) { + t.Fatalf("Expect opts: %+v, got: %+v", expected, opts) + } +} \ No newline at end of file diff --git a/docs/hello-signing.md b/docs/hello-signing.md index 82435ae1f..b0777ba2c 100644 --- a/docs/hello-signing.md +++ b/docs/hello-signing.md @@ -107,7 +107,7 @@ To get things started quickly, the Notation cli supports self-signed certificate ## Verify a Container Image Using Notation Signatures -Notation provides a trust policy [`~/.config/notation/trustpolicy.json`] for users to specify trusted identities which will sign the artifiacts, and level of signature verification to use. A trust policy is a JSON document, below example works for the current case. +Notation provides a trust policy for users to specify trusted identities which will sign the artifiacts, and level of signature verification to use. A trust policy is a JSON document, below example works for the current case. ``` { diff --git a/internal/ioutil/print.go b/internal/ioutil/print.go index 7156f9ed3..4be3c8b34 100644 --- a/internal/ioutil/print.go +++ b/internal/ioutil/print.go @@ -80,3 +80,38 @@ func printOutcomes(tw *tabwriter.Writer, outcomes []*verification.SignatureVerif fmt.Printf("%s\n\n", outcome.Error.Error()) } } + +func PrintPolicyMap(w io.Writer, v []*verification.TrustPolicy) error { + tw := newTabWriter(w) + fmt.Fprintln(tw, "NAME\tSCOPE\tVERIFICATION_LEVEL\tTRUST_STORE\tTRUSTED_IDENTITY\t") + for _, policy := range v { + fmt.Fprintf( + tw, + "%s\t%s\t%+v\t%s\t%s\n", + policy.Name, + policy.RegistryScopes, + policy.SignatureVerification, + policy.TrustStores, + policy.TrustedIdentities, + ) + } + return tw.Flush() +} + +func PrintPolicyNames(w io.Writer, v []*verification.TrustPolicy, header string) error { + tw := newTabWriter(w) + fmt.Fprintln(tw, header) + for _, policy := range v { + fmt.Fprintln(tw, policy.Name) + } + return tw.Flush() +} + +func PrintDeletedPolicyNames(w io.Writer, v []string) error { + tw := newTabWriter(w) + fmt.Fprintln(tw, "Deleted Policies:") + for _, name := range v { + fmt.Fprintln(tw, name) + } + return tw.Flush() +}