github_app_access_token: add support for private key fact (#8989)
* github_app_access_token: add support for private key fact Adds support for specifying the GitHub App private key via an ansible fact instead of a path to a file. This is useful when you want to generate registration tokens for a remote host but don't want to put secrets on the host. * Add license file * Fix pep8 formatting * Add changelog fragment * Run sanity tests on changelog * Apply suggestions from code review Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> Co-authored-by: Felix Fontein <felix@fontein.de> * Add input validation check * Add import * Apply suggestions from code review Co-authored-by: Felix Fontein <felix@fontein.de> * Add error for mutually exclusive options * Update plugins/lookup/github_app_access_token.py Co-authored-by: Felix Fontein <felix@fontein.de> --------- Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> Co-authored-by: Felix Fontein <felix@fontein.de>pull/9060/head
parent
9fb686fe35
commit
5b3b7a1fb1
|
@ -0,0 +1,2 @@
|
||||||
|
minor_changes:
|
||||||
|
- github_app_access_token lookup plugin - adds new ``private_key`` parameter (https://github.com/ansible-collections/community.general/pull/8989).
|
|
@ -19,7 +19,7 @@ DOCUMENTATION = '''
|
||||||
key_path:
|
key_path:
|
||||||
description:
|
description:
|
||||||
- Path to your private key.
|
- Path to your private key.
|
||||||
required: true
|
- Either O(key_path) or O(private_key) must be specified.
|
||||||
type: path
|
type: path
|
||||||
app_id:
|
app_id:
|
||||||
description:
|
description:
|
||||||
|
@ -34,6 +34,12 @@ DOCUMENTATION = '''
|
||||||
- Alternatively, you can use PyGithub (U(https://github.com/PyGithub/PyGithub)) to get your installation ID.
|
- Alternatively, you can use PyGithub (U(https://github.com/PyGithub/PyGithub)) to get your installation ID.
|
||||||
required: true
|
required: true
|
||||||
type: str
|
type: str
|
||||||
|
private_key:
|
||||||
|
description:
|
||||||
|
- GitHub App private key in PEM file format as string.
|
||||||
|
- Either O(key_path) or O(private_key) must be specified.
|
||||||
|
type: str
|
||||||
|
version_added: 10.0.0
|
||||||
token_expiry:
|
token_expiry:
|
||||||
description:
|
description:
|
||||||
- How long the token should last for in seconds.
|
- How long the token should last for in seconds.
|
||||||
|
@ -71,7 +77,7 @@ import time
|
||||||
import json
|
import json
|
||||||
from ansible.module_utils.urls import open_url
|
from ansible.module_utils.urls import open_url
|
||||||
from ansible.module_utils.six.moves.urllib.error import HTTPError
|
from ansible.module_utils.six.moves.urllib.error import HTTPError
|
||||||
from ansible.errors import AnsibleError
|
from ansible.errors import AnsibleError, AnsibleOptionsError
|
||||||
from ansible.plugins.lookup import LookupBase
|
from ansible.plugins.lookup import LookupBase
|
||||||
from ansible.utils.display import Display
|
from ansible.utils.display import Display
|
||||||
|
|
||||||
|
@ -84,8 +90,10 @@ else:
|
||||||
display = Display()
|
display = Display()
|
||||||
|
|
||||||
|
|
||||||
def read_key(path):
|
def read_key(path, private_key=None):
|
||||||
try:
|
try:
|
||||||
|
if private_key:
|
||||||
|
return jwk_from_pem(private_key.encode('utf-8'))
|
||||||
with open(path, 'rb') as pem_file:
|
with open(path, 'rb') as pem_file:
|
||||||
return jwk_from_pem(pem_file.read())
|
return jwk_from_pem(pem_file.read())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -132,8 +140,8 @@ def post_request(generated_jwt, installation_id):
|
||||||
return json_data.get('token')
|
return json_data.get('token')
|
||||||
|
|
||||||
|
|
||||||
def get_token(key_path, app_id, installation_id, expiry=600):
|
def get_token(key_path, app_id, installation_id, private_key, expiry=600):
|
||||||
jwk = read_key(key_path)
|
jwk = read_key(key_path, private_key)
|
||||||
generated_jwt = encode_jwt(app_id, jwk, exp=expiry)
|
generated_jwt = encode_jwt(app_id, jwk, exp=expiry)
|
||||||
return post_request(generated_jwt, installation_id)
|
return post_request(generated_jwt, installation_id)
|
||||||
|
|
||||||
|
@ -146,10 +154,16 @@ class LookupModule(LookupBase):
|
||||||
|
|
||||||
self.set_options(var_options=variables, direct=kwargs)
|
self.set_options(var_options=variables, direct=kwargs)
|
||||||
|
|
||||||
|
if not (self.get_option("key_path") or self.get_option("private_key")):
|
||||||
|
raise AnsibleOptionsError("One of key_path or private_key is required")
|
||||||
|
if self.get_option("key_path") and self.get_option("private_key"):
|
||||||
|
raise AnsibleOptionsError("key_path and private_key are mutually exclusive")
|
||||||
|
|
||||||
t = get_token(
|
t = get_token(
|
||||||
self.get_option('key_path'),
|
self.get_option('key_path'),
|
||||||
self.get_option('app_id'),
|
self.get_option('app_id'),
|
||||||
self.get_option('installation_id'),
|
self.get_option('installation_id'),
|
||||||
|
self.get_option('private_key'),
|
||||||
self.get_option('token_expiry'),
|
self.get_option('token_expiry'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEogIBAAKCAQEAr/EjKUujUdliSX79ZlDwq/+RCnOF1JCrekWGOOK4YGqgfJBM
|
||||||
|
Z/CLHYTW+BQAH172NTEwLlegwJpXtalae9WVhyMs4sFm7nxSZsFjRK7Gof1tuFbD
|
||||||
|
i4+GGlu4kci7xVcxzoZoVvswX4Xw/9MCg/Je35H8xbugwsWYg+ou79e0wx0fYU4d
|
||||||
|
dwiUte8K+/d1l5acMQuqcnUfJLRmXvw3w7hyemB51EPTGqkpcA4KmYns1W12ianD
|
||||||
|
Zo9/d2kLC2mcyxDkHmqWCv9vfUVrKB7yIC8DU5uY/acFtBaVE1yyvI+1lCCkxNWX
|
||||||
|
5IDbpP/xRJk68B0WXKaU2IdFUVYSD48u3nSZoQIDAQABAoIBAGLL9KOevqIagK+m
|
||||||
|
qKKItuzOgOKuhisb5b6uRbWx0jkKBv6LhOwkzemQi6oYiQ0UpQqviU+sky80PCZd
|
||||||
|
Z9r7z5Bn9y+JzMQEeb0LwTNzNUUHa1JFHl9DA9nPQXBTmOUyllxTa0nUmZA6RV9S
|
||||||
|
XSo8snu2nYtnVdmpXYBNw3eY1/9rb1blXEZHLJbCTaTX3MuWDYuJ4G0K6EArjSwG
|
||||||
|
DDGhOWIWfkk3zZAHqdsrJxgqXIx2Cv9m40hmC0XMwqh8/H3j4kZZhdglJhNbvnBM
|
||||||
|
8ZKRzpMOP1hbGATmi9A1HU+o6BpdIl1dyMRiod3WjSS7CKvs8BVR0XMK/SXDV9Wl
|
||||||
|
Jy6kwYUCgYEA4HwzV/YT+cTb61VL5ICj871m5VMaGJD96dOrnX33QYRNw5aLRc35
|
||||||
|
HMaJ1t5Bp0d2J5h0mPoQkSvQxuPYfaytTYknSUE/bObYNMEnII3XeTmA8ILlG7kV
|
||||||
|
8OQah66GMKjyHocie2PxUuu9BWtuPvZJDrOuR9Pmw5aH6+oiXBSM0YcCgYEAyKRW
|
||||||
|
FHtDGC8ZHgBaytaGvlbVo3RTKboQgYqzf9HdvzWHlSbeZVuCk6MWtSNU+5+26RBK
|
||||||
|
FCI8FTBxqY/vai9zRgp/1u3jY3N1WIsowBgBV7C84IP6gEr/FAJlx++Eqzfmx1W7
|
||||||
|
lU3/0IJ/jYS7D6C4aADifo4aGF0mFHFBk7sfpZcCgYBaIyTOnf15XgVcIjy9/LVY
|
||||||
|
amXFkS+6S4XY/Og87dZ5VTGQZoN3vPPZDRNN1qKQE46q6Xlv74D1eZ10Lwq/s7VG
|
||||||
|
m9rNfEiGZs7Lp/8ZADtT7rYKXNS35AKeXkkU0AwLv9qwTVyYJRJCVGvqoC99UpEV
|
||||||
|
OSqyprBTOr9LCBFR3eKJQwKBgHXRqoqUZy3IWmN3qdj6aF1U+Fbnc/5IuHCZVhZ0
|
||||||
|
0lX5xQgcrvOt7NttJWRwvvKTMwFhA18XS1jV/aioUNp1yqcSe0dmoeRAZGP+M4u5
|
||||||
|
jPBFZGQim/LCF09UqRfi2nEAfpAHFAP0rYdvWh9sFbxzkFXiTx4pq8Eq0bWnW+64
|
||||||
|
Lzk5AoGAVeV9KgqJZLbl2Vbii3bJOuvtNeHOkIYPIoU6kgox9qp1derccWuMtwLT
|
||||||
|
PjhnWuCAX5dN1d7Rve4EovkjvuomDuZy6NCQlmLA6ff2pxtcJAkGy8Blc5VQeWs9
|
||||||
|
i9DUFz2Sx6olEO7MDykj4B6O2YNlAwb8xq1oivE24laZPufprwI=
|
||||||
|
-----END RSA PRIVATE KEY-----
|
|
@ -0,0 +1,3 @@
|
||||||
|
Copyright (c) Ansible Project
|
||||||
|
GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
|
@ -0,0 +1,30 @@
|
||||||
|
####################################################################
|
||||||
|
# WARNING: These are designed specifically for Ansible tests #
|
||||||
|
# and should not be used as examples of how to write Ansible roles #
|
||||||
|
####################################################################
|
||||||
|
|
||||||
|
# Test code for the github_app_access_token plugin.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2017-2018, Abhijeet Kasurde <akasurde@redhat.com>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
- name: Install JWT
|
||||||
|
ansible.builtin.pip:
|
||||||
|
name:
|
||||||
|
- jwt
|
||||||
|
|
||||||
|
- name: Read file
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
github_app_private_key: "{{ lookup('ansible.builtin.file', 'app-private-key.pem') }}"
|
||||||
|
|
||||||
|
- name: Generate Github App Token
|
||||||
|
register: github_app_access_token
|
||||||
|
ignore_errors: true
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
github_app_token: "{{ lookup('community.general.github_app_access_token', app_id=github_app_id, installation_id=github_app_installation_id, private_key=github_app_private_key) }}"
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- github_app_access_token is failed
|
||||||
|
- '"Github return error" in github_app_access_token.msg'
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
github_app_id: 123456
|
||||||
|
github_app_installation_id: 123456
|
|
@ -32,7 +32,7 @@ class MockResponse(MagicMock):
|
||||||
|
|
||||||
class TestLookupModule(unittest.TestCase):
|
class TestLookupModule(unittest.TestCase):
|
||||||
|
|
||||||
def test_get_token(self):
|
def test_get_token_with_file(self):
|
||||||
with patch.multiple("ansible_collections.community.general.plugins.lookup.github_app_access_token",
|
with patch.multiple("ansible_collections.community.general.plugins.lookup.github_app_access_token",
|
||||||
open=mock_open(read_data="foo_bar"),
|
open=mock_open(read_data="foo_bar"),
|
||||||
open_url=MagicMock(return_value=MockResponse()),
|
open_url=MagicMock(return_value=MockResponse()),
|
||||||
|
@ -50,3 +50,21 @@ class TestLookupModule(unittest.TestCase):
|
||||||
token_expiry=600
|
token_expiry=600
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_get_token_with_fact(self):
|
||||||
|
with patch.multiple("ansible_collections.community.general.plugins.lookup.github_app_access_token",
|
||||||
|
open_url=MagicMock(return_value=MockResponse()),
|
||||||
|
jwk_from_pem=MagicMock(return_value='private_key'),
|
||||||
|
jwt_instance=MockJWT(),
|
||||||
|
HAS_JWT=True):
|
||||||
|
lookup = lookup_loader.get('community.general.github_app_access_token')
|
||||||
|
self.assertListEqual(
|
||||||
|
[MockResponse.response_token],
|
||||||
|
lookup.run(
|
||||||
|
[],
|
||||||
|
app_id="app_id",
|
||||||
|
installation_id="installation_id",
|
||||||
|
private_key="foo_bar",
|
||||||
|
token_expiry=600
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue