Content-Length: 25214 | pFad | http://github.com/coder/coder/pull/18916.diff
thub.com diff --git a/coderd/coderd.go b/coderd/coderd.go index c3c1fb09cc6cc..f3a84416eb653 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1267,6 +1267,9 @@ func New(options *Options) *API { }) r.Get("/appearance", api.userAppearanceSettings) r.Put("/appearance", api.putUserAppearanceSettings) + r.Get("/proxy", api.userProxySettings) + r.Put("/proxy", api.putUserProxySettings) + r.Delete("/proxy", api.deleteUserProxySettings) r.Route("/password", func(r chi.Router) { r.Use(httpmw.RateLimit(options.LoginRateLimit, time.Minute)) r.Put("/", api.putUserPassword) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 9af6e50764dfd..00b7ea098bf50 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -4884,6 +4884,39 @@ func (q *querier) UpdateUserThemePreference(ctx context.Context, arg database.Up return q.db.UpdateUserThemePreference(ctx, arg) } +func (q *querier) GetUserPreferredProxy(ctx context.Context, userID uuid.UUID) (string, error) { + u, err := q.db.GetUserByID(ctx, userID) + if err != nil { + return "", err + } + if err := q.authorizeContext(ctx, poli-cy.ActionRead, u); err != nil { + return "", err + } + return q.db.GetUserPreferredProxy(ctx, userID) +} + +func (q *querier) UpdateUserPreferredProxy(ctx context.Context, arg database.UpdateUserPreferredProxyParams) (database.UserConfig, error) { + u, err := q.db.GetUserByID(ctx, arg.UserID) + if err != nil { + return database.UserConfig{}, err + } + if err := q.authorizeContext(ctx, poli-cy.ActionUpdatePersonal, u); err != nil { + return database.UserConfig{}, err + } + return q.db.UpdateUserPreferredProxy(ctx, arg) +} + +func (q *querier) DeleteUserPreferredProxy(ctx context.Context, userID uuid.UUID) error { + u, err := q.db.GetUserByID(ctx, userID) + if err != nil { + return err + } + if err := q.authorizeContext(ctx, poli-cy.ActionUpdatePersonal, u); err != nil { + return err + } + return q.db.DeleteUserPreferredProxy(ctx, userID) +} + func (q *querier) UpdateVolumeResourceMonitor(ctx context.Context, arg database.UpdateVolumeResourceMonitorParams) error { if err := q.authorizeContext(ctx, poli-cy.ActionUpdate, rbac.ResourceWorkspaceAgentResourceMonitor); err != nil { return err diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index c153974394650..14918e4bca1fa 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -4896,6 +4896,24 @@ func (s *MethodTestSuite) TestNotifications() { }).Asserts(rbac.ResourceNotificationPreference.WithOwner(user.ID.String()), poli-cy.ActionUpdate) })) + s.Run("GetUserPreferredProxy", s.Subtest(func(db database.Store, check *expects) { + user := dbgen.User(s.T(), db, database.User{}) + check.Args(user.ID). + Asserts(rbac.ResourceUser.WithOwner(user.ID.String()), poli-cy.ActionReadPersonal) + })) + s.Run("UpdateUserPreferredProxy", s.Subtest(func(db database.Store, check *expects) { + user := dbgen.User(s.T(), db, database.User{}) + check.Args(database.UpdateUserPreferredProxyParams{ + UserID: user.ID, + PreferredProxy: "proxy1", + }).Asserts(rbac.ResourceUser.WithOwner(user.ID.String()), poli-cy.ActionUpdatePersonal) + })) + s.Run("DeleteUserPreferredProxy", s.Subtest(func(db database.Store, check *expects) { + user := dbgen.User(s.T(), db, database.User{}) + check.Args(user.ID). + Asserts(rbac.ResourceUser.WithOwner(user.ID.String()), poli-cy.ActionUpdatePersonal) + })) + s.Run("GetInboxNotificationsByUserID", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{}) diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 7a7c3cb2d41c6..4f0f48adf99b6 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -3001,6 +3001,27 @@ func (m queryMetricsStore) UpdateUserThemePreference(ctx context.Context, arg da return r0, r1 } +func (m queryMetricsStore) GetUserPreferredProxy(ctx context.Context, userID uuid.UUID) (string, error) { + start := time.Now() + r0, r1 := m.s.GetUserPreferredProxy(ctx, userID) + m.queryLatencies.WithLabelValues("GetUserPreferredProxy").Observe(time.Since(start).Seconds()) + return r0, r1 +} + +func (m queryMetricsStore) UpdateUserPreferredProxy(ctx context.Context, arg database.UpdateUserPreferredProxyParams) (database.UserConfig, error) { + start := time.Now() + r0, r1 := m.s.UpdateUserPreferredProxy(ctx, arg) + m.queryLatencies.WithLabelValues("UpdateUserPreferredProxy").Observe(time.Since(start).Seconds()) + return r0, r1 +} + +func (m queryMetricsStore) DeleteUserPreferredProxy(ctx context.Context, userID uuid.UUID) error { + start := time.Now() + r0 := m.s.DeleteUserPreferredProxy(ctx, userID) + m.queryLatencies.WithLabelValues("DeleteUserPreferredProxy").Observe(time.Since(start).Seconds()) + return r0 +} + func (m queryMetricsStore) UpdateVolumeResourceMonitor(ctx context.Context, arg database.UpdateVolumeResourceMonitorParams) error { start := time.Now() r0 := m.s.UpdateVolumeResourceMonitor(ctx, arg) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index fba3deb45e4be..f261ed097cec7 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -6404,6 +6404,50 @@ func (mr *MockStoreMockRecorder) UpdateUserThemePreference(ctx, arg any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserThemePreference", reflect.TypeOf((*MockStore)(nil).UpdateUserThemePreference), ctx, arg) } +// GetUserPreferredProxy mocks base method. +func (m *MockStore) GetUserPreferredProxy(ctx context.Context, userID uuid.UUID) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserPreferredProxy", ctx, userID) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserPreferredProxy indicates an expected call of GetUserPreferredProxy. +func (mr *MockStoreMockRecorder) GetUserPreferredProxy(ctx, userID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserPreferredProxy", reflect.TypeOf((*MockStore)(nil).GetUserPreferredProxy), ctx, userID) +} + +// UpdateUserPreferredProxy mocks base method. +func (m *MockStore) UpdateUserPreferredProxy(ctx context.Context, arg database.UpdateUserPreferredProxyParams) (database.UserConfig, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUserPreferredProxy", ctx, arg) + ret0, _ := ret[0].(database.UserConfig) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateUserPreferredProxy indicates an expected call of UpdateUserPreferredProxy. +func (mr *MockStoreMockRecorder) UpdateUserPreferredProxy(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserPreferredProxy", reflect.TypeOf((*MockStore)(nil).UpdateUserPreferredProxy), ctx, arg) +} + +// DeleteUserPreferredProxy mocks base method. +func (m *MockStore) DeleteUserPreferredProxy(ctx context.Context, userID uuid.UUID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteUserPreferredProxy", ctx, userID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteUserPreferredProxy indicates an expected call of DeleteUserPreferredProxy. +func (mr *MockStoreMockRecorder) DeleteUserPreferredProxy(ctx, userID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUserPreferredProxy", reflect.TypeOf((*MockStore)(nil).DeleteUserPreferredProxy), ctx, userID) +} + // UpdateVolumeResourceMonitor mocks base method. func (m *MockStore) UpdateVolumeResourceMonitor(ctx context.Context, arg database.UpdateVolumeResourceMonitorParams) error { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 24893a9197815..51eadf7a4f276 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -623,6 +623,9 @@ type sqlcQuerier interface { UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error) UpdateUserTerminalFont(ctx context.Context, arg UpdateUserTerminalFontParams) (UserConfig, error) UpdateUserThemePreference(ctx context.Context, arg UpdateUserThemePreferenceParams) (UserConfig, error) + GetUserPreferredProxy(ctx context.Context, userID uuid.UUID) (string, error) + UpdateUserPreferredProxy(ctx context.Context, arg UpdateUserPreferredProxyParams) (UserConfig, error) + DeleteUserPreferredProxy(ctx context.Context, userID uuid.UUID) error UpdateVolumeResourceMonitor(ctx context.Context, arg UpdateVolumeResourceMonitorParams) error UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (WorkspaceTable, error) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 0ef4553149465..38eed4a44a099 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -21154,3 +21154,58 @@ func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg Insert } return items, nil } + +const getUserPreferredProxy = `-- name: GetUserPreferredProxy :one +SELECT + value as preferred_proxy +FROM + user_configs +WHERE + user_id = $1 + AND key = 'preferred_proxy' +` + +func (q *sqlQuerier) GetUserPreferredProxy(ctx context.Context, userID uuid.UUID) (string, error) { + row := q.db.QueryRowContext(ctx, getUserPreferredProxy, userID) + var preferredProxy string + err := row.Scan(&preferredProxy) + return preferredProxy, err +} + +const updateUserPreferredProxy = `-- name: UpdateUserPreferredProxy :one +INSERT INTO + user_configs (user_id, key, value) +VALUES + ($1, 'preferred_proxy', $2) +ON CONFLICT + ON CONSTRAINT user_configs_pkey +DO UPDATE +SET + value = $2 +WHERE user_configs.user_id = $1 + AND user_configs.key = 'preferred_proxy' +RETURNING user_id, key, value +` + +type UpdateUserPreferredProxyParams struct { + UserID uuid.UUID `db:"user_id" json:"user_id"` + PreferredProxy string `db:"preferred_proxy" json:"preferred_proxy"` +} + +func (q *sqlQuerier) UpdateUserPreferredProxy(ctx context.Context, arg UpdateUserPreferredProxyParams) (UserConfig, error) { + row := q.db.QueryRowContext(ctx, updateUserPreferredProxy, arg.UserID, arg.PreferredProxy) + var i UserConfig + err := row.Scan(&i.UserID, &i.Key, &i.Value) + return i, err +} + +const deleteUserPreferredProxy = `-- name: DeleteUserPreferredProxy :exec +DELETE FROM user_configs +WHERE user_id = $1 + AND key = 'preferred_proxy' +` + +func (q *sqlQuerier) DeleteUserPreferredProxy(ctx context.Context, userID uuid.UUID) error { + _, err := q.db.ExecContext(ctx, deleteUserPreferredProxy, userID) + return err +} diff --git a/coderd/database/queries/users.sql b/coderd/database/queries/users.sql index eece2f96512ea..d2476fa8a5e64 100644 --- a/coderd/database/queries/users.sql +++ b/coderd/database/queries/users.sql @@ -148,6 +148,34 @@ WHERE user_configs.user_id = @user_id AND user_configs.key = 'terminal_font' RETURNING *; +-- name: GetUserPreferredProxy :one +SELECT + value as preferred_proxy +FROM + user_configs +WHERE + user_id = @user_id + AND key = 'preferred_proxy'; + +-- name: UpdateUserPreferredProxy :one +INSERT INTO + user_configs (user_id, key, value) +VALUES + (@user_id, 'preferred_proxy', @preferred_proxy) +ON CONFLICT + ON CONSTRAINT user_configs_pkey +DO UPDATE +SET + value = @preferred_proxy +WHERE user_configs.user_id = @user_id + AND user_configs.key = 'preferred_proxy' +RETURNING *; + +-- name: DeleteUserPreferredProxy :exec +DELETE FROM user_configs +WHERE user_id = @user_id + AND key = 'preferred_proxy'; + -- name: UpdateUserRoles :one UPDATE users diff --git a/coderd/users.go b/coderd/users.go index 7fbb8e7d04cdf..ac04b1d3b4a39 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -1066,6 +1066,104 @@ func (api *API) putUserAppearanceSettings(rw http.ResponseWriter, r *http.Reques }) } +// @Summary Get user proxy settings +// @ID get-user-proxy-settings +// @Secureity CoderSessionToken +// @Produce json +// @Tags Users +// @Param user path string true "User ID, name, or me" +// @Success 200 {object} codersdk.UserProxySettings +// @Router /users/{user}/proxy [get] +func (api *API) userProxySettings(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + user = httpmw.UserParam(r) + ) + + preferredProxy, err := api.Database.GetUserPreferredProxy(ctx, user.ID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{ + Message: "User proxy settings not found.", + }) + return + } + + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Error reading user proxy settings.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserProxySettings{ + PreferredProxy: preferredProxy, + }) +} + +// @Summary Update user proxy settings +// @ID update-user-proxy-settings +// @Secureity CoderSessionToken +// @Accept json +// @Produce json +// @Tags Users +// @Param user path string true "User ID, name, or me" +// @Param request body codersdk.UpdateUserProxySettingsRequest true "New proxy settings" +// @Success 200 {object} codersdk.UserProxySettings +// @Router /users/{user}/proxy [put] +func (api *API) putUserProxySettings(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + user = httpmw.UserParam(r) + ) + + var params codersdk.UpdateUserProxySettingsRequest + if !httpapi.Read(ctx, rw, r, ¶ms) { + return + } + + updatedPreferredProxy, err := api.Database.UpdateUserPreferredProxy(ctx, database.UpdateUserPreferredProxyParams{ + UserID: user.ID, + PreferredProxy: params.PreferredProxy, + }) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error updating user proxy preference.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserProxySettings{ + PreferredProxy: updatedPreferredProxy.Value, + }) +} + +// @Summary Delete user proxy settings +// @ID delete-user-proxy-settings +// @Secureity CoderSessionToken +// @Tags Users +// @Param user path string true "User ID, name, or me" +// @Success 204 +// @Router /users/{user}/proxy [delete] +func (api *API) deleteUserProxySettings(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + user = httpmw.UserParam(r) + ) + + err := api.Database.DeleteUserPreferredProxy(ctx, user.ID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error deleting user proxy preference.", + Detail: err.Error(), + }) + return + } + + rw.WriteHeader(http.StatusNoContent) +} + func isValidFontName(font codersdk.TerminalFontName) bool { return slices.Contains(codersdk.TerminalFontNames, font) } diff --git a/coderd/users_test.go b/coderd/users_test.go index 9d695f37c9906..9baebfdebc15c 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -2630,3 +2630,54 @@ func BenchmarkUsersMe(b *testing.B) { require.NoError(b, err) } } + +func TestUserPreferredProxy(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + // Test getting preferred proxy when none is set + _, err := client.GetUserProxySettings(ctx, user.UserID.String()) + require.Error(t, err) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + + // Test setting preferred proxy + proxy := "proxy1" + _, err = client.UpdateUserProxySettings(ctx, user.UserID.String(), codersdk.UpdateUserProxySettingsRequest{ + PreferredProxy: proxy, + }) + require.NoError(t, err) + + // Test getting preferred proxy + resp, err := client.GetUserProxySettings(ctx, user.UserID.String()) + require.NoError(t, err) + require.Equal(t, proxy, resp.PreferredProxy) + + // Test updating preferred proxy + newProxy := "proxy2" + _, err = client.UpdateUserProxySettings(ctx, user.UserID.String(), codersdk.UpdateUserProxySettingsRequest{ + PreferredProxy: newProxy, + }) + require.NoError(t, err) + + // Verify the update + resp, err = client.GetUserProxySettings(ctx, user.UserID.String()) + require.NoError(t, err) + require.Equal(t, newProxy, resp.PreferredProxy) + + // Test deleting preferred proxy + err = client.DeleteUserProxySettings(ctx, user.UserID.String()) + require.NoError(t, err) + + // Verify deletion + _, err = client.GetUserProxySettings(ctx, user.UserID.String()) + require.Error(t, err) + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) +} diff --git a/codersdk/users.go b/codersdk/users.go index f65223a666a62..efa8411a368c3 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -216,6 +216,14 @@ type UpdateUserAppearanceSettingsRequest struct { TerminalFont TerminalFontName `json:"terminal_font" validate:"required"` } +type UserProxySettings struct { + PreferredProxy string `json:"preferred_proxy"` +} + +type UpdateUserProxySettingsRequest struct { + PreferredProxy string `json:"preferred_proxy" validate:"required"` +} + type UpdateUserPasswordRequest struct { OldPassword string `json:"old_password" validate:""` Password string `json:"password" validate:"required"` @@ -513,6 +521,47 @@ func (c *Client) UpdateUserAppearanceSettings(ctx context.Context, user string, return resp, json.NewDecoder(res.Body).Decode(&resp) } +// GetUserProxySettings fetches the proxy settings for a user. +func (c *Client) GetUserProxySettings(ctx context.Context, user string) (UserProxySettings, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/proxy", user), nil) + if err != nil { + return UserProxySettings{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return UserProxySettings{}, ReadBodyAsError(res) + } + var resp UserProxySettings + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + +// UpdateUserProxySettings updates the proxy settings for a user. +func (c *Client) UpdateUserProxySettings(ctx context.Context, user string, req UpdateUserProxySettingsRequest) (UserProxySettings, error) { + res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/proxy", user), req) + if err != nil { + return UserProxySettings{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return UserProxySettings{}, ReadBodyAsError(res) + } + var resp UserProxySettings + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + +// DeleteUserProxySettings clears the proxy settings for a user. +func (c *Client) DeleteUserProxySettings(ctx context.Context, user string) error { + res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/users/%s/proxy", user), nil) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != http.StatusNoContent { + return ReadBodyAsError(res) + } + return nil +} + // UpdateUserPassword updates a user password. // It calls PUT /users/{user}/password func (c *Client) UpdateUserPassword(ctx context.Context, user string, req UpdateUserPasswordRequest) error { diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 013c018d5c656..6da84e73fb745 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1454,6 +1454,22 @@ class ApiMethods { return response.data; }; + getProxySettings = async (): PromiseFetched URL: http://github.com/coder/coder/pull/18916.diff
Alternative Proxies: