From 083fb0ebe17028dac74d3de9399e1da8a59ce63b Mon Sep 17 00:00:00 2001 From: Nathan Coad Date: Fri, 12 Jan 2024 12:55:49 +1100 Subject: [PATCH] allow user to move secret between safes --- README.md | 78 ++++++++++++++++++++++++++++--------- controllers/storeSecrets.go | 43 ++++++++++++++++++-- models/user.go | 2 + 3 files changed, 101 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 0103703..45733bc 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Written by Nathan Coad (nathan.coad@dell.com) Provide REST API to store and retrieve secrets with associated username, device name and optionally device class. Secrets are stored in sqlite database once encrypted using an AES256 block cipher wrapped in Galois Counter Mode with the standard nonce length. -All secret operations (Create, Read, Update or Delete) require successful authentication. A JWT token is returned upon login, which must be provided for all other operations. +All secret operations (Create, Read, Update or Delete) or user maangement operations require successful authentication. A JWT token is returned upon login, which must be provided for any other operation. Users must be a member of a single group. Groups can have access to multiple safes. Groups can have read-only or read-write access to safes. Only users with an admin role can perform user-related operations such as creating users or groups, or creating/deleting safes. Users and groups can be either locally defined or sourced from LDAP lookups. @@ -83,7 +83,7 @@ WantedBy=multi-user.target ### Login **POST** `/api/login` -Data +Body ``` { "username": "example_username", @@ -95,7 +95,7 @@ This API call will return a JWT token that must be present for any other API cal ### Unlock **POST** `/api/admin/unlock` -Data +Body ``` { "secretKey": "Example32ByteSecretKey0123456789" @@ -112,6 +112,7 @@ This API call can only be made once after the service has started. Subsequent ca **POST** `/api/admin/user/add` Create a new user record by specifying groupId + Body ``` { @@ -122,6 +123,7 @@ Body ``` Create a new user record by specifying groupName + Body ``` { @@ -165,6 +167,7 @@ This operation can only be performed by a user that is admin enabled. Lists curr **POST** `/api/admin/permission/add` Create a new read-only permission directly to a user + Body ``` { @@ -182,6 +185,7 @@ Creates a new permission mapping user/group to safe. Currently the create permis Delete permission by specifying description + Body ``` { @@ -191,6 +195,7 @@ Body ``` Delete permission by specifying permission id + Body ``` { @@ -214,6 +219,7 @@ This operation can only be performed by a user with that is admin enabled. Lists **POST** `/api/admin/group/add` Create a new group corresponding with an LDAP group + Body ``` { @@ -224,6 +230,7 @@ Body ``` Create a new local admin group + Body ``` { @@ -242,6 +249,7 @@ Ldap group must be specified via the full distinguishedName. The simplest way to **POST** `/api/admin/group/delete` Delete group by specifying group name + Body ``` { @@ -251,6 +259,7 @@ Body ``` Delete group by specifying group id + Body ``` { @@ -266,21 +275,37 @@ Deleting a group will also impact all permissions based on that group. For that ### Secrets Operations #### Store -POST `/api/secret/store` +**POST** `/api/secret/store` -Data +Store secret if user only has access to a single safe + +Body ``` { - "deviceName": "", - "deviceCategory": "", - "userName": "", - "secretValue": "" + "deviceName": "device.example.com", + "deviceCategory": "appliance", + "userName": "example-user", + "secretValue": "example-password" +} +``` +Store secret if user only has access to multiple safes + +Body +``` +{ + "safeId": 1, + "deviceName": "device.example.com", + "deviceCategory": "appliance", + "userName": "example-user", + "secretValue": "example-password" } ``` -Must be logged in to execute this command. Role of current user cannot be a ReadOnly role. Secret will be stored with the RoleId of the currently logged in user. Either deviceName or deviceCategory can be blank but not both. +Must be logged in to execute this command. Permission assigned to logged-in user cannot be a ReadOnly role. Either deviceName or deviceCategory can be blank but not both. -If a secret exists for this RoleId and matching deviceName and deviceCategory then an error will be generated. +If a secret exists with a matching deviceName and deviceCategory in a safe that the user has access to, then an error will be generated. + +If the current user has access to multiple safes, then the destination safeId will also need to be specified. #### Retrieve POST `/api/secret/retrieve` @@ -315,19 +340,36 @@ Search for a secret specified by deviceCategory using a GET request. Must be logged in to execute this command. Only secrets registered with the current user's RoleId can be retrieved. #### Update -POST `/api/secret/update` +**POST** `/api/secret/update` -Data +Update secret value for existing secret record + +Body ``` { - "deviceName": "", - "deviceCategory": "", - "userName": "", - "secretValue": "" + "deviceName": "device.example.com", + "deviceCategory": "appliance", + "userName": "example-user", + "secretValue": "new-password" } ``` -Users with ReadOnly role will receive Forbidden error when calling this API endpoint. The values specified in deviceName and deviceCategory must match exactly one existing secret record for the RoleId of the currently logged in user. Wildcards are supported for deviceName and deviceCategory. +Move secret into different safe + +Body +``` +{ + "safeId": 2, + "deviceName": "device.example.com", + "deviceCategory": "appliance", + "userName": "example-user", + "secretValue": "example-password" +} +``` + +The values specified in deviceName and deviceCategory must match exactly one existing secret record for the safes available to the currently logged in user. Wildcards are supported for deviceName and deviceCategory. + +If a user has read-write access to multiple safes, then specifying a different safeId to the one currently holding the secret will allow the secret to be moved into the other safe. #### List GET `/api/secret/list` diff --git a/controllers/storeSecrets.go b/controllers/storeSecrets.go index 976221d..04c42a2 100644 --- a/controllers/storeSecrets.go +++ b/controllers/storeSecrets.go @@ -229,7 +229,7 @@ func CheckUpdateSecretAllowed(s *models.Secret, user_id int) (int, error) { func UpdateSecret(c *gin.Context) { var err error var input SecretInput - var user_id int + var UserId int if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "UpdateSecret error binding to input JSON : " + err.Error()}) @@ -260,7 +260,7 @@ func UpdateSecret(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "error determining user"}) return } else { - user_id = val.(int) + UserId = val.(int) //log.Printf("user_id: %v\n", user_id) } @@ -271,10 +271,12 @@ func UpdateSecret(c *gin.Context) { s.DeviceName = input.DeviceName s.DeviceCategory = input.DeviceCategory - secretList, err := models.SecretsGetAllowed(&s, user_id) + secretList, err := models.SecretsGetAllowed(&s, UserId) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("error determining secret : '%s'", err)}) + errString := fmt.Sprintf("error determining secret : '%s'", err) + log.Printf("UpdateSecret %s\n", errString) + c.JSON(http.StatusBadRequest, gin.H{"error": errString}) return } @@ -293,6 +295,39 @@ func UpdateSecret(c *gin.Context) { return } + // Check for correct safe + if input.SafeId > 0 { + if input.SafeId != secretList[0].Secret.SafeId { + + // Check if user has access to the new safe + allowedSafes, err := models.UserGetSafesAllowed(UserId) + if err != nil { + errString := fmt.Sprintf("error determining allowed safes : '%s'", err) + log.Printf("UpdateSecret %s\n", errString) + c.JSON(http.StatusBadRequest, gin.H{"error": errString}) + return + } + + allowedFound := false + for i := range allowedSafes { + if allowedSafes[i].SafeId == input.SafeId { + allowedFound = true + break + } + } + + if !allowedFound { + errString := "secret cannot be moved into inaccessible safe" + log.Printf("UpdateSecret %s\n", errString) + c.JSON(http.StatusBadRequest, gin.H{"error": errString}) + return + } + + log.Printf("UpdateSecret moving secret id '%d' into safe id '%d'\n", secretList[0].SecretId, input.SafeId) + s.SafeId = input.SafeId + } + } + s.SecretId = secretList[0].SecretId // check for empty fields in the update request and update from the existing record diff --git a/models/user.go b/models/user.go index 892ee5c..89c55f2 100644 --- a/models/user.go +++ b/models/user.go @@ -280,6 +280,7 @@ func UserLdapNewLoginCheck(username string, password string) (User, error) { return u, nil } +/* // StoreLdapUser creates a user record in the database and returns the corresponding userId func StoreLdapUser(u *User) error { @@ -287,6 +288,7 @@ func StoreLdapUser(u *User) error { return nil } +*/ func UserGetByID(uid uint) (User, error) {