diff --git a/src/zabapgit.prog.abap b/src/zabapgit.prog.abap index 1c7a15d73..b81885ed0 100644 --- a/src/zabapgit.prog.abap +++ b/src/zabapgit.prog.abap @@ -53,6 +53,7 @@ INCLUDE zabapgit_stage. INCLUDE zabapgit_git_helpers. INCLUDE zabapgit_repo. INCLUDE zabapgit_stage_logic. +INCLUDE zabapgit_2fa. INCLUDE zabapgit_http. INCLUDE zabapgit_git. INCLUDE zabapgit_objects. diff --git a/src/zabapgit_2fa.prog.abap b/src/zabapgit_2fa.prog.abap new file mode 100644 index 000000000..c41e0acb5 --- /dev/null +++ b/src/zabapgit_2fa.prog.abap @@ -0,0 +1,586 @@ +*&---------------------------------------------------------------------* +*& Include ZABAPGIT_2FA +*&---------------------------------------------------------------------* + +"! Exception base class for two factor authentication related errors +CLASS lcx_2fa_error DEFINITION INHERITING FROM cx_static_check. + PUBLIC SECTION. + METHODS: + constructor IMPORTING is_textid LIKE textid OPTIONAL + ix_previous LIKE previous OPTIONAL + iv_error_text TYPE csequence OPTIONAL, + get_text REDEFINITION. + DATA: + mv_text TYPE string READ-ONLY. + PROTECTED SECTION. + METHODS: + get_default_text RETURNING VALUE(rv_text) TYPE string. +ENDCLASS. + +CLASS lcx_2fa_error IMPLEMENTATION. + METHOD constructor. + super->constructor( textid = is_textid previous = ix_previous ). + mv_text = iv_error_text. + ENDMETHOD. + + METHOD get_text. + IF mv_text IS NOT INITIAL. + result = mv_text. + ELSEIF get_default_text( ) IS NOT INITIAL. + result = get_default_text( ). + ELSE. + result = super->get_text( ). + ENDIF. + ENDMETHOD. + + METHOD get_default_text. + rv_text = 'Error in two factor authentication.' ##NO_TEXT. + ENDMETHOD. +ENDCLASS. + +CLASS lcx_2fa_auth_failed DEFINITION INHERITING FROM lcx_2fa_error FINAL. + PROTECTED SECTION. + METHODS: + get_default_text REDEFINITION. +ENDCLASS. + +CLASS lcx_2fa_auth_failed IMPLEMENTATION. + METHOD get_default_text. + rv_text = 'Authentication failed using 2FA.' ##NO_TEXT. + ENDMETHOD. +ENDCLASS. + +CLASS lcx_2fa_token_gen_failed DEFINITION INHERITING FROM lcx_2fa_error FINAL. + PROTECTED SECTION. + METHODS: + get_default_text REDEFINITION. +ENDCLASS. + +CLASS lcx_2fa_token_gen_failed IMPLEMENTATION. + METHOD get_default_text. + rv_text = 'Two factor access token generation failed.' ##NO_TEXT. + ENDMETHOD. +ENDCLASS. + +CLASS lcx_2fa_unsupported DEFINITION INHERITING FROM lcx_2fa_error FINAL. + PROTECTED SECTION. + METHODS: + get_default_text REDEFINITION. +ENDCLASS. + +CLASS lcx_2fa_unsupported IMPLEMENTATION. + METHOD get_default_text. + rv_text = 'The service is not supported for two factor authentication.' ##NO_TEXT. + ENDMETHOD. +ENDCLASS. + +CLASS lcx_2fa_no_cached_token DEFINITION INHERITING FROM lcx_2fa_error FINAL. + PROTECTED SECTION. + METHODS: + get_default_text REDEFINITION. +ENDCLASS. + +CLASS lcx_2fa_no_cached_token IMPLEMENTATION. + METHOD get_default_text. + rv_text = 'Cached two factor access token requested but not available.' ##NO_TEXT. + ENDMETHOD. +ENDCLASS. + +CLASS lcx_2fa_cache_deletion_failed DEFINITION INHERITING FROM lcx_2fa_error FINAL. + PROTECTED SECTION. + METHODS: + get_default_text REDEFINITION. +ENDCLASS. + +CLASS lcx_2fa_cache_deletion_failed IMPLEMENTATION. + METHOD get_default_text. + rv_text = 'Cache deletion failed.' ##NO_TEXT. + ENDMETHOD. +ENDCLASS. + +"! Defines a two factor authentication authenticator +"!

+"! 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. The authenticator can +"! also store and retrieve the access token it generated. +"!

+"!

+"! LCL_2FA_AUTHENTICATOR_REGISTRY can be used to find a suitable implementation for a given +"! repository. +"!

