From d90d8d220f465cf6954343ecd882ec0b249d2d0b Mon Sep 17 00:00:00 2001 From: Trevor Johnson Date: Fri, 23 Sep 2022 14:14:05 -0700 Subject: [PATCH] APIGOV-23624 Rotate application credentials (#107) * APIGOV-23624 update * APIGOV-23624 update * APIGOV-23624 tests * APIGOV-23624 - update deps * APIGOV-23624 update * APIGOV-23624 - update deps Co-authored-by: Trevor Johnson Co-authored-by: Jason Collins --- go.mod | 4 +- go.sum | 4 +- pkg/anypoint/client.go | 8 ++ pkg/anypoint/mocks.go | 4 + pkg/cmd/discovery/root.go | 48 ++++-------- pkg/cmd/discovery/root_test.go | 4 +- pkg/discovery/agent.go | 7 ++ pkg/discovery/publish.go | 1 + pkg/discovery/servicehandler.go | 69 ++++++++++++------ pkg/discovery/servicehandler_test.go | 2 +- pkg/discovery/types.go | 1 + pkg/subscription/mock.go | 12 ++- pkg/subscription/mulesubscriptionclient.go | 6 ++ pkg/subscription/provision.go | 37 +++++++++- pkg/subscription/provision_test.go | 85 +++++++++++++++++++++- 15 files changed, 220 insertions(+), 72 deletions(-) diff --git a/go.mod b/go.mod index 66f1a60..60a2207 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,10 @@ module github.com/Axway/agents-mulesoft go 1.18 -// replace github.com/Axway/agent-sdk => /home/ubuntu/go/src/github.com/Axway/agent-sdk +//replace github.com/Axway/agent-sdk => /home/ubuntu/go/src/github.com/Axway/agent-sdk require ( - github.com/Axway/agent-sdk v1.1.32 + github.com/Axway/agent-sdk v1.1.35-0.20220923023651-3b9feb426670 github.com/elastic/beats/v7 v7.17.5 github.com/getkin/kin-openapi v0.76.0 github.com/sirupsen/logrus v1.8.1 diff --git a/go.sum b/go.sum index e7bd6b2..6911cb7 100644 --- a/go.sum +++ b/go.sum @@ -36,8 +36,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Axway/agent-sdk v1.1.32 h1:sYcV2muSOY6QkHHdT/fQbr0VoTxCesqArVFHTunN28M= -github.com/Axway/agent-sdk v1.1.32/go.mod h1:X65UL9Tulf0Gyq4UQzLZtn/A6B+B07sCI65JSZLH9sg= +github.com/Axway/agent-sdk v1.1.35-0.20220923023651-3b9feb426670 h1:XU2ywR8+z+gjMH4XPeciLo8cIYw8Oi8i0DRwLKzuJkI= +github.com/Axway/agent-sdk v1.1.35-0.20220923023651-3b9feb426670/go.mod h1:dj9d7QcFV6kAh4I2dApyCVi/LxT9hVVAD/Bmh15rn4g= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= github.com/Azure/go-autorest v12.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= diff --git a/pkg/anypoint/client.go b/pkg/anypoint/client.go index 305201e..789e264 100644 --- a/pkg/anypoint/client.go +++ b/pkg/anypoint/client.go @@ -45,6 +45,7 @@ type Client interface { OnConfigChange(mulesoftConfig *config.MulesoftConfig) DeleteContract(apiID string, contractID string) error RevokeContract(apiID, contractID string) error + ResetAppSecret(appID int64) (*Application, error) } type AnalyticsClient interface { @@ -413,6 +414,13 @@ func (c *AnypointClient) CreateClientApplication(apiInstanceID string, app *AppR return &application, nil } +func (c *AnypointClient) ResetAppSecret(appID int64) (*Application, error) { + url := fmt.Sprintf("%s/exchange/api/v2/organizations/%s/applications/%v/secret/reset", c.baseURL, c.auth.GetOrgID(), appID) + application := &Application{} + err := c.invokeJSONPost(url, nil, []byte{}, application) + return application, err +} + func (c *AnypointClient) DeleteClientApplication(appID int64) error { url := fmt.Sprintf("%s/exchange/api/v2/organizations/%s/applications/%v", c.baseURL, c.auth.GetOrgID(), appID) diff --git a/pkg/anypoint/mocks.go b/pkg/anypoint/mocks.go index cfab8da..f714163 100644 --- a/pkg/anypoint/mocks.go +++ b/pkg/anypoint/mocks.go @@ -147,3 +147,7 @@ func (m *MockAnypointClient) RevokeContract(apiID, contractID string) error { args := m.Called() return args.Error(0) } + +func (m *MockAnypointClient) ResetAppSecret(appID int64) (*Application, error) { + return nil, nil +} diff --git a/pkg/cmd/discovery/root.go b/pkg/cmd/discovery/root.go index e4a4eeb..86c7587 100644 --- a/pkg/cmd/discovery/root.go +++ b/pkg/cmd/discovery/root.go @@ -3,7 +3,6 @@ package discovery import ( "fmt" - prov "github.com/Axway/agent-sdk/pkg/apic/provisioning" "github.com/Axway/agent-sdk/pkg/migrate" "github.com/Axway/agent-sdk/pkg/util" "github.com/Axway/agent-sdk/pkg/util/log" @@ -71,9 +70,20 @@ func initConfig(centralConfig corecfg.CentralConfig) (interface{}, error) { if util.IsNotTest() { client = anypoint.NewClient(conf.MulesoftConfig) - sm, err := initSubscriptionManager(client, agent.GetCentralClient()) - if err != nil { - return nil, fmt.Errorf("error while initializing the subscription manager %s", err) + muleSubClient := subs.NewMuleSubscriptionClient(client) + entry := logrus.NewEntry(log.Get()) + var sm *subs.Manager + + if centralConfig.IsMarketplaceSubsEnabled() { + agent.RegisterProvisioner(subs.NewProvisioner(muleSubClient, entry)) + agent.NewAPIKeyAccessRequestBuilder().Register() + agent.NewOAuthCredentialRequestBuilder(agent.WithCRDOAuthSecret()).IsRenewable().Register() + } else { + var err error + sm, err = initUCSubscriptionManager(client, agent.GetCentralClient()) + if err != nil { + return nil, fmt.Errorf("error while initializing the subscription manager %s", err) + } } discoveryAgent = discovery.NewAgent(conf, client, sm) @@ -81,7 +91,7 @@ func initConfig(centralConfig corecfg.CentralConfig) (interface{}, error) { return conf, nil } -func initSubscriptionManager(apc anypoint.Client, central apic.Client) (*subs.Manager, error) { +func initUCSubscriptionManager(apc anypoint.Client, central apic.Client) (*subs.Manager, error) { entry := logrus.NewEntry(log.Get()) muleSubClient := subs.NewMuleSubscriptionClient(apc) clientID := subs.NewClientIDContract() @@ -103,33 +113,5 @@ func initSubscriptionManager(apc anypoint.Client, central apic.Client) (*subs.Ma // start polling for subscriptions subManager.Start() - agent.RegisterProvisioner(subs.NewProvisioner(muleSubClient, entry)) - agent.NewAPIKeyAccessRequestBuilder().Register() - newCredentialReq().Register() - return sm, nil } - -func newCredentialReq() prov.CredentialRequestBuilder { - id := prov.NewSchemaPropertyBuilder(). - SetName(common.ClientID). - SetLabel(common.ClientIDLabel). - SetRequired(). - IsString(). - IsEncrypted() - - secret := prov.NewSchemaPropertyBuilder(). - SetName(common.ClientSecret). - SetLabel(common.ClientSecretLabel). - SetRequired(). - IsString(). - IsEncrypted() - - return agent.NewCredentialRequestBuilder(). - SetName(prov.APIKeyCRD). - SetProvisionSchema( - prov.NewSchemaBuilder(). - AddProperty(id). - AddProperty(secret), - ) -} diff --git a/pkg/cmd/discovery/root_test.go b/pkg/cmd/discovery/root_test.go index f15cae4..56a5269 100644 --- a/pkg/cmd/discovery/root_test.go +++ b/pkg/cmd/discovery/root_test.go @@ -44,7 +44,7 @@ func Test_initSubscriptionManager(t *testing.T) { return &apic.MockSubscriptionManager{} } - manager, err := initSubscriptionManager(mc, cc) + manager, err := initUCSubscriptionManager(mc, cc) assert.NotNil(t, manager) assert.Nil(t, err) @@ -59,6 +59,6 @@ func Test_initSubscriptionManager(t *testing.T) { cc.RegisterSubscriptionSchemaMock = func(_ apic.SubscriptionSchema, _ bool) error { return fmt.Errorf("failed") } - _, err = initSubscriptionManager(mc, cc) + _, err = initUCSubscriptionManager(mc, cc) assert.NotNil(t, err) } diff --git a/pkg/discovery/agent.go b/pkg/discovery/agent.go index 6a2a6c3..9dacd54 100644 --- a/pkg/discovery/agent.go +++ b/pkg/discovery/agent.go @@ -6,6 +6,7 @@ import ( "strings" "syscall" + "github.com/Axway/agent-sdk/pkg/util" "github.com/Axway/agents-mulesoft/pkg/subscription" coreAgent "github.com/Axway/agent-sdk/pkg/agent" @@ -50,6 +51,12 @@ func NewAgent(cfg *config.AgentConfig, client anypoint.Client, sm subscription.S cache: c, } + if util.IsNil(sm) { + svcHandler.mode = marketplace + } else { + svcHandler.mode = catalog + } + disc := &discovery{ apiChan: apiChan, cache: c, diff --git a/pkg/discovery/publish.go b/pkg/discovery/publish.go index 80e4d5d..d6ab72c 100644 --- a/pkg/discovery/publish.go +++ b/pkg/discovery/publish.go @@ -90,5 +90,6 @@ func BuildServiceBody(service *ServiceDetail) (apic.ServiceBody, error) { SetURL(service.URL). SetVersion(service.Version). SetAccessRequestDefinitionName(service.AccessRequestDefinition, false). + SetCredentialRequestDefinitions(service.CRDs). Build() } diff --git a/pkg/discovery/servicehandler.go b/pkg/discovery/servicehandler.go index bdbc281..a4063ae 100644 --- a/pkg/discovery/servicehandler.go +++ b/pkg/discovery/servicehandler.go @@ -8,6 +8,7 @@ import ( "strconv" "strings" + "github.com/Axway/agent-sdk/pkg/apic/provisioning" "github.com/Axway/agent-sdk/pkg/cache" "github.com/Axway/agent-sdk/pkg/util/oas" "github.com/Axway/agents-mulesoft/pkg/common" @@ -27,6 +28,11 @@ import ( "sigs.k8s.io/yaml" ) +const ( + marketplace = "marketplace" + catalog = "unified-catalog" +) + // ServiceHandler converts a mulesoft asset to an array of ServiceDetails type ServiceHandler interface { ToServiceDetails(asset *anypoint.Asset) []*ServiceDetail @@ -40,6 +46,7 @@ type serviceHandler struct { client anypoint.Client schemas subs.SchemaStore cache cache.Cache + mode string } func (s *serviceHandler) OnConfigChange(cfg *config.MulesoftConfig) { @@ -94,7 +101,7 @@ func (s *serviceHandler) getServiceDetail(asset *anypoint.Asset, api *anypoint.A if err != nil { return nil, err } - authPolicy, configuration, isSLABased := getAuthPolicy(policies) + authPolicy, configuration, isSLABased := getAuthPolicy(policies, s.mode) logger = logger.WithField("policy", authPolicy) isAlreadyPublished, checksum := isPublished(api, authPolicy, s.cache) @@ -114,27 +121,37 @@ func (s *serviceHandler) getServiceDetail(asset *anypoint.Asset, api *anypoint.A apiID := strconv.FormatInt(api.ID, 10) - subSchName := s.schemas.GetSubscriptionSchemaName(common.PolicyDetail{ - Policy: authPolicy, - IsSLABased: isSLABased, - APIId: apiID, - }) - - // If the API has a new SLA Tier policy, create a new subscription schema for it - if subSchName == "" && isSLABased { - // Get details of the SLA tiers - tiers, err1 := s.client.GetSLATiers(api.ID) - if err1 != nil { - return nil, err1 - } - schema, err1 := s.createSLATierSchema(apiID, tiers, agent.GetCentralClient()) - if err1 != nil { - return nil, err1 + var crds []string + subSchName := "" + ard := "" + if s.mode == marketplace { + if authPolicy == apic.Oauth { + ard = provisioning.APIKeyARD + crds = []string{provisioning.OAuthSecretCRD} } + } else { + subSchName = s.schemas.GetSubscriptionSchemaName(common.PolicyDetail{ + Policy: authPolicy, + IsSLABased: isSLABased, + APIId: apiID, + }) + + // If the API has a new SLA Tier policy, create a new subscription schema for it + if subSchName == "" && isSLABased { + // Get details of the SLA tiers + tiers, err1 := s.client.GetSLATiers(api.ID) + if err1 != nil { + return nil, err1 + } + schema, err1 := s.createSLATierSchema(apiID, tiers, agent.GetCentralClient()) + if err1 != nil { + return nil, err1 + } - logger.Infof("schema registered") + logger.Infof("schema registered") - subSchName = schema.GetSubscriptionName() + subSchName = schema.GetSubscriptionName() + } } exchangeAsset, err := s.client.GetExchangeAsset(api.GroupID, api.AssetID, api.AssetVersion) @@ -179,7 +196,8 @@ func (s *serviceHandler) getServiceDetail(asset *anypoint.Asset, api *anypoint.A } return &ServiceDetail{ - AccessRequestDefinition: subSchName, + AccessRequestDefinition: ard, + CRDs: crds, APIName: api.AssetID, APISpec: modifiedSpec, AuthPolicy: authPolicy, @@ -349,16 +367,21 @@ func getSpecType(file *anypoint.ExchangeFile, specContent []byte) (string, error } // getAuthPolicy gets the authentication policy type. -func getAuthPolicy(policies anypoint.Policies) (string, map[string]interface{}, bool) { +func getAuthPolicy(policies anypoint.Policies, mode string) (string, map[string]interface{}, bool) { + authPolicy := apic.Apikey + if mode == marketplace { + authPolicy = apic.Oauth + } + for _, policy := range policies.Policies { if policy.Template.AssetID == common.ClientIDEnforcement { conf := getMapFromInterface(policy.Configuration) - return apic.Apikey, conf, false + return authPolicy, conf, false } if strings.Contains(policy.Template.AssetID, common.SLABased) { conf := getMapFromInterface(policy.Configuration) - return apic.Apikey, conf, true + return authPolicy, conf, true } if policy.Template.AssetID == common.ExternalOauth { diff --git a/pkg/discovery/servicehandler_test.go b/pkg/discovery/servicehandler_test.go index d98fea1..847506e 100644 --- a/pkg/discovery/servicehandler_test.go +++ b/pkg/discovery/servicehandler_test.go @@ -437,7 +437,7 @@ func Test_getAuthPolicy(t *testing.T) { } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - policy, conf, _ := getAuthPolicy(tc.policies) + policy, conf, _ := getAuthPolicy(tc.policies, catalog) assert.Equal(t, policy, tc.expected) assert.NotNil(t, conf) }) diff --git a/pkg/discovery/types.go b/pkg/discovery/types.go index ab30e78..cf447a0 100644 --- a/pkg/discovery/types.go +++ b/pkg/discovery/types.go @@ -9,6 +9,7 @@ type ServiceDetail struct { APISpec []byte APIUpdateSeverity string AuthPolicy string + CRDs []string Description string Documentation []byte ID string diff --git a/pkg/subscription/mock.go b/pkg/subscription/mock.go index 1c0e19b..4ef7d79 100644 --- a/pkg/subscription/mock.go +++ b/pkg/subscription/mock.go @@ -5,9 +5,11 @@ import ( ) type MockMuleSubscriptionClient struct { - app *anypoint.Application - err error - contract *anypoint.Contract + app *anypoint.Application + newApp *anypoint.Application + err error + rotateErr error + contract *anypoint.Contract } func (m *MockMuleSubscriptionClient) CreateApp(appName, apiID, description string) (*anypoint.Application, error) { @@ -29,3 +31,7 @@ func (m *MockMuleSubscriptionClient) DeleteContract(apiID, contractID string) er func (m *MockMuleSubscriptionClient) GetApp(id string) (*anypoint.Application, error) { return m.app, m.err } + +func (m *MockMuleSubscriptionClient) ResetAppSecret(appID int64) (*anypoint.Application, error) { + return m.newApp, m.rotateErr +} diff --git a/pkg/subscription/mulesubscriptionclient.go b/pkg/subscription/mulesubscriptionclient.go index 3a27864..292b4cc 100644 --- a/pkg/subscription/mulesubscriptionclient.go +++ b/pkg/subscription/mulesubscriptionclient.go @@ -13,6 +13,7 @@ type MuleSubscriptionClient interface { DeleteApp(appID int64) error DeleteContract(apiID, contractID string) error GetApp(id string) (*anypoint.Application, error) + ResetAppSecret(appID int64) (*anypoint.Application, error) } type muleSubscription struct { @@ -26,6 +27,11 @@ func NewMuleSubscriptionClient(client anypoint.Client) MuleSubscriptionClient { } } +// ResetAppSecret resets the secret for an app +func (c muleSubscription) ResetAppSecret(appID int64) (*anypoint.Application, error) { + return c.client.ResetAppSecret(appID) +} + // GetApp gets a mulesoft app by id func (c muleSubscription) GetApp(id string) (*anypoint.Application, error) { return c.client.GetClientApplication(id) diff --git a/pkg/subscription/provision.go b/pkg/subscription/provision.go index aeba606..b0eb099 100644 --- a/pkg/subscription/provision.go +++ b/pkg/subscription/provision.go @@ -164,16 +164,45 @@ func (p provisioner) CredentialProvision(req prov.CredentialRequest) (prov.Reque return p.failed(rs, fmt.Errorf("failed to retrieve app: %s", err)), nil } - cr := prov.NewCredentialBuilder().SetCredential(map[string]interface{}{ - common.ClientID: app.ClientID, - common.ClientSecret: app.ClientSecret, - }) + cr := prov.NewCredentialBuilder().SetOAuthIDAndSecret(app.ClientID, app.ClientSecret) p.log.Info("created credentials") return rs.Success(), cr } +func (p provisioner) CredentialUpdate(req prov.CredentialRequest) (prov.RequestStatus, prov.Credential) { + p.log.Info("updating credential for app %s", req.GetApplicationName()) + rs := prov.NewRequestStatusBuilder() + + appID := req.GetApplicationDetailsValue(common.AppID) + appID64, err := strconv.ParseInt(appID, 10, 64) + if err != nil { + return p.failed(rs, fmt.Errorf("failed to convert appID to int64. %s", err)), nil + } + + // return right away if the action is rotate + if req.GetCredentialAction() != prov.Rotate { + return p.failed(rs, fmt.Errorf("%s is not available for mulesoft credentials", req.GetCredentialAction())), nil + } + + app, err := p.client.GetApp(appID) + if err != nil { + return p.failed(rs, fmt.Errorf("failed to rotate application secret: %s", err)), nil + } + + secret, err := p.client.ResetAppSecret(appID64) + if err != nil { + return p.failed(rs, fmt.Errorf("failed to rotate application secret: %s", err)), nil + } + + cr := prov.NewCredentialBuilder().SetOAuthIDAndSecret(app.ClientID, secret.ClientSecret) + + p.log.Infof("updated credentials for app %s", req.GetApplicationName()) + + return rs.Success(), cr +} + func (p provisioner) failed(rs prov.RequestStatusBuilder, err error) prov.RequestStatus { rs.SetMessage(err.Error()) p.log.Error(err) diff --git a/pkg/subscription/provision_test.go b/pkg/subscription/provision_test.go index b970a5a..de336d0 100644 --- a/pkg/subscription/provision_test.go +++ b/pkg/subscription/provision_test.go @@ -314,8 +314,89 @@ func TestCredentialProvision(t *testing.T) { assert.Equal(t, tc.status.String(), status.GetStatus().String()) if tc.status.String() == prov.Success.String() { assert.NotNil(t, cr) - assert.Contains(t, cr.GetData(), common.ClientSecret) - assert.Contains(t, cr.GetData(), common.ClientID) + assert.Contains(t, cr.GetData(), prov.OauthClientSecret) + assert.Contains(t, cr.GetData(), prov.OauthClientID) + } else { + assert.Nil(t, cr) + } + }) + } +} + +func TestCredentialUpdate(t *testing.T) { + tests := []struct { + name string + appName string + appID string + getAppErr error + rotateErr error + status prov.Status + action prov.CredentialAction + }{ + { + name: "should update credentials", + appName: "app1", + appID: "65432", + status: prov.Success, + action: prov.Rotate, + }, + { + name: "should fail to update credentials when the action is not rotate", + appName: "app1", + appID: "65432", + status: prov.Error, + action: prov.Suspend, + }, + { + name: "should fail to update credentials when making the api call", + appName: "app1", + appID: "65432", + rotateErr: fmt.Errorf("error"), + status: prov.Error, + action: prov.Rotate, + }, + { + name: "should return an error when app is not found", + appName: "app1", + appID: "65432", + status: prov.Error, + action: prov.Rotate, + getAppErr: fmt.Errorf("failed to get app"), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + app := &anypoint.Application{ + ClientID: "12345", + ClientSecret: "lajksdf", + } + newApp := &anypoint.Application{ + ClientID: "12345", + ClientSecret: "uihgobfjd", + } + client := &MockMuleSubscriptionClient{ + err: tc.getAppErr, + rotateErr: tc.rotateErr, + app: app, + newApp: newApp, + } + prv := NewProvisioner(client, logrus.StandardLogger()) + req := mock.MockCredentialRequest{ + AppName: tc.appName, + AppDetails: map[string]string{ + common.AppID: tc.appID, + }, + Action: tc.action, + } + + status, cr := prv.CredentialUpdate(req) + assert.Equal(t, tc.status.String(), status.GetStatus().String()) + if tc.status.String() == prov.Success.String() { + assert.NotNil(t, cr) + assert.Contains(t, cr.GetData(), prov.OauthClientSecret) + assert.Contains(t, cr.GetData(), prov.OauthClientID) + assert.NotEqual(t, app.ClientSecret, cr.GetData()[prov.OauthClientSecret]) } else { assert.Nil(t, cr) }