Skip to content

Commit 5c08b48

Browse files
committed
feat: add notation policy init command
Scaffolds a starter trustpolicy.oci.json from flags so new users don't have to hand-write one. Mirrors the existing blob policy init, minus the --global flag (OCI policies scope by registry, not a global toggle) and adds --registry-scope which defaults to the wildcard "*". Validates the generated document before writing and won't clobber an existing policy without --force. Also documents the subcommand in specs/cmd/policy.md. Signed-off-by: Chris (ChrisJr404) <11917633+ChrisJr404@users.noreply.github.com>
1 parent 2c82269 commit 5c08b48

4 files changed

Lines changed: 302 additions & 0 deletions

File tree

cmd/notation/policy/cmd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func Cmd() *cobra.Command {
2323
}
2424

2525
command.AddCommand(
26+
initCmd(),
2627
showCmd(),
2728
importCmd(),
2829
)

cmd/notation/policy/init.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright The Notary Project Authors.
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package policy
15+
16+
import (
17+
"encoding/json"
18+
"fmt"
19+
"os"
20+
21+
"github.com/notaryproject/notation-go/dir"
22+
"github.com/notaryproject/notation-go/verifier/trustpolicy"
23+
"github.com/notaryproject/notation/v2/cmd/notation/internal/display"
24+
"github.com/notaryproject/notation/v2/cmd/notation/internal/display/output"
25+
"github.com/notaryproject/notation/v2/internal/osutil"
26+
"github.com/spf13/cobra"
27+
)
28+
29+
// wildcardRegistryScope is the registry scope that matches any registry. It is
30+
// the default scope for a starter policy so verification applies everywhere
31+
// until the user narrows it down.
32+
const wildcardRegistryScope = "*"
33+
34+
type initOpts struct {
35+
printer *output.Printer
36+
name string
37+
registryScopes []string
38+
trustStores []string
39+
trustedIdentities []string
40+
force bool
41+
}
42+
43+
func initCmd() *cobra.Command {
44+
opts := initOpts{}
45+
command := &cobra.Command{
46+
Use: `init [flags] --name <policy_name> --trust-store "<store_type>:<store_name>" --trusted-identity "<trusted_identity>"`,
47+
Short: "Initialize OCI trust policy configuration",
48+
Long: `Initialize OCI trust policy configuration.
49+
50+
The generated policy statement applies to all registries by default (registry scope "*"). Use --registry-scope to pin it to specific repositories.
51+
52+
Example - init an OCI trust policy configuration with a trust store and a trusted identity:
53+
notation policy init --name examplePolicy --trust-store ca:exampleStore --trusted-identity "x509.subject: C=US, ST=WA, O=acme-rockets.io"
54+
55+
Example - init an OCI trust policy configuration scoped to specific repositories:
56+
notation policy init --name examplePolicy --registry-scope registry.acme-rockets.io/software/net-monitor --trust-store ca:exampleStore --trusted-identity "x509.subject: C=US, ST=WA, O=acme-rockets.io"
57+
58+
Example - init an OCI trust policy configuration with multiple trust stores and trusted identities:
59+
notation policy init --name examplePolicy --trust-store ca:exampleStore --trust-store ca:exampleStore2 --trusted-identity "x509.subject: C=US, ST=WA, O=acme-rockets.io" --trusted-identity "x509.subject: C=US, ST=WA, L=Seattle, O=wabbit-networks.io"
60+
61+
Example - init an OCI trust policy configuration with any trusted identity:
62+
notation policy init --name examplePolicy --trust-store ca:exampleStore --trusted-identity "*"
63+
64+
Example - init an OCI trust policy configuration without prompt:
65+
notation policy init --name examplePolicy --trust-store ca:exampleStore --trusted-identity "x509.subject: C=US, ST=WA, O=acme-rockets.io" --force
66+
`,
67+
Args: cobra.ExactArgs(0),
68+
PreRun: func(cmd *cobra.Command, args []string) {
69+
opts.printer = output.NewPrinter(cmd.OutOrStdout(), cmd.OutOrStderr())
70+
},
71+
RunE: func(cmd *cobra.Command, args []string) error {
72+
return runInit(&opts)
73+
},
74+
}
75+
76+
command.Flags().StringVarP(&opts.name, "name", "n", "", "name of the OCI trust policy")
77+
command.Flags().StringArrayVar(&opts.registryScopes, "registry-scope", []string{wildcardRegistryScope}, "registry scope the policy applies to, e.g. \"registry.acme-rockets.io/software/net-monitor\"; defaults to \"*\" for all registries")
78+
command.Flags().StringArrayVar(&opts.trustStores, "trust-store", nil, "trust store in the format \"<store_type>:<store_name>\"")
79+
command.Flags().StringArrayVar(&opts.trustedIdentities, "trusted-identity", nil, "trusted identity, use the format \"x509.subject:<subject_of_signing_certificate>\" for x509 CA scheme and \"<signing_authority_identity>\" for x509 signingAuthority scheme")
80+
command.Flags().BoolVar(&opts.force, "force", false, "override the existing OCI trust policy configuration, never prompt (default --force=false)")
81+
command.MarkFlagRequired("name")
82+
command.MarkFlagRequired("trust-store")
83+
command.MarkFlagRequired("trusted-identity")
84+
return command
85+
}
86+
87+
func runInit(opts *initOpts) error {
88+
ociPolicy := trustpolicy.OCIDocument{
89+
Version: "1.0",
90+
TrustPolicies: []trustpolicy.OCITrustPolicy{
91+
{
92+
Name: opts.name,
93+
SignatureVerification: trustpolicy.SignatureVerification{
94+
VerificationLevel: trustpolicy.LevelStrict.Name,
95+
},
96+
RegistryScopes: opts.registryScopes,
97+
TrustStores: opts.trustStores,
98+
TrustedIdentities: opts.trustedIdentities,
99+
},
100+
},
101+
}
102+
if err := ociPolicy.Validate(); err != nil {
103+
return fmt.Errorf("invalid OCI policy: %w", err)
104+
}
105+
106+
// optional confirmation
107+
if _, err := trustpolicy.LoadOCIDocument(); err == nil {
108+
if !opts.force {
109+
confirmed, err := display.AskForConfirmation(os.Stdin, "The OCI trust policy configuration already exists, do you want to overwrite it?", opts.force)
110+
if err != nil {
111+
return err
112+
}
113+
if !confirmed {
114+
return nil
115+
}
116+
} else {
117+
opts.printer.PrintErrorf("Warning: existing OCI trust policy configuration will be overwritten\n")
118+
}
119+
}
120+
121+
policyPath, err := dir.ConfigFS().SysPath(dir.PathOCITrustPolicy)
122+
if err != nil {
123+
return fmt.Errorf("failed to obtain path of OCI trust policy configuration: %w", err)
124+
}
125+
policyJSON, err := json.MarshalIndent(ociPolicy, "", " ")
126+
if err != nil {
127+
return fmt.Errorf("failed to marshal OCI trust policy: %w", err)
128+
}
129+
if err = osutil.WriteFile(policyPath, policyJSON); err != nil {
130+
return fmt.Errorf("failed to write OCI trust policy configuration: %w", err)
131+
}
132+
133+
return opts.printer.Printf("Successfully initialized OCI trust policy file to %s.\n", policyPath)
134+
}

