01Resolve a SharePoint site id from its hostname and pathVerified
GET /sites/{hostname}:/{server-relative-path} returns the site resource for a site you only know by URL. Every later call for its drives and lists uses the id from this response.
- Split the site URL into the hostname (example.sharepoint.com) and the server-relative path (/sites/docs)
- Passing "hostname:/path" to by_site_id() in msgraph-sdk percent-encodes ":" and "/", which changes the documented URL shape (checked by printing the request URL). Build the documented URL and call it through with_url()
- Print the site's id, displayName and webUrl, and keep the id in the SITE_ID environment variable
- The least privileged permission is delegated Sites.Read.All. To limit an app to one site, use Sites.Selected (separate tip)
"""Resolve a SharePoint site id from its URL (hostname + server-relative path)."""
import asyncio
import os
from urllib.parse import urlparse
from azure.identity import DeviceCodeCredential
from msgraph import GraphServiceClient
from msgraph.generated.sites.item.site_item_request_builder import SiteItemRequestBuilder
SCOPES = ["Sites.Read.All"]
SITE_URL = os.environ.get("SITE_URL", "https://example.sharepoint.com/sites/docs")
def site_path_url(site_url):
# by_site_id() percent-encodes ":" and "/", so build the documented URL shape
u = urlparse(site_url)
return "https://graph.microsoft.com/v1.0/sites/%s:%s" % (u.hostname, u.path.rstrip("/"))
async def main():
cred = DeviceCodeCredential(client_id=os.environ["CLIENT_ID"], tenant_id=os.environ["TENANT_ID"])
client = GraphServiceClient(credentials=cred, scopes=SCOPES)
builder = SiteItemRequestBuilder(client.request_adapter, site_path_url(SITE_URL))
site = await builder.get()
print("id:", site.id)
print("name:", site.display_name)
print("webUrl:", site.web_url)
if __name__ == "__main__":
asyncio.run(main())Output(2026-09-12)
https://graph.microsoft.com/v1.0/sites/example.sharepoint.com:/sites/docsUsing msgraph-sdk and DeviceCodeCredential, write Python that takes a SharePoint site URL, splits out the hostname and path, calls GET /sites/{hostname}:/{path}, and prints the site id and webUrl. by_site_id encodes ":", so build the URL and call it with with_url.
Write the path exactly as it appears in the URL. If the site does not exist or you lack access, Graph returns an error, so confirm you got an id before moving on. Sites.Read.All covers documents and list items in every site the signed-in user can read. Pass the id through an environment variable instead of hard-coding it.
- Availability
- Microsoft Graph v1.0. Least privileged permission is Sites.Read.All for both delegated (work or school account) and application. Personal Microsoft accounts are not supported. Uses msgraph-sdk and azure-identity.
- Requires
- msgraph-sdk 1.62.0, azure-identity 1.25.3
- Permissions
- Sites.Read.All (delegated)
- Tested
- run up to the sign-in step (not connected to Microsoft 365); aggregation/checking logic run on sample data (2026-09-12, Python 3.12.10 (venv) / Windows 11, msgraph-sdk 1.62.0, azure-identity 1.25.3)
- Practical edition
- SharePoint / OneDrive ── Use SharePoint site agents to find internal documents
- Source
- Microsoft Learn, "Get a site resource by path"
Microsoft Learn, "Get a SharePoint Site" - Verified
- 2026-09-12 (v1)
Supporting passages from the sources
In addition to retrieving a site by ID you can retrieve a site based on server-relative URL path.
If you have the server-relative URL for a site resource, you can construct a request as follows:
02List a site's document libraries and the items in one folderVerified
GET /sites/{siteId}/drives lists a site's document libraries (drives). Pick one by name, then list a folder's children with $select so only the needed properties come back, following @odata.nextLink across pages.
- Get the drives of the SITE_ID site and pick the drive whose name matches the library
- Address the folder by path: build /drives/{drive-id}/root:/{path}:/children and call it with with_url(), because by_drive_item_id() percent-encodes ":" and "/"
- Use $select for name, size, lastModifiedDateTime, webUrl, folder and file to keep responses small
- While odata_next_link is present, call that URL with with_url() to get the next page
- Tell folders from files by whether folder or file is set, and write out_children.csv
"""List a site's document libraries, then every item in one folder of a library."""
import asyncio
import csv
import os
from urllib.parse import quote
from azure.identity import DeviceCodeCredential
from msgraph import GraphServiceClient
SCOPES = ["Files.Read.All"]
LIBRARY = os.environ.get("LIBRARY_NAME", "Documents")
FOLDER_PATH = os.environ.get("FOLDER_PATH", "Reports")
SELECT = "name,size,lastModifiedDateTime,webUrl,folder,file"
def to_row(item):
kind = "folder" if item.folder else "file"
return [kind, item.name, item.size or 0, item.last_modified_date_time, item.web_url]
async def main():
cred = DeviceCodeCredential(client_id=os.environ["CLIENT_ID"], tenant_id=os.environ["TENANT_ID"])
client = GraphServiceClient(credentials=cred, scopes=SCOPES)
drives = await client.sites.by_site_id(os.environ["SITE_ID"]).drives.get()
drive = next((d for d in drives.value or [] if d.name == LIBRARY), None)
if drive is None:
print("library not found:", LIBRARY, [d.name for d in drives.value or []])
return
# documented path form; the SDK would encode ":" inside an item id
url = "https://graph.microsoft.com/v1.0/drives/%s/root:/%s:/children?$select=%s" % (
drive.id, quote(FOLDER_PATH), SELECT)
builder = client.drives.by_drive_id(drive.id).items.by_drive_item_id("root").children
page = await builder.with_url(url).get()
rows = []
while page:
rows += [to_row(i) for i in page.value or []]
if not page.odata_next_link:
break
page = await builder.with_url(page.odata_next_link).get()
with open("out_children.csv", "w", encoding="utf-8-sig", newline="") as f:
w = csv.writer(f)
w.writerow(["kind", "name", "size", "modified", "webUrl"])
w.writerows(rows)
print("items:", len(rows))
if __name__ == "__main__":
asyncio.run(main())Output(2026-09-12)
['folder', '2026', 0, None, None]
['file', 'a.docx', 1200, None, None]With msgraph-sdk, write Python that lists the drives of the SITE_ID site, picks the library by name, reads every page of a folder's children (by path, via the root:/{path}:/children URL) with $select, and writes a CSV.
The API table lists delegated Files.Read as least privileged, but its description is reading the signed-in user's files. For site libraries this tip uses Files.Read.All, described as reading all files the signed-in user can access. Large folders mean many calls, so narrow the scope.
- Availability
- Microsoft Graph v1.0. Delegated Files.Read.All (documented as not requiring admin consent). Uses msgraph-sdk and azure-identity.
- Requires
- msgraph-sdk 1.62.0, azure-identity 1.25.3
- Permissions
- Files.Read.All (delegated)
- Tested
- run up to the sign-in step (not connected to Microsoft 365); aggregation/checking logic run on sample data (2026-09-12, Python 3.12.10 (venv) / Windows 11, msgraph-sdk 1.62.0, azure-identity 1.25.3)
- Practical edition
- Working with OneDrive ── Scope Copilot to a SharePoint library or folder
- Source
- Microsoft Learn, "List available drives"
Microsoft Learn, "List children of a driveItem"
Microsoft Learn, "Microsoft Graph permissions reference" - Verified
- 2026-09-12 (v1)
Supporting passages from the sources
To list the document libraries for a site, your app requests the drives relationship on the Site.
GET https://graph.microsoft.com/v1.0/drives/{drive-id}/root:/{path-relative-to-root}:/children
Allows the app to read all files the signed-in user can access.
Allows the app to read the signed-in user's files.
03Download a file's content from a document libraryVerified
GET /drives/{drive-id}/items/{item-id}/content returns the file's primary stream. The response is a 302 redirect to a preauthenticated download URL; content.get() in msgraph-sdk returns bytes, which you write to a new local file.
- Identify the item with DRIVE_ID and ITEM_ID; get item ids from a children or delta listing
- First read name, size and file with $select, and stop if file is missing (a folder, for example). Only items with the file property can be downloaded
- Write the bytes from content.get() to a new file name with an out_ prefix, never overwriting an existing file
- Keep only the file name, dropping any path parts, so nothing is written outside the target folder
- For ranged downloads of large files, send the Range header to the @microsoft.graph.downloadUrl URL, not to the /content request
"""Download one file from a drive to a new local file (never overwrites)."""
import asyncio
import os
from pathlib import Path
from azure.identity import DeviceCodeCredential
from kiota_abstractions.base_request_configuration import RequestConfiguration
from msgraph import GraphServiceClient
from msgraph.generated.drives.item.items.item.drive_item_item_request_builder import (
DriveItemItemRequestBuilder,
)
SCOPES = ["Files.Read.All"]
def target_path(name, folder="."):
# keep only the file name and add an out_ prefix; refuse to overwrite
path = Path(folder) / ("out_" + Path(name).name)
if path.exists():
raise FileExistsError(path)
return path
async def main():
cred = DeviceCodeCredential(client_id=os.environ["CLIENT_ID"], tenant_id=os.environ["TENANT_ID"])
client = GraphServiceClient(credentials=cred, scopes=SCOPES)
item_rb = client.drives.by_drive_id(os.environ["DRIVE_ID"]).items.by_drive_item_id(os.environ["ITEM_ID"])
query = DriveItemItemRequestBuilder.DriveItemItemRequestBuilderGetQueryParameters(
select=["name", "size", "file"])
item = await item_rb.get(request_configuration=RequestConfiguration(query_parameters=query))
if item.file is None:
print("not a file:", item.name)
return
data = await item_rb.content.get()
path = target_path(item.name)
path.write_bytes(data)
print("saved", path, len(data), "bytes")
if __name__ == "__main__":
asyncio.run(main())Output(2026-09-12)
out_報告書.docxWith msgraph-sdk, write Python that checks name and file (via $select) for the DRIVE_ID/ITEM_ID item, downloads it with content.get(), and saves it under an out_-prefixed name. Stop on folders and never overwrite an existing file.
This copies file content to your machine, so follow sensitivity labels and your organization's rules on moving data. The preauthenticated URL returns the content without a token, so keep it out of logs and chats. The local test blocks the network, so following the 302 was not checked against a live response.
- Availability
- Microsoft Graph v1.0. Delegated Files.Read.All. Uses msgraph-sdk and azure-identity.
- Requires
- msgraph-sdk 1.62.0, azure-identity 1.25.3
- Permissions
- Files.Read.All (delegated)
- Tested
- run up to the sign-in step (not connected to Microsoft 365); aggregation/checking logic run on sample data (2026-09-12, Python 3.12.10 (venv) / Windows 11, msgraph-sdk 1.62.0, azure-identity 1.25.3)
- Source
- Microsoft Learn, "Download driveItem content"
- Verified
- 2026-09-12 (v1)
Supporting passages from the sources
Returns a 302 Found response redirecting to a preauthenticated download URL for the file, which is the same URL available through the @microsoft.graph.downloadUrl property on the driveItem.
Only driveItem objects with the file property can be downloaded.
You must append the Range header to the actual @microsoft.graph.downloadUrl URL and not to the request for /content.
04Read list items with column values via expand=fields(select=...)Verified
Adding expand=fields(select=Col1,Col2) to GET /sites/{site-id}/lists/{list-id}/items returns only the named column values in each item's fields. In SharePoint every file is also a list item, so library columns can be read the same way.
- Pass fields(select=FileLeafRef,DocId,Status,ReviewDue,Owner) to expand, using the API-facing column name rather than the display name
- Column values land in ListItem.fields.additional_data (a dict), so read them by column name
- Results can span several pages, so follow odata_next_link
- When narrowing with $filter, use indexed columns; only one indexed field can be filtered at a time
- Hidden column values are not returned by default; name those columns in select when you need them
"""Read list items with selected column values and write them to CSV."""
import asyncio
import csv
import os
from azure.identity import DeviceCodeCredential
from kiota_abstractions.base_request_configuration import RequestConfiguration
from msgraph import GraphServiceClient
from msgraph.generated.sites.item.lists.item.items.items_request_builder import ItemsRequestBuilder
SCOPES = ["Sites.Read.All"]
COLUMNS = ["FileLeafRef", "DocId", "Status", "ReviewDue", "Owner"]
def flatten(item_id, web_url, fields):
row = {"id": item_id, "webUrl": web_url}
for c in COLUMNS:
row[c] = fields.get(c)
return row
async def main():
cred = DeviceCodeCredential(client_id=os.environ["CLIENT_ID"], tenant_id=os.environ["TENANT_ID"])
client = GraphServiceClient(credentials=cred, scopes=SCOPES)
items_rb = client.sites.by_site_id(os.environ["SITE_ID"]).lists.by_list_id(os.environ["LIST_ID"]).items
query = ItemsRequestBuilder.ItemsRequestBuilderGetQueryParameters(
expand=["fields(select=%s)" % ",".join(COLUMNS)])
page = await items_rb.get(request_configuration=RequestConfiguration(query_parameters=query))
rows = []
while page:
for it in page.value or []:
fields = it.fields.additional_data if it.fields else {}
rows.append(flatten(it.id, it.web_url, fields))
if not page.odata_next_link:
break
page = await items_rb.with_url(page.odata_next_link).get()
with open("out_list_items.csv", "w", encoding="utf-8-sig", newline="") as f:
w = csv.DictWriter(f, fieldnames=["id", "webUrl"] + COLUMNS)
w.writeheader()
w.writerows(rows)
print("rows:", len(rows))
if __name__ == "__main__":
asyncio.run(main())Output(2026-09-12)
15 R-2026-001 NoneWith msgraph-sdk, write Python that reads every page of the SITE_ID/LIST_ID list items with expand=fields(select=FileLeafRef,DocId,Status,ReviewDue,Owner) and writes item id, webUrl and the column values to a CSV. Read values from fields.additional_data.
If a column name in select is wrong, that column is simply missing from the result; check the API names with the list's columns (separate tip). Sites.Read.All reaches every site you can read, so consider Sites.Selected to confine an app to one site. Decide where exported CSVs with people's names are stored and for how long.
- Availability
- Microsoft Graph v1.0. Least privileged permission is Sites.Read.All for delegated and application. Personal Microsoft accounts are not supported.
- Requires
- msgraph-sdk 1.62.0, azure-identity 1.25.3
- Permissions
- Sites.Read.All (delegated)
- Tested
- run up to the sign-in step (not connected to Microsoft 365); aggregation/checking logic run on sample data (2026-09-12, Python 3.12.10 (venv) / Windows 11, msgraph-sdk 1.62.0, azure-identity 1.25.3)
- Practical edition
- Working with OneDrive ── Scope Copilot to a SharePoint library or folder
- Source
- Microsoft Learn, "List items"
Microsoft Learn, "columnDefinition resource type"
Microsoft Learn, "Overview of Selected permissions in OneDrive and SharePoint" - Verified
- 2026-09-12 (v1)
Supporting passages from the sources
GET /sites/{site-id}/lists/{list-id}/items?expand=fields(select=Column1,Column2)
When filtering on indexed fields, the service can only filter one indexed field at a time.
To list hidden field values on listItems, include the desired columns by name in your $select statement.
Within SharePoint, all files are list items, but all list items are not files.
05Fetch only what changed since the last run with a drive delta queryVerified
Following GET /drives/{drive-id}/root/delta to its last page returns an @odata.deltaLink. Save it to a file and call that URL on the next run to get only what was added, changed or deleted since then.
- On the first run call /drives/{drive-id}/root/delta and follow @odata.nextLink while it is returned
- Save the @odata.deltaLink from the last page to out_delta_state.json and reuse the URL as is, without parsing it
- On the next run call the saved deltaLink with with_url(); remove items that carry the deleted facet from your local record
- If the service answers 410 Gone, drop the saved state and start again with a full enumeration
- To track changes from now on without reading everything, request token=latest to get just a deltaLink
"""Track changes in a drive: save the deltaLink and resume from it on the next run."""
import asyncio
import json
import os
from pathlib import Path
from azure.identity import DeviceCodeCredential
from kiota_abstractions.api_error import APIError
from msgraph import GraphServiceClient
SCOPES = ["Files.Read.All"]
STATE = Path(os.environ.get("DELTA_STATE", "out_delta_state.json"))
def first_link(drive_id):
return "https://graph.microsoft.com/v1.0/drives/%s/root/delta" % drive_id
def load_link(drive_id):
if STATE.exists():
return json.loads(STATE.read_text(encoding="utf-8"))["deltaLink"]
return first_link(drive_id)
def summarize(items):
changed = [i["name"] for i in items if not i["deleted"]]
removed = [i["id"] for i in items if i["deleted"]]
return changed, removed
async def read_all(builder, link):
items, page = [], await builder.with_url(link).get()
while True:
items += [{"id": i.id, "name": i.name, "deleted": i.deleted is not None} for i in page.value or []]
if not page.odata_next_link:
return items, page.odata_delta_link
page = await builder.with_url(page.odata_next_link).get()
async def main():
drive_id = os.environ["DRIVE_ID"]
cred = DeviceCodeCredential(client_id=os.environ["CLIENT_ID"], tenant_id=os.environ["TENANT_ID"])
client = GraphServiceClient(credentials=cred, scopes=SCOPES)
builder = client.drives.by_drive_id(drive_id).items.by_drive_item_id("root").delta
try:
items, delta_link = await read_all(builder, load_link(drive_id))
except APIError as e:
if e.response_status_code != 410:
raise
# the saved token can no longer be used: enumerate everything again
items, delta_link = await read_all(builder, first_link(drive_id))
changed, removed = summarize(items)
print("changed:", len(changed), "deleted:", len(removed))
for name in changed[:10]:
print(" ", name)
STATE.write_text(json.dumps({"deltaLink": delta_link}), encoding="utf-8")
if __name__ == "__main__":
asyncio.run(main())Output(2026-09-12)
(['a.docx'], ['2'])
delta_link_attr TrueWith msgraph-sdk, write Python that runs a delta query on the DRIVE_ID drive, follows nextLink to the end, saves the deltaLink to a JSON file, and resumes from it next time. On 410, restart with a full enumeration, and print how many items were deleted.
Deltas include deletions, so remember to remove them locally. Do not save the deltaLink when a run fails midway, so the next run reads the same range again. After a 410 you must re-read everything and reconcile it with your local record.
- Availability
- Microsoft Graph v1.0. Delegated Files.Read.All (the API table lists Files.Read as least privileged). OneDrive and SharePoint support token=latest.
- Requires
- msgraph-sdk 1.62.0, azure-identity 1.25.3
- Permissions
- Files.Read.All (delegated)
- Tested
- run up to the sign-in step (not connected to Microsoft 365); aggregation/checking logic run on sample data (2026-09-12, Python 3.12.10 (venv) / Windows 11, msgraph-sdk 1.62.0, azure-identity 1.25.3)
- Source
- Microsoft Learn, "driveItem: delta"
Microsoft Learn, "Use delta query to track changes in Microsoft Graph data" - Verified
- 2026-09-12 (v1)
Supporting passages from the sources
To check for changes in the future, call delta again with the @odata.deltaLink from the previous response.
In these cases the service returns an HTTP 410 Gone error with an error response containing one of the error codes below, and a Location header containing a new nextLink that starts a fresh delta enumeration from scratch.
you can copy and apply the @odata.nextLink or @odata.deltaLink URL to the next delta function call without having to inspect the contents of the URL, including its state token.
To retrieve the latest deltaLink, call delta with a query string parameter ?token=latest.
06Confine an app to assigned sites with Sites.SelectedVerified
Consenting to Sites.Selected alone grants no access. Access to a site starts only after a role such as read is assigned to the app with POST /sites/{siteId}/permissions. It works in both application and delegated modes.
- Add Sites.Selected (application or delegated) to the app in Entra ID and consent. At this point the app can read no site
- On the site side, call POST /sites/{siteId}/permissions with roles (read, write, owner or fullcontrol) and the app in grantedToIdentities. Making this assignment requires Sites.FullControl.All
- For app-only runs, get the token with a certificate (CertificateCredential) locally or a managed identity on Azure, using the .default scope
- In delegated mode the app can use only what both the app and the signed-in user are allowed
- To stop access, delete the site assignment or revoke the consent in Entra ID; either one blocks the app
"""App-only read of a site that was assigned to this app through Sites.Selected."""
import asyncio
import os
from azure.identity import CertificateCredential, ManagedIdentityCredential
from msgraph import GraphServiceClient
# app-only tokens use .default; the app registration holds Sites.Selected
SCOPES = ["https://graph.microsoft.com/.default"]
def make_credential():
cert = os.environ.get("CERT_PATH", "")
if cert and os.path.exists(cert):
return CertificateCredential(
tenant_id=os.environ["TENANT_ID"], client_id=os.environ["CLIENT_ID"], certificate_path=cert)
# on Azure (Functions, VM) use a managed identity instead of a certificate file
return ManagedIdentityCredential(client_id=os.environ.get("AZURE_CLIENT_ID"))
def describe(site_name, lists):
lines = ["site: %s" % site_name]
lines += [" list: %s %s" % (name, url) for name, url in lists]
return "\n".join(lines)
async def main():
client = GraphServiceClient(credentials=make_credential(), scopes=SCOPES)
site_rb = client.sites.by_site_id(os.environ["SITE_ID"])
site = await site_rb.get()
lists = await site_rb.lists.get()
print(describe(site.display_name, [(x.display_name, x.web_url) for x in lists.value or []]))
if __name__ == "__main__":
asyncio.run(main())Output(2026-09-12)
site: docs
list: 文書ライブラリ https://example.sharepoint.com/sites/docs/Shared%20Documents
ManagedIdentityCredentialWrite msgraph-sdk Python for an app granted through Sites.Selected that prints the SITE_ID site name and its lists. Use CertificateCredential when the CERT_PATH certificate exists, otherwise ManagedIdentityCredential, with the https://graph.microsoft.com/.default scope.
The docs call app-only tokens higher risk because no user is present, and prefer delegated when possible. Sites.FullControl.All, needed for the assignment, is powerful, so an administrator makes the assignment and the reading app never holds it. Assigning to lists or files breaks inheritance, so watch the limits on unique permissions.
- Availability
- Microsoft Graph v1.0. Sites.Selected exists for application and delegated (application requires admin consent). The site assignments are configured in SharePoint Online.
- Requires
- msgraph-sdk 1.62.0, azure-identity 1.25.3
- Permissions
- Sites.Selected (application)
- Tested
- run up to the sign-in step (not connected to Microsoft 365); aggregation/checking logic run on sample data (2026-09-12, Python 3.12.10 (venv) / Windows 11, msgraph-sdk 1.62.0, azure-identity 1.25.3)
- Practical edition
- Grounding Copilot in Your Stored Content ── Copilot surfaces only data you can at least view
- Source
- Microsoft Learn, "Overview of Selected permissions in OneDrive and SharePoint"
Microsoft Learn, "Microsoft Graph permissions reference" - Verified
- 2026-09-12 (v1)
Supporting passages from the sources
Selected scopes require an explicit assignment action; an application consented for Lists.SelectedOperations.Selected would initially have no access.
Now, lists, list items, folders, and files are also supported, and all Selected scopes now support delegated and application modes.
In the delegated scenario, both the application and user permissions are calculated and then intersected
Because you can grant full control permissions to a site collection by using Sites.Selected, this requirement is necessarily high.
Application only scenarios have no user present and are considered higher risk.
The specific site collections and the permissions granted will be configured in SharePoint Online.
07Find anyone and organization-wide sharing links on files in a folderVerified
In GET /drives/{drive-id}/items/{item-id}/permissions, entries with a link are sharing links: scope anonymous means anyone can use it, organization means anyone signed in to the same tenant. Walk a folder's files and write the broad links to a CSV.
- Collect file ids and names from the folder's children; permissions cannot be expanded in a listing, so call it per file
- Get each file's permissions and keep only entries that have a link
- Treat scope anonymous or organization as broad, and record link.type, roles and expirationDateTime with it
- Run as the file owner; a non-owner only gets the sharing permissions that apply to them
- Review the CSV with the owners and remove unneeded links through a separate process (this code removes nothing)
"""List anyone/organization sharing links on the files of one folder (read only)."""
import asyncio
import csv
import os
from urllib.parse import quote
from azure.identity import DeviceCodeCredential
from msgraph import GraphServiceClient
SCOPES = ["Files.Read.All"]
FOLDER_PATH = os.environ.get("FOLDER_PATH", "Reports")
BROAD = {"anonymous", "organization"}
def broad_links(file_name, perms):
out = []
for p in perms:
link = p.get("link") or {}
if link.get("scope") in BROAD:
out.append([file_name, link["scope"], link.get("type"), ",".join(p.get("roles") or []),
p.get("expirationDateTime") or ""])
return out
def as_dict(p):
link = {"scope": p.link.scope, "type": p.link.type} if p.link else None
exp = p.expiration_date_time.isoformat() if p.expiration_date_time else ""
return {"roles": p.roles, "link": link, "expirationDateTime": exp}
async def main():
cred = DeviceCodeCredential(client_id=os.environ["CLIENT_ID"], tenant_id=os.environ["TENANT_ID"])
client = GraphServiceClient(credentials=cred, scopes=SCOPES)
drive_rb = client.drives.by_drive_id(os.environ["DRIVE_ID"])
url = "https://graph.microsoft.com/v1.0/drives/%s/root:/%s:/children?$select=id,name,file" % (
os.environ["DRIVE_ID"], quote(FOLDER_PATH))
children_rb = drive_rb.items.by_drive_item_id("root").children
page, files = await children_rb.with_url(url).get(), []
while page:
files += [i for i in page.value or [] if i.file]
if not page.odata_next_link:
break
page = await children_rb.with_url(page.odata_next_link).get()
rows = []
for f in files:
perms = await drive_rb.items.by_drive_item_id(f.id).permissions.get()
rows += broad_links(f.name, [as_dict(p) for p in perms.value or []])
with open("out_broad_links.csv", "w", encoding="utf-8-sig", newline="") as fh:
w = csv.writer(fh)
w.writerow(["file", "scope", "type", "roles", "expires"])
w.writerows(rows)
print("files:", len(files), "broad links:", len(rows))
if __name__ == "__main__":
asyncio.run(main())Output(2026-09-12)
broad: 2
['提案書.docx', 'anonymous', 'view', 'read', '']
['提案書.docx', 'organization', 'edit', 'write', '2026-12-31']With msgraph-sdk, write Python that fetches permissions for each file under FOLDER_PATH in the DRIVE_ID drive and writes only sharing links whose link.scope is anonymous or organization to a CSV (file, scope, type, roles, expiration). Put the check in its own function.
Non-owner callers see only sharing that applies to them, so an empty result does not prove a file is safe. OneDrive for Business and SharePoint libraries do not return inheritedFrom, so this output cannot tell you whether a permission came from a parent. Use the admin reports for tenant-wide oversharing.
- Availability
- Microsoft Graph v1.0. Delegated Files.Read.All (the API table lists Files.Read as least privileged).
- Requires
- msgraph-sdk 1.62.0, azure-identity 1.25.3
- Permissions
- Files.Read.All (delegated)
- Tested
- run up to the sign-in step (not connected to Microsoft 365); aggregation/checking logic run on sample data (2026-09-12, Python 3.12.10 (venv) / Windows 11, msgraph-sdk 1.62.0, azure-identity 1.25.3)
- Practical edition
- Working with OneDrive ── Your sharing settings define what Copilot can surface
Grounding Copilot in Your Stored Content ── Find oversharing with data access governance reports - Source
- Microsoft Learn, "sharingLink resource type"
Microsoft Learn, "List sharing permissions on a driveItem"
Microsoft Learn, "permission resource type" - Verified
- 2026-09-12 (v1)
Supporting passages from the sources
Value anonymous indicates the link is usable by anyone, organization indicates the link is only usable for users signed into the same tenant.
For a non-owner caller, only the sharing permissions that apply to the caller are returned.
The permissions relationship of DriveItem cannot be expanded as part of a call to get DriveItem or a collection of DriveItems.
OneDrive for Business and SharePoint document libraries don't return the inheritedFrom property.
08Audit library columns for empty required values and past-due datesVerified
Read GET .../lists/{list-id}/columns to find columns with required set to true, then compare each item's fields to catch empty values. A date column earlier than the reference date counts as past due. The checks live in audit(), tested with sample JSON.
- Read the columns and collect the name of each column where required is true and hidden and readOnly are false; name is the key used in fields
- Read items with expand=fields(select=...) for just the required and date columns
- audit() separates items with a missing or empty required value from items whose ReviewDue is before the reference date
- Pass the reference date as an argument, and use a fixed date in tests so results do not change day to day
- Write out_column_audit.csv and ask the owners to check it; the code does not change any values
"""Audit a library: empty required columns and past-due dates (report only)."""
import asyncio
import csv
import datetime
import os
from azure.identity import DeviceCodeCredential
from kiota_abstractions.base_request_configuration import RequestConfiguration
from msgraph import GraphServiceClient
from msgraph.generated.sites.item.lists.item.items.items_request_builder import ItemsRequestBuilder
SCOPES = ["Sites.Read.All"]
DATE_COL = os.environ.get("DATE_COLUMN", "ReviewDue")
def audit(items, required, date_col, today):
rows = []
for it in items:
f = it.get("fields") or {}
name = f.get("FileLeafRef", it.get("id"))
for col in required:
if f.get(col) in (None, ""):
rows.append([it.get("id"), name, "missing:" + col, it.get("webUrl")])
due = f.get(date_col)
if due and str(due)[:10] < today:
rows.append([it.get("id"), name, "overdue", it.get("webUrl")])
return rows
async def fetch(client, site_id, list_id):
list_rb = client.sites.by_site_id(site_id).lists.by_list_id(list_id)
cols = await list_rb.columns.get()
required = [c.name for c in cols.value or [] if c.required and not c.hidden and not c.read_only]
select = ",".join(["FileLeafRef", DATE_COL] + required)
query = ItemsRequestBuilder.ItemsRequestBuilderGetQueryParameters(expand=["fields(select=%s)" % select])
page = await list_rb.items.get(request_configuration=RequestConfiguration(query_parameters=query))
items = []
while page:
items += [{"id": i.id, "webUrl": i.web_url, "fields": i.fields.additional_data if i.fields else {}}
for i in page.value or []]
if not page.odata_next_link:
break
page = await list_rb.items.with_url(page.odata_next_link).get()
return items, required
async def main():
cred = DeviceCodeCredential(client_id=os.environ["CLIENT_ID"], tenant_id=os.environ["TENANT_ID"])
client = GraphServiceClient(credentials=cred, scopes=SCOPES)
items, required = await fetch(client, os.environ["SITE_ID"], os.environ["LIST_ID"])
rows = audit(items, required, DATE_COL, datetime.date.today().isoformat())
with open("out_column_audit.csv", "w", encoding="utf-8-sig", newline="") as f:
w = csv.writer(f)
w.writerow(["id", "file", "problem", "webUrl"])
w.writerows(rows)
print("required:", required, "problems:", len(rows))
if __name__ == "__main__":
asyncio.run(main())Output(2026-09-12)
missing: 14 overdue: 2
['1', '文書01.pdf', 'missing:Owner', 'https://example.sharepoint.com/sites/docs/Shared%20Documents/01.pdf']
['2', '文書02.pdf', 'missing:Status', 'https://example.sharepoint.com/sites/docs/Shared%20Documents/02.pdf']
['2', '文書02.pdf', 'overdue', 'https://example.sharepoint.com/sites/docs/Shared%20Documents/02.pdf']
['5', '文書05.pdf', 'missing:DocId', 'https://example.sharepoint.com/sites/docs/Shared%20Documents/05.pdf']Write a Python audit(items, required, date_col, today) that takes Graph listItem JSON and reports items with empty required columns and items whose ReviewDue is before the reference date. Get required columns from the list's columns, and keep the msgraph-sdk calls in a separate function.
required describes the column; it does not mean existing items actually hold a value. Dates are compared as ISO strings, so align the comparison for columns that include time or time zone. If owner names are exported, decide where the file is kept and who sees it. Have a person review the result before notifying anyone.
- Availability
- Microsoft Graph v1.0. Sites.Read.All is the least privileged permission for reading both columns and items.
- Requires
- msgraph-sdk 1.62.0, azure-identity 1.25.3
- Permissions
- Sites.Read.All (delegated)
- Tested
- run up to the sign-in step (not connected to Microsoft 365); aggregation/checking logic run on sample data (2026-09-12, Python 3.12.10 (venv) / Windows 11, msgraph-sdk 1.62.0, azure-identity 1.25.3)
- Practical edition
- Working with OneDrive ── Scope Copilot to a SharePoint library or folder
- Source
- Microsoft Learn, "columnDefinition resource type"
Microsoft Learn, "List columnDefinitions in a list"
Microsoft Learn, "List items" - Verified
- 2026-09-12 (v1)
Supporting passages from the sources
Specifies whether the column value isn't optional.
The API-facing name of the column as it appears in the fields on a listItem.
GET /sites/{site-id}/lists/{list-id}/columns