openproject icon indicating copy to clipboard operation
openproject copied to clipboard

[#43193] Remove OAuth cookie after successful Nextcloud authorization

Open apfohl opened this issue 2 years ago • 9 comments

https://community.openproject.org/work_packages/43193

ATTENTION: This PR refactors a method with a functional approach for demonstration purposes. It is fully encapsulated and locally done and does not interfere with any other code.

Please give your opinion on the change. Contact @apfohl for explanation if needed.

apfohl avatar Jul 19 '22 14:07 apfohl

@apfohl FYI: The unit tests check failed. I haven't looked into it.

wielinde avatar Jul 19 '22 15:07 wielinde

I have see that and look into it tomorrow.

apfohl avatar Jul 19 '22 15:07 apfohl

Fixed implementation to pass specs.

The failing feature specs are activity related.

apfohl avatar Jul 20 '22 06:07 apfohl

@opf/backend Please have a look.

apfohl avatar Jul 20 '22 09:07 apfohl

One of the remarks from the demo was a question by Markus I believe regarding debuggability. I think the do notation really marks it hard to debug due to its method/block wrapping. This is the trace moving through the method until getting the result.

Frame number: 0/87

From: /home/oliver/openproject/dev/app/services/oauth_clients/redirect_uri_from_state_service.rb:46 OAuthClients::RedirectUriFromStateService#call:

    44: def call
    45:   binding.pry
 => 46:   process(@cookies, @state)
    47:     .fmap { |uri| ServiceResult.success(result: uri) }
    48:     .value_or ServiceResult.failure
    49: end

[1] pry(#<OAuthClients::RedirectUriFromStateService>)> s

From: /home/oliver/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/dry-monads-1.4.0/lib/dry/monads/do.rb:134 #<Module:0x00007fd55869aae8>#process:

    129: 
    130:         # @api private
    131:         def wrap_method(target, method, visibility)
    132:           target.module_eval(<<-RUBY, __FILE__, __LINE__ + 1)
    133:             #{VISIBILITY_WORD[visibility]} def #{method}(#{DELEGATE}) # private def create_acccount(...)
 => 134:               if block_given?                                         #   if block_given?
    135:                 super                                                 #     super
    136:               else                                                    #   else
    137:                 Do.() { super { |*ms| Do.bind(ms) } }                 #     Do.() { super { |*ms| Do.bind(ms) } }
    138:               end                                                     #   end
    139:             end                                                       # end

[1] pry(#<OAuthClients::RedirectUriFromStateService>):1> s

From: /home/oliver/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/dry-monads-1.4.0/lib/dry/monads/do.rb:137 #<Module:0x00007fd55869aae8>#process:

    132:           target.module_eval(<<-RUBY, __FILE__, __LINE__ + 1)
    133:             #{VISIBILITY_WORD[visibility]} def #{method}(#{DELEGATE}) # private def create_acccount(...)
    134:               if block_given?                                         #   if block_given?
    135:                 super                                                 #     super
    136:               else                                                    #   else
 => 137:                 Do.() { super { |*ms| Do.bind(ms) } }                 #     Do.() { super { |*ms| Do.bind(ms) } }
    138:               end                                                     #   end
    139:             end                                                       # end
    140:           RUBY
    141:         end
    142: 

[1] pry(#<OAuthClients::RedirectUriFromStateService>)> 
<internal:warning>:51:in `warn': _pry_ is deprecated, use pry_instance instead (StructuredWarnings::BuiltInWarning)

From: /home/oliver/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/dry-monads-1.4.0/lib/dry/monads/do/mixin.rb:40 Dry::Monads::Do::Mixin#call:

    39: def call
 => 40:   yield
    41: rescue Halt => e
    42:   e.result
    43: end

[1] pry(Dry::Monads::Do)> 
<internal:warning>:51:in `warn': _pry_ is deprecated, use pry_instance instead (StructuredWarnings::BuiltInWarning)

From: /home/oliver/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/dry-monads-1.4.0/lib/dry/monads/do.rb:137 #<Module:0x00007fd55869aae8>#process:

    132:           target.module_eval(<<-RUBY, __FILE__, __LINE__ + 1)
    133:             #{VISIBILITY_WORD[visibility]} def #{method}(#{DELEGATE}) # private def create_acccount(...)
    134:               if block_given?                                         #   if block_given?
    135:                 super                                                 #     super
    136:               else                                                    #   else
 => 137:                 Do.() { super { |*ms| Do.bind(ms) } }                 #     Do.() { super { |*ms| Do.bind(ms) } }
    138:               end                                                     #   end
    139:             end                                                       # end
    140:           RUBY
    141:         end
    142: 

[1] pry(#<OAuthClients::RedirectUriFromStateService>)> 
<internal:warning>:51:in `warn': _pry_ is deprecated, use pry_instance instead (StructuredWarnings::BuiltInWarning)

From: /home/oliver/openproject/dev/app/services/oauth_clients/redirect_uri_from_state_service.rb:54 #<Module:0x00007fd55869aae8>#process:

    49:     end
    50: 
    51:     private
    52: 
    53:     def process(cookies, state)
 => 54:       uuid = yield state
    55:       uri = yield callback_uri_from_cookies cookies, "oauth_state_#{uuid}"
    56:       callback_uri = yield validate_callback_uri uri
    57: 
    58:       Some(callback_uri)
    59:     end
    53:     def process(cookies, state)
 => 54:       uuid = yield state
    55:       uri = yield callback_uri_from_cookies cookies, "oauth_state_#{uuid}"
    56:       callback_uri = yield validate_callback_uri uri
    57: 
    58:       Some(callback_uri)
    59:     end

[1] pry(#<OAuthClients::RedirectUriFromStateService>)> s

From: /home/oliver/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/dry-monads-1.4.0/lib/dry/monads/do.rb:137 #<Module:0x00007fd55869aae8>#process:

    132:           target.module_eval(<<-RUBY, __FILE__, __LINE__ + 1)
    133:             #{VISIBILITY_WORD[visibility]} def #{method}(#{DELEGATE}) # private def create_acccount(...)
    134:               if block_given?                                         #   if block_given?
    135:                 super                                                 #     super
    136:               else                                                    #   else
 => 137:                 Do.() { super { |*ms| Do.bind(ms) } }                 #     Do.() { super { |*ms| Do.bind(ms) } }
    138:               end                                                     #   end
    139:             end                                                       # end
    140:           RUBY
    141:         end
    142: 

[1] pry(#<OAuthClients::RedirectUriFromStateService>)> s

From: /home/oliver/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/dry-monads-1.4.0/lib/dry/monads/do/mixin.rb:47 Dry::Monads::Do::Mixin#bind:

    46: def bind(monads)
 => 47:   monads = Do.coerce_to_monad(Array(monads))
    48:   unwrapped = monads.map do |result|
    49:     monad = result.to_monad
    50:     monad.or { Do.halt(monad) }.value!
    51:   end
    52:   monads.size == 1 ? unwrapped[0] : unwrapped
    53: end

[1] pry(Dry::Monads::Do)> 
<internal:warning>:51:in `warn': _pry_ is deprecated, use pry_instance instead (StructuredWarnings::BuiltInWarning)

From: /home/oliver/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/dry-monads-1.4.0/lib/dry/monads/do.rb:156 Dry::Monads::Do.coerce_to_monad:

    155: def coerce_to_monad(monads)
 => 156:   return monads if monads.size != 1
    157: 
    158:   first = monads[0]
    159: 
    160:   case first
    161:   when ::Array
    162:     [List.coerce(first).traverse]
    163:   when List
    164:     [first.traverse]
    165:   else
    166:     monads
    167:   end
    168: end

[1] pry(Dry::Monads::Do)> 
<internal:warning>:51:in `warn': _pry_ is deprecated, use pry_instance instead (StructuredWarnings::BuiltInWarning)

From: /home/oliver/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/dry-monads-1.4.0/lib/dry/monads/do.rb:158 Dry::Monads::Do.coerce_to_monad:

    155: def coerce_to_monad(monads)
    156:   return monads if monads.size != 1
    157: 
 => 158:   first = monads[0]
    159: 
    160:   case first
    161:   when ::Array
    162:     [List.coerce(first).traverse]
    163:   when List
    164:     [first.traverse]
    165:   else
    166:     monads
    167:   end
    168: end

[1] pry(Dry::Monads::Do)> 
<internal:warning>:51:in `warn': _pry_ is deprecated, use pry_instance instead (StructuredWarnings::BuiltInWarning)

From: /home/oliver/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/dry-monads-1.4.0/lib/dry/monads/do.rb:160 Dry::Monads::Do.coerce_to_monad:

    155: def coerce_to_monad(monads)
    156:   return monads if monads.size != 1
    157: 
    158:   first = monads[0]
    159: 
 => 160:   case first
    161:   when ::Array
    162:     [List.coerce(first).traverse]
    163:   when List
    164:     [first.traverse]
    165:   else
    166:     monads
    167:   end
    168: end

[1] pry(Dry::Monads::Do)> 
<internal:warning>:51:in `warn': _pry_ is deprecated, use pry_instance instead (StructuredWarnings::BuiltInWarning)

From: /home/oliver/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/dry-monads-1.4.0/lib/dry/monads/do.rb:166 Dry::Monads::Do.coerce_to_monad:

    155: def coerce_to_monad(monads)
    156:   return monads if monads.size != 1
    157: 
    158:   first = monads[0]
    159: 
    160:   case first
    161:   when ::Array
    162:     [List.coerce(first).traverse]
    163:   when List
    164:     [first.traverse]
    165:   else
 => 166:     monads
    167:   end
    168: end

[1] pry(Dry::Monads::Do)> 
<internal:warning>:51:in `warn': _pry_ is deprecated, use pry_instance instead (StructuredWarnings::BuiltInWarning)

From: /home/oliver/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/dry-monads-1.4.0/lib/dry/monads/do/mixin.rb:48 Dry::Monads::Do::Mixin#bind:

    46: def bind(monads)
    47:   monads = Do.coerce_to_monad(Array(monads))
 => 48:   unwrapped = monads.map do |result|
    49:     monad = result.to_monad
    50:     monad.or { Do.halt(monad) }.value!
    51:   end
    52:   monads.size == 1 ? unwrapped[0] : unwrapped
    53: end

[1] pry(Dry::Monads::Do)> n

From: /home/oliver/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/dry-monads-1.4.0/lib/dry/monads/do/mixin.rb:52 Dry::Monads::Do::Mixin#bind:

    46: def bind(monads)
    47:   monads = Do.coerce_to_monad(Array(monads))
    48:   unwrapped = monads.map do |result|
    49:     monad = result.to_monad
    50:     monad.or { Do.halt(monad) }.value!
    51:   end
 => 52:   monads.size == 1 ? unwrapped[0] : unwrapped
    53: end

[1] pry(Dry::Monads::Do)> 
<internal:warning>:51:in `warn': _pry_ is deprecated, use pry_instance instead (StructuredWarnings::BuiltInWarning)

From: /home/oliver/openproject/dev/app/services/oauth_clients/redirect_uri_from_state_service.rb:55 #<Module:0x00007fd55869aae8>#process:

    50: 
    51:     private
    52: 
    53:     def process(cookies, state)
    54:       uuid = yield state
 => 55:       uri = yield callback_uri_from_cookies cookies, "oauth_state_#{uuid}"
    56:       callback_uri = yield validate_callback_uri uri
    57: 
    58:       Some(callback_uri)
    59:     end
    60: 

[1] pry(#<OAuthClients::RedirectUriFromStateService>)> 
<internal:warning>:51:in `warn': _pry_ is deprecated, use pry_instance instead (StructuredWarnings::BuiltInWarning)

From: /home/oliver/openproject/dev/app/services/oauth_clients/redirect_uri_from_state_service.rb:56 #<Module:0x00007fd55869aae8>#process:

    51:     private
    52: 
    53:     def process(cookies, state)
    54:       uuid = yield state
    55:       uri = yield callback_uri_from_cookies cookies, "oauth_state_#{uuid}"
 => 56:       callback_uri = yield validate_callback_uri uri
    57: 
    58:       Some(callback_uri)
    59:     end
    60: 
    61:     def callback_uri_from_cookies(cookies, name)

[1] pry(#<OAuthClients::RedirectUriFromStateService>)> 
<internal:warning>:51:in `warn': _pry_ is deprecated, use pry_instance instead (StructuredWarnings::BuiltInWarning)

From: /home/oliver/openproject/dev/app/services/oauth_clients/redirect_uri_from_state_service.rb:58 #<Module:0x00007fd55869aae8>#process:

    53:     def process(cookies, state)
    54:       uuid = yield state
    55:       uri = yield callback_uri_from_cookies cookies, "oauth_state_#{uuid}"
    56:       callback_uri = yield validate_callback_uri uri
    57: 
 => 58:       Some(callback_uri)
    59:     end
    60: 
    61:     def callback_uri_from_cookies(cookies, name)
    62:       Maybe(cookies[name])
    63:     end

[1] pry(#<OAuthClients::RedirectUriFromStateService>)> 
<internal:warning>:51:in `warn': _pry_ is deprecated, use pry_instance instead (StructuredWarnings::BuiltInWarning)

From: /home/oliver/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/dry-monads-1.4.0/lib/dry/monads/maybe.rb:131 Dry::Monads::Maybe::Some#fmap:

    130: def fmap(*args, &block)
 => 131:   next_value = bind(*args, &block)
    132: 
    133:   if next_value.nil?
    134:     if self.class.warn_on_implicit_nil_coercion
    135:       Core::Deprecations.warn(
    136:         "Block passed to Some#fmap returned `nil` and was chained to None. "\
    137:         "This is literally an unlawful behavior and it will not be supported in "\
    138:         "dry-monads 2. \nPlease, replace `.fmap` with `.maybe` in places where you "\
    139:         "expect `nil` as block result.\n"\
    140:         "You can opt out of these warnings with\n"\
    141:         "Dry::Monads::Maybe.warn_on_implicit_nil_coercion false",
    142:         uplevel: 0,
    143:         tag: :'dry-monads'
    144:       )
    145:     end
    146:     Monads.None()
    147:   else
    148:     Some.new(next_value)
    149:   end
    150: end

[1] pry(#<Dry::Monads::Maybe::Some>)> n

From: /home/oliver/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/dry-monads-1.4.0/lib/dry/monads/maybe.rb:133 Dry::Monads::Maybe::Some#fmap:

    130: def fmap(*args, &block)
    131:   next_value = bind(*args, &block)
    132: 
 => 133:   if next_value.nil?
    134:     if self.class.warn_on_implicit_nil_coercion
    135:       Core::Deprecations.warn(
    136:         "Block passed to Some#fmap returned `nil` and was chained to None. "\
    137:         "This is literally an unlawful behavior and it will not be supported in "\
    138:         "dry-monads 2. \nPlease, replace `.fmap` with `.maybe` in places where you "\
    139:         "expect `nil` as block result.\n"\
    140:         "You can opt out of these warnings with\n"\
    141:         "Dry::Monads::Maybe.warn_on_implicit_nil_coercion false",
    142:         uplevel: 0,
    143:         tag: :'dry-monads'
    144:       )
    145:     end
    146:     Monads.None()
    147:   else
    148:     Some.new(next_value)
    149:   end
    150: end

[1] pry(#<Dry::Monads::Maybe::Some>)> 
<internal:warning>:51:in `warn': _pry_ is deprecated, use pry_instance instead (StructuredWarnings::BuiltInWarning)

From: /home/oliver/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/dry-monads-1.4.0/lib/dry/monads/maybe.rb:148 Dry::Monads::Maybe::Some#fmap:

    130: def fmap(*args, &block)
    131:   next_value = bind(*args, &block)
    132: 
    133:   if next_value.nil?
    134:     if self.class.warn_on_implicit_nil_coercion
    135:       Core::Deprecations.warn(
    136:         "Block passed to Some#fmap returned `nil` and was chained to None. "\
    137:         "This is literally an unlawful behavior and it will not be supported in "\
    138:         "dry-monads 2. \nPlease, replace `.fmap` with `.maybe` in places where you "\
    139:         "expect `nil` as block result.\n"\
    140:         "You can opt out of these warnings with\n"\
    141:         "Dry::Monads::Maybe.warn_on_implicit_nil_coercion false",
    142:         uplevel: 0,
    143:         tag: :'dry-monads'
    144:       )
    145:     end
    146:     Monads.None()
    147:   else
 => 148:     Some.new(next_value)
    149:   end
    150: end

[1] pry(#<Dry::Monads::Maybe::Some>)> s

From: /home/oliver/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/dry-monads-1.4.0/lib/dry/monads/maybe.rb:113 Dry::Monads::Maybe::Some#initialize:

    112: def initialize(value = Undefined)
 => 113:   raise ArgumentError, "nil cannot be some" if value.nil?
    114: 
    115:   super()
    116: 
    117:   @value = Undefined.default(value, Unit)
    118: end

[1] pry(#<Dry::Monads::Maybe::Some>)> n

From: /home/oliver/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/dry-monads-1.4.0/lib/dry/monads/maybe.rb:115 Dry::Monads::Maybe::Some#initialize:

    112: def initialize(value = Undefined)
    113:   raise ArgumentError, "nil cannot be some" if value.nil?
    114: 
 => 115:   super()
    116: 
    117:   @value = Undefined.default(value, Unit)
    118: end

[1] pry(#<Dry::Monads::Maybe::Some>)> s

From: /home/oliver/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/dry-monads-1.4.0/lib/dry/monads/maybe.rb:117 Dry::Monads::Maybe::Some#initialize:

    112: def initialize(value = Undefined)
    113:   raise ArgumentError, "nil cannot be some" if value.nil?
    114: 
    115:   super()
    116: 
 => 117:   @value = Undefined.default(value, Unit)
    118: end

[1] pry(#<Dry::Monads::Maybe::Some>)> s

From: /home/oliver/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/dry-core-0.7.1/lib/dry/core/constants.rb:66 self.default:

    65: def undefined.default(x, y = self) # rubocop:disable Naming/MethodParameterName
 => 66:   if equal?(x)
    67:     if equal?(y)
    68:       yield
    69:     else
    70:       y
    71:     end
    72:   else
    73:     x
    74:   end
    75: end

[1] pry(#<Object>)> n

From: /home/oliver/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/dry-core-0.7.1/lib/dry/core/constants.rb:73 self.default:

    65: def undefined.default(x, y = self) # rubocop:disable Naming/MethodParameterName
    66:   if equal?(x)
    67:     if equal?(y)
    68:       yield
    69:     else
    70:       y
    71:     end
    72:   else
 => 73:     x
    74:   end
    75: end

[1] pry(#<Object>)> 
<internal:warning>:51:in `warn': _pry_ is deprecated, use pry_instance instead (StructuredWarnings::BuiltInWarning)

From: /home/oliver/.rbenv/versions/3.1.2/lib/ruby/3.1.0/ostruct.rb:127 OpenStruct#initialize:

    126: def initialize(hash=nil)
 => 127:   if hash
    128:     update_to_values!(hash)
    129:   else
    130:     @table = {}
    131:   end
    132: end

[1] pry(#<Shared::ServiceState>)> 
<internal:warning>:51:in `warn': _pry_ is deprecated, use pry_instance instead (StructuredWarnings::BuiltInWarning)

From: /home/oliver/.rbenv/versions/3.1.2/lib/ruby/3.1.0/ostruct.rb:130 OpenStruct#initialize:

    126: def initialize(hash=nil)
    127:   if hash
    128:     update_to_values!(hash)
    129:   else
 => 130:     @table = {}
    131:   end
    132: end

[1] pry(#<Shared::ServiceState>)> 
<internal:warning>:51:in `warn': _pry_ is deprecated, use pry_instance instead (StructuredWarnings::BuiltInWarning)

From: /home/oliver/openproject/dev/app/services/service_result.rb:59 ServiceResult.failure:

    53: def self.failure(errors: nil,
    54:                  message: nil,
    55:                  message_type: nil,
    56:                  state: ::Shared::ServiceState.new,
    57:                  dependent_results: [],
    58:                  result: nil)
 => 59:   new(success: false,
    60:       errors:,
    61:       message:,
    62:       message_type:,
    63:       state:,
    64:       dependent_results:,
    65:       result:)
    66: end

[1] pry(ServiceResult)> 


oliverguenther avatar Jul 20 '22 09:07 oliverguenther

One of the remarks from the demo was a question by Markus I believe regarding debuggability. I think the do notation really marks it hard to debug due to its method/block wrapping. This is the trace moving through the method until getting the result.

Yes I see and know what you mean. I experienced this with other languages too. And there is no easy solution to that. At least I don't see one. Changing it to a chain of .bind()/.flat_map() calls will get you also only a little bit better debuggability, as you still have the blocks/lambdas that are given to .bind(). This would also be true for adding .bind() to ServiceResult.

What I used to do in this circumstance was to add breakpoints to the functions that are passed to .bind(). Once you reach the end of that function just continue to the next break point. As debugging the implementation of the gem is mostly never needed.

It is not as easy as having a more procedural approach. But it gains more expressiveness. I would also like an opinion on that, if you would like to write something about that.

apfohl avatar Jul 20 '22 11:07 apfohl

DO NOT MERGE. IT'S OPEN FOR DISCUSSION.

apfohl avatar Jul 20 '22 14:07 apfohl

@apfohl I converted this PR into a draft so nobody accidentally merges it.

wielinde avatar Jul 20 '22 15:07 wielinde

@oliverguenther @wielinde I moved the real implementation to https://github.com/opf/openproject/pull/11008.

apfohl avatar Jul 21 '22 07:07 apfohl