mirror of
https://github.com/abapGit/abapGit.git
synced 2025-05-03 05:18:59 +08:00

* line break parameters * Update zcl_abapgit_gui_page_repo_sett.clas.abap * Update zcl_abapgit_gui_page_repo_sett.clas.abap * fix
354 lines
11 KiB
ABAP
354 lines
11 KiB
ABAP
CLASS zcl_abapgit_2fa_github_auth DEFINITION
|
|
PUBLIC
|
|
INHERITING FROM zcl_abapgit_2fa_auth_base
|
|
CREATE PUBLIC .
|
|
|
|
PUBLIC SECTION.
|
|
|
|
METHODS constructor
|
|
IMPORTING
|
|
!iv_override TYPE string OPTIONAL .
|
|
|
|
METHODS zif_abapgit_2fa_authenticator~authenticate
|
|
REDEFINITION .
|
|
METHODS zif_abapgit_2fa_authenticator~delete_access_tokens
|
|
REDEFINITION .
|
|
METHODS zif_abapgit_2fa_authenticator~end
|
|
REDEFINITION .
|
|
METHODS zif_abapgit_2fa_authenticator~is_2fa_required
|
|
REDEFINITION .
|
|
PROTECTED SECTION.
|
|
|
|
DATA mv_github_api_url TYPE string VALUE `https://api.github.com/` ##NO_TEXT.
|
|
PRIVATE SECTION.
|
|
|
|
CONSTANTS c_otp_header_name TYPE string VALUE `X-Github-OTP` ##NO_TEXT.
|
|
CONSTANTS c_restendpoint_authorizations TYPE string VALUE `/authorizations` ##NO_TEXT.
|
|
DATA mi_authenticated_session TYPE REF TO if_http_client .
|
|
|
|
CLASS-METHODS set_new_token_request
|
|
IMPORTING
|
|
!ii_request TYPE REF TO if_http_request .
|
|
CLASS-METHODS get_token_from_response
|
|
IMPORTING
|
|
!ii_response TYPE REF TO if_http_response
|
|
RETURNING
|
|
VALUE(rv_token) TYPE string .
|
|
CLASS-METHODS set_list_token_request
|
|
IMPORTING
|
|
!ii_request TYPE REF TO if_http_request .
|
|
CLASS-METHODS get_tobedel_tokens_from_resp
|
|
IMPORTING
|
|
!ii_response TYPE REF TO if_http_response
|
|
RETURNING
|
|
VALUE(rt_ids) TYPE string_table .
|
|
CLASS-METHODS 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
|
|
zcx_abapgit_2fa_auth_failed
|
|
zcx_abapgit_2fa_comm_error .
|
|
ENDCLASS.
|
|
|
|
|
|
|
|
CLASS ZCL_ABAPGIT_2FA_GITHUB_AUTH IMPLEMENTATION.
|
|
|
|
|
|
METHOD constructor.
|
|
|
|
DATA: lv_match TYPE string.
|
|
|
|
IF iv_override IS SUPPLIED.
|
|
lv_match = iv_override.
|
|
ELSE.
|
|
lv_match = '^https?://(www\.)?github.com.*$'.
|
|
ENDIF.
|
|
|
|
super->constructor( lv_match ).
|
|
|
|
ENDMETHOD.
|
|
|
|
|
|
METHOD get_authenticated_client.
|
|
DATA: lv_http_code TYPE i,
|
|
lv_http_code_description TYPE string.
|
|
|
|
" 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
|
|
ri_client = get_http_client_for_url( mv_github_api_url ).
|
|
|
|
" 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 = c_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 zcx_abapgit_2fa_auth_failed
|
|
EXPORTING
|
|
mv_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 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 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 set_del_token_request.
|
|
DATA: lv_url TYPE string.
|
|
|
|
lv_url = |{ c_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( 'DELETE' ).
|
|
ENDMETHOD.
|
|
|
|
|
|
METHOD set_list_token_request.
|
|
ii_request->set_header_field( name = if_http_header_fields_sap=>request_uri
|
|
value = c_restendpoint_authorizations ).
|
|
ii_request->set_method( if_http_request=>co_request_method_get ).
|
|
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 = c_restendpoint_authorizations ).
|
|
ii_request->set_method( if_http_request=>co_request_method_post ).
|
|
ENDMETHOD.
|
|
|
|
|
|
METHOD zif_abapgit_2fa_authenticator~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( 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 zcx_abapgit_2fa_gen_failed
|
|
EXPORTING
|
|
mv_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 zcx_abapgit_2fa_gen_failed
|
|
EXPORTING
|
|
mv_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 zif_abapgit_2fa_authenticator~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 string_table.
|
|
|
|
FIELD-SYMBOLS: <lv_id> 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 zcx_abapgit_2fa_del_failed
|
|
EXPORTING
|
|
mv_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 <lv_id> WHERE table_line IS NOT INITIAL.
|
|
set_del_token_request( ii_request = li_http_client->request
|
|
iv_token_id = <lv_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 zcx_abapgit_2fa_del_failed
|
|
EXPORTING
|
|
mv_text = |Could not delete token '{ <lv_id> }': | &&
|
|
|{ lv_http_code } { lv_http_code_description }|.
|
|
ENDIF.
|
|
ENDLOOP.
|
|
ENDMETHOD.
|
|
|
|
|
|
METHOD zif_abapgit_2fa_authenticator~end.
|
|
super->end( ).
|
|
FREE mi_authenticated_session.
|
|
ENDMETHOD.
|
|
|
|
|
|
METHOD zif_abapgit_2fa_authenticator~is_2fa_required.
|
|
|
|
DATA: li_client TYPE REF TO if_http_client.
|
|
|
|
li_client = get_http_client_for_url( mv_github_api_url ).
|
|
|
|
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 = c_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.
|
|
* if the code fails here with a SSL error, make sure STRUST is setup to
|
|
* work with https://api.github.com
|
|
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( c_otp_header_name ) CP 'required*'.
|
|
rv_required = abap_true.
|
|
ENDIF.
|
|
ENDMETHOD.
|
|
ENDCLASS.
|