Emre Göçmen Blog

How are Security and Authorization Policies Implemented in SAP OData Services?

5 min. read
927 views
0 comments

Emre Göçmen

Author

How are Security and Authorization Policies Implemented in SAP OData Services?

How to Implement Security and Authorization Policies in SAP OData Services

While SAP OData services provide flexibility and power in data access, securing these services is of critical importance. In this comprehensive guide, we will address the security and authorization policies you need to implement to protect your SAP OData services.


Authentication and Authorization

1. Authentication Methods

Implementing strong authentication mechanisms for your OData services is a cornerstone of security:

Basic Authentication: The simplest method, but should only be used with HTTPS.

* Basic Authentication Configuration in SAP Gateway
* Service configuration in SICF transaction
DATA: lo_server        TYPE REF TO if_http_server,
      lo_authentication TYPE REF TO if_http_authentication.

lo_server ?= server.
lo_authentication = lo_server->get_authentication( ).

* Basic authentication check
IF lo_authentication->authenticate( ) NE abap_true.
  lo_server->response->set_status( code = 401 reason = 'Unauthorized' ).
  lo_server->response->set_header_field( name = 'WWW-Authenticate' value = 'Basic' ).
  RETURN.
ENDIF.

SAP Logon Ticket: Used for Single Sign-On (SSO) between SAP systems.

* SAP Logon Ticket validation
DATA: lv_ticket TYPE string,
      ls_ticket_info TYPE STANDARD TABLE OF bapiticket.

* Get ticket from HTTP header
lv_ticket = server->request->get_header_field( 'SAP-PASSPORT' ).

* Validate the ticket
CALL FUNCTION 'RSTS_EVALUATE_TICKET'
  EXPORTING
    ticket            = lv_ticket
  TABLES
    ticket_info       = ls_ticket_info
  EXCEPTIONS
    ticket_not_found  = 1
    invalid_signature = 2
    invalid_ticket    = 3
    OTHERS            = 4.

IF sy-subrc NE 0.
  server->response->set_status( code = 401 reason = 'Unauthorized' ).
  RETURN.
ENDIF.

OAuth 2.0: A secure protocol preferred for modern web services, especially for integration with external systems.

* Using SAP's OAuth library for OAuth integration
DATA: lo_oauth_client TYPE REF TO cl_oauth2_client,
      lv_token TYPE string,
      lv_valid TYPE abap_bool.

* Create OAuth client
CREATE OBJECT lo_oauth_client
  EXPORTING
    i_profile = 'ZOAUTH_PROFILE'.  " OAuth configuration profile

* Get and validate access token
lv_token = server->request->get_header_field( 'Authorization' ).
lv_token = replace( val = lv_token sub = 'Bearer ' with = '' ).

lv_valid = lo_oauth_client->validate_token( lv_token ).

IF lv_valid EQ abap_false.
  server->response->set_status( code = 401 reason = 'Unauthorized' ).
  RETURN.
ENDIF.

2. Authorization Strategies

While authentication answers the question "who are you?", authorization answers "what can you do?":

Service-Level Authorization: Permission control implemented through SAP Gateway authorization.

* Define service authorization object in /IWFND/GW_CLIENT transaction
* Then create roles for the relevant authorization object in PFCG transaction

* Authority check in SICF service handler
DATA: lo_server TYPE REF TO if_http_server,
      ls_user_info TYPE usr02.

lo_server ?= server.
SELECT SINGLE * FROM usr02 INTO ls_user_info
  WHERE bname = sy-uname.

* Service access control
AUTHORITY-CHECK OBJECT 'S_SERVICE'
  ID 'SERVICE' FIELD 'ZODATA_SRV'
  ID 'ACTVT'   FIELD '16'.  " 16 = Execute

IF sy-subrc NE 0.
  lo_server->response->set_status( code = 403 reason = 'Forbidden' ).
  RETURN.
ENDIF.

Data Access Control: Implement data access controls to limit what data a user can view or modify.

