Skip to content
This repository was archived by the owner on Sep 30, 2024. It is now read-only.

Commit b386b54

Browse files
authored
event_logs: instrument SOAP-related event logs (#44325)
1 parent cc7bd73 commit b386b54

File tree

20 files changed

+483
-87
lines changed

20 files changed

+483
-87
lines changed

cmd/frontend/auth/user.go

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import (
55
"encoding/json"
66
"fmt"
77

8-
"github.com/inconshreveable/log15"
8+
sglog "github.com/sourcegraph/log"
99

1010
"github.com/sourcegraph/sourcegraph/internal/actor"
11+
"github.com/sourcegraph/sourcegraph/internal/auth"
1112
"github.com/sourcegraph/sourcegraph/internal/authz"
1213
"github.com/sourcegraph/sourcegraph/internal/database"
1314
"github.com/sourcegraph/sourcegraph/internal/deviceid"
@@ -62,14 +63,15 @@ func GetAndSaveUser(ctx context.Context, db database.DB, op GetAndSaveUserOp) (u
6263
return MockGetAndSaveUser(ctx, op)
6364
}
6465

65-
extacc := db.UserExternalAccounts()
66+
externalAccountsStore := db.UserExternalAccounts()
67+
logger := sglog.Scoped("authGetAndSaveUser", "get and save user authenticated by external providers")
6668

6769
userID, userSaved, extAcctSaved, safeErrMsg, err := func() (int32, bool, bool, string, error) {
6870
if actor := actor.FromContext(ctx); actor.IsAuthenticated() {
6971
return actor.UID, false, false, "", nil
7072
}
7173

72-
uid, lookupByExternalErr := extacc.LookupUserAndSave(ctx, op.ExternalAccount, op.ExternalAccountData)
74+
uid, lookupByExternalErr := externalAccountsStore.LookupUserAndSave(ctx, op.ExternalAccount, op.ExternalAccountData)
7375
if lookupByExternalErr == nil {
7476
return uid, false, true, "", nil
7577
}
@@ -104,8 +106,18 @@ func GetAndSaveUser(ctx context.Context, db database.DB, op GetAndSaveUserOp) (u
104106
return 0, false, false, "It looks like this is your first time signing in with this external identity. Sourcegraph couldn't link it to an existing user, because no verified email was provided. Ask your site admin to configure the auth provider to include the user's verified email on sign-in.", lookupByExternalErr
105107
}
106108

107-
// If CreateIfNotExist is true, create the new user, regardless of whether the email was verified or not.
108-
userID, err := extacc.CreateUserAndSave(ctx, op.UserProps, op.ExternalAccount, op.ExternalAccountData)
109+
act := &actor.Actor{
110+
SourcegraphOperator: op.ExternalAccount.ServiceType == auth.SourcegraphOperatorProviderType,
111+
}
112+
113+
// If CreateIfNotExist is true, create the new user, regardless of whether the
114+
// email was verified or not.
115+
//
116+
// NOTE: It is important to propagate the correct context that carries the
117+
// information of the actor, especially whether the actor is a Sourcegraph
118+
// operator or not.
119+
ctx = actor.WithActor(ctx, act)
120+
userID, err := externalAccountsStore.CreateUserAndSave(ctx, op.UserProps, op.ExternalAccount, op.ExternalAccountData)
109121
switch {
110122
case database.IsUsernameExists(err):
111123
return 0, false, false, fmt.Sprintf("Username %q already exists, but no verified email matched %q", op.UserProps.Username, op.UserProps.Email), err
@@ -114,26 +126,68 @@ func GetAndSaveUser(ctx context.Context, db database.DB, op GetAndSaveUserOp) (u
114126
case err != nil:
115127
return 0, false, false, "Unable to create a new user account due to a unexpected error. Ask a site admin for help.", err
116128
}
129+
act.UID = userID
117130

118131
if err = db.Authz().GrantPendingPermissions(ctx, &database.GrantPendingPermissionsArgs{
119132
UserID: userID,
120133
Perm: authz.Read,
121134
Type: authz.PermRepos,
122135
}); err != nil {
123-
log15.Error("Failed to grant user pending permissions", "userID", userID, "error", err)
136+
logger.Error(
137+
"failed to grant user pending permissions",
138+
sglog.Int32("userID", userID),
139+
sglog.Error(err),
140+
)
141+
// OK to continue, since this is a best-effort to improve the UX with some initial permissions available.
124142
}
125143

126-
serviceTypeArg := json.RawMessage(fmt.Sprintf(`{"serviceType": %q}`, op.ExternalAccount.ServiceType))
127-
if logErr := usagestats.LogBackendEvent(db, actor.FromContext(ctx).UID, deviceid.FromContext(ctx), "ExternalAuthSignupSucceeded", serviceTypeArg, serviceTypeArg, featureflag.GetEvaluatedFlagSet(ctx), nil); logErr != nil {
128-
log15.Warn("Failed to log event ExternalAuthSignupSucceded", "error", logErr)
144+
const eventName = "ExternalAuthSignupSucceeded"
145+
args, err := json.Marshal(map[string]any{
146+
// NOTE: The conventional name should be "service_type", but keeping as-is for
147+
// backwards capability.
148+
"serviceType": op.ExternalAccount.ServiceType,
149+
})
150+
if err != nil {
151+
logger.Error(
152+
"failed to marshal JSON for event log argument",
153+
sglog.String("eventName", eventName),
154+
sglog.Error(err),
155+
)
156+
// OK to continue, we still want the event log to be created
157+
}
158+
159+
// NOTE: It is important to propagate the correct context that carries the
160+
// information of the actor, especially whether the actor is a Sourcegraph
161+
// operator or not.
162+
err = usagestats.LogEvent(
163+
ctx,
164+
db,
165+
usagestats.Event{
166+
EventName: eventName,
167+
UserID: act.UID,
168+
Argument: args,
169+
Source: "BACKEND",
170+
},
171+
)
172+
if err != nil {
173+
logger.Error(
174+
"failed to log event",
175+
sglog.String("eventName", eventName),
176+
sglog.Error(err),
177+
)
129178
}
130179

131180
return userID, true, true, "", nil
132181
}()
133182
if err != nil {
183+
const eventName = "ExternalAuthSignupFailed"
134184
serviceTypeArg := json.RawMessage(fmt.Sprintf(`{"serviceType": %q}`, op.ExternalAccount.ServiceType))
135-
if logErr := usagestats.LogBackendEvent(db, actor.FromContext(ctx).UID, deviceid.FromContext(ctx), "ExternalAuthSignupFailed", serviceTypeArg, serviceTypeArg, featureflag.GetEvaluatedFlagSet(ctx), nil); logErr != nil {
136-
log15.Warn("Failed to log event ExternalAuthSignUpFailed", "error", logErr)
185+
if logErr := usagestats.LogBackendEvent(db, actor.FromContext(ctx).UID, deviceid.FromContext(ctx), eventName, serviceTypeArg, serviceTypeArg, featureflag.GetEvaluatedFlagSet(ctx), nil); logErr != nil {
186+
logger.Error(
187+
"failed to log event",
188+
sglog.String("eventName", eventName),
189+
sglog.Error(err),
190+
)
137191
}
138192
return 0, safeErrMsg, err
139193
}
@@ -162,7 +216,7 @@ func GetAndSaveUser(ctx context.Context, db database.DB, op GetAndSaveUserOp) (u
162216

163217
// Create/update the external account and ensure it's associated with the user ID
164218
if !extAcctSaved {
165-
err := extacc.AssociateUserAndSave(ctx, userID, op.ExternalAccount, op.ExternalAccountData)
219+
err := externalAccountsStore.AssociateUserAndSave(ctx, userID, op.ExternalAccount, op.ExternalAccountData)
166220
if err != nil {
167221
return 0, "Unexpected error associating the external account with your Sourcegraph user. The most likely cause for this problem is that another Sourcegraph user is already linked with this external account. A site admin or the other user can unlink the account to fix this problem.", err
168222
}
@@ -172,7 +226,12 @@ func GetAndSaveUser(ctx context.Context, db database.DB, op GetAndSaveUserOp) (u
172226
Perm: authz.Read,
173227
Type: authz.PermRepos,
174228
}); err != nil {
175-
log15.Error("Failed to grant user pending permissions", "userID", userID, "error", err)
229+
logger.Error(
230+
"failed to grant user pending permissions",
231+
sglog.Int32("userID", userID),
232+
sglog.Error(err),
233+
)
234+
// OK to continue, since this is a best-effort to improve the UX with some initial permissions available.
176235
}
177236
}
178237

cmd/frontend/auth/user_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ import (
77
"testing"
88

99
"github.com/davecgh/go-spew/spew"
10+
mockrequire "github.com/derision-test/go-mockgen/testutil/require"
1011
"github.com/sergi/go-diff/diffmatchpatch"
12+
"github.com/stretchr/testify/require"
1113

1214
"github.com/sourcegraph/sourcegraph/internal/actor"
15+
"github.com/sourcegraph/sourcegraph/internal/auth"
1316
"github.com/sourcegraph/sourcegraph/internal/database"
1417
"github.com/sourcegraph/sourcegraph/internal/errcode"
1518
"github.com/sourcegraph/sourcegraph/internal/extsvc"
@@ -419,6 +422,49 @@ func TestGetAndSaveUser(t *testing.T) {
419422
}
420423
})
421424
}
425+
426+
t.Run("Sourcegraph operator actor should be propagated", func(t *testing.T) {
427+
ctx := context.Background()
428+
429+
errNotFound := &errcode.Mock{
430+
IsNotFound: true,
431+
}
432+
usersStore := database.NewMockUserStore()
433+
usersStore.GetByVerifiedEmailFunc.SetDefaultReturn(nil, errNotFound)
434+
externalAccountsStore := database.NewMockUserExternalAccountsStore()
435+
externalAccountsStore.LookupUserAndSaveFunc.SetDefaultReturn(0, errNotFound)
436+
externalAccountsStore.CreateUserAndSaveFunc.SetDefaultHook(func(ctx context.Context, _ database.NewUser, _ extsvc.AccountSpec, _ extsvc.AccountData) (int32, error) {
437+
require.True(t, actor.FromContext(ctx).SourcegraphOperator, "the actor should be a Sourcegraph operator")
438+
return 1, nil
439+
})
440+
eventLogsStore := database.NewMockEventLogStore()
441+
eventLogsStore.BulkInsertFunc.SetDefaultHook(func(ctx context.Context, _ []*database.Event) error {
442+
require.True(t, actor.FromContext(ctx).SourcegraphOperator, "the actor should be a Sourcegraph operator")
443+
return nil
444+
})
445+
db := database.NewMockDB()
446+
db.UsersFunc.SetDefaultReturn(usersStore)
447+
db.UserExternalAccountsFunc.SetDefaultReturn(externalAccountsStore)
448+
db.AuthzFunc.SetDefaultReturn(database.NewMockAuthzStore())
449+
db.EventLogsFunc.SetDefaultReturn(eventLogsStore)
450+
451+
_, _, err := GetAndSaveUser(
452+
ctx,
453+
db,
454+
GetAndSaveUserOp{
455+
UserProps: database.NewUser{
456+
EmailIsVerified: true,
457+
},
458+
ExternalAccount: extsvc.AccountSpec{
459+
ServiceType: auth.SourcegraphOperatorProviderType,
460+
},
461+
ExternalAccountData: extsvc.AccountData{},
462+
CreateIfNotExist: true,
463+
},
464+
)
465+
require.NoError(t, err)
466+
mockrequire.Called(t, externalAccountsStore.CreateUserAndSaveFunc)
467+
})
422468
}
423469

424470
type userInfo struct {

cmd/frontend/graphqlbackend/graphqlbackend.go

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
sgtrace "github.com/sourcegraph/sourcegraph/internal/trace"
3333
"github.com/sourcegraph/sourcegraph/internal/trace/policy"
3434
"github.com/sourcegraph/sourcegraph/internal/types"
35+
"github.com/sourcegraph/sourcegraph/internal/usagestats"
3536
"github.com/sourcegraph/sourcegraph/lib/errors"
3637
)
3738

@@ -43,11 +44,13 @@ var (
4344
}, []string{"type", "field", "error", "source", "request_name"})
4445
)
4546

46-
type prometheusTracer struct {
47+
type requestTracer struct {
48+
DB database.DB
4749
tracer trace.OpenTracingTracer
50+
logger sglog.Logger
4851
}
4952

50-
func (t *prometheusTracer) TraceQuery(ctx context.Context, queryString string, operationName string, variables map[string]any, varTypes map[string]*introspection.Type) (context.Context, trace.TraceQueryFinishFunc) {
53+
func (t *requestTracer) TraceQuery(ctx context.Context, queryString string, operationName string, variables map[string]any, varTypes map[string]*introspection.Type) (context.Context, trace.TraceQueryFinishFunc) {
5154
start := time.Now()
5255
var finish trace.TraceQueryFinishFunc
5356
if policy.ShouldTrace(ctx) {
@@ -64,6 +67,48 @@ func (t *prometheusTracer) TraceQuery(ctx context.Context, queryString string, o
6467
currentUserID = a.UID
6568
}
6669

70+
// 🚨 SECURITY: We want to log every single operation the Sourcegraph operator
71+
// has done on the instance, so we need to do additional logging here. Sometimes
72+
// we would end up having logging twice for the same operation (here and the web
73+
// app), but we would not want to risk missing logging operations. Also in the
74+
// future, we expect audit logging of Sourcegraph operators to live outside the
75+
// instance, which makes this pattern less of a concern in terms of redundancy.
76+
if a.SourcegraphOperator {
77+
const eventName = "SourcegraphOperatorGraphQLRequest"
78+
args, err := json.Marshal(map[string]any{
79+
"queryString": queryString,
80+
"variables": variables,
81+
})
82+
if err != nil {
83+
t.logger.Error(
84+
"failed to marshal JSON for event log argument",
85+
sglog.String("eventName", eventName),
86+
sglog.Error(err),
87+
)
88+
}
89+
90+
// NOTE: It is important to propagate the correct context that carries the
91+
// information of the actor, especially whether the actor is a Sourcegraph
92+
// operator or not.
93+
err = usagestats.LogEvent(
94+
ctx,
95+
t.DB,
96+
usagestats.Event{
97+
EventName: eventName,
98+
UserID: a.UID,
99+
Argument: args,
100+
Source: "BACKEND",
101+
},
102+
)
103+
if err != nil {
104+
t.logger.Error(
105+
"failed to log event",
106+
sglog.String("eventName", eventName),
107+
sglog.Error(err),
108+
)
109+
}
110+
}
111+
67112
// Requests made by our JS frontend and other internal things will have a concrete name attached to the
68113
// request which allows us to (softly) differentiate it from end-user API requests. For example,
69114
// /.api/graphql?Foobar where Foobar is the name of the request we make. If there is not a request name,
@@ -96,7 +141,7 @@ VARIABLES
96141
}
97142
}
98143

99-
func (prometheusTracer) TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]any) (context.Context, trace.TraceFieldFinishFunc) {
144+
func (requestTracer) TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]any) (context.Context, trace.TraceFieldFinishFunc) {
100145
// We don't call into t.OpenTracingTracer.TraceField since it generates too many spans which is really hard to read.
101146
start := time.Now()
102147
return ctx, func(err *gqlerrors.QueryError) {
@@ -111,7 +156,7 @@ func (prometheusTracer) TraceField(ctx context.Context, label, typeName, fieldNa
111156
}
112157
}
113158

114-
func (t prometheusTracer) TraceValidation(ctx context.Context) trace.TraceValidationFinishFunc {
159+
func (t requestTracer) TraceValidation(ctx context.Context) trace.TraceValidationFinishFunc {
115160
var finish trace.TraceValidationFinishFunc
116161
if policy.ShouldTrace(ctx) {
117162
finish = t.tracer.TraceValidation(ctx)
@@ -461,10 +506,14 @@ func NewSchema(
461506
}
462507
}
463508

509+
logger := sglog.Scoped("GraphQL", "general GraphQL logging")
464510
return graphql.ParseSchema(
465511
strings.Join(schemas, "\n"),
466512
resolver,
467-
graphql.Tracer(&prometheusTracer{}),
513+
graphql.Tracer(&requestTracer{
514+
DB: db,
515+
logger: logger,
516+
}),
468517
graphql.UseStringDescriptions(),
469518
)
470519
}

cmd/frontend/graphqlbackend/search_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ func TestSearch(t *testing.T) {
117117
gsClient.ResolveRevisionFunc.SetDefaultHook(tc.repoRevsMock)
118118

119119
sr := newSchemaResolver(db, gsClient)
120-
schema, err := graphql.ParseSchema(mainSchema, sr, graphql.Tracer(&prometheusTracer{}))
120+
schema, err := graphql.ParseSchema(mainSchema, sr, graphql.Tracer(&requestTracer{}))
121121
if err != nil {
122122
t.Fatal(err)
123123
}

cmd/frontend/internal/cli/http.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func newExternalHTTPHandler(
6363
// 🚨 SECURITY: The HTTP API should not accept cookies as authentication, except from trusted
6464
// origins, to avoid CSRF attacks. See session.CookieMiddlewareWithCSRFSafety for details.
6565
apiHandler = session.CookieMiddlewareWithCSRFSafety(logger, db, apiHandler, corsAllowHeader, isTrustedOrigin) // API accepts cookies with special header
66-
apiHandler = internalhttpapi.AccessTokenAuthMiddleware(db, apiHandler) // API accepts access tokens
66+
apiHandler = internalhttpapi.AccessTokenAuthMiddleware(db, logger, apiHandler) // API accepts access tokens
6767
apiHandler = requestclient.HTTPMiddleware(apiHandler)
6868
apiHandler = gziphandler.GzipHandler(apiHandler)
6969
if envvar.SourcegraphDotComMode() {
@@ -85,8 +85,8 @@ func newExternalHTTPHandler(
8585
appHandler = actor.AnonymousUIDMiddleware(appHandler)
8686
appHandler = authMiddlewares.App(appHandler) // 🚨 SECURITY: auth middleware
8787
appHandler = middleware.OpenGraphMetadataMiddleware(db.FeatureFlags(), appHandler)
88-
appHandler = session.CookieMiddleware(logger, db, appHandler) // app accepts cookies
89-
appHandler = internalhttpapi.AccessTokenAuthMiddleware(db, appHandler) // app accepts access tokens
88+
appHandler = session.CookieMiddleware(logger, db, appHandler) // app accepts cookies
89+
appHandler = internalhttpapi.AccessTokenAuthMiddleware(db, logger, appHandler) // app accepts access tokens
9090
appHandler = requestclient.HTTPMiddleware(appHandler)
9191
if envvar.SourcegraphDotComMode() {
9292
appHandler = deviceid.Middleware(appHandler)

0 commit comments

Comments
 (0)