diff --git a/changelog.txt b/changelog.txt index 62f52c34a..bbec9de61 100644 --- a/changelog.txt +++ b/changelog.txt @@ -8,6 +8,10 @@ Legend + : added - : removed +2017-02-25 v1.27.0 +------------------ ++ Two factor authentication with github.com + 2017-01-22 v1.26.0 ------------------ + XML ignore initial fields diff --git a/src/zabapgit.prog.abap b/src/zabapgit.prog.abap index b17174b1e..0e9a562ba 100644 --- a/src/zabapgit.prog.abap +++ b/src/zabapgit.prog.abap @@ -3,7 +3,7 @@ REPORT zabapgit LINE-SIZE 100. * See http://www.abapgit.org CONSTANTS: gc_xml_version TYPE string VALUE 'v1.0.0', "#EC NOTEXT - gc_abap_version TYPE string VALUE 'v1.26.5'. "#EC NOTEXT + gc_abap_version TYPE string VALUE 'v1.27.0'. "#EC NOTEXT ******************************************************************************** * The MIT License (MIT) @@ -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..5a2883313 --- /dev/null +++ b/src/zabapgit_2fa.prog.abap @@ -0,0 +1,792 @@ +*&---------------------------------------------------------------------* +*& 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_token_del_failed DEFINITION INHERITING FROM lcx_2fa_error FINAL. + PROTECTED SECTION. + METHODS: + get_default_text REDEFINITION. +ENDCLASS. + +CLASS lcx_2fa_token_del_failed IMPLEMENTATION. + METHOD get_default_text. + rv_text = 'Deleting previous access tokens failed.' ##NO_TEXT. + ENDMETHOD. +ENDCLASS. + +CLASS lcx_2fa_communication_error DEFINITION INHERITING FROM lcx_2fa_error FINAL. + PROTECTED SECTION. + METHODS: + get_default_text REDEFINITION. +ENDCLASS. + +CLASS lcx_2fa_communication_error IMPLEMENTATION. + METHOD get_default_text. + rv_text = 'Communication error.' ##NO_TEXT. + ENDMETHOD. +ENDCLASS. + +CLASS lcx_2fa_illegal_state DEFINITION INHERITING FROM lcx_2fa_error FINAL. + PROTECTED SECTION. + METHODS: + get_default_text REDEFINITION. +ENDCLASS. + +CLASS lcx_2fa_illegal_state IMPLEMENTATION. + METHOD get_default_text. + rv_text = 'Illegal state.' ##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. +"!

+"!

+"! 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: TYPE string. + + li_http_client = get_authenticated_client( iv_username = iv_username + iv_password = iv_password + iv_2fa_token = iv_2fa_token ). + + set_list_token_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 <> 200. + RAISE EXCEPTION TYPE lcx_2fa_token_del_failed + EXPORTING + iv_error_text = |Could not fetch current 2FA authorizations: | && + |{ lv_http_code } { lv_http_code_description }|. + ENDIF. + + lt_tobedeleted_tokens = get_tobedel_tokens_from_resp( li_http_client->response ). + LOOP AT lt_tobedeleted_tokens ASSIGNING WHERE table_line IS NOT INITIAL. + set_del_token_request( ii_request = li_http_client->request + iv_token_id = ). + 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 <> 204. + RAISE EXCEPTION TYPE lcx_2fa_token_del_failed + EXPORTING + iv_error_text = |Could not delete token '{ }': | && + |{ lv_http_code } { lv_http_code_description }|. + ENDIF. + ENDLOOP. + ENDMETHOD. + + METHOD get_authenticated_client. + DATA: lv_http_code TYPE i, + lv_http_code_description TYPE string, + lo_settings TYPE REF TO lcl_settings. + + " If there is a cached client return it instead + IF is_session_running( ) = abap_true AND mi_authenticated_session IS BOUND. + ri_client = mi_authenticated_session. + RETURN. + ENDIF. + + " Try to login to GitHub API with username, password and 2fa token + + 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 = ri_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. + + " https://developer.github.com/v3/auth/#working-with-two-factor-authentication + ri_client->propertytype_accept_cookie = if_http_client=>co_enabled. + ri_client->request->set_header_field( name = gc_otp_header_name value = iv_2fa_token ). + ri_client->authenticate( username = iv_username password = iv_password ). + ri_client->propertytype_logon_popup = if_http_client=>co_disabled. + + ri_client->send( EXCEPTIONS OTHERS = 1 ). + IF sy-subrc <> 0. + raise_comm_error_from_sy( ). + ENDIF. + + ri_client->receive( EXCEPTIONS OTHERS = 1 ). + IF sy-subrc <> 0. + raise_comm_error_from_sy( ). + ENDIF. + + " Check if authentication has succeeded + ri_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. + + " Cache the authenticated http session / client to avoid unnecessary additional authentication + IF is_session_running( ) = abap_true. + mi_authenticated_session = ri_client. + ENDIF. + ENDMETHOD. + + METHOD end. + super->end( ). + FREE mi_authenticated_session. + 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, + "! Offer to use two factor authentication if supported and required + "!