* Data access control example (in DPC_EXT class)
METHOD materials_get_entityset.
  " First fetch all data
  SELECT * FROM mara INTO TABLE @DATA(lt_materials).
  
  " Apply authority check
  LOOP AT lt_materials ASSIGNING FIELD-SYMBOL(<fs_material>).
    " Material group authority check
    AUTHORITY-CHECK OBJECT 'M_MATE_MAT'
                    ID 'MATKL' FIELD <fs_material>-matkl
                    ID 'ACTVT' FIELD '03'.  " 03 = Display
    
    IF sy-subrc NE 0.
      " User doesn't have permission to see this material, remove from list
      DELETE lt_materials WHERE matnr = <fs_material>-matnr.
    ENDIF.
  ENDLOOP.
  
  " Copy results to OData response
  MOVE-CORRESPONDING lt_materials TO et_entityset.
ENDMETHOD.

Operation-Level Authorization: Define specific authorization rules for CRUD (Create, Read, Update, Delete) operations.

* Authority check for CRUD operations (in DPC_EXT class)
METHOD check_authority_for_operation.
  IMPORTING
    iv_operation TYPE string
    iv_entity_type TYPE string
  RETURNING
    rv_authorized TYPE abap_bool.
    
  CASE iv_operation.
    WHEN 'CREATE'.
      AUTHORITY-CHECK OBJECT 'ZMATERIAL'
        ID 'ACTVT' FIELD '01'.  " 01 = Create
        
    WHEN 'UPDATE'.
      AUTHORITY-CHECK OBJECT 'ZMATERIAL'
        ID 'ACTVT' FIELD '02'.  " 02 = Change
        
    WHEN 'DELETE'.
      AUTHORITY-CHECK OBJECT 'ZMATERIAL'
        ID 'ACTVT' FIELD '06'.  " 06 = Delete
        
    WHEN 'READ'.
      AUTHORITY-CHECK OBJECT 'ZMATERIAL'
        ID 'ACTVT' FIELD '03'.  " 03 = Display
  ENDCASE.
  
  " Return response based on authority check result
  IF sy-subrc = 0.
    rv_authorized = abap_true.
  ELSE.
    rv_authorized = abap_false.
  ENDIF.
ENDMETHOD.

Data Privacy and Protection

1. Data Leak Prevention

Transmitting only necessary data through OData services is important for both security and performance:

Using $select: Encourage OData clients to select only the fields they need.

* Enable select in Entity Set metadata (in SEGW transaction)
lo_entity_set->set_selectable( abap_true ).

* Example client usage:
* /sap/opu/odata/SAP/ZODATA_SRV/MaterialSet?$select=Matnr,Maktx

Limiting Data Scope: Avoid returning very large datasets.

* Limiting data scope in DPC_EXT class
METHOD limit_result_set.
  IMPORTING
    it_data TYPE ANY TABLE
    iv_max_records TYPE i DEFAULT 1000
  RETURNING
    rt_limited_data TYPE STANDARD TABLE.
    
  " Copy data not exceeding maximum record count
  DATA(lv_count) = COND #( WHEN lines( it_data ) > iv_max_records
                           THEN iv_max_records
                           ELSE lines( it_data ) ).
                           
  rt_limited_data = VALUE #( FOR i = 1 UNTIL i > lv_count
                              ( it_data[ i ] ) ).
ENDMETHOD.

2. Data Masking and Encryption

Use masking and encryption techniques to protect sensitive data:

Sensitive Data Masking: Display masked credit card numbers, SSNs, and other sensitive information.

* Data masking example in DPC_EXT class
METHOD mask_sensitive_data.
  IMPORTING
    iv_value TYPE string
    iv_type  TYPE string
  RETURNING
    rv_masked TYPE string.
    
  CASE iv_type.
    WHEN 'CREDIT_CARD'.
      " Mask credit card number except last 4 digits
      rv_masked = COND #( WHEN strlen( iv_value ) > 4
                          THEN |XXXX-XXXX-XXXX-{ right( iv_value, 4 ) }|
                          ELSE iv_value ).
                          
    WHEN 'SSN'.
      " Mask SSN's middle digits
      rv_masked = COND #( WHEN strlen( iv_value ) = 9
                          THEN |{ substring( val = iv_value off = 0 len = 3 ) }-XX-{ right( iv_value, 4 ) }|
                          ELSE iv_value ).
                          
    WHEN OTHERS.
      rv_masked = iv_value.
  ENDCASE.
ENDMETHOD.

Data Encryption: Use encryption for particularly privacy-sensitive data.

