Skip to content

Commit 9d6879b

Browse files
author
Vadim
committed
Add migration examples: export/import users/groups/sites to CSV
1 parent 953cc7a commit 9d6879b

4 files changed

Lines changed: 364 additions & 0 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""
2+
Export groups with members to CSV — group structure for migration
3+
mapping and planning.
4+
5+
Exports all groups with their membership list, ownership, group
6+
type, visibility, and creation date. Useful for:
7+
- Mapping group structure during tenant-to-tenant migration
8+
- Membership audit and cleanup
9+
- Security group review
10+
11+
Requires delegated permission ``Group.Read.All``,
12+
``Directory.Read.All``.
13+
14+
https://learn.microsoft.com/en-us/graph/api/group-list
15+
"""
16+
17+
import csv
18+
import sys
19+
20+
from office365.graph_client import GraphClient
21+
from tests import test_client_id, test_client_secret, test_tenant
22+
23+
OUTPUT_FILE = "/tmp/groups_export.csv"
24+
25+
26+
def main():
27+
client = GraphClient(tenant=test_tenant).with_client_secret(test_client_id, test_client_secret)
28+
29+
print("Fetching groups...")
30+
groups = client.groups.get_all().execute_query()
31+
print(f" Found {len(groups)} groups\n")
32+
33+
with open(OUTPUT_FILE, "w", newline="") as f:
34+
writer = csv.writer(f)
35+
writer.writerow([
36+
"groupId", "displayName", "mail", "groupType", "visibility",
37+
"description", "createdDateTime", "memberCount",
38+
"members (UPN)", "owners (UPN)",
39+
])
40+
41+
for g in groups:
42+
gid = g.id or "?"
43+
display = g.display_name or "(unnamed)"
44+
mail = g.mail or ""
45+
gtype = "Microsoft 365" if g.mail_enabled and not g.security_enabled else \
46+
"Security" if g.security_enabled else "Distribution"
47+
visibility = g.visibility or "?"
48+
desc = (g.description or "")[:80]
49+
created = g.created_date_time.strftime("%Y-%m-%d") if g.created_date_time else ""
50+
51+
# Load members and owners
52+
member_upns = []
53+
owner_upns = []
54+
55+
try:
56+
members = g.members.get().execute_query()
57+
member_upns = [m.user_principal_name for m in members if hasattr(m, "user_principal_name")][:20]
58+
except Exception:
59+
pass
60+
61+
try:
62+
owners = g.owners.get().execute_query()
63+
owner_upns = [o.user_principal_name for o in owners if hasattr(o, "user_principal_name")]
64+
except Exception:
65+
pass
66+
67+
writer.writerow([
68+
gid, display, mail, gtype, visibility,
69+
desc, created, len(member_upns),
70+
";".join(member_upns) if member_upns else "",
71+
";".join(owner_upns) if owner_upns else "",
72+
])
73+
74+
print(f" {display:40s} type={gtype:15s} members={len(member_upns):>4} owners={len(owner_upns)}")
75+
76+
print(f"\n✓ Exported {len(groups)} groups to {OUTPUT_FILE}")
77+
78+
79+
if __name__ == "__main__":
80+
main()
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""
2+
Export SharePoint sites to CSV — site inventory for migration
3+
planning and tenant management.
4+
5+
Exports all SharePoint Online sites with URL, title, template,
6+
owner, storage usage/quota, compatibility level, hub association,
7+
and last content modified date.
8+
9+
Essential for:
10+
- SharePoint migration planning (source inventory)
11+
- Tenant consolidation or split
12+
- Storage governance and legacy site identification
13+
14+
Requires delegated permission ``Sites.Read.All``.
15+
16+
https://learn.microsoft.com/en-us/sharepoint/dev/apis/rest-api/tenant/GetSitePropertiesFromSharePoint
17+
"""
18+
19+
import csv
20+
import sys
21+
22+
from office365.sharepoint.client_context import ClientContext
23+
from office365.sharepoint.tenant.administration.tenant import Tenant
24+
from tests import test_admin_site_url, test_client_id, test_client_secret, test_tenant
25+
26+
OUTPUT_FILE = "/tmp/sharepoint_sites_export.csv"
27+
_KB = 1024
28+
29+
30+
def format_mb(mb_value: int) -> str:
31+
if mb_value >= _KB:
32+
return f"{mb_value / _KB:.1f} GB"
33+
return f"{mb_value} MB"
34+
35+
36+
def main():
37+
ctx = ClientContext(test_admin_site_url).with_client_secret(test_tenant, test_client_id, test_client_secret)
38+
tenant = Tenant(ctx)
39+
40+
print("Fetching SharePoint sites...")
41+
sites = tenant.get_site_properties_from_sharepoint().execute_query()
42+
print(f" Found {len(sites)} sites\n")
43+
44+
with open(OUTPUT_FILE, "w", newline="") as f:
45+
writer = csv.writer(f)
46+
writer.writerow([
47+
"url", "title", "template", "owner", "storageQuotaMB",
48+
"storageUsageMB", "storageUsagePct", "compatibilityLevel",
49+
"sharingCapability", "lockState", "lastContentModifiedDate",
50+
"hubSite", "createdDate", "websCount",
51+
])
52+
53+
for site in sites:
54+
url = getattr(site, "url", "?")
55+
title = getattr(site, "title", "(untitled)")
56+
template = getattr(site, "template", "?")
57+
owner = getattr(site, "owner_login_name", "?") or getattr(site, "owner", "?")
58+
quota = getattr(site, "storage_quota", 0) or 0
59+
usage = getattr(site, "storage_usage_current", 0) or 0
60+
pct = round((usage / quota) * 100, 1) if quota > 0 else 0
61+
compat = getattr(site, "compatibility_level", "?")
62+
sharing = getattr(site, "sharing_capability", "?")
63+
lock = getattr(site, "lock_state", "")
64+
last_mod = getattr(site, "last_content_modified_date", "")
65+
if hasattr(last_mod, "strftime"):
66+
last_mod = last_mod.strftime("%Y-%m-%d")
67+
created = getattr(site, "created_time", "")
68+
if hasattr(created, "strftime"):
69+
created = created.strftime("%Y-%m-%d")
70+
is_hub = getattr(site, "is_hub_site", False)
71+
webs = getattr(site, "webs_count", "?")
72+
73+
writer.writerow([url, title, template, owner, quota, usage, pct, compat, sharing, lock, last_mod, is_hub, created, webs])
74+
print(f" {title[:40]:40s} {format_mb(usage):>10s} / {format_mb(quota):>10s} ({pct}%)")
75+
76+
print(f"\n✓ Exported {len(sites)} sites to {OUTPUT_FILE}")
77+
78+
79+
if __name__ == "__main__":
80+
main()
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""
2+
Export users to CSV — full user inventory for migration planning.
3+
4+
Exports key user properties to a CSV file for integration with
5+
migration tools, HR systems, or audit reporting.
6+
7+
Fields: UPN, display name, given name, surname, department, job
8+
title, office location, city, country, mobile/phone, enabled
9+
status, mail, MFA registered, creation date, last sign-in date,
10+
assigned license SKUs.
11+
12+
Requires delegated permission ``User.Read.All``,
13+
``Organization.Read.All``, ``AuditLog.Read.All`` (for last sign-in),
14+
``UserAuthenticationMethod.Read.All`` (for MFA status).
15+
16+
https://learn.microsoft.com/en-us/graph/api/user-list
17+
"""
18+
19+
import csv
20+
import sys
21+
22+
from office365.graph_client import GraphClient
23+
from tests import test_client_id, test_client_secret, test_tenant
24+
25+
OUTPUT_FILE = "/tmp/users_export.csv"
26+
FIELDS = [
27+
"userPrincipalName", "displayName", "givenName", "surname",
28+
"department", "jobTitle", "officeLocation", "city", "country",
29+
"mobilePhone", "businessPhones", "mail", "accountEnabled",
30+
"userType", "createdDateTime", "signInActivity",
31+
"assignedLicenses",
32+
]
33+
34+
35+
def get_license_skus(client: GraphClient) -> dict:
36+
"""Map SKU IDs to human-readable names."""
37+
skus = client.subscribed_skus.get().execute_query()
38+
return {s.sku_id: s.sku_part_number for s in skus if hasattr(s, "sku_id") and hasattr(s, "sku_part_number")}
39+
40+
41+
def get_mfa_status(client: GraphClient) -> dict[str, dict]:
42+
"""Get MFA and passwordless registration status per user."""
43+
try:
44+
details = client.reports.authentication_methods.user_registration_details.get().execute_query()
45+
return {d.user_principal_name.upper(): d for d in details}
46+
except Exception:
47+
return {}
48+
49+
50+
def main():
51+
client = GraphClient(tenant=test_tenant).with_client_secret(test_client_id, test_client_secret)
52+
53+
print("Fetching users...")
54+
users = client.users.get_all().execute_query()
55+
print(f" Found {len(users)} users")
56+
57+
sku_map = get_license_skus(client)
58+
mfa_map = get_mfa_status(client)
59+
print(f" Resolved {len(sku_map)} license SKUs")
60+
61+
with open(OUTPUT_FILE, "w", newline="") as f:
62+
writer = csv.DictWriter(f, fieldnames=FIELDS + ["licenseSkus", "mfaRegistered", "passwordlessCapable", "lastSignIn"])
63+
writer.writeheader()
64+
65+
for u in users:
66+
upn = u.user_principal_name or ""
67+
mfa = mfa_map.get(upn.upper())
68+
last_signin = u.properties.get("signInActivity")
69+
if hasattr(last_signin, "last_sign_in_date_time"):
70+
last_signin = last_signin.last_sign_in_date_time
71+
else:
72+
last_signin = u.properties.get("signInActivity/lastSignInDateTime", "")
73+
74+
licenses = []
75+
if u.assigned_licenses:
76+
for lic in u.assigned_licenses:
77+
sku_id = lic.properties.get("skuId", "")
78+
licenses.append(sku_map.get(sku_id, sku_id))
79+
80+
writer.writerow({
81+
"userPrincipalName": upn,
82+
"displayName": u.display_name or "",
83+
"givenName": u.given_name or "",
84+
"surname": u.surname or "",
85+
"department": u.department or "",
86+
"jobTitle": u.job_title or "",
87+
"officeLocation": u.office_location or "",
88+
"city": u.city or "",
89+
"country": u.country or "",
90+
"mobilePhone": u.mobile_phone or "",
91+
"businessPhones": ", ".join(u.business_phones) if u.business_phones else "",
92+
"mail": u.mail or "",
93+
"accountEnabled": u.account_enabled,
94+
"userType": u.user_type or "",
95+
"createdDateTime": u.created_date_time.strftime("%Y-%m-%d") if u.created_date_time else "",
96+
"signInActivity": str(last_signin)[:10] if last_signin else "",
97+
"assignedLicenses": ";".join(licenses) if licenses else "",
98+
"licenseSkus": ";".join(licenses) if licenses else "",
99+
"mfaRegistered": str(mfa.is_mfa_registered) if mfa else "",
100+
"passwordlessCapable": str(mfa.is_passwordless_capable) if mfa else "",
101+
"lastSignIn": str(last_signin)[:10] if last_signin else "",
102+
})
103+
104+
print(f"\n✓ Exported {len(users)} users to {OUTPUT_FILE}")
105+
106+
107+
if __name__ == "__main__":
108+
main()
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""
2+
Import users from CSV — bulk provision users from a structured file.
3+
4+
Reads a CSV with user properties and creates each user in Entra ID.
5+
Useful for:
6+
- Tenant-to-tenant migration user provisioning
7+
- HR system integration (new hire provisioning)
8+
- Bulk user creation for testing or staging environments
9+
10+
CSV format:
11+
userPrincipalName,givenName,surname,displayName,department,jobTitle,
12+
officeLocation,city,country,mobilePhone,mail,password,accountEnabled
13+
14+
Requires delegated permission ``User.ReadWrite.All``.
15+
16+
https://learn.microsoft.com/en-us/graph/api/user-post-users
17+
"""
18+
19+
import csv
20+
import sys
21+
from io import StringIO
22+
23+
from office365.graph_client import GraphClient
24+
from tests import create_unique_name, test_client_id, test_client_secret, test_tenant
25+
26+
SAMPLE_CSV = """userPrincipalName,givenName,surname,displayName,department,jobTitle,officeLocation,city,country,mobilePhone,mail,password,accountEnabled
27+
user1@contoso.com,John,Doe,John Doe,Engineering,Developer,Seattle,Seattle,USA,+1-555-0101,,TempP@ss123,True
28+
user2@contoso.com,Jane,Smith,Jane Smith,Marketing,Manager,New York,New York,USA,+1-555-0102,,TempP@ss456,True
29+
user3@contoso.com,Bob,Johnson,Bob Johnson,Sales,Representative,Chicago,Chicago,USA,+1-555-0103,,TempP@ss789,True
30+
"""
31+
32+
33+
def import_users_from_csv(csv_content: str) -> list[dict]:
34+
"""Import users from CSV content.
35+
36+
Returns list of result dicts with status per user.
37+
"""
38+
client = GraphClient(tenant=test_tenant).with_client_secret(test_client_id, test_client_secret)
39+
reader = csv.DictReader(StringIO(csv_content))
40+
results = []
41+
42+
for row in reader:
43+
upn = row["userPrincipalName"].strip()
44+
password = row.get("password", "").strip() or create_unique_name("P@ssw0rd")
45+
46+
try:
47+
# Build user profile
48+
user = client.users.add_with_password(
49+
userPrincipalName=upn,
50+
givenName=row.get("givenName", "").strip(),
51+
surname=row.get("surname", "").strip(),
52+
displayName=row.get("displayName", "").strip() or row.get("givenName", "").strip(),
53+
department=row.get("department", "").strip(),
54+
jobTitle=row.get("jobTitle", "").strip(),
55+
officeLocation=row.get("officeLocation", "").strip(),
56+
city=row.get("city", "").strip(),
57+
country=row.get("country", "").strip(),
58+
mobilePhone=row.get("mobilePhone", "").strip(),
59+
mail=row.get("mail", "").strip() or upn,
60+
password=password,
61+
accountEnabled=row.get("accountEnabled", "True").strip().lower() == "true",
62+
).execute_query()
63+
64+
results.append({"user": upn, "status": "success", "id": user.id})
65+
except Exception as e:
66+
results.append({"user": upn, "status": "failed", "error": str(e)})
67+
68+
return results
69+
70+
71+
def main():
72+
print("Importing users from CSV\n")
73+
74+
# In production, read from a file:
75+
# with open("users.csv") as f:
76+
# csv_content = f.read()
77+
csv_content = SAMPLE_CSV
78+
79+
results = import_users_from_csv(csv_content)
80+
81+
success = [r for r in results if r["status"] == "success"]
82+
failed = [r for r in results if r["status"] == "failed"]
83+
84+
print(f"Total: {len(results)}")
85+
print(f"Created: {len(success)}")
86+
print(f"Failed: {len(failed)}\n")
87+
88+
for r in failed:
89+
print(f" ✗ {r['user']}: {r.get('error', 'Unknown')}")
90+
91+
for r in success:
92+
print(f" ✓ {r['user']} (id: {r['id'][:20]}...)")
93+
94+
95+
if __name__ == "__main__":
96+
main()

0 commit comments

Comments
 (0)