cmd/notation/policy/init_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright The Notary Project Authors.
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package policy
15+
16+
import (
17+
"encoding/json"
18+
"os"
19+
"path/filepath"
20+
"testing"
21+
22+
"github.com/notaryproject/notation-go/dir"
23+
"github.com/notaryproject/notation-go/verifier/trustpolicy"
24+
"github.com/notaryproject/notation/v2/cmd/notation/internal/display/output"
25+
)
26+
27+
func newInitOpts() *initOpts {
28+
return &initOpts{
29+
printer: output.NewPrinter(os.Stdout, os.Stderr),
30+
name: "test-policy",
31+
registryScopes: []string{wildcardRegistryScope},
32+
trustStores: []string{"ca:test-store"},
33+
trustedIdentities: []string{"x509.subject: C=US, ST=WA, O=acme-rockets.io"},
34+
}
35+
}
36+
37+
func TestRunInit(t *testing.T) {
38+
defer func(old string) { dir.UserConfigDir = old }(dir.UserConfigDir)
39+
40+
t.Run("writes a valid starter policy", func(t *testing.T) {
41+
tempRoot := t.TempDir()
42+
dir.UserConfigDir = tempRoot
43+
44+
if err := runInit(newInitOpts()); err != nil {
45+
t.Fatalf("runInit failed: %v", err)
46+
}
47+
48+
policyPath := filepath.Join(tempRoot, "trustpolicy.oci.json")
49+
data, err := os.ReadFile(policyPath)
50+
if err != nil {
51+
t.Fatalf("expected policy file at %s: %v", policyPath, err)
52+
}
53+
var doc trustpolicy.OCIDocument
54+
if err := json.Unmarshal(data, &doc); err != nil {
55+
t.Fatalf("generated policy is not valid JSON: %v", err)
56+
}
57+
if err := doc.Validate(); err != nil {
58+
t.Fatalf("generated policy did not validate: %v", err)
59+
}
60+
if len(doc.TrustPolicies) != 1 || doc.TrustPolicies[0].Name != "test-policy" {
61+
t.Fatalf("unexpected policy content: %+v", doc)
62+
}
63+
if got := doc.TrustPolicies[0].RegistryScopes; len(got) != 1 || got[0] != wildcardRegistryScope {
64+
t.Fatalf("expected wildcard registry scope by default, got %v", got)
65+
}
66+
})
67+
68+
t.Run("refuses invalid input", func(t *testing.T) {
69+
tempRoot := t.TempDir()
70+
dir.UserConfigDir = tempRoot
71+
opts := newInitOpts()
72+
// wildcard scope cannot be combined with another scope
73+
opts.registryScopes = []string{wildcardRegistryScope, "registry.acme-rockets.io/software/net-monitor"}
74+
if err := runInit(opts); err == nil {
75+
t.Fatal("expected validation error for wildcard combined with another scope, got nil")
76+
}
77+
})
78+
79+
t.Run("force overwrites an existing policy", func(t *testing.T) {
80+
tempRoot := t.TempDir()
81+
dir.UserConfigDir = tempRoot
82+
policyPath := filepath.Join(tempRoot, "trustpolicy.oci.json")
83+
if err := os.WriteFile(policyPath, []byte("existing junk"), 0600); err != nil {
84+
t.Fatalf("seeding existing policy failed: %v", err)
85+
}
86+
87+
opts := newInitOpts()
88+
opts.force = true
89+
if err := runInit(opts); err != nil {
90+
t.Fatalf("runInit with --force failed: %v", err)
91+
}
92+
var doc trustpolicy.OCIDocument
93+
data, err := os.ReadFile(policyPath)
94+
if err != nil {
95+
t.Fatalf("reading overwritten policy failed: %v", err)
96+
}
97+
if err := json.Unmarshal(data, &doc); err != nil {
98+
t.Fatalf("overwritten policy is not valid JSON: %v", err)
99+
}
100+
if err := doc.Validate(); err != nil {
101+
t.Fatalf("overwritten policy did not validate: %v", err)
102+
}
103+
})
104+
}

