À mesure que nous évoluons vers un état d’esprit de confiance zéro, les limites des mesures de sécurité grossières telles que le système RBAC traditionnel deviennent évidentes. Un élément essentiel du passage à la confiance zéro, qui passe souvent inaperçu, est le passage d’une sécurité à granularité grossière à une sécurité à granularité fine.
L'autorisation fine résout ce problème en basant l'accès sur des attributs tels que les rôles d'utilisateur, les actions et même le contexte comme l'heure ou le lieu, et un contrôle d'accès aussi détaillé est vital pour les applications modernes. Cet article explique comment
Grâce aux fonctionnalités de ZITADEL telles que les rôles, les métadonnées et les actions, les utilisateurs peuvent obtenir un contrôle d'accès très détaillé adapté à un environnement de confiance zéro. De plus, ZITADEL peut travailler avec des services d'autorisation externes.
ZITADEL est un
Ses principaux objectifs incluent la fourniture de fonctionnalités clé en main pour l'authentification, l'autorisation, la connexion et l'authentification unique (SSO), tout en permettant la personnalisation via les interfaces utilisateur.
Il est livré avec une piste d'audit complète pour suivre toutes les modifications, permet aux développeurs d'étendre les fonctionnalités avec du code personnalisé (actions), prend en charge des normes largement reconnues telles que OIDC, OAuth, SAML et LDAP, met l'accent sur la facilité d'utilisation et l'évolutivité et propose des API complètes pour intégration polyvalente.
ZITADEL utilise RBAC pour gérer les autorisations des utilisateurs, où les autorisations sont liées aux rôles, et les utilisateurs se voient attribuer ces rôles. Cela simplifie la gestion des accès des utilisateurs en fonction de leurs rôles organisationnels. Une fonctionnalité supplémentaire permet de déléguer des rôles à d'autres organisations, facilitant ainsi le partage d'autorisations avec des entités externes.
Ceci est particulièrement utile pour les organisations interconnectées ou hiérarchiques.
Bien que ces fonctionnalités offrent un contrôle d'accès robuste, elles pourraient ne pas suffire pour les besoins d'autorisation complexes, d'où l'importance d'explorer l'autorisation à granularité fine dans ZITADEL.
ZITADEL améliore le RBAC traditionnel en introduisant sa dynamique
Grâce aux actions de ZITADEL, des scripts post-authentification peuvent être créés pour analyser les attributs spécifiques des utilisateurs et bloquer l'accès si nécessaire.
Les actions peuvent également établir des revendications personnalisées pour renforcer le système ABAC, permettant des modèles d'autorisation avancés qui restreignent l'accès en fonction d'attributs tels que l'emplacement, l'heure ou tout autre facteur définissable.
ZITADEL permet aux administrateurs ou aux développeurs autorisés d'ajouter des métadonnées personnalisées aux utilisateurs et aux organisations, amplifiant ainsi les possibilités de contrôle d'accès plus précis.
Il prend en charge les réclamations agrégées en collectant des données supplémentaires provenant de systèmes externes tels que des outils CRM ou RH. ZITADEL peut également gérer des ressources uniques, telles que des commandes d'expédition ou des appareils IoT, et déterminer l'accès en fonction d'attributs tels que le sous-utilisateur, les rôles, les réclamations, l'adresse IP, etc.
Malgré les fonctionnalités complètes fournies avec ZITADEL, il peut y avoir des cas où une approche plus personnalisée ou plus fine est nécessaire.
Actuellement, le moyen le plus efficace de mettre en œuvre une autorisation précise dans ZITADEL consiste à utiliser une logique d'application personnalisée pour les petits projets ou pour les projets à plus grande échelle, en tirant parti d'un outil tiers disponible tel que warrant.dev , cerbos.dev , etc.
Ces outils peuvent s'intégrer à ZITADEL, améliorant ainsi votre capacité d'autorisation nuancée et précise.
Disons qu'il existe une hypothétique application de salle de rédaction dans une entreprise de médias, qui communique avec une API back-end. Les journalistes l'utilisent pour écrire, tandis que les éditeurs éditent et publient ces articles. Cette API, écrite en Python Flask dans cet exemple, possède des points de terminaison spécifiques et l'accès à ces points de terminaison dépend du rôle de l'utilisateur et de son expérience. Les points finaux :
write_article
: réservé aux journalistes pour écrire.
edit_article
: Juste pour que les éditeurs puissent éditer des articles.
review_articles
: Pour que les journalistes seniors et les rédacteurs intermédiaires et seniors révisent les articles.
publish_article
: Pour les journalistes intermédiaires et seniors et les rédacteurs seniors à publier. En interne, l'API utilise un JWT émis par ZITADEL pour vérifier qui fait des requêtes. Les utilisateurs doivent envoyer un JWT valide dans l'en-tête de leur demande. Ce JWT a été obtenu lorsque l'utilisateur s'est connecté.
Le JWT contient des informations sur l'utilisateur, comme son rôle et son expérience. Ces informations, contenues dans les revendications personnalisées, sont essentielles à ce cas d'utilisation. Le backend décide si l'utilisateur peut accéder à la ressource demandée en fonction de ces informations.
journalist
ou editor
. C’est la clé car cela définit qui obtient quel accès dans notre configuration. Gestion de l'expérience/de l'ancienneté : outre les rôles, l'expérience d'un utilisateur (comme junior
, intermediate
et senior
dans notre exemple) est suivie. Si l'expérience d'un utilisateur change, ZITADEL la met à jour sous forme de métadonnées. Si aucun niveau d'expérience n'est mentionné lorsqu'un utilisateur intègre ZITADEL, le système suppose simplement qu'il est « junior ».
Séparation des préoccupations : lors de la conception de cette API, une attention particulière a été accordée à la séparation nette de la logique métier et des règles de contrôle d'accès. Ceci est crucial pour la maintenabilité et l’évolutivité de l’application. En séparant la logique métier et les règles d’accès, nous obtenons une conception plus propre et modulaire.
Cela nous permet de mettre à jour les actions commerciales et les règles d'accès sans s'affecter mutuellement. Cela augmente la maintenabilité du code et facilite sa gestion à mesure que l'application évolue.
De plus, cette conception rend le système plus sécurisé car les règles d'accès sont séparées de la logique métier principale, réduisant ainsi le risque d'introduction accidentelle de failles de sécurité lors de la modification de la logique métier.
Créez l'organisation Media House, accédez à Projets et créez un nouveau projet appelé Newsroom.
Dans le projet Newsroom, cliquez sur le bouton Nouveau pour créer une nouvelle application.
Accédez à l’onglet Utilisateurs de votre organisation comme indiqué ci-dessous, puis accédez à l’onglet Utilisateurs du service . Nous allons créer des utilisateurs de service dans cette démo. Pour ajouter un utilisateur de service, cliquez sur le bouton Nouveau .
Ensuite, ajoutez les détails de l'utilisateur du service, sélectionnez JWT pour Type de jeton d'accès, puis cliquez sur Créer .
Cliquez sur le bouton Actions dans le coin supérieur droit. Sélectionnez Générer un secret client dans le menu déroulant.
Copiez votre identifiant client et votre secret client. Cliquez sur Fermer .
Vous disposez désormais d’un utilisateur du service, ainsi que de ses informations d’identification client.
Accédez à Autorisations . Cliquez sur Nouveau .
Sélectionnez l'utilisateur et le projet pour lequel l'autorisation doit être créée. Cliquez sur Continuer .
Vous pouvez sélectionner un rôle ici. Sélectionnez le rôle de journaliste pour l'utilisateur actuel. Cliquez sur Enregistrer .
Vous pouvez voir que l'utilisateur du service Lois Lane a désormais le rôle de journaliste dans le projet Newsroom .
Ajoutons maintenant des métadonnées au profil de l'utilisateur pour indiquer son niveau d'ancienneté. Utilisez « experience_level » comme clé et pour sa valeur, choisissez entre « junior », « intermédiaire » ou « senior ».
Bien que nous puissions généralement supposer que ces métadonnées sont définies via un appel API effectué par l'application RH, pour des raisons de simplicité et de facilité de compréhension, nous définirons les métadonnées directement dans la console.
Accédez à Métadonnées . Cliquez sur Modifier .
Fournissez experience_level comme clé et senior comme valeur. Cliquez sur l'icône Enregistrer, puis cliquez sur le bouton Fermer .
L'utilisateur dispose désormais des métadonnées requises associées à son compte.
Vous pouvez également ajouter quelques utilisateurs de service supplémentaires avec différents rôles et niveaux d'expérience (à l'aide de métadonnées) pour tester la démo en utilisant les étapes précédentes.
1. Cliquez sur Actions . Cliquez sur Nouveau pour créer une nouvelle action.
2. Dans la section Créer une action , donnez à l'action le même nom que le nom de la fonction, c'est-à-dire assignRoleAndExperienceClaims. Dans le champ script, copiez/collez le code suivant, puis cliquez sur Ajouter .
function assignRoleAndExperienceClaims(ctx, api) { // Check if grants and metadata exist if (!ctx.v1.user.grants || !ctx.v1.claims['urn:zitadel:iam:user:metadata']) { return; } // Decode experience level from Base64 - metadata is Base64 encoded let experience_encoded = ctx.v1.claims['urn:zitadel:iam:user:metadata'].experience_level; let experience = ''; try { experience = decodeURIComponent(escape(String.fromCharCode.apply(null, experience_encoded.split('').map(function(c) { return '0x' + ('0' + c.charCodeAt(0).toString(16)).slice(-2); })))); } catch (e) { return; // If decoding fails, stop executing the function } // Check if the experience level exists if (!experience) { return; } // Iterate through the user's grants ctx.v1.user.grants.grants.forEach(grant => { // Iterate through the roles of each grant grant.roles.forEach(role => { // Check if the user is a journalist if (role === 'journalist') { // Set custom claims with the user's role and experience level api.v1.claims.setClaim('journalist:experience_level', experience); } // Check if the user is an editor else if (role === 'editor') { // Set custom claims with the user's role and experience level api.v1.claims.setClaim('editor:experience_level', experience); } }); }); }
Désormais, lorsqu'un utilisateur demande un jeton d'accès, l'action sera exécutée, transformant les rôles utilisateur et les métadonnées au format requis et les ajoutant en tant que revendication personnalisée au jeton. Cette revendication personnalisée peut ensuite être utilisée par des applications tierces pour gérer l'accès des utilisateurs de manière plus précise.
Clonez le projet depuis GitHub :
Exécutez la commande ci-dessous pour cloner le projet à partir de ce référentiel GitHub :
git clone https://github.com/zitadel/example-fine-grained-authorization.git
Accédez au répertoire du projet :
Après le clonage, accédez au répertoire du projet avec
cd example-fine-grained-authorization
.
Configurer un environnement Python :
Assurez-vous que Python 3 et pip sont installés. Vous pouvez vérifier cela en exécutant
python3 --version
etpip3 --version
dans votre terminal. Si Python ou pip n'est pas installé, vous devrez les installer.
Ensuite, créez un nouvel environnement virtuel pour ce projet en exécutant
python3 -m venv env
.
Activez l'environnement en exécutant :
.\env\Scripts\activate
source env/bin/activate
Après avoir exécuté cette commande, votre terminal doit indiquer que vous travaillez maintenant dans l'environnement virtuel env.
Installer les dépendances :
Avec le terminal dans le répertoire du projet (celui contenant exigences.txt), exécutez
pip3 install -r requirements.txt
pour installer les dépendances nécessaires.
Configurer les variables d'environnement :
Le projet nécessite certaines variables d'environnement. Remplissez le fichier .env
avec les valeurs que nous avons récupérées de ZITADEL.
PROJECT_ID="<YOUR PROJECT ID>" ZITADEL_DOMAIN="<YOUR INSTANCE DOMAIN eg https://instance-as23uy.zitadel.cloud>" ZITADEL_TOKEN_URL="<YOUR TOKEN URL eg https://instance-as23uy.zitadel.cloud/oauth/v2/token" CLIENT_ID="<YOUR SERVICE USER'S CLIENT ID FROM THE GENERATED CLIENT CREDENTIALS eg sj_Alice>" CLIENT_SECRET="<YOUR SERVICE USER'S SECRET FROM THE GENERATED CLIENT CREDENTIALS"> ZITADEL_INTROSPECTION_URL="<YOUR INTROSPECTION URL eg https://instance-as23uy.zitadel.cloud/oauth/v2/introspect>" API_CLIENT_ID="<THE CLIENT ID OF YOUR API APPLICATION FOR BASIC AUTH eg 324545668690006737@api>" API_CLIENT_SECRET="<THE CLIENT SECRET OF YOUR API APPLICATION FOR BASIC AUTH>"
Exécutez l'application :
L'API Flask (dans app.py
) utilise des jetons JWT et des revendications personnalisées pour un contrôle d'accès précis. Il vérifie le niveau d'expérience de la revendication personnalisée pour les rôles journalist
et editor
à chaque demande, en utilisant ces informations pour décider si l'utilisateur authentifié peut accéder au point de terminaison demandé.
app.py
from flask import Flask, jsonify from auth import token_required from access_control import authorize_access app = Flask(__name__) # Define the /write_article route. @app.route('/write_article', methods=['POST']) @token_required def write_article(): authorization = authorize_access('write_article') if authorization is not True: return authorization # Resource-specific code goes here... return jsonify({"message": "Article written successfully!"}), 200 # Define the /edit_article route. @app.route('/edit_article', methods=['PUT']) @token_required def edit_article(): authorization = authorize_access('edit_article') if authorization is not True: return authorization # Resource-specific code goes here... return jsonify({"message": "Article edited successfully!"}), 200 # Define the /review_article route. @app.route('/review_articles', methods=['GET']) @token_required def review_article(): authorization = authorize_access('review_article') if authorization is not True: return authorization # Resource-specific code goes here... return jsonify({"message": "Article review accessed successfully!"}), 200 # Define the /publish_article route. @app.route('/publish_article', methods=['POST']) @token_required def publish_article(): authorization = authorize_access('publish_article') if authorization is not True: return authorization # Resource-specific code goes here... return jsonify({"message": "Article published successfully!"}), 200 # Add more endpoints as needed... if __name__ == '__main__': app.run(debug=True)
auth.py
import os import jwt import requests from functools import wraps from flask import request, jsonify, g ZITADEL_INTROSPECTION_URL = os.getenv('ZITADEL_INTROSPECTION_URL') API_CLIENT_ID = os.getenv('API_CLIENT_ID') API_CLIENT_SECRET = os.getenv('API_CLIENT_SECRET') # This function checks the token introspection and populates the flask.g variable with the user's token def token_required(f): @wraps(f) def decorated(*args, **kwargs): token = request.headers.get('Authorization') if not token: abort(401) # Return status code 401 for Unauthorized if there's no token else: token = token.split(' ')[1] # The token is in the format "Bearer <token>", we want to extract the actual token # Call the introspection endpoint introspection_response = requests.post( ZITADEL_INTROSPECTION_URL, auth=(API_CLIENT_ID, API_CLIENT_SECRET), data={'token': token} ) if not introspection_response.json().get('active', False): return jsonify({"message": "Invalid token"}), 403 # Decode the token and print it for inspection decoded_token = jwt.decode(token, options={"verify_signature": False}) print(f"\n\n***** Decoded Token: {decoded_token} \n\n******") # Add the decoded token to Flask's global context g.token = decoded_token return f(*args, **kwargs) return decorated
access_control.py (exemple de code simulant un moteur de règles)
import base64 import jwt from flask import g, jsonify # The access_requirements dictionary represents your access control rules. access_requirements = { 'write_article': [{'role': 'journalist', 'experience_level': 'junior'}, {'role': 'journalist', 'experience_level': 'intermediate'}, {'role': 'journalist', 'experience_level': 'senior'}], 'edit_article': [{'role': 'editor', 'experience_level': 'junior'}, {'role': 'editor', 'experience_level': 'intermediate'}, {'role': 'editor', 'experience_level': 'senior'}], 'review_articles': [{'role': 'journalist', 'experience_level': 'senior'}, {'role': 'editor', 'experience_level': 'intermediate'}, {'role': 'editor', 'experience_level': 'senior'}], 'publish_article': [{'role': 'journalist', 'experience_level': 'intermediate'}, {'role': 'journalist', 'experience_level': 'senior'}, {'role': 'editor', 'experience_level': 'senior'}] # Add more endpoints as needed... } # This function checks if the user is authorized to access the given endpoint. def authorize_access(endpoint): # We assume that the token has already been decoded in auth.py decoded_token = g.token # Initialize role and experience_level variables role = None experience_level = None for claim, value in decoded_token.items(): if ':experience_level' in claim: role, _ = claim.split(':') experience_level = base64.b64decode(value).decode('utf-8') break # If there's no role in the token, return an error if not role: return jsonify({"message": "Missing role"}), 403 # If there's a role in the token but no experience level, default the experience level to 'junior' if role and not experience_level: experience_level = 'junior' # If there's no role or experience level in the token, return an error if not role or not experience_level: return jsonify({"message": "Missing role or experience level"}), 403 # Get the requirements for the requested endpoint endpoint_requirements = access_requirements.get(endpoint) # If the endpoint is not in the access control list, return an error if not endpoint_requirements: return jsonify({"message": "Endpoint not found in access control list"}), 403 # Check if the user's role and experience level meet the requirements for the requested endpoint for requirement in endpoint_requirements: required_role = requirement['role'] required_experience_level = requirement['experience_level'] # Experience level hierarchy experience_levels = ['junior', 'intermediate', 'senior'] if role == required_role and experience_levels.index(experience_level) >= experience_levels.index(required_experience_level): return True #return jsonify({"message": "Access denied"}), 403 return jsonify({"message": f"Access denied! You are a {experience_level} {role} and therefore cannot access {endpoint}"}), 403
Exécutez l'application Flask en exécutant :
python3 app.py
Si tout est correctement configuré, votre application Flask devrait maintenant être exécutée.
Ce projet a été développé et testé avec Python 3, alors assurez-vous d'utiliser un interpréteur Python 3.
Assurez-vous d'avoir cloné le référentiel et installé les dépendances nécessaires comme décrit précédemment.
Exécutez le script client_credentials_token_generator.py
pour générer un jeton d'accès.
client_credentials_token_generator.py
import os import requests import base64 from dotenv import load_dotenv load_dotenv() ZITADEL_DOMAIN = os.getenv("ZITADEL_DOMAIN") CLIENT_ID = os.getenv("CLIENT_ID") CLIENT_SECRET = os.getenv("CLIENT_SECRET") ZITADEL_TOKEN_URL = os.getenv("ZITADEL_TOKEN_URL") PROJECT_ID = os.getenv("PROJECT_ID") # Encode the client ID and client secret in Base64 client_credentials = f"{CLIENT_ID}:{CLIENT_SECRET}".encode("utf-8") base64_client_credentials = base64.b64encode(client_credentials).decode("utf-8") # Request an OAuth token from ZITADEL headers = { "Content-Type": "application/x-www-form-urlencoded", "Authorization": f"Basic {base64_client_credentials}" } data = { "grant_type": "client_credentials", "scope": f"openid profile email urn:zitadel:iam:org:project:id:{PROJECT_ID}:aud urn:zitadel:iam:org:projects:roles urn:zitadel:iam:user:metadata" } response = requests.post(ZITADEL_TOKEN_URL, headers=headers, data=data) if response.status_code == 200: access_token = response.json()["access_token"] print(f"Response: {response.json()}") print(f"Access token: {access_token}") else: print(f"Error: {response.status_code} - {response.text}")
Ouvrez votre terminal et accédez au répertoire du projet, puis exécutez le script en utilisant python3 :
python3 client_credentials_token_generator.py
En cas de succès, cela imprimera un jeton d'accès à votre terminal. Il s'agit du jeton que vous utiliserez pour authentifier vos requêtes auprès de l'API.
Si vous n'avez pas démarré l'API Flask plus tôt, exécutez l'API en ouvrant un autre terminal dans le répertoire du projet et en exécutant :
python3 app.py
Le serveur API devrait maintenant être opérationnel et prêt à accepter les demandes.
Désormais, vous pouvez utiliser cURL ou tout autre client HTTP (comme Postman) pour envoyer des requêtes à l'API. N'oubliez pas de remplacer your_access_token
dans les commandes curl par le jeton d'accès que vous avez obtenu à l'étape 2.
Scénario 1 : un éditeur junior tente de modifier un article (succès)
L'utilisateur avec le rôle editor
et le niveau d'expérience junior
tente d'appeler le point de terminaison edit_article
.
curl -H "Authorization: Bearer <your_access_token>" -X POST http://localhost:5000/edit_article
Résultat attendu : {"message": "Article edited successfully"}
Scénario 2 : un éditeur junior tente de publier un article (échec)
L'utilisateur avec le rôle editor
et le niveau d'expérience junior
tente d'appeler le point de terminaison publish_article
.
curl -H "Authorization: Bearer <your_access_token>" -X POST http://localhost:5000/publish_article
Résultat attendu : {"message": "Access denied! You are a junior editor and therefore cannot access publish_article"}
Scénario 3 : Un journaliste principal essaie d'écrire un article (succès)
L'utilisateur avec le rôle journalist
et le niveau d'expérience senior
tente d'appeler le point de terminaison write_article
.
curl -H "Authorization: Bearer <your_access_token>" -X POST http://localhost:5000/write_article
Résultat attendu : {"message": "Article written successfully"}
Scénario 4 : Un jeune journaliste tente de réviser des articles (échec)
L'utilisateur avec le rôle journalist
et le niveau d'expérience « junior » tente d'appeler le point de terminaison review_articles
.
curl -H "Authorization: Bearer <your_access_token>" -X POST http://localhost:5000/review_articles
Résultat attendu : {"message": "Access denied! You are a junior journalist and therefore cannot access review_articles"}
Scénario 5 : le rédacteur en chef essaie de réviser des articles (succès)
L'utilisateur avec le rôle editor
et le niveau d'expérience senior
tente d'accéder au point de terminaison review_articles
.
curl -H "Authorization: Bearer <your_access_token>" -X POST http://localhost:5000/review_articles
Résultat attendu : {"message": "Article reviewed successfully"}
Scénario 6 : Un journaliste intermédiaire tente de publier un article (succès)
L'utilisateur avec le rôle journalist
et le niveau d'expérience intermediate
tente d'accéder au point de terminaison publish_article
.
curl -H "Authorization: Bearer <your_access_token>" -X POST http://localhost:5000/publish_article
{"message": "Article published successfully"}
Dans cet article, nous avons exploré l'importance de passer du RBAC traditionnel à une approche d'autorisation plus détaillée et plus fine utilisant ZITADEL.
Nous avons approfondi ses fonctionnalités telles que les actions dynamiques pour ABAC, la possibilité d'intégration avec des outils tiers, et vu comment ces fonctionnalités peuvent être appliquées pratiquement dans un scénario réel.
À mesure que les exigences en matière de cybersécurité augmentent, des plateformes comme ZITADEL fournissent les solutions nécessaires aux défis d'autorisation complexes.