fix: gate MCP user tokens on owner-scoped personal RBAC (#27945)

Re-gates every `mcp_server_user_tokens` dbauthz method from
`rbac.ResourceDeploymentConfig` onto owner-scoped personal actions,
mirroring the `external_auth_links` prior art. These are per-user
credential rows, not deployment config, and the old gate forced every
call site to elevate with `dbauthz.AsSystemRestricted`.

## Summary

- Reads gate on `ActionReadPersonal` and writes on
`ActionUpdatePersonal` against the token owner
(`MCPServerUserToken.RBACObject()` derives from `user_id`). The id-keyed
refresh and refresh-failure mutations resolve the owner via a
`GetMCPServerUserTokenByID` prefetch inside dbauthz; the mutation
queries cannot retarget a row's owner, so there is no fetch-authorize
race.
- All seven token-path `AsSystemRestricted` elevations in
`coderd/mcp.go` and all four in chatd are removed along with their
`//nolint:gocritic` markers; handlers use the request actor.
- The daemon-wide chatd subject keeps only `ActionReadPersonal` on
`ResourceUser`. Background token refresh and failure persistence use a
new per-user `AsChatdTokenOwner` subject holding only user-level
`ActionUpdatePersonal` scoped to the token owner (both mutations resolve
the row via the underlying store, so the subject needs no read grant),
and chatd never holds site-wide personal-write access. `TestAsChatd`
pins the complete `ResourceUser` action set and
`TestMCPServerUserTokensAuth` exercises both subjects against the real
authorizer.
- New strict-authorizer test (`rbac.NewAuthorizer` against real rows):
owner passes get/upsert/delete, a stranger fails, and an org admin of
another org fails.

No user-visible behavior change. Stacked on #27944. Completes the MCP
org-separation stack.

Closes https://linear.app/codercom/issue/CODAGT-806

UAT: regression pass on a dogfood instance: OAuth2 connect redirect,
member list/status with redaction, self-scoped disconnect (cannot touch
another user's token), end-to-end chat MCP tool invocation, and
lower-stack audit/ACL smoke checks.

> Mux (AI agent) authored this PR on Mike's behalf.

<!-- mux-attribution: model=claude-fable-5 thinking=high -->
This commit is contained in:
Michael Suchacz
2026-08-19 19:36:59 +00:00
committed by GitHub
parent 7ca7c30f40
commit a3eb1ee930
15 changed files with 717 additions and 86 deletions
+49 -23
View File
@@ -494,6 +494,29 @@ var (
}.WithCachedASTValue()
}
// MCP token refresh writes use this owner-scoped subject so the shared
// chatd context cannot update arbitrary users' personal data. Reads use
// the daemon-wide chatd subject.
subjectChatdTokenOwner = func(userID uuid.UUID) rbac.Subject {
return rbac.Subject{
Type: rbac.SubjectTypeChatdTokenOwner,
FriendlyName: "Chatd Token Owner",
ID: userID.String(),
Roles: rbac.Roles([]rbac.Role{
{
Identifier: rbac.RoleIdentifier{Name: "chatdtokenowner"},
DisplayName: "Chatd Token Owner",
Site: []rbac.Permission{},
User: rbac.Permissions(map[string][]policy.Action{
rbac.ResourceUser.Type: {policy.ActionUpdatePersonal},
}),
ByOrgID: map[string]rbac.OrgPermissions{},
},
}),
Scope: rbac.ScopeAll,
}.WithCachedASTValue()
}
subjectSystemRestricted = rbac.Subject{
Type: rbac.SubjectTypeSystemRestricted,
FriendlyName: "System",
@@ -795,7 +818,10 @@ var (
rbac.ResourceWorkspace.Type: {policy.ActionRead, policy.ActionUpdate},
rbac.ResourceDeploymentConfig.Type: {policy.ActionRead},
rbac.ResourceMCPServerConfig.Type: {policy.ActionRead},
rbac.ResourceUser.Type: {policy.ActionReadPersonal},
// Site-wide UpdatePersonal would let chatd write any
// user's personal data; token writes use the per-user
// AsChatdTokenOwner subject instead.
rbac.ResourceUser.Type: {policy.ActionReadPersonal},
}),
User: []rbac.Permission{},
ByOrgID: map[string]rbac.OrgPermissions{},
@@ -930,6 +956,12 @@ func AsChatdKeyMinter(ctx context.Context, userID uuid.UUID) context.Context {
return As(ctx, subjectChatdKeyMinter(userID))
}
// AsChatdTokenOwner returns a context with an actor that persists MCP
// OAuth2 token refresh results for the specified token owner only.
func AsChatdTokenOwner(ctx context.Context, userID uuid.UUID) context.Context {
return As(ctx, subjectChatdTokenOwner(userID))
}
// AsSystemRestricted returns a context with an actor that has permissions
// required for various system operations (login, logout, metrics cache).
// DO NOT USE THIS UNLESS YOU HAVE ABSOLUTELY NO OTHER CHOICE. Prefer using a
@@ -2295,10 +2327,9 @@ func (q *querier) DeleteMCPServerConfigByID(ctx context.Context, id uuid.UUID) e
}
func (q *querier) DeleteMCPServerUserToken(ctx context.Context, arg database.DeleteMCPServerUserTokenParams) error {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil {
return err
}
return q.db.DeleteMCPServerUserToken(ctx, arg)
return fetchAndExec(q.log, q.auth, policy.ActionUpdatePersonal, func(ctx context.Context, arg database.DeleteMCPServerUserTokenParams) (database.MCPServerUserToken, error) {
return q.db.GetMCPServerUserToken(ctx, database.GetMCPServerUserTokenParams(arg))
}, q.db.DeleteMCPServerUserToken)(ctx, arg)
}
func (q *querier) DeleteMCPServerUserTokensByConfigID(ctx context.Context, mcpServerConfigID uuid.UUID) error {
@@ -4082,17 +4113,15 @@ func (q *querier) GetMCPServerConfigsByOrganization(ctx context.Context, organiz
}
func (q *querier) GetMCPServerUserToken(ctx context.Context, arg database.GetMCPServerUserTokenParams) (database.MCPServerUserToken, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceDeploymentConfig); err != nil {
return database.MCPServerUserToken{}, err
}
return q.db.GetMCPServerUserToken(ctx, arg)
return fetchWithAction(q.log, q.auth, policy.ActionReadPersonal, q.db.GetMCPServerUserToken)(ctx, arg)
}
func (q *querier) GetMCPServerUserTokenByID(ctx context.Context, id uuid.UUID) (database.MCPServerUserToken, error) {
return fetchWithAction(q.log, q.auth, policy.ActionReadPersonal, q.db.GetMCPServerUserTokenByID)(ctx, id)
}
func (q *querier) GetMCPServerUserTokensByUserID(ctx context.Context, userID uuid.UUID) ([]database.MCPServerUserToken, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceDeploymentConfig); err != nil {
return nil, err
}
return q.db.GetMCPServerUserTokensByUserID(ctx, userID)
return fetchWithPostFilter(q.auth, policy.ActionReadPersonal, q.db.GetMCPServerUserTokensByUserID)(ctx, userID)
}
func (q *querier) GetNextPendingWorkspaceBuildOrchestrationForUpdate(ctx context.Context) (database.WorkspaceBuildOrchestration, error) {
@@ -6990,10 +7019,10 @@ func (q *querier) MarkChatsContextDirtyByAgent(ctx context.Context, arg database
}
func (q *querier) MarkMCPServerUserTokenRefreshFailure(ctx context.Context, arg database.MarkMCPServerUserTokenRefreshFailureParams) (database.MCPServerUserToken, error) {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil {
return database.MCPServerUserToken{}, err
fetch := func(ctx context.Context, arg database.MarkMCPServerUserTokenRefreshFailureParams) (database.MCPServerUserToken, error) {
return q.db.GetMCPServerUserTokenByID(ctx, arg.ID)
}
return q.db.MarkMCPServerUserTokenRefreshFailure(ctx, arg)
return fetchAndQuery(q.log, q.auth, policy.ActionUpdatePersonal, fetch, q.db.MarkMCPServerUserTokenRefreshFailure)(ctx, arg)
}
func (q *querier) OIDCClaimFieldValues(ctx context.Context, args database.OIDCClaimFieldValuesParams) ([]string, error) {
@@ -7685,10 +7714,10 @@ func (q *querier) UpdateMCPServerConfigACLByID(ctx context.Context, arg database
}
func (q *querier) UpdateMCPServerUserTokenFromRefresh(ctx context.Context, arg database.UpdateMCPServerUserTokenFromRefreshParams) (database.MCPServerUserToken, error) {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil {
return database.MCPServerUserToken{}, err
fetch := func(ctx context.Context, arg database.UpdateMCPServerUserTokenFromRefreshParams) (database.MCPServerUserToken, error) {
return q.db.GetMCPServerUserTokenByID(ctx, arg.ID)
}
return q.db.UpdateMCPServerUserTokenFromRefresh(ctx, arg)
return fetchAndQuery(q.log, q.auth, policy.ActionUpdatePersonal, fetch, q.db.UpdateMCPServerUserTokenFromRefresh)(ctx, arg)
}
func (q *querier) UpdateMemberRoles(ctx context.Context, arg database.UpdateMemberRolesParams) (database.OrganizationMember, error) {
@@ -9040,10 +9069,7 @@ func (q *querier) UpsertLogoURL(ctx context.Context, value string) error {
}
func (q *querier) UpsertMCPServerUserToken(ctx context.Context, arg database.UpsertMCPServerUserTokenParams) (database.MCPServerUserToken, error) {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil {
return database.MCPServerUserToken{}, err
}
return q.db.UpsertMCPServerUserToken(ctx, arg)
return insertWithAction(q.log, q.auth, rbac.ResourceUserObject(arg.UserID), policy.ActionUpdatePersonal, q.db.UpsertMCPServerUserToken)(ctx, arg)
}
func (q *querier) UpsertNotificationReportGeneratorLog(ctx context.Context, arg database.UpsertNotificationReportGeneratorLogParams) error {
+31 -12
View File
@@ -1638,13 +1638,15 @@ func (s *MethodTestSuite) TestChats() {
dbm.EXPECT().DeleteMCPServerConfigByID(gomock.Any(), config.ID).Return(nil).AnyTimes()
check.Args(config.ID).Asserts(config, policy.ActionDelete)
}))
s.Run("DeleteMCPServerUserToken", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
s.Run("DeleteMCPServerUserToken", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
arg := database.DeleteMCPServerUserTokenParams{
MCPServerConfigID: uuid.New(),
UserID: uuid.New(),
}
token := testutil.Fake(s.T(), faker, database.MCPServerUserToken{MCPServerConfigID: arg.MCPServerConfigID, UserID: arg.UserID})
dbm.EXPECT().GetMCPServerUserToken(gomock.Any(), database.GetMCPServerUserTokenParams(arg)).Return(token, nil).AnyTimes()
dbm.EXPECT().DeleteMCPServerUserToken(gomock.Any(), arg).Return(nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate)
check.Args(arg).Asserts(token, policy.ActionUpdatePersonal)
}))
s.Run("DeleteMCPServerUserTokensByConfigID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
config := testutil.Fake(s.T(), faker, database.MCPServerConfig{})
@@ -1716,13 +1718,19 @@ func (s *MethodTestSuite) TestChats() {
}
token := testutil.Fake(s.T(), faker, database.MCPServerUserToken{MCPServerConfigID: arg.MCPServerConfigID, UserID: arg.UserID})
dbm.EXPECT().GetMCPServerUserToken(gomock.Any(), arg).Return(token, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceDeploymentConfig, policy.ActionRead).Returns(token)
check.Args(arg).Asserts(token, policy.ActionReadPersonal).Returns(token)
}))
s.Run("GetMCPServerUserTokenByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
token := testutil.Fake(s.T(), faker, database.MCPServerUserToken{})
dbm.EXPECT().GetMCPServerUserTokenByID(gomock.Any(), token.ID).Return(token, nil).AnyTimes()
check.Args(token.ID).Asserts(token, policy.ActionReadPersonal).Returns(token)
}))
s.Run("GetMCPServerUserTokensByUserID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
userID := uuid.New()
tokens := []database.MCPServerUserToken{testutil.Fake(s.T(), faker, database.MCPServerUserToken{UserID: userID})}
dbm.EXPECT().GetMCPServerUserTokensByUserID(gomock.Any(), userID).Return(tokens, nil).AnyTimes()
check.Args(userID).Asserts(rbac.ResourceDeploymentConfig, policy.ActionRead).Returns(tokens)
tokenA := testutil.Fake(s.T(), faker, database.MCPServerUserToken{UserID: userID})
tokenB := testutil.Fake(s.T(), faker, database.MCPServerUserToken{UserID: userID})
dbm.EXPECT().GetMCPServerUserTokensByUserID(gomock.Any(), userID).Return([]database.MCPServerUserToken{tokenA, tokenB}, nil).AnyTimes()
check.Args(userID).Asserts(tokenA, policy.ActionReadPersonal, tokenB, policy.ActionReadPersonal).OutOfOrder().Returns([]database.MCPServerUserToken{tokenA, tokenB})
}))
s.Run("InsertMCPServerConfig", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
arg := database.InsertMCPServerConfigParams{
@@ -1803,8 +1811,9 @@ func (s *MethodTestSuite) TestChats() {
AccessToken: "refreshed-access-token",
TokenType: "bearer",
}
dbm.EXPECT().GetMCPServerUserTokenByID(gomock.Any(), token.ID).Return(token, nil).AnyTimes()
dbm.EXPECT().UpdateMCPServerUserTokenFromRefresh(gomock.Any(), arg).Return(token, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate).Returns(token)
check.Args(arg).Asserts(token, policy.ActionUpdatePersonal).Returns(token)
}))
s.Run("UpsertMCPServerUserToken", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
arg := database.UpsertMCPServerUserTokenParams{
@@ -1815,7 +1824,7 @@ func (s *MethodTestSuite) TestChats() {
}
token := testutil.Fake(s.T(), faker, database.MCPServerUserToken{MCPServerConfigID: arg.MCPServerConfigID, UserID: arg.UserID})
dbm.EXPECT().UpsertMCPServerUserToken(gomock.Any(), arg).Return(token, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate).Returns(token)
check.Args(arg).Asserts(rbac.ResourceUserObject(arg.UserID), policy.ActionUpdatePersonal).Returns(token)
}))
s.Run("MarkMCPServerUserTokenRefreshFailure", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
token := testutil.Fake(s.T(), faker, database.MCPServerUserToken{})
@@ -1824,8 +1833,9 @@ func (s *MethodTestSuite) TestChats() {
UpdatedAt: token.UpdatedAt,
OauthRefreshFailureReason: "invalid_grant",
}
dbm.EXPECT().GetMCPServerUserTokenByID(gomock.Any(), token.ID).Return(token, nil).AnyTimes()
dbm.EXPECT().MarkMCPServerUserTokenRefreshFailure(gomock.Any(), arg).Return(token, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate).Returns(token)
check.Args(arg).Asserts(token, policy.ActionUpdatePersonal).Returns(token)
}))
}
@@ -7602,9 +7612,18 @@ func TestAsChatd(t *testing.T) {
err = auth.Authorize(ctx, actor, policy.ActionUpdate, rbac.ResourceDeploymentConfig)
require.Error(t, err, "deployment config update should not be allowed")
// User read_personal (needed for GetUserChatCustomPrompt).
err = auth.Authorize(ctx, actor, policy.ActionReadPersonal, rbac.ResourceUser)
require.NoError(t, err, "user read_personal should be allowed")
// Pin the complete ResourceUser action set: read_personal (user
// chat custom prompts) only. Token refresh persistence uses the
// per-user AsChatdTokenOwner subject, so a future site-wide
// personal-write grant fails here.
for _, action := range rbac.ResourceUser.AvailableActions() {
err := auth.Authorize(ctx, actor, action, rbac.ResourceUser)
if action == policy.ActionReadPersonal {
require.NoError(t, err, "user %s should be allowed", action)
} else {
require.Error(t, err, "user %s should be denied", action)
}
}
})
t.Run("DeniedActions", func(t *testing.T) {
@@ -0,0 +1,182 @@
package dbauthz_test
import (
"context"
"testing"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
"cdr.dev/slog/v3/sloggers/slogtest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/rolestore"
"github.com/coder/coder/v2/testutil"
)
// TestMCPServerUserTokensAuth exercises the owner-personal gating of
// mcp_server_user_tokens against the real RBAC authorizer, which the
// FakeAuthorizer-based method suite cannot do.
func TestMCPServerUserTokensAuth(t *testing.T) {
t.Parallel()
setupCtx := testutil.Context(t, testutil.WaitLong)
authz := rbac.NewAuthorizer(prometheus.NewRegistry())
store, _ := dbtestutil.NewDB(t)
db := dbauthz.New(store, authz, slogtest.Make(t, &slogtest.Options{
IgnoreErrors: true,
}), coderdtest.AccessControlStorePointer())
org := dbgen.Organization(t, store, database.Organization{})
otherOrg := dbgen.Organization(t, store, database.Organization{})
owner := dbgen.User(t, store, database.User{})
stranger := dbgen.User(t, store, database.User{})
otherOrgAdmin := dbgen.User(t, store, database.User{})
memberSubject := func(userID string) rbac.Subject {
return rbac.Subject{
ID: userID,
Roles: must(rolestore.Expand(
setupCtx,
store,
[]rbac.RoleIdentifier{rbac.RoleMember(), rbac.ScopedRoleOrgMember(org.ID)},
)),
Groups: []string{},
Scope: rbac.ExpandableScope(rbac.ScopeAll),
}
}
requireAuthorized := func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
}
requireNotAuthorized := func(t *testing.T, err error) {
t.Helper()
require.Error(t, err)
require.True(t, dbauthz.IsNotAuthorizedError(err), "expected authorization failure, got: %v", err)
}
testCases := []struct {
Name string
Subject rbac.Subject
Check func(t *testing.T, err error)
}{
{
Name: "Owner",
Subject: memberSubject(owner.ID.String()),
Check: requireAuthorized,
},
{
Name: "Stranger",
Subject: memberSubject(stranger.ID.String()),
Check: requireNotAuthorized,
},
{
Name: "OtherOrgAdmin",
Subject: rbac.Subject{
ID: otherOrgAdmin.ID.String(),
Roles: rbac.Roles(must(rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgAdmin(otherOrg.ID)}.Expand())),
Groups: []string{},
Scope: rbac.ExpandableScope(rbac.ScopeAll),
},
Check: requireNotAuthorized,
},
}
t.Run("ChatdSubjects", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
cfg := dbgen.MCPServerConfig(t, store, database.MCPServerConfig{
OrganizationID: org.ID,
})
token, err := store.UpsertMCPServerUserToken(ctx, database.UpsertMCPServerUserTokenParams{
MCPServerConfigID: cfg.ID,
UserID: owner.ID,
AccessToken: "chatd-access-token",
TokenType: "bearer",
})
require.NoError(t, err)
markFailure := func(ctx context.Context) error {
_, err := db.MarkMCPServerUserTokenRefreshFailure(ctx, database.MarkMCPServerUserTokenRefreshFailureParams{
ID: token.ID,
UpdatedAt: token.UpdatedAt,
OauthRefreshFailureReason: "test",
})
return err
}
// The daemon-wide chatd subject may read tokens but must not
// hold personal-write access anywhere.
chatdCtx := dbauthz.AsChatd(ctx)
_, err = db.GetMCPServerUserToken(chatdCtx, database.GetMCPServerUserTokenParams{
MCPServerConfigID: cfg.ID,
UserID: owner.ID,
})
requireAuthorized(t, err)
requireNotAuthorized(t, markFailure(chatdCtx))
// The token-owner subject is write-only: reads belong to the
// daemon-wide chatd subject.
_, err = db.GetMCPServerUserToken(
dbauthz.AsChatdTokenOwner(ctx, owner.ID),
database.GetMCPServerUserTokenParams{
MCPServerConfigID: cfg.ID,
UserID: owner.ID,
},
)
requireNotAuthorized(t, err)
// The per-user token-owner subject writes only its own
// owner's rows.
requireNotAuthorized(t, markFailure(dbauthz.AsChatdTokenOwner(ctx, stranger.ID)))
require.NoError(t, markFailure(dbauthz.AsChatdTokenOwner(ctx, owner.ID)))
})
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
// A per-case config keeps parallel subtests from sharing
// token rows; the raw store seeds bypassing authz.
testCtx := testutil.Context(t, testutil.WaitLong)
cfg := dbgen.MCPServerConfig(t, store, database.MCPServerConfig{
OrganizationID: org.ID,
})
_, err := store.UpsertMCPServerUserToken(testCtx, database.UpsertMCPServerUserTokenParams{
MCPServerConfigID: cfg.ID,
UserID: owner.ID,
AccessToken: "seed-access-token",
TokenType: "bearer",
})
require.NoError(t, err)
ctx := dbauthz.As(testCtx, tc.Subject)
_, err = db.GetMCPServerUserToken(ctx, database.GetMCPServerUserTokenParams{
MCPServerConfigID: cfg.ID,
UserID: owner.ID,
})
tc.Check(t, err)
_, err = db.UpsertMCPServerUserToken(ctx, database.UpsertMCPServerUserTokenParams{
MCPServerConfigID: cfg.ID,
UserID: owner.ID,
AccessToken: "updated-access-token",
TokenType: "bearer",
})
tc.Check(t, err)
err = db.DeleteMCPServerUserToken(ctx, database.DeleteMCPServerUserTokenParams{
MCPServerConfigID: cfg.ID,
UserID: owner.ID,
})
tc.Check(t, err)
})
}
}
+8
View File
@@ -2369,6 +2369,14 @@ func (m queryMetricsStore) GetMCPServerUserToken(ctx context.Context, arg databa
return r0, r1
}
func (m queryMetricsStore) GetMCPServerUserTokenByID(ctx context.Context, id uuid.UUID) (database.MCPServerUserToken, error) {
start := time.Now()
r0, r1 := m.s.GetMCPServerUserTokenByID(ctx, id)
m.queryLatencies.WithLabelValues("GetMCPServerUserTokenByID").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetMCPServerUserTokenByID").Inc()
return r0, r1
}
func (m queryMetricsStore) GetMCPServerUserTokensByUserID(ctx context.Context, userID uuid.UUID) ([]database.MCPServerUserToken, error) {
start := time.Now()
r0, r1 := m.s.GetMCPServerUserTokensByUserID(ctx, userID)
+15
View File
@@ -4424,6 +4424,21 @@ func (mr *MockStoreMockRecorder) GetMCPServerUserToken(ctx, arg any) *gomock.Cal
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMCPServerUserToken", reflect.TypeOf((*MockStore)(nil).GetMCPServerUserToken), ctx, arg)
}
// GetMCPServerUserTokenByID mocks base method.
func (m *MockStore) GetMCPServerUserTokenByID(ctx context.Context, id uuid.UUID) (database.MCPServerUserToken, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMCPServerUserTokenByID", ctx, id)
ret0, _ := ret[0].(database.MCPServerUserToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetMCPServerUserTokenByID indicates an expected call of GetMCPServerUserTokenByID.
func (mr *MockStoreMockRecorder) GetMCPServerUserTokenByID(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMCPServerUserTokenByID", reflect.TypeOf((*MockStore)(nil).GetMCPServerUserTokenByID), ctx, id)
}
// GetMCPServerUserTokensByUserID mocks base method.
func (m *MockStore) GetMCPServerUserTokensByUserID(ctx context.Context, userID uuid.UUID) ([]database.MCPServerUserToken, error) {
m.ctrl.T.Helper()
+4 -3
View File
@@ -661,9 +661,10 @@ func (u GetUsersRow) RBACObject() rbac.Object {
return rbac.ResourceUserObject(u.ID)
}
func (u GitSSHKey) RBACObject() rbac.Object { return rbac.ResourceUserObject(u.UserID) }
func (u ExternalAuthLink) RBACObject() rbac.Object { return rbac.ResourceUserObject(u.UserID) }
func (u UserLink) RBACObject() rbac.Object { return rbac.ResourceUserObject(u.UserID) }
func (u GitSSHKey) RBACObject() rbac.Object { return rbac.ResourceUserObject(u.UserID) }
func (u ExternalAuthLink) RBACObject() rbac.Object { return rbac.ResourceUserObject(u.UserID) }
func (u UserLink) RBACObject() rbac.Object { return rbac.ResourceUserObject(u.UserID) }
func (u MCPServerUserToken) RBACObject() rbac.Object { return rbac.ResourceUserObject(u.UserID) }
func (u ExternalAuthLink) OAuthToken() *oauth2.Token {
return &oauth2.Token{
+1
View File
@@ -651,6 +651,7 @@ type sqlcQuerier interface {
GetMCPServerConfigByOrganizationAndSlug(ctx context.Context, arg GetMCPServerConfigByOrganizationAndSlugParams) (MCPServerConfig, error)
GetMCPServerConfigsByOrganization(ctx context.Context, organizationID uuid.UUID) ([]MCPServerConfig, error)
GetMCPServerUserToken(ctx context.Context, arg GetMCPServerUserTokenParams) (MCPServerUserToken, error)
GetMCPServerUserTokenByID(ctx context.Context, id uuid.UUID) (MCPServerUserToken, error)
GetMCPServerUserTokensByUserID(ctx context.Context, userID uuid.UUID) ([]MCPServerUserToken, error)
// Must be called from within a transaction. The row lock is released
// when the transaction ends.
+29
View File
@@ -17628,6 +17628,35 @@ func (q *sqlQuerier) GetMCPServerUserToken(ctx context.Context, arg GetMCPServer
return i, err
}
const getMCPServerUserTokenByID = `-- name: GetMCPServerUserTokenByID :one
SELECT
id, mcp_server_config_id, user_id, access_token, access_token_key_id, refresh_token, refresh_token_key_id, token_type, expiry, created_at, updated_at, oauth_refresh_failure_reason
FROM
mcp_server_user_tokens
WHERE
id = $1::uuid
`
func (q *sqlQuerier) GetMCPServerUserTokenByID(ctx context.Context, id uuid.UUID) (MCPServerUserToken, error) {
row := q.db.QueryRowContext(ctx, getMCPServerUserTokenByID, id)
var i MCPServerUserToken
err := row.Scan(
&i.ID,
&i.MCPServerConfigID,
&i.UserID,
&i.AccessToken,
&i.AccessTokenKeyID,
&i.RefreshToken,
&i.RefreshTokenKeyID,
&i.TokenType,
&i.Expiry,
&i.CreatedAt,
&i.UpdatedAt,
&i.OauthRefreshFailureReason,
)
return i, err
}
const getMCPServerUserTokensByUserID = `-- name: GetMCPServerUserTokensByUserID :many
SELECT
id, mcp_server_config_id, user_id, access_token, access_token_key_id, refresh_token, refresh_token_key_id, token_type, expiry, created_at, updated_at, oauth_refresh_failure_reason
@@ -204,6 +204,14 @@ WHERE
mcp_server_config_id = @mcp_server_config_id::uuid
AND user_id = @user_id::uuid;
-- name: GetMCPServerUserTokenByID :one
SELECT
*
FROM
mcp_server_user_tokens
WHERE
id = @id::uuid;
-- name: GetMCPServerUserTokensByUserID :many
SELECT
*