From b193e01520c3712a215ccb80b29731d5b0df1c79 Mon Sep 17 00:00:00 2001 From: iripiri Date: Thu, 15 Aug 2024 09:35:58 +0200 Subject: [PATCH 01/21] [WIP] add usergroups endpoints Signed-off-by: iripiri --- README.md | 5 + database/models.go | 20 +++ database/permissions.go | 24 +++ database/roles.go | 5 + doc/api/docs.go | 366 ++++++++++++++++++++++++++-------------- doc/api/responses.go | 4 + doc/api/swagger.json | 366 ++++++++++++++++++++++++++-------------- doc/api/swagger.yaml | 324 +++++++++++++++++++++-------------- routes/register.go | 2 + 9 files changed, 728 insertions(+), 388 deletions(-) diff --git a/README.md b/README.md index 634af0d..e7672ad 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,11 @@ go run start.go --help ``` to get a list of available parameters and default values +### Generating Docs +```bash +$(go env GOPATH)/bin/swag init --generalInfo start.go --output ./doc/api --parseDependency +``` + ## Environment variables | Variable | Description | diff --git a/database/models.go b/database/models.go index 438f451..bd65d2b 100644 --- a/database/models.go +++ b/database/models.go @@ -44,12 +44,32 @@ type User struct { Mail string `json:"mail" gorm:"default:''"` // Role of user Role string `json:"role" gorm:"default:'user'"` + // Group of user by which the user is added to scenarios + Group string `json:"group" gorm:"default:''"` // Indicating status of user (false means user is inactive and should not be able to login) Active bool `json:"active" gorm:"default:true"` // Scenarios to which user has access Scenarios []*Scenario `json:"-" gorm:"many2many:user_scenarios;"` } +// ScenarioMapping data model +type ScenarioMapping struct { + Model + // ID of Scenario + ScenarioID uint `json:"scenarioID"` + // Duplicate Scenario or add to existing Scenario + Duplicate bool `json:"duplicate"` +} + +// UserGroup data model +type UserGroup struct { + Model + // Name of user group + Name string `json:"name" gorm:"unique;not null"` + // Scenarios that belong to the user group + ScenarioMappings []*ScenarioMapping `json:"scenarioMappings" gorm:"foreignkey:UserGroupID"` +} + // Scenario data model type Scenario struct { Model diff --git a/database/permissions.go b/database/permissions.go index ec2fb66..924a028 100644 --- a/database/permissions.go +++ b/database/permissions.go @@ -142,6 +142,30 @@ func CheckSignalPermissions(c *gin.Context, operation CRUD) (bool, Signal) { } +func CheckUserGroupPermissions(c *gin.Context, operation CRUD, userGroupSourceID string, dabIDBody int) (bool, UserGroup) { + + var usrgrp UserGroup + + err := ValidateRole(c, ModelUserGroup, operation) + if err != nil { + helper.UnprocessableEntityError(c, fmt.Sprintf("Access denied (role validation failed): %v", err.Error())) + return false, usrgrp + } + + groupID, err := helper.GetIDOfElement(c, "usergroupID", userGroupSourceID, dabIDBody) + if err != nil { + return false, usrgrp + } + + db := GetDB() + err = db.Find(&usrgrp, uint(groupID)).Error + if helper.DBNotFoundError(c, err, strconv.Itoa(groupID), "UserGroup") { + return false, usrgrp + } + + return true, usrgrp +} + func CheckDashboardPermissions(c *gin.Context, operation CRUD, dabIDSource string, dabIDBody int) (bool, Dashboard) { var dab Dashboard diff --git a/database/roles.go b/database/roles.go index c7d24f6..05ebd03 100644 --- a/database/roles.go +++ b/database/roles.go @@ -19,6 +19,7 @@ package database import ( "fmt" + "github.com/gin-gonic/gin" ) @@ -33,6 +34,7 @@ type ModelName string const ModelUser = ModelName("user") const ModelUsers = ModelName("users") const ModelScenario = ModelName("scenario") +const ModelUserGroup = ModelName("usergroup") const ModelInfrastructureComponent = ModelName("ic") const ModelInfrastructureComponentAction = ModelName("icaction") const ModelDashboard = ModelName("dashboard") @@ -73,6 +75,7 @@ var Roles = RoleActions{ ModelUser: crud, ModelUsers: crud, ModelScenario: crud, + ModelUserGroup: crud, ModelComponentConfiguration: crud, ModelInfrastructureComponent: crud, ModelInfrastructureComponentAction: crud, @@ -86,6 +89,7 @@ var Roles = RoleActions{ ModelUser: _ru_, ModelUsers: none, ModelScenario: crud, + ModelUserGroup: _r__, ModelComponentConfiguration: crud, ModelInfrastructureComponent: _r__, ModelInfrastructureComponentAction: _ru_, @@ -117,6 +121,7 @@ var Roles = RoleActions{ ModelInfrastructureComponentAction: none, ModelUser: none, ModelUsers: none, + ModelUserGroup: none, ModelSignal: none, ModelFile: _r__, ModelResult: none, diff --git a/doc/api/docs.go b/doc/api/docs.go index 31ee603..1abda89 100644 --- a/doc/api/docs.go +++ b/doc/api/docs.go @@ -2702,6 +2702,69 @@ const docTemplate = `{ } } }, + "/usergroups": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "usergroups" + ], + "summary": "Add a user group", + "operationId": "addUserGroup", + "parameters": [ + { + "description": "User group to be added", + "name": "inputUserGroup", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/usergroup.addUserGroupRequest" + } + } + ], + "responses": { + "200": { + "description": "user group that was added", + "schema": { + "$ref": "#/definitions/api.ResponseUserGroup" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "404": { + "description": "Not found", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "422": { + "description": "Unprocessable entity", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + } + } + } + }, "/users": { "get": { "security": [ @@ -3330,6 +3393,9 @@ const docTemplate = `{ "api.ResponseUser": { "type": "object" }, + "api.ResponseUserGroup": { + "type": "object" + }, "api.ResponseUsers": { "type": "object" }, @@ -3358,27 +3424,27 @@ const docTemplate = `{ "component_configuration.validNewConfig": { "type": "object", "required": [ - "Name", - "ScenarioID", - "StartParameters" + "name", + "scenarioID", + "startParameters" ], "properties": { - "FileIDs": { + "fileIDs": { "type": "array", "items": { "type": "integer" } }, - "ICID": { + "icid": { "type": "integer" }, - "Name": { + "name": { "type": "string" }, - "ScenarioID": { + "scenarioID": { "type": "integer" }, - "StartParameters": { + "startParameters": { "$ref": "#/definitions/postgres.Jsonb" } } @@ -3386,19 +3452,19 @@ const docTemplate = `{ "component_configuration.validUpdatedConfig": { "type": "object", "properties": { - "FileIDs": { + "fileIDs": { "type": "array", "items": { "type": "integer" } }, - "ICID": { + "icid": { "type": "integer" }, - "Name": { + "name": { "type": "string" }, - "StartParameters": { + "startParameters": { "$ref": "#/definitions/postgres.Jsonb" } } @@ -3520,21 +3586,21 @@ const docTemplate = `{ "dashboard.validNewDashboard": { "type": "object", "required": [ - "Grid", - "Name", - "ScenarioID" + "grid", + "name", + "scenarioID" ], "properties": { - "Grid": { + "grid": { "type": "integer" }, - "Height": { + "height": { "type": "integer" }, - "Name": { + "name": { "type": "string" }, - "ScenarioID": { + "scenarioID": { "type": "integer" } } @@ -3572,55 +3638,55 @@ const docTemplate = `{ "infrastructure_component.validNewIC": { "type": "object", "required": [ - "Category", - "ManagedExternally", - "Name", - "Type" + "category", + "managedExternally", + "name", + "type" ], "properties": { - "APIURL": { + "apiurl": { "type": "string" }, - "Category": { + "category": { "type": "string" }, - "CreateParameterSchema": { + "createParameterSchema": { "$ref": "#/definitions/postgres.Jsonb" }, - "Description": { + "description": { "type": "string" }, - "Location": { + "location": { "type": "string" }, - "ManagedExternally": { + "managedExternally": { "type": "boolean" }, - "Manager": { + "manager": { "type": "string" }, - "Name": { + "name": { "type": "string" }, - "StartParameterSchema": { + "startParameterSchema": { "$ref": "#/definitions/postgres.Jsonb" }, - "State": { + "state": { "type": "string" }, - "StatusUpdateRaw": { + "statusUpdateRaw": { "$ref": "#/definitions/postgres.Jsonb" }, - "Type": { - "type": "string" - }, - "UUID": { + "type": { "type": "string" }, - "Uptime": { + "uptime": { "type": "number" }, - "WebsocketURL": { + "uuid": { + "type": "string" + }, + "websocketURL": { "type": "string" } } @@ -3628,46 +3694,46 @@ const docTemplate = `{ "infrastructure_component.validUpdatedIC": { "type": "object", "properties": { - "APIURL": { + "apiurl": { "type": "string" }, - "Category": { + "category": { "type": "string" }, - "CreateParameterSchema": { + "createParameterSchema": { "$ref": "#/definitions/postgres.Jsonb" }, - "Description": { + "description": { "type": "string" }, - "Location": { + "location": { "type": "string" }, - "Manager": { + "manager": { "type": "string" }, - "Name": { + "name": { "type": "string" }, - "StartParameterSchema": { + "startParameterSchema": { "$ref": "#/definitions/postgres.Jsonb" }, - "State": { + "state": { "type": "string" }, - "StatusUpdateRaw": { + "statusUpdateRaw": { "$ref": "#/definitions/postgres.Jsonb" }, - "Type": { - "type": "string" - }, - "UUID": { + "type": { "type": "string" }, - "Uptime": { + "uptime": { "type": "number" }, - "WebsocketURL": { + "uuid": { + "type": "string" + }, + "websocketURL": { "type": "string" } } @@ -3702,23 +3768,23 @@ const docTemplate = `{ "result.validNewResult": { "type": "object", "required": [ - "ConfigSnapshots", - "ScenarioID" + "configSnapshots", + "scenarioID" ], "properties": { - "ConfigSnapshots": { + "configSnapshots": { "$ref": "#/definitions/postgres.Jsonb" }, - "Description": { + "description": { "type": "string" }, - "ResultFileIDs": { + "resultFileIDs": { "type": "array", "items": { "type": "integer" } }, - "ScenarioID": { + "scenarioID": { "type": "integer" } } @@ -3759,14 +3825,14 @@ const docTemplate = `{ "scenario.validNewScenario": { "type": "object", "required": [ - "Name", - "StartParameters" + "name", + "startParameters" ], "properties": { - "Name": { + "name": { "type": "string" }, - "StartParameters": { + "startParameters": { "$ref": "#/definitions/postgres.Jsonb" } } @@ -3774,13 +3840,13 @@ const docTemplate = `{ "scenario.validUpdatedScenario": { "type": "object", "properties": { - "IsLocked": { + "isLocked": { "type": "boolean" }, - "Name": { + "name": { "type": "string" }, - "StartParameters": { + "startParameters": { "$ref": "#/definitions/postgres.Jsonb" } } @@ -3804,32 +3870,32 @@ const docTemplate = `{ "signal.validNewSignal": { "type": "object", "required": [ - "ConfigID", - "Direction", - "Index", - "Name" + "configID", + "direction", + "index", + "name" ], "properties": { - "ConfigID": { + "configID": { "type": "integer" }, - "Direction": { + "direction": { "type": "string", "enum": [ "in", "out" ] }, - "Index": { + "index": { "type": "integer" }, - "Name": { + "name": { "type": "string" }, - "ScalingFactor": { + "scalingFactor": { "type": "number" }, - "Unit": { + "unit": { "type": "string" } } @@ -3837,16 +3903,16 @@ const docTemplate = `{ "signal.validUpdatedSignal": { "type": "object", "properties": { - "Index": { + "index": { "type": "integer" }, - "Name": { + "name": { "type": "string" }, - "ScalingFactor": { + "scalingFactor": { "type": "number" }, - "Unit": { + "unit": { "type": "string" } } @@ -3862,14 +3928,14 @@ const docTemplate = `{ "user.loginRequest": { "type": "object", "required": [ - "Password", - "Username" + "password", + "username" ], "properties": { - "Password": { + "password": { "type": "string" }, - "Username": { + "username": { "type": "string" } } @@ -3885,20 +3951,20 @@ const docTemplate = `{ "user.validNewUser": { "type": "object", "required": [ - "Mail", - "Password", - "Role", - "Username" + "mail", + "password", + "role", + "username" ], "properties": { - "Mail": { + "mail": { "type": "string" }, - "Password": { + "password": { "type": "string", "minLength": 6 }, - "Role": { + "role": { "type": "string", "enum": [ "Admin", @@ -3906,7 +3972,7 @@ const docTemplate = `{ "Guest" ] }, - "Username": { + "username": { "type": "string", "minLength": 3 } @@ -3915,25 +3981,25 @@ const docTemplate = `{ "user.validUpdatedRequest": { "type": "object", "properties": { - "Active": { + "active": { "type": "string", "enum": [ "yes", "no" ] }, - "Mail": { + "mail": { "type": "string" }, - "OldPassword": { + "oldPassword": { "type": "string", "minLength": 6 }, - "Password": { + "password": { "type": "string", "minLength": 6 }, - "Role": { + "role": { "type": "string", "enum": [ "Admin", @@ -3941,12 +4007,52 @@ const docTemplate = `{ "Guest" ] }, - "Username": { + "username": { "type": "string", "minLength": 3 } } }, + "usergroup.addUserGroupRequest": { + "type": "object", + "properties": { + "userGroup": { + "$ref": "#/definitions/usergroup.validNewUserGroup" + } + } + }, + "usergroup.validNewScenarioMapping": { + "type": "object", + "required": [ + "scenarioID" + ], + "properties": { + "duplicate": { + "type": "boolean" + }, + "scenarioID": { + "type": "integer" + } + } + }, + "usergroup.validNewUserGroup": { + "type": "object", + "required": [ + "name", + "scenarioMappings" + ], + "properties": { + "name": { + "type": "string" + }, + "scenarioMappings": { + "type": "array", + "items": { + "$ref": "#/definitions/usergroup.validNewScenarioMapping" + } + } + } + }, "widget.addWidgetRequest": { "type": "object", "properties": { @@ -3966,52 +4072,52 @@ const docTemplate = `{ "widget.validNewWidget": { "type": "object", "required": [ - "DashboardID", - "Height", - "Type", - "Width" + "dashboardID", + "height", + "type", + "width" ], "properties": { - "CustomProperties": { + "customProperties": { "$ref": "#/definitions/postgres.Jsonb" }, - "DashboardID": { + "dashboardID": { "type": "integer" }, - "Height": { + "height": { "type": "integer" }, - "IsLocked": { + "isLocked": { "type": "boolean" }, - "MinHeight": { + "minHeight": { "type": "integer" }, - "MinWidth": { + "minWidth": { "type": "integer" }, - "Name": { + "name": { "type": "string" }, - "SignalIDs": { + "signalIDs": { "type": "array", "items": { "type": "integer" } }, - "Type": { + "type": { "type": "string" }, - "Width": { + "width": { "type": "integer" }, - "X": { + "x": { "type": "integer" }, - "Y": { + "y": { "type": "integer" }, - "Z": { + "z": { "type": "integer" } } @@ -4019,43 +4125,43 @@ const docTemplate = `{ "widget.validUpdatedWidget": { "type": "object", "properties": { - "CustomProperties": { + "customProperties": { "$ref": "#/definitions/postgres.Jsonb" }, - "Height": { + "height": { "type": "integer" }, - "IsLocked": { + "isLocked": { "type": "boolean" }, - "MinHeight": { + "minHeight": { "type": "integer" }, - "MinWidth": { + "minWidth": { "type": "integer" }, - "Name": { + "name": { "type": "string" }, - "SignalIDs": { + "signalIDs": { "type": "array", "items": { "type": "integer" } }, - "Type": { + "type": { "type": "string" }, - "Width": { + "width": { "type": "integer" }, - "X": { + "x": { "type": "integer" }, - "Y": { + "y": { "type": "integer" }, - "Z": { + "z": { "type": "integer" } } diff --git a/doc/api/responses.go b/doc/api/responses.go index e071660..9e9b36b 100644 --- a/doc/api/responses.go +++ b/doc/api/responses.go @@ -60,6 +60,10 @@ type ResponseScenario struct { scenario database.Scenario } +type ResponseUserGroup struct { + scenario database.UserGroup +} + type ResponseConfigs struct { configs []database.ComponentConfiguration } diff --git a/doc/api/swagger.json b/doc/api/swagger.json index 69cd77a..a489033 100644 --- a/doc/api/swagger.json +++ b/doc/api/swagger.json @@ -2694,6 +2694,69 @@ } } }, + "/usergroups": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "usergroups" + ], + "summary": "Add a user group", + "operationId": "addUserGroup", + "parameters": [ + { + "description": "User group to be added", + "name": "inputUserGroup", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/usergroup.addUserGroupRequest" + } + } + ], + "responses": { + "200": { + "description": "user group that was added", + "schema": { + "$ref": "#/definitions/api.ResponseUserGroup" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "404": { + "description": "Not found", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "422": { + "description": "Unprocessable entity", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + } + } + } + }, "/users": { "get": { "security": [ @@ -3322,6 +3385,9 @@ "api.ResponseUser": { "type": "object" }, + "api.ResponseUserGroup": { + "type": "object" + }, "api.ResponseUsers": { "type": "object" }, @@ -3350,27 +3416,27 @@ "component_configuration.validNewConfig": { "type": "object", "required": [ - "Name", - "ScenarioID", - "StartParameters" + "name", + "scenarioID", + "startParameters" ], "properties": { - "FileIDs": { + "fileIDs": { "type": "array", "items": { "type": "integer" } }, - "ICID": { + "icid": { "type": "integer" }, - "Name": { + "name": { "type": "string" }, - "ScenarioID": { + "scenarioID": { "type": "integer" }, - "StartParameters": { + "startParameters": { "$ref": "#/definitions/postgres.Jsonb" } } @@ -3378,19 +3444,19 @@ "component_configuration.validUpdatedConfig": { "type": "object", "properties": { - "FileIDs": { + "fileIDs": { "type": "array", "items": { "type": "integer" } }, - "ICID": { + "icid": { "type": "integer" }, - "Name": { + "name": { "type": "string" }, - "StartParameters": { + "startParameters": { "$ref": "#/definitions/postgres.Jsonb" } } @@ -3512,21 +3578,21 @@ "dashboard.validNewDashboard": { "type": "object", "required": [ - "Grid", - "Name", - "ScenarioID" + "grid", + "name", + "scenarioID" ], "properties": { - "Grid": { + "grid": { "type": "integer" }, - "Height": { + "height": { "type": "integer" }, - "Name": { + "name": { "type": "string" }, - "ScenarioID": { + "scenarioID": { "type": "integer" } } @@ -3564,55 +3630,55 @@ "infrastructure_component.validNewIC": { "type": "object", "required": [ - "Category", - "ManagedExternally", - "Name", - "Type" + "category", + "managedExternally", + "name", + "type" ], "properties": { - "APIURL": { + "apiurl": { "type": "string" }, - "Category": { + "category": { "type": "string" }, - "CreateParameterSchema": { + "createParameterSchema": { "$ref": "#/definitions/postgres.Jsonb" }, - "Description": { + "description": { "type": "string" }, - "Location": { + "location": { "type": "string" }, - "ManagedExternally": { + "managedExternally": { "type": "boolean" }, - "Manager": { + "manager": { "type": "string" }, - "Name": { + "name": { "type": "string" }, - "StartParameterSchema": { + "startParameterSchema": { "$ref": "#/definitions/postgres.Jsonb" }, - "State": { + "state": { "type": "string" }, - "StatusUpdateRaw": { + "statusUpdateRaw": { "$ref": "#/definitions/postgres.Jsonb" }, - "Type": { - "type": "string" - }, - "UUID": { + "type": { "type": "string" }, - "Uptime": { + "uptime": { "type": "number" }, - "WebsocketURL": { + "uuid": { + "type": "string" + }, + "websocketURL": { "type": "string" } } @@ -3620,46 +3686,46 @@ "infrastructure_component.validUpdatedIC": { "type": "object", "properties": { - "APIURL": { + "apiurl": { "type": "string" }, - "Category": { + "category": { "type": "string" }, - "CreateParameterSchema": { + "createParameterSchema": { "$ref": "#/definitions/postgres.Jsonb" }, - "Description": { + "description": { "type": "string" }, - "Location": { + "location": { "type": "string" }, - "Manager": { + "manager": { "type": "string" }, - "Name": { + "name": { "type": "string" }, - "StartParameterSchema": { + "startParameterSchema": { "$ref": "#/definitions/postgres.Jsonb" }, - "State": { + "state": { "type": "string" }, - "StatusUpdateRaw": { + "statusUpdateRaw": { "$ref": "#/definitions/postgres.Jsonb" }, - "Type": { - "type": "string" - }, - "UUID": { + "type": { "type": "string" }, - "Uptime": { + "uptime": { "type": "number" }, - "WebsocketURL": { + "uuid": { + "type": "string" + }, + "websocketURL": { "type": "string" } } @@ -3694,23 +3760,23 @@ "result.validNewResult": { "type": "object", "required": [ - "ConfigSnapshots", - "ScenarioID" + "configSnapshots", + "scenarioID" ], "properties": { - "ConfigSnapshots": { + "configSnapshots": { "$ref": "#/definitions/postgres.Jsonb" }, - "Description": { + "description": { "type": "string" }, - "ResultFileIDs": { + "resultFileIDs": { "type": "array", "items": { "type": "integer" } }, - "ScenarioID": { + "scenarioID": { "type": "integer" } } @@ -3751,14 +3817,14 @@ "scenario.validNewScenario": { "type": "object", "required": [ - "Name", - "StartParameters" + "name", + "startParameters" ], "properties": { - "Name": { + "name": { "type": "string" }, - "StartParameters": { + "startParameters": { "$ref": "#/definitions/postgres.Jsonb" } } @@ -3766,13 +3832,13 @@ "scenario.validUpdatedScenario": { "type": "object", "properties": { - "IsLocked": { + "isLocked": { "type": "boolean" }, - "Name": { + "name": { "type": "string" }, - "StartParameters": { + "startParameters": { "$ref": "#/definitions/postgres.Jsonb" } } @@ -3796,32 +3862,32 @@ "signal.validNewSignal": { "type": "object", "required": [ - "ConfigID", - "Direction", - "Index", - "Name" + "configID", + "direction", + "index", + "name" ], "properties": { - "ConfigID": { + "configID": { "type": "integer" }, - "Direction": { + "direction": { "type": "string", "enum": [ "in", "out" ] }, - "Index": { + "index": { "type": "integer" }, - "Name": { + "name": { "type": "string" }, - "ScalingFactor": { + "scalingFactor": { "type": "number" }, - "Unit": { + "unit": { "type": "string" } } @@ -3829,16 +3895,16 @@ "signal.validUpdatedSignal": { "type": "object", "properties": { - "Index": { + "index": { "type": "integer" }, - "Name": { + "name": { "type": "string" }, - "ScalingFactor": { + "scalingFactor": { "type": "number" }, - "Unit": { + "unit": { "type": "string" } } @@ -3854,14 +3920,14 @@ "user.loginRequest": { "type": "object", "required": [ - "Password", - "Username" + "password", + "username" ], "properties": { - "Password": { + "password": { "type": "string" }, - "Username": { + "username": { "type": "string" } } @@ -3877,20 +3943,20 @@ "user.validNewUser": { "type": "object", "required": [ - "Mail", - "Password", - "Role", - "Username" + "mail", + "password", + "role", + "username" ], "properties": { - "Mail": { + "mail": { "type": "string" }, - "Password": { + "password": { "type": "string", "minLength": 6 }, - "Role": { + "role": { "type": "string", "enum": [ "Admin", @@ -3898,7 +3964,7 @@ "Guest" ] }, - "Username": { + "username": { "type": "string", "minLength": 3 } @@ -3907,25 +3973,25 @@ "user.validUpdatedRequest": { "type": "object", "properties": { - "Active": { + "active": { "type": "string", "enum": [ "yes", "no" ] }, - "Mail": { + "mail": { "type": "string" }, - "OldPassword": { + "oldPassword": { "type": "string", "minLength": 6 }, - "Password": { + "password": { "type": "string", "minLength": 6 }, - "Role": { + "role": { "type": "string", "enum": [ "Admin", @@ -3933,12 +3999,52 @@ "Guest" ] }, - "Username": { + "username": { "type": "string", "minLength": 3 } } }, + "usergroup.addUserGroupRequest": { + "type": "object", + "properties": { + "userGroup": { + "$ref": "#/definitions/usergroup.validNewUserGroup" + } + } + }, + "usergroup.validNewScenarioMapping": { + "type": "object", + "required": [ + "scenarioID" + ], + "properties": { + "duplicate": { + "type": "boolean" + }, + "scenarioID": { + "type": "integer" + } + } + }, + "usergroup.validNewUserGroup": { + "type": "object", + "required": [ + "name", + "scenarioMappings" + ], + "properties": { + "name": { + "type": "string" + }, + "scenarioMappings": { + "type": "array", + "items": { + "$ref": "#/definitions/usergroup.validNewScenarioMapping" + } + } + } + }, "widget.addWidgetRequest": { "type": "object", "properties": { @@ -3958,52 +4064,52 @@ "widget.validNewWidget": { "type": "object", "required": [ - "DashboardID", - "Height", - "Type", - "Width" + "dashboardID", + "height", + "type", + "width" ], "properties": { - "CustomProperties": { + "customProperties": { "$ref": "#/definitions/postgres.Jsonb" }, - "DashboardID": { + "dashboardID": { "type": "integer" }, - "Height": { + "height": { "type": "integer" }, - "IsLocked": { + "isLocked": { "type": "boolean" }, - "MinHeight": { + "minHeight": { "type": "integer" }, - "MinWidth": { + "minWidth": { "type": "integer" }, - "Name": { + "name": { "type": "string" }, - "SignalIDs": { + "signalIDs": { "type": "array", "items": { "type": "integer" } }, - "Type": { + "type": { "type": "string" }, - "Width": { + "width": { "type": "integer" }, - "X": { + "x": { "type": "integer" }, - "Y": { + "y": { "type": "integer" }, - "Z": { + "z": { "type": "integer" } } @@ -4011,43 +4117,43 @@ "widget.validUpdatedWidget": { "type": "object", "properties": { - "CustomProperties": { + "customProperties": { "$ref": "#/definitions/postgres.Jsonb" }, - "Height": { + "height": { "type": "integer" }, - "IsLocked": { + "isLocked": { "type": "boolean" }, - "MinHeight": { + "minHeight": { "type": "integer" }, - "MinWidth": { + "minWidth": { "type": "integer" }, - "Name": { + "name": { "type": "string" }, - "SignalIDs": { + "signalIDs": { "type": "array", "items": { "type": "integer" } }, - "Type": { + "type": { "type": "string" }, - "Width": { + "width": { "type": "integer" }, - "X": { + "x": { "type": "integer" }, - "Y": { + "y": { "type": "integer" }, - "Z": { + "z": { "type": "integer" } } diff --git a/doc/api/swagger.yaml b/doc/api/swagger.yaml index e4d08e1..eda18bc 100644 --- a/doc/api/swagger.yaml +++ b/doc/api/swagger.yaml @@ -34,6 +34,8 @@ definitions: type: object api.ResponseUser: type: object + api.ResponseUserGroup: + type: object api.ResponseUsers: type: object api.ResponseWidget: @@ -52,34 +54,34 @@ definitions: type: object component_configuration.validNewConfig: properties: - FileIDs: + fileIDs: items: type: integer type: array - ICID: + icid: type: integer - Name: + name: type: string - ScenarioID: + scenarioID: type: integer - StartParameters: + startParameters: $ref: '#/definitions/postgres.Jsonb' required: - - Name - - ScenarioID - - StartParameters + - name + - scenarioID + - startParameters type: object component_configuration.validUpdatedConfig: properties: - FileIDs: + fileIDs: items: type: integer type: array - ICID: + icid: type: integer - Name: + name: type: string - StartParameters: + startParameters: $ref: '#/definitions/postgres.Jsonb' type: object config.Authentication: @@ -157,18 +159,18 @@ definitions: type: object dashboard.validNewDashboard: properties: - Grid: + grid: type: integer - Height: + height: type: integer - Name: + name: type: string - ScenarioID: + scenarioID: type: integer required: - - Grid - - Name - - ScenarioID + - grid + - name + - scenarioID type: object dashboard.validUpdatedDashboard: properties: @@ -191,71 +193,71 @@ definitions: type: object infrastructure_component.validNewIC: properties: - APIURL: + apiurl: type: string - Category: + category: type: string - CreateParameterSchema: + createParameterSchema: $ref: '#/definitions/postgres.Jsonb' - Description: + description: type: string - Location: + location: type: string - ManagedExternally: + managedExternally: type: boolean - Manager: + manager: type: string - Name: + name: type: string - StartParameterSchema: + startParameterSchema: $ref: '#/definitions/postgres.Jsonb' - State: + state: type: string - StatusUpdateRaw: + statusUpdateRaw: $ref: '#/definitions/postgres.Jsonb' - Type: + type: type: string - UUID: - type: string - Uptime: + uptime: type: number - WebsocketURL: + uuid: + type: string + websocketURL: type: string required: - - Category - - ManagedExternally - - Name - - Type + - category + - managedExternally + - name + - type type: object infrastructure_component.validUpdatedIC: properties: - APIURL: + apiurl: type: string - Category: + category: type: string - CreateParameterSchema: + createParameterSchema: $ref: '#/definitions/postgres.Jsonb' - Description: + description: type: string - Location: + location: type: string - Manager: + manager: type: string - Name: + name: type: string - StartParameterSchema: + startParameterSchema: $ref: '#/definitions/postgres.Jsonb' - State: + state: type: string - StatusUpdateRaw: + statusUpdateRaw: $ref: '#/definitions/postgres.Jsonb' - Type: - type: string - UUID: + type: type: string - Uptime: + uptime: type: number - WebsocketURL: + uuid: + type: string + websocketURL: type: string type: object postgres.Jsonb: @@ -277,19 +279,19 @@ definitions: type: object result.validNewResult: properties: - ConfigSnapshots: + configSnapshots: $ref: '#/definitions/postgres.Jsonb' - Description: + description: type: string - ResultFileIDs: + resultFileIDs: items: type: integer type: array - ScenarioID: + scenarioID: type: integer required: - - ConfigSnapshots - - ScenarioID + - configSnapshots + - scenarioID type: object result.validUpdatedResult: properties: @@ -314,21 +316,21 @@ definitions: type: object scenario.validNewScenario: properties: - Name: + name: type: string - StartParameters: + startParameters: $ref: '#/definitions/postgres.Jsonb' required: - - Name - - StartParameters + - name + - startParameters type: object scenario.validUpdatedScenario: properties: - IsLocked: + isLocked: type: boolean - Name: + name: type: string - StartParameters: + startParameters: $ref: '#/definitions/postgres.Jsonb' type: object signal.addSignalRequest: @@ -343,36 +345,36 @@ definitions: type: object signal.validNewSignal: properties: - ConfigID: + configID: type: integer - Direction: + direction: enum: - in - out type: string - Index: + index: type: integer - Name: + name: type: string - ScalingFactor: + scalingFactor: type: number - Unit: + unit: type: string required: - - ConfigID - - Direction - - Index - - Name + - configID + - direction + - index + - name type: object signal.validUpdatedSignal: properties: - Index: + index: type: integer - Name: + name: type: string - ScalingFactor: + scalingFactor: type: number - Unit: + unit: type: string type: object user.addUserRequest: @@ -382,13 +384,13 @@ definitions: type: object user.loginRequest: properties: - Password: + password: type: string - Username: + username: type: string required: - - Password - - Username + - password + - username type: object user.updateUserRequest: properties: @@ -397,51 +399,77 @@ definitions: type: object user.validNewUser: properties: - Mail: + mail: type: string - Password: + password: minLength: 6 type: string - Role: + role: enum: - Admin - User - Guest type: string - Username: + username: minLength: 3 type: string required: - - Mail - - Password - - Role - - Username + - mail + - password + - role + - username type: object user.validUpdatedRequest: properties: - Active: + active: enum: - "yes" - "no" type: string - Mail: + mail: type: string - OldPassword: + oldPassword: minLength: 6 type: string - Password: + password: minLength: 6 type: string - Role: + role: enum: - Admin - User - Guest type: string - Username: + username: minLength: 3 type: string type: object + usergroup.addUserGroupRequest: + properties: + userGroup: + $ref: '#/definitions/usergroup.validNewUserGroup' + type: object + usergroup.validNewScenarioMapping: + properties: + duplicate: + type: boolean + scenarioID: + type: integer + required: + - scenarioID + type: object + usergroup.validNewUserGroup: + properties: + name: + type: string + scenarioMappings: + items: + $ref: '#/definitions/usergroup.validNewScenarioMapping' + type: array + required: + - name + - scenarioMappings + type: object widget.addWidgetRequest: properties: widget: @@ -454,67 +482,67 @@ definitions: type: object widget.validNewWidget: properties: - CustomProperties: + customProperties: $ref: '#/definitions/postgres.Jsonb' - DashboardID: + dashboardID: type: integer - Height: + height: type: integer - IsLocked: + isLocked: type: boolean - MinHeight: + minHeight: type: integer - MinWidth: + minWidth: type: integer - Name: + name: type: string - SignalIDs: + signalIDs: items: type: integer type: array - Type: + type: type: string - Width: + width: type: integer - X: + x: type: integer - "Y": + "y": type: integer - Z: + z: type: integer required: - - DashboardID - - Height - - Type - - Width + - dashboardID + - height + - type + - width type: object widget.validUpdatedWidget: properties: - CustomProperties: + customProperties: $ref: '#/definitions/postgres.Jsonb' - Height: + height: type: integer - IsLocked: + isLocked: type: boolean - MinHeight: + minHeight: type: integer - MinWidth: + minWidth: type: integer - Name: + name: type: string - SignalIDs: + signalIDs: items: type: integer type: array - Type: + type: type: string - Width: + width: type: integer - X: + x: type: integer - "Y": + "y": type: integer - Z: + z: type: integer type: object info: @@ -2272,6 +2300,46 @@ paths: summary: Update a signal tags: - signals + /usergroups: + post: + consumes: + - application/json + operationId: addUserGroup + parameters: + - description: User group to be added + in: body + name: inputUserGroup + required: true + schema: + $ref: '#/definitions/usergroup.addUserGroupRequest' + produces: + - application/json + responses: + "200": + description: user group that was added + schema: + $ref: '#/definitions/api.ResponseUserGroup' + "400": + description: Bad request + schema: + $ref: '#/definitions/api.ResponseError' + "404": + description: Not found + schema: + $ref: '#/definitions/api.ResponseError' + "422": + description: Unprocessable entity + schema: + $ref: '#/definitions/api.ResponseError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api.ResponseError' + security: + - Bearer: [] + summary: Add a user group + tags: + - usergroups /users: get: operationId: GetUsers diff --git a/routes/register.go b/routes/register.go index 38adf9a..73b5b69 100644 --- a/routes/register.go +++ b/routes/register.go @@ -43,6 +43,7 @@ import ( "git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/scenario" "git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/signal" "git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/user" + "git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/usergroup" "git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/widget" "github.com/gin-gonic/gin" "github.com/zpatrick/go-config" @@ -84,6 +85,7 @@ func RegisterEndpoints(router *gin.Engine, api *gin.RouterGroup) { api.Use(user.Authentication()) scenario.RegisterScenarioEndpoints(api.Group("/scenarios")) + usergroup.RegisterUserGroupEndpoints(api.Group("/usergroups")) component_configuration.RegisterComponentConfigurationEndpoints(api.Group("/configs")) signal.RegisterSignalEndpoints(api.Group("/signals")) dashboard.RegisterDashboardEndpoints(api.Group("/dashboards")) From 69d898b00918e7953e257712769b66e96a6b7ddb Mon Sep 17 00:00:00 2001 From: iripiri Date: Fri, 30 Aug 2024 19:24:04 +0200 Subject: [PATCH 02/21] [WIP] add usergroup endpoints & tests Signed-off-by: iripiri --- database/database.go | 4 + database/models.go | 16 +- database/permissions.go | 11 +- doc/api/docs.go | 260 ++++++++++++++++++++++- doc/api/responses.go | 6 +- doc/api/swagger.json | 260 ++++++++++++++++++++++- doc/api/swagger.yaml | 165 +++++++++++++- routes/usergroup/usergroup_endpoints.go | 198 +++++++++++++++++ routes/usergroup/usergroup_methods.go | 113 ++++++++++ routes/usergroup/usergroup_test.go | 105 +++++++++ routes/usergroup/usergroup_validators.go | 94 ++++++++ 11 files changed, 1215 insertions(+), 17 deletions(-) create mode 100644 routes/usergroup/usergroup_endpoints.go create mode 100644 routes/usergroup/usergroup_methods.go create mode 100644 routes/usergroup/usergroup_test.go create mode 100644 routes/usergroup/usergroup_validators.go diff --git a/database/database.go b/database/database.go index 79ec7bd..7d1c45f 100644 --- a/database/database.go +++ b/database/database.go @@ -105,6 +105,8 @@ func DropTables() { DBpool.DropTableIfExists(&File{}) DBpool.DropTableIfExists(&Scenario{}) DBpool.DropTableIfExists(&User{}) + DBpool.DropTableIfExists(&UserGroup{}) + DBpool.DropTableIfExists(&ScenarioMapping{}) DBpool.DropTableIfExists(&Dashboard{}) DBpool.DropTableIfExists(&Widget{}) DBpool.DropTableIfExists(&Result{}) @@ -120,6 +122,8 @@ func MigrateModels() { DBpool.AutoMigrate(&File{}) DBpool.AutoMigrate(&Scenario{}) DBpool.AutoMigrate(&User{}) + DBpool.AutoMigrate(&UserGroup{}) + DBpool.AutoMigrate(&ScenarioMapping{}) DBpool.AutoMigrate(&Dashboard{}) DBpool.AutoMigrate(&Widget{}) DBpool.AutoMigrate(&Result{}) diff --git a/database/models.go b/database/models.go index bd65d2b..83037d7 100644 --- a/database/models.go +++ b/database/models.go @@ -44,20 +44,22 @@ type User struct { Mail string `json:"mail" gorm:"default:''"` // Role of user Role string `json:"role" gorm:"default:'user'"` - // Group of user by which the user is added to scenarios - Group string `json:"group" gorm:"default:''"` // Indicating status of user (false means user is inactive and should not be able to login) Active bool `json:"active" gorm:"default:true"` // Scenarios to which user has access Scenarios []*Scenario `json:"-" gorm:"many2many:user_scenarios;"` + // Groups of user + UserGroup []*UserGroup `json:"-" gorm:"many2many:user_groups;"` } // ScenarioMapping data model type ScenarioMapping struct { Model - // ID of Scenario - ScenarioID uint `json:"scenarioID"` - // Duplicate Scenario or add to existing Scenario + ScenarioID uint `json:"scenarioID" ` // Foreign key to Scenario + Scenario Scenario `json:"-" gorm:"foreignkey:ScenarioID"` + UserGroupID uint `json:"-"` // Foreign key to UserGroup + UserGroup UserGroup `json:"-" gorm:"foreignkey:UserGroupID"` + // Whether to duplicate Scenario or add users to existing Scenario Duplicate bool `json:"duplicate"` } @@ -67,7 +69,9 @@ type UserGroup struct { // Name of user group Name string `json:"name" gorm:"unique;not null"` // Scenarios that belong to the user group - ScenarioMappings []*ScenarioMapping `json:"scenarioMappings" gorm:"foreignkey:UserGroupID"` + ScenarioMappings []ScenarioMapping `json:"scenarioMappings" gorm:"foreignkey:UserGroupID"` + // Users that belong to the user group + Users []*User `json:"users" gorm:"many2many:user_groups;"` } // Scenario data model diff --git a/database/permissions.go b/database/permissions.go index 924a028..30bdab7 100644 --- a/database/permissions.go +++ b/database/permissions.go @@ -142,7 +142,7 @@ func CheckSignalPermissions(c *gin.Context, operation CRUD) (bool, Signal) { } -func CheckUserGroupPermissions(c *gin.Context, operation CRUD, userGroupSourceID string, dabIDBody int) (bool, UserGroup) { +func CheckUserGroupPermissions(c *gin.Context, operation CRUD, userGroupIDSource string, usergroupIDBody int) (bool, UserGroup) { var usrgrp UserGroup @@ -152,13 +152,18 @@ func CheckUserGroupPermissions(c *gin.Context, operation CRUD, userGroupSourceID return false, usrgrp } - groupID, err := helper.GetIDOfElement(c, "usergroupID", userGroupSourceID, dabIDBody) + if operation == Create { + return true, usrgrp + } + + groupID, err := helper.GetIDOfElement(c, "userGroupID", userGroupIDSource, usergroupIDBody) if err != nil { return false, usrgrp } db := GetDB() - err = db.Find(&usrgrp, uint(groupID)).Error + err = db.Preload("ScenarioMappings.Scenario").First(&usrgrp, groupID).Error + if helper.DBNotFoundError(c, err, strconv.Itoa(groupID), "UserGroup") { return false, usrgrp } diff --git a/doc/api/docs.go b/doc/api/docs.go index 1abda89..a2dba6f 100644 --- a/doc/api/docs.go +++ b/doc/api/docs.go @@ -2703,6 +2703,47 @@ const docTemplate = `{ } }, "/usergroups": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "usergroups" + ], + "summary": "Get all user groups", + "operationId": "getUserGroups", + "responses": { + "200": { + "description": "List of user groups", + "schema": { + "$ref": "#/definitions/api.ResponseUserGroups" + } + }, + "404": { + "description": "Not found", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "422": { + "description": "Unprocessable entity", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + } + } + }, "post": { "security": [ { @@ -2765,6 +2806,182 @@ const docTemplate = `{ } } }, + "/usergroups/{usergroupID}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "usergroups" + ], + "summary": "Get user group by ID", + "operationId": "getUserGroup", + "parameters": [ + { + "type": "integer", + "description": "User group ID", + "name": "usergroupID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "requested user group", + "schema": { + "$ref": "#/definitions/api.ResponseUserGroup" + } + }, + "403": { + "description": "Access forbidden.", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "404": { + "description": "Not found", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "422": { + "description": "Unprocessable entity", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "usergroups" + ], + "summary": "Update a user group", + "operationId": "updateUserGroup", + "parameters": [ + { + "description": "User group to be updated", + "name": "inputUserGroup", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/usergroup.updateUserGroupRequest" + } + }, + { + "type": "integer", + "description": "User group ID", + "name": "usergroupID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "User group that was updated", + "schema": { + "$ref": "#/definitions/api.ResponseUserGroup" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "404": { + "description": "Not found", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "422": { + "description": "Unprocessable entity", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "usergroups" + ], + "summary": "Delete a user group", + "operationId": "deleteUserGroup", + "parameters": [ + { + "type": "integer", + "description": "User group ID", + "name": "usergroupID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "deleted user group", + "schema": { + "$ref": "#/definitions/api.ResponseUserGroup" + } + }, + "404": { + "description": "Not found", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "422": { + "description": "Unprocessable entity", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + } + } + } + }, "/users": { "get": { "security": [ @@ -3396,6 +3613,9 @@ const docTemplate = `{ "api.ResponseUserGroup": { "type": "object" }, + "api.ResponseUserGroups": { + "type": "object" + }, "api.ResponseUsers": { "type": "object" }, @@ -4021,6 +4241,14 @@ const docTemplate = `{ } } }, + "usergroup.updateUserGroupRequest": { + "type": "object", + "properties": { + "userGroup": { + "$ref": "#/definitions/usergroup.validUpdatedUserGroup" + } + } + }, "usergroup.validNewScenarioMapping": { "type": "object", "required": [ @@ -4038,12 +4266,13 @@ const docTemplate = `{ "usergroup.validNewUserGroup": { "type": "object", "required": [ - "name", - "scenarioMappings" + "name" ], "properties": { "name": { - "type": "string" + "type": "string", + "maxLength": 100, + "minLength": 3 }, "scenarioMappings": { "type": "array", @@ -4053,6 +4282,31 @@ const docTemplate = `{ } } }, + "usergroup.validUpdatedScenarioMapping": { + "type": "object", + "properties": { + "duplicate": { + "type": "boolean" + }, + "scenarioID": { + "type": "integer" + } + } + }, + "usergroup.validUpdatedUserGroup": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "scenarioMappings": { + "type": "array", + "items": { + "$ref": "#/definitions/usergroup.validUpdatedScenarioMapping" + } + } + } + }, "widget.addWidgetRequest": { "type": "object", "properties": { diff --git a/doc/api/responses.go b/doc/api/responses.go index 9e9b36b..66f5aaa 100644 --- a/doc/api/responses.go +++ b/doc/api/responses.go @@ -61,7 +61,11 @@ type ResponseScenario struct { } type ResponseUserGroup struct { - scenario database.UserGroup + usergroup database.UserGroup +} + +type ResponseUserGroups struct { + usergroup []database.UserGroup } type ResponseConfigs struct { diff --git a/doc/api/swagger.json b/doc/api/swagger.json index a489033..8ea7f23 100644 --- a/doc/api/swagger.json +++ b/doc/api/swagger.json @@ -2695,6 +2695,47 @@ } }, "/usergroups": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "usergroups" + ], + "summary": "Get all user groups", + "operationId": "getUserGroups", + "responses": { + "200": { + "description": "List of user groups", + "schema": { + "$ref": "#/definitions/api.ResponseUserGroups" + } + }, + "404": { + "description": "Not found", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "422": { + "description": "Unprocessable entity", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + } + } + }, "post": { "security": [ { @@ -2757,6 +2798,182 @@ } } }, + "/usergroups/{usergroupID}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "usergroups" + ], + "summary": "Get user group by ID", + "operationId": "getUserGroup", + "parameters": [ + { + "type": "integer", + "description": "User group ID", + "name": "usergroupID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "requested user group", + "schema": { + "$ref": "#/definitions/api.ResponseUserGroup" + } + }, + "403": { + "description": "Access forbidden.", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "404": { + "description": "Not found", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "422": { + "description": "Unprocessable entity", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "usergroups" + ], + "summary": "Update a user group", + "operationId": "updateUserGroup", + "parameters": [ + { + "description": "User group to be updated", + "name": "inputUserGroup", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/usergroup.updateUserGroupRequest" + } + }, + { + "type": "integer", + "description": "User group ID", + "name": "usergroupID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "User group that was updated", + "schema": { + "$ref": "#/definitions/api.ResponseUserGroup" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "404": { + "description": "Not found", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "422": { + "description": "Unprocessable entity", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "usergroups" + ], + "summary": "Delete a user group", + "operationId": "deleteUserGroup", + "parameters": [ + { + "type": "integer", + "description": "User group ID", + "name": "usergroupID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "deleted user group", + "schema": { + "$ref": "#/definitions/api.ResponseUserGroup" + } + }, + "404": { + "description": "Not found", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "422": { + "description": "Unprocessable entity", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + } + } + } + }, "/users": { "get": { "security": [ @@ -3388,6 +3605,9 @@ "api.ResponseUserGroup": { "type": "object" }, + "api.ResponseUserGroups": { + "type": "object" + }, "api.ResponseUsers": { "type": "object" }, @@ -4013,6 +4233,14 @@ } } }, + "usergroup.updateUserGroupRequest": { + "type": "object", + "properties": { + "userGroup": { + "$ref": "#/definitions/usergroup.validUpdatedUserGroup" + } + } + }, "usergroup.validNewScenarioMapping": { "type": "object", "required": [ @@ -4030,12 +4258,13 @@ "usergroup.validNewUserGroup": { "type": "object", "required": [ - "name", - "scenarioMappings" + "name" ], "properties": { "name": { - "type": "string" + "type": "string", + "maxLength": 100, + "minLength": 3 }, "scenarioMappings": { "type": "array", @@ -4045,6 +4274,31 @@ } } }, + "usergroup.validUpdatedScenarioMapping": { + "type": "object", + "properties": { + "duplicate": { + "type": "boolean" + }, + "scenarioID": { + "type": "integer" + } + } + }, + "usergroup.validUpdatedUserGroup": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "scenarioMappings": { + "type": "array", + "items": { + "$ref": "#/definitions/usergroup.validUpdatedScenarioMapping" + } + } + } + }, "widget.addWidgetRequest": { "type": "object", "properties": { diff --git a/doc/api/swagger.yaml b/doc/api/swagger.yaml index eda18bc..00e0e57 100644 --- a/doc/api/swagger.yaml +++ b/doc/api/swagger.yaml @@ -36,6 +36,8 @@ definitions: type: object api.ResponseUserGroup: type: object + api.ResponseUserGroups: + type: object api.ResponseUsers: type: object api.ResponseWidget: @@ -449,6 +451,11 @@ definitions: userGroup: $ref: '#/definitions/usergroup.validNewUserGroup' type: object + usergroup.updateUserGroupRequest: + properties: + userGroup: + $ref: '#/definitions/usergroup.validUpdatedUserGroup' + type: object usergroup.validNewScenarioMapping: properties: duplicate: @@ -461,6 +468,8 @@ definitions: usergroup.validNewUserGroup: properties: name: + maxLength: 100 + minLength: 3 type: string scenarioMappings: items: @@ -468,7 +477,22 @@ definitions: type: array required: - name - - scenarioMappings + type: object + usergroup.validUpdatedScenarioMapping: + properties: + duplicate: + type: boolean + scenarioID: + type: integer + type: object + usergroup.validUpdatedUserGroup: + properties: + name: + type: string + scenarioMappings: + items: + $ref: '#/definitions/usergroup.validUpdatedScenarioMapping' + type: array type: object widget.addWidgetRequest: properties: @@ -2301,6 +2325,32 @@ paths: tags: - signals /usergroups: + get: + operationId: getUserGroups + produces: + - application/json + responses: + "200": + description: List of user groups + schema: + $ref: '#/definitions/api.ResponseUserGroups' + "404": + description: Not found + schema: + $ref: '#/definitions/api.ResponseError' + "422": + description: Unprocessable entity + schema: + $ref: '#/definitions/api.ResponseError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api.ResponseError' + security: + - Bearer: [] + summary: Get all user groups + tags: + - usergroups post: consumes: - application/json @@ -2340,6 +2390,119 @@ paths: summary: Add a user group tags: - usergroups + /usergroups/{usergroupID}: + delete: + operationId: deleteUserGroup + parameters: + - description: User group ID + in: path + name: usergroupID + required: true + type: integer + produces: + - application/json + responses: + "200": + description: deleted user group + schema: + $ref: '#/definitions/api.ResponseUserGroup' + "404": + description: Not found + schema: + $ref: '#/definitions/api.ResponseError' + "422": + description: Unprocessable entity + schema: + $ref: '#/definitions/api.ResponseError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api.ResponseError' + security: + - Bearer: [] + summary: Delete a user group + tags: + - usergroups + get: + operationId: getUserGroup + parameters: + - description: User group ID + in: path + name: usergroupID + required: true + type: integer + produces: + - application/json + responses: + "200": + description: requested user group + schema: + $ref: '#/definitions/api.ResponseUserGroup' + "403": + description: Access forbidden. + schema: + $ref: '#/definitions/api.ResponseError' + "404": + description: Not found + schema: + $ref: '#/definitions/api.ResponseError' + "422": + description: Unprocessable entity + schema: + $ref: '#/definitions/api.ResponseError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api.ResponseError' + security: + - Bearer: [] + summary: Get user group by ID + tags: + - usergroups + put: + consumes: + - application/json + operationId: updateUserGroup + parameters: + - description: User group to be updated + in: body + name: inputUserGroup + required: true + schema: + $ref: '#/definitions/usergroup.updateUserGroupRequest' + - description: User group ID + in: path + name: usergroupID + required: true + type: integer + produces: + - application/json + responses: + "200": + description: User group that was updated + schema: + $ref: '#/definitions/api.ResponseUserGroup' + "400": + description: Bad request + schema: + $ref: '#/definitions/api.ResponseError' + "404": + description: Not found + schema: + $ref: '#/definitions/api.ResponseError' + "422": + description: Unprocessable entity + schema: + $ref: '#/definitions/api.ResponseError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api.ResponseError' + security: + - Bearer: [] + summary: Update a user group + tags: + - usergroups /users: get: operationId: GetUsers diff --git a/routes/usergroup/usergroup_endpoints.go b/routes/usergroup/usergroup_endpoints.go new file mode 100644 index 0000000..b9e6a14 --- /dev/null +++ b/routes/usergroup/usergroup_endpoints.go @@ -0,0 +1,198 @@ +/** +* This file is part of VILLASweb-backend-go +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*********************************************************************************/ + +package usergroup + +import ( + "net/http" + + "git.rwth-aachen.de/acs/public/villas/web-backend-go/database" + "git.rwth-aachen.de/acs/public/villas/web-backend-go/helper" + "github.com/gin-gonic/gin" +) + +func RegisterUserGroupEndpoints(r *gin.RouterGroup) { + r.POST("", addUserGroup) + r.PUT("/:userGroupID", updateUserGroup) + r.GET("", getUserGroups) + r.GET("/:userGroupID", getUserGroup) + r.DELETE("/:userGroupID", deleteUserGroup) +} + +// addUserGroup godoc +// @Summary Add a user group +// @ID addUserGroup +// @Accept json +// @Produce json +// @Tags usergroups +// @Success 200 {object} api.ResponseUserGroup "user group that was added" +// @Failure 400 {object} api.ResponseError "Bad request" +// @Failure 404 {object} api.ResponseError "Not found" +// @Failure 422 {object} api.ResponseError "Unprocessable entity" +// @Failure 500 {object} api.ResponseError "Internal server error" +// @Param inputUserGroup body usergroup.addUserGroupRequest true "User group to be added" +// @Router /usergroups [post] +// @Security Bearer +func addUserGroup(c *gin.Context) { + ok, _ := database.CheckUserGroupPermissions(c, database.Create, "none", -1) + if !ok { + return + } + + var req addUserGroupRequest + if err := c.ShouldBindJSON(&req); err != nil { + helper.BadRequestError(c, err.Error()) + return + } + + // Validate the request + if err := req.validate(); err != nil { + helper.UnprocessableEntityError(c, err.Error()) + return + } + + // Create the new user group from the request + newUserGroup := req.createUserGroup() + + // Save the new user group to the database + err := newUserGroup.save() + if !helper.DBError(c, err) { + c.JSON(http.StatusOK, gin.H{"usergroup": newUserGroup.UserGroup}) + } +} + +// updateUserGroup godoc +// @Summary Update a user group +// @ID updateUserGroup +// @Tags usergroups +// @Accept json +// @Produce json +// @Success 200 {object} api.ResponseUserGroup "User group that was updated" +// @Failure 400 {object} api.ResponseError "Bad request" +// @Failure 404 {object} api.ResponseError "Not found" +// @Failure 422 {object} api.ResponseError "Unprocessable entity" +// @Failure 500 {object} api.ResponseError "Internal server error" +// @Param inputUserGroup body usergroup.updateUserGroupRequest true "User group to be updated" +// @Param usergroupID path int true "User group ID" +// @Router /usergroups/{usergroupID} [put] +// @Security Bearer +func updateUserGroup(c *gin.Context) { + ok, oldUserGroup_r := database.CheckUserGroupPermissions(c, database.Update, "path", -1) + if !ok { + return + } + + var req updateUserGroupRequest + if err := c.ShouldBindJSON(&req); err != nil { + helper.BadRequestError(c, err.Error()) + return + } + + if err := req.UserGroup.validate(); err != nil { + helper.BadRequestError(c, err.Error()) + return + } + + var oldUserGroup UserGroup + oldUserGroup.UserGroup = oldUserGroup_r + updatedUserGroup := req.updatedUserGroup(oldUserGroup) + + // update the user group in the database + err := oldUserGroup.update(updatedUserGroup, req.UserGroup.ScenarioMappings) + if !helper.DBError(c, err) { + c.JSON(http.StatusOK, gin.H{"usergroup": updatedUserGroup.UserGroup}) + } +} + +// getUserGroups godoc +// @Summary Get all user groups +// @ID getUserGroups +// @Produce json +// @Tags usergroups +// @Success 200 {object} api.ResponseUserGroups "List of user groups" +// @Failure 404 {object} api.ResponseError "Not found" +// @Failure 422 {object} api.ResponseError "Unprocessable entity" +// @Failure 500 {object} api.ResponseError "Internal server error" +// @Router /usergroups [get] +// @Security Bearer +func getUserGroups(c *gin.Context) { + + err := database.ValidateRole(c, database.ModelUserGroup, database.Read) + if err != nil { + helper.UnprocessableEntityError(c, err.Error()) + return + } + + db := database.GetDB() + var usergroups []database.UserGroup + err = db.Preload("ScenarioMappings.Scenario").Order("ID asc").Find(&usergroups).Error + if !helper.DBError(c, err) { + c.JSON(http.StatusOK, gin.H{"usergroups": usergroups}) + } +} + +// getUserGroup godoc +// @Summary Get user group by ID +// @ID getUserGroup +// @Produce json +// @Tags usergroups +// @Success 200 {object} api.ResponseUserGroup "requested user group" +// @Failure 403 {object} api.ResponseError "Access forbidden." +// @Failure 404 {object} api.ResponseError "Not found" +// @Failure 422 {object} api.ResponseError "Unprocessable entity" +// @Failure 500 {object} api.ResponseError "Internal server error" +// @Param usergroupID path int true "User group ID" +// @Router /usergroups/{usergroupID} [get] +// @Security Bearer +func getUserGroup(c *gin.Context) { + ok, ug := database.CheckUserGroupPermissions(c, database.Read, "path", -1) + if !ok { + return + } + + c.JSON(http.StatusOK, gin.H{"usergroup": ug}) +} + +// deleteUserGroup godoc +// @Summary Delete a user group +// @ID deleteUserGroup +// @Tags usergroups +// @Produce json +// @Success 200 {object} api.ResponseUserGroup "deleted user group" +// @Failure 404 {object} api.ResponseError "Not found" +// @Failure 422 {object} api.ResponseError "Unprocessable entity" +// @Failure 500 {object} api.ResponseError "Internal server error" +// @Param usergroupID path int true "User group ID" +// @Router /usergroups/{usergroupID} [delete] +// @Security Bearer +func deleteUserGroup(c *gin.Context) { + + ok, ug_r := database.CheckUserGroupPermissions(c, database.Delete, "path", -1) + if !ok { + return + } + + var ug UserGroup + ug.UserGroup = ug_r + + // Try to remove user group + err := ug.remove() + if !helper.DBError(c, err) { + c.JSON(http.StatusOK, gin.H{"usergroup": ug}) + } + +} diff --git a/routes/usergroup/usergroup_methods.go b/routes/usergroup/usergroup_methods.go new file mode 100644 index 0000000..c999eb1 --- /dev/null +++ b/routes/usergroup/usergroup_methods.go @@ -0,0 +1,113 @@ +/** +* This file is part of VILLASweb-backend-go +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*********************************************************************************/ + +package usergroup + +import ( + "git.rwth-aachen.de/acs/public/villas/web-backend-go/database" +) + +type UserGroup struct { + database.UserGroup +} + +type ScenarioMapping struct { + database.ScenarioMapping +} + +func (ug *UserGroup) save() error { + db := database.GetDB() + err := db.Create(ug).Error + return err +} + +func (ug *UserGroup) update(updatedUserGroup UserGroup, reqScenarioMappings []validUpdatedScenarioMapping) error { + ug.Name = updatedUserGroup.Name + + db := database.GetDB() + err := db.Model(ug).Update(updatedUserGroup).Error + if err != nil { + return err + } + /* + err = db.Model(ug).Updates(database.UserGroup{Name: ug.Name}).Error + if err != nil { + return err + } + */ + + return updateScenarioMappings(ug.ID, reqScenarioMappings) +} + +func updateScenarioMappings(groupID uint, reqScenarioMappings []validUpdatedScenarioMapping) error { + var oldMappings []database.ScenarioMapping + db := database.GetDB() + err := db.Where("user_group_id = ?", groupID).Find(&oldMappings).Error + if err != nil { + return err + } + + oldMappingsMap := make(map[uint]database.ScenarioMapping) + for _, mapping := range oldMappings { + oldMappingsMap[mapping.ScenarioID] = mapping + } + + // Handle ScenarioMappings (add/update/delete) + for _, reqMapping := range reqScenarioMappings { + if oldMapping, exists := oldMappingsMap[reqMapping.ScenarioID]; exists { + // Update + oldMapping.Duplicate = reqMapping.Duplicate + err = db.Save(&oldMapping).Error + if err != nil { + return err + } + delete(oldMappingsMap, reqMapping.ScenarioID) + } else { + // Add + newMapping := database.ScenarioMapping{ + ScenarioID: reqMapping.ScenarioID, + UserGroupID: groupID, + Duplicate: reqMapping.Duplicate, + } + err = db.Create(&newMapping).Error + if err != nil { + return err + } + } + } + + // Delete old mappings that were not in the request + for _, mapping := range oldMappingsMap { + err = db.Delete(&mapping).Error + if err != nil { + return err + } + } + return nil +} + +func (ug *UserGroup) byID(id uint) error { + db := database.GetDB() + err := db.Find(ug, id).Error + return err +} + +func (u *UserGroup) remove() error { + db := database.GetDB() + err := db.Delete(u).Error + return err +} diff --git a/routes/usergroup/usergroup_test.go b/routes/usergroup/usergroup_test.go new file mode 100644 index 0000000..7a884c9 --- /dev/null +++ b/routes/usergroup/usergroup_test.go @@ -0,0 +1,105 @@ +/** +* This file is part of VILLASweb-backend-go +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*********************************************************************************/ + +package usergroup + +import ( + "os" + "testing" + + "git.rwth-aachen.de/acs/public/villas/web-backend-go/configuration" + "git.rwth-aachen.de/acs/public/villas/web-backend-go/database" + "git.rwth-aachen.de/acs/public/villas/web-backend-go/helper" + "git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/user" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +var router *gin.Engine + +type ScenarioMappingRequest struct { + ScenarioID uint `json:"scenarioID"` + Duplicate bool `json:"duplicate"` +} + +type UserGroupRequest struct { + Name string `json:"name"` + ScenarioMappings []ScenarioMappingRequest `json:"scenarioMappings"` +} + +var newUserGroupOneMapping = UserGroupRequest{ + Name: "UserGroup1", + ScenarioMappings: []ScenarioMappingRequest{ + { + ScenarioID: 1, + Duplicate: false, + }, + }, +} + +func TestMain(m *testing.M) { + err := configuration.InitConfig() + if err != nil { + panic(m) + } + + err = database.InitDB(configuration.GlobalConfig, true) + if err != nil { + panic(m) + } + defer database.DBpool.Close() + + router = gin.Default() + api := router.Group("/api/v2") + + user.RegisterAuthenticate(api.Group("/authenticate")) + api.Use(user.Authentication()) + + // user endpoints required to set user to inactive + user.RegisterUserEndpoints(api.Group("/users")) + RegisterUserGroupEndpoints(api.Group("/usergroups")) + + os.Exit(m.Run()) +} + +func TestAddUserGroup(t *testing.T) { + + database.DropTables() + database.MigrateModels() + assert.NoError(t, database.AddTestUsers()) + + token, err := helper.AuthenticateForTest(router, database.AdminCredentials) + assert.NoError(t, err) + + // try to POST with non JSON body + // should return a bad request error + code, resp, err := helper.TestEndpoint(router, token, + "/api/v2/usergroups", "POST", "this is not a JSON") + assert.NoError(t, err) + assert.Equalf(t, 400, code, "Response body: \n%v\n", resp) + + // Test with valid user group + code, resp, err = helper.TestEndpoint(router, token, "/api/v2/usergroups", + "POST", helper.KeyModels{"usergroup": newUserGroupOneMapping}) + assert.NoError(t, err) + assert.Equalf(t, 200, code, "Response body: \n%v\n", resp) + // Test with valid user group and one mapping + // Test with valid user group and multiple mappings + // Test with invalid user group + // Test with invalid user group and one mapping + // Test with invalid user group and multiple mappings +} diff --git a/routes/usergroup/usergroup_validators.go b/routes/usergroup/usergroup_validators.go new file mode 100644 index 0000000..0186545 --- /dev/null +++ b/routes/usergroup/usergroup_validators.go @@ -0,0 +1,94 @@ +/** +* This file is part of VILLASweb-backend-go +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*********************************************************************************/ + +package usergroup + +import ( + "git.rwth-aachen.de/acs/public/villas/web-backend-go/database" + "gopkg.in/go-playground/validator.v9" +) + +var validate *validator.Validate + +type validNewUserGroup struct { + Name string `form:"name" validate:"required,min=3,max=100"` + ScenarioMappings []validNewScenarioMapping `form:"scenarioMappings" validate:"dive"` +} + +type validNewScenarioMapping struct { + ScenarioID uint `form:"scenario_id" validate:"required"` + Duplicate bool `form:"duplicate" validate:"omitempty"` +} + +type validUpdatedUserGroup struct { + Name string `form:"name" validate:"omitempty"` + ScenarioMappings []validUpdatedScenarioMapping `form:"scenarioMappings" validate:"omitempty"` +} + +type validUpdatedScenarioMapping struct { + ScenarioID uint `form:"scenarioID" validate:"omitempty"` + Duplicate bool `form:"duplicate" validate:"omitempty"` +} + +type addUserGroupRequest struct { + UserGroup validNewUserGroup `json:"userGroup"` +} + +type updateUserGroupRequest struct { + UserGroup validUpdatedUserGroup `json:"userGroup"` +} + +func (r *addUserGroupRequest) validate() error { + validate = validator.New() + errs := validate.Struct(r) + return errs +} + +func (r *validUpdatedUserGroup) validate() error { + validate = validator.New() + errs := validate.Struct(r) + return errs +} + +func (r *addUserGroupRequest) createUserGroup() UserGroup { + var ug UserGroup + ug.Name = r.UserGroup.Name + ug.ScenarioMappings = convertScenarioMappings(r.UserGroup.ScenarioMappings) + return ug +} + +func convertScenarioMappings(validMappings []validNewScenarioMapping) []database.ScenarioMapping { + scenarioMappings := make([]database.ScenarioMapping, len(validMappings)) + for i, v := range validMappings { + scenarioMappings[i] = database.ScenarioMapping{ + ScenarioID: v.ScenarioID, + Duplicate: v.Duplicate, + } + } + return scenarioMappings +} + +func (r *updateUserGroupRequest) updatedUserGroup(oldUserGroup UserGroup) UserGroup { + // Use the old UserGroup as a basis for the updated UserGroup `ug` + ug := oldUserGroup + + if r.UserGroup.Name != "string" && r.UserGroup.Name != "" { + ug.Name = r.UserGroup.Name + } + + return ug +} From 55f061014ec708248c8295a63d3b0836382c8e6c Mon Sep 17 00:00:00 2001 From: iripiri Date: Mon, 2 Sep 2024 18:04:45 +0200 Subject: [PATCH 03/21] Added endpoints for usergroup, duplicate scenario(s) when adding user to usergroup Signed-off-by: iripiri --- database/models.go | 4 +- doc/api/docs.go | 168 ++++++++++++++++++++++++ doc/api/swagger.json | 168 ++++++++++++++++++++++++ doc/api/swagger.yaml | 108 +++++++++++++++ routes/user/authenticate_endpoint.go | 6 +- routes/user/scenario_duplication.go | 2 +- routes/usergroup/usergroup_endpoints.go | 146 ++++++++++++++++++++ routes/usergroup/usergroup_methods.go | 43 ++++++ routes/usergroup/usergroup_test.go | 24 +++- 9 files changed, 661 insertions(+), 8 deletions(-) diff --git a/database/models.go b/database/models.go index 83037d7..e8afd05 100644 --- a/database/models.go +++ b/database/models.go @@ -49,7 +49,7 @@ type User struct { // Scenarios to which user has access Scenarios []*Scenario `json:"-" gorm:"many2many:user_scenarios;"` // Groups of user - UserGroup []*UserGroup `json:"-" gorm:"many2many:user_groups;"` + UserGroups []*UserGroup `json:"-" gorm:"many2many:user_groups_users;"` } // ScenarioMapping data model @@ -71,7 +71,7 @@ type UserGroup struct { // Scenarios that belong to the user group ScenarioMappings []ScenarioMapping `json:"scenarioMappings" gorm:"foreignkey:UserGroupID"` // Users that belong to the user group - Users []*User `json:"users" gorm:"many2many:user_groups;"` + Users []*User `json:"users" gorm:"many2many:user_groups_users;"` } // Scenario data model diff --git a/doc/api/docs.go b/doc/api/docs.go index a2dba6f..295dcdb 100644 --- a/doc/api/docs.go +++ b/doc/api/docs.go @@ -2982,6 +2982,174 @@ const docTemplate = `{ } } }, + "/usergroups/{usergroupID}/user": { + "put": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "usergroups" + ], + "summary": "Add a user to a a user group", + "operationId": "addUserToUserGroup", + "parameters": [ + { + "type": "integer", + "description": "User group ID", + "name": "usergroupID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "User name", + "name": "username", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "User that was added to user group", + "schema": { + "$ref": "#/definitions/api.ResponseUser" + } + }, + "404": { + "description": "Not found", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "422": { + "description": "Unprocessable entity", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "usergroups" + ], + "summary": "Delete a user from a user group", + "operationId": "deleteUserFromUserGroup", + "parameters": [ + { + "type": "integer", + "description": "User group ID", + "name": "usergroupID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "User name", + "name": "username", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "User that was deleted from user group", + "schema": { + "$ref": "#/definitions/api.ResponseUser" + } + }, + "404": { + "description": "Not found", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "422": { + "description": "Unprocessable entity", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + } + } + } + }, + "/usergroups/{usergroupID}/users/": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "usergroups" + ], + "summary": "Get users of a user group", + "operationId": "getUserGroupUsers", + "parameters": [ + { + "type": "integer", + "description": "User group ID", + "name": "usergroupID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Array of users that are in the user group", + "schema": { + "$ref": "#/definitions/api.ResponseUsers" + } + }, + "404": { + "description": "Not found", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "422": { + "description": "Unprocessable entity", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + } + } + } + }, "/users": { "get": { "security": [ diff --git a/doc/api/swagger.json b/doc/api/swagger.json index 8ea7f23..93e2e37 100644 --- a/doc/api/swagger.json +++ b/doc/api/swagger.json @@ -2974,6 +2974,174 @@ } } }, + "/usergroups/{usergroupID}/user": { + "put": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "usergroups" + ], + "summary": "Add a user to a a user group", + "operationId": "addUserToUserGroup", + "parameters": [ + { + "type": "integer", + "description": "User group ID", + "name": "usergroupID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "User name", + "name": "username", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "User that was added to user group", + "schema": { + "$ref": "#/definitions/api.ResponseUser" + } + }, + "404": { + "description": "Not found", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "422": { + "description": "Unprocessable entity", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "usergroups" + ], + "summary": "Delete a user from a user group", + "operationId": "deleteUserFromUserGroup", + "parameters": [ + { + "type": "integer", + "description": "User group ID", + "name": "usergroupID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "User name", + "name": "username", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "User that was deleted from user group", + "schema": { + "$ref": "#/definitions/api.ResponseUser" + } + }, + "404": { + "description": "Not found", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "422": { + "description": "Unprocessable entity", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + } + } + } + }, + "/usergroups/{usergroupID}/users/": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "usergroups" + ], + "summary": "Get users of a user group", + "operationId": "getUserGroupUsers", + "parameters": [ + { + "type": "integer", + "description": "User group ID", + "name": "usergroupID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Array of users that are in the user group", + "schema": { + "$ref": "#/definitions/api.ResponseUsers" + } + }, + "404": { + "description": "Not found", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "422": { + "description": "Unprocessable entity", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api.ResponseError" + } + } + } + } + }, "/users": { "get": { "security": [ diff --git a/doc/api/swagger.yaml b/doc/api/swagger.yaml index 00e0e57..bca42f0 100644 --- a/doc/api/swagger.yaml +++ b/doc/api/swagger.yaml @@ -2503,6 +2503,114 @@ paths: summary: Update a user group tags: - usergroups + /usergroups/{usergroupID}/user: + delete: + operationId: deleteUserFromUserGroup + parameters: + - description: User group ID + in: path + name: usergroupID + required: true + type: integer + - description: User name + in: query + name: username + required: true + type: string + produces: + - application/json + responses: + "200": + description: User that was deleted from user group + schema: + $ref: '#/definitions/api.ResponseUser' + "404": + description: Not found + schema: + $ref: '#/definitions/api.ResponseError' + "422": + description: Unprocessable entity + schema: + $ref: '#/definitions/api.ResponseError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api.ResponseError' + security: + - Bearer: [] + summary: Delete a user from a user group + tags: + - usergroups + put: + operationId: addUserToUserGroup + parameters: + - description: User group ID + in: path + name: usergroupID + required: true + type: integer + - description: User name + in: query + name: username + required: true + type: string + produces: + - application/json + responses: + "200": + description: User that was added to user group + schema: + $ref: '#/definitions/api.ResponseUser' + "404": + description: Not found + schema: + $ref: '#/definitions/api.ResponseError' + "422": + description: Unprocessable entity + schema: + $ref: '#/definitions/api.ResponseError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api.ResponseError' + security: + - Bearer: [] + summary: Add a user to a a user group + tags: + - usergroups + /usergroups/{usergroupID}/users/: + get: + operationId: getUserGroupUsers + parameters: + - description: User group ID + in: path + name: usergroupID + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Array of users that are in the user group + schema: + $ref: '#/definitions/api.ResponseUsers' + "404": + description: Not found + schema: + $ref: '#/definitions/api.ResponseError' + "422": + description: Unprocessable entity + schema: + $ref: '#/definitions/api.ResponseError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api.ResponseError' + security: + - Bearer: [] + summary: Get users of a user group + tags: + - usergroups /users: get: operationId: GetUsers diff --git a/routes/user/authenticate_endpoint.go b/routes/user/authenticate_endpoint.go index 51fcd1d..7176ff3 100644 --- a/routes/user/authenticate_endpoint.go +++ b/routes/user/authenticate_endpoint.go @@ -277,14 +277,14 @@ func authenticateExternal(c *gin.Context) (User, error) { } duplicateName := fmt.Sprintf("%s %s", so.Name, myUser.Username) - alreadyDuplicated := isAlreadyDuplicated(duplicateName) + alreadyDuplicated := IsAlreadyDuplicated(duplicateName) if alreadyDuplicated { log.Printf("Scenario %d already duplicated for user %s", so.ID, myUser.Username) return myUser, nil } if groupedScenario.Duplicate { - duplicateScenarioForUser(so, &myUser.User, "") + DuplicateScenarioForUser(so, &myUser.User, "") } else { // add user to scenario err = db.Model(&so).Association("Users").Append(&(myUser.User)).Error if err != nil { @@ -300,7 +300,7 @@ func authenticateExternal(c *gin.Context) (User, error) { return myUser, nil } -func isAlreadyDuplicated(duplicatedName string) bool { +func IsAlreadyDuplicated(duplicatedName string) bool { db := database.GetDB() var scenarios []database.Scenario db.Find(&scenarios, "name = ?", duplicatedName) diff --git a/routes/user/scenario_duplication.go b/routes/user/scenario_duplication.go index 57b8ecf..5755f16 100644 --- a/routes/user/scenario_duplication.go +++ b/routes/user/scenario_duplication.go @@ -30,7 +30,7 @@ import ( "github.com/jinzhu/gorm/dialects/postgres" ) -func duplicateScenarioForUser(s database.Scenario, user *database.User, uuidstr string) { +func DuplicateScenarioForUser(s database.Scenario, user *database.User, uuidstr string) { // get all component configs of the scenario db := database.GetDB() diff --git a/routes/usergroup/usergroup_endpoints.go b/routes/usergroup/usergroup_endpoints.go index b9e6a14..d9cb75d 100644 --- a/routes/usergroup/usergroup_endpoints.go +++ b/routes/usergroup/usergroup_endpoints.go @@ -18,10 +18,14 @@ package usergroup import ( + "fmt" + "log" "net/http" + "strconv" "git.rwth-aachen.de/acs/public/villas/web-backend-go/database" "git.rwth-aachen.de/acs/public/villas/web-backend-go/helper" + "git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/user" "github.com/gin-gonic/gin" ) @@ -31,6 +35,9 @@ func RegisterUserGroupEndpoints(r *gin.RouterGroup) { r.GET("", getUserGroups) r.GET("/:userGroupID", getUserGroup) r.DELETE("/:userGroupID", deleteUserGroup) + r.GET("/:userGroupID/users", getUserGroupUsers) + r.PUT("/:userGroupID/user", addUserToUserGroup) + r.DELETE("/:userGroupID/user", deleteUserFromUserGroup) } // addUserGroup godoc @@ -196,3 +203,142 @@ func deleteUserGroup(c *gin.Context) { } } + +// getUserGroupUsers godoc +// @Summary Get users of a user group +// @ID getUserGroupUsers +// @Produce json +// @Tags usergroups +// @Success 200 {object} api.ResponseUsers "Array of users that are in the user group" +// @Failure 404 {object} api.ResponseError "Not found" +// @Failure 422 {object} api.ResponseError "Unprocessable entity" +// @Failure 500 {object} api.ResponseError "Internal server error" +// @Param usergroupID path int true "User group ID" +// @Router /usergroups/{usergroupID}/users/ [get] +// @Security Bearer +func getUserGroupUsers(c *gin.Context) { + + ok, ug_r := database.CheckUserGroupPermissions(c, database.Read, "path", -1) + if !ok { + return + } + + var ug UserGroup + ug.UserGroup = ug_r + + // Find all users of user group + allUsers, _, err := ug.getUsers() + if helper.DBError(c, err) { + return + } + + c.JSON(http.StatusOK, gin.H{"users": allUsers}) +} + +// addUserToUserGroup godoc +// @Summary Add a user to a a user group +// @ID addUserToUserGroup +// @Tags usergroups +// @Produce json +// @Success 200 {object} api.ResponseUser "User that was added to user group" +// @Failure 404 {object} api.ResponseError "Not found" +// @Failure 422 {object} api.ResponseError "Unprocessable entity" +// @Failure 500 {object} api.ResponseError "Internal server error" +// @Param usergroupID path int true "User group ID" +// @Param username query string true "User name" +// @Router /usergroups/{usergroupID}/user [put] +// @Security Bearer +func addUserToUserGroup(c *gin.Context) { + + ok, ug_r := database.CheckUserGroupPermissions(c, database.Update, "path", -1) + if !ok { + return + } + + var ug UserGroup + ug.UserGroup = ug_r + + username := c.Request.URL.Query().Get("username") + var u database.User + db := database.GetDB() + err := db.Find(&u, "Username = ?", username).Error + if helper.DBNotFoundError(c, err, username, "User") { + return + } + + if !u.Active { + helper.BadRequestError(c, "bad user") + return + } + + err = ug.addUser(&(u)) + if helper.DBError(c, err) { + return + } + + for _, sm := range ug.ScenarioMappings { + var s database.Scenario + err = db.Find(&s, "ID = ?", sm.ScenarioID).Error + if helper.DBNotFoundError(c, err, strconv.Itoa(int(sm.ScenarioID)), "Scenario") { + return + } + + duplicateName := fmt.Sprintf("%s %s", s.Name, u.Username) + alreadyDuplicated := user.IsAlreadyDuplicated(duplicateName) + if alreadyDuplicated { + log.Printf("Scenario %d already duplicated for user %s", s.ID, u.Username) + } + + if sm.Duplicate { + // Duplicate scenario + user.DuplicateScenarioForUser(s, &u, "") + } else { + // Add user to scenario + err = db.Model(&s).Association("Users").Append(&u).Error + if helper.DBError(c, err) { + return + } + } + } + + c.JSON(http.StatusOK, gin.H{"user": u}) +} + +// deleteUserFromUserGroup godoc +// @Summary Delete a user from a user group +// @ID deleteUserFromUserGroup +// @Tags usergroups +// @Produce json +// @Success 200 {object} api.ResponseUser "User that was deleted from user group" +// @Failure 404 {object} api.ResponseError "Not found" +// @Failure 422 {object} api.ResponseError "Unprocessable entity" +// @Failure 500 {object} api.ResponseError "Internal server error" +// @Param usergroupID path int true "User group ID" +// @Param username query string true "User name" +// @Router /usergroups/{usergroupID}/user [delete] +// @Security Bearer +func deleteUserFromUserGroup(c *gin.Context) { + + ok, ug_r := database.CheckUserGroupPermissions(c, database.Update, "path", -1) + if !ok { + return + } + + var ug UserGroup + ug.UserGroup = ug_r + + username := c.Request.URL.Query().Get("username") + var u database.User + db := database.GetDB() + err := db.Find(&u, "Username = ?", username).Error + if helper.DBNotFoundError(c, err, username, "User") { + return + } + + err = ug.deleteUser(username) + if helper.DBError(c, err) { + return + } + + c.JSON(http.StatusOK, gin.H{"user": u}) +} diff --git a/routes/usergroup/usergroup_methods.go b/routes/usergroup/usergroup_methods.go index c999eb1..fb25845 100644 --- a/routes/usergroup/usergroup_methods.go +++ b/routes/usergroup/usergroup_methods.go @@ -19,6 +19,7 @@ package usergroup import ( "git.rwth-aachen.de/acs/public/villas/web-backend-go/database" + "github.com/jinzhu/gorm" ) type UserGroup struct { @@ -111,3 +112,45 @@ func (u *UserGroup) remove() error { err := db.Delete(u).Error return err } + +func (ug *UserGroup) getUsers() ([]database.User, int, error) { + db := database.GetDB() + var users []database.User + err := db.Order("ID asc").Model(ug).Where("Active = ?", true).Related(&users, "Users").Error + return users, len(users), err +} + +func (ug *UserGroup) addUser(u *database.User) error { + db := database.GetDB() + err := db.Model(ug).Association("Users").Append(u).Error + return err +} + +func (ug *UserGroup) deleteUser(username string) error { + db := database.GetDB() + + var deletedUser database.User + err := db.Find(&deletedUser, "Username = ?", username).Error + if err != nil { + return err + } + + no_users := db.Model(ug).Association("Users").Count() + + if no_users > 0 { + // remove user from user group + err = db.Model(ug).Association("Users").Delete(&deletedUser).Error + if err != nil { + return err + } + // remove user group from user + err = db.Model(&deletedUser).Association("UserGroups").Delete(ug).Error + if err != nil { + return err + } + } else { + return gorm.ErrRecordNotFound + } + + return nil +} diff --git a/routes/usergroup/usergroup_test.go b/routes/usergroup/usergroup_test.go index 7a884c9..e49e5ff 100644 --- a/routes/usergroup/usergroup_test.go +++ b/routes/usergroup/usergroup_test.go @@ -51,6 +51,20 @@ var newUserGroupOneMapping = UserGroupRequest{ }, } +var newUserGroupTwoMappings = UserGroupRequest{ + Name: "UserGroup2", + ScenarioMappings: []ScenarioMappingRequest{ + { + ScenarioID: 1, + Duplicate: false, + }, + { + ScenarioID: 2, + Duplicate: true, + }, + }, +} + func TestMain(m *testing.M) { err := configuration.InitConfig() if err != nil { @@ -92,12 +106,18 @@ func TestAddUserGroup(t *testing.T) { assert.NoError(t, err) assert.Equalf(t, 400, code, "Response body: \n%v\n", resp) - // Test with valid user group + // Test with valid user group with one scenario mapping code, resp, err = helper.TestEndpoint(router, token, "/api/v2/usergroups", "POST", helper.KeyModels{"usergroup": newUserGroupOneMapping}) assert.NoError(t, err) assert.Equalf(t, 200, code, "Response body: \n%v\n", resp) - // Test with valid user group and one mapping + + // Test with valid user group and two scenario mappings + code, resp, err = helper.TestEndpoint(router, token, "/api/v2/usergroups", + "POST", helper.KeyModels{"usergroup": newUserGroupTwoMappings}) + assert.NoError(t, err) + assert.Equalf(t, 200, code, "Response body: \n%v\n", resp) + // Test with valid user group and multiple mappings // Test with invalid user group // Test with invalid user group and one mapping From f9a504b8a6450e4d007f0c1f08a40ce03836898b Mon Sep 17 00:00:00 2001 From: iripiri Date: Tue, 3 Sep 2024 09:53:05 +0200 Subject: [PATCH 04/21] corrections for staticcheck Signed-off-by: iripiri --- routes/user/user_test.go | 4 ++-- routes/usergroup/usergroup_methods.go | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/routes/user/user_test.go b/routes/user/user_test.go index a7121ee..dd97a8f 100644 --- a/routes/user/user_test.go +++ b/routes/user/user_test.go @@ -924,7 +924,7 @@ func TestDuplicateScenarioForUser(t *testing.T) { err = addFakeIC(uuidDup, session, token) assert.NoError(t, err) - duplicateScenarioForUser(originalSo, &myUser.User, uuidDup) + DuplicateScenarioForUser(originalSo, &myUser.User, uuidDup) /*** Check duplicated scenario for correctness ***/ var dplScenarios []database.Scenario @@ -1125,7 +1125,7 @@ func TestScenarioDuplicationAlreadyDuplicatedIC(t *testing.T) { } log.Println("------------------") - duplicateScenarioForUser(originalSo, &myUser.User, "") + DuplicateScenarioForUser(originalSo, &myUser.User, "") /*** Check duplicated scenario for correctness ***/ var dplScenarios []database.Scenario diff --git a/routes/usergroup/usergroup_methods.go b/routes/usergroup/usergroup_methods.go index fb25845..9eda1de 100644 --- a/routes/usergroup/usergroup_methods.go +++ b/routes/usergroup/usergroup_methods.go @@ -101,12 +101,6 @@ func updateScenarioMappings(groupID uint, reqScenarioMappings []validUpdatedScen return nil } -func (ug *UserGroup) byID(id uint) error { - db := database.GetDB() - err := db.Find(ug, id).Error - return err -} - func (u *UserGroup) remove() error { db := database.GetDB() err := db.Delete(u).Error From c9016ac5b870acbe55b35e75c157af310b0b6e11 Mon Sep 17 00:00:00 2001 From: SystemsPurge Date: Tue, 15 Oct 2024 13:45:23 +0200 Subject: [PATCH 05/21] Rewrote deleteUserFromUserGroup handler Signed-off-by: SystemsPurge --- routes/usergroup/usergroup_endpoints.go | 31 ++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/routes/usergroup/usergroup_endpoints.go b/routes/usergroup/usergroup_endpoints.go index d9cb75d..a0a89c3 100644 --- a/routes/usergroup/usergroup_endpoints.go +++ b/routes/usergroup/usergroup_endpoints.go @@ -340,5 +340,34 @@ func deleteUserFromUserGroup(c *gin.Context) { return } - c.JSON(http.StatusOK, gin.H{"user": u}) + for _, sm := range ug.ScenarioMappings { + var sc database.Scenario + err = db.Find(&sc, "ID = ?", sm.ScenarioID).Error + if sm.Duplicate { + //scenario not found, only referenced by id in group + if helper.DBError(c, err) { + continue + } + + duplicateName := fmt.Sprintf("%s %s", sc.Name, username) + err = db.Find(&sc, "Name = ?", duplicateName).Error + //duplicate not found, likely does not exist, continue + if helper.DBError(c, err) { + continue + } + + err = db.Delete(&sc).Error + //could not delete duplicate, highly unlikely after previous check + //check anyway + if helper.DBError(c, err) { + continue + } + } else { + err = db.Model(&sc).Association("Users").Delete(&u).Error + if helper.DBError(c, err) { // only if user already not associated, continue + continue + } + } + } + c.JSON(http.StatusOK, gin.H{"usergroup": ug}) } From 7197facca09257109cdc31235c57306622220c1e Mon Sep 17 00:00:00 2001 From: SystemsPurge Date: Tue, 15 Oct 2024 15:35:11 +0200 Subject: [PATCH 06/21] Rewrote add user to usergroup test Signed-off-by: SystemsPurge --- routes/usergroup/usergroup_test.go | 65 ++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/routes/usergroup/usergroup_test.go b/routes/usergroup/usergroup_test.go index e49e5ff..bcf6198 100644 --- a/routes/usergroup/usergroup_test.go +++ b/routes/usergroup/usergroup_test.go @@ -18,6 +18,8 @@ package usergroup import ( + "encoding/json" + "fmt" "os" "testing" @@ -41,6 +43,15 @@ type UserGroupRequest struct { ScenarioMappings []ScenarioMappingRequest `json:"scenarioMappings"` } +type UserRequest struct { + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + OldPassword string `json:"oldPassword,omitempty"` + Mail string `json:"mail,omitempty"` + Role string `json:"role,omitempty"` + Active string `json:"active,omitempty"` +} + var newUserGroupOneMapping = UserGroupRequest{ Name: "UserGroup1", ScenarioMappings: []ScenarioMappingRequest{ @@ -123,3 +134,57 @@ func TestAddUserGroup(t *testing.T) { // Test with invalid user group and one mapping // Test with invalid user group and multiple mappings } + +func TestAddUserToGroup(t *testing.T) { + // Prep DB + database.DropTables() + database.MigrateModels() + adminpw, _ := database.AddAdminUser(configuration.GlobalConfig) + + //Auth + token, _ := helper.AuthenticateForTest(router, database.Credentials{Username: "admin", Password: adminpw}) + + //Post necessities + usr := UserRequest{Username: "adrienmarie", Password: "Legendre", Role: "User", Mail: "lsq@harmonics.de"} + helper.TestEndpoint(router, token, "/api/v2/users", "POST", helper.KeyModels{"user": usr}) + helper.TestEndpoint(router, token, "/api/v2/scenarios", "POST", helper.KeyModels{"scenario": database.Scenario{Name: "scenarioNoDups"}}) + helper.TestEndpoint(router, token, "/api/v2/scenarios", "POST", helper.KeyModels{"scenario": database.Scenario{Name: "scenarioDups"}}) + helper.TestEndpoint(router, token, "/api/v2/usergroups", "POST", helper.KeyModels{"usergroup": newUserGroupTwoMappings}) + + //Add user + code, _, err := helper.TestEndpoint(router, token, "/api/v2/usergroups/1/user?username=adrienmarie", "PUT", struct{}{}) + assert.Equal(t, 200, code) + assert.NoError(t, err) + + //get scenarios + _, res, _ := helper.TestEndpoint(router, token, "/api/v2/scenarios", "GET", struct{}{}) + var scenariosMap map[string]([]database.Scenario) + json.Unmarshal(res.Bytes(), &scenariosMap) + scenarios := scenariosMap["scenarios"] + + //Actual checks + assert.Equal(t, 3, len(scenarios)) + for _, v := range scenarios { + path := fmt.Sprintf("/api/v2/scenarios/%d/users", v.ID) + var usersMap map[string]([]database.User) + _, res, _ = helper.TestEndpoint(router, token, path, "GET", struct{}{}) + json.Unmarshal(res.Bytes(), &usersMap) + users := usersMap["users"] + switch v.ID { + case 1: //no dups + assert.Equal(t, "scenarioNoDups", v.Name) + assert.Equal(t, 2, len(users)) + case 2: // with dups + assert.Equal(t, "scenarioDups", v.Name) + assert.Equal(t, 1, len(users)) + assert.Equal(t, "admin", users[0].Username) + case 3: // duped scenario + assert.Equal(t, "scenarioDups adrienmarie", v.Name) + assert.Equal(t, 1, len(users)) + assert.Equal(t, "adrienmarie", users[0].Username) + default: + // should not happen, fail manually + assert.Equal(t, 0, 1) + } + } +} From e83236c3164ebc6623282d0152b3e7bf364c6997 Mon Sep 17 00:00:00 2001 From: SystemsPurge Date: Tue, 15 Oct 2024 20:02:15 +0200 Subject: [PATCH 07/21] Rewrote test accounting for multiple users Signed-off-by: SystemsPurge --- routes/usergroup/usergroup_test.go | 32 +++++++++++++++++------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/routes/usergroup/usergroup_test.go b/routes/usergroup/usergroup_test.go index bcf6198..19cb7d1 100644 --- a/routes/usergroup/usergroup_test.go +++ b/routes/usergroup/usergroup_test.go @@ -21,6 +21,7 @@ import ( "encoding/json" "fmt" "os" + "strconv" "testing" "git.rwth-aachen.de/acs/public/villas/web-backend-go/configuration" @@ -145,16 +146,19 @@ func TestAddUserToGroup(t *testing.T) { token, _ := helper.AuthenticateForTest(router, database.Credentials{Username: "admin", Password: adminpw}) //Post necessities - usr := UserRequest{Username: "adrienmarie", Password: "Legendre", Role: "User", Mail: "lsq@harmonics.de"} - helper.TestEndpoint(router, token, "/api/v2/users", "POST", helper.KeyModels{"user": usr}) helper.TestEndpoint(router, token, "/api/v2/scenarios", "POST", helper.KeyModels{"scenario": database.Scenario{Name: "scenarioNoDups"}}) helper.TestEndpoint(router, token, "/api/v2/scenarios", "POST", helper.KeyModels{"scenario": database.Scenario{Name: "scenarioDups"}}) helper.TestEndpoint(router, token, "/api/v2/usergroups", "POST", helper.KeyModels{"usergroup": newUserGroupTwoMappings}) - //Add user - code, _, err := helper.TestEndpoint(router, token, "/api/v2/usergroups/1/user?username=adrienmarie", "PUT", struct{}{}) - assert.Equal(t, 200, code) - assert.NoError(t, err) + for n_users := 0; n_users < 3; n_users++ { + //Add user + n := strconv.Itoa(n_users + 1) + usr := UserRequest{Username: "usr" + n, Password: "legendre" + n, Role: "User", Mail: "usr" + n + "@harmonics.de"} + helper.TestEndpoint(router, token, "/api/v2/users", "POST", helper.KeyModels{"user": usr}) + code, _, err := helper.TestEndpoint(router, token, "/api/v2/usergroups/1/user?username=usr"+n, "PUT", struct{}{}) + assert.Equal(t, 200, code) + assert.NoError(t, err) + } //get scenarios _, res, _ := helper.TestEndpoint(router, token, "/api/v2/scenarios", "GET", struct{}{}) @@ -163,7 +167,8 @@ func TestAddUserToGroup(t *testing.T) { scenarios := scenariosMap["scenarios"] //Actual checks - assert.Equal(t, 3, len(scenarios)) + assert.Equal(t, 5, len(scenarios)) + for _, v := range scenarios { path := fmt.Sprintf("/api/v2/scenarios/%d/users", v.ID) var usersMap map[string]([]database.User) @@ -173,18 +178,17 @@ func TestAddUserToGroup(t *testing.T) { switch v.ID { case 1: //no dups assert.Equal(t, "scenarioNoDups", v.Name) - assert.Equal(t, 2, len(users)) + assert.Equal(t, 4, len(users)) case 2: // with dups assert.Equal(t, "scenarioDups", v.Name) assert.Equal(t, 1, len(users)) assert.Equal(t, "admin", users[0].Username) - case 3: // duped scenario - assert.Equal(t, "scenarioDups adrienmarie", v.Name) - assert.Equal(t, 1, len(users)) - assert.Equal(t, "adrienmarie", users[0].Username) default: - // should not happen, fail manually - assert.Equal(t, 0, 1) + usr := "usr" + strconv.Itoa(int(v.ID-2)) // shift ids by the first two scenarios + assert.Equal(t, "scenarioDups "+usr, v.Name) + assert.Equal(t, 1, len(users)) + assert.Equal(t, usr, users[0].Username) } } + } From 5972c68e85c5078d9680c241d9dc034fec8f48b7 Mon Sep 17 00:00:00 2001 From: SystemsPurge Date: Tue, 15 Oct 2024 21:26:34 +0200 Subject: [PATCH 08/21] Added delete user from usergroup test Signed-off-by: SystemsPurge --- routes/usergroup/usergroup_endpoints.go | 32 +++++++------- routes/usergroup/usergroup_test.go | 58 +++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 15 deletions(-) diff --git a/routes/usergroup/usergroup_endpoints.go b/routes/usergroup/usergroup_endpoints.go index a0a89c3..2b29c72 100644 --- a/routes/usergroup/usergroup_endpoints.go +++ b/routes/usergroup/usergroup_endpoints.go @@ -335,37 +335,39 @@ func deleteUserFromUserGroup(c *gin.Context) { return } + if !u.Active { + helper.BadRequestError(c, "bad user") + return + } + err = ug.deleteUser(username) if helper.DBError(c, err) { return } - for _, sm := range ug.ScenarioMappings { var sc database.Scenario err = db.Find(&sc, "ID = ?", sm.ScenarioID).Error + if helper.DBError(c, err) { + return + } + if sm.Duplicate { - //scenario not found, only referenced by id in group + var nsc database.Scenario + duplicateName := fmt.Sprintf("%s %s", sc.Name, u.Username) + err = db.Find(&nsc, "Name = ?", duplicateName).Error if helper.DBError(c, err) { - continue + return } - duplicateName := fmt.Sprintf("%s %s", sc.Name, username) - err = db.Find(&sc, "Name = ?", duplicateName).Error - //duplicate not found, likely does not exist, continue + err = db.Delete(&nsc).Error if helper.DBError(c, err) { - continue + return } - err = db.Delete(&sc).Error - //could not delete duplicate, highly unlikely after previous check - //check anyway - if helper.DBError(c, err) { - continue - } } else { err = db.Model(&sc).Association("Users").Delete(&u).Error - if helper.DBError(c, err) { // only if user already not associated, continue - continue + if helper.DBError(c, err) { + return } } } diff --git a/routes/usergroup/usergroup_test.go b/routes/usergroup/usergroup_test.go index 19cb7d1..ec8513f 100644 --- a/routes/usergroup/usergroup_test.go +++ b/routes/usergroup/usergroup_test.go @@ -23,6 +23,7 @@ import ( "os" "strconv" "testing" + "time" "git.rwth-aachen.de/acs/public/villas/web-backend-go/configuration" "git.rwth-aachen.de/acs/public/villas/web-backend-go/database" @@ -192,3 +193,60 @@ func TestAddUserToGroup(t *testing.T) { } } + +func TestDeleteUserFromGroup(t *testing.T) { + // Prep DB + database.DropTables() + database.MigrateModels() + adminpw, _ := database.AddAdminUser(configuration.GlobalConfig) + + //Auth + token, _ := helper.AuthenticateForTest(router, database.Credentials{Username: "admin", Password: adminpw}) + + //Post necessities + helper.TestEndpoint(router, token, "/api/v2/scenarios", "POST", helper.KeyModels{"scenario": database.Scenario{Name: "scenarioNoDups"}}) + helper.TestEndpoint(router, token, "/api/v2/scenarios", "POST", helper.KeyModels{"scenario": database.Scenario{Name: "scenarioDups"}}) + helper.TestEndpoint(router, token, "/api/v2/usergroups", "POST", helper.KeyModels{"usergroup": newUserGroupTwoMappings}) + + //Add 2 users + for n_users := 0; n_users < 2; n_users++ { + //Add user + n := strconv.Itoa(n_users + 1) + usr := UserRequest{Username: "usr" + n, Password: "legendre" + n, Role: "User", Mail: "usr" + n + "@harmonics.de"} + helper.TestEndpoint(router, token, "/api/v2/users", "POST", helper.KeyModels{"user": usr}) + code, _, err := helper.TestEndpoint(router, token, "/api/v2/usergroups/1/user?username=usr"+n, "PUT", struct{}{}) + assert.Equal(t, 200, code) + assert.NoError(t, err) + } + time.Sleep(time.Second) + //Delete one + helper.TestEndpoint(router, token, "/api/v2/usergroups/1/user?username=usr1", "DELETE", struct{}{}) + //get scenarios + _, res, _ := helper.TestEndpoint(router, token, "/api/v2/scenarios", "GET", struct{}{}) + var scenariosMap map[string]([]database.Scenario) + json.Unmarshal(res.Bytes(), &scenariosMap) + scenarios := scenariosMap["scenarios"] + + //Actual checks + assert.Equal(t, 3, len(scenarios)) + for _, v := range scenarios { + path := fmt.Sprintf("/api/v2/scenarios/%d/users", v.ID) + var usersMap map[string]([]database.User) + _, res, _ = helper.TestEndpoint(router, token, path, "GET", struct{}{}) + json.Unmarshal(res.Bytes(), &usersMap) + users := usersMap["users"] + switch v.ID { + case 1: //no dups + assert.Equal(t, "scenarioNoDups", v.Name) + assert.Equal(t, 2, len(users)) + case 2: // with dups + assert.Equal(t, "scenarioDups", v.Name) + assert.Equal(t, 1, len(users)) + assert.Equal(t, "admin", users[0].Username) + default: // remaining duped scenario + assert.Equal(t, "scenarioDups usr2", v.Name) + assert.Equal(t, 1, len(users)) + assert.Equal(t, "usr2", users[0].Username) + } + } +} From c4d632f3f04adbbf3e28d530f2206cb89830e189 Mon Sep 17 00:00:00 2001 From: SystemsPurge Date: Wed, 16 Oct 2024 10:38:23 +0200 Subject: [PATCH 09/21] Fixed missing registration of scenario endpoints Signed-off-by: SystemsPurge --- routes/usergroup/usergroup_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/routes/usergroup/usergroup_test.go b/routes/usergroup/usergroup_test.go index ec8513f..52b88a2 100644 --- a/routes/usergroup/usergroup_test.go +++ b/routes/usergroup/usergroup_test.go @@ -23,11 +23,11 @@ import ( "os" "strconv" "testing" - "time" "git.rwth-aachen.de/acs/public/villas/web-backend-go/configuration" "git.rwth-aachen.de/acs/public/villas/web-backend-go/database" "git.rwth-aachen.de/acs/public/villas/web-backend-go/helper" + "git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/scenario" "git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/user" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" @@ -95,7 +95,7 @@ func TestMain(m *testing.M) { user.RegisterAuthenticate(api.Group("/authenticate")) api.Use(user.Authentication()) - + scenario.RegisterScenarioEndpoints(api.Group("/scenarios")) // user endpoints required to set user to inactive user.RegisterUserEndpoints(api.Group("/users")) RegisterUserGroupEndpoints(api.Group("/usergroups")) @@ -218,7 +218,6 @@ func TestDeleteUserFromGroup(t *testing.T) { assert.Equal(t, 200, code) assert.NoError(t, err) } - time.Sleep(time.Second) //Delete one helper.TestEndpoint(router, token, "/api/v2/usergroups/1/user?username=usr1", "DELETE", struct{}{}) //get scenarios From 8da1b1fa7603c98237297985ca5eb5a0f4dfaf6a Mon Sep 17 00:00:00 2001 From: SystemsPurge Date: Wed, 16 Oct 2024 11:35:46 +0200 Subject: [PATCH 10/21] Added scenario ID existence checks on usergroup add Signed-off-by: SystemsPurge --- routes/usergroup/usergroup_endpoints.go | 11 +++++++++++ routes/usergroup/usergroup_test.go | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/routes/usergroup/usergroup_endpoints.go b/routes/usergroup/usergroup_endpoints.go index 2b29c72..68d1f13 100644 --- a/routes/usergroup/usergroup_endpoints.go +++ b/routes/usergroup/usergroup_endpoints.go @@ -18,6 +18,7 @@ package usergroup import ( + "errors" "fmt" "log" "net/http" @@ -27,6 +28,7 @@ import ( "git.rwth-aachen.de/acs/public/villas/web-backend-go/helper" "git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/user" "github.com/gin-gonic/gin" + "github.com/jinzhu/gorm" ) func RegisterUserGroupEndpoints(r *gin.RouterGroup) { @@ -74,6 +76,15 @@ func addUserGroup(c *gin.Context) { // Create the new user group from the request newUserGroup := req.createUserGroup() + db := database.GetDB() + for _, sm := range newUserGroup.ScenarioMappings { + var sc database.Scenario + if err := db.Find(&sc, "ID = ?", sm.ScenarioID).Error; errors.Is(err, gorm.ErrRecordNotFound) { + helper.NotFoundError(c, + "Scenario mappings referencing inexistent scenario ID: "+strconv.Itoa(int(sm.ScenarioID))) + return + } + } // Save the new user group to the database err := newUserGroup.save() diff --git a/routes/usergroup/usergroup_test.go b/routes/usergroup/usergroup_test.go index 52b88a2..f295cba 100644 --- a/routes/usergroup/usergroup_test.go +++ b/routes/usergroup/usergroup_test.go @@ -119,7 +119,15 @@ func TestAddUserGroup(t *testing.T) { assert.NoError(t, err) assert.Equalf(t, 400, code, "Response body: \n%v\n", resp) + //Test with inexitent scenario + code, resp, err = helper.TestEndpoint(router, token, "/api/v2/usergroups", + "POST", helper.KeyModels{"usergroup": newUserGroupOneMapping}) + assert.NoError(t, err) + assert.Equalf(t, 404, code, "Response body: \n%v\n", resp) + // Test with valid user group with one scenario mapping + helper.TestEndpoint(router, token, "/api/v2/scenarios", "POST", helper.KeyModels{"scenario": database.Scenario{Name: "scenario1"}}) + helper.TestEndpoint(router, token, "/api/v2/scenarios", "POST", helper.KeyModels{"scenario": database.Scenario{Name: "scenario2"}}) code, resp, err = helper.TestEndpoint(router, token, "/api/v2/usergroups", "POST", helper.KeyModels{"usergroup": newUserGroupOneMapping}) assert.NoError(t, err) From 15ff90ff81a3f9ec5f3280416cc2c7a440498a9e Mon Sep 17 00:00:00 2001 From: SystemsPurge Date: Wed, 16 Oct 2024 11:54:22 +0200 Subject: [PATCH 11/21] Added duplicate scenario deletion on usergroup delete and its test Signed-off-by: SystemsPurge --- routes/usergroup/usergroup_endpoints.go | 30 ++++++++++- routes/usergroup/usergroup_test.go | 68 +++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/routes/usergroup/usergroup_endpoints.go b/routes/usergroup/usergroup_endpoints.go index 68d1f13..518f28a 100644 --- a/routes/usergroup/usergroup_endpoints.go +++ b/routes/usergroup/usergroup_endpoints.go @@ -206,9 +206,37 @@ func deleteUserGroup(c *gin.Context) { var ug UserGroup ug.UserGroup = ug_r + users, _, err := ug.getUsers() + if helper.DBError(c, err) { + return + } + + scenarioMappings := ug.ScenarioMappings + db := database.GetDB() + for _, sm := range scenarioMappings { + if sm.Duplicate { + var sc database.Scenario + err = db.Find(&sc, "ID = ?", sm.ScenarioID).Error + if helper.DBError(c, err) { + return + } + for _, u := range users { + duplicateName := fmt.Sprintf("%s %s", sc.Name, u.Username) + var nsc database.Scenario + err = db.Find(&nsc, "Name = ?", duplicateName).Error + if helper.DBError(c, err) { + return + } + err = db.Delete(&nsc).Error + if helper.DBError(c, err) { + return + } + } + } + } // Try to remove user group - err := ug.remove() + err = ug.remove() if !helper.DBError(c, err) { c.JSON(http.StatusOK, gin.H{"usergroup": ug}) } diff --git a/routes/usergroup/usergroup_test.go b/routes/usergroup/usergroup_test.go index f295cba..f1660d7 100644 --- a/routes/usergroup/usergroup_test.go +++ b/routes/usergroup/usergroup_test.go @@ -78,6 +78,24 @@ var newUserGroupTwoMappings = UserGroupRequest{ }, } +var deleteTestUg = UserGroupRequest{ + Name: "UserGroup3", + ScenarioMappings: []ScenarioMappingRequest{ + { + ScenarioID: 1, + Duplicate: false, + }, + { + ScenarioID: 2, + Duplicate: true, + }, + { + ScenarioID: 3, + Duplicate: true, + }, + }, +} + func TestMain(m *testing.M) { err := configuration.InitConfig() if err != nil { @@ -257,3 +275,53 @@ func TestDeleteUserFromGroup(t *testing.T) { } } } + +func TestDeleteUserGroup(t *testing.T) { + // Prep DB + database.DropTables() + database.MigrateModels() + adminpw, _ := database.AddAdminUser(configuration.GlobalConfig) + + //Auth + token, _ := helper.AuthenticateForTest(router, database.Credentials{Username: "admin", Password: adminpw}) + + //Post necessities + helper.TestEndpoint(router, token, "/api/v2/scenarios", "POST", helper.KeyModels{"scenario": database.Scenario{Name: "scenarioNoDups"}}) + helper.TestEndpoint(router, token, "/api/v2/scenarios", "POST", helper.KeyModels{"scenario": database.Scenario{Name: "scenarioDups1"}}) + helper.TestEndpoint(router, token, "/api/v2/scenarios", "POST", helper.KeyModels{"scenario": database.Scenario{Name: "scenarioDups2"}}) + helper.TestEndpoint(router, token, "/api/v2/usergroups", "POST", helper.KeyModels{"usergroup": deleteTestUg}) + + //Add 2 users + for n_users := 0; n_users < 2; n_users++ { + //Add user + n := strconv.Itoa(n_users + 1) + usr := UserRequest{Username: "usr" + n, Password: "legendre" + n, Role: "User", Mail: "usr" + n + "@harmonics.de"} + helper.TestEndpoint(router, token, "/api/v2/users", "POST", helper.KeyModels{"user": usr}) + helper.TestEndpoint(router, token, "/api/v2/usergroups/1/user?username=usr"+n, "PUT", struct{}{}) + } + + //delete usergroup + code, _, err := helper.TestEndpoint(router, token, "/api/v2/usergroups/1", "DELETE", struct{}{}) + assert.Equal(t, 200, code) + assert.NoError(t, err) + + //get scenarios + _, res, _ := helper.TestEndpoint(router, token, "/api/v2/scenarios", "GET", struct{}{}) + var scenariosMap map[string]([]database.Scenario) + json.Unmarshal(res.Bytes(), &scenariosMap) + scenarios := scenariosMap["scenarios"] + + //Actual checks + assert.Equal(t, 3, len(scenarios)) + var names []string + for _, sc := range scenarios { + names = append(names, sc.Name) + } + assert.Contains(t, names, "scenarioNoDups") + assert.Contains(t, names, "scenarioDups1") + assert.Contains(t, names, "scenarioDups2") + assert.NotContains(t, names, "scenarioDups1 usr1") + assert.NotContains(t, names, "scenarioDups2 usr1") + assert.NotContains(t, names, "scenarioDups1 usr2") + assert.NotContains(t, names, "scenarioDups2 usr2") +} From 2282b5109c4115d98a8c28a2bb1c2af436deb54a Mon Sep 17 00:00:00 2001 From: SystemsPurge Date: Wed, 16 Oct 2024 15:24:17 +0200 Subject: [PATCH 12/21] Rewrote some scenario duplication checks to account for the MtoM relationship with usergroups Signed-off-by: SystemsPurge --- routes/user/authenticate_endpoint.go | 15 ------- routes/user/scenario_duplication.go | 60 ++++++++++++++++++++++++- routes/usergroup/usergroup_endpoints.go | 51 ++++++++------------- routes/usergroup/usergroup_methods.go | 12 +---- 4 files changed, 80 insertions(+), 58 deletions(-) diff --git a/routes/user/authenticate_endpoint.go b/routes/user/authenticate_endpoint.go index 7176ff3..61588be 100644 --- a/routes/user/authenticate_endpoint.go +++ b/routes/user/authenticate_endpoint.go @@ -276,13 +276,6 @@ func authenticateExternal(c *gin.Context) (User, error) { continue } - duplicateName := fmt.Sprintf("%s %s", so.Name, myUser.Username) - alreadyDuplicated := IsAlreadyDuplicated(duplicateName) - if alreadyDuplicated { - log.Printf("Scenario %d already duplicated for user %s", so.ID, myUser.Username) - return myUser, nil - } - if groupedScenario.Duplicate { DuplicateScenarioForUser(so, &myUser.User, "") } else { // add user to scenario @@ -299,11 +292,3 @@ func authenticateExternal(c *gin.Context) (User, error) { return myUser, nil } - -func IsAlreadyDuplicated(duplicatedName string) bool { - db := database.GetDB() - var scenarios []database.Scenario - db.Find(&scenarios, "name = ?", duplicatedName) - - return (len(scenarios) > 0) -} diff --git a/routes/user/scenario_duplication.go b/routes/user/scenario_duplication.go index 5755f16..5e23f34 100644 --- a/routes/user/scenario_duplication.go +++ b/routes/user/scenario_duplication.go @@ -30,8 +30,39 @@ import ( "github.com/jinzhu/gorm/dialects/postgres" ) -func DuplicateScenarioForUser(s database.Scenario, user *database.User, uuidstr string) { +func IsAlreadyDuplicated(sc *database.Scenario, u *database.User) bool { + duplicateName := fmt.Sprintf("%s %s", sc.Name, u.Username) + db := database.GetDB() + var scenarios []database.Scenario + db.Find(&scenarios, "name = ?", duplicateName) + + return (len(scenarios) > 0) +} + +// check if access of U to SC is exclusively granted by UG +func IsExclusiveAccess(sc *database.Scenario, u *database.User, ug *database.UserGroup) bool { + db := database.GetDB() + var ugs []database.UserGroup + db.Model(u).Association("UserGroups").Find(&ugs) + for _, asc_ug := range ugs { + if ug.ID == asc_ug.ID { + continue + } + var sms []database.ScenarioMapping + db.Model(&asc_ug).Association("ScenarioMappings").Find(&sms) + for _, sm := range sms { + if sm.ScenarioID == sc.ID { + return false + } + } + } + return true +} +func DuplicateScenarioForUser(s database.Scenario, user *database.User, uuidstr string) { + if IsAlreadyDuplicated(&s, user) { + return + } // get all component configs of the scenario db := database.GetDB() var configs []database.ComponentConfiguration @@ -124,6 +155,33 @@ func DuplicateScenarioForUser(s database.Scenario, user *database.User, uuidstr } } +func RemoveDuplicate(sc *database.Scenario, u *database.User) error { + db := database.GetDB() + + var nsc database.Scenario + duplicateName := fmt.Sprintf("%s %s", sc.Name, u.Username) + err := db.Find(&nsc, "Name = ?", duplicateName).Error + if err != nil { + return err + } + + err = db.Delete(&nsc).Error + return err +} + +func RemoveAccess(sc *database.Scenario, u *database.User, ug *database.UserGroup) error { + if !IsExclusiveAccess(sc, u, ug) { + return nil + } + db := database.GetDB() + err := db.Model(&sc).Association("Users").Delete(&u).Error + if err != nil { + return err + } + err = db.Model(&u).Association("Scenarios").Delete(&sc).Error + return err +} + func duplicateScenario(s database.Scenario, icIds map[uint]uint, user *database.User) error { db := database.GetDB() diff --git a/routes/usergroup/usergroup_endpoints.go b/routes/usergroup/usergroup_endpoints.go index 518f28a..5769a21 100644 --- a/routes/usergroup/usergroup_endpoints.go +++ b/routes/usergroup/usergroup_endpoints.go @@ -19,8 +19,6 @@ package usergroup import ( "errors" - "fmt" - "log" "net/http" "strconv" @@ -214,21 +212,21 @@ func deleteUserGroup(c *gin.Context) { scenarioMappings := ug.ScenarioMappings db := database.GetDB() for _, sm := range scenarioMappings { + var sc database.Scenario + err = db.Find(&sc, "ID = ?", sm.ScenarioID).Error + if helper.DBError(c, err) { + return + } if sm.Duplicate { - var sc database.Scenario - err = db.Find(&sc, "ID = ?", sm.ScenarioID).Error - if helper.DBError(c, err) { - return - } - for _, u := range users { - duplicateName := fmt.Sprintf("%s %s", sc.Name, u.Username) - var nsc database.Scenario - err = db.Find(&nsc, "Name = ?", duplicateName).Error + err = user.RemoveDuplicate(&sc, &u) if helper.DBError(c, err) { return } - err = db.Delete(&nsc).Error + } + } else { + for _, u := range users { + err = user.RemoveAccess(&sc, &u, &ug_r) if helper.DBError(c, err) { return } @@ -322,12 +320,6 @@ func addUserToUserGroup(c *gin.Context) { return } - duplicateName := fmt.Sprintf("%s %s", s.Name, u.Username) - alreadyDuplicated := user.IsAlreadyDuplicated(duplicateName) - if alreadyDuplicated { - log.Printf("Scenario %d already duplicated for user %s", s.ID, u.Username) - } - if sm.Duplicate { // Duplicate scenario user.DuplicateScenarioForUser(s, &u, "") @@ -379,36 +371,31 @@ func deleteUserFromUserGroup(c *gin.Context) { return } - err = ug.deleteUser(username) - if helper.DBError(c, err) { - return - } for _, sm := range ug.ScenarioMappings { + var sc database.Scenario - err = db.Find(&sc, "ID = ?", sm.ScenarioID).Error + err := db.Find(&sc, "ID = ?", sm.ScenarioID).Error if helper.DBError(c, err) { return } if sm.Duplicate { - var nsc database.Scenario - duplicateName := fmt.Sprintf("%s %s", sc.Name, u.Username) - err = db.Find(&nsc, "Name = ?", duplicateName).Error - if helper.DBError(c, err) { - return - } - - err = db.Delete(&nsc).Error + err = user.RemoveDuplicate(&sc, &u) if helper.DBError(c, err) { return } } else { - err = db.Model(&sc).Association("Users").Delete(&u).Error + err = user.RemoveAccess(&sc, &u, &ug.UserGroup) if helper.DBError(c, err) { return } } + + } + err = ug.deleteUser(&u) + if helper.DBError(c, err) { + return } c.JSON(http.StatusOK, gin.H{"usergroup": ug}) } diff --git a/routes/usergroup/usergroup_methods.go b/routes/usergroup/usergroup_methods.go index 9eda1de..1ff35f6 100644 --- a/routes/usergroup/usergroup_methods.go +++ b/routes/usergroup/usergroup_methods.go @@ -120,20 +120,12 @@ func (ug *UserGroup) addUser(u *database.User) error { return err } -func (ug *UserGroup) deleteUser(username string) error { +func (ug *UserGroup) deleteUser(deletedUser *database.User) error { db := database.GetDB() - - var deletedUser database.User - err := db.Find(&deletedUser, "Username = ?", username).Error - if err != nil { - return err - } - no_users := db.Model(ug).Association("Users").Count() - if no_users > 0 { // remove user from user group - err = db.Model(ug).Association("Users").Delete(&deletedUser).Error + err := db.Model(ug).Association("Users").Delete(&deletedUser).Error if err != nil { return err } From 08ba939aa747a05619972e3ee44aab428453bb0a Mon Sep 17 00:00:00 2001 From: SystemsPurge Date: Wed, 16 Oct 2024 17:25:15 +0200 Subject: [PATCH 13/21] Made remove duplicate also remove duplicated ics Signed-off-by: SystemsPurge --- routes/user/scenario_duplication.go | 19 +++++++- routes/usergroup/usergroup_test.go | 76 ++++++++++++++++++++++++----- 2 files changed, 83 insertions(+), 12 deletions(-) diff --git a/routes/user/scenario_duplication.go b/routes/user/scenario_duplication.go index 5e23f34..0df49cd 100644 --- a/routes/user/scenario_duplication.go +++ b/routes/user/scenario_duplication.go @@ -164,8 +164,25 @@ func RemoveDuplicate(sc *database.Scenario, u *database.User) error { if err != nil { return err } + var configs []database.ComponentConfiguration + err = db.Model(&sc).Related(&configs, "ComponentConfigurations").Error + if err != nil { + return err + } + //if ic is kubernetes simulator and already duplicated => delete + for _, config := range configs { + var ic database.InfrastructureComponent + err = db.Find(&ic, config.ICID).Error + if err != nil { + return err + } + + if ic.Type == "kubernetes" && ic.Category == "simulator" && strings.Contains(ic.Name, u.Username) { + db.Delete(&ic) + } + } + err = db.Select("Files", "Dashboards", "ComponentConfigurations").Delete(&nsc).Error - err = db.Delete(&nsc).Error return err } diff --git a/routes/usergroup/usergroup_test.go b/routes/usergroup/usergroup_test.go index f1660d7..97f59e9 100644 --- a/routes/usergroup/usergroup_test.go +++ b/routes/usergroup/usergroup_test.go @@ -244,8 +244,13 @@ func TestDeleteUserFromGroup(t *testing.T) { assert.Equal(t, 200, code) assert.NoError(t, err) } + //we add usr1 to a group that doubles its right of acces to scenario 1 + helper.TestEndpoint(router, token, "/api/v2/usergroups", "POST", helper.KeyModels{"usergroup": newUserGroupOneMapping}) + helper.TestEndpoint(router, token, "/api/v2/usergroups/2/user?username=usr1", "PUT", struct{}{}) + //Delete one helper.TestEndpoint(router, token, "/api/v2/usergroups/1/user?username=usr1", "DELETE", struct{}{}) + //get scenarios _, res, _ := helper.TestEndpoint(router, token, "/api/v2/scenarios", "GET", struct{}{}) var scenariosMap map[string]([]database.Scenario) @@ -261,9 +266,9 @@ func TestDeleteUserFromGroup(t *testing.T) { json.Unmarshal(res.Bytes(), &usersMap) users := usersMap["users"] switch v.ID { - case 1: //no dups + case 1: //no dups should still contain usr1 through ug2 assert.Equal(t, "scenarioNoDups", v.Name) - assert.Equal(t, 2, len(users)) + assert.Equal(t, 3, len(users)) case 2: // with dups assert.Equal(t, "scenarioDups", v.Name) assert.Equal(t, 1, len(users)) @@ -274,6 +279,24 @@ func TestDeleteUserFromGroup(t *testing.T) { assert.Equal(t, "usr2", users[0].Username) } } + //Delete from other + helper.TestEndpoint(router, token, "/api/v2/usergroups/2/user?username=usr1", "DELETE", struct{}{}) + + _, res, _ = helper.TestEndpoint(router, token, "/api/v2/scenarios/1", "GET", struct{}{}) + var scenarioMap map[string](database.Scenario) + json.Unmarshal(res.Bytes(), &scenarioMap) + scenario := scenarioMap["scenario"] + + path := fmt.Sprintf("/api/v2/scenarios/%d/users", scenario.ID) + var usersMap map[string]([]database.User) + _, res, _ = helper.TestEndpoint(router, token, path, "GET", struct{}{}) + json.Unmarshal(res.Bytes(), &usersMap) + users := usersMap["users"] + + assert.Equal(t, 2, len(users)) + for _, u := range users { + assert.NotEqual(t, "usr1", u.Username) + } } func TestDeleteUserGroup(t *testing.T) { @@ -300,6 +323,10 @@ func TestDeleteUserGroup(t *testing.T) { helper.TestEndpoint(router, token, "/api/v2/usergroups/1/user?username=usr"+n, "PUT", struct{}{}) } + //we add usr1 to a group that doubles its right of acces to scenario 1 + helper.TestEndpoint(router, token, "/api/v2/usergroups", "POST", helper.KeyModels{"usergroup": newUserGroupOneMapping}) + helper.TestEndpoint(router, token, "/api/v2/usergroups/2/user?username=usr1", "PUT", struct{}{}) + //delete usergroup code, _, err := helper.TestEndpoint(router, token, "/api/v2/usergroups/1", "DELETE", struct{}{}) assert.Equal(t, 200, code) @@ -313,15 +340,42 @@ func TestDeleteUserGroup(t *testing.T) { //Actual checks assert.Equal(t, 3, len(scenarios)) - var names []string + var exists []string = []string{"scenarioNoDups", "scenarioDups1", "scenarioDups2"} + var deleted []string = []string{"scenarioDups1 usr1", "scenarioDups2 usr1", "scenarioDups1 usr2", "scenarioDups2 usr2"} for _, sc := range scenarios { - names = append(names, sc.Name) + assert.Contains(t, exists, sc.Name) + assert.NotContains(t, deleted, sc.Name) + if sc.Name == "scenarioNoDups" { + _, res, _ = helper.TestEndpoint(router, token, "/api/v2/scenarios/1", "GET", struct{}{}) + var scenarioMap map[string](database.Scenario) + json.Unmarshal(res.Bytes(), &scenarioMap) + scenario := scenarioMap["scenario"] + + path := fmt.Sprintf("/api/v2/scenarios/%d/users", scenario.ID) + var usersMap map[string]([]database.User) + _, res, _ = helper.TestEndpoint(router, token, path, "GET", struct{}{}) + json.Unmarshal(res.Bytes(), &usersMap) + users := usersMap["users"] + assert.Equal(t, 2, len(users)) + } + } + //Delete from other + helper.TestEndpoint(router, token, "/api/v2/usergroups/2/user?username=usr1", "DELETE", struct{}{}) + + _, res, _ = helper.TestEndpoint(router, token, "/api/v2/scenarios/1", "GET", struct{}{}) + var scenarioMap map[string](database.Scenario) + json.Unmarshal(res.Bytes(), &scenarioMap) + scenario := scenarioMap["scenario"] + + path := fmt.Sprintf("/api/v2/scenarios/%d/users", scenario.ID) + var usersMap map[string]([]database.User) + _, res, _ = helper.TestEndpoint(router, token, path, "GET", struct{}{}) + json.Unmarshal(res.Bytes(), &usersMap) + users := usersMap["users"] + + assert.Equal(t, 1, len(users)) + for _, u := range users { + assert.NotEqual(t, "usr1", u.Username) + assert.NotEqual(t, "usr2", u.Username) } - assert.Contains(t, names, "scenarioNoDups") - assert.Contains(t, names, "scenarioDups1") - assert.Contains(t, names, "scenarioDups2") - assert.NotContains(t, names, "scenarioDups1 usr1") - assert.NotContains(t, names, "scenarioDups2 usr1") - assert.NotContains(t, names, "scenarioDups1 usr2") - assert.NotContains(t, names, "scenarioDups2 usr2") } From d710627b7d648df9da72daabadd2e8ad66034b0a Mon Sep 17 00:00:00 2001 From: SystemsPurge Date: Thu, 17 Oct 2024 11:02:32 +0200 Subject: [PATCH 14/21] Made remove duplicate send action over amqp Signed-off-by: SystemsPurge --- routes/user/scenario_duplication.go | 39 ++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/routes/user/scenario_duplication.go b/routes/user/scenario_duplication.go index 0df49cd..0ba0ff7 100644 --- a/routes/user/scenario_duplication.go +++ b/routes/user/scenario_duplication.go @@ -178,11 +178,44 @@ func RemoveDuplicate(sc *database.Scenario, u *database.User) error { } if ic.Type == "kubernetes" && ic.Category == "simulator" && strings.Contains(ic.Name, u.Username) { - db.Delete(&ic) + + msg := `{"uuid": "` + ic.UUID + `"}` + + type Action struct { + Act string `json:"action"` + When int64 `json:"when"` + Parameters json.RawMessage `json:"parameters,omitempty"` + Model json.RawMessage `json:"model,omitempty"` + Results json.RawMessage `json:"results,omitempty"` + } + + actionCreate := Action{ + Act: "delete", + When: time.Now().Unix(), + Parameters: json.RawMessage(msg), + } + + payload, err := json.Marshal(actionCreate) + if err != nil { + return err + } + + if session != nil { + if session.IsReady { + err = session.Send(payload, ic.Manager) + if err != nil { + db.Delete(&ic) + } + return err + } else { + return fmt.Errorf("could not send IC create action, AMQP session is not ready") + } + } else { + return fmt.Errorf("could not send IC create action, AMQP session is nil") + } } } - err = db.Select("Files", "Dashboards", "ComponentConfigurations").Delete(&nsc).Error - + err = db.Select("Files", "Dashboards", "ComponentConfigurations", "Results").Delete(&nsc).Error return err } From 0bc01629ad304c997db43e5532339443f6625d15 Mon Sep 17 00:00:00 2001 From: SystemsPurge Date: Thu, 17 Oct 2024 15:51:13 +0200 Subject: [PATCH 15/21] Added updateUserGroup test Signed-off-by: SystemsPurge --- routes/user/scenario_duplication.go | 4 +- routes/usergroup/usergroup_methods.go | 70 +++++++++++- routes/usergroup/usergroup_test.go | 150 ++++++++++++++++++++++++++ 3 files changed, 219 insertions(+), 5 deletions(-) diff --git a/routes/user/scenario_duplication.go b/routes/user/scenario_duplication.go index 0ba0ff7..273cfd3 100644 --- a/routes/user/scenario_duplication.go +++ b/routes/user/scenario_duplication.go @@ -179,8 +179,6 @@ func RemoveDuplicate(sc *database.Scenario, u *database.User) error { if ic.Type == "kubernetes" && ic.Category == "simulator" && strings.Contains(ic.Name, u.Username) { - msg := `{"uuid": "` + ic.UUID + `"}` - type Action struct { Act string `json:"action"` When int64 `json:"when"` @@ -192,7 +190,7 @@ func RemoveDuplicate(sc *database.Scenario, u *database.User) error { actionCreate := Action{ Act: "delete", When: time.Now().Unix(), - Parameters: json.RawMessage(msg), + Parameters: json.RawMessage(`{"uuid": "` + ic.UUID + `"}`), } payload, err := json.Marshal(actionCreate) diff --git a/routes/usergroup/usergroup_methods.go b/routes/usergroup/usergroup_methods.go index 1ff35f6..6c60bb3 100644 --- a/routes/usergroup/usergroup_methods.go +++ b/routes/usergroup/usergroup_methods.go @@ -18,7 +18,10 @@ package usergroup import ( + "fmt" + "git.rwth-aachen.de/acs/public/villas/web-backend-go/database" + "git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/user" "github.com/jinzhu/gorm" ) @@ -51,10 +54,10 @@ func (ug *UserGroup) update(updatedUserGroup UserGroup, reqScenarioMappings []va } */ - return updateScenarioMappings(ug.ID, reqScenarioMappings) + return ug.updateScenarioMappings(ug.ID, reqScenarioMappings) } -func updateScenarioMappings(groupID uint, reqScenarioMappings []validUpdatedScenarioMapping) error { +func (ug *UserGroup) updateScenarioMappings(groupID uint, reqScenarioMappings []validUpdatedScenarioMapping) error { var oldMappings []database.ScenarioMapping db := database.GetDB() err := db.Where("user_group_id = ?", groupID).Find(&oldMappings).Error @@ -62,6 +65,13 @@ func updateScenarioMappings(groupID uint, reqScenarioMappings []validUpdatedScen return err } + var users []database.User + err = db.Model(ug).Association("Users").Find(&users).Error + fmt.Println(users) + if err != nil { + return err + } + oldMappingsMap := make(map[uint]database.ScenarioMapping) for _, mapping := range oldMappings { oldMappingsMap[mapping.ScenarioID] = mapping @@ -69,8 +79,33 @@ func updateScenarioMappings(groupID uint, reqScenarioMappings []validUpdatedScen // Handle ScenarioMappings (add/update/delete) for _, reqMapping := range reqScenarioMappings { + var sc database.Scenario + err = db.Find(&sc, "ID = ?", reqMapping.ScenarioID).Error + if err != nil { + return err + } if oldMapping, exists := oldMappingsMap[reqMapping.ScenarioID]; exists { // Update + if oldMapping.Duplicate != reqMapping.Duplicate { + if reqMapping.Duplicate { + for _, u := range users { + user.RemoveAccess(&sc, &u, &ug.UserGroup) + user.DuplicateScenarioForUser(sc, &u, "") + } + } else { + for _, u := range users { + err = user.RemoveDuplicate(&sc, &u) + if err != nil { + return err + } + err = db.Model(&sc).Association("Users").Append(&u).Error + if err != nil { + return err + } + + } + } + } oldMapping.Duplicate = reqMapping.Duplicate err = db.Save(&oldMapping).Error if err != nil { @@ -84,6 +119,20 @@ func updateScenarioMappings(groupID uint, reqScenarioMappings []validUpdatedScen UserGroupID: groupID, Duplicate: reqMapping.Duplicate, } + + if reqMapping.Duplicate { + for _, u := range users { + user.DuplicateScenarioForUser(sc, &u, "") + } + } else { + for _, u := range users { + err = db.Model(&sc).Association("Users").Append(&u).Error + if err != nil { + return err + } + + } + } err = db.Create(&newMapping).Error if err != nil { return err @@ -93,6 +142,23 @@ func updateScenarioMappings(groupID uint, reqScenarioMappings []validUpdatedScen // Delete old mappings that were not in the request for _, mapping := range oldMappingsMap { + var nsc database.Scenario + err = db.Find(&nsc, "ID = ?", mapping.ScenarioID).Error + if err != nil { + return err + } + if mapping.Duplicate { + for _, u := range users { + err = user.RemoveDuplicate(&nsc, &u) + if err != nil { + return err + } + } + } else { + for _, u := range users { + user.RemoveAccess(&nsc, &u, &ug.UserGroup) + } + } err = db.Delete(&mapping).Error if err != nil { return err diff --git a/routes/usergroup/usergroup_test.go b/routes/usergroup/usergroup_test.go index 97f59e9..c84bb8a 100644 --- a/routes/usergroup/usergroup_test.go +++ b/routes/usergroup/usergroup_test.go @@ -96,6 +96,66 @@ var deleteTestUg = UserGroupRequest{ }, } +var initUpdateTestUg = UserGroupRequest{ + Name: "UserGroup4", + ScenarioMappings: []ScenarioMappingRequest{ + { + ScenarioID: 1, + Duplicate: true, + }, + { + ScenarioID: 2, + Duplicate: false, + }, + { + ScenarioID: 3, + Duplicate: true, + }, + { + ScenarioID: 4, + Duplicate: false, + }, + { + ScenarioID: 5, + Duplicate: true, + }, + { + ScenarioID: 6, + Duplicate: false, + }, + }, +} + +var updateTestUg = UserGroupRequest{ + Name: "UserGroup4", + ScenarioMappings: []ScenarioMappingRequest{ + { + ScenarioID: 1, + Duplicate: false, + }, + { + ScenarioID: 2, + Duplicate: true, + }, + { + ScenarioID: 5, + Duplicate: true, + }, + { + ScenarioID: 6, + Duplicate: false, + }, + { + ScenarioID: 7, + Duplicate: true, + }, + { + ScenarioID: 8, + Duplicate: false, + }, + }, +} + func TestMain(m *testing.M) { err := configuration.InitConfig() if err != nil { @@ -379,3 +439,93 @@ func TestDeleteUserGroup(t *testing.T) { assert.NotEqual(t, "usr2", u.Username) } } + +func TestUpdateUserGroup(t *testing.T) { + // Prep DB + database.DropTables() + database.MigrateModels() + adminpw, _ := database.AddAdminUser(configuration.GlobalConfig) + + //Auth + token, _ := helper.AuthenticateForTest(router, database.Credentials{Username: "admin", Password: adminpw}) + + //Post necessities + helper.TestEndpoint(router, token, "/api/v2/scenarios", "POST", helper.KeyModels{"scenario": database.Scenario{Name: "changeDups"}}) + helper.TestEndpoint(router, token, "/api/v2/scenarios", "POST", helper.KeyModels{"scenario": database.Scenario{Name: "changeNoDups"}}) + helper.TestEndpoint(router, token, "/api/v2/scenarios", "POST", helper.KeyModels{"scenario": database.Scenario{Name: "removeDups"}}) + helper.TestEndpoint(router, token, "/api/v2/scenarios", "POST", helper.KeyModels{"scenario": database.Scenario{Name: "removeNoDups"}}) + helper.TestEndpoint(router, token, "/api/v2/scenarios", "POST", helper.KeyModels{"scenario": database.Scenario{Name: "unchangedDups"}}) + helper.TestEndpoint(router, token, "/api/v2/scenarios", "POST", helper.KeyModels{"scenario": database.Scenario{Name: "unchangedNoDups"}}) + helper.TestEndpoint(router, token, "/api/v2/scenarios", "POST", helper.KeyModels{"scenario": database.Scenario{Name: "addDups"}}) + helper.TestEndpoint(router, token, "/api/v2/scenarios", "POST", helper.KeyModels{"scenario": database.Scenario{Name: "addNoDups"}}) + helper.TestEndpoint(router, token, "/api/v2/usergroups", "POST", helper.KeyModels{"usergroup": initUpdateTestUg}) + + //Add user + usr := UserRequest{Username: "usr1", Password: "legendre1", Role: "User", Mail: "usr1@harmonics.de"} + helper.TestEndpoint(router, token, "/api/v2/users", "POST", helper.KeyModels{"user": usr}) + helper.TestEndpoint(router, token, "/api/v2/usergroups/1/user?username=usr1", "PUT", struct{}{}) + + //update group + helper.TestEndpoint(router, token, "/api/v2/usergroups/1", "PUT", helper.KeyModels{"usergroup": updateTestUg}) + + //get scenarios + _, res, _ := helper.TestEndpoint(router, token, "/api/v2/scenarios", "GET", struct{}{}) + var scenariosRes map[string]([]database.Scenario) + json.Unmarshal(res.Bytes(), &scenariosRes) + scenarios := scenariosRes["scenarios"] + assert.Equal(t, 11, len(scenarios)) + var scenariosMap map[string](database.Scenario) = make(map[string](database.Scenario)) + for _, s := range scenarios { + scenariosMap[s.Name] = s + } + + //scenarios that transformed into/remained/got added as duplicated (6) + for _, name := range []string{"addDups", "unchangedDups", "changeNoDups"} { + sc, exists := scenariosMap[name] + assert.True(t, exists) + path := fmt.Sprintf("/api/v2/scenarios/%d/users", sc.ID) + var usersMap map[string]([]database.User) + _, res, _ = helper.TestEndpoint(router, token, path, "GET", struct{}{}) + json.Unmarshal(res.Bytes(), &usersMap) + users := usersMap["users"] + assert.Equal(t, 1, len(users)) + + sc, exists = scenariosMap[name+" usr1"] + assert.True(t, exists) + _, res, _ = helper.TestEndpoint(router, token, path, "GET", struct{}{}) + json.Unmarshal(res.Bytes(), &usersMap) + users = usersMap["users"] + assert.Equal(t, 1, len(users)) + } + + //scenarios that transformed into/remained/got added as single (+3) + for _, name := range []string{"addNoDups", "unchangedNoDups", "changeDups"} { + sc, exists := scenariosMap[name] + assert.True(t, exists) + path := fmt.Sprintf("/api/v2/scenarios/%d/users", sc.ID) + var usersMap map[string]([]database.User) + _, res, _ = helper.TestEndpoint(router, token, path, "GET", struct{}{}) + json.Unmarshal(res.Bytes(), &usersMap) + users := usersMap["users"] + assert.Equal(t, 2, len(users)) + + _, exists = scenariosMap[name+" usr1"] + assert.False(t, exists) + } + + //scenarios that got removed (+2 = 11) + for _, name := range []string{"removeDups", "removeNoDups"} { + sc, exists := scenariosMap[name] + assert.True(t, exists) + path := fmt.Sprintf("/api/v2/scenarios/%d/users", sc.ID) + var usersMap map[string]([]database.User) + _, res, _ = helper.TestEndpoint(router, token, path, "GET", struct{}{}) + json.Unmarshal(res.Bytes(), &usersMap) + users := usersMap["users"] + assert.Equal(t, 1, len(users)) + + _, exists = scenariosMap[name+" usr1"] + assert.False(t, exists) + } + +} From 4310cb69a6396026c6619140b3f2f5483d8b48b8 Mon Sep 17 00:00:00 2001 From: SystemsPurge Date: Thu, 17 Oct 2024 15:58:28 +0200 Subject: [PATCH 16/21] Fixed mistake in test Signed-off-by: SystemsPurge --- routes/usergroup/usergroup_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/routes/usergroup/usergroup_test.go b/routes/usergroup/usergroup_test.go index c84bb8a..838f73f 100644 --- a/routes/usergroup/usergroup_test.go +++ b/routes/usergroup/usergroup_test.go @@ -491,6 +491,7 @@ func TestUpdateUserGroup(t *testing.T) { assert.Equal(t, 1, len(users)) sc, exists = scenariosMap[name+" usr1"] + path = fmt.Sprintf("/api/v2/scenarios/%d/users", sc.ID) assert.True(t, exists) _, res, _ = helper.TestEndpoint(router, token, path, "GET", struct{}{}) json.Unmarshal(res.Bytes(), &usersMap) From 2a2c5ca77f687ef4e3850a7264d126275327d740 Mon Sep 17 00:00:00 2001 From: SystemsPurge Date: Thu, 17 Oct 2024 16:09:22 +0200 Subject: [PATCH 17/21] Removed unused code Signed-off-by: SystemsPurge --- configuration/config.go | 40 ---------------------------------------- routes/user/user_test.go | 38 -------------------------------------- start.go | 14 ++------------ 3 files changed, 2 insertions(+), 90 deletions(-) diff --git a/configuration/config.go b/configuration/config.go index ce01828..7bbd5fa 100644 --- a/configuration/config.go +++ b/configuration/config.go @@ -20,14 +20,11 @@ package configuration import ( "flag" "fmt" - "io" "log" "os" "sort" "strings" - "gopkg.in/yaml.v3" - "github.com/zpatrick/go-config" ) @@ -197,40 +194,3 @@ func remove(arr []GroupedScenario, index int) []GroupedScenario { arr[index] = arr[len(arr)-1] return arr[:len(arr)-1] } - -func ReadGroupsFile(path string) error { - _, err := os.Stat(path) - if err != nil { - return err - } - - yamlFile, err := os.Open(path) - if err != nil { - return fmt.Errorf("error opening yaml file for groups: %v", err) - } - log.Println("Successfully opened yaml groups file", path) - - defer yamlFile.Close() - - byteValue, _ := io.ReadAll(yamlFile) - - err = yaml.Unmarshal(byteValue, &ScenarioGroupMap) - if err != nil { - return fmt.Errorf("error unmarshalling yaml into ScenarioGroupMap: %v", err) - } - - for _, group := range ScenarioGroupMap { - for i, scenario := range group { - // remove invalid values that might have been introduced by typos - // (Unmarshal sets default values when it doesn't find a field) - if scenario.Scenario == 0 { - log.Println("Removing entry from ScenarioGroupMap, check for typos in the yaml!") - remove(group, i) - } - } - } - - log.Println("ScenarioGroupMap", ScenarioGroupMap) - - return nil -} diff --git a/routes/user/user_test.go b/routes/user/user_test.go index dd97a8f..30a81a9 100644 --- a/routes/user/user_test.go +++ b/routes/user/user_test.go @@ -27,7 +27,6 @@ import ( "net/http" "net/http/httptest" "os" - "strings" "testing" "time" @@ -159,43 +158,6 @@ func TestAuthenticate(t *testing.T) { } -func TestUserGroups(t *testing.T) { - // Create new user - // (user, email and groups are read from request headers in real case) - var myUser User - username := "Fridolin" - email := "Fridolin@rwth-aachen.de" - role := "User" - userGroups := strings.Split("testGroup1,testGroup2", ",") - - err := myUser.byUsername(username) - assert.Error(t, err) - myUser, err = NewUser(username, "", email, role, true) - assert.NoError(t, err) - - // Read groups file - err = configuration.ReadGroupsFile("notexisting.yaml") - assert.Error(t, err) - - err = configuration.ReadGroupsFile("../../configuration/groups.yaml") - assert.NoError(t, err) - - // Check whether duplicate flag is saved correctly in configuration - for _, group := range userGroups { - if gsarray, ok := configuration.ScenarioGroupMap[group]; ok { - for _, groupedScenario := range gsarray { - if group == "testGroup1" && groupedScenario.Scenario == 1 { - assert.Equal(t, true, groupedScenario.Duplicate) - } else if group == "testGroup2" && groupedScenario.Scenario == 4 { - assert.Equal(t, true, groupedScenario.Duplicate) - } else { - assert.Equal(t, false, groupedScenario.Duplicate) - } - } - } - } -} - func TestAuthenticateQueryToken(t *testing.T) { database.DropTables() diff --git a/start.go b/start.go index 48c5690..6767160 100644 --- a/start.go +++ b/start.go @@ -18,10 +18,11 @@ package main import ( "fmt" - "git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/user" "log" "time" + "git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/user" + "git.rwth-aachen.de/acs/public/villas/web-backend-go/configuration" "git.rwth-aachen.de/acs/public/villas/web-backend-go/database" "git.rwth-aachen.de/acs/public/villas/web-backend-go/helper" @@ -85,17 +86,6 @@ func main() { log.Fatalf("Error reading port from global configuration: %s, aborting.", err) } - gPath, _ := configuration.GlobalConfig.String("groups.path") - - if gPath != "" { - err = configuration.ReadGroupsFile(gPath) - if err != nil { - log.Fatalf("Error reading groups YAML file: %s, aborting.", err) - } - } else { - log.Println("WARNING: path to groups yaml file not set, I am not initializing the scenario-groups mapping.") - } - // Init database err = database.InitDB(configuration.GlobalConfig, dbClear == "true") if err != nil { From b014a1efe065add040dce9eb427e73f451628d5e Mon Sep 17 00:00:00 2001 From: SystemsPurge Date: Thu, 17 Oct 2024 16:14:23 +0200 Subject: [PATCH 18/21] Fix staticcheck Signed-off-by: SystemsPurge --- configuration/config.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/configuration/config.go b/configuration/config.go index 7bbd5fa..ace383f 100644 --- a/configuration/config.go +++ b/configuration/config.go @@ -189,8 +189,3 @@ func InitConfig() error { } return nil } - -func remove(arr []GroupedScenario, index int) []GroupedScenario { - arr[index] = arr[len(arr)-1] - return arr[:len(arr)-1] -} From a932c7c1d98b4a63c1232c1447fd60d094f1eab2 Mon Sep 17 00:00:00 2001 From: SystemsPurge Date: Fri, 18 Oct 2024 15:42:47 +0200 Subject: [PATCH 19/21] Fixed mistake in ic deletion when clearing usergroup Signed-off-by: SystemsPurge --- routes/user/scenario_duplication.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/routes/user/scenario_duplication.go b/routes/user/scenario_duplication.go index 273cfd3..74d769f 100644 --- a/routes/user/scenario_duplication.go +++ b/routes/user/scenario_duplication.go @@ -165,7 +165,7 @@ func RemoveDuplicate(sc *database.Scenario, u *database.User) error { return err } var configs []database.ComponentConfiguration - err = db.Model(&sc).Related(&configs, "ComponentConfigurations").Error + err = db.Model(&nsc).Related(&configs, "ComponentConfigurations").Error if err != nil { return err } @@ -176,9 +176,10 @@ func RemoveDuplicate(sc *database.Scenario, u *database.User) error { if err != nil { return err } - if ic.Type == "kubernetes" && ic.Category == "simulator" && strings.Contains(ic.Name, u.Username) { + msg := `{"uuid": "` + ic.UUID + `"}` + type Action struct { Act string `json:"action"` When int64 `json:"when"` @@ -190,7 +191,7 @@ func RemoveDuplicate(sc *database.Scenario, u *database.User) error { actionCreate := Action{ Act: "delete", When: time.Now().Unix(), - Parameters: json.RawMessage(`{"uuid": "` + ic.UUID + `"}`), + Parameters: json.RawMessage(msg), } payload, err := json.Marshal(actionCreate) @@ -202,9 +203,13 @@ func RemoveDuplicate(sc *database.Scenario, u *database.User) error { if session.IsReady { err = session.Send(payload, ic.Manager) if err != nil { - db.Delete(&ic) + return err } - return err + err = db.Delete(&ic).Error + if err != nil { + return err + } + } else { return fmt.Errorf("could not send IC create action, AMQP session is not ready") } From e5b8df304d8b0aaa7016b933ed302f6fdf3fa02a Mon Sep 17 00:00:00 2001 From: SystemsPurge Date: Thu, 31 Oct 2024 09:31:04 +0100 Subject: [PATCH 20/21] Loosened restrictions on usergroup delete/user delete from usergroup Signed-off-by: SystemsPurge --- routes/user/scenario_duplication.go | 19 ++++++++----------- routes/usergroup/usergroup_endpoints.go | 24 ++++++++++++------------ 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/routes/user/scenario_duplication.go b/routes/user/scenario_duplication.go index 74d769f..ffede3a 100644 --- a/routes/user/scenario_duplication.go +++ b/routes/user/scenario_duplication.go @@ -169,12 +169,12 @@ func RemoveDuplicate(sc *database.Scenario, u *database.User) error { if err != nil { return err } - //if ic is kubernetes simulator and already duplicated => delete + for _, config := range configs { var ic database.InfrastructureComponent err = db.Find(&ic, config.ICID).Error if err != nil { - return err + continue } if ic.Type == "kubernetes" && ic.Category == "simulator" && strings.Contains(ic.Name, u.Username) { @@ -196,18 +196,18 @@ func RemoveDuplicate(sc *database.Scenario, u *database.User) error { payload, err := json.Marshal(actionCreate) if err != nil { - return err + continue } if session != nil { if session.IsReady { err = session.Send(payload, ic.Manager) if err != nil { - return err + continue } err = db.Delete(&ic).Error if err != nil { - return err + continue } } else { @@ -227,11 +227,8 @@ func RemoveAccess(sc *database.Scenario, u *database.User, ug *database.UserGrou return nil } db := database.GetDB() - err := db.Model(&sc).Association("Users").Delete(&u).Error - if err != nil { - return err - } - err = db.Model(&u).Association("Scenarios").Delete(&sc).Error + db.Model(&sc).Association("Users").Delete(&u) + err := db.Model(&u).Association("Scenarios").Delete(&sc).Error return err } @@ -692,7 +689,7 @@ func duplicateIC(ic database.InfrastructureComponent, userName string, uuidstr s `"category": "` + lastUpdate.Properties.Category + `",` + `"type": "` + lastUpdate.Properties.Type + `",` + `"uuid": "` + newUUID + `",` + - `"jobname": "` + lastUpdate.Properties.Job.MetaData.JobName + `-` + userName + `",` + + `"jobname": "` + strings.ToLower(lastUpdate.Properties.Job.MetaData.JobName) + `-` + strings.ToLower(userName) + `",` + `"activeDeadlineSeconds": "` + strconv.Itoa(lastUpdate.Properties.Job.Spec.Active) + `",` + `"containername": "` + lastUpdate.Properties.Job.Spec.Template.Spec.Containers[0].Name + `-` + userName + `",` + `"image": "` + lastUpdate.Properties.Job.Spec.Template.Spec.Containers[0].Image + `",` + diff --git a/routes/usergroup/usergroup_endpoints.go b/routes/usergroup/usergroup_endpoints.go index 5769a21..718a056 100644 --- a/routes/usergroup/usergroup_endpoints.go +++ b/routes/usergroup/usergroup_endpoints.go @@ -214,21 +214,21 @@ func deleteUserGroup(c *gin.Context) { for _, sm := range scenarioMappings { var sc database.Scenario err = db.Find(&sc, "ID = ?", sm.ScenarioID).Error - if helper.DBError(c, err) { - return + if err != nil { + continue } if sm.Duplicate { for _, u := range users { err = user.RemoveDuplicate(&sc, &u) - if helper.DBError(c, err) { - return + if err != nil { + continue } } } else { for _, u := range users { err = user.RemoveAccess(&sc, &u, &ug_r) - if helper.DBError(c, err) { - return + if err != nil { + continue } } } @@ -375,20 +375,20 @@ func deleteUserFromUserGroup(c *gin.Context) { var sc database.Scenario err := db.Find(&sc, "ID = ?", sm.ScenarioID).Error - if helper.DBError(c, err) { - return + if err != nil { + continue } if sm.Duplicate { err = user.RemoveDuplicate(&sc, &u) - if helper.DBError(c, err) { - return + if err != nil { + continue } } else { err = user.RemoveAccess(&sc, &u, &ug.UserGroup) - if helper.DBError(c, err) { - return + if err != nil { + continue } } From 81a025513694577492201fc9deb205d4de83d065 Mon Sep 17 00:00:00 2001 From: SystemsPurge Date: Fri, 8 Nov 2024 14:46:02 +0100 Subject: [PATCH 21/21] More checks on job metadata naming Signed-off-by: SystemsPurge --- routes/user/scenario_duplication.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/user/scenario_duplication.go b/routes/user/scenario_duplication.go index ffede3a..4272d5a 100644 --- a/routes/user/scenario_duplication.go +++ b/routes/user/scenario_duplication.go @@ -689,7 +689,7 @@ func duplicateIC(ic database.InfrastructureComponent, userName string, uuidstr s `"category": "` + lastUpdate.Properties.Category + `",` + `"type": "` + lastUpdate.Properties.Type + `",` + `"uuid": "` + newUUID + `",` + - `"jobname": "` + strings.ToLower(lastUpdate.Properties.Job.MetaData.JobName) + `-` + strings.ToLower(userName) + `",` + + `"jobname": "` + strings.Replace(strings.ToLower(lastUpdate.Properties.Job.MetaData.JobName), "_", "-", -1) + `-` + strings.Replace(strings.ToLower(userName), "_", "-", -1) + `",` + `"activeDeadlineSeconds": "` + strconv.Itoa(lastUpdate.Properties.Job.Spec.Active) + `",` + `"containername": "` + lastUpdate.Properties.Job.Spec.Template.Spec.Containers[0].Name + `-` + userName + `",` + `"image": "` + lastUpdate.Properties.Job.Spec.Template.Spec.Containers[0].Image + `",` +