Class: Users::RefreshAuthorizedProjectsService

Inherits:
Object
  • Object
show all
Defined in:
app/services/users/refresh_authorized_projects_service.rb

Overview

Service for refreshing the authorized projects of a user.

This particular service class can not be used to update data for the same user concurrently. Doing so could lead to an incorrect state. To ensure this doesn't happen a caller must synchronize access (e.g. using `Gitlab::ExclusiveLease`).

Usage:

user = User.find_by(username: 'alice')
service = Users::RefreshAuthorizedProjectsService.new(some_user)
service.execute

Constant Summary collapse

LEASE_TIMEOUT =
1.minute.to_i

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(user, incorrect_auth_found_callback: nil, missing_auth_found_callback: nil) ⇒ RefreshAuthorizedProjectsService

user - The User for which to refresh the authorized projects.


22
23
24
25
26
27
28
29
30
31
# File 'app/services/users/refresh_authorized_projects_service.rb', line 22

def initialize(user, incorrect_auth_found_callback: nil, missing_auth_found_callback: nil)
  @user = user
  @incorrect_auth_found_callback = incorrect_auth_found_callback
  @missing_auth_found_callback = missing_auth_found_callback

  # We need an up to date User object that has access to all relations that
  # may have been created earlier. The only way to ensure this is to reload
  # the User object.
  user.reset
end

Instance Attribute Details

#userObject (readonly)

Returns the value of attribute user


17
18
19
# File 'app/services/users/refresh_authorized_projects_service.rb', line 17

def user
  @user
end

Instance Method Details

#current_authorizationsObject


114
115
116
# File 'app/services/users/refresh_authorized_projects_service.rb', line 114

def current_authorizations
  @current_authorizations ||= user.project_authorizations.select(:project_id, :access_level)
end

#current_authorizations_per_projectObject


110
111
112
# File 'app/services/users/refresh_authorized_projects_service.rb', line 110

def current_authorizations_per_project
  current_authorizations.index_by(&:project_id)
end

#executeObject


33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'app/services/users/refresh_authorized_projects_service.rb', line 33

def execute
  lease_key = "refresh_authorized_projects:#{user.id}"
  lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)

  until uuid = lease.try_obtain
    # Keep trying until we obtain the lease. If we don't do so we may end up
    # not updating the list of authorized projects properly. To prevent
    # hammering Redis too much we'll wait for a bit between retries.
    sleep(0.1)
  end

  begin
    execute_without_lease
  ensure
    Gitlab::ExclusiveLease.cancel(lease_key, uuid)
  end
end

#execute_without_leaseObject

This method returns the updated User object.


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
# File 'app/services/users/refresh_authorized_projects_service.rb', line 52

def execute_without_lease
  current = current_authorizations_per_project
  fresh = fresh_access_levels_per_project

  # Delete projects that have more than one authorizations associated with
  # the user. The correct authorization is added to the ``add`` array in the
  # next stage.
  remove = projects_with_duplicates
  current.except!(*projects_with_duplicates)

  remove |= current.each_with_object([]) do |(project_id, row), array|
    # rows not in the new list or with a different access level should be
    # removed.
    if !fresh[project_id] || fresh[project_id] != row.access_level
      if incorrect_auth_found_callback
        incorrect_auth_found_callback.call(project_id, row.access_level)
      end

      array << row.project_id
    end
  end

  add = fresh.each_with_object([]) do |(project_id, level), array|
    # rows not in the old list or with a different access level should be
    # added.
    if !current[project_id] || current[project_id].access_level != level
      if missing_auth_found_callback
        missing_auth_found_callback.call(project_id, level)
      end

      array << [user.id, project_id, level]
    end
  end

  update_authorizations(remove, add)
end

#fresh_access_levels_per_projectObject


104
105
106
107
108
# File 'app/services/users/refresh_authorized_projects_service.rb', line 104

def fresh_access_levels_per_project
  fresh_authorizations.each_with_object({}) do |row, hash|
    hash[row.project_id] = row.access_level
  end
end

#fresh_authorizationsObject


118
119
120
# File 'app/services/users/refresh_authorized_projects_service.rb', line 118

def fresh_authorizations
  Gitlab::ProjectAuthorizations.new(user).calculate
end

#update_authorizations(remove = [], add = []) ⇒ Object

Updates the list of authorizations for the current user.

remove - The IDs of the authorization rows to remove. add - Rows to insert in the form `[user id, project id, access level]`


93
94
95
96
97
98
99
100
101
102
# File 'app/services/users/refresh_authorized_projects_service.rb', line 93

def update_authorizations(remove = [], add = [])
  User.transaction do
    user.remove_project_authorizations(remove) unless remove.empty?
    ProjectAuthorization.insert_authorizations(add) unless add.empty?
  end

  # Since we batch insert authorization rows, Rails' associations may get
  # out of sync. As such we force a reload of the User object.
  user.reset
end