mirror of
https://github.com/ansible-collections/kubernetes.core.git
synced 2026-03-27 05:43:02 +00:00
add support for user impersonation for k8s modules (#250)
add support for user impersonation for k8s modules SUMMARY k8s module should not allow user to perform operation using impersonation as describe here https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation This pull request closes #40 ISSUE TYPE Feature Pull Request COMPONENT NAME ADDITIONAL INFORMATION Reviewed-by: Mike Graves <mgraves@redhat.com> Reviewed-by: Abhijeet Kasurde <None> Reviewed-by: None <None>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
minor_changes:
|
||||
- k8s - add support for user impersonation. (https://github.com/ansible-collections/kubernetes/core/issues/40).
|
||||
@@ -205,6 +205,14 @@
|
||||
tags:
|
||||
- always
|
||||
|
||||
- name: Include user_impersonation.yml
|
||||
include_tasks:
|
||||
file: tasks/user_impersonation.yml
|
||||
apply:
|
||||
tags: [ user_impersonation, k8s ]
|
||||
tags:
|
||||
- always
|
||||
|
||||
roles:
|
||||
- role: helm
|
||||
tags:
|
||||
|
||||
211
molecule/default/tasks/user_impersonation.yml
Normal file
211
molecule/default/tasks/user_impersonation.yml
Normal file
@@ -0,0 +1,211 @@
|
||||
- block:
|
||||
- set_fact:
|
||||
test_ns: "impersonate"
|
||||
pod_name: "impersonate-pod"
|
||||
# this use will have authorization to list/create pods in the namespace
|
||||
user_01: "authorized-sa-01"
|
||||
# No authorization attached to this user, will use 'user_01' for impersonation
|
||||
user_02: "unauthorize-sa-01"
|
||||
|
||||
- name: Ensure namespace
|
||||
kubernetes.core.k8s:
|
||||
kind: Namespace
|
||||
name: "{{ test_ns }}"
|
||||
|
||||
- name: Get cluster information
|
||||
kubernetes.core.k8s_cluster_info:
|
||||
register: cluster_info
|
||||
no_log: true
|
||||
|
||||
- set_fact:
|
||||
cluster_host: "{{ cluster_info['connection']['host'] }}"
|
||||
|
||||
- name: Create Service account
|
||||
kubernetes.core.k8s:
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: "{{ item }}"
|
||||
namespace: "{{ test_ns }}"
|
||||
with_items:
|
||||
- "{{ user_01 }}"
|
||||
- "{{ user_02 }}"
|
||||
|
||||
- name: Read Service Account - user_01
|
||||
kubernetes.core.k8s_info:
|
||||
kind: ServiceAccount
|
||||
namespace: "{{ test_ns }}"
|
||||
name: "{{ user_01 }}"
|
||||
register: result
|
||||
|
||||
- name: Get secret details
|
||||
kubernetes.core.k8s_info:
|
||||
kind: Secret
|
||||
namespace: '{{ test_ns }}'
|
||||
name: '{{ result.resources[0].secrets[0].name }}'
|
||||
no_log: true
|
||||
register: _secret
|
||||
|
||||
- set_fact:
|
||||
user_01_api_token: "{{ _secret.resources[0]['data']['token'] | b64decode }}"
|
||||
|
||||
- name: Read Service Account - user_02
|
||||
kubernetes.core.k8s_info:
|
||||
kind: ServiceAccount
|
||||
namespace: "{{ test_ns }}"
|
||||
name: "{{ user_02 }}"
|
||||
register: result
|
||||
|
||||
- name: Get secret details
|
||||
kubernetes.core.k8s_info:
|
||||
kind: Secret
|
||||
namespace: '{{ test_ns }}'
|
||||
name: '{{ result.resources[0].secrets[0].name }}'
|
||||
no_log: true
|
||||
register: _secret
|
||||
|
||||
- set_fact:
|
||||
user_02_api_token: "{{ _secret.resources[0]['data']['token'] | b64decode }}"
|
||||
|
||||
- name: Create Role to manage pod on the namespace
|
||||
kubernetes.core.k8s:
|
||||
namespace: "{{ test_ns }}"
|
||||
definition:
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: pod-manager
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["create", "get", "delete", "list", "patch"]
|
||||
|
||||
- name: Attach Role to the user_01
|
||||
kubernetes.core.k8s:
|
||||
namespace: "{{ test_ns }}"
|
||||
definition:
|
||||
kind: RoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: pod-manager-binding
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: "{{ user_01 }}"
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: pod-manager
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
|
||||
- name: Create Pod using user_01 credentials
|
||||
kubernetes.core.k8s:
|
||||
api_key: "{{ user_01_api_token }}"
|
||||
host: "{{ cluster_host }}"
|
||||
validate_certs: no
|
||||
namespace: "{{ test_ns }}"
|
||||
name: "{{ pod_name }}"
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
labels:
|
||||
test: "impersonate"
|
||||
spec:
|
||||
containers:
|
||||
- name: c0
|
||||
image: busybox
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- while true;do date;sleep 5; done
|
||||
|
||||
- name: Delete Pod using user_02 credentials should failed
|
||||
kubernetes.core.k8s:
|
||||
api_key: "{{ user_02_api_token }}"
|
||||
host: "{{ cluster_host }}"
|
||||
validate_certs: no
|
||||
namespace: "{{ test_ns }}"
|
||||
name: "{{ pod_name }}"
|
||||
kind: Pod
|
||||
state: absent
|
||||
register: delete_pod
|
||||
ignore_errors: true
|
||||
|
||||
- name: Assert that operation has failed
|
||||
assert:
|
||||
that:
|
||||
- delete_pod is failed
|
||||
- delete_pod.reason == 'Forbidden'
|
||||
|
||||
- name: Delete Pod using user_02 credentials and impersonation to user_01
|
||||
kubernetes.core.k8s:
|
||||
api_key: "{{ user_02_api_token }}"
|
||||
host: "{{ cluster_host }}"
|
||||
validate_certs: no
|
||||
impersonate_user: "system:serviceaccount:{{ test_ns }}:{{ user_01 }}"
|
||||
namespace: "{{ test_ns }}"
|
||||
name: "{{ pod_name }}"
|
||||
kind: Pod
|
||||
state: absent
|
||||
ignore_errors: true
|
||||
register: delete_pod_2
|
||||
|
||||
- name: Assert that operation has failed
|
||||
assert:
|
||||
that:
|
||||
- delete_pod_2 is failed
|
||||
- delete_pod_2.reason == 'Forbidden'
|
||||
- '"cannot impersonate resource" in delete_pod_2.msg'
|
||||
|
||||
- name: Create Role to impersonate user_01
|
||||
kubernetes.core.k8s:
|
||||
namespace: "{{ test_ns }}"
|
||||
definition:
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: sa-impersonate
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources:
|
||||
- serviceaccounts
|
||||
verbs:
|
||||
- impersonate
|
||||
resourceNames:
|
||||
- "{{ user_01 }}"
|
||||
|
||||
- name: Attach Role to the user_02
|
||||
kubernetes.core.k8s:
|
||||
namespace: "{{ test_ns }}"
|
||||
definition:
|
||||
kind: RoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: sa-impersonate-binding
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: "{{ user_02 }}"
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: sa-impersonate
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
|
||||
- name: Delete Pod using user_02 credentials should succeed now
|
||||
kubernetes.core.k8s:
|
||||
api_key: "{{ user_02_api_token }}"
|
||||
host: "{{ cluster_host }}"
|
||||
validate_certs: no
|
||||
impersonate_user: "system:serviceaccount:{{ test_ns }}:{{ user_01 }}"
|
||||
namespace: "{{ test_ns }}"
|
||||
name: "{{ pod_name }}"
|
||||
kind: Pod
|
||||
state: absent
|
||||
|
||||
always:
|
||||
- name: Ensure namespace is deleted
|
||||
kubernetes.core.k8s:
|
||||
state: absent
|
||||
kind: Namespace
|
||||
name: "{{ test_ns }}"
|
||||
wait: yes
|
||||
ignore_errors: true
|
||||
@@ -119,6 +119,19 @@ options:
|
||||
- Please note that the current version of the k8s python client library does not support setting this flag to True yet.
|
||||
- "The fix for this k8s python library is here: https://github.com/kubernetes-client/python-base/pull/169"
|
||||
type: bool
|
||||
impersonate_user:
|
||||
description:
|
||||
- Username to impersonate for the operation.
|
||||
- Can also be specified via K8S_AUTH_IMPERSONATE_USER environment.
|
||||
type: str
|
||||
version_added: 2.3.0
|
||||
impersonate_groups:
|
||||
description:
|
||||
- Group(s) to impersonate for the operation.
|
||||
- "Can also be specified via K8S_AUTH_IMPERSONATE_GROUPS environment. Example: 'Group1,Group2'"
|
||||
type: list
|
||||
elements: str
|
||||
version_added: 2.3.0
|
||||
notes:
|
||||
- "To avoid SSL certificate validation errors when C(validate_certs) is I(True), the full
|
||||
certificate chain for the API server must be provided via C(ca_cert) or in the
|
||||
|
||||
@@ -32,6 +32,8 @@ AUTH_ARG_SPEC = {
|
||||
"no_proxy": {"type": "str"},
|
||||
"proxy_headers": {"type": "dict", "options": AUTH_PROXY_HEADERS_SPEC},
|
||||
"persist_config": {"type": "bool"},
|
||||
"impersonate_user": {},
|
||||
"impersonate_groups": {"type": "list", "elements": "str"},
|
||||
}
|
||||
|
||||
WAIT_ARG_SPEC = dict(
|
||||
|
||||
@@ -127,7 +127,7 @@ except ImportError as e:
|
||||
K8S_IMP_ERR = traceback.format_exc()
|
||||
|
||||
|
||||
def configuration_digest(configuration):
|
||||
def configuration_digest(configuration, **kwargs):
|
||||
m = hashlib.sha256()
|
||||
for k in AUTH_ARG_MAP:
|
||||
if not hasattr(configuration, k):
|
||||
@@ -140,10 +140,32 @@ def configuration_digest(configuration):
|
||||
m.update(content.encode())
|
||||
else:
|
||||
m.update(str(v).encode())
|
||||
for k in kwargs:
|
||||
content = "{0}: {1}".format(k, kwargs.get(k))
|
||||
m.update(content.encode())
|
||||
digest = m.hexdigest()
|
||||
return digest
|
||||
|
||||
|
||||
class unique_string(str):
|
||||
_low = None
|
||||
|
||||
def __hash__(self):
|
||||
return id(self)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self is other
|
||||
|
||||
def lower(self):
|
||||
if self._low is None:
|
||||
lower = str.lower(self)
|
||||
if str.__eq__(lower, self):
|
||||
self._low = self
|
||||
else:
|
||||
self._low = unique_string(lower)
|
||||
return self._low
|
||||
|
||||
|
||||
def get_api_client(module=None, **kwargs):
|
||||
auth = {}
|
||||
|
||||
@@ -219,9 +241,14 @@ def get_api_client(module=None, **kwargs):
|
||||
"Failed to set no_proxy due to: %s",
|
||||
)
|
||||
|
||||
configuration = None
|
||||
if auth_set("username", "password", "host") or auth_set("api_key", "host"):
|
||||
# We have enough in the parameters to authenticate, no need to load incluster or kubeconfig
|
||||
pass
|
||||
arg_init = {}
|
||||
# api_key will be set later in this function
|
||||
for key in ("username", "password", "host"):
|
||||
arg_init[key] = auth.get(key)
|
||||
configuration = kubernetes.client.Configuration(**arg_init)
|
||||
elif auth_set("kubeconfig") or auth_set("context"):
|
||||
try:
|
||||
_load_config()
|
||||
@@ -240,10 +267,11 @@ def get_api_client(module=None, **kwargs):
|
||||
|
||||
# Override any values in the default configuration with Ansible parameters
|
||||
# As of kubernetes-client v12.0.0, get_default_copy() is required here
|
||||
try:
|
||||
configuration = kubernetes.client.Configuration().get_default_copy()
|
||||
except AttributeError:
|
||||
configuration = kubernetes.client.Configuration()
|
||||
if not configuration:
|
||||
try:
|
||||
configuration = kubernetes.client.Configuration().get_default_copy()
|
||||
except AttributeError:
|
||||
configuration = kubernetes.client.Configuration()
|
||||
|
||||
for key, value in iteritems(auth):
|
||||
if key in AUTH_ARG_MAP.keys() and value is not None:
|
||||
@@ -257,14 +285,46 @@ def get_api_client(module=None, **kwargs):
|
||||
else:
|
||||
setattr(configuration, key, value)
|
||||
|
||||
digest = configuration_digest(configuration)
|
||||
api_client = kubernetes.client.ApiClient(configuration)
|
||||
impersonate_map = {
|
||||
"impersonate_user": "Impersonate-User",
|
||||
"impersonate_groups": "Impersonate-Group",
|
||||
}
|
||||
api_digest = {}
|
||||
|
||||
headers = {}
|
||||
for arg_name, header_name in impersonate_map.items():
|
||||
value = None
|
||||
if module and module.params.get(arg_name) is not None:
|
||||
value = module.params.get(arg_name)
|
||||
elif arg_name in kwargs and kwargs.get(arg_name) is not None:
|
||||
value = kwargs.get(arg_name)
|
||||
else:
|
||||
value = os.getenv("K8S_AUTH_{0}".format(arg_name.upper()), None)
|
||||
if value is not None:
|
||||
if AUTH_ARG_SPEC[arg_name].get("type") == "list":
|
||||
value = [x for x in env_value.split(",") if x != ""]
|
||||
if value:
|
||||
if isinstance(value, list):
|
||||
api_digest[header_name] = ",".join(sorted(value))
|
||||
for v in value:
|
||||
api_client.set_default_header(
|
||||
header_name=unique_string(header_name), header_value=v
|
||||
)
|
||||
else:
|
||||
api_digest[header_name] = value
|
||||
api_client.set_default_header(
|
||||
header_name=header_name, header_value=value
|
||||
)
|
||||
|
||||
digest = configuration_digest(configuration, **api_digest)
|
||||
if digest in get_api_client._pool:
|
||||
client = get_api_client._pool[digest]
|
||||
return client
|
||||
|
||||
try:
|
||||
client = k8sdynamicclient.K8SDynamicClient(
|
||||
kubernetes.client.ApiClient(configuration), discoverer=LazyDiscoverer
|
||||
api_client, discoverer=LazyDiscoverer
|
||||
)
|
||||
except Exception as err:
|
||||
_raise_or_fail(err, "Failed to get client due to %s")
|
||||
|
||||
Reference in New Issue
Block a user