<commit_before># Authentication provider configuration and helpers.
#
# Userinfo parsing in done through jsonpath :
#   http://goessner.net/articles/JsonPath/
#
# You can define multiple optional jsonpaths and separate them with a 
# semicolon.  For example '$..username;$..login' will run two queries and use
# the first value it finds.
#
# When redirecting a user to auth provider's authorization url, use:
#   Location: AuthProvider.authorize_url(state: <app_generated_state>)
#
# When you want to fetch the userinfo for this user from the auth provider,
# use:
#   AuthProvider.get_userinfo(<access_token_of_the_user>)
#
# To exchange an authorization_code to a real actual access token, use
#   AuthProvider.get_token(<auth_code>)
require 'singleton'
require 'uri'
require 'jsonpath'
require 'httpclient'

require_relative '../helpers/config_helper'

class AuthProvider < OpenStruct
  include Singleton
  include ConfigHelper # adds a .config method
  include Logging

  # Minimum fields for authentication to work if by luck the defaults are ok
  REQUIRED_FIELDS = [
      :client_id, :client_secret, :authorize_endpoint,
      :token_endpoint, :userinfo_endpoint, :userinfo_scope,
      :root_url
  ]

  def self.reset_instance
    Singleton.send :__init__, self
    self
  end

  # Initializes a new auth provider instance.
  def initialize
    # The table syntax is for initializing an OpenStruct.
    @table = {}
    @table[:client_id] = config['oauth2.client_id']
    @table[:client_secret] = config['oauth2.client_secret']
    @table[:authorize_endpoint] = config['oauth2.authorize_endpoint']
    @table[:code_requires_basic_auth] = config['oauth2.code_requires_basic_auth'] || false
    if @table[:code_requires_basic_auth].kind_of?(String)
      @table[:code_requires_basic_auth] = @table[:code_requires_basic_auth] == "true"
    end
    @table[:token_endpoint] = config['oauth2.token_endpoint']
    @table[:token_method] = config['oauth2.token_method'] || 'post'
    @table[:token_post_content_type] = config['oauth2.token_post_content_type'] || 'application/json'
    @table[:userinfo_scope] = config['oauth2.userinfo_scope'] || 'user:email'
    @table[:userinfo_endpoint] = config['oauth2.userinfo_endpoint']
    @table[:userinfo_username_jsonpath] = config['oauth2.userinfo_username_jsonpath'] || '$..username;$..login'
    @table[:userinfo_email_jsonpath] = config['oauth2.userinfo_email_jsonpath'] || '$..email;$..emails;$..primary_email'
    @table[:userinfo_user_id_jsonpath] = config['oauth2.userinfo_user_id_jsonpath'] || '$..id;$..uid;$..userid,$..user_id'
    @table[:root_url] = config['server.root_url']
  end

  def is_kontena?
    return false unless self[:authorize_endpoint]
    config['cloud.provider_is_kontena'].to_s == "true" || URI.parse(self[:authorize_endpoint]).host.end_with?('kontena.io')
  end

  def update_kontena
    return unless is_kontena?
    return unless valid?

    uri = URI.parse(config['cloud.api_url'] || "https://cloud-api.kontena.io")
    uri.path = '/master'

    client = HTTPClient.new
    if config['cloud.ignore_invalid_ssl'].to_s == 'true'
      client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
    end
    client.set_auth(nil, self.client_id, self.client_secret)
    client.force_basic_auth = true

    body = {
      data: {
        attributes: {
          'redirect-uri' => callback_url,
          'url'          => self.root_url
        }
      }
    }

    response = client.request(
      :put,
      uri.to_s,
      header: {
        'Content-Type' => 'application/json',
        'Accept' => 'application/json'
      },
      body: body.to_json
    )
  end

  def missing_fields
    REQUIRED_FIELDS.select { |field| self[field].nil? || self[field].strip == "" }
  end

  # Returns true when all required fields have values. These are the minimum settings that
  # are required for the module to work.
  def valid?
    missing_fields.empty?
  end

  def callback_url
    @callback_url ||= self.root_url.nil? ? nil : URI.join(self.root_url, 'cb')
  end

  # URL to the authentication provider authorization endpoint
  def authorize_url(state: nil, scope: nil)
    uri = URI.parse(self[:authorize_endpoint])
    uri.query = URI.encode_www_form(
      {
        response_type: 'code',
        client_id:     client_id,
        scope:         scope || userinfo_scope,
        state:         state,
        redirect_uri:  callback_url
      }.reject {|_,v| v.nil? }
    )
    uri.to_s
  end
<commit_msg>Use .to_s for URI.join in callback url, remove memoization (#1106)<commit_after># Authentication provider configuration and helpers.
#
# Userinfo parsing in done through jsonpath :
#   http://goessner.net/articles/JsonPath/
#
# You can define multiple optional jsonpaths and separate them with a 
# semicolon.  For example '$..username;$..login' will run two queries and use
# the first value it finds.
#
# When redirecting a user to auth provider's authorization url, use:
#   Location: AuthProvider.authorize_url(state: <app_generated_state>)
#
# When you want to fetch the userinfo for this user from the auth provider,
# use:
#   AuthProvider.get_userinfo(<access_token_of_the_user>)
#
# To exchange an authorization_code to a real actual access token, use
#   AuthProvider.get_token(<auth_code>)
require 'singleton'
require 'uri'
require 'jsonpath'
require 'httpclient'

require_relative '../helpers/config_helper'

class AuthProvider < OpenStruct
  include Singleton
  include ConfigHelper # adds a .config method
  include Logging

  # Minimum fields for authentication to work if by luck the defaults are ok
  REQUIRED_FIELDS = [
      :client_id, :client_secret, :authorize_endpoint,
      :token_endpoint, :userinfo_endpoint, :userinfo_scope,
      :root_url
  ]

  def self.reset_instance
    Singleton.send :__init__, self
    self
  end

  # Initializes a new auth provider instance.
  def initialize
    # The table syntax is for initializing an OpenStruct.
    @table = {}
    @table[:client_id] = config['oauth2.client_id']
    @table[:client_secret] = config['oauth2.client_secret']
    @table[:authorize_endpoint] = config['oauth2.authorize_endpoint']
    @table[:code_requires_basic_auth] = config['oauth2.code_requires_basic_auth'] || false
    if @table[:code_requires_basic_auth].kind_of?(String)
      @table[:code_requires_basic_auth] = @table[:code_requires_basic_auth] == "true"
    end
    @table[:token_endpoint] = config['oauth2.token_endpoint']
    @table[:token_method] = config['oauth2.token_method'] || 'post'
    @table[:token_post_content_type] = config['oauth2.token_post_content_type'] || 'application/json'
    @table[:userinfo_scope] = config['oauth2.userinfo_scope'] || 'user:email'
    @table[:userinfo_endpoint] = config['oauth2.userinfo_endpoint']
    @table[:userinfo_username_jsonpath] = config['oauth2.userinfo_username_jsonpath'] || '$..username;$..login'
    @table[:userinfo_email_jsonpath] = config['oauth2.userinfo_email_jsonpath'] || '$..email;$..emails;$..primary_email'
    @table[:userinfo_user_id_jsonpath] = config['oauth2.userinfo_user_id_jsonpath'] || '$..id;$..uid;$..userid,$..user_id'
    @table[:root_url] = config['server.root_url']
  end

  def is_kontena?
    return false unless self[:authorize_endpoint]
    config['cloud.provider_is_kontena'].to_s == "true" || URI.parse(self[:authorize_endpoint]).host.end_with?('kontena.io')
  end

  def update_kontena
    return unless is_kontena?
    return unless valid?

    uri = URI.parse(config['cloud.api_url'] || "https://cloud-api.kontena.io")
    uri.path = '/master'

    client = HTTPClient.new
    if config['cloud.ignore_invalid_ssl'].to_s == 'true'
      client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
    end
    client.set_auth(nil, self.client_id, self.client_secret)
    client.force_basic_auth = true

    body = {
      data: {
        attributes: {
          'redirect-uri' => callback_url,
          'url'          => self.root_url
        }
      }
    }

    response = client.request(
      :put,
      uri.to_s,
      header: {
        'Content-Type' => 'application/json',
        'Accept' => 'application/json'
      },
      body: body.to_json
    )
  end

  def missing_fields
    REQUIRED_FIELDS.select { |field| self[field].nil? || self[field].strip == "" }
  end

  # Returns true when all required fields have values. These are the minimum settings that
  # are required for the module to work.
  def valid?
    missing_fields.empty?
  end

  def callback_url
    self.root_url.nil? ? nil : URI.join(self.root_url, 'cb').to_s
  end

  # URL to the authentication provider authorization endpoint
  def authorize_url(state: nil, scope: nil)
    uri = URI.parse(self[:authorize_endpoint])
    uri.query = URI.encode_www_form(
      {
        response_type: 'code',
        client_id:     client_id,
        scope:         scope || userinfo_scope,
        state:         state,
        redirect_uri:  callback_url
      }.reject {|_,v| v.nil? }
    )
    uri.to_s
  end

{'fraction_non_alphanumeric': 0.1201645175633615, 'fraction_numerical': 0.0031124944419742106, 'mean_word_length': 3.6640746500777603, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 17, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3852019', 'n_tokens_mistral': 2934, 'n_tokens_neox': 2871, 'n_words': 638}