Skip to content

Commit 1959290

Browse files
chore: iaw uses org setting insted of feature flag (#396)
Co-authored-by: Peter Schäfer <[email protected]>
1 parent d297f61 commit 1959290

File tree

8 files changed

+218
-11
lines changed

8 files changed

+218
-11
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ require (
3131
github.com/oapi-codegen/oapi-codegen/v2 v2.4.1
3232
github.com/oapi-codegen/runtime v1.1.1
3333
github.com/patrickmn/go-cache v2.1.0+incompatible
34-
github.com/snyk/error-catalog-golang-public v0.0.0-20250625135845-2d6f9a31f318
34+
github.com/snyk/error-catalog-golang-public v0.0.0-20250812140843-a01d75260003
3535
github.com/subosito/gotenv v1.6.0
3636
golang.org/x/net v0.38.0
3737
golang.org/x/sync v0.13.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,8 @@ github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnB
230230
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
231231
github.com/snyk/code-client-go v1.21.3 h1:2+HPXCA9FGn3gaI1Jw1C4Ifn/NRAbSnmohFUvz4GC4I=
232232
github.com/snyk/code-client-go v1.21.3/go.mod h1:WH6lNkJc785hfXmwhixxWHix3O6z+1zwz40oK8vl/zg=
233-
github.com/snyk/error-catalog-golang-public v0.0.0-20250625135845-2d6f9a31f318 h1:2bNOlUstBBWHa3doBvdOBlMSu8AC01IHyNexT9MoKiM=
234-
github.com/snyk/error-catalog-golang-public v0.0.0-20250625135845-2d6f9a31f318/go.mod h1:Ytttq7Pw4vOCu9NtRQaOeDU2dhBYUyNBe6kX4+nIIQ4=
233+
github.com/snyk/error-catalog-golang-public v0.0.0-20250812140843-a01d75260003 h1:qeXih9sVe/WvhccE3MfEgglnSVKN1xTQBcsA/N96Kzo=
234+
github.com/snyk/error-catalog-golang-public v0.0.0-20250812140843-a01d75260003/go.mod h1:Ytttq7Pw4vOCu9NtRQaOeDU2dhBYUyNBe6kX4+nIIQ4=
235235
github.com/snyk/go-httpauth v0.0.0-20231117135515-eb445fea7530 h1:s9PHNkL6ueYRiAKNfd8OVxlUOqU3qY0VDbgCD1f6WQY=
236236
github.com/snyk/go-httpauth v0.0.0-20231117135515-eb445fea7530/go.mod h1:88KbbvGYlmLgee4OcQ19yr0bNpXpOr2kciOthaSzCAg=
237237
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=

internal/api/api.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type ApiClient interface {
2525
GetUserMe() (string, error)
2626
GetSelf() (contract.SelfResponse, error)
2727
GetSastSettings(orgId string) (*sast_contract.SastResponse, error)
28+
GetOrgSettings(orgId string) (*contract.OrgSettingsResponse, error)
2829
}
2930

3031
var _ ApiClient = (*snykApiClient)(nil)
@@ -247,6 +248,29 @@ func (a *snykApiClient) GetSastSettings(orgId string) (*sast_contract.SastRespon
247248
return &response, err
248249
}
249250

251+
func (a *snykApiClient) GetOrgSettings(orgId string) (*contract.OrgSettingsResponse, error) {
252+
endpoint := fmt.Sprintf("%s/v1/org/%s/settings", a.url, url.QueryEscape(orgId))
253+
254+
res, err := a.client.Get(endpoint)
255+
if err != nil {
256+
return nil, fmt.Errorf("unable to retrieve org settings: %w", err)
257+
}
258+
//goland:noinspection GoUnhandledErrorResult
259+
defer res.Body.Close()
260+
261+
body, err := io.ReadAll(res.Body)
262+
if err != nil {
263+
return nil, fmt.Errorf("unable to retrieve org settings: %w", err)
264+
}
265+
266+
var response contract.OrgSettingsResponse
267+
if err = json.Unmarshal(body, &response); err != nil {
268+
return nil, fmt.Errorf("unable to retrieve org settings (status: %d): %w", res.StatusCode, err)
269+
}
270+
271+
return &response, err
272+
}
273+
250274
// clientGet performs an HTTP GET request to the Snyk API, handling query parameters,
251275
// API versioning, and basic error checking.
252276
//
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package contract
2+
3+
// API reference: https://docs.snyk.io/snyk-api/reference/organizations-v1#get-org-orgid-settings
4+
5+
type OrgIgnoreSettings struct {
6+
ReasonRequired bool `json:"reasonRequired,omitempty"`
7+
AutoApproveIgnores bool `json:"autoApproveIgnores,omitempty"`
8+
ApprovalWorkflowEnabled bool `json:"approvalWorkflowEnabled,omitempty"`
9+
}
10+
11+
type OrgRequestAccessSettings struct {
12+
Enabled bool `json:"enabled,omitempty"`
13+
}
14+
15+
type OrgSettingsResponse struct {
16+
Ignores *OrgIgnoreSettings `json:"ignores"`
17+
RequestAccess *OrgRequestAccessSettings `json:"requestAccess"`
18+
}

internal/mocks/api.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/local_workflows/ignore_workflow/config.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/google/uuid"
88

99
"github.com/snyk/error-catalog-golang-public/cli"
10+
"github.com/snyk/go-application-framework/internal/api"
1011
policyApi "github.com/snyk/go-application-framework/internal/api/policy/2024-10-15"
1112
"github.com/snyk/go-application-framework/pkg/configuration"
1213
"github.com/snyk/go-application-framework/pkg/local_workflows/local_models"
@@ -43,6 +44,32 @@ func addCreateIgnoreDefaultConfigurationValues(invocationCtx workflow.Invocation
4344
})
4445
}
4546