specs/cmd/policy.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,30 @@ Usage:
8181
8282
Available Commands:
8383
import import OCI trust policy configuration from a JSON file
84+
init initialize OCI trust policy configuration
8485
show show OCI trust policy configuration
8586
8687
Flags:
8788
-h, --help help for policy
8889
```
8990

91+
### notation policy init
92+
93+
```text
94+
Initialize OCI trust policy configuration.
95+
96+
Usage:
97+
notation policy init [flags] --name <policy_name> --trust-store "<store_type>:<store_name>" --trusted-identity "<trusted_identity>"
98+
99+
Flags:
100+
--force override the existing OCI trust policy configuration, never prompt (default --force=false)
101+
-h, --help help for init
102+
-n, --name name of the OCI trust policy
103+
--registry-scope stringArray registry scope the policy applies to, e.g. "registry.acme-rockets.io/software/net-monitor"; defaults to "*" for all registries
104+
--trust-store stringArray trust store in the format "<store_type>:<store_name>"
105+
--trusted-identity stringArray trusted identity, use the format "x509.subject:<subject_of_signing_certificate>" for x509 CA scheme and "<signing_authority_identity>" for x509 signingAuthority scheme
106+
```
107+
90108
### notation policy import
91109

92110
```text
@@ -114,6 +132,51 @@ Flags:
114132

