github.com/justincormack/cli@v0.0.0-20201215022714-831ebeae9675/cli/command/trust/inspect_pretty_test.go (about) 1 package trust 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/hex" 7 "io" 8 "io/ioutil" 9 "testing" 10 11 "github.com/docker/cli/cli/trust" 12 "github.com/docker/cli/internal/test" 13 notaryfake "github.com/docker/cli/internal/test/notary" 14 "github.com/docker/docker/api/types" 15 dockerClient "github.com/docker/docker/client" 16 "github.com/theupdateframework/notary" 17 "github.com/theupdateframework/notary/client" 18 "github.com/theupdateframework/notary/tuf/data" 19 "github.com/theupdateframework/notary/tuf/utils" 20 "gotest.tools/v3/assert" 21 is "gotest.tools/v3/assert/cmp" 22 "gotest.tools/v3/golden" 23 ) 24 25 // TODO(n4ss): remove common tests with the regular inspect command 26 27 type fakeClient struct { 28 dockerClient.Client 29 } 30 31 func (c *fakeClient) Info(ctx context.Context) (types.Info, error) { 32 return types.Info{}, nil 33 } 34 35 func (c *fakeClient) ImageInspectWithRaw(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) { 36 return types.ImageInspect{}, []byte{}, nil 37 } 38 39 func (c *fakeClient) ImagePush(ctx context.Context, image string, options types.ImagePushOptions) (io.ReadCloser, error) { 40 return &utils.NoopCloser{Reader: bytes.NewBuffer([]byte{})}, nil 41 } 42 43 func TestTrustInspectPrettyCommandErrors(t *testing.T) { 44 testCases := []struct { 45 name string 46 args []string 47 expectedError string 48 }{ 49 { 50 name: "not-enough-args", 51 expectedError: "requires at least 1 argument", 52 }, 53 { 54 name: "sha-reference", 55 args: []string{"870d292919d01a0af7e7f056271dc78792c05f55f49b9b9012b6d89725bd9abd"}, 56 expectedError: "invalid repository name", 57 }, 58 { 59 name: "invalid-img-reference", 60 args: []string{"ALPINE"}, 61 expectedError: "invalid reference format", 62 }, 63 } 64 for _, tc := range testCases { 65 cmd := newInspectCommand( 66 test.NewFakeCli(&fakeClient{})) 67 cmd.SetArgs(tc.args) 68 cmd.SetOut(ioutil.Discard) 69 cmd.Flags().Set("pretty", "true") 70 assert.ErrorContains(t, cmd.Execute(), tc.expectedError) 71 } 72 } 73 74 func TestTrustInspectPrettyCommandOfflineErrors(t *testing.T) { 75 cli := test.NewFakeCli(&fakeClient{}) 76 cli.SetNotaryClient(notaryfake.GetOfflineNotaryRepository) 77 cmd := newInspectCommand(cli) 78 cmd.Flags().Set("pretty", "true") 79 cmd.SetArgs([]string{"nonexistent-reg-name.io/image"}) 80 cmd.SetOut(ioutil.Discard) 81 assert.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image") 82 83 cli = test.NewFakeCli(&fakeClient{}) 84 cli.SetNotaryClient(notaryfake.GetOfflineNotaryRepository) 85 cmd = newInspectCommand(cli) 86 cmd.Flags().Set("pretty", "true") 87 cmd.SetArgs([]string{"nonexistent-reg-name.io/image:tag"}) 88 cmd.SetOut(ioutil.Discard) 89 assert.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image") 90 } 91 92 func TestTrustInspectPrettyCommandUninitializedErrors(t *testing.T) { 93 cli := test.NewFakeCli(&fakeClient{}) 94 cli.SetNotaryClient(notaryfake.GetUninitializedNotaryRepository) 95 cmd := newInspectCommand(cli) 96 cmd.Flags().Set("pretty", "true") 97 cmd.SetArgs([]string{"reg/unsigned-img"}) 98 cmd.SetOut(ioutil.Discard) 99 assert.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img") 100 101 cli = test.NewFakeCli(&fakeClient{}) 102 cli.SetNotaryClient(notaryfake.GetUninitializedNotaryRepository) 103 cmd = newInspectCommand(cli) 104 cmd.Flags().Set("pretty", "true") 105 cmd.SetArgs([]string{"reg/unsigned-img:tag"}) 106 cmd.SetOut(ioutil.Discard) 107 assert.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img:tag") 108 } 109 110 func TestTrustInspectPrettyCommandEmptyNotaryRepoErrors(t *testing.T) { 111 cli := test.NewFakeCli(&fakeClient{}) 112 cli.SetNotaryClient(notaryfake.GetEmptyTargetsNotaryRepository) 113 cmd := newInspectCommand(cli) 114 cmd.Flags().Set("pretty", "true") 115 cmd.SetArgs([]string{"reg/img:unsigned-tag"}) 116 cmd.SetOut(ioutil.Discard) 117 assert.NilError(t, cmd.Execute()) 118 assert.Check(t, is.Contains(cli.OutBuffer().String(), "No signatures for reg/img:unsigned-tag")) 119 assert.Check(t, is.Contains(cli.OutBuffer().String(), "Administrative keys for reg/img")) 120 121 cli = test.NewFakeCli(&fakeClient{}) 122 cli.SetNotaryClient(notaryfake.GetEmptyTargetsNotaryRepository) 123 cmd = newInspectCommand(cli) 124 cmd.Flags().Set("pretty", "true") 125 cmd.SetArgs([]string{"reg/img"}) 126 cmd.SetOut(ioutil.Discard) 127 assert.NilError(t, cmd.Execute()) 128 assert.Check(t, is.Contains(cli.OutBuffer().String(), "No signatures for reg/img")) 129 assert.Check(t, is.Contains(cli.OutBuffer().String(), "Administrative keys for reg/img")) 130 } 131 132 func TestTrustInspectPrettyCommandFullRepoWithoutSigners(t *testing.T) { 133 cli := test.NewFakeCli(&fakeClient{}) 134 cli.SetNotaryClient(notaryfake.GetLoadedWithNoSignersNotaryRepository) 135 cmd := newInspectCommand(cli) 136 cmd.Flags().Set("pretty", "true") 137 cmd.SetArgs([]string{"signed-repo"}) 138 assert.NilError(t, cmd.Execute()) 139 140 golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-pretty-full-repo-no-signers.golden") 141 } 142 143 func TestTrustInspectPrettyCommandOneTagWithoutSigners(t *testing.T) { 144 cli := test.NewFakeCli(&fakeClient{}) 145 cli.SetNotaryClient(notaryfake.GetLoadedWithNoSignersNotaryRepository) 146 cmd := newInspectCommand(cli) 147 cmd.Flags().Set("pretty", "true") 148 cmd.SetArgs([]string{"signed-repo:green"}) 149 assert.NilError(t, cmd.Execute()) 150 151 golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-pretty-one-tag-no-signers.golden") 152 } 153 154 func TestTrustInspectPrettyCommandFullRepoWithSigners(t *testing.T) { 155 cli := test.NewFakeCli(&fakeClient{}) 156 cli.SetNotaryClient(notaryfake.GetLoadedNotaryRepository) 157 cmd := newInspectCommand(cli) 158 cmd.Flags().Set("pretty", "true") 159 cmd.SetArgs([]string{"signed-repo"}) 160 assert.NilError(t, cmd.Execute()) 161 162 golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-pretty-full-repo-with-signers.golden") 163 } 164 165 func TestTrustInspectPrettyCommandUnsignedTagInSignedRepo(t *testing.T) { 166 cli := test.NewFakeCli(&fakeClient{}) 167 cli.SetNotaryClient(notaryfake.GetLoadedNotaryRepository) 168 cmd := newInspectCommand(cli) 169 cmd.Flags().Set("pretty", "true") 170 cmd.SetArgs([]string{"signed-repo:unsigned"}) 171 assert.NilError(t, cmd.Execute()) 172 173 golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-pretty-unsigned-tag-with-signers.golden") 174 } 175 176 func TestNotaryRoleToSigner(t *testing.T) { 177 assert.Check(t, is.Equal(releasedRoleName, notaryRoleToSigner(data.CanonicalTargetsRole))) 178 assert.Check(t, is.Equal(releasedRoleName, notaryRoleToSigner(trust.ReleasesRole))) 179 assert.Check(t, is.Equal("signer", notaryRoleToSigner("targets/signer"))) 180 assert.Check(t, is.Equal("docker/signer", notaryRoleToSigner("targets/docker/signer"))) 181 182 // It's nonsense for other base roles to have signed off on a target, but this function leaves role names intact 183 for _, role := range data.BaseRoles { 184 if role == data.CanonicalTargetsRole { 185 continue 186 } 187 assert.Check(t, is.Equal(role.String(), notaryRoleToSigner(role))) 188 } 189 assert.Check(t, is.Equal("notarole", notaryRoleToSigner(data.RoleName("notarole")))) 190 } 191 192 // check if a role name is "released": either targets/releases or targets TUF roles 193 func TestIsReleasedTarget(t *testing.T) { 194 assert.Check(t, isReleasedTarget(trust.ReleasesRole)) 195 for _, role := range data.BaseRoles { 196 assert.Check(t, is.Equal(role == data.CanonicalTargetsRole, isReleasedTarget(role))) 197 } 198 assert.Check(t, !isReleasedTarget(data.RoleName("targets/not-releases"))) 199 assert.Check(t, !isReleasedTarget(data.RoleName("random"))) 200 assert.Check(t, !isReleasedTarget(data.RoleName("targets/releases/subrole"))) 201 } 202 203 // creates a mock delegation with a given name and no keys 204 func mockDelegationRoleWithName(name string) data.DelegationRole { 205 baseRole := data.NewBaseRole( 206 data.RoleName(name), 207 notary.MinThreshold, 208 ) 209 return data.DelegationRole{BaseRole: baseRole, Paths: []string{}} 210 } 211 212 func TestMatchEmptySignatures(t *testing.T) { 213 // first try empty targets 214 emptyTgts := []client.TargetSignedStruct{} 215 216 matchedSigRows := matchReleasedSignatures(emptyTgts) 217 assert.Check(t, is.Len(matchedSigRows, 0)) 218 } 219 220 func TestMatchUnreleasedSignatures(t *testing.T) { 221 // try an "unreleased" target with 3 signatures, 0 rows will appear 222 unreleasedTgts := []client.TargetSignedStruct{} 223 224 tgt := client.Target{Name: "unreleased", Hashes: data.Hashes{notary.SHA256: []byte("hash")}} 225 for _, unreleasedRole := range []string{"targets/a", "targets/b", "targets/c"} { 226 unreleasedTgts = append(unreleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName(unreleasedRole), Target: tgt}) 227 } 228 229 matchedSigRows := matchReleasedSignatures(unreleasedTgts) 230 assert.Check(t, is.Len(matchedSigRows, 0)) 231 } 232 233 func TestMatchOneReleasedSingleSignature(t *testing.T) { 234 // now try only 1 "released" target with no additional sigs, 1 row will appear with 0 signers 235 oneReleasedTgt := []client.TargetSignedStruct{} 236 237 // make and append the "released" target to our mock input 238 releasedTgt := client.Target{Name: "released", Hashes: data.Hashes{notary.SHA256: []byte("released-hash")}} 239 oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: releasedTgt}) 240 241 // make and append 3 non-released signatures on the "unreleased" target 242 unreleasedTgt := client.Target{Name: "unreleased", Hashes: data.Hashes{notary.SHA256: []byte("hash")}} 243 for _, unreleasedRole := range []string{"targets/a", "targets/b", "targets/c"} { 244 oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName(unreleasedRole), Target: unreleasedTgt}) 245 } 246 247 matchedSigRows := matchReleasedSignatures(oneReleasedTgt) 248 assert.Check(t, is.Len(matchedSigRows, 1)) 249 250 outputRow := matchedSigRows[0] 251 // Empty signers because "targets/releases" doesn't show up 252 assert.Check(t, is.Len(outputRow.Signers, 0)) 253 assert.Check(t, is.Equal(releasedTgt.Name, outputRow.SignedTag)) 254 assert.Check(t, is.Equal(hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.Digest)) 255 } 256 257 func TestMatchOneReleasedMultiSignature(t *testing.T) { 258 // now try only 1 "released" target with 3 additional sigs, 1 row will appear with 3 signers 259 oneReleasedTgt := []client.TargetSignedStruct{} 260 261 // make and append the "released" target to our mock input 262 releasedTgt := client.Target{Name: "released", Hashes: data.Hashes{notary.SHA256: []byte("released-hash")}} 263 oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: releasedTgt}) 264 265 // make and append 3 non-released signatures on both the "released" and "unreleased" targets 266 unreleasedTgt := client.Target{Name: "unreleased", Hashes: data.Hashes{notary.SHA256: []byte("hash")}} 267 for _, unreleasedRole := range []string{"targets/a", "targets/b", "targets/c"} { 268 oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName(unreleasedRole), Target: unreleasedTgt}) 269 oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName(unreleasedRole), Target: releasedTgt}) 270 } 271 272 matchedSigRows := matchReleasedSignatures(oneReleasedTgt) 273 assert.Check(t, is.Len(matchedSigRows, 1)) 274 275 outputRow := matchedSigRows[0] 276 // We should have three signers 277 assert.Check(t, is.DeepEqual(outputRow.Signers, []string{"a", "b", "c"})) 278 assert.Check(t, is.Equal(releasedTgt.Name, outputRow.SignedTag)) 279 assert.Check(t, is.Equal(hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.Digest)) 280 } 281 282 func TestMatchMultiReleasedMultiSignature(t *testing.T) { 283 // now try 3 "released" targets with additional sigs to show 3 rows as follows: 284 // target-a is signed by targets/releases and targets/a - a will be the signer 285 // target-b is signed by targets/releases, targets/a, targets/b - a and b will be the signers 286 // target-c is signed by targets/releases, targets/a, targets/b, targets/c - a, b, and c will be the signers 287 multiReleasedTgts := []client.TargetSignedStruct{} 288 // make target-a, target-b, and target-c 289 targetA := client.Target{Name: "target-a", Hashes: data.Hashes{notary.SHA256: []byte("target-a-hash")}} 290 targetB := client.Target{Name: "target-b", Hashes: data.Hashes{notary.SHA256: []byte("target-b-hash")}} 291 targetC := client.Target{Name: "target-c", Hashes: data.Hashes{notary.SHA256: []byte("target-c-hash")}} 292 293 // have targets/releases "sign" on all of these targets so they are released 294 multiReleasedTgts = append(multiReleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: targetA}) 295 multiReleasedTgts = append(multiReleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: targetB}) 296 multiReleasedTgts = append(multiReleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: targetC}) 297 298 // targets/a signs off on all three targets (target-a, target-b, target-c): 299 for _, tgt := range []client.Target{targetA, targetB, targetC} { 300 multiReleasedTgts = append(multiReleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/a"), Target: tgt}) 301 } 302 303 // targets/b signs off on the final two targets (target-b, target-c): 304 for _, tgt := range []client.Target{targetB, targetC} { 305 multiReleasedTgts = append(multiReleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/b"), Target: tgt}) 306 } 307 308 // targets/c only signs off on the last target (target-c): 309 multiReleasedTgts = append(multiReleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/c"), Target: targetC}) 310 311 matchedSigRows := matchReleasedSignatures(multiReleasedTgts) 312 assert.Check(t, is.Len(matchedSigRows, 3)) 313 314 // note that the output is sorted by tag name, so we can reliably index to validate data: 315 outputTargetA := matchedSigRows[0] 316 assert.Check(t, is.DeepEqual(outputTargetA.Signers, []string{"a"})) 317 assert.Check(t, is.Equal(targetA.Name, outputTargetA.SignedTag)) 318 assert.Check(t, is.Equal(hex.EncodeToString(targetA.Hashes[notary.SHA256]), outputTargetA.Digest)) 319 320 outputTargetB := matchedSigRows[1] 321 assert.Check(t, is.DeepEqual(outputTargetB.Signers, []string{"a", "b"})) 322 assert.Check(t, is.Equal(targetB.Name, outputTargetB.SignedTag)) 323 assert.Check(t, is.Equal(hex.EncodeToString(targetB.Hashes[notary.SHA256]), outputTargetB.Digest)) 324 325 outputTargetC := matchedSigRows[2] 326 assert.Check(t, is.DeepEqual(outputTargetC.Signers, []string{"a", "b", "c"})) 327 assert.Check(t, is.Equal(targetC.Name, outputTargetC.SignedTag)) 328 assert.Check(t, is.Equal(hex.EncodeToString(targetC.Hashes[notary.SHA256]), outputTargetC.Digest)) 329 } 330 331 func TestMatchReleasedSignatureFromTargets(t *testing.T) { 332 // now try only 1 "released" target with no additional sigs, one rows will appear 333 oneReleasedTgt := []client.TargetSignedStruct{} 334 // make and append the "released" target to our mock input 335 releasedTgt := client.Target{Name: "released", Hashes: data.Hashes{notary.SHA256: []byte("released-hash")}} 336 oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName(data.CanonicalTargetsRole.String()), Target: releasedTgt}) 337 matchedSigRows := matchReleasedSignatures(oneReleasedTgt) 338 assert.Check(t, is.Len(matchedSigRows, 1)) 339 outputRow := matchedSigRows[0] 340 // Empty signers because "targets" doesn't show up 341 assert.Check(t, is.Len(outputRow.Signers, 0)) 342 assert.Check(t, is.Equal(releasedTgt.Name, outputRow.SignedTag)) 343 assert.Check(t, is.Equal(hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.Digest)) 344 } 345 346 func TestGetSignerRolesWithKeyIDs(t *testing.T) { 347 roles := []data.Role{ 348 { 349 RootRole: data.RootRole{ 350 KeyIDs: []string{"key11"}, 351 }, 352 Name: "targets/alice", 353 }, 354 { 355 RootRole: data.RootRole{ 356 KeyIDs: []string{"key21", "key22"}, 357 }, 358 Name: "targets/releases", 359 }, 360 { 361 RootRole: data.RootRole{ 362 KeyIDs: []string{"key31"}, 363 }, 364 Name: data.CanonicalTargetsRole, 365 }, 366 { 367 RootRole: data.RootRole{ 368 KeyIDs: []string{"key41", "key01"}, 369 }, 370 Name: data.CanonicalRootRole, 371 }, 372 { 373 RootRole: data.RootRole{ 374 KeyIDs: []string{"key51"}, 375 }, 376 Name: data.CanonicalSnapshotRole, 377 }, 378 { 379 RootRole: data.RootRole{ 380 KeyIDs: []string{"key61"}, 381 }, 382 Name: data.CanonicalTimestampRole, 383 }, 384 { 385 RootRole: data.RootRole{ 386 KeyIDs: []string{"key71", "key72"}, 387 }, 388 Name: "targets/bob", 389 }, 390 } 391 expectedSignerRoleToKeyIDs := map[string][]string{ 392 "alice": {"key11"}, 393 "bob": {"key71", "key72"}, 394 } 395 396 signerRoleToKeyIDs := getDelegationRoleToKeyMap(roles) 397 assert.Check(t, is.DeepEqual(expectedSignerRoleToKeyIDs, signerRoleToKeyIDs)) 398 } 399 400 func TestFormatAdminRole(t *testing.T) { 401 aliceRole := data.Role{ 402 RootRole: data.RootRole{ 403 KeyIDs: []string{"key11"}, 404 }, 405 Name: "targets/alice", 406 } 407 aliceRoleWithSigs := client.RoleWithSignatures{Role: aliceRole, Signatures: nil} 408 assert.Check(t, is.Equal("", formatAdminRole(aliceRoleWithSigs))) 409 410 releasesRole := data.Role{ 411 RootRole: data.RootRole{ 412 KeyIDs: []string{"key11"}, 413 }, 414 Name: "targets/releases", 415 } 416 releasesRoleWithSigs := client.RoleWithSignatures{Role: releasesRole, Signatures: nil} 417 assert.Check(t, is.Equal("", formatAdminRole(releasesRoleWithSigs))) 418 419 timestampRole := data.Role{ 420 RootRole: data.RootRole{ 421 KeyIDs: []string{"key11"}, 422 }, 423 Name: data.CanonicalTimestampRole, 424 } 425 timestampRoleWithSigs := client.RoleWithSignatures{Role: timestampRole, Signatures: nil} 426 assert.Check(t, is.Equal("", formatAdminRole(timestampRoleWithSigs))) 427 428 snapshotRole := data.Role{ 429 RootRole: data.RootRole{ 430 KeyIDs: []string{"key11"}, 431 }, 432 Name: data.CanonicalSnapshotRole, 433 } 434 snapshotRoleWithSigs := client.RoleWithSignatures{Role: snapshotRole, Signatures: nil} 435 assert.Check(t, is.Equal("", formatAdminRole(snapshotRoleWithSigs))) 436 437 rootRole := data.Role{ 438 RootRole: data.RootRole{ 439 KeyIDs: []string{"key11"}, 440 }, 441 Name: data.CanonicalRootRole, 442 } 443 rootRoleWithSigs := client.RoleWithSignatures{Role: rootRole, Signatures: nil} 444 assert.Check(t, is.Equal("Root Key:\tkey11\n", formatAdminRole(rootRoleWithSigs))) 445 446 targetsRole := data.Role{ 447 RootRole: data.RootRole{ 448 KeyIDs: []string{"key99", "abc", "key11"}, 449 }, 450 Name: data.CanonicalTargetsRole, 451 } 452 targetsRoleWithSigs := client.RoleWithSignatures{Role: targetsRole, Signatures: nil} 453 assert.Check(t, is.Equal("Repository Key:\tabc, key11, key99\n", formatAdminRole(targetsRoleWithSigs))) 454 } 455 456 func TestPrintSignerInfoSortOrder(t *testing.T) { 457 roleToKeyIDs := map[string][]string{ 458 "signer2-foo": {"B"}, 459 "signer10-foo": {"C"}, 460 "signer1-foo": {"A"}, 461 } 462 463 expected := `SIGNER KEYS 464 signer1-foo A 465 signer2-foo B 466 signer10-foo C 467 ` 468 buf := new(bytes.Buffer) 469 assert.NilError(t, printSignerInfo(buf, roleToKeyIDs)) 470 assert.Check(t, is.Equal(expected, buf.String())) 471 }