Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: CCP secret manager lookup(https://github.com/ansible-collections/google.cloud/pull/357) #628

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions plugins/lookup/gcp_secret_access.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2020, Pavlo Bashynskyi (@levonet) <[email protected]>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

DOCUMENTATION = r"""
---
lookup: gcp_secret_access
author:
- Pavlo Bashynskyi (@levonet)
short_description: Retrieve secrets from GCP Secret Manager
requirements:
- python >= 2.7
- google-auth >= 1.26.0
- google-cloud-secret-manager >= 1.0.0
description:
- Retrieve secret contents from GCP Secret Manager.
- Accessing to secret content requires the Secret Manager Secret Accessor role (C(roles/secretmanager.secretAccessor)) on the secret, project, folder, or organization.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Accessing to secret content requires the Secret Manager Secret Accessor role (C(roles/secretmanager.secretAccessor)) on the secret, project, folder, or organization.
- Accessing to secret content requires:
- the Secret Manager Secret Accessor role (C(roles/secretmanager.secretAccessor)) on the secret, project, folder, or organization.

options:
secret:
description:
- Secret name or resource id. Resource id should be in format C(projects/*/secrets/*/versions/*).
- The project option is required if a secret name is used instead of resource id.
required: True
type: str
version:
description: Version id of secret. You can also access the latest version of a secret by specifying "C(latest)" as the version.
type: str
default: latest
project:
description: The Google Cloud Platform project to use.
type: str
env:
- name: GCP_PROJECT
access_token:
description:
- The Google Cloud access token. If specified, C(service_account_file) will be ignored.
type: str
env:
- name: GCP_ACCESS_TOKEN
service_account_file:
description:
- The path of a Service Account JSON file if serviceaccount is selected as type.
type: path
env:
- name: GOOGLE_APPLICATION_CREDENTIALS
- name: GCP_SERVICE_ACCOUNT_FILE
notes:
- When I(secret) is the first option in the term string, C(secret=) is not required (see examples).
- If you’re running your application elsewhere, you should download a service account JSON keyfile and point to it using the secret option or an environment variable C(GOOGLE_APPLICATION_CREDENTIALS="/path/to/keyfile.json").
Copy link

@jack-rep jack-rep Jun 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- If youre running your application elsewhere, you should download a service account JSON keyfile and point to it using the secret option or an environment variable C(GOOGLE_APPLICATION_CREDENTIALS="/path/to/keyfile.json").
- If you're running your application elsewhere, you should download a service account JSON keyfile and
- point to it using the secret option or an environment variable C(GOOGLE_APPLICATION_CREDENTIALS="/path/to/keyfile.json").

"""

EXAMPLES = r"""
- ansible.builtin.debug:
msg: "{{ lookup('google.cloud.gcp_secret_access', secret='hola', project='test_project') }}"

- ansible.builtin.debug:
msg: "{{ lookup('google.cloud.gcp_secret_access', 'hola', project='test_project') }}"

- name: using resource id instead of secret name
ansible.builtin.debug:
msg: "{{ lookup('google.cloud.gcp_secret_access', 'projects/112233445566/secrets/hola/versions/1') }}"

- name: using service account file
ansible.builtin.debug:
msg: "{{ lookup('google.cloud.gcp_secret_access', 'hola', project='test_project', service_account_file='/path/to/keyfile.json') }}"
"""

RETURN = r"""
_raw:
description:
- secrets requested
"""

from ansible.errors import AnsibleError
from ansible.plugins.lookup import LookupBase
from ansible_collections.google.cloud.plugins.plugin_utils.gcp_utils import GcpSecretLookup

try:
from google.cloud import secretmanager

HAS_GOOGLE_SECRET_MANAGER_LIBRARY = True
except ImportError:
HAS_GOOGLE_SECRET_MANAGER_LIBRARY = False


class GcpSecretAccessLookup(GcpSecretLookup):
def run(self, terms, variables=None, **kwargs):
self.set_plugin_name('google.cloud.gcp_secret_access')
self.process_options(terms, variables=None, **kwargs)

response = self.client(secretmanager).access_secret_version(request={"name": self.name})
payload = response.payload.data.decode("UTF-8")

return [payload]


class LookupModule(LookupBase):
def run(self, terms, variables=None, **kwargs):
if not HAS_GOOGLE_SECRET_MANAGER_LIBRARY:
raise AnsibleError("Please install the google-cloud-secret-manager Python library")

return GcpSecretAccessLookup().run(terms, variables=variables, **kwargs)
92 changes: 92 additions & 0 deletions plugins/lookup/gcp_secret_resource_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2020, Pavlo Bashynskyi (@levonet) <[email protected]>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

DOCUMENTATION = r"""
---
lookup: gcp_secret_resource_id
author:
- Pavlo Bashynskyi (@levonet)
short_description: Retrieve resource id of secret version from GCP Secret Manager
requirements:
- python >= 2.7
- google-auth >= 1.26.0
- google-cloud-secret-manager >= 1.0.0
description:
- Retrieve resource id of secret version from GCP Secret Manager.
options:
secret:
description:
- Secret name or resource id. Resource id should be in format C(projects/*/secrets/*/versions/*).
- The project option is required if a secret name is used instead of resource id.
required: True
type: str
version:
description: Version id of secret. You can also access the latest version of a secret by specifying "C(latest)" as the version.
type: str
default: latest
project:
description: The Google Cloud Platform project to use.
type: str
env:
- name: GCP_PROJECT
access_token:
description:
- The Google Cloud access token. If specified, C(service_account_file) will be ignored.
type: str
env:
- name: GCP_ACCESS_TOKEN
service_account_file:
description:
- The path of a Service Account JSON file if serviceaccount is selected as type.
type: path
env:
- name: GOOGLE_APPLICATION_CREDENTIALS
- name: GCP_SERVICE_ACCOUNT_FILE
notes:
- When I(secret) is the first option in the term string, C(secret=) is not required (see examples).
- If you’re running your application elsewhere, you should download a service account JSON keyfile and point to it using the secret option or an environment variable C(GOOGLE_APPLICATION_CREDENTIALS="/path/to/keyfile.json").
Copy link

@jack-rep jack-rep Jun 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- If youre running your application elsewhere, you should download a service account JSON keyfile and point to it using the secret option or an environment variable C(GOOGLE_APPLICATION_CREDENTIALS="/path/to/keyfile.json").
- If you're running your application elsewhere, you should download a service account JSON keyfile and
- point to it using the secret option or an environment variable C(GOOGLE_APPLICATION_CREDENTIALS="/path/to/keyfile.json").

"""

EXAMPLES = r"""
- ansible.builtin.debug:
msg: "{{ lookup('google.cloud.gcp_secret_resource_id', secret='hola', project='test_project') }}"
"""

RETURN = r"""
_raw:
description:
- resource id of secret version
"""

from ansible.errors import AnsibleError
from ansible.plugins.lookup import LookupBase
from ansible_collections.google.cloud.plugins.plugin_utils.gcp_utils import GcpSecretLookup

try:
from google.cloud import secretmanager

HAS_GOOGLE_SECRET_MANAGER_LIBRARY = True
except ImportError:
HAS_GOOGLE_SECRET_MANAGER_LIBRARY = False


class GcpSecretResourceIdLookup(GcpSecretLookup):
def run(self, terms, variables=None, **kwargs):
self.set_plugin_name('google.cloud.gcp_secret_resource_id')
self.process_options(terms, variables=None, **kwargs)

response = self.client(secretmanager).get_secret_version(request={"name": self.name})

return [response.name]


class LookupModule(LookupBase):
def run(self, terms, variables=None, **kwargs):

if not HAS_GOOGLE_SECRET_MANAGER_LIBRARY:
raise AnsibleError("Please install the google-cloud-secret-manager Python library")

return GcpSecretResourceIdLookup().run(terms, variables=variables, **kwargs)
105 changes: 105 additions & 0 deletions plugins/plugin_utils/gcp_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2020, Pavlo Bashynskyi (@levonet) <[email protected]>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)

