Introduction
In the previous article on listing and managing user capabilities in an OCI IAM Domain, we focused on discovering what each user is allowed to do — for example, whether a user can create API keys, Auth Tokens, or SMTP credentials. That tells us who can create credentials, but many times we also need to know who actually used those capabilities. For example: “did anyone really create an API key?”, “which users currently have auth tokens?”, or “are there any customer secret keys configured for this account?”. Without that second part, you only have theoretical access, not the real, current state of credentials in the domain.
In many environments, however, you also need to answer a second question:
“This user is allowed to create credentials — do they actually have any configured right now?”
This post shows a small Python utility that complements the previous one by enumerating users, checking their capabilities, and then querying the domain for the corresponding credentials. The script writes everything into a CSV for easy review or downstream processing.
What Problem Does This Solve
Tenant admins and security teams often need to:
-
identify users who can create credentials;
-
distinguish those from users who actually have such credentials;
Relying only on “capability = true/false” isn’t enough. This script correlates capability with actual configured items.
Approach
- List all users from the Identity Domain, including the capabilities SCIM extension.
- For each capability that is enabled for that user, call the corresponding list API in the Identity Domains client, filtered by user.
- Record how many credentials of that type exist.
- Write all results to CSV.
Here is the sample python script:
Note: This is a sample script provided for educational and illustrative purposes only. It should be tested thoroughly in a non-production or sandbox environment before being used in any customer tenancy. Ensure appropriate error handling, logging, and access controls are in place when adapting this for use in real-world environments.
import argparse
import csv
import logging
import oci
from oci.exceptions import ServiceError
# Adjust for your environment
CONFIG_PATH = "~/.oci/config"
PROFILE_NAME = "DEFAULT"
SCIM_COUNT = 1 # we only need the count
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
def get_client(domain_url: str):
"""Create IdentityDomainsClient for the given domain."""
config = oci.config.from_file(CONFIG_PATH, PROFILE_NAME)
return oci.identity_domains.IdentityDomainsClient(config, domain_url)
USER_ATTRS = (
"id,userName,"
"urn:ietf:params:scim:schemas:oracle:idcs:extension:capabilities:User"
)
def iter_all_users(client):
"""Yield all users (with capabilities) from the domain."""
pages = oci.pagination.list_call_get_all_results_generator(
client.list_users,
"response",
attributes=USER_ATTRS,
count=200,
)
for resp in pages:
for u in getattr(resp.data, "resources", []) or []:
yield u
def scim_count(client, method_name: str, user_id: str):
"""
Invoke the list_* API for the specified user and return (count, error).
error is None on success.
"""
try:
method = getattr(client, method_name)
except AttributeError:
return None, f"NO_METHOD:{method_name}"
try:
r = method(
filter=f'user.value eq "{user_id}"',
count=SCIM_COUNT,
attributes="id",
)
total = getattr(r.data, "total_results", None) or getattr(r.data, "totalResults", None)
return int(total or 0), None
except ServiceError as e:
if e.status in (401, 403):
return None, "UNAUTHORIZED"
return None, f"ERROR:{e.status}"
def extract_caps(user):
caps = getattr(
user,
"urn_ietf_params_scim_schemas_oracle_idcs_extension_capabilities_user",
None,
)
def flag(name):
return bool(getattr(caps, name, False)) if caps else False
return {
"api_keys": flag("can_use_api_keys"),
"auth_tokens": flag("can_use_auth_tokens"),
"customer_secret_keys": flag("can_use_customer_secret_keys"),
"db_credentials": flag("can_use_db_credentials"),
"oauth2_client_credentials": flag("can_use_o_auth2_client_credentials"),
"smtp_credentials": flag("can_use_smtp_credentials"),
}
LIST_METHODS = {
"api_keys": "list_api_keys",
"auth_tokens": "list_auth_tokens",
"customer_secret_keys": "list_customer_secret_keys",
"db_credentials": "list_user_db_credentials",
"oauth2_client_credentials": "list_o_auth2_client_credentials",
"smtp_credentials": "list_smtp_credentials",
}
def audit(domain_url: str, csv_path: str = "user_capabilities_audit.csv"):
client = get_client(domain_url)
rows = []
unauthorized_seen = set()
for user in iter_all_users(client):
caps = extract_caps(user)
row = {
"user_name": getattr(user, "user_name", "") or getattr(user, "userName", ""),
"can_use_api_keys": caps["api_keys"],
"can_use_auth_tokens": caps["auth_tokens"],
"can_use_customer_secret_keys": caps["customer_secret_keys"],
"can_use_db_credentials": caps["db_credentials"],
"can_use_oauth2_client_credentials": caps["oauth2_client_credentials"],
"can_use_smtp_credentials": caps["smtp_credentials"],
"api_keys_count": 0,
"auth_tokens_count": 0,
"customer_secret_keys_count": 0,
"db_credentials_count": 0,
"oauth2_client_credentials_count": 0,
"smtp_credentials_count": 0,
"api_keys_status": "",
"auth_tokens_status": "",
"customer_secret_keys_status": "",
"db_credentials_status": "",
"oauth2_client_credentials_status": "",
"smtp_credentials_status": "",
}
for key, enabled in caps.items():
method_name = LIST_METHODS[key]
count_col = f"{key}_count"
status_col = f"{key}_status"
if not enabled:
# capability not granted to this user
continue
count, err = scim_count(client, method_name, user.id)
if err == "UNAUTHORIZED":
row[count_col] = "UNAUTHORIZED"
row[status_col] = "UNAUTHORIZED"
unauthorized_seen.add(key)
elif err:
row[count_col] = err
row[status_col] = err
else:
row[count_col] = count
row[status_col] = (
"ENABLED & CONFIGURED" if count > 0 else "ENABLED, None CONFIGURED"
)
rows.append(row)
fieldnames = [
"user_name",
"can_use_api_keys",
"can_use_auth_tokens",
"can_use_customer_secret_keys",
"can_use_db_credentials",
"can_use_oauth2_client_credentials",
"can_use_smtp_credentials",
"api_keys_count",
"auth_tokens_count",
"customer_secret_keys_count",
"db_credentials_count",
"oauth2_client_credentials_count",
"smtp_credentials_count",
"api_keys_status",
"auth_tokens_status",
"customer_secret_keys_status",
"db_credentials_status",
"oauth2_client_credentials_status",
"smtp_credentials_status",
]
with open(csv_path, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
logging.info("Wrote %d users to %s", len(rows), csv_path)
if unauthorized_seen:
logging.warning(
"Some credential types could not be listed due to authorization: %s",
", ".join(sorted(unauthorized_seen)),
)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Audit actual credential usage for users in an OCI Identity Domain"
)
parser.add_argument(
"--domain-url",
required=True,
help="Identity Domain base URL, e.g. https://idcs-xxxxx.identity.oraclecloud.com",
)
parser.add_argument(
"--csv",
default="user_capabilities_audit.csv",
help="Output CSV file path",
)
args = parser.parse_args()
audit(args.domain_url, csv_path=args.csv)
How to run the Script
python audit_capabilities.py \ --domain-url https://idcs-xxxxx.identity.oraclecloud.com \ --csv capabilities_audit.csv
This will produce a CSV with one row per user.
Here is a snippet of a sample output, (Please zoom in for a clear view)

Interpreting the Output
-
can_use_*columns tell you what was granted in IAM. -
*_countcolumns tell you what is actually configured. -
*_statusprovides a label i.e to say if the capability is ENABLED or DISABLED or ENABLED & CONFIGURED
This makes it straightforward to:
-
find users who were granted a capability but never used it;
-
find users actually holding credentials;
Conclusion
This post is intended as a direct companion to the earlier “list capabilities” article. Together they provide configuration visibility (who can do what) and who actually did create a credential. That combination is often required for audits, cleanup campaigns, and periodic access reviews.
If you want this to emit JSON instead of CSV or to filter to a single capability type (for example, “show me just SMTP credentials”), the same structure can be extended very easily.