* Encrypting data using SAP Secure Store and Forward (SSF)
METHOD encrypt_sensitive_data.
  IMPORTING
    iv_data TYPE string
  RETURNING
    rv_encrypted TYPE string.
    
  DATA: lv_envelope   TYPE xstring,
        lv_subject    TYPE string,
        lv_plain_data TYPE xstring.
        
  " Convert data to binary format
  CALL FUNCTION 'SCMS_STRING_TO_XSTRING'
    EXPORTING
      text   = iv_data
    IMPORTING
      buffer = lv_plain_data
    EXCEPTIONS
      failed = 1
      OTHERS = 2.
      
  IF sy-subrc <> 0.
    RETURN.
  ENDIF.
  
  " Encrypt the data
  CALL FUNCTION 'SSF_KRN_ENVELOPE'
    EXPORTING
      ostr_input                  = lv_plain_data
    IMPORTING
      ostr_output                 = lv_envelope
    EXCEPTIONS
      ssf_krn_error               = 1
      ssf_krn_noop                = 2
      ssf_krn_nomemory            = 3
      ssf_krn_opinv               = 4
      ssf_krn_nossflib            = 5
      ssf_krn_recipient_error     = 6
      ssf_krn_input_data_error    = 7
      ssf_krn_invalid_par         = 8
      ssf_krn_invalid_parlen      = 9
      ssf_fb_input_parameter_error = 10
      OTHERS                      = 11.
      
  IF sy-subrc = 0.
    " Base64 encode the encrypted data
    CALL FUNCTION 'SCMS_XSTRING_TO_BASE64'
      EXPORTING
        buffer        = lv_envelope
      IMPORTING
        output_string = rv_encrypted.
  ENDIF.
ENDMETHOD.

Firewalls and Attack Prevention

1. OData Endpoint Security

Protect your OData endpoints with various security measures:

API Rate Limiting: Implement request limiting to prevent DoS (Denial of Service) attacks.

* Simple request counter implementation (memory-based)
METHOD check_rate_limit.
  IMPORTING
    iv_user_id TYPE string
  RETURNING
    rv_allowed TYPE abap_bool.
    
  DATA: lv_current_timestamp TYPE timestampl,
        lv_max_requests TYPE i VALUE 100,  " Maximum 100 requests in 10 minutes
        lv_time_window TYPE i VALUE 600.   " 10 minutes (in seconds)
        
  GET TIME STAMP FIELD lv_current_timestamp.
  
  " Check the user's request counter
  SELECT COUNT(*) FROM zapi_request_log INTO @DATA(lv_request_count)
    WHERE user_id = @iv_user_id
      AND request_timestamp > @lv_current_timestamp - @lv_time_window.
      
  " Reject if request count exceeds limit
  IF lv_request_count >= lv_max_requests.
    rv_allowed = abap_false.
  ELSE.
    " Log the request
    INSERT INTO zapi_request_log VALUES @( VALUE #(
      user_id = iv_user_id
      request_timestamp = lv_current_timestamp
    ) ).
    
    rv_allowed = abap_true.
  ENDIF.
ENDMETHOD.

Whitelist IP Filtering: Allow requests only from trusted IP addresses.

* IP check (in SICF handler)
METHOD check_allowed_ip.
  IMPORTING
    io_server TYPE REF TO if_http_server
  RETURNING
    rv_allowed TYPE abap_bool.
    
  DATA: lv_ip_address TYPE string.
  
  " Get client IP address
  lv_ip_address = io_server->request->get_header_field( '~remote_addr' ).
  
  " Check against allowed IP list
  SELECT COUNT(*) FROM zallowed_ips INTO @DATA(lv_count)
    WHERE ip_address = @lv_ip_address.
    
  rv_allowed = COND #( WHEN lv_count > 0 THEN abap_true ELSE abap_false ).
ENDMETHOD.

2. Preventing Injection Attacks

Some of the most common attack vectors in OData services are injection attacks:

SQL Injection Prevention: Prevent SQL injection by using parameterized queries.

* SQL Injection Prevention Methods
* 1. Always use parameterized queries:

" WRONG - vulnerable to SQL injection:
* DATA(lv_where) = |MATNR = '{ iv_matnr }'|.
* SELECT * FROM mara WHERE (lv_where) INTO TABLE @lt_result.

" RIGHT - parameterized query:
DATA(lv_matnr) = iv_matnr.
SELECT * FROM mara WHERE matnr = @lv_matnr INTO TABLE @lt_result.

