Skip to content

Commit

Permalink
Add Kubernetes Auth Method (#408)
Browse files Browse the repository at this point in the history
* Release v0.7.2 (#371)

* Tweak Travis CI Configuration (#360)

* drop sudo: false

* remove explicit dist

* simplify build matrix

* change flake8 toxenv name to avoid matching tox-travis default prefixes

* Remove unnecessary `export PATH` "script" step

* Simplify env from list to k/v string

* Clarifying comment re: flake8

* Simplify "allowed failures" row matching

* Speed up overall build time with fast_finish: true

* Rearrange comment to same line being commented on

* Include python 3.7 build jobs

* Bump 0.11 and 1.0 Vault vers to latest patch ver

* Also run flake8 for python 3.7

* Also include "py37" on the tox side of things

* Keep "dist: xenial" for python 3.7 availability

* Tweak flake8 job names for readability

* Shorten comment

* Backporting Master (#362)

* fix double slash (#352)

When called, double slash results in 301 HTTP code and the redirect which is necessary

* Release v0.7.1 (#357)

* Develop is the new integration branch

* Handle "Misses" for Identity Secrets Lookups (#331)

* Regression tests for group lookup misses

* Return None for group lookup misses

* Regression tests for entity lookup misses

* Return None for entity lookup misses

* Also update docstrings

* Move Test Cases out of Package Directory (#334)

* use utils function to get test data path

* rename test => tests and move contents into config_files subdir

* Move scripts subdir under tests dir in root

* Move generate.sh into scripts subdir

* Update paths in generate_test_cert.sh

* clean up config_files readme a smidge

* move test dir into repo root dir

* update import paths

* start breaking out test utils into module

* Break out mock_github_request_handler

* Break out hvac_integration_test_case

* Break out server_manager

* Remove .coveragerc until / unless it is needed once again

* Add Okta Auth Method Class (#341)

* Rename auth subdir under unit_tests

* bonus GCP docs heading fix

* Add Okta auth method docs

* Add Okta auth method test cases

* Add Okta auth method class implementation

* Add pretty_print arg to create_or_update_policy (#342)

* Add pretty_print arg

* Skip new test on Vault v0.11.0

* Bump Vault Versions - Vault v1.0.0 (#344)

* fix TOXENV for 3.6 jobs

* Drop v0.8.3, add v1.0.0

* Update readme

* Handle different response keys in Vault v1.0.0

* Also work around new list response return type

* Add get_generate_root_otp utils method for v1.0.0 and/or previous vers

* Update missed TOXENV arg under allow_failures dict

* Call out why v0.11.0 is hanging about

* Clarify name/purpose of vault ver comparison methods

* Fix identity group conditionals (#346)

* Add regression tests

* Fix member entity/group ids logic in group methods

* DRY up conditional logic

* Add missing docstring content

* Gcp login doc update for issue 345 (#350)

* Clarify source links

* add google-api-python-client example

* Clean up unintentional modification

* Fix For read_health_status() Exception Handling (#347)

* Add url param for create_client

* Install consul for test cases involving Vault HA

* Update test harness with optional Vault HA / consul set up

* Add regerssion test cases for issue #339

* Add raise_exception param to requests

* Use raise_exception param in read_health_status method

* Add ipaddress module per github.com/urllib3/urllib3/issues/1117?

Somehow ended up with this urllib3 error for python 2.7 otherwise:
"urllib3.connection: ERROR: Certificate did not match expected hostname:
127.0.0.1. Certificate: {'serialNumber': u'8D267F50728FF454',
'subject': ((('commonName', u'localhost'),),),
'notAfter': 'May 14 22:44:13 2025 GMT',
'notBefore': u'May 17 22:44:13 2015 GMT',
'subjectAltName': (('DNS', 'localhost'), ('IP Address', '127.0.0.1')),
'issuer': ((('commonName', u'localhost'),),), 'version': 3L}

* Clarify docstring a bit

* Also add cases to cover both HEAD and GET methods

* Remove standby node magic strings; use method instead

* Fix seal_status Call (#354)

* Add regression tests

* Fix seal_status call

* More meaningful assertion

* Fix Request Redirection Handling (#348)

* simplify chained comparison

* Ensure regression unit test case coverage for paths/redirects

* Revert redirection handling back to the requests module

* Handle double slashes in paths

* Fix syntax for python 2.7

* Log when we transform a requested url

* Explictly assert that we have the expect requests in mocker history

* Clarify lease docs (#355)

* Updates for upcoming release 0.7.1

* Bump patch version to 0.7.1

* prune tests from packages (#356)

* Wait for test kvv2 secrets engine to show up in list (#361)

* Set "skip_missing_interpreters" to true in global tox config (#363)

* Set "skip_missing_interpreters" to true in global tox config

* go full env string expansion 😝 cause why not

* Fix For Intermittent Health Test Case Failure (#364)

* Ensure we get an active node when needed

* Remove unneeded debug call

* Simplify flake8 env for travis-ci + tox (#365)

* Simplify flake8 env for travis-ci + tox

* Add missing comma

* Test Documentation Compilation (#366)

* Update docs requirements to be more explicit

* Test that docs can build cleanly

* Fix m2r requirement

* Clearer job name

* Default to first python ver in the matrix...

* Further clarify job name

* Reorder tox directives a smidge...

* Cleanup setup.py a bit (#367)

* Update author / author_email

* Move some auxiliary logic into methods

* gmai.com -> gmail.com

* reorder author names

* Use pip-compile For All Requirements (#368)

* Add section covering requirement updates

* Add .in req files, breakout parser (pyhcl) extra_require

* pip-compile all the things

* Pull in latest reqs for docs for good measure

* Add update-all-reqs Makefile targets

* Define "parser" requirements in just one place

* Add clarifying comment

* Simplify new Makefile targets a smidge

* comment bout comments

* Makefile clarifying comment

* Use abs paths starting from setup.py location

* Also dynamically populate install_requires

* Revert requirements loading in setup.py; tis a silly thing to do

* Bump install_requires / extras_require min versions

* Drop extra "parser" requirements as its not strictly needed

* Drop use of "version" file (#369)

* Drop use of "version" file

* Clarify updated bumpversion release step

* Fix grammerz

* Remove inadvertently committed hvac/version file

* Organize imports

* Add AWS Secrets Engine Class (#370)

* auto generated script

* Include Docs

* "Implement" Aws class

* Tweak docstrings and whatnot

* Param tweaks

* update convert ttl for aws secrets return values

* First pass on aws secrets engine tests

* Fix headings

* Cleanup unused mock server logic, additional role params

* Accept policy_document dict param type

* Start filling in aws secrets docs

* E501 line too long (162 > 160 characters)

* First pass at handling legacy params

* Different status code from Vault v0.11.0 :\

* Fill in legacy_params-related comments / docstrings

* Also update docs

* Add contents section and upper case heading

* Adding a Twitter Badge (#372)

* Split up icons with linebreaks

* Add Twitter badge for @hvac_python

* Adding Header image (#373)

* Add header image

* Update twitter handle

* Update content email and test URLs

* Commit header image

* Update header image URL to final resting place

* Changelog updates for v0.7.2 release

* Update release steps

* Update copyright date

* Bump version: 0.7.1 → 0.7.2

* Clean up vestigial version target reference

* add auth method for Kubernetes

* add tests for Kubernetes auth method

* add function  for check certificate PEM format

* update project common files

* change dict multiline to oneline

* fix gcp integration tests with bound_service_accounts
  • Loading branch information
jsporna authored and jeffwecan committed Mar 27, 2019
1 parent 150ad20 commit 25f2cfc
Show file tree
Hide file tree
Showing 9 changed files with 683 additions and 18 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Expand Up @@ -13,3 +13,7 @@ test/*.log

# sphinx build folder
docs/_build/

.idea/
venv/
.envrc
10 changes: 10 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,15 @@
# Changelog

## 0.8.0 (March 26th, 2019)

IMPROVMENTS:

* Support for the Kubernetes auth method

BUG FIXES:

* Fix for comparision `recovery_threshold` and `recovery_shares` during initialization.

## 0.7.2 (January 1st, 2019)

IMPROVEMENTS:
Expand Down
4 changes: 3 additions & 1 deletion hvac/api/auth_methods/__init__.py
Expand Up @@ -5,6 +5,7 @@
from hvac.api.auth_methods.azure import Azure
from hvac.api.auth_methods.gcp import Gcp
from hvac.api.auth_methods.github import Github
from hvac.api.auth_methods.kubernetes import Kubernetes
from hvac.api.auth_methods.ldap import Ldap
from hvac.api.auth_methods.mfa import Mfa
from hvac.api.auth_methods.okta import Okta
Expand All @@ -16,6 +17,7 @@
'Azure',
'Gcp',
'Github',
'Kubernetes',
'Ldap',
'Mfa',
'Okta'
Expand All @@ -28,6 +30,7 @@ class AuthMethods(VaultApiCategory):
Azure,
Github,
Gcp,
Kubernetes,
Ldap,
Mfa,
Okta
Expand All @@ -38,7 +41,6 @@ class AuthMethods(VaultApiCategory):
'AliCloud',
'Aws',
'Jwt',
'Kubernetes',
'Radius',
'Cert',
'Token',
Expand Down
245 changes: 245 additions & 0 deletions hvac/api/auth_methods/kubernetes.py
@@ -0,0 +1,245 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Kubernetes methods module."""
from hvac import exceptions
from hvac.api.vault_api_base import VaultApiBase
from hvac.utils import validate_list_of_strings_param, comma_delimited_to_list, validate_pem_format

DEFAULT_MOUNT_POINT = 'kubernetes'


class Kubernetes(VaultApiBase):
"""Kubernetes Auth Method (API).
Reference: https://www.vaultproject.io/api/auth/kubernetes/index.html
"""
def configure(self, kubernetes_host, kubernetes_ca_cert='', token_reviewer_jwt='', pem_keys=None,
mount_point=DEFAULT_MOUNT_POINT):
"""Configure the connection parameters for Kubernetes.
This path honors the distinction between the create and update capabilities inside ACL policies.
Supported methods:
POST: /auth/{mount_point}/config. Produces: 204 (empty body)
:param kubernetes_host: Host must be a host string, a host:port pair, or a URL to the base of the
Kubernetes API server. Example: https://k8s.example.com:443
:type kubernetes_host: str | unicode
:param kubernetes_ca_cert: PEM encoded CA cert for use by the TLS client used to talk with the Kubernetes API.
NOTE: Every line must end with a newline: \n
:type kubernetes_ca_cert: str | unicode
:param token_reviewer_jwt: A service account JWT used to access the TokenReview API to validate other
JWTs during login. If not set the JWT used for login will be used to access the API.
:type token_reviewer_jwt: str | unicode
:param pem_keys: Optional list of PEM-formatted public keys or certificates used to verify the signatures of
Kubernetes service account JWTs. If a certificate is given, its public key will be extracted. Not every
installation of Kubernetes exposes these keys.
:type pem_keys: list
:param mount_point: The "path" the method/backend was mounted on.
:type mount_point: str | unicode
:return: The response of the configure_method request.
:rtype: requests.Response
"""
if pem_keys is None:
pem_keys = []

list_of_pem_params = {
'kubernetes_ca_cert': kubernetes_ca_cert,
'pem_keys': pem_keys
}
for param_name, param_argument in list_of_pem_params.items():
validate_pem_format(
param_name=param_name,
param_argument=param_argument,
)

params = {
'kubernetes_host': kubernetes_host,
'kubernetes_ca_cert': kubernetes_ca_cert,
'token_reviewer_jwt': token_reviewer_jwt,
'pem_keys': pem_keys,
}
api_path = '/v1/auth/{mount_point}/config'.format(
mount_point=mount_point
)
return self._adapter.post(
url=api_path,
json=params,
)

def read_config(self, mount_point=DEFAULT_MOUNT_POINT):
"""Return the previously configured config, including credentials.
Supported methods:
GET: /auth/{mount_point}/config. Produces: 200 application/json
:param mount_point: The "path" the kubernetes auth method was mounted on.
:type mount_point: str | unicode
:return: The data key from the JSON response of the request.
:rtype: dict
"""
api_path = '/v1/auth/{mount_point}/config'.format(mount_point=mount_point)
response = self._adapter.get(
url=api_path,
)
return response.json().get('data')

def create_role(self, name, bound_service_account_names, bound_service_account_namespaces, ttl="", max_ttl="",
period="", policies=None, mount_point=DEFAULT_MOUNT_POINT):
"""Create a role in the method.
Registers a role in the auth method. Role types have specific entities that can perform login operations
against this endpoint. Constraints specific to the role type must be set on the role. These are applied to
the authenticated entities attempting to login.
Supported methods:
POST: /auth/{mount_point}/role/{name}. Produces: 204 (empty body)
:param name: Name of the role.
:type name: str | unicode
:param bound_service_account_names: List of service account names able to access this role. If set to "*"
all names are allowed, both this and bound_service_account_namespaces can not be "*".
:type bound_service_account_names: list | str | unicode
:param bound_service_account_namespaces: List of namespaces allowed to access this role. If set to "*" all
namespaces are allowed, both this and bound_service_account_names can not be set to "*".
:type bound_service_account_namespaces: list | str | unicode
:param ttl: The TTL period of tokens issued using this role in seconds.
:type ttl: str | unicode
:param max_ttl: The maximum allowed lifetime of tokens issued in seconds using this role.
:type max_ttl: str | unicode
:param period: If set, indicates that the token generated using this role should never expire. The token should
be renewed within the duration specified by this value. At each renewal, the token's TTL will be set to the
value of this parameter.
:type period: str | unicode
:param policies: Policies to be set on tokens issued using this role.
:type policies: list | str | unicode
:param mount_point: The "path" the azure auth method was mounted on.
:type mount_point: str | unicode
:return: The response of the request.
:rtype: requests.Response
"""
list_of_strings_params = {
'bound_service_account_names': bound_service_account_names,
'bound_service_account_namespaces': bound_service_account_namespaces,
'policies': policies
}
for param_name, param_argument in list_of_strings_params.items():
validate_list_of_strings_param(
param_name=param_name,
param_argument=param_argument,
)

if bound_service_account_names in ("*", ["*"]) and bound_service_account_namespaces in ("*", ["*"]):
error_msg = 'unsupported combination of `bind_service_account_names` and ' \
'`bound_service_account_namespaces` arguments. Both of them can not be set to `*`'
raise exceptions.ParamValidationError(error_msg)

params = {
'bound_service_account_names': comma_delimited_to_list(bound_service_account_names),
'bound_service_account_namespaces': comma_delimited_to_list(bound_service_account_namespaces),
'ttl': ttl,
'max_ttl': max_ttl,
'period': period,
'policies': comma_delimited_to_list(policies),
}

api_path = '/v1/auth/{mount_point}/role/{name}'.format(mount_point=mount_point, name=name)
return self._adapter.post(
url=api_path,
json=params,
)

def read_role(self, name, mount_point=DEFAULT_MOUNT_POINT):
"""Returns the previously registered role configuration.
Supported methods:
POST: /auth/{mount_point}/role/{name}. Produces: 200 application/json
:param name: Name of the role.
:type name: str | unicode
:param mount_point: The "path" the kubernetes auth method was mounted on.
:type mount_point: str | unicode
:return: The "data" key from the JSON response of the request.
:rtype: dict
"""
api_path = '/v1/auth/{mount_point}/role/{name}'.format(
mount_point=mount_point,
name=name,
)
response = self._adapter.get(
url=api_path,
)
return response.json().get('data')

def list_roles(self, mount_point=DEFAULT_MOUNT_POINT):
"""List all the roles that are registered with the plugin.
Supported methods:
LIST: /auth/{mount_point}/roles. Produces: 200 application/json
:param mount_point: The "path" the kubernetes auth method was mounted on.
:type mount_point: str | unicode
:return: The "data" key from the JSON response of the request.
:rtype: dict
"""
api_path = '/v1/auth/{mount_point}/roles'.format(mount_point=mount_point)
response = self._adapter.list(
url=api_path,
)
return response.json().get('data')

def delete_role(self, name, mount_point=DEFAULT_MOUNT_POINT):
"""Delete the previously registered role.
Supported methods:
DELETE: /auth/{mount_point}/role/{name}. Produces: 204 (empty body)
:param name: Name of the role.
:type name: str | unicode
:param mount_point: The "path" the kubernetes auth method was mounted on.
:type mount_point: str | unicode
:return: The response of the request.
:rtype: requests.Response
"""
api_path = '/v1/auth/{mount_point}/role/{name}'.format(
mount_point=mount_point,
name=name,
)
return self._adapter.delete(
url=api_path,
)

def login(self, role, jwt, use_token=True, mount_point=DEFAULT_MOUNT_POINT):
"""Fetch a token.
This endpoint takes a signed JSON Web Token (JWT) and a role name for some entity. It verifies the JWT signature
to authenticate that entity and then authorizes the entity for the given role.
Supported methods:
POST: /auth/{mount_point}/login. Produces: 200 application/json
:param role: Name of the role against which the login is being attempted.
:type role: str | unicode
:param jwt: Signed JSON Web Token (JWT) from Azure MSI.
:type jwt: str | unicode
:param use_token: if True, uses the token in the response received from the auth request to set the "token"
attribute on the the :py:meth:`hvac.adapters.Adapter` instance under the _adapater Client attribute.
:type use_token: bool
:param mount_point: The "path" the azure auth method was mounted on.
:type mount_point: str | unicode
:return: The JSON response of the request.
:rtype: dict
"""
params = {
'role': role,
'jwt': jwt,
}

api_path = '/v1/auth/{mount_point}/login'.format(mount_point=mount_point)
response = self._adapter.login(
url=api_path,
use_token=use_token,
json=params,
)
return response
46 changes: 45 additions & 1 deletion hvac/utils.py
Expand Up @@ -196,8 +196,10 @@ def validate_list_of_strings_param(param_name, param_argument):
"""
if param_argument is None:
param_argument = []
if isinstance(param_argument, str):
param_argument = param_argument.split(',')
if not isinstance(param_argument, list) or not all([isinstance(p, str) for p in param_argument]):
error_msg = 'unsupported {param} argument provided "{arg}" ({arg_type}), required type: List[str]"'
error_msg = 'unsupported {param} argument provided "{arg}" ({arg_type}), required type: List[str]'
raise exceptions.ParamValidationError(error_msg.format(
param=param_name,
arg=param_argument,
Expand All @@ -216,3 +218,45 @@ def list_to_comma_delimited(list_param):
if list_param is None:
list_param = []
return ','.join(list_param)


def comma_delimited_to_list(list_param):
"""Convert comma-delimited list / string into a list of strings
:param list_param: Comma-delimited string
:type list_param: str | unicode
:return: A list of strings
:rtype: list
"""
if isinstance(list_param, list):
return list_param
if isinstance(list_param, str):
return list_param.split(',')
else:
return []


def validate_pem_format(param_name, param_argument):
"""Validate that an argument is a PEM-formatted public key or certificate
:param param_name: The name of the parameter being validate. Used in any resulting exception messages.
:type param_name: str | unicode
:param param_argument: The argument to validate
:type param_argument: str | unicode
:return True if the argument is validate False otherwise
:rtype: bool
"""

def _check_pem(arg):
arg = arg.strip()
if not arg.startswith('-----BEGIN CERTIFICATE-----') \
or not arg.endswith('-----END CERTIFICATE-----'):
return False
return True

if isinstance(param_argument, str):
param_argument = [param_argument]

if not isinstance(param_argument, list) or not all(_check_pem(p) for p in param_argument):
error_msg = 'unsupported {param} public key / certificate format, required type: PEM'
raise exceptions.ParamValidationError(error_msg.format(param=param_name))

0 comments on commit 25f2cfc

Please sign in to comment.