Allow vault and item titles in item path

Items can now be accessed by either vaults/<vault_id>/items/<item_id> or vaults/<vault_title>/items/<item_title>
This commit is contained in:
jillianwilson
2020-12-17 16:31:48 -04:00
parent 824f54b4fa
commit dae6e51112
9 changed files with 176 additions and 28 deletions

View File

@@ -77,9 +77,7 @@ kind: OnePasswordItem # {insert_new_name}
metadata:
name: {item_name} #this name will also be used for naming the generated kubernetes secret
spec:
item-path: "vaults/{vaultId}/items/{itemId}"
# where vaultId is the id of the vault in which to find the item
# where itemId is the id of the item that you want to store as a Kubernetes Secret
item-path: "vaults/{vault_id_or_title}/items/{item_id_or_title}"
```
Deploy the OnePasswordItem to Kubernetes:
@@ -104,7 +102,7 @@ kind: Deployment
metadata:
name: deployment-example
annotations:
onepasswordoperator/item-path: "vaults/{vaultId}/items/{itemId}"
onepasswordoperator/item-path: "vaults/{vault_id_or_title}/items/{item_id_or_title}"
onepasswordoperator/item-name: "{secret_name}"
```
@@ -114,6 +112,13 @@ Note: Deleting the Deployment that you've created will automatically delete the
If a 1Password Item that is linked to a Kubernetes Secret is updated within the `POLLING_INTERVAL` the associated Kubernetes Secret will be updated. Furthermore, any deployments using that secret will be given a rolling restart.
---
**NOTE**
If multiple 1Password vaults/items have the same `title` when using a title in the access path, the desired action will be performed on the oldest vault/item. Furthermore, titles that include white space characters cannot be used.
---
## Development
### Running Tests

View File

@@ -28,7 +28,7 @@ spec:
- name: OPERATOR_NAME
value: "onepassword-connect-operator"
- name: OP_CONNECT_HOST
value: "http://secret-service:8080"
value: "http://onepassword-connect:8080"
- name: POLLING_INTERVAL
value: "10"
- name: OP_CONNECT_TOKEN

2
go.mod
View File

@@ -3,7 +3,7 @@ module github.com/1Password/onepassword-operator
go 1.13
require (
github.com/1Password/connect-sdk-go v0.0.1
github.com/1Password/connect-sdk-go v0.0.2
github.com/go-logr/logr v0.1.0 // indirect
github.com/operator-framework/operator-sdk v0.19.0
github.com/pkg/errors v0.9.1 // indirect

2
go.sum
View File

@@ -21,6 +21,8 @@ contrib.go.opencensus.io/exporter/ocagent v0.6.0/go.mod h1:zmKjrJcdo0aYcVS7bmEeS
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/1Password/connect-sdk-go v0.0.1 h1:qsFZQDQ+JirZRwSom/p6zzNqkkcYAYx4EXivUyPhvBo=
github.com/1Password/connect-sdk-go v0.0.1/go.mod h1:br2BWk2sqgJFnOFK5WSDfBBmwQ6E7hV9LoPqrtHGRNY=
github.com/1Password/connect-sdk-go v0.0.2 h1:IBamxGS17zItC9TRwp/0G0Fh1GRV3mqOkcWvpK05Mx8=
github.com/1Password/connect-sdk-go v0.0.2/go.mod h1:br2BWk2sqgJFnOFK5WSDfBBmwQ6E7hV9LoPqrtHGRNY=
github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4=
github.com/Azure/azure-pipeline-go v0.2.2/go.mod h1:4rQ/NZncSvGqNkkOsNpOU1tgoNuIlp9AfUH5G1tvCHc=
github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=

View File

@@ -5,23 +5,27 @@ import (
)
type TestClient struct {
GetVaultsFunc func() ([]onepassword.Vault, error)
GetItemFunc func(uuid string, vaultUUID string) (*onepassword.Item, error)
GetItemsFunc func(vaultUUID string) ([]onepassword.Item, error)
GetItemByTitleFunc func(title string, vaultUUID string) (*onepassword.Item, error)
CreateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
UpdateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
DeleteItemFunc func(item *onepassword.Item, vaultUUID string) error
GetVaultsFunc func() ([]onepassword.Vault, error)
GetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error)
GetItemFunc func(uuid string, vaultUUID string) (*onepassword.Item, error)
GetItemsFunc func(vaultUUID string) ([]onepassword.Item, error)
GetItemsByTitleFunc func(title string, vaultUUID string) ([]onepassword.Item, error)
GetItemByTitleFunc func(title string, vaultUUID string) (*onepassword.Item, error)
CreateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
UpdateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
DeleteItemFunc func(item *onepassword.Item, vaultUUID string) error
}
var (
GetGetVaultsFunc func() ([]onepassword.Vault, error)
GetGetItemFunc func(uuid string, vaultUUID string) (*onepassword.Item, error)
DoGetItemByTitleFunc func(title string, vaultUUID string) (*onepassword.Item, error)
DoCreateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
DoDeleteItemFunc func(item *onepassword.Item, vaultUUID string) error
DoGetItemsFunc func(vaultUUID string) ([]onepassword.Item, error)
DoUpdateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
GetGetVaultsFunc func() ([]onepassword.Vault, error)
DoGetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error)
GetGetItemFunc func(uuid string, vaultUUID string) (*onepassword.Item, error)
DoGetItemsByTitleFunc func(title string, vaultUUID string) ([]onepassword.Item, error)
DoGetItemByTitleFunc func(title string, vaultUUID string) (*onepassword.Item, error)
DoCreateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
DoDeleteItemFunc func(item *onepassword.Item, vaultUUID string) error
DoGetItemsFunc func(vaultUUID string) ([]onepassword.Item, error)
DoUpdateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
)
// Do is the mock client's `Do` func
@@ -29,6 +33,10 @@ func (m *TestClient) GetVaults() ([]onepassword.Vault, error) {
return GetGetVaultsFunc()
}
func (m *TestClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error) {
return DoGetVaultsByTitleFunc(title)
}
func (m *TestClient) GetItem(uuid string, vaultUUID string) (*onepassword.Item, error) {
return GetGetItemFunc(uuid, vaultUUID)
}
@@ -37,6 +45,10 @@ func (m *TestClient) GetItems(vaultUUID string) ([]onepassword.Item, error) {
return DoGetItemsFunc(vaultUUID)
}
func (m *TestClient) GetItemsByTitle(title, vaultUUID string) ([]onepassword.Item, error) {
return DoGetItemsByTitleFunc(title, vaultUUID)
}
func (m *TestClient) GetItemByTitle(title string, vaultUUID string) (*onepassword.Item, error) {
return DoGetItemByTitleFunc(title, vaultUUID)
}