* 2. Securely building dynamic WHERE conditions for SELECT:
DATA: lt_where_clauses TYPE STANDARD TABLE OF string,
      lv_where TYPE string.
      
" Build dynamic filter conditions safely
IF iv_matnr IS NOT INITIAL.
  APPEND |MATNR = @LV_MATNR| TO lt_where_clauses.
ENDIF.

IF iv_matkl IS NOT INITIAL.
  APPEND |MATKL = @LV_MATKL| TO lt_where_clauses.
ENDIF.

" Combine WHERE conditions
IF lt_where_clauses IS NOT INITIAL.
  lv_where = CONCAT_LINES_OF( table = lt_where_clauses sep = | AND | ).
  
  " Always create query with parameter references
  SELECT * FROM mara WHERE (lv_where) INTO TABLE @lt_result.
ENDIF.

XSS (Cross-Site Scripting) Prevention: Properly validate and sanitize user inputs.

* Sanitizing user input against XSS attacks
METHOD sanitize_user_input.
  IMPORTING
    iv_input TYPE string
  RETURNING
    rv_sanitized TYPE string.
    
  " Convert HTML special characters
  rv_sanitized = iv_input.
  
  " Convert < and > characters
  REPLACE ALL OCCURRENCES OF '<' IN rv_sanitized WITH '<'.
  REPLACE ALL OCCURRENCES OF '>' IN rv_sanitized WITH '>'.
  
  " Convert quote characters
  REPLACE ALL OCCURRENCES OF '"' IN rv_sanitized WITH '"'.
  REPLACE ALL OCCURRENCES OF '''' IN rv_sanitized WITH '''.
  
  " Remove potentially harmful script tags
  REPLACE ALL OCCURRENCES OF REGEX '.*?' IN rv_sanitized WITH '' IGNORING CASE.
  REPLACE ALL OCCURRENCES OF REGEX 'javascript:' IN rv_sanitized WITH '' IGNORING CASE.
  REPLACE ALL OCCURRENCES OF REGEX 'onerror=' IN rv_sanitized WITH '' IGNORING CASE.
  
  RETURN rv_sanitized.
ENDMETHOD.

Error Management and Secure Feedback

1. Secure Error Handling

Error messages can expose vulnerabilities, so they must be handled carefully:

Hiding Sensitive Information: Avoid sharing sensitive information in error messages shown to users.

* Secure error handling in DPC_EXT class
METHOD handle_exception_safely.
  IMPORTING
    ix_exception TYPE REF TO cx_root
    io_response TYPE REF TO /iwbep/if_mgw_response
  RETURNING
    rv_handled TYPE abap_bool.
    
  DATA: lv_message TYPE string,
        lv_tech_message TYPE string.
        
  " Get technical error message
  lv_tech_message = ix_exception->get_text( ).
  
  " Log technical details to error log (for debugging)
  log_error_to_database(
    iv_error_text = lv_tech_message
    iv_user = sy-uname
    iv_timestamp = sy-datum && sy-uzeit
  ).
  
  " Create appropriate and secure message for user
  CASE TYPE OF ix_exception.
    WHEN cx_sy_open_sql_db.
      " Database error - don't show technical details
      lv_message = 'A database access error occurred. Please try again later.'.
      
    WHEN cx_sy_authorization_error.
      " Authorization error
      lv_message = 'You do not have permission for this operation.'.
      
    WHEN OTHERS.
      " Generic error message
      lv_message = 'An unexpected error occurred during the operation. Please contact your system administrator for assistance.'.
  ENDCASE.
  
  " Add error message to response
  io_response->set_message_text( lv_message ).
  
  " Set appropriate HTTP status code
  CASE TYPE OF ix_exception.
    WHEN cx_sy_authorization_error.
      io_response->set_status_code( /iwbep/if_mgw_core_types=>gcs_http_status_codes-forbidden ). " 403
    WHEN cx_sy_open_sql_db.
      io_response->set_status_code( /iwbep/if_mgw_core_types=>gcs_http_status_codes-internal_server_error ). " 500
    WHEN OTHERS.
      io_response->set_status_code( /iwbep/if_mgw_core_types=>gcs_http_status_codes-bad_request ). " 400
  ENDCASE.
  
  " Error has been handled
  rv_handled = abap_true.
