diff --git a/cmd/wg-portal/main.go b/cmd/wg-portal/main.go index 6bd46fb..2d8c29b 100644 --- a/cmd/wg-portal/main.go +++ b/cmd/wg-portal/main.go @@ -105,7 +105,7 @@ func main() { apiFrontend := handlersV0.NewRestApi(cfg, backend) - apiV1BackendUsers := backendV1.NewUserService(cfg, database, database) + apiV1BackendUsers := backendV1.NewUserService(cfg, userManager) apiV1EndpointUsers := handlersV1.NewUserEndpoint(apiV1BackendUsers) apiV1 := handlersV1.NewRestApi(userManager, apiV1EndpointUsers) diff --git a/internal/app/api/core/assets/doc/v1_swagger.json b/internal/app/api/core/assets/doc/v1_swagger.json index e7a7776..8429349 100644 --- a/internal/app/api/core/assets/doc/v1_swagger.json +++ b/internal/app/api/core/assets/doc/v1_swagger.json @@ -40,6 +40,12 @@ } } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, "500": { "description": "Internal Server Error", "schema": { @@ -65,6 +71,15 @@ ], "summary": "Get a specific user record by its internal identifier.", "operationId": "users_handleByIdGet", + "parameters": [ + { + "type": "string", + "description": "The user identifier.", + "name": "id", + "in": "path", + "required": true + } + ], "responses": { "200": { "description": "OK", @@ -72,6 +87,12 @@ "$ref": "#/definitions/models.User" } }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, "403": { "description": "Forbidden", "schema": { @@ -91,6 +112,204 @@ } } } + }, + "put": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Only admins can update existing records.", + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Update a user record.", + "operationId": "users_handleUpdatePut", + "parameters": [ + { + "type": "string", + "description": "The user identifier", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "The user data", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.User" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.User" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + }, + "delete": { + "security": [ + { + "BasicAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Delete the user record.", + "operationId": "users_handleDelete", + "parameters": [ + { + "type": "string", + "description": "The user identifier", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No content if deletion was successful" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + } + }, + "/user/new": { + "post": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Only admins can create new records.", + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Create a new user record.", + "operationId": "users_handleCreatePost", + "parameters": [ + { + "description": "The user data.", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.User" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.User" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } } } }, @@ -116,9 +335,13 @@ "type": "object", "properties": { "ApiEnabled": { - "description": "If this field is set, the user is allowed to use the RESTful API.", + "description": "If this field is set, the user is allowed to use the RESTful API. This field is read-only.", "type": "boolean" }, + "ApiToken": { + "description": "The API token of the user. This field is never populated on bulk read operations.", + "type": "string" + }, "Department": { "description": "The department of the user. This field is optional.", "type": "string" @@ -163,8 +386,12 @@ "description": "Additional notes about the user. This field is optional.", "type": "string" }, + "Password": { + "description": "The password of the user. This field is never populated on read operations.", + "type": "string" + }, "PeerCount": { - "description": "The number of peers linked to the user.", + "description": "The number of peers linked to the user. This field is read-only.", "type": "integer" }, "Phone": { diff --git a/internal/app/api/core/assets/doc/v1_swagger.yaml b/internal/app/api/core/assets/doc/v1_swagger.yaml index 26c2a33..2694603 100644 --- a/internal/app/api/core/assets/doc/v1_swagger.yaml +++ b/internal/app/api/core/assets/doc/v1_swagger.yaml @@ -16,8 +16,12 @@ definitions: properties: ApiEnabled: description: If this field is set, the user is allowed to use the RESTful - API. + API. This field is read-only. type: boolean + ApiToken: + description: The API token of the user. This field is never populated on bulk + read operations. + type: string Department: description: The department of the user. This field is optional. type: string @@ -52,8 +56,12 @@ definitions: Notes: description: Additional notes about the user. This field is optional. type: string + Password: + description: The password of the user. This field is never populated on read + operations. + type: string PeerCount: - description: The number of peers linked to the user. + description: The number of peers linked to the user. This field is read-only. type: integer Phone: description: The phone number of the user. This field is optional. @@ -88,6 +96,10 @@ paths: items: $ref: '#/definitions/models.User' type: array + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' "500": description: Internal Server Error schema: @@ -98,10 +110,54 @@ paths: tags: - Users /user/id/{id}: + delete: + operationId: users_handleDelete + parameters: + - description: The user identifier + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No content if deletion was successful + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "403": + description: Forbidden + schema: + $ref: '#/definitions/models.Error' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Delete the user record. + tags: + - Users get: description: Normal users can only access their own record. Admins can access all records. operationId: users_handleByIdGet + parameters: + - description: The user identifier. + in: path + name: id + required: true + type: string produces: - application/json responses: @@ -109,6 +165,10 @@ paths: description: OK schema: $ref: '#/definitions/models.User' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' "403": description: Forbidden schema: @@ -126,6 +186,96 @@ paths: summary: Get a specific user record by its internal identifier. tags: - Users + put: + description: Only admins can update existing records. + operationId: users_handleUpdatePut + parameters: + - description: The user identifier + in: path + name: id + required: true + type: string + - description: The user data + in: body + name: request + required: true + schema: + $ref: '#/definitions/models.User' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.User' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "403": + description: Forbidden + schema: + $ref: '#/definitions/models.Error' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Update a user record. + tags: + - Users + /user/new: + post: + description: Only admins can create new records. + operationId: users_handleCreatePost + parameters: + - description: The user data. + in: body + name: request + required: true + schema: + $ref: '#/definitions/models.User' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.User' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "403": + description: Forbidden + schema: + $ref: '#/definitions/models.Error' + "409": + description: Conflict + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Create a new user record. + tags: + - Users securityDefinitions: BasicAuth: type: basic diff --git a/internal/app/api/v1/backend/user_service.go b/internal/app/api/v1/backend/user_service.go index b077daa..8677ca6 100644 --- a/internal/app/api/v1/backend/user_service.go +++ b/internal/app/api/v1/backend/user_service.go @@ -4,38 +4,29 @@ import ( "context" "errors" "fmt" - "math" - "sync" - "github.com/h44z/wg-portal/internal/app/users" "github.com/h44z/wg-portal/internal/config" "github.com/h44z/wg-portal/internal/domain" ) -type UserDatabaseRepo interface { +type UserManagerRepo interface { GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) GetAllUsers(ctx context.Context) ([]domain.User, error) - FindUsers(ctx context.Context, search string) ([]domain.User, error) - SaveUser(ctx context.Context, id domain.UserIdentifier, updateFunc func(u *domain.User) (*domain.User, error)) error + CreateUser(ctx context.Context, user *domain.User) (*domain.User, error) + UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error) DeleteUser(ctx context.Context, id domain.UserIdentifier) error } -type PeerDatabaseRepo interface { - GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error) -} - type UserService struct { cfg *config.Config - users UserDatabaseRepo - peers PeerDatabaseRepo + users UserManagerRepo } -func NewUserService(cfg *config.Config, users UserDatabaseRepo, peers PeerDatabaseRepo) *UserService { +func NewUserService(cfg *config.Config, users UserManagerRepo) *UserService { return &UserService{ cfg: cfg, users: users, - peers: peers, } } @@ -44,31 +35,12 @@ func (s UserService) GetUsers(ctx context.Context) ([]domain.User, error) { return nil, err } - users, err := s.users.GetAllUsers(ctx) + allUsers, err := s.users.GetAllUsers(ctx) if err != nil { - return nil, fmt.Errorf("unable to load users: %w", err) + return nil, err } - ch := make(chan *domain.User) - wg := sync.WaitGroup{} - workers := int(math.Min(float64(len(users)), 10)) - wg.Add(workers) - for i := 0; i < workers; i++ { - go func() { - defer wg.Done() - for user := range ch { - peers, _ := s.peers.GetUserPeers(ctx, user.Identifier) // ignore error, list will be empty in error case - user.LinkedPeerCount = len(peers) - } - }() - } - for i := range users { - ch <- &users[i] - } - close(ch) - wg.Wait() - - return users, nil + return allUsers, nil } func (s UserService) GetUserById(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) { @@ -76,13 +48,14 @@ func (s UserService) GetUserById(ctx context.Context, id domain.UserIdentifier) return nil, errors.Join(err, domain.ErrNoPermission) } - user, err := s.users.GetUser(ctx, id) - if err != nil { - return nil, fmt.Errorf("unable to load user %s: %w", id, err) + if s.cfg.Advanced.ApiAdminOnly && !domain.GetUserInfo(ctx).IsAdmin { + return nil, errors.Join(errors.New("only admins can access this endpoint"), domain.ErrNoPermission) } - peers, _ := s.peers.GetUserPeers(ctx, user.Identifier) // ignore error, list will be empty in error case - user.LinkedPeerCount = len(peers) + user, err := s.users.GetUser(ctx, id) + if err != nil { + return nil, err + } return user, nil } @@ -92,30 +65,43 @@ func (s UserService) CreateUser(ctx context.Context, user *domain.User) (*domain return nil, err } - existingUser, err := s.users.GetUser(ctx, user.Identifier) - if err != nil && !errors.Is(err, domain.ErrNotFound) { - return nil, fmt.Errorf("unable to load existing user %s: %w", user.Identifier, err) - } - if existingUser != nil { - return nil, errors.Join(fmt.Errorf("user %s already exists", user.Identifier), domain.ErrDuplicateEntry) - } - - if err := users.ValidateCreation(ctx, user); err != nil { - return nil, errors.Join(fmt.Errorf("creation not allowed: %w", err), domain.ErrInvalidData) - } - - err = user.HashPassword() + createdUser, err := s.users.CreateUser(ctx, user) if err != nil { return nil, err } - err = s.users.SaveUser(ctx, user.Identifier, func(u *domain.User) (*domain.User, error) { - user.CopyCalculatedAttributes(u) - return user, nil - }) - if err != nil { - return nil, fmt.Errorf("creation failure: %w", err) + return createdUser, nil +} + +func (s UserService) UpdateUser(ctx context.Context, id domain.UserIdentifier, user *domain.User) ( + *domain.User, + error, +) { + if err := domain.ValidateAdminAccessRights(ctx); err != nil { + return nil, err } - return user, nil + if id != user.Identifier { + return nil, fmt.Errorf("user id mismatch: %s != %s: %w", id, user.Identifier, domain.ErrInvalidData) + } + + updatedUser, err := s.users.UpdateUser(ctx, user) + if err != nil { + return nil, err + } + + return updatedUser, nil +} + +func (s UserService) DeleteUser(ctx context.Context, id domain.UserIdentifier) error { + if err := domain.ValidateAdminAccessRights(ctx); err != nil { + return err + } + + err := s.users.DeleteUser(ctx, id) + if err != nil { + return err + } + + return nil } diff --git a/internal/app/api/v1/handlers/endpoint_users.go b/internal/app/api/v1/handlers/endpoint_users.go index 252c54b..2990724 100644 --- a/internal/app/api/v1/handlers/endpoint_users.go +++ b/internal/app/api/v1/handlers/endpoint_users.go @@ -5,7 +5,6 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/h44z/wg-portal/internal/app/api/v0/model" "github.com/h44z/wg-portal/internal/app/api/v1/models" "github.com/h44z/wg-portal/internal/domain" ) @@ -14,6 +13,8 @@ type UserService interface { GetUsers(ctx context.Context) ([]domain.User, error) GetUserById(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) CreateUser(ctx context.Context, user *domain.User) (*domain.User, error) + UpdateUser(ctx context.Context, id domain.UserIdentifier, user *domain.User) (*domain.User, error) + DeleteUser(ctx context.Context, id domain.UserIdentifier) error } type UserEndpoint struct { @@ -35,6 +36,9 @@ func (e UserEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenti apiGroup.GET("/all", authenticator.LoggedIn(ScopeAdmin), e.handleAllGet()) apiGroup.GET("/id/:id", authenticator.LoggedIn(), e.handleByIdGet()) + apiGroup.POST("/new", authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost()) + apiGroup.PUT("/id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleUpdatePut()) + apiGroup.DELETE("/id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleDelete()) } // handleAllGet returns a gorm Handler function. @@ -44,6 +48,7 @@ func (e UserEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenti // @Summary Get all user records. // @Produce json // @Success 200 {object} []models.User +// @Failure 401 {object} models.Error // @Failure 500 {object} models.Error // @Router /user/all [get] // @Security BasicAuth @@ -70,7 +75,7 @@ func (e UserEndpoint) handleAllGet() gin.HandlerFunc { // @Param id path string true "The user identifier." // @Produce json // @Success 200 {object} models.User -// @Failure 403 {object} models.Error +// @Failure 401 {object} models.Error // @Failure 403 {object} models.Error // @Failure 404 {object} models.Error // @Failure 500 {object} models.Error @@ -106,10 +111,12 @@ func (e UserEndpoint) handleByIdGet() gin.HandlerFunc { // @Produce json // @Success 200 {object} models.User // @Failure 400 {object} models.Error +// @Failure 401 {object} models.Error // @Failure 403 {object} models.Error // @Failure 409 {object} models.Error // @Failure 500 {object} models.Error // @Router /user/new [post] +// @Security BasicAuth func (e UserEndpoint) handleCreatePost() gin.HandlerFunc { return func(c *gin.Context) { ctx := domain.SetUserInfoFromGin(c) @@ -142,41 +149,36 @@ func (e UserEndpoint) handleCreatePost() gin.HandlerFunc { // @Produce json // @Success 200 {object} models.User // @Failure 400 {object} models.Error +// @Failure 401 {object} models.Error // @Failure 403 {object} models.Error // @Failure 404 {object} models.Error // @Failure 500 {object} models.Error -// @Router /user/{id} [put] +// @Router /user/id/{id} [put] +// @Security BasicAuth func (e UserEndpoint) handleUpdatePut() gin.HandlerFunc { return func(c *gin.Context) { - // TODO: implement ctx := domain.SetUserInfoFromGin(c) - id := Base64UrlDecode(c.Param("id")) + id := c.Param("id") if id == "" { - c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "missing user id"}) + c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"}) return } - var user model.User + var user models.User err := c.BindJSON(&user) if err != nil { - c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: err.Error()}) + c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()}) return } - if id != user.Identifier { - c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "user id mismatch"}) - return - } - - updateUser, err := e.app.UpdateUser(ctx, model.NewDomainUser(&user)) + updateUser, err := e.users.UpdateUser(ctx, domain.UserIdentifier(id), models.NewDomainUser(&user)) if err != nil { - c.JSON(http.StatusInternalServerError, - model.Error{Code: http.StatusInternalServerError, Message: err.Error()}) + c.JSON(ParseServiceError(err)) return } - c.JSON(http.StatusOK, model.NewUser(updateUser, false)) + c.JSON(http.StatusOK, models.NewUser(updateUser, true)) } } @@ -188,24 +190,27 @@ func (e UserEndpoint) handleUpdatePut() gin.HandlerFunc { // @Produce json // @Param id path string true "The user identifier" // @Success 204 "No content if deletion was successful" -// @Failure 400 {object} model.Error -// @Failure 500 {object} model.Error -// @Router /user/{id} [delete] +// @Failure 400 {object} models.Error +// @Failure 401 {object} models.Error +// @Failure 403 {object} models.Error +// @Failure 404 {object} models.Error +// @Failure 500 {object} models.Error +// @Router /user/id/{id} [delete] +// @Security BasicAuth func (e UserEndpoint) handleDelete() gin.HandlerFunc { return func(c *gin.Context) { // TODO: implement ctx := domain.SetUserInfoFromGin(c) - id := Base64UrlDecode(c.Param("id")) + id := c.Param("id") if id == "" { - c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "missing user id"}) + c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"}) return } - err := e.app.DeleteUser(ctx, domain.UserIdentifier(id)) + err := e.users.DeleteUser(ctx, domain.UserIdentifier(id)) if err != nil { - c.JSON(http.StatusInternalServerError, - model.Error{Code: http.StatusInternalServerError, Message: err.Error()}) + c.JSON(ParseServiceError(err)) return } diff --git a/internal/app/users/user_manager.go b/internal/app/users/user_manager.go index 9b78974..4a1a8da 100644 --- a/internal/app/users/user_manager.go +++ b/internal/app/users/user_manager.go @@ -102,7 +102,7 @@ func (m Manager) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain user, err := m.users.GetUser(ctx, id) if err != nil { - return nil, fmt.Errorf("unable to load peer %s: %w", id, err) + return nil, fmt.Errorf("unable to load user %s: %w", id, err) } peers, _ := m.peers.GetUserPeers(ctx, id) // ignore error, list will be empty in error case @@ -194,10 +194,10 @@ func (m Manager) CreateUser(ctx context.Context, user *domain.User) (*domain.Use return nil, fmt.Errorf("unable to load existing user %s: %w", user.Identifier, err) } if existingUser != nil { - return nil, fmt.Errorf("user %s already exists", user.Identifier) + return nil, errors.Join(fmt.Errorf("user %s already exists", user.Identifier), domain.ErrDuplicateEntry) } - if err := ValidateCreation(ctx, user); err != nil { + if err := m.validateCreation(ctx, user); err != nil { return nil, fmt.Errorf("creation not allowed: %w", err) } @@ -302,33 +302,33 @@ func (m Manager) validateModifications(ctx context.Context, old, new *domain.Use } if err := old.EditAllowed(new); err != nil { - return fmt.Errorf("no access: %w", err) + return errors.Join(fmt.Errorf("no access: %w", err), domain.ErrInvalidData) } if err := old.CanChangePassword(); err != nil && string(new.Password) != "" { - return fmt.Errorf("no access: %w", err) + return errors.Join(fmt.Errorf("no access: %w", err), domain.ErrInvalidData) } if currentUser.Id == old.Identifier && old.IsAdmin && !new.IsAdmin { - return fmt.Errorf("cannot remove own admin rights") + return fmt.Errorf("cannot remove own admin rights: %w", domain.ErrInvalidData) } if currentUser.Id == old.Identifier && new.IsDisabled() { - return fmt.Errorf("cannot disable own user") + return fmt.Errorf("cannot disable own user: %w", domain.ErrInvalidData) } if currentUser.Id == old.Identifier && new.IsLocked() { - return fmt.Errorf("cannot lock own user") + return fmt.Errorf("cannot lock own user: %w", domain.ErrInvalidData) } if old.Source != new.Source { - return fmt.Errorf("cannot change user source") + return fmt.Errorf("cannot change user source: %w", domain.ErrInvalidData) } return nil } -func ValidateCreation(ctx context.Context, new *domain.User) error { +func (m Manager) validateCreation(ctx context.Context, new *domain.User) error { currentUser := domain.GetUserInfo(ctx) if !currentUser.IsAdmin { @@ -336,23 +336,32 @@ func ValidateCreation(ctx context.Context, new *domain.User) error { } if new.Identifier == "" { - return fmt.Errorf("invalid user identifier") + return fmt.Errorf("invalid user identifier: %w", domain.ErrInvalidData) } - if new.Identifier == "all" { // the all user identifier collides with the rest api routes - return fmt.Errorf("reserved user identifier") + if new.Identifier == "all" { // the 'all' user identifier collides with the rest api routes + return fmt.Errorf("reserved user identifier: %w", domain.ErrInvalidData) } - if new.Identifier == "new" { // the new user identifier collides with the rest api routes - return fmt.Errorf("reserved user identifier") + if new.Identifier == "new" { // the 'new' user identifier collides with the rest api routes + return fmt.Errorf("reserved user identifier: %w", domain.ErrInvalidData) + } + + if new.Identifier == "id" { // the 'id' user identifier collides with the rest api routes + return fmt.Errorf("reserved user identifier: %w", domain.ErrInvalidData) + } + + if new.Identifier == domain.CtxSystemAdminId || new.Identifier == domain.CtxUnknownUserId { + return fmt.Errorf("reserved user identifier: %w", domain.ErrInvalidData) } if new.Source != domain.UserSourceDatabase { - return fmt.Errorf("invalid user source: %s, only %s is allowed", new.Source, domain.UserSourceDatabase) + return fmt.Errorf("invalid user source: %s, only %s is allowed: %w", + new.Source, domain.UserSourceDatabase, domain.ErrInvalidData) } if string(new.Password) == "" { - return fmt.Errorf("invalid password") + return fmt.Errorf("invalid password: %w", domain.ErrInvalidData) } return nil @@ -362,15 +371,15 @@ func (m Manager) validateDeletion(ctx context.Context, del *domain.User) error { currentUser := domain.GetUserInfo(ctx) if !currentUser.IsAdmin { - return fmt.Errorf("insufficient permissions") + return domain.ErrNoPermission } if err := del.DeleteAllowed(); err != nil { - return fmt.Errorf("no access: %w", err) + return errors.Join(fmt.Errorf("no access: %w", err), domain.ErrInvalidData) } if currentUser.Id == del.Identifier { - return fmt.Errorf("cannot delete own user") + return fmt.Errorf("cannot delete own user: %w", domain.ErrInvalidData) } return nil @@ -380,7 +389,7 @@ func (m Manager) validateApiChange(ctx context.Context, user *domain.User) error currentUser := domain.GetUserInfo(ctx) if currentUser.Id != user.Identifier { - return fmt.Errorf("cannot change API access of user") + return fmt.Errorf("cannot change API access of user: %w", domain.ErrNoPermission) } return nil diff --git a/internal/config/config.go b/internal/config/config.go index d378b3a..4ebf537 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -39,6 +39,7 @@ type Config struct { ExpiryCheckInterval time.Duration `yaml:"expiry_check_interval"` RulePrioOffset int `yaml:"rule_prio_offset"` RouteTableOffset int `yaml:"route_table_offset"` + ApiAdminOnly bool `yaml:"api_admin_only"` // if true, only admin users can access the API } `yaml:"advanced"` Statistics struct { diff --git a/internal/domain/context.go b/internal/domain/context.go index 8134cca..ebe11aa 100644 --- a/internal/domain/context.go +++ b/internal/domain/context.go @@ -94,7 +94,7 @@ func ValidateUserAccessRights(ctx context.Context, requiredUser UserIdentifier) } logrus.Warnf("insufficient permissions for %s (want %s), stack: %s", sessionUser.Id, requiredUser, GetStackTrace()) - return fmt.Errorf("insufficient permissions") + return ErrNoPermission } // ValidateAdminAccessRights checks if the current user has admin access rights. @@ -106,5 +106,5 @@ func ValidateAdminAccessRights(ctx context.Context) error { } logrus.Warnf("insufficient admin permissions for %s, stack: %s", sessionUser.Id, GetStackTrace()) - return fmt.Errorf("insufficient permissions") + return ErrNoPermission }