diff --git a/go.mod b/go.mod index 3cf814531..bc7c3e791 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/getkin/kin-openapi v0.123.0 github.com/go-chi/chi/v5 v5.0.10 github.com/go-chi/cors v1.2.1 + github.com/go-playground/validator/v10 v10.19.0 github.com/google/uuid v1.5.0 github.com/kelseyhightower/envconfig v1.4.0 github.com/oapi-codegen/runtime v1.1.1 @@ -49,11 +50,14 @@ require ( github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.0.2 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.20.2 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.8 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/gnostic-models v0.6.8 // indirect @@ -69,6 +73,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -86,12 +91,13 @@ require ( go.opentelemetry.io/otel/metric v1.19.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.uber.org/multierr v1.10.0 // indirect + golang.org/x/crypto v0.19.0 // indirect golang.org/x/exp v0.0.0-20231226003508-02704c960a9b // indirect - golang.org/x/net v0.19.0 // indirect + golang.org/x/net v0.21.0 // indirect golang.org/x/oauth2 v0.13.0 // indirect golang.org/x/sync v0.5.0 // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/term v0.15.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/term v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/appengine v1.6.8 // indirect diff --git a/go.sum b/go.sum index bd8b243bc..6a503f17a 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBd github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/getkin/kin-openapi v0.123.0 h1:zIik0mRwFNLyvtXK274Q6ut+dPh6nlxBp0x7mNrPhs8= github.com/getkin/kin-openapi v0.123.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= @@ -43,6 +45,14 @@ github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw= github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= +github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= @@ -96,6 +106,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= @@ -197,6 +209,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/exp v0.0.0-20231226003508-02704c960a9b h1:kLiC65FbiHWFAOu+lxwNPujcsl8VYyTYYEZnsOO1WK4= golang.org/x/exp v0.0.0-20231226003508-02704c960a9b/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -208,8 +222,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -226,12 +240,12 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= diff --git a/rebac-admin-backend/v1/core.go b/rebac-admin-backend/v1/core.go index 6d03f3a96..4b219a358 100644 --- a/rebac-admin-backend/v1/core.go +++ b/rebac-admin-backend/v1/core.go @@ -99,7 +99,8 @@ func newReBACAdminBackendWithService(params ReBACAdminBackendParams, handler res // Handler returns HTTP handlers implementing the ReBAC Admin OpenAPI spec. func (b *ReBACAdminBackend) Handler(baseURL string) http.Handler { baseURL, _ = strings.CutSuffix(baseURL, "/") - return resources.HandlerWithOptions(b.handler, resources.ChiServerOptions{ + h := newHandlerWithValidation(b.handler) + return resources.HandlerWithOptions(h, resources.ChiServerOptions{ BaseURL: baseURL + "/v1", ErrorHandlerFunc: func(w http.ResponseWriter, _ *http.Request, err error) { writeErrorResponse(w, err) diff --git a/rebac-admin-backend/v1/error.go b/rebac-admin-backend/v1/error.go index f81224ccc..58ccdd6bc 100644 --- a/rebac-admin-backend/v1/error.go +++ b/rebac-admin-backend/v1/error.go @@ -46,6 +46,14 @@ func NewNotFoundError(message string) error { } } +// NewMissingRequestBodyError returns an error instance that represents a missing request body error. +func NewMissingRequestBodyError(message string) error { + return &errorWithStatus{ + status: http.StatusBadRequest, + message: fmt.Sprintf("missing request body: %s", message), + } +} + // NewValidationError returns an error instance that represents an input validation error. func NewValidationError(message string) error { return &errorWithStatus{ @@ -54,6 +62,14 @@ func NewValidationError(message string) error { } } +// NewRequestBodyValidationError returns an error instance that represents a request body validation error. +func NewRequestBodyValidationError(message string) error { + return &errorWithStatus{ + status: http.StatusBadRequest, + message: fmt.Sprintf("invalid request body: %s", message), + } +} + // NewUnknownError returns an error instance that represents an unknown internal error. func NewUnknownError(message string) error { return &errorWithStatus{ diff --git a/rebac-admin-backend/v1/groups.go b/rebac-admin-backend/v1/groups.go index 67ee6f2be..4c5fb4bd3 100644 --- a/rebac-admin-backend/v1/groups.go +++ b/rebac-admin-backend/v1/groups.go @@ -4,7 +4,6 @@ package v1 import ( - "encoding/json" "net/http" "github.com/canonical/identity-platform-admin-ui/rebac-admin-backend/v1/resources" @@ -36,21 +35,25 @@ func (h handler) GetGroups(w http.ResponseWriter, req *http.Request, params reso func (h handler) PostGroups(w http.ResponseWriter, req *http.Request) { ctx := req.Context() - group := new(resources.Group) - defer req.Body.Close() + body, err := getRequestBodyFromContext(req.Context()) + if err != nil { + writeErrorResponse(w, err) + return + } - if err := json.NewDecoder(req.Body).Decode(group); err != nil { - writeErrorResponse(w, NewValidationError("request doesn't match the expected schema")) + group, ok := body.(*resources.Group) + if !ok { + writeErrorResponse(w, NewMissingRequestBodyError("")) return } - group, err := h.Groups.CreateGroup(ctx, group) + result, err := h.Groups.CreateGroup(ctx, group) if err != nil { writeServiceErrorResponse(w, h.GroupsErrorMapper, err) return } - writeResponse(w, http.StatusCreated, group) + writeResponse(w, http.StatusCreated, result) } // DeleteGroupsItem deletes the specified group identified by the provided ID. @@ -86,26 +89,25 @@ func (h handler) GetGroupsItem(w http.ResponseWriter, req *http.Request, id stri func (h handler) PutGroupsItem(w http.ResponseWriter, req *http.Request, id string) { ctx := req.Context() - group := new(resources.Group) - defer req.Body.Close() - - if err := json.NewDecoder(req.Body).Decode(group); err != nil { - writeErrorResponse(w, NewValidationError("request doesn't match the expected schema")) + body, err := getRequestBodyFromContext(req.Context()) + if err != nil { + writeErrorResponse(w, err) return } - if id != *group.Id { - writeErrorResponse(w, NewValidationError("group ID from path does not match the Group object")) + group, ok := body.(*resources.Group) + if !ok { + writeErrorResponse(w, NewMissingRequestBodyError("")) return } - group, err := h.Groups.UpdateGroup(ctx, group) + result, err := h.Groups.UpdateGroup(ctx, group) if err != nil { writeServiceErrorResponse(w, h.GroupsErrorMapper, err) return } - writeResponse(w, http.StatusOK, group) + writeResponse(w, http.StatusOK, result) } // GetGroupsItemEntitlements returns the list of entitlements for a group identified by the provided ID. @@ -134,15 +136,19 @@ func (h handler) GetGroupsItemEntitlements(w http.ResponseWriter, req *http.Requ func (h handler) PatchGroupsItemEntitlements(w http.ResponseWriter, req *http.Request, id string) { ctx := req.Context() - patchesRequest := new(resources.GroupEntitlementsPatchRequestBody) - defer req.Body.Close() + body, err := getRequestBodyFromContext(req.Context()) + if err != nil { + writeErrorResponse(w, err) + return + } - if err := json.NewDecoder(req.Body).Decode(patchesRequest); err != nil { - writeErrorResponse(w, NewValidationError("request doesn't match the expected schema")) + groupEntitlements, ok := body.(*resources.GroupEntitlementsPatchRequestBody) + if !ok { + writeErrorResponse(w, NewMissingRequestBodyError("")) return } - _, err := h.Groups.PatchGroupEntitlements(ctx, id, patchesRequest.Patches) + _, err = h.Groups.PatchGroupEntitlements(ctx, id, groupEntitlements.Patches) if err != nil { writeServiceErrorResponse(w, h.GroupsErrorMapper, err) return @@ -177,15 +183,19 @@ func (h handler) GetGroupsItemIdentities(w http.ResponseWriter, req *http.Reques func (h handler) PatchGroupsItemIdentities(w http.ResponseWriter, req *http.Request, id string) { ctx := req.Context() - patchesRequest := new(resources.GroupIdentitiesPatchRequestBody) - defer req.Body.Close() + body, err := getRequestBodyFromContext(req.Context()) + if err != nil { + writeErrorResponse(w, err) + return + } - if err := json.NewDecoder(req.Body).Decode(patchesRequest); err != nil { - writeErrorResponse(w, NewValidationError("request doesn't match the expected schema")) + groupIdentities, ok := body.(*resources.GroupIdentitiesPatchRequestBody) + if !ok { + writeErrorResponse(w, NewMissingRequestBodyError("")) return } - _, err := h.Groups.PatchGroupIdentities(ctx, id, patchesRequest.Patches) + _, err = h.Groups.PatchGroupIdentities(ctx, id, groupIdentities.Patches) if err != nil { writeServiceErrorResponse(w, h.GroupsErrorMapper, err) return @@ -220,15 +230,19 @@ func (h handler) GetGroupsItemRoles(w http.ResponseWriter, req *http.Request, id func (h handler) PatchGroupsItemRoles(w http.ResponseWriter, req *http.Request, id string) { ctx := req.Context() - patchesRequest := new(resources.GroupRolesPatchRequestBody) - defer req.Body.Close() + body, err := getRequestBodyFromContext(req.Context()) + if err != nil { + writeErrorResponse(w, err) + return + } - if err := json.NewDecoder(req.Body).Decode(patchesRequest); err != nil { - writeErrorResponse(w, NewValidationError("request doesn't match the expected schema")) + groupRoles, ok := body.(*resources.GroupRolesPatchRequestBody) + if !ok { + writeErrorResponse(w, NewMissingRequestBodyError("")) return } - _, err := h.Groups.PatchGroupRoles(ctx, id, patchesRequest.Patches) + _, err = h.Groups.PatchGroupRoles(ctx, id, groupRoles.Patches) if err != nil { writeServiceErrorResponse(w, h.GroupsErrorMapper, err) return diff --git a/rebac-admin-backend/v1/groups_test.go b/rebac-admin-backend/v1/groups_test.go index 530a5fcb9..636ad1d48 100644 --- a/rebac-admin-backend/v1/groups_test.go +++ b/rebac-admin-backend/v1/groups_test.go @@ -4,7 +4,6 @@ package v1 import ( - "bytes" "encoding/json" "errors" "fmt" @@ -102,8 +101,7 @@ func TestHandler_Groups_Success(t *testing.T) { Return(&mockGroupObject, nil) }, triggerFunc: func(h handler, w *httptest.ResponseRecorder) { - groupBody, _ := json.Marshal(mockGroupObject) - mockRequest := httptest.NewRequest(http.MethodPost, "/groups", bytes.NewReader(groupBody)) + mockRequest := newTestRequest(http.MethodPost, "/groups", &mockGroupObject) h.PostGroups(w, mockRequest) }, expectedStatus: http.StatusCreated, @@ -131,8 +129,7 @@ func TestHandler_Groups_Success(t *testing.T) { Return(&mockGroupObject, nil) }, triggerFunc: func(h handler, w *httptest.ResponseRecorder) { - groupBody, _ := json.Marshal(mockGroupObject) - mockRequest := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/groups/%s", mockGroupId), bytes.NewReader(groupBody)) + mockRequest := newTestRequest(http.MethodPut, fmt.Sprintf("/groups/%s", mockGroupId), &mockGroupObject) h.PutGroupsItem(w, mockRequest, mockGroupId) }, expectedStatus: http.StatusOK, @@ -176,15 +173,15 @@ func TestHandler_Groups_Success(t *testing.T) { Return(true, nil) }, triggerFunc: func(h handler, w *httptest.ResponseRecorder) { - patchesBody, _ := json.Marshal(resources.GroupIdentitiesPatchRequestBody{ + patches := resources.GroupIdentitiesPatchRequestBody{ Patches: []resources.GroupIdentitiesPatchItem{ { Identity: *mockIdentities.Data[0].Id, Op: "add", }, }, - }) - mockRequest := httptest.NewRequest(http.MethodPatch, fmt.Sprintf("/groups/%s/identities", mockGroupId), bytes.NewReader(patchesBody)) + } + mockRequest := newTestRequest(http.MethodPatch, fmt.Sprintf("/groups/%s/identities", mockGroupId), &patches) h.PatchGroupsItemIdentities(w, mockRequest, mockGroupId) }, expectedStatus: http.StatusOK, @@ -214,15 +211,15 @@ func TestHandler_Groups_Success(t *testing.T) { Return(true, nil) }, triggerFunc: func(h handler, w *httptest.ResponseRecorder) { - patchesBody, _ := json.Marshal(resources.GroupRolesPatchRequestBody{ + patches := resources.GroupRolesPatchRequestBody{ Patches: []resources.GroupRolesPatchItem{ { Role: *mockRoles.Data[0].Id, Op: "add", }, }, - }) - mockRequest := httptest.NewRequest(http.MethodPatch, fmt.Sprintf("/groups/%s/roles", mockGroupId), bytes.NewReader(patchesBody)) + } + mockRequest := newTestRequest(http.MethodPatch, fmt.Sprintf("/groups/%s/roles", mockGroupId), &patches) h.PatchGroupsItemRoles(w, mockRequest, mockGroupId) }, expectedStatus: http.StatusOK, @@ -252,15 +249,15 @@ func TestHandler_Groups_Success(t *testing.T) { Return(true, nil) }, triggerFunc: func(h handler, w *httptest.ResponseRecorder) { - patchesBody, _ := json.Marshal(resources.GroupEntitlementsPatchRequestBody{ + patches := resources.GroupEntitlementsPatchRequestBody{ Patches: []resources.GroupEntitlementsPatchItem{ { Entitlement: mockEntitlements.Data[0], Op: "add", }, }, - }) - mockRequest := httptest.NewRequest(http.MethodPatch, fmt.Sprintf("/groups/%s/entitlements", mockGroupId), bytes.NewReader(patchesBody)) + } + mockRequest := newTestRequest(http.MethodPatch, fmt.Sprintf("/groups/%s/entitlements", mockGroupId), &patches) h.PatchGroupsItemEntitlements(w, mockRequest, mockGroupId) }, expectedStatus: http.StatusOK, @@ -298,85 +295,6 @@ func TestHandler_Groups_Success(t *testing.T) { } -func TestHandler_Groups_ValidationErrors(t *testing.T) { - c := qt.New(t) - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - // need a value that is not a struct to trigger Decode error - mockInvalidRequestBody := true - - invalidRequestBody, _ := json.Marshal(mockInvalidRequestBody) - - type EndpointTest struct { - name string - triggerFunc func(h handler, w *httptest.ResponseRecorder) - } - - tests := []EndpointTest{ - { - name: "TestPostGroupsFailureInvalidRequest", - triggerFunc: func(h handler, w *httptest.ResponseRecorder) { - req := httptest.NewRequest(http.MethodPost, "/groups", bytes.NewReader(invalidRequestBody)) - h.PostGroups(w, req) - }, - }, - { - name: "TestPutGroupsFailureInvalidRequest", - triggerFunc: func(h handler, w *httptest.ResponseRecorder) { - req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/groups/%s", mockGroupId), bytes.NewReader(invalidRequestBody)) - h.PutGroupsItem(w, req, mockGroupId) - }, - }, - { - name: "TestPatchGroupsIdentitiesFailureInvalidRequest", - triggerFunc: func(h handler, w *httptest.ResponseRecorder) { - req := httptest.NewRequest(http.MethodPatch, fmt.Sprintf("/groups/%s/identities", mockGroupId), bytes.NewReader(invalidRequestBody)) - h.PatchGroupsItemIdentities(w, req, mockGroupId) - }, - }, - { - name: "TestPatchGroupsRolesFailureInvalidRequest", - triggerFunc: func(h handler, w *httptest.ResponseRecorder) { - req := httptest.NewRequest(http.MethodPatch, fmt.Sprintf("/groups/%s/roles", mockGroupId), bytes.NewReader(invalidRequestBody)) - h.PatchGroupsItemRoles(w, req, mockGroupId) - }, - }, - { - name: "TestPatchGroupsEntitlementsFailureInvalidRequest", - triggerFunc: func(h handler, w *httptest.ResponseRecorder) { - req := httptest.NewRequest(http.MethodPatch, fmt.Sprintf("/groups/%s/entitlements", mockGroupId), bytes.NewReader(invalidRequestBody)) - h.PatchGroupsItemEntitlements(w, req, mockGroupId) - }, - }, - } - for _, test := range tests { - tt := test - c.Run(tt.name, func(c *qt.C) { - mockWriter := httptest.NewRecorder() - sut := handler{} - - tt.triggerFunc(sut, mockWriter) - - result := mockWriter.Result() - defer result.Body.Close() - - c.Assert(result.StatusCode, qt.Equals, http.StatusBadRequest) - - data, err := io.ReadAll(result.Body) - c.Assert(err, qt.IsNil) - - response := new(resources.Response) - - err = json.Unmarshal(data, response) - c.Assert(err, qt.IsNil) - - c.Assert(response.Status, qt.Equals, http.StatusBadRequest) - c.Assert(response.Message, qt.Equals, "Bad Request: request doesn't match the expected schema") - }) - } -} - func TestHandler_Groups_ServiceBackendFailures(t *testing.T) { c := qt.New(t) ctrl := gomock.NewController(t) @@ -413,8 +331,8 @@ func TestHandler_Groups_ServiceBackendFailures(t *testing.T) { mockService.EXPECT().CreateGroup(gomock.Any(), gomock.Any()).Return(nil, mockError) }, triggerFunc: func(h handler, w *httptest.ResponseRecorder) { - group, _ := json.Marshal(&resources.Group{}) - request := httptest.NewRequest(http.MethodPost, "/groups", bytes.NewReader(group)) + mockGroup := &resources.Group{} + request := newTestRequest(http.MethodPost, "/groups", mockGroup) h.PostGroups(w, request) }, }, @@ -444,8 +362,8 @@ func TestHandler_Groups_ServiceBackendFailures(t *testing.T) { mockService.EXPECT().UpdateGroup(gomock.Any(), gomock.Any()).Return(nil, mockError) }, triggerFunc: func(h handler, w *httptest.ResponseRecorder) { - group, _ := json.Marshal(&resources.Group{Id: &mockGroupId}) - request := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/groups/%s", mockGroupId), bytes.NewReader(group)) + mockGroup := &resources.Group{Id: &mockGroupId} + request := newTestRequest(http.MethodPut, fmt.Sprintf("/groups/%s", mockGroupId), mockGroup) h.PutGroupsItem(w, request, mockGroupId) }, }, @@ -466,8 +384,8 @@ func TestHandler_Groups_ServiceBackendFailures(t *testing.T) { mockService.EXPECT().PatchGroupIdentities(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, mockError) }, triggerFunc: func(h handler, w *httptest.ResponseRecorder) { - patches, _ := json.Marshal(&resources.GroupIdentitiesPatchRequestBody{}) - request := httptest.NewRequest(http.MethodPatch, fmt.Sprintf("/groups/%s/identities", mockGroupId), bytes.NewReader(patches)) + patches := &resources.GroupIdentitiesPatchRequestBody{} + request := newTestRequest(http.MethodPatch, fmt.Sprintf("/groups/%s/identities", mockGroupId), patches) h.PatchGroupsItemIdentities(w, request, mockGroupId) }, }, @@ -488,8 +406,8 @@ func TestHandler_Groups_ServiceBackendFailures(t *testing.T) { mockService.EXPECT().PatchGroupRoles(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, mockError) }, triggerFunc: func(h handler, w *httptest.ResponseRecorder) { - patches, _ := json.Marshal(&resources.GroupRolesPatchRequestBody{}) - request := httptest.NewRequest(http.MethodPatch, fmt.Sprintf("/groups/%s/roles", mockGroupId), bytes.NewReader(patches)) + patches := &resources.GroupRolesPatchRequestBody{} + request := newTestRequest(http.MethodPatch, fmt.Sprintf("/groups/%s/roles", mockGroupId), patches) h.PatchGroupsItemRoles(w, request, mockGroupId) }, }, @@ -510,8 +428,8 @@ func TestHandler_Groups_ServiceBackendFailures(t *testing.T) { mockService.EXPECT().PatchGroupEntitlements(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, mockError) }, triggerFunc: func(h handler, w *httptest.ResponseRecorder) { - patches, _ := json.Marshal(&resources.GroupEntitlementsPatchRequestBody{}) - request := httptest.NewRequest(http.MethodPatch, fmt.Sprintf("/groups/%s/entitlements", mockGroupId), bytes.NewReader(patches)) + patches := &resources.GroupEntitlementsPatchRequestBody{} + request := newTestRequest(http.MethodPatch, fmt.Sprintf("/groups/%s/entitlements", mockGroupId), patches) h.PatchGroupsItemEntitlements(w, request, mockGroupId) }, }, diff --git a/rebac-admin-backend/v1/groups_validation.go b/rebac-admin-backend/v1/groups_validation.go new file mode 100644 index 000000000..2c323a358 --- /dev/null +++ b/rebac-admin-backend/v1/groups_validation.go @@ -0,0 +1,54 @@ +// Copyright 2024 Canonical Ltd. +// SPDX-License-Identifier: Apache-2.0 + +package v1 + +import ( + "net/http" + + "github.com/canonical/identity-platform-admin-ui/rebac-admin-backend/v1/resources" +) + +// PostGroups validates request body for the PostGroups method and delegates to the underlying handler. +func (v handlerWithValidation) PostGroups(w http.ResponseWriter, r *http.Request) { + body := &resources.Group{} + v.validateRequestBody(body, w, r, func(w http.ResponseWriter, r *http.Request) { + v.ServerInterface.PostGroups(w, r) + }) +} + +// PutGroupsItem validates request body for the PutGroupsItem method and delegates to the underlying handler. +func (v handlerWithValidation) PutGroupsItem(w http.ResponseWriter, r *http.Request, id string) { + body := &resources.Group{} + v.validateRequestBody(body, w, r, func(w http.ResponseWriter, r *http.Request) { + if body.Id == nil || id != *body.Id { + writeErrorResponse(w, NewRequestBodyValidationError("group ID from path does not match the Group object")) + return + } + v.ServerInterface.PutGroupsItem(w, r, id) + }) +} + +// PatchGroupsItemEntitlements validates request body for the PatchGroupsItemEntitlements method and delegates to the underlying handler. +func (v handlerWithValidation) PatchGroupsItemEntitlements(w http.ResponseWriter, r *http.Request, id string) { + body := &resources.GroupEntitlementsPatchRequestBody{} + v.validateRequestBody(body, w, r, func(w http.ResponseWriter, r *http.Request) { + v.ServerInterface.PatchGroupsItemEntitlements(w, r, id) + }) +} + +// PatchGroupsItemIdentities validates request body for the PatchGroupsItemIdentities method and delegates to the underlying handler. +func (v handlerWithValidation) PatchGroupsItemIdentities(w http.ResponseWriter, r *http.Request, id string) { + body := &resources.GroupIdentitiesPatchRequestBody{} + v.validateRequestBody(body, w, r, func(w http.ResponseWriter, r *http.Request) { + v.ServerInterface.PatchGroupsItemIdentities(w, r, id) + }) +} + +// PatchGroupsItemRoles validates request body for the PatchGroupsItemRoles method and delegates to the underlying handler. +func (v handlerWithValidation) PatchGroupsItemRoles(w http.ResponseWriter, r *http.Request, id string) { + body := &resources.GroupRolesPatchRequestBody{} + v.validateRequestBody(body, w, r, func(w http.ResponseWriter, r *http.Request) { + v.ServerInterface.PatchGroupsItemRoles(w, r, id) + }) +} diff --git a/rebac-admin-backend/v1/groups_validation_test.go b/rebac-admin-backend/v1/groups_validation_test.go new file mode 100644 index 000000000..9e64555d2 --- /dev/null +++ b/rebac-admin-backend/v1/groups_validation_test.go @@ -0,0 +1,387 @@ +// Copyright 2024 Canonical Ltd. +// SPDX-License-Identifier: Apache-2.0 + +package v1 + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "regexp" + "testing" + + qt "github.com/frankban/quicktest" + "go.uber.org/mock/gomock" + + "github.com/canonical/identity-platform-admin-ui/rebac-admin-backend/v1/resources" +) + +//go:generate mockgen -package resources -destination ./resources/mock_generated_server.go -source=./resources/generated_server.go + +func TestHandlerWithValidation_Groups(t *testing.T) { + c := qt.New(t) + + writeResponse := func(w http.ResponseWriter, status int, body any) { + raw, _ := json.Marshal(body) + w.WriteHeader(status) + _, _ = w.Write(raw) + } + + validEntitlement := resources.EntityEntitlement{ + EntitlementType: "some-entitlement-type", + EntityName: "some-entity-name", + EntityType: "some-entity-type", + } + + const ( + kindValidationFailure int = 0 + kindSuccessful int = 1 + kindBadJSON int = 2 + ) + + tests := []struct { + name string + requestBodyRaw string + requestBody any + setupHandlerMock func(mockHandler *resources.MockServerInterface) + triggerFunc func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) + kind int + expectedPatterns []string + }{{ + name: "PostGroups: success", + kind: kindSuccessful, + requestBody: resources.Group{Name: "foo"}, + setupHandlerMock: func(mockHandler *resources.MockServerInterface) { + mockHandler.EXPECT(). + PostGroups(gomock.Any(), gomock.Any()). + Do(func(w http.ResponseWriter, _ *http.Request) { + writeResponse(w, http.StatusOK, resources.Response{ + Status: http.StatusOK, + }) + }) + }, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PostGroups(w, r) + }, + }, { + name: "PostGroups: failure; invalid JSON", + kind: kindBadJSON, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PostGroups(w, r) + }, + }, { + name: "PostGroups: failure; empty", + expectedPatterns: []string{"'Name' failed on the 'required' tag"}, + requestBody: resources.Group{}, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PostGroups(w, r) + }, + }, { + name: "PutGroupsItem: success", + kind: kindSuccessful, + requestBody: resources.Group{Name: "foo", Id: stringPtr("some-id")}, + setupHandlerMock: func(mockHandler *resources.MockServerInterface) { + mockHandler.EXPECT(). + PutGroupsItem(gomock.Any(), gomock.Any(), "some-id"). + Do(func(w http.ResponseWriter, _ *http.Request, _ string) { + writeResponse(w, http.StatusOK, resources.Response{ + Status: http.StatusOK, + }) + }) + }, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PutGroupsItem(w, r, "some-id") + }, + }, { + name: "PutGroupsItem: failure; invalid JSON", + kind: kindBadJSON, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PutGroupsItem(w, r, "some-id") + }, + }, { + name: "PutGroupsItem: failure; empty", + expectedPatterns: []string{"'Name' failed on the 'required' tag"}, + requestBody: resources.Group{}, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PutGroupsItem(w, r, "some-id") + }, + }, { + name: "PutGroupsItem: failure; nil id", + expectedPatterns: []string{"group ID from path does not match the Group object"}, + requestBody: resources.Group{Name: "foo"}, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PutGroupsItem(w, r, "some-id") + }, + }, { + name: "PutGroupsItem: failure; id mismatch", + expectedPatterns: []string{"group ID from path does not match the Group object"}, + requestBody: resources.Group{Name: "foo", Id: stringPtr("some-other-id")}, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PutGroupsItem(w, r, "some-id") + }, + }, { + name: "PatchGroupsItemEntitlements: success", + kind: kindSuccessful, + requestBody: resources.GroupEntitlementsPatchRequestBody{ + Patches: []resources.GroupEntitlementsPatchItem{{ + Op: "add", + Entitlement: validEntitlement, + }}, + }, + setupHandlerMock: func(mockHandler *resources.MockServerInterface) { + mockHandler.EXPECT(). + PatchGroupsItemEntitlements(gomock.Any(), gomock.Any(), "some-id"). + Do(func(w http.ResponseWriter, _ *http.Request, _ string) { + writeResponse(w, http.StatusOK, resources.Response{ + Status: http.StatusOK, + }) + }) + }, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PatchGroupsItemEntitlements(w, r, "some-id") + }, + }, { + name: "PatchGroupsItemEntitlements: failure; invalid JSON", + kind: kindBadJSON, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PatchGroupsItemEntitlements(w, r, "some-id") + }, + }, { + name: "PatchGroupsItemEntitlements: failure; nil patch array", + expectedPatterns: []string{"'Patches' failed on the 'required' tag"}, + requestBody: resources.GroupEntitlementsPatchRequestBody{}, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PatchGroupsItemEntitlements(w, r, "some-id") + }, + }, { + name: "PatchGroupsItemEntitlements: failure; empty patch array", + expectedPatterns: []string{"'Patches' failed on the 'gt' tag"}, + requestBody: resources.GroupEntitlementsPatchRequestBody{ + Patches: []resources.GroupEntitlementsPatchItem{}, + }, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PatchGroupsItemEntitlements(w, r, "some-id") + }, + }, { + name: "PatchGroupsItemEntitlements: failure; invalid op", + expectedPatterns: []string{"'Op' failed on the 'oneof' tag"}, + requestBody: resources.GroupEntitlementsPatchRequestBody{ + Patches: []resources.GroupEntitlementsPatchItem{{ + Op: "some-invalid-op", + Entitlement: validEntitlement, + }}, + }, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PatchGroupsItemEntitlements(w, r, "some-id") + }, + }, { + name: "PatchGroupsItemEntitlements: failure; invalid entitlement", + expectedPatterns: []string{ + "'EntitlementType' failed on the 'required' tag", + "'EntityName' failed on the 'required' tag", + "'EntityType' failed on the 'required' tag", + }, + requestBody: resources.GroupEntitlementsPatchRequestBody{ + Patches: []resources.GroupEntitlementsPatchItem{{ + Op: "add", + Entitlement: resources.EntityEntitlement{}, + }}, + }, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PatchGroupsItemEntitlements(w, r, "some-id") + }, + }, { + name: "PatchGroupsItemIdentities: success", + kind: kindSuccessful, + requestBody: resources.GroupIdentitiesPatchRequestBody{ + Patches: []resources.GroupIdentitiesPatchItem{{ + Op: "add", + Identity: "some-identity", + }}, + }, + setupHandlerMock: func(mockHandler *resources.MockServerInterface) { + mockHandler.EXPECT(). + PatchGroupsItemIdentities(gomock.Any(), gomock.Any(), "some-id"). + Do(func(w http.ResponseWriter, _ *http.Request, _ string) { + writeResponse(w, http.StatusOK, resources.Response{ + Status: http.StatusOK, + }) + }) + }, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PatchGroupsItemIdentities(w, r, "some-id") + }, + }, { + name: "PatchGroupsItemIdentities: failure; invalid JSON", + kind: kindBadJSON, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PatchGroupsItemIdentities(w, r, "some-id") + }, + }, { + name: "PatchGroupsItemIdentities: failure; nil patch array", + expectedPatterns: []string{"'Patches' failed on the 'required' tag"}, + requestBody: resources.GroupIdentitiesPatchRequestBody{}, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PatchGroupsItemIdentities(w, r, "some-id") + }, + }, { + name: "PatchGroupsItemIdentities: failure; empty patch array", + expectedPatterns: []string{"'Patches' failed on the 'gt' tag"}, + requestBody: resources.GroupIdentitiesPatchRequestBody{ + Patches: []resources.GroupIdentitiesPatchItem{}, + }, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PatchGroupsItemIdentities(w, r, "some-id") + }, + }, { + name: "PatchGroupsItemIdentities: failure; empty identity", + expectedPatterns: []string{"'Identity' failed on the 'required' tag"}, + requestBody: resources.GroupIdentitiesPatchRequestBody{ + Patches: []resources.GroupIdentitiesPatchItem{{ + Op: "add", + Identity: "", + }}, + }, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PatchGroupsItemIdentities(w, r, "some-id") + }, + }, { + name: "PatchGroupsItemIdentities: failure; invalid op", + expectedPatterns: []string{"'Op' failed on the 'oneof' tag"}, + requestBody: resources.GroupIdentitiesPatchRequestBody{ + Patches: []resources.GroupIdentitiesPatchItem{{ + Op: "some-invalid-op", + Identity: "some-identity", + }}, + }, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PatchGroupsItemIdentities(w, r, "some-id") + }, + }, { + name: "PatchGroupsItemRoles: success", + kind: kindSuccessful, + requestBody: resources.GroupRolesPatchRequestBody{ + Patches: []resources.GroupRolesPatchItem{{ + Op: "add", + Role: "some-role", + }}, + }, + setupHandlerMock: func(mockHandler *resources.MockServerInterface) { + mockHandler.EXPECT(). + PatchGroupsItemRoles(gomock.Any(), gomock.Any(), "some-id"). + Do(func(w http.ResponseWriter, _ *http.Request, _ string) { + writeResponse(w, http.StatusOK, resources.Response{ + Status: http.StatusOK, + }) + }) + }, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PatchGroupsItemRoles(w, r, "some-id") + }, + }, { + name: "PatchGroupsItemRoles: failure; invalid JSON", + kind: kindBadJSON, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PatchGroupsItemRoles(w, r, "some-id") + }, + }, { + name: "PatchGroupsItemRoles: failure; nil patch array", + expectedPatterns: []string{"'Patches' failed on the 'required' tag"}, + requestBody: resources.GroupRolesPatchRequestBody{}, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PatchGroupsItemRoles(w, r, "some-id") + }, + }, { + name: "PatchGroupsItemRoles: failure; empty patch array", + expectedPatterns: []string{"'Patches' failed on the 'gt' tag"}, + requestBody: resources.GroupRolesPatchRequestBody{ + Patches: []resources.GroupRolesPatchItem{}, + }, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PatchGroupsItemRoles(w, r, "some-id") + }, + }, { + name: "PatchGroupsItemRoles: failure; empty role", + expectedPatterns: []string{"'Role' failed on the 'required' tag"}, + requestBody: resources.GroupRolesPatchRequestBody{ + Patches: []resources.GroupRolesPatchItem{{ + Op: "add", + Role: "", + }}, + }, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PatchGroupsItemRoles(w, r, "some-id") + }, + }, { + name: "PatchGroupsItemRoles: failure; invalid op", + expectedPatterns: []string{"'Op' failed on the 'oneof' tag"}, + requestBody: resources.GroupRolesPatchRequestBody{ + Patches: []resources.GroupRolesPatchItem{{ + Op: "some-invalid-op", + Role: "some-role", + }}, + }, + triggerFunc: func(sut *handlerWithValidation, w http.ResponseWriter, r *http.Request) { + sut.PatchGroupsItemRoles(w, r, "some-id") + }, + }, + } + + for _, t := range tests { + tt := t + c.Run(tt.name, func(c *qt.C) { + ctrl := gomock.NewController(c) + defer ctrl.Finish() + + mockHandler := resources.NewMockServerInterface(ctrl) + if tt.setupHandlerMock != nil { + tt.setupHandlerMock(mockHandler) + } + + sut := newHandlerWithValidation(mockHandler) + + var req *http.Request + if tt.requestBody != nil { + raw, err := json.Marshal(tt.requestBody) + c.Assert(err, qt.IsNil) + // Note that request method/URL shouldn't be important at the handler. + req, _ = http.NewRequest(http.MethodGet, "/blah", bytes.NewReader(raw)) + } else { + // Note that request method/URL shouldn't be important at the handler. + req, _ = http.NewRequest(http.MethodGet, "/blah", bytes.NewReader([]byte(tt.requestBodyRaw))) + } + + mockWriter := httptest.NewRecorder() + tt.triggerFunc(sut, mockWriter, req) + + response := mockWriter.Result() + if tt.kind == kindSuccessful { + c.Assert(response.StatusCode, qt.Equals, http.StatusOK) + } else { + c.Assert(response.StatusCode, qt.Equals, http.StatusBadRequest) + + defer response.Body.Close() + responseBody, err := io.ReadAll(response.Body) + c.Assert(err, qt.IsNil) + + parsedResponse := &resources.Response{} + err = json.Unmarshal(responseBody, parsedResponse) + c.Assert(err, qt.IsNil) + c.Assert(parsedResponse.Status, qt.Equals, http.StatusBadRequest) + + if tt.kind == kindBadJSON { + c.Assert(parsedResponse.Message, qt.Matches, "Bad Request: missing request body: request body is not a valid JSON") + } else if tt.kind == kindValidationFailure { + c.Assert(parsedResponse.Message, qt.Matches, regexp.MustCompile("Bad Request: invalid request body: .+")) + } + + for _, pattern := range tt.expectedPatterns { + c.Assert(parsedResponse.Message, qt.Matches, regexp.MustCompile(fmt.Sprintf(".*%s.*", pattern))) + } + } + }) + } +} diff --git a/rebac-admin-backend/v1/resources/generated_server.go b/rebac-admin-backend/v1/resources/generated_server.go index bc5c73b31..32209e019 100644 --- a/rebac-admin-backend/v1/resources/generated_server.go +++ b/rebac-admin-backend/v1/resources/generated_server.go @@ -421,6 +421,27 @@ func (siw *ServerInterfaceWrapper) GetIdentityProviders(w http.ResponseWriter, r return } + headers := r.Header + + // ------------- Optional header parameter "Next-Page-Token" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("Next-Page-Token")]; found { + var NextPageToken PaginationNextTokenHeader + n := len(valueList) + if n != 1 { + siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "Next-Page-Token", Count: n}) + return + } + + err = runtime.BindStyledParameterWithOptions("simple", "Next-Page-Token", valueList[0], &NextPageToken, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "Next-Page-Token", Err: err}) + return + } + + params.NextPageToken = &NextPageToken + + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { siw.Handler.GetIdentityProviders(w, r, params) })) @@ -480,6 +501,27 @@ func (siw *ServerInterfaceWrapper) GetAvailableIdentityProviders(w http.Response return } + headers := r.Header + + // ------------- Optional header parameter "Next-Page-Token" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("Next-Page-Token")]; found { + var NextPageToken PaginationNextTokenHeader + n := len(valueList) + if n != 1 { + siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "Next-Page-Token", Count: n}) + return + } + + err = runtime.BindStyledParameterWithOptions("simple", "Next-Page-Token", valueList[0], &NextPageToken, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "Next-Page-Token", Err: err}) + return + } + + params.NextPageToken = &NextPageToken + + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { siw.Handler.GetAvailableIdentityProviders(w, r, params) })) @@ -625,6 +667,27 @@ func (siw *ServerInterfaceWrapper) GetEntitlements(w http.ResponseWriter, r *htt return } + headers := r.Header + + // ------------- Optional header parameter "Next-Page-Token" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("Next-Page-Token")]; found { + var NextPageToken PaginationNextTokenHeader + n := len(valueList) + if n != 1 { + siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "Next-Page-Token", Count: n}) + return + } + + err = runtime.BindStyledParameterWithOptions("simple", "Next-Page-Token", valueList[0], &NextPageToken, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "Next-Page-Token", Err: err}) + return + } + + params.NextPageToken = &NextPageToken + + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { siw.Handler.GetEntitlements(w, r, params) })) @@ -692,6 +755,27 @@ func (siw *ServerInterfaceWrapper) GetGroups(w http.ResponseWriter, r *http.Requ return } + headers := r.Header + + // ------------- Optional header parameter "Next-Page-Token" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("Next-Page-Token")]; found { + var NextPageToken PaginationNextTokenHeader + n := len(valueList) + if n != 1 { + siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "Next-Page-Token", Count: n}) + return + } + + err = runtime.BindStyledParameterWithOptions("simple", "Next-Page-Token", valueList[0], &NextPageToken, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "Next-Page-Token", Err: err}) + return + } + + params.NextPageToken = &NextPageToken + + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { siw.Handler.GetGroups(w, r, params) })) @@ -838,6 +922,27 @@ func (siw *ServerInterfaceWrapper) GetGroupsItemEntitlements(w http.ResponseWrit return } + headers := r.Header + + // ------------- Optional header parameter "Next-Page-Token" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("Next-Page-Token")]; found { + var NextPageToken PaginationNextTokenHeader + n := len(valueList) + if n != 1 { + siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "Next-Page-Token", Count: n}) + return + } + + err = runtime.BindStyledParameterWithOptions("simple", "Next-Page-Token", valueList[0], &NextPageToken, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "Next-Page-Token", Err: err}) + return + } + + params.NextPageToken = &NextPageToken + + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { siw.Handler.GetGroupsItemEntitlements(w, r, id, params) })) @@ -917,6 +1022,27 @@ func (siw *ServerInterfaceWrapper) GetGroupsItemIdentities(w http.ResponseWriter return } + headers := r.Header + + // ------------- Optional header parameter "Next-Page-Token" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("Next-Page-Token")]; found { + var NextPageToken PaginationNextTokenHeader + n := len(valueList) + if n != 1 { + siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "Next-Page-Token", Count: n}) + return + } + + err = runtime.BindStyledParameterWithOptions("simple", "Next-Page-Token", valueList[0], &NextPageToken, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "Next-Page-Token", Err: err}) + return + } + + params.NextPageToken = &NextPageToken + + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { siw.Handler.GetGroupsItemIdentities(w, r, id, params) })) @@ -996,6 +1122,27 @@ func (siw *ServerInterfaceWrapper) GetGroupsItemRoles(w http.ResponseWriter, r * return } + headers := r.Header + + // ------------- Optional header parameter "Next-Page-Token" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("Next-Page-Token")]; found { + var NextPageToken PaginationNextTokenHeader + n := len(valueList) + if n != 1 { + siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "Next-Page-Token", Count: n}) + return + } + + err = runtime.BindStyledParameterWithOptions("simple", "Next-Page-Token", valueList[0], &NextPageToken, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "Next-Page-Token", Err: err}) + return + } + + params.NextPageToken = &NextPageToken + + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { siw.Handler.GetGroupsItemRoles(w, r, id, params) })) @@ -1074,6 +1221,27 @@ func (siw *ServerInterfaceWrapper) GetIdentities(w http.ResponseWriter, r *http. return } + headers := r.Header + + // ------------- Optional header parameter "Next-Page-Token" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("Next-Page-Token")]; found { + var NextPageToken PaginationNextTokenHeader + n := len(valueList) + if n != 1 { + siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "Next-Page-Token", Count: n}) + return + } + + err = runtime.BindStyledParameterWithOptions("simple", "Next-Page-Token", valueList[0], &NextPageToken, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "Next-Page-Token", Err: err}) + return + } + + params.NextPageToken = &NextPageToken + + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { siw.Handler.GetIdentities(w, r, params) })) @@ -1220,6 +1388,27 @@ func (siw *ServerInterfaceWrapper) GetIdentitiesItemEntitlements(w http.Response return } + headers := r.Header + + // ------------- Optional header parameter "Next-Page-Token" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("Next-Page-Token")]; found { + var NextPageToken PaginationNextTokenHeader + n := len(valueList) + if n != 1 { + siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "Next-Page-Token", Count: n}) + return + } + + err = runtime.BindStyledParameterWithOptions("simple", "Next-Page-Token", valueList[0], &NextPageToken, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "Next-Page-Token", Err: err}) + return + } + + params.NextPageToken = &NextPageToken + + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { siw.Handler.GetIdentitiesItemEntitlements(w, r, id, params) })) @@ -1299,6 +1488,27 @@ func (siw *ServerInterfaceWrapper) GetIdentitiesItemGroups(w http.ResponseWriter return } + headers := r.Header + + // ------------- Optional header parameter "Next-Page-Token" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("Next-Page-Token")]; found { + var NextPageToken PaginationNextTokenHeader + n := len(valueList) + if n != 1 { + siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "Next-Page-Token", Count: n}) + return + } + + err = runtime.BindStyledParameterWithOptions("simple", "Next-Page-Token", valueList[0], &NextPageToken, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "Next-Page-Token", Err: err}) + return + } + + params.NextPageToken = &NextPageToken + + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { siw.Handler.GetIdentitiesItemGroups(w, r, id, params) })) @@ -1378,6 +1588,27 @@ func (siw *ServerInterfaceWrapper) GetIdentitiesItemRoles(w http.ResponseWriter, return } + headers := r.Header + + // ------------- Optional header parameter "Next-Page-Token" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("Next-Page-Token")]; found { + var NextPageToken PaginationNextTokenHeader + n := len(valueList) + if n != 1 { + siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "Next-Page-Token", Count: n}) + return + } + + err = runtime.BindStyledParameterWithOptions("simple", "Next-Page-Token", valueList[0], &NextPageToken, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "Next-Page-Token", Err: err}) + return + } + + params.NextPageToken = &NextPageToken + + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { siw.Handler.GetIdentitiesItemRoles(w, r, id, params) })) @@ -1456,6 +1687,27 @@ func (siw *ServerInterfaceWrapper) GetResources(w http.ResponseWriter, r *http.R return } + headers := r.Header + + // ------------- Optional header parameter "Next-Page-Token" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("Next-Page-Token")]; found { + var NextPageToken PaginationNextTokenHeader + n := len(valueList) + if n != 1 { + siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "Next-Page-Token", Count: n}) + return + } + + err = runtime.BindStyledParameterWithOptions("simple", "Next-Page-Token", valueList[0], &NextPageToken, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "Next-Page-Token", Err: err}) + return + } + + params.NextPageToken = &NextPageToken + + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { siw.Handler.GetResources(w, r, params) })) @@ -1508,6 +1760,27 @@ func (siw *ServerInterfaceWrapper) GetRoles(w http.ResponseWriter, r *http.Reque return } + headers := r.Header + + // ------------- Optional header parameter "Next-Page-Token" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("Next-Page-Token")]; found { + var NextPageToken PaginationNextTokenHeader + n := len(valueList) + if n != 1 { + siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "Next-Page-Token", Count: n}) + return + } + + err = runtime.BindStyledParameterWithOptions("simple", "Next-Page-Token", valueList[0], &NextPageToken, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "Next-Page-Token", Err: err}) + return + } + + params.NextPageToken = &NextPageToken + + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { siw.Handler.GetRoles(w, r, params) })) @@ -1654,6 +1927,27 @@ func (siw *ServerInterfaceWrapper) GetRolesItemEntitlements(w http.ResponseWrite return } + headers := r.Header + + // ------------- Optional header parameter "Next-Page-Token" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("Next-Page-Token")]; found { + var NextPageToken PaginationNextTokenHeader + n := len(valueList) + if n != 1 { + siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "Next-Page-Token", Count: n}) + return + } + + err = runtime.BindStyledParameterWithOptions("simple", "Next-Page-Token", valueList[0], &NextPageToken, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "Next-Page-Token", Err: err}) + return + } + + params.NextPageToken = &NextPageToken + + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { siw.Handler.GetRolesItemEntitlements(w, r, id, params) })) diff --git a/rebac-admin-backend/v1/resources/generated_types.go b/rebac-admin-backend/v1/resources/generated_types.go index 1f283b07b..d45726b52 100644 --- a/rebac-admin-backend/v1/resources/generated_types.go +++ b/rebac-admin-backend/v1/resources/generated_types.go @@ -21,6 +21,7 @@ const ( GET CapabilityMethods = "GET" PATCH CapabilityMethods = "PATCH" POST CapabilityMethods = "POST" + PUT CapabilityMethods = "PUT" ) // Defines values for GroupEntitlementsPatchItemOp. @@ -97,15 +98,15 @@ type CapabilityMethods string // Entity defines model for Entity. type Entity struct { - Id string `json:"id"` - Type string `json:"type"` + Id string `json:"id" validate:"required"` + Type string `json:"type" validate:"required"` } // EntityEntitlement defines model for EntityEntitlement. type EntityEntitlement struct { - EntitlementType string `json:"entitlement_type"` - EntityName string `json:"entity_name"` - EntityType string `json:"entity_type"` + EntitlementType string `json:"entitlement_type" validate:"required"` + EntityName string `json:"entity_name" validate:"required"` + EntityType string `json:"entity_type" validate:"required"` } // EntityEntitlementItem defines model for EntityEntitlementItem. @@ -251,13 +252,13 @@ type GetRolesResponse struct { // Group defines model for Group. type Group struct { Id *string `json:"id,omitempty"` - Name string `json:"name"` + Name string `json:"name" validate:"required"` } // GroupEntitlementsPatchItem defines model for GroupEntitlementsPatchItem. type GroupEntitlementsPatchItem struct { Entitlement EntityEntitlement `json:"entitlement"` - Op GroupEntitlementsPatchItemOp `json:"op"` + Op GroupEntitlementsPatchItemOp `json:"op" validate:"required,oneof=add remove"` } // GroupEntitlementsPatchItemOp defines model for GroupEntitlementsPatchItem.Op. @@ -265,13 +266,13 @@ type GroupEntitlementsPatchItemOp string // GroupEntitlementsPatchRequestBody defines model for GroupEntitlementsPatchRequestBody. type GroupEntitlementsPatchRequestBody struct { - Patches []GroupEntitlementsPatchItem `json:"patches"` + Patches []GroupEntitlementsPatchItem `json:"patches" validate:"required,gt=0,dive"` } // GroupIdentitiesPatchItem defines model for GroupIdentitiesPatchItem. type GroupIdentitiesPatchItem struct { - Identity string `json:"identity"` - Op GroupIdentitiesPatchItemOp `json:"op"` + Identity string `json:"identity" validate:"required"` + Op GroupIdentitiesPatchItemOp `json:"op" validate:"required,oneof=add remove"` } // GroupIdentitiesPatchItemOp defines model for GroupIdentitiesPatchItem.Op. @@ -279,13 +280,13 @@ type GroupIdentitiesPatchItemOp string // GroupIdentitiesPatchRequestBody defines model for GroupIdentitiesPatchRequestBody. type GroupIdentitiesPatchRequestBody struct { - Patches []GroupIdentitiesPatchItem `json:"patches"` + Patches []GroupIdentitiesPatchItem `json:"patches" validate:"required,gt=0,dive"` } // GroupRolesPatchItem defines model for GroupRolesPatchItem. type GroupRolesPatchItem struct { - Op GroupRolesPatchItemOp `json:"op"` - Role string `json:"role"` + Op GroupRolesPatchItemOp `json:"op" validate:"required,oneof=add remove"` + Role string `json:"role" validate:"required"` } // GroupRolesPatchItemOp defines model for GroupRolesPatchItem.Op. @@ -293,7 +294,7 @@ type GroupRolesPatchItemOp string // GroupRolesPatchRequestBody defines model for GroupRolesPatchRequestBody. type GroupRolesPatchRequestBody struct { - Patches []GroupRolesPatchItem `json:"patches"` + Patches []GroupRolesPatchItem `json:"patches" validate:"required,gt=0,dive"` } // Groups defines model for Groups. @@ -308,9 +309,9 @@ type Identities struct { // Identity defines model for Identity. type Identity struct { - AddedBy string `json:"addedBy"` + AddedBy string `json:"addedBy" validate:"required"` Certificate *string `json:"certificate,omitempty"` - Email string `json:"email"` + Email string `json:"email" validate:"required"` FirstName *string `json:"firstName,omitempty"` Groups *int `json:"groups,omitempty"` Id *string `json:"id,omitempty"` @@ -319,13 +320,13 @@ type Identity struct { LastName *string `json:"lastName,omitempty"` Permissions *int `json:"permissions,omitempty"` Roles *int `json:"roles,omitempty"` - Source string `json:"source"` + Source string `json:"source" validate:"required"` } // IdentityEntitlementsPatchItem defines model for IdentityEntitlementsPatchItem. type IdentityEntitlementsPatchItem struct { Entitlement EntityEntitlement `json:"entitlement"` - Op IdentityEntitlementsPatchItemOp `json:"op"` + Op IdentityEntitlementsPatchItemOp `json:"op" validate:"required,oneof=add remove"` } // IdentityEntitlementsPatchItemOp defines model for IdentityEntitlementsPatchItem.Op. @@ -333,13 +334,13 @@ type IdentityEntitlementsPatchItemOp string // IdentityEntitlementsPatchRequestBody defines model for IdentityEntitlementsPatchRequestBody. type IdentityEntitlementsPatchRequestBody struct { - Patches []IdentityEntitlementsPatchItem `json:"patches"` + Patches []IdentityEntitlementsPatchItem `json:"patches" validate:"required,gt=0,dive"` } // IdentityGroupsPatchItem defines model for IdentityGroupsPatchItem. type IdentityGroupsPatchItem struct { - Group string `json:"group"` - Op IdentityGroupsPatchItemOp `json:"op"` + Group string `json:"group" validate:"required"` + Op IdentityGroupsPatchItemOp `json:"op" validate:"required,oneof=add remove"` } // IdentityGroupsPatchItemOp defines model for IdentityGroupsPatchItem.Op. @@ -347,7 +348,7 @@ type IdentityGroupsPatchItemOp string // IdentityGroupsPatchRequestBody defines model for IdentityGroupsPatchRequestBody. type IdentityGroupsPatchRequestBody struct { - Patches []IdentityGroupsPatchItem `json:"patches"` + Patches []IdentityGroupsPatchItem `json:"patches" validate:"required,gt=0,dive"` } // IdentityProvider defines model for IdentityProvider. @@ -364,7 +365,7 @@ type IdentityProvider struct { RedirectUrl *string `json:"redirectUrl,omitempty"` StoreTokens *bool `json:"storeTokens,omitempty"` StoreTokensReadable *bool `json:"storeTokensReadable,omitempty"` - SyncMode *IdentityProviderSyncMode `json:"syncMode,omitempty"` + SyncMode *IdentityProviderSyncMode `json:"syncMode,omitempty" validate:"oneof=import"` TrustEmail *bool `json:"trustEmail,omitempty"` } @@ -378,8 +379,8 @@ type IdentityProviders struct { // IdentityRolesPatchItem defines model for IdentityRolesPatchItem. type IdentityRolesPatchItem struct { - Op IdentityRolesPatchItemOp `json:"op"` - Role string `json:"role"` + Op IdentityRolesPatchItemOp `json:"op" validate:"required,oneof=add remove"` + Role string `json:"role" validate:"required"` } // IdentityRolesPatchItemOp defines model for IdentityRolesPatchItem.Op. @@ -387,7 +388,7 @@ type IdentityRolesPatchItemOp string // IdentityRolesPatchRequestBody defines model for IdentityRolesPatchRequestBody. type IdentityRolesPatchRequestBody struct { - Patches []IdentityRolesPatchItem `json:"patches"` + Patches []IdentityRolesPatchItem `json:"patches" validate:"required,gt=0,dive"` } // Resource defines model for Resource. @@ -431,9 +432,9 @@ type ResponseMeta struct { // Role defines model for Role. type Role struct { - Entitlements *[]RoleEntitlement `json:"entitlements,omitempty"` + Entitlements *[]RoleEntitlement `json:"entitlements,omitempty" validate:"dive"` Id *string `json:"id,omitempty"` - Name string `json:"name"` + Name string `json:"name" validate:"required"` } // RoleEntitlement defines model for RoleEntitlement. @@ -446,7 +447,7 @@ type RoleEntitlement struct { // RoleEntitlementsPatchItem defines model for RoleEntitlementsPatchItem. type RoleEntitlementsPatchItem struct { Entitlement EntityEntitlement `json:"entitlement"` - Op RoleEntitlementsPatchItemOp `json:"op"` + Op RoleEntitlementsPatchItemOp `json:"op" validate:"required,oneof=add remove"` } // RoleEntitlementsPatchItemOp defines model for RoleEntitlementsPatchItem.Op. @@ -454,7 +455,7 @@ type RoleEntitlementsPatchItemOp string // RoleEntitlementsPatchRequestBody defines model for RoleEntitlementsPatchRequestBody. type RoleEntitlementsPatchRequestBody struct { - Patches []RoleEntitlementsPatchItem `json:"patches"` + Patches []RoleEntitlementsPatchItem `json:"patches" validate:"required,gt=0,dive"` } // Roles defines model for Roles. @@ -468,6 +469,9 @@ type FilterParam = string // PaginationNextToken defines model for PaginationNextToken. type PaginationNextToken = string +// PaginationNextTokenHeader defines model for PaginationNextTokenHeader. +type PaginationNextTokenHeader = string + // PaginationPage defines model for PaginationPage. type PaginationPage = int @@ -496,6 +500,9 @@ type GetIdentityProvidersParams struct { // NextToken The continuation token to retrieve the next set of results NextToken *PaginationNextToken `form:"nextToken,omitempty" json:"nextToken,omitempty"` + + // NextPageToken The continuation token to retrieve the next set of results + NextPageToken *PaginationNextTokenHeader `json:"Next-Page-Token,omitempty"` } // GetAvailableIdentityProvidersParams defines parameters for GetAvailableIdentityProviders. @@ -508,6 +515,9 @@ type GetAvailableIdentityProvidersParams struct { // NextToken The continuation token to retrieve the next set of results NextToken *PaginationNextToken `form:"nextToken,omitempty" json:"nextToken,omitempty"` + + // NextPageToken The continuation token to retrieve the next set of results + NextPageToken *PaginationNextTokenHeader `json:"Next-Page-Token,omitempty"` } // GetEntitlementsParams defines parameters for GetEntitlements. @@ -523,6 +533,9 @@ type GetEntitlementsParams struct { // Filter A string to filter results by Filter *FilterParam `form:"filter,omitempty" json:"filter,omitempty"` + + // NextPageToken The continuation token to retrieve the next set of results + NextPageToken *PaginationNextTokenHeader `json:"Next-Page-Token,omitempty"` } // GetGroupsParams defines parameters for GetGroups. @@ -538,6 +551,9 @@ type GetGroupsParams struct { // Filter A string to filter results by Filter *FilterParam `form:"filter,omitempty" json:"filter,omitempty"` + + // NextPageToken The continuation token to retrieve the next set of results + NextPageToken *PaginationNextTokenHeader `json:"Next-Page-Token,omitempty"` } // GetGroupsItemEntitlementsParams defines parameters for GetGroupsItemEntitlements. @@ -550,6 +566,9 @@ type GetGroupsItemEntitlementsParams struct { // NextToken The continuation token to retrieve the next set of results NextToken *PaginationNextToken `form:"nextToken,omitempty" json:"nextToken,omitempty"` + + // NextPageToken The continuation token to retrieve the next set of results + NextPageToken *PaginationNextTokenHeader `json:"Next-Page-Token,omitempty"` } // GetGroupsItemIdentitiesParams defines parameters for GetGroupsItemIdentities. @@ -562,6 +581,9 @@ type GetGroupsItemIdentitiesParams struct { // NextToken The continuation token to retrieve the next set of results NextToken *PaginationNextToken `form:"nextToken,omitempty" json:"nextToken,omitempty"` + + // NextPageToken The continuation token to retrieve the next set of results + NextPageToken *PaginationNextTokenHeader `json:"Next-Page-Token,omitempty"` } // GetGroupsItemRolesParams defines parameters for GetGroupsItemRoles. @@ -574,6 +596,9 @@ type GetGroupsItemRolesParams struct { // NextToken The continuation token to retrieve the next set of results NextToken *PaginationNextToken `form:"nextToken,omitempty" json:"nextToken,omitempty"` + + // NextPageToken The continuation token to retrieve the next set of results + NextPageToken *PaginationNextTokenHeader `json:"Next-Page-Token,omitempty"` } // GetIdentitiesParams defines parameters for GetIdentities. @@ -589,6 +614,9 @@ type GetIdentitiesParams struct { // Filter A string to filter results by Filter *FilterParam `form:"filter,omitempty" json:"filter,omitempty"` + + // NextPageToken The continuation token to retrieve the next set of results + NextPageToken *PaginationNextTokenHeader `json:"Next-Page-Token,omitempty"` } // GetIdentitiesItemEntitlementsParams defines parameters for GetIdentitiesItemEntitlements. @@ -601,6 +629,9 @@ type GetIdentitiesItemEntitlementsParams struct { // NextToken The continuation token to retrieve the next set of results NextToken *PaginationNextToken `form:"nextToken,omitempty" json:"nextToken,omitempty"` + + // NextPageToken The continuation token to retrieve the next set of results + NextPageToken *PaginationNextTokenHeader `json:"Next-Page-Token,omitempty"` } // GetIdentitiesItemGroupsParams defines parameters for GetIdentitiesItemGroups. @@ -613,6 +644,9 @@ type GetIdentitiesItemGroupsParams struct { // NextToken The continuation token to retrieve the next set of results NextToken *PaginationNextToken `form:"nextToken,omitempty" json:"nextToken,omitempty"` + + // NextPageToken The continuation token to retrieve the next set of results + NextPageToken *PaginationNextTokenHeader `json:"Next-Page-Token,omitempty"` } // GetIdentitiesItemRolesParams defines parameters for GetIdentitiesItemRoles. @@ -625,6 +659,9 @@ type GetIdentitiesItemRolesParams struct { // NextToken The continuation token to retrieve the next set of results NextToken *PaginationNextToken `form:"nextToken,omitempty" json:"nextToken,omitempty"` + + // NextPageToken The continuation token to retrieve the next set of results + NextPageToken *PaginationNextTokenHeader `json:"Next-Page-Token,omitempty"` } // GetResourcesParams defines parameters for GetResources. @@ -638,6 +675,9 @@ type GetResourcesParams struct { // NextToken The continuation token to retrieve the next set of results NextToken *PaginationNextToken `form:"nextToken,omitempty" json:"nextToken,omitempty"` EntityType *string `form:"entityType,omitempty" json:"entityType,omitempty"` + + // NextPageToken The continuation token to retrieve the next set of results + NextPageToken *PaginationNextTokenHeader `json:"Next-Page-Token,omitempty"` } // GetRolesParams defines parameters for GetRoles. @@ -653,6 +693,9 @@ type GetRolesParams struct { // Filter A string to filter results by Filter *FilterParam `form:"filter,omitempty" json:"filter,omitempty"` + + // NextPageToken The continuation token to retrieve the next set of results + NextPageToken *PaginationNextTokenHeader `json:"Next-Page-Token,omitempty"` } // GetRolesItemEntitlementsParams defines parameters for GetRolesItemEntitlements. @@ -665,6 +708,9 @@ type GetRolesItemEntitlementsParams struct { // NextToken The continuation token to retrieve the next set of results NextToken *PaginationNextToken `form:"nextToken,omitempty" json:"nextToken,omitempty"` + + // NextPageToken The continuation token to retrieve the next set of results + NextPageToken *PaginationNextTokenHeader `json:"Next-Page-Token,omitempty"` } // PostIdentityProvidersJSONRequestBody defines body for PostIdentityProviders for application/json ContentType. @@ -715,70 +761,73 @@ type PatchRolesItemEntitlementsJSONRequestBody = RoleEntitlementsPatchRequestBod // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "zrSWoAAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/rebac-admin-backend/v1/response.go b/rebac-admin-backend/v1/response.go index 5b12c378d..c5e79f947 100644 --- a/rebac-admin-backend/v1/response.go +++ b/rebac-admin-backend/v1/response.go @@ -17,13 +17,19 @@ func writeErrorResponse(w http.ResponseWriter, err error) { body, err := json.Marshal(resp) if err != nil { w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("unexpected marshalling error")) + if _, err := w.Write([]byte("unexpected marshalling error")); err != nil { + // TODO(CSS-7642): we should log the error. + return + } return } setJSONContentTypeHeader(w) w.WriteHeader(int(resp.Status)) - w.Write(body) + if _, err := w.Write(body); err != nil { + // TODO(CSS-7642): we should log the error. + return + } } // mapErrorResponse returns a Response instance filled with the given error. @@ -60,7 +66,10 @@ func writeResponse(w http.ResponseWriter, status int, responseObject interface{} setJSONContentTypeHeader(w) w.WriteHeader(status) - w.Write(data) + if _, err := w.Write(data); err != nil { + // TODO(CSS-7642): we should log the error. + return + } } // mapServiceErrorResponse maps errors thrown by services to the designated diff --git a/rebac-admin-backend/v1/util_test.go b/rebac-admin-backend/v1/util_test.go new file mode 100644 index 000000000..8572657a8 --- /dev/null +++ b/rebac-admin-backend/v1/util_test.go @@ -0,0 +1,18 @@ +// Copyright 2024 Canonical Ltd. +// SPDX-License-Identifier: Apache-2.0 + +package v1 + +import ( + "net/http" + "net/http/httptest" +) + +func newTestRequest[T any](method, path string, body *T) *http.Request { + r := httptest.NewRequest(method, path, nil) + return newRequestWithBodyInContext(r, body) +} + +func stringPtr(s string) *string { + return &s +} diff --git a/rebac-admin-backend/v1/validation.go b/rebac-admin-backend/v1/validation.go new file mode 100644 index 000000000..078939b55 --- /dev/null +++ b/rebac-admin-backend/v1/validation.go @@ -0,0 +1,77 @@ +// Copyright 2024 Canonical Ltd. +// SPDX-License-Identifier: Apache-2.0 + +package v1 + +import ( + "context" + "encoding/json" + "net/http" + + "github.com/go-playground/validator/v10" + + "github.com/canonical/identity-platform-admin-ui/rebac-admin-backend/v1/resources" +) + +// handlerWithValidation decorates a given handler with validation logic. The +// request body is parsed into a safely-typed value and passed to the handler +// via context. +type handlerWithValidation struct { + // Wrapped/decorated handler + resources.ServerInterface + + validate *validator.Validate +} + +// newHandlerWithValidation returns a new instance of the validationHandlerDecorator struct. +func newHandlerWithValidation(handler resources.ServerInterface) *handlerWithValidation { + return &handlerWithValidation{ + ServerInterface: handler, + validate: validator.New(), + } +} + +// requestBodyContextKey is the context key to retrieve the parsed request body struct instance. +type requestBodyContextKey struct{} + +// getRequestBodyFromContext fetches request body from given context. If the value +// was not found in the given context, this will return an error. +func getRequestBodyFromContext(ctx context.Context) (any, error) { + body := ctx.Value(requestBodyContextKey{}) + if body == nil { + return nil, NewMissingRequestBodyError("request body is not available") + } + return body, nil +} + +// newRequestWithBodyInContext sets the given body in a new request instance context +// and returns the new request. +func newRequestWithBodyInContext(r *http.Request, body any) *http.Request { + return r.WithContext(context.WithValue(r.Context(), requestBodyContextKey{}, body)) +} + +// parseRequestBody parses request body as JSON and populates the given body instance. +func parseRequestBody(body any, r *http.Request) error { + defer r.Body.Close() + if err := json.NewDecoder(r.Body).Decode(body); err != nil { + return NewMissingRequestBodyError("request body is not a valid JSON") + } + return nil +} + +// validateRequestBody is a helper method to avoid repetition. It parses +// request body, validates it against the given validator instance and if it's +// okay, will delegate to the provided callback with a new HTTP request instance +// with the parse body in the context. +func (v handlerWithValidation) validateRequestBody(body any, w http.ResponseWriter, r *http.Request, f func(w http.ResponseWriter, r *http.Request)) { + err := parseRequestBody(body, r) + if err != nil { + writeErrorResponse(w, err) + return + } + if err := v.validate.Struct(body); err != nil { + writeErrorResponse(w, NewRequestBodyValidationError(err.Error())) + return + } + f(w, newRequestWithBodyInContext(r, body)) +}