ENDMETHOD.

2. Security Incident Monitoring

Continuously monitor the security status of your OData services:

Audit Logging: Log all OData accesses and security incidents.

* Creating OData access audit log
METHOD log_odata_access.
  IMPORTING
    iv_entity_name   TYPE string
    iv_operation     TYPE string
    iv_user          TYPE syuname
    iv_source_ip     TYPE string
    iv_success       TYPE abap_bool
    iv_error_message TYPE string OPTIONAL.
    
  " Create log record
  DATA: ls_log TYPE zodata_access_log.
  
  ls_log-entity_name   = iv_entity_name.
  ls_log-operation     = iv_operation.
  ls_log-user_id       = iv_user.
  ls_log-source_ip     = iv_source_ip.
  ls_log-access_time   = sy-datum && sy-uzeit.
  ls_log-success_flag  = iv_success.
  ls_log-error_message = iv_error_message.
  
  " Add log record to database
  INSERT zodata_access_log FROM ls_log.
ENDMETHOD.

Abnormal Behavior Detection: Detect suspicious or abnormal access patterns and generate alerts.

* Suspicious access detection
METHOD detect_suspicious_activity.
  IMPORTING
    iv_user TYPE syuname
  RETURNING
    rv_suspicious TYPE abap_bool.
    
  DATA: lv_current_time TYPE tzntstmpl,
        lv_threshold TYPE i VALUE 50,  " More than 50 requests in 5 minutes is suspicious
        lv_timeframe TYPE i VALUE 300. " 5 minutes (in seconds)
        
  GET TIME STAMP FIELD lv_current_time.
  
  " Check request count in the last 5 minutes
  SELECT COUNT(*) FROM zodata_access_log INTO @DATA(lv_request_count)
    WHERE user_id = @iv_user
      AND access_time > @lv_current_time - @lv_timeframe.
      
  " Check failed login attempts
  SELECT COUNT(*) FROM zodata_access_log INTO @DATA(lv_failed_count)
    WHERE user_id = @iv_user
      AND access_time > @lv_current_time - @lv_timeframe
      AND success_flag = @abap_false.
      
  " Check access from different IP addresses
  SELECT COUNT( DISTINCT source_ip ) FROM zodata_access_log 
    INTO @DATA(lv_ip_count)
    WHERE user_id = @iv_user
      AND access_time > @lv_current_time - @lv_timeframe.
      
  " Check for suspicious activity
  rv_suspicious = COND #( WHEN lv_request_count > lv_threshold 
                            OR lv_failed_count > 5
                            OR lv_ip_count > 3
                          THEN abap_true
                          ELSE abap_false ).
                          
  " Send notification when suspicious activity is detected
  IF rv_suspicious = abap_true.
    send_security_alert(
      iv_alert_type = 'SUSPICIOUS_ACTIVITY'
      iv_user = iv_user
      iv_details = |{ lv_request_count } requests, { lv_failed_count } failed, { lv_ip_count } different IPs|
    ).
  ENDIF.
ENDMETHOD.

Application-Level Security

1. Communication Security

Protect the communication between your OData services and clients:

HTTPS Requirement: Mandate the use of HTTPS for all OData traffic.

* HTTPS requirement in SICF
IF server->request->get_header_field( '~server_protocol' ) NE 'HTTPS'.
  " Reject non-HTTPS requests
  server->response->set_status( code = 403 reason = 'HTTPS Required' ).
  server->response->set_cdata( 'This service is only accessible via HTTPS.' ).
  RETURN.
ENDIF.

HTTP Security Headers: Configure HTTP security headers for additional security.

* Setting security headers
METHOD set_security_headers.
  IMPORTING
    io_response TYPE REF TO if_http_response.
    
  " Content Security Policy (CSP) - helps prevent XSS attacks
  io_response->set_header_field( name = 'Content-Security-Policy'
                                value = 'default-src ''self''' ).
                                
  " X-XSS-Protection - enables XSS protection in some browsers
  io_response->set_header_field( name = 'X-XSS-Protection'
                                value = '1; mode=block' ).
                                
  " X-Content-Type-Options - prevents MIME type misuse
  io_response->set_header_field( name = 'X-Content-Type-Options'
                                value = 'nosniff' ).
                                
  " Referrer-Policy - controls which referrer information is sent in external links
  io_response->set_header_field( name = 'Referrer-Policy'
                                value = 'same-origin' ).
                                
  " Strict-Transport-Security - forces HTTPS usage
  io_response->set_header_field( name = 'Strict-Transport-Security'
                                value = 'max-age=31536000; includeSubDomains' ).
