mirror of
https://github.com/open-webui/oikb.git
synced 2026-07-09 16:00:53 +00:00
feat: SharePoint certificate-based authentication (#3)
Add SHAREPOINT_CERTIFICATE_PATH as a secure alternative to SHAREPOINT_CLIENT_SECRET for authenticating with SharePoint via Microsoft Graph API. - JWT assertion flow using cryptography + PyJWT - SHAREPOINT_CERTIFICATE_PASSWORD for encrypted PEM keys - Mutual exclusivity validation (secret vs cert) - New optional extra: pip install oikb[sharepoint-cert] Closes #3
This commit is contained in:
parent
04ccb30d72
commit
dcf241da7f
4 changed files with 212 additions and 16 deletions
|
|
@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## [0.3.6] - 2026-05-28
|
||||
|
||||
### Added
|
||||
|
||||
- **SharePoint**: certificate-based authentication via `SHAREPOINT_CERTIFICATE_PATH` env var — more secure alternative to client secret auth. Supports encrypted PEM keys with `SHAREPOINT_CERTIFICATE_PASSWORD`. Requires `pip install oikb[sharepoint-cert]`.
|
||||
|
||||
## [0.3.5] - 2025-05-21
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ A complete guide to syncing content into Open WebUI Knowledge Bases.
|
|||
- [GitLab / Bitbucket](#gitlab--bitbucket)
|
||||
- [Confluence](#confluence)
|
||||
- [Cloud Storage (S3 / GCS / Azure)](#cloud-storage-s3--gcs--azure)
|
||||
- [SharePoint](#sharepoint)
|
||||
- [All Connectors](#all-connectors)
|
||||
- [Filtering](#filtering)
|
||||
- [Include / Exclude Globs](#include--exclude-globs)
|
||||
|
|
@ -258,6 +259,61 @@ oikb sync azure://container/prefix --kb-id your-kb-id
|
|||
|
||||
Uses standard cloud SDK credentials (AWS profiles, service accounts, etc.).
|
||||
|
||||
### SharePoint
|
||||
|
||||
Sync a SharePoint document library:
|
||||
|
||||
```bash
|
||||
oikb sync sharepoint:mysite.sharepoint.com --kb-id your-kb-id
|
||||
oikb sync sharepoint:mysite.sharepoint.com/Documents --kb-id your-kb-id
|
||||
```
|
||||
|
||||
SharePoint supports two authentication methods:
|
||||
|
||||
#### Client Secret (simpler setup)
|
||||
|
||||
Set these environment variables:
|
||||
|
||||
```bash
|
||||
export SHAREPOINT_TENANT_ID=your-tenant-id
|
||||
export SHAREPOINT_CLIENT_ID=your-client-id
|
||||
export SHAREPOINT_CLIENT_SECRET=your-client-secret
|
||||
```
|
||||
|
||||
#### Certificate Auth (recommended for production)
|
||||
|
||||
Certificate-based auth is more secure and follows Microsoft best practices for production environments.
|
||||
|
||||
First install the required extras:
|
||||
|
||||
```bash
|
||||
pip install oikb[sharepoint-cert]
|
||||
```
|
||||
|
||||
Then set:
|
||||
|
||||
```bash
|
||||
export SHAREPOINT_TENANT_ID=your-tenant-id
|
||||
export SHAREPOINT_CLIENT_ID=your-client-id
|
||||
export SHAREPOINT_CERTIFICATE_PATH=/path/to/cert.pem
|
||||
# Optional — only needed if the PEM key is encrypted:
|
||||
export SHAREPOINT_CERTIFICATE_PASSWORD=your-password
|
||||
```
|
||||
|
||||
The PEM file must contain both the private key and the certificate. The connector extracts the thumbprint and signs a JWT assertion automatically.
|
||||
|
||||
> **Note:** `SHAREPOINT_CLIENT_SECRET` and `SHAREPOINT_CERTIFICATE_PATH` are mutually exclusive — set one or the other.
|
||||
|
||||
#### .oikb.yaml example
|
||||
|
||||
```yaml
|
||||
sources:
|
||||
- name: legal-docs
|
||||
source: sharepoint:contoso.sharepoint.com/Legal
|
||||
kb-id: abc123
|
||||
interval: 1h
|
||||
```
|
||||
|
||||
### All Connectors
|
||||
|
||||
44 connectors available. See the full list:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "oikb"
|
||||
version = "0.3.5"
|
||||
version = "0.3.6"
|
||||
description = "Sync anything to Open WebUI Knowledge Bases"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
|
|
@ -31,6 +31,7 @@ gmail = ["google-api-python-client>=2.100", "google-auth>=2.25"]
|
|||
gsites = ["google-api-python-client>=2.100", "google-auth>=2.25"]
|
||||
web = ["beautifulsoup4>=4.12"]
|
||||
oracle = ["oci"]
|
||||
sharepoint-cert = ["cryptography>=42.0", "PyJWT>=2.8"]
|
||||
all = [
|
||||
"boto3>=1.34",
|
||||
"google-cloud-storage>=2.14",
|
||||
|
|
@ -40,6 +41,8 @@ all = [
|
|||
"google-auth>=2.25",
|
||||
"beautifulsoup4>=4.12",
|
||||
"oci",
|
||||
"cryptography>=42.0",
|
||||
"PyJWT>=2.8",
|
||||
]
|
||||
dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "respx>=0.21"]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,22 @@
|
|||
"""SharePoint connector — sync a document library to a Knowledge Base.
|
||||
|
||||
Uses Microsoft Graph API. Auth via SHAREPOINT_TENANT_ID, SHAREPOINT_CLIENT_ID,
|
||||
SHAREPOINT_CLIENT_SECRET env vars (app-only auth).
|
||||
and one of:
|
||||
- SHAREPOINT_CLIENT_SECRET (client secret auth)
|
||||
- SHAREPOINT_CERTIFICATE_PATH (certificate auth — more secure, recommended
|
||||
for production). Optionally set SHAREPOINT_CERTIFICATE_PASSWORD for
|
||||
encrypted PEM keys.
|
||||
|
||||
The two auth methods are mutually exclusive.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
|
@ -24,6 +34,8 @@ class SharePointConnector(BaseConnector):
|
|||
tenant_id: str | None = None,
|
||||
client_id: str | None = None,
|
||||
client_secret: str | None = None,
|
||||
certificate_path: str | None = None,
|
||||
certificate_password: str | None = None,
|
||||
):
|
||||
self.site = site
|
||||
self.library = library
|
||||
|
|
@ -31,25 +43,44 @@ class SharePointConnector(BaseConnector):
|
|||
tid = tenant_id or os.environ.get("SHAREPOINT_TENANT_ID", "")
|
||||
cid = client_id or os.environ.get("SHAREPOINT_CLIENT_ID", "")
|
||||
secret = client_secret or os.environ.get("SHAREPOINT_CLIENT_SECRET", "")
|
||||
cert_path = certificate_path or os.environ.get("SHAREPOINT_CERTIFICATE_PATH", "")
|
||||
cert_password = certificate_password or os.environ.get("SHAREPOINT_CERTIFICATE_PASSWORD", "")
|
||||
|
||||
if not all([tid, cid, secret]):
|
||||
if not tid or not cid:
|
||||
raise ValueError(
|
||||
"SharePoint credentials required. Set env vars:\n"
|
||||
" SHAREPOINT_TENANT_ID, SHAREPOINT_CLIENT_ID, SHAREPOINT_CLIENT_SECRET"
|
||||
" SHAREPOINT_TENANT_ID, SHAREPOINT_CLIENT_ID, and either\n"
|
||||
" SHAREPOINT_CLIENT_SECRET or SHAREPOINT_CERTIFICATE_PATH"
|
||||
)
|
||||
|
||||
# Get access token.
|
||||
token_resp = httpx.post(
|
||||
f"https://login.microsoftonline.com/{tid}/oauth2/v2.0/token",
|
||||
data={
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": cid,
|
||||
"client_secret": secret,
|
||||
"scope": "https://graph.microsoft.com/.default",
|
||||
},
|
||||
)
|
||||
token_resp.raise_for_status()
|
||||
access_token = token_resp.json()["access_token"]
|
||||
if secret and cert_path:
|
||||
raise ValueError(
|
||||
"SHAREPOINT_CLIENT_SECRET and SHAREPOINT_CERTIFICATE_PATH are "
|
||||
"mutually exclusive. Set one or the other, not both."
|
||||
)
|
||||
|
||||
if not secret and not cert_path:
|
||||
raise ValueError(
|
||||
"SharePoint auth method required. Set one of:\n"
|
||||
" SHAREPOINT_CLIENT_SECRET (client secret)\n"
|
||||
" SHAREPOINT_CERTIFICATE_PATH (certificate)"
|
||||
)
|
||||
|
||||
token_url = f"https://login.microsoftonline.com/{tid}/oauth2/v2.0/token"
|
||||
|
||||
if cert_path:
|
||||
access_token = _get_token_via_certificate(
|
||||
token_url=token_url,
|
||||
client_id=cid,
|
||||
certificate_path=cert_path,
|
||||
certificate_password=cert_password or None,
|
||||
)
|
||||
else:
|
||||
access_token = _get_token_via_secret(
|
||||
token_url=token_url,
|
||||
client_id=cid,
|
||||
client_secret=secret,
|
||||
)
|
||||
|
||||
self._http = httpx.Client(
|
||||
base_url="https://graph.microsoft.com/v1.0",
|
||||
|
|
@ -109,6 +140,106 @@ class SharePointConnector(BaseConnector):
|
|||
self._http.close()
|
||||
|
||||
|
||||
# ── Auth helpers ────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _get_token_via_secret(token_url: str, client_id: str, client_secret: str) -> str:
|
||||
"""Obtain an access token using client ID + client secret."""
|
||||
token_resp = httpx.post(
|
||||
token_url,
|
||||
data={
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"scope": "https://graph.microsoft.com/.default",
|
||||
},
|
||||
)
|
||||
token_resp.raise_for_status()
|
||||
return token_resp.json()["access_token"]
|
||||
|
||||
|
||||
def _get_token_via_certificate(
|
||||
token_url: str,
|
||||
client_id: str,
|
||||
certificate_path: str,
|
||||
certificate_password: str | None = None,
|
||||
) -> str:
|
||||
"""Obtain an access token using client ID + certificate (JWT assertion).
|
||||
|
||||
Reads a PEM file that contains both the private key and the certificate.
|
||||
Builds a signed JWT assertion per the Microsoft identity platform spec:
|
||||
https://learn.microsoft.com/en-us/entra/identity-platform/certificate-credentials
|
||||
"""
|
||||
try:
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Certificate auth requires the 'cryptography' package.\n"
|
||||
"Install it with: pip install oikb[sharepoint-cert]"
|
||||
)
|
||||
|
||||
try:
|
||||
import jwt
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Certificate auth requires the 'PyJWT' package.\n"
|
||||
"Install it with: pip install oikb[sharepoint-cert]"
|
||||
)
|
||||
|
||||
# Load PEM file.
|
||||
pem_path = os.path.expanduser(certificate_path)
|
||||
if not os.path.isfile(pem_path):
|
||||
raise FileNotFoundError(f"Certificate file not found: {pem_path}")
|
||||
|
||||
with open(pem_path, "rb") as f:
|
||||
pem_data = f.read()
|
||||
|
||||
password_bytes = certificate_password.encode() if certificate_password else None
|
||||
|
||||
# Load private key.
|
||||
private_key = serialization.load_pem_private_key(pem_data, password=password_bytes)
|
||||
|
||||
# Load certificate to extract thumbprint.
|
||||
cert = x509.load_pem_x509_certificate(pem_data)
|
||||
thumbprint = cert.fingerprint(cert.signature_hash_algorithm or x509.hashes.SHA256())
|
||||
x5t = base64.urlsafe_b64encode(thumbprint).rstrip(b"=").decode("ascii")
|
||||
|
||||
# Build JWT assertion.
|
||||
now = int(time.time())
|
||||
claims = {
|
||||
"aud": token_url,
|
||||
"iss": client_id,
|
||||
"sub": client_id,
|
||||
"jti": str(uuid.uuid4()),
|
||||
"iat": now,
|
||||
"nbf": now,
|
||||
"exp": now + 600, # 10 minute validity
|
||||
}
|
||||
headers = {
|
||||
"x5t": x5t,
|
||||
}
|
||||
|
||||
assertion = jwt.encode(claims, private_key, algorithm="RS256", headers=headers)
|
||||
|
||||
# Exchange assertion for access token.
|
||||
token_resp = httpx.post(
|
||||
token_url,
|
||||
data={
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": client_id,
|
||||
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
|
||||
"client_assertion": assertion,
|
||||
"scope": "https://graph.microsoft.com/.default",
|
||||
},
|
||||
)
|
||||
token_resp.raise_for_status()
|
||||
return token_resp.json()["access_token"]
|
||||
|
||||
|
||||
# ── Source parser ───────────────────────────────────────────────
|
||||
|
||||
|
||||
def parse_sharepoint_source(source: str) -> dict[str, str | None]:
|
||||
"""Parse sharepoint:site/library or sharepoint:site."""
|
||||
source = source.removeprefix("sharepoint:")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue