Skip to content

Commit cc84bee

Browse files
committed
refactor: CEL expression for keySelector
Signed-off-by: KevFan <chfan@redhat.com>
1 parent c6650df commit cc84bee

File tree

9 files changed

+160
-74
lines changed

9 files changed

+160
-74
lines changed

api/v1beta3/auth_config_types.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,11 +356,14 @@ type ApiKeyAuthenticationSpec struct {
356356
// +kubebuilder:default:=false
357357
AllNamespaces bool `json:"allNamespaces,omitempty"`
358358

359-
// List of keys within the selected Kubernetes secret that contain valid API credentials.
359+
// A Common Expression Language (CEL) expression that evaluates to a list of string keys within the selected Kubernetes
360+
// secret that contain valid API credentials. The keys of the selected Kubernetes secret are available for evaluation
361+
// in the following structure: `{"keys": ["key1", "key2"]}`.
360362
// Authorino will attempt to authenticate using any matching key. If no keys are defined, the default "api-key" will be used.
361363
// If no match is found, the Kubernetes secret is not considered a valid Authorino API Key secret and is ignored.
364+
// String expressions are supported (https://pkg.go.dev/github.com/google/cel-go/ext#Strings).
362365
// +optional
363-
KeySelectors []string `json:"keySelectors,omitempty"`
366+
KeySelector CelExpression `json:"keySelector,omitempty"`
364367
}
365368

366369
// Settings to fetch the JSON Web Key Set (JWKS) for the JWT authentication.

api/v1beta3/zz_generated.deepcopy.go

Lines changed: 0 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

controllers/auth_config_controller.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,8 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf
261261
if err != nil {
262262
return nil, err
263263
}
264-
translatedIdentity.APIKey = identity_evaluators.NewApiKeyIdentity(identityCfgName, selector, namespace, identity.ApiKey.KeySelectors, authCred, r.Client, ctxWithLogger)
264+
265+
translatedIdentity.APIKey = identity_evaluators.NewApiKeyIdentity(identityCfgName, selector, namespace, string(identity.ApiKey.KeySelector), authCred, r.Client, ctxWithLogger)
265266

266267
// MTLS
267268
case api.X509ClientCertificateAuthentication:

