diff --git a/internal/api/api.go b/internal/api/api.go index 17c83c3de..762155331 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -25,6 +25,7 @@ type ApiClient interface { GetUserMe() (string, error) GetSelf() (contract.SelfResponse, error) GetSastSettings(orgId string) (*sast_contract.SastResponse, error) + GetOrgSettings(orgId string) (*contract.OrgSettingsResponse, error) } var _ ApiClient = (*snykApiClient)(nil) @@ -247,6 +248,21 @@ func (a *snykApiClient) GetSastSettings(orgId string) (*sast_contract.SastRespon return &response, err } +func (a *snykApiClient) GetOrgSettings(orgId string) (*contract.OrgSettingsResponse, error) { + endpoint := fmt.Sprintf("/v1/org/%s/settings", url.QueryEscape(orgId)) + body, err := clientGet(a, endpoint, nil) + if err != nil { + return nil, fmt.Errorf("unable to retrieve org settings: %w", err) + } + + var response contract.OrgSettingsResponse + if err = json.Unmarshal(body, &response); err != nil { + return nil, err + } + + return &response, err +} + // clientGet performs an HTTP GET request to the Snyk API, handling query parameters, // API versioning, and basic error checking. // diff --git a/internal/api/contract/OrgSettingsResponse.go b/internal/api/contract/OrgSettingsResponse.go new file mode 100644 index 000000000..850c07847 --- /dev/null +++ b/internal/api/contract/OrgSettingsResponse.go @@ -0,0 +1,16 @@ +package contract + +type OrgIgnoreSettings struct { + ReasonRequired bool `json:"reasonRequired,omitempty"` + AutoApproveIgnores bool `json:"autoApproveIgnores,omitempty"` + ApprovalWorkflowEnabled bool `json:"approvalWorkflowEnabled,omitempty"` +} + +type OrgRequestAccessSettings struct { + Enabled bool `json:"enabled,omitempty"` +} + +type OrgSettingsResponse struct { + Ignores *OrgIgnoreSettings `json:"ignores"` + RequestAccess *OrgRequestAccessSettings `json:"requestAccess"` +} diff --git a/internal/mocks/api.go b/internal/mocks/api.go index dfbaca4d6..4435b5b93 100644 --- a/internal/mocks/api.go +++ b/internal/mocks/api.go @@ -81,6 +81,21 @@ func (mr *MockApiClientMockRecorder) GetOrgIdFromSlug(slugName interface{}) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrgIdFromSlug", reflect.TypeOf((*MockApiClient)(nil).GetOrgIdFromSlug), slugName) } +// GetOrgSettings mocks base method. +func (m *MockApiClient) GetOrgSettings(orgId string) (*contract.OrgSettingsResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetOrgSettings", orgId) + ret0, _ := ret[0].(*contract.OrgSettingsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetOrgSettings indicates an expected call of GetOrgSettings. +func (mr *MockApiClientMockRecorder) GetOrgSettings(orgId interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrgSettings", reflect.TypeOf((*MockApiClient)(nil).GetOrgSettings), orgId) +} + // GetOrganizations mocks base method. func (m *MockApiClient) GetOrganizations(limit int) (*contract.OrganizationsResponse, error) { m.ctrl.T.Helper() diff --git a/pkg/local_workflows/ignore_workflow/config.go b/pkg/local_workflows/ignore_workflow/config.go index 94a79d59d..56b94f81b 100644 --- a/pkg/local_workflows/ignore_workflow/config.go +++ b/pkg/local_workflows/ignore_workflow/config.go @@ -7,6 +7,7 @@ import ( "github.com/google/uuid" "github.com/snyk/error-catalog-golang-public/cli" + "github.com/snyk/go-application-framework/internal/api" policyApi "github.com/snyk/go-application-framework/internal/api/policy/2024-10-15" "github.com/snyk/go-application-framework/pkg/configuration" "github.com/snyk/go-application-framework/pkg/local_workflows/local_models" @@ -43,6 +44,32 @@ func addCreateIgnoreDefaultConfigurationValues(invocationCtx workflow.Invocation }) } +func getOrgIgnoreApprovalEnabled(engine workflow.Engine) configuration.DefaultValueFunction { + return func(_ configuration.Configuration, existingValue interface{}) (interface{}, error) { + if existingValue != nil { + return existingValue, nil + } + + config := engine.GetConfiguration() + org := config.GetString(configuration.ORGANIZATION) + client := engine.GetNetworkAccess().GetHttpClient() + url := config.GetString(configuration.API_URL) + apiClient := api.NewApi(url, client) + + settings, err := apiClient.GetOrgSettings(org) + if err != nil { + engine.GetLogger().Err(err).Msg("Failed to access settings.") + return nil, err + } + + if settings.Ignores == nil { + return false, nil + } + + return settings.Ignores.ApprovalWorkflowEnabled, nil + } +} + func remoteRepoUrlDefaultFunc(existingValue interface{}, config configuration.Configuration) (interface{}, error) { if existingValue != nil && existingValue != "" { return existingValue, nil diff --git a/pkg/local_workflows/ignore_workflow/ignore_workflow.go b/pkg/local_workflows/ignore_workflow/ignore_workflow.go index 03e75d188..c44753935 100644 --- a/pkg/local_workflows/ignore_workflow/ignore_workflow.go +++ b/pkg/local_workflows/ignore_workflow/ignore_workflow.go @@ -14,11 +14,11 @@ import ( "github.com/snyk/code-client-go/sarif" "github.com/snyk/error-catalog-golang-public/cli" + "github.com/snyk/error-catalog-golang-public/snyk_errors" policyApi "github.com/snyk/go-application-framework/internal/api/policy/2024-10-15" "github.com/snyk/go-application-framework/pkg/configuration" - "github.com/snyk/go-application-framework/pkg/local_workflows" - "github.com/snyk/go-application-framework/pkg/local_workflows/config_utils" + localworkflows "github.com/snyk/go-application-framework/pkg/local_workflows" "github.com/snyk/go-application-framework/pkg/local_workflows/local_models" "github.com/snyk/go-application-framework/pkg/utils/git" "github.com/snyk/go-application-framework/pkg/workflow" @@ -60,6 +60,8 @@ const ( interactiveEnsureVersionControlMessage = "👉🏼 Ensure the code containing the issue is committed and pushed to remote origin, so approvers can review it." interactiveIgnoreRequestSubmissionMessage = "✅ Your ignore request has been submitted." + + ConfigIgnoreApprovalEnabled = "internal_iaw_enabled" ) var reasonPromptHelpMap = map[string]string{ @@ -88,7 +90,7 @@ func InitIgnoreWorkflows(engine workflow.Engine) error { return err } - config_utils.AddFeatureFlagToConfig(engine, configuration.FF_IAW_ENABLED, "ignoreApprovalWorkflow") + engine.GetConfiguration().AddDefaultValue(ConfigIgnoreApprovalEnabled, getOrgIgnoreApprovalEnabled(engine)) return nil } @@ -100,8 +102,19 @@ func ignoreCreateWorkflowEntryPoint(invocationCtx workflow.InvocationContext, _ config := invocationCtx.GetConfiguration() id := invocationCtx.GetWorkflowIdentifier() - if !config.GetBool(configuration.FF_IAW_ENABLED) { - return nil, cli.NewFeatureUnderDevelopmentError("") + if !config.GetBool(ConfigIgnoreApprovalEnabled) { + orgName := config.GetString(configuration.ORGANIZATION_SLUG) + return nil, snyk_errors.Error{ + Type: "https://docs.snyk.io/scan-with-snyk/error-catalog#snyk-cli-0016", + Title: "Feature not enabled", + Description: "This feature is disabled for your current organization. You can enable it in the settings or switch to an organization where it's already enabled.", + StatusCode: 403, + ErrorCode: "SNYK-CLI-0016", + Classification: "ACTIONABLE", + Links: []string{}, + Level: "error", + Detail: fmt.Sprintf("The Ignore Approval Workflow feature must be enabled for the %s organization. Enable it in your organization settings: Settings > General > Ignore approval workflow for Snyk Code.", orgName), + } } interactive := config.GetBool(InteractiveKey) diff --git a/pkg/local_workflows/ignore_workflow/ignore_workflow_test.go b/pkg/local_workflows/ignore_workflow/ignore_workflow_test.go index 9d8945394..bc8d3e4f5 100644 --- a/pkg/local_workflows/ignore_workflow/ignore_workflow_test.go +++ b/pkg/local_workflows/ignore_workflow/ignore_workflow_test.go @@ -51,7 +51,7 @@ func setupMockIgnoreContext(t *testing.T, payload string, statusCode int) *mocks config := configuration.New() config.Set(configuration.API_URL, "https://api.snyk.io") config.Set(configuration.ORGANIZATION, uuid.New().String()) - config.Set(configuration.FF_IAW_ENABLED, true) + config.Set(ConfigIgnoreApprovalEnabled, true) // setup mocks ctrl := gomock.NewController(t) networkAccessMock := mocks.NewMockNetworkAccess(ctrl) @@ -306,7 +306,7 @@ func Test_ignoreCreateWorkflowEntryPoint(t *testing.T) { invocationContext := setupMockIgnoreContext(t, "{}", http.StatusCreated) config := invocationContext.GetConfiguration() - config.Set(configuration.FF_IAW_ENABLED, false) + config.Set(ConfigIgnoreApprovalEnabled, false) config.Set(InteractiveKey, false) @@ -330,7 +330,7 @@ func setupInteractiveMockContext(t *testing.T, mockApiResponse string, mockApiSt config := configuration.New() config.Set(configuration.API_URL, "https://api.snyk.io") config.Set(configuration.ORGANIZATION, uuid.New().String()) - config.Set(configuration.FF_IAW_ENABLED, true) + config.Set(ConfigIgnoreApprovalEnabled, true) config.Set(InteractiveKey, true) // Always interactive config.Set(EnrichResponseKey, true) config.Set(configuration.ORGANIZATION_SLUG, "some-org")