+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, + "! 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 + get_service_id_from_url IMPORTING iv_url TYPE string + RETURNING VALUE(rv_id) TYPE string, + "! Check if there is a cached access token (for the current user) + "! @parameter iv_url | Repository url + "! @parameter rv_available | Token is cached + is_cached_access_token_avail IMPORTING iv_url TYPE string + RETURNING VALUE(rv_available) TYPE abap_bool, + "! Get a cached access token + "!

+ "! Username and password are also parameters to decrypt the token if needed. They must no + "! necessarily be provided if the used authenticator does not use encryption. + "!

+ "! @parameter iv_url | Repository url + "! @parameter iv_username | Username + "! @parameter iv_password | Password + "! @parameter rv_token | Access token + "! @raising lcx_2fa_no_cached_token | There is no cached token + get_cached_access_token IMPORTING iv_url TYPE string + iv_username TYPE string OPTIONAL + iv_password TYPE string OPTIONAL + RETURNING VALUE(rv_token) TYPE string + RAISING lcx_2fa_no_cached_token, + "! Delete a cached token + "! @parameter iv_url | Repository url + "! @raising lcx_2fa_cache_deletion_failed | Deletion failed + delete_cached_access_token IMPORTING iv_url TYPE string + RAISING lcx_2fa_cache_deletion_failed. +ENDINTERFACE. + +"! Default LIF_2FA-AUTHENTICATOR implememtation +"!

+"! This uses the user settings to store cached access tokens and encrypts / decrypts them as needed. +"!

+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_cached_access_token_avail FOR lif_2fa_authenticator~is_cached_access_token_avail, + get_cached_access_token FOR lif_2fa_authenticator~get_cached_access_token, + delete_cached_token FOR lif_2fa_authenticator~delete_cached_access_token. + 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. + METHODS: + "! Subclass implementation of LIF_2FA_AUTHENTICATOR=>AUTHENTICATE + "!

+ "! The caller will take care of caching the 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_internal ABSTRACT 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, + "! Helper method to raise class based exception after traditional exception was raised + "!

+ "! sy-msg... must be set right before calling! + "!

