diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml new file mode 100644 index 000000000..ee71975bb --- /dev/null +++ b/.github/workflows/api-tests.yml @@ -0,0 +1,82 @@ +# Copyright (c) Abstract Machines +# SPDX-License-Identifier: Apache-2.0 + +name: Property Based Tests + +on: + push: + branches: + - main + paths: + - ".github/workflows/api-tests.yml" + - "api/**" + - "auth/**" + - "bootstrap/**" + - "certs/**" + - "consumers/**" + - "http/**" + - "invitations/**" + - "provision/**" + - "readers/**" + - "things/**" + - "twins/**" + - "users/**" + pull_request: + branches: + - main + paths: + - ".github/workflows/api-tests.yml" + - "api/**" + - "auth/**" + - "bootstrap/**" + - "certs/**" + - "consumers/**" + - "http/**" + - "invitations/**" + - "provision/**" + - "readers/**" + - "things/**" + - "twins/**" + - "users/**" + +env: + TOKEN_URL: http://localhost:9002/users/tokens/issue + USER_IDENTITY: admin@example.com + USER_SECRET: 12345678 + +jobs: + api-test: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Install Go + uses: actions/setup-go@v4 + with: + go-version: 1.21.x + cache-dependency-path: "go.sum" + + - name: Build images + run: make all -j $(nproc) && make dockers_dev -j $(nproc) + + - name: Start containers + run: make run up args="-d" && sleep 10 + + - name: Set access token + run: | + export USER_TOKEN=$(curl -sSX POST $TOKEN_URL -H "Content-Type: application/json" -d "{\"identity\": \"$USER_IDENTITY\",\"secret\": \"$USER_SECRET\"}" | jq -r .access_token) + echo "USER_TOKEN=$USER_TOKEN" >> $GITHUB_ENV + + - name: Run Users API tests + uses: schemathesis/action@v1 + with: + schema: api/openapi/users.yml + base-url: http://localhost:9002 + checks: all + report: false + args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-unique-data --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links' + + - name: Stop containers + if: always() + run: make run down args="-v" diff --git a/.gitignore b/.gitignore index 8d9ae5778..60286b531 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ tools/provision/mgconn.toml # coverage files coverage + +# Schemathesis +.hypothesis diff --git a/Makefile b/Makefile index 9e70fdf80..5c35d2e1d 100644 --- a/Makefile +++ b/Makefile @@ -103,14 +103,14 @@ FILTERED_SERVICES = $(filter-out $(RUN_ADDON_ARGS), $(SERVICES)) all: $(SERVICES) -.PHONY: all $(SERVICES) dockers dockers_dev latest release run run_addons grpc_mtls_certs check_mtls check_certs +.PHONY: all $(SERVICES) dockers dockers_dev latest release run run_addons grpc_mtls_certs check_mtls check_certs test_api clean: rm -rf ${BUILD_DIR} cleandocker: # Stops containers and removes containers, networks, volumes, and images created by up - docker-compose -f docker/docker-compose.yml --profile $(DOCKER_PROFILE) -p $(DOCKER_PROJECT) down --rmi all -v --remove-orphans + docker compose -f docker/docker-compose.yml --profile $(DOCKER_PROFILE) -p $(DOCKER_PROJECT) down --rmi all -v --remove-orphans ifdef pv # Remove unused volumes @@ -130,6 +130,16 @@ test: done go test -v --race -count 1 -tags test -coverprofile=coverage/coverage.out $$(go list ./... | grep -v 'consumers\|readers\|postgres\|internal\|opcua\|cmd') +test_api: + @which st > /dev/null || (echo "schemathesis not found, please install it from https://github.com/schemathesis/schemathesis#getting-started" && exit 1) + st run --contrib-unique-data --contrib-openapi-formats-uuid \ + --checks all \ + --data-generation-method all \ + --stateful=links \ + --base-url http://localhost:9002 \ + --header "Authorization: Bearer $(USER_TOKEN)" \ + api/openapi/users.yml + proto: protoc -I. --go_out=. --go_opt=paths=source_relative pkg/messaging/*.proto protoc -I. --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative ./*.proto @@ -240,16 +250,16 @@ run: check_certs change_config ifeq ($(MG_ES_TYPE), redis) sed -i "s/MG_ES_TYPE=.*/MG_ES_TYPE=redis/" docker/.env sed -i "s/MG_ES_URL=.*/MG_ES_URL=$$\{MG_REDIS_URL}/" docker/.env - docker-compose -f docker/docker-compose.yml --profile $(DOCKER_PROFILE) --profile redis -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args) + docker compose -f docker/docker-compose.yml --profile $(DOCKER_PROFILE) --profile redis -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args) else sed -i "s,MG_ES_TYPE=.*,MG_ES_TYPE=$$\{MG_MESSAGE_BROKER_TYPE}," docker/.env sed -i "s,MG_ES_URL=.*,MG_ES_URL=$$\{MG_$(shell echo ${MG_MESSAGE_BROKER_TYPE} | tr 'a-z' 'A-Z')_URL\}," docker/.env - docker-compose -f docker/docker-compose.yml --env-file docker/.env --profile $(DOCKER_PROFILE) -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args) + docker compose -f docker/docker-compose.yml --env-file docker/.env --profile $(DOCKER_PROFILE) -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args) endif run_addons: check_certs $(call change_config) $(foreach SVC,$(RUN_ADDON_ARGS),$(if $(filter $(SVC),$(ADDON_SERVICES) $(EXTERNAL_SERVICES)),,$(error Invalid Service $(SVC)))) @for SVC in $(RUN_ADDON_ARGS); do \ - MG_ADDONS_CERTS_PATH_PREFIX="../." docker-compose -f docker/addons/$$SVC/docker-compose.yml -p $(DOCKER_PROJECT) --env-file ./docker/.env $(DOCKER_COMPOSE_COMMAND) $(args) & \ + MG_ADDONS_CERTS_PATH_PREFIX="../." docker compose -f docker/addons/$$SVC/docker-compose.yml -p $(DOCKER_PROJECT) --env-file ./docker/.env $(DOCKER_COMPOSE_COMMAND) $(args) & \ done diff --git a/api/openapi/users.yml b/api/openapi/users.yml index 668c5833d..0c4f6d72f 100644 --- a/api/openapi/users.yml +++ b/api/openapi/users.yml @@ -34,6 +34,7 @@ tags: paths: /users: post: + operationId: createUser tags: - Users summary: Registers user account @@ -49,6 +50,8 @@ paths: description: Failed due to malformed JSON. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "409": description: Failed due to using an existing identity. "415": @@ -57,6 +60,7 @@ paths: $ref: "#/components/responses/ServiceError" get: + operationId: listUsers tags: - Users summary: List users @@ -95,6 +99,7 @@ paths: /users/profile: get: + operationId: getProfile summary: Gets info on currently logged in user. description: | Gets info on currently logged in user. Info is obtained using @@ -115,6 +120,7 @@ paths: /users/{userID}: get: + operationId: getUser summary: Retrieves a user description: | Retrieves a specific user that is identifier by the user ID. @@ -131,6 +137,8 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. "422": @@ -139,6 +147,7 @@ paths: $ref: "#/components/responses/ServiceError" patch: + operationId: updateUser summary: Updates name and metadata of the user. description: | Updates name and metadata of the user with provided ID. Name and metadata @@ -156,15 +165,22 @@ paths: $ref: "#/components/responses/UserRes" "400": description: Failed due to malformed JSON. + "403": + description: Failed to perform authorization over the entity. "404": description: Failed due to non existing user. "401": description: Missing or invalid access token provided. + "415": + description: Missing or invalid content type. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" /users/{userID}/tags: patch: + operationId: updateUserTags summary: Updates tags the user. description: | Updates tags of the user with provided ID. Tags is updated using @@ -182,15 +198,22 @@ paths: $ref: "#/components/responses/UserRes" "400": description: Failed due to malformed JSON. + "403": + description: Failed to perform authorization over the entity. "404": description: Failed due to non existing user. "401": description: Missing or invalid access token provided. + "415": + description: Missing or invalid content type. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" /users/{userID}/identity: patch: + operationId: updateUserIdentity summary: Updates Identity of the user. description: | Updates identity of the user with provided ID. Identity is @@ -208,15 +231,22 @@ paths: $ref: "#/components/responses/UserRes" "400": description: Failed due to malformed JSON. + "403": + description: Failed to perform authorization over the entity. "404": description: Failed due to non existing user. "401": description: Missing or invalid access token provided. + "415": + description: Missing or invalid content type. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" /users/{userID}/role: patch: + operationId: updateUserRole summary: Updates the user role. description: | Updates role for the user with provided ID. @@ -233,15 +263,22 @@ paths: $ref: "#/components/responses/UserRes" "400": description: Failed due to malformed JSON. + "403": + description: Failed to perform authorization over the entity. "404": description: Failed due to non existing user. "401": description: Missing or invalid access token provided. + "415": + description: Missing or invalid content type. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" /users/{userID}/disable: post: + operationId: disableUser summary: Disables a user description: | Disables a specific user that is identifier by the user ID. @@ -258,8 +295,14 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. + "409": + description: Failed due to already disabled user. + "415": + description: Missing or invalid content type. "422": description: Database can't process request. "500": @@ -267,6 +310,7 @@ paths: /users/{userID}/enable: post: + operationId: enableUser summary: Enables a user description: | Enables a specific user that is identifier by the user ID. @@ -283,8 +327,14 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. + "409": + description: Failed due to already enabled user. + "415": + description: Missing or invalid content type. "422": description: Database can't process request. "500": @@ -292,6 +342,7 @@ paths: /users/secret: patch: + operationId: updateUserSecret summary: Updates Secret of currently logged in user. description: | Updates secret of currently logged in user. Secret is updated using @@ -307,15 +358,18 @@ paths: $ref: "#/components/responses/UserRes" "400": description: Failed due to malformed JSON. - "404": - description: Failed due to non existing user. "401": description: Missing or invalid access token provided. + "404": + description: Failed due to non existing user. + "415": + description: Missing or invalid content type. "500": $ref: "#/components/responses/ServiceError" /password/reset-request: post: + operationId: requestPasswordReset summary: User password reset request description: | Generates a reset token and sends and @@ -338,6 +392,7 @@ paths: /password/reset: put: + operationId: resetPassword summary: User password reset endpoint description: | When user gets reset token, after he submitted @@ -352,6 +407,8 @@ paths: description: User link . "400": description: Failed due to malformed JSON. + "401": + description: Missing or invalid access token provided. "415": description: Missing or invalid content type. "500": @@ -359,6 +416,7 @@ paths: /groups/{groupID}/users: get: + operationId: listUsersInGroup tags: - Users summary: List users in a group @@ -386,6 +444,8 @@ paths: description: | Missing or invalid access token provided. This endpoint is available only for administrators. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. "422": @@ -395,6 +455,7 @@ paths: /channels/{channelID}/users: get: + operationId: listUsersInChannel tags: - Users summary: List users in a channel @@ -422,6 +483,8 @@ paths: description: | Missing or invalid access token provided. This endpoint is available only for administrators. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. "422": @@ -431,6 +494,7 @@ paths: /users/tokens/issue: post: + operationId: issueToken summary: Issue Token description: | Issue Access and Refresh Token used for authenticating into the system. @@ -441,8 +505,12 @@ paths: responses: "200": $ref: "#/components/responses/TokenRes" + "400": + description: Failed due to malformed JSON. "404": description: A non-existent entity request. + "415": + description: Missing or invalid content type. "422": description: Database can't process request. "500": @@ -450,6 +518,7 @@ paths: /users/tokens/refresh: post: + operationId: refreshToken summary: Refresh Token description: | Refreshes Access and Refresh Token used for authenticating into the system. @@ -460,8 +529,12 @@ paths: responses: "200": $ref: "#/components/responses/TokenRes" + "400": + description: Failed due to malformed JSON. "404": description: A non-existent entity request. + "415": + description: Missing or invalid content type. "422": description: Database can't process request. "500": @@ -469,6 +542,7 @@ paths: /groups: post: + operationId: createGroup tags: - Groups summary: Creates new group @@ -486,6 +560,8 @@ paths: description: Failed due to malformed JSON. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "409": description: Failed due to using an existing identity. "415": @@ -494,6 +570,7 @@ paths: $ref: "#/components/responses/ServiceError" get: + operationId: listGroups summary: Lists groups. description: | Lists groups up to a max level of hierarchy that can be fetched in one @@ -520,6 +597,8 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: Group does not exist. "500": @@ -527,6 +606,7 @@ paths: /groups/{groupID}: get: + operationId: getGroup summary: Gets group info. description: | Gets info on a group specified by id. @@ -543,12 +623,15 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: Group does not exist. "500": $ref: "#/components/responses/ServiceError" put: + operationId: updateGroup summary: Updates group data. description: | Updates Name, Description or Metadata of a group. @@ -567,13 +650,18 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: Group does not exist. + "415": + description: Missing or invalid content type. "500": $ref: "#/components/responses/ServiceError" /groups/{groupID}/children: get: + operationId: listChildren summary: List children of a certain group description: | Lists groups up to a max level of hierarchy that can be fetched in one @@ -601,6 +689,8 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: Group does not exist. "500": @@ -608,6 +698,7 @@ paths: /groups/{groupID}/parents: get: + operationId: listParents summary: List parents of a certain group description: | Lists groups up to a max level of hierarchy that can be fetched in one @@ -635,6 +726,8 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: Group does not exist. "500": @@ -642,6 +735,7 @@ paths: /groups/{groupID}/enable: post: + operationId: enableGroup summary: Enables a group description: | Enables a specific group that is identifier by the group ID. @@ -658,8 +752,14 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. + "409": + description: Failed due to already enabled group. + "415": + description: Missing or invalid content type. "422": description: Database can't process request. "500": @@ -667,6 +767,7 @@ paths: /groups/{groupID}/disable: post: + operationId: disableGroup summary: Disables a group description: | Disables a specific group that is identifier by the group ID. @@ -683,8 +784,14 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. + "409": + description: Failed due to already disabled group. + "415": + description: Missing or invalid content type. "422": description: Database can't process request. "500": @@ -692,6 +799,7 @@ paths: /groups/{groupID}/users/assign: post: + operationId: assignUser summary: Assigns a user to a group description: | Assigns a specific user to a group that is identifier by the group ID. @@ -710,8 +818,12 @@ paths: description: Failed due to malformed group's ID. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. + "415": + description: Missing or invalid content type. "422": description: Database can't process request. "500": @@ -719,6 +831,7 @@ paths: /groups/{groupID}/users/unassign: post: + operationId: unassignUser summary: Unassigns a user to a group description: | Unassigns a specific user to a group that is identifier by the group ID. @@ -737,8 +850,12 @@ paths: description: Failed due to malformed group's ID. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. + "415": + description: Missing or invalid content type. "422": description: Database can't process request. "500": @@ -746,6 +863,7 @@ paths: /channels/{memberID}/groups: get: + operationId: listGroupsInChannel summary: Get group associated with the member description: | Gets groups associated with the channel member specified by id. @@ -767,6 +885,8 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: Group does not exist. "500": @@ -774,6 +894,7 @@ paths: /users/{memberID}/groups: get: + operationId: listGroupsByUser summary: Get group associated with the member description: | Gets groups associated with the user member specified by id. @@ -795,6 +916,8 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: Group does not exist. "500": @@ -802,6 +925,7 @@ paths: /health: get: + operationId: health summary: Retrieves service health check info. tags: - health @@ -1157,7 +1281,7 @@ components: required: - groups - total - - level + - offset MembershipsPage: type: object @@ -1266,7 +1390,7 @@ components: properties: role: type: string - enum: ["admin","user"] + enum: ["admin", "user"] example: user description: User role example. required: @@ -1405,6 +1529,9 @@ components: schema: type: string format: uuid + minLength: 36 + maxLength: 36 + pattern: "^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" required: true example: bb7edb32-2eac-4aad-aebe-ed96fe073879 @@ -1432,6 +1559,7 @@ components: in: query schema: type: string + pattern: "^[^\u0000-\u001F]*$" required: false example: "admin@example.com" @@ -1521,6 +1649,9 @@ components: schema: type: string format: uuid + minLength: 36 + maxLength: 36 + pattern: "^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" required: true example: bb7edb32-2eac-4aad-aebe-ed96fe073879 @@ -1531,6 +1662,9 @@ components: schema: type: string format: uuid + minLength: 36 + maxLength: 36 + pattern: "^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" required: true example: bb7edb32-2eac-4aad-aebe-ed96fe073879 @@ -1541,6 +1675,9 @@ components: schema: type: string format: uuid + minLength: 36 + maxLength: 36 + pattern: "^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" required: true example: bb7edb32-2eac-4aad-aebe-ed96fe073879 @@ -1769,6 +1906,43 @@ components: application/json: schema: $ref: "#/components/schemas/User" + links: + get: + operationId: getUser + parameters: + userID: $response.body#/id + get_groups: + operationId: listUsersInGroup + parameters: + groupID: $response.body#/id + get_channels: + operationId: listUsersInChannel + parameters: + channelID: $response.body#/id + update: + operationId: updateUser + parameters: + userID: $response.body#/id + update_tags: + operationId: updateUserTags + parameters: + userID: $response.body#/id + update_identity: + operationId: updateUserIdentity + parameters: + userID: $response.body#/id + update_role: + operationId: updateUserRole + parameters: + userID: $response.body#/id + disable: + operationId: disableUser + parameters: + userID: $response.body#/id + enable: + operationId: enableUser + parameters: + userID: $response.body#/id UserRes: description: Data retrieved. @@ -1776,6 +1950,23 @@ components: application/json: schema: $ref: "#/components/schemas/User" + links: + get_groups: + operationId: listUsersInGroup + parameters: + groupID: $response.body#/id + get_channels: + operationId: listUsersInChannel + parameters: + channelID: $response.body#/id + disable: + operationId: disableUser + parameters: + userID: $response.body#/id + enable: + operationId: enableUser + parameters: + userID: $response.body#/id UserPageRes: description: Data retrieved. @@ -1803,6 +1994,47 @@ components: application/json: schema: $ref: "#/components/schemas/Group" + links: + get: + operationId: getGroup + parameters: + groupID: $response.body#/id + get_children: + operationId: listGroups + parameters: + groupID: $response.body#/id + get_parent: + operationId: getGroup + parameters: + groupID: $response.body#/id + get_channels: + operationId: listGroupsInChannel + parameters: + memberID: $response.body#/id + get_users: + operationId: listGroupsByUser + parameters: + memberID: $response.body#/id + update: + operationId: updateGroup + parameters: + groupID: $response.body#/id + disable: + operationId: disableGroup + parameters: + groupID: $response.body#/id + enable: + operationId: enableGroup + parameters: + groupID: $response.body#/id + assign: + operationId: assignUser + parameters: + groupID: $response.body#/id + unassign: + operationId: unassignUser + parameters: + groupID: $response.body#/id GroupRes: description: Data retrieved. @@ -1810,6 +2042,39 @@ components: application/json: schema: $ref: "#/components/schemas/Group" + links: + get_children: + operationId: listGroups + parameters: + groupID: $response.body#/id + get_parent: + operationId: getGroup + parameters: + groupID: $response.body#/id + get_channels: + operationId: listGroupsInChannel + parameters: + memberID: $response.body#/id + get_users: + operationId: listGroupsByUser + parameters: + memberID: $response.body#/id + disable: + operationId: disableGroup + parameters: + groupID: $response.body#/id + enable: + operationId: enableGroup + parameters: + groupID: $response.body#/id + assign: + operationId: assignUser + parameters: + groupID: $response.body#/id + unassign: + operationId: unassignUser + parameters: + groupID: $response.body#/id GroupPageRes: description: Data retrieved. @@ -1844,6 +2109,11 @@ components: type: string example: access description: User access token type. + links: + refresh: + operationId: refreshToken + parameters: + refresh_token: $response.body#/refresh_token HealthRes: description: Service Health Check. diff --git a/auth/api/grpc/server.go b/auth/api/grpc/server.go index 5c36468f3..a4464673b 100644 --- a/auth/api/grpc/server.go +++ b/auth/api/grpc/server.go @@ -542,7 +542,8 @@ func encodeError(err error) error { err == apiutil.ErrMissingEmail, err == apiutil.ErrBearerToken: return status.Error(codes.Unauthenticated, err.Error()) - case errors.Contains(err, errors.ErrAuthorization): + case errors.Contains(err, errors.ErrAuthorization), + errors.Contains(err, errors.ErrDomainAuthorization): return status.Error(codes.PermissionDenied, err.Error()) default: return status.Error(codes.Internal, err.Error()) diff --git a/internal/api/common.go b/internal/api/common.go index 1041fdd61..60bbe8fdb 100644 --- a/internal/api/common.go +++ b/internal/api/common.go @@ -13,6 +13,7 @@ import ( "github.com/absmach/magistrala/internal/postgres" mgclients "github.com/absmach/magistrala/pkg/clients" "github.com/absmach/magistrala/pkg/errors" + repoerr "github.com/absmach/magistrala/pkg/errors/repository" svcerr "github.com/absmach/magistrala/pkg/errors/service" "github.com/gofrs/uuid" ) @@ -113,16 +114,32 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) { errors.Contains(err, apiutil.ErrMissingMemberType), errors.Contains(err, apiutil.ErrMissingMemberKind), errors.Contains(err, apiutil.ErrLimitSize), - errors.Contains(err, apiutil.ErrNameSize): + errors.Contains(err, apiutil.ErrNameSize), + errors.Contains(err, apiutil.ErrInvalidStatus), + errors.Contains(err, apiutil.ErrInvalidRole), + errors.Contains(err, apiutil.ErrMissingEmail), + errors.Contains(err, apiutil.ErrMissingHost), + errors.Contains(err, apiutil.ErrMissingIdentity), + errors.Contains(err, apiutil.ErrMissingSecret), + errors.Contains(err, apiutil.ErrMissingPass), + errors.Contains(err, apiutil.ErrMissingConfPass), + errors.Contains(err, apiutil.ErrInvalidResetPass), + errors.Contains(err, apiutil.ErrMissingRelation), + errors.Contains(err, errors.ErrPasswordFormat), + errors.Contains(err, apiutil.ErrInvalidLevel), + errors.Contains(err, apiutil.ErrInvalidQueryParams): w.WriteHeader(http.StatusBadRequest) case errors.Contains(err, svcerr.ErrAuthentication), + errors.Contains(err, errors.ErrLogin), errors.Contains(err, apiutil.ErrBearerToken): w.WriteHeader(http.StatusUnauthorized) case errors.Contains(err, svcerr.ErrNotFound): w.WriteHeader(http.StatusNotFound) - case errors.Contains(err, svcerr.ErrConflict): + case errors.Contains(err, svcerr.ErrConflict), + errors.Contains(err, errors.ErrStatusAlreadyAssigned): w.WriteHeader(http.StatusConflict) - case errors.Contains(err, svcerr.ErrAuthorization): + case errors.Contains(err, svcerr.ErrAuthorization), + errors.Contains(err, errors.ErrDomainAuthorization): w.WriteHeader(http.StatusForbidden) case errors.Contains(err, postgres.ErrMemberAlreadyAssigned): w.WriteHeader(http.StatusConflict) @@ -131,8 +148,10 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) { case errors.Contains(err, svcerr.ErrCreateEntity), errors.Contains(err, svcerr.ErrUpdateEntity), errors.Contains(err, svcerr.ErrViewEntity), + errors.Contains(err, repoerr.ErrAddPolicies), + errors.Contains(err, repoerr.ErrDeletePolicies), errors.Contains(err, svcerr.ErrRemoveEntity): - w.WriteHeader(http.StatusInternalServerError) + w.WriteHeader(http.StatusUnprocessableEntity) default: w.WriteHeader(http.StatusInternalServerError) } diff --git a/internal/apiutil/errors.go b/internal/apiutil/errors.go index 25eb3fad5..927216cfc 100644 --- a/internal/apiutil/errors.go +++ b/internal/apiutil/errors.go @@ -114,12 +114,12 @@ var ( // ErrMissingRelation indicates missing relation. ErrMissingRelation = errors.New("missing relation") + // ErrInvalidRelation indicates an invalid relation. + ErrInvalidRelation = errors.New("invalid relation") + // ErrInvalidAPIKey indicates an invalid API key type. ErrInvalidAPIKey = errors.New("invalid api key type") - // ErrMaxLevelExceeded indicates an invalid group level. - ErrMaxLevelExceeded = errors.New("invalid group level (should be lower than 5)") - // ErrBootstrapState indicates an invalid boostrap state. ErrBootstrapState = errors.New("invalid bootstrap state") diff --git a/internal/apiutil/transport.go b/internal/apiutil/transport.go index a97179088..fa8402262 100644 --- a/internal/apiutil/transport.go +++ b/internal/apiutil/transport.go @@ -135,16 +135,28 @@ func ReadNumQuery[N number](r *http.Request, key string, def N) (N, error) { switch any(def).(type) { case int64: v, err := strconv.ParseInt(val, 10, 64) - return N(v), err + if err != nil { + return 0, errors.Wrap(ErrInvalidQueryParams, err) + } + return N(v), nil case uint64: v, err := strconv.ParseUint(val, 10, 64) - return N(v), err + if err != nil { + return 0, errors.Wrap(ErrInvalidQueryParams, err) + } + return N(v), nil case uint16: v, err := strconv.ParseUint(val, 10, 16) - return N(v), err + if err != nil { + return 0, errors.Wrap(ErrInvalidQueryParams, err) + } + return N(v), nil case float64: v, err := strconv.ParseFloat(val, 64) - return N(v), err + if err != nil { + return 0, errors.Wrap(ErrInvalidQueryParams, err) + } + return N(v), nil default: return def, nil } diff --git a/internal/groups/api/responses.go b/internal/groups/api/responses.go index fcf941d79..4e45d7a26 100644 --- a/internal/groups/api/responses.go +++ b/internal/groups/api/responses.go @@ -121,9 +121,9 @@ type groupPageRes struct { } type pageRes struct { - Limit uint64 `json:"limit"` + Limit uint64 `json:"limit,omitempty"` Offset uint64 `json:"offset"` - Total uint64 `json:"total,omitempty"` + Total uint64 `json:"total"` Level uint64 `json:"level,omitempty"` } diff --git a/internal/groups/postgres/groups.go b/internal/groups/postgres/groups.go index 13ad242d7..15c09d551 100644 --- a/internal/groups/postgres/groups.go +++ b/internal/groups/postgres/groups.go @@ -215,8 +215,6 @@ func (repo groupRepository) RetrieveByIDs(ctx context.Context, gm mggroups.Page, } q = fmt.Sprintf("%s %s ORDER BY g.updated_at LIMIT :limit OFFSET :offset;", q, query) - fmt.Println(q) - fmt.Printf("%+v\n", gm) dbPage, err := toDBGroupPage(gm) if err != nil { return mggroups.Page{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) diff --git a/internal/groups/service.go b/internal/groups/service.go index e258309fd..a341fd143 100644 --- a/internal/groups/service.go +++ b/internal/groups/service.go @@ -13,6 +13,8 @@ import ( "github.com/absmach/magistrala/internal/apiutil" mgclients "github.com/absmach/magistrala/pkg/clients" "github.com/absmach/magistrala/pkg/errors" + repoerr "github.com/absmach/magistrala/pkg/errors/repository" + svcerr "github.com/absmach/magistrala/pkg/errors/service" "github.com/absmach/magistrala/pkg/groups" "golang.org/x/sync/errgroup" ) @@ -20,8 +22,6 @@ import ( var ( errParentUnAuthz = errors.New("failed to authorize parent group") errMemberKind = errors.New("invalid member kind") - errAddPolicies = errors.New("failed to add policies") - errDeletePolicies = errors.New("failed to delete policies") errRetrieveGroups = errors.New("failed to retrieve groups") errGroupIDs = errors.New("invalid group ids") ) @@ -284,10 +284,10 @@ func (svc service) checkSuperAdmin(ctx context.Context, userID string) error { Object: auth.MagistralaObject, }) if err != nil { - return err + return errors.Wrap(svcerr.ErrAuthorization, err) } if !res.Authorized { - return errors.ErrAuthorization + return svcerr.ErrAuthorization } return nil } @@ -445,7 +445,7 @@ func (svc service) Assign(ctx context.Context, token, groupID, relation, memberK } if _, err := svc.auth.AddPolicies(ctx, &policies); err != nil { - return errors.Wrap(errAddPolicies, err) + return errors.Wrap(repoerr.ErrAddPolicies, err) } return nil @@ -596,7 +596,7 @@ func (svc service) Unassign(ctx context.Context, token, groupID, relation, membe } if _, err := svc.auth.DeletePolicies(ctx, &policies); err != nil { - return errors.Wrap(errDeletePolicies, err) + return errors.Wrap(repoerr.ErrDeletePolicies, err) } return nil } @@ -641,7 +641,7 @@ func (svc service) changeGroupStatus(ctx context.Context, token string, group gr return groups.Group{}, err } if dbGroup.Status == group.Status { - return groups.Group{}, mgclients.ErrStatusAlreadyAssigned + return groups.Group{}, errors.ErrStatusAlreadyAssigned } group.UpdatedBy = id @@ -670,10 +670,10 @@ func (svc service) authorize(ctx context.Context, subjectType, subject, permissi } res, err := svc.auth.Authorize(ctx, req) if err != nil { - return "", err + return "", errors.Wrap(svcerr.ErrAuthorization, err) } if !res.GetAuthorized() { - return "", errors.ErrAuthorization + return "", svcerr.ErrAuthorization } return res.GetId(), nil } @@ -689,10 +689,10 @@ func (svc service) authorizeKind(ctx context.Context, subjectType, subjectKind, } res, err := svc.auth.Authorize(ctx, req) if err != nil { - return "", err + return "", errors.Wrap(svcerr.ErrAuthorization, err) } if !res.GetAuthorized() { - return "", errors.ErrAuthorization + return "", svcerr.ErrAuthorization } return res.GetId(), nil } diff --git a/invitations/api/endpoint_test.go b/invitations/api/endpoint_test.go index 2b35759e5..7088c8133 100644 --- a/invitations/api/endpoint_test.go +++ b/invitations/api/endpoint_test.go @@ -170,7 +170,7 @@ func TestListInvitation(t *testing.T) { desc: "with invalid offset", token: validToken, query: "offset=invalid", - status: http.StatusInternalServerError, + status: http.StatusBadRequest, contentType: validContenType, svcErr: nil, }, @@ -186,7 +186,7 @@ func TestListInvitation(t *testing.T) { desc: "with invalid limit", token: validToken, query: "limit=invalid", - status: http.StatusInternalServerError, + status: http.StatusBadRequest, contentType: validContenType, svcErr: nil, }, @@ -202,7 +202,7 @@ func TestListInvitation(t *testing.T) { desc: "with duplicate user_id", token: validToken, query: "user_id=1&user_id=2", - status: http.StatusInternalServerError, + status: http.StatusBadRequest, contentType: validContenType, svcErr: nil, }, @@ -218,7 +218,7 @@ func TestListInvitation(t *testing.T) { desc: "with duplicate invited_by", token: validToken, query: "invited_by=1&invited_by=2", - status: http.StatusInternalServerError, + status: http.StatusBadRequest, contentType: validContenType, svcErr: nil, }, @@ -234,7 +234,7 @@ func TestListInvitation(t *testing.T) { desc: "with duplicate relation", token: validToken, query: "relation=1&relation=2", - status: http.StatusInternalServerError, + status: http.StatusBadRequest, contentType: validContenType, svcErr: nil, }, @@ -250,7 +250,7 @@ func TestListInvitation(t *testing.T) { desc: "with duplicate domain_id", token: validToken, query: "domain_id=1&domain_id=2", - status: http.StatusInternalServerError, + status: http.StatusBadRequest, contentType: validContenType, svcErr: nil, }, diff --git a/invitations/api/requests_test.go b/invitations/api/requests_test.go index 1f1192a92..a2e22660a 100644 --- a/invitations/api/requests_test.go +++ b/invitations/api/requests_test.go @@ -4,7 +4,6 @@ package api import ( - "errors" "fmt" "testing" @@ -14,11 +13,7 @@ import ( "github.com/stretchr/testify/assert" ) -var ( - errMissingRelation = errors.New("missing relation") - errInvalidRelation = errors.New("invalid relation") - valid = "valid" -) +var valid = "valid" func TestSendInvitationReqValidation(t *testing.T) { cases := []struct { @@ -79,7 +74,7 @@ func TestSendInvitationReqValidation(t *testing.T) { Relation: "", Resend: true, }, - err: errMissingRelation, + err: apiutil.ErrMissingRelation, }, { desc: "invalid relation", @@ -90,7 +85,7 @@ func TestSendInvitationReqValidation(t *testing.T) { Relation: "invalid", Resend: true, }, - err: errInvalidRelation, + err: apiutil.ErrInvalidRelation, }, } diff --git a/invitations/invitations.go b/invitations/invitations.go index 2bd150171..c33cc1f19 100644 --- a/invitations/invitations.go +++ b/invitations/invitations.go @@ -6,15 +6,10 @@ package invitations import ( "context" "encoding/json" - "errors" "time" "github.com/absmach/magistrala/auth" -) - -var ( - errMissingRelation = errors.New("missing relation") - errInvalidRelation = errors.New("invalid relation") + "github.com/absmach/magistrala/internal/apiutil" ) // Invitation is an invitation to join a domain. @@ -121,7 +116,7 @@ type Repository interface { // It returns an error if the relation is empty or invalid. func CheckRelation(relation string) error { if relation == "" { - return errMissingRelation + return apiutil.ErrMissingRelation } if relation != auth.AdministratorRelation && relation != auth.EditorRelation && @@ -132,7 +127,7 @@ func CheckRelation(relation string) error { relation != auth.RoleGroupRelation && relation != auth.GroupRelation && relation != auth.PlatformRelation { - return errInvalidRelation + return apiutil.ErrInvalidRelation } return nil diff --git a/invitations/invitations_test.go b/invitations/invitations_test.go index 942eaec76..2c367d7a7 100644 --- a/invitations/invitations_test.go +++ b/invitations/invitations_test.go @@ -4,19 +4,14 @@ package invitations_test import ( - "errors" "fmt" "testing" + "github.com/absmach/magistrala/internal/apiutil" "github.com/absmach/magistrala/invitations" "github.com/stretchr/testify/assert" ) -var ( - errMissingRelation = errors.New("missing relation") - errInvalidRelation = errors.New("invalid relation") -) - func TestInvitation_MarshalJSON(t *testing.T) { cases := []struct { desc string @@ -60,8 +55,8 @@ func TestCheckRelation(t *testing.T) { relation string err error }{ - {"", errMissingRelation}, - {"admin", errInvalidRelation}, + {"", apiutil.ErrMissingRelation}, + {"admin", apiutil.ErrInvalidRelation}, {"editor", nil}, {"viewer", nil}, {"member", nil}, diff --git a/invitations/service_test.go b/invitations/service_test.go index c1c1da521..ee6c3215d 100644 --- a/invitations/service_test.go +++ b/invitations/service_test.go @@ -5,7 +5,6 @@ package invitations_test import ( "context" - "errors" "math/rand" "testing" "time" @@ -13,6 +12,7 @@ import ( "github.com/absmach/magistrala" "github.com/absmach/magistrala/auth" authmocks "github.com/absmach/magistrala/auth/mocks" + "github.com/absmach/magistrala/internal/apiutil" "github.com/absmach/magistrala/internal/testsutil" "github.com/absmach/magistrala/invitations" "github.com/absmach/magistrala/invitations/mocks" @@ -79,7 +79,7 @@ func TestSendInvitation(t *testing.T) { token: validToken, tokenUserID: testsutil.GenerateUUID(t), req: invitations.Invitation{Relation: "invalid"}, - err: errors.New("invalid relation"), + err: apiutil.ErrInvalidRelation, authNErr: nil, domainErr: nil, adminErr: nil, diff --git a/pkg/clients/status.go b/pkg/clients/status.go index 9d4c59e2f..82a716d51 100644 --- a/pkg/clients/status.go +++ b/pkg/clients/status.go @@ -5,7 +5,6 @@ package clients import ( "encoding/json" - "errors" "strings" "github.com/absmach/magistrala/internal/apiutil" @@ -36,9 +35,6 @@ const ( Unknown = "unknown" ) -// ErrStatusAlreadyAssigned indicated that the client or group has already been assigned the status. -var ErrStatusAlreadyAssigned = errors.New("status already assigned") - // String converts client/group status to string literal. func (s Status) String() string { switch s { diff --git a/pkg/errors/repository/types.go b/pkg/errors/repository/types.go index 2637d79b3..56e107427 100644 --- a/pkg/errors/repository/types.go +++ b/pkg/errors/repository/types.go @@ -48,4 +48,10 @@ var ( // ErrInvalidSecret indicates invalid secret. ErrInvalidSecret = errors.New("missing secret") + + // ErrAddPolicies indicates failed to add policies. + ErrAddPolicies = errors.New("failed to add policies") + + // ErrDeletePolicies indicates failed to delete policies. + ErrDeletePolicies = errors.New("failed to delete policies") ) diff --git a/pkg/errors/service/types.go b/pkg/errors/service/types.go index 538e369bd..29e28f79a 100644 --- a/pkg/errors/service/types.go +++ b/pkg/errors/service/types.go @@ -42,4 +42,13 @@ var ( // ErrInvalidRole indicates that an invalid role. ErrInvalidRole = errors.New("invalid client role") + + // ErrRecoveryToken indicates error in generating password recovery token. + ErrRecoveryToken = errors.New("failed to generate password recovery token") + + // ErrFailedPolicyUpdate indicates a failure to update user policy. + ErrFailedPolicyUpdate = errors.New("failed to update user policy") + + // ErrFailedOwnerUpdate indicates a failure to update user policy. + ErrFailedOwnerUpdate = errors.New("failed to update user owner") ) diff --git a/pkg/errors/types.go b/pkg/errors/types.go index 74adc6ad4..b5bd7ffda 100644 --- a/pkg/errors/types.go +++ b/pkg/errors/types.go @@ -45,6 +45,12 @@ var ( // ErrLogin indicates wrong login credentials. ErrLogin = New("invalid user id or secret") + // ErrPasswordFormat indicates weak password. + ErrPasswordFormat = errors.New("password does not meet the requirements") + // ErrUnsupportedContentType indicates invalid content type. ErrUnsupportedContentType = errors.New("invalid content type") + + // ErrStatusAlreadyAssigned indicated that the client or group has already been assigned the status. + ErrStatusAlreadyAssigned = errors.New("status already assigned") ) diff --git a/pkg/groups/errors.go b/pkg/groups/errors.go index 24b1a29d9..b6665fa0b 100644 --- a/pkg/groups/errors.go +++ b/pkg/groups/errors.go @@ -14,7 +14,4 @@ var ( // ErrDisableGroup indicates error in disabling group. ErrDisableGroup = errors.New("failed to disable group") - - // ErrStatusAlreadyAssigned indicated that the group has already been assigned the status. - ErrStatusAlreadyAssigned = errors.New("status already assigned") ) diff --git a/pkg/sdk/go/channels_test.go b/pkg/sdk/go/channels_test.go index 215631879..263ee6ae7 100644 --- a/pkg/sdk/go/channels_test.go +++ b/pkg/sdk/go/channels_test.go @@ -109,7 +109,7 @@ func TestCreateChannel(t *testing.T) { Status: mgclients.EnabledStatus.String(), }, token: token, - err: errors.NewSDKErrorWithStatus(errors.ErrCreateEntity, http.StatusInternalServerError), + err: errors.NewSDKErrorWithStatus(errors.ErrCreateEntity, http.StatusUnprocessableEntity), }, { desc: "create channel with invalid owner", @@ -119,7 +119,7 @@ func TestCreateChannel(t *testing.T) { Status: mgclients.EnabledStatus.String(), }, token: token, - err: errors.NewSDKErrorWithStatus(sdk.ErrFailedCreation, http.StatusInternalServerError), + err: errors.NewSDKErrorWithStatus(sdk.ErrFailedCreation, http.StatusUnprocessableEntity), }, { desc: "create channel with missing name", @@ -475,7 +475,7 @@ func TestUpdateChannel(t *testing.T) { }, response: sdk.Channel{}, token: invalidToken, - err: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden), + err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrAuthorization, svcerr.ErrAuthorization), http.StatusForbidden), }, { desc: "update channel description with invalid token", @@ -485,7 +485,7 @@ func TestUpdateChannel(t *testing.T) { }, response: sdk.Channel{}, token: invalidToken, - err: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden), + err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrAuthorization, svcerr.ErrAuthorization), http.StatusForbidden), }, { desc: "update channel metadata with invalid token", @@ -497,7 +497,7 @@ func TestUpdateChannel(t *testing.T) { }, response: sdk.Channel{}, token: invalidToken, - err: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden), + err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrAuthorization, svcerr.ErrAuthorization), http.StatusForbidden), }, { desc: "update channel that can't be marshalled", diff --git a/pkg/sdk/go/groups_test.go b/pkg/sdk/go/groups_test.go index 4b3776714..d278e2a66 100644 --- a/pkg/sdk/go/groups_test.go +++ b/pkg/sdk/go/groups_test.go @@ -94,7 +94,7 @@ func TestCreateGroup(t *testing.T) { ParentID: mocks.WrongID, Status: clients.EnabledStatus.String(), }, - err: errors.NewSDKErrorWithStatus(svcerr.ErrCreateEntity, http.StatusInternalServerError), + err: errors.NewSDKErrorWithStatus(svcerr.ErrCreateEntity, http.StatusUnprocessableEntity), }, { desc: "create group with invalid owner", @@ -104,7 +104,7 @@ func TestCreateGroup(t *testing.T) { OwnerID: mocks.WrongID, Status: clients.EnabledStatus.String(), }, - err: errors.NewSDKErrorWithStatus(sdk.ErrFailedCreation, http.StatusInternalServerError), + err: errors.NewSDKErrorWithStatus(sdk.ErrFailedCreation, http.StatusUnprocessableEntity), }, { desc: "create group with missing name", @@ -731,7 +731,7 @@ func TestUpdateGroup(t *testing.T) { }, response: sdk.Group{}, token: invalidToken, - err: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden), + err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrAuthorization, svcerr.ErrAuthorization), http.StatusForbidden), }, { desc: "update group description with invalid token", @@ -741,7 +741,7 @@ func TestUpdateGroup(t *testing.T) { }, response: sdk.Group{}, token: invalidToken, - err: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden), + err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrAuthorization, svcerr.ErrAuthorization), http.StatusForbidden), }, { desc: "update group metadata with invalid token", @@ -753,7 +753,7 @@ func TestUpdateGroup(t *testing.T) { }, response: sdk.Group{}, token: invalidToken, - err: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden), + err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrAuthorization, svcerr.ErrAuthorization), http.StatusForbidden), }, { desc: "update a group that can't be marshalled", diff --git a/pkg/sdk/go/things_test.go b/pkg/sdk/go/things_test.go index c3bd7a973..dc158d98f 100644 --- a/pkg/sdk/go/things_test.go +++ b/pkg/sdk/go/things_test.go @@ -81,7 +81,7 @@ func TestCreateThing(t *testing.T) { response: sdk.Thing{}, token: token, repoErr: sdk.ErrFailedCreation, - err: errors.NewSDKErrorWithStatus(errors.Wrap(sdk.ErrFailedCreation, repoerr.ErrCreateEntity), http.StatusInternalServerError), + err: errors.NewSDKErrorWithStatus(errors.Wrap(sdk.ErrFailedCreation, repoerr.ErrCreateEntity), http.StatusUnprocessableEntity), }, { desc: "register empty thing", @@ -231,7 +231,7 @@ func TestCreateThings(t *testing.T) { things: thingsList, response: []sdk.Thing{}, token: token, - err: errors.NewSDKErrorWithStatus(errors.Wrap(sdk.ErrFailedCreation, sdk.ErrFailedCreation), http.StatusInternalServerError), + err: errors.NewSDKErrorWithStatus(errors.Wrap(sdk.ErrFailedCreation, sdk.ErrFailedCreation), http.StatusUnprocessableEntity), }, { desc: "register empty things", diff --git a/pkg/sdk/go/tokens_test.go b/pkg/sdk/go/tokens_test.go index 593d86e7b..4c194cd83 100644 --- a/pkg/sdk/go/tokens_test.go +++ b/pkg/sdk/go/tokens_test.go @@ -62,7 +62,7 @@ func TestIssueToken(t *testing.T) { desc: "issue token for an empty user", login: sdk.Login{}, token: &magistrala.Token{}, - err: errors.NewSDKErrorWithStatus(errors.Wrap(apiutil.ErrValidation, apiutil.ErrMissingIdentity), http.StatusInternalServerError), + err: errors.NewSDKErrorWithStatus(errors.Wrap(apiutil.ErrValidation, apiutil.ErrMissingIdentity), http.StatusBadRequest), }, { desc: "issue token for invalid identity", diff --git a/pkg/sdk/go/users_test.go b/pkg/sdk/go/users_test.go index a138ddef8..e46995094 100644 --- a/pkg/sdk/go/users_test.go +++ b/pkg/sdk/go/users_test.go @@ -85,7 +85,7 @@ func TestCreateClient(t *testing.T) { client: user, response: sdk.User{}, token: token, - err: errors.NewSDKErrorWithStatus(errors.Wrap(apiutil.ErrValidation, sdk.ErrFailedCreation), http.StatusInternalServerError), + err: errors.NewSDKErrorWithStatus(errors.Wrap(apiutil.ErrValidation, sdk.ErrFailedCreation), http.StatusUnprocessableEntity), }, { desc: "register empty user", @@ -366,7 +366,6 @@ func TestListClients(t *testing.T) { for _, tc := range cases { pm := sdk.PageMetadata{ Status: tc.status, - Total: total, Offset: tc.offset, Limit: tc.limit, Name: tc.name, @@ -919,7 +918,7 @@ func TestUpdateClientRole(t *testing.T) { client: client2, response: sdk.User{}, token: validToken, - err: errors.NewSDKErrorWithStatus(errors.Wrap(users.ErrFailedOwnerUpdate, users.ErrFailedOwnerUpdate), http.StatusInternalServerError), + err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrFailedOwnerUpdate, svcerr.ErrFailedOwnerUpdate), http.StatusInternalServerError), }, { desc: "update a user that can't be marshalled", diff --git a/things/service.go b/things/service.go index dc46fa052..f40ef70ce 100644 --- a/things/service.go +++ b/things/service.go @@ -437,7 +437,7 @@ func (svc service) changeClientStatus(ctx context.Context, token string, client return mgclients.Client{}, errors.Wrap(repoerr.ErrNotFound, err) } if dbClient.Status == client.Status { - return mgclients.Client{}, mgclients.ErrStatusAlreadyAssigned + return mgclients.Client{}, errors.ErrStatusAlreadyAssigned } client.UpdatedBy = userID diff --git a/things/service_test.go b/things/service_test.go index 717e16a57..4379adaf3 100644 --- a/things/service_test.go +++ b/things/service_test.go @@ -788,7 +788,7 @@ func TestEnableClient(t *testing.T) { token: validToken, client: enabledClient1, response: enabledClient1, - err: mgclients.ErrStatusAlreadyAssigned, + err: errors.ErrStatusAlreadyAssigned, }, { desc: "enable non-existing client", @@ -911,7 +911,7 @@ func TestDisableClient(t *testing.T) { token: validToken, client: disabledClient1, response: mgclients.Client{}, - err: mgclients.ErrStatusAlreadyAssigned, + err: errors.ErrStatusAlreadyAssigned, }, { desc: "disable non-existing client", diff --git a/users/api/clients.go b/users/api/clients.go index 32dea1a2b..0208569e3 100644 --- a/users/api/clients.go +++ b/users/api/clients.go @@ -26,6 +26,7 @@ func clientsHandler(svc users.Service, r *chi.Mux, logger mglog.Logger) http.Han opts := []kithttp.ServerOption{ kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)), } + r.Route("/users", func(r chi.Router) { r.Post("/", otelhttp.NewHandler(kithttp.NewServer( registrationEndpoint(svc), @@ -83,20 +84,6 @@ func clientsHandler(svc users.Service, r *chi.Mux, logger mglog.Logger) http.Han opts..., ), "update_client_identity").ServeHTTP) - r.Post("/password/reset-request", otelhttp.NewHandler(kithttp.NewServer( - passwordResetRequestEndpoint(svc), - decodePasswordResetRequest, - api.EncodeResponse, - opts..., - ), "password_reset_req").ServeHTTP) - - r.Put("/password/reset", otelhttp.NewHandler(kithttp.NewServer( - passwordResetEndpoint(svc), - decodePasswordReset, - api.EncodeResponse, - opts..., - ), "password_reset").ServeHTTP) - r.Patch("/{id}/role", otelhttp.NewHandler(kithttp.NewServer( updateClientRoleEndpoint(svc), decodeUpdateClientRole, @@ -133,6 +120,22 @@ func clientsHandler(svc users.Service, r *chi.Mux, logger mglog.Logger) http.Han ), "disable_client").ServeHTTP) }) + r.Route("/password", func(r chi.Router) { + r.Post("/reset-request", otelhttp.NewHandler(kithttp.NewServer( + passwordResetRequestEndpoint(svc), + decodePasswordResetRequest, + api.EncodeResponse, + opts..., + ), "password_reset_req").ServeHTTP) + + r.Put("/reset", otelhttp.NewHandler(kithttp.NewServer( + passwordResetEndpoint(svc), + decodePasswordReset, + api.EncodeResponse, + opts..., + ), "password_reset").ServeHTTP) + }) + // Ideal location: users service, groups endpoint. // Reason for placing here : // SpiceDB provides list of user ids in given user_group_id @@ -220,7 +223,7 @@ func decodeListClients(_ context.Context, r *http.Request) (interface{}, error) } oid, err := apiutil.ReadStringQuery(r, api.OwnerKey, "") if err != nil { - return nil, err + return nil, errors.Wrap(apiutil.ErrValidation, err) } visibility, err := apiutil.ReadStringQuery(r, api.VisibilityKey, "") if err != nil { diff --git a/users/api/responses.go b/users/api/responses.go index 716f5a912..fb05de493 100644 --- a/users/api/responses.go +++ b/users/api/responses.go @@ -25,8 +25,8 @@ var ( type pageRes struct { Limit uint64 `json:"limit,omitempty"` - Offset uint64 `json:"offset,omitempty"` - Total uint64 `json:"total,omitempty"` + Offset uint64 `json:"offset"` + Total uint64 `json:"total"` } type createClientRes struct { diff --git a/users/service.go b/users/service.go index 8bf17a720..572f44453 100644 --- a/users/service.go +++ b/users/service.go @@ -18,17 +18,6 @@ import ( ) var ( - // ErrRecoveryToken indicates error in generating password recovery token. - ErrRecoveryToken = errors.New("failed to generate password recovery token") - - // ErrPasswordFormat indicates weak password. - ErrPasswordFormat = errors.New("password does not meet the requirements") - - // ErrFailedPolicyUpdate indicates a failure to update user policy. - ErrFailedPolicyUpdate = errors.New("failed to update user policy") - - // ErrFailedOwnerUpdate indicates a failure to update user policy. - ErrFailedOwnerUpdate = errors.New("failed to update user owner") // ErrAddPolicies indictaed a failre to add policies. errAddPolicies = errors.New("failed to add policies") @@ -268,7 +257,7 @@ func (svc service) GenerateResetToken(ctx context.Context, email, host string) e } token, err := svc.auth.Issue(ctx, issueReq) if err != nil { - return errors.Wrap(ErrRecoveryToken, err) + return errors.Wrap(svcerr.ErrRecoveryToken, err) } return svc.SendPasswordReset(ctx, host, email, client.Name, token.AccessToken) @@ -287,7 +276,7 @@ func (svc service) ResetSecret(ctx context.Context, resetToken, secret string) e return errors.ErrNotFound } if !svc.passRegex.MatchString(secret) { - return ErrPasswordFormat + return errors.ErrPasswordFormat } secret, err = svc.hasher.Hash(secret) if err != nil { @@ -313,7 +302,7 @@ func (svc service) UpdateClientSecret(ctx context.Context, token, oldSecret, new return mgclients.Client{}, errors.Wrap(svcerr.ErrAuthentication, err) } if !svc.passRegex.MatchString(newSecret) { - return mgclients.Client{}, ErrPasswordFormat + return mgclients.Client{}, errors.ErrPasswordFormat } dbClient, err := svc.clients.RetrieveByID(ctx, id) if err != nil { @@ -355,7 +344,7 @@ func (svc service) UpdateClientRole(ctx context.Context, token string, cli mgcli } if err := svc.updateClientPolicy(ctx, cli.ID, cli.Role); err != nil { - return mgclients.Client{}, errors.Wrap(ErrFailedPolicyUpdate, err) + return mgclients.Client{}, errors.Wrap(svcerr.ErrFailedPolicyUpdate, err) } client, err = svc.clients.UpdateRole(ctx, client) if err != nil { @@ -363,7 +352,7 @@ func (svc service) UpdateClientRole(ctx context.Context, token string, cli mgcli if errRollback := svc.updateClientPolicy(ctx, cli.ID, mgclients.UserRole); errRollback != nil { return mgclients.Client{}, errors.Wrap(err, errors.Wrap(repoerr.ErrRollbackTx, errRollback)) } - return mgclients.Client{}, errors.Wrap(ErrFailedOwnerUpdate, err) + return mgclients.Client{}, errors.Wrap(svcerr.ErrFailedOwnerUpdate, err) } return client, nil } @@ -409,7 +398,7 @@ func (svc service) changeClientStatus(ctx context.Context, token string, client return mgclients.Client{}, errors.Wrap(repoerr.ErrNotFound, err) } if dbClient.Status == client.Status { - return mgclients.Client{}, mgclients.ErrStatusAlreadyAssigned + return mgclients.Client{}, errors.ErrStatusAlreadyAssigned } client.UpdatedBy = tokenUserID return svc.clients.ChangeStatus(ctx, client) @@ -494,7 +483,7 @@ func (svc *service) authorize(ctx context.Context, subjType, subjKind, subj, per } if !res.GetAuthorized() { - return "", errors.Wrap(svcerr.ErrAuthorization, err) + return "", svcerr.ErrAuthorization } return res.GetId(), nil } diff --git a/users/service_test.go b/users/service_test.go index 8ed9ecaa2..a1ddb3837 100644 --- a/users/service_test.go +++ b/users/service_test.go @@ -993,7 +993,7 @@ func TestEnableClient(t *testing.T) { token: validToken, client: enabledClient1, response: enabledClient1, - err: mgclients.ErrStatusAlreadyAssigned, + err: errors.ErrStatusAlreadyAssigned, }, { desc: "enable non-existing client", @@ -1123,7 +1123,7 @@ func TestDisableClient(t *testing.T) { token: validToken, client: disabledClient1, response: mgclients.Client{}, - err: mgclients.ErrStatusAlreadyAssigned, + err: errors.ErrStatusAlreadyAssigned, }, { desc: "disable non-existing client",