-
Notifications
You must be signed in to change notification settings - Fork 135
Add Secret Manager Lookup plugins #357
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. | ||
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"). | ||
""" | ||
|
||
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) |
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"). | ||
""" | ||
|
||
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) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should scope be added? |
||
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)) | ||
|
||
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)) | ||
|
||
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) | ||
) | ||
|
||
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 = f"projects/{self.project_id}/secrets/{self.secret_id}/versions/{self.version_id}" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could this be switched away from f-strings? we don't use them today, and I think that syntax-wise this repository is still python2 compatible. Not that we can always have it be py2 compatible, but I'd like to avoid py3 syntax to make things py2-usable as much as possible for now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this file needs a copyright / license notice too?