115133
## Usage
116134

135+
### Initialize trust policy configuration
136+
137+
New users often don't have a trust policy yet and hand-writing `trustpolicy.oci.json` from the specification is error prone. `notation policy init` scaffolds a valid single-statement policy from flags so the file is ready for `notation verify` right away.
138+
139+
```shell
140+
notation policy init --name "wabbit-networks-images" --trust-store "ca:wabbit-networks" --trusted-identity "x509.subject:C=US,ST=WA,O=wabbit-networks.io"
141+
```
142+
143+
The statement is generated with `signatureVerification.level` set to `strict`. Sample output for a successful initialization:
144+
145+
```jsonc
146+
{
147+
"version": "1.0",
148+
"trustPolicies": [
149+
{
150+
"name": "wabbit-networks-images",
151+
"registryScopes": [ "*" ],
152+
"signatureVerification": {
153+
"level": "strict"
154+
},
155+
"trustStores": [ "ca:wabbit-networks" ],
156+
"trustedIdentities": [
157+
"x509.subject:C=US,ST=WA,O=wabbit-networks.io"
158+
]
159+
}
160+
]
161+
}
162+
```
163+
164+
The `--trust-store` and `--trusted-identity` flags can be repeated to configure multiple trust stores or trusted identities. To trust any identity, set `--trusted-identity` to `"*"`; this is not recommended for production and cannot be combined with other values.
165+
166+
Unlike a blob trust policy, an OCI trust policy statement is bound to registry scopes rather than a global flag. By default the generated statement uses the wildcard scope `"*"` so it applies to every registry. Use `--registry-scope` (repeatable) to pin it to specific repositories instead:
167+
168+
```shell
169+
notation policy init --name "wabbit-networks-images" --registry-scope "registry.acme-rockets.io/software/net-monitor" --trust-store "ca:wabbit-networks" --trusted-identity "x509.subject:C=US,ST=WA,O=wabbit-networks.io"
170+
```
171+
172+
The generated policy is validated according to [trust policy properties](https://github.com/notaryproject/notaryproject/specs/trust-store-trust-policy.md#trust-policy-properties) before it is written; if validation fails no file is written and the reason is printed to standard error.
173+
174+
If a trust policy configuration already exists, the command prompts for confirmation before overwriting it. Use the `--force` flag to overwrite without a prompt:
175+
176+
```shell
177+
notation policy init --force --name "wabbit-networks-images" --trust-store "ca:wabbit-networks" --trusted-identity "x509.subject:C=US,ST=WA,O=wabbit-networks.io"
178+
```
179+
117180
### Import trust policy configuration from a JSON file
118181

119182
An example of import trust policy configuration from a JSON file:

0 commit comments

Comments
 (0)