__metaclass__ = type

import io
import json
import os
import re

try:
import google.oauth2.credentials
from google.auth import identity_pool
from google.oauth2 import service_account
HAS_GOOGLE_LIBRARIES = True
except ImportError:
HAS_GOOGLE_LIBRARIES = False

from ansible.errors import AnsibleError


# Handles all authentication and options for GCP Secrets Manager API calls in Lookup plugins.
class GcpSecretLookup():
def __init__(self):
if not HAS_GOOGLE_LIBRARIES:
raise AnsibleError("Please install the google-auth library")

self.plugin_name = ''
self.secret_id = None
self.version_id = None
self.project_id = None
self.access_token = None
self.service_account_file = None
self.scope = ["https://www.googleapis.com/auth/cloud-platform"]

def set_plugin_name(self, name):
self.plugin_name = name

def client(self, secretmanager):
if self.access_token is not None:
credentials=google.oauth2.credentials.Credentials(self.access_token, scopes=self.scope)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
credentials=google.oauth2.credentials.Credentials(self.access_token, scopes=self.scope)
credentials = google.oauth2.credentials.Credentials(self.access_token, scopes=self.scope)

return secretmanager.SecretManagerServiceClient(credentials=credentials)

if self.service_account_file is not None:
path = os.path.realpath(os.path.expanduser(self.service_account_file))
if not os.path.exists(path):
raise AnsibleError("File {} was not found.".format(path))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
raise AnsibleError("File {} was not found.".format(path))
raise AnsibleError("File {0} was not found.".format(path))