View File

@@ -6,13 +6,26 @@ import (
"github.com/1Password/connect-sdk-go/connect"
"github.com/1Password/connect-sdk-go/onepassword"
logf "sigs.k8s.io/controller-runtime/pkg/log"
)
var logger = logf.Log.WithName("retrieve_item")
func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*onepassword.Item, error) {
vaultId, itemId, err := ParseVaultIdAndItemIdFromPath(path)
vaultValue, itemValue, err := ParseVaultAndItemFromPath(path)
if err != nil {
return nil, err
}
vaultId, err := getVaultId(opConnectClient, vaultValue)
if err != nil {
return nil, err
}
itemId, err := getItemId(opConnectClient, itemValue, vaultId)
if err != nil {
return nil, err
}
item, err := opConnectClient.GetItem(itemId, vaultId)
if err != nil {
return nil, err
@@ -20,10 +33,60 @@ func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*one
return item, nil
}
func ParseVaultIdAndItemIdFromPath(path string) (string, string, error) {
func ParseVaultAndItemFromPath(path string) (string, string, error) {
splitPath := strings.Split(path, "/")
if len(splitPath) == 4 && splitPath[0] == "vaults" && splitPath[2] == "items" {
return splitPath[1], splitPath[3], nil
}
return "", "", fmt.Errorf("%q is not an acceptable path for One Password item. Must be of the format: `vaults/{vault_id}/items/{item_id}`", path)
}
func getVaultId(client connect.Client, vaultIdentifier string) (string, error) {
if !IsValidClientUUID(vaultIdentifier) {
vaults, err := client.GetVaultsByTitle(vaultIdentifier)
if err != nil {
return "", err
}
if len(vaults) == 0 {
return "", fmt.Errorf("No vaults found with identifier %q", vaultIdentifier)
}
oldestVault := vaults[0]
if len(vaults) > 1 {
for _, returnedVault := range vaults {
if returnedVault.CreatedAt.Before(oldestVault.CreatedAt) {
oldestVault = returnedVault
}
}
logger.Info(fmt.Sprintf("%v 1Password vaults found with the title %q. Will use vault %q as it is the oldest.", len(vaults), vaultIdentifier, oldestVault.ID))
}
vaultIdentifier = oldestVault.ID
}
return vaultIdentifier, nil
}
func getItemId(client connect.Client, itemIdentifier string, vaultId string) (string, error) {
if !IsValidClientUUID(itemIdentifier) {
items, err := client.GetItemsByTitle(itemIdentifier, vaultId)
if err != nil {
return "", err
}
if len(items) == 0 {
return "", fmt.Errorf("No items found with identifier %q", itemIdentifier)
}
oldestItem := items[0]
if len(items) > 1 {
for _, returnedItem := range items {
if returnedItem.CreatedAt.Before(oldestItem.CreatedAt) {
oldestItem = returnedItem
}
}
logger.Info(fmt.Sprintf("%v 1Password items found with the title %q. Will use item %q as it is the oldest.", len(items), itemIdentifier, oldestItem.ID))
}
itemIdentifier = oldestItem.ID
}
return itemIdentifier, nil
}

20
pkg/onepassword/uuid.go Normal file
View File

@@ -0,0 +1,20 @@
package onepassword
// UUIDLength defines the required length of UUIDs
const UUIDLength = 26
// IsValidClientUUID returns true if the given client uuid is valid.
func IsValidClientUUID(uuid string) bool {
if len(uuid) != UUIDLength {
return false
}
for _, c := range uuid {
valid := (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')
if !valid {
return false
}
}
return true
}

View File

@@ -24,8 +24,10 @@ const (
// Client Represents an available 1Password Connect API to connect to
type Client interface {
GetVaults() ([]onepassword.Vault, error)
GetVaultsByTitle(uuid string) ([]onepassword.Vault, error)
GetItem(uuid string, vaultUUID string) (*onepassword.Item, error)
GetItems(vaultUUID string) ([]onepassword.Item, error)
GetItemsByTitle(title string, vaultUUID string) ([]onepassword.Item, error)
GetItemByTitle(title string, vaultUUID string) (*onepassword.Item, error)
CreateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
UpdateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
@@ -127,6 +129,39 @@ func (rs *restClient) GetVaults() ([]onepassword.Vault, error) {
return vaults, nil
}
func (rs *restClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error) {
span := rs.tracer.StartSpan("GetVaultsByTitle")
defer span.Finish()
filter := url.QueryEscape(fmt.Sprintf("title eq \"%s\"", title))
itemURL := fmt.Sprintf("/v1/vaults?filter=%s", filter)
request, err := rs.buildRequest(http.MethodGet, itemURL, http.NoBody, span)
if err != nil {
return nil, err
}
response, err := rs.client.Do(request)
if err != nil {
return nil, err
}
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Unable to retrieve vaults. Receieved %q for %q", response.Status, itemURL)
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
vaults := []onepassword.Vault{}
if err := json.Unmarshal(body, &vaults); err != nil {
return nil, err
}
return vaults, nil
}
// GetItem Get a specific Item from the 1Password Connect API
func (rs *restClient) GetItem(uuid string, vaultUUID string) (*onepassword.Item, error) {
span := rs.tracer.StartSpan("GetItem")
@@ -163,6 +198,21 @@ func (rs *restClient) GetItem(uuid string, vaultUUID string) (*onepassword.Item,
func (rs *restClient) GetItemByTitle(title string, vaultUUID string) (*onepassword.Item, error) {
span := rs.tracer.StartSpan("GetItemByTitle")
defer span.Finish()
items, err := rs.GetItemsByTitle(title, vaultUUID)
if err != nil {
return nil, err
}
if len(items) != 1 {
return nil, fmt.Errorf("Found %d item(s) in vault %q with title %q", len(items), vaultUUID, title)
}
return rs.GetItem(items[0].ID, items[0].Vault.ID)
}
func (rs *restClient) GetItemsByTitle(title string, vaultUUID string) ([]onepassword.Item, error) {
span := rs.tracer.StartSpan("GetItemsByTitle")
defer span.Finish()
filter := url.QueryEscape(fmt.Sprintf("title eq \"%s\"", title))
itemURL := fmt.Sprintf("/v1/vaults/%s/items?filter=%s", vaultUUID, filter)
@@ -190,11 +240,7 @@ func (rs *restClient) GetItemByTitle(title string, vaultUUID string) (*onepasswo
return nil, err
}
if len(items) != 1 {
return nil, fmt.Errorf("Found %d item(s) in vault %q with title %q", len(items), vaultUUID, title)
}
return rs.GetItem(items[0].ID, items[0].Vault.ID)
return items, nil
}
func (rs *restClient) GetItems(vaultUUID string) ([]onepassword.Item, error) {

2
vendor/modules.txt vendored
View File

@@ -1,6 +1,6 @@
# cloud.google.com/go v0.49.0
cloud.google.com/go/compute/metadata
# github.com/1Password/connect-sdk-go v0.0.1
# github.com/1Password/connect-sdk-go v0.0.2
github.com/1Password/connect-sdk-go/connect
github.com/1Password/connect-sdk-go/onepassword
# github.com/Azure/go-autorest/autorest v0.9.3-0.20191028180845-3492b2aff503