ENDMETHOD.

2. Security Implementation and Improvement

Continuously improve the security posture of your OData services:

Security Testing: Regularly perform penetration tests and vulnerability scans.

* Security testing checklist:
* 1. Using open-source security scanners for OData services
* 2. Authorization bypass tests
* 3. SQL injection and XSS tests
* 4. DoS attack resilience tests
* 5. Authentication security tests

* Example test scenario code (for automation):
METHOD run_security_tests.
  DATA: lt_test_results TYPE STANDARD TABLE OF zsecurity_test_result.
  
  " Authorization bypass test
  lt_test_results = VALUE #( BASE lt_test_results
    ( test_id = 'AUTH-001'
      test_name = 'Entity access without role'
      result = test_entity_access_without_role( )
      timestamp = sy-datum && sy-uzeit ) ).
      
  " Filter bypass test
  lt_test_results = VALUE #( BASE lt_test_results
    ( test_id = 'FILTER-001'
      test_name = 'SQL Injection Filter'
      result = test_sql_injection_filter( )
      timestamp = sy-datum && sy-uzeit ) ).
      
  " Save test results
  MODIFY zsecurity_test_log FROM TABLE lt_test_results.
  
  " Generate alert
  DATA(lv_failed_tests) = REDUCE i( INIT x = 0 FOR wa IN lt_test_results
                                  WHERE ( result = abap_false ) NEXT x = x + 1 ).
  
  IF lv_failed_tests > 0.
    send_security_alert(
      iv_alert_type = 'SECURITY_TEST_FAILED'
      iv_details = |{ lv_failed_tests } security tests failed|
    ).
  ENDIF.
ENDMETHOD.

Security Updates: Keep your OData components and dependencies up to date.

* SAP Note check function
METHOD check_sap_security_notes.
  IMPORTING
    iv_component TYPE string
  RETURNING
    rt_missing_notes TYPE STANDARD TABLE OF string.
    
  DATA: lt_implemented_notes TYPE STANDARD TABLE OF string,
        lt_required_notes TYPE STANDARD TABLE OF string.
  
  " Check SAP Notes implemented in your system
  CALL FUNCTION 'SNOTE_GET_IMPLEMENTED_NOTES'
    TABLES
      notetab = lt_implemented_notes.
      
  " Get required security notes for this component (example)
  CASE iv_component.
    WHEN 'ODATA'.
      lt_required_notes = VALUE #( ( '1234567' ) ( '2345678' ) ( '3456789' ) ).
    WHEN 'GATEWAY'.
      lt_required_notes = VALUE #( ( '2345678' ) ( '3456789' ) ( '4567890' ) ).
    WHEN OTHERS.
      " Default security notes
      lt_required_notes = VALUE #( ( '1111111' ) ( '2222222' ) ).
  ENDCASE.
  
  " Determine missing notes
  LOOP AT lt_required_notes INTO DATA(lv_required_note).
    READ TABLE lt_implemented_notes TRANSPORTING NO FIELDS
      WITH KEY table_line = lv_required_note.
      
    IF sy-subrc <> 0.
      " Note not implemented
      APPEND lv_required_note TO rt_missing_notes.
    ENDIF.
  ENDLOOP.
ENDMETHOD.

Conclusion

Implementing security and authorization policies in SAP OData services is critical to ensuring your data security and preventing unauthorized access. Using the strategies and code examples covered in this guide, you can protect your OData services with strong authentication, robust authorization, data protection, and attack prevention mechanisms.

By implementing these security measures, you can provide secure, reliable, and performant OData services for both internal users and external consumers.

As with any security strategy, no single measure is sufficient. By adopting a defense-in-depth approach and implementing multiple layers of security, you can significantly enhance the security of your SAP OData services.

Comments

0

You must be logged in to comment.

No comments yet.

Be the first to comment.

Emre Göçmen

Author & Developer

I write about my experiences as a SAP ABAP & Full Stack developer.

Category

SAP

SAP

Subscribe to Newsletter

Subscribe to my newsletter to get notified about new articles.