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

Revoke tokens created by the modules and lookups #287

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
18 changes: 18 additions & 0 deletions plugins/doc_fragments/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,16 @@ class ModuleDocFragment(object):
description: For C(cert) auth, path to the private key file to authenticate with, in PEM format.
type: path
version_added: 1.4.0
revoke_ephemeral_token:
description:
- When C(true), tokens created implicitly by auth methods will be revoked when the operation they are used for is completed.
- For example, calling M(community.hashi_vault.vault_read) with C(userpass) auth will perform a C(userpass) login to retrieve a token,
perform a read with that token, then attempt to revoke the token so it can no longer be used.
- Revocation is considered best-effort. Errors on revocaton will not result in execution failure.
A warning will be emitted on revocation failure but in some circumstances, like non-revocation failures, the warning may not be displayed.
type: bool
default: false
version_added: 3.3.0
'''

PLUGINS = r'''
Expand Down Expand Up @@ -306,4 +316,12 @@ class ModuleDocFragment(object):
ini:
- section: hashi_vault_collection
key: cert_auth_private_key
revoke_ephemeral_token:
env:
- name: ANSIBLE_HASHI_VAULT_REVOKE_EPHEMERAL_TOKEN
ini:
- section: hashi_vault_collection
key: revoke_ephemeral_token
vars:
- name: ansible_hashi_vault_revoke_ephemeral_token
'''
5 changes: 3 additions & 2 deletions plugins/lookup/hashi_vault.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,11 +271,12 @@ def run(self, terms, variables=None, **kwargs):
self.client = self.helper.get_vault_client(**client_args)

try:
self.authenticator.authenticate(self.client)
auth = self.authenticator.authenticate(self.client)
except (NotImplementedError, HashiVaultValueError) as e:
raise AnsibleError(e)

ret.extend(self.get())
with auth:
ret.extend(self.get())

return ret

Expand Down
31 changes: 16 additions & 15 deletions plugins/lookup/vault_kv1_get.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,26 +195,27 @@ def run(self, terms, variables=None, **kwargs):

try:
self.authenticator.validate()
self.authenticator.authenticate(client)
auth = self.authenticator.authenticate(client)
except (NotImplementedError, HashiVaultValueError) as e:
raise AnsibleError(e)

for term in terms:
try:
raw = client.secrets.kv.v1.read_secret(path=term, mount_point=engine_mount_point)
except hvac.exceptions.Forbidden as e:
raise_from(AnsibleError("Forbidden: Permission Denied to path ['%s']." % term), e)
except hvac.exceptions.InvalidPath as e:
if 'Invalid path for a versioned K/V secrets engine' in str(e):
msg = "Invalid path for a versioned K/V secrets engine ['%s']. If this is a KV version 2 path, use community.hashi_vault.vault_kv2_get."
else:
msg = "Invalid or missing path ['%s']."
with auth:
for term in terms:
try:
raw = client.secrets.kv.v1.read_secret(path=term, mount_point=engine_mount_point)
except hvac.exceptions.Forbidden as e:
raise_from(AnsibleError("Forbidden: Permission Denied to path ['%s']." % term), e)
except hvac.exceptions.InvalidPath as e:
if 'Invalid path for a versioned K/V secrets engine' in str(e):
msg = "Invalid path for a versioned K/V secrets engine ['%s']. If this is a KV version 2 path, use community.hashi_vault.vault_kv2_get."
else:
msg = "Invalid or missing path ['%s']."

raise_from(AnsibleError(msg % (term,)), e)
raise_from(AnsibleError(msg % (term,)), e)

metadata = raw.copy()
data = metadata.pop('data')
metadata = raw.copy()
data = metadata.pop('data')

ret.append(dict(raw=raw, data=data, secret=data, metadata=metadata))
ret.append(dict(raw=raw, data=data, secret=data, metadata=metadata))

return ret
37 changes: 20 additions & 17 deletions plugins/lookup/vault_kv2_get.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,25 +219,28 @@ def run(self, terms, variables=None, **kwargs):

try:
self.authenticator.validate()
self.authenticator.authenticate(client)
auth = self.authenticator.authenticate(client)
except (NotImplementedError, HashiVaultValueError) as e:
raise AnsibleError(e)

for term in terms:
try:
raw = client.secrets.kv.v2.read_secret_version(path=term, version=version, mount_point=engine_mount_point)
except hvac.exceptions.Forbidden as e:
raise_from(AnsibleError("Forbidden: Permission Denied to path ['%s']." % term), e)
except hvac.exceptions.InvalidPath as e:
raise_from(
AnsibleError("Invalid or missing path ['%s'] with secret version '%s'. Check the path or secret version." % (term, version or 'latest')),
e
)

data = raw['data']
metadata = data['metadata']
secret = data['data']

ret.append(dict(raw=raw, data=data, secret=secret, metadata=metadata))
with auth:
for term in terms:
try:
raw = client.secrets.kv.v2.read_secret_version(path=term, version=version, mount_point=engine_mount_point)
except hvac.exceptions.Forbidden as e:
raise_from(AnsibleError("Forbidden: Permission Denied to path ['%s']." % term), e)
except hvac.exceptions.InvalidPath as e:
raise_from(
AnsibleError(
"Invalid or missing path ['%s'] with secret version '%s'. Check the path or secret version." % (term, version or 'latest')
),
e
)

data = raw['data']
metadata = data['metadata']
secret = data['data']

ret.append(dict(raw=raw, data=data, secret=secret, metadata=metadata))

return ret
3 changes: 2 additions & 1 deletion plugins/lookup/vault_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
- "The C(token) auth method will only return full information if I(token_validate=True).
If the token does not have the C(lookup-self) capability, this will fail. If I(token_validate=False), only the token value itself
will be returned in the structure."
- I(revoke_ephemeral_token) has no effect in this lookup, since the token is the desired result.
extends_documentation_fragment:
- community.hashi_vault.connection
- community.hashi_vault.connection.plugins
Expand Down Expand Up @@ -135,7 +136,7 @@ def run(self, terms, variables=None, **kwargs):

try:
self.authenticator.validate()
response = self.authenticator.authenticate(client)
response = self.authenticator.authenticate(client).raw
except (NotImplementedError, HashiVaultValueError) as e:
raise AnsibleError(e)

Expand Down
19 changes: 10 additions & 9 deletions plugins/lookup/vault_read.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,19 +119,20 @@ def run(self, terms, variables=None, **kwargs):

try:
self.authenticator.validate()
self.authenticator.authenticate(client)
auth = self.authenticator.authenticate(client)
except (NotImplementedError, HashiVaultValueError) as e:
raise AnsibleError(e)

for term in terms:
try:
data = client.read(term)
except hvac.exceptions.Forbidden:
raise AnsibleError("Forbidden: Permission Denied to path '%s'." % term)
with auth:
for term in terms:
try:
data = client.read(term)
except hvac.exceptions.Forbidden:
raise AnsibleError("Forbidden: Permission Denied to path '%s'." % term)

if data is None:
raise AnsibleError("The path '%s' doesn't seem to exist." % term)
if data is None:
raise AnsibleError("The path '%s' doesn't seem to exist." % term)

ret.append(data)
ret.append(data)

return ret
14 changes: 13 additions & 1 deletion plugins/lookup/vault_token_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
- In check mode, this module will not create a token, and will instead return a basic structure with an empty token.
However, this may not be useful if the token is required for follow on tasks.
It may be better to use this module with I(check_mode=no) in order to have a valid token that can be used.
- Ephemeral tokens B(will not be revoked) when I(revoke_ephemeral_token=true) unless I(orphan=true), otherwise the child tokens would also be revoked.
extends_documentation_fragment:
- community.hashi_vault.connection
- community.hashi_vault.connection.plugins
Expand Down Expand Up @@ -166,6 +167,10 @@ def run(self, terms, variables=None, **kwargs):

pass_thru_options = self._options_adapter.get_filled_options(*self.PASS_THRU_OPTION_NAMES)

orphan = self.get_option('orphan')
if orphan:
pass_thru_options['no_parent'] = True

orphan_options = pass_thru_options.copy()

for key in pass_thru_options.keys():
Expand All @@ -174,7 +179,11 @@ def run(self, terms, variables=None, **kwargs):

response = None

if self.get_option('orphan'):
revoke_token = {}
if orphan:
revoke_token['revoke_token'] = None

if orphan:
try:
try:
# this method was added in hvac 1.0.0
Expand All @@ -185,11 +194,14 @@ def run(self, terms, variables=None, **kwargs):
# See: https://github.com/hvac/hvac/issues/758
response = client.create_token(orphan=True, **orphan_options)
except Exception as e:
self.authenticator.logout(client, **revoke_token)
raise AnsibleError(e)
else:
try:
response = client.auth.token.create(**pass_thru_options)
except Exception as e:
self.authenticator.logout(client, **revoke_token)
raise AnsibleError(e)

self.authenticator.logout(client, **revoke_token)
return [response]
51 changes: 26 additions & 25 deletions plugins/lookup/vault_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,35 +157,36 @@ def run(self, terms, variables=None, **kwargs):

try:
self.authenticator.validate()
self.authenticator.authenticate(client)
auth = self.authenticator.authenticate(client)
except (NotImplementedError, HashiVaultValueError) as e:
raise_from(AnsibleError(e), e)

for term in terms:
try:
response = client.write(path=term, wrap_ttl=wrap_ttl, **data)
except hvac.exceptions.Forbidden as e:
raise_from(AnsibleError("Forbidden: Permission Denied to path '%s'." % term), e)
except hvac.exceptions.InvalidPath as e:
raise_from(AnsibleError("The path '%s' doesn't seem to exist." % term), e)
except hvac.exceptions.InternalServerError as e:
raise_from(AnsibleError("Internal Server Error: %s" % str(e)), e)

# https://github.com/hvac/hvac/issues/797
# HVAC returns a raw response object when the body is not JSON.
# That includes 204 responses, which are successful with no body.
# So we will try to detect that and a act accordingly.
# A better way may be to implement our own adapter for this
# collection, but it's a little premature to do that.
if hasattr(response, 'json') and callable(response.json):
if response.status_code == 204:
output = {}
with auth:
for term in terms:
try:
response = client.write(path=term, wrap_ttl=wrap_ttl, **data)
except hvac.exceptions.Forbidden as e:
raise_from(AnsibleError("Forbidden: Permission Denied to path '%s'." % term), e)
except hvac.exceptions.InvalidPath as e:
raise_from(AnsibleError("The path '%s' doesn't seem to exist." % term), e)
except hvac.exceptions.InternalServerError as e:
raise_from(AnsibleError("Internal Server Error: %s" % str(e)), e)

# https://github.com/hvac/hvac/issues/797
# HVAC returns a raw response object when the body is not JSON.
# That includes 204 responses, which are successful with no body.
# So we will try to detect that and a act accordingly.
# A better way may be to implement our own adapter for this
# collection, but it's a little premature to do that.
if hasattr(response, 'json') and callable(response.json):
if response.status_code == 204:
output = {}
else:
display.warning('Vault returned status code %i and an unparsable body.' % response.status_code)
output = response.content
else:
display.warning('Vault returned status code %i and an unparsable body.' % response.status_code)
output = response.content
else:
output = response
output = response

ret.append(output)
ret.append(output)

return ret
2 changes: 1 addition & 1 deletion plugins/module_utils/_auth_method_approle.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ def authenticate(self, client, use_token=True):
self.warn("HVAC should be updated to version 0.10.6 or higher. Deprecated method 'auth_approle' will be used.")
response = client.auth_approle(use_token=use_token, **params)

return response
return self.get_context(client, response)
2 changes: 1 addition & 1 deletion plugins/module_utils/_auth_method_aws_iam.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,4 @@ def authenticate(self, client, use_token=True):
self.warn("HVAC should be updated to version 0.9.3 or higher. Deprecated method 'auth_aws_iam' will be used.")
client.auth_aws_iam(use_token=use_token, **params)

return response
return self.get_context(client, response)
2 changes: 1 addition & 1 deletion plugins/module_utils/_auth_method_azure.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,4 @@ def validate(self):
def authenticate(self, client, use_token=True):
params = self._auth_azure_login_params
response = client.auth.azure.login(use_token=use_token, **params)
return response
return self.get_context(client, response)
2 changes: 1 addition & 1 deletion plugins/module_utils/_auth_method_cert.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ def authenticate(self, client, use_token=True):
except NotImplementedError:
raise NotImplementedError("cert authentication requires HVAC version 0.10.12 or higher.")

return response
return self.get_context(client, response)
2 changes: 1 addition & 1 deletion plugins/module_utils/_auth_method_jwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,4 @@ def authenticate(self, client, use_token=True):
if use_token:
client.token = response['auth']['client_token']

return response
return self.get_context(client, response)
2 changes: 1 addition & 1 deletion plugins/module_utils/_auth_method_ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ def authenticate(self, client, use_token=True):
self.warn("HVAC should be updated to version 0.7.0 or higher. Deprecated method 'auth_ldap' will be used.")
response = client.auth_ldap(use_token=use_token, **params)

return response
return self.get_context(client, response)
5 changes: 4 additions & 1 deletion plugins/module_utils/_auth_method_none.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,7 @@ def validate(self):
pass

def authenticate(self, client, use_token=False):
return None
return self.get_context(client, None)

def logout(self, client, revoke_token=False):
return super(HashiVaultAuthMethodNone, self).logout(client, revoke_token)
6 changes: 5 additions & 1 deletion plugins/module_utils/_auth_method_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,8 @@ def authenticate(self, client, use_token=True, lookup_self=False):
if validate:
raise HashiVaultValueError("Invalid Vault Token Specified.")

return self._simulate_login_response(token, response)
full_response = self._simulate_login_response(token, response)
return self.get_context(client, full_response)

def logout(self, client, revoke_token=False):
return super(HashiVaultAuthMethodToken, self).logout(client, revoke_token)
2 changes: 1 addition & 1 deletion plugins/module_utils/_auth_method_userpass.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@ def authenticate(self, client, use_token=True):
if use_token:
client.token = response['auth']['client_token']

return response
return self.get_context(client, response)
5 changes: 5 additions & 0 deletions plugins/module_utils/_authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class HashiVaultAuthenticator():
azure_resource=dict(type='str', default='https://management.azure.com/'),
cert_auth_private_key=dict(type='path', no_log=False),
cert_auth_public_key=dict(type='path'),
revoke_ephemeral_token=dict(type='bool', default=False),
)

def __init__(self, option_adapter, warning_callback, deprecate_callback):
Expand Down Expand Up @@ -101,3 +102,7 @@ def validate(self, *args, **kwargs):
def authenticate(self, *args, **kwargs):
method = self._get_method_object(kwargs.pop('method', None))
return method.authenticate(*args, **kwargs)

def logout(self, *args, **kwargs):
method = self._get_method_object(kwargs.pop('method', None))
return method.logout(*args, **kwargs)
Loading