credentials do hlavicky + crypto + docu
This commit is contained in:
@@ -1,29 +1,185 @@
|
|||||||
# Microsoft 365 Service
|
# Microsoft 365 Service
|
||||||
|
|
||||||
FastAPI service for server-to-server communication with Microsoft 365 through Microsoft Graph.
|
FastAPI služba pro server-to-server komunikaci s Microsoft 365 přes Microsoft Graph.
|
||||||
|
|
||||||
## Configuration
|
## Konfigurace
|
||||||
|
|
||||||
Create an app registration in Microsoft Entra ID, grant the required Microsoft Graph application permissions, and expose these environment variables:
|
V Microsoft Entra ID vytvořte app registration, přidělte požadovaná aplikační oprávnění pro Microsoft Graph a nastavte tyto environment proměnné:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
MS365_TENANT_ID=00000000-0000-0000-0000-000000000000
|
MS365_TENANT_ID=00000000-0000-0000-0000-000000000000
|
||||||
MS365_CLIENT_ID=00000000-0000-0000-0000-000000000000
|
MS365_CLIENT_ID=00000000-0000-0000-0000-000000000000
|
||||||
MS365_CLIENT_SECRET=secret-value
|
MS365_CLIENT_SECRET=secret-value
|
||||||
|
MS365_CREDENTIAL_ENCODING_SECRET=shared-secret-for-header-credentials
|
||||||
MS365_GRAPH_BASE_URL=https://graph.microsoft.com/v1.0
|
MS365_GRAPH_BASE_URL=https://graph.microsoft.com/v1.0
|
||||||
MS365_GRAPH_SCOPE=https://graph.microsoft.com/.default
|
MS365_GRAPH_SCOPE=https://graph.microsoft.com/.default
|
||||||
MS365_REQUEST_TIMEOUT_SECONDS=30
|
MS365_REQUEST_TIMEOUT_SECONDS=30
|
||||||
```
|
```
|
||||||
|
|
||||||
The service uses the OAuth2 client credentials flow, so Microsoft Graph permissions must be application permissions approved by an administrator.
|
Služba používá OAuth2 client credentials flow, takže oprávnění pro Microsoft Graph musí být aplikační oprávnění schválená administrátorem.
|
||||||
|
|
||||||
## Implemented Services
|
## Credentials v request hlavičkách
|
||||||
|
|
||||||
- Users: list and read users.
|
Pokud se credentials předávají pro každý request v hlavičkách místo environment proměnných, použijte vlastní HTTP hlavičky s prefixem `X-`. Názvy hlaviček musí odpovídat vzoru `X-MS365-(NAME)` a hodnoty hlaviček musí obsahovat zakódované credentials, ne plaintext secrety.
|
||||||
- Mail: list messages and send email, including file attachments.
|
|
||||||
- Calendar: list and create events, including Teams online meetings.
|
Doporučené hlavičky:
|
||||||
- OneDrive: list root files and upload small files.
|
|
||||||
- Groups and Teams: list groups and list team channels.
|
```http
|
||||||
|
X-MS365-Tenant-Id: <encoded MS365_TENANT_ID>
|
||||||
|
X-MS365-Client-Id: <encoded MS365_CLIENT_ID>
|
||||||
|
X-MS365-Client-Secret: <encoded MS365_CLIENT_SECRET>
|
||||||
|
X-MS365-Credential-Version: v1
|
||||||
|
```
|
||||||
|
|
||||||
|
Tyto hlavičky nesou stejné Microsoft Entra aplikační credentials, které by jinak byly nastavené přes `MS365_TENANT_ID`, `MS365_CLIENT_ID` a `MS365_CLIENT_SECRET`. Rozdíl je pouze ve způsobu přenosu: každá hodnota je před vložením do HTTP hlavičky zašifrovaná.
|
||||||
|
|
||||||
|
Zakódovaná hodnota musí být službou replikovatelně zpracovatelná, ale nesmí být čitelná bez sdíleného secretu, který zná volající i služba. Nepoužívejte samotné Base64, URL encoding, ROT encoding ani jinou reverzibilní obfuskaci bez tajného klíče.
|
||||||
|
|
||||||
|
Doporučený formát zakódované hodnoty:
|
||||||
|
|
||||||
|
```text
|
||||||
|
v1.<base64url nonce>.<base64url ciphertext-and-auth-tag>
|
||||||
|
```
|
||||||
|
|
||||||
|
Pravidla zpracování:
|
||||||
|
|
||||||
|
- Dekódovat pouze hlavičky s prefixem `X-MS365-`.
|
||||||
|
- Před dekódováním credentials ověřit verzi.
|
||||||
|
- Každou zakódovanou hodnotu dešifrovat a autentizovat pomocí AES-256-GCM.
|
||||||
|
- Šifrovací klíč odvodit z `MS365_CREDENTIAL_ENCODING_SECRET` pomocí HKDF-SHA256.
|
||||||
|
- Použít associated data navázaná na název hlavičky, například `X-MS365-Client-Secret`, aby zakódovanou hodnotu nešlo přesunout mezi hlavičkami.
|
||||||
|
- Odmítnout chybějící, poškozené, expirované hodnoty nebo hodnoty, které neprojdou autentizací.
|
||||||
|
- Nikdy nelogovat dekódované credentials ani celé zakódované hodnoty hlaviček. Maximálně logovat název hlavičky, verzi credentials a krátký fingerprint.
|
||||||
|
|
||||||
|
Příklad payloadu před šifrováním:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"value": "tenant-id-client-id-or-client-secret",
|
||||||
|
"issued_at": "2026-05-28T00:00:00Z",
|
||||||
|
"expires_at": "2026-05-28T01:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Tím zůstanou hodnoty v hlavičkách bezpečné i při průchodu systémy, které vidí HTTP metadata, a zároveň je může deterministicky zpracovat každá instance služby, která zná sdílený secret.
|
||||||
|
|
||||||
|
### Příprava header credentials v .NET
|
||||||
|
|
||||||
|
Volající musí použít stejný sdílený secret, jaký má služba v `MS365_CREDENTIAL_ENCODING_SECRET`. Každá credential hodnota se šifruje samostatně a váže se na cílový název hlavičky.
|
||||||
|
|
||||||
|
Příklad pro .NET 8+:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
static string Base64Url(byte[] value)
|
||||||
|
{
|
||||||
|
return Convert.ToBase64String(value)
|
||||||
|
.TrimEnd('=')
|
||||||
|
.Replace('+', '-')
|
||||||
|
.Replace('/', '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[] HkdfSha256(byte[] inputKeyMaterial, byte[] salt, byte[] info, int length)
|
||||||
|
{
|
||||||
|
using var hmacExtract = new HMACSHA256(salt);
|
||||||
|
var pseudoRandomKey = hmacExtract.ComputeHash(inputKeyMaterial);
|
||||||
|
|
||||||
|
var output = new List<byte>();
|
||||||
|
var previous = Array.Empty<byte>();
|
||||||
|
var counter = 1;
|
||||||
|
|
||||||
|
while (output.Count < length)
|
||||||
|
{
|
||||||
|
using var hmacExpand = new HMACSHA256(pseudoRandomKey);
|
||||||
|
var blockInput = previous
|
||||||
|
.Concat(info)
|
||||||
|
.Concat(new[] { (byte)counter })
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
previous = hmacExpand.ComputeHash(blockInput);
|
||||||
|
output.AddRange(previous);
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.Take(length).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
static string EncodeCredential(string value, string headerName, string sharedSecret)
|
||||||
|
{
|
||||||
|
var payload = JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
value,
|
||||||
|
issued_at = DateTimeOffset.UtcNow.ToString("O"),
|
||||||
|
expires_at = DateTimeOffset.UtcNow.AddHours(1).ToString("O")
|
||||||
|
});
|
||||||
|
|
||||||
|
var salt = Encoding.UTF8.GetBytes("microsoft-365-service.credentials.v1");
|
||||||
|
var key = HkdfSha256(
|
||||||
|
Encoding.UTF8.GetBytes(sharedSecret),
|
||||||
|
salt,
|
||||||
|
Encoding.UTF8.GetBytes(headerName),
|
||||||
|
32);
|
||||||
|
|
||||||
|
var nonce = RandomNumberGenerator.GetBytes(12);
|
||||||
|
var plaintext = Encoding.UTF8.GetBytes(payload);
|
||||||
|
var ciphertext = new byte[plaintext.Length];
|
||||||
|
var tag = new byte[16];
|
||||||
|
var aad = Encoding.UTF8.GetBytes(headerName);
|
||||||
|
|
||||||
|
using var aes = new AesGcm(key, tagSizeInBytes: 16);
|
||||||
|
aes.Encrypt(nonce, plaintext, ciphertext, tag, aad);
|
||||||
|
|
||||||
|
var ciphertextAndTag = ciphertext.Concat(tag).ToArray();
|
||||||
|
return $"v1.{Base64Url(nonce)}.{Base64Url(ciphertextAndTag)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
var sharedSecret = "same-value-as-MS365_CREDENTIAL_ENCODING_SECRET";
|
||||||
|
|
||||||
|
var headers = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["X-MS365-Tenant-Id"] = EncodeCredential(
|
||||||
|
"00000000-0000-0000-0000-000000000000",
|
||||||
|
"X-MS365-Tenant-Id",
|
||||||
|
sharedSecret),
|
||||||
|
["X-MS365-Client-Id"] = EncodeCredential(
|
||||||
|
"00000000-0000-0000-0000-000000000000",
|
||||||
|
"X-MS365-Client-Id",
|
||||||
|
sharedSecret),
|
||||||
|
["X-MS365-Client-Secret"] = EncodeCredential(
|
||||||
|
"client-secret-value",
|
||||||
|
"X-MS365-Client-Secret",
|
||||||
|
sharedSecret),
|
||||||
|
["X-MS365-Credential-Version"] = "v1"
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Příklad requestu:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var http = new HttpClient();
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Get, "https://service.example.com/users?top=25");
|
||||||
|
|
||||||
|
foreach (var header in headers)
|
||||||
|
{
|
||||||
|
request.Headers.Add(header.Key, header.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var response = await http.SendAsync(request);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementované služby
|
||||||
|
|
||||||
|
- Users: výpis a načtení uživatelů.
|
||||||
|
- Mail: výpis zpráv a odesílání e-mailů včetně příloh.
|
||||||
|
- Calendar: výpis a vytváření událostí včetně Teams online meetingů.
|
||||||
|
- OneDrive: výpis souborů v rootu a upload malých souborů.
|
||||||
|
- Groups a Teams: výpis skupin a výpis kanálů týmu.
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
@@ -43,24 +199,24 @@ GET /groups?top=25
|
|||||||
GET /teams/{team_id}/channels
|
GET /teams/{team_id}/channels
|
||||||
```
|
```
|
||||||
|
|
||||||
`user_id` can be a Microsoft Graph user id or user principal name, for example `jane@example.com`.
|
`user_id` může být Microsoft Graph user id nebo user principal name, například `jane@example.com`.
|
||||||
|
|
||||||
## Required Graph Permissions
|
## Požadovaná Graph oprávnění
|
||||||
|
|
||||||
Grant only the permissions your deployment actually uses:
|
Přidělte pouze oprávnění, která dané nasazení skutečně používá:
|
||||||
|
|
||||||
- Users: `User.Read.All`
|
- Users: `User.Read.All`
|
||||||
- Mail read: `Mail.Read`
|
- Čtení mailů: `Mail.Read`
|
||||||
- Mail send: `Mail.Send`
|
- Odesílání mailů: `Mail.Send`
|
||||||
- Calendar read/write: `Calendars.ReadWrite`
|
- Čtení a zápis do kalendáře: `Calendars.ReadWrite`
|
||||||
- OneDrive read/write: `Files.ReadWrite.All`
|
- Čtení a zápis do OneDrive: `Files.ReadWrite.All`
|
||||||
- Groups and Teams channel listing: `Group.Read.All`, `Team.ReadBasic.All`, `Channel.ReadBasic.All`
|
- Výpis skupin a Teams kanálů: `Group.Read.All`, `Team.ReadBasic.All`, `Channel.ReadBasic.All`
|
||||||
|
|
||||||
## Run Locally
|
## Lokální spuštění
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
uvicorn app.main:app --reload
|
uvicorn app.main:app --reload
|
||||||
```
|
```
|
||||||
|
|
||||||
Open `http://localhost:8000/docs` for the generated OpenAPI UI.
|
Vygenerované OpenAPI UI otevřete na `http://localhost:8000/docs`.
|
||||||
|
|||||||
+10
-1
@@ -1,5 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, replace
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -10,6 +10,7 @@ class Settings:
|
|||||||
tenant_id: str = os.getenv("MS365_TENANT_ID", "")
|
tenant_id: str = os.getenv("MS365_TENANT_ID", "")
|
||||||
client_id: str = os.getenv("MS365_CLIENT_ID", "")
|
client_id: str = os.getenv("MS365_CLIENT_ID", "")
|
||||||
client_secret: str = os.getenv("MS365_CLIENT_SECRET", "")
|
client_secret: str = os.getenv("MS365_CLIENT_SECRET", "")
|
||||||
|
credential_encoding_secret: str = os.getenv("MS365_CREDENTIAL_ENCODING_SECRET", "")
|
||||||
graph_base_url: str = os.getenv("MS365_GRAPH_BASE_URL", "https://graph.microsoft.com/v1.0")
|
graph_base_url: str = os.getenv("MS365_GRAPH_BASE_URL", "https://graph.microsoft.com/v1.0")
|
||||||
graph_scope: str = os.getenv("MS365_GRAPH_SCOPE", "https://graph.microsoft.com/.default")
|
graph_scope: str = os.getenv("MS365_GRAPH_SCOPE", "https://graph.microsoft.com/.default")
|
||||||
request_timeout_seconds: float = float(os.getenv("MS365_REQUEST_TIMEOUT_SECONDS", "30"))
|
request_timeout_seconds: float = float(os.getenv("MS365_REQUEST_TIMEOUT_SECONDS", "30"))
|
||||||
@@ -22,5 +23,13 @@ class Settings:
|
|||||||
def is_configured(self) -> bool:
|
def is_configured(self) -> bool:
|
||||||
return bool(self.tenant_id and self.client_id and self.client_secret)
|
return bool(self.tenant_id and self.client_id and self.client_secret)
|
||||||
|
|
||||||
|
def with_credentials(self, tenant_id: str, client_id: str, client_secret: str) -> "Settings":
|
||||||
|
return replace(
|
||||||
|
self,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
client_id=client_id,
|
||||||
|
client_secret=client_secret,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import base64
|
||||||
|
import binascii
|
||||||
|
import json
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from cryptography.exceptions import InvalidTag
|
||||||
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||||
|
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
||||||
|
from fastapi import Header, HTTPException, status
|
||||||
|
|
||||||
|
from .config import Settings, settings
|
||||||
|
|
||||||
|
|
||||||
|
CREDENTIAL_VERSION = "v1"
|
||||||
|
_HKDF_SALT = b"microsoft-365-service.credentials.v1"
|
||||||
|
|
||||||
|
|
||||||
|
def _b64url_decode(value: str) -> bytes:
|
||||||
|
padding = "=" * (-len(value) % 4)
|
||||||
|
try:
|
||||||
|
return base64.urlsafe_b64decode((value + padding).encode("ascii"))
|
||||||
|
except (binascii.Error, UnicodeEncodeError) as exc:
|
||||||
|
raise ValueError("invalid base64url") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _derive_key(shared_secret: str, header_name: str) -> bytes:
|
||||||
|
return HKDF(
|
||||||
|
algorithm=hashes.SHA256(),
|
||||||
|
length=32,
|
||||||
|
salt=_HKDF_SALT,
|
||||||
|
info=header_name.encode("utf-8"),
|
||||||
|
).derive(shared_secret.encode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def decode_credential_header(encoded_value: str, *, header_name: str, shared_secret: str) -> str:
|
||||||
|
parts = encoded_value.split(".")
|
||||||
|
if len(parts) != 3 or parts[0] != CREDENTIAL_VERSION:
|
||||||
|
raise ValueError("unsupported credential encoding version")
|
||||||
|
|
||||||
|
nonce = _b64url_decode(parts[1])
|
||||||
|
ciphertext = _b64url_decode(parts[2])
|
||||||
|
if len(nonce) != 12:
|
||||||
|
raise ValueError("invalid nonce length")
|
||||||
|
|
||||||
|
key = _derive_key(shared_secret, header_name)
|
||||||
|
aad = header_name.encode("utf-8")
|
||||||
|
plaintext = AESGCM(key).decrypt(nonce, ciphertext, aad)
|
||||||
|
payload = json.loads(plaintext.decode("utf-8"))
|
||||||
|
|
||||||
|
expires_at = payload.get("expires_at")
|
||||||
|
if expires_at:
|
||||||
|
expires_at_datetime = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
|
||||||
|
if expires_at_datetime <= datetime.now(timezone.utc):
|
||||||
|
raise ValueError("credential value is expired")
|
||||||
|
|
||||||
|
value = payload.get("value")
|
||||||
|
if not isinstance(value, str) or not value:
|
||||||
|
raise ValueError("credential payload must contain a non-empty value")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
async def get_request_settings(
|
||||||
|
tenant_id: Annotated[
|
||||||
|
str | None,
|
||||||
|
Header(
|
||||||
|
alias="X-MS365-Tenant-Id",
|
||||||
|
description="Encoded value of MS365_TENANT_ID, the Microsoft Entra tenant id. Format: v1.<base64url nonce>.<base64url ciphertext+tag>.",
|
||||||
|
),
|
||||||
|
] = None,
|
||||||
|
client_id: Annotated[
|
||||||
|
str | None,
|
||||||
|
Header(
|
||||||
|
alias="X-MS365-Client-Id",
|
||||||
|
description="Encoded value of MS365_CLIENT_ID, the Microsoft Entra application client id. Format: v1.<base64url nonce>.<base64url ciphertext+tag>.",
|
||||||
|
),
|
||||||
|
] = None,
|
||||||
|
client_secret: Annotated[
|
||||||
|
str | None,
|
||||||
|
Header(
|
||||||
|
alias="X-MS365-Client-Secret",
|
||||||
|
description="Encoded value of MS365_CLIENT_SECRET, the Microsoft Entra application client secret. Format: v1.<base64url nonce>.<base64url ciphertext+tag>.",
|
||||||
|
),
|
||||||
|
] = None,
|
||||||
|
credential_version: Annotated[
|
||||||
|
str | None,
|
||||||
|
Header(
|
||||||
|
alias="X-MS365-Credential-Version",
|
||||||
|
description="Credential encoding version. Currently supported value: v1.",
|
||||||
|
),
|
||||||
|
] = None,
|
||||||
|
) -> Settings:
|
||||||
|
header_values = {
|
||||||
|
"X-MS365-Tenant-Id": tenant_id,
|
||||||
|
"X-MS365-Client-Id": client_id,
|
||||||
|
"X-MS365-Client-Secret": client_secret,
|
||||||
|
}
|
||||||
|
provided = {name: value for name, value in header_values.items() if value}
|
||||||
|
if not provided:
|
||||||
|
return settings
|
||||||
|
|
||||||
|
missing = [name for name, value in header_values.items() if not value]
|
||||||
|
if missing:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail={"message": "Incomplete Microsoft 365 credential headers.", "missing_headers": missing},
|
||||||
|
)
|
||||||
|
|
||||||
|
if credential_version != CREDENTIAL_VERSION:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=f"X-MS365-Credential-Version must be {CREDENTIAL_VERSION}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not settings.credential_encoding_secret:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="MS365_CREDENTIAL_ENCODING_SECRET is not configured.",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
decoded = {
|
||||||
|
name: decode_credential_header(
|
||||||
|
value,
|
||||||
|
header_name=name,
|
||||||
|
shared_secret=settings.credential_encoding_secret,
|
||||||
|
)
|
||||||
|
for name, value in provided.items()
|
||||||
|
}
|
||||||
|
except (InvalidTag, ValueError, json.JSONDecodeError, UnicodeDecodeError) as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Microsoft 365 credential headers are malformed or cannot be authenticated.",
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
return settings.with_credentials(
|
||||||
|
tenant_id=decoded["X-MS365-Tenant-Id"],
|
||||||
|
client_id=decoded["X-MS365-Client-Id"],
|
||||||
|
client_secret=decoded["X-MS365-Client-Secret"],
|
||||||
|
)
|
||||||
+10
-10
@@ -2,17 +2,17 @@ from typing import Any
|
|||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, Query, Response, status
|
from fastapi import APIRouter, Body, Depends, Query, Response, status
|
||||||
|
|
||||||
from .config import settings
|
from .config import Settings
|
||||||
|
from .credentials import get_request_settings
|
||||||
from .graph_client import MicrosoftGraphClient
|
from .graph_client import MicrosoftGraphClient
|
||||||
from .schemas import CalendarEventRequest, DriveUploadRequest, SendMailRequest
|
from .schemas import CalendarEventRequest, DriveUploadRequest, SendMailRequest
|
||||||
from .services import CalendarService, DriveService, GroupsService, MailService, UsersService
|
from .services import CalendarService, DriveService, GroupsService, MailService, UsersService
|
||||||
|
|
||||||
router = APIRouter(tags=["microsoft365"])
|
router = APIRouter(tags=["microsoft365"])
|
||||||
graph_client = MicrosoftGraphClient(settings)
|
|
||||||
|
|
||||||
|
|
||||||
def get_graph_client() -> MicrosoftGraphClient:
|
def get_graph_client(request_settings: Settings = Depends(get_request_settings)) -> MicrosoftGraphClient:
|
||||||
return graph_client
|
return MicrosoftGraphClient(request_settings)
|
||||||
|
|
||||||
|
|
||||||
def get_users_service(graph: MicrosoftGraphClient = Depends(get_graph_client)) -> UsersService:
|
def get_users_service(graph: MicrosoftGraphClient = Depends(get_graph_client)) -> UsersService:
|
||||||
@@ -36,13 +36,13 @@ def get_groups_service(graph: MicrosoftGraphClient = Depends(get_graph_client))
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/status")
|
@router.get("/status")
|
||||||
def microsoft365_status() -> dict[str, Any]:
|
def microsoft365_status(request_settings: Settings = Depends(get_request_settings)) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"configured": settings.is_configured,
|
"configured": request_settings.is_configured,
|
||||||
"tenant_id": bool(settings.tenant_id),
|
"tenant_id": bool(request_settings.tenant_id),
|
||||||
"client_id": bool(settings.client_id),
|
"client_id": bool(request_settings.client_id),
|
||||||
"client_secret": bool(settings.client_secret),
|
"client_secret": bool(request_settings.client_secret),
|
||||||
"graph_base_url": settings.graph_base_url,
|
"graph_base_url": request_settings.graph_base_url,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
fastapi
|
fastapi
|
||||||
httpx
|
httpx
|
||||||
|
cryptography
|
||||||
pydantic[email]
|
pydantic[email]
|
||||||
uvicorn[standard]
|
uvicorn[standard]
|
||||||
|
|||||||
Reference in New Issue
Block a user