Skip to content

Commit 5b624e9

Browse files
committed
enhance: add user ID based authorization
This change also changes the UI API calls to to always use user IDs instead of usernames. Signed-off-by: Donnie Adams <[email protected]>
1 parent de6aa70 commit 5b624e9

File tree

11 files changed

+64
-63
lines changed

11 files changed

+64
-63
lines changed

pkg/api/authz/authz.go

-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ var staticRules = map[string][]string{
6767
"/api/assistants",
6868
"GET /api/me",
6969
"DELETE /api/me",
70-
"PATCH /api/users/{id}",
7170
"POST /api/llm-proxy/",
7271
"POST /api/prompt",
7372
"GET /api/models",

pkg/api/authz/resources.go

+9
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ var apiResources = []string{
148148
"GET /api/tool-references",
149149
"GET /api/tool-references/{id}",
150150
"GET /{ui}/projects/{id}",
151+
"GET /api/users/{user_id}",
152+
"PATCH /api/users/{user_id}",
153+
"GET /api/users/{user_id}/activities",
154+
"GET /api/users/{user_id}/usage",
155+
"GET /api/users/{user_id}/total-usage",
151156
}
152157

153158
type Resources struct {
@@ -191,6 +196,10 @@ func (a *Authorizer) evaluateResources(req *http.Request, vars GetVar, user user
191196
ToolID: vars("tool_id"),
192197
}
193198

199+
if !a.checkUser(user, vars("user_id")) {
200+
return false, nil
201+
}
202+
194203
if ok, err := a.checkAssistant(req, &resources, user); !ok || err != nil {
195204
return false, err
196205
}

pkg/api/authz/user.go

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package authz
2+
3+
import (
4+
"slices"
5+
6+
"k8s.io/apiserver/pkg/authentication/user"
7+
)
8+
9+
func (a *Authorizer) checkUser(user user.Info, userID string) bool {
10+
return userID == "" ||
11+
userID == user.GetUID() ||
12+
slices.Contains(user.GetGroups(), AdminGroup)
13+
}

pkg/gateway/client/user.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,10 @@ func (c *Client) UserByID(ctx context.Context, id string) (*types.User, error) {
8080
return u, c.decryptUser(ctx, u)
8181
}
8282

83-
func (c *Client) DeleteUser(ctx context.Context, storageClient kclient.Client, username string) (*types.User, error) {
83+
func (c *Client) DeleteUser(ctx context.Context, storageClient kclient.Client, userID string) (*types.User, error) {
8484
existingUser := new(types.User)
8585
if err := c.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
86-
if err := tx.Where("hashed_username = ?", hash.String(username)).First(existingUser).Error; err != nil {
86+
if err := tx.Where("id = ?", userID).First(existingUser).Error; err != nil {
8787
return err
8888
}
8989

@@ -127,10 +127,10 @@ func (c *Client) DeleteUser(ctx context.Context, storageClient kclient.Client, u
127127
return existingUser, c.decryptUser(ctx, existingUser)
128128
}
129129

130-
func (c *Client) UpdateUser(ctx context.Context, actingUserIsAdmin bool, updatedUser *types.User, username string) (*types.User, error) {
130+
func (c *Client) UpdateUser(ctx context.Context, actingUserIsAdmin bool, updatedUser *types.User, userID string) (*types.User, error) {
131131
existingUser := new(types.User)
132132
return existingUser, c.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
133-
if err := tx.Where("hashed_username = ?", hash.String(username)).First(existingUser).Error; err != nil {
133+
if err := tx.Where("id = ?", userID).First(existingUser).Error; err != nil {
134134
return err
135135
}
136136

@@ -186,8 +186,8 @@ func (c *Client) UpdateUser(ctx context.Context, actingUserIsAdmin bool, updated
186186
})
187187
}
188188

189-
func (c *Client) UpdateUserInternalStatus(ctx context.Context, username string, internal bool) error {
190-
return c.db.WithContext(ctx).Model(new(types.User)).Where("hashed_username = ?", hash.String(username)).Update("internal", internal).Error
189+
func (c *Client) UpdateUserInternalStatus(ctx context.Context, userID string, internal bool) error {
190+
return c.db.WithContext(ctx).Model(new(types.User)).Where("id = ?", userID).Update("internal", internal).Error
191191
}
192192

193193
func (c *Client) UpdateProfileIconIfNeeded(ctx context.Context, user *types.User, authProviderName, authProviderNamespace, authProviderURL string) error {

pkg/gateway/server/router.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ func (s *Server) AddRoutes(mux *server.Server) {
2929
mux.HandleFunc("POST /api/logout-all", wrap(s.logoutAll))
3030
mux.HandleFunc("GET /api/users", wrap(s.getUsers))
3131
mux.HandleFunc("POST /api/encrypt-all-users", wrap(s.encryptAllUsersAndIdentities))
32-
mux.HandleFunc("GET /api/users/{username_or_id}", wrap(s.getUser))
32+
mux.HandleFunc("GET /api/users/{user_id}", wrap(s.getUser))
3333
mux.HandleFunc("GET /api/users/{user_id}/activities", wrap(s.activitiesByUser))
3434
mux.HandleFunc("GET /api/users/{user_id}/usage", wrap(s.usageForUser))
3535
mux.HandleFunc("GET /api/users/{user_id}/total-usage", wrap(s.totalUsageForUser))
36-
mux.HandleFunc("PATCH /api/users/{username}", wrap(s.updateUser))
37-
mux.HandleFunc("POST /api/users/{username}/internal", wrap(s.markUserInternal))
38-
mux.HandleFunc("POST /api/users/{username}/external", wrap(s.markUserExternal))
39-
mux.HandleFunc("DELETE /api/users/{username}", wrap(s.deleteUser))
36+
mux.HandleFunc("PATCH /api/users/{user_id}", wrap(s.updateUser))
37+
mux.HandleFunc("POST /api/users/{user_id}/internal", wrap(s.markUserInternal))
38+
mux.HandleFunc("POST /api/users/{user_id}/external", wrap(s.markUserExternal))
39+
mux.HandleFunc("DELETE /api/users/{user_id}", wrap(s.deleteUser))
4040
mux.HandleFunc("GET /api/active-users", wrap(s.activeUsers))
4141

4242
mux.HandleFunc("GET /api/token-usage", wrap(s.systemTokenUsageByUser))

pkg/gateway/server/user.go

+20-37
Original file line numberDiff line numberDiff line change
@@ -83,26 +83,16 @@ func (s *Server) encryptAllUsersAndIdentities(apiContext api.Context) error {
8383
}
8484

8585
func (s *Server) getUser(apiContext api.Context) error {
86-
var (
87-
getByID = apiContext.URL.Query().Get("by-id") == "true"
88-
usernameOrID = apiContext.PathValue("username_or_id")
89-
user *types.User
90-
)
86+
userID := apiContext.PathValue("user_id")
9187

92-
if usernameOrID == "" {
93-
return types2.NewErrHTTP(http.StatusBadRequest, "username path parameter is required")
94-
}
95-
96-
var err error
97-
if getByID {
98-
user, err = apiContext.GatewayClient.UserByID(apiContext.Context(), usernameOrID)
99-
} else {
100-
user, err = apiContext.GatewayClient.User(apiContext.Context(), usernameOrID)
88+
if userID == "" {
89+
return types2.NewErrHTTP(http.StatusBadRequest, "user_id path parameter is required")
10190
}
10291

92+
user, err := apiContext.GatewayClient.UserByID(apiContext.Context(), userID)
10393
if err != nil {
10494
if errors.Is(err, gorm.ErrRecordNotFound) {
105-
return types2.NewErrNotFound("user %s not found", usernameOrID)
95+
return types2.NewErrNotFound("user %s not found", userID)
10696
}
10797
return fmt.Errorf("failed to get user: %v", err)
10898
}
@@ -111,16 +101,9 @@ func (s *Server) getUser(apiContext api.Context) error {
111101
}
112102

113103
func (s *Server) updateUser(apiContext api.Context) error {
114-
requestingUsername := apiContext.User.GetName()
115-
actingUserIsAdmin := apiContext.UserIsAdmin()
116-
117-
username := apiContext.PathValue("username")
118-
if username == "" {
119-
return types2.NewErrHTTP(http.StatusBadRequest, "username path parameter is required")
120-
}
121-
122-
if !actingUserIsAdmin && requestingUsername != username {
123-
return types2.NewErrHTTP(http.StatusForbidden, "only admins can update other users")
104+
userID := apiContext.PathValue("user_id")
105+
if userID == "" {
106+
return types2.NewErrHTTP(http.StatusBadRequest, "user_id path parameter is required")
124107
}
125108

126109
user := new(types.User)
@@ -135,7 +118,7 @@ func (s *Server) updateUser(apiContext api.Context) error {
135118
}
136119

137120
status := http.StatusInternalServerError
138-
existingUser, err := apiContext.GatewayClient.UpdateUser(apiContext.Context(), actingUserIsAdmin, user, username)
121+
existingUser, err := apiContext.GatewayClient.UpdateUser(apiContext.Context(), apiContext.UserIsAdmin(), user, userID)
139122
if err != nil {
140123
if errors.Is(err, gorm.ErrRecordNotFound) {
141124
status = http.StatusNotFound
@@ -161,14 +144,14 @@ func (s *Server) markUserExternal(apiContext api.Context) error {
161144
}
162145

163146
func (s *Server) changeUserInternalStatus(apiContext api.Context, internal bool) error {
164-
username := apiContext.PathValue("username")
165-
if username == "" {
166-
return types2.NewErrHTTP(http.StatusBadRequest, "username path parameter is required")
147+
userID := apiContext.PathValue("user_id")
148+
if userID == "" {
149+
return types2.NewErrHTTP(http.StatusBadRequest, "user_id path parameter is required")
167150
}
168151

169-
if err := apiContext.GatewayClient.UpdateUserInternalStatus(apiContext.Context(), username, internal); err != nil {
152+
if err := apiContext.GatewayClient.UpdateUserInternalStatus(apiContext.Context(), userID, internal); err != nil {
170153
if errors.Is(err, gorm.ErrRecordNotFound) {
171-
return types2.NewErrNotFound("user %s not found", username)
154+
return types2.NewErrNotFound("user %s not found", userID)
172155
}
173156
return types2.NewErrHTTP(http.StatusInternalServerError, fmt.Sprintf("failed to update user: %v", err))
174157
}
@@ -177,10 +160,10 @@ func (s *Server) changeUserInternalStatus(apiContext api.Context, internal bool)
177160
}
178161

179162
func (s *Server) deleteUser(apiContext api.Context) (err error) {
180-
username := apiContext.PathValue("username")
181-
if username == "" {
163+
userID := apiContext.PathValue("user_id")
164+
if userID == "" {
182165
// This is the "delete me" API
183-
username = apiContext.User.GetName()
166+
userID = apiContext.User.GetUID()
184167
defer func() {
185168
if err == nil {
186169
// If everything was successful, remove the cookie so the user isn't authenticated again.
@@ -189,16 +172,16 @@ func (s *Server) deleteUser(apiContext api.Context) (err error) {
189172
}()
190173
}
191174

192-
existingUser, err := apiContext.GatewayClient.User(apiContext.Context(), username)
175+
existingUser, err := apiContext.GatewayClient.UserByID(apiContext.Context(), userID)
193176
if err != nil {
194177
if errors.Is(err, gorm.ErrRecordNotFound) {
195-
return types2.NewErrNotFound("user %s not found", username)
178+
return types2.NewErrNotFound("user %s not found", userID)
196179
}
197180
return fmt.Errorf("failed to get user: %v", err)
198181
}
199182

200183
status := http.StatusInternalServerError
201-
_, err = apiContext.GatewayClient.DeleteUser(apiContext.Context(), apiContext.Storage, username)
184+
_, err = apiContext.GatewayClient.DeleteUser(apiContext.Context(), apiContext.Storage, userID)
202185
if err != nil {
203186
if errors.Is(err, gorm.ErrRecordNotFound) {
204187
status = http.StatusNotFound

ui/admin/app/components/thread/ThreadMeta.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export function ThreadMeta({
107107
} = useThreadTasks(isAgent ? thread.id : undefined);
108108

109109
const { data: user } = useSWR(
110-
...UserService.getUser.swr({ username: thread.userID })
110+
...UserService.getUser.swr({ userId: thread.userID })
111111
);
112112

113113
const { dialogProps, interceptAsync } = useConfirmationDialog();

ui/admin/app/components/user/UserActionsDropdown.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export function UserActionsDropdown({ user }: { user: User }) {
9494
<Button
9595
type="button"
9696
variant="destructive"
97-
onClick={() => deleteUser.execute(user.username)}
97+
onClick={() => deleteUser.execute(user.id)}
9898
>
9999
Delete
100100
</Button>

ui/admin/app/components/user/UserUpdateForm.tsx

+2-4
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export function UserUpdateForm({
9191
) {
9292
setBootstrapDialogOpen(true);
9393
} else {
94-
updateUser.execute(user.username, data);
94+
updateUser.execute(user.id, data);
9595
}
9696
});
9797

@@ -176,9 +176,7 @@ export function UserUpdateForm({
176176
<Button
177177
loading={updateUser.isLoading}
178178
disabled={updateUser.isLoading}
179-
onClick={() =>
180-
updateUser.execute(user.username, form.getValues())
181-
}
179+
onClick={() => updateUser.execute(user.id, form.getValues())}
182180
>
183181
Confirm
184182
</Button>

ui/admin/app/lib/routers/apiRoutes.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -228,10 +228,9 @@ export const ApiRoutes = {
228228
},
229229
users: {
230230
base: () => buildUrl("/users"),
231-
updateUser: (username: string) => buildUrl(`/users/${username}`),
232-
deleteUser: (username: string) => buildUrl(`/users/${username}`),
233-
getOne: (username: string) =>
234-
buildUrl(`/users/${username}`, { "by-id": true }),
231+
updateUser: (userId: string) => buildUrl(`/users/${userId}`),
232+
deleteUser: (userId: string) => buildUrl(`/users/${userId}`),
233+
getOne: (userId: string) => buildUrl(`/users/${userId}`),
235234
},
236235
workspace: {
237236
getTables: (namespace: TableNamespace, entityId: string) =>

ui/admin/app/lib/service/api/userService.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,13 @@ async function updateUser(username: string, user: Partial<User>) {
4545
}
4646

4747
const handleGetUser = createFetcher(
48-
z.object({ username: z.string() }),
49-
async ({ username }, { signal }) => {
50-
const { url } = ApiRoutes.users.getOne(username);
48+
z.object({ userId: z.string() }),
49+
async ({ userId }, { signal }) => {
50+
const { url } = ApiRoutes.users.getOne(userId);
5151
const { data } = await request<User>({ url, signal });
5252
return data;
5353
},
54-
() => ApiRoutes.users.getOne(":username").path
54+
() => ApiRoutes.users.getOne(":userId").path
5555
);
5656

5757
async function deleteUser(username: string) {

0 commit comments

Comments
 (0)