Problem:
Wenn ich versuche, Benutzer mithilfe von abzurufen API-Endpunkt https://vssps.dev.azure.com/{organizati ... -preview.1, ich stoße auf diesen Fehler:
Code: Select all
ERROR:__main__:Error fetching users: Expecting value: line 3 column 1 (char 4)
The raw response indicates an empty or malformed response from the API.
Code: Select all
WARNING:__main__:User not found in Azure DevOps: john.doe@example.com. Check casing and presence in Azure DevOps.
Was ich habe Versucht:
Normalisierte E-Mails: Konvertierte sowohl die CSV-Datei als auch die abgerufene „mailAddress“ zum Vergleich in Kleinbuchstaben.
Fallback auf „principalName“: Verwendet „principalName“ als sekundäre Kennung, wenn „mailAddress“ nicht vorhanden war gefunden.
Protokollierung: Protokolliert alle abgerufenen Benutzer und ihre Attribute zum Debuggen:
Code: Select all
logger.debug(f"Fetched users: {[user.get('mailAddress') for user in users]}")
The log shows no users being fetched (Fetched 0 users from Azure DevOps).
Das Personal Access Token (PAT) verfügt über die Bereiche Graph (Lesen) und Benutzerberechtigungen verwalten.
Das PAT funktioniert für andere API-Aufrufe in Azure DevOps.
Skript-Highlights:
Hier ist die Funktion zum Abrufen von Benutzern:
Code: Select all
def fetch_all_users():
users = []
continuation_token = None
while True:
url = f"{BASE_URL}/graph/users?api-version=7.1-preview.1"
if continuation_token:
url += f"&continuationToken={continuation_token}"
try:
response = requests.get(url, headers=HEADERS, timeout=30)
logger.debug(f"Raw response: {response.status_code}, Content: {response.text}")
response.raise_for_status()
data = response.json()
users.extend(data.get("value", []))
continuation_token = data.get("continuationToken")
if not continuation_token:
break
except Exception as e:
logger.error(f"Error fetching users: {e}")
break
logger.info(f"Fetched {len(users)} users from Azure DevOps.")
return users
Organisations-URL: https://dev.azure.com/myorganization
Basis-API-URL: https://vssps.dev.azure.com /myorganization/_apis
API-Version: 7.1-preview.1
Python-Version: 3.10
Fragen:
Warum funktioniert /graph/users Gibt der Endpunkt eine leere oder fehlerhafte Antwort zurück? Gibt es einen anderen Endpunkt, den ich verwenden sollte?
Gibt es eine Möglichkeit, Benutzer mit E-Mail-Abgleich ohne Berücksichtigung der Groß-/Kleinschreibung in Azure DevOps zuverlässig abzurufen?
Könnte dies mit Einstellungen auf Organisationsebene in Azure DevOps zusammenhängen, die den Zugriff einschränken zu Benutzerdaten?
Wie kann ich dies weiter debuggen, um sicherzustellen, dass die Benutzer korrekt abgerufen und zugeordnet werden?
Für Hinweise oder Vorschläge wäre ich sehr dankbar! Lassen Sie mich wissen, wenn weitere Details benötigt werden.
Hier ist das gesamte Skript:
Code: Select all
import os
import csv
import requests
import logging
import time
from requests.exceptions import RequestException
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
ORGANIZATION = "company"
PAT = "your_actual_pat_value"
CSV_FILE_PATH = "...Report.csv"
BASE_URL = "https://vssps.dev.azure.com/company/_apis"
HEADERS = {
"Content-Type": "application/json",
"Authorization": f"Basic {requests.auth._basic_auth_str('', PAT)}"
}
def validate_csv_columns(required_columns, csv_reader):
missing_columns = [col for col in required_columns if col not in csv_reader.fieldnames]
if missing_columns:
raise ValueError(f"CSV is missing required columns: {missing_columns}. Found: {csv_reader.fieldnames}")
def fetch_all_users():
users = []
continuation_token = None
while True:
url = f"{BASE_URL}/graph/users?api-version=7.1-preview.1"
if continuation_token:
url += f"&continuationToken={continuation_token}"
try:
response = requests.get(url, headers=HEADERS, timeout=30)
logger.debug(f"Raw response: {response.status_code}, Content: {response.text}")
response.raise_for_status()
data = response.json()
users.extend(data.get("value", []))
continuation_token = data.get("continuationToken")
if not continuation_token:
break
except RequestException as e:
logger.error(f"Error fetching users: {e}")
break
except ValueError as ve:
logger.error(f"Malformed JSON response: {ve}")
break
logger.info(f"Fetched {len(users)} users from Azure DevOps.")
logger.debug(f"Fetched users: {[user.get('mailAddress') for user in users]}")
return users
def get_user_descriptor_map():
users = fetch_all_users()
user_map = defaultdict(str)
for user in users:
email = user.get("mailAddress", "").strip()
principal_name = user.get("principalName", "").strip()
descriptor = user.get("descriptor")
if email:
user_map[email.lower()] = descriptor
if principal_name and principal_name.lower() not in user_map:
user_map[principal_name.lower()] = descriptor
return user_map
def remove_direct_assignment(user_descriptor):
url = f"{BASE_URL}/graph/memberships/{user_descriptor}?api-version=7.1-preview.1"
try:
response = requests.delete(url, headers=HEADERS)
response.raise_for_status()
if response.status_code == 204:
logger.info(f"Successfully removed direct assignment for user: {user_descriptor}")
except RequestException as e:
logger.error(f"Error removing direct assignment for {user_descriptor}: {e}")
def process_users_in_parallel(user_descriptors):
with ThreadPoolExecutor(max_workers=10) as executor:
executor.map(remove_direct_assignment, user_descriptors)
def process_csv():
try:
user_map = get_user_descriptor_map()
with open(CSV_FILE_PATH, mode='r') as file:
csv_reader = csv.DictReader(file)
validate_csv_columns(["UserEmail"], csv_reader)
user_descriptors = []
for row in csv_reader:
email = row.get("UserEmail", "").strip().lower()
if not email:
logger.warning("Skipping row with missing UserEmail field")
continue
logger.info(f"Processing user: {email}")
user_descriptor = user_map.get(email)
if user_descriptor:
user_descriptors.append(user_descriptor)
else:
logger.warning(f"User not found in Azure DevOps: {email}. Check casing and presence in Azure DevOps.")
process_users_in_parallel(user_descriptors)
except FileNotFoundError:
logger.error(f"CSV file not found: {CSV_FILE_PATH}")
except ValueError as ve:
logger.error(f"CSV validation error: {ve}")
except Exception as e:
logger.error(f"Unexpected error: {e}")
if __name__ == "__main__":
try:
logger.info("Starting the process to remove direct assignments.")
process_csv()
logger.info("Script completed successfully.")
import sys
sys.exit(0)
except Exception as e:
logger.error(f"Script terminated with errors: {e}")
sys.exit(1)