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/configuration/config.go b/configuration/config.go index ce01828..ace383f 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" ) @@ -192,45 +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] -} - -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/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 438f451..e8afd05 100644 --- a/database/models.go +++ b/database/models.go @@ -48,6 +48,30 @@ type User struct { Active bool `json:"active" gorm:"default:true"` // Scenarios to which user has access Scenarios []*Scenario `json:"-" gorm:"many2many:user_scenarios;"` + // Groups of user + UserGroups []*UserGroup `json:"-" gorm:"many2many:user_groups_users;"` +} + +// ScenarioMapping data model +type ScenarioMapping struct { + Model + 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"` +} + +// 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"` + // Users that belong to the user group + Users []*User `json:"users" gorm:"many2many:user_groups_users;"` } // Scenario data model diff --git a/database/permissions.go b/database/permissions.go index ec2fb66..30bdab7 100644 --- a/database/permissions.go +++ b/database/permissions.go @@ -142,6 +142,35 @@ func CheckSignalPermissions(c *gin.Context, operation CRUD) (bool, Signal) { } +func CheckUserGroupPermissions(c *gin.Context, operation CRUD, userGroupIDSource string, usergroupIDBody 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 + } + + if operation == Create { + return true, usrgrp + } + + groupID, err := helper.GetIDOfElement(c, "userGroupID", userGroupIDSource, usergroupIDBody) + if err != nil { + return false, usrgrp + } + + db := GetDB() + err = db.Preload("ScenarioMappings.Scenario").First(&usrgrp, 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..295dcdb 100644 --- a/doc/api/docs.go +++ b/doc/api/docs.go @@ -2702,6 +2702,454 @@ 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": [ + { + "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" + } + } + } + } + }, + "/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" + } + } + } + } + }, + "/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": [ @@ -3330,6 +3778,12 @@ const docTemplate = `{ "api.ResponseUser": { "type": "object" }, + "api.ResponseUserGroup": { + "type": "object" + }, + "api.ResponseUserGroups": { + "type": "object" + }, "api.ResponseUsers": { "type": "object" }, @@ -3358,27 +3812,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 +3840,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 +3974,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 +4026,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": { "type": "string" }, - "UUID": { - "type": "string" - }, - "Uptime": { + "uptime": { "type": "number" }, - "WebsocketURL": { + "uuid": { + "type": "string" + }, + "websocketURL": { "type": "string" } } @@ -3628,46 +4082,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 +4156,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 +4213,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 +4228,13 @@ const docTemplate = `{ "scenario.validUpdatedScenario": { "type": "object", "properties": { - "IsLocked": { + "isLocked": { "type": "boolean" }, - "Name": { + "name": { "type": "string" }, - "StartParameters": { + "startParameters": { "$ref": "#/definitions/postgres.Jsonb" } } @@ -3804,32 +4258,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 +4291,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 +4316,14 @@ const docTemplate = `{ "user.loginRequest": { "type": "object", "required": [ - "Password", - "Username" + "password", + "username" ], "properties": { - "Password": { + "password": { "type": "string" }, - "Username": { + "username": { "type": "string" } } @@ -3885,20 +4339,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 +4360,7 @@ const docTemplate = `{ "Guest" ] }, - "Username": { + "username": { "type": "string", "minLength": 3 } @@ -3915,25 +4369,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,9 +4395,83 @@ const docTemplate = `{ "Guest" ] }, - "Username": { + "username": { + "type": "string", + "minLength": 3 + } + } + }, + "usergroup.addUserGroupRequest": { + "type": "object", + "properties": { + "userGroup": { + "$ref": "#/definitions/usergroup.validNewUserGroup" + } + } + }, + "usergroup.updateUserGroupRequest": { + "type": "object", + "properties": { + "userGroup": { + "$ref": "#/definitions/usergroup.validUpdatedUserGroup" + } + } + }, + "usergroup.validNewScenarioMapping": { + "type": "object", + "required": [ + "scenarioID" + ], + "properties": { + "duplicate": { + "type": "boolean" + }, + "scenarioID": { + "type": "integer" + } + } + }, + "usergroup.validNewUserGroup": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { "type": "string", + "maxLength": 100, "minLength": 3 + }, + "scenarioMappings": { + "type": "array", + "items": { + "$ref": "#/definitions/usergroup.validNewScenarioMapping" + } + } + } + }, + "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" + } } } }, @@ -3966,52 +4494,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 +4547,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..66f5aaa 100644 --- a/doc/api/responses.go +++ b/doc/api/responses.go @@ -60,6 +60,14 @@ type ResponseScenario struct { scenario database.Scenario } +type ResponseUserGroup struct { + usergroup database.UserGroup +} + +type ResponseUserGroups struct { + usergroup []database.UserGroup +} + type ResponseConfigs struct { configs []database.ComponentConfiguration } diff --git a/doc/api/swagger.json b/doc/api/swagger.json index 69cd77a..93e2e37 100644 --- a/doc/api/swagger.json +++ b/doc/api/swagger.json @@ -2694,6 +2694,454 @@ } } }, + "/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": [ + { + "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" + } + } + } + } + }, + "/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" + } + } + } + } + }, + "/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": [ @@ -3322,6 +3770,12 @@ "api.ResponseUser": { "type": "object" }, + "api.ResponseUserGroup": { + "type": "object" + }, + "api.ResponseUserGroups": { + "type": "object" + }, "api.ResponseUsers": { "type": "object" }, @@ -3350,27 +3804,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 +3832,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 +3966,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 +4018,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": { "type": "string" }, - "UUID": { - "type": "string" - }, - "Uptime": { + "uptime": { "type": "number" }, - "WebsocketURL": { + "uuid": { + "type": "string" + }, + "websocketURL": { "type": "string" } } @@ -3620,46 +4074,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 +4148,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 +4205,14 @@ "scenario.validNewScenario": { "type": "object", "required": [ - "Name", - "StartParameters" + "name", + "startParameters" ], "properties": { - "Name": { + "name": { "type": "string" }, - "StartParameters": { + "startParameters": { "$ref": "#/definitions/postgres.Jsonb" } } @@ -3766,13 +4220,13 @@ "scenario.validUpdatedScenario": { "type": "object", "properties": { - "IsLocked": { + "isLocked": { "type": "boolean" }, - "Name": { + "name": { "type": "string" }, - "StartParameters": { + "startParameters": { "$ref": "#/definitions/postgres.Jsonb" } } @@ -3796,32 +4250,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 +4283,16 @@ "signal.validUpdatedSignal": { "type": "object", "properties": { - "Index": { + "index": { "type": "integer" }, - "Name": { + "name": { "type": "string" }, - "ScalingFactor": { + "scalingFactor": { "type": "number" }, - "Unit": { + "unit": { "type": "string" } } @@ -3854,14 +4308,14 @@ "user.loginRequest": { "type": "object", "required": [ - "Password", - "Username" + "password", + "username" ], "properties": { - "Password": { + "password": { "type": "string" }, - "Username": { + "username": { "type": "string" } } @@ -3877,20 +4331,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 +4352,7 @@ "Guest" ] }, - "Username": { + "username": { "type": "string", "minLength": 3 } @@ -3907,25 +4361,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,9 +4387,83 @@ "Guest" ] }, - "Username": { + "username": { + "type": "string", + "minLength": 3 + } + } + }, + "usergroup.addUserGroupRequest": { + "type": "object", + "properties": { + "userGroup": { + "$ref": "#/definitions/usergroup.validNewUserGroup" + } + } + }, + "usergroup.updateUserGroupRequest": { + "type": "object", + "properties": { + "userGroup": { + "$ref": "#/definitions/usergroup.validUpdatedUserGroup" + } + } + }, + "usergroup.validNewScenarioMapping": { + "type": "object", + "required": [ + "scenarioID" + ], + "properties": { + "duplicate": { + "type": "boolean" + }, + "scenarioID": { + "type": "integer" + } + } + }, + "usergroup.validNewUserGroup": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { "type": "string", + "maxLength": 100, "minLength": 3 + }, + "scenarioMappings": { + "type": "array", + "items": { + "$ref": "#/definitions/usergroup.validNewScenarioMapping" + } + } + } + }, + "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" + } } } }, @@ -3958,52 +4486,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 +4539,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..bca42f0 100644 --- a/doc/api/swagger.yaml +++ b/doc/api/swagger.yaml @@ -34,6 +34,10 @@ definitions: type: object api.ResponseUser: type: object + api.ResponseUserGroup: + type: object + api.ResponseUserGroups: + type: object api.ResponseUsers: type: object api.ResponseWidget: @@ -52,34 +56,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 +161,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 +195,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: string - UUID: + type: 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: type: string - UUID: - type: string - Uptime: + uptime: type: number - WebsocketURL: + uuid: + type: string + websocketURL: type: string type: object postgres.Jsonb: @@ -277,19 +281,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 +318,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 +347,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 +386,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 +401,99 @@ 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.updateUserGroupRequest: + properties: + userGroup: + $ref: '#/definitions/usergroup.validUpdatedUserGroup' + type: object + usergroup.validNewScenarioMapping: + properties: + duplicate: + type: boolean + scenarioID: + type: integer + required: + - scenarioID + type: object + usergroup.validNewUserGroup: + properties: + name: + maxLength: 100 + minLength: 3 + type: string + scenarioMappings: + items: + $ref: '#/definitions/usergroup.validNewScenarioMapping' + type: array + required: + - name + 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: widget: @@ -454,67 +506,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 +2324,293 @@ paths: summary: Update a signal 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 + 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 + /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 + /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/register.go b/routes/register.go index 97375a8..35bfc36 100644 --- a/routes/register.go +++ b/routes/register.go @@ -42,6 +42,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" @@ -83,6 +84,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")) diff --git a/routes/user/authenticate_endpoint.go b/routes/user/authenticate_endpoint.go index 51fcd1d..61588be 100644 --- a/routes/user/authenticate_endpoint.go +++ b/routes/user/authenticate_endpoint.go @@ -276,15 +276,8 @@ 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, "") + DuplicateScenarioForUser(so, &myUser.User, "") } else { // add user to scenario err = db.Model(&so).Association("Users").Append(&(myUser.User)).Error if err != nil { @@ -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 57b8ecf..4272d5a 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,83 @@ 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 + } + var configs []database.ComponentConfiguration + err = db.Model(&nsc).Related(&configs, "ComponentConfigurations").Error + if err != nil { + return err + } + + for _, config := range configs { + var ic database.InfrastructureComponent + err = db.Find(&ic, config.ICID).Error + if err != nil { + continue + } + 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"` + 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 { + continue + } + + if session != nil { + if session.IsReady { + err = session.Send(payload, ic.Manager) + if err != nil { + continue + } + err = db.Delete(&ic).Error + if err != nil { + continue + } + + } 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", "Results").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() + db.Model(&sc).Association("Users").Delete(&u) + 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() @@ -581,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.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 + `",` + diff --git a/routes/user/user_test.go b/routes/user/user_test.go index a7121ee..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() @@ -924,7 +886,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 +1087,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_endpoints.go b/routes/usergroup/usergroup_endpoints.go new file mode 100644 index 0000000..718a056 --- /dev/null +++ b/routes/usergroup/usergroup_endpoints.go @@ -0,0 +1,401 @@ +/** +* 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 ( + "errors" + "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" + "github.com/jinzhu/gorm" +) + +func RegisterUserGroupEndpoints(r *gin.RouterGroup) { + r.POST("", addUserGroup) + r.PUT("/:userGroupID", updateUserGroup) + 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 +// @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() + 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() + 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 + users, _, err := ug.getUsers() + if helper.DBError(c, err) { + return + } + + scenarioMappings := ug.ScenarioMappings + db := database.GetDB() + for _, sm := range scenarioMappings { + var sc database.Scenario + err = db.Find(&sc, "ID = ?", sm.ScenarioID).Error + if err != nil { + continue + } + if sm.Duplicate { + for _, u := range users { + err = user.RemoveDuplicate(&sc, &u) + if err != nil { + continue + } + } + } else { + for _, u := range users { + err = user.RemoveAccess(&sc, &u, &ug_r) + if err != nil { + continue + } + } + } + } + // Try to remove user group + err = ug.remove() + if !helper.DBError(c, err) { + c.JSON(http.StatusOK, gin.H{"usergroup": ug}) + } + +} + +// 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 + } + + 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 + } + + if !u.Active { + helper.BadRequestError(c, "bad user") + return + } + + for _, sm := range ug.ScenarioMappings { + + var sc database.Scenario + err := db.Find(&sc, "ID = ?", sm.ScenarioID).Error + if err != nil { + continue + } + + if sm.Duplicate { + err = user.RemoveDuplicate(&sc, &u) + if err != nil { + continue + } + + } else { + err = user.RemoveAccess(&sc, &u, &ug.UserGroup) + if err != nil { + continue + } + } + + } + 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 new file mode 100644 index 0000000..6c60bb3 --- /dev/null +++ b/routes/usergroup/usergroup_methods.go @@ -0,0 +1,208 @@ +/** +* 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 ( + "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" +) + +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 ug.updateScenarioMappings(ug.ID, reqScenarioMappings) +} + +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 + if err != nil { + 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 + } + + // 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 { + return err + } + delete(oldMappingsMap, reqMapping.ScenarioID) + } else { + // Add + newMapping := database.ScenarioMapping{ + ScenarioID: reqMapping.ScenarioID, + 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 + } + } + } + + // 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 + } + } + return nil +} + +func (u *UserGroup) remove() error { + db := database.GetDB() + 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(deletedUser *database.User) error { + db := database.GetDB() + 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 new file mode 100644 index 0000000..838f73f --- /dev/null +++ b/routes/usergroup/usergroup_test.go @@ -0,0 +1,532 @@ +/** +* 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 ( + "encoding/json" + "fmt" + "os" + "strconv" + "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/scenario" + "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"` +} + +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{ + { + ScenarioID: 1, + Duplicate: false, + }, + }, +} + +var newUserGroupTwoMappings = UserGroupRequest{ + Name: "UserGroup2", + ScenarioMappings: []ScenarioMappingRequest{ + { + ScenarioID: 1, + Duplicate: false, + }, + { + ScenarioID: 2, + Duplicate: true, + }, + }, +} + +var deleteTestUg = UserGroupRequest{ + Name: "UserGroup3", + ScenarioMappings: []ScenarioMappingRequest{ + { + ScenarioID: 1, + Duplicate: false, + }, + { + ScenarioID: 2, + Duplicate: true, + }, + { + ScenarioID: 3, + Duplicate: true, + }, + }, +} + +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 { + 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()) + scenario.RegisterScenarioEndpoints(api.Group("/scenarios")) + // 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 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) + assert.Equalf(t, 200, code, "Response body: \n%v\n", resp) + + // 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 + // 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 + 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}) + + 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{}{}) + var scenariosMap map[string]([]database.Scenario) + json.Unmarshal(res.Bytes(), &scenariosMap) + scenarios := scenariosMap["scenarios"] + + //Actual checks + 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) + _, 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, 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) + default: + 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) + } + } + +} + +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) + } + //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) + 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 should still contain usr1 through ug2 + assert.Equal(t, "scenarioNoDups", v.Name) + assert.Equal(t, 3, 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) + } + } + //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) { + // 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{}{}) + } + + //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) + 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 exists []string = []string{"scenarioNoDups", "scenarioDups1", "scenarioDups2"} + var deleted []string = []string{"scenarioDups1 usr1", "scenarioDups2 usr1", "scenarioDups1 usr2", "scenarioDups2 usr2"} + for _, sc := range scenarios { + 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) + } +} + +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"] + 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) + 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) + } + +} 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 +} 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 {