keycloak / terraform-provider-keycloak

Terraform provider for Keycloak
https://registry.terraform.io/providers/mrparkers/keycloak/latest/docs
Apache License 2.0
650 stars 317 forks source link

Adds support for the ExcludeIssuerFromAuthResponse option on OpenIdClient #934

Open sebght opened 9 months ago

sebght commented 9 months ago

Solves https://github.com/mrparkers/terraform-provider-keycloak/issues/928

What has been changed :

paul-pch commented 8 months ago

I've been struggling with this exat configuration. Thanks for the PR !

sebght commented 8 months ago

My tests have led me to think that the option is changed to false until it's not explicitly given to the API... This means everytime you terraform apply your client resource, it will set it back to false... Kind of a thorn in the side for ppl I guess (for me it is 😅). Should we consider a v4.4.1 @mrparkers ?

sebght commented 8 months ago

Nevermind, I did more precise tests and they prove that the option is not overwritten every apply. Sorry for the spamming !

bturos commented 2 months ago

Hi @sebght ! Thanks for working on adding support for this option Any chance this could be merged soon? This would help a lot in my current project 🙂

mwalczykpl commented 2 months ago

Good job @sebght, can't wait to see that released! 🚀

sebght commented 2 months ago

Well the repository is quite on pause currently, but I hope that this will change and that this PR will move on ! Follow the news here : https://github.com/mrparkers/terraform-provider-keycloak/issues/964

B3ns44d commented 1 month ago

Hello @sebght, Thank you for the merge request. We're currently encountering this issue after upgrading Keycloak to the latest version. Is there any chance this could be merged soon? In the meantime, we've created a script as a workaround until an official release is available.

import requests
import argparse
import logging
from typing import List, Optional

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)

def get_token(idp_host: str, username: str, password: str) -> Optional[str]:
    """Get access token from the Identity Provider"""
    url = f"{idp_host}/realms/master/protocol/openid-connect/token"
    payload = {
        "username": username,
        "password": password,
        "grant_type": "password",
        "client_id": "admin-cli",
    }
    headers = {"Content-Type": "application/x-www-form-urlencoded"}

    response = requests.post(url, data=payload, headers=headers)

    if response.status_code == 200:
        logger.info("Successfully obtained token")
        return response.json().get("access_token")
    else:
        logger.error(f"Failed to get token: {response.status_code}, {response.text}")
        return None

def get_realms(idp_host: str, token: str) -> List[str]:
    """Fetch a list of realms"""
    url = f"{idp_host}/admin/realms"
    headers = {"Authorization": f"Bearer {token}"}

    response = requests.get(url, headers=headers)

    if response.status_code == 200:
        logger.info("Successfully fetched realms")
        realms = response.json()
        return [realm["realm"] for realm in realms]
    else:
        logger.error(f"Failed to fetch realms: {response.status_code}, {response.text}")
        return []

def get_client_ids(idp_host: str, realm: str, token: str) -> List[str]:
    """Fetch client IDs that contain 'ui' in their clientId"""
    url = f"{idp_host}/admin/realms/{realm}/clients"
    headers = {"Authorization": f"Bearer {token}"}

    response = requests.get(url, headers=headers)

    if response.status_code == 200:
        logger.info(f"Successfully fetched clients for realm {realm}")
        clients = response.json()
        return [client["id"] for client in clients if "ui" in client["clientId"]]
    else:
        logger.error(f"Failed to fetch clients for realm {realm}: {response.status_code}, {response.text}")
        return []

def update_client(idp_host: str, realm: str, client_id: str, token: str) -> None:
    """Update the client's attribute to exclude issuer from the URL"""
    url = f"{idp_host}/admin/realms/{realm}/clients/{client_id}"
    headers = {"Authorization": f"Bearer {token}"}

    # Fetch client details
    response = requests.get(url, headers=headers)

    if response.status_code == 200:
        client_details = response.json()
        client_name = client_details["clientId"]
        logger.info(f"Updating client {client_name} in realm {realm}...")

        # Update client attribute
        client_details["attributes"]["exclude.issuer.from.auth.response"] = "true"

        # Send the updated client details
        update_response = requests.put(
            url, headers=headers, json=client_details
        )

        if update_response.status_code == 204:
            logger.info(f"Client {client_name} updated successfully.")
        else:
            logger.error(
                f"Failed to update client {client_name}: {update_response.status_code}, {update_response.text}")
    else:
        logger.error(f"Failed to fetch client details: {response.status_code}, {response.text}")

def main(idp_host: str, username: str, password: str) -> None:
    """Main function to handle the logic of fetching realms and updating clients"""
    token = get_token(idp_host, username, password)

    if not token:
        logger.error("Failed to retrieve token. Exiting.")
        return

    realms = get_realms(idp_host, token)
    if not realms:
        logger.error("No realms found. Exiting.")
        return

    for realm in realms:
        logger.info(f"Processing realm: {realm}")
        client_ids = get_client_ids(idp_host, realm, token)

        for client_id in client_ids:
            update_client(idp_host, realm, client_id, token)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Keycloak Client Attribute Updater")
    parser.add_argument("--host", required=True, help="Identity Provider host URL")
    parser.add_argument("-u", "--username", required=True, help="Username for authentication")
    parser.add_argument("-p", "--password", required=True, help="Password for authentication")

    args = parser.parse_args()

    main(args.host, args.username, args.password)