Merge pull request #1 from 1Password/get_by_title

Allow vault and item titles in item path
This commit is contained in:
Jillian W
2020-12-18 12:59:22 -04:00
committed by GitHub
9 changed files with 176 additions and 28 deletions

View File

@@ -77,9 +77,7 @@ kind: OnePasswordItem # {insert_new_name}
metadata: metadata:
name: {item_name} #this name will also be used for naming the generated kubernetes secret name: {item_name} #this name will also be used for naming the generated kubernetes secret
spec: spec:
item-path: "vaults/{vaultId}/items/{itemId}" item-path: "vaults/{vault_id_or_title}/items/{item_id_or_title}"
# 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
``` ```
Deploy the OnePasswordItem to Kubernetes: Deploy the OnePasswordItem to Kubernetes:
@@ -104,7 +102,7 @@ kind: Deployment
metadata: metadata:
name: deployment-example name: deployment-example
annotations: 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}" 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. 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 ## Development
### Running Tests ### Running Tests

View File

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

2
go.mod
View File

@@ -3,7 +3,7 @@ module github.com/1Password/onepassword-operator
go 1.13 go 1.13
require ( 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/go-logr/logr v0.1.0 // indirect
github.com/operator-framework/operator-sdk v0.19.0 github.com/operator-framework/operator-sdk v0.19.0
github.com/pkg/errors v0.9.1 // indirect 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= 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 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.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.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-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= 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 { type TestClient struct {
GetVaultsFunc func() ([]onepassword.Vault, error) GetVaultsFunc func() ([]onepassword.Vault, error)
GetItemFunc func(uuid string, vaultUUID string) (*onepassword.Item, error) GetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error)
GetItemsFunc func(vaultUUID string) ([]onepassword.Item, error) GetItemFunc func(uuid string, vaultUUID string) (*onepassword.Item, error)
GetItemByTitleFunc func(title string, vaultUUID string) (*onepassword.Item, error) GetItemsFunc func(vaultUUID string) ([]onepassword.Item, error)
CreateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) GetItemsByTitleFunc func(title string, vaultUUID string) ([]onepassword.Item, error)
UpdateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) GetItemByTitleFunc func(title string, vaultUUID string) (*onepassword.Item, error)
DeleteItemFunc func(item *onepassword.Item, vaultUUID string) 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 ( var (
GetGetVaultsFunc func() ([]onepassword.Vault, error) GetGetVaultsFunc func() ([]onepassword.Vault, error)
GetGetItemFunc func(uuid string, vaultUUID string) (*onepassword.Item, error) DoGetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error)
DoGetItemByTitleFunc func(title string, vaultUUID string) (*onepassword.Item, error) GetGetItemFunc func(uuid string, vaultUUID string) (*onepassword.Item, error)
DoCreateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) DoGetItemsByTitleFunc func(title string, vaultUUID string) ([]onepassword.Item, error)
DoDeleteItemFunc func(item *onepassword.Item, vaultUUID string) error DoGetItemByTitleFunc func(title string, vaultUUID string) (*onepassword.Item, error)
DoGetItemsFunc func(vaultUUID string) ([]onepassword.Item, error) DoCreateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
DoUpdateItemFunc 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 // Do is the mock client's `Do` func
@@ -29,6 +33,10 @@ func (m *TestClient) GetVaults() ([]onepassword.Vault, error) {
return GetGetVaultsFunc() 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) { func (m *TestClient) GetItem(uuid string, vaultUUID string) (*onepassword.Item, error) {
return GetGetItemFunc(uuid, vaultUUID) return GetGetItemFunc(uuid, vaultUUID)
} }
@@ -37,6 +45,10 @@ func (m *TestClient) GetItems(vaultUUID string) ([]onepassword.Item, error) {
return DoGetItemsFunc(vaultUUID) 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) { func (m *TestClient) GetItemByTitle(title string, vaultUUID string) (*onepassword.Item, error) {
return DoGetItemByTitleFunc(title, vaultUUID) return DoGetItemByTitleFunc(title, vaultUUID)
} }

View File

@@ -6,13 +6,26 @@ import (
"github.com/1Password/connect-sdk-go/connect" "github.com/1Password/connect-sdk-go/connect"
"github.com/1Password/connect-sdk-go/onepassword" "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) { func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*onepassword.Item, error) {
vaultId, itemId, err := ParseVaultIdAndItemIdFromPath(path) vaultValue, itemValue, err := ParseVaultAndItemFromPath(path)
if err != nil { if err != nil {
return nil, err 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) item, err := opConnectClient.GetItem(itemId, vaultId)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -20,10 +33,60 @@ func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*one
return item, nil return item, nil
} }
func ParseVaultIdAndItemIdFromPath(path string) (string, string, error) { func ParseVaultAndItemFromPath(path string) (string, string, error) {
splitPath := strings.Split(path, "/") splitPath := strings.Split(path, "/")
if len(splitPath) == 4 && splitPath[0] == "vaults" && splitPath[2] == "items" { if len(splitPath) == 4 && splitPath[0] == "vaults" && splitPath[2] == "items" {
return splitPath[1], splitPath[3], nil 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) 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 // Client Represents an available 1Password Connect API to connect to
type Client interface { type Client interface {
GetVaults() ([]onepassword.Vault, error) GetVaults() ([]onepassword.Vault, error)
GetVaultsByTitle(uuid string) ([]onepassword.Vault, error)
GetItem(uuid string, vaultUUID string) (*onepassword.Item, error) GetItem(uuid string, vaultUUID string) (*onepassword.Item, error)
GetItems(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) GetItemByTitle(title string, vaultUUID string) (*onepassword.Item, error)
CreateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) CreateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
UpdateItem(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 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 // GetItem Get a specific Item from the 1Password Connect API
func (rs *restClient) GetItem(uuid string, vaultUUID string) (*onepassword.Item, error) { func (rs *restClient) GetItem(uuid string, vaultUUID string) (*onepassword.Item, error) {
span := rs.tracer.StartSpan("GetItem") 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) { func (rs *restClient) GetItemByTitle(title string, vaultUUID string) (*onepassword.Item, error) {
span := rs.tracer.StartSpan("GetItemByTitle") span := rs.tracer.StartSpan("GetItemByTitle")
defer span.Finish() 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)) filter := url.QueryEscape(fmt.Sprintf("title eq \"%s\"", title))
itemURL := fmt.Sprintf("/v1/vaults/%s/items?filter=%s", vaultUUID, filter) 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 return nil, err
} }
if len(items) != 1 { return items, nil
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) GetItems(vaultUUID string) ([]onepassword.Item, error) { 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 v0.49.0
cloud.google.com/go/compute/metadata 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/connect
github.com/1Password/connect-sdk-go/onepassword github.com/1Password/connect-sdk-go/onepassword
# github.com/Azure/go-autorest/autorest v0.9.3-0.20191028180845-3492b2aff503 # github.com/Azure/go-autorest/autorest v0.9.3-0.20191028180845-3492b2aff503