diff --git a/.gitignore b/.gitignore index 36b13f1..24c3e52 100644 --- a/.gitignore +++ b/.gitignore @@ -174,3 +174,7 @@ cython_debug/ # PyPI configuration file .pypirc + +# ---> .NET (examples) +bin/ +obj/ diff --git a/README.md b/README.md index af3dec2..299c65c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ V Microsoft Entra ID vytvořte app registration, přidělte požadovaná aplika MS365_TENANT_ID=00000000-0000-0000-0000-000000000000 MS365_CLIENT_ID=00000000-0000-0000-0000-000000000000 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_SCOPE=https://graph.microsoft.com/.default MS365_REQUEST_TIMEOUT_SECONDS=30 @@ -20,157 +19,28 @@ Služba používá OAuth2 client credentials flow, takže oprávnění pro Micro ## Credentials v request hlavičkách -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. - -Doporučené hlavičky: +Credentials se posílají v každém requestu v hlavičkách jako obyčejné hodnoty: ```http -X-MS365-Tenant-Id: -X-MS365-Client-Id: -X-MS365-Client-Secret: -X-MS365-Credential-Version: v1 +X-MS365-Tenant-Id: +X-MS365-Client-Id: +X-MS365-Client-Secret: ``` -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á. +Všechny tři hlavičky musí být pohromadě, jinak služba vrátí 400. Když se nepošle +žádná, použijí se `MS365_TENANT_ID`, `MS365_CLIENT_ID` a `MS365_CLIENT_SECRET` +z prostředí služby. -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. +Příklad v PowerShellu: -Doporučený formát zakódované hodnoty: - -```text -v1.. -``` - -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" +```powershell +$Headers = @{ + "X-MS365-Tenant-Id" = $TenantId + "X-MS365-Client-Id" = $ClientId + "X-MS365-Client-Secret" = $ClientSecret } -``` - -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(); - var previous = Array.Empty(); - 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 -{ - ["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(); +Invoke-RestMethod -Method Get -Headers $Headers ` + -Uri "https://services.csbot.cz/apps/microsoft-365-service/users/$UserMailbox/calendar" ``` ## Multitenant napojení klientů (CSBOT MS365 Connector) @@ -192,7 +62,8 @@ Microsoft 365 tenanty jednotlivých klientů. Podrobné zadání je v Notion str zákazníkovi. Tenant isolation (customer id -> tenant id, mailbox, plan id, bucket id) musí držet volající backend, hodnoty nesmí přicházet přímo od LLM. -Postup testu po consentu klienta: `GET /status`, `GET /users/{mailbox}/calendar/view`, +Postup testu po consentu klienta: `GET /status`, `GET /users/{mailbox}/calendar` +(vrátí id a name kalendáře, nejlevnější ověření přístupu), `GET /users/{mailbox}/calendar/view`, `POST /users/{mailbox}/calendar/events` + `DELETE`, `GET /planner/plans/{plan_id}/tasks`, `POST /planner/tasks` + `DELETE`. @@ -200,7 +71,7 @@ Postup testu po consentu klienta: `GET /status`, `GET /users/{mailbox}/calendar/ - 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 událostí, calendarView v zadaném rozsahu, volné termíny (getSchedule), +- Calendar: načtení kalendáře schránky (id, name), výpis událostí, calendarView v zadaném rozsahu, volné termíny (getSchedule), načtení, vytvoření, úprava a smazání události včetně Teams online meetingů. - Planner: výpis plánů skupiny, bucketů a tasků plánu, načtení, vytvoření, úprava a smazání tasku. Úprava a smazání používají ETag (`If-Match`). Když hlavička chybí, @@ -220,6 +91,7 @@ GET /users/{user_id} GET /users/{user_id}/mail/messages?folder=Inbox&top=25 POST /users/{user_id}/mail/send GET /admin-consent/url?redirect_uri=...&state=... +GET /users/{user_id}/calendar GET /users/{user_id}/calendar/events?top=25 POST /users/{user_id}/calendar/events GET /users/{user_id}/calendar/view?start=...&end=...&top=25&time_zone=Europe/Prague @@ -267,8 +139,7 @@ omezení na konkrétní mailbox se řeší na straně klienta v Exchange Online ## Ukázky pro klienty - `examples/csharp/ListUsersTop10`: konzolová aplikace .NET 8, volá `GET /users?top=10` - a obsahuje třídu `CredentialEncoder` pro šifrování hlaviček `X-MS365-*`. - Popis v `examples/csharp/README.md`. + s hlavičkami `X-MS365-*`. Popis v `examples/csharp/README.md`. ## Lokální spuštění @@ -284,6 +155,11 @@ Vygenerované OpenAPI UI otevřete na `http://localhost:8000/docs`. ## Záznam změn +- 2026-09-22: šifrování hlaviček `X-MS365-*` odstraněno, hodnoty se posílají tak, jak jsou. + Zrušena proměnná `MS365_CREDENTIAL_ENCODING_SECRET` a závislost `cryptography`. +- 2026-09-22: `GET /users/{user_id}/calendar` (objekt kalendáře schránky, id a name) pro + read-only ověření přístupu ke kalendáři klienta přes e-mail schránky. + - 2026-09-09: verze 1.1.0. Podle Notion zadání "MS365 práva konektoru" doplněn Calendar (calendarView, getSchedule, get/patch/delete události), Planner (plány, buckety, tasky, create/patch/delete s ETag) a `GET /admin-consent/url`. Kód ověřen jen importem aplikace diff --git a/app/config.py b/app/config.py index fc6c2cd..85ec562 100644 --- a/app/config.py +++ b/app/config.py @@ -10,7 +10,6 @@ class Settings: tenant_id: str = os.getenv("MS365_TENANT_ID", "") client_id: str = os.getenv("MS365_CLIENT_ID", "") 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_scope: str = os.getenv("MS365_GRAPH_SCOPE", "https://graph.microsoft.com/.default") request_timeout_seconds: float = float(os.getenv("MS365_REQUEST_TIMEOUT_SECONDS", "30")) diff --git a/app/credentials.py b/app/credentials.py index 0fd2d65..5b3174a 100644 --- a/app/credentials.py +++ b/app/credentials.py @@ -1,96 +1,34 @@ -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...", + description="Microsoft Entra tenant id of the client (Directory / tenant ID).", ), ] = 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...", + description="Application (client) ID of the CSBOT Entra app registration.", ), ] = 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...", - ), - ] = None, - credential_version: Annotated[ - str | None, - Header( - alias="X-MS365-Credential-Version", - description="Credential encoding version. Currently supported value: v1.", + description="Client secret VALUE of the CSBOT Entra app registration (not the secret id).", ), ] = None, ) -> Settings: + """Per-request credentials. All three headers together, or none (then env values are used).""" header_values = { "X-MS365-Tenant-Id": tenant_id, "X-MS365-Client-Id": client_id, @@ -107,35 +45,8 @@ async def get_request_settings( 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"], + tenant_id=provided["X-MS365-Tenant-Id"], + client_id=provided["X-MS365-Client-Id"], + client_secret=provided["X-MS365-Client-Secret"], ) diff --git a/app/routes.py b/app/routes.py index d87205d..d8dcf4f 100644 --- a/app/routes.py +++ b/app/routes.py @@ -119,6 +119,12 @@ async def send_mail( response.status_code = status.HTTP_202_ACCEPTED +@router.get("/users/{user_id}/calendar") +async def get_calendar(user_id: str, service: CalendarService = Depends(get_calendar_service)) -> Any: + """user_id is a Graph user id or the mailbox address, e.g. servis@firma.cz.""" + return await service.get_calendar(user_id=user_id) + + @router.get("/users/{user_id}/calendar/events") async def list_events( user_id: str, diff --git a/app/services.py b/app/services.py index 4d922e4..789b53f 100644 --- a/app/services.py +++ b/app/services.py @@ -91,6 +91,10 @@ class CalendarService: def __init__(self, graph: MicrosoftGraphClient) -> None: self._graph = graph + async def get_calendar(self, user_id: str) -> Any: + """Default calendar of the mailbox (id, name, owner). Cheapest read-only access check.""" + return await self._graph.request("GET", f"/users/{graph_segment(user_id)}/calendar") + async def list_events(self, user_id: str, top: int = 25) -> Any: params = { "$top": top, diff --git a/examples/csharp/ListUsersTop10/Program.cs b/examples/csharp/ListUsersTop10/Program.cs index db67014..4b99a76 100644 --- a/examples/csharp/ListUsersTop10/Program.cs +++ b/examples/csharp/ListUsersTop10/Program.cs @@ -3,29 +3,22 @@ // Sluzba: https://services.csbot.cz/apps/microsoft-365-service // Dokumentace (Swagger): https://services.csbot.cz/apps/microsoft-365-service/docs // -// Credentials se posilaji v hlavickach X-MS365-*. Kazda hodnota je zasifrovana -// AES-256-GCM klicem odvozenym pres HKDF-SHA256 ze sdileneho secretu -// (MS365_CREDENTIAL_ENCODING_SECRET na strane sluzby). Format hodnoty: -// v1.. +// Credentials se posilaji v hlavickach X-MS365-* jako obycejne hodnoty. // // Spusteni: // set MS365_SERVICE_BASE_URL=https://services.csbot.cz/apps/microsoft-365-service -// set MS365_TENANT_ID= -// set MS365_CLIENT_ID= -// set MS365_CLIENT_SECRET= -// set MS365_CREDENTIAL_ENCODING_SECRET= +// set MS365_TENANT_ID= +// set MS365_CLIENT_ID= +// set MS365_CLIENT_SECRET= // dotnet run using System.Net.Http.Headers; -using System.Security.Cryptography; -using System.Text; using System.Text.Json; var baseUrl = Env("MS365_SERVICE_BASE_URL", "https://services.csbot.cz/apps/microsoft-365-service"); var tenantId = Env("MS365_TENANT_ID"); var clientId = Env("MS365_CLIENT_ID"); var clientSecret = Env("MS365_CLIENT_SECRET"); -var sharedSecret = Env("MS365_CREDENTIAL_ENCODING_SECRET"); const int top = 10; @@ -33,17 +26,16 @@ using var http = new HttpClient { BaseAddress = new Uri(baseUrl.TrimEnd('/') + " http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); using var request = new HttpRequestMessage(HttpMethod.Get, $"users?top={top}"); -request.Headers.Add("X-MS365-Tenant-Id", CredentialEncoder.Encode(tenantId, "X-MS365-Tenant-Id", sharedSecret)); -request.Headers.Add("X-MS365-Client-Id", CredentialEncoder.Encode(clientId, "X-MS365-Client-Id", sharedSecret)); -request.Headers.Add("X-MS365-Client-Secret", CredentialEncoder.Encode(clientSecret, "X-MS365-Client-Secret", sharedSecret)); -request.Headers.Add("X-MS365-Credential-Version", "v1"); +request.Headers.Add("X-MS365-Tenant-Id", tenantId); +request.Headers.Add("X-MS365-Client-Id", clientId); +request.Headers.Add("X-MS365-Client-Secret", clientSecret); using var response = await http.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { - // 400 = spatne/prosle hlavicky, 502 = chyba z Microsoft Graph (detail je v tele), + // 400 = neuplne hlavicky, 502 = chyba z Microsoft Graph (detail je v tele), // 403 text/plain = request neprosel pres reverzni proxy (IP allowlist pro GET). Console.Error.WriteLine($"HTTP {(int)response.StatusCode} {response.ReasonPhrase}"); Console.Error.WriteLine(body); @@ -80,49 +72,3 @@ static string Env(string name, string? fallback = null) } throw new InvalidOperationException($"Chybi environment promenna {name}."); } - -/// -/// Sifrovani hodnot pro hlavicky X-MS365-*. Musi presne odpovidat dekodovani na strane sluzby -/// (app/credentials.py): HKDF-SHA256 se salt "microsoft-365-service.credentials.v1" -/// a info = nazev hlavicky, AES-256-GCM s 12B nonce, 16B tagem a AAD = nazev hlavicky. -/// -static class CredentialEncoder -{ - private static readonly byte[] HkdfSalt = Encoding.UTF8.GetBytes("microsoft-365-service.credentials.v1"); - - public static string Encode(string value, string headerName, string sharedSecret, TimeSpan? validity = null) - { - var now = DateTimeOffset.UtcNow; - var payload = JsonSerializer.Serialize(new - { - value, - issued_at = now.ToString("O"), - expires_at = now.Add(validity ?? TimeSpan.FromHours(1)).ToString("O"), - }); - - var headerBytes = Encoding.UTF8.GetBytes(headerName); - var key = HKDF.DeriveKey( - HashAlgorithmName.SHA256, - Encoding.UTF8.GetBytes(sharedSecret), - outputLength: 32, - salt: HkdfSalt, - info: headerBytes); - - var nonce = RandomNumberGenerator.GetBytes(12); - var plaintext = Encoding.UTF8.GetBytes(payload); - var ciphertext = new byte[plaintext.Length]; - var tag = new byte[16]; - - using var aes = new AesGcm(key, tagSizeInBytes: 16); - aes.Encrypt(nonce, plaintext, ciphertext, tag, associatedData: headerBytes); - - var ciphertextAndTag = new byte[ciphertext.Length + tag.Length]; - ciphertext.CopyTo(ciphertextAndTag, 0); - tag.CopyTo(ciphertextAndTag, ciphertext.Length); - - return $"v1.{Base64Url(nonce)}.{Base64Url(ciphertextAndTag)}"; - } - - private static string Base64Url(byte[] data) => - Convert.ToBase64String(data).TrimEnd('=').Replace('+', '-').Replace('/', '_'); -} diff --git a/examples/csharp/ListUsersTop10/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs b/examples/csharp/ListUsersTop10/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs deleted file mode 100644 index 2217181..0000000 --- a/examples/csharp/ListUsersTop10/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs +++ /dev/null @@ -1,4 +0,0 @@ -// -using System; -using System.Reflection; -[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v8.0", FrameworkDisplayName = ".NET 8.0")] diff --git a/examples/csharp/ListUsersTop10/obj/Debug/net8.0/ListUsersTop10.AssemblyInfo.cs b/examples/csharp/ListUsersTop10/obj/Debug/net8.0/ListUsersTop10.AssemblyInfo.cs deleted file mode 100644 index b3bf715..0000000 --- a/examples/csharp/ListUsersTop10/obj/Debug/net8.0/ListUsersTop10.AssemblyInfo.cs +++ /dev/null @@ -1,22 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -using System; -using System.Reflection; - -[assembly: System.Reflection.AssemblyCompanyAttribute("ListUsersTop10")] -[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] -[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+275bd6c46764ec8db008fa2fa8c30673bb96f3d8")] -[assembly: System.Reflection.AssemblyProductAttribute("ListUsersTop10")] -[assembly: System.Reflection.AssemblyTitleAttribute("ListUsersTop10")] -[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] - -// Generated by the MSBuild WriteCodeFragment class. - diff --git a/examples/csharp/ListUsersTop10/obj/Debug/net8.0/ListUsersTop10.AssemblyInfoInputs.cache b/examples/csharp/ListUsersTop10/obj/Debug/net8.0/ListUsersTop10.AssemblyInfoInputs.cache deleted file mode 100644 index 48ca0b9..0000000 --- a/examples/csharp/ListUsersTop10/obj/Debug/net8.0/ListUsersTop10.AssemblyInfoInputs.cache +++ /dev/null @@ -1 +0,0 @@ -502171e0ece4b0be631032bff07cea1c6b79151ad77b399268a548f2c009e1d0 diff --git a/examples/csharp/ListUsersTop10/obj/Debug/net8.0/ListUsersTop10.GeneratedMSBuildEditorConfig.editorconfig b/examples/csharp/ListUsersTop10/obj/Debug/net8.0/ListUsersTop10.GeneratedMSBuildEditorConfig.editorconfig deleted file mode 100644 index 7dd5c4e..0000000 --- a/examples/csharp/ListUsersTop10/obj/Debug/net8.0/ListUsersTop10.GeneratedMSBuildEditorConfig.editorconfig +++ /dev/null @@ -1,18 +0,0 @@ -is_global = true -build_property.TargetFramework = net8.0 -build_property.TargetFrameworkIdentifier = .NETCoreApp -build_property.TargetFrameworkVersion = v8.0 -build_property.TargetPlatformMinVersion = -build_property.UsingMicrosoftNETSdkWeb = -build_property.ProjectTypeGuids = -build_property.InvariantGlobalization = -build_property.PlatformNeutralAssembly = -build_property.EnforceExtendedAnalyzerRules = -build_property.EntryPointFilePath = -build_property._SupportedPlatformList = Linux,macOS,Windows -build_property.RootNamespace = ListUsersTop10 -build_property.ProjectDir = D:\GitHubRepository\Hracicky\x\ms365\microsoft-365-service\examples\csharp\ListUsersTop10\ -build_property.EnableComHosting = -build_property.EnableGeneratedComInterfaceComImportInterop = -build_property.EffectiveAnalysisLevelStyle = 8.0 -build_property.EnableCodeStyleSeverity = diff --git a/examples/csharp/ListUsersTop10/obj/Debug/net8.0/ListUsersTop10.GlobalUsings.g.cs b/examples/csharp/ListUsersTop10/obj/Debug/net8.0/ListUsersTop10.GlobalUsings.g.cs deleted file mode 100644 index d12bcbc..0000000 --- a/examples/csharp/ListUsersTop10/obj/Debug/net8.0/ListUsersTop10.GlobalUsings.g.cs +++ /dev/null @@ -1,8 +0,0 @@ -// -global using System; -global using System.Collections.Generic; -global using System.IO; -global using System.Linq; -global using System.Net.Http; -global using System.Threading; -global using System.Threading.Tasks; diff --git a/examples/csharp/ListUsersTop10/obj/Debug/net8.0/ListUsersTop10.assets.cache b/examples/csharp/ListUsersTop10/obj/Debug/net8.0/ListUsersTop10.assets.cache deleted file mode 100644 index 297ea05..0000000 Binary files a/examples/csharp/ListUsersTop10/obj/Debug/net8.0/ListUsersTop10.assets.cache and /dev/null differ diff --git a/examples/csharp/ListUsersTop10/obj/ListUsersTop10.csproj.nuget.dgspec.json b/examples/csharp/ListUsersTop10/obj/ListUsersTop10.csproj.nuget.dgspec.json deleted file mode 100644 index 80ce806..0000000 --- a/examples/csharp/ListUsersTop10/obj/ListUsersTop10.csproj.nuget.dgspec.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "format": 1, - "restore": { - "D:\\GitHubRepository\\Hracicky\\x\\ms365\\microsoft-365-service\\examples\\csharp\\ListUsersTop10\\ListUsersTop10.csproj": {} - }, - "projects": { - "D:\\GitHubRepository\\Hracicky\\x\\ms365\\microsoft-365-service\\examples\\csharp\\ListUsersTop10\\ListUsersTop10.csproj": { - "version": "1.0.0", - "restore": { - "projectUniqueName": "D:\\GitHubRepository\\Hracicky\\x\\ms365\\microsoft-365-service\\examples\\csharp\\ListUsersTop10\\ListUsersTop10.csproj", - "projectName": "ListUsersTop10", - "projectPath": "D:\\GitHubRepository\\Hracicky\\x\\ms365\\microsoft-365-service\\examples\\csharp\\ListUsersTop10\\ListUsersTop10.csproj", - "packagesPath": "C:\\Users\\GamingPC\\.nuget\\packages\\", - "outputPath": "D:\\GitHubRepository\\Hracicky\\x\\ms365\\microsoft-365-service\\examples\\csharp\\ListUsersTop10\\obj\\", - "projectStyle": "PackageReference", - "fallbackFolders": [ - "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages" - ], - "configFilePaths": [ - "C:\\Users\\GamingPC\\AppData\\Roaming\\NuGet\\NuGet.Config", - "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config", - "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config" - ], - "originalTargetFrameworks": [ - "net8.0" - ], - "sources": { - "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {}, - "C:\\Program Files\\dotnet\\library-packs": {}, - "D:\\CustomNuGetPackages": {}, - "https://api.nuget.org/v3/index.json": {} - }, - "frameworks": { - "net8.0": { - "framework": "net8.0", - "targetAlias": "net8.0", - "projectReferences": {} - } - }, - "warningProperties": { - "warnAsError": [ - "NU1605" - ] - }, - "restoreAuditProperties": { - "enableAudit": "true", - "auditLevel": "low", - "auditMode": "direct" - }, - "SdkAnalysisLevel": "10.0.400" - }, - "frameworks": { - "net8.0": { - "framework": "net8.0", - "targetAlias": "net8.0", - "imports": [ - "net461", - "net462", - "net47", - "net471", - "net472", - "net48", - "net481" - ], - "assetTargetFallback": true, - "warn": true, - "frameworkReferences": { - "Microsoft.NETCore.App": { - "privateAssets": "all" - } - }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.400/PortableRuntimeIdentifierGraph.json" - } - } - } - } -} \ No newline at end of file diff --git a/examples/csharp/ListUsersTop10/obj/ListUsersTop10.csproj.nuget.g.props b/examples/csharp/ListUsersTop10/obj/ListUsersTop10.csproj.nuget.g.props deleted file mode 100644 index 31462d3..0000000 --- a/examples/csharp/ListUsersTop10/obj/ListUsersTop10.csproj.nuget.g.props +++ /dev/null @@ -1,16 +0,0 @@ - - - - True - NuGet - $(MSBuildThisFileDirectory)project.assets.json - $(UserProfile)\.nuget\packages\ - C:\Users\GamingPC\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages - PackageReference - 7.0.0 - - - - - - \ No newline at end of file diff --git a/examples/csharp/ListUsersTop10/obj/ListUsersTop10.csproj.nuget.g.targets b/examples/csharp/ListUsersTop10/obj/ListUsersTop10.csproj.nuget.g.targets deleted file mode 100644 index 3dc06ef..0000000 --- a/examples/csharp/ListUsersTop10/obj/ListUsersTop10.csproj.nuget.g.targets +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/examples/csharp/ListUsersTop10/obj/project.assets.json b/examples/csharp/ListUsersTop10/obj/project.assets.json deleted file mode 100644 index 8d2ed4e..0000000 --- a/examples/csharp/ListUsersTop10/obj/project.assets.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "version": 4, - "targets": { - "net8.0": {} - }, - "libraries": {}, - "projectFileDependencyGroups": { - "net8.0": [] - }, - "packageFolders": { - "C:\\Users\\GamingPC\\.nuget\\packages\\": {}, - "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages": {} - }, - "project": { - "version": "1.0.0", - "restore": { - "projectUniqueName": "D:\\GitHubRepository\\Hracicky\\x\\ms365\\microsoft-365-service\\examples\\csharp\\ListUsersTop10\\ListUsersTop10.csproj", - "projectName": "ListUsersTop10", - "projectPath": "D:\\GitHubRepository\\Hracicky\\x\\ms365\\microsoft-365-service\\examples\\csharp\\ListUsersTop10\\ListUsersTop10.csproj", - "packagesPath": "C:\\Users\\GamingPC\\.nuget\\packages\\", - "outputPath": "D:\\GitHubRepository\\Hracicky\\x\\ms365\\microsoft-365-service\\examples\\csharp\\ListUsersTop10\\obj\\", - "projectStyle": "PackageReference", - "fallbackFolders": [ - "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages" - ], - "configFilePaths": [ - "C:\\Users\\GamingPC\\AppData\\Roaming\\NuGet\\NuGet.Config", - "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config", - "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config" - ], - "originalTargetFrameworks": [ - "net8.0" - ], - "sources": { - "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {}, - "C:\\Program Files\\dotnet\\library-packs": {}, - "D:\\CustomNuGetPackages": {}, - "https://api.nuget.org/v3/index.json": {} - }, - "frameworks": { - "net8.0": { - "framework": "net8.0", - "targetAlias": "net8.0", - "projectReferences": {} - } - }, - "warningProperties": { - "warnAsError": [ - "NU1605" - ] - }, - "restoreAuditProperties": { - "enableAudit": "true", - "auditLevel": "low", - "auditMode": "direct" - }, - "SdkAnalysisLevel": "10.0.400" - }, - "frameworks": { - "net8.0": { - "framework": "net8.0", - "targetAlias": "net8.0", - "imports": [ - "net461", - "net462", - "net47", - "net471", - "net472", - "net48", - "net481" - ], - "assetTargetFallback": true, - "warn": true, - "frameworkReferences": { - "Microsoft.NETCore.App": { - "privateAssets": "all" - } - }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.400/PortableRuntimeIdentifierGraph.json" - } - } - } -} \ No newline at end of file diff --git a/examples/csharp/ListUsersTop10/obj/project.nuget.cache b/examples/csharp/ListUsersTop10/obj/project.nuget.cache deleted file mode 100644 index 0163ef1..0000000 --- a/examples/csharp/ListUsersTop10/obj/project.nuget.cache +++ /dev/null @@ -1,8 +0,0 @@ -{ - "version": 2, - "dgSpecHash": "4FfrW7UlSBU=", - "success": true, - "projectFilePath": "D:\\GitHubRepository\\Hracicky\\x\\ms365\\microsoft-365-service\\examples\\csharp\\ListUsersTop10\\ListUsersTop10.csproj", - "expectedPackageFiles": [], - "logs": [] -} \ No newline at end of file diff --git a/examples/csharp/README.md b/examples/csharp/README.md index bce7c23..98941a3 100644 --- a/examples/csharp/README.md +++ b/examples/csharp/README.md @@ -3,31 +3,26 @@ ## ListUsersTop10 Konzolova aplikace (.NET 8), ktera zavola `GET /users?top=10` a vypise -displayName, userPrincipalName, mail a id kazdeho uzivatele. - -Obsahuje tridu `CredentialEncoder`, ktera sifruje hodnoty pro hlavicky -`X-MS365-Tenant-Id`, `X-MS365-Client-Id` a `X-MS365-Client-Secret` presne tak, -jak je sluzba dekoduje v `app/credentials.py`. Tridu lze zkopirovat do -libovolneho projektu, nema zadne externi zavislosti. +displayName, userPrincipalName, mail a id kazdeho uzivatele. Credentials +posila v hlavickach `X-MS365-Tenant-Id`, `X-MS365-Client-Id` +a `X-MS365-Client-Secret` jako obycejne hodnoty. Nema zadne externi zavislosti. ### Spusteni ```powershell cd examples/csharp/ListUsersTop10 $env:MS365_SERVICE_BASE_URL = "https://services.csbot.cz/apps/microsoft-365-service" -$env:MS365_TENANT_ID = "" -$env:MS365_CLIENT_ID = "" -$env:MS365_CLIENT_SECRET = "" -$env:MS365_CREDENTIAL_ENCODING_SECRET = "" +$env:MS365_TENANT_ID = "" +$env:MS365_CLIENT_ID = "" +$env:MS365_CLIENT_SECRET = "" dotnet run ``` ### Co ocekavat - `200` a JSON ve tvaru Microsoft Graph: `{ "value": [ ... ], "@odata.nextLink": "..." }`. -- `400` kdyz hlavicky chybi, jsou neuplne, prosle nebo je spatny sdileny secret. +- `400` kdyz hlavicky chybi nebo jsou neuplne. - `502` kdyz Microsoft Graph nebo prihlaseni k Entra ID selze, detail je v tele odpovedi. -- `503` kdyz sluzba nema nastaveny `MS365_CREDENTIAL_ENCODING_SECRET`. - `403 text/plain` kdyz GET neprojde pres reverzni proxy (IP allowlist), tj. problem neni ve sluzbe, ale v sitovem pristupu klienta. diff --git a/requirements.txt b/requirements.txt index a729554..8780c48 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ fastapi httpx -cryptography pydantic[email] uvicorn[standard]