Skip to content

Instantly share code, notes, and snippets.

@rleap-m
Last active February 13, 2026 15:07
Show Gist options
  • Select an option

  • Save rleap-m/ea7718c3b323b2134d092c30afad86d8 to your computer and use it in GitHub Desktop.

Select an option

Save rleap-m/ea7718c3b323b2134d092c30afad86d8 to your computer and use it in GitHub Desktop.
Inspect Kubernetes RBAC artifacts generated by MKE4 to represent orgs, teams, and grants
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_NAME="$(basename "$0")"
VERSION="1.0.0"
usage() {
cat <<EOF
Usage:
${SCRIPT_NAME} <command> [options]
Purpose:
Inspect Kubernetes RBAC artifacts generated by MKE4 to represent orgs, teams, and grants.
Commands:
topology List MKE org/team ClusterRoles (aggregate roots + team layers)
crb List MKE ClusterRoleBindings (cluster-scoped bindings)
rb List MKE RoleBindings across all namespaces (namespaced bindings)
bindings List both CRBs and RBs
all List topology + bindings
Options:
--org <name> Filter by mke.org.name
--team <name> Filter by mke.team.name
--user <username> Filter by subject username (subjects[].name), e.g. "jdoe"
--user-id <mke-name> Filter by mke.user.name label, e.g. "mke-name-6d6f7368..."
--user-type <type> Filter by mke.user.type (e.g. ldap-group, ldap, member, individual)
--list-subjects For bindings, print one row per subject (adds SUBJ_NAME column)
--list-selectors For topology, print one row per selector term (structured columns)
--debug Print the kubectl command being executed (readable, truncated)
-v, --version Show script version
-h, --help Show this help
Notes:
- Kubernetes label selectors do not support OR. When no filters are provided, the tool queries:
1) org/team-labeled objects (mke.org.name present)
2) individual-user-labeled objects (mke.user.type=individual)
- --user filters by subjects[].name and therefore applies to individual-user bindings. It is
implemented by fetching the individual-user domain and filtering in the template.
Examples:
${SCRIPT_NAME} topology
${SCRIPT_NAME} topology --list-selectors
${SCRIPT_NAME} crb
${SCRIPT_NAME} crb --user-type ldap-group
${SCRIPT_NAME} crb --user jdoe
${SCRIPT_NAME} crb --user-id mke-name-6d6f7368697572
${SCRIPT_NAME} rb --user jdoe
${SCRIPT_NAME} rb --user-type individual
EOF
}
version() {
echo "${SCRIPT_NAME} version ${VERSION}"
}
die() { echo "ERROR: $*" >&2; exit 1; }
debug="0"
list_subjects="0"
list_selectors="0"
debug_kubectl() {
local resource="$1"
local selector="$2"
local template_desc="$3"
local template_len="$4"
if [[ "$debug" == "1" ]]; then
{
echo "DEBUG:"
echo " kubectl get ${resource} \\"
echo " -l \"${selector}\" \\"
echo " -o go-template='${template_desc}…' (len=${template_len})"
echo
} >&2
fi
}
print_table() {
# Split only on tabs so spaces inside fields don't create fake columns.
column -t -s $'\t'
}
command="${1:-}"
if [[ -z "${command}" || "${command}" == "-h" || "${command}" == "--help" ]]; then
usage
exit 0
fi
if [[ "${command}" == "-v" || "${command}" == "--version" ]]; then
version
exit 0
fi
if [[ "${command}" == --* ]]; then
echo "ERROR: Missing command." >&2
echo >&2
echo "Expected one of: topology, crb, rb, bindings, all" >&2
echo >&2
echo "Example:" >&2
echo " ${SCRIPT_NAME} crb --user-type ldap-group" >&2
exit 1
fi
shift || true
org=""
team=""
user="" # subjects[].name (human username)
user_id="" # mke.user.name (opaque/stable id label)
user_type=""
while [[ $# -gt 0 ]]; do
case "$1" in
--org) org="${2:-}"; shift 2 ;;
--team) team="${2:-}"; shift 2 ;;
--user) user="${2:-}"; shift 2 ;;
--user-id) user_id="${2:-}"; shift 2 ;;
--user-type) user_type="${2:-}"; shift 2 ;;
--list-subjects) list_subjects="1"; shift ;;
--list-selectors) list_selectors="1"; shift ;;
--debug) debug="1"; shift ;;
-v|--version) version; exit 0 ;;
-h|--help) usage; exit 0 ;;
*) die "Unknown argument: $1" ;;
esac
done
# -----------------------------
# Domain selection logic
# -----------------------------
# org/team domain: objects labeled with mke.org.name (org/team topology, membership CRBs, grant RBs)
# user domain: objects labeled with mke.user.* (individual user grants via RB/CRB)
#
# Because label selectors can’t do OR, we sometimes query both domains and merge output.
want_org_domain() {
# If org/team filters are used, user explicitly wants org-domain.
if [[ -n "${org}" || -n "${team}" ]]; then
return 0
fi
# If user-id or subject user is requested, it’s user-domain.
if [[ -n "${user_id}" || -n "${user}" ]]; then
return 1
fi
# If user-type is provided:
# - individual => user-domain only
# - anything else => org-domain only
if [[ -n "${user_type}" ]]; then
if [[ "${user_type}" == "individual" ]]; then
return 1
fi
return 0
fi
# No filters => include org-domain.
return 0
}
want_user_domain() {
# If org/team filters are used, user explicitly wants org-domain only.
if [[ -n "${org}" || -n "${team}" ]]; then
return 1
fi
# If user-id or subject user is requested, include user-domain.
if [[ -n "${user_id}" || -n "${user}" ]]; then
return 0
fi
# If user-type is provided:
# - individual => user-domain
# - anything else => org-domain only
if [[ -n "${user_type}" ]]; then
if [[ "${user_type}" == "individual" ]]; then
return 0
fi
return 1
fi
# No filters => include user-domain (default individual grants).
return 0
}
# -----------------------------
# Label selector builders
# -----------------------------
build_selector_org_domain() {
# Base: anything with mke.org.name label
local sel="mke.org.name"
[[ -n "${org}" ]] && sel="${sel},mke.org.name=${org}"
[[ -n "${team}" ]] && sel="${sel},mke.team.name=${team}"
[[ -n "${user_type}" ]] && sel="${sel},mke.user.type=${user_type}"
echo "${sel}"
}
build_selector_user_domain() {
# Base: user-grant objects
# If user_id provided, prefer exact match.
if [[ -n "${user_id}" ]]; then
local sel="mke.user.name=${user_id}"
[[ -n "${user_type}" ]] && sel="${sel},mke.user.type=${user_type}"
echo "${sel}"
return
fi
# If user_type provided, use it (often "individual" here).
if [[ -n "${user_type}" ]]; then
echo "mke.user.type=${user_type}"
return
fi
# Default: individual grants
echo "mke.user.type=individual"
}
build_selector_topology() {
# Topology objects are org/team ClusterRoles; they live in org-domain.
local sel="mke.org.name"
[[ -n "${org}" ]] && sel="${sel},mke.org.name=${org}"
[[ -n "${team}" ]] && sel="${sel},mke.team.name=${team}"
echo "${sel}"
}
# -----------------------------
# Commands
# -----------------------------
list_topology() {
local selector template
selector="$(build_selector_topology)"
if [[ "${list_selectors}" == "1" ]]; then
# One row per selector term:
# - matchLabels => SEL_TYPE=label, SEL_KEY=key, SEL_OP="=", SEL_VALUES=value
# - matchExpressions => SEL_TYPE=expr, SEL_KEY=key, SEL_OP=<operator>, SEL_VALUES=csv or <none>
#
# Note: kubectl go-template does NOT include sprig funcs like join, so CSV is built manually.
template='{{printf "NAME\tORG\tTEAM\tAGG\tRULES\tSEL_TYPE\tSEL_KEY\tSEL_OP\tSEL_VALUES\n"}}{{range .items}}{{ $it := . }}{{ $agg := "no" }}{{ if $it.aggregationRule }}{{ $agg = "yes" }}{{ end }}{{ $rules := "no" }}{{ if $it.rules }}{{ $rules = "yes" }}{{ end }}{{ $org := (or (index $it.metadata.labels "mke.org.name") "<none>") }}{{ $team := (or (index $it.metadata.labels "mke.team.name") "<none>") }}{{ if and $it.aggregationRule $it.aggregationRule.clusterRoleSelectors }}{{ range $it.aggregationRule.clusterRoleSelectors }}{{ if .matchLabels }}{{ range $k, $v := .matchLabels }}{{ printf "%s\t%s\t%s\t%s\t%s\tlabel\t%s\t=\t%s\n" $it.metadata.name $org $team $agg $rules $k $v }}{{ end }}{{ end }}{{ if .matchExpressions }}{{ range .matchExpressions }}{{ $vals := "<none>" }}{{ if .values }}{{ $vals = "" }}{{ $first := true }}{{ range .values }}{{ if not $first }}{{ $vals = printf "%s,%s" $vals . }}{{ else }}{{ $vals = printf "%s" . }}{{ end }}{{ $first = false }}{{ end }}{{ end }}{{ printf "%s\t%s\t%s\t%s\t%s\texpr\t%s\t%s\t%s\n" $it.metadata.name $org $team $agg $rules .key .operator $vals }}{{ end }}{{ end }}{{ end }}{{ else }}{{ printf "%s\t%s\t%s\t%s\t%s\t<none>\t<none>\t<none>\t<none>\n" $it.metadata.name $org $team $agg $rules }}{{ end }}{{end}}'
debug_kubectl \
"clusterroles" \
"${selector}" \
"topology (list) columns: NAME ORG TEAM AGG RULES SEL_TYPE SEL_KEY SEL_OP SEL_VALUES" \
"${#template}"
kubectl get clusterroles -l "${selector}" -o go-template="${template}" | print_table
return
fi
# Default: compact view with selector-group count (RULES before SEL_COUNT for column alignment w/ list-selectors)
template='{{printf "NAME\tORG\tTEAM\tAGG\tRULES\tSEL_COUNT\n"}}{{range .items}}{{ $agg := "no" }}{{ if .aggregationRule }}{{ $agg = "yes" }}{{ end }}{{ $rules := "no" }}{{ if .rules }}{{ $rules = "yes" }}{{ end }}{{ $selc := 0 }}{{ if .aggregationRule }}{{ $selc = (len .aggregationRule.clusterRoleSelectors) }}{{ end }}{{printf "%s\t%s\t%s\t%s\t%s\t%d\n" .metadata.name (or (index .metadata.labels "mke.org.name") "<none>") (or (index .metadata.labels "mke.team.name") "<none>") $agg $rules $selc}}{{end}}'
debug_kubectl \
"clusterroles" \
"${selector}" \
"topology columns: NAME ORG TEAM AGG RULES SEL_COUNT" \
"${#template}"
kubectl get clusterroles -l "${selector}" -o go-template="${template}" | print_table
}
list_crb() {
local selector_org selector_user header rows_org rows_user
selector_org="$(build_selector_org_domain)"
selector_user="$(build_selector_user_domain)"
if [[ "${list_subjects}" == "1" ]]; then
header="NAME\tORG\tTEAM\tUSER_TYPE\tSUBJ_KIND\tSUBJ_NAME\tROLE_REF\n"
# Org-domain rows (no subject-name filtering here)
rows_org='{{range .items}}{{ $it := . }}{{ $org := (or (index $it.metadata.labels "mke.org.name") "<none>") }}{{ $team := (or (index $it.metadata.labels "mke.team.name") "<none>") }}{{ $ut := (or (index $it.metadata.labels "mke.user.type") "<none>") }}{{ if $it.subjects }}{{range $it.subjects}}{{printf "%s\t%s\t%s\t%s\t%s\t%s\t%s/%s\n" $it.metadata.name $org $team $ut (or .kind "<none>") (or .name "<none>") $it.roleRef.kind $it.roleRef.name}}{{end}}{{ else }}{{printf "%s\t%s\t%s\t%s\t%s\t%s\t%s/%s\n" $it.metadata.name $org $team $ut "<none>" "<none>" $it.roleRef.kind $it.roleRef.name}}{{ end }}{{end}}'
# User-domain rows (optionally filter by --user username)
if [[ -n "${user}" ]]; then
rows_user='{{ $want := "'"${user}"'" }}{{range .items}}{{ $it := . }}{{ $org := (or (index $it.metadata.labels "mke.org.name") "<none>") }}{{ $team := (or (index $it.metadata.labels "mke.team.name") "<none>") }}{{ $ut := (or (index $it.metadata.labels "mke.user.type") "<none>") }}{{ if $it.subjects }}{{range $it.subjects}}{{ if eq .name $want }}{{printf "%s\t%s\t%s\t%s\t%s\t%s\t%s/%s\n" $it.metadata.name $org $team $ut (or .kind "<none>") (or .name "<none>") $it.roleRef.kind $it.roleRef.name}}{{ end }}{{end}}{{ end }}{{end}}'
else
rows_user="${rows_org}"
fi
{
printf "%b" "${header}"
if want_org_domain; then
debug_kubectl "clusterrolebindings" "${selector_org}" "CRB (list) columns: NAME ORG TEAM USER_TYPE SUBJ_KIND SUBJ_NAME ROLE_REF (org-domain)" "${#rows_org}"
kubectl get clusterrolebindings -l "${selector_org}" -o go-template="${rows_org}" || true
fi
if want_user_domain; then
debug_kubectl "clusterrolebindings" "${selector_user}" "CRB (list) columns: NAME ORG TEAM USER_TYPE SUBJ_KIND SUBJ_NAME ROLE_REF (user-domain)" "${#rows_user}"
kubectl get clusterrolebindings -l "${selector_user}" -o go-template="${rows_user}" || true
fi
} | print_table
return
fi
header="NAME\tORG\tTEAM\tUSER_TYPE\tSUBJ_KIND\tSUBJ_COUNT\tROLE_REF\n"
rows_org='{{range .items}}{{ $sc := 0 }}{{ if .subjects }}{{ $sc = (len .subjects) }}{{ end }}{{ $sk := "<none>" }}{{ if .subjects }}{{ $sk = (or (index .subjects 0).kind "<none>") }}{{ end }}{{printf "%s\t%s\t%s\t%s\t%s\t%d\t%s/%s\n" .metadata.name (or (index .metadata.labels "mke.org.name") "<none>") (or (index .metadata.labels "mke.team.name") "<none>") (or (index .metadata.labels "mke.user.type") "<none>") $sk $sc .roleRef.kind .roleRef.name}}{{end}}'
if [[ -n "${user}" ]]; then
# Only print rows where first subject matches the requested username (common shape for individual grants).
rows_user='{{ $want := "'"${user}"'" }}{{range .items}}{{ if and .subjects (eq (index .subjects 0).name $want) }}{{ $sc := 1 }}{{ $sk := (or (index .subjects 0).kind "<none>") }}{{printf "%s\t%s\t%s\t%s\t%s\t%d\t%s/%s\n" .metadata.name (or (index .metadata.labels "mke.org.name") "<none>") (or (index .metadata.labels "mke.team.name") "<none>") (or (index .metadata.labels "mke.user.type") "<none>") $sk $sc .roleRef.kind .roleRef.name}}{{ end }}{{end}}'
else
rows_user="${rows_org}"
fi
{
printf "%b" "${header}"
if want_org_domain; then
debug_kubectl "clusterrolebindings" "${selector_org}" "CRB columns: NAME ORG TEAM USER_TYPE SUBJ_KIND SUBJ_COUNT ROLE_REF (org-domain)" "${#rows_org}"
kubectl get clusterrolebindings -l "${selector_org}" -o go-template="${rows_org}" || true
fi
if want_user_domain; then
debug_kubectl "clusterrolebindings" "${selector_user}" "CRB columns: NAME ORG TEAM USER_TYPE SUBJ_KIND SUBJ_COUNT ROLE_REF (user-domain)" "${#rows_user}"
kubectl get clusterrolebindings -l "${selector_user}" -o go-template="${rows_user}" || true
fi
} | print_table
}
list_rb() {
local selector_org selector_user header rows_org rows_user
selector_org="$(build_selector_org_domain)"
selector_user="$(build_selector_user_domain)"
if [[ "${list_subjects}" == "1" ]]; then
header="NAMESPACE\tNAME\tORG\tTEAM\tUSER_TYPE\tBINDING_TYPE\tSUBJ_KIND\tSUBJ_NAME\tROLE_REF\n"
rows_org='{{range .items}}{{ $it := . }}{{ $org := (or (index $it.metadata.labels "mke.org.name") "<none>") }}{{ $team := (or (index $it.metadata.labels "mke.team.name") "<none>") }}{{ $ut := (or (index $it.metadata.labels "mke.user.type") "<none>") }}{{ $bt := "enforcement" }}{{ if eq (index $it.metadata.labels "mke.grant.record") "true" }}{{ $bt = "record" }}{{ end }}{{ if $it.subjects }}{{range $it.subjects}}{{printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s/%s\n" $it.metadata.namespace $it.metadata.name $org $team $ut $bt (or .kind "<none>") (or .name "<none>") $it.roleRef.kind $it.roleRef.name}}{{end}}{{ else }}{{printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s/%s\n" $it.metadata.namespace $it.metadata.name $org $team $ut $bt "<none>" "<none>" $it.roleRef.kind $it.roleRef.name}}{{ end }}{{end}}'
if [[ -n "${user}" ]]; then
rows_user='{{ $want := "'"${user}"'" }}{{range .items}}{{ $it := . }}{{ $org := (or (index $it.metadata.labels "mke.org.name") "<none>") }}{{ $team := (or (index $it.metadata.labels "mke.team.name") "<none>") }}{{ $ut := (or (index $it.metadata.labels "mke.user.type") "<none>") }}{{ $bt := "enforcement" }}{{ if eq (index $it.metadata.labels "mke.grant.record") "true" }}{{ $bt = "record" }}{{ end }}{{ if $it.subjects }}{{range $it.subjects}}{{ if eq .name $want }}{{printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s/%s\n" $it.metadata.namespace $it.metadata.name $org $team $ut $bt (or .kind "<none>") (or .name "<none>") $it.roleRef.kind $it.roleRef.name}}{{ end }}{{end}}{{ end }}{{end}}'
else
rows_user="${rows_org}"
fi
{
printf "%b" "${header}"
if want_org_domain; then
debug_kubectl "rolebindings (all namespaces)" "${selector_org}" "RB (list) columns: NAMESPACE NAME ORG TEAM USER_TYPE BINDING_TYPE SUBJ_KIND SUBJ_NAME ROLE_REF (org-domain)" "${#rows_org}"
kubectl get rolebindings -A -l "${selector_org}" -o go-template="${rows_org}" || true
fi
if want_user_domain; then
debug_kubectl "rolebindings (all namespaces)" "${selector_user}" "RB (list) columns: NAMESPACE NAME ORG TEAM USER_TYPE BINDING_TYPE SUBJ_KIND SUBJ_NAME ROLE_REF (user-domain)" "${#rows_user}"
kubectl get rolebindings -A -l "${selector_user}" -o go-template="${rows_user}" || true
fi
} | print_table
return
fi
header="NAMESPACE\tNAME\tORG\tTEAM\tUSER_TYPE\tBINDING_TYPE\tSUBJ_KIND\tSUBJ_COUNT\tROLE_REF\n"
rows_org='{{range .items}}{{ $sc := 0 }}{{ if .subjects }}{{ $sc = (len .subjects) }}{{ end }}{{ $sk := "<none>" }}{{ if .subjects }}{{ $sk = (or (index .subjects 0).kind "<none>") }}{{ end }}{{ $bt := "enforcement" }}{{ if eq (index .metadata.labels "mke.grant.record") "true" }}{{ $bt = "record" }}{{ end }}{{printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%d\t%s/%s\n" .metadata.namespace .metadata.name (or (index .metadata.labels "mke.org.name") "<none>") (or (index .metadata.labels "mke.team.name") "<none>") (or (index .metadata.labels "mke.user.type") "<none>") $bt $sk $sc .roleRef.kind .roleRef.name}}{{end}}'
if [[ -n "${user}" ]]; then
rows_user='{{ $want := "'"${user}"'" }}{{range .items}}{{ if and .subjects (eq (index .subjects 0).name $want) }}{{ $sc := 1 }}{{ $sk := (or (index .subjects 0).kind "<none>") }}{{ $bt := "enforcement" }}{{ if eq (index .metadata.labels "mke.grant.record") "true" }}{{ $bt = "record" }}{{ end }}{{printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%d\t%s/%s\n" .metadata.namespace .metadata.name (or (index .metadata.labels "mke.org.name") "<none>") (or (index .metadata.labels "mke.team.name") "<none>") (or (index .metadata.labels "mke.user.type") "<none>") $bt $sk $sc .roleRef.kind .roleRef.name}}{{ end }}{{end}}'
else
rows_user="${rows_org}"
fi
{
printf "%b" "${header}"
if want_org_domain; then
debug_kubectl "rolebindings (all namespaces)" "${selector_org}" "RB columns: NAMESPACE NAME ORG TEAM USER_TYPE BINDING_TYPE SUBJ_KIND SUBJ_COUNT ROLE_REF (org-domain)" "${#rows_org}"
kubectl get rolebindings -A -l "${selector_org}" -o go-template="${rows_org}" || true
fi
if want_user_domain; then
debug_kubectl "rolebindings (all namespaces)" "${selector_user}" "RB columns: NAMESPACE NAME ORG TEAM USER_TYPE BINDING_TYPE SUBJ_KIND SUBJ_COUNT ROLE_REF (user-domain)" "${#rows_user}"
kubectl get rolebindings -A -l "${selector_user}" -o go-template="${rows_user}" || true
fi
} | print_table
}
list_bindings() {
echo "== ClusterRoleBindings =="
list_crb
echo
echo "== RoleBindings (all namespaces) =="
list_rb
}
list_all() {
echo "== RBAC topology (org/team roles) =="
list_topology
echo
echo "== RBAC enforcement bindings =="
list_bindings
}
case "${command}" in
topology) list_topology ;;
crb) list_crb ;;
rb) list_rb ;;
bindings) list_bindings ;;
all) list_all ;;
*) die "Unknown command: ${command} (try --help)" ;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment