Support GitHub two factor authentication

This commit is contained in:
Fabian Lupa 2017-01-13 20:21:55 +01:00
parent cdcbb12e9d
commit 36b817b405
No known key found for this signature in database
GPG Key ID: 727817E619B6CA48
6 changed files with 908 additions and 25 deletions

View File

@ -53,6 +53,7 @@ INCLUDE zabapgit_stage.
INCLUDE zabapgit_git_helpers. INCLUDE zabapgit_git_helpers.
INCLUDE zabapgit_repo. INCLUDE zabapgit_repo.
INCLUDE zabapgit_stage_logic. INCLUDE zabapgit_stage_logic.
INCLUDE zabapgit_2fa.
INCLUDE zabapgit_http. INCLUDE zabapgit_http.
INCLUDE zabapgit_git. INCLUDE zabapgit_git.
INCLUDE zabapgit_objects. INCLUDE zabapgit_objects.

586
src/zabapgit_2fa.prog.abap Normal file
View File

@ -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
"! <p>
"! 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.
"! </p>
"! <p>
"! <em>LCL_2FA_AUTHENTICATOR_REGISTRY</em> can be used to find a suitable implementation for a given
"! repository.
"! </p>
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
"! <p>
"! 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.
"! </p>
"! @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 <em>LIF_2FA-AUTHENTICATOR</em> implememtation
"! <p>
"! This uses the user settings to store cached access tokens and encrypts / decrypts them as needed.
"! </p>
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
"! <em>SUPPORTS_URL</em>
constructor IMPORTING iv_supported_url_regex TYPE clike.
PROTECTED SECTION.
METHODS:
"! Subclass implementation of <em>LIF_2FA_AUTHENTICATOR=&gtAUTHENTICATE</em>
"! <p>
"! The caller will take care of caching the token.
"! </p>
"! @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
"! <p>
"! <em>sy-msg...</em> must be set right before calling!
"! </p>
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 <em>LIF_2FA_AUTHENTICATOR</em> 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: <lo_authenticator> LIKE LINE OF gt_registered_authenticators.
LOOP AT gt_registered_authenticators ASSIGNING <lo_authenticator>.
IF <lo_authenticator>->supports_url( iv_url ) = abap_true.
ro_authenticator = <lo_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.

48
src/zabapgit_2fa.prog.xml Normal file
View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<abapGit version="v1.0.0" serializer="LCL_OBJECT_PROG" serializer_version="v1.0.0">
<asx:abap xmlns:asx="http://www.sap.com/abapxml" version="1.0">
<asx:values>
<PROGDIR>
<NAME>ZABAPGIT_2FA</NAME>
<STATE>A</STATE>
<SQLX/>
<EDTX/>
<VARCL>X</VARCL>
<DBAPL>S</DBAPL>
<DBNA>D$</DBNA>
<CLAS/>
<TYPE/>
<OCCURS/>
<SUBC>I</SUBC>
<APPL/>
<SECU/>
<CNAM/>
<CDAT>0000-00-00</CDAT>
<UNAM/>
<UDAT>0000-00-00</UDAT>
<VERN/>
<LEVL/>
<RSTAT/>
<RMAND/>
<RLOAD/>
<FIXPT>X</FIXPT>
<SSET/>
<SDATE>0000-00-00</SDATE>
<STIME/>
<IDATE>0000-00-00</IDATE>
<ITIME/>
<LDBNAME>D$S</LDBNAME>
<UCCHECK>X</UCCHECK>
</PROGDIR>
<TPOOL>
<item>
<ID>R</ID>
<KEY/>
<ENTRY>ZABAPGIT_2FA</ENTRY>
<LENGTH>12</LENGTH>
<SPLIT/>
</item>
</TPOOL>
</asx:values>
</asx:abap>
</abapGit>

View File

@ -469,21 +469,82 @@ CLASS lcl_http IMPLEMENTATION.
METHOD acquire_login_details. METHOD acquire_login_details.
DATA: lv_default_user TYPE string, DATA: lv_default_user TYPE string,
lv_user TYPE string, lv_user TYPE string,
lv_pass TYPE string, lv_pass TYPE string,
lo_digest TYPE REF TO lcl_http_digest. 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_default_user = lcl_app=>user( )->get_repo_username( iv_url ).
lv_user = lv_default_user. 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( lcl_password_dialog=>popup(
EXPORTING 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 CHANGING
cv_user = lv_user cv_user = lv_user
cv_pass = lv_pass ). 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. IF lv_user IS INITIAL.
lcx_exception=>raise( 'HTTP 401, unauthorized' ). lcx_exception=>raise( 'HTTP 401, unauthorized' ).
@ -494,6 +555,29 @@ CLASS lcl_http IMPLEMENTATION.
iv_username = lv_user ). iv_username = lv_user ).
ENDIF. 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' ). rv_scheme = ii_client->response->get_header_field( 'www-authenticate' ).
FIND REGEX '^(\w+)' IN rv_scheme SUBMATCHES rv_scheme. FIND REGEX '^(\w+)' IN rv_scheme SUBMATCHES rv_scheme.

View File

@ -16,6 +16,13 @@ SELECTION-SCREEN BEGIN OF LINE.
SELECTION-SCREEN COMMENT 1(10) s_pass FOR FIELD p_pass. SELECTION-SCREEN COMMENT 1(10) s_pass FOR FIELD p_pass.
PARAMETERS: p_pass TYPE string LOWER CASE VISIBLE LENGTH 40 ##SEL_WRONG. PARAMETERS: p_pass TYPE string LOWER CASE VISIBLE LENGTH 40 ##SEL_WRONG.
SELECTION-SCREEN END OF LINE. 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. SELECTION-SCREEN END OF SCREEN 1002.
*----------------------------------------------------------------------- *-----------------------------------------------------------------------
@ -24,14 +31,26 @@ SELECTION-SCREEN END OF SCREEN 1002.
CLASS lcl_password_dialog DEFINITION FINAL. CLASS lcl_password_dialog DEFINITION FINAL.
PUBLIC SECTION. 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 CLASS-METHODS popup
IMPORTING 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 CHANGING
cv_user TYPE string cv_user TYPE string
cv_pass TYPE string. cv_pass TYPE string
cv_2fa_token TYPE string.
CLASS-METHODS on_screen_init. CLASS-METHODS on_screen_init.
CLASS-METHODS on_screen_output. CLASS-METHODS on_screen_output.
@ -40,7 +59,10 @@ CLASS lcl_password_dialog DEFINITION FINAL.
iv_ucomm TYPE syucomm. iv_ucomm TYPE syucomm.
PRIVATE SECTION. 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 ENDCLASS. "lcl_password_dialog DEFINITION
@ -52,17 +74,22 @@ CLASS lcl_password_dialog IMPLEMENTATION.
p_url = iv_repo_url. p_url = iv_repo_url.
p_user = cv_user. p_user = cv_user.
mv_confirm = abap_false. 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. CALL SELECTION-SCREEN dynnr STARTING AT 5 5 ENDING AT 60 8.
IF mv_confirm = abap_true. IF mv_confirm = abap_true.
cv_user = p_user. cv_user = p_user.
cv_pass = p_pass. cv_pass = p_pass.
cv_2fa_token = p_2fat.
ELSE. ELSE.
CLEAR: cv_user, cv_pass. CLEAR: cv_user, cv_pass, cv_2fa_token.
ENDIF. 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 ENDMETHOD. "popup
@ -71,6 +98,7 @@ CLASS lcl_password_dialog IMPLEMENTATION.
s_url = 'Repo URL' ##NO_TEXT. s_url = 'Repo URL' ##NO_TEXT.
s_user = 'User' ##NO_TEXT. s_user = 'User' ##NO_TEXT.
s_pass = 'Password' ##NO_TEXT. s_pass = 'Password' ##NO_TEXT.
s_2fat = '2FA' ##NO_TEXT.
ENDMETHOD. "on_screen_init ENDMETHOD. "on_screen_init
METHOD on_screen_output. METHOD on_screen_output.
@ -78,19 +106,51 @@ CLASS lcl_password_dialog IMPLEMENTATION.
ASSERT sy-dynnr = dynnr. ASSERT sy-dynnr = dynnr.
CLEAR p_2fat.
LOOP AT SCREEN. LOOP AT SCREEN.
IF screen-name = 'P_URL'. CASE screen-name.
screen-input = '0'. WHEN 'P_URL'.
screen-intensified = '1'. screen-input = '0'.
screen-display_3d = '0'. screen-intensified = '1'.
MODIFY SCREEN. screen-display_3d = '0'.
ENDIF. MODIFY SCREEN.
IF screen-name = 'P_PASS'.
screen-invisible = '1'. 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. MODIFY SCREEN.
ENDIF. ENDIF.
ENDLOOP. 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 " Program RSSYSTDB, GUI Status %_CSP
PERFORM set_pf_status IN PROGRAM rsdbrunt IF FOUND. PERFORM set_pf_status IN PROGRAM rsdbrunt IF FOUND.
APPEND 'NONE' TO lt_ucomm. "Button Check APPEND 'NONE' TO lt_ucomm. "Button Check
@ -102,28 +162,42 @@ CLASS lcl_password_dialog IMPLEMENTATION.
TABLES TABLES
p_exclude = lt_ucomm. 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'. SET CURSOR FIELD 'P_PASS'.
ENDIF. ENDIF.
ENDMETHOD. "on_screen_output ENDMETHOD. "on_screen_output
METHOD on_screen_event. METHOD on_screen_event.
DATA: lv_answer TYPE c.
ASSERT sy-dynnr = dynnr. ASSERT sy-dynnr = dynnr.
" CRET - F8 " CRET - F8
" OTHERS - simulate Enter press " OTHERS - simulate Enter press
CASE iv_ucomm. 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'. WHEN 'CRET'.
mv_confirm = abap_true. mv_confirm = abap_true.
LEAVE TO SCREEN 0.
WHEN OTHERS. "TODO REFACTOR !!! A CLUTCH ! WHEN OTHERS. "TODO REFACTOR !!! A CLUTCH !
" This will work unless any new specific logic appear " This will work unless any new specific logic appear
" for other commands. The problem is that the password dialog " for other commands. The problem is that the password dialog
" does not have Enter event (or I don't know how to activate it ;) " does not have Enter event (or I don't know how to activate it ;)
" so Enter issues previous command from previous screen " so Enter issues previous command from previous screen
" But for now this works :) Fortunately Esc produces another flow " But for now this works :) Fortunately Esc produces another flow
mv_confirm = abap_true. * mv_confirm = abap_true.
LEAVE TO SCREEN 0. * LEAVE TO SCREEN 0.
ENDCASE. ENDCASE.
ENDMETHOD. "on_screen_event ENDMETHOD. "on_screen_event