controllers/secret_controller_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ func newSecretReconcilerTest(mockCtrl *gomock.Controller, secretLabels map[strin
9494
indexedAuthConfig := &evaluators.AuthConfig{
9595
Labels: map[string]string{"namespace": "authorino", "name": "api-protection"},
9696
IdentityConfigs: []auth.AuthConfigEvaluator{&fakeAPIKeyIdentityConfig{
97-
evaluator: identity_evaluators.NewApiKeyIdentity("api-key", apiKeyLabelSelectors, "", []string{}, auth.NewAuthCredential("", ""), fakeK8sClient, context.TODO()),
97+
evaluator: identity_evaluators.NewApiKeyIdentity("api-key", apiKeyLabelSelectors, "", "", auth.NewAuthCredential("", ""), fakeK8sClient, context.TODO()),
9898
}},
9999
}
100100
indexMock := mock_index.NewMockIndex(mockCtrl)

install/crd/authorino.kuadrant.io_authconfigs.yaml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2393,14 +2393,15 @@ spec:
23932393
Whether Authorino should look for API key secrets in all namespaces or only in the same namespace as the AuthConfig.
23942394
Enabling this option in namespaced Authorino instances has no effect.
23952395
type: boolean
2396-
keySelectors:
2396+
keySelector:
23972397
description: |-
2398-
List of keys within the selected Kubernetes secret that contain valid API credentials.
2398+
A Common Expression Language (CEL) expression that evaluates to a list of string keys within the selected Kubernetes
2399+
secret that contain valid API credentials. The keys of the selected Kubernetes secret are available for evaluation
2400+
in the following structure: `{"keys": ["key1", "key2"]}`.
23992401
Authorino will attempt to authenticate using any matching key. If no keys are defined, the default "api-key" will be used.
24002402
If no match is found, the Kubernetes secret is not considered a valid Authorino API Key secret and is ignored.
2401-
items:
2402-
type: string
2403-
type: array
2403+
String expressions are supported (https://pkg.go.dev/github.com/google/cel-go/ext#Strings).
2404+
type: string
24042405
selector:
24052406
description: Label selector used by Authorino to match secrets
24062407
from the cluster storing valid credentials to authenticate

install/manifests.yaml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2660,14 +2660,15 @@ spec:
26602660
Whether Authorino should look for API key secrets in all namespaces or only in the same namespace as the AuthConfig.
26612661
Enabling this option in namespaced Authorino instances has no effect.
26622662
type: boolean
2663-
keySelectors:
2663+
keySelector:
26642664
description: |-
2665-
List of keys within the selected Kubernetes secret that contain valid API credentials.
2665+
A Common Expression Language (CEL) expression that evaluates to a list of string keys within the selected Kubernetes
2666+
secret that contain valid API credentials. The keys of the selected Kubernetes secret are available for evaluation
2667+
in the following structure: `{"keys": ["key1", "key2"]}`.
26662668
Authorino will attempt to authenticate using any matching key. If no keys are defined, the default "api-key" will be used.
26672669
If no match is found, the Kubernetes secret is not considered a valid Authorino API Key secret and is ignored.
2668-
items:
2669-
type: string
2670-
type: array
2670+
String expressions are supported (https://pkg.go.dev/github.com/google/cel-go/ext#Strings).
2671+
type: string
26712672
selector:
26722673
description: Label selector used by Authorino to match secrets
26732674
from the cluster storing valid credentials to authenticate

pkg/evaluators/identity/api_key.go

Lines changed: 92 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ package identity
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"sync"
78

9+
"github.com/google/cel-go/common/types"
10+
"github.com/google/cel-go/common/types/ref"
811
"github.com/samber/lo"
912

1013
"github.com/kuadrant/authorino/pkg/auth"
14+
"github.com/kuadrant/authorino/pkg/expressions"
15+
"github.com/kuadrant/authorino/pkg/expressions/cel"
1116
"github.com/kuadrant/authorino/pkg/log"
1217

1318
k8s "k8s.io/api/core/v1"
@@ -17,40 +22,49 @@ import (
1722
)
1823

1924
const (
20-
defaultAPIKeySelector = "api_key"
21-
invalidApiKeyMsg = "the API Key provided is invalid"
22-
credentialsFetchingErrorMsg = "Something went wrong fetching the authorized credentials"
25+
defaultKeySelectorExpression = `['api_key']`
26+
invalidApiKeyMsg = "the API Key provided is invalid"
27+
credentialsFetchingErrorMsg = "Something went wrong fetching the authorized credentials"
2328
)
2429

2530
type APIKey struct {
2631
auth.AuthCredentials
2732

28-
Name string `yaml:"name"`
29-
LabelSelectors k8s_labels.Selector `yaml:"labelSelectors"`
30-
Namespace string `yaml:"namespace"`
31-
KeySelectors []string `yaml:"keySelectors"`
33+
Name string `yaml:"name"`
34+
LabelSelectors k8s_labels.Selector `yaml:"labelSelectors"`
35+
Namespace string `yaml:"namespace"`
36+
KeySelectorExpression expressions.Value `yaml:"keySelector"`
3237

3338
// Map of API Key value to secret
3439
secrets map[string]k8s.Secret
3540
mutex sync.RWMutex
3641
k8sClient k8s_client.Reader
3742
}
3843

39-
func NewApiKeyIdentity(name string, labelSelectors k8s_labels.Selector, namespace string, keySelectors []string, authCred auth.AuthCredentials, k8sClient k8s_client.Reader, ctx context.Context) *APIKey {
40-
if len(keySelectors) == 0 {
41-
keySelectors = append(keySelectors, defaultAPIKeySelector)
44+
func NewApiKeyIdentity(name string, labelSelectors k8s_labels.Selector, namespace string, keySelectorExpression string, authCred auth.AuthCredentials, k8sClient k8s_client.Reader, ctx context.Context) *APIKey {
45+
if keySelectorExpression == "" {
46+
keySelectorExpression = defaultKeySelectorExpression
4247
}
48+
49+
logger := log.FromContext(ctx).WithName("apikey")
50+
51+
expr, err := cel.NewKeySelectorExpression(keySelectorExpression)
52+
if err != nil {
53+
logger.Error(err, "failed to create key selector expression")
54+
return nil
55+
}
56+
4357
apiKey := &APIKey{
44-
AuthCredentials: authCred,
45-
Name: name,
46-
LabelSelectors: labelSelectors,
47-
Namespace: namespace,
48-
KeySelectors: keySelectors,
49-
secrets: make(map[string]k8s.Secret),
50-
k8sClient: k8sClient,
58+
AuthCredentials: authCred,
59+
Name: name,
60+
LabelSelectors: labelSelectors,
61+
Namespace: namespace,
62+
KeySelectorExpression: expr,
63+
secrets: make(map[string]k8s.Secret),
64+
k8sClient: k8sClient,
5165
}
5266
if err := apiKey.loadSecrets(context.TODO()); err != nil {
53-
log.FromContext(ctx).WithName("apikey").Error(err, credentialsFetchingErrorMsg)
67+
logger.Error(err, credentialsFetchingErrorMsg)
5468
}
5569
return apiKey
5670
}
@@ -70,7 +84,7 @@ func (a *APIKey) loadSecrets(ctx context.Context) error {
7084
defer a.mutex.Unlock()
7185

7286
for _, secret := range secretList.Items {
73-
a.appendK8sSecretBasedIdentity(secret)
87+
a.appendK8sSecretBasedIdentity(ctx, secret)
7488
}
7589

7690
return nil
@@ -105,9 +119,6 @@ func (a *APIKey) AddK8sSecretBasedIdentity(ctx context.Context, new k8s.Secret)
105119
return
106120
}
107121

108-
a.mutex.Lock()
109-
defer a.mutex.Unlock()
110-
111122
logger := log.FromContext(ctx).WithName("apikey")
112123

113124
// Get all current keys in the map that match the new secret name and namespace
@@ -116,7 +127,10 @@ func (a *APIKey) AddK8sSecretBasedIdentity(ctx context.Context, new k8s.Secret)
116127
})
117128

118129
// get api keys from new secret
119-
newAPIKeys := a.getValuesFromSecret(new)
130+
newAPIKeys := a.getValuesFromSecret(ctx, new)
131+
132+
a.mutex.Lock()
133+
defer a.mutex.Unlock()
120134

121135
for _, newKey := range newAPIKeys {
122136
a.secrets[newKey] = new
@@ -157,8 +171,8 @@ func (a *APIKey) withinScope(namespace string) bool {
157171

158172
// Appends the K8s Secret to the cache of API keys
159173
// Caution! This function is not thread-safe. Make sure to acquire a lock before calling it.
160-
func (a *APIKey) appendK8sSecretBasedIdentity(secret k8s.Secret) bool {
161-
values := a.getValuesFromSecret(secret)
174+
func (a *APIKey) appendK8sSecretBasedIdentity(ctx context.Context, secret k8s.Secret) bool {
175+
values := a.getValuesFromSecret(ctx, secret)
162176
for _, value := range values {
163177
a.secrets[value] = secret
164178
}
@@ -167,16 +181,61 @@ func (a *APIKey) appendK8sSecretBasedIdentity(secret k8s.Secret) bool {
167181
return len(values) != 0
168182
}
169183

170-
// getValuesFromSecret extracts the values from the secret based on APIKey KeySelectors
171-
func (a *APIKey) getValuesFromSecret(secret k8s.Secret) []string {
172-
var keys []string
173-
for _, key := range a.KeySelectors {
174-
value, isAPIKeySecret := secret.Data[key]
184+
// getValuesFromSecret extracts the values from the secret based on APIKey KeySelector expression
185+
func (a *APIKey) getValuesFromSecret(ctx context.Context, secret k8s.Secret) []string {
186+
logger := log.FromContext(ctx).WithName("apikey")
187+
188+
// Extract secret keys
189+
secretKeys := lo.Keys(secret.Data)
175190

176-
if isAPIKeySecret && len(value) > 0 {
177-
keys = append(keys, string(value))
191+
// Prepare JSON for CEL evaluation
192+
jsonBytes, err := json.Marshal(map[string][]string{cel.RootSecretKeysBinding: secretKeys})
193+
if err != nil {
194+
logger.Error(err, "failed to marshal secret keys to JSON")
195+
return nil
196+
}
197+
198+
// Evaluate CEL expression
199+
evaluated, err := a.KeySelectorExpression.ResolveFor(string(jsonBytes))
200+
if err != nil {
201+
logger.Error(err, "failed to resolve key selector expression")
202+
return nil
203+
}
204+
205+
// Convert evaluated result to a slice of strings
206+
selectedKeys, ok := convertToStringSlice(evaluated)
207+
if !ok {
208+
logger.Error(fmt.Errorf("unexpected type for resolved key"), "expected string or []string", "value", evaluated)
209+
return nil
210+
}
211+
212+
// Extract values for the selected keys
213+
values := make([]string, 0, len(selectedKeys))
214+
for _, key := range selectedKeys {
215+
if v, exists := secret.Data[key]; exists && len(v) > 0 {
216+
values = append(values, string(v))
217+
}
218+
}
219+
220+
return values
221+
}
222+
223+
// Helper function to safely convert an interface{} of type []ref.Val to []string
224+
func convertToStringSlice(value any) ([]string, bool) {
225+
items, ok := value.([]ref.Val)
226+
if !ok {
227+
return nil, false
228+
}
229+
230+
out := make([]string, len(items))
231+
for i, item := range items {
232+
if item.Type() == types.StringType {
233+
out[i] = item.Value().(string)
234+
} else {
235+
// unexpected type
236+
return nil, false
178237
}
179238
}
180239

181-
return keys
240+
return out, true
182241
}

pkg/evaluators/identity/api_key_test.go

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ var (
2323
)
2424

2525
func TestConstants(t *testing.T) {
26-
assert.Equal(t, defaultAPIKeySelector, "api_key")
26+
assert.Equal(t, defaultKeySelectorExpression, `['api_key']`)
2727
assert.Equal(t, invalidApiKeyMsg, "the API Key provided is invalid")
2828
}
2929

@@ -32,13 +32,11 @@ func TestNewApiKeyIdentityAllNamespaces(t *testing.T) {
3232
defer ctrl.Finish()
3333

3434
selector, _ := k8s_labels.Parse("planet=coruscant")
35-
apiKey := NewApiKeyIdentity("jedi", selector, "", []string{}, mock_auth.NewMockAuthCredentials(ctrl), testAPIKeyK8sClient, context.TODO())
35+
apiKey := NewApiKeyIdentity("jedi", selector, "", "", mock_auth.NewMockAuthCredentials(ctrl), testAPIKeyK8sClient, context.TODO())
3636

3737
assert.Equal(t, apiKey.Name, "jedi")
3838
assert.Equal(t, apiKey.LabelSelectors.String(), "planet=coruscant")
3939
assert.Equal(t, apiKey.Namespace, "")
40-
assert.Equal(t, len(apiKey.KeySelectors), 1)
41-
assert.Equal(t, apiKey.KeySelectors[0], defaultAPIKeySelector)
4240
assert.Equal(t, len(apiKey.secrets), 2)
4341
_, exists := apiKey.secrets["ObiWanKenobiLightSaber"]
4442
assert.Check(t, exists)
@@ -53,13 +51,11 @@ func TestNewApiKeyIdentitySingleNamespace(t *testing.T) {
5351
defer ctrl.Finish()
5452

5553
selector, _ := k8s_labels.Parse("planet=coruscant")
56-
apiKey := NewApiKeyIdentity("jedi", selector, "ns1", []string{}, mock_auth.NewMockAuthCredentials(ctrl), testAPIKeyK8sClient, context.TODO())
54+
apiKey := NewApiKeyIdentity("jedi", selector, "ns1", "", mock_auth.NewMockAuthCredentials(ctrl), testAPIKeyK8sClient, context.TODO())
5755

5856
assert.Equal(t, apiKey.Name, "jedi")
5957
assert.Equal(t, apiKey.LabelSelectors.String(), "planet=coruscant")
6058
assert.Equal(t, apiKey.Namespace, "ns1")
61-
assert.Equal(t, len(apiKey.KeySelectors), 1)
62-
assert.Equal(t, apiKey.KeySelectors[0], defaultAPIKeySelector)
6359
assert.Equal(t, len(apiKey.secrets), 1)
6460
_, exists := apiKey.secrets["ObiWanKenobiLightSaber"]
6561
assert.Check(t, exists)
@@ -74,14 +70,11 @@ func TestNewApiKeyIdentityMultipleKeySelectors(t *testing.T) {
7470
defer ctrl.Finish()
7571

7672
selector, _ := k8s_labels.Parse("planet=coruscant")
77-
apiKey := NewApiKeyIdentity("jedi", selector, "ns1", []string{"no_op", "api_key_2"}, mock_auth.NewMockAuthCredentials(ctrl), testAPIKeyK8sClient, context.TODO())
73+
apiKey := NewApiKeyIdentity("jedi", selector, "ns1", "['no_op','api_key_2']", mock_auth.NewMockAuthCredentials(ctrl), testAPIKeyK8sClient, context.TODO())
7874

7975
assert.Equal(t, apiKey.Name, "jedi")
8076
assert.Equal(t, apiKey.LabelSelectors.String(), "planet=coruscant")
8177
assert.Equal(t, apiKey.Namespace, "ns1")
82-
assert.Equal(t, len(apiKey.KeySelectors), 2)
83-
assert.Equal(t, apiKey.KeySelectors[0], "no_op")
84-
assert.Equal(t, apiKey.KeySelectors[1], "api_key_2")
8578
assert.Equal(t, len(apiKey.secrets), 1)
8679
_, exists := apiKey.secrets["ObiWanKenobiLightSaber"]
8780
assert.Check(t, !exists)
@@ -102,7 +95,7 @@ func TestCallSuccess(t *testing.T) {
10295
authCredMock.EXPECT().GetCredentialsFromReq(gomock.Any()).Return("ObiWanKenobiLightSaber", nil)
10396

10497
selector, _ := k8s_labels.Parse("planet=coruscant")
105-
apiKey := NewApiKeyIdentity("jedi", selector, "", []string{}, authCredMock, testAPIKeyK8sClient, context.TODO())
98+
apiKey := NewApiKeyIdentity("jedi", selector, "", "", authCredMock, testAPIKeyK8sClient, context.TODO())
10699
auth, err := apiKey.Call(pipelineMock, context.TODO())
107100

108101
assert.NilError(t, err)
@@ -118,7 +111,7 @@ func TestCallNoApiKeyFail(t *testing.T) {
118111
authCredMock.EXPECT().GetCredentialsFromReq(gomock.Any()).Return("", fmt.Errorf("something went wrong getting the API Key"))
119112

120113
selector, _ := k8s_labels.Parse("planet=coruscant")
121-
apiKey := NewApiKeyIdentity("jedi", selector, "", []string{}, authCredMock, testAPIKeyK8sClient, context.TODO())
114+
apiKey := NewApiKeyIdentity("jedi", selector, "", "", authCredMock, testAPIKeyK8sClient, context.TODO())
122115

123116
_, err := apiKey.Call(pipelineMock, context.TODO())
124117

@@ -134,15 +127,15 @@ func TestCallInvalidApiKeyFail(t *testing.T) {
134127
authCredMock.EXPECT().GetCredentialsFromReq(gomock.Any()).Return("ASithLightSaber", nil)
135128

136129
selector, _ := k8s_labels.Parse("planet=coruscant")
137-
apiKey := NewApiKeyIdentity("jedi", selector, "", []string{}, authCredMock, testAPIKeyK8sClient, context.TODO())
130+
apiKey := NewApiKeyIdentity("jedi", selector, "", "", authCredMock, testAPIKeyK8sClient, context.TODO())
138131
_, err := apiKey.Call(pipelineMock, context.TODO())
139132

140133
assert.Error(t, err, "the API Key provided is invalid")
141134
}
142135

143136
func TestLoadSecretsSuccess(t *testing.T) {
144137
selector, _ := k8s_labels.Parse("planet=coruscant")
145-
apiKey := NewApiKeyIdentity("X-API-KEY", selector, "", []string{}, nil, testAPIKeyK8sClient, nil)
138+
apiKey := NewApiKeyIdentity("X-API-KEY", selector, "", "", nil, testAPIKeyK8sClient, nil)
146139

147140
err := apiKey.loadSecrets(context.TODO())
148141
assert.NilError(t, err)
@@ -159,7 +152,7 @@ func TestLoadSecretsSuccess(t *testing.T) {
159152

160153
func TestLoadSecretsFail(t *testing.T) {
161154
selector, _ := k8s_labels.Parse("planet=coruscant")
162-
apiKey := NewApiKeyIdentity("X-API-KEY", selector, "", []string{}, nil, &flawedAPIkeyK8sClient{}, context.TODO())
155+
apiKey := NewApiKeyIdentity("X-API-KEY", selector, "", "", nil, &flawedAPIkeyK8sClient{}, context.TODO())
163156

164157
err := apiKey.loadSecrets(context.TODO())
165158
assert.Error(t, err, "something terribly wrong happened")
@@ -173,7 +166,7 @@ func BenchmarkAPIKeyAuthn(b *testing.B) {
173166
authCredMock := mock_auth.NewMockAuthCredentials(ctrl)
174167
authCredMock.EXPECT().GetCredentialsFromReq(gomock.Any()).Return("ObiWanKenobiLightSaber", nil).MinTimes(1)
175168
selector, _ := k8s_labels.Parse("planet=coruscant")
176-
apiKey := NewApiKeyIdentity("jedi", selector, "", []string{}, authCredMock, testAPIKeyK8sClient, context.TODO())
169+
apiKey := NewApiKeyIdentity("jedi", selector, "", "", authCredMock, testAPIKeyK8sClient, context.TODO())
177170

178171
var err error
179172
b.ResetTimer()

0 commit comments

Comments
 (0)