github.com/Axway/agent-sdk@v1.1.101/pkg/agent/handler/credential_test.go (about)

     1  package handler
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"crypto/rand"
     7  	"crypto/rsa"
     8  	"crypto/sha256"
     9  	"crypto/x509"
    10  	"encoding/base64"
    11  	"encoding/json"
    12  	"encoding/pem"
    13  	"fmt"
    14  	"net/http"
    15  	"os"
    16  	"strings"
    17  	"testing"
    18  	"time"
    19  
    20  	apiv1 "github.com/Axway/agent-sdk/pkg/apic/apiserver/models/api/v1"
    21  	management "github.com/Axway/agent-sdk/pkg/apic/apiserver/models/management/v1alpha1"
    22  	defs "github.com/Axway/agent-sdk/pkg/apic/definitions"
    23  	prov "github.com/Axway/agent-sdk/pkg/apic/provisioning"
    24  	"github.com/Axway/agent-sdk/pkg/apic/provisioning/mock"
    25  	"github.com/Axway/agent-sdk/pkg/authz/oauth"
    26  	"github.com/Axway/agent-sdk/pkg/config"
    27  	"github.com/Axway/agent-sdk/pkg/util"
    28  	"github.com/Axway/agent-sdk/pkg/util/log"
    29  	"github.com/Axway/agent-sdk/pkg/watchmanager/proto"
    30  	"github.com/stretchr/testify/assert"
    31  )
    32  
    33  func TestCredentialHandler(t *testing.T) {
    34  	crdRI, _ := crd.AsInstance()
    35  
    36  	tests := []struct {
    37  		action           proto.Event_Type
    38  		expectedProvType string
    39  		getAppErr        error
    40  		getCrdErr        error
    41  		hasError         bool
    42  		isRenew          bool
    43  		inboundStatus    string
    44  		inboundState     prov.CredentialAction
    45  		name             string
    46  		outboundStatus   string
    47  		subError         error
    48  		appStatus        string
    49  	}{
    50  		{
    51  			action:           proto.Event_CREATED,
    52  			expectedProvType: provision,
    53  			inboundStatus:    prov.Pending.String(),
    54  			name:             "should handle a create event for a Credential when status is pending",
    55  			outboundStatus:   prov.Success.String(),
    56  		},
    57  		{
    58  			action:           proto.Event_UPDATED,
    59  			expectedProvType: provision,
    60  			inboundStatus:    prov.Pending.String(),
    61  			name:             "should handle an update event for a Credential when status is pending",
    62  			outboundStatus:   prov.Success.String(),
    63  		},
    64  		{
    65  			action:         proto.Event_CREATED,
    66  			inboundStatus:  prov.Pending.String(),
    67  			name:           "should return nil with the appStatus is not success",
    68  			outboundStatus: prov.Error.String(),
    69  			appStatus:      prov.Error.String(),
    70  		},
    71  		{
    72  			action: proto.Event_SUBRESOURCEUPDATED,
    73  			name:   "should return nil when the event is for subresources",
    74  		},
    75  		{
    76  			action:        proto.Event_UPDATED,
    77  			inboundStatus: prov.Error.String(),
    78  			name:          "should return nil and not process anything when the Credential status is set to Error",
    79  		},
    80  		{
    81  			action:        proto.Event_UPDATED,
    82  			inboundStatus: prov.Success.String(),
    83  			name:          "should return nil and not process anything when the Credential status is set to Success",
    84  		},
    85  		{
    86  			action:         proto.Event_CREATED,
    87  			getAppErr:      fmt.Errorf("error getting managed app"),
    88  			inboundStatus:  prov.Pending.String(),
    89  			name:           "should handle an error when retrieving the managed app, and set a failed status",
    90  			outboundStatus: prov.Error.String(),
    91  		},
    92  		{
    93  			action:         proto.Event_CREATED,
    94  			getCrdErr:      fmt.Errorf("error getting credential request definition"),
    95  			inboundStatus:  prov.Pending.String(),
    96  			name:           "should handle an error when retrieving the credential request definition, and set a failed status",
    97  			outboundStatus: prov.Error.String(),
    98  		},
    99  		{
   100  			action:           proto.Event_CREATED,
   101  			expectedProvType: provision,
   102  			hasError:         true,
   103  			inboundStatus:    prov.Pending.String(),
   104  			name:             "should handle an error when updating the Credential subresources",
   105  			outboundStatus:   prov.Success.String(),
   106  			subError:         fmt.Errorf("error updating subresources"),
   107  		},
   108  		{
   109  			action: proto.Event_CREATED,
   110  			name:   "should return nil and not process anything when the status field is empty",
   111  		},
   112  	}
   113  
   114  	for _, tc := range tests {
   115  		t.Run(tc.name, func(t *testing.T) {
   116  			credApp.SubResources["status"].(map[string]interface{})["level"] = prov.Success.String()
   117  			if tc.appStatus != "" {
   118  				credApp.SubResources["status"].(map[string]interface{})["level"] = tc.appStatus
   119  			}
   120  
   121  			cred := credential
   122  			cred.Status.Level = tc.inboundStatus
   123  			cred.Spec.State.Name = tc.inboundState.String()
   124  			if tc.inboundState.String() == "" {
   125  				cred.Spec.State.Name = apiv1.Active
   126  			}
   127  
   128  			p := &mockCredProv{
   129  				t: t,
   130  				expectedStatus: mock.MockRequestStatus{
   131  					Status: prov.Success,
   132  					Msg:    "msg",
   133  					Properties: map[string]string{
   134  						"status_key": "status_val",
   135  					},
   136  				},
   137  				expectedAppDetails:  util.GetAgentDetails(credApp),
   138  				expectedCredDetails: util.GetAgentDetails(&cred),
   139  				expectedManagedApp:  credAppRefName,
   140  				expectedCredType:    cred.Spec.CredentialRequestDefinition,
   141  			}
   142  
   143  			c := &credClient{
   144  				t:              t,
   145  				expectedStatus: tc.outboundStatus,
   146  				managedApp:     credApp,
   147  				crd:            crdRI,
   148  				getAppErr:      tc.getAppErr,
   149  				getCrdErr:      tc.getCrdErr,
   150  				subError:       tc.subError,
   151  			}
   152  
   153  			handler := NewCredentialHandler(p, c, nil)
   154  			v := handler.(*credentials)
   155  			v.encryptSchema = func(_, _ map[string]interface{}, _, _, _ string) (map[string]interface{}, error) {
   156  				return map[string]interface{}{}, nil
   157  			}
   158  
   159  			ri, _ := cred.AsInstance()
   160  			err := handler.Handle(NewEventContext(tc.action, nil, ri.Kind, ri.Name), nil, ri)
   161  			assert.Equal(t, tc.expectedProvType, p.expectedProvType)
   162  
   163  			if tc.hasError {
   164  				assert.Error(t, err)
   165  			} else {
   166  				assert.Nil(t, err)
   167  			}
   168  		})
   169  	}
   170  }
   171  
   172  func TestCredentialHandler_deleting(t *testing.T) {
   173  	crdRI, _ := crd.AsInstance()
   174  
   175  	tests := []struct {
   176  		name             string
   177  		outboundStatus   prov.Status
   178  		resourceState    string
   179  		provStatus       string
   180  		expectedProvType string
   181  		specState        string
   182  		specStateReason  string
   183  		skipFinalizers   bool
   184  	}{
   185  		{
   186  			name:             "should deprovision with no error",
   187  			outboundStatus:   prov.Success,
   188  			expectedProvType: deprovision,
   189  			resourceState:    apiv1.ResourceDeleting,
   190  			provStatus:       prov.Success.String(),
   191  		},
   192  		{
   193  			name:             "should deprovision expired with no error and not Deleting",
   194  			expectedProvType: deprovision,
   195  			outboundStatus:   prov.Success,
   196  			provStatus:       prov.Pending.String(),
   197  			specState:        apiv1.Inactive,
   198  			specStateReason:  prov.CredExpDetail,
   199  		},
   200  		{
   201  			name:           "should not deprovision when error and not Deleting",
   202  			outboundStatus: prov.Success,
   203  			provStatus:     prov.Error.String(),
   204  		},
   205  		{
   206  			name:             "should deprovision when and Deleting",
   207  			outboundStatus:   prov.Success,
   208  			provStatus:       prov.Error.String(),
   209  			resourceState:    apiv1.ResourceDeleting,
   210  			expectedProvType: deprovision,
   211  		},
   212  		{
   213  			name:             "should fail to deprovision and set the status to error",
   214  			outboundStatus:   prov.Error,
   215  			expectedProvType: deprovision,
   216  			resourceState:    apiv1.ResourceDeleting,
   217  			provStatus:       prov.Success.String(),
   218  		},
   219  		{
   220  			name:           "should not deprovision with no agent finalizers",
   221  			resourceState:  apiv1.ResourceDeleting,
   222  			provStatus:     prov.Success.String(),
   223  			outboundStatus: prov.Success,
   224  			skipFinalizers: true,
   225  		},
   226  	}
   227  
   228  	for _, tc := range tests {
   229  		t.Run(tc.name, func(t *testing.T) {
   230  			cred := credential
   231  			cred.Spec.State.Name = tc.specState
   232  			cred.Spec.State.Reason = tc.specStateReason
   233  			cred.Status.Level = tc.provStatus
   234  			cred.Status.Reasons = []apiv1.ResourceStatusReason{
   235  				{
   236  					Type:   tc.provStatus,
   237  					Detail: tc.specStateReason,
   238  				},
   239  			}
   240  			cred.Metadata.State = tc.resourceState
   241  			if !tc.skipFinalizers {
   242  				cred.Finalizers = []apiv1.Finalizer{{Name: crFinalizer}}
   243  			}
   244  			p := &mockCredProv{
   245  				t: t,
   246  				expectedStatus: mock.MockRequestStatus{
   247  					Status: tc.outboundStatus,
   248  					Msg:    "msg",
   249  					Properties: map[string]string{
   250  						"status_key": "status_val",
   251  					},
   252  				},
   253  				expectedAppDetails:  util.GetAgentDetails(mApp),
   254  				expectedCredDetails: util.GetAgentDetails(&cred),
   255  				expectedManagedApp:  credAppRefName,
   256  				expectedCredType:    cred.Spec.CredentialRequestDefinition,
   257  			}
   258  
   259  			c := &credClient{
   260  				crd:            crdRI,
   261  				expectedStatus: tc.outboundStatus.String(),
   262  				isDeleting:     true,
   263  				managedApp:     credApp,
   264  				t:              t,
   265  			}
   266  
   267  			handler := NewCredentialHandler(p, c, nil)
   268  			v := handler.(*credentials)
   269  			v.encryptSchema = func(_, _ map[string]interface{}, _, _, _ string) (map[string]interface{}, error) {
   270  				return map[string]interface{}{}, nil
   271  			}
   272  
   273  			ri, _ := cred.AsInstance()
   274  			err := handler.Handle(NewEventContext(proto.Event_UPDATED, nil, ri.Kind, ri.Name), nil, ri)
   275  			assert.Nil(t, err)
   276  			assert.Equal(t, tc.expectedProvType, p.expectedProvType)
   277  
   278  			if tc.outboundStatus.String() == prov.Success.String() && tc.specStateReason != prov.CredExpDetail {
   279  				assert.False(t, c.createSubCalled)
   280  			} else {
   281  				assert.True(t, c.createSubCalled)
   282  			}
   283  		})
   284  	}
   285  }
   286  
   287  func TestCredentialHandler_update(t *testing.T) {
   288  	crdRI, _ := crd.AsInstance()
   289  
   290  	tests := []struct {
   291  		name             string
   292  		isRotating       bool
   293  		inboundStatus    string
   294  		inboundSpecState string
   295  		inboundState     string
   296  		outboundState    string
   297  		expectedProvType string
   298  		outboundStatus   prov.Status
   299  	}{
   300  		{
   301  			name:             "should update credential on rotate",
   302  			isRotating:       true,
   303  			inboundState:     apiv1.Active,
   304  			expectedProvType: update,
   305  		},
   306  		{
   307  			name:             "should update credential on suspend",
   308  			inboundSpecState: apiv1.Active,
   309  			inboundState:     apiv1.Inactive,
   310  			outboundState:    apiv1.Active,
   311  			expectedProvType: update,
   312  		},
   313  		{
   314  			name:             "should update credential on enable",
   315  			inboundSpecState: apiv1.Inactive,
   316  			inboundState:     apiv1.Active,
   317  			outboundState:    apiv1.Inactive,
   318  			expectedProvType: update,
   319  		},
   320  		{
   321  			name:             "should update credential on suspend and rotate",
   322  			inboundSpecState: apiv1.Active,
   323  			inboundState:     apiv1.Inactive,
   324  			outboundState:    apiv1.Active,
   325  			expectedProvType: update,
   326  			isRotating:       true,
   327  		},
   328  		{
   329  			name:             "should update credential on rotate and enable",
   330  			inboundSpecState: apiv1.Inactive,
   331  			inboundState:     apiv1.Active,
   332  			outboundState:    apiv1.Inactive,
   333  			expectedProvType: update,
   334  			isRotating:       true,
   335  		},
   336  	}
   337  	for _, tc := range tests {
   338  		t.Run(tc.name, func(t *testing.T) {
   339  			cred := credential
   340  			cred.Status.Level = tc.inboundStatus
   341  			if tc.inboundStatus == "" {
   342  				cred.Status.Level = prov.Pending.String()
   343  			}
   344  			cred.Metadata.State = tc.inboundStatus
   345  			cred.State.Name = tc.inboundState
   346  			cred.Spec.State.Name = tc.inboundSpecState
   347  			cred.Spec.State.Rotate = tc.isRotating
   348  			cred.Finalizers = []apiv1.Finalizer{{Name: crFinalizer}}
   349  
   350  			if tc.outboundStatus.String() == "" {
   351  				tc.outboundStatus = prov.Success
   352  			}
   353  
   354  			p := &mockCredProv{
   355  				t:                t,
   356  				expectedProvType: tc.expectedProvType,
   357  				expectedStatus: mock.MockRequestStatus{
   358  					Status: tc.outboundStatus,
   359  					Msg:    "msg",
   360  				},
   361  				expectedAppDetails:  util.GetAgentDetails(credApp),
   362  				expectedCredDetails: util.GetAgentDetails(&cred),
   363  				expectedManagedApp:  credAppRefName,
   364  				expectedCredType:    cred.Spec.CredentialRequestDefinition,
   365  			}
   366  
   367  			c := &credClient{
   368  				crd:            crdRI,
   369  				expectedStatus: tc.outboundStatus.String(),
   370  				managedApp:     credApp,
   371  				t:              t,
   372  			}
   373  
   374  			handler := NewCredentialHandler(p, c, nil)
   375  			v := handler.(*credentials)
   376  			v.encryptSchema = func(_, _ map[string]interface{}, _, _, _ string) (map[string]interface{}, error) {
   377  				return map[string]interface{}{}, nil
   378  			}
   379  
   380  			ri, _ := cred.AsInstance()
   381  			err := handler.Handle(NewEventContext(proto.Event_UPDATED, nil, ri.Kind, ri.Name), nil, ri)
   382  			assert.Nil(t, err)
   383  			assert.Equal(t, tc.expectedProvType, p.expectedProvType)
   384  		})
   385  	}
   386  }
   387  
   388  func TestCredentialHandler_wrong_kind(t *testing.T) {
   389  	c := &mockClient{}
   390  	p := &mockCredProv{}
   391  	handler := NewCredentialHandler(p, c, nil)
   392  	ri := &apiv1.ResourceInstance{
   393  		ResourceMeta: apiv1.ResourceMeta{
   394  			GroupVersionKind: management.EnvironmentGVK(),
   395  		},
   396  	}
   397  	err := handler.Handle(NewEventContext(proto.Event_CREATED, nil, ri.Kind, ri.Name), nil, ri)
   398  	assert.Nil(t, err)
   399  }
   400  
   401  func Test_creds(t *testing.T) {
   402  	c := provCreds{
   403  		managedApp: "app-name",
   404  		credType:   "api-key",
   405  		credDetails: map[string]interface{}{
   406  			"abc": "123",
   407  		},
   408  		appDetails: map[string]interface{}{
   409  			"def": "456",
   410  		},
   411  		credData: map[string]interface{}{
   412  			"def": "789",
   413  		},
   414  		id: "cred-id",
   415  		credSchema: map[string]interface{}{
   416  			"properties": "test",
   417  		},
   418  		credProvSchema: map[string]interface{}{
   419  			"properties": "test",
   420  		},
   421  	}
   422  
   423  	assert.Equal(t, c.managedApp, c.GetApplicationName())
   424  	assert.Equal(t, c.credType, c.GetCredentialType())
   425  	assert.Equal(t, c.id, c.GetID())
   426  	assert.Equal(t, c.credData, c.GetCredentialData())
   427  	assert.Equal(t, c.credDetails["abc"], c.GetCredentialDetailsValue("abc"))
   428  	assert.Equal(t, c.appDetails["def"], c.GetApplicationDetailsValue("def"))
   429  	assert.Equal(t, c.credSchema, c.GetCredentialSchema())
   430  	assert.Equal(t, c.credProvSchema, c.GetCredentialProvisionSchema())
   431  	assert.Empty(t, c.GetCredentialSchemaDetailsValue("prop"))
   432  
   433  	c.credSchemaDetails = map[string]interface{}{
   434  		"detail": "test",
   435  	}
   436  	assert.Equal(t, c.credSchemaDetails["prop"], c.GetCredentialSchemaDetailsValue("prop"))
   437  
   438  	c.credDetails = nil
   439  	c.appDetails = nil
   440  	assert.Empty(t, c.GetApplicationDetailsValue("app_details_key"))
   441  	assert.Empty(t, c.GetCredentialDetailsValue("access_details_key"))
   442  	assert.Empty(t, c.GetCredentialSchemaDetailsValue("invalid_key"))
   443  }
   444  
   445  func TestIDPCredentialProvisioning(t *testing.T) {
   446  	crdRI, _ := crd.AsInstance()
   447  	s := oauth.NewMockIDPServer()
   448  	defer s.Close()
   449  
   450  	publicKey, err := os.ReadFile("../../authz/oauth/testdata/publickey")
   451  	assert.Nil(t, err)
   452  
   453  	certificate, err := os.ReadFile("../../authz/oauth/testdata/client_cert.pem")
   454  	assert.Nil(t, err)
   455  	tests := []struct {
   456  		name               string
   457  		metadataURL        string
   458  		tokenURL           string
   459  		expectedProvType   string
   460  		outboundStatus     prov.Status
   461  		registrationStatus int
   462  		handlerInvoked     bool
   463  		hasError           bool
   464  		authMethod         string
   465  		jwks               []byte
   466  	}{
   467  		{
   468  			name:               "should provision IDP credential with no error",
   469  			metadataURL:        s.GetMetadataURL(),
   470  			tokenURL:           s.GetTokenURL(),
   471  			expectedProvType:   provision,
   472  			outboundStatus:     prov.Success,
   473  			registrationStatus: http.StatusCreated,
   474  			authMethod:         config.ClientSecretBasic,
   475  		},
   476  		{
   477  			name:           "should fail to provision and set the status to error",
   478  			metadataURL:    s.GetMetadataURL(),
   479  			tokenURL:       "test",
   480  			outboundStatus: prov.Error,
   481  			authMethod:     config.ClientSecretBasic,
   482  		},
   483  		{
   484  			name:           "should fail to provision with no jwks for private_kwy_jwt",
   485  			metadataURL:    s.GetMetadataURL(),
   486  			tokenURL:       s.GetTokenURL(),
   487  			outboundStatus: prov.Error,
   488  			authMethod:     config.PrivateKeyJWT,
   489  		},
   490  		{
   491  			name:           "should fail to provision with no jwks for tls_client_auth",
   492  			metadataURL:    s.GetMetadataURL(),
   493  			tokenURL:       s.GetTokenURL(),
   494  			outboundStatus: prov.Error,
   495  			authMethod:     config.TLSClientAuth,
   496  		},
   497  		{
   498  			name:           "should fail to provision with no jwks for self_signed_tls_client_auth",
   499  			metadataURL:    s.GetMetadataURL(),
   500  			tokenURL:       s.GetTokenURL(),
   501  			outboundStatus: prov.Error,
   502  			authMethod:     config.SelfSignedTLSClientAuth,
   503  		},
   504  		{
   505  			name:           "should fail to provision with invalid jwks for private_kwy_jwt",
   506  			metadataURL:    s.GetMetadataURL(),
   507  			tokenURL:       s.GetTokenURL(),
   508  			outboundStatus: prov.Error,
   509  			authMethod:     config.PrivateKeyJWT,
   510  			jwks:           []byte("invalid-private-key"),
   511  		},
   512  		{
   513  			name:           "should fail to provision with invalid jwks for tls_client_auth",
   514  			metadataURL:    s.GetMetadataURL(),
   515  			tokenURL:       s.GetTokenURL(),
   516  			outboundStatus: prov.Error,
   517  			authMethod:     config.TLSClientAuth,
   518  			jwks:           []byte("invalid-certificate"),
   519  		},
   520  		{
   521  			name:               "should fail to provision with invalid jwks for private_kwy_jwt",
   522  			metadataURL:        s.GetMetadataURL(),
   523  			tokenURL:           s.GetTokenURL(),
   524  			outboundStatus:     prov.Success,
   525  			expectedProvType:   provision,
   526  			authMethod:         config.PrivateKeyJWT,
   527  			jwks:               publicKey,
   528  			registrationStatus: http.StatusCreated,
   529  		},
   530  		{
   531  			name:               "should fail to provision with invalid jwks for tls_client_auth",
   532  			metadataURL:        s.GetMetadataURL(),
   533  			tokenURL:           s.GetTokenURL(),
   534  			outboundStatus:     prov.Success,
   535  			expectedProvType:   provision,
   536  			authMethod:         config.TLSClientAuth,
   537  			jwks:               certificate,
   538  			registrationStatus: http.StatusCreated,
   539  		},
   540  	}
   541  	for _, tc := range tests {
   542  		t.Run(tc.name, func(t *testing.T) {
   543  			idpConfig := createIDPConfig(s)
   544  			idpProviderRegistry := oauth.NewIdpRegistry()
   545  			idpProviderRegistry.RegisterProvider(context.Background(), idpConfig, config.NewTLSConfig(), "", 30*time.Second)
   546  
   547  			cred := credential
   548  			cred.Status.Level = prov.Pending.String()
   549  
   550  			cred.Spec.Data = map[string]interface{}{
   551  				prov.IDPTokenURL:          tc.tokenURL,
   552  				prov.OauthGrantType:       "client_credentials",
   553  				prov.OauthTokenAuthMethod: tc.authMethod,
   554  				prov.OauthJwks:            string(tc.jwks),
   555  				prov.OauthCertificate:     string(tc.jwks),
   556  			}
   557  
   558  			p := &mockCredProv{
   559  				t: t,
   560  				expectedStatus: mock.MockRequestStatus{
   561  					Status: prov.Success,
   562  					Msg:    "msg",
   563  					Properties: map[string]string{
   564  						"status_key": "status_val",
   565  					},
   566  				},
   567  				expectedAppDetails:  util.GetAgentDetails(credApp),
   568  				expectedCredDetails: util.GetAgentDetails(&cred),
   569  				expectedManagedApp:  credAppRefName,
   570  				expectedCredType:    cred.Spec.CredentialRequestDefinition,
   571  			}
   572  
   573  			c := &credClient{
   574  				crd:            crdRI,
   575  				expectedStatus: tc.outboundStatus.String(),
   576  				managedApp:     credApp,
   577  				t:              t,
   578  			}
   579  
   580  			handler := NewCredentialHandler(p, c, idpProviderRegistry)
   581  			v := handler.(*credentials)
   582  			v.encryptSchema = func(_, _ map[string]interface{}, _, _, _ string) (map[string]interface{}, error) {
   583  				return map[string]interface{}{}, nil
   584  			}
   585  
   586  			ri, _ := cred.AsInstance()
   587  			s.SetRegistrationResponseCode(tc.registrationStatus)
   588  			err := handler.Handle(NewEventContext(proto.Event_UPDATED, nil, ri.Kind, ri.Name), nil, ri)
   589  			assert.Nil(t, err)
   590  			if !tc.hasError {
   591  				assert.Equal(t, tc.expectedProvType, p.expectedProvType)
   592  			} else {
   593  				assert.NotNil(t, err)
   594  			}
   595  		})
   596  	}
   597  }
   598  
   599  func TestIDPCredentialDeprovisioning(t *testing.T) {
   600  	crdRI, _ := crd.AsInstance()
   601  	s := oauth.NewMockIDPServer()
   602  	defer s.Close()
   603  
   604  	tests := []struct {
   605  		name               string
   606  		metadataURL        string
   607  		tokenURL           string
   608  		outboundStatus     prov.Status
   609  		registrationStatus int
   610  		handlerInvoked     bool
   611  	}{
   612  		{
   613  			name:               "should deprovision IDP credential with no error",
   614  			metadataURL:        s.GetMetadataURL(),
   615  			tokenURL:           s.GetTokenURL(),
   616  			outboundStatus:     prov.Success,
   617  			registrationStatus: http.StatusNoContent,
   618  			handlerInvoked:     true,
   619  		},
   620  		{
   621  			name:           "should fail to deprovision and set the status to error",
   622  			metadataURL:    s.GetMetadataURL(),
   623  			tokenURL:       "test",
   624  			outboundStatus: prov.Error,
   625  			handlerInvoked: false,
   626  		},
   627  	}
   628  	for _, tc := range tests {
   629  		t.Run(tc.name, func(t *testing.T) {
   630  			idpConfig := createIDPConfig(s)
   631  			idpProviderRegistry := oauth.NewIdpRegistry()
   632  			idpProviderRegistry.RegisterProvider(context.Background(), idpConfig, config.NewTLSConfig(), "", 30*time.Second)
   633  
   634  			cred := credential
   635  			cred.Status.Level = prov.Success.String()
   636  			cred.Metadata.State = apiv1.ResourceDeleting
   637  			cred.Finalizers = []apiv1.Finalizer{{Name: crFinalizer}}
   638  			cred.Spec.Data = map[string]interface{}{
   639  				"idpTokenURL": tc.tokenURL,
   640  			}
   641  
   642  			p := &mockCredProv{
   643  				t: t,
   644  				expectedStatus: mock.MockRequestStatus{
   645  					Status: tc.outboundStatus,
   646  					Msg:    "msg",
   647  					Properties: map[string]string{
   648  						"status_key": "status_val",
   649  					},
   650  				},
   651  				expectedAppDetails:  util.GetAgentDetails(mApp),
   652  				expectedCredDetails: util.GetAgentDetails(&cred),
   653  				expectedManagedApp:  credAppRefName,
   654  				expectedCredType:    cred.Spec.CredentialRequestDefinition,
   655  			}
   656  
   657  			c := &credClient{
   658  				crd:            crdRI,
   659  				expectedStatus: tc.outboundStatus.String(),
   660  				managedApp:     credApp,
   661  				isDeleting:     true,
   662  				t:              t,
   663  			}
   664  
   665  			handler := NewCredentialHandler(p, c, idpProviderRegistry)
   666  			v := handler.(*credentials)
   667  			v.encryptSchema = func(_, _ map[string]interface{}, _, _, _ string) (map[string]interface{}, error) {
   668  				return map[string]interface{}{}, nil
   669  			}
   670  
   671  			ri, _ := cred.AsInstance()
   672  			s.SetRegistrationResponseCode(tc.registrationStatus)
   673  			err := handler.Handle(NewEventContext(proto.Event_UPDATED, nil, ri.Kind, ri.Name), nil, ri)
   674  			assert.Nil(t, err)
   675  			if tc.handlerInvoked {
   676  				assert.Equal(t, deprovision, p.expectedProvType)
   677  				if tc.outboundStatus.String() == prov.Success.String() {
   678  					assert.False(t, c.createSubCalled)
   679  				} else {
   680  					assert.True(t, c.createSubCalled)
   681  				}
   682  			} else {
   683  				assert.False(t, c.createSubCalled)
   684  			}
   685  		})
   686  	}
   687  
   688  }
   689  
   690  type mockCredProv struct {
   691  	expectedAppDetails  map[string]interface{}
   692  	expectedCredDetails map[string]interface{}
   693  	expectedCredType    string
   694  	expectedManagedApp  string
   695  	expectedProvType    string
   696  	expectedStatus      mock.MockRequestStatus
   697  	t                   *testing.T
   698  }
   699  
   700  func (m *mockCredProv) CredentialProvision(cr prov.CredentialRequest) (status prov.RequestStatus, credentails prov.Credential) {
   701  	m.expectedProvType = provision
   702  	v := cr.(*provCreds)
   703  	assert.Equal(m.t, m.expectedAppDetails, v.appDetails)
   704  	assert.Equal(m.t, m.expectedCredDetails, v.credDetails)
   705  	assert.Equal(m.t, m.expectedManagedApp, v.managedApp)
   706  	assert.Equal(m.t, m.expectedCredType, v.credType)
   707  	return m.expectedStatus, &mockProvCredential{}
   708  }
   709  
   710  func (m *mockCredProv) CredentialDeprovision(cr prov.CredentialRequest) (status prov.RequestStatus) {
   711  	m.expectedProvType = deprovision
   712  	v := cr.(*provCreds)
   713  	assert.Equal(m.t, m.expectedAppDetails, v.appDetails)
   714  	assert.Equal(m.t, m.expectedCredDetails, v.credDetails)
   715  	assert.Equal(m.t, m.expectedManagedApp, v.managedApp)
   716  	assert.Equal(m.t, m.expectedCredType, v.credType)
   717  	return m.expectedStatus
   718  }
   719  
   720  func (m *mockCredProv) CredentialUpdate(cr prov.CredentialRequest) (status prov.RequestStatus, credentails prov.Credential) {
   721  	m.expectedProvType = update
   722  	v := cr.(*provCreds)
   723  	assert.Equal(m.t, m.expectedAppDetails, v.appDetails)
   724  	assert.Equal(m.t, m.expectedCredDetails, v.credDetails)
   725  	assert.Equal(m.t, m.expectedManagedApp, v.managedApp)
   726  	assert.Equal(m.t, m.expectedCredType, v.credType)
   727  	return m.expectedStatus, &mockProvCredential{}
   728  }
   729  
   730  type mockProvCredential struct{}
   731  
   732  func (m *mockProvCredential) GetData() map[string]interface{} {
   733  	return map[string]interface{}{}
   734  }
   735  
   736  func (m *mockProvCredential) GetExpirationTime() time.Time {
   737  	return time.Now()
   738  }
   739  
   740  func decrypt(pk *rsa.PrivateKey, alg string, data map[string]interface{}) map[string]interface{} {
   741  	enc := func(v string) ([]byte, error) {
   742  		switch alg {
   743  		case "RSA-OAEP":
   744  			bts, _ := base64.StdEncoding.DecodeString(v)
   745  			return rsa.DecryptOAEP(sha256.New(), rand.Reader, pk, bts, nil)
   746  		case "PKCS":
   747  			bts, _ := base64.StdEncoding.DecodeString(v)
   748  			return rsa.DecryptPKCS1v15(rand.Reader, pk, bts)
   749  		default:
   750  			return nil, fmt.Errorf("unexpected algorithm")
   751  		}
   752  	}
   753  
   754  	for key, value := range data {
   755  		v, ok := value.(string)
   756  		if !ok {
   757  			continue
   758  		}
   759  
   760  		bts, err := enc(v)
   761  		if err != nil {
   762  			log.Errorf("Failed to decrypt: %s\n", err)
   763  			continue
   764  		}
   765  		data[key] = string(bts)
   766  	}
   767  
   768  	return data
   769  }
   770  
   771  func Test_encrypt(t *testing.T) {
   772  	var crdSchema = `{
   773      "type": "object",
   774      "$schema": "http://json-schema.org/draft-07/schema#",
   775      "required": [
   776          "abc"
   777      ],
   778      "properties": {
   779          "one": {
   780              "type": "string",
   781              "description": "abc.",
   782  						"x-axway-encrypted": true
   783          },
   784          "two": {
   785              "type": "string",
   786              "description": "def."
   787          },
   788          "three": {
   789              "type": "string",
   790              "description": "ghi.",
   791  						"x-axway-encrypted": true
   792          }
   793      },
   794      "description": "sample."
   795  }`
   796  
   797  	crd := map[string]interface{}{}
   798  	err := json.Unmarshal([]byte(crdSchema), &crd)
   799  	assert.Nil(t, err)
   800  
   801  	pub, priv, err := newKeyPair()
   802  	assert.Nil(t, err)
   803  
   804  	tests := []struct {
   805  		alg           string
   806  		hasErr        bool
   807  		hasEncryptErr bool
   808  		hash          string
   809  		name          string
   810  		publicKey     string
   811  		privateKey    string
   812  	}{
   813  		{
   814  			name:       "should encrypt when the algorithm is PKCS",
   815  			alg:        "PKCS",
   816  			hash:       "SHA256",
   817  			publicKey:  pub,
   818  			privateKey: priv,
   819  		},
   820  		{
   821  			name:       "should encrypt when the algorithm is RSA-OAEP",
   822  			alg:        "RSA-OAEP",
   823  			hash:       "SHA256",
   824  			publicKey:  pub,
   825  			privateKey: priv,
   826  		},
   827  		{
   828  			name:       "should return an error when the algorithm is unknown",
   829  			hasErr:     true,
   830  			alg:        "fake",
   831  			hash:       "SHA256",
   832  			publicKey:  pub,
   833  			privateKey: priv,
   834  		},
   835  		{
   836  			name:       "should return an error when the hash is unknown",
   837  			hasErr:     true,
   838  			alg:        "RSA-OAEP",
   839  			hash:       "fake",
   840  			publicKey:  pub,
   841  			privateKey: priv,
   842  		},
   843  		{
   844  			name:       "should return an error when the public key cannot be parsed",
   845  			hasErr:     true,
   846  			alg:        "RSA-OAEP",
   847  			hash:       "SHA256",
   848  			publicKey:  "fake",
   849  			privateKey: priv,
   850  		},
   851  	}
   852  
   853  	for _, tc := range tests {
   854  		t.Run(tc.name, func(t *testing.T) {
   855  			schemaData := map[string]interface{}{
   856  				"one":   "abc",
   857  				"two":   "def",
   858  				"three": "ghi",
   859  			}
   860  
   861  			encrypted, err := encryptSchema(crd, schemaData, tc.publicKey, tc.alg, tc.hash)
   862  			if tc.hasErr {
   863  				assert.Error(t, err)
   864  			} else {
   865  				assert.NotEqual(t, "abc", schemaData["one"])
   866  				assert.Equal(t, "def", schemaData["two"])
   867  				assert.NotEqual(t, "ghi", schemaData["three"])
   868  
   869  				decrypted := decrypt(parsePrivateKey(tc.privateKey), tc.alg, encrypted)
   870  				assert.Equal(t, "abc", decrypted["one"])
   871  				assert.Equal(t, "def", decrypted["two"])
   872  				assert.Equal(t, "ghi", decrypted["three"])
   873  			}
   874  		})
   875  	}
   876  
   877  }
   878  
   879  type credClient struct {
   880  	managedApp      *apiv1.ResourceInstance
   881  	crd             *apiv1.ResourceInstance
   882  	getAppErr       error
   883  	getCrdErr       error
   884  	createSubCalled bool
   885  	subError        error
   886  	expectedStatus  string
   887  	t               *testing.T
   888  	isDeleting      bool
   889  }
   890  
   891  func (m *credClient) GetResource(url string) (*apiv1.ResourceInstance, error) {
   892  	if strings.Contains(url, "/managedapplications") {
   893  		return m.managedApp, m.getAppErr
   894  	}
   895  	if strings.Contains(url, "/credentialrequestdefinitions") {
   896  		return m.crd, m.getCrdErr
   897  	}
   898  
   899  	return nil, fmt.Errorf("mock client - resource not found")
   900  }
   901  
   902  func (m *credClient) CreateSubResource(_ apiv1.ResourceMeta, subs map[string]interface{}) error {
   903  	if statusI, ok := subs["status"]; ok {
   904  		status := statusI.(*apiv1.ResourceStatus)
   905  		assert.Equal(m.t, m.expectedStatus, status.Level, status.Reasons)
   906  	}
   907  	m.createSubCalled = true
   908  	return m.subError
   909  }
   910  
   911  func (m *credClient) UpdateResourceFinalizer(ri *apiv1.ResourceInstance, _, _ string, addAction bool) (*apiv1.ResourceInstance, error) {
   912  	if m.isDeleting {
   913  		assert.False(m.t, addAction, "addAction should be false when the resource is deleting")
   914  	} else {
   915  		assert.True(m.t, addAction, "addAction should be true when the resource is not deleting")
   916  	}
   917  
   918  	return nil, nil
   919  }
   920  
   921  func (m *credClient) UpdateResourceInstance(ri apiv1.Interface) (*apiv1.ResourceInstance, error) {
   922  	return nil, nil
   923  }
   924  
   925  func parsePrivateKey(priv string) *rsa.PrivateKey {
   926  	block, _ := pem.Decode([]byte(priv))
   927  	if block == nil {
   928  		panic("failed to parse PEM block containing the public key")
   929  	}
   930  
   931  	pk, err := x509.ParsePKCS1PrivateKey(block.Bytes)
   932  	if err != nil {
   933  		panic("failed to parse private key: " + err.Error())
   934  	}
   935  
   936  	return pk
   937  }
   938  
   939  func newKeyPair() (public string, private string, err error) {
   940  	priv, err := rsa.GenerateKey(rand.Reader, 2048)
   941  	if err != nil {
   942  		return "", "", err
   943  	}
   944  
   945  	pkBts := x509.MarshalPKCS1PrivateKey(priv)
   946  	fmt.Println(pkBts)
   947  	pvBlock := &pem.Block{
   948  		Type:  "RSA PRIVATE KEY",
   949  		Bytes: pkBts,
   950  	}
   951  
   952  	privBuff := bytes.NewBuffer([]byte{})
   953  	err = pem.Encode(privBuff, pvBlock)
   954  	if err != nil {
   955  		return "", "", err
   956  	}
   957  
   958  	pubKeyBts, err := x509.MarshalPKIXPublicKey(&priv.PublicKey)
   959  	if err != nil {
   960  		return "", "", err
   961  	}
   962  
   963  	pubKeyBlock := &pem.Block{
   964  		Type:  "PUBLIC KEY",
   965  		Bytes: pubKeyBts,
   966  	}
   967  
   968  	pubKeyBuff := bytes.NewBuffer([]byte{})
   969  	err = pem.Encode(pubKeyBuff, pubKeyBlock)
   970  	if err != nil {
   971  		return "", "", err
   972  	}
   973  
   974  	return pubKeyBuff.String(), privBuff.String(), nil
   975  }
   976  
   977  const credAppRefName = "managed-app-name"
   978  
   979  var credApp = &apiv1.ResourceInstance{
   980  	ResourceMeta: apiv1.ResourceMeta{
   981  		Name: credAppRefName,
   982  		SubResources: map[string]interface{}{
   983  			defs.XAgentDetails: map[string]interface{}{
   984  				"sub_managed_app_key": "sub_managed_app_val",
   985  			},
   986  			"status": map[string]interface{}{
   987  				"level": prov.Success.String(),
   988  			},
   989  		},
   990  	},
   991  }
   992  
   993  var crd = &management.CredentialRequestDefinition{
   994  	ResourceMeta: apiv1.ResourceMeta{
   995  		Name: credAppRefName,
   996  		SubResources: map[string]interface{}{
   997  			defs.XAgentDetails: map[string]interface{}{
   998  				"sub_crd_key": "sub_crd_val",
   999  			},
  1000  		},
  1001  	},
  1002  	Owner:      nil,
  1003  	References: management.CredentialRequestDefinitionReferences{},
  1004  	Spec: management.CredentialRequestDefinitionSpec{
  1005  		Schema: nil,
  1006  		Provision: &management.CredentialRequestDefinitionSpecProvision{
  1007  			Schema: map[string]interface{}{
  1008  				"properties": map[string]interface{}{},
  1009  			},
  1010  		},
  1011  		Webhooks: nil,
  1012  	},
  1013  }
  1014  
  1015  var credential = management.Credential{
  1016  	ResourceMeta: apiv1.ResourceMeta{
  1017  		Metadata: apiv1.Metadata{
  1018  			ID: "11",
  1019  			Scope: apiv1.MetadataScope{
  1020  				Kind: management.EnvironmentGVK().Kind,
  1021  				Name: "env-1",
  1022  			},
  1023  		},
  1024  		SubResources: map[string]interface{}{
  1025  			defs.XAgentDetails: map[string]interface{}{
  1026  				"sub_credential_key": "sub_credential_val",
  1027  			},
  1028  		},
  1029  	},
  1030  	Spec: management.CredentialSpec{
  1031  		CredentialRequestDefinition: "api-key",
  1032  		ManagedApplication:          credAppRefName,
  1033  		Data:                        nil,
  1034  		State: management.CredentialSpecState{
  1035  			Name: apiv1.Active,
  1036  		},
  1037  	},
  1038  	Status: &apiv1.ResourceStatus{
  1039  		Level: "",
  1040  	},
  1041  }
  1042  
  1043  func createIDPConfig(s oauth.MockIDPServer) *config.IDPConfiguration {
  1044  	return &config.IDPConfiguration{
  1045  		Name:        "test",
  1046  		Type:        "okta",
  1047  		MetadataURL: s.GetMetadataURL(),
  1048  		AuthConfig: &config.IDPAuthConfiguration{
  1049  			Type:         "client",
  1050  			ClientID:     "test",
  1051  			ClientSecret: "test",
  1052  		},
  1053  		GrantType:        oauth.GrantTypeClientCredentials,
  1054  		ClientScopes:     "read,write",
  1055  		AuthMethod:       config.ClientSecretBasic,
  1056  		AuthResponseType: oauth.AuthResponseToken,
  1057  		ExtraProperties:  config.ExtraProperties{"key": "value"},
  1058  	}
  1059  }