View File

@ -406,6 +406,21 @@ CLASS lcl_persistence_user DEFINITION FINAL CREATE PRIVATE FRIENDS lcl_app.
RETURNING VALUE(rv_email) TYPE string RETURNING VALUE(rv_email) TYPE string
RAISING lcx_exception. 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 METHODS toggle_hide_files
RETURNING VALUE(rv_hide) TYPE abap_bool RETURNING VALUE(rv_hide) TYPE abap_bool
RAISING lcx_exception. RAISING lcx_exception.
@ -455,6 +470,13 @@ CLASS lcl_persistence_user DEFINITION FINAL CREATE PRIVATE FRIENDS lcl_app.
END OF ty_repo_config. END OF ty_repo_config.
TYPES: ty_repo_config_tt TYPE STANDARD TABLE OF ty_repo_config WITH DEFAULT KEY. 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, TYPES: BEGIN OF ty_user,
username TYPE string, username TYPE string,
email 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, changes_only TYPE abap_bool,
diff_unified TYPE abap_bool, diff_unified TYPE abap_bool,
favorites TYPE tt_favorites, favorites TYPE tt_favorites,
two_fact_cfg TYPE ty_git_hosting_service_tab,
END OF ty_user. END OF ty_user.
METHODS constructor METHODS constructor
@ -496,6 +519,16 @@ CLASS lcl_persistence_user DEFINITION FINAL CREATE PRIVATE FRIENDS lcl_app.
is_repo_config TYPE ty_repo_config is_repo_config TYPE ty_repo_config
RAISING lcx_exception. 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 ENDCLASS. "lcl_persistence_user DEFINITION
CLASS lcl_persistence_user IMPLEMENTATION. CLASS lcl_persistence_user IMPLEMENTATION.
@ -639,6 +672,48 @@ CLASS lcl_persistence_user IMPLEMENTATION.
ENDMETHOD. "update_repo_config 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 <ls_2fa_config> TYPE ty_git_hosting_service.
ls_user = read( ).
lv_key = to_lower( iv_service_id ).
READ TABLE ls_user-two_fact_cfg ASSIGNING <ls_2fa_config> WITH KEY service_id = lv_key.
IF sy-subrc IS NOT INITIAL.
APPEND INITIAL LINE TO ls_user-two_fact_cfg ASSIGNING <ls_2fa_config>.
ENDIF.
<ls_2fa_config> = is_2fa_config.
<ls_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. METHOD set_repo_username.
DATA: ls_repo_config TYPE ty_repo_config. DATA: ls_repo_config TYPE ty_repo_config.
@ -671,6 +746,21 @@ CLASS lcl_persistence_user IMPLEMENTATION.
ENDMETHOD. "get_repo_email 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. METHOD toggle_hide_files.
DATA ls_user TYPE ty_user. DATA ls_user TYPE ty_user.