Skip to content

Commit

Permalink
feat/csp_implementation (#1)
Browse files Browse the repository at this point in the history
* 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
harshsharma071988 and snootan committed Mar 1, 2023
1 parent 2e13bcf commit 1ded538
Show file tree
Hide file tree
Showing 17 changed files with 865 additions and 60 deletions.
4 changes: 2 additions & 2 deletions src/api/v1alpha1/inspectionpolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,9 @@ type Governor struct {
// Api url to send telemetry data
// +kubebuilder:validation:Optional
URL string `json:"url"`
// Api token for user authentication
// Secret name where CSP api token is stored in cnsi-system namespace
// +kubebuilder:validation:Optional
APIToken string `json:"apiToken"`
CspSecretName string `json:"cspSecretName"`
}

// FollowupAction defines what actions should be applied when security expectations are matched.
Expand Down
5 changes: 5 additions & 0 deletions src/cmd/inspector/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func main() {
k8sClient, err := client.New(ctrl.GetConfigOrDie(), client.Options{
Scheme: scheme,
})

if err != nil {
log.Error(err, "unable to create k8s client")
os.Exit(1)
Expand All @@ -56,6 +57,10 @@ func main() {
os.Exit(1)
}

if inspectionPolicy.Spec.Inspection.Assessment.Governor.Enabled {
ctx = context.WithValue(ctx, "cspSecretName", inspectionPolicy.Spec.Inspection.Assessment.Governor.CspSecretName)
}

runner := inspection.NewController().
WithScheme(scheme).
WithK8sClient(k8sClient).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,8 @@ spec:
governor:
description: Indicate whether to config of governor
properties:
apiToken:
description: Api token for user authentication
cspSecretName:
description: Secret name where CSP api token is stored in cnsi-system namespace
type: string
clusterId:
description: Unique identifier of the cluster
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@ spec:
governor:
description: Indicate whether to config of governor
properties:
apiToken:
description: Api token for user authentication
cspSecretName:
description: Secret name where CSP api token is stored in cnsi-system namespace
type: string
clusterId:
description: Unique identifier of the cluster
Expand Down
1 change: 1 addition & 0 deletions src/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ require (
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/elastic/elastic-transport-go/v8 v8.1.0 // indirect
github.com/emicklei/go-restful/v3 v3.8.0 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
github.com/felixge/httpsnoop v1.0.2 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
Expand Down
1 change: 1 addition & 0 deletions src/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o=
Expand Down
115 changes: 115 additions & 0 deletions src/lib/cspauth/csp_auth.go
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
}
139 changes: 139 additions & 0 deletions src/lib/cspauth/csp_auth_test.go
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
}
30 changes: 30 additions & 0 deletions src/lib/cspauth/mock_token_manager.go
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
}
Loading

0 comments on commit 1ded538

Please sign in to comment.