summaryrefslogtreecommitdiff
path: root/lib/gitlab/lfs_token.rb
blob: 26b81847d37a63ff294ebeb21b268748f575ed09 (plain)
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
# frozen_string_literal: true

module Gitlab
  class LfsToken
    module LfsTokenHelper
      def user?
        actor.is_a?(User)
      end

      def actor_name
        user? ? actor.username : "lfs+deploy-key-#{actor.id}"
      end
    end

    include LfsTokenHelper

    DEFAULT_EXPIRE_TIME = 1800

    attr_accessor :actor

    def initialize(actor)
      @actor =
        case actor
        when DeployKey, User
          actor
        when Key
          actor.user
        else
          raise 'Bad Actor'
        end
    end

    def token(expire_time: DEFAULT_EXPIRE_TIME)
      HMACToken.new(actor).token(expire_time)
    end

    def token_valid?(token_to_check)
      HMACToken.new(actor).token_valid?(token_to_check) ||
        LegacyRedisDeviseToken.new(actor).token_valid?(token_to_check)
    end

    def deploy_key_pushable?(project)
      actor.is_a?(DeployKey) && actor.can_push_to?(project)
    end

    def type
      user? ? :lfs_token : :lfs_deploy_token
    end

    private # rubocop:disable Lint/UselessAccessModifier

    class HMACToken
      include LfsTokenHelper

      def initialize(actor)
        @actor = actor
      end

      def token(expire_time)
        hmac_token = JSONWebToken::HMACToken.new(secret)
        hmac_token.expire_time = Time.now + expire_time
        hmac_token[:data] = { actor: actor_name }
        hmac_token.encoded
      end

      def token_valid?(token_to_check)
        decoded_token = JSONWebToken::HMACToken.decode(token_to_check, secret).first
        decoded_token.dig('data', 'actor') == actor_name
      rescue JWT::DecodeError
        false
      end

      private

      attr_reader :actor

      def secret
        salt + key
      end

      def salt
        case actor
        when DeployKey, Key
          actor.fingerprint.delete(':').first(16)
        when User
          # Take the last 16 characters as they're more unique than the first 16
          actor.id.to_s + actor.encrypted_password.last(16)
        end
      end

      def key
        # Take 16 characters of attr_encrypted_db_key_base, as that's what the
        # cipher needs exactly
        Settings.attr_encrypted_db_key_base.first(16)
      end
    end

    # TODO: LegacyRedisDeviseToken and references need to be removed after
    # next released milestone
    #
    class LegacyRedisDeviseToken
      TOKEN_LENGTH = 50
      DEFAULT_EXPIRY_TIME = 1800 * 1000 # 30 mins

      def initialize(actor)
        @actor = actor
      end

      def token_valid?(token_to_check)
        Devise.secure_compare(stored_token, token_to_check)
      end

      def stored_token
        Gitlab::Redis::SharedState.with { |redis| redis.get(state_key) }
      end

      # This method exists purely to facilitate legacy testing to ensure the
      # same redis key is used.
      #
      def store_new_token(expiry_time_in_ms = DEFAULT_EXPIRY_TIME)
        Gitlab::Redis::SharedState.with do |redis|
          new_token = Devise.friendly_token(TOKEN_LENGTH)
          redis.set(state_key, new_token, px: expiry_time_in_ms)
          new_token
        end
      end

      private

      attr_reader :actor

      def state_key
        "gitlab:lfs_token:#{actor.class.name.underscore}_#{actor.id}"
      end
    end
  end
end