47+
func getOrgIgnoreApprovalEnabled(engine workflow.Engine) configuration.DefaultValueFunction {
48+
return func(_ configuration.Configuration, existingValue interface{}) (interface{}, error) {
49+
if existingValue != nil {
50+
return existingValue, nil
51+
}
52+
53+
config := engine.GetConfiguration()
54+
org := config.GetString(configuration.ORGANIZATION)
55+
client := engine.GetNetworkAccess().GetHttpClient()
56+
url := config.GetString(configuration.API_URL)
57+
apiClient := api.NewApi(url, client)
58+
59+
settings, err := apiClient.GetOrgSettings(org)
60+
if err != nil {
61+
engine.GetLogger().Err(err).Msg("Failed to access settings.")
62+
return nil, err
63+
}
64+
65+
if settings.Ignores != nil && settings.Ignores.ApprovalWorkflowEnabled {
66+
return true, nil
67+
}
68+
69+
return false, nil
70+
}
71+
}
72+
4673
func remoteRepoUrlDefaultFunc(existingValue interface{}, config configuration.Configuration) (interface{}, error) {
4774
if existingValue != nil && existingValue != "" {
4875
return existingValue, nil

pkg/local_workflows/ignore_workflow/ignore_workflow.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ import (
1010

1111
"github.com/google/uuid"
1212
"github.com/rs/zerolog"
13+
"github.com/snyk/error-catalog-golang-public/snyk_errors"
1314
"github.com/spf13/pflag"
1415

1516
"github.com/snyk/code-client-go/sarif"
1617
"github.com/snyk/error-catalog-golang-public/cli"
1718

1819
policyApi "github.com/snyk/go-application-framework/internal/api/policy/2024-10-15"
1920
"github.com/snyk/go-application-framework/pkg/configuration"
20-
"github.com/snyk/go-application-framework/pkg/local_workflows"
21-
"github.com/snyk/go-application-framework/pkg/local_workflows/config_utils"
21+
localworkflows "github.com/snyk/go-application-framework/pkg/local_workflows"
2222
"github.com/snyk/go-application-framework/pkg/local_workflows/local_models"
2323
"github.com/snyk/go-application-framework/pkg/utils/git"
2424
"github.com/snyk/go-application-framework/pkg/workflow"
@@ -60,6 +60,8 @@ const (
6060

6161
interactiveEnsureVersionControlMessage = "👉🏼 Ensure the code containing the issue is committed and pushed to remote origin, so approvers can review it."
6262
interactiveIgnoreRequestSubmissionMessage = "✅ Your ignore request has been submitted."
63+
64+
ConfigIgnoreApprovalEnabled = "internal_iaw_enabled"
6365
)
6466

6567
var reasonPromptHelpMap = map[string]string{
@@ -88,7 +90,7 @@ func InitIgnoreWorkflows(engine workflow.Engine) error {
8890
return err
8991
}
9092

91-
config_utils.AddFeatureFlagToConfig(engine, configuration.FF_IAW_ENABLED, "ignoreApprovalWorkflow")
93+
engine.GetConfiguration().AddDefaultValue(ConfigIgnoreApprovalEnabled, getOrgIgnoreApprovalEnabled(engine))
9294

9395
return nil
9496
}
@@ -100,8 +102,17 @@ func ignoreCreateWorkflowEntryPoint(invocationCtx workflow.InvocationContext, _
100102
config := invocationCtx.GetConfiguration()
101103
id := invocationCtx.GetWorkflowIdentifier()
102104

103-
if !config.GetBool(configuration.FF_IAW_ENABLED) {
104-
return nil, cli.NewFeatureUnderDevelopmentError("")
105+
enabled, enabledError := config.GetBoolWithError(ConfigIgnoreApprovalEnabled)
106+
if enabledError != nil {
107+
return nil, enabledError
108+
}
109+
110+
if !enabled {
111+
orgName := config.GetString(configuration.ORGANIZATION_SLUG)
112+
appUrl := config.GetString(configuration.WEB_APP_URL)
113+
settingsUrl := fmt.Sprintf("%s/org/%s/manage/settings", appUrl, orgName)
114+
disabledError := cli.NewFeatureNotEnabledError(fmt.Sprintf(`Ignore Approval Workflow is disabled for "%s".`, orgName), snyk_errors.WithLinks([]string{settingsUrl}))
115+
return nil, disabledError
105116
}
106117

107118
interactive := config.GetBool(InteractiveKey)

pkg/local_workflows/ignore_workflow/ignore_workflow_test.go

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/snyk/error-catalog-golang-public/snyk_errors"
1818
"github.com/stretchr/testify/assert"
1919

20+
"github.com/snyk/go-application-framework/internal/api/contract"
2021
policyApi "github.com/snyk/go-application-framework/internal/api/policy/2024-10-15"
2122
"github.com/snyk/go-application-framework/pkg/configuration"
2223
localworkflows "github.com/snyk/go-application-framework/pkg/local_workflows"
@@ -51,7 +52,7 @@ func setupMockIgnoreContext(t *testing.T, payload string, statusCode int) *mocks
5152
config := configuration.New()
5253
config.Set(configuration.API_URL, "https://api.snyk.io")
5354
config.Set(configuration.ORGANIZATION, uuid.New().String())
54-
config.Set(configuration.FF_IAW_ENABLED, true)
55+
config.Set(ConfigIgnoreApprovalEnabled, true)
5556
// setup mocks
5657
ctrl := gomock.NewController(t)
5758
networkAccessMock := mocks.NewMockNetworkAccess(ctrl)
@@ -306,7 +307,7 @@ func Test_ignoreCreateWorkflowEntryPoint(t *testing.T) {
306307

307308
invocationContext := setupMockIgnoreContext(t, "{}", http.StatusCreated)
308309
config := invocationContext.GetConfiguration()
309-
config.Set(configuration.FF_IAW_ENABLED, false)
310+
config.Set(ConfigIgnoreApprovalEnabled, false)
310311

311312
config.Set(InteractiveKey, false)
312313

@@ -330,7 +331,7 @@ func setupInteractiveMockContext(t *testing.T, mockApiResponse string, mockApiSt
330331
config := configuration.New()
331332
config.Set(configuration.API_URL, "https://api.snyk.io")
332333
config.Set(configuration.ORGANIZATION, uuid.New().String())
333-
config.Set(configuration.FF_IAW_ENABLED, true)
334+
config.Set(ConfigIgnoreApprovalEnabled, true)
334335
config.Set(InteractiveKey, true) // Always interactive
335336
config.Set(EnrichResponseKey, true)
336337
config.Set(configuration.ORGANIZATION_SLUG, "some-org")
@@ -705,3 +706,114 @@ func Test_getExpireValue(t *testing.T) {
705706
assert.Nil(t, result)
706707
})
707708
}
709+
710+
func Test_getOrgIgnoreApprovalEnabled(t *testing.T) {
711+
t.Run("returns existing value when not nil", func(t *testing.T) {
712+
ctrl := gomock.NewController(t)
713+
defer ctrl.Finish()
714+
715+
mockEngine := mocks.NewMockEngine(ctrl)
716+
defaultValueFunc := getOrgIgnoreApprovalEnabled(mockEngine)
717+
718+
result, err := defaultValueFunc(nil, true)
719+
assert.NoError(t, err)
720+
assert.Equal(t, true, result)
721+
722+
result, err = defaultValueFunc(nil, false)
723+
assert.NoError(t, err)
724+
assert.Equal(t, false, result)
725+
})
726+
727+
t.Run("approval workflow enabled", func(t *testing.T) {
728+
result, err := setupMockEngineForOrgSettings(t, &contract.OrgSettingsResponse{
729+
Ignores: &contract.OrgIgnoreSettings{ApprovalWorkflowEnabled: true},
730+
})
731+
732+
assert.NoError(t, err)
733+
assert.Equal(t, true, result)
734+
})
735+
736+
t.Run("approval workflow disabled", func(t *testing.T) {
737+
result, err := setupMockEngineForOrgSettings(t, &contract.OrgSettingsResponse{
738+
Ignores: &contract.OrgIgnoreSettings{ApprovalWorkflowEnabled: false},
739+
})
740+
741+
assert.NoError(t, err)
742+
assert.Equal(t, false, result)
743+
})
744+
745+
t.Run("ignores field is nil", func(t *testing.T) {
746+
result, err := setupMockEngineForOrgSettings(t, &contract.OrgSettingsResponse{
747+
Ignores: nil,
748+
})
749+
750+
assert.NoError(t, err)
751+
assert.Equal(t, false, result)
752+
})
753+
754+
t.Run("API call fails", func(t *testing.T) {
755+
ctrl := gomock.NewController(t)
756+
defer ctrl.Finish()
757+
758+
logger := zerolog.Logger{}
759+
orgId := uuid.New().String()
760+
apiUrl := "https://api.snyk.io"
761+
762+
mockEngine := mocks.NewMockEngine(ctrl)
763+
mockConfig := mocks.NewMockConfiguration(ctrl)
764+
mockNetworkAccess := mocks.NewMockNetworkAccess(ctrl)
765+
766+
httpClient := localworkflows.NewTestClient(func(req *http.Request) *http.Response {
767+
return &http.Response{
768+
StatusCode: http.StatusInternalServerError,
769+
Body: io.NopCloser(bytes.NewBufferString("Internal Server Error")),
770+
}
771+
})
772+
773+
mockEngine.EXPECT().GetConfiguration().Return(mockConfig)
774+
mockEngine.EXPECT().GetNetworkAccess().Return(mockNetworkAccess)
775+
mockEngine.EXPECT().GetLogger().Return(&logger)
776+
mockConfig.EXPECT().GetString(configuration.ORGANIZATION).Return(orgId)
777+
mockConfig.EXPECT().GetString(configuration.API_URL).Return(apiUrl)
778+
mockNetworkAccess.EXPECT().GetHttpClient().Return(httpClient)
779+
780+
defaultValueFunc := getOrgIgnoreApprovalEnabled(mockEngine)
781+
result, err := defaultValueFunc(nil, nil)
782+
783+
assert.Error(t, err)
784+
assert.Nil(t, result)
785+
assert.Contains(t, err.Error(), "unable to retrieve org settings")
786+
})
787+
}
788+
789+
func setupMockEngineForOrgSettings(t *testing.T, response *contract.OrgSettingsResponse) (interface{}, error) {
790+
t.Helper()
791+
ctrl := gomock.NewController(t)
792+
defer ctrl.Finish()
793+
794+
orgId := uuid.New().String()
795+
apiUrl := "https://api.snyk.io"
796+
797+
responseJSON, err := json.Marshal(response)
798+
assert.NoError(t, err)
799+
800+
mockEngine := mocks.NewMockEngine(ctrl)
801+
mockConfig := mocks.NewMockConfiguration(ctrl)
802+
mockNetworkAccess := mocks.NewMockNetworkAccess(ctrl)
803+
804+
httpClient := localworkflows.NewTestClient(func(req *http.Request) *http.Response {
805+
return &http.Response{
806+
StatusCode: http.StatusOK,
807+
Body: io.NopCloser(bytes.NewBuffer(responseJSON)),
808+
}
809+
})
810+
811+
mockEngine.EXPECT().GetConfiguration().Return(mockConfig)
812+
mockEngine.EXPECT().GetNetworkAccess().Return(mockNetworkAccess)
813+
mockConfig.EXPECT().GetString(configuration.ORGANIZATION).Return(orgId)
814+
mockConfig.EXPECT().GetString(configuration.API_URL).Return(apiUrl)
815+
mockNetworkAccess.EXPECT().GetHttpClient().Return(httpClient)
816+
817+
defaultValueFunc := getOrgIgnoreApprovalEnabled(mockEngine)
818+
return defaultValueFunc(nil, nil)
819+
}

0 commit comments

Comments
 (0)