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  }