File: auth.rb

package info (click to toggle)
ruby-vagrant-cloud 3.1.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 560 kB
  • sloc: ruby: 5,343; makefile: 7
file content (140 lines) | stat: -rw-r--r-- 4,578 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
require "oauth2"

module VagrantCloud
  class Auth

    # Default authentication URL
    DEFAULT_AUTH_URL = "https://auth.idp.hashicorp.com".freeze
    # Default authorize path
    DEFAULT_AUTH_PATH = "/oauth2/auth".freeze
    # Default token path
    DEFAULT_TOKEN_PATH = "/oauth2/token".freeze
    # Number of seconds to pad token expiry
    TOKEN_EXPIRY_PADDING = 5

    # HCP configuration for generating authentication tokens
    #
    # @param [String] client_id Service principal client ID
    # @param [String] client_secret Service principal client secret
    # @param [String] auth_url Authentication URL end point
    # @param [String] auth_path Authorization path (relative to end point)
    # @param [String] token_path Token path (relative to end point)
    HCPConfig = Struct.new(:client_id, :client_secret, :auth_url, :auth_path, :token_path, keyword_init: true) do
      # Raise exception if any values are missing
      def validate!
        [:client_id, :client_secret, :auth_url, :auth_path, :token_path].each do |name|
          raise ArgumentError,
            "Missing required HCP authentication configuration value: HCP_#{name.to_s.upcase}" if self.send(name).to_s.empty?
        end
      end
    end

    # HCP token
    #
    # @param [String] token HCP token value
    # @param [Integer] expires_at Epoch seconds
    HCPToken = Struct.new(:token, :expires_at, keyword_init: true) do
      # Raise exception if any values are missing
      def validate!
        [:token, :expires_at].each do |name|
          raise ArgumentError,
            "Missing required token value - #{name.inspect}" if self.send(name).nil?
        end
      end

      # @return [Boolean] token is expired
      # @note Will show token as expired TOKEN_EXPIRY_PADDING
      # seconds prior to actual expiry
      def expired?
        validate!

        Time.now.to_i > (expires_at - TOKEN_EXPIRY_PADDING)
      end

      # @return [Boolean] token is not expired
      def valid?
        !expired?
      end
    end

    # Create a new auth instance
    #
    # @param [String] access_token Static access token
    # @note If no access token is provided, the token will be extracted
    # from the VAGRANT_CLOUD_TOKEN environment variable. If that value
    # is not set, the HCP_CLIENT_ID and HCP_CLIENT_SECRET environment
    # variables will be checked. If found, tokens will be generated as
    # needed using the client id and secret. Otherwise, no token will
    # will be available.
    def initialize(access_token: nil)
      @token = access_token

      # The Vagrant Cloud token has precedence over
      # anything else, so if it is set then it is
      # the only value used.
      @token = ENV["VAGRANT_CLOUD_TOKEN"] if @token.nil?

      # If there is no token set, attempt to load HCP configuration
      if @token.to_s.empty? && (ENV["HCP_CLIENT_ID"] || ENV["HCP_CLIENT_SECRET"])
        @config = HCPConfig.new(
          client_id: ENV["HCP_CLIENT_ID"],
          client_secret: ENV["HCP_CLIENT_SECRET"],
          auth_url: ENV.fetch("HCP_AUTH_URL", DEFAULT_AUTH_URL),
          auth_path: ENV.fetch("HCP_AUTH_PATH", DEFAULT_AUTH_PATH),
          token_path: ENV.fetch("HCP_TOKEN_PATH", DEFAULT_TOKEN_PATH)
        )

        # Validate configuration is populated
        @config.validate!
      end
    end

    # @return [String] authentication token
    def token
      # If a static token is defined, use that value
      return @token if @token

      # If no configuration is set, there is no auth to provide
      return if @config.nil?

      # If an HCP token exists and is not expired
      return @hcp_token.token if @hcp_token&.valid?

      # Generate a new HCP token
      refresh_token!

      @hcp_token.token
    end

    # @return [Boolean] Authentication token is available
    def available?
      !!(@token || @config)
    end

    private

    # Refresh the HCP oauth2 token.
    # @todo rescue exceptions and make them nicer
    def refresh_token!
      client = OAuth2::Client.new(
        @config.client_id,
        @config.client_secret,
        site: @config.auth_url,
        authorize_url: @config.auth_path,
        token_url: @config.token_path,
      )

      begin
        response = client.client_credentials.get_token
        @hcp_token = HCPToken.new(
          token: response.token,
          expires_at: response.expires_at,
        )
      rescue OAuth2::Error => err
        raise Error::ClientError::AuthenticationError,
          err.response.body.chomp,
          err.response.status
      end
    end
  end
end