github.com/cilium/cilium@v1.16.2/operator/auth/spire/client_test.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package spire 5 6 import ( 7 "context" 8 "fmt" 9 "reflect" 10 "testing" 11 12 entryv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/entry/v1" 13 "github.com/spiffe/spire-api-sdk/proto/spire/api/types" 14 "github.com/stretchr/testify/require" 15 "google.golang.org/grpc" 16 corev1 "k8s.io/api/core/v1" 17 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 19 "github.com/cilium/cilium/pkg/k8s/client" 20 ) 21 22 type mockEntryClient struct { 23 ListEntriesFunc func(ctx context.Context, in *entryv1.ListEntriesRequest, opts ...grpc.CallOption) (*entryv1.ListEntriesResponse, error) 24 BatchCreateEntryFunc func(ctx context.Context, in *entryv1.BatchCreateEntryRequest, opts ...grpc.CallOption) (*entryv1.BatchCreateEntryResponse, error) 25 BatchUpdateEntryFunc func(ctx context.Context, in *entryv1.BatchUpdateEntryRequest, opts ...grpc.CallOption) (*entryv1.BatchUpdateEntryResponse, error) 26 BatchDeleteEntryFunc func(ctx context.Context, in *entryv1.BatchDeleteEntryRequest, opts ...grpc.CallOption) (*entryv1.BatchDeleteEntryResponse, error) 27 SyncAuthorizedEntriesFunc func(ctx context.Context, opts ...grpc.CallOption) (entryv1.Entry_SyncAuthorizedEntriesClient, error) 28 } 29 30 func (m mockEntryClient) CountEntries(ctx context.Context, in *entryv1.CountEntriesRequest, opts ...grpc.CallOption) (*entryv1.CountEntriesResponse, error) { 31 panic("implement me") 32 } 33 34 func (m mockEntryClient) ListEntries(ctx context.Context, in *entryv1.ListEntriesRequest, opts ...grpc.CallOption) (*entryv1.ListEntriesResponse, error) { 35 return m.ListEntriesFunc(ctx, in, opts...) 36 } 37 38 func (m mockEntryClient) GetEntry(ctx context.Context, in *entryv1.GetEntryRequest, opts ...grpc.CallOption) (*types.Entry, error) { 39 panic("implement me") 40 } 41 42 func (m mockEntryClient) BatchCreateEntry(ctx context.Context, in *entryv1.BatchCreateEntryRequest, opts ...grpc.CallOption) (*entryv1.BatchCreateEntryResponse, error) { 43 return m.BatchCreateEntryFunc(ctx, in, opts...) 44 } 45 46 func (m mockEntryClient) BatchUpdateEntry(ctx context.Context, in *entryv1.BatchUpdateEntryRequest, opts ...grpc.CallOption) (*entryv1.BatchUpdateEntryResponse, error) { 47 return m.BatchUpdateEntryFunc(ctx, in, opts...) 48 } 49 50 func (m mockEntryClient) BatchDeleteEntry(ctx context.Context, in *entryv1.BatchDeleteEntryRequest, opts ...grpc.CallOption) (*entryv1.BatchDeleteEntryResponse, error) { 51 return m.BatchDeleteEntryFunc(ctx, in, opts...) 52 } 53 54 func (m mockEntryClient) GetAuthorizedEntries(ctx context.Context, in *entryv1.GetAuthorizedEntriesRequest, opts ...grpc.CallOption) (*entryv1.GetAuthorizedEntriesResponse, error) { 55 panic("implement me") 56 } 57 58 func (m mockEntryClient) SyncAuthorizedEntries(ctx context.Context, opts ...grpc.CallOption) (entryv1.Entry_SyncAuthorizedEntriesClient, error) { 59 panic("implement me") 60 } 61 62 func TestClient_Upsert(t *testing.T) { 63 cfg := ClientConfig{ 64 SpiffeTrustDomain: "dummy.trusted.domain", 65 } 66 type fields struct { 67 entry entryv1.EntryClient 68 } 69 type args struct { 70 id string 71 } 72 tests := []struct { 73 name string 74 fields fields 75 args args 76 wantErr bool 77 }{ 78 { 79 name: "client not initialized", 80 wantErr: true, 81 }, 82 { 83 name: "unable to list entry due to unknown error", 84 args: args{ 85 id: "dummy-id", 86 }, 87 fields: fields{ 88 entry: mockEntryClient{ 89 ListEntriesFunc: func(ctx context.Context, in *entryv1.ListEntriesRequest, opts ...grpc.CallOption) (*entryv1.ListEntriesResponse, error) { 90 require.Equal(t, in, &entryv1.ListEntriesRequest{ 91 Filter: &entryv1.ListEntriesRequest_Filter{ 92 BySpiffeId: &types.SPIFFEID{ 93 TrustDomain: "dummy.trusted.domain", 94 Path: "/identity/dummy-id", 95 }, 96 ByParentId: &types.SPIFFEID{ 97 TrustDomain: "dummy.trusted.domain", 98 Path: "/cilium-operator", 99 }, 100 BySelectors: &types.SelectorMatch{ 101 Selectors: []*types.Selector{ 102 { 103 Type: "cilium", 104 Value: "mutual-auth", 105 }, 106 }, 107 Match: types.SelectorMatch_MATCH_EXACT, 108 }, 109 }, 110 }) 111 return nil, fmt.Errorf("something is wrong") 112 }, 113 }, 114 }, 115 wantErr: true, 116 }, 117 { 118 name: "entry does not exist with not found error", 119 args: args{ 120 id: "dummy-id", 121 }, 122 fields: fields{ 123 entry: mockEntryClient{ 124 ListEntriesFunc: func(ctx context.Context, in *entryv1.ListEntriesRequest, opts ...grpc.CallOption) (*entryv1.ListEntriesResponse, error) { 125 require.Equal(t, in, &entryv1.ListEntriesRequest{ 126 Filter: &entryv1.ListEntriesRequest_Filter{ 127 BySpiffeId: &types.SPIFFEID{ 128 TrustDomain: "dummy.trusted.domain", 129 Path: "/identity/dummy-id", 130 }, 131 ByParentId: &types.SPIFFEID{ 132 TrustDomain: "dummy.trusted.domain", 133 Path: "/cilium-operator", 134 }, 135 BySelectors: &types.SelectorMatch{ 136 Selectors: []*types.Selector{ 137 { 138 Type: "cilium", 139 Value: "mutual-auth", 140 }, 141 }, 142 Match: types.SelectorMatch_MATCH_EXACT, 143 }, 144 }, 145 }) 146 return nil, fmt.Errorf("NotFound") 147 }, 148 BatchCreateEntryFunc: func(ctx context.Context, in *entryv1.BatchCreateEntryRequest, opts ...grpc.CallOption) (*entryv1.BatchCreateEntryResponse, error) { 149 require.ElementsMatch(t, in.Entries, []*types.Entry{ 150 { 151 SpiffeId: &types.SPIFFEID{ 152 TrustDomain: "dummy.trusted.domain", 153 Path: "/identity/dummy-id", 154 }, 155 ParentId: &types.SPIFFEID{ 156 TrustDomain: "dummy.trusted.domain", 157 Path: "/cilium-operator", 158 }, 159 Selectors: defaultSelectors, 160 }, 161 }) 162 return &entryv1.BatchCreateEntryResponse{}, nil 163 }, 164 }, 165 }, 166 }, 167 { 168 name: "entry exists", 169 args: args{ 170 id: "dummy-id", 171 }, 172 fields: fields{ 173 entry: mockEntryClient{ 174 ListEntriesFunc: func(ctx context.Context, in *entryv1.ListEntriesRequest, opts ...grpc.CallOption) (*entryv1.ListEntriesResponse, error) { 175 require.Equal(t, in, &entryv1.ListEntriesRequest{ 176 Filter: &entryv1.ListEntriesRequest_Filter{ 177 BySpiffeId: &types.SPIFFEID{ 178 TrustDomain: "dummy.trusted.domain", 179 Path: "/identity/dummy-id", 180 }, 181 ByParentId: &types.SPIFFEID{ 182 TrustDomain: "dummy.trusted.domain", 183 Path: "/cilium-operator", 184 }, 185 BySelectors: &types.SelectorMatch{ 186 Selectors: []*types.Selector{ 187 { 188 Type: "cilium", 189 Value: "mutual-auth", 190 }, 191 }, 192 Match: types.SelectorMatch_MATCH_EXACT, 193 }, 194 }, 195 }) 196 return &entryv1.ListEntriesResponse{ 197 Entries: []*types.Entry{{}}, 198 }, nil 199 }, 200 BatchUpdateEntryFunc: func(ctx context.Context, in *entryv1.BatchUpdateEntryRequest, opts ...grpc.CallOption) (*entryv1.BatchUpdateEntryResponse, error) { 201 require.ElementsMatch(t, in.Entries, []*types.Entry{ 202 { 203 SpiffeId: &types.SPIFFEID{ 204 TrustDomain: "dummy.trusted.domain", 205 Path: "/identity/dummy-id", 206 }, 207 ParentId: &types.SPIFFEID{ 208 TrustDomain: "dummy.trusted.domain", 209 Path: "/cilium-operator", 210 }, 211 Selectors: defaultSelectors, 212 }, 213 }) 214 return &entryv1.BatchUpdateEntryResponse{}, nil 215 }, 216 }, 217 }, 218 }, 219 } 220 for _, tt := range tests { 221 t.Run(tt.name, func(t *testing.T) { 222 c := &Client{ 223 cfg: cfg, 224 entry: tt.fields.entry, 225 } 226 if err := c.Upsert(context.Background(), tt.args.id); (err != nil) != tt.wantErr { 227 t.Errorf("Upsert() error = %v, wantErr %v", err, tt.wantErr) 228 } 229 }) 230 } 231 } 232 233 func TestClient_Delete(t *testing.T) { 234 cfg := ClientConfig{ 235 SpiffeTrustDomain: "dummy.trusted.domain", 236 } 237 type fields struct { 238 entry entryv1.EntryClient 239 } 240 type args struct { 241 id string 242 } 243 tests := []struct { 244 name string 245 fields fields 246 args args 247 wantErr bool 248 }{ 249 { 250 name: "client not initialized", 251 wantErr: true, 252 }, 253 { 254 name: "unable to list entries due to unknown error", 255 args: args{ 256 id: "dummy-id", 257 }, 258 fields: fields{ 259 entry: mockEntryClient{ 260 ListEntriesFunc: func(ctx context.Context, in *entryv1.ListEntriesRequest, opts ...grpc.CallOption) (*entryv1.ListEntriesResponse, error) { 261 require.Equal(t, in, &entryv1.ListEntriesRequest{ 262 Filter: &entryv1.ListEntriesRequest_Filter{ 263 BySpiffeId: &types.SPIFFEID{ 264 TrustDomain: "dummy.trusted.domain", 265 Path: "/identity/dummy-id", 266 }, 267 ByParentId: &types.SPIFFEID{ 268 TrustDomain: "dummy.trusted.domain", 269 Path: "/cilium-operator", 270 }, 271 BySelectors: &types.SelectorMatch{ 272 Selectors: []*types.Selector{ 273 { 274 Type: "cilium", 275 Value: "mutual-auth", 276 }, 277 }, 278 Match: types.SelectorMatch_MATCH_EXACT, 279 }, 280 }, 281 }) 282 return nil, fmt.Errorf("something is wrong") 283 }, 284 }, 285 }, 286 wantErr: true, 287 }, 288 { 289 name: "unable to list entries due to not found error", 290 args: args{ 291 id: "dummy-id", 292 }, 293 fields: fields{ 294 entry: mockEntryClient{ 295 ListEntriesFunc: func(ctx context.Context, in *entryv1.ListEntriesRequest, opts ...grpc.CallOption) (*entryv1.ListEntriesResponse, error) { 296 require.Equal(t, in, &entryv1.ListEntriesRequest{ 297 Filter: &entryv1.ListEntriesRequest_Filter{ 298 BySpiffeId: &types.SPIFFEID{ 299 TrustDomain: "dummy.trusted.domain", 300 Path: "/identity/dummy-id", 301 }, 302 ByParentId: &types.SPIFFEID{ 303 TrustDomain: "dummy.trusted.domain", 304 Path: "/cilium-operator", 305 }, 306 BySelectors: &types.SelectorMatch{ 307 Selectors: []*types.Selector{ 308 { 309 Type: "cilium", 310 Value: "mutual-auth", 311 }, 312 }, 313 Match: types.SelectorMatch_MATCH_EXACT, 314 }, 315 }, 316 }) 317 return nil, fmt.Errorf("NotFound") 318 }, 319 }, 320 }, 321 }, 322 { 323 name: "entry does not exist", 324 args: args{ 325 id: "dummy-id", 326 }, 327 fields: fields{ 328 entry: mockEntryClient{ 329 ListEntriesFunc: func(ctx context.Context, in *entryv1.ListEntriesRequest, opts ...grpc.CallOption) (*entryv1.ListEntriesResponse, error) { 330 require.Equal(t, in, &entryv1.ListEntriesRequest{ 331 Filter: &entryv1.ListEntriesRequest_Filter{ 332 BySpiffeId: &types.SPIFFEID{ 333 TrustDomain: "dummy.trusted.domain", 334 Path: "/identity/dummy-id", 335 }, 336 ByParentId: &types.SPIFFEID{ 337 TrustDomain: "dummy.trusted.domain", 338 Path: "/cilium-operator", 339 }, 340 BySelectors: &types.SelectorMatch{ 341 Selectors: []*types.Selector{ 342 { 343 Type: "cilium", 344 Value: "mutual-auth", 345 }, 346 }, 347 Match: types.SelectorMatch_MATCH_EXACT, 348 }, 349 }, 350 }) 351 return &entryv1.ListEntriesResponse{}, nil 352 }, 353 }, 354 }, 355 }, 356 { 357 name: "entry exists", 358 args: args{ 359 id: "dummy-id", 360 }, 361 fields: fields{ 362 entry: mockEntryClient{ 363 ListEntriesFunc: func(ctx context.Context, in *entryv1.ListEntriesRequest, opts ...grpc.CallOption) (*entryv1.ListEntriesResponse, error) { 364 require.Equal(t, in, &entryv1.ListEntriesRequest{ 365 Filter: &entryv1.ListEntriesRequest_Filter{ 366 BySpiffeId: &types.SPIFFEID{ 367 TrustDomain: "dummy.trusted.domain", 368 Path: "/identity/dummy-id", 369 }, 370 ByParentId: &types.SPIFFEID{ 371 TrustDomain: "dummy.trusted.domain", 372 Path: "/cilium-operator", 373 }, 374 BySelectors: &types.SelectorMatch{ 375 Selectors: []*types.Selector{ 376 { 377 Type: "cilium", 378 Value: "mutual-auth", 379 }, 380 }, 381 Match: types.SelectorMatch_MATCH_EXACT, 382 }, 383 }, 384 }) 385 return &entryv1.ListEntriesResponse{ 386 Entries: []*types.Entry{{ 387 Id: "auto-generated-dummy-id", 388 }}, 389 }, nil 390 }, 391 BatchDeleteEntryFunc: func(ctx context.Context, in *entryv1.BatchDeleteEntryRequest, opts ...grpc.CallOption) (*entryv1.BatchDeleteEntryResponse, error) { 392 require.Equal(t, in, &entryv1.BatchDeleteEntryRequest{ 393 Ids: []string{"auto-generated-dummy-id"}, 394 }) 395 return &entryv1.BatchDeleteEntryResponse{}, nil 396 }, 397 }, 398 }, 399 }, 400 } 401 for _, tt := range tests { 402 t.Run(tt.name, func(t *testing.T) { 403 c := &Client{ 404 cfg: cfg, 405 entry: tt.fields.entry, 406 } 407 if err := c.Delete(context.Background(), tt.args.id); (err != nil) != tt.wantErr { 408 t.Errorf("Delete() error = %v, wantErr %v", err, tt.wantErr) 409 } 410 }) 411 } 412 } 413 414 func Test_resolvedK8sService(t *testing.T) { 415 _, c := client.NewFakeClientset() 416 _, _ = c.CoreV1().Services("dummy-namespace").Create(context.Background(), &corev1.Service{ 417 ObjectMeta: metav1.ObjectMeta{ 418 Name: "valid-service", 419 }, 420 Spec: corev1.ServiceSpec{ 421 ClusterIP: "10.0.0.1", 422 }, 423 Status: corev1.ServiceStatus{}, 424 }, metav1.CreateOptions{}) 425 type args struct { 426 client client.Clientset 427 address string 428 } 429 tests := []struct { 430 name string 431 args args 432 want *string 433 wantedErr error 434 }{ 435 { 436 name: "address not following <service-name>.<ns>.svc(.*) format", 437 args: args{ 438 address: "192.168.0.1:8081", 439 }, 440 want: addressOf("192.168.0.1:8081"), 441 }, 442 { 443 name: "another address not following <service-name>.<ns>.svc(.*) format", 444 args: args{ 445 address: "my-spire-server.com:8081", 446 }, 447 want: addressOf("my-spire-server.com:8081"), 448 }, 449 { 450 name: "invalid service dns", 451 args: args{ 452 address: "dummy-service.ns.svc:8081", 453 client: c, 454 }, 455 wantedErr: fmt.Errorf("services \"dummy-service\" not found"), 456 }, 457 { 458 name: "valid k8s service dns, but no port", 459 args: args{ 460 address: "valid-service.dummy-namespace.svc", 461 client: c, 462 }, 463 wantedErr: fmt.Errorf("address valid-service.dummy-namespace.svc: missing port in address"), 464 }, 465 { 466 name: "valid k8s service dns", 467 args: args{ 468 address: "valid-service.dummy-namespace.svc:8081", 469 client: c, 470 }, 471 want: addressOf("10.0.0.1:8081"), 472 }, 473 } 474 for _, tt := range tests { 475 t.Run(tt.name, func(t *testing.T) { 476 got, err := resolvedK8sService(context.Background(), tt.args.client, tt.args.address) 477 if tt.wantedErr != nil && (err == nil || !reflect.DeepEqual(err.Error(), tt.wantedErr.Error())) { 478 t.Errorf("resolvedK8sService() error = %v, wantErr %v", err, tt.wantedErr) 479 return 480 } 481 if !reflect.DeepEqual(got, tt.want) { 482 t.Errorf("resolvedK8sService() got = %v, want %v", got, tt.want) 483 } 484 }) 485 } 486 } 487 488 func addressOf[T any](v T) *T { 489 return &v 490 }