+ "! 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: 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. + + METHOD use_2fa_if_required. + DATA: li_authenticator TYPE REF TO lif_2fa_authenticator, + lv_2fa_token TYPE string, + lv_use_2fa TYPE abap_bool, + lv_access_token TYPE string, + lx_ex TYPE REF TO cx_root. + + IF is_url_supported( iv_url ) = abap_false. + RETURN. + ENDIF. + + TRY. + li_authenticator = get_authenticator_for_url( iv_url ). + li_authenticator->begin( ). + + " Is two factor authentication required for this account? + IF li_authenticator->is_2fa_required( iv_url = iv_url + iv_username = cv_username + iv_password = cv_password ) = abap_true. + + " Get a 2FA token (app/sms) + CALL FUNCTION 'POPUP_GET_STRING' + EXPORTING + label = 'Two factor auth. token' + IMPORTING + value = lv_2fa_token + okay = lv_use_2fa. + IF lv_use_2fa = abap_false. + lcx_exception=>raise( 'Authentication cancelled' ). + ENDIF. + + " Delete an old access token if it exists + li_authenticator->delete_access_tokens( iv_url = iv_url + iv_username = cv_username + iv_password = cv_password + iv_2fa_token = lv_2fa_token ). + + " Get a new access token + lv_access_token = li_authenticator->authenticate( iv_url = iv_url + iv_username = cv_username + iv_password = cv_password + iv_2fa_token = lv_2fa_token ). + + " Use the access token instead of the password + cv_password = lv_access_token. + ENDIF. + + li_authenticator->end( ). + + CATCH lcx_2fa_error INTO lx_ex. + TRY. + li_authenticator->end( ). + CATCH lcx_2fa_illegal_state ##NO_HANDLER. + ENDTRY. + + RAISE EXCEPTION TYPE lcx_exception + EXPORTING + iv_text = |2FA error: { lx_ex->get_text( ) }| + ix_previous = lx_ex. + ENDTRY. + ENDMETHOD. +ENDCLASS. diff --git a/src/zabapgit_2fa.prog.xml b/src/zabapgit_2fa.prog.xml new file mode 100644 index 000000000..048b86095 --- /dev/null +++ b/src/zabapgit_2fa.prog.xml @@ -0,0 +1,25 @@ + + + + + + ZABAPGIT_2FA + A + X + S + D$ + I + X + D$S + X + + + + R + ZABAPGIT_2FA + 12 + + + + + diff --git a/src/zabapgit_css_common.w3mi.data.css b/src/zabapgit_css_common.w3mi.data.css index 5a8ee5538..2528aa62f 100644 --- a/src/zabapgit_css_common.w3mi.data.css +++ b/src/zabapgit_css_common.w3mi.data.css @@ -37,6 +37,8 @@ form input:focus, textarea:focus { .grey { color: lightgrey !important; } .darkgrey { color: #808080 !important; } .attention { color: red !important; } +.error { color: #d41919 !important; } +.warning { color: #e4ae0d !important; } .blue { color: #5e8dc9 !important; } .red { color: red !important; } @@ -105,6 +107,19 @@ span.page_title { padding-left: 0.4em; } +/* ERROR LOG */ + +div.log { + padding: 6px; + margin: 4px; + background-color: #fee6e6; + border: 1px #fdcece solid; + border-radius: 4px; +} + +div.log > span { display:block; } +div.log .octicon { padding-right: 6px; } + /* MENU */ div.menu { display: inline; } div.menu .menu_end { border-right: 0px !important; } diff --git a/src/zabapgit_css_common.w3mi.xml b/src/zabapgit_css_common.w3mi.xml index 7f7372fc2..cbea95037 100644 --- a/src/zabapgit_css_common.w3mi.xml +++ b/src/zabapgit_css_common.w3mi.xml @@ -3,7 +3,7 @@ ZABAPGIT_CSS_COMMON - + Abapgit common CSS MI @@ -15,7 +15,7 @@ MI ZABAPGIT_CSS_COMMON filename - ~wwwtmp.css + common.css MI diff --git a/src/zabapgit_definitions.prog.abap b/src/zabapgit_definitions.prog.abap index 478591319..30a6e3e40 100644 --- a/src/zabapgit_definitions.prog.abap +++ b/src/zabapgit_definitions.prog.abap @@ -227,6 +227,7 @@ CONSTANTS: BEGIN OF gc_action, go_debuginfo TYPE string VALUE 'go_debuginfo', go_settings TYPE string VALUE 'go_settings', go_tutorial TYPE string VALUE 'go_tutorial', + jump TYPE string VALUE 'jump', jump_pkg TYPE string VALUE 'jump_pkg', END OF gc_action. diff --git a/src/zabapgit_gui_router.prog.abap b/src/zabapgit_gui_router.prog.abap index c70393e4a..d910693cb 100644 --- a/src/zabapgit_gui_router.prog.abap +++ b/src/zabapgit_gui_router.prog.abap @@ -35,8 +35,8 @@ CLASS lcl_gui_router DEFINITION FINAL. RAISING lcx_exception. METHODS get_page_stage - IMPORTING iv_key TYPE lcl_persistence_repo=>ty_repo-key - RETURNING VALUE(ri_page) TYPE REF TO lif_gui_page + IMPORTING iv_key TYPE lcl_persistence_repo=>ty_repo-key + RETURNING VALUE(ri_page) TYPE REF TO lif_gui_page RAISING lcx_exception. METHODS get_page_db_by_name @@ -310,7 +310,7 @@ CLASS lcl_gui_router IMPLEMENTATION. CREATE OBJECT lo_stage_page EXPORTING - io_repo = lo_repo. + io_repo = lo_repo. ri_page = lo_stage_page. diff --git a/src/zabapgit_http.prog.abap b/src/zabapgit_http.prog.abap index 11c1fb551..b8e12a738 100644 --- a/src/zabapgit_http.prog.abap +++ b/src/zabapgit_http.prog.abap @@ -469,10 +469,10 @@ 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, + lo_digest TYPE REF TO lcl_http_digest. lv_default_user = lcl_app=>user( )->get_repo_username( iv_url ). @@ -480,10 +480,10 @@ CLASS lcl_http IMPLEMENTATION. lcl_password_dialog=>popup( EXPORTING - iv_repo_url = iv_url + iv_repo_url = iv_url CHANGING - cv_user = lv_user - cv_pass = lv_pass ). + cv_user = lv_user + cv_pass = lv_pass ). IF lv_user IS INITIAL. lcx_exception=>raise( 'HTTP 401, unauthorized' ). @@ -494,6 +494,14 @@ CLASS lcl_http IMPLEMENTATION. iv_username = lv_user ). ENDIF. + " Offer two factor authentication if it is available and required + lcl_2fa_authenticator_registry=>use_2fa_if_required( + EXPORTING + iv_url = iv_url + CHANGING + cv_username = lv_user + cv_password = lv_pass ). + rv_scheme = ii_client->response->get_header_field( 'www-authenticate' ). FIND REGEX '^(\w+)' IN rv_scheme SUBMATCHES rv_scheme. diff --git a/src/zabapgit_object_tobj.prog.abap b/src/zabapgit_object_tobj.prog.abap index 408c2dd6e..63e83801b 100644 --- a/src/zabapgit_object_tobj.prog.abap +++ b/src/zabapgit_object_tobj.prog.abap @@ -27,7 +27,18 @@ CLASS lcl_object_tobj IMPLEMENTATION. ENDMETHOD. "lif_object~has_changed_since METHOD lif_object~changed_by. - rv_user = c_user_unknown. " todo + + DATA: lv_type_pos TYPE i. + + lv_type_pos = strlen( ms_item-obj_name ) - 1. + + SELECT SINGLE luser FROM objh INTO rv_user + WHERE objectname = ms_item-obj_name(lv_type_pos) + AND objecttype = ms_item-obj_name+lv_type_pos. "#EC CI_GENBUFF + IF sy-subrc <> 0. + rv_user = c_user_unknown. + ENDIF. + ENDMETHOD. METHOD lif_object~get_metadata. diff --git a/src/zabapgit_page_stage.prog.abap b/src/zabapgit_page_stage.prog.abap index 9de53d4d6..2b4f85f7a 100644 --- a/src/zabapgit_page_stage.prog.abap +++ b/src/zabapgit_page_stage.prog.abap @@ -12,7 +12,8 @@ CLASS lcl_gui_page_stage DEFINITION FINAL INHERITING FROM lcl_gui_page. METHODS: constructor - IMPORTING io_repo TYPE REF TO lcl_repo_online + IMPORTING + io_repo TYPE REF TO lcl_repo_online RAISING lcx_exception, lif_gui_page~on_event REDEFINITION. @@ -35,7 +36,10 @@ CLASS lcl_gui_page_stage DEFINITION FINAL INHERITING FROM lcl_gui_page. iv_context TYPE string RETURNING VALUE(ro_html) TYPE REF TO lcl_html, render_menu - RETURNING VALUE(ro_html) TYPE REF TO lcl_html. + RETURNING VALUE(ro_html) TYPE REF TO lcl_html, + read_last_changed_by + IMPORTING is_file TYPE ty_file + RETURNING VALUE(rv_user) TYPE xubname. METHODS process_stage_list IMPORTING it_postdata TYPE cnht_post_data_tab @@ -66,7 +70,6 @@ CLASS lcl_gui_page_stage IMPLEMENTATION. FIELD-SYMBOLS: LIKE LINE OF ms_files-local. - CASE iv_action. WHEN c_action-stage_all. mo_stage->reset_all( ). @@ -112,7 +115,6 @@ CLASS lcl_gui_page_stage IMPLEMENTATION. lcl_path=>split_file_location( EXPORTING iv_fullpath = -name IMPORTING ev_path = ls_file-path ev_filename = ls_file-filename ). - CASE -value. WHEN lcl_stage=>c_method-add. READ TABLE ms_files-local ASSIGNING @@ -142,7 +144,6 @@ CLASS lcl_gui_page_stage IMPLEMENTATION. FIELD-SYMBOLS: LIKE LINE OF ms_files-remote, LIKE LINE OF ms_files-local. - CREATE OBJECT ro_html. ro_html->add( '' ). @@ -158,6 +159,7 @@ CLASS lcl_gui_page_stage IMPLEMENTATION. iv_act = |{ gc_action-go_diff }?key={ mo_repo->get_key( ) }| ). ENDIF. ro_html->add(''). + ro_html->add(''). ro_html->add(''). ro_html->add(''). ENDAT. @@ -191,7 +193,8 @@ CLASS lcl_gui_page_stage IMPLEMENTATION. METHOD render_file. - DATA lv_param TYPE string. + DATA: lv_param TYPE string, + lv_user TYPE xubname. CREATE OBJECT ro_html. @@ -207,6 +210,9 @@ CLASS lcl_gui_page_stage IMPLEMENTATION. ro_html->add( '' ). + + lv_user = read_last_changed_by( is_file ). + ro_html->add( | | ). WHEN 'remote'. ro_html->add( '' ). ro_html->add( || ). @@ -263,4 +269,18 @@ CLASS lcl_gui_page_stage IMPLEMENTATION. ENDMETHOD. "scripts + METHOD read_last_changed_by. + DATA: ls_local_file TYPE ty_file_item, + lt_files_local type ty_files_item_tt. + TRY. + lt_files_local = mo_repo->get_files_local( ). + READ TABLE lt_files_local INTO ls_local_file WITH KEY file = is_file. + IF sy-subrc = 0. + rv_user = lcl_objects=>changed_by( ls_local_file-item ). + ENDIF. + CATCH lcx_exception. + CLEAR rv_user. "Should not raise errors if user last changed by was not found + ENDTRY. + ENDMETHOD. + ENDCLASS. diff --git a/src/zabapgit_persistence.prog.abap b/src/zabapgit_persistence.prog.abap index 25205845c..18e27bca7 100644 --- a/src/zabapgit_persistence.prog.abap +++ b/src/zabapgit_persistence.prog.abap @@ -1,4 +1,4 @@ -*&---------------------------- +*&---------------------------------------------------------------------* *& Include ZABAPGIT_PERSISTENCE *&---------------------------------------------------------------------* diff --git a/src/zabapgit_stage_logic.prog.abap b/src/zabapgit_stage_logic.prog.abap index 20266e33d..14e05763d 100644 --- a/src/zabapgit_stage_logic.prog.abap +++ b/src/zabapgit_stage_logic.prog.abap @@ -30,11 +30,13 @@ ENDCLASS. CLASS lcl_stage_logic IMPLEMENTATION. METHOD get. + rs_files-local = io_repo->get_files_local( ). rs_files-remote = io_repo->get_files_remote( ). remove_identical( CHANGING cs_files = rs_files ). - remove_ignored( EXPORTING io_repo = io_repo - CHANGING cs_files = rs_files ). + remove_ignored( EXPORTING io_repo = io_repo + CHANGING cs_files = rs_files ). + ENDMETHOD. METHOD count. @@ -58,10 +60,14 @@ CLASS lcl_stage_logic IMPLEMENTATION. lv_index = sy-tabix. IF io_repo->get_dot_abapgit( )->is_ignored( - iv_path = -path + iv_path = -path iv_filename = -filename ) = abap_true. DELETE cs_files-remote INDEX lv_index. + ELSEIF -path = gc_root_dir AND -filename = gc_dot_abapgit. + " Remove .abapgit from remotes - it cannot be removed or ignored + DELETE cs_files-remote INDEX lv_index. ENDIF. + ENDLOOP. ENDMETHOD. diff --git a/src/zabapgit_util.prog.abap b/src/zabapgit_util.prog.abap index de87acd11..55a455651 100644 --- a/src/zabapgit_util.prog.abap +++ b/src/zabapgit_util.prog.abap @@ -1069,16 +1069,14 @@ CLASS lcl_log IMPLEMENTATION. RETURN. ENDIF. - ro_html->add( '
' ). LOOP AT mt_log ASSIGNING . - CONCATENATE -msgv1 - -msgv2 - -msgv3 - -msgv4 INTO lv_string SEPARATED BY space. + CONCATENATE -msgv1 -msgv2 -msgv3 -msgv4 + INTO lv_string SEPARATED BY space. + ro_html->add( '' ). + ro_html->add_icon( iv_name = 'alert' iv_class = 'error' ). " warning CSS exists too ro_html->add( lv_string ). - ro_html->add( '
' ). + ro_html->add( '
' ). ENDLOOP. - ro_html->add( '
' ). ENDMETHOD. diff --git a/src/zabapgit_view_repo.prog.abap b/src/zabapgit_view_repo.prog.abap index cba7161f1..2a4a128be 100644 --- a/src/zabapgit_view_repo.prog.abap +++ b/src/zabapgit_view_repo.prog.abap @@ -146,7 +146,7 @@ CLASS lcl_gui_view_repo_content IMPLEMENTATION. lo_log = lo_browser->get_log( ). IF mo_repo->is_offline( ) = abap_false AND lo_log->count( ) > 0. - ro_html->add( '
' ). + ro_html->add( '
' ). ro_html->add( lo_log->to_html( ) ). " shows eg. list of unsupported objects ro_html->add( '
' ). ENDIF. @@ -278,6 +278,10 @@ CLASS lcl_gui_view_repo_content IMPLEMENTATION. iv_act = |{ gc_action-repo_remote_change }?{ lv_key }| ). lo_tb_advanced->add( iv_txt = 'Make off-line' iv_act = |{ gc_action-repo_remote_detach }?{ lv_key }| ). + IF iv_rstate IS INITIAL AND iv_lstate IS INITIAL. + lo_tb_advanced->add( iv_txt = 'Force stage' + iv_act = |{ gc_action-go_stage }?{ lv_key }| ). + ENDIF. ELSE. lo_tb_advanced->add( iv_txt = 'Make on-line' iv_act = |{ gc_action-repo_remote_attach }?{ lv_key }| ).
Last changed by
' ). ro_html->add_a( iv_txt = 'diff' iv_act = |{ gc_action-go_diff }?{ lv_param }| ). ro_html->add( '{ lv_user }ignoreremove-