allow user to move secret between safes
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2024-01-12 12:55:49 +11:00
parent a3333cebb6
commit 083fb0ebe1
3 changed files with 101 additions and 22 deletions

View File

@@ -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. 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. 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 ### Login
**POST** `/api/login` **POST** `/api/login`
Data Body
``` ```
{ {
"username": "example_username", "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 ### Unlock
**POST** `/api/admin/unlock` **POST** `/api/admin/unlock`
Data Body
``` ```
{ {
"secretKey": "Example32ByteSecretKey0123456789" "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` **POST** `/api/admin/user/add`
Create a new user record by specifying groupId Create a new user record by specifying groupId
Body Body
``` ```
{ {
@@ -122,6 +123,7 @@ Body
``` ```
Create a new user record by specifying groupName Create a new user record by specifying groupName
Body 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` **POST** `/api/admin/permission/add`
Create a new read-only permission directly to a user Create a new read-only permission directly to a user
Body Body
``` ```
{ {
@@ -182,6 +185,7 @@ Creates a new permission mapping user/group to safe. Currently the create permis
Delete permission by specifying description Delete permission by specifying description
Body Body
``` ```
{ {
@@ -191,6 +195,7 @@ Body
``` ```
Delete permission by specifying permission id Delete permission by specifying permission id
Body 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` **POST** `/api/admin/group/add`
Create a new group corresponding with an LDAP group Create a new group corresponding with an LDAP group
Body Body
``` ```
{ {
@@ -224,6 +230,7 @@ Body
``` ```
Create a new local admin group Create a new local admin group
Body Body
``` ```
{ {
@@ -242,6 +249,7 @@ Ldap group must be specified via the full distinguishedName. The simplest way to
**POST** `/api/admin/group/delete` **POST** `/api/admin/group/delete`
Delete group by specifying group name Delete group by specifying group name
Body Body
``` ```
{ {
@@ -251,6 +259,7 @@ Body
``` ```
Delete group by specifying group id Delete group by specifying group id
Body Body
``` ```
{ {
@@ -266,21 +275,37 @@ Deleting a group will also impact all permissions based on that group. For that
### Secrets Operations ### Secrets Operations
#### Store #### Store
POST `/api/secret/store` **POST** `/api/secret/store`
Data Store secret if user only has access to a single safe
Body
``` ```
{ {
"deviceName": "", "deviceName": "device.example.com",
"deviceCategory": "", "deviceCategory": "appliance",
"userName": "", "userName": "example-user",
"secretValue": "" "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 #### Retrieve
POST `/api/secret/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. Must be logged in to execute this command. Only secrets registered with the current user's RoleId can be retrieved.
#### Update #### Update
POST `/api/secret/update` **POST** `/api/secret/update`
Data Update secret value for existing secret record
Body
``` ```
{ {
"deviceName": "", "deviceName": "device.example.com",
"deviceCategory": "", "deviceCategory": "appliance",
"userName": "", "userName": "example-user",
"secretValue": "" "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 #### List
GET `/api/secret/list` GET `/api/secret/list`

View File

@@ -229,7 +229,7 @@ func CheckUpdateSecretAllowed(s *models.Secret, user_id int) (int, error) {
func UpdateSecret(c *gin.Context) { func UpdateSecret(c *gin.Context) {
var err error var err error
var input SecretInput var input SecretInput
var user_id int var UserId int
if err := c.ShouldBindJSON(&input); err != nil { if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "UpdateSecret error binding to input JSON : " + err.Error()}) 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"}) c.JSON(http.StatusBadRequest, gin.H{"error": "error determining user"})
return return
} else { } else {
user_id = val.(int) UserId = val.(int)
//log.Printf("user_id: %v\n", user_id) //log.Printf("user_id: %v\n", user_id)
} }
@@ -271,10 +271,12 @@ func UpdateSecret(c *gin.Context) {
s.DeviceName = input.DeviceName s.DeviceName = input.DeviceName
s.DeviceCategory = input.DeviceCategory s.DeviceCategory = input.DeviceCategory
secretList, err := models.SecretsGetAllowed(&s, user_id) secretList, err := models.SecretsGetAllowed(&s, UserId)
if err != nil { 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 return
} }
@@ -293,6 +295,39 @@ func UpdateSecret(c *gin.Context) {
return 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 s.SecretId = secretList[0].SecretId
// check for empty fields in the update request and update from the existing record // check for empty fields in the update request and update from the existing record

View File

@@ -280,6 +280,7 @@ func UserLdapNewLoginCheck(username string, password string) (User, error) {
return u, nil return u, nil
} }
/*
// StoreLdapUser creates a user record in the database and returns the corresponding userId // StoreLdapUser creates a user record in the database and returns the corresponding userId
func StoreLdapUser(u *User) error { func StoreLdapUser(u *User) error {
@@ -287,6 +288,7 @@ func StoreLdapUser(u *User) error {
return nil return nil
} }
*/
func UserGetByID(uid uint) (User, error) { func UserGetByID(uid uint) (User, error) {