-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add governor api call provision to send cluster telemetry data Signed-off-by: Nootan Singh <[email protected]> * Added clusterId, url and apiToken as part of governor policy Signed-off-by: Nootan Singh <[email protected]> * [BEGONIA-62] ADD CSP support in agent * [BEGONIA-71] Add CSP implementation --> Added code to get CSP api-token from secret --> Added code to send request to CSP and get access token for governor backend --> Added code to send token with every request to governor backend * refactored some code * removed unused file * removed unused file 1 * removed unused file 2 * refactored some code * removed unwanted changes * removed gitignore file changes * ran make manifests to generate manifests automatically * removed dependecy on csp gitlab library * Added UTs and addressed comments Signed-off-by: Nootan Singh <[email protected]> * added UTs Signed-off-by: harshsharma071988 <[email protected]> * setting correct context while update telemetry Signed-off-by: harshsharma071988 <[email protected]> * fixed governor url config and governor api response status Signed-off-by: harshsharma071988 <[email protected]> * Added server url of governor api Signed-off-by: Nootan Singh <[email protected]> * changed accessSecret to API_TOKEN Signed-off-by: harshsharma071988 <[email protected]> * handled empty namespace case and empty workload case * Added UTs for new files * Resolved minor comments * resolved minor comments Signed-off-by: harshsharma071988 <[email protected]> * removed compilation issue with %w Signed-off-by: harshsharma071988 <[email protected]> * CSP Refresh used using config maps + UTs added * removed fmt and used log Signed-off-by: harshsharma071988 <[email protected]> * Used Errorf in place of Error for logging Signed-off-by: harshsharma071988 <[email protected]> * changed comment Signed-off-by: harshsharma071988 <[email protected]> * Used Secret in place of ConfigMap for storing access token of governor. --------- Signed-off-by: Nootan Singh <[email protected]> Signed-off-by: harshsharma071988 <[email protected]> Co-authored-by: Nootan Singh <[email protected]>
- Loading branch information
1 parent
2e13bcf
commit 1ded538
Showing
17 changed files
with
865 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
package cspauth | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"github.com/vmware-tanzu/cloud-native-security-inspector/src/lib/log" | ||
"github.com/vmware-tanzu/cloud-native-security-inspector/src/lib/retry" | ||
v12 "k8s.io/api/core/v1" | ||
"k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/client-go/kubernetes" | ||
"math" | ||
"time" | ||
) | ||
|
||
const ( | ||
tokenMaxAgeSeconds = 1700 | ||
apiToken = "API_TOKEN" | ||
accessTokenSecretName = "governor-accesstoken" | ||
governorTokenExpiresIn = "governorAccessTokenExpiresIn" | ||
governorAccessTokenKey = "governorAccessToken" | ||
Retry = 3 | ||
) | ||
|
||
var RetryDelay time.Duration = 5 | ||
|
||
// Provider is an interface to interact with an authorization service | ||
type Provider interface { | ||
// GetBearerToken retrieves a short-lived access token to use in a single HTTP request | ||
GetBearerToken(kubernetes.Interface, context.Context, string, string) (string, error) | ||
} | ||
|
||
type CspAuth struct { | ||
CspClient CSPClient | ||
|
||
apiToken string | ||
} | ||
|
||
func (a *CspAuth) GetBearerToken(clientSet kubernetes.Interface, ctx context.Context, cspSecretNamespace string, cspSecretName string) (string, error) { | ||
accessSecret, err := getOrCreateSecretForAccessToken(clientSet, ctx, cspSecretNamespace) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
accessToken := string(accessSecret.Data[governorAccessTokenKey]) | ||
expiresIn := string(accessSecret.Data[governorTokenExpiresIn]) | ||
accessTokenExpiresIn, _ := time.Parse(time.Layout, expiresIn) | ||
|
||
if accessToken == "" || time.Now().After(accessTokenExpiresIn) { | ||
apiToken, err := getCSPTokenFromSecret(clientSet, ctx, cspSecretNamespace, cspSecretName) | ||
if err != nil { | ||
return "", fmt.Errorf("Failed to fetch CSP api-token: %w", err) | ||
} | ||
a.apiToken = apiToken | ||
if err := a.refreshToken(ctx, clientSet, cspSecretNamespace, accessSecret); err != nil { | ||
return "", err | ||
} | ||
} | ||
return string(accessSecret.Data[governorAccessTokenKey]), nil | ||
} | ||
|
||
func (a *CspAuth) refreshToken(ctx context.Context, clientSet kubernetes.Interface, cspSecretNamespace string, accessTokenSecret *v12.Secret) error { | ||
return retry.NewRetry( | ||
retry.WithName("auth token refresh"), | ||
retry.WithMaxAttempts(Retry), | ||
retry.WithIncrementDelay(RetryDelay*time.Second, RetryDelay*time.Second), | ||
).Run(ctx, func() (bool, error) { | ||
now := time.Now() | ||
cspAuthResponse, err := a.CspClient.GetCspAuthorization(ctx, a.apiToken) | ||
if err != nil { | ||
log.Error(err, "We got an error back from CSP") | ||
return false, nil | ||
} | ||
|
||
expiresIn := time.Duration(math.Min(float64(cspAuthResponse.ExpiresIn), tokenMaxAgeSeconds)) * time.Second | ||
formattedExpiration := now.Add(expiresIn).Format(time.Layout) | ||
|
||
log.Infof("Refreshed access token for governor: %s which expires in %s", cspAuthResponse.AccessToken, formattedExpiration) | ||
accessTokenSecret.Data[governorAccessTokenKey] = []byte(cspAuthResponse.AccessToken) | ||
accessTokenSecret.Data[governorTokenExpiresIn] = []byte(formattedExpiration) | ||
_, err = clientSet.CoreV1().Secrets(cspSecretNamespace).Update(ctx, accessTokenSecret, v1.UpdateOptions{}) | ||
if err != nil { | ||
log.Error(err, "We got an error updating access token secret") | ||
return false, nil | ||
} | ||
log.Infof("Obtained CSP access token, next refresh in %s\n", expiresIn) | ||
return true, nil | ||
}) | ||
} | ||
|
||
func getCSPTokenFromSecret(clientSet kubernetes.Interface, ctx context.Context, ns string, secretName string) (string, error) { | ||
secret, err := clientSet.CoreV1().Secrets(ns).Get(ctx, secretName, v1.GetOptions{}) | ||
if err != nil { | ||
log.Error(err, "Failed to fetch secret") | ||
return "", err | ||
} | ||
cspApiToken := string(secret.Data[apiToken]) | ||
return cspApiToken, err | ||
} | ||
|
||
func getOrCreateSecretForAccessToken(clientSet kubernetes.Interface, ctx context.Context, ns string) (*v12.Secret, error) { | ||
secret, err := clientSet.CoreV1().Secrets(ns).Get(ctx, accessTokenSecretName, v1.GetOptions{}) | ||
if err != nil { | ||
log.Warning(err, "Failed to fetch secret for access token, Now Trying to create new secret for same") | ||
secret = &v12.Secret{} | ||
secret.Name = accessTokenSecretName | ||
secret.Namespace = ns | ||
secret.Data = map[string][]byte{} | ||
secret, err = clientSet.CoreV1().Secrets(ns).Create(ctx, secret, v1.CreateOptions{}) | ||
if err != nil { | ||
log.Error(err, "Failed to create secret for storing access token.") | ||
return nil, err | ||
} | ||
} | ||
return secret, err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
package cspauth | ||
|
||
import ( | ||
"context" | ||
v12 "k8s.io/api/core/v1" | ||
"k8s.io/apimachinery/pkg/runtime" | ||
"k8s.io/client-go/kubernetes/fake" | ||
"testing" | ||
) | ||
|
||
const ( | ||
ApiToken = "API_TOKEN" | ||
GovernorAccessTokenKey = "governorAccessToken" | ||
) | ||
|
||
func TestNewCSPAuthSuccessCase(t *testing.T) { | ||
|
||
RetryDelay = 1 | ||
secret := &v12.Secret{} | ||
secret.Name = "csp-secret" | ||
secret.Namespace = "csp-namespace" | ||
secret.Data = map[string][]byte{ApiToken: []byte("test-api-token")} | ||
|
||
errorSecret := &v12.Secret{} | ||
errorSecret.Name = "csp-secret" | ||
errorSecret.Namespace = "csp-namespace" | ||
errorSecret.Data = map[string][]byte{ApiToken: []byte(SendError)} | ||
|
||
accessSecret := &v12.Secret{} | ||
accessSecret.Name = "governor-accesstoken" | ||
accessSecret.Namespace = "csp-namespace" | ||
accessSecret.Data = map[string][]byte{GovernorAccessTokenKey: []byte("test-access-token")} | ||
|
||
tt := []struct { | ||
name string | ||
secretObject *v12.Secret | ||
accessSecret *v12.Secret | ||
wantErr bool | ||
}{ | ||
{ | ||
name: "Get CSP Auth should Pass", | ||
secretObject: secret, | ||
accessSecret: accessSecret, | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "Get CSP Auth should fail because no secret found for csp api-token", | ||
secretObject: nil, | ||
accessSecret: accessSecret, | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "Get CSP Auth should fail with giving up refresh retry(3times)", | ||
secretObject: errorSecret, | ||
accessSecret: accessSecret, | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "Get CSP Auth should pass with accessSecret not found", | ||
secretObject: secret, | ||
accessSecret: nil, | ||
wantErr: false, | ||
}, | ||
} | ||
|
||
for i := range tt { | ||
tc := tt[i] | ||
|
||
t.Run(tc.name, func(t *testing.T) { | ||
t.Parallel() | ||
|
||
objects := make([]runtime.Object, 0) | ||
|
||
if tc.secretObject != nil { | ||
objects = append(objects, tc.secretObject) | ||
} | ||
if tc.accessSecret != nil { | ||
objects = append(objects, tc.accessSecret) | ||
} | ||
clientSet := fake.NewSimpleClientset(objects...) | ||
|
||
tokenManager := NewMockCSPClient() | ||
provider := &CspAuth{CspClient: tokenManager} | ||
auth, err := provider.GetBearerToken(clientSet, context.Background(), secret.Namespace, secret.Name) | ||
|
||
if tc.wantErr && (auth != "" || err == nil) { | ||
t.Fatal("NewCSPAuth call failed on tc: " + tc.name) | ||
} | ||
|
||
if !tc.wantErr && (auth == "" || err != nil) { | ||
t.Fatal("NewCSPAuth call failed on tc: " + tc.name) | ||
} | ||
}) | ||
} | ||
|
||
} | ||
|
||
func TestGetBearerTokenSuccess(t *testing.T) { | ||
secret := &v12.Secret{} | ||
secret.Name = "csp-secret" | ||
secret.Namespace = "csp-namespace" | ||
secret.Data = map[string][]byte{ApiToken: []byte("test-api-token")} | ||
|
||
clientSet := fake.NewSimpleClientset(secret) | ||
|
||
tokenManager := NewMockCSPClient() | ||
provider := &CspAuth{CspClient: tokenManager} | ||
authToken, _ := provider.GetBearerToken(clientSet, context.Background(), secret.Namespace, secret.Name) | ||
|
||
if authToken != DummyAccessToken { | ||
t.Fatal("GetBearer must not fail in this test case!") | ||
} | ||
} | ||
|
||
func TestGetBearerTokenReturnSameTokenSuccess(t *testing.T) { | ||
secret := &v12.Secret{} | ||
secret.Name = "csp-secret" | ||
secret.Namespace = "csp-namespace" | ||
secret.Data = map[string][]byte{ApiToken: []byte("test-api-token")} | ||
|
||
clientSet := fake.NewSimpleClientset(secret) | ||
|
||
tokenManager := NewMockCSPClient() | ||
provider := &CspAuth{CspClient: tokenManager} | ||
authToken, _ := provider.GetBearerToken(clientSet, context.Background(), secret.Namespace, secret.Name) | ||
|
||
if authToken != DummyAccessToken { | ||
t.Fatal("GetBearer must not fail in this test case!") | ||
} | ||
|
||
tokenPrev := DummyAccessToken | ||
DummyAccessToken = "changed-dummy-access-token" | ||
authToken1, _ := provider.GetBearerToken(clientSet, context.Background(), secret.Namespace, secret.Name) | ||
|
||
if authToken != authToken1 { | ||
t.Fatal("GetBearer must return same token if called consequently, \nAuth1: " + authToken + "\n Auth2: " + authToken1) | ||
} | ||
DummyAccessToken = tokenPrev | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package cspauth | ||
|
||
import ( | ||
"context" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
var ( | ||
DummyAccessToken = "dummy-access-token" | ||
SendError = "send-error" | ||
) | ||
|
||
// MockCSPClient is a mock of the CSPClient interface | ||
type MockCSPClient struct { | ||
} | ||
|
||
// NewMockCSPClient creates a new mock instance | ||
func NewMockCSPClient() *MockCSPClient { | ||
return &MockCSPClient{} | ||
} | ||
|
||
func (m *MockCSPClient) GetCspAuthorization(ctx context.Context, apiToken string) (*CSPAuthorizeResponse, error) { | ||
if apiToken == SendError { | ||
return nil, errors.New("Failed to get CSP Auth") | ||
} | ||
response := CSPAuthorizeResponse{} | ||
response.AccessToken = DummyAccessToken | ||
response.ExpiresIn = 1000 | ||
return &response, nil | ||
} |
Oops, something went wrong.