+"! Authenticators support one or multiple services and are able to generate access tokens using the +"! service's API using the users username, password and two factor authentication token +"! (app/sms/tokengenerator). With these access tokens the user can be authenticated to the service's +"! implementation of the git http api, just like the "normal" password would. +"!
+"!+"! LCL_2FA_AUTHENTICATOR_REGISTRY can be used to find a suitable implementation for a given +"! repository. +"!
+"!+"! Using the begin and end methods an internal session can be started and +"! completed in which internal state necessary for multiple methods will be cached. This can be +"! used to avoid having multiple http sessions between authenticate and +"! delete_access_tokens. +"!
+INTERFACE lif_2fa_authenticator. + METHODS: + "! Generate an access token + "! @parameter iv_url | Repository url + "! @parameter iv_username | Username + "! @parameter iv_password | Password + "! @parameter iv_2fa_token | Two factor token + "! @parameter rv_access_token | Generated access token + "! @raising lcx_2fa_auth_failed | Authentication failed + "! @raising lcx_2fa_token_gen_failed | Token generation failed + authenticate IMPORTING iv_url TYPE string + iv_username TYPE string + iv_password TYPE string + iv_2fa_token TYPE string + RETURNING VALUE(rv_access_token) TYPE string + RAISING lcx_2fa_auth_failed + lcx_2fa_token_gen_failed + lcx_2fa_communication_error, + "! Check if this authenticator instance supports the give repository url + "! @parameter iv_url | Repository url + "! @parameter rv_supported | Is supported + supports_url IMPORTING iv_url TYPE string + RETURNING VALUE(rv_supported) TYPE abap_bool, + "! Get a unique identifier for the service that hosts the repository + "! @parameter iv_url | Repository url + "! @parameter rv_id | Service id + "! @raising lcx_2fa_unsupported | Url is not supported + get_service_id_from_url IMPORTING iv_url TYPE string + RETURNING VALUE(rv_id) TYPE string + RAISING lcx_2fa_unsupported, + "! Check if two factor authentication is required + "! @parameter iv_url | Repository url + "! @parameter iv_username | Username + "! @parameter iv_password | Password + "! @parameter rv_required | 2FA is required + is_2fa_required IMPORTING iv_url TYPE string + iv_username TYPE string + iv_password TYPE string + RETURNING VALUE(rv_required) TYPE abap_bool + RAISING lcx_2fa_communication_error, + "! Delete all previously created access tokens for abapGit + "! @parameter iv_url | Repository url + "! @parameter iv_username | Username + "! @parameter iv_password | Password + "! @parameter iv_2fa_token | Two factor token + "! @raising lcx_2fa_token_del_failed | Token deletion failed + "! @raising lcx_2fa_auth_failed | Authentication failed + delete_access_tokens IMPORTING iv_url TYPE string + iv_username TYPE string + iv_password TYPE string + iv_2fa_token TYPE string + RAISING lcx_2fa_token_del_failed + lcx_2fa_communication_error + lcx_2fa_auth_failed, + "! Begin an authenticator session that uses internal caching for authorizations + "! @raising lcx_2fa_illegal_state | Session already started + begin RAISING lcx_2fa_illegal_state, + "! End an authenticator session and clear internal caches + "! @raising lcx_2fa_illegal_state | Session not running + end RAISING lcx_2fa_illegal_state. +ENDINTERFACE. + +"! Default LIF_2FA-AUTHENTICATOR implememtation +CLASS lcl_2fa_authenticator_base DEFINITION + ABSTRACT + CREATE PUBLIC. + + PUBLIC SECTION. + INTERFACES: + lif_2fa_authenticator. + ALIASES: + authenticate FOR lif_2fa_authenticator~authenticate, + supports_url FOR lif_2fa_authenticator~supports_url, + get_service_id_from_url FOR lif_2fa_authenticator~get_service_id_from_url, + is_2fa_required FOR lif_2fa_authenticator~is_2fa_required, + delete_access_tokens FOR lif_2fa_authenticator~delete_access_tokens, + begin FOR lif_2fa_authenticator~begin, + end FOR lif_2fa_authenticator~end. + METHODS: + "! @parameter iv_supported_url_regex | Regular expression to check if a repository url is + "! supported, used for default implementation of + "! SUPPORTS_URL + constructor IMPORTING iv_supported_url_regex TYPE clike. + PROTECTED SECTION. + CLASS-METHODS: + "! Helper method to raise class based exception after traditional exception was raised + "!+ "! sy-msg... must be set right before calling! + "!
+ raise_comm_error_from_sy RAISING lcx_2fa_communication_error. + METHODS: + "! @parameter rv_running | Internal session is currently active + is_session_running RETURNING VALUE(rv_running) TYPE abap_bool. + PRIVATE SECTION. + DATA: + mo_url_regex TYPE REF TO cl_abap_regex, + mv_session_running TYPE abap_bool. +ENDCLASS. + +CLASS lcl_2fa_authenticator_base IMPLEMENTATION. + METHOD constructor. + CREATE OBJECT mo_url_regex + EXPORTING + pattern = iv_supported_url_regex + ignore_case = abap_true. + ENDMETHOD. + + METHOD authenticate. + RAISE EXCEPTION TYPE lcx_2fa_auth_failed. " Needs to be overwritten in subclasses + ENDMETHOD. + + METHOD supports_url. + rv_supported = mo_url_regex->create_matcher( text = iv_url )->match( ). + ENDMETHOD. + + METHOD get_service_id_from_url. + rv_id = 'UNKNOWN SERVICE'. " Please overwrite in subclasses + ENDMETHOD. + + METHOD is_2fa_required. + rv_required = abap_false. + ENDMETHOD. + + METHOD delete_access_tokens. + RAISE EXCEPTION TYPE lcx_2fa_token_del_failed. " Needs to be overwritten in subclasses + ENDMETHOD. + + METHOD raise_comm_error_from_sy. + DATA: lv_error_msg TYPE string. + + MESSAGE ID sy-msgid TYPE sy-msgty NUMBER sy-msgno + WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4 + INTO lv_error_msg. + RAISE EXCEPTION TYPE lcx_2fa_communication_error + EXPORTING + iv_error_text = |Communication error: { lv_error_msg }| ##NO_TEXT. + ENDMETHOD. + + METHOD begin. + IF mv_session_running = abap_true. + RAISE EXCEPTION TYPE lcx_2fa_illegal_state. + ENDIF. + + mv_session_running = abap_true. + ENDMETHOD. + + METHOD end. + IF mv_session_running = abap_false. + RAISE EXCEPTION TYPE lcx_2fa_illegal_state. + ENDIF. + + mv_session_running = abap_false. + ENDMETHOD. + + METHOD is_session_running. + rv_running = mv_session_running. + ENDMETHOD. +ENDCLASS. + +CLASS lcl_2fa_github_authenticator DEFINITION + INHERITING FROM lcl_2fa_authenticator_base + FINAL + CREATE PUBLIC. + + PUBLIC SECTION. + METHODS: + constructor, + get_service_id_from_url REDEFINITION, + authenticate REDEFINITION, + is_2fa_required REDEFINITION, + delete_access_tokens REDEFINITION, + end REDEFINITION. + PROTECTED SECTION. + PRIVATE SECTION. + CONSTANTS: + gc_github_api_url TYPE string VALUE `https://api.github.com/`, + gc_otp_header_name TYPE string VALUE `X-Github-OTP`, + gc_restendpoint_authorizations TYPE string VALUE `/authorizations`. + CLASS-METHODS: + set_new_token_request IMPORTING ii_request TYPE REF TO if_http_request, + get_token_from_response IMPORTING ii_response TYPE REF TO if_http_response + RETURNING VALUE(rv_token) TYPE string, + parse_repo_from_url IMPORTING iv_url TYPE string + RETURNING VALUE(rv_repo_name) TYPE string, + set_list_token_request IMPORTING ii_request TYPE REF TO if_http_request, + get_tobedel_tokens_from_resp IMPORTING ii_response TYPE REF TO if_http_response + RETURNING VALUE(rt_ids) TYPE stringtab, + set_del_token_request IMPORTING ii_request TYPE REF TO if_http_request + iv_token_id TYPE string. + METHODS: + get_authenticated_client IMPORTING iv_username TYPE string + iv_password TYPE string + iv_2fa_token TYPE string + RETURNING VALUE(ri_client) TYPE REF TO if_http_client + RAISING lcx_2fa_auth_failed + lcx_2fa_communication_error. + DATA: + mi_authenticated_session TYPE REF TO if_http_client. +ENDCLASS. + +CLASS lcl_2fa_github_authenticator IMPLEMENTATION. + METHOD constructor. + super->constructor( '^https?://(www\.)?github.com.*$' ). + ENDMETHOD. + + METHOD authenticate. + DATA: li_http_client TYPE REF TO if_http_client, + lv_http_code TYPE i, + lv_http_code_description TYPE string. + + " 1. Try to login to GitHub API + li_http_client = get_authenticated_client( iv_username = iv_username + iv_password = iv_password + iv_2fa_token = iv_2fa_token ). + + " 2. Create an access token which can be used instead of a password + " https://developer.github.com/v3/oauth_authorizations/#create-a-new-authorization + + set_new_token_request( ii_request = li_http_client->request ). + + li_http_client->send( EXCEPTIONS OTHERS = 1 ). + IF sy-subrc <> 0. + raise_comm_error_from_sy( ). + ENDIF. + + li_http_client->receive( EXCEPTIONS OTHERS = 1 ). + IF sy-subrc <> 0. + raise_comm_error_from_sy( ). + ENDIF. + + li_http_client->response->get_status( + IMPORTING + code = lv_http_code + reason = lv_http_code_description ). + IF lv_http_code <> 201. + RAISE EXCEPTION TYPE lcx_2fa_token_gen_failed + EXPORTING + iv_error_text = |Token generation failed: { lv_http_code } { lv_http_code_description }|. + ENDIF. + + rv_access_token = get_token_from_response( li_http_client->response ). + IF rv_access_token IS INITIAL. + RAISE EXCEPTION TYPE lcx_2fa_token_gen_failed + EXPORTING + iv_error_text = 'Token generation failed: parser error' ##NO_TEXT. + ENDIF. + + " GitHub might need some time until the new token is ready to use, give it a second + CALL FUNCTION 'RZL_SLEEP'. + ENDMETHOD. + + METHOD set_new_token_request. + DATA: lv_json_string TYPE string. + + lv_json_string = `{"scopes":["repo"],"note":"Generated by abapGit","fingerprint":"abapGit2FA"}`. + + ii_request->set_data( cl_abap_codepage=>convert_to( lv_json_string ) ). + ii_request->set_header_field( name = if_http_header_fields_sap=>request_uri + value = gc_restendpoint_authorizations ). + ii_request->set_method( if_http_request=>co_request_method_post ). + ENDMETHOD. + + METHOD set_list_token_request. + ii_request->set_header_field( name = if_http_header_fields_sap=>request_uri + value = gc_restendpoint_authorizations ). + ii_request->set_method( if_http_request=>co_request_method_get ). + ENDMETHOD. + + METHOD set_del_token_request. + DATA: lv_url TYPE string. + + lv_url = |{ gc_restendpoint_authorizations }/{ iv_token_id }|. + + ii_request->set_header_field( name = if_http_header_fields_sap=>request_uri + value = lv_url ). + " Other methods than POST and GET do not have constants unfortunately + " ii_request->set_method( if_http_request=>co_request_method_delete ). + ii_request->set_method( 'DELETE' ). + ENDMETHOD. + + METHOD get_token_from_response. + CONSTANTS: lc_search_regex TYPE string VALUE `.*"token":"([^"]*).*$`. + DATA: lv_response TYPE string, + lo_regex TYPE REF TO cl_abap_regex, + lo_matcher TYPE REF TO cl_abap_matcher. + + lv_response = cl_abap_codepage=>convert_from( ii_response->get_data( ) ). + + CREATE OBJECT lo_regex + EXPORTING + pattern = lc_search_regex. + + lo_matcher = lo_regex->create_matcher( text = lv_response ). + IF lo_matcher->match( ) = abap_true. + rv_token = lo_matcher->get_submatch( 1 ). + ENDIF. + ENDMETHOD. + + METHOD get_tobedel_tokens_from_resp. + CONSTANTS: lc_search_regex TYPE string + VALUE `\{"id": ?(\d+)[^\{]*"app":\{[^\{^\}]*\}[^\{]*"fingerprint": ?` & + `"abapGit2FA"[^\{]*\}`. + DATA: lv_response TYPE string, + lo_regex TYPE REF TO cl_abap_regex, + lo_matcher TYPE REF TO cl_abap_matcher. + + lv_response = cl_abap_codepage=>convert_from( ii_response->get_data( ) ). + + CREATE OBJECT lo_regex + EXPORTING + pattern = lc_search_regex. + + lo_matcher = lo_regex->create_matcher( text = lv_response ). + WHILE lo_matcher->find_next( ) = abap_true. + APPEND lo_matcher->get_submatch( 1 ) TO rt_ids. + ENDWHILE. + ENDMETHOD. + + METHOD parse_repo_from_url. + CONSTANTS: lc_search_regex TYPE string VALUE 'https?:\/\/(www\.)?github.com\/(.*)$'. + DATA: lo_regex TYPE REF TO cl_abap_regex, + lo_matcher TYPE REF TO cl_abap_matcher. + + CREATE OBJECT lo_regex + EXPORTING + pattern = lc_search_regex. + + lo_matcher = lo_regex->create_matcher( text = iv_url ). + IF lo_matcher->match( ) = abap_true. + rv_repo_name = lo_matcher->get_submatch( 1 ). + ELSE. + rv_repo_name = '???' ##NO_TEXT. + ENDIF. + ENDMETHOD. + + METHOD get_service_id_from_url. + rv_id = 'github'. + ENDMETHOD. + + METHOD is_2fa_required. + DATA: li_client TYPE REF TO if_http_client, + lv_header_value TYPE string, + lo_settings TYPE REF TO lcl_settings. + + lo_settings = lcl_app=>settings( )->read( ). + + cl_http_client=>create_by_url( + EXPORTING + url = gc_github_api_url + ssl_id = 'ANONYM' + proxy_host = lo_settings->get_proxy_url( ) + proxy_service = lo_settings->get_proxy_port( ) + IMPORTING + client = li_client + EXCEPTIONS + argument_not_found = 1 + plugin_not_active = 2 + internal_error = 3 + OTHERS = 4 ). + IF sy-subrc <> 0. + raise_comm_error_from_sy( ). + ENDIF. + + li_client->propertytype_logon_popup = if_http_client=>co_disabled. + + " The request needs to use something other than GET and it needs to be send to an endpoint + " to trigger a SMS. + li_client->request->set_header_field( name = if_http_header_fields_sap=>request_uri + value = gc_restendpoint_authorizations ). + li_client->request->set_method( if_http_request=>co_request_method_post ). + + " Try to authenticate, if 2FA is required there will be a specific response header + li_client->authenticate( username = iv_username password = iv_password ). + + li_client->send( EXCEPTIONS OTHERS = 1 ). + IF sy-subrc <> 0. + raise_comm_error_from_sy( ). + ENDIF. + + li_client->receive( EXCEPTIONS OTHERS = 1 ). + IF sy-subrc <> 0. + raise_comm_error_from_sy( ). + ENDIF. + + " The response will either be UNAUTHORIZED or MALFORMED which is both fine. + + IF li_client->response->get_header_field( gc_otp_header_name ) CP 'required*'. + rv_required = abap_true. + ENDIF. + ENDMETHOD. + + METHOD delete_access_tokens. + DATA: li_http_client TYPE REF TO if_http_client, + lv_http_code TYPE i, + lv_http_code_description TYPE string, + lt_tobedeleted_tokens TYPE stringtab. + FIELD-SYMBOLS:+ "! This uses GUI functionality to display a popup to request the user to enter a two factor + "! token. Also an dummy authentication request might be used to find out if two factor + "! authentication is required for the account. + "!
+ "! @parameter iv_url | Url of the repository / service + "! @parameter cv_username | Username + "! @parameter cv_password | Password, will be replaced by an access token if two factor + "! authentication succeeds + "! @raising lcx_exception | Error in two factor authentication + use_2fa_if_required IMPORTING iv_url TYPE string + CHANGING cv_username TYPE string + cv_password TYPE string + RAISING lcx_exception. + CLASS-DATA: + "! All authenticators managed by the registry + gt_registered_authenticators TYPE HASHED TABLE OF REF TO lif_2fa_authenticator + WITH UNIQUE KEY table_line READ-ONLY. + PROTECTED SECTION. + PRIVATE SECTION. +ENDCLASS. + +CLASS lcl_2fa_authenticator_registry IMPLEMENTATION. + METHOD class_constructor. + DEFINE register. + CREATE OBJECT li_authenticator TYPE &1. + INSERT li_authenticator INTO TABLE gt_registered_authenticators. + END-OF-DEFINITION. + + DATA: li_authenticator TYPE REF TO lif_2fa_authenticator. + + " If there are new authenticators these need to be added here manually. + " I do not think there is an equivalent to SEO_INTERFACE_IMPLEM_GET_ALL for local classes + " without invoking the compiler directly. + register: lcl_2fa_github_authenticator. + ENDMETHOD. + + METHOD get_authenticator_for_url. + FIELD-SYMBOLS:' ). + ro_html->add_icon( iv_name = lv_icon iv_hint = lv_hint ). + ro_html->add( |{ io_repo->get_name( ) }| ). + IF io_repo->is_offline( ) = abap_false. + lo_repo_online ?= io_repo. + ro_html->add( |{ lo_repo_online->get_url( ) }| ). + ENDIF. + ro_html->add( ' | ' ). + + ro_html->add( '' ). + + IF abap_true = lcl_app=>user( )->is_favorite_repo( io_repo->get_key( ) ). + lv_icon = 'star/blue' ##NO_TEXT. + ELSE. + lv_icon = 'star/grey' ##NO_TEXT. + ENDIF. + ro_html->add_a( iv_act = |{ gc_action-repo_toggle_fav }?{ io_repo->get_key( ) }| + iv_txt = lcl_html=>icon( iv_name = lv_icon + iv_class = 'pad-sides' + iv_hint = 'Click to toggle favorite' ) ). + + IF lo_pback->exists( io_repo->get_key( ) ) = abap_true. + ro_html->add( 'BG' ). + ENDIF. + + IF io_repo->is_write_protected( ) = abap_true. + ro_html->add_icon( iv_name = 'lock/darkgrey' iv_hint = 'Locked from pulls' ). + ENDIF. + + IF io_repo->is_offline( ) = abap_false. + lo_repo_online ?= io_repo. + IF iv_show_branch = abap_true. + IF iv_branch IS INITIAL. + ro_html->add( render_branch_span( iv_branch = lo_repo_online->get_branch_name( ) + io_repo = lo_repo_online + iv_interactive = iv_interactive_branch ) ). + ELSE. + ro_html->add( render_branch_span( iv_branch = iv_branch + io_repo = lo_repo_online + iv_interactive = iv_interactive_branch ) ). + ENDIF. + ENDIF. + ENDIF. + + IF iv_show_package = abap_true. + ro_html->add_icon( iv_name = 'package/darkgrey' iv_hint = 'SAP package' ). + ro_html->add( '' ). + ro_html->add_a( iv_txt = io_repo->get_package( ) + iv_act = |{ gc_action-jump_pkg }?{ io_repo->get_package( ) }| ). + ro_html->add( '' ). + ENDIF. + + ro_html->add( ' | ' ). + ro_html->add( '