Skip to content

Commit 5d49724

Browse files
committed
feat: allow the admin user to impersonificate users
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 01f4543 commit 5d49724

13 files changed

Lines changed: 230 additions & 121 deletions

File tree

core/http/endpoints/localai/agent_collections.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func CreateCollectionEndpoint(app *application.Application) echo.HandlerFunc {
6969
func UploadToCollectionEndpoint(app *application.Application) echo.HandlerFunc {
7070
return func(c echo.Context) error {
7171
svc := app.AgentPoolService()
72-
userID := getUserID(c)
72+
userID := effectiveUserID(c)
7373
name := c.Param("name")
7474
file, err := c.FormFile("file")
7575
if err != nil {
@@ -96,7 +96,7 @@ func UploadToCollectionEndpoint(app *application.Application) echo.HandlerFunc {
9696
func ListCollectionEntriesEndpoint(app *application.Application) echo.HandlerFunc {
9797
return func(c echo.Context) error {
9898
svc := app.AgentPoolService()
99-
userID := getUserID(c)
99+
userID := effectiveUserID(c)
100100
entries, err := svc.ListCollectionEntriesForUser(userID, c.Param("name"))
101101
if err != nil {
102102
if strings.Contains(err.Error(), "not found") {
@@ -114,7 +114,7 @@ func ListCollectionEntriesEndpoint(app *application.Application) echo.HandlerFun
114114
func GetCollectionEntryContentEndpoint(app *application.Application) echo.HandlerFunc {
115115
return func(c echo.Context) error {
116116
svc := app.AgentPoolService()
117-
userID := getUserID(c)
117+
userID := effectiveUserID(c)
118118
entryParam := c.Param("*")
119119
entry, err := url.PathUnescape(entryParam)
120120
if err != nil {
@@ -137,7 +137,7 @@ func GetCollectionEntryContentEndpoint(app *application.Application) echo.Handle
137137
func SearchCollectionEndpoint(app *application.Application) echo.HandlerFunc {
138138
return func(c echo.Context) error {
139139
svc := app.AgentPoolService()
140-
userID := getUserID(c)
140+
userID := effectiveUserID(c)
141141
var payload struct {
142142
Query string `json:"query"`
143143
MaxResults int `json:"max_results"`
@@ -162,7 +162,7 @@ func SearchCollectionEndpoint(app *application.Application) echo.HandlerFunc {
162162
func ResetCollectionEndpoint(app *application.Application) echo.HandlerFunc {
163163
return func(c echo.Context) error {
164164
svc := app.AgentPoolService()
165-
userID := getUserID(c)
165+
userID := effectiveUserID(c)
166166
if err := svc.ResetCollectionForUser(userID, c.Param("name")); err != nil {
167167
if strings.Contains(err.Error(), "not found") {
168168
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
@@ -176,7 +176,7 @@ func ResetCollectionEndpoint(app *application.Application) echo.HandlerFunc {
176176
func DeleteCollectionEntryEndpoint(app *application.Application) echo.HandlerFunc {
177177
return func(c echo.Context) error {
178178
svc := app.AgentPoolService()
179-
userID := getUserID(c)
179+
userID := effectiveUserID(c)
180180
var payload struct {
181181
Entry string `json:"entry"`
182182
}
@@ -200,7 +200,7 @@ func DeleteCollectionEntryEndpoint(app *application.Application) echo.HandlerFun
200200
func AddCollectionSourceEndpoint(app *application.Application) echo.HandlerFunc {
201201
return func(c echo.Context) error {
202202
svc := app.AgentPoolService()
203-
userID := getUserID(c)
203+
userID := effectiveUserID(c)
204204
var payload struct {
205205
URL string `json:"url"`
206206
UpdateInterval int `json:"update_interval"`
@@ -224,7 +224,7 @@ func AddCollectionSourceEndpoint(app *application.Application) echo.HandlerFunc
224224
func RemoveCollectionSourceEndpoint(app *application.Application) echo.HandlerFunc {
225225
return func(c echo.Context) error {
226226
svc := app.AgentPoolService()
227-
userID := getUserID(c)
227+
userID := effectiveUserID(c)
228228
var payload struct {
229229
URL string `json:"url"`
230230
}
@@ -242,7 +242,7 @@ func RemoveCollectionSourceEndpoint(app *application.Application) echo.HandlerFu
242242
func GetCollectionEntryRawFileEndpoint(app *application.Application) echo.HandlerFunc {
243243
return func(c echo.Context) error {
244244
svc := app.AgentPoolService()
245-
userID := getUserID(c)
245+
userID := effectiveUserID(c)
246246
entryParam := c.Param("*")
247247
entry, err := url.PathUnescape(entryParam)
248248
if err != nil {
@@ -262,7 +262,7 @@ func GetCollectionEntryRawFileEndpoint(app *application.Application) echo.Handle
262262
func ListCollectionSourcesEndpoint(app *application.Application) echo.HandlerFunc {
263263
return func(c echo.Context) error {
264264
svc := app.AgentPoolService()
265-
userID := getUserID(c)
265+
userID := effectiveUserID(c)
266266
sources, err := svc.ListCollectionSourcesForUser(userID, c.Param("name"))
267267
if err != nil {
268268
if strings.Contains(err.Error(), "not found") {

core/http/endpoints/localai/agent_skills.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ func CreateSkillEndpoint(app *application.Application) echo.HandlerFunc {
132132
func GetSkillEndpoint(app *application.Application) echo.HandlerFunc {
133133
return func(c echo.Context) error {
134134
svc := app.AgentPoolService()
135-
userID := getUserID(c)
135+
userID := effectiveUserID(c)
136136
skill, err := svc.GetSkillForUser(userID, c.Param("name"))
137137
if err != nil {
138138
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
@@ -144,7 +144,7 @@ func GetSkillEndpoint(app *application.Application) echo.HandlerFunc {
144144
func UpdateSkillEndpoint(app *application.Application) echo.HandlerFunc {
145145
return func(c echo.Context) error {
146146
svc := app.AgentPoolService()
147-
userID := getUserID(c)
147+
userID := effectiveUserID(c)
148148
var payload struct {
149149
Description string `json:"description"`
150150
Content string `json:"content"`
@@ -170,7 +170,7 @@ func UpdateSkillEndpoint(app *application.Application) echo.HandlerFunc {
170170
func DeleteSkillEndpoint(app *application.Application) echo.HandlerFunc {
171171
return func(c echo.Context) error {
172172
svc := app.AgentPoolService()
173-
userID := getUserID(c)
173+
userID := effectiveUserID(c)
174174
if err := svc.DeleteSkillForUser(userID, c.Param("name")); err != nil {
175175
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
176176
}
@@ -181,7 +181,7 @@ func DeleteSkillEndpoint(app *application.Application) echo.HandlerFunc {
181181
func ExportSkillEndpoint(app *application.Application) echo.HandlerFunc {
182182
return func(c echo.Context) error {
183183
svc := app.AgentPoolService()
184-
userID := getUserID(c)
184+
userID := effectiveUserID(c)
185185
name := c.Param("*")
186186
data, err := svc.ExportSkillForUser(userID, name)
187187
if err != nil {
@@ -223,7 +223,7 @@ func ImportSkillEndpoint(app *application.Application) echo.HandlerFunc {
223223
func ListSkillResourcesEndpoint(app *application.Application) echo.HandlerFunc {
224224
return func(c echo.Context) error {
225225
svc := app.AgentPoolService()
226-
userID := getUserID(c)
226+
userID := effectiveUserID(c)
227227
resources, skill, err := svc.ListSkillResourcesForUser(userID, c.Param("name"))
228228
if err != nil {
229229
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
@@ -261,7 +261,7 @@ func ListSkillResourcesEndpoint(app *application.Application) echo.HandlerFunc {
261261
func GetSkillResourceEndpoint(app *application.Application) echo.HandlerFunc {
262262
return func(c echo.Context) error {
263263
svc := app.AgentPoolService()
264-
userID := getUserID(c)
264+
userID := effectiveUserID(c)
265265
content, info, err := svc.GetSkillResourceForUser(userID, c.Param("name"), c.Param("*"))
266266
if err != nil {
267267
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})

core/http/endpoints/localai/agents.go

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ func wantsAllUsers(c echo.Context) bool {
4141
return c.QueryParam("all_users") == "true" && isAdminUser(c)
4242
}
4343

44+
// effectiveUserID returns the user ID to scope operations to.
45+
// SECURITY: Only admins may supply ?user_id=<id> to operate on another user's
46+
// resources. Non-admin callers always get their own ID regardless of query params.
47+
func effectiveUserID(c echo.Context) string {
48+
if targetUID := c.QueryParam("user_id"); targetUID != "" && isAdminUser(c) {
49+
return targetUID
50+
}
51+
return getUserID(c)
52+
}
53+
4454
func ListAgentsEndpoint(app *application.Application) echo.HandlerFunc {
4555
return func(c echo.Context) error {
4656
svc := app.AgentPoolService()
@@ -99,7 +109,7 @@ func CreateAgentEndpoint(app *application.Application) echo.HandlerFunc {
99109
func GetAgentEndpoint(app *application.Application) echo.HandlerFunc {
100110
return func(c echo.Context) error {
101111
svc := app.AgentPoolService()
102-
userID := getUserID(c)
112+
userID := effectiveUserID(c)
103113
name := c.Param("name")
104114
ag := svc.GetAgentForUser(userID, name)
105115
if ag == nil {
@@ -114,7 +124,7 @@ func GetAgentEndpoint(app *application.Application) echo.HandlerFunc {
114124
func UpdateAgentEndpoint(app *application.Application) echo.HandlerFunc {
115125
return func(c echo.Context) error {
116126
svc := app.AgentPoolService()
117-
userID := getUserID(c)
127+
userID := effectiveUserID(c)
118128
name := c.Param("name")
119129
var cfg state.AgentConfig
120130
if err := c.Bind(&cfg); err != nil {
@@ -133,7 +143,7 @@ func UpdateAgentEndpoint(app *application.Application) echo.HandlerFunc {
133143
func DeleteAgentEndpoint(app *application.Application) echo.HandlerFunc {
134144
return func(c echo.Context) error {
135145
svc := app.AgentPoolService()
136-
userID := getUserID(c)
146+
userID := effectiveUserID(c)
137147
name := c.Param("name")
138148
if err := svc.DeleteAgentForUser(userID, name); err != nil {
139149
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -145,7 +155,7 @@ func DeleteAgentEndpoint(app *application.Application) echo.HandlerFunc {
145155
func GetAgentConfigEndpoint(app *application.Application) echo.HandlerFunc {
146156
return func(c echo.Context) error {
147157
svc := app.AgentPoolService()
148-
userID := getUserID(c)
158+
userID := effectiveUserID(c)
149159
name := c.Param("name")
150160
cfg := svc.GetAgentConfigForUser(userID, name)
151161
if cfg == nil {
@@ -158,7 +168,7 @@ func GetAgentConfigEndpoint(app *application.Application) echo.HandlerFunc {
158168
func PauseAgentEndpoint(app *application.Application) echo.HandlerFunc {
159169
return func(c echo.Context) error {
160170
svc := app.AgentPoolService()
161-
userID := getUserID(c)
171+
userID := effectiveUserID(c)
162172
if err := svc.PauseAgentForUser(userID, c.Param("name")); err != nil {
163173
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
164174
}
@@ -169,7 +179,7 @@ func PauseAgentEndpoint(app *application.Application) echo.HandlerFunc {
169179
func ResumeAgentEndpoint(app *application.Application) echo.HandlerFunc {
170180
return func(c echo.Context) error {
171181
svc := app.AgentPoolService()
172-
userID := getUserID(c)
182+
userID := effectiveUserID(c)
173183
if err := svc.ResumeAgentForUser(userID, c.Param("name")); err != nil {
174184
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
175185
}
@@ -180,7 +190,7 @@ func ResumeAgentEndpoint(app *application.Application) echo.HandlerFunc {
180190
func GetAgentStatusEndpoint(app *application.Application) echo.HandlerFunc {
181191
return func(c echo.Context) error {
182192
svc := app.AgentPoolService()
183-
userID := getUserID(c)
193+
userID := effectiveUserID(c)
184194
name := c.Param("name")
185195
history := svc.GetAgentStatusForUser(userID, name)
186196
if history == nil {
@@ -209,7 +219,7 @@ func GetAgentStatusEndpoint(app *application.Application) echo.HandlerFunc {
209219
func GetAgentObservablesEndpoint(app *application.Application) echo.HandlerFunc {
210220
return func(c echo.Context) error {
211221
svc := app.AgentPoolService()
212-
userID := getUserID(c)
222+
userID := effectiveUserID(c)
213223
name := c.Param("name")
214224
history, err := svc.GetAgentObservablesForUser(userID, name)
215225
if err != nil {
@@ -225,7 +235,7 @@ func GetAgentObservablesEndpoint(app *application.Application) echo.HandlerFunc
225235
func ClearAgentObservablesEndpoint(app *application.Application) echo.HandlerFunc {
226236
return func(c echo.Context) error {
227237
svc := app.AgentPoolService()
228-
userID := getUserID(c)
238+
userID := effectiveUserID(c)
229239
name := c.Param("name")
230240
if err := svc.ClearAgentObservablesForUser(userID, name); err != nil {
231241
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
@@ -237,7 +247,7 @@ func ClearAgentObservablesEndpoint(app *application.Application) echo.HandlerFun
237247
func ChatWithAgentEndpoint(app *application.Application) echo.HandlerFunc {
238248
return func(c echo.Context) error {
239249
svc := app.AgentPoolService()
240-
userID := getUserID(c)
250+
userID := effectiveUserID(c)
241251
name := c.Param("name")
242252
var payload struct {
243253
Message string `json:"message"`
@@ -266,7 +276,7 @@ func ChatWithAgentEndpoint(app *application.Application) echo.HandlerFunc {
266276
func AgentSSEEndpoint(app *application.Application) echo.HandlerFunc {
267277
return func(c echo.Context) error {
268278
svc := app.AgentPoolService()
269-
userID := getUserID(c)
279+
userID := effectiveUserID(c)
270280
name := c.Param("name")
271281
manager := svc.GetSSEManagerForUser(userID, name)
272282
if manager == nil {
@@ -294,7 +304,7 @@ func GetAgentConfigMetaEndpoint(app *application.Application) echo.HandlerFunc {
294304
func ExportAgentEndpoint(app *application.Application) echo.HandlerFunc {
295305
return func(c echo.Context) error {
296306
svc := app.AgentPoolService()
297-
userID := getUserID(c)
307+
userID := effectiveUserID(c)
298308
name := c.Param("name")
299309
data, err := svc.ExportAgentForUser(userID, name)
300310
if err != nil {

core/http/react-ui/src/pages/AgentChat.jsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState, useEffect, useRef, useCallback, useMemo } from 'react'
2-
import { useParams, useNavigate, useOutletContext } from 'react-router-dom'
2+
import { useParams, useNavigate, useOutletContext, useSearchParams } from 'react-router-dom'
33
import { agentsApi } from '../utils/api'
44
import { apiUrl } from '../utils/basePath'
55
import { renderMarkdown, highlightAll } from '../utils/markdown'
@@ -86,6 +86,8 @@ export default function AgentChat() {
8686
const { name } = useParams()
8787
const navigate = useNavigate()
8888
const { addToast } = useOutletContext()
89+
const [searchParams] = useSearchParams()
90+
const userId = searchParams.get('user_id') || undefined
8991

9092
const {
9193
conversations, activeConversation, activeId,
@@ -126,7 +128,7 @@ export default function AgentChat() {
126128

127129
// Connect to SSE endpoint — only reconnect when agent name changes
128130
useEffect(() => {
129-
const url = apiUrl(`/api/agents/${encodeURIComponent(name)}/sse`)
131+
const url = apiUrl(agentsApi.sseUrl(name, userId))
130132
const es = new EventSource(url)
131133
eventSourceRef.current = es
132134

@@ -223,7 +225,7 @@ export default function AgentChat() {
223225
es.close()
224226
eventSourceRef.current = null
225227
}
226-
}, [name, addToast, nextId])
228+
}, [name, userId, addToast, nextId])
227229

228230
// Auto-scroll to bottom
229231
useEffect(() => {
@@ -305,12 +307,12 @@ export default function AgentChat() {
305307
if (textareaRef.current) textareaRef.current.style.height = 'auto'
306308
setProcessingChatId(activeId)
307309
try {
308-
await agentsApi.chat(name, msg)
310+
await agentsApi.chat(name, msg, userId)
309311
} catch (err) {
310312
addToast(`Failed to send message: ${err.message}`, 'error')
311313
setProcessingChatId(null)
312314
}
313-
}, [input, processing, name, activeId, addToast])
315+
}, [input, processing, name, activeId, addToast, userId])
314316

315317
const handleKeyDown = (e) => {
316318
if (e.key === 'Enter' && !e.shiftKey) {
@@ -493,7 +495,7 @@ export default function AgentChat() {
493495
<i className="fas fa-layer-group" /> {artifacts.length}
494496
</button>
495497
)}
496-
<button className="btn btn-secondary btn-sm" onClick={() => navigate(`/app/agents/${encodeURIComponent(name)}/status`)} title="View status & observables">
498+
<button className="btn btn-secondary btn-sm" onClick={() => navigate(`/app/agents/${encodeURIComponent(name)}/status${userId ? `?user_id=${encodeURIComponent(userId)}` : ''}`)} title="View status & observables">
497499
<i className="fas fa-chart-bar" /> Status
498500
</button>
499501
<button className="btn btn-secondary btn-sm" onClick={() => clearMessages()} disabled={messages.length === 0} title="Clear chat history">

core/http/react-ui/src/pages/AgentCreate.jsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState, useEffect, useMemo } from 'react'
2-
import { useParams, useNavigate, useLocation, useOutletContext } from 'react-router-dom'
2+
import { useParams, useNavigate, useLocation, useOutletContext, useSearchParams } from 'react-router-dom'
33
import { agentsApi } from '../utils/api'
44
import SearchableModelSelect from '../components/SearchableModelSelect'
55
import Toggle from '../components/Toggle'
@@ -269,6 +269,8 @@ export default function AgentCreate() {
269269
const navigate = useNavigate()
270270
const location = useLocation()
271271
const { addToast } = useOutletContext()
272+
const [searchParams] = useSearchParams()
273+
const userId = searchParams.get('user_id') || undefined
272274
const isEdit = !!name
273275
const importedConfig = location.state?.importedConfig || null
274276

@@ -308,7 +310,7 @@ export default function AgentCreate() {
308310
try {
309311
const [metaData, config] = await Promise.all([
310312
agentsApi.configMeta().catch(() => null),
311-
isEdit ? agentsApi.getConfig(name).catch(() => null) : Promise.resolve(null),
313+
isEdit ? agentsApi.getConfig(name, userId).catch(() => null) : Promise.resolve(null),
312314
])
313315
if (metaData) setMeta(metaData)
314316

@@ -384,7 +386,7 @@ export default function AgentCreate() {
384386
}
385387

386388
if (isEdit) {
387-
await agentsApi.update(name, payload)
389+
await agentsApi.update(name, payload, userId)
388390
addToast(`Agent "${form.name}" updated`, 'success')
389391
} else {
390392
await agentsApi.create(payload)

0 commit comments

Comments
 (0)