diff --git a/README.md b/README.md index a8866f0..956ef6b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/deploy/operator.yaml b/deploy/operator.yaml index bf83c74..4da8555 100644 --- a/deploy/operator.yaml +++ b/deploy/operator.yaml @@ -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 diff --git a/go.mod b/go.mod index 9fddd96..d45f40a 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 778078e..fbc1b30 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/mocks/mocksecretserver.go b/pkg/mocks/mocksecretserver.go index 7efcab5..489182f 100644 --- a/pkg/mocks/mocksecretserver.go +++ b/pkg/mocks/mocksecretserver.go @@ -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) } diff --git a/pkg/onepassword/items.go b/pkg/onepassword/items.go index bceee0e..11c4914 100644 --- a/pkg/onepassword/items.go +++ b/pkg/onepassword/items.go @@ -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 +} diff --git a/pkg/onepassword/uuid.go b/pkg/onepassword/uuid.go new file mode 100644 index 0000000..4d250f0 --- /dev/null +++ b/pkg/onepassword/uuid.go @@ -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 +} diff --git a/vendor/github.com/1Password/connect-sdk-go/connect/client.go b/vendor/github.com/1Password/connect-sdk-go/connect/client.go index 0d9d89f..ba05753 100644 --- a/vendor/github.com/1Password/connect-sdk-go/connect/client.go +++ b/vendor/github.com/1Password/connect-sdk-go/connect/client.go @@ -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) { diff --git a/vendor/modules.txt b/vendor/modules.txt index f5a39c5..74cde81 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -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