+ raise_internal_error_from_sy FINAL RAISING lcx_2fa_auth_failed. + PRIVATE SECTION. + DATA: + mo_url_regex TYPE REF TO cl_abap_regex. +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. + DATA: lv_encrypted_token TYPE string. + + rv_access_token = authenticate_internal( iv_url = iv_url + iv_username = iv_username + iv_password = iv_password + iv_2fa_token = iv_2fa_token ). + + " Store the access token, by default in the user settings + + " 1. Encrypt it +* lv_encrypted_token = cl_encryption_helper=>encrypt_symmetric( iv_text = rv_access_token +* iv_key = iv_password ). + " TODO: Find something like the above for symmetric encryption + lv_encrypted_token = rv_access_token. + + " 2. Store it + TRY. + lcl_app=>user( )->set_2fa_access_token( iv_service_id = get_service_id_from_url( iv_url ) + iv_username = iv_username + iv_token = lv_encrypted_token ). + CATCH lcx_exception ##NO_HANDLER. + " Not the biggest of deals if caching the token fails + ENDTRY. + 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_cached_access_token_avail. + DATA: lv_service_id TYPE string. + + lv_service_id = get_service_id_from_url( iv_url ). + + " Default storage location is user settings + TRY. + rv_available = boolc( lcl_app=>user( )->get_2fa_access_token( lv_service_id ) + IS NOT INITIAL ). + CATCH lcx_exception. + rv_available = abap_false. + ENDTRY. + ENDMETHOD. + + METHOD get_cached_access_token. + DATA: lv_access_token_encrypted TYPE string, + lx_error TYPE REF TO cx_root. + + TRY. + lv_access_token_encrypted + = lcl_app=>user( )->get_2fa_access_token( get_service_id_from_url( iv_url ) ). + CATCH lcx_exception INTO lx_error. + RAISE EXCEPTION TYPE lcx_2fa_no_cached_token + EXPORTING + ix_previous = lx_error + iv_error_text = lx_error->get_text( ). + ENDTRY. + + IF lv_access_token_encrypted IS INITIAL. + RAISE EXCEPTION TYPE lcx_2fa_no_cached_token. + ENDIF. + + " TODO: Decryption +* rv_token = cl_encryption_helper=>decrypt_symmetric( iv_encrypted = rv_access_token +* iv_key = iv_password ). + rv_token = lv_access_token_encrypted. + ENDMETHOD. + + METHOD delete_cached_token. + DATA: lx_ex TYPE REF TO cx_root. + + TRY. + " Default storage location is user settings + lcl_app=>user( )->delete_2fa_config( get_service_id_from_url( iv_url ) ). + CATCH lcx_exception INTO lx_ex. + RAISE EXCEPTION TYPE lcx_2fa_cache_deletion_failed + EXPORTING + ix_previous = lx_ex + iv_error_text = |Cache deletion failed: { lx_ex->get_text( ) }|. + ENDTRY. + ENDMETHOD. + + METHOD raise_internal_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_auth_failed + EXPORTING + iv_error_text = |Internal error: { lv_error_msg }| ##NO_TEXT. + 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. + PROTECTED SECTION. + METHODS: + authenticate_internal REDEFINITION. + PRIVATE SECTION. + METHODS: + set_access_token_request IMPORTING ii_entity TYPE REF TO if_rest_entity + iv_repo_name TYPE string, + get_token_from_response IMPORTING ii_entity TYPE REF TO if_rest_entity + RETURNING VALUE(rv_token) TYPE string, + parse_repo_from_url IMPORTING iv_url TYPE string + RETURNING VALUE(rv_repo_name) TYPE string. +ENDCLASS. + +CLASS lcl_2fa_github_authenticator IMPLEMENTATION. + METHOD constructor. + super->constructor( 'https?:\/\/(www\.)?github.com.*$' ). + ENDMETHOD. + + METHOD authenticate_internal. + CONSTANTS: lc_github_api_url TYPE string VALUE `https://api.github.com/`, + lc_otp_header_name TYPE string VALUE `X-Github-OTP`, + lc_restendpoint_authorizations TYPE string VALUE `/authorizations`. + DATA: li_rest_client TYPE REF TO if_rest_client, + li_http_client TYPE REF TO if_http_client, + lv_http_code TYPE i, + lv_http_code_description TYPE string, + li_request_entity TYPE REF TO if_rest_entity, + li_response_entity TYPE REF TO if_rest_entity, + lv_binary_response TYPE xstring, + BEGIN OF ls_success_response, + token TYPE string, + END OF ls_success_response. + + " 1. Try to login to GitHub API with username, password and 2fa token + cl_http_client=>create_by_url( + EXPORTING + url = lc_github_api_url + IMPORTING + client = li_http_client + EXCEPTIONS + argument_not_found = 1 + plugin_not_active = 2 + internal_error = 3 + OTHERS = 4 ). + IF sy-subrc <> 0. + raise_internal_error_from_sy( ). + ENDIF. + + " https://developer.github.com/v3/auth/#working-with-two-factor-authentication + li_http_client->propertytype_accept_cookie = if_http_client=>co_enabled. + li_http_client->request->set_header_field( name = lc_otp_header_name value = iv_2fa_token ). + li_http_client->authenticate( username = iv_username password = iv_password ). + li_http_client->propertytype_logon_popup = if_http_client=>co_disabled. + + li_http_client->send( EXCEPTIONS OTHERS = 1 ). + IF sy-subrc <> 0. + raise_internal_error_from_sy( ). + ENDIF. + + li_http_client->receive( EXCEPTIONS OTHERS = 1 ). + IF sy-subrc <> 0. + raise_internal_error_from_sy( ). + ENDIF. + + " Check if authentication has succeeded + li_http_client->response->get_status( + IMPORTING + code = lv_http_code + reason = lv_http_code_description + ). + IF lv_http_code <> 200. + RAISE EXCEPTION TYPE lcx_2fa_auth_failed + EXPORTING + iv_error_text = |Authentication failed: { lv_http_code_description }|. + ENDIF. + + " Authentication worked, now the rest client can be used + CREATE OBJECT li_rest_client TYPE cl_rest_http_client + EXPORTING + io_http_client = li_http_client. + + + " 2. Create an access token which can be used instead of a password + " https://developer.github.com/v3/oauth_authorizations/#create-a-new-authorization + + li_request_entity = li_rest_client->create_request_entity( ). + set_access_token_request( ii_entity = li_request_entity + iv_repo_name = parse_repo_from_url( iv_url ) ). + li_rest_client->set_request_header( iv_name = if_http_header_fields_sap=>request_uri + iv_value = lc_restendpoint_authorizations ). + li_rest_client->post( li_request_entity ). + + lv_http_code = li_rest_client->get_status( ). + IF lv_http_code <> 201. + RAISE EXCEPTION TYPE lcx_2fa_token_gen_failed + EXPORTING + iv_error_text = |Token generation failed: { lv_http_code }|. + ENDIF. + + rv_access_token = get_token_from_response( li_rest_client->get_response_entity( ) ). + IF rv_access_token IS INITIAL. + RAISE EXCEPTION TYPE lcx_2fa_token_gen_failed. + ENDIF. + ENDMETHOD. + + METHOD set_access_token_request. + CONSTANTS: BEGIN OF lc_create_access_token_request, + scopes TYPE string VALUE 'repo', + note TYPE string VALUE 'abapGit', + END OF lc_create_access_token_request. + DATA: lo_json_writer TYPE REF TO cl_sxml_string_writer, + lt_scopes TYPE stringtab, + lt_rest_parvalues TYPE abap_trans_srcbind_tab, + ls_rest_line LIKE LINE OF lt_rest_parvalues, + lt_result_parvalues TYPE abap_trans_resbind_tab, + ls_result_line LIKE LINE OF lt_result_parvalues, + lr_data_ref TYPE REF TO data, + lv_note TYPE string, + lv_fingerprint TYPE string. + + lo_json_writer = cl_sxml_string_writer=>create( type = if_sxml=>co_xt_json ). + APPEND lc_create_access_token_request-scopes TO lt_scopes. + + GET REFERENCE OF lc_create_access_token_request-scopes INTO lr_data_ref. + ls_rest_line-name = 'scopes'. + ls_rest_line-value = lr_data_ref. + APPEND ls_rest_line TO lt_rest_parvalues. + + GET REFERENCE OF lc_create_access_token_request-note INTO lr_data_ref. + ls_rest_line-name = 'note'. + ls_rest_line-value = lr_data_ref. + APPEND ls_rest_line TO lt_rest_parvalues. + + " The fingerprint must be unique, otherwise only one token can be generated, unless the user + " deletes it in Github's settings. This is problematic if he deletes it in abapGit but keeps it + " on GitHub. + lv_fingerprint = |abapGit-{ sy-sysid }-{ sy-uname }-{ sy-datum }-{ sy-uzeit }|. + GET REFERENCE OF lv_fingerprint INTO lr_data_ref. + ls_rest_line-name = 'fingerprint'. + ls_rest_line-value = lr_data_ref. + APPEND ls_rest_line TO lt_rest_parvalues. + + " Dynamic source table is used because otherwise identifiers will always be written in uppercase + " which is not supported by the GitHub's API. + CALL TRANSFORMATION id SOURCE (lt_rest_parvalues) + RESULT XML lo_json_writer. + + ii_entity->set_string_data( cl_abap_codepage=>convert_from( lo_json_writer->get_output( ) ) ). + ENDMETHOD. + + METHOD get_token_from_response. + CONSTANTS: lc_token_field_name TYPE string VALUE 'token'. + DATA: lt_result_parvalues TYPE abap_trans_resbind_tab, + ls_result_line LIKE LINE OF lt_result_parvalues, + lr_data_ref TYPE REF TO data, + lv_binary_response TYPE xstring. + + GET REFERENCE OF rv_token INTO lr_data_ref. + ls_result_line-name = lc_token_field_name. + ls_result_line-value = lr_data_ref. + APPEND ls_result_line TO lt_result_parvalues. + + lv_binary_response = ii_entity->get_binary_data( ). + CALL TRANSFORMATION id SOURCE XML lv_binary_response + RESULT (lt_result_parvalues). + ENDMETHOD. + + METHOD parse_repo_from_url. + DATA: lo_regex TYPE REF TO cl_abap_regex, + lo_matcher TYPE REF TO cl_abap_matcher. + + CREATE OBJECT lo_regex + EXPORTING + pattern = 'https?:\/\/(www\.)?github.com\/(.*)$'. + + 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. +ENDCLASS. + +"! Static registry class to find LIF_2FA_AUTHENTICATOR instances +CLASS lcl_2fa_authenticator_registry DEFINITION + FINAL + CREATE PRIVATE. + + PUBLIC SECTION. + CLASS-METHODS: + class_constructor, + "! Retrieve an authenticator instance by url + "! @parameter iv_url | Url of the repository / service + "! @parameter ro_authenticator | Found authenticator instance + "! @raising lcx_2fa_unsupported | No authenticator found that supports the service + get_authenticator_for_url IMPORTING iv_url TYPE string + RETURNING VALUE(ro_authenticator) TYPE REF TO lif_2fa_authenticator + RAISING lcx_2fa_unsupported, + "! Check if there is a two factor authenticator available for the url + "! @parameter iv_url | Url of the repository / service + "! @parameter rv_supported | 2FA is supported + is_url_supported IMPORTING iv_url TYPE string + RETURNING VALUE(rv_supported) TYPE abap_bool. + 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: LIKE LINE OF gt_registered_authenticators. + + LOOP AT gt_registered_authenticators ASSIGNING . + IF ->supports_url( iv_url ) = abap_true. + ro_authenticator = . + RETURN. + ENDIF. + ENDLOOP. + + RAISE EXCEPTION TYPE lcx_2fa_unsupported. + ENDMETHOD. + + METHOD is_url_supported. + TRY. + get_authenticator_for_url( iv_url ). + rv_supported = abap_true. + CATCH lcx_2fa_unsupported ##NO_HANDLER. + ENDTRY. + ENDMETHOD. +ENDCLASS. \ No newline at end of file diff --git a/src/zabapgit_2fa.prog.xml b/src/zabapgit_2fa.prog.xml new file mode 100644 index 000000000..84863cedb --- /dev/null +++ b/src/zabapgit_2fa.prog.xml @@ -0,0 +1,48 @@ + + + + + + ZABAPGIT_2FA + A + + + X + S + D$ + + + + I + + + + 0000-00-00 + + 0000-00-00 + + + + + + X + + 0000-00-00 + + 0000-00-00 + + D$S + X + + + + R + + ZABAPGIT_2FA + 12 + + + + + + diff --git a/src/zabapgit_http.prog.abap b/src/zabapgit_http.prog.abap index 11c1fb551..bca230808 100644 --- a/src/zabapgit_http.prog.abap +++ b/src/zabapgit_http.prog.abap @@ -469,21 +469,82 @@ CLASS lcl_http IMPLEMENTATION. METHOD acquire_login_details. - DATA: lv_default_user TYPE string, - lv_user TYPE string, - lv_pass TYPE string, - lo_digest TYPE REF TO lcl_http_digest. + DATA: lv_default_user TYPE string, + lv_user TYPE string, + lv_pass TYPE string, + lv_2fa_token TYPE string, + lv_access_token TYPE string, + lo_digest TYPE REF TO lcl_http_digest, + lv_2fa_available TYPE abap_bool, + li_authenticator TYPE REF TO lif_2fa_authenticator, + lx_error TYPE REF TO cx_root, + lv_popup_mode TYPE lcl_password_dialog=>gty_mode, + lv_popup_requested_token_del TYPE abap_bool, + lv_service_id TYPE string. lv_default_user = lcl_app=>user( )->get_repo_username( iv_url ). lv_user = lv_default_user. + + IF lcl_2fa_authenticator_registry=>is_url_supported( iv_url ) = abap_true. + TRY. + li_authenticator = lcl_2fa_authenticator_registry=>get_authenticator_for_url( iv_url ). + lv_service_id = li_authenticator->get_service_id_from_url( iv_url ). + IF li_authenticator->is_cached_access_token_avail( iv_url ) = abap_true. + lv_popup_mode = lcl_password_dialog=>gc_modes-unlock_2fa_token. + ELSE. + lv_popup_mode = lcl_password_dialog=>gc_modes-user_pass_2fa. + ENDIF. + + CATCH lcx_2fa_error. + lv_2fa_available = abap_false. + CLEAR lv_access_token. + ENDTRY. + ELSE. + lv_popup_mode = lcl_password_dialog=>gc_modes-user_pass. + ENDIF. + lcl_password_dialog=>popup( EXPORTING - iv_repo_url = iv_url + iv_repo_url = iv_url + iv_mode = lv_popup_mode + IMPORTING + ev_delete_token = lv_popup_requested_token_del CHANGING - cv_user = lv_user - cv_pass = lv_pass ). + cv_user = lv_user + cv_pass = lv_pass + cv_2fa_token = lv_2fa_token ). + + IF lv_popup_requested_token_del = abap_true. + TRY. + li_authenticator->delete_cached_access_token( lv_service_id ). + CATCH lcx_2fa_cache_deletion_failed INTO lx_error. + RAISE EXCEPTION TYPE lcx_exception + EXPORTING + iv_text = lx_error->get_text( ) + ix_previous = lx_error. + ENDTRY. + + " Cancel authentication, no credentials were provided. This will cause the next http request + " somewhere up the callstack to result in a 401 error. + RETURN. + ENDIF. + + " Unlock cached access token + IF lv_popup_mode = lcl_password_dialog=>gc_modes-unlock_2fa_token. + TRY. + ASSERT li_authenticator IS BOUND. + lv_access_token = li_authenticator->get_cached_access_token( iv_url = iv_url + iv_username = lv_user + iv_password = lv_pass ). + CATCH lcx_2fa_no_cached_token INTO lx_error. + RAISE EXCEPTION TYPE lcx_exception + EXPORTING + iv_text = lx_error->get_text( ) + ix_previous = lx_error. + ENDTRY. + ENDIF. IF lv_user IS INITIAL. lcx_exception=>raise( 'HTTP 401, unauthorized' ). @@ -494,6 +555,29 @@ CLASS lcl_http IMPLEMENTATION. iv_username = lv_user ). ENDIF. + IF lv_access_token IS INITIAL AND lv_2fa_token IS NOT INITIAL. + " There is no cached access token but the user provided a two factor token to generate a new + " access token + TRY. + ASSERT li_authenticator IS BOUND. + lv_access_token = li_authenticator->authenticate( iv_url = iv_url + iv_username = lv_user + iv_password = lv_pass + iv_2fa_token = lv_2fa_token ). + CATCH lcx_2fa_error INTO lx_error. + RAISE EXCEPTION TYPE lcx_exception + EXPORTING + iv_text = lx_error->get_text( ) + ix_previous = lx_error. + ENDTRY. + ENDIF. + + " If there is an access token by now use that as the password instead because two factor + " authentication was requested. + IF lv_access_token IS NOT INITIAL. + lv_pass = lv_access_token. + ENDIF. + rv_scheme = ii_client->response->get_header_field( 'www-authenticate' ). FIND REGEX '^(\w+)' IN rv_scheme SUBMATCHES rv_scheme. diff --git a/src/zabapgit_password_dialog.prog.abap b/src/zabapgit_password_dialog.prog.abap index 354f0d44d..b87406890 100644 --- a/src/zabapgit_password_dialog.prog.abap +++ b/src/zabapgit_password_dialog.prog.abap @@ -16,6 +16,13 @@ SELECTION-SCREEN BEGIN OF LINE. SELECTION-SCREEN COMMENT 1(10) s_pass FOR FIELD p_pass. PARAMETERS: p_pass TYPE string LOWER CASE VISIBLE LENGTH 40 ##SEL_WRONG. SELECTION-SCREEN END OF LINE. +SELECTION-SCREEN BEGIN OF LINE. +PARAMETERS: p_en2fa TYPE abap_bool DEFAULT abap_false USER-COMMAND u1 MODIF ID m1 AS CHECKBOX. +SELECTION-SCREEN COMMENT 4(6) s_2fat FOR FIELD p_2fat MODIF ID m1. +SELECTION-SCREEN POSITION 12. +PARAMETERS: p_2fat TYPE string LOWER CASE VISIBLE LENGTH 40 MODIF ID m1. +SELECTION-SCREEN END OF LINE. +SELECTION-SCREEN FUNCTION KEY 1. SELECTION-SCREEN END OF SCREEN 1002. *----------------------------------------------------------------------- @@ -24,14 +31,26 @@ SELECTION-SCREEN END OF SCREEN 1002. CLASS lcl_password_dialog DEFINITION FINAL. PUBLIC SECTION. - CONSTANTS dynnr TYPE char4 VALUE '1002'. + TYPES: + gty_mode TYPE i. + CONSTANTS: + dynnr TYPE char4 VALUE '1002', + BEGIN OF gc_modes, + user_pass TYPE gty_mode VALUE 1, + user_pass_2fa TYPE gty_mode VALUE 2, + unlock_2fa_token TYPE gty_mode VALUE 3, + END OF gc_modes. CLASS-METHODS popup IMPORTING - iv_repo_url TYPE string + iv_repo_url TYPE string + iv_mode TYPE gty_mode DEFAULT gc_modes-user_pass + EXPORTING + ev_delete_token TYPE abap_bool CHANGING - cv_user TYPE string - cv_pass TYPE string. + cv_user TYPE string + cv_pass TYPE string + cv_2fa_token TYPE string. CLASS-METHODS on_screen_init. CLASS-METHODS on_screen_output. @@ -40,7 +59,10 @@ CLASS lcl_password_dialog DEFINITION FINAL. iv_ucomm TYPE syucomm. PRIVATE SECTION. - CLASS-DATA mv_confirm TYPE abap_bool. + CLASS-DATA: + mv_confirm TYPE abap_bool, + gv_mode TYPE gty_mode, + gv_delete_token TYPE abap_bool. ENDCLASS. "lcl_password_dialog DEFINITION @@ -52,17 +74,22 @@ CLASS lcl_password_dialog IMPLEMENTATION. p_url = iv_repo_url. p_user = cv_user. mv_confirm = abap_false. + gv_mode = iv_mode. + gv_delete_token = abap_false. CALL SELECTION-SCREEN dynnr STARTING AT 5 5 ENDING AT 60 8. IF mv_confirm = abap_true. cv_user = p_user. cv_pass = p_pass. + cv_2fa_token = p_2fat. ELSE. - CLEAR: cv_user, cv_pass. + CLEAR: cv_user, cv_pass, cv_2fa_token. ENDIF. - CLEAR: p_url, p_user, p_pass. + ev_delete_token = gv_delete_token. + + CLEAR: p_url, p_user, p_pass, p_2fat. ENDMETHOD. "popup @@ -71,6 +98,7 @@ CLASS lcl_password_dialog IMPLEMENTATION. s_url = 'Repo URL' ##NO_TEXT. s_user = 'User' ##NO_TEXT. s_pass = 'Password' ##NO_TEXT. + s_2fat = '2FA' ##NO_TEXT. ENDMETHOD. "on_screen_init METHOD on_screen_output. @@ -78,19 +106,51 @@ CLASS lcl_password_dialog IMPLEMENTATION. ASSERT sy-dynnr = dynnr. + CLEAR p_2fat. + LOOP AT SCREEN. - IF screen-name = 'P_URL'. - screen-input = '0'. - screen-intensified = '1'. - screen-display_3d = '0'. - MODIFY SCREEN. - ENDIF. - IF screen-name = 'P_PASS'. - screen-invisible = '1'. + CASE screen-name. + WHEN 'P_URL'. + screen-input = '0'. + screen-intensified = '1'. + screen-display_3d = '0'. + MODIFY SCREEN. + + WHEN 'P_PASS'. + screen-invisible = '1'. + MODIFY SCREEN. + + WHEN 'P_USER'. + IF gv_mode = gc_modes-unlock_2fa_token. + screen-input = 0. + MODIFY SCREEN. + ENDIF. + + WHEN 'P_2FAT'. + IF p_en2fa = abap_true. + screen-input = '1'. + ELSE. + screen-input = '0'. + p_2fat = 'Two factor authentication token' ##NO_TEXT. + ENDIF. + MODIFY SCREEN. + + ENDCASE. + + IF screen-group1 = 'M1' AND gv_mode <> gc_modes-user_pass_2fa. + screen-invisible = '1'. MODIFY SCREEN. ENDIF. ENDLOOP. + IF gv_mode = gc_modes-unlock_2fa_token. + s_title = 'Unlock two factor authentication token' ##NO_TEXT. + sscrfields-functxt_01 = 'Remove token' ##NO_TEXT. + ELSE. + s_title = 'Login'. + CLEAR sscrfields-functxt_01. + ENDIF. + " Program RSSYSTDB, GUI Status %_CSP PERFORM set_pf_status IN PROGRAM rsdbrunt IF FOUND. APPEND 'NONE' TO lt_ucomm. "Button Check @@ -102,28 +162,42 @@ CLASS lcl_password_dialog IMPLEMENTATION. TABLES p_exclude = lt_ucomm. - IF p_user IS NOT INITIAL. + IF p_user IS NOT INITIAL AND p_pass IS NOT INITIAL AND p_en2fa = abap_true. + SET CURSOR FIELD 'P_2FAT'. + ELSEIF p_user IS NOT INITIAL. SET CURSOR FIELD 'P_PASS'. ENDIF. ENDMETHOD. "on_screen_output METHOD on_screen_event. + DATA: lv_answer TYPE c. ASSERT sy-dynnr = dynnr. " CRET - F8 " OTHERS - simulate Enter press CASE iv_ucomm. + WHEN 'FC01'. " Delete two factor code + CALL FUNCTION 'POPUP_TO_CONFIRM' + EXPORTING + text_question = 'Do you really want to remove this two factor access token?' + IMPORTING + answer = lv_answer. + IF lv_answer = '1'. + gv_delete_token = abap_true. + LEAVE TO SCREEN 0. + ENDIF. WHEN 'CRET'. mv_confirm = abap_true. + LEAVE TO SCREEN 0. WHEN OTHERS. "TODO REFACTOR !!! A CLUTCH ! " This will work unless any new specific logic appear " for other commands. The problem is that the password dialog " does not have Enter event (or I don't know how to activate it ;) " so Enter issues previous command from previous screen " But for now this works :) Fortunately Esc produces another flow - mv_confirm = abap_true. - LEAVE TO SCREEN 0. +* mv_confirm = abap_true. +* LEAVE TO SCREEN 0. ENDCASE. ENDMETHOD. "on_screen_event diff --git a/src/zabapgit_persistence.prog.abap b/src/zabapgit_persistence.prog.abap index 7232279af..e8e8d96a1 100644 --- a/src/zabapgit_persistence.prog.abap +++ b/src/zabapgit_persistence.prog.abap @@ -406,6 +406,21 @@ CLASS lcl_persistence_user DEFINITION FINAL CREATE PRIVATE FRIENDS lcl_app. RETURNING VALUE(rv_email) TYPE string RAISING lcx_exception. + METHODS get_2fa_access_token + IMPORTING iv_service_id TYPE string + RETURNING VALUE(rv_token) TYPE string + RAISING lcx_exception. + + METHODS set_2fa_access_token + IMPORTING iv_service_id TYPE string + iv_username TYPE string + iv_token TYPE string + RAISING lcx_exception. + + METHODS delete_2fa_config + IMPORTING iv_service_id TYPE string + RAISING lcx_exception. + METHODS toggle_hide_files RETURNING VALUE(rv_hide) TYPE abap_bool RAISING lcx_exception. @@ -455,6 +470,13 @@ CLASS lcl_persistence_user DEFINITION FINAL CREATE PRIVATE FRIENDS lcl_app. END OF ty_repo_config. TYPES: ty_repo_config_tt TYPE STANDARD TABLE OF ty_repo_config WITH DEFAULT KEY. + TYPES: BEGIN OF ty_git_hosting_service, + service_id TYPE string, + username TYPE string, + access_token TYPE string, + END OF ty_git_hosting_service. + TYPES: ty_git_hosting_service_tab TYPE STANDARD TABLE OF ty_git_hosting_service WITH DEFAULT KEY. + TYPES: BEGIN OF ty_user, username TYPE string, email TYPE string, @@ -464,6 +486,7 @@ CLASS lcl_persistence_user DEFINITION FINAL CREATE PRIVATE FRIENDS lcl_app. changes_only TYPE abap_bool, diff_unified TYPE abap_bool, favorites TYPE tt_favorites, + two_fact_cfg TYPE ty_git_hosting_service_tab, END OF ty_user. METHODS constructor @@ -496,6 +519,16 @@ CLASS lcl_persistence_user DEFINITION FINAL CREATE PRIVATE FRIENDS lcl_app. is_repo_config TYPE ty_repo_config RAISING lcx_exception. + METHODS read_2fa_config + IMPORTING iv_service_id TYPE ty_git_hosting_service-service_id + RETURNING VALUE(rs_2fa_config) TYPE ty_git_hosting_service + RAISING lcx_exception. + + METHODS update_2fa_config + IMPORTING iv_service_id TYPE ty_git_hosting_service-service_id + is_2fa_config TYPE ty_git_hosting_service + RAISING lcx_exception. + ENDCLASS. "lcl_persistence_user DEFINITION CLASS lcl_persistence_user IMPLEMENTATION. @@ -639,6 +672,48 @@ CLASS lcl_persistence_user IMPLEMENTATION. ENDMETHOD. "update_repo_config + METHOD read_2fa_config. + DATA: lt_2fa_config TYPE ty_git_hosting_service_tab, + lv_key TYPE string. + + lv_key = to_lower( iv_service_id ). + lt_2fa_config = read( )-two_fact_cfg. + READ TABLE lt_2fa_config INTO rs_2fa_config WITH KEY service_id = lv_key. + ENDMETHOD. + + METHOD update_2fa_config. + DATA: ls_user TYPE ty_user, + lv_key TYPE string. + FIELD-SYMBOLS TYPE ty_git_hosting_service. + + ls_user = read( ). + lv_key = to_lower( iv_service_id ). + + READ TABLE ls_user-two_fact_cfg ASSIGNING WITH KEY service_id = lv_key. + IF sy-subrc IS NOT INITIAL. + APPEND INITIAL LINE TO ls_user-two_fact_cfg ASSIGNING . + ENDIF. + = is_2fa_config. + -service_id = lv_key. + + update( ls_user ). + ENDMETHOD. + + METHOD delete_2fa_config. + DATA: ls_user TYPE ty_user, + lv_key TYPE string. + + ls_user = read( ). + lv_key = to_lower( iv_service_id ). + + DELETE ls_user-two_fact_cfg WHERE service_id = lv_key. + IF sy-subrc <> 0. + lcx_exception=>raise( '2FA config could not be deleted.' ) ##NO_TEXT. + ENDIF. + + update( ls_user ). + ENDMETHOD. + METHOD set_repo_username. DATA: ls_repo_config TYPE ty_repo_config. @@ -671,6 +746,21 @@ CLASS lcl_persistence_user IMPLEMENTATION. ENDMETHOD. "get_repo_email + METHOD get_2fa_access_token. + rv_token = read_2fa_config( iv_service_id )-access_token. + ENDMETHOD. + + METHOD set_2fa_access_token. + DATA: ls_config TYPE ty_git_hosting_service. + + ls_config = read_2fa_config( iv_service_id ). + ls_config-service_id = iv_service_id. + ls_config-username = iv_username. + ls_config-access_token = iv_token. + + update_2fa_config( iv_service_id = iv_service_id is_2fa_config = ls_config ). + ENDMETHOD. + METHOD toggle_hide_files. DATA ls_user TYPE ty_user.