From a3433a133c6e89340879226c08f73860b328c830 Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Mon, 16 Jun 2025 11:37:53 +0100 Subject: [PATCH 01/34] App Descriptor changes --- csar/Definitions/AppDescriptor.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csar/Definitions/AppDescriptor.yaml b/csar/Definitions/AppDescriptor.yaml index 0fb0a96..aa9af3f 100644 --- a/csar/Definitions/AppDescriptor.yaml +++ b/csar/Definitions/AppDescriptor.yaml @@ -7,4 +7,4 @@ APPComponent: NameofComponent: eric-oss-hello-world-python-app Version: VERSION Path: OtherDefinitions/ASD/eric-oss-hello-world-python-appASD.yaml - ArtefactType: Microservice + ArtefactType: ASD From 516b92f601e5a524a0eef142e9c93d4268f3f046 Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Tue, 17 Jun 2025 08:45:15 +0100 Subject: [PATCH 02/34] MTLS implementation changes --- .../templates/_helpers.tpl | 19 ++++++++- .../templates/deployment/deployment.yaml | 13 ++++++ .../values.yaml | 7 ++++ csar/Definitions/AppDescriptor.yaml | 14 ++++--- .../SecurityManagement/security-metadata.json | 3 ++ eric-oss-hello-world-python-app/config.py | 8 +++- eric-oss-hello-world-python-app/login.py | 41 ++++++++++++++----- requirements.txt | 2 +- 8 files changed, 89 insertions(+), 18 deletions(-) create mode 100644 csar/OtherDefinitions/SecurityManagement/security-metadata.json diff --git a/charts/eric-oss-hello-world-python-app/templates/_helpers.tpl b/charts/eric-oss-hello-world-python-app/templates/_helpers.tpl index b584d95..25f27e0 100644 --- a/charts/eric-oss-hello-world-python-app/templates/_helpers.tpl +++ b/charts/eric-oss-hello-world-python-app/templates/_helpers.tpl @@ -341,4 +341,21 @@ Define the annotations for security policy */}} {{- define "eric-oss-hello-world-python-app.securityPolicy.annotations" -}} # Automatically generated annotations for documentation purposes. -{{- end -}} \ No newline at end of file +{{- end -}} + +{{/* +Define the function to get the secret name + */}} +{{- define "eric-oss-hello-world-python-app.clientSecret" -}} +{{- $clientSecret := "" -}} +{{- if .Values.global }} + {{- if .Values.global.clientCredentials }} + {{- if .Values.global.clientCredentials.secret }} + {{- if .Values.global.clientCredentials.secret.name }} + {{- $clientSecret = .Values.global.clientCredentials.secret.name }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} +{{- print $clientSecret }} +{{- end }} \ No newline at end of file diff --git a/charts/eric-oss-hello-world-python-app/templates/deployment/deployment.yaml b/charts/eric-oss-hello-world-python-app/templates/deployment/deployment.yaml index 511d61e..78bd37c 100644 --- a/charts/eric-oss-hello-world-python-app/templates/deployment/deployment.yaml +++ b/charts/eric-oss-hello-world-python-app/templates/deployment/deployment.yaml @@ -63,6 +63,10 @@ spec: secret: secretName: {{ index .Values "appSecretName" | quote }} defaultMode: 420 + - name: client-creds + secret: + secretName: {{ include "eric-oss-hello-world-python-app.clientSecret" . | quote }} + defaultMode: 420 containers: - name: eric-oss-hello-world-python-app image: {{ template "eric-oss-hello-world-python-app.imagePath" (dict "imageId" "eric-oss-hello-world-python-app" "values" .Values "files" .Files) }} @@ -89,6 +93,9 @@ spec: - name: app-certs mountPath: {{ index .Values "appCertMountPath" | default .Values.instantiationDefaults.appCertMountPath | quote }} readOnly: true + - name: client-creds + mountPath: {{ index .Values "clientCredsMountPath" | default .Values.instantiationDefaults.clientCredsMountPath | quote }} + readOnly: true env: - name: IAM_CLIENT_ID value: {{ index .Values "clientId" | quote }} @@ -108,6 +115,12 @@ spec: value: {{ index .Values "appCertFileName" | quote }} - name: APP_CERT_FILE_PATH value: {{ index .Values "appCertMountPath" | default .Values.instantiationDefaults.appCertMountPath | quote }} + - name: CLIENT_CREDS_FILE_PATH + value: {{ index .Values "clientCredsMountPath" | default .Values.instantiationDefaults.clientCredsMountPath | quote }} + - name: CLIENT_ID_FILE_NAME + value: {{ .Values.global.clientCredentials.secret.clientIdKey | quote }} + - name: AUTHENTICATION_TYPE + value: {{ index .Values "authenticationType" | default .Values.instantiationDefaults.authenticationType | quote }} - name: SERVICE_NAME value: {{ .Chart.Name }} - name: CONTAINER_NAME diff --git a/charts/eric-oss-hello-world-python-app/values.yaml b/charts/eric-oss-hello-world-python-app/values.yaml index 3acc20f..2c4b392 100644 --- a/charts/eric-oss-hello-world-python-app/values.yaml +++ b/charts/eric-oss-hello-world-python-app/values.yaml @@ -126,3 +126,10 @@ podPriority: instantiationDefaults: platformCaCertMountPath: "/etc/tls-ca/platform/" appCertMountPath: "/etc/tls/log/" + clientCredsMountPath: "/etc/client-creds/" + +global: + clientCredentials: + secret: + clientIdKey: "clientId" + name: "-cc" diff --git a/csar/Definitions/AppDescriptor.yaml b/csar/Definitions/AppDescriptor.yaml index aa9af3f..1dc6c86 100644 --- a/csar/Definitions/AppDescriptor.yaml +++ b/csar/Definitions/AppDescriptor.yaml @@ -3,8 +3,12 @@ Description of an APP: APPName: eric-oss-hello-world-python-app APPVersion: VERSION APPType: rApp -APPComponent: - NameofComponent: eric-oss-hello-world-python-app - Version: VERSION - Path: OtherDefinitions/ASD/eric-oss-hello-world-python-appASD.yaml - ArtefactType: ASD +AppComponentList: + - NameofComponent: eric-oss-hello-world-python-app + Version: VERSION + Path: OtherDefinitions/ASD/eric-oss-hello-world-python-appASD.yaml + ArtefactType: ASD + - NameofComponent: security-mgmt + Version: 1.0.0 + Path: OtherDefinitions/SecurityManagement + ArtefactType: SecurityManagement diff --git a/csar/OtherDefinitions/SecurityManagement/security-metadata.json b/csar/OtherDefinitions/SecurityManagement/security-metadata.json new file mode 100644 index 0000000..e3f8d86 --- /dev/null +++ b/csar/OtherDefinitions/SecurityManagement/security-metadata.json @@ -0,0 +1,3 @@ +{ + "authenticatorType": "client-x509" +} \ No newline at end of file diff --git a/eric-oss-hello-world-python-app/config.py b/eric-oss-hello-world-python-app/config.py index e9bb85b..f075aff 100644 --- a/eric-oss-hello-world-python-app/config.py +++ b/eric-oss-hello-world-python-app/config.py @@ -15,6 +15,9 @@ def get_config(): app_key = get_os_env_string("APP_KEY", "") app_cert = get_os_env_string("APP_CERT", "") app_cert_file_path = get_os_env_string("APP_CERT_FILE_PATH", "") + authentication_type = get_os_env_string("AUTHENTICATION_TYPE", "") + client_creds_file_path = get_os_env_string("CLIENT_CREDS_FILE_PATH", "") + client_id_file_name = get_os_env_string("CLIENT_ID_FILE_NAME", "") config = { "iam_client_id": iam_client_id, @@ -26,7 +29,10 @@ def get_config(): "log_endpoint": log_endpoint, "app_key": app_key, "app_cert": app_cert, - "app_cert_file_path": app_cert_file_path + "app_cert_file_path": app_cert_file_path, + "authentication_type": authentication_type, + "client_creds_file_path": client_creds_file_path, + "client_id_file_name": client_id_file_name } return config diff --git a/eric-oss-hello-world-python-app/login.py b/eric-oss-hello-world-python-app/login.py index 4112a04..5b2882b 100644 --- a/eric-oss-hello-world-python-app/login.py +++ b/eric-oss-hello-world-python-app/login.py @@ -23,14 +23,8 @@ def login(): headers = { "Content-Type": "application/x-www-form-urlencoded" } - form_data = { - "grant_type": "client_credentials", - "client_id": config.get("iam_client_id"), - "client_secret": config.get("iam_client_secret"), - "tenant_id": "master" - } try: - resp = tls_login(login_url, form_data, headers) + resp = tls_login(login_url, headers) except LoginError: return None, 0 @@ -39,16 +33,43 @@ def login(): time_until_expiry -= 10 # add a buffer to ensure our session doesn't expire mid-request return token, time_until_expiry -def tls_login(url, form_data, headers): +def tls_login(url, headers): ''' This function sends an HTTP POST request with TLS for the login operation ''' config = get_config() - cert = os.path.join("/", config.get("ca_cert_file_path"), config.get("ca_cert_file_name")) + ca_cert = os.path.join("/", config.get("ca_cert_file_path"), config.get("ca_cert_file_name")) + app_cert = os.path.join("/", config.get("app_cert_file_path"), config.get("app_cert")) + app_key = os.path.join("/", config.get("app_cert_file_path"), config.get("app_key")) + authentication_type = config.get("authentication_type").lower() + form_data = {"grant_type": "client_credentials", "tenant_id": "master"} + cert = None + + if authentication_type == "client-x509": + client_id_path = os.path.join(config.get("client_creds_file_path"), config.get("client_id_file_name")) + form_data["client_id"] = read_file(client_id_path) + cert = (app_cert, app_key) + elif authentication_type == "client-secret": + form_data["client_id"] = config.get("iam_client_id") + form_data["client_secret"] = config.get("iam_client_secret") + else: + raise LoginError(f"Unsupported authentication type: {authentication_type}") + try: - response = requests.post(url, data=form_data, headers = headers, timeout=5, verify=cert) + response = requests.post( + url, + data=form_data, + headers=headers, + timeout=5, + verify=ca_cert, + cert=cert + ) if response.status_code != 200: raise LoginError(f"Login failed ({response.status_code})") except Exception as exception: raise LoginError(f"Login failed ({exception})") from exception return response.content + +def read_file(path): + with open(path, "r") as f: + return f.read().strip() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f37ed8d..50df0a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ flask==3.0.1 -requests==2.32.0 +requests==2.32.4 prometheus-client==0.20.0 \ No newline at end of file From a64cc6a520105735ebdc04867adc4d9abfd576ef Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Tue, 17 Jun 2025 09:39:04 +0100 Subject: [PATCH 03/34] client-secret modified as legacy-client-secret --- eric-oss-hello-world-python-app/login.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eric-oss-hello-world-python-app/login.py b/eric-oss-hello-world-python-app/login.py index 5b2882b..0707412 100644 --- a/eric-oss-hello-world-python-app/login.py +++ b/eric-oss-hello-world-python-app/login.py @@ -49,7 +49,7 @@ def tls_login(url, headers): client_id_path = os.path.join(config.get("client_creds_file_path"), config.get("client_id_file_name")) form_data["client_id"] = read_file(client_id_path) cert = (app_cert, app_key) - elif authentication_type == "client-secret": + elif authentication_type == "legacy-client-secret": form_data["client_id"] = config.get("iam_client_id") form_data["client_secret"] = config.get("iam_client_secret") else: From cb5ed452446716a59fdd7c79b817759da9c2a05a Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Tue, 17 Jun 2025 12:21:50 +0100 Subject: [PATCH 04/34] review comments addressed --- eric-oss-hello-world-python-app/login.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/eric-oss-hello-world-python-app/login.py b/eric-oss-hello-world-python-app/login.py index 0707412..9dc6819 100644 --- a/eric-oss-hello-world-python-app/login.py +++ b/eric-oss-hello-world-python-app/login.py @@ -47,7 +47,8 @@ def tls_login(url, headers): if authentication_type == "client-x509": client_id_path = os.path.join(config.get("client_creds_file_path"), config.get("client_id_file_name")) - form_data["client_id"] = read_file(client_id_path) + with open(client_id_path, "r") as f: + form_data["client_id"] = f.read().strip() cert = (app_cert, app_key) elif authentication_type == "legacy-client-secret": form_data["client_id"] = config.get("iam_client_id") @@ -68,8 +69,4 @@ def tls_login(url, headers): raise LoginError(f"Login failed ({response.status_code})") except Exception as exception: raise LoginError(f"Login failed ({exception})") from exception - return response.content - -def read_file(path): - with open(path, "r") as f: - return f.read().strip() \ No newline at end of file + return response.content \ No newline at end of file From a378fe8cf578b6b0216ccb54944ce71eacbafe6f Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Tue, 17 Jun 2025 19:44:56 +0100 Subject: [PATCH 05/34] README Tutorial Changes --- README.md | 213 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 148 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 6999855..c6bf902 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,8 @@ with the correct Python Sample App version. Run the following commands from within your project directory `eric-oss-hello-world-python-app-`. +**Note:** The App code present in the SDK portal ZIP package uses mTLS for communication with the platform. + ```bash mkdir -p helloworldAppPackage ``` @@ -166,10 +168,10 @@ See [Client Access to REST APIs](https://developer.intelligentautomationplatform Use the following command to generate a valid access token: ```bash -curl --cacert --request POST \ +curl --cert --key --cacert --request POST \ https:///auth/realms/master/protocol/openid-connect/token \ --header 'content-type: application/x-www-form-urlencoded' \ ---data "grant_type=client_credentials&client_id=&client_secret=" +--data "grant_type=client_credentials&client_id=" ``` This command returns an access token, which is used in the commands in the @@ -193,7 +195,7 @@ To start the onboarding of the Hello World CSAR app, run the following command in a command line tool. ```bash -curl --cacert --location --request POST 'https:///app-onboarding/v2/app-packages' \ +curl --cert --key --cacert --location --request POST 'https:///app-onboarding/v2/app-packages' \ --header 'Authorization: Bearer ' \ --header 'accept: application/json' \ --form 'file=@"/helloworldAppPackage.csar"' @@ -205,8 +207,8 @@ Example of command result: { "fileName": "helloworldAppPackage.csar", "onboardingJob": { - "id": "a2f0a43d-730a-4991-8481-746c3e76556e", - "href": "app-onboarding/v2/onboarding-jobs/a2f0a43d-730a-4991-8481-746c3e76556e" + "id": "af036040-a732-4af9-b65a-8103da56c35c", + "href": "/onboarding-jobs/af036040-a732-4af9-b65a-8103da56c35c" } } ``` @@ -217,7 +219,7 @@ This is the `JOB_ID`. Use the `JOB_ID` to get the status of the onboarding process in the following commands: ```bash -curl --cacert --location --request GET 'https:///app-onboarding/v2/onboarding-jobs/' \ +curl --cert --key --cacert --location --request GET 'https:///app-onboarding/v2/onboarding-jobs/' \ --header 'Authorization: Bearer ' \ --header 'accept: application/json' ``` @@ -229,37 +231,43 @@ Example of command result: ```json { - "id": "a2f0a43d-730a-4991-8481-746c3e76556e", + "id": "af036040-a732-4af9-b65a-8103da56c35c", "fileName": "helloworldAppPackage.csar", "packageVersion": "3.1.1-0", - "packageSize": "53.1282MiB", + "packageSize": "51.7659MiB", "vendor": "Ericsson", "type": "rApp", - "onboardStartedAt": "2024-09-13T09:48:53.239542Z", + "onboardStartedAt": "2025-05-31T13:51:56.616Z", "status": "ONBOARDED", - "onboardEndedAt": "2024-09-13T09:49:01.299826Z", + "onboardEndedAt": "2025-05-31T13:51:59.955Z", "events": [ { "type": "INFO", - "title": "Stored 1 out of 3 artifacts", - "detail": "Uploaded eric-oss-hello-world-python-app", - "occurredAt": "2024-09-13T09:48:57.556164Z" + "title": "Stored 1 out of 4 artifacts", + "detail": "Uploaded eric-oss-hello-world-python-appASD.yaml", + "occurredAt": "2025-05-31T13:51:58.042Z" }, { "type": "INFO", - "title": "Stored 2 out of 3 artifacts", - "detail": "Uploaded eric-oss-hello-world-python-appASD.yaml", - "occurredAt": "2024-09-13T09:48:57.556165Z" + "title": "Stored 2 out of 4 artifacts", + "detail": "Uploaded eric-oss-hello-world-python-app", + "occurredAt": "2025-05-31T13:51:58.043Z" }, { "type": "INFO", - "title": "Stored 3 out of 3 artifacts", + "title": "Stored 3 out of 4 artifacts", "detail": "Uploaded docker.tar", - "occurredAt": "2024-09-13T09:49:00.962182Z" + "occurredAt": "2025-05-31T13:51:59.792Z" + }, + { + "type": "INFO", + "title": "Stored 4 out of 4 artifacts", + "detail": "Uploaded security-metadata.json", + "occurredAt": "2025-05-31T13:51:59.812Z" } ], "self": { - "href": "app-onboarding/v2/onboarding-jobs/a2f0a43d-730a-4991-8481-746c3e76556e" + "href": "/onboarding-jobs/af036040-a732-4af9-b65a-8103da56c35c" }, "app": { "id": "rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0", @@ -273,7 +281,7 @@ command (rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0 in the example). Run the following command to initialize the App. ```bash -curl --cacert --location --request POST 'https:///app-lifecycle-management/v3/apps//initialization-actions' \ +curl --cert --key --cacert --location --request POST 'https:///app-lifecycle-management/v3/apps//initialization-actions' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' \ -d '{"action": "INITIALIZE"}' @@ -286,7 +294,7 @@ Example of command result: "app": { "status": "INITIALIZING", "id": "rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0", - "href": "/app-lifecycle-management/v3/apps/rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0" + "href": "/apps/rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0" } } ``` @@ -294,7 +302,7 @@ Example of command result: Repeat the following command until the status is changed to `INITIALIZED`. ```shell -curl --cacert --location --request GET 'https:///app-lifecycle-management/v3/apps/' \ +curl --cert --key --cacert --location --request GET 'https:///app-lifecycle-management/v3/apps/' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' ``` @@ -309,11 +317,11 @@ Example of command result: "name": "eric-oss-hello-world-python-app", "version": "3.1.1-0", "mode": "DISABLED", - "status": "INITIALIZING", - "createdAt": "2024-09-13T09:49:01.273Z", + "status": "INITIALIZED", + "createdAt": "2025-05-31T13:51:59.931Z", "components": [ { - "type": "MICROSERVICE", + "type": "ASD", "name": "eric-oss-hello-world-python-app", "version": "3.1.1-0", "artifacts": [ @@ -321,12 +329,23 @@ Example of command result: "name": "docker.tar", "type": "IMAGE" }, + { + "name": "eric-oss-hello-world-python-appASD.yaml", + "type": "OPAQUE" + }, { "name": "eric-oss-hello-world-python-app", "type": "HELM" - }, + } + ] + }, + { + "type": "SECURITYMANAGEMENT", + "name": "security-mgmt", + "version": "1.0.0", + "artifacts": [ { - "name": "eric-oss-hello-world-python-appASD.yaml", + "name": "security-metadata.json", "type": "OPAQUE" } ] @@ -339,9 +358,28 @@ Example of command result: } ], "roles": [], - "events": [], + "events": [ + { + "type": "INITIALIZE", + "title": "SUCCEEDED", + "detail": "INITIALIZE has successfully completed", + "createdAt": "2025-05-31T13:55:50.421Z" + }, + { + "type": "INITIALIZE", + "title": "STARTED", + "detail": "INITIALIZE has started", + "createdAt": "2025-05-31T13:55:34.171Z" + }, + { + "type": "CREATE", + "title": "SUCCEEDED", + "detail": "CREATE has successfully completed", + "createdAt": "2025-05-31T13:51:59.945Z" + } + ], "self": { - "href": "/app-lifecycle-management/v3/apps/rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0" + "href": "/apps/rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0" } } ``` @@ -349,7 +387,7 @@ Example of command result: Run the following command to switch the app mode from 'DISABLED' to 'ENABLED'. ```bash -curl --cacert --location --request PUT 'https:///app-lifecycle-management/v3/apps//mode' \ +curl --cert --key --cacert --location --request PUT 'https:///app-lifecycle-management/v3/apps//mode' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' \ -d '{"mode": "ENABLED"}' @@ -362,7 +400,7 @@ Example of command result: "mode": "ENABLED", "app": { "id": "rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0", - "href": "/app-lifecycle-management/v3/apps/rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0" + "href": "/apps/rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0" } } ``` @@ -386,14 +424,15 @@ This section describes how the App can communicate with IAM and produce logs to sample App first communicates with IAM to obtain a client token (login) before returning the "Hello World!!" string output. - The `platformCaCertSecretName` and `platformCaCertFileName` to enable - secure TLS communication. Refer to + secure communication. Refer to [App Certificate Provisioning Developer Guide](https://developer.intelligentautomationplatform.ericsson.net/#capabilities/app-cert-provisioning/developer-guide) to understand how certificates are loaded into the App during instantiation for secure communication. - - The `appSecretName`, `logEndpoint`, - `appKeyFileName`, `appCertFileName` - for mTLS communication. For more information on the variable values + - The `logEndpoint` endpoint designed to capture log data, supports only + mTLS communication. For more information on the variable values required, see [App Logging Developer Guide to Produce logs](https://developer.intelligentautomationplatform.ericsson.net/#capabilities/app-logging/how-to-produce-logs?chapter=identify-environment-and-secret-variables-names). + - The `appSecretName`, `appKeyFileName`, `appCertFileName` + used for mTLS communication to verify the client. ### Steps for Instantiation @@ -406,7 +445,7 @@ Run the following commands to start the instantiation process using the #### Create App Instance ```shell -curl --cacert --location --request POST 'https:///app-lifecycle-management/v3/app-instances' \ +curl --cert --key --cacert --location --request POST 'https:///app-lifecycle-management/v3/app-instances' \ --header 'accept: application/json' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' \ @@ -419,35 +458,44 @@ Example command result: ```json { - "id": "rapp-ericsson-eric-oss-hello-world-python-app-28057851", + "id": "rapp-ericsson-eric-oss-hello-world-python-app-68129972", "appId": "rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0", "status": "UNDEPLOYED", "credentials": { - "clientId": "rapp-ericsson-eric-oss-hello-world-python-app-28057851" + "clientId": "rapp-ericsson-eric-oss-hello-world-python-app-68129972" }, "componentInstances": [ { "name": "eric-oss-hello-world-python-app", "version": "3.1.1-0", - "type": "MICROSERVICE", + "type": "ASD", "deployState": "UNDEPLOYED", "properties": { + "userDefinedHelmParameters": {}, "namespace": "", "timeout": 5 } + }, + { + "name": "security-mgmt", + "version": "1.0.0", + "type": "SECURITYMANAGEMENT", + "properties": { + "authenticatorType": "client-x509" + } } ], "self": { - "href": "/app-lifecycle-management/v3/app-instances/rapp-ericsson-eric-oss-hello-world-python-app-28057851" + "href": "/app-instances/rapp-ericsson-eric-oss-hello-world-python-app-68129972" }, "app": { - "href": "/app-lifecycle-management/v3/apps/rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0" + "href": "/apps/rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0" } } ``` An app-instance `id` is shown in the command result - (rapp-ericsson-eric-oss-hello-world-python-app-28057851 in the example). This + (rapp-ericsson-eric-oss-hello-world-python-app-68129972 in the example). This is the `APP_INSTANCE_ID` used in the following commands. #### Deploy App Instance @@ -455,8 +503,13 @@ An app-instance `id` is shown in the command result > All `userDefinedHelmParameters` are required for successful instantiation of your App. + **Note:** The `authenticationType` defines the authentication method the sample app + will use to communicate with IAM - set to `client-x509` for mTLS or + `client-secret` for TLS. This parameter is only used to navigate between TLS and mTLS + within the app code. + ```shell -curl --cacert --location --request POST 'https:///app-lifecycle-management/v3/app-instances//deployment-actions' \ +curl --cert --key --cacert --location --request POST 'https:///app-lifecycle-management/v3/app-instances//deployment-actions' \ --header 'accept: application/json' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' \ @@ -475,13 +528,14 @@ curl --cacert --location --request POST 'https://", "platformCaCertFileName": "", "appKeyFileName": "", - "appCertFileName": "" + "appCertFileName": "", + "authenticationType": "" } } } ] } -}' \ +}' ``` See the following example command result: @@ -502,7 +556,8 @@ See the following example command result: "appSecretName": "", "logEndpoint": "", "appKeyFileName": "", - "appCertFileName": "" + "appCertFileName": "", + "authenticationType": "", } } } @@ -510,7 +565,7 @@ See the following example command result: }, "appInstance": { "status": "DEPLOYING", - "href": "/app-lifecycle-management/v3/app-instances/rapp-ericsson-eric-oss-hello-world-python-app-28057851" + "href": "/app-instances/rapp-ericsson-eric-oss-hello-world-python-app-68129972" } } ``` @@ -520,7 +575,7 @@ Use the App instance ID in the following command to check the instantiation to `"status":"DEPLOYED"`. ```shell -curl --cacert --location --request GET 'https:///app-lifecycle-management/v3/app-instances/' \ +curl --cert --key --cacert --location --request GET 'https:///app-lifecycle-management/v3/app-instances/' \ --header 'accept: application/json' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' @@ -528,41 +583,69 @@ curl --cacert --location --request GET 'https://", - "platformCaCertFileName": "", "iamBaseUrl": "https://", - "appSecretName": "", "logEndpoint": "", + "platformCaCertSecretName": "", + "appSecretName": "", + "platformCaCertFileName": "", "appKeyFileName": "", - "appCertFileName": "" + "appCertFileName": "", + "authenticationType": "", }, "namespace": "", "timeout": 5 } + }, + { + "name": "security-mgmt", + "version": "1.0.0", + "type": "SECURITYMANAGEMENT", + "properties": { + "authenticatorType": "client-x509" + } + } + ], + "events": [ + { + "type": "DEPLOY", + "title": "SUCCEEDED", + "detail": "DEPLOY has successfully completed", + "createdAt": "2025-05-31T14:04:16.297Z" + }, + { + "type": "DEPLOY", + "title": "STARTED", + "detail": "DEPLOY has started", + "createdAt": "2025-05-31T14:04:15.609Z" + }, + { + "type": "CREATE", + "title": "SUCCEEDED", + "detail": "CREATE has successfully completed", + "createdAt": "2025-05-31T14:01:01.753Z" } ], - "events": [], "self": { - "href": "/app-lifecycle-management/v3/app-instances/rapp-ericsson-eric-oss-hello-world-python-app-28057851" + "href": "/app-instances/rapp-ericsson-eric-oss-hello-world-python-app-68129972" }, "app": { - "href": "/app-lifecycle-management/v3/apps/rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0" + "href": "/apps/rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0" } } ``` @@ -578,7 +661,7 @@ For details, see [Service Exposure - Developer Guide](https://developer.intellig To create an API to be onboarded, run the following commands: ```bash -curl --cacert --location --request POST 'https:///hub/apiprovisioning/v1/admin/v3/apis' \ +curl --cert --key --cacert --location --request POST 'https:///hub/apiprovisioning/v1/admin/v3/apis' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ @@ -604,7 +687,7 @@ To create an endpoint for the previously generated API, run the following command: ```bash -curl --cacert --location --request POST 'https:///hub/apiprovisioning/v1/admin/v3/apis/hello-world-python-route-001/endpoints' \ +curl --cert --key --cacert --location --request POST 'https:///hub/apiprovisioning/v1/admin/v3/apis/hello-world-python-route-001/endpoints' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ @@ -617,7 +700,7 @@ To bind the plugin for authorization of the previously generated API, run the following command: ```bash -curl --cacert --location --request PUT 'https:///hub/apiprovisioning/v1/admin/v3/apis/hello-world-python-route-001/phases/auth/plugin-list' \ +curl --cert --key --cacert --location --request PUT 'https:///hub/apiprovisioning/v1/admin/v3/apis/hello-world-python-route-001/phases/auth/plugin-list' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '[ @@ -631,7 +714,7 @@ To configure the binded plugin for authorization, run the following command: ```bash -curl --cacert --location --request PUT 'https:///hub/apiprovisioning/v1/admin/v3/apis/hello-world-python-route-001/plugins/requestPartyTokenInterceptor/configuration' \ +curl --cert --key --cacert --location --request PUT 'https:///hub/apiprovisioning/v1/admin/v3/apis/hello-world-python-route-001/plugins/requestPartyTokenInterceptor/configuration' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ @@ -649,7 +732,7 @@ Role-Based Access Control (RBAC) configuration is required. To add the RBAC policy run the following curl command: ```bash -curl --cacert --location --request POST 'https:///idm/rolemgmt/v1/extapp/rbac' \ +curl --cert --key --cacert --location --request POST 'https:///idm/rolemgmt/v1/extapp/rbac' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' \ --data '{ From da5c5de654768bb5676c35bc70d12259b78b6846 Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Tue, 17 Jun 2025 19:49:13 +0100 Subject: [PATCH 06/34] Modified client-secret to legacy-client-secret --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c6bf902..5f2ad8f 100644 --- a/README.md +++ b/README.md @@ -505,7 +505,7 @@ An app-instance `id` is shown in the command result **Note:** The `authenticationType` defines the authentication method the sample app will use to communicate with IAM - set to `client-x509` for mTLS or - `client-secret` for TLS. This parameter is only used to navigate between TLS and mTLS + `legacy-client-secret` for TLS. This parameter is only used to navigate between TLS and mTLS within the app code. ```shell From 1829b632e86d79a839ee42f69ed48216ae513e9d Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Tue, 17 Jun 2025 22:16:44 +0100 Subject: [PATCH 07/34] Minor changes in readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5f2ad8f..bfc9014 100644 --- a/README.md +++ b/README.md @@ -152,8 +152,8 @@ Contact the platform administrator to request the following: - A CA certificate, needed for secure communication with the platform APIs. - Client Access to the platform with the required roles, needed for authorized communication with the platform APIs used to *onboard and instantiate* the -Hello World App. You will receive a Client ID and Client Secret from the -platform administrator. Include the required roles listed below in the request. +Hello World App. You will receive a Client ID from the platform administrator. +Include the required roles listed below in the request. | Role | Role Description | | --------------------------------------------------------------- | ----------------------------------------------------------------------------- | From 9b5e0faae7a956d465e09418e085103bfa40ed58 Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Tue, 17 Jun 2025 22:30:12 +0100 Subject: [PATCH 08/34] Corrections - proof read --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bfc9014..31539bf 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,12 @@ ls ./csar-output ### Prerequisites for Onboarding -Contact the platform administrator to request the following: +Provide the following to platform administrator: + +- A end-entity ClientCA certificate. +- Subject DN of end-entity client certificate. + +Request the following from platform administrator: - A CA certificate, needed for secure communication with the platform APIs. - Client Access to the platform with the required roles, needed for authorized From 42f82221adb824455402345773abe9b417224bc8 Mon Sep 17 00:00:00 2001 From: Kara Date: Wed, 18 Jun 2025 13:54:49 +0100 Subject: [PATCH 09/34] Added unit tests and reformatted files --- eric-oss-hello-world-python-app/config.py | 64 +++++------ eric-oss-hello-world-python-app/login.py | 65 ++++++----- eric-oss-hello-world-python-app/main.py | 72 +++++++----- .../mtls_logging.py | 73 ++++++++---- .../tests/client_id_example | 1 + .../tests/conftest.py | 105 ++++++++++++++---- .../tests/test_login.py | 43 +++---- .../tests/test_main.py | 30 ++--- .../tests/test_mtls_logging.py | 31 ++++-- 9 files changed, 306 insertions(+), 178 deletions(-) create mode 100644 eric-oss-hello-world-python-app/tests/client_id_example diff --git a/eric-oss-hello-world-python-app/config.py b/eric-oss-hello-world-python-app/config.py index f075aff..b311f92 100644 --- a/eric-oss-hello-world-python-app/config.py +++ b/eric-oss-hello-world-python-app/config.py @@ -1,44 +1,46 @@ -'''This module handles environment variables''' +"""This module handles environment variables""" + import os + def get_config(): - ''' + """ get env and return config with all env vals required - ''' - iam_client_id = get_os_env_string("IAM_CLIENT_ID", "") - iam_client_secret = get_os_env_string("IAM_CLIENT_SECRET", "") - iam_base_url = get_os_env_string("IAM_BASE_URL", "") - ca_cert_file_name = get_os_env_string("CA_CERT_FILE_NAME", "") - ca_cert_file_path = get_os_env_string("CA_CERT_FILE_PATH", "") - log_ctrl_file = get_os_env_string("LOG_CTRL_FILE", "") - log_endpoint = get_os_env_string("LOG_ENDPOINT", "") - app_key = get_os_env_string("APP_KEY", "") - app_cert = get_os_env_string("APP_CERT", "") - app_cert_file_path = get_os_env_string("APP_CERT_FILE_PATH", "") - authentication_type = get_os_env_string("AUTHENTICATION_TYPE", "") - client_creds_file_path = get_os_env_string("CLIENT_CREDS_FILE_PATH", "") - client_id_file_name = get_os_env_string("CLIENT_ID_FILE_NAME", "") + """ + iam_client_id = get_os_env_string("IAM_CLIENT_ID", "") + iam_client_secret = get_os_env_string("IAM_CLIENT_SECRET", "") + iam_base_url = get_os_env_string("IAM_BASE_URL", "") + ca_cert_file_name = get_os_env_string("CA_CERT_FILE_NAME", "") + ca_cert_file_path = get_os_env_string("CA_CERT_FILE_PATH", "") + log_ctrl_file = get_os_env_string("LOG_CTRL_FILE", "") + log_endpoint = get_os_env_string("LOG_ENDPOINT", "") + app_key = get_os_env_string("APP_KEY", "") + app_cert = get_os_env_string("APP_CERT", "") + app_cert_file_path = get_os_env_string("APP_CERT_FILE_PATH", "") + authentication_type = get_os_env_string("AUTHENTICATION_TYPE", "") + client_creds_file_path = get_os_env_string("CLIENT_CREDS_FILE_PATH", "") + client_id_file_name = get_os_env_string("CLIENT_ID_FILE_NAME", "") config = { - "iam_client_id": iam_client_id, - "iam_client_secret": iam_client_secret, - "iam_base_url": iam_base_url, - "ca_cert_file_name": ca_cert_file_name, - "ca_cert_file_path": ca_cert_file_path, - "log_ctrl_file": log_ctrl_file, - "log_endpoint": log_endpoint, - "app_key": app_key, - "app_cert": app_cert, - "app_cert_file_path": app_cert_file_path, - "authentication_type": authentication_type, - "client_creds_file_path": client_creds_file_path, - "client_id_file_name": client_id_file_name + "iam_client_id": iam_client_id, + "iam_client_secret": iam_client_secret, + "iam_base_url": iam_base_url, + "ca_cert_file_name": ca_cert_file_name, + "ca_cert_file_path": ca_cert_file_path, + "log_ctrl_file": log_ctrl_file, + "log_endpoint": log_endpoint, + "app_key": app_key, + "app_cert": app_cert, + "app_cert_file_path": app_cert_file_path, + "authentication_type": authentication_type, + "client_creds_file_path": client_creds_file_path, + "client_id_file_name": client_id_file_name, } return config def get_os_env_string(env_name, default_value): - ''' + """ get env - ''' + """ return os.getenv(env_name, default_value).strip() diff --git a/eric-oss-hello-world-python-app/login.py b/eric-oss-hello-world-python-app/login.py index 9dc6819..a706bbf 100644 --- a/eric-oss-hello-world-python-app/login.py +++ b/eric-oss-hello-world-python-app/login.py @@ -1,72 +1,77 @@ -''' +""" This module performs client credentials grant authentication by sending HTTP requests with TLS and with required environment variables. - ''' + """ + import os from urllib.parse import urljoin import json import requests +import time from config import get_config + class LoginError(Exception): """Raised when EIC login fails""" + def login(): - ''' + """ Get bearer token for accessing platform REST APIs: https://developer.intelligentautomationplatform.ericsson.net/#tutorials/app-authentication - ''' + """ config = get_config() login_path = "/auth/realms/master/protocol/openid-connect/token" login_url = urljoin(config.get("iam_base_url"), login_path) - headers = { - "Content-Type": "application/x-www-form-urlencoded" - } - try: - resp = tls_login(login_url, headers) - except LoginError: - return None, 0 - - resp = json.loads(resp.decode('utf-8')) + headers = {"Content-Type": "application/x-www-form-urlencoded"} + resp = tls_login(login_url, headers) + resp = json.loads(resp.decode("utf-8")) token, time_until_expiry = resp["access_token"], resp["expires_in"] - time_until_expiry -= 10 # add a buffer to ensure our session doesn't expire mid-request - return token, time_until_expiry + time_until_expiry -= ( + 10 # add a buffer to ensure our session doesn't expire mid-request + ) + return token, time.time() + time_until_expiry + def tls_login(url, headers): - ''' + """ This function sends an HTTP POST request with TLS for the login operation - ''' + """ config = get_config() - ca_cert = os.path.join("/", config.get("ca_cert_file_path"), config.get("ca_cert_file_name")) - app_cert = os.path.join("/", config.get("app_cert_file_path"), config.get("app_cert")) + ca_cert = os.path.join( + "/", config.get("ca_cert_file_path"), config.get("ca_cert_file_name") + ) + app_cert = os.path.join( + "/", config.get("app_cert_file_path"), config.get("app_cert") + ) app_key = os.path.join("/", config.get("app_cert_file_path"), config.get("app_key")) authentication_type = config.get("authentication_type").lower() form_data = {"grant_type": "client_credentials", "tenant_id": "master"} cert = None if authentication_type == "client-x509": - client_id_path = os.path.join(config.get("client_creds_file_path"), config.get("client_id_file_name")) - with open(client_id_path, "r") as f: - form_data["client_id"] = f.read().strip() + client_id_path = os.path.join( + config.get("client_creds_file_path"), config.get("client_id_file_name") + ) + try: + with open(client_id_path, "r") as f: + form_data["client_id"] = f.read().strip() + except OSError as e: + raise LoginError(f"Error while reading client id: {e}") cert = (app_cert, app_key) elif authentication_type == "legacy-client-secret": form_data["client_id"] = config.get("iam_client_id") form_data["client_secret"] = config.get("iam_client_secret") else: raise LoginError(f"Unsupported authentication type: {authentication_type}") - + try: response = requests.post( - url, - data=form_data, - headers=headers, - timeout=5, - verify=ca_cert, - cert=cert + url, data=form_data, headers=headers, timeout=5, verify=ca_cert, cert=cert ) if response.status_code != 200: raise LoginError(f"Login failed ({response.status_code})") except Exception as exception: raise LoginError(f"Login failed ({exception})") from exception - return response.content \ No newline at end of file + return response.content diff --git a/eric-oss-hello-world-python-app/main.py b/eric-oss-hello-world-python-app/main.py index 2ce8e11..170f8a3 100755 --- a/eric-oss-hello-world-python-app/main.py +++ b/eric-oss-hello-world-python-app/main.py @@ -1,46 +1,57 @@ #!/usr/bin/env python3 -''' +""" Flask Application for Hello World Service This Python script defines a Flask application that implements a simple "Hello World" service along with a health check and metrics endpoints. -''' +""" import time from flask import abort from flask import Flask from login import login from mtls_logging import MtlsLogging, Severity from werkzeug.middleware.dispatcher import DispatcherMiddleware -from prometheus_client import disable_created_metrics, make_wsgi_app, CollectorRegistry, Counter +from prometheus_client import ( + disable_created_metrics, + make_wsgi_app, + CollectorRegistry, + Counter, +) SERVICE_PREFIX = "python_hello_world" + class Application(Flask): - '''The Flask application itself. Subclassed for testing.''' + """The Flask application itself. Subclassed for testing.""" + def __init__(self): super().__init__(__name__) disable_created_metrics() self.counters = {"total_failed": 0, "total_requests": 0} self.session = {"token": None, "expiry_time": 0} self.create_metrics() - self.wsgi_app = DispatcherMiddleware(self.wsgi_app, { - '/sample-app/python/metrics': make_wsgi_app(registry=self.registry) - }) + self.wsgi_app = DispatcherMiddleware( + self.wsgi_app, + {"/sample-app/python/metrics": make_wsgi_app(registry=self.registry)}, + ) self.logger = MtlsLogging() @self.route("/sample-app/python/") def root(): - '''This route returns a 400 Bad Request HTTP response.''' - self.logger.log("400 Bad request: User tried accessing '/sample-app/python/'", Severity.INFO) + """This route returns a 400 Bad Request HTTP response.""" + self.logger.log( + "400 Bad request: User tried accessing '/sample-app/python/'", + Severity.INFO, + ) abort(400) @self.route("/sample-app/python/hello") def hello(): - ''' + """ This route performs a login operation and returns a simple "Hello World!" greeting and increments the total request counter. - ''' + """ self.update_session() self.requests_total.inc() self.logger.log("200 OK: Hello World!", Severity.INFO) @@ -48,35 +59,40 @@ def hello(): @self.route("/sample-app/python/health") def health(): - ''' + """ This route provides a simple health check endpoint, returning "Ok" to indicate that the application is healthy. - ''' + """ self.update_session() self.logger.log("200 OK: Health check", Severity.INFO) return "Ok\n" - def update_session(self): - '''Refresh session if it expires.''' + """Refresh session if it expires.""" if int(time.time()) >= self.session["expiry_time"]: - self.session["token"], self.session["expiry_time"] = login() - if not self.session["token"]: - # since the token isn't used for anything, - # this is just a WARNING level log instead of ERROR - self.logger.log("Login failed", Severity.WARNING) + try: + self.session["token"], self.session["expiry_time"] = login() + except Exception as e: + # since the token isn't used for anything, + # this is just a WARNING level log instead of ERROR + self.logger.log(f"Login failed: {e}", Severity.WARNING) + def create_metrics(self): self.registry = CollectorRegistry() - self.requests_total = Counter(namespace=SERVICE_PREFIX, - name="requests_total", - documentation="Total number of API requests") - self.requests_failed = Counter(namespace=SERVICE_PREFIX, - name="requests_failed_total", - documentation="Total number of API request failures") + self.requests_total = Counter( + namespace=SERVICE_PREFIX, + name="requests_total", + documentation="Total number of API requests", + ) + self.requests_failed = Counter( + namespace=SERVICE_PREFIX, + name="requests_failed_total", + documentation="Total number of API request failures", + ) self.registry.register(self.requests_total) self.registry.register(self.requests_failed) -if __name__ == '__main__': +if __name__ == "__main__": instance = Application() - instance.run(host = '0.0.0.0', port = '8050') + instance.run(host="0.0.0.0", port="8050") diff --git a/eric-oss-hello-world-python-app/mtls_logging.py b/eric-oss-hello-world-python-app/mtls_logging.py index dc91c70..096584a 100644 --- a/eric-oss-hello-world-python-app/mtls_logging.py +++ b/eric-oss-hello-world-python-app/mtls_logging.py @@ -1,4 +1,4 @@ -'''This module handles mTLS logging''' +"""This module handles mTLS logging""" import json import os @@ -11,18 +11,21 @@ class Severity(IntEnum): - '''We use this to map the logging library severity to the mTLS logging''' + """We use this to map the logging library severity to the mTLS logging""" + DEBUG = 10 INFO = 20 WARNING = 30 ERROR = 40 CRITICAL = 50 -#pylint: disable=too-few-public-methods + +# pylint: disable=too-few-public-methods class MtlsLogging: - '''mTLS logger which will log to STDOUT, as well as Log Aggregator''' + """mTLS logger which will log to STDOUT, as well as Log Aggregator""" + def __init__(self, level=None): - werkzeug_logger = logging.getLogger('werkzeug') + werkzeug_logger = logging.getLogger("werkzeug") werkzeug_logger.setLevel(logging.ERROR) self.config = get_config() self.logger = logging.getLogger(__name__) @@ -35,7 +38,9 @@ def __init__(self, level=None): level = Severity.INFO if self.config["log_ctrl_file"]: # If level is defined in charts\eric-oss-hello-world-python-app\logcontrol.json - with open(self.config["log_ctrl_file"], "r", encoding="utf-8") as log_ctrl_file: + with open( + self.config["log_ctrl_file"], "r", encoding="utf-8" + ) as log_ctrl_file: log_ctrl = json.load(log_ctrl_file) container_name = get_os_env_string("CONTAINER_NAME", "") for obj in log_ctrl: @@ -54,31 +59,30 @@ def __init__(self, level=None): self.logger.addHandler(handler) self.log(f"Level set to: {level}", Severity.INFO) - def log(self, message, severity): - ''' + """ Send request to log aggregator with mTLS - ''' + """ # Determine if certs are set. - cert_available = (self.config.get("ca_cert_file_name") != "" - and self.config.get("ca_cert_file_path") != "" - and self.config.get("app_cert") != "" - and self.config.get("app_key") != "" - and self.config.get("app_cert_file_path") != "") + cert_available = ( + self.config.get("ca_cert_file_name") != "" + and self.config.get("ca_cert_file_path") != "" + and self.config.get("app_cert") != "" + and self.config.get("app_key") != "" + and self.config.get("app_cert_file_path") != "" + ) log_url = self.config.get("log_endpoint") time = datetime.now(timezone.utc).isoformat() - headers = { - "Content-Type": "application/json" - } + headers = {"Content-Type": "application/json"} json_data = { "timestamp": time, "version": "0.0.1", "message": message, "service_id": "rapp-eric-oss-hello-world-python-app", - "severity": severity.name.lower() + "severity": severity.name.lower(), } # print to console @@ -94,11 +98,32 @@ def log(self, message, severity): elif severity >= self.logger.getEffectiveLevel(): # send log to log transformer try: - ca_cert = os.path.join("/", self.config.get("ca_cert_file_path"), self.config.get("ca_cert_file_name")) - app_cert = os.path.join("/", self.config.get("app_cert_file_path"), self.config.get("app_cert")) - app_key = os.path.join("/", self.config.get("app_cert_file_path"), self.config.get("app_key")) - requests.post(f"https://{log_url}", json=json_data, timeout=5, - headers = headers, verify=ca_cert, cert=(app_cert, app_key)) - except (requests.exceptions.InvalidURL, requests.exceptions.MissingSchema) as exception: + ca_cert = os.path.join( + "/", + self.config.get("ca_cert_file_path"), + self.config.get("ca_cert_file_name"), + ) + app_cert = os.path.join( + "/", + self.config.get("app_cert_file_path"), + self.config.get("app_cert"), + ) + app_key = os.path.join( + "/", + self.config.get("app_cert_file_path"), + self.config.get("app_key"), + ) + requests.post( + f"https://{log_url}", + json=json_data, + timeout=5, + headers=headers, + verify=ca_cert, + cert=(app_cert, app_key), + ) + except ( + requests.exceptions.InvalidURL, + requests.exceptions.MissingSchema, + ) as exception: # logs to console if failed to log to log transformer self.logger.error("Request failed for mTLS logging: %s", exception) diff --git a/eric-oss-hello-world-python-app/tests/client_id_example b/eric-oss-hello-world-python-app/tests/client_id_example new file mode 100644 index 0000000..7f6e5ba --- /dev/null +++ b/eric-oss-hello-world-python-app/tests/client_id_example @@ -0,0 +1 @@ +IAM_CLIENT_ID \ No newline at end of file diff --git a/eric-oss-hello-world-python-app/tests/conftest.py b/eric-oss-hello-world-python-app/tests/conftest.py index 8206429..da72dd4 100644 --- a/eric-oss-hello-world-python-app/tests/conftest.py +++ b/eric-oss-hello-world-python-app/tests/conftest.py @@ -1,17 +1,20 @@ -''' +""" Configure a Flask fixture based off the Application defined in main.py -''' +""" import os import pytest import requests_mock from prometheus_client import REGISTRY as GLOBAL_METRICS_REGISTRY +from urllib.parse import urljoin from main import Application from config import get_config + def pytest_generate_tests(): populate_environment_variables() + @pytest.fixture(name="mock_log_api") def fixture_mock_log_api(config): log_endpoint = f"https://{config.get('log_endpoint')}" @@ -20,53 +23,111 @@ def fixture_mock_log_api(config): yield request_mocker +def match_request_data(request): + uses_x509 = request.cert and all( + [ + parameter in request.text + for parameter in [ + "grant_type=client_credentials", + "tenant_id=master", + "client_id=IAM_CLIENT_ID", + ] + ] + ) + uses_legacy = all( + [ + parameter in request.text + for parameter in [ + "grant_type=client_credentials", + "tenant_id=master", + "client_id=IAM_CLIENT_ID", + "client_secret=IAM_CLIENT_SECRET", + ] + ] + ) + return uses_x509 or uses_legacy + + +@pytest.fixture(name="mock_login_api") +def fixture_mock_login_api(config): + login_endpoint = urljoin( + config.get("iam_base_url"), "/auth/realms/master/protocol/openid-connect/token" + ) + with requests_mock.Mocker() as request_mocker: + request_mocker.post( + login_endpoint, + request_headers={"Content-Type": "application/x-www-form-urlencoded"}, + additional_matcher=match_request_data, + json={ + "access_token": "2YotnFZFEjr1zCsicMWpAA", + "token_type": "example", + "expires_in": 3600, + "example_parameter": "example_value", + }, + ) # Example reply from OAuth2 spec: https://datatracker.ietf.org/doc/html/rfc6749#section-4.4.3 + yield request_mocker + + @pytest.fixture(name="app") def fixture_app(mock_log_api): # pylint: disable=unused-argument - '''Create a fixture out of our Application, which will be used by any test_*.py file''' + """Create a fixture out of our Application, which will be used by any test_*.py file""" application = Application() - application.config.update({ - "TESTING": True, - }) + application.config.update( + { + "TESTING": True, + } + ) # Why 'yield'? See: https://docs.pytest.org/en/7.1.x/how-to/fixtures.html#dynamic-scope - yield application + yield application GLOBAL_METRICS_REGISTRY.unregister(application.requests_total) GLOBAL_METRICS_REGISTRY.unregister(application.requests_failed) @pytest.fixture() def client(app): - '''Every time a test wants a client, give it a new copy of our Application''' + """Every time a test wants a client, give it a new copy of our Application""" return app.test_client() @pytest.fixture(name="config") def fixture_config(): - '''Every time a test wants a config, give it a stub''' + """Every time a test wants a config, give it a stub""" return get_config() + @pytest.fixture(scope="function") def no_log_certs(): # Remove references to log certs to simulate them being undefined. # This would simulate a user not setting these at instantiation time. - os.environ["APP_KEY"] = "" - os.environ["APP_CERT"] = "" - os.environ["APP_CERT_FILE_PATH"] = "" - + os.environ["APP_KEY"] = "" + os.environ["APP_CERT"] = "" + os.environ["APP_CERT_FILE_PATH"] = "" + # Why 'yield'? See: https://docs.pytest.org/en/7.1.x/how-to/fixtures.html#dynamic-scope yield populate_environment_variables() +@pytest.fixture(scope="function") +def legacy_authentication(): + os.environ["AUTHENTICATION_TYPE"] = "legacy-client-secret" + yield + populate_environment_variables() + + def populate_environment_variables(): - os.environ["IAM_CLIENT_ID"] = "IAM_CLIENT_ID" - os.environ["IAM_CLIENT_SECRET"] = "IAM_CLIENT_SECRET" - os.environ["IAM_BASE_URL"] = "https://www.iam-base-url.com" - os.environ["CA_CERT_FILE_NAME"] = "CA_CERT_FILE_NAME" - os.environ["CA_CERT_FILE_PATH"] = "CA_CERT_MOUNT_PATH" - os.environ["LOG_ENDPOINT"] = "LOG_ENDPOINT" - os.environ["APP_KEY"] = "APP_KEY" - os.environ["APP_CERT"] = "APP_CERT" - os.environ["APP_CERT_FILE_PATH"] = "APP_CERT_FILE_PATH" \ No newline at end of file + os.environ["IAM_CLIENT_ID"] = "IAM_CLIENT_ID" + os.environ["IAM_CLIENT_SECRET"] = "IAM_CLIENT_SECRET" + os.environ["IAM_BASE_URL"] = "https://www.iam-base-url.com" + os.environ["CA_CERT_FILE_NAME"] = "CA_CERT_FILE_NAME" + os.environ["CA_CERT_FILE_PATH"] = "CA_CERT_MOUNT_PATH" + os.environ["LOG_ENDPOINT"] = "LOG_ENDPOINT" + os.environ["APP_KEY"] = "APP_KEY" + os.environ["APP_CERT"] = "APP_CERT" + os.environ["APP_CERT_FILE_PATH"] = "APP_CERT_FILE_PATH" + os.environ["AUTHENTICATION_TYPE"] = "client-x509" + os.environ["CLIENT_CREDS_FILE_PATH"] = "eric-oss-hello-world-python-app/tests/" + os.environ["CLIENT_ID_FILE_NAME"] = "client_id_example" diff --git a/eric-oss-hello-world-python-app/tests/test_login.py b/eric-oss-hello-world-python-app/tests/test_login.py index d0e32d1..8150896 100644 --- a/eric-oss-hello-world-python-app/tests/test_login.py +++ b/eric-oss-hello-world-python-app/tests/test_login.py @@ -1,26 +1,31 @@ -'''Tests which ensure the application handles Authentication & Authorisation properly''' +"""Tests which ensure the application handles Authentication & Authorisation properly""" + from urllib.parse import urljoin -from login import login +from login import login, LoginError +import pytest +import time -def test_login_receives_token(requests_mock, config): - '''Check if we receive a token''' - login_url = urljoin(config.get("iam_base_url"), "/auth/realms/master/protocol/openid-connect/token") - requests_mock.post(login_url, json = { - "access_token":"2YotnFZFEjr1zCsicMWpAA", - "token_type":"example", - "expires_in":3600, - "example_parameter":"example_value" - }) # Example reply from OAuth2 spec: https://datatracker.ietf.org/doc/html/rfc6749#section-4.4.3 +def test_login_receives_token_x509(mock_login_api, config): + """Check if we receive a token""" token, expiry = login() - assert token == "2YotnFZFEjr1zCsicMWpAA" and expiry + 10 == 3600 + assert token == "2YotnFZFEjr1zCsicMWpAA" and expiry > time.time() -def test_login_bad_credentials(requests_mock, config): - '''Ensure we get an error if credentials are bad''' - login_url = urljoin(config.get("iam_base_url"), "/auth/realms/master/protocol/openid-connect/token") - requests_mock.post(login_url, status_code=400, json = { - "error":"invalid_request" - }) # Example reply from OAuth2 spec: https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 +def test_login_receives_token_legacy(mock_login_api, config, legacy_authentication): + """Check if we receive a token""" token, expiry = login() - assert not token and expiry == 0 + assert token == "2YotnFZFEjr1zCsicMWpAA" and expiry > time.time() + + +def test_login_bad_credentials(requests_mock, config): + """Ensure we get an error if credentials are bad""" + login_url = urljoin( + config.get("iam_base_url"), "/auth/realms/master/protocol/openid-connect/token" + ) + requests_mock.post( + login_url, status_code=400, json={"error": "invalid_request"} + ) # Example reply from OAuth2 spec: https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 + with pytest.raises(LoginError) as e: + login() + assert "400" in e diff --git a/eric-oss-hello-world-python-app/tests/test_main.py b/eric-oss-hello-world-python-app/tests/test_main.py index 994731b..d99baab 100644 --- a/eric-oss-hello-world-python-app/tests/test_main.py +++ b/eric-oss-hello-world-python-app/tests/test_main.py @@ -1,62 +1,66 @@ -'''Tests which cover the routes of the application''' +"""Tests which cover the routes of the application""" + def test_get_root_returns_bad_response(client): - ''' + """ GET to "/" 400 Bad Request - ''' + """ response = client.get("/sample-app/python/") assert response.status_code == 400 def test_get_hello_returns_hello_world(client): - ''' + """ GET to "/hello" 200 OK Body "Hello World!\n" - ''' + """ response = client.get("/sample-app/python/hello") assert [response.text, response.status_code] == ["Hello World!\n", 200] def test_get_metrics_returns_metrics(client): - ''' + """ GET to "/metrics" 200 OK Body containing Prometheus-compatible metrics - ''' + """ response = client.get("/sample-app/python/metrics") assert response.status_code == 200 assert "python_hello_world_requests_total 0.0" in response.text assert "python_hello_world_requests_failed_total 0.0" in response.text + def test_metrics_does_not_expose_created(client): - ''' + """ GET to "/metrics" 200 OK Body does not contain _created gauges for Prometheus-compatible metrics - ''' + """ response = client.get("/sample-app/python/metrics") assert response.status_code == 200 assert "_created" not in response.text + def test_metrics_successfully_increments(client): - ''' + """ GET to "/metrics" 200 OK Body containing metrics which have incremented by 1 - ''' + """ client.get("/sample-app/python/hello") response = client.get("/sample-app/python/metrics") assert response.status_code == 200 assert "python_hello_world_requests_total 1.0" in response.text assert "python_hello_world_requests_failed_total 0.0" in response.text + def test_get_health_returns_health_check(client): - ''' + """ GET to "/health" 200 OK Body "Ok\n" - ''' + """ response = client.get("/sample-app/python/health") assert [response.text, response.status_code] == ["Ok\n", 200] diff --git a/eric-oss-hello-world-python-app/tests/test_mtls_logging.py b/eric-oss-hello-world-python-app/tests/test_mtls_logging.py index a5c9091..42cffdc 100644 --- a/eric-oss-hello-world-python-app/tests/test_mtls_logging.py +++ b/eric-oss-hello-world-python-app/tests/test_mtls_logging.py @@ -1,27 +1,33 @@ -'''Tests which cover the app's logging, both to STDOUT and to Log Aggregator''' +"""Tests which cover the app's logging, both to STDOUT and to Log Aggregator""" + from unittest import mock import requests from mtls_logging import MtlsLogging, Severity def test_log_stdout_and_mtls(caplog): - '''Ensure any log is sent both to STDOUT and through HTTPS''' + """Ensure any log is sent both to STDOUT and through HTTPS""" message = "Message which should appear in both STDOUT and sent as a POST request" - with_mocked_post(send_log, message, Severity.DEBUG, Severity.CRITICAL).assert_called() + with_mocked_post( + send_log, message, Severity.DEBUG, Severity.CRITICAL + ).assert_called() assert message in caplog.text + def test_log_stdout_and_not_mtls(no_log_certs, caplog): # pylint: disable=unused-argument - '''Ensure log is only sent to STDOUT when missing log certs''' + """Ensure log is only sent to STDOUT when missing log certs""" message = "Message which should appear in STDOUT" - error_message = ("Missing TLS logging additional parameter(s): log_ctrl_file app_key app_cert app_cert_file_path") - with_mocked_post(send_log, message, Severity.DEBUG, Severity.CRITICAL).assert_not_called() + error_message = "Missing TLS logging additional parameter(s): log_ctrl_file app_key app_cert app_cert_file_path" + with_mocked_post( + send_log, message, Severity.DEBUG, Severity.CRITICAL + ).assert_not_called() assert message in caplog.text assert error_message in caplog.text def test_log_level_matching_severity(caplog): - '''Ensure a log will output if it matches the severity level of the logger''' + """Ensure a log will output if it matches the severity level of the logger""" for severity in Severity: message = f"Message sent with severity {severity}, should be logged in STDOUT and through POST" with_mocked_post(send_log, message, severity, severity).assert_called() @@ -29,17 +35,20 @@ def test_log_level_matching_severity(caplog): def test_log_ignored(caplog): - '''Ensure that a log will be ignored if we set the minimum severity higher''' + """Ensure that a log will be ignored if we set the minimum severity higher""" message = "Message which should appear in both STDOUT and sent as a POST request" # This test will still call once because the logger announces its log level as INFO severity - with_mocked_post(send_log, message, Severity.INFO, Severity.DEBUG).assert_called_once() + with_mocked_post( + send_log, message, Severity.INFO, Severity.DEBUG + ).assert_called_once() assert not message in caplog.text ################################### HELPERS ################################### + def with_mocked_post(log_function, message, logger_level, log_level): - '''Send a log with mocked POST request, ensure the request is called''' + """Send a log with mocked POST request, ensure the request is called""" with mock.patch.object(requests, "post") as mock_post: mock_post.return_value.status_code = 201 mock_post.return_value.text = message @@ -48,6 +57,6 @@ def with_mocked_post(log_function, message, logger_level, log_level): def send_log(message, logger_level, log_level): - '''Send a log through the MTLS logger''' + """Send a log through the MTLS logger""" logger = MtlsLogging(logger_level) logger.log(message, log_level) From b93b7865b509692b5f866eda72e1824dc057e517 Mon Sep 17 00:00:00 2001 From: Kara Date: Wed, 18 Jun 2025 14:29:35 +0100 Subject: [PATCH 10/34] Fixed markdownlint issues --- README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 31539bf..91fca7c 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,8 @@ with the correct Python Sample App version. Run the following commands from within your project directory `eric-oss-hello-world-python-app-`. -**Note:** The App code present in the SDK portal ZIP package uses mTLS for communication with the platform. +**Note:** The App code present in the SDK portal ZIP package uses +mTLS for communication with the platform. ```bash mkdir -p helloworldAppPackage @@ -157,7 +158,7 @@ Request the following from platform administrator: - A CA certificate, needed for secure communication with the platform APIs. - Client Access to the platform with the required roles, needed for authorized communication with the platform APIs used to *onboard and instantiate* the -Hello World App. You will receive a Client ID from the platform administrator. +Hello World App. You will receive a Client ID from the platform administrator. Include the required roles listed below in the request. | Role | Role Description | @@ -437,7 +438,7 @@ This section describes how the App can communicate with IAM and produce logs to mTLS communication. For more information on the variable values required, see [App Logging Developer Guide to Produce logs](https://developer.intelligentautomationplatform.ericsson.net/#capabilities/app-logging/how-to-produce-logs?chapter=identify-environment-and-secret-variables-names). - The `appSecretName`, `appKeyFileName`, `appCertFileName` - used for mTLS communication to verify the client. + used for mTLS communication to verify the client. ### Steps for Instantiation @@ -508,10 +509,11 @@ An app-instance `id` is shown in the command result > All `userDefinedHelmParameters` are required for successful instantiation of your App. - **Note:** The `authenticationType` defines the authentication method the sample app - will use to communicate with IAM - set to `client-x509` for mTLS or - `legacy-client-secret` for TLS. This parameter is only used to navigate between TLS and mTLS - within the app code. + **Note:** The `authenticationType` defines the authentication method + the sample app will use to communicate with + IAM - set to `client-x509` for mTLS or `legacy-client-secret` for TLS. + This parameter is only used to navigate + between TLS and mTLS within the app code. ```shell curl --cert --key --cacert --location --request POST 'https:///app-lifecycle-management/v3/app-instances//deployment-actions' \ From 7c41a6b9a16e2be678dbd8a432bb1f1da1292231 Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Wed, 18 Jun 2025 14:48:40 +0100 Subject: [PATCH 11/34] Review Comments --- README.md | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 91fca7c..e434d7c 100644 --- a/README.md +++ b/README.md @@ -79,8 +79,7 @@ with the correct Python Sample App version. Run the following commands from within your project directory `eric-oss-hello-world-python-app-`. -**Note:** The App code present in the SDK portal ZIP package uses -mTLS for communication with the platform. +**Note:** mTLS is used for communication with the platform. ```bash mkdir -p helloworldAppPackage @@ -148,12 +147,12 @@ ls ./csar-output ### Prerequisites for Onboarding -Provide the following to platform administrator: +Provide the following to the platform administrator: -- A end-entity ClientCA certificate. +- A end-entity client CA certificate. - Subject DN of end-entity client certificate. -Request the following from platform administrator: +Request the following from the platform administrator: - A CA certificate, needed for secure communication with the platform APIs. - Client Access to the platform with the required roles, needed for authorized @@ -434,11 +433,11 @@ This section describes how the App can communicate with IAM and produce logs to [App Certificate Provisioning Developer Guide](https://developer.intelligentautomationplatform.ericsson.net/#capabilities/app-cert-provisioning/developer-guide) to understand how certificates are loaded into the App during instantiation for secure communication. - - The `logEndpoint` endpoint designed to capture log data, supports only - mTLS communication. For more information on the variable values - required, see [App Logging Developer Guide to Produce logs](https://developer.intelligentautomationplatform.ericsson.net/#capabilities/app-logging/how-to-produce-logs?chapter=identify-environment-and-secret-variables-names). + - The `logEndpoint` which facilitates streaming App logs to platform, + supports only mTLS communication. For more information on the variable + values required, see [App Logging Developer Guide to Produce logs](https://developer.intelligentautomationplatform.ericsson.net/#capabilities/app-logging/how-to-produce-logs?chapter=identify-environment-and-secret-variables-names). - The `appSecretName`, `appKeyFileName`, `appCertFileName` - used for mTLS communication to verify the client. + used for mTLS communication to verify the App. ### Steps for Instantiation @@ -509,11 +508,18 @@ An app-instance `id` is shown in the command result > All `userDefinedHelmParameters` are required for successful instantiation of your App. - **Note:** The `authenticationType` defines the authentication method - the sample app will use to communicate with - IAM - set to `client-x509` for mTLS or `legacy-client-secret` for TLS. - This parameter is only used to navigate - between TLS and mTLS within the app code. +`authenticationType` defines the authentication +method the App will use to communicate with the platform. + +Set: + - `client-x509` for mTLS. + - `legacy-client-secret` for TLS. + + +`authenticationType` is used by the App when retrieving a token. If `client-x509` is set, +a Security Management Component must be configured in the `AppDescriptor.yaml`. Refer to [App Access to REST APIs](#tutorials/app-authentication) for more information. + + ```shell curl --cert --key --cacert --location --request POST 'https:///app-lifecycle-management/v3/app-instances//deployment-actions' \ From 0336301dc36ee1128dd0eee939d8703af42612e5 Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Wed, 18 Jun 2025 17:09:40 +0100 Subject: [PATCH 12/34] PO Review comments addressed --- README.md | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index e434d7c..2fc5af8 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ Move the created .tgz file to the OtherDefinitions/ASD directory. mv eric-oss-hello-world-python-app-.tgz ./helloworldAppPackage/OtherDefinitions/ASD/ ``` -Create a folder where the CSAR app package will be stored into. +Create a folder where the CSAR App package will be stored into. ```bash mkdir csar-output @@ -117,7 +117,7 @@ directory. docker save proj-eric-oss-drop/eric-oss-hello-world-python-app: -o csar-output/docker.tar ``` -Run the following command locally to create a CSAR app package using the +Run the following command locally to create a CSAR App package using the eric-oss-app-package-tool. ```bash @@ -196,8 +196,7 @@ access token: Onboard the **Hello World CSAR App Package** using [App Administration](https://developer.intelligentautomationplatform.ericsson.net/#capabilities/app-administration/developer-guide-manage?chapter=onboard). -To start the onboarding of the Hello World CSAR app, -run the following command in a command line tool. +Run the following command. ```bash curl --cert --key --cacert --location --request POST 'https:///app-onboarding/v2/app-packages' \ @@ -219,7 +218,7 @@ Example of command result: ``` An onboarding-job `id` is shown in the command result -(a2f0a43d-730a-4991-8481-746c3e76556e in the example). +(af036040-a732-4af9-b65a-8103da56c35c in the example). This is the `JOB_ID`. Use the `JOB_ID` to get the status of the onboarding process in the following commands: @@ -389,7 +388,7 @@ Example of command result: } ``` -Run the following command to switch the app mode from 'DISABLED' to 'ENABLED'. +Run the following command to switch the App mode from 'DISABLED' to 'ENABLED'. ```bash curl --cert --key --cacert --location --request PUT 'https:///app-lifecycle-management/v3/apps//mode' \ @@ -424,20 +423,20 @@ This section describes how the App can communicate with IAM and produce logs to certificates key, certificates, and the secrets which store them. The details of the secrets, keys, certs and EIC endpoint details will be passed to App Administration through the `userDefinedHelmParameters` when - instantiating the App. The required parameters are: + instantiating the App. Refer to + [App Certificate Provisioning Developer Guide](https://developer.intelligentautomationplatform.ericsson.net/#capabilities/app-cert-provisioning/developer-guide) + to understand how certificates are loaded into the App during + instantiation for secure communication. The required parameters are: + - The `iamBaseUrl`, as the `/sample-app/python/hello` endpoint of this sample App first communicates with IAM to obtain a client token (login) before returning the "Hello World!!" string output. - - The `platformCaCertSecretName` and `platformCaCertFileName` to enable - secure communication. Refer to - [App Certificate Provisioning Developer Guide](https://developer.intelligentautomationplatform.ericsson.net/#capabilities/app-cert-provisioning/developer-guide) - to understand how certificates are loaded into the App during - instantiation for secure communication. + - The`appSecretName`, `appKeyFileName`, `appCertFileName`, + `platformCaCertSecretName` and `platformCaCertFileName` to enable + secure communication between the App and the platform. - The `logEndpoint` which facilitates streaming App logs to platform, supports only mTLS communication. For more information on the variable values required, see [App Logging Developer Guide to Produce logs](https://developer.intelligentautomationplatform.ericsson.net/#capabilities/app-logging/how-to-produce-logs?chapter=identify-environment-and-secret-variables-names). - - The `appSecretName`, `appKeyFileName`, `appCertFileName` - used for mTLS communication to verify the App. ### Steps for Instantiation @@ -508,18 +507,13 @@ An app-instance `id` is shown in the command result > All `userDefinedHelmParameters` are required for successful instantiation of your App. -`authenticationType` defines the authentication +The `userDefinedHelmParameters`, `authenticationType` defines the authentication method the App will use to communicate with the platform. Set: - `client-x509` for mTLS. - - `legacy-client-secret` for TLS. - - -`authenticationType` is used by the App when retrieving a token. If `client-x509` is set, -a Security Management Component must be configured in the `AppDescriptor.yaml`. Refer to [App Access to REST APIs](#tutorials/app-authentication) for more information. - +`authenticationType` is used by the App when retrieving a token. ```shell curl --cert --key --cacert --location --request POST 'https:///app-lifecycle-management/v3/app-instances//deployment-actions' \ @@ -666,6 +660,10 @@ curl --cert --key Date: Fri, 20 Jun 2025 11:20:35 +0100 Subject: [PATCH 13/34] Changes in Read --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 2fc5af8..ac50bcc 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,9 @@ mv Dockerfile-template Dockerfile Replace `` in the Dockerfile with a slim Python base image. Refer to: [Python on Dockerâ„¢ Hub](https://hub.docker.com/_/python) +Extract the downloaded sample App package. Using a command line tool, +go inside the extracted eric-oss-hello-world-python-app- directory. + Run the following command to build the image. ```bash From 11a443bc464cff4312f0dde71502deaa49eaf571 Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Fri, 20 Jun 2025 11:23:10 +0100 Subject: [PATCH 14/34] Readme changes --- README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ac50bcc..042e10d 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,14 @@ This is a simple Hello World Python App with the following three endpoints: the number of successful and failed invocations of the '/sample-app/python/hello' endpoint. -## Build Docker Image +## Build Docker + +Extract the downloaded sample App package. Using a command line tool, +go inside the extracted eric-oss-hello-world-python-app- directory. + +```bash +cd /eric-oss-hello-world-python-app- +``` Rename the `Dockerfile-template` file to `Dockerfile`. @@ -35,9 +42,6 @@ mv Dockerfile-template Dockerfile Replace `` in the Dockerfile with a slim Python base image. Refer to: [Python on Dockerâ„¢ Hub](https://hub.docker.com/_/python) -Extract the downloaded sample App package. Using a command line tool, -go inside the extracted eric-oss-hello-world-python-app- directory. - Run the following command to build the image. ```bash From 27eba0937a4eb65cc3d2bc19f3c38b7840b21c29 Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Fri, 20 Jun 2025 14:16:01 +0100 Subject: [PATCH 15/34] ReadMe --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 042e10d..255d471 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,10 @@ This is a simple Hello World Python App with the following three endpoints: the number of successful and failed invocations of the '/sample-app/python/hello' endpoint. -## Build Docker +## Build Docker -Extract the downloaded sample App package. Using a command line tool, -go inside the extracted eric-oss-hello-world-python-app- directory. +Extract the downloaded sample App package. Using a command line tool, + go inside the extracted eric-oss-hello-world-python-app- directory. ```bash cd /eric-oss-hello-world-python-app- @@ -518,6 +518,7 @@ The `userDefinedHelmParameters`, `authenticationType` defines the authentication method the App will use to communicate with the platform. Set: + - `client-x509` for mTLS. `authenticationType` is used by the App when retrieving a token. @@ -669,7 +670,8 @@ Within the log viewer, you can filter for App Logging and view the results. **Note:** For TLS communication with platform, set `authenticationType` as `legacy-client-secret` and remove the Security Management Component defined - in the `AppDescriptor.yaml`. Refer to [App Access to REST APIs](#tutorials/app-authentication) for more information. + in the `AppDescriptor.yaml`. Refer to [App Access to REST APIs](https://developer.intelligentautomationplatform.ericsson.net/#tutorials/app-authentication) + for more information. #### Onboard the Hello World Python App APIs From 0072326d2a84d4e0d59e8142833c72c5c8251bf4 Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Fri, 20 Jun 2025 14:49:23 +0100 Subject: [PATCH 16/34] README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 255d471..c7c875c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ **Note:** If you need help accessing the EIAP Ecosystem, contact support -at this **email address:** intelligent.automation.platform@ericsson.com +at this **email address:** ## Introduction @@ -27,7 +27,7 @@ This is a simple Hello World Python App with the following three endpoints: ## Build Docker Extract the downloaded sample App package. Using a command line tool, - go inside the extracted eric-oss-hello-world-python-app- directory. + go inside the extracted `eric-oss-hello-world-python-app-` directory. ```bash cd /eric-oss-hello-world-python-app- From 8735507cfe520a5a8ead84fdb18df3806834c954 Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Fri, 20 Jun 2025 16:35:31 +0100 Subject: [PATCH 17/34] Improvements --- eric-oss-hello-world-python-app/login.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eric-oss-hello-world-python-app/login.py b/eric-oss-hello-world-python-app/login.py index a706bbf..ea9045a 100644 --- a/eric-oss-hello-world-python-app/login.py +++ b/eric-oss-hello-world-python-app/login.py @@ -51,7 +51,7 @@ def tls_login(url, headers): cert = None if authentication_type == "client-x509": - client_id_path = os.path.join( + client_id_path = os.path.join("/", config.get("client_creds_file_path"), config.get("client_id_file_name") ) try: From 0385956465ac5615e69bdb8857f92df9d9407d2b Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Fri, 20 Jun 2025 16:47:05 +0100 Subject: [PATCH 18/34] Test cases fix --- eric-oss-hello-world-python-app/tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eric-oss-hello-world-python-app/tests/conftest.py b/eric-oss-hello-world-python-app/tests/conftest.py index da72dd4..edd19db 100644 --- a/eric-oss-hello-world-python-app/tests/conftest.py +++ b/eric-oss-hello-world-python-app/tests/conftest.py @@ -129,5 +129,5 @@ def populate_environment_variables(): os.environ["APP_CERT"] = "APP_CERT" os.environ["APP_CERT_FILE_PATH"] = "APP_CERT_FILE_PATH" os.environ["AUTHENTICATION_TYPE"] = "client-x509" - os.environ["CLIENT_CREDS_FILE_PATH"] = "eric-oss-hello-world-python-app/tests/" + os.environ["CLIENT_CREDS_FILE_PATH"] = "/eric-oss-hello-world-python-app/tests/" os.environ["CLIENT_ID_FILE_NAME"] = "client_id_example" From a752e8836c803419469f0dd5093c1c99d67c6dff Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Fri, 20 Jun 2025 16:57:21 +0100 Subject: [PATCH 19/34] Revert --- eric-oss-hello-world-python-app/login.py | 2 +- eric-oss-hello-world-python-app/tests/conftest.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eric-oss-hello-world-python-app/login.py b/eric-oss-hello-world-python-app/login.py index ea9045a..a1d4896 100644 --- a/eric-oss-hello-world-python-app/login.py +++ b/eric-oss-hello-world-python-app/login.py @@ -45,7 +45,7 @@ def tls_login(url, headers): app_cert = os.path.join( "/", config.get("app_cert_file_path"), config.get("app_cert") ) - app_key = os.path.join("/", config.get("app_cert_file_path"), config.get("app_key")) + app_key = os.path.join(config.get("app_cert_file_path"), config.get("app_key")) authentication_type = config.get("authentication_type").lower() form_data = {"grant_type": "client_credentials", "tenant_id": "master"} cert = None diff --git a/eric-oss-hello-world-python-app/tests/conftest.py b/eric-oss-hello-world-python-app/tests/conftest.py index edd19db..da72dd4 100644 --- a/eric-oss-hello-world-python-app/tests/conftest.py +++ b/eric-oss-hello-world-python-app/tests/conftest.py @@ -129,5 +129,5 @@ def populate_environment_variables(): os.environ["APP_CERT"] = "APP_CERT" os.environ["APP_CERT_FILE_PATH"] = "APP_CERT_FILE_PATH" os.environ["AUTHENTICATION_TYPE"] = "client-x509" - os.environ["CLIENT_CREDS_FILE_PATH"] = "/eric-oss-hello-world-python-app/tests/" + os.environ["CLIENT_CREDS_FILE_PATH"] = "eric-oss-hello-world-python-app/tests/" os.environ["CLIENT_ID_FILE_NAME"] = "client_id_example" From fe6817a52d16e91e4acb235d17133278265c0662 Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Fri, 20 Jun 2025 17:07:45 +0100 Subject: [PATCH 20/34] : --- eric-oss-hello-world-python-app/login.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eric-oss-hello-world-python-app/login.py b/eric-oss-hello-world-python-app/login.py index a1d4896..0eb8bc2 100644 --- a/eric-oss-hello-world-python-app/login.py +++ b/eric-oss-hello-world-python-app/login.py @@ -51,7 +51,7 @@ def tls_login(url, headers): cert = None if authentication_type == "client-x509": - client_id_path = os.path.join("/", + client_id_path = os.path.join( config.get("client_creds_file_path"), config.get("client_id_file_name") ) try: From 88a9fa26cbd360571d1b58e2423b2a3bdc886ce5 Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Fri, 20 Jun 2025 21:07:26 +0100 Subject: [PATCH 21/34] Improvements --- eric-oss-hello-world-python-app/login.py | 2 +- eric-oss-hello-world-python-app/tests/conftest.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eric-oss-hello-world-python-app/login.py b/eric-oss-hello-world-python-app/login.py index 0eb8bc2..6ab9c74 100644 --- a/eric-oss-hello-world-python-app/login.py +++ b/eric-oss-hello-world-python-app/login.py @@ -51,7 +51,7 @@ def tls_login(url, headers): cert = None if authentication_type == "client-x509": - client_id_path = os.path.join( + client_id_path = os.path.join("/", config.get("client_creds_file_path"), config.get("client_id_file_name") ) try: diff --git a/eric-oss-hello-world-python-app/tests/conftest.py b/eric-oss-hello-world-python-app/tests/conftest.py index da72dd4..78754ce 100644 --- a/eric-oss-hello-world-python-app/tests/conftest.py +++ b/eric-oss-hello-world-python-app/tests/conftest.py @@ -129,5 +129,5 @@ def populate_environment_variables(): os.environ["APP_CERT"] = "APP_CERT" os.environ["APP_CERT_FILE_PATH"] = "APP_CERT_FILE_PATH" os.environ["AUTHENTICATION_TYPE"] = "client-x509" - os.environ["CLIENT_CREDS_FILE_PATH"] = "eric-oss-hello-world-python-app/tests/" + os.environ["CLIENT_CREDS_FILE_PATH"] = os.path.relpath(os.path.dirname(__file__), "/") os.environ["CLIENT_ID_FILE_NAME"] = "client_id_example" From 317e4975e6285737a6759d68fdb61537daf93517 Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Fri, 20 Jun 2025 23:04:52 +0100 Subject: [PATCH 22/34] backward compatilibility --- .../templates/deployment/deployment.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/charts/eric-oss-hello-world-python-app/templates/deployment/deployment.yaml b/charts/eric-oss-hello-world-python-app/templates/deployment/deployment.yaml index 78bd37c..4147916 100644 --- a/charts/eric-oss-hello-world-python-app/templates/deployment/deployment.yaml +++ b/charts/eric-oss-hello-world-python-app/templates/deployment/deployment.yaml @@ -63,10 +63,12 @@ spec: secret: secretName: {{ index .Values "appSecretName" | quote }} defaultMode: 420 + {{- if eq (index .Values "authenticationType") "client-x509" }} - name: client-creds secret: secretName: {{ include "eric-oss-hello-world-python-app.clientSecret" . | quote }} defaultMode: 420 + {{- end }} containers: - name: eric-oss-hello-world-python-app image: {{ template "eric-oss-hello-world-python-app.imagePath" (dict "imageId" "eric-oss-hello-world-python-app" "values" .Values "files" .Files) }} @@ -93,9 +95,11 @@ spec: - name: app-certs mountPath: {{ index .Values "appCertMountPath" | default .Values.instantiationDefaults.appCertMountPath | quote }} readOnly: true + {{- if eq (index .Values "authenticationType") "client-x509" }} - name: client-creds mountPath: {{ index .Values "clientCredsMountPath" | default .Values.instantiationDefaults.clientCredsMountPath | quote }} readOnly: true + {{- end }} env: - name: IAM_CLIENT_ID value: {{ index .Values "clientId" | quote }} @@ -115,10 +119,12 @@ spec: value: {{ index .Values "appCertFileName" | quote }} - name: APP_CERT_FILE_PATH value: {{ index .Values "appCertMountPath" | default .Values.instantiationDefaults.appCertMountPath | quote }} + {{- if eq (index .Values "authenticationType") "client-x509" }} - name: CLIENT_CREDS_FILE_PATH value: {{ index .Values "clientCredsMountPath" | default .Values.instantiationDefaults.clientCredsMountPath | quote }} - name: CLIENT_ID_FILE_NAME value: {{ .Values.global.clientCredentials.secret.clientIdKey | quote }} + {{- end }} - name: AUTHENTICATION_TYPE value: {{ index .Values "authenticationType" | default .Values.instantiationDefaults.authenticationType | quote }} - name: SERVICE_NAME From 52a0288442357cd61cdf9ee1a7643083f3dbaa3d Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Tue, 24 Jun 2025 09:30:35 +0100 Subject: [PATCH 23/34] minor changes --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c7c875c..9413a9e 100644 --- a/README.md +++ b/README.md @@ -161,11 +161,10 @@ Provide the following to the platform administrator: Request the following from the platform administrator: -- A CA certificate, needed for secure communication with the platform APIs. -- Client Access to the platform with the required roles, needed for authorized +- A platform CA certificate, needed for secure communication with the platform APIs. +- Client ID to access the platform with the required roles, needed for authorized communication with the platform APIs used to *onboard and instantiate* the -Hello World App. You will receive a Client ID from the platform administrator. -Include the required roles listed below in the request. +Hello World App. Include the required roles listed below in the request. | Role | Role Description | | --------------------------------------------------------------- | ----------------------------------------------------------------------------- | @@ -668,7 +667,7 @@ curl --cert --key Date: Tue, 24 Jun 2025 18:10:23 +0100 Subject: [PATCH 24/34] SDK Review comments addressed --- README.md | 23 ++++--------------- .../templates/deployment/deployment.yaml | 8 ------- eric-oss-hello-world-python-app/config.py | 2 -- eric-oss-hello-world-python-app/login.py | 7 +++--- eric-oss-hello-world-python-app/main.py | 2 +- .../tests/conftest.py | 7 ------ .../tests/test_login.py | 4 ++-- 7 files changed, 10 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 9413a9e..9afe5bd 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,8 @@ with the correct Python Sample App version. Run the following commands from within your project directory `eric-oss-hello-world-python-app-`. -**Note:** mTLS is used for communication with the platform. +**Note:** X.509 Certificates are used for authentication and + cermTLS is used for communication with the platform. ```bash mkdir -p helloworldAppPackage @@ -436,7 +437,8 @@ This section describes how the App can communicate with IAM and produce logs to - The `iamBaseUrl`, as the `/sample-app/python/hello` endpoint of this sample App first communicates with IAM to obtain a client token (login) - before returning the "Hello World!!" string output. + before returning the "Hello World!!" string output. This endpoint should be + R1 host. - The`appSecretName`, `appKeyFileName`, `appCertFileName`, `platformCaCertSecretName` and `platformCaCertFileName` to enable secure communication between the App and the platform. @@ -513,15 +515,6 @@ An app-instance `id` is shown in the command result > All `userDefinedHelmParameters` are required for successful instantiation of your App. -The `userDefinedHelmParameters`, `authenticationType` defines the authentication -method the App will use to communicate with the platform. - -Set: - - - `client-x509` for mTLS. - -`authenticationType` is used by the App when retrieving a token. - ```shell curl --cert --key --cacert --location --request POST 'https:///app-lifecycle-management/v3/app-instances//deployment-actions' \ --header 'accept: application/json' \ @@ -543,7 +536,6 @@ curl --cert --key ", "appKeyFileName": "", "appCertFileName": "", - "authenticationType": "" } } } @@ -571,7 +563,6 @@ See the following example command result: "logEndpoint": "", "appKeyFileName": "", "appCertFileName": "", - "authenticationType": "", } } } @@ -620,7 +611,6 @@ curl --cert --key ", "appKeyFileName": "", "appCertFileName": "", - "authenticationType": "", }, "namespace": "", "timeout": 5 @@ -667,11 +657,6 @@ curl --cert --key = self.session["expiry_time"]: try: - self.session["token"], self.session["expiry_time"] = login() + self.session["token"], self.session["expiry_time"] = login() # if you want to use legacy client authentication, call login("legacy-client-secret") except Exception as e: # since the token isn't used for anything, # this is just a WARNING level log instead of ERROR diff --git a/eric-oss-hello-world-python-app/tests/conftest.py b/eric-oss-hello-world-python-app/tests/conftest.py index 78754ce..29e3181 100644 --- a/eric-oss-hello-world-python-app/tests/conftest.py +++ b/eric-oss-hello-world-python-app/tests/conftest.py @@ -111,12 +111,6 @@ def no_log_certs(): populate_environment_variables() -@pytest.fixture(scope="function") -def legacy_authentication(): - os.environ["AUTHENTICATION_TYPE"] = "legacy-client-secret" - yield - populate_environment_variables() - def populate_environment_variables(): os.environ["IAM_CLIENT_ID"] = "IAM_CLIENT_ID" @@ -128,6 +122,5 @@ def populate_environment_variables(): os.environ["APP_KEY"] = "APP_KEY" os.environ["APP_CERT"] = "APP_CERT" os.environ["APP_CERT_FILE_PATH"] = "APP_CERT_FILE_PATH" - os.environ["AUTHENTICATION_TYPE"] = "client-x509" os.environ["CLIENT_CREDS_FILE_PATH"] = os.path.relpath(os.path.dirname(__file__), "/") os.environ["CLIENT_ID_FILE_NAME"] = "client_id_example" diff --git a/eric-oss-hello-world-python-app/tests/test_login.py b/eric-oss-hello-world-python-app/tests/test_login.py index 8150896..6a271cf 100644 --- a/eric-oss-hello-world-python-app/tests/test_login.py +++ b/eric-oss-hello-world-python-app/tests/test_login.py @@ -12,9 +12,9 @@ def test_login_receives_token_x509(mock_login_api, config): assert token == "2YotnFZFEjr1zCsicMWpAA" and expiry > time.time() -def test_login_receives_token_legacy(mock_login_api, config, legacy_authentication): +def test_login_receives_token_legacy(mock_login_api, config,): """Check if we receive a token""" - token, expiry = login() + token, expiry = login("legacy-client-secret") assert token == "2YotnFZFEjr1zCsicMWpAA" and expiry > time.time() From d820314a1af6add06f29801250c3ebc3f60582e4 Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Tue, 24 Jun 2025 18:22:56 +0100 Subject: [PATCH 25/34] Some more --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9afe5bd..572584f 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ from within your project directory `eric-oss-hello-world-python-app-`. **Note:** X.509 Certificates are used for authentication and - cermTLS is used for communication with the platform. + mTLS is used for communication with the platform. ```bash mkdir -p helloworldAppPackage @@ -177,6 +177,8 @@ Hello World App. Include the required roles listed below in the request. See [Client Access to REST APIs](https://developer.intelligentautomationplatform.ericsson.net/#tutorials/client-access) for further details on client authentication. +**Note:** Replace `` with the valid R1 host provided by plaform administrator. + Use the following command to generate a valid access token: ```bash @@ -437,8 +439,8 @@ This section describes how the App can communicate with IAM and produce logs to - The `iamBaseUrl`, as the `/sample-app/python/hello` endpoint of this sample App first communicates with IAM to obtain a client token (login) - before returning the "Hello World!!" string output. This endpoint should be - R1 host. + before returning the "Hello World!!" string output. This `iamBaseUrl` + should be R1 host. - The`appSecretName`, `appKeyFileName`, `appCertFileName`, `platformCaCertSecretName` and `platformCaCertFileName` to enable secure communication between the App and the platform. From e880c07df37c08acf6b519b3160c4148846f0b73 Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Tue, 24 Jun 2025 19:55:40 +0100 Subject: [PATCH 26/34] Change --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 572584f..8dbb2bb 100644 --- a/README.md +++ b/README.md @@ -667,7 +667,7 @@ For details, see [Service Exposure - Developer Guide](https://developer.intellig To create an API to be onboarded, run the following commands: ```bash -curl --cert --key --cacert --location --request POST 'https:///hub/apiprovisioning/v1/admin/v3/apis' \ +curl --cacert --location --request POST 'https:///hub/apiprovisioning/v1/admin/v3/apis' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ @@ -693,7 +693,7 @@ To create an endpoint for the previously generated API, run the following command: ```bash -curl --cert --key --cacert --location --request POST 'https:///hub/apiprovisioning/v1/admin/v3/apis/hello-world-python-route-001/endpoints' \ +curl --cacert --location --request POST 'https:///hub/apiprovisioning/v1/admin/v3/apis/hello-world-python-route-001/endpoints' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ @@ -706,7 +706,7 @@ To bind the plugin for authorization of the previously generated API, run the following command: ```bash -curl --cert --key --cacert --location --request PUT 'https:///hub/apiprovisioning/v1/admin/v3/apis/hello-world-python-route-001/phases/auth/plugin-list' \ +curl --cacert --location --request PUT 'https:///hub/apiprovisioning/v1/admin/v3/apis/hello-world-python-route-001/phases/auth/plugin-list' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '[ @@ -720,7 +720,7 @@ To configure the binded plugin for authorization, run the following command: ```bash -curl --cert --key --cacert --location --request PUT 'https:///hub/apiprovisioning/v1/admin/v3/apis/hello-world-python-route-001/plugins/requestPartyTokenInterceptor/configuration' \ +curl --cacert --location --request PUT 'https:///hub/apiprovisioning/v1/admin/v3/apis/hello-world-python-route-001/plugins/requestPartyTokenInterceptor/configuration' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ @@ -738,7 +738,7 @@ Role-Based Access Control (RBAC) configuration is required. To add the RBAC policy run the following curl command: ```bash -curl --cert --key --cacert --location --request POST 'https:///idm/rolemgmt/v1/extapp/rbac' \ +curl --cacert --location --request POST 'https:///idm/rolemgmt/v1/extapp/rbac' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' \ --data '{ From 946d7f2771296379bf30f8589813759d1ab469fe Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Wed, 25 Jun 2025 10:48:59 +0100 Subject: [PATCH 27/34] REadME --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8dbb2bb..f1a26a4 100644 --- a/README.md +++ b/README.md @@ -86,8 +86,8 @@ with the correct Python Sample App version. Run the following commands from within your project directory `eric-oss-hello-world-python-app-`. -**Note:** X.509 Certificates are used for authentication and - mTLS is used for communication with the platform. +**Note:** X.509 certificates are used for authentication, and + mTLS uses them to secure communication between the App and the platform. ```bash mkdir -p helloworldAppPackage @@ -667,7 +667,7 @@ For details, see [Service Exposure - Developer Guide](https://developer.intellig To create an API to be onboarded, run the following commands: ```bash -curl --cacert --location --request POST 'https:///hub/apiprovisioning/v1/admin/v3/apis' \ +curl --cert --key --cacert --location --request POST 'https:///hub/apiprovisioning/v1/admin/v3/apis' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ @@ -693,7 +693,7 @@ To create an endpoint for the previously generated API, run the following command: ```bash -curl --cacert --location --request POST 'https:///hub/apiprovisioning/v1/admin/v3/apis/hello-world-python-route-001/endpoints' \ +curl --cert --key --cacert --location --request POST 'https:///hub/apiprovisioning/v1/admin/v3/apis/hello-world-python-route-001/endpoints' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ @@ -706,7 +706,7 @@ To bind the plugin for authorization of the previously generated API, run the following command: ```bash -curl --cacert --location --request PUT 'https:///hub/apiprovisioning/v1/admin/v3/apis/hello-world-python-route-001/phases/auth/plugin-list' \ +curl --cert --key --cacert --location --request PUT 'https:///hub/apiprovisioning/v1/admin/v3/apis/hello-world-python-route-001/phases/auth/plugin-list' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '[ @@ -720,7 +720,7 @@ To configure the binded plugin for authorization, run the following command: ```bash -curl --cacert --location --request PUT 'https:///hub/apiprovisioning/v1/admin/v3/apis/hello-world-python-route-001/plugins/requestPartyTokenInterceptor/configuration' \ +curl --cert --key --cacert --location --request PUT 'https:///hub/apiprovisioning/v1/admin/v3/apis/hello-world-python-route-001/plugins/requestPartyTokenInterceptor/configuration' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ @@ -738,7 +738,7 @@ Role-Based Access Control (RBAC) configuration is required. To add the RBAC policy run the following curl command: ```bash -curl --cacert --location --request POST 'https:///idm/rolemgmt/v1/extapp/rbac' \ +curl --cert --key --cacert --location --request POST 'https:///idm/rolemgmt/v1/extapp/rbac' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' \ --data '{ From 2f21b42e835c9e2e25bc4014d359651d2951257e Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Wed, 25 Jun 2025 12:04:09 +0100 Subject: [PATCH 28/34] Changes --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f1a26a4..8c102b4 100644 --- a/README.md +++ b/README.md @@ -437,10 +437,9 @@ This section describes how the App can communicate with IAM and produce logs to to understand how certificates are loaded into the App during instantiation for secure communication. The required parameters are: - - The `iamBaseUrl`, as the `/sample-app/python/hello` endpoint of this - sample App first communicates with IAM to obtain a client token (login) - before returning the "Hello World!!" string output. This `iamBaseUrl` - should be R1 host. + - The `iamBaseUrl` must point to the R1 host, as the `/sample-app/python/hello` + endpoint in this sample app first communicates with IAM to obtain a + client token (login) before returning the "Hello World!!" string response. - The`appSecretName`, `appKeyFileName`, `appCertFileName`, `platformCaCertSecretName` and `platformCaCertFileName` to enable secure communication between the App and the platform. From fd322f13957df3e5ea065b3893e0e3dc89c969a9 Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Wed, 25 Jun 2025 12:05:48 +0100 Subject: [PATCH 29/34] quotes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c102b4..1b8c951 100644 --- a/README.md +++ b/README.md @@ -437,7 +437,7 @@ This section describes how the App can communicate with IAM and produce logs to to understand how certificates are loaded into the App during instantiation for secure communication. The required parameters are: - - The `iamBaseUrl` must point to the R1 host, as the `/sample-app/python/hello` + - The `iamBaseUrl` must point to the `R1 host`, as the `/sample-app/python/hello` endpoint in this sample app first communicates with IAM to obtain a client token (login) before returning the "Hello World!!" string response. - The`appSecretName`, `appKeyFileName`, `appCertFileName`, From e54ccf397cf31649e986115993bc3bc60b67a858 Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Wed, 25 Jun 2025 12:37:28 +0100 Subject: [PATCH 30/34] Spell check --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1b8c951..0843ee6 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ Hello World App. Include the required roles listed below in the request. See [Client Access to REST APIs](https://developer.intelligentautomationplatform.ericsson.net/#tutorials/client-access) for further details on client authentication. -**Note:** Replace `` with the valid R1 host provided by plaform administrator. +**Note:** Replace `` with the valid `R1 host` provided by platform administrator. Use the following command to generate a valid access token: From d9f0e6b35392ca43abb5643b93afecbc1047d433 Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Thu, 26 Jun 2025 13:53:07 +0100 Subject: [PATCH 31/34] Review Comments --- README.md | 10 ++----- eric-oss-hello-world-python-app/login.py | 36 ++++++++++-------------- eric-oss-hello-world-python-app/main.py | 2 +- 3 files changed, 19 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 0843ee6..f2a9605 100644 --- a/README.md +++ b/README.md @@ -155,14 +155,10 @@ ls ./csar-output ### Prerequisites for Onboarding -Provide the following to the platform administrator: +Contact the platform administrator to request the following: -- A end-entity client CA certificate. -- Subject DN of end-entity client certificate. - -Request the following from the platform administrator: - -- A platform CA certificate, needed for secure communication with the platform APIs. +- A Client certificate, Client key and platform CA certificate, + needed for secure communication with the platform APIs. - Client ID to access the platform with the required roles, needed for authorized communication with the platform APIs used to *onboard and instantiate* the Hello World App. Include the required roles listed below in the request. diff --git a/eric-oss-hello-world-python-app/login.py b/eric-oss-hello-world-python-app/login.py index c13a0d9..b0e1df3 100644 --- a/eric-oss-hello-world-python-app/login.py +++ b/eric-oss-hello-world-python-app/login.py @@ -16,7 +16,7 @@ class LoginError(Exception): """Raised when EIC login fails""" -def login(authentication_type = "client-x509"): +def login(): """ Get bearer token for accessing platform REST APIs: https://developer.intelligentautomationplatform.ericsson.net/#tutorials/app-authentication @@ -25,7 +25,7 @@ def login(authentication_type = "client-x509"): login_path = "/auth/realms/master/protocol/openid-connect/token" login_url = urljoin(config.get("iam_base_url"), login_path) headers = {"Content-Type": "application/x-www-form-urlencoded"} - resp = tls_login(login_url, headers, authentication_type) + resp = tls_login(login_url, headers) resp = json.loads(resp.decode("utf-8")) token, time_until_expiry = resp["access_token"], resp["expires_in"] time_until_expiry -= ( @@ -34,7 +34,7 @@ def login(authentication_type = "client-x509"): return token, time.time() + time_until_expiry -def tls_login(url, headers, authentication_type = "client-x509"): +def tls_login(url, headers): """ This function sends an HTTP POST request with TLS for the login operation """ @@ -45,26 +45,20 @@ def tls_login(url, headers, authentication_type = "client-x509"): app_cert = os.path.join( "/", config.get("app_cert_file_path"), config.get("app_cert") ) - app_key = os.path.join(config.get("app_cert_file_path"), config.get("app_key")) + app_key = os.path.join( + "/", config.get("app_cert_file_path"), config.get("app_key")) + client_id_path = os.path.join( + "/", config.get("client_creds_file_path"), config.get("client_id_file_name") + ) form_data = {"grant_type": "client_credentials", "tenant_id": "master"} - cert = None - - if authentication_type == "client-x509": - client_id_path = os.path.join("/", - config.get("client_creds_file_path"), config.get("client_id_file_name") - ) - try: - with open(client_id_path, "r") as f: - form_data["client_id"] = f.read().strip() - except OSError as e: - raise LoginError(f"Error while reading client id: {e}") - cert = (app_cert, app_key) - elif authentication_type == "legacy-client-secret": - form_data["client_id"] = config.get("iam_client_id") - form_data["client_secret"] = config.get("iam_client_secret") - else: - raise LoginError(f"Unsupported authentication type: {authentication_type}") + cert = (app_cert, app_key) + try: + with open(client_id_path, "r") as f: + form_data["client_id"] = f.read().strip() + except OSError as e: + raise LoginError(f"Error while reading client id: {e}") + try: response = requests.post( url, data=form_data, headers=headers, timeout=5, verify=ca_cert, cert=cert diff --git a/eric-oss-hello-world-python-app/main.py b/eric-oss-hello-world-python-app/main.py index 758e7dd..170f8a3 100755 --- a/eric-oss-hello-world-python-app/main.py +++ b/eric-oss-hello-world-python-app/main.py @@ -71,7 +71,7 @@ def update_session(self): """Refresh session if it expires.""" if int(time.time()) >= self.session["expiry_time"]: try: - self.session["token"], self.session["expiry_time"] = login() # if you want to use legacy client authentication, call login("legacy-client-secret") + self.session["token"], self.session["expiry_time"] = login() except Exception as e: # since the token isn't used for anything, # this is just a WARNING level log instead of ERROR From ecb5b0f4ee458e616ae252b1e96f2644a76abacc Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Thu, 26 Jun 2025 13:57:20 +0100 Subject: [PATCH 32/34] Test cases --- eric-oss-hello-world-python-app/tests/test_login.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/eric-oss-hello-world-python-app/tests/test_login.py b/eric-oss-hello-world-python-app/tests/test_login.py index 6a271cf..a210ea2 100644 --- a/eric-oss-hello-world-python-app/tests/test_login.py +++ b/eric-oss-hello-world-python-app/tests/test_login.py @@ -12,12 +12,6 @@ def test_login_receives_token_x509(mock_login_api, config): assert token == "2YotnFZFEjr1zCsicMWpAA" and expiry > time.time() -def test_login_receives_token_legacy(mock_login_api, config,): - """Check if we receive a token""" - token, expiry = login("legacy-client-secret") - assert token == "2YotnFZFEjr1zCsicMWpAA" and expiry > time.time() - - def test_login_bad_credentials(requests_mock, config): """Ensure we get an error if credentials are bad""" login_url = urljoin( From ebc89f5d437b5362d8cfe64415e1fa3870d4c518 Mon Sep 17 00:00:00 2001 From: erjxsrn Date: Fri, 27 Jun 2025 11:21:15 +0100 Subject: [PATCH 33/34] Review Changes --- .../templates/_helpers.tpl | 2 +- csar/Definitions/AppDescriptor.yaml | 2 +- .../SecurityManagement/security-metadata.json | 2 +- eric-oss-hello-world-python-app/config.py | 8 ++------ eric-oss-hello-world-python-app/login.py | 9 +++------ eric-oss-hello-world-python-app/main.py | 2 -- eric-oss-hello-world-python-app/mtls_logging.py | 9 +-------- eric-oss-hello-world-python-app/tests/conftest.py | 7 ++----- eric-oss-hello-world-python-app/tests/test_login.py | 4 +--- requirements.txt | 2 +- 10 files changed, 13 insertions(+), 34 deletions(-) diff --git a/charts/eric-oss-hello-world-python-app/templates/_helpers.tpl b/charts/eric-oss-hello-world-python-app/templates/_helpers.tpl index 25f27e0..1b2f747 100644 --- a/charts/eric-oss-hello-world-python-app/templates/_helpers.tpl +++ b/charts/eric-oss-hello-world-python-app/templates/_helpers.tpl @@ -358,4 +358,4 @@ Define the function to get the secret name {{- end }} {{- end }} {{- print $clientSecret }} -{{- end }} \ No newline at end of file +{{- end }} diff --git a/csar/Definitions/AppDescriptor.yaml b/csar/Definitions/AppDescriptor.yaml index 5936c4a..1dc6c86 100644 --- a/csar/Definitions/AppDescriptor.yaml +++ b/csar/Definitions/AppDescriptor.yaml @@ -11,4 +11,4 @@ AppComponentList: - NameofComponent: security-mgmt Version: 1.0.0 Path: OtherDefinitions/SecurityManagement - ArtefactType: SecurityManagement \ No newline at end of file + ArtefactType: SecurityManagement diff --git a/csar/OtherDefinitions/SecurityManagement/security-metadata.json b/csar/OtherDefinitions/SecurityManagement/security-metadata.json index e3f8d86..cd02ad6 100644 --- a/csar/OtherDefinitions/SecurityManagement/security-metadata.json +++ b/csar/OtherDefinitions/SecurityManagement/security-metadata.json @@ -1,3 +1,3 @@ { "authenticatorType": "client-x509" -} \ No newline at end of file +} diff --git a/eric-oss-hello-world-python-app/config.py b/eric-oss-hello-world-python-app/config.py index b9e35b2..603fa5e 100644 --- a/eric-oss-hello-world-python-app/config.py +++ b/eric-oss-hello-world-python-app/config.py @@ -4,9 +4,7 @@ def get_config(): - """ - get env and return config with all env vals required - """ + """get env and return config with all env vals required""" iam_client_id = get_os_env_string("IAM_CLIENT_ID", "") iam_client_secret = get_os_env_string("IAM_CLIENT_SECRET", "") iam_base_url = get_os_env_string("IAM_BASE_URL", "") @@ -38,7 +36,5 @@ def get_config(): def get_os_env_string(env_name, default_value): - """ - get env - """ + """get env""" return os.getenv(env_name, default_value).strip() diff --git a/eric-oss-hello-world-python-app/login.py b/eric-oss-hello-world-python-app/login.py index b0e1df3..4da2e89 100644 --- a/eric-oss-hello-world-python-app/login.py +++ b/eric-oss-hello-world-python-app/login.py @@ -7,15 +7,14 @@ import os from urllib.parse import urljoin import json -import requests import time +import requests from config import get_config class LoginError(Exception): """Raised when EIC login fails""" - def login(): """ Get bearer token for accessing platform REST APIs: @@ -35,9 +34,7 @@ def login(): def tls_login(url, headers): - """ - This function sends an HTTP POST request with TLS for the login operation - """ + """This function sends an HTTP POST request with TLS for the login operation""" config = get_config() ca_cert = os.path.join( "/", config.get("ca_cert_file_path"), config.get("ca_cert_file_name") @@ -58,7 +55,7 @@ def tls_login(url, headers): form_data["client_id"] = f.read().strip() except OSError as e: raise LoginError(f"Error while reading client id: {e}") - + try: response = requests.post( url, data=form_data, headers=headers, timeout=5, verify=ca_cert, cert=cert diff --git a/eric-oss-hello-world-python-app/main.py b/eric-oss-hello-world-python-app/main.py index 170f8a3..8d3611b 100755 --- a/eric-oss-hello-world-python-app/main.py +++ b/eric-oss-hello-world-python-app/main.py @@ -20,10 +20,8 @@ SERVICE_PREFIX = "python_hello_world" - class Application(Flask): """The Flask application itself. Subclassed for testing.""" - def __init__(self): super().__init__(__name__) disable_created_metrics() diff --git a/eric-oss-hello-world-python-app/mtls_logging.py b/eric-oss-hello-world-python-app/mtls_logging.py index 096584a..47fc012 100644 --- a/eric-oss-hello-world-python-app/mtls_logging.py +++ b/eric-oss-hello-world-python-app/mtls_logging.py @@ -1,5 +1,4 @@ """This module handles mTLS logging""" - import json import os import logging @@ -12,18 +11,15 @@ class Severity(IntEnum): """We use this to map the logging library severity to the mTLS logging""" - DEBUG = 10 INFO = 20 WARNING = 30 ERROR = 40 CRITICAL = 50 - # pylint: disable=too-few-public-methods class MtlsLogging: """mTLS logger which will log to STDOUT, as well as Log Aggregator""" - def __init__(self, level=None): werkzeug_logger = logging.getLogger("werkzeug") werkzeug_logger.setLevel(logging.ERROR) @@ -60,10 +56,7 @@ def __init__(self, level=None): self.log(f"Level set to: {level}", Severity.INFO) def log(self, message, severity): - """ - Send request to log aggregator with mTLS - """ - + """Send request to log aggregator with mTLS""" # Determine if certs are set. cert_available = ( self.config.get("ca_cert_file_name") != "" diff --git a/eric-oss-hello-world-python-app/tests/conftest.py b/eric-oss-hello-world-python-app/tests/conftest.py index 29e3181..c5cda80 100644 --- a/eric-oss-hello-world-python-app/tests/conftest.py +++ b/eric-oss-hello-world-python-app/tests/conftest.py @@ -1,12 +1,9 @@ -""" -Configure a Flask fixture based off the Application defined in main.py -""" - +"""Configure a Flask fixture based off the Application defined in main.py""" import os +from urllib.parse import urljoin import pytest import requests_mock from prometheus_client import REGISTRY as GLOBAL_METRICS_REGISTRY -from urllib.parse import urljoin from main import Application from config import get_config diff --git a/eric-oss-hello-world-python-app/tests/test_login.py b/eric-oss-hello-world-python-app/tests/test_login.py index a210ea2..6b0ffab 100644 --- a/eric-oss-hello-world-python-app/tests/test_login.py +++ b/eric-oss-hello-world-python-app/tests/test_login.py @@ -1,10 +1,8 @@ """Tests which ensure the application handles Authentication & Authorisation properly""" - from urllib.parse import urljoin +import time from login import login, LoginError import pytest -import time - def test_login_receives_token_x509(mock_login_api, config): """Check if we receive a token""" diff --git a/requirements.txt b/requirements.txt index 50df0a4..b9f5b91 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ flask==3.0.1 requests==2.32.4 -prometheus-client==0.20.0 \ No newline at end of file +prometheus-client==0.20.0 From 5ebd88f2cf0d7ad091def1ac305181e3bcbe84c0 Mon Sep 17 00:00:00 2001 From: Kara Date: Fri, 27 Jun 2025 11:36:35 +0100 Subject: [PATCH 34/34] Updated version --- version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version b/version index acf9bf0..0c89fc9 100644 --- a/version +++ b/version @@ -1 +1 @@ -3.2.2 \ No newline at end of file +4.0.0 \ No newline at end of file