Skip to content

Commit 1e1a90c

Browse files
authored
fix(identity): add API fallback for user search with cache bypass option (#1844)
1 parent ce91f2c commit 1e1a90c

File tree

4 files changed

+144
-67
lines changed

4 files changed

+144
-67
lines changed

app/controllers/cache_controller.rb

Lines changed: 133 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -227,55 +227,137 @@ def domain_projects
227227
end
228228

229229
def users
230-
items =
231-
ObjectCache.find_objects(
230+
# Skip cache for user lookups to prevent reassigning roles to outdated/deleted users.
231+
# The ObjectCache may contain stale user records for users that have been deleted
232+
# or deactivated in the identity service. By fetching directly from the API, we ensure
233+
# only currently active and valid users can be found and assigned roles.
234+
skip_cache = !!params[:nocache]
235+
term = params[:term]
236+
domain_id = params[:domain]
237+
238+
239+
# Try cache first unless skip_cache is true
240+
items = unless skip_cache
241+
cached_users = ObjectCache.find_objects(
232242
type: 'user',
233-
term: params[:name] || params[:term] || '',
243+
term: params[:name] || term || '',
234244
include_scope: false,
235245
paginate: false
236246
) do |scope|
237-
scope.where(domain_id: params[:domain]).order(:name)
247+
scope.where(domain_id: domain_id).order(:name)
238248
end
239-
240-
unless items.nil? || items.empty?
241-
items = items.to_a.map do |u|
242-
{
243-
id: u.payload['description'],
244-
name: u.name,
245-
key: u.name,
246-
uid: u.id,
247-
full_name: u.payload['description'],
248-
email: u.payload['email']
249-
}
250-
end
251-
else
252-
# search live against API and then retry
253-
filter = {domain_id: params[:domain]}
254-
if params[:term]
255-
filter[:name__contains] = params[:term]
249+
250+
# Map cached results if found
251+
if cached_users.present?
252+
cached_users.to_a.map do |u|
253+
{
254+
id: u.payload['description'],
255+
name: u.name,
256+
key: u.name,
257+
uid: u.id,
258+
full_name: u.payload['description'],
259+
email: u.payload['email']
260+
}
261+
end
256262
end
257-
items = service_user.identity.users(filter, format: :raw)
258-
259-
if params[:term]
260-
# find by id, for the case the term is an id instead of name
261-
user = service_user.identity.find_user(params[:term], format: :raw)
262-
items << user if user
263+
end
264+
265+
# If cache didn't return results, fetch from API
266+
if items.blank?
267+
# Try by ID first if term is provided
268+
if term.present?
269+
user = service_user.identity.find_user(term, format: :raw)
270+
if user
271+
items = [format_user(user)]
272+
render json: items
273+
return
274+
end
263275
end
264-
265-
items = items.map do |u|
266-
{
267-
id: u['description'],
268-
name: u['name'],
269-
key: u['name'],
270-
uid: u['id'],
271-
full_name: u['description'],
272-
email: u['email']
273-
}
276+
277+
# Build filter for users query
278+
filter = { domain_id: domain_id }
279+
280+
# If term provided, search by both name and description
281+
all_users = if term.present?
282+
# Get users matching name
283+
items_by_name = service_user.identity.users(
284+
filter.merge(name__contains: term),
285+
format: :raw
286+
)
287+
288+
# Get users matching description
289+
items_by_description = service_user.identity.users(
290+
filter.merge(description__contains: term),
291+
format: :raw
292+
)
293+
294+
# Combine and deduplicate by id
295+
(items_by_name + items_by_description).uniq { |u| u['id'] }
296+
else
297+
# Get all users in domain
298+
service_user.identity.users(filter, format: :raw)
274299
end
275-
end
300+
301+
# Format results
302+
items = all_users.map { |u| format_user(u) }
303+
end
304+
276305
render json: items
277306
end
278307

308+
309+
310+
311+
# def users
312+
# items =
313+
# ObjectCache.find_objects(
314+
# type: 'user',
315+
# term: params[:name] || params[:term] || '',
316+
# include_scope: false,
317+
# paginate: false
318+
# ) do |scope|
319+
# scope.where(domain_id: params[:domain]).order(:name)
320+
# end
321+
322+
# unless items.nil? || items.empty?
323+
# items = items.to_a.map do |u|
324+
# {
325+
# id: u.payload['description'],
326+
# name: u.name,
327+
# key: u.name,
328+
# uid: u.id,
329+
# full_name: u.payload['description'],
330+
# email: u.payload['email']
331+
# }
332+
# end
333+
# else
334+
# # search live against API and then retry
335+
# filter = {domain_id: params[:domain]}
336+
# if params[:term]
337+
# filter[:name__contains] = params[:term]
338+
# end
339+
# items = service_user.identity.users(filter, format: :raw)
340+
341+
# if params[:term]
342+
# # find by id, for the case the term is an id instead of name
343+
# user = service_user.identity.find_user(params[:term], format: :raw)
344+
# items << user if user
345+
# end
346+
347+
# items = items.map do |u|
348+
# {
349+
# id: u['description'],
350+
# name: u['name'],
351+
# key: u['name'],
352+
# uid: u['id'],
353+
# full_name: u['description'],
354+
# email: u['email']
355+
# }
356+
# end
357+
# end
358+
# render json: items
359+
# end
360+
279361
def groups
280362
items =
281363
ObjectCache.find_objects(
@@ -420,4 +502,17 @@ def where_current_token_scope(scope, enforce_scope = @enforce_scope)
420502
scope.where('domain_id IS NULL OR project_id IS NULL')
421503
end
422504
end
505+
506+
private
507+
508+
def format_user(user)
509+
{
510+
id: user['description'],
511+
name: user['name'],
512+
key: user['name'],
513+
uid: user['id'],
514+
full_name: user['description'],
515+
email: user['email']
516+
}
517+
end
423518
end

app/javascript/lib/components/autocomplete_field.jsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,18 @@ export class AutocompleteField extends React.Component {
3737
// the identity to search for groups or users (ONLY FOR groups or users)
3838
handleSearch = (searchTerm) => {
3939
let path
40+
let skipCache = false
4041
switch (this.props.type) {
4142
case "projects":
4243
path = "projects"
4344
break
4445
case "users":
4546
path = "users"
47+
// Always bypass cache for user lookups (nocache=true).
48+
// This prevents outdated or deleted users from appearing in search results
49+
// and being reassigned to roles/projects. Only active users from the
50+
// Identity API should be selectable.
51+
skipCache = true
4652
break
4753
case "groups":
4854
path = "groups"
@@ -51,6 +57,7 @@ export class AutocompleteField extends React.Component {
5157

5258
const params = { term: searchTerm }
5359
if (this.props.domainId) params["domain"] = this.props.domainId
60+
if (skipCache) params["nocache"] = "true"
5461

5562
this.setState({ isLoading: true, options: [] })
5663
ajaxHelper

config/environments/development.rb

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,6 @@
11
require "active_support/core_ext/integer/time"
22

33
Rails.application.configure do
4-
5-
# Disable asset digests in development
6-
config.assets.digest = false
7-
8-
# Don't compile assets on every request
9-
config.assets.compile = true # This should already be default
10-
11-
# Prevent asset recompilation
12-
config.assets.check_precompiled_asset = false
13-
144
# Settings specified here will take precedence over those in config/application.rb.
155

166
# In the development environment your application's code is reloaded any time

plugins/identity/app/javascript/widgets/role_assignments/components/application.jsx

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,17 @@ import React from "react"
77
const RoleAssignments = ({ activeTab, projectId, domainId }) => {
88
// update browser address bar
99
const handleSelect = useCallback((tab) => {
10-
const newHref = window.location.href.replace(
11-
/^(.*active_tab=)(.*)$/,
12-
"$1" + tab
13-
)
10+
const newHref = window.location.href.replace(/^(.*active_tab=)(.*)$/, "$1" + tab)
1411
window.history.replaceState({}, "Tab " + tab, newHref)
1512
})
1613

1714
return (
18-
<Tabs
19-
defaultActiveKey={activeTab || "userRoles"}
20-
id="item_payload"
21-
onSelect={handleSelect}
22-
>
15+
<Tabs defaultActiveKey={activeTab || "userRoles"} id="item_payload" onSelect={handleSelect}>
2316
<Tab eventKey="userRoles" title="User Role Assignments">
24-
<ProjectRoleAssignments
25-
projectId={projectId}
26-
projectDomainId={domainId}
27-
type="user"
28-
/>
17+
<ProjectRoleAssignments projectId={projectId} projectDomainId={domainId} type="user" />
2918
</Tab>
3019
<Tab eventKey="groupRoles" title="Group Role Assignments">
31-
<ProjectRoleAssignments
32-
projectId={projectId}
33-
projectDomainId={domainId}
34-
type="group"
35-
/>
20+
<ProjectRoleAssignments projectId={projectId} projectDomainId={domainId} type="group" />
3621
</Tab>
3722
<Tab eventKey="roleInfos" title="Role Infos">
3823
<RoleInfos />

0 commit comments

Comments
 (0)