with io.open(path, "r") as file_obj:
try:
info = json.load(file_obj)
except ValueError as e:
raise AnsibleError("File {} is not a valid json file.".format(path))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
raise AnsibleError("File {} is not a valid json file.".format(path))
raise AnsibleError("File {0} is not a valid json file.".format(path))


credential_type = info.get("type")
if credential_type == "authorized_user":
credentials = google.oauth2.credentials.Credentials.from_authorized_user_info(info, scopes=self.scope)
elif credential_type == "service_account":
credentials = service_account.Credentials.from_service_account_info(info, scopes=self.scope)
elif credential_type == "external_account":
if info.get("subject_token_type") == "urn:ietf:params:aws:token-type:aws4_request":
from google.auth import aws
credentials = aws.Credentials.from_info(info, scopes=self.scope)
else:
credentials = identity_pool.Credentials.from_info(info, scopes=self.scope)
else:
raise AnsibleError(
"Type is {}, expected one of authorized_user, service_account, external_account.".format(credential_type)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Type is {}, expected one of authorized_user, service_account, external_account.".format(credential_type)
"Type is {0}, expected one of authorized_user, service_account, external_account.".format(credential_type)

)

return secretmanager.SecretManagerServiceClient(credentials=credentials)

return secretmanager.SecretManagerServiceClient()

def process_options(self, terms, variables=None, **kwargs):
self.secret_id = kwargs.get('secret')
self.version_id = kwargs.get('version', 'latest')
self.project_id = kwargs.get('project', os.getenv('GCP_PROJECT'))
self.access_token = kwargs.get('access_token', os.getenv('GCP_ACCESS_TOKEN'))
self.service_account_file = kwargs.get('service_account_file', os.getenv('GOOGLE_APPLICATION_CREDENTIALS'))

if len(terms) > 1:
raise AnsibleError("{0} lookup plugin can have only one secret name or resource id".format(self.plugin_name))

if self.secret_id is None and len(terms) == 1:
self.secret_id = terms[0]

regex = r'^projects/([^/]+)/secrets/([^/]+)/versions/(.+)$'
match = re.match(regex, self.secret_id)
if match:
self.name = self.secret_id
self.project_id = match.group(1)
self.secret_id = match.group(2)
self.version_id = match.group(3)
return

if self.project_id is None:
raise AnsibleError("{0} lookup plugin required option: project or resource id".format(self.plugin_name))

if self.secret_id is None:
raise AnsibleError("{0} lookup plugin required option: secret or resource id".format(self.plugin_name))

self.name = "projects/{}/secrets/{}/versions/{}".format(self.project_id, self.secret_id, self.version_id)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.name = "projects/{}/secrets/{}/versions/{}".format(self.project_id, self.secret_id, self.version_id)
self.name = "projects/{0}/secrets/{1}/versions/{2}".format(self.project_id, self.secret_id, self.version_id)

Loading