diff --git a/CHANGELOG.md b/CHANGELOG.md index 43232925..09dc3e0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # EDGEGRID GOLANG RELEASE NOTES +## 8.4.0 (Aug 22, 2024) + +#### FEATURES/ENHANCEMENTS: + +* APPSEC + * Added field `ClientLists` to `RuleConditions` and `AttackGroupConditions` + * The `RequestBodyInspectionLimitOverride` field has been added in the following structures: + * `GetAdvancedSettingsRequestBodyResponse`, + * `UpdateAdvancedSettingsRequestBodyRequest`, + * `UpdateAdvancedSettingsRequestBodyResponse`, + * `RemoveAdvancedSettingsRequestBodyRequest`, + * `RemoveAdvancedSettingsRequestBodyResponse` + +* IAM + * Added new methods: + * [GetProperty](https://techdocs.akamai.com/iam-api/reference/get-property) + * [ListProperties](https://techdocs.akamai.com/iam-api/reference/get-properties) + * [MoveProperty](https://techdocs.akamai.com/iam-api/reference/put-property) + * `MapPropertyIDToName` - to provide property name for given IAM property ID + +* PAPI + * Added new method `MapPropertyNameToID` to provide PAPI property ID for given property name + ## 8.3.0 (July 09, 2024) #### FEATURES/ENHANCEMENTS: diff --git a/go.mod b/go.mod index 26495052..a4b26851 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/akamai/AkamaiOPEN-edgegrid-golang/v8 go 1.21 require ( - github.com/akamai/AkamaiOPEN-edgegrid-golang/v7 v7.6.1 github.com/apex/log v1.9.0 github.com/go-ozzo/ozzo-validation/v4 v4.3.0 github.com/google/uuid v1.1.1 @@ -20,10 +19,12 @@ require ( github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect - github.com/jtolds/gls v4.20.0+incompatible // indirect + github.com/kr/text v0.2.0 // indirect + github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/smartystreets/goconvey v1.6.4 // indirect github.com/stretchr/objx v0.5.0 // indirect + gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c527ac51..f829348e 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/akamai/AkamaiOPEN-edgegrid-golang/v7 v7.6.1 h1:KrYkNvCKBGPs/upjgJCojZnnmt5XdEPWS4L2zRQm7+o= -github.com/akamai/AkamaiOPEN-edgegrid-golang/v7 v7.6.1/go.mod h1:gajRk0oNRQj4bHUc2SGAvAp/gPestSpuvK4QXU1QtPA= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= github.com/apex/log v1.9.0 h1:FHtw/xuaM8AgmvDDTI9fiwoAL25Sq2cxojnZICUU8l0= @@ -11,6 +9,7 @@ github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06 github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -54,6 +53,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= @@ -88,6 +88,7 @@ go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6m golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= @@ -100,6 +101,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= diff --git a/pkg/appsec/advanced_settings_request_body.go b/pkg/appsec/advanced_settings_request_body.go index c36e92c1..0228c5b2 100644 --- a/pkg/appsec/advanced_settings_request_body.go +++ b/pkg/appsec/advanced_settings_request_body.go @@ -44,33 +44,38 @@ type ( // GetAdvancedSettingsRequestBodyResponse is returned from a call to GetAdvancedSettingsRequestBody. GetAdvancedSettingsRequestBodyResponse struct { - RequestBodyInspectionLimitInKB RequestBodySizeLimit `json:"requestBodyInspectionLimitInKB"` + RequestBodyInspectionLimitInKB RequestBodySizeLimit `json:"requestBodyInspectionLimitInKB"` + RequestBodyInspectionLimitOverride bool `json:"override"` } // UpdateAdvancedSettingsRequestBodyRequest is used to update the Request body settings for a configuration or policy. UpdateAdvancedSettingsRequestBodyRequest struct { - ConfigID int - Version int - PolicyID string - RequestBodyInspectionLimitInKB RequestBodySizeLimit `json:"requestBodyInspectionLimitInKB"` + ConfigID int + Version int + PolicyID string + RequestBodyInspectionLimitInKB RequestBodySizeLimit `json:"requestBodyInspectionLimitInKB"` + RequestBodyInspectionLimitOverride bool `json:"override"` } // UpdateAdvancedSettingsRequestBodyResponse is returned from a call to UpdateAdvancedSettingsRequestBody. UpdateAdvancedSettingsRequestBodyResponse struct { - RequestBodyInspectionLimitInKB RequestBodySizeLimit `json:"requestBodyInspectionLimitInKB"` + RequestBodyInspectionLimitInKB RequestBodySizeLimit `json:"requestBodyInspectionLimitInKB"` + RequestBodyInspectionLimitOverride bool `json:"override"` } // RemoveAdvancedSettingsRequestBodyRequest is used to reset the Request body settings for a configuration or policy. RemoveAdvancedSettingsRequestBodyRequest struct { - ConfigID int - Version int - PolicyID string - RequestBodyInspectionLimitInKB RequestBodySizeLimit `json:"requestBodyInspectionLimitInKB"` + ConfigID int + Version int + PolicyID string + RequestBodyInspectionLimitInKB RequestBodySizeLimit `json:"requestBodyInspectionLimitInKB"` + RequestBodyInspectionLimitOverride bool `json:"override"` } // RemoveAdvancedSettingsRequestBodyResponse is returned from a call to RemoveAdvancedSettingsRequestBody. RemoveAdvancedSettingsRequestBodyResponse struct { - RequestBodyInspectionLimitInKB RequestBodySizeLimit `json:"requestBodyInspectionLimitInKB"` + RequestBodyInspectionLimitInKB RequestBodySizeLimit `json:"requestBodyInspectionLimitInKB"` + RequestBodyInspectionLimitOverride bool `json:"override"` } // RequestBodySizeLimit is used to create an "enum" of possible types default, 8, 16, 32 diff --git a/pkg/appsec/advanced_settings_request_body_test.go b/pkg/appsec/advanced_settings_request_body_test.go index 92dc1588..e85eba2c 100644 --- a/pkg/appsec/advanced_settings_request_body_test.go +++ b/pkg/appsec/advanced_settings_request_body_test.go @@ -164,7 +164,7 @@ func TestAppsecGetAdvancedSettingsRequestBodyPolicy(t *testing.T) { result := GetAdvancedSettingsRequestBodyResponse{} - respData := compactJSON(loadFixtureBytes("testdata/TestAdvancedSettingsRequestBody/AdvancedSettingsRequestBody.json")) + respData := compactJSON(loadFixtureBytes("testdata/TestAdvancedSettingsRequestBody/AdvancedSettingsRequestBodyPolicy.json")) err := json.Unmarshal([]byte(respData), &result) require.NoError(t, err) @@ -318,13 +318,13 @@ func TestAppsecUpdateAdvancedSettingsRequestBody(t *testing.T) { func TestAppsecUpdateAdvancedSettingsRequestBodyPolicy(t *testing.T) { result := UpdateAdvancedSettingsRequestBodyResponse{} - respData := compactJSON(loadFixtureBytes("testdata/TestAdvancedSettingsRequestBody/AdvancedSettingsRequestBody.json")) + respData := compactJSON(loadFixtureBytes("testdata/TestAdvancedSettingsRequestBody/AdvancedSettingsRequestBodyPolicy.json")) err := json.Unmarshal([]byte(respData), &result) require.NoError(t, err) req := UpdateAdvancedSettingsRequestBodyRequest{} - reqData := compactJSON(loadFixtureBytes("testdata/TestAdvancedSettingsRequestBody/AdvancedSettingsRequestBody.json")) + reqData := compactJSON(loadFixtureBytes("testdata/TestAdvancedSettingsRequestBody/AdvancedSettingsRequestBodyPolicy.json")) err = json.Unmarshal([]byte(reqData), &req) require.NoError(t, err) @@ -351,6 +351,26 @@ func TestAppsecUpdateAdvancedSettingsRequestBodyPolicy(t *testing.T) { expectedResponse: &result, expectedPath: "/appsec/v1/configs/43253/versions/15/security-policies/test_policy/advanced-settings/request-body", }, + "400 invalid input error": { + params: UpdateAdvancedSettingsRequestBodyRequest{ + ConfigID: 43253, + Version: 15, + }, + responseStatus: http.StatusBadRequest, + responseBody: ` + { + "detail": "The value of the request body size parameter must be one of [default, 8, 16, 32]", + "title": "Invalid Input Error", + "type": "internal_error" + }`, + expectedPath: "/appsec/v1/configs/43253/versions/15/advanced-settings/request-body", + withError: &Error{ + Type: "internal_error", + Title: "Invalid Input Error", + Detail: "The value of the request body size parameter must be one of [default, 8, 16, 32]", + StatusCode: http.StatusBadRequest, + }, + }, "500 internal server error": { params: UpdateAdvancedSettingsRequestBodyRequest{ ConfigID: 43253, @@ -398,3 +418,139 @@ func TestAppsecUpdateAdvancedSettingsRequestBodyPolicy(t *testing.T) { }) } } + +func TestAppsecUpdateAdvancedSettingsRequestBodyPolicyWithInvalidValue(t *testing.T) { + result := UpdateAdvancedSettingsRequestBodyResponse{} + + respData := compactJSON(loadFixtureBytes("testdata/TestAdvancedSettingsRequestBody/AdvancedSettingsRequestBodyPolicyWithInvalidValue.json")) + err := json.Unmarshal([]byte(respData), &result) + require.NoError(t, err) + + req := UpdateAdvancedSettingsRequestBodyRequest{} + + reqData := compactJSON(loadFixtureBytes("testdata/TestAdvancedSettingsRequestBody/AdvancedSettingsRequestBodyPolicyWithInvalidValue.json")) + err = json.Unmarshal([]byte(reqData), &req) + require.NoError(t, err) + + tests := map[string]struct { + params UpdateAdvancedSettingsRequestBodyRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *UpdateAdvancedSettingsRequestBodyResponse + withError error + headers http.Header + }{ + "400 invalid input error": { + params: UpdateAdvancedSettingsRequestBodyRequest{ + ConfigID: 43253, + Version: 15, + RequestBodyInspectionLimitInKB: req.RequestBodyInspectionLimitInKB, + RequestBodyInspectionLimitOverride: req.RequestBodyInspectionLimitOverride, + }, + responseStatus: http.StatusBadRequest, + responseBody: ` + { + "detail": "The value of the request body size parameter must be one of [default, 8, 16, 32]", + "title": "Invalid Input Error", + "type": "internal_error" + }`, + expectedPath: "/appsec/v1/configs/43253/versions/15/advanced-settings/request-body", + withError: &Error{ + Type: "internal_error", + Title: "Invalid Input Error", + Detail: "The value of the request body size parameter must be one of [default, 8, 16, 32]", + StatusCode: http.StatusBadRequest, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPut, r.Method) + w.WriteHeader(test.responseStatus) + if len(test.responseBody) > 0 { + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + } + })) + client := mockAPIClient(t, mockServer) + result, err := client.UpdateAdvancedSettingsRequestBody( + session.ContextWithOptions( + context.Background(), + session.WithContextHeaders(test.headers)), test.params) + if test.withError != nil { + assert.True(t, errors.Is(err, test.withError), "want: %s; got: %s", test.withError, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func TestAppsecUpdateAdvancedSettingsRequestBodyPolicyWithOverrideUnset(t *testing.T) { + result := RemoveAdvancedSettingsRequestBodyResponse{} + + respData := compactJSON(loadFixtureBytes("testdata/TestAdvancedSettingsRequestBody/AdvancedSettingsRequestBodyPolicyWithOverrideUnsetResponse.json")) + err := json.Unmarshal([]byte(respData), &result) + require.NoError(t, err) + + req := RemoveAdvancedSettingsRequestBodyRequest{} + + reqData := compactJSON(loadFixtureBytes("testdata/TestAdvancedSettingsRequestBody/AdvancedSettingsRequestBodyPolicyWithOverrideUnsetRequest.json")) + err = json.Unmarshal([]byte(reqData), &req) + require.NoError(t, err) + + tests := map[string]struct { + params RemoveAdvancedSettingsRequestBodyRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *RemoveAdvancedSettingsRequestBodyResponse + withError error + headers http.Header + }{ + "200 Success": { + params: RemoveAdvancedSettingsRequestBodyRequest{ + ConfigID: 43253, + Version: 15, + PolicyID: "test_policy", + RequestBodyInspectionLimitInKB: req.RequestBodyInspectionLimitInKB, + RequestBodyInspectionLimitOverride: req.RequestBodyInspectionLimitOverride, + }, + headers: http.Header{ + "Content-Type": []string{"application/json;charset=UTF-8"}, + }, + responseStatus: http.StatusCreated, + responseBody: respData, + expectedResponse: &result, + expectedPath: "/appsec/v1/configs/43253/versions/15/security-policies/test_policy/advanced-settings/request-body", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPut, r.Method) + w.WriteHeader(test.responseStatus) + if len(test.responseBody) > 0 { + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + } + })) + client := mockAPIClient(t, mockServer) + result, err := client.RemoveAdvancedSettingsRequestBody( + session.ContextWithOptions( + context.Background(), + session.WithContextHeaders(test.headers)), test.params) + if test.withError != nil { + assert.True(t, errors.Is(err, test.withError), "want: %s; got: %s", test.withError, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} diff --git a/pkg/appsec/attack_group.go b/pkg/appsec/attack_group.go index abe95470..2647b167 100644 --- a/pkg/appsec/attack_group.go +++ b/pkg/appsec/attack_group.go @@ -63,6 +63,7 @@ type ( ValueCase bool `json:"valueCase,omitempty"` ValueWildcard bool `json:"valueWildcard,omitempty"` UseHeaders bool `json:"useHeaders,omitempty"` + ClientLists []string `json:"clientLists,omitempty"` } // AttackGroupAdvancedCriteria describes the hostname and path criteria used to limit the scope of an exception. diff --git a/pkg/appsec/export_configuration.go b/pkg/appsec/export_configuration.go index dd946bc8..0c208542 100644 --- a/pkg/appsec/export_configuration.go +++ b/pkg/appsec/export_configuration.go @@ -694,7 +694,8 @@ type ( // RequestBody is returned as part of GetExportConfigurationResponse. RequestBody struct { - RequestBodyInspectionLimitInKB string `json:"requestBodyInspectionLimitInKB"` + RequestBodyInspectionLimitInKB string `json:"requestBodyInspectionLimitInKB"` + RequestBodyInspectionLimitOverride bool `json:"override"` } // ConditionsExp is returned as part of GetExportConfigurationResponse. diff --git a/pkg/appsec/rule.go b/pkg/appsec/rule.go index bd996dd9..30d7bb04 100644 --- a/pkg/appsec/rule.go +++ b/pkg/appsec/rule.go @@ -111,6 +111,7 @@ type ( ValueCase bool `json:"valueCase,omitempty"` ValueWildcard bool `json:"valueWildcard,omitempty"` UseHeaders bool `json:"useHeaders,omitempty"` + ClientLists []string `json:"clientLists,omitempty"` } // RuleException is used to describe the exceptions for a rule. diff --git a/pkg/appsec/testdata/TestAdvancedSettingsRequestBody/AdvancedSettingsRequestBodyPolicy.json b/pkg/appsec/testdata/TestAdvancedSettingsRequestBody/AdvancedSettingsRequestBodyPolicy.json new file mode 100644 index 00000000..7560ef0e --- /dev/null +++ b/pkg/appsec/testdata/TestAdvancedSettingsRequestBody/AdvancedSettingsRequestBodyPolicy.json @@ -0,0 +1,4 @@ +{ + "override": true, + "requestBodyInspectionLimitInKB": "32" +} \ No newline at end of file diff --git a/pkg/appsec/testdata/TestAdvancedSettingsRequestBody/AdvancedSettingsRequestBodyPolicyWithInvalidValue.json b/pkg/appsec/testdata/TestAdvancedSettingsRequestBody/AdvancedSettingsRequestBodyPolicyWithInvalidValue.json new file mode 100644 index 00000000..021deb58 --- /dev/null +++ b/pkg/appsec/testdata/TestAdvancedSettingsRequestBody/AdvancedSettingsRequestBodyPolicyWithInvalidValue.json @@ -0,0 +1,4 @@ +{ + "override": true, + "requestBodyInspectionLimitInKB": "abcd" +} \ No newline at end of file diff --git a/pkg/appsec/testdata/TestAdvancedSettingsRequestBody/AdvancedSettingsRequestBodyPolicyWithOverrideUnsetRequest.json b/pkg/appsec/testdata/TestAdvancedSettingsRequestBody/AdvancedSettingsRequestBodyPolicyWithOverrideUnsetRequest.json new file mode 100644 index 00000000..a6edb021 --- /dev/null +++ b/pkg/appsec/testdata/TestAdvancedSettingsRequestBody/AdvancedSettingsRequestBodyPolicyWithOverrideUnsetRequest.json @@ -0,0 +1,4 @@ +{ + "override": false, + "requestBodyInspectionLimitInKB": "16" +} \ No newline at end of file diff --git a/pkg/appsec/testdata/TestAdvancedSettingsRequestBody/AdvancedSettingsRequestBodyPolicyWithOverrideUnsetResponse.json b/pkg/appsec/testdata/TestAdvancedSettingsRequestBody/AdvancedSettingsRequestBodyPolicyWithOverrideUnsetResponse.json new file mode 100644 index 00000000..e4231bad --- /dev/null +++ b/pkg/appsec/testdata/TestAdvancedSettingsRequestBody/AdvancedSettingsRequestBodyPolicyWithOverrideUnsetResponse.json @@ -0,0 +1,3 @@ +{ + "requestBodyInspectionLimitInKB": "default" +} \ No newline at end of file diff --git a/pkg/appsec/testdata/TestExportConfiguration/ExportConfiguration.json b/pkg/appsec/testdata/TestExportConfiguration/ExportConfiguration.json index d80d1125..cd8ddfe4 100644 --- a/pkg/appsec/testdata/TestExportConfiguration/ExportConfiguration.json +++ b/pkg/appsec/testdata/TestExportConfiguration/ExportConfiguration.json @@ -1851,6 +1851,9 @@ } } } + }, + "RequestBody": { + "requestBodyInspectionLimitInKB" : "8" } } ], diff --git a/pkg/appsec/testdata/TestPenaltyBoxConditions/PenaltyBoxConditions.json b/pkg/appsec/testdata/TestPenaltyBoxConditions/PenaltyBoxConditions.json index 09d0c537..4844cbf3 100644 --- a/pkg/appsec/testdata/TestPenaltyBoxConditions/PenaltyBoxConditions.json +++ b/pkg/appsec/testdata/TestPenaltyBoxConditions/PenaltyBoxConditions.json @@ -8,6 +8,15 @@ ], "order": 0, "positiveMatch": true + }, + { + "type": "clientListMatch", + "clientLists": [ + "149526_REPUTATIONALLOWLISTSECC" + ], + "useHeaders": false, + "order": 0, + "positiveMatch": true } ] } diff --git a/pkg/appsec/testdata/TestRule/Rules.json b/pkg/appsec/testdata/TestRule/Rules.json index 30643ce3..6f9601b7 100644 --- a/pkg/appsec/testdata/TestRule/Rules.json +++ b/pkg/appsec/testdata/TestRule/Rules.json @@ -143,10 +143,19 @@ "/p2" ], "positiveMatch": true + }, + { + "type": "clientListMatch", + "clientLists": [ + "/p1", + "/p2" + ], + "positiveMatch": true, + "userHeaders": true } ] }, "id": 950908 } ] -} \ No newline at end of file +} diff --git a/pkg/iam/iam.go b/pkg/iam/iam.go index 622ef8c0..e3476951 100644 --- a/pkg/iam/iam.go +++ b/pkg/iam/iam.go @@ -17,6 +17,7 @@ type ( IAM interface { BlockedProperties Groups + Properties Roles Support UserLock diff --git a/pkg/iam/mocks.go b/pkg/iam/mocks.go index 9b2cbb03..f3b5a392 100644 --- a/pkg/iam/mocks.go +++ b/pkg/iam/mocks.go @@ -316,3 +316,49 @@ func (m *Mock) SetUserPassword(ctx context.Context, request SetUserPasswordReque return args.Error(0) } + +func (m *Mock) ListProperties(ctx context.Context, request ListPropertiesRequest) (*ListPropertiesResponse, error) { + args := m.Called(ctx, request) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(*ListPropertiesResponse), args.Error(1) +} + +func (m *Mock) GetProperty(ctx context.Context, request GetPropertyRequest) (*GetPropertyResponse, error) { + args := m.Called(ctx, request) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(*GetPropertyResponse), args.Error(1) +} + +func (m *Mock) MoveProperty(ctx context.Context, request MovePropertyRequest) error { + args := m.Called(ctx, request) + + return args.Error(0) +} + +func (m *Mock) MapPropertyIDToName(ctx context.Context, request MapPropertyIDToNameRequest) (*string, error) { + args := m.Called(ctx, request) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(*string), args.Error(1) +} + +func (m *Mock) MapPropertyNameToID(ctx context.Context, request MapPropertyNameToIDRequest) (*int64, error) { + args := m.Called(ctx, request) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(*int64), args.Error(1) +} diff --git a/pkg/iam/properties.go b/pkg/iam/properties.go new file mode 100644 index 00000000..415108c8 --- /dev/null +++ b/pkg/iam/properties.go @@ -0,0 +1,289 @@ +package iam + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/edgegriderr" + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +type ( + // Properties is the IAM properties API interface + Properties interface { + // ListProperties lists the properties for the current account or other managed accounts using the accountSwitchKey parameter. + // + // See: https://techdocs.akamai.com/iam-api/reference/get-properties + ListProperties(context.Context, ListPropertiesRequest) (*ListPropertiesResponse, error) + + // GetProperty lists a property's details. + // + // See: https://techdocs.akamai.com/iam-api/reference/get-property + GetProperty(context.Context, GetPropertyRequest) (*GetPropertyResponse, error) + + // MoveProperty moves a property from one group to another group. + // + // See: https://techdocs.akamai.com/iam-api/reference/put-property + MoveProperty(context.Context, MovePropertyRequest) error + + // MapPropertyIDToName returns property name for given (IAM) property ID + // Mainly to be used to map (IAM) Property ID to (PAPI) Property ID + // To finish the mapping, please use papi.MapPropertyNameToID + MapPropertyIDToName(context.Context, MapPropertyIDToNameRequest) (*string, error) + } + + // ListPropertiesRequest contains the request parameters for the list properties operation. + ListPropertiesRequest struct { + GroupID int64 + Actions bool + } + + // GetPropertyRequest contains the request parameters for the get property operation. + GetPropertyRequest struct { + PropertyID int64 + GroupID int64 + } + + // MapPropertyNameToIDRequest is the argument for MapPropertyNameToID + MapPropertyNameToIDRequest string + + // ListPropertiesResponse holds the response data from ListProperties. + ListPropertiesResponse []Property + + // GetPropertyResponse holds the response data from GetProperty. + GetPropertyResponse struct { + ARLConfigFile string `json:"arlConfigFile"` + CreatedBy string `json:"createdBy"` + CreatedDate time.Time `json:"createdDate"` + GroupID int64 `json:"groupId"` + GroupName string `json:"groupName"` + ModifiedBy string `json:"modifiedBy"` + ModifiedDate time.Time `json:"modifiedDate"` + PropertyID int64 `json:"propertyId"` + PropertyName string `json:"propertyName"` + } + + // MovePropertyRequest contains the request parameters for the MoveProperty operation. + MovePropertyRequest struct { + PropertyID int64 + BodyParams MovePropertyReqBody + } + + // MovePropertyReqBody contains body parameters for the MoveProperty operation. + MovePropertyReqBody struct { + DestinationGroupID int64 `json:"destinationGroupId"` + SourceGroupID int64 `json:"sourceGroupId"` + } + + // Property holds the property details. + Property struct { + PropertyID int64 `json:"propertyId"` + PropertyName string `json:"propertyName"` + PropertyTypeDescription string `json:"propertyTypeDescription"` + GroupID int64 `json:"groupId"` + GroupName string `json:"groupName"` + Actions PropertyActions `json:"actions"` + } + + // PropertyActions specifies activities available for the property. + PropertyActions struct { + Move bool `json:"move"` + } + + // MapPropertyIDToNameRequest is the argument for MapPropertyIDToName + MapPropertyIDToNameRequest struct { + PropertyID int64 + GroupID int64 + } +) + +// Validate validates GetPropertyRequest +func (r GetPropertyRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "PropertyID": validation.Validate(r.PropertyID, validation.Required), + "GroupID": validation.Validate(r.GroupID, validation.Required), + }) +} + +// Validate validates MapPropertyIDToNameRequest +func (r MapPropertyIDToNameRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "PropertyID": validation.Validate(r.PropertyID, validation.Required), + "GroupID": validation.Validate(r.GroupID, validation.Required), + }) +} + +// Validate validates MovePropertyRequest +func (r MovePropertyRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "PropertyID": validation.Validate(r.PropertyID, validation.Required), + "BodyParams": validation.Validate(r.BodyParams, validation.Required), + }) +} + +// Validate validates MovePropertyReqBody +func (r MovePropertyReqBody) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "DestinationGroupID": validation.Validate(r.DestinationGroupID, validation.Required), + "SourceGroupID": validation.Validate(r.SourceGroupID, validation.Required), + }) +} + +var ( + // ErrListProperties is returned when ListProperties fails + ErrListProperties = errors.New("list properties") + // ErrGetProperty is returned when GetProperty fails + ErrGetProperty = errors.New("get property") + // ErrMoveProperty is returned when MoveProperty fails + ErrMoveProperty = errors.New("move property") + // ErrMapPropertyIDToName is returned when MapPropertyIDToName fails + ErrMapPropertyIDToName = errors.New("map property by id") + // ErrMapPropertyNameToID is returned when MapPropertyNameToID fails + ErrMapPropertyNameToID = errors.New("map property by name") + // ErrNoProperty is returned when MapPropertyNameToID did not find given property + ErrNoProperty = errors.New("no such property") +) + +func (i *iam) ListProperties(ctx context.Context, params ListPropertiesRequest) (*ListPropertiesResponse, error) { + logger := i.Log(ctx) + logger.Debug("ListProperties") + + uri, err := url.Parse("/identity-management/v3/user-admin/properties") + if err != nil { + return nil, fmt.Errorf("%w: failed to parse url: %s", ErrListProperties, err) + } + + q := uri.Query() + q.Add("actions", strconv.FormatBool(params.Actions)) + if params.GroupID != 0 { + q.Add("groupId", strconv.FormatInt(params.GroupID, 10)) + } + uri.RawQuery = q.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil) + if err != nil { + return nil, fmt.Errorf("%w: failed to create request: %s", ErrListProperties, err) + } + + var result ListPropertiesResponse + resp, err := i.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("%w: request failed: %s", ErrListProperties, err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s: %w", ErrListProperties, i.Error(resp)) + } + + return &result, nil +} + +func (i *iam) GetProperty(ctx context.Context, params GetPropertyRequest) (*GetPropertyResponse, error) { + logger := i.Log(ctx) + logger.Debug("GetProperty") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%s: %w:\n%s", ErrGetProperty, ErrStructValidation, err) + } + + uri, err := url.Parse(fmt.Sprintf("/identity-management/v3/user-admin/properties/%d", params.PropertyID)) + if err != nil { + return nil, fmt.Errorf("%w: failed to parse url: %s", ErrGetProperty, err) + } + + q := uri.Query() + q.Add("groupId", strconv.FormatInt(params.GroupID, 10)) + uri.RawQuery = q.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil) + if err != nil { + return nil, fmt.Errorf("%w: failed to create request: %s", ErrGetProperty, err) + } + + var result GetPropertyResponse + resp, err := i.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("%w: request failed: %s", ErrGetProperty, err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s: %w", ErrGetProperty, i.Error(resp)) + } + + return &result, nil +} + +func (i *iam) MoveProperty(ctx context.Context, params MovePropertyRequest) error { + logger := i.Log(ctx) + logger.Debug("MoveProperty") + + if err := params.Validate(); err != nil { + return fmt.Errorf("%s: %w: %s", ErrMoveProperty, ErrStructValidation, err) + } + + uri := fmt.Sprintf("/identity-management/v3/user-admin/properties/%d", params.PropertyID) + + req, err := http.NewRequestWithContext(ctx, http.MethodPut, uri, nil) + if err != nil { + return fmt.Errorf("%w: failed to create request: %s", ErrMoveProperty, err) + } + + resp, err := i.Exec(req, nil, params.BodyParams) + if err != nil { + return fmt.Errorf("%w: request failed: %s", ErrMoveProperty, err) + } + + if resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("%s: %w", ErrMoveProperty, i.Error(resp)) + } + + return nil +} + +func (i *iam) MapPropertyIDToName(ctx context.Context, params MapPropertyIDToNameRequest) (*string, error) { + logger := i.Log(ctx) + logger.Debug("MapPropertyIDToName") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%s: %w:\n%s", ErrMapPropertyIDToName, ErrStructValidation, err) + } + + req := GetPropertyRequest{ + PropertyID: params.PropertyID, + GroupID: params.GroupID, + } + + property, err := i.GetProperty(ctx, req) + if err != nil { + return nil, fmt.Errorf("%w: request failed: %s", ErrMapPropertyIDToName, err) + } + + return &property.PropertyName, nil +} + +func (i *iam) MapPropertyNameToID(ctx context.Context, name MapPropertyNameToIDRequest) (*int64, error) { + logger := i.Log(ctx) + logger.Debug("MapPropertyNameToID") + + if name == "" { + return nil, fmt.Errorf("%s: %w:\n name cannot be blank", ErrMapPropertyNameToID, ErrStructValidation) + } + + properties, err := i.ListProperties(ctx, ListPropertiesRequest{}) + if err != nil { + return nil, fmt.Errorf("%w: request failed: %s", ErrMapPropertyNameToID, err) + } + + for _, property := range *properties { + if property.PropertyName == string(name) { + return &property.PropertyID, nil + } + } + + return nil, fmt.Errorf("%w: %s", ErrNoProperty, name) +} diff --git a/pkg/iam/properties_test.go b/pkg/iam/properties_test.go new file mode 100644 index 00000000..a0a130e1 --- /dev/null +++ b/pkg/iam/properties_test.go @@ -0,0 +1,417 @@ +package iam + +import ( + "context" + "errors" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/internal/test" + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/ptr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestListProperties(t *testing.T) { + tests := map[string]struct { + params ListPropertiesRequest + responseStatus int + expectedPath string + responseBody string + expectedResponse *ListPropertiesResponse + withError func(*testing.T, error) + }{ + "200 OK - no query params": { + params: ListPropertiesRequest{}, + responseStatus: http.StatusOK, + expectedPath: "/identity-management/v3/user-admin/properties?actions=false", + responseBody: ` +[ + { + "propertyId": 1, + "propertyName": "property1", + "propertyTypeDescription": "Site", + "groupId": 11, + "groupName": "group1" + }, + { + "propertyId": 2, + "propertyName": "property2", + "propertyTypeDescription": "Site", + "groupId": 22, + "groupName": "group2" + } +] +`, + expectedResponse: &ListPropertiesResponse{ + { + PropertyID: 1, + PropertyName: "property1", + PropertyTypeDescription: "Site", + GroupID: 11, + GroupName: "group1", + }, + { + PropertyID: 2, + PropertyName: "property2", + PropertyTypeDescription: "Site", + GroupID: 22, + GroupName: "group2", + }, + }, + }, + "200 OK - with query params": { + params: ListPropertiesRequest{ + Actions: true, + GroupID: 12345, + }, + responseStatus: http.StatusOK, + expectedPath: "/identity-management/v3/user-admin/properties?actions=true&groupId=12345", + responseBody: ` +[ + { + "propertyId": 1, + "propertyName": "property1", + "propertyTypeDescription": "Site", + "groupId": 12345, + "groupName": "group1", + "actions": { + "move": false + } + } +] +`, + expectedResponse: &ListPropertiesResponse{ + { + PropertyID: 1, + PropertyName: "property1", + PropertyTypeDescription: "Site", + GroupID: 12345, + GroupName: "group1", + Actions: PropertyActions{ + Move: false, + }, + }, + }, + }, + "200 OK - no properties": { + params: ListPropertiesRequest{}, + responseStatus: http.StatusOK, + expectedPath: "/identity-management/v3/user-admin/properties?actions=false", + responseBody: `[]`, + expectedResponse: &ListPropertiesResponse{}, + }, + "500 internal server error": { + params: ListPropertiesRequest{}, + responseStatus: http.StatusInternalServerError, + expectedPath: "/identity-management/v3/user-admin/properties?actions=false", + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error processing request", + "status": 500 + }`, + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error processing request", + StatusCode: http.StatusInternalServerError, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + users, err := client.ListProperties(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, test.expectedResponse, users) + }) + } +} + +func TestGetProperty(t *testing.T) { + tests := map[string]struct { + params GetPropertyRequest + responseStatus int + expectedPath string + responseBody string + expectedResponse *GetPropertyResponse + withError func(*testing.T, error) + }{ + "200 OK": { + params: GetPropertyRequest{ + PropertyID: 1, + GroupID: 11, + }, + responseStatus: http.StatusOK, + expectedPath: "/identity-management/v3/user-admin/properties/1?groupId=11", + responseBody: ` +{ + "createdDate": "2023-08-18T09:10:37.000Z", + "createdBy": "user1", + "modifiedDate": "2023-08-18T09:10:37.000Z", + "modifiedBy": "user2", + "groupName": "group1", + "groupId": 11, + "arlConfigFile": "test.xml", + "propertyId": 1, + "propertyName": "name1" +} +`, + expectedResponse: &GetPropertyResponse{ + ARLConfigFile: "test.xml", + CreatedBy: "user1", + CreatedDate: test.NewTimeFromString(t, "2023-08-18T09:10:37.000Z"), + GroupID: 11, + GroupName: "group1", + ModifiedBy: "user2", + ModifiedDate: test.NewTimeFromString(t, "2023-08-18T09:10:37.000Z"), + PropertyID: 1, + PropertyName: "name1", + }, + }, + "validation errors": { + params: GetPropertyRequest{}, + withError: func(t *testing.T, err error) { + assert.Equal(t, "get property: struct validation:\nGroupID: cannot be blank\nPropertyID: cannot be blank", err.Error()) + }, + }, + "404 not found": { + params: GetPropertyRequest{ + PropertyID: 1, + GroupID: 11, + }, + responseStatus: http.StatusNotFound, + expectedPath: "/identity-management/v3/user-admin/properties/1?groupId=11", + responseBody: ` +{ + "instance": "", + "httpStatus": 404, + "detail": "", + "title": "Property not found", + "type": "/useradmin-api/error-types/1806" +} +`, + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "/useradmin-api/error-types/1806", + Title: "Property not found", + StatusCode: http.StatusNotFound, + HTTPStatus: http.StatusNotFound, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + users, err := client.GetProperty(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, test.expectedResponse, users) + }) + } +} + +func TestMoveProperty(t *testing.T) { + tests := map[string]struct { + params MovePropertyRequest + expectedPath string + expectedRequestBody string + responseStatus int + responseBody string + withError func(*testing.T, error) + }{ + "204 OK": { + params: MovePropertyRequest{ + PropertyID: 1, + BodyParams: MovePropertyReqBody{ + DestinationGroupID: 22, + SourceGroupID: 11, + }, + }, + expectedRequestBody: ` +{ + "destinationGroupId": 22, + "sourceGroupId": 11 +}`, + responseStatus: http.StatusNoContent, + expectedPath: "/identity-management/v3/user-admin/properties/1", + }, + "validation errors": { + params: MovePropertyRequest{}, + withError: func(t *testing.T, err error) { + assert.Equal(t, "move property: struct validation: BodyParams: DestinationGroupID: cannot be blank\nSourceGroupID: cannot be blank\nPropertyID: cannot be blank", err.Error()) + }, + }, + "400 not allowed": { + params: MovePropertyRequest{ + PropertyID: 1, + BodyParams: MovePropertyReqBody{ + DestinationGroupID: 22, + SourceGroupID: 11, + }, + }, + responseStatus: http.StatusBadRequest, + expectedPath: "/identity-management/v3/user-admin/properties/1", + responseBody: ` +{ + "instance": "", + "httpStatus": 400, + "detail": "Property move is not allowed from the group 11", + "title": "Validation Exception", + "type": "/useradmin-api/error-types/1806" +} +`, + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "/useradmin-api/error-types/1806", + Title: "Validation Exception", + Detail: "Property move is not allowed from the group 11", + HTTPStatus: http.StatusBadRequest, + StatusCode: http.StatusBadRequest, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodPut, r.Method) + if test.expectedRequestBody != "" { + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + assert.JSONEq(t, test.expectedRequestBody, string(body)) + } + w.WriteHeader(test.responseStatus) + if test.responseBody != "" { + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + } + })) + client := mockAPIClient(t, mockServer) + err := client.MoveProperty(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + assert.NoError(t, err) + }) + } +} + +func TestMapPropertyIDToName(t *testing.T) { + tests := map[string]struct { + params MapPropertyIDToNameRequest + responseStatus int + responseBody string + expectedResponse *string + withError func(*testing.T, error) + }{ + "200 OK": { + params: MapPropertyIDToNameRequest{ + PropertyID: 1, + GroupID: 11, + }, + responseStatus: http.StatusOK, + responseBody: ` +{ + "createdDate": "2023-08-18T09:10:37.000Z", + "createdBy": "user1", + "modifiedDate": "2023-08-18T09:10:37.000Z", + "modifiedBy": "user2", + "groupName": "group1", + "groupId": 11, + "arlConfigFile": "test.xml", + "propertyId": 1, + "propertyName": "name1" +} +`, + expectedResponse: ptr.To("name1"), + }, + "validation errors": { + params: MapPropertyIDToNameRequest{}, + withError: func(t *testing.T, err error) { + assert.Equal(t, "map property by id: struct validation:\nGroupID: cannot be blank\nPropertyID: cannot be blank", err.Error()) + }, + }, + "404 not found": { + params: MapPropertyIDToNameRequest{ + PropertyID: 1, + GroupID: 11, + }, + responseStatus: http.StatusNotFound, + responseBody: ` + { + "instance": "", + "httpStatus": 404, + "detail": "", + "title": "Property not found", + "type": "/useradmin-api/error-types/1806" + } + `, + withError: func(t *testing.T, err error) { + assert.Equal(t, err.Error(), `map property by id: request failed: get property: API error: +{ + "type": "/useradmin-api/error-types/1806", + "title": "Property not found", + "detail": "", + "statusCode": 404, + "httpStatus": 404 +}`) + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + users, err := client.MapPropertyIDToName(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, test.expectedResponse, users) + }) + } +} diff --git a/pkg/papi/activation_test.go b/pkg/papi/activation_test.go index 23d6f73f..31d0c574 100644 --- a/pkg/papi/activation_test.go +++ b/pkg/papi/activation_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestPapi_CreateActivation(t *testing.T) { +func TestPapiCreateActivation(t *testing.T) { tests := map[string]struct { request CreateActivationRequest responseStatus int @@ -321,7 +321,7 @@ func TestPapi_CreateActivation(t *testing.T) { } } -func TestPapi_GetActivations(t *testing.T) { +func TestPapiGetActivations(t *testing.T) { tests := map[string]struct { request GetActivationsRequest responseStatus int @@ -459,7 +459,7 @@ func TestPapi_GetActivations(t *testing.T) { } } -func TestPapi_GetActivation(t *testing.T) { +func TestPapiGetActivation(t *testing.T) { tests := map[string]struct { request GetActivationRequest responseStatus int @@ -648,7 +648,7 @@ func TestPapi_GetActivation(t *testing.T) { } } -func TestPapi_CancelActivation(t *testing.T) { +func TestPapiCancelActivation(t *testing.T) { tests := map[string]struct { request CancelActivationRequest responseStatus int diff --git a/pkg/papi/clientsettings_test.go b/pkg/papi/clientsettings_test.go index 5d32a803..480295ef 100644 --- a/pkg/papi/clientsettings_test.go +++ b/pkg/papi/clientsettings_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestPapi_GetClientSettings(t *testing.T) { +func TestPapiGetClientSettings(t *testing.T) { tests := map[string]struct { responseStatus int responseBody string @@ -77,7 +77,7 @@ func TestPapi_GetClientSettings(t *testing.T) { } } -func TestPapi_UpdateClientSettings(t *testing.T) { +func TestPapiUpdateClientSettings(t *testing.T) { tests := map[string]struct { params ClientSettingsBody responseStatus int diff --git a/pkg/papi/contract_test.go b/pkg/papi/contract_test.go index 37b5df57..cc060263 100644 --- a/pkg/papi/contract_test.go +++ b/pkg/papi/contract_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestPapi_GetContracts(t *testing.T) { +func TestPapiGetContracts(t *testing.T) { tests := map[string]struct { responseStatus int responseBody string diff --git a/pkg/papi/cpcode_test.go b/pkg/papi/cpcode_test.go index 45c09406..32f70e08 100644 --- a/pkg/papi/cpcode_test.go +++ b/pkg/papi/cpcode_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestPapi_GetCPCodes(t *testing.T) { +func TestPapiGetCPCodes(t *testing.T) { tests := map[string]struct { params GetCPCodesRequest responseStatus int @@ -129,7 +129,7 @@ func TestPapi_GetCPCodes(t *testing.T) { } } -func TestPapi_GetCPCode(t *testing.T) { +func TestPapiGetCPCode(t *testing.T) { tests := map[string]struct { params GetCPCodeRequest responseStatus int @@ -401,7 +401,7 @@ func TestGetCPCodeDetail(t *testing.T) { } } -func TestPapi_CreateCPCode(t *testing.T) { +func TestPapiCreateCPCode(t *testing.T) { tests := map[string]struct { params CreateCPCodeRequest responseStatus int diff --git a/pkg/papi/edgehostname_test.go b/pkg/papi/edgehostname_test.go index e77c757f..4e6de8d9 100644 --- a/pkg/papi/edgehostname_test.go +++ b/pkg/papi/edgehostname_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestPapi_GetEdgeHostnames(t *testing.T) { +func TestPapiGetEdgeHostnames(t *testing.T) { tests := map[string]struct { params GetEdgeHostnamesRequest responseStatus int @@ -136,7 +136,7 @@ func TestPapi_GetEdgeHostnames(t *testing.T) { } } -func TestPapi_GetEdgeHostname(t *testing.T) { +func TestPapiGetEdgeHostname(t *testing.T) { tests := map[string]struct { params GetEdgeHostnameRequest responseStatus int @@ -311,7 +311,7 @@ func TestPapi_GetEdgeHostname(t *testing.T) { } } -func TestPapi_CreateEdgeHostname(t *testing.T) { +func TestPapiCreateEdgeHostname(t *testing.T) { tests := map[string]struct { params CreateEdgeHostnameRequest responseStatus int diff --git a/pkg/papi/group_test.go b/pkg/papi/group_test.go index 203cb751..46c1b198 100644 --- a/pkg/papi/group_test.go +++ b/pkg/papi/group_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestPapi_GetGroups(t *testing.T) { +func TestPapiGetGroups(t *testing.T) { tests := map[string]struct { responseStatus int responseBody string diff --git a/pkg/papi/mocks.go b/pkg/papi/mocks.go index fef342f0..ebd499bf 100644 --- a/pkg/papi/mocks.go +++ b/pkg/papi/mocks.go @@ -679,3 +679,23 @@ func (p *Mock) CancelIncludeActivation(ctx context.Context, r CancelIncludeActiv return args.Get(0).(*CancelIncludeActivationResponse), args.Error(1) } + +func (p *Mock) MapPropertyIDToName(ctx context.Context, r MapPropertyIDToNameRequest) (*string, error) { + args := p.Called(ctx, r) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(*string), args.Error(1) +} + +func (p *Mock) MapPropertyNameToID(ctx context.Context, r MapPropertyNameToIDRequest) (*string, error) { + args := p.Called(ctx, r) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(*string), args.Error(1) +} diff --git a/pkg/papi/products_test.go b/pkg/papi/products_test.go index 27688ac3..906d76ac 100644 --- a/pkg/papi/products_test.go +++ b/pkg/papi/products_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestPapi_GetProducts(t *testing.T) { +func TestPapiGetProducts(t *testing.T) { tests := map[string]struct { params GetProductsRequest responseStatus int diff --git a/pkg/papi/property.go b/pkg/papi/property.go index 88d8280f..90fe520c 100644 --- a/pkg/papi/property.go +++ b/pkg/papi/property.go @@ -33,6 +33,11 @@ type ( // // https://techdocs.akamai.com/property-mgr/reference/delete-property RemoveProperty(ctx context.Context, params RemovePropertyRequest) (*RemovePropertyResponse, error) + + // MapPropertyNameToID returns (PAPI) property ID for given property name + // Mainly to be used to map (IAM) Property ID to (PAPI) Property ID + // To get property name for the mapping, please use iam.MapPropertyIDToName + MapPropertyNameToID(context.Context, MapPropertyNameToIDRequest) (*string, error) } // PropertyCloneFrom optionally identifies another property instance to clone when making a POST request to create a new property @@ -122,6 +127,20 @@ type ( RemovePropertyResponse struct { Message string `json:"message"` } + + // MapPropertyIDToNameRequest is the argument for MapPropertyIDToName + MapPropertyIDToNameRequest struct { + GroupID string + ContractID string + PropertyID string + } + + // MapPropertyNameToIDRequest is the argument for MapPropertyNameToID + MapPropertyNameToIDRequest struct { + GroupID string + ContractID string + Name string + } ) // Validate validates GetPropertiesRequest @@ -173,6 +192,22 @@ func (v RemovePropertyRequest) Validate() error { }.Filter() } +// Validate validates MapPropertyIDToNameRequest +func (v MapPropertyIDToNameRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "PropertyID": validation.Validate(v.PropertyID, validation.Required), + }) +} + +// Validate validates RemovePropertyRequest +func (v MapPropertyNameToIDRequest) Validate() error { + return edgegriderr.ParseValidationErrors(validation.Errors{ + "GroupID": validation.Validate(v.GroupID, validation.Required), + "ContractID": validation.Validate(v.ContractID, validation.Required), + "Name": validation.Validate(v.Name, validation.Required), + }) +} + var ( // ErrGetProperties represents error when fetching properties fails ErrGetProperties = errors.New("fetching properties") @@ -182,6 +217,12 @@ var ( ErrCreateProperty = errors.New("creating property") // ErrRemoveProperty represents error when removing property fails ErrRemoveProperty = errors.New("removing property") + // ErrMapPropertyIDToName represents error when mapping property by ID fails + ErrMapPropertyIDToName = errors.New("map property by ID") + // ErrMapPropertyNameToID represents error when mapping property by Name fails + ErrMapPropertyNameToID = errors.New("map property by name") + // ErrNoProperty is returned when finding property by name did not find given property + ErrNoProperty = errors.New("no such property") ) func (p *papi) GetProperties(ctx context.Context, params GetPropertiesRequest) (*GetPropertiesResponse, error) { @@ -349,3 +390,35 @@ func (p *papi) RemoveProperty(ctx context.Context, params RemovePropertyRequest) return &rval, nil } + +func (p *papi) MapPropertyIDToName(ctx context.Context, params MapPropertyIDToNameRequest) (*string, error) { + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%s: %w: %s", ErrMapPropertyIDToName, ErrStructValidation, err) + } + + property, err := p.GetProperty(ctx, GetPropertyRequest{ContractID: params.ContractID, GroupID: params.GroupID, PropertyID: params.PropertyID}) + if err != nil { + return nil, fmt.Errorf("%s: %w", ErrMapPropertyIDToName, err) + } + + return &property.Property.PropertyName, nil +} + +func (p *papi) MapPropertyNameToID(ctx context.Context, params MapPropertyNameToIDRequest) (*string, error) { + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%s: %w: %s", ErrMapPropertyNameToID, ErrStructValidation, err) + } + + properties, err := p.GetProperties(ctx, GetPropertiesRequest{ContractID: params.ContractID, GroupID: params.GroupID}) + if err != nil { + return nil, fmt.Errorf("%s: %w", ErrMapPropertyNameToID, err) + } + + for _, property := range properties.Properties.Items { + if property.PropertyName == params.Name { + return &property.PropertyID, nil + } + } + + return nil, fmt.Errorf("%w: %s", ErrNoProperty, params.Name) +} diff --git a/pkg/papi/property_test.go b/pkg/papi/property_test.go index 83bf1236..844eec0f 100644 --- a/pkg/papi/property_test.go +++ b/pkg/papi/property_test.go @@ -7,12 +7,13 @@ import ( "net/http/httptest" "testing" + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/ptr" "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/tools" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestPapi_GetProperties(t *testing.T) { +func TestPapiGetProperties(t *testing.T) { tests := map[string]struct { request GetPropertiesRequest responseStatus int @@ -117,7 +118,7 @@ func TestPapi_GetProperties(t *testing.T) { } } -func TestPapi_GetProperty(t *testing.T) { +func TestPapiGetProperty(t *testing.T) { tests := map[string]struct { request GetPropertyRequest responseStatus int @@ -254,7 +255,7 @@ func TestPapi_GetProperty(t *testing.T) { } } -func TestPapi_CreateProperty(t *testing.T) { +func TestPapiCreateProperty(t *testing.T) { tests := map[string]struct { request CreatePropertyRequest responseStatus int @@ -345,7 +346,7 @@ func TestPapi_CreateProperty(t *testing.T) { } } -func TestPapi_RemoveProperty(t *testing.T) { +func TestPapiRemoveProperty(t *testing.T) { tests := map[string]struct { request RemovePropertyRequest responseStatus int @@ -421,3 +422,118 @@ func TestPapi_RemoveProperty(t *testing.T) { }) } } + +func TestPapiMapPropertyNameToID(t *testing.T) { + listPropertiesResponse := ` +{ + "properties": { + "items": [ + { + "accountId": "act_1-1TJZFB", + "contractId": "ctr_1-1TJZH5", + "groupId": "grp_15166", + "propertyId": "prp_175780", + "propertyName": "example.com", + "latestVersion": 2, + "stagingVersion": 1, + "productId": "prp_175780", + "productionVersion": null, + "assetId": "aid_101", + "note": "Notes about example.com" + }, + { + "accountId": "act_1-1TJZFB", + "contractId": "ctr_1-1TJZH5", + "groupId": "grp_15166", + "propertyId": "prp_175781", + "propertyName": "example2.com", + "latestVersion": 1, + "stagingVersion": 1, + "productId": "prp_175780", + "productionVersion": null, + "assetId": "aid_101", + "note": "Notes about example2.com" + } + ] + } +}` + tests := map[string]struct { + request MapPropertyNameToIDRequest + responseStatus int + responseBody string + expectedResponse *string + withError func(*testing.T, error) + }{ + "200 OK": { + request: MapPropertyNameToIDRequest{ + ContractID: "ctr_1-1TJZFW", + GroupID: "grp_15166", + Name: "example.com", + }, + responseStatus: http.StatusOK, + responseBody: listPropertiesResponse, + expectedResponse: ptr.To("prp_175780"), + }, + "200 property not found": { + request: MapPropertyNameToIDRequest{ + ContractID: "ctr_1-1TJZFW", + GroupID: "grp_15166", + Name: "example3.com", + }, + responseStatus: http.StatusOK, + responseBody: listPropertiesResponse, + withError: func(t *testing.T, err error) { + assert.True(t, errors.Is(err, ErrNoProperty)) + }, + }, + "500 internal server error": { + request: MapPropertyNameToIDRequest{ + ContractID: "ctr_1-1TJZFW", + GroupID: "grp_15166", + Name: "example.com", + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error fetching properties", + "status": 500 + }`, + withError: func(t *testing.T, err error) { + assert.Equal(t, err.Error(), `map property by name: fetching properties: API error: +{ + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error fetching properties", + "statusCode": 500 +}`) + }, + }, + "validation error": { + request: MapPropertyNameToIDRequest{}, + withError: func(t *testing.T, err error) { + assert.Equal(t, err.Error(), "map property by name: struct validation: ContractID: cannot be blank\nGroupID: cannot be blank\nName: cannot be blank") + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + result, err := client.MapPropertyNameToID(context.Background(), test.request) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} diff --git a/pkg/papi/propertyhostname_test.go b/pkg/papi/propertyhostname_test.go index f1422091..8b8c44cf 100644 --- a/pkg/papi/propertyhostname_test.go +++ b/pkg/papi/propertyhostname_test.go @@ -11,7 +11,7 @@ import ( "github.com/tj/assert" ) -func TestPapi_GetPropertyVersionHostnames(t *testing.T) { +func TestPapiGetPropertyVersionHostnames(t *testing.T) { tests := map[string]struct { params GetPropertyVersionHostnamesRequest responseStatus int @@ -149,7 +149,7 @@ func TestPapi_GetPropertyVersionHostnames(t *testing.T) { } } -func TestPapi_UpdatePropertyVersionHostnames(t *testing.T) { +func TestPapiUpdatePropertyVersionHostnames(t *testing.T) { tests := map[string]struct { params UpdatePropertyVersionHostnamesRequest responseStatus int diff --git a/pkg/papi/propertyversion_test.go b/pkg/papi/propertyversion_test.go index 5730570b..7f088abb 100644 --- a/pkg/papi/propertyversion_test.go +++ b/pkg/papi/propertyversion_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestPapi_GetPropertyVersions(t *testing.T) { +func TestPapiGetPropertyVersions(t *testing.T) { tests := map[string]struct { params GetPropertyVersionsRequest responseStatus int @@ -138,7 +138,7 @@ func TestPapi_GetPropertyVersions(t *testing.T) { } } -func TestPapi_GetPropertyVersion(t *testing.T) { +func TestPapiGetPropertyVersion(t *testing.T) { tests := map[string]struct { params GetPropertyVersionRequest responseStatus int @@ -311,7 +311,7 @@ func TestPapi_GetPropertyVersion(t *testing.T) { } } -func TestPapi_CreatePropertyVersion(t *testing.T) { +func TestPapiCreatePropertyVersion(t *testing.T) { tests := map[string]struct { params CreatePropertyVersionRequest responseStatus int @@ -459,7 +459,7 @@ func TestPapi_CreatePropertyVersion(t *testing.T) { } } -func TestPapi_GetLatestVersion(t *testing.T) { +func TestPapiGetLatestVersion(t *testing.T) { tests := map[string]struct { params GetLatestVersionRequest responseStatus int @@ -633,7 +633,7 @@ func TestPapi_GetLatestVersion(t *testing.T) { } } -func TestPapi_GetAvailableBehaviors(t *testing.T) { +func TestPapiGetAvailableBehaviors(t *testing.T) { tests := map[string]struct { params GetAvailableBehaviorsRequest responseStatus int @@ -814,7 +814,7 @@ func TestPapi_GetAvailableBehaviors(t *testing.T) { } } -func TestPapi_GetAvailableCriteria(t *testing.T) { +func TestPapiGetAvailableCriteria(t *testing.T) { tests := map[string]struct { params GetAvailableCriteriaRequest responseStatus int @@ -995,7 +995,7 @@ func TestPapi_GetAvailableCriteria(t *testing.T) { } } -func TestPapi_ListAvailableIncludes(t *testing.T) { +func TestPapiListAvailableIncludes(t *testing.T) { tests := map[string]struct { params ListAvailableIncludesRequest responseStatus int @@ -1801,7 +1801,7 @@ func TestPapi_ListAvailableIncludes(t *testing.T) { } } -func TestPapi_ListReferencedIncludes(t *testing.T) { +func TestPapiListReferencedIncludes(t *testing.T) { tests := map[string]struct { params ListReferencedIncludesRequest responseStatus int diff --git a/pkg/papi/rule_test.go b/pkg/papi/rule_test.go index a33e795d..4075005e 100644 --- a/pkg/papi/rule_test.go +++ b/pkg/papi/rule_test.go @@ -14,7 +14,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestPapi_GetRuleTree(t *testing.T) { +func TestPapiGetRuleTree(t *testing.T) { tests := map[string]struct { params GetRuleTreeRequest responseStatus int @@ -643,7 +643,7 @@ func TestPapi_GetRuleTree(t *testing.T) { } } -func TestPapi_UpdateRuleTree(t *testing.T) { +func TestPapiUpdateRuleTree(t *testing.T) { tests := map[string]struct { params UpdateRulesRequest requestBody string diff --git a/pkg/papi/ruleformats_test.go b/pkg/papi/ruleformats_test.go index d2fe57b6..934e5f08 100644 --- a/pkg/papi/ruleformats_test.go +++ b/pkg/papi/ruleformats_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestPapi_GetRuleFormats(t *testing.T) { +func TestPapiGetRuleFormats(t *testing.T) { tests := map[string]struct { responseStatus int responseBody string diff --git a/pkg/papi/search_test.go b/pkg/papi/search_test.go index 2d33978a..6cbdb7e3 100644 --- a/pkg/papi/search_test.go +++ b/pkg/papi/search_test.go @@ -14,7 +14,7 @@ import ( "github.com/tj/assert" ) -func TestPapi_SearchProperties(t *testing.T) { +func TestPapiSearchProperties(t *testing.T) { tests := map[string]struct { params SearchRequest responseStatus int