pyxecm 3.8.2__tar.gz → 3.8.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyxecm-3.8.2 → pyxecm-3.8.3}/PKG-INFO +1 -1
- {pyxecm-3.8.2 → pyxecm-3.8.3}/pyproject.toml +1 -1
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_customizer/guidewire.py +1 -3
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_customizer/sap.py +328 -1
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_customizer/sap_models.py +56 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_customizer/settings.py +3 -2
- {pyxecm-3.8.2 → pyxecm-3.8.3}/.gitignore +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/README.md +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm/__init__.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm/avts.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm/coreshare.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm/helper/__init__.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm/helper/assoc.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm/helper/data.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm/helper/logadapter.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm/helper/web.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm/helper/xml.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm/otac.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm/otawp.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm/otca.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm/otcs.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm/otds.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm/otiv.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm/otkd.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm/otmm.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm/otpd.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm/py.typed +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/__init__.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/__main__.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/app.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/auth/__init__.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/auth/functions.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/auth/models.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/auth/router.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/common/__init__.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/common/functions.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/common/metrics.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/common/models.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/common/router.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/settings.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/terminal/__init__.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/terminal/router.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/v1_maintenance/__init__.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/v1_maintenance/functions.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/v1_maintenance/models.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/v1_maintenance/router.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/v1_otcs/__init__.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/v1_otcs/functions.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/v1_otcs/router.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/v1_payload/__init__.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/v1_payload/functions.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/v1_payload/log_reader.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/v1_payload/models.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_api/v1_payload/router.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_customizer/__init__.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_customizer/__main__.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_customizer/browser_automation.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_customizer/customizer.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_customizer/exceptions.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_customizer/k8s.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_customizer/log.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_customizer/m365.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_customizer/manager_push.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_customizer/payload.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_customizer/payload_list.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_customizer/salesforce.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_customizer/servicenow.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_customizer/successfactors.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_customizer/translate.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_customizer/version.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_maintenance_page/__init__.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_maintenance_page/__main__.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_maintenance_page/app.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_maintenance_page/settings.py +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_maintenance_page/static/favicon.avif +0 -0
- {pyxecm-3.8.2 → pyxecm-3.8.3}/src/pyxecm_maintenance_page/templates/maintenance.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pyxecm
|
|
3
|
-
Version: 3.8.
|
|
3
|
+
Version: 3.8.3
|
|
4
4
|
Summary: A Python library to interact with Opentext Content Management Rest API
|
|
5
5
|
Project-URL: Homepage, https://github.com/opentext/pyxecm
|
|
6
6
|
Author-email: Kai Gatzweiler <kgatzweiler@opentext.com>, "Dr. Marc Diefenbruch" <mdiefenb@opentext.com>
|
|
@@ -1541,9 +1541,7 @@ class Guidewire:
|
|
|
1541
1541
|
policies = response.get("data", [])
|
|
1542
1542
|
if filter_field and filter_value is not None and isinstance(policies, list):
|
|
1543
1543
|
response["data"] = [
|
|
1544
|
-
policy
|
|
1545
|
-
for policy in policies
|
|
1546
|
-
if policy.get("attributes", {}).get(filter_field) == filter_value
|
|
1544
|
+
policy for policy in policies if policy.get("attributes", {}).get(filter_field) == filter_value
|
|
1547
1545
|
]
|
|
1548
1546
|
response["count"] = len(response["data"])
|
|
1549
1547
|
|
|
@@ -119,6 +119,8 @@ from lxml import etree
|
|
|
119
119
|
from pydantic import BaseModel, ValidationError
|
|
120
120
|
|
|
121
121
|
from .sap_models import (
|
|
122
|
+
SAPAuthCheckState,
|
|
123
|
+
SAPAuthCheckStatusModel,
|
|
122
124
|
SAPBusinessPartnerQueryModel,
|
|
123
125
|
SAPBusinessPartnerResponseModel,
|
|
124
126
|
SAPCompanyCodeQueryModel,
|
|
@@ -302,7 +304,8 @@ ODATA_V4_SERVICE_ROOT_PATTERN = re.compile(r"^(.*?/sap/opu/odata4/(?:[^/]+/){5}\
|
|
|
302
304
|
|
|
303
305
|
|
|
304
306
|
# Registry of the OData services this module uses. Extend this if
|
|
305
|
-
# new SAP APIs are supported by the SAP class
|
|
307
|
+
# new SAP APIs are supported by the SAP class. The OAuth scope IDs are
|
|
308
|
+
# generated by SAP and can differ between SAP systems - see get_oauth_scope():
|
|
306
309
|
SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
307
310
|
"API_MAINTENANCEORDER": SAPODataServiceModel(
|
|
308
311
|
name="API_MAINTENANCEORDER",
|
|
@@ -310,6 +313,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
310
313
|
# deprecated version 0001 - both are identical for the fields we use:
|
|
311
314
|
path="/sap/opu/odata/sap/API_MAINTENANCEORDER;v=0002",
|
|
312
315
|
odata_version=2,
|
|
316
|
+
oauth_scope="ZAPI_MAINTENANCEORDER_0002",
|
|
313
317
|
entity_sets=["MaintenanceOrder", "MaintenanceOrderOperation"],
|
|
314
318
|
description="Plant Maintenance - Maintenance Order (Create, Read, Update)",
|
|
315
319
|
),
|
|
@@ -318,6 +322,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
318
322
|
name="API_MAINTNOTIFICATION",
|
|
319
323
|
path="/sap/opu/odata/sap/API_MAINTNOTIFICATION",
|
|
320
324
|
odata_version=2,
|
|
325
|
+
oauth_scope="ZAPI_MAINTNOTIFICATION_0001",
|
|
321
326
|
entity_sets=["MaintenanceNotification", "MaintenanceNotificationItem"],
|
|
322
327
|
description="Plant Maintenance - Maintenance Notification (Create, Read, Update)",
|
|
323
328
|
),
|
|
@@ -325,6 +330,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
325
330
|
name="API_SALES_ORDER_SRV",
|
|
326
331
|
path="/sap/opu/odata/sap/API_SALES_ORDER_SRV",
|
|
327
332
|
odata_version=2,
|
|
333
|
+
oauth_scope="ZAPI_SALES_ORDER_SRV_0001",
|
|
328
334
|
entity_sets=["A_SalesOrder", "A_SalesOrderItem"],
|
|
329
335
|
description="Sales - Sales Order (Create, Read, Update, Delete)",
|
|
330
336
|
),
|
|
@@ -332,6 +338,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
332
338
|
name="API_SALES_CONTRACT_SRV",
|
|
333
339
|
path="/sap/opu/odata/sap/API_SALES_CONTRACT_SRV",
|
|
334
340
|
odata_version=2,
|
|
341
|
+
oauth_scope="ZAPI_SALES_CONTRACT_SRV_0001",
|
|
335
342
|
entity_sets=["A_SalesContract", "A_SalesContractItem"],
|
|
336
343
|
description="Sales - Sales Contract (Create, Read, Update, Delete)",
|
|
337
344
|
),
|
|
@@ -341,6 +348,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
341
348
|
name="API_PURCHASECONTRACT_PROCESS_SRV",
|
|
342
349
|
path="/sap/opu/odata/sap/API_PURCHASECONTRACT_PROCESS_SRV",
|
|
343
350
|
odata_version=2,
|
|
351
|
+
oauth_scope="ZAPI_PURCHASECONTRACT_PROCESS_SRV_0001",
|
|
344
352
|
entity_sets=["A_PurchaseContract", "A_PurchaseContractItem"],
|
|
345
353
|
description="Purchasing - Purchase Contract (Create, Read, Update)",
|
|
346
354
|
),
|
|
@@ -350,6 +358,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
350
358
|
name="API_PURCHASEREQUISITION_2",
|
|
351
359
|
path="/sap/opu/odata4/sap/api_purchaserequisition_2/srvd_a2x/sap/purchaserequisition/0001",
|
|
352
360
|
odata_version=4,
|
|
361
|
+
oauth_scope="OV4:API_PURCHASEREQUISITION_2",
|
|
353
362
|
entity_sets=["PurchaseReqn", "PurchaseReqnItem", "PurchaseReqnAcctAssgmt"],
|
|
354
363
|
description="Purchasing - Purchase Requisition (Create, Read, Update)",
|
|
355
364
|
),
|
|
@@ -357,6 +366,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
357
366
|
name="API_MATERIAL_DOCUMENT_SRV",
|
|
358
367
|
path="/sap/opu/odata/sap/API_MATERIAL_DOCUMENT_SRV",
|
|
359
368
|
odata_version=2,
|
|
369
|
+
oauth_scope="ZAPI_MATERIAL_DOCUMENT_SRV_0001",
|
|
360
370
|
entity_sets=["A_MaterialDocumentHeader", "A_MaterialDocumentItem"],
|
|
361
371
|
description="Inventory Management - Goods Movement / Material Document (Create, Read, Cancel)",
|
|
362
372
|
),
|
|
@@ -365,6 +375,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
365
375
|
name="API_MAINTORDERCONFIRMATION",
|
|
366
376
|
path="/sap/opu/odata/sap/API_MAINTORDERCONFIRMATION",
|
|
367
377
|
odata_version=2,
|
|
378
|
+
oauth_scope="ZAPI_MAINTORDERCONFIRMATION_0001",
|
|
368
379
|
entity_sets=["MaintOrderConfirmation"],
|
|
369
380
|
description="Plant Maintenance - Maintenance Order Confirmation (Create, Read, Cancel)",
|
|
370
381
|
),
|
|
@@ -372,6 +383,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
372
383
|
name="API_MAINTENANCEPLAN",
|
|
373
384
|
path="/sap/opu/odata/sap/API_MAINTENANCEPLAN",
|
|
374
385
|
odata_version=2,
|
|
386
|
+
oauth_scope="ZAPI_MAINTENANCEPLAN_0001",
|
|
375
387
|
entity_sets=["MaintenancePlan", "MaintenancePlanCycle", "MaintenanceItem"],
|
|
376
388
|
description="Plant Maintenance - Maintenance Plan (Create, Read, Update, Scheduling)",
|
|
377
389
|
),
|
|
@@ -381,6 +393,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
381
393
|
name="API_PLANNEDORDER",
|
|
382
394
|
path="/sap/opu/odata4/sap/api_plannedorder/srvd_a2x/sap/plannedorder/0001",
|
|
383
395
|
odata_version=4,
|
|
396
|
+
oauth_scope="OV4:API_PLANNEDORDER",
|
|
384
397
|
entity_sets=["PlannedOrderHeader"],
|
|
385
398
|
description="Production Planning - Planned Order (Create, Read, Update, Delete, Schedule)",
|
|
386
399
|
),
|
|
@@ -388,6 +401,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
388
401
|
name="API_PRODUCTION_ORDER_2_SRV",
|
|
389
402
|
path="/sap/opu/odata/sap/API_PRODUCTION_ORDER_2_SRV",
|
|
390
403
|
odata_version=2,
|
|
404
|
+
oauth_scope="ZAPI_PRODUCTION_ORDER_2_SRV_0001",
|
|
391
405
|
entity_sets=["A_ProductionOrder_2", "A_ProductionOrderOperation_2", "A_ProductionOrderStatus_2"],
|
|
392
406
|
description="Production Planning - Production Order (Create, Read, Update, Convert, Status)",
|
|
393
407
|
),
|
|
@@ -396,6 +410,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
396
410
|
name="API_PLANT_2",
|
|
397
411
|
path="/sap/opu/odata4/sap/api_plant_2/srvd_a2x/sap/plant/0001",
|
|
398
412
|
odata_version=4,
|
|
413
|
+
oauth_scope="OV4:API_PLANT_2",
|
|
399
414
|
entity_sets=["Plant"],
|
|
400
415
|
description="Master Data - Plant (Read)",
|
|
401
416
|
),
|
|
@@ -403,6 +418,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
403
418
|
name="API_STORAGELOCATION_SRV",
|
|
404
419
|
path="/sap/opu/odata/sap/API_STORAGELOCATION_SRV",
|
|
405
420
|
odata_version=2,
|
|
421
|
+
oauth_scope="ZAPI_STORAGELOCATION_SRV_0001",
|
|
406
422
|
entity_sets=["StorageLocation"],
|
|
407
423
|
description="Master Data - Storage Location (Read)",
|
|
408
424
|
),
|
|
@@ -410,6 +426,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
410
426
|
name="API_PRODUCT_SRV",
|
|
411
427
|
path="/sap/opu/odata/sap/API_PRODUCT_SRV",
|
|
412
428
|
odata_version=2,
|
|
429
|
+
oauth_scope="ZAPI_PRODUCT_SRV_0001",
|
|
413
430
|
entity_sets=["A_Product", "A_ProductPlant", "A_ProductDescription"],
|
|
414
431
|
description="Master Data - Product / Material (Read)",
|
|
415
432
|
),
|
|
@@ -417,6 +434,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
417
434
|
name="API_PRODUCTION_VERSION",
|
|
418
435
|
path="/sap/opu/odata4/sap/api_production_version/srvd_a2x/sap/productionversion/0001",
|
|
419
436
|
odata_version=4,
|
|
437
|
+
oauth_scope="OV4:API_PRODUCTION_VERSION",
|
|
420
438
|
entity_sets=["ProductionVersion"],
|
|
421
439
|
description="Master Data - Production Version (Read)",
|
|
422
440
|
),
|
|
@@ -424,6 +442,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
424
442
|
name="API_WORK_CENTERS",
|
|
425
443
|
path="/sap/opu/odata/sap/API_WORK_CENTERS",
|
|
426
444
|
odata_version=2,
|
|
445
|
+
oauth_scope="ZAPI_WORK_CENTERS_0001",
|
|
427
446
|
entity_sets=["A_WorkCenters"],
|
|
428
447
|
description="Master Data - Work Center (Read)",
|
|
429
448
|
),
|
|
@@ -431,6 +450,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
431
450
|
name="API_EQUIPMENT",
|
|
432
451
|
path="/sap/opu/odata/sap/API_EQUIPMENT",
|
|
433
452
|
odata_version=2,
|
|
453
|
+
oauth_scope="ZAPI_EQUIPMENT_0001",
|
|
434
454
|
entity_sets=["Equipment"],
|
|
435
455
|
description="Master Data - Equipment (Read)",
|
|
436
456
|
),
|
|
@@ -438,6 +458,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
438
458
|
name="API_FUNCTIONALLOCATION",
|
|
439
459
|
path="/sap/opu/odata/sap/API_FUNCTIONALLOCATION",
|
|
440
460
|
odata_version=2,
|
|
461
|
+
oauth_scope="ZAPI_FUNCTIONALLOCATION_0001",
|
|
441
462
|
entity_sets=["FunctionalLocation"],
|
|
442
463
|
description="Master Data - Functional Location (Read)",
|
|
443
464
|
),
|
|
@@ -445,6 +466,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
445
466
|
name="API_BUSINESS_PARTNER",
|
|
446
467
|
path="/sap/opu/odata/sap/API_BUSINESS_PARTNER",
|
|
447
468
|
odata_version=2,
|
|
469
|
+
oauth_scope="ZAPI_BUSINESS_PARTNER_0001",
|
|
448
470
|
entity_sets=["A_BusinessPartner", "A_BusinessPartnerRole"],
|
|
449
471
|
description="Master Data - Business Partner (Read)",
|
|
450
472
|
),
|
|
@@ -452,6 +474,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
452
474
|
name="API_COMPANYCODE_SRV",
|
|
453
475
|
path="/sap/opu/odata/sap/API_COMPANYCODE_SRV",
|
|
454
476
|
odata_version=2,
|
|
477
|
+
oauth_scope="ZAPI_COMPANYCODE_SRV_0001",
|
|
455
478
|
entity_sets=["A_CompanyCode"],
|
|
456
479
|
description="Master Data - Company Code (Read)",
|
|
457
480
|
),
|
|
@@ -459,6 +482,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
459
482
|
name="API_SALESORGANIZATION_SRV",
|
|
460
483
|
path="/sap/opu/odata/sap/API_SALESORGANIZATION_SRV",
|
|
461
484
|
odata_version=2,
|
|
485
|
+
oauth_scope="ZAPI_SALESORGANIZATION_SRV_0001",
|
|
462
486
|
entity_sets=["A_SalesOrganization"],
|
|
463
487
|
description="Master Data - Sales Organization (Read)",
|
|
464
488
|
),
|
|
@@ -466,6 +490,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
466
490
|
name="API_PURCHASINGORGANIZATION",
|
|
467
491
|
path="/sap/opu/odata4/sap/api_purchasingorganization/srvd_a2x/sap/purchasingorganization/0001",
|
|
468
492
|
odata_version=4,
|
|
493
|
+
oauth_scope="OV4:API_PURCHASINGORGANIZATION",
|
|
469
494
|
entity_sets=["A_PurchasingOrganization"],
|
|
470
495
|
description="Master Data - Purchasing Organization (Read)",
|
|
471
496
|
),
|
|
@@ -473,6 +498,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
473
498
|
name="API_COSTCENTER_SRV",
|
|
474
499
|
path="/sap/opu/odata/sap/API_COSTCENTER_SRV",
|
|
475
500
|
odata_version=2,
|
|
501
|
+
oauth_scope="ZAPI_COSTCENTER_SRV_0001",
|
|
476
502
|
entity_sets=["A_CostCenter"],
|
|
477
503
|
description="Master Data - Cost Center (Read)",
|
|
478
504
|
),
|
|
@@ -480,6 +506,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
480
506
|
name="API_PROFITCENTER_SRV",
|
|
481
507
|
path="/sap/opu/odata/sap/API_PROFITCENTER_SRV",
|
|
482
508
|
odata_version=2,
|
|
509
|
+
oauth_scope="ZAPI_PROFITCENTER_SRV_0001",
|
|
483
510
|
entity_sets=["A_ProfitCenter"],
|
|
484
511
|
description="Master Data - Profit Center (Read)",
|
|
485
512
|
),
|
|
@@ -487,6 +514,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
487
514
|
name="API_GLACCOUNTINCHARTOFACCOUNTS_SRV",
|
|
488
515
|
path="/sap/opu/odata/sap/API_GLACCOUNTINCHARTOFACCOUNTS_SRV",
|
|
489
516
|
odata_version=2,
|
|
517
|
+
oauth_scope="ZAPI_GLACCOUNTINCHARTOFACCOUNTS_SRV_0001",
|
|
490
518
|
entity_sets=["A_GLAccountInChartOfAccounts"],
|
|
491
519
|
description="Master Data - G/L Account in Chart of Accounts (Read)",
|
|
492
520
|
),
|
|
@@ -497,6 +525,7 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
497
525
|
name="API_PURCHASEORDER_2",
|
|
498
526
|
path="/sap/opu/odata4/sap/api_purchaseorder_2/srvd_a2x/sap/purchaseorder/0001",
|
|
499
527
|
odata_version=4,
|
|
528
|
+
oauth_scope="OV4:API_PURCHASEORDER_2",
|
|
500
529
|
entity_sets=[
|
|
501
530
|
"PurchaseOrder",
|
|
502
531
|
"PurchaseOrderItem",
|
|
@@ -507,6 +536,32 @@ SAP_ODATA_SERVICES: dict[str, SAPODataServiceModel] = {
|
|
|
507
536
|
),
|
|
508
537
|
}
|
|
509
538
|
|
|
539
|
+
|
|
540
|
+
def get_oauth_scope(services: list[str] | None = None) -> str:
|
|
541
|
+
"""Build the scope parameter of the OAuth token request from SAP_ODATA_SERVICES.
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
services (list[str] | None, optional):
|
|
545
|
+
Names of the OData services in SAP_ODATA_SERVICES to get the scopes for.
|
|
546
|
+
Defaults to None (all services in SAP_ODATA_SERVICES).
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
str:
|
|
550
|
+
Space-separated scope IDs, e.g. "ZAPI_MAINTENANCEORDER_0002 ZAPI_MAINTNOTIFICATION_0001".
|
|
551
|
+
Services that are not registered or have no scope are skipped.
|
|
552
|
+
|
|
553
|
+
"""
|
|
554
|
+
|
|
555
|
+
if services is None:
|
|
556
|
+
services = list(SAP_ODATA_SERVICES)
|
|
557
|
+
|
|
558
|
+
return " ".join(
|
|
559
|
+
SAP_ODATA_SERVICES[service].oauth_scope
|
|
560
|
+
for service in services
|
|
561
|
+
if service in SAP_ODATA_SERVICES and SAP_ODATA_SERVICES[service].oauth_scope
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
|
|
510
565
|
# OData V4 transmits Edm.Decimal values (quantities, prices) as JSON numbers by default.
|
|
511
566
|
# With IEEE754Compatible=true they are transmitted as strings, which avoids any loss of
|
|
512
567
|
# precision and matches Python's Decimal (pydantic serializes Decimal as string):
|
|
@@ -1998,6 +2053,278 @@ class SAP:
|
|
|
1998
2053
|
|
|
1999
2054
|
# end method definition
|
|
2000
2055
|
|
|
2056
|
+
def check_oauth_token_endpoint(self, timeout: float = REQUEST_TIMEOUT) -> SAPAuthCheckStatusModel:
|
|
2057
|
+
"""Check if SAP's OAuth 2.0 token endpoint is activated and reachable.
|
|
2058
|
+
|
|
2059
|
+
This is the first step of the SAML 2.0 bearer assertion flow used for principal
|
|
2060
|
+
propagation (see get_oauth_access_token()) - before any OAuth client, SAML trust
|
|
2061
|
+
or user is involved. An intentionally incomplete token request without client
|
|
2062
|
+
credentials and grant_type is sent. A live endpoint answers it with HTTP 401 (the
|
|
2063
|
+
ICF node requires the client authentication) or a well-formed OAuth error (e.g.
|
|
2064
|
+
"invalid_request"), which is enough to tell apart the typical setup problems:
|
|
2065
|
+
* The ICF node /sap/bc/sec/oauth2/token is not active (transaction SICF).
|
|
2066
|
+
* The SAP system is not reachable at all (DNS, firewall, TLS, timeout).
|
|
2067
|
+
* The base URL points to something else (e.g. a login page of a reverse proxy).
|
|
2068
|
+
|
|
2069
|
+
Args:
|
|
2070
|
+
timeout (float, optional):
|
|
2071
|
+
Timeout for the check request in seconds. Defaults to REQUEST_TIMEOUT.
|
|
2072
|
+
|
|
2073
|
+
Returns:
|
|
2074
|
+
SAPAuthCheckStatusModel:
|
|
2075
|
+
The result of the check. Use status.ok to check the result.
|
|
2076
|
+
|
|
2077
|
+
Example:
|
|
2078
|
+
>>> status = sap.check_oauth_token_endpoint()
|
|
2079
|
+
>>> if not status.ok:
|
|
2080
|
+
... print(status.hint)
|
|
2081
|
+
|
|
2082
|
+
"""
|
|
2083
|
+
|
|
2084
|
+
name = "OAuth 2.0 token endpoint"
|
|
2085
|
+
|
|
2086
|
+
if not self._base_url:
|
|
2087
|
+
return SAPAuthCheckStatusModel(
|
|
2088
|
+
name=name,
|
|
2089
|
+
state=SAPAuthCheckState.NOT_CONFIGURED,
|
|
2090
|
+
message="The SAP object has been initialized without a base URL.",
|
|
2091
|
+
hint="Set base_url to enable OData / OAuth checks.",
|
|
2092
|
+
)
|
|
2093
|
+
|
|
2094
|
+
token_url = self.oauth_token_url()
|
|
2095
|
+
self.logger.info("Checking SAP OAuth 2.0 token endpoint at -> %s ...", token_url)
|
|
2096
|
+
|
|
2097
|
+
try:
|
|
2098
|
+
response = requests.post(
|
|
2099
|
+
url=token_url,
|
|
2100
|
+
data={},
|
|
2101
|
+
headers={"User-Agent": USER_AGENT, "Accept": "application/json"},
|
|
2102
|
+
verify=self._verify_ssl,
|
|
2103
|
+
timeout=timeout,
|
|
2104
|
+
)
|
|
2105
|
+
except requests.exceptions.SSLError as ssl_error:
|
|
2106
|
+
status = SAPAuthCheckStatusModel(
|
|
2107
|
+
name=name,
|
|
2108
|
+
state=SAPAuthCheckState.UNREACHABLE,
|
|
2109
|
+
message=str(ssl_error),
|
|
2110
|
+
hint="TLS handshake failed. Check the certificate of the SAP system or set verify_ssl=False "
|
|
2111
|
+
"for test systems with self-signed certificates.",
|
|
2112
|
+
)
|
|
2113
|
+
except requests.exceptions.RequestException as request_error:
|
|
2114
|
+
status = SAPAuthCheckStatusModel(
|
|
2115
|
+
name=name,
|
|
2116
|
+
state=SAPAuthCheckState.UNREACHABLE,
|
|
2117
|
+
message=str(request_error),
|
|
2118
|
+
hint="Check the base URL (protocol, hostname, HTTPS port), DNS resolution and firewall rules.",
|
|
2119
|
+
)
|
|
2120
|
+
else:
|
|
2121
|
+
try:
|
|
2122
|
+
body = response.json()
|
|
2123
|
+
except ValueError:
|
|
2124
|
+
body = {}
|
|
2125
|
+
if not isinstance(body, dict):
|
|
2126
|
+
body = {}
|
|
2127
|
+
|
|
2128
|
+
if response.status_code == HTTPStatus.NOT_FOUND or (
|
|
2129
|
+
response.status_code == HTTPStatus.FORBIDDEN and "service cannot be reached" in response.text.lower()
|
|
2130
|
+
):
|
|
2131
|
+
# SAP answers 404 for an unknown path and 403 "Service cannot be reached" for an inactive ICF node:
|
|
2132
|
+
status = SAPAuthCheckStatusModel(
|
|
2133
|
+
name=name,
|
|
2134
|
+
state=SAPAuthCheckState.NOT_ACTIVATED,
|
|
2135
|
+
http_status=response.status_code,
|
|
2136
|
+
message="HTTP {} - endpoint not found or not active".format(response.status_code),
|
|
2137
|
+
hint="Activate the ICF node /sap/bc/sec/oauth2/token in transaction SICF.",
|
|
2138
|
+
)
|
|
2139
|
+
elif response.status_code == HTTPStatus.UNAUTHORIZED:
|
|
2140
|
+
# The ICF node authenticates the OAuth client before the OAuth handler runs, so a live
|
|
2141
|
+
# endpoint rejects this request without credentials:
|
|
2142
|
+
status = SAPAuthCheckStatusModel(
|
|
2143
|
+
name=name,
|
|
2144
|
+
state=SAPAuthCheckState.OK,
|
|
2145
|
+
http_status=response.status_code,
|
|
2146
|
+
message="HTTP 401 - endpoint is active and requires the OAuth client authentication",
|
|
2147
|
+
)
|
|
2148
|
+
elif "error" in body:
|
|
2149
|
+
# Any well-formed OAuth error (e.g. "invalid_request" for the missing grant_type)
|
|
2150
|
+
# proves the endpoint itself is live and speaking OAuth 2.0:
|
|
2151
|
+
status = SAPAuthCheckStatusModel(
|
|
2152
|
+
name=name,
|
|
2153
|
+
state=SAPAuthCheckState.OK,
|
|
2154
|
+
http_status=response.status_code,
|
|
2155
|
+
message="{} - {}".format(body.get("error"), body.get("error_description", "")),
|
|
2156
|
+
)
|
|
2157
|
+
else:
|
|
2158
|
+
status = SAPAuthCheckStatusModel(
|
|
2159
|
+
name=name,
|
|
2160
|
+
state=SAPAuthCheckState.ERROR,
|
|
2161
|
+
http_status=response.status_code,
|
|
2162
|
+
message="HTTP {} - unexpected response (not a JSON OAuth error)".format(response.status_code),
|
|
2163
|
+
hint="Check that the base URL points to the SAP Gateway and not to a login page or reverse proxy.",
|
|
2164
|
+
)
|
|
2165
|
+
|
|
2166
|
+
if status.ok:
|
|
2167
|
+
self.logger.info("SAP OAuth 2.0 token endpoint is active; message -> %s", status.message)
|
|
2168
|
+
else:
|
|
2169
|
+
self.logger.error(
|
|
2170
|
+
"SAP OAuth 2.0 token endpoint is not usable; state -> '%s'; message -> %s; hint -> %s",
|
|
2171
|
+
status.state.value,
|
|
2172
|
+
status.message,
|
|
2173
|
+
status.hint,
|
|
2174
|
+
)
|
|
2175
|
+
|
|
2176
|
+
return status
|
|
2177
|
+
|
|
2178
|
+
# end method definition
|
|
2179
|
+
|
|
2180
|
+
def check_principal_propagation(
|
|
2181
|
+
self,
|
|
2182
|
+
user_id: str,
|
|
2183
|
+
client_id: str,
|
|
2184
|
+
client_secret: str,
|
|
2185
|
+
scope: str | None = None,
|
|
2186
|
+
nameid_format: str = SAML_NAMEID_FORMAT_UNSPECIFIED,
|
|
2187
|
+
recipient: str | None = None,
|
|
2188
|
+
) -> SAPAuthCheckStatusModel:
|
|
2189
|
+
"""Check the full SAML 2.0 bearer assertion / OAuth 2.0 principal propagation round trip.
|
|
2190
|
+
|
|
2191
|
+
Runs exactly what get_oauth_access_token_for_user() does for a real user: mint a
|
|
2192
|
+
SAML 2.0 assertion for user_id (get_saml_assertion()) and exchange it for an SAP
|
|
2193
|
+
access token (get_oauth_access_token()). Use this with a dedicated test user to
|
|
2194
|
+
verify the setup end-to-end - unlike check_oauth_token_endpoint() this needs a working OAuth
|
|
2195
|
+
client (SOAUTH2), a trusted SAML 2.0 identity provider (SAML2) and a valid NameID
|
|
2196
|
+
mapping for user_id. The message of a failed check (SAP's own OAuth error, e.g.
|
|
2197
|
+
"invalid_client", "invalid_grant", "unauthorized_client" or "invalid_scope") is kept
|
|
2198
|
+
as returned - see the hint for what it typically means.
|
|
2199
|
+
|
|
2200
|
+
Args:
|
|
2201
|
+
user_id (str):
|
|
2202
|
+
The user to propagate, e.g. a dedicated technical test user. See
|
|
2203
|
+
get_saml_assertion() for the security implications.
|
|
2204
|
+
client_id (str):
|
|
2205
|
+
The OAuth 2.0 client ID registered in SOAUTH2.
|
|
2206
|
+
client_secret (str):
|
|
2207
|
+
The password of the SU01 system user behind that OAuth client.
|
|
2208
|
+
scope (str | None, optional):
|
|
2209
|
+
Space-separated scope IDs to request, e.g. "API_MAINTENANCEORDER_0002".
|
|
2210
|
+
Defaults to None (the default scopes of the OAuth client).
|
|
2211
|
+
nameid_format (str, optional):
|
|
2212
|
+
The NameID format - must match transaction SAML2. Defaults to
|
|
2213
|
+
"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified".
|
|
2214
|
+
recipient (str | None, optional):
|
|
2215
|
+
The Recipient of the assertion - only needed behind a reverse proxy (see
|
|
2216
|
+
get_saml_assertion()). Defaults to None = oauth_token_url().
|
|
2217
|
+
|
|
2218
|
+
Returns:
|
|
2219
|
+
SAPAuthCheckStatusModel:
|
|
2220
|
+
The result of the check. Use status.ok to check the result.
|
|
2221
|
+
|
|
2222
|
+
Example:
|
|
2223
|
+
>>> status = sap.check_principal_propagation(user_id="jane.smith", client_id="PYXECM_OAUTH", client_secret="...")
|
|
2224
|
+
>>> if not status.ok:
|
|
2225
|
+
... print(status.message, "-", status.hint)
|
|
2226
|
+
|
|
2227
|
+
"""
|
|
2228
|
+
|
|
2229
|
+
name = "SAML 2.0 / OAuth 2.0 principal propagation"
|
|
2230
|
+
|
|
2231
|
+
if not self._base_url:
|
|
2232
|
+
return SAPAuthCheckStatusModel(
|
|
2233
|
+
name=name,
|
|
2234
|
+
state=SAPAuthCheckState.NOT_CONFIGURED,
|
|
2235
|
+
message="The SAP object has been initialized without a base URL.",
|
|
2236
|
+
hint="Set base_url to enable OData / OAuth checks.",
|
|
2237
|
+
)
|
|
2238
|
+
|
|
2239
|
+
# Same two steps as get_oauth_access_token_for_user() - separately, so a problem of
|
|
2240
|
+
# pyxecm's own identity provider configuration is not mistaken for an SAP-side error:
|
|
2241
|
+
assertion = self.get_saml_assertion(user_id=user_id, nameid_format=nameid_format, recipient=recipient)
|
|
2242
|
+
if not assertion:
|
|
2243
|
+
status = SAPAuthCheckStatusModel(
|
|
2244
|
+
name=name,
|
|
2245
|
+
state=SAPAuthCheckState.NOT_CONFIGURED
|
|
2246
|
+
if self._saml_idp_private_key is None
|
|
2247
|
+
else SAPAuthCheckState.ERROR,
|
|
2248
|
+
message=self.last_error or "unknown error",
|
|
2249
|
+
hint="Check saml_idp_private_key, saml_idp_certificate, saml_issuer and saml_audience of the SAP "
|
|
2250
|
+
"object and the user_id - no request has been sent to SAP.",
|
|
2251
|
+
)
|
|
2252
|
+
self.logger.error(
|
|
2253
|
+
"%s failed; state -> '%s'; message -> %s; hint -> %s",
|
|
2254
|
+
name,
|
|
2255
|
+
status.state.value,
|
|
2256
|
+
status.message,
|
|
2257
|
+
status.hint,
|
|
2258
|
+
)
|
|
2259
|
+
return status
|
|
2260
|
+
|
|
2261
|
+
token = self.get_oauth_access_token(
|
|
2262
|
+
assertion=assertion, client_id=client_id, client_secret=client_secret, scope=scope
|
|
2263
|
+
)
|
|
2264
|
+
if token:
|
|
2265
|
+
status = SAPAuthCheckStatusModel(
|
|
2266
|
+
name=name,
|
|
2267
|
+
state=SAPAuthCheckState.OK,
|
|
2268
|
+
message="Received access token for user -> '{}' (scope -> '{}', expires in -> {}s)".format(
|
|
2269
|
+
user_id,
|
|
2270
|
+
token.get("scope", ""),
|
|
2271
|
+
token.get("expires_in", "?"),
|
|
2272
|
+
),
|
|
2273
|
+
)
|
|
2274
|
+
self.logger.info("%s succeeded; message -> %s", name, status.message)
|
|
2275
|
+
return status
|
|
2276
|
+
|
|
2277
|
+
error = self.last_error or "unknown error"
|
|
2278
|
+
error_lower = error.lower()
|
|
2279
|
+
# Wrong client credentials fail the logon of the ICF node already - SAP then answers
|
|
2280
|
+
# HTTP 401 without an OAuth error (see get_oauth_access_token() for the format):
|
|
2281
|
+
if "invalid_client" in error_lower or "status -> 401/" in error_lower:
|
|
2282
|
+
state, hint = (
|
|
2283
|
+
SAPAuthCheckState.REJECTED,
|
|
2284
|
+
(
|
|
2285
|
+
"Check client_id/client_secret - they must match an SU01 system user with grant type "
|
|
2286
|
+
"'SAML 2.0 Bearer Assertion' configured in transaction SOAUTH2."
|
|
2287
|
+
),
|
|
2288
|
+
)
|
|
2289
|
+
elif "invalid_grant" in error_lower:
|
|
2290
|
+
state, hint = (
|
|
2291
|
+
SAPAuthCheckState.REJECTED,
|
|
2292
|
+
(
|
|
2293
|
+
"SAP rejected the SAML assertion. Check the trusted identity provider (signing certificate, "
|
|
2294
|
+
"issuer, audience) in transaction SAML2 and that user_id resolves to an SAP user (NameID mapping)."
|
|
2295
|
+
),
|
|
2296
|
+
)
|
|
2297
|
+
elif "unauthorized_client" in error_lower:
|
|
2298
|
+
state, hint = (
|
|
2299
|
+
SAPAuthCheckState.REJECTED,
|
|
2300
|
+
(
|
|
2301
|
+
"The OAuth client is not authorized for the 'SAML 2.0 Bearer Assertion' grant type - enable it "
|
|
2302
|
+
"in transaction SOAUTH2."
|
|
2303
|
+
),
|
|
2304
|
+
)
|
|
2305
|
+
elif "invalid_scope" in error_lower:
|
|
2306
|
+
state, hint = (
|
|
2307
|
+
SAPAuthCheckState.REJECTED,
|
|
2308
|
+
(
|
|
2309
|
+
"The requested scope is not assigned to the OAuth client. Enable OAuth for the OData service(s) "
|
|
2310
|
+
"in /IWFND/MAINT_SERVICE and assign the scope(s) to the client in SOAUTH2."
|
|
2311
|
+
),
|
|
2312
|
+
)
|
|
2313
|
+
else:
|
|
2314
|
+
state, hint = (
|
|
2315
|
+
SAPAuthCheckState.ERROR,
|
|
2316
|
+
"Check the SAP Gateway error log (transaction /IWFND/ERROR_LOG) and the system log (SM21).",
|
|
2317
|
+
)
|
|
2318
|
+
|
|
2319
|
+
status = SAPAuthCheckStatusModel(name=name, state=state, message=error, hint=hint)
|
|
2320
|
+
self.logger.error(
|
|
2321
|
+
"%s failed; state -> '%s'; message -> %s; hint -> %s", name, status.state.value, status.message, status.hint
|
|
2322
|
+
)
|
|
2323
|
+
|
|
2324
|
+
return status
|
|
2325
|
+
|
|
2326
|
+
# end method definition
|
|
2327
|
+
|
|
2001
2328
|
@staticmethod
|
|
2002
2329
|
def _quote_key(value: str) -> str:
|
|
2003
2330
|
"""Escape a value for use in an OData key or filter string literal (single quotes are doubled).
|
|
@@ -906,6 +906,10 @@ class SAPODataServiceModel(BaseModel):
|
|
|
906
906
|
e.g. "/sap/opu/odata/sap/API_MAINTENANCEORDER".
|
|
907
907
|
odata_version (int):
|
|
908
908
|
OData protocol version of the service (2 or 4).
|
|
909
|
+
oauth_scope (str):
|
|
910
|
+
OAuth 2.0 scope ID of the service, e.g. "ZAPI_MAINTENANCEORDER_0002". SAP generates
|
|
911
|
+
it when OAuth is enabled for the service - it can differ between SAP systems.
|
|
912
|
+
Empty if the service has no scope.
|
|
909
913
|
entity_sets (list[str]):
|
|
910
914
|
Entity sets that must be exposed by the service. Used by check_odata_apis()
|
|
911
915
|
to detect services that are active but do not expose what we need
|
|
@@ -918,6 +922,7 @@ class SAPODataServiceModel(BaseModel):
|
|
|
918
922
|
name: str
|
|
919
923
|
path: str
|
|
920
924
|
odata_version: int = Field(default=2, ge=2, le=4)
|
|
925
|
+
oauth_scope: str = ""
|
|
921
926
|
entity_sets: list[str] = Field(default_factory=list)
|
|
922
927
|
description: str = ""
|
|
923
928
|
|
|
@@ -980,6 +985,57 @@ class SAPODataServiceStatusModel(BaseModel):
|
|
|
980
985
|
# end method definition
|
|
981
986
|
|
|
982
987
|
|
|
988
|
+
class SAPAuthCheckState(StrEnum):
|
|
989
|
+
"""Possible results of an authentication / principal propagation health check."""
|
|
990
|
+
|
|
991
|
+
OK = "ok" # the checked step succeeded
|
|
992
|
+
NOT_CONFIGURED = (
|
|
993
|
+
"not_configured" # required constructor parameters are missing (e.g. base_url, saml_idp_private_key)
|
|
994
|
+
)
|
|
995
|
+
NOT_ACTIVATED = "not_activated" # the SAP endpoint is not activated/provisioned (ICF node, SOAUTH2, SAML2)
|
|
996
|
+
REJECTED = "rejected" # SAP rejected the request (wrong credentials, invalid client, invalid grant, ...)
|
|
997
|
+
UNREACHABLE = "unreachable" # network / DNS / TLS / timeout problem
|
|
998
|
+
ERROR = "error" # any other error
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
class SAPAuthCheckStatusModel(BaseModel):
|
|
1002
|
+
"""Result of a single step of an authentication / principal propagation health check.
|
|
1003
|
+
|
|
1004
|
+
Used by SAP.check_oauth_token_endpoint() and SAP.check_principal_propagation() - see there for the
|
|
1005
|
+
checks they perform.
|
|
1006
|
+
|
|
1007
|
+
Attributes:
|
|
1008
|
+
name (str):
|
|
1009
|
+
Short name of the checked step, e.g. "OAuth 2.0 token endpoint".
|
|
1010
|
+
state (SAPAuthCheckState):
|
|
1011
|
+
The result of the check.
|
|
1012
|
+
http_status (int | None):
|
|
1013
|
+
The HTTP status code of the check request, if one was sent.
|
|
1014
|
+
message (str):
|
|
1015
|
+
The (error) message returned by SAP or the request library.
|
|
1016
|
+
hint (str):
|
|
1017
|
+
A hint how to fix the problem (empty if the check succeeded).
|
|
1018
|
+
ok (bool):
|
|
1019
|
+
True if the check succeeded (computed - also included in model_dump() / model_dump_json()).
|
|
1020
|
+
|
|
1021
|
+
"""
|
|
1022
|
+
|
|
1023
|
+
name: str
|
|
1024
|
+
state: SAPAuthCheckState
|
|
1025
|
+
http_status: int | None = None
|
|
1026
|
+
message: str = ""
|
|
1027
|
+
hint: str = ""
|
|
1028
|
+
|
|
1029
|
+
@computed_field
|
|
1030
|
+
@property
|
|
1031
|
+
def ok(self) -> bool:
|
|
1032
|
+
"""Return True if the check succeeded."""
|
|
1033
|
+
|
|
1034
|
+
return self.state == SAPAuthCheckState.OK
|
|
1035
|
+
|
|
1036
|
+
# end method definition
|
|
1037
|
+
|
|
1038
|
+
|
|
983
1039
|
class SAPODataErrorDetailModel(BaseModel):
|
|
984
1040
|
"""A single detail message of an SAP OData error response."""
|
|
985
1041
|
|
|
@@ -500,8 +500,9 @@ class CustomizerSettingsSAP(BaseModel):
|
|
|
500
500
|
default="",
|
|
501
501
|
description=(
|
|
502
502
|
"Space-separated SAP OAuth 2.0 scope IDs requested for the access tokens, e.g. "
|
|
503
|
-
"'API_MAINTENANCEORDER_0002 API_MAINTNOTIFICATION_0001'.
|
|
504
|
-
"of the
|
|
503
|
+
"'API_MAINTENANCEORDER_0002 API_MAINTNOTIFICATION_0001'. Only required if the scope IDs "
|
|
504
|
+
"of the SAP system differ from the ones in SAP_ODATA_SERVICES (sap.py). Empty = the "
|
|
505
|
+
"scopes of all services in SAP_ODATA_SERVICES."
|
|
505
506
|
),
|
|
506
507
|
)
|
|
507
508
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|