From 8f0b1df0d81d7b5f098ca5ccdf6c9b43cbce093f Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Wed, 22 Mar 2023 15:18:35 +0000 Subject: [PATCH 1/2] fix(audit): audit login/logout for new 3rd-party auth --- coderd/userauth.go | 4 ++-- coderd/userauth_test.go | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/coderd/userauth.go b/coderd/userauth.go index 9418d384833cc..9f92f0615ab94 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -424,7 +424,6 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) { }) return } - aReq.UserID = user.ID cookie, key, err := api.oauthLogin(r, oauthLoginParams{ User: user, @@ -453,6 +452,7 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) { return } aReq.New = key + aReq.UserID = key.UserID http.SetCookie(rw, cookie) @@ -705,7 +705,6 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) { }) return } - aReq.UserID = user.ID cookie, key, err := api.oauthLogin(r, oauthLoginParams{ User: user, @@ -736,6 +735,7 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) { return } aReq.New = key + aReq.UserID = key.UserID http.SetCookie(rw, cookie) diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go index dd13b7f944221..2d39e9f4523ae 100644 --- a/coderd/userauth_test.go +++ b/coderd/userauth_test.go @@ -14,6 +14,7 @@ import ( "github.com/coreos/go-oidc/v3/oidc" "github.com/golang-jwt/jwt" "github.com/google/go-github/v43/github" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/oauth2" @@ -332,6 +333,7 @@ func TestUserOAuth2Github(t *testing.T) { require.Equal(t, "/hello-world", user.AvatarURL) require.Len(t, auditor.AuditLogs, numLogs) + require.NotEqual(t, auditor.AuditLogs[numLogs-1].UserID, uuid.Nil) require.Equal(t, database.AuditActionLogin, auditor.AuditLogs[numLogs-1].Action) }) t.Run("SignupAllowedTeam", func(t *testing.T) { @@ -753,6 +755,7 @@ func TestUserOIDC(t *testing.T) { require.Equal(t, tc.Username, user.Username) require.Len(t, auditor.AuditLogs, numLogs) + require.NotEqual(t, auditor.AuditLogs[numLogs-1].UserID, uuid.Nil) require.Equal(t, database.AuditActionLogin, auditor.AuditLogs[numLogs-1].Action) } From 89ad0f91b4ffbeed16c112af25964b9756ab951a Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Wed, 22 Mar 2023 18:32:48 +0000 Subject: [PATCH 2/2] no longer auditing unknown users --- coderd/audit/request.go | 11 ++-- coderd/userauth_test.go | 52 ------------------- coderd/users_test.go | 9 +--- .../AuditLogDescription.test.tsx | 20 ------- .../AuditLogDescription.tsx | 4 +- site/src/testHelpers/entities.ts | 6 --- 6 files changed, 10 insertions(+), 92 deletions(-) diff --git a/coderd/audit/request.go b/coderd/audit/request.go index 5627e5c3826dd..98359803b473a 100644 --- a/coderd/audit/request.go +++ b/coderd/audit/request.go @@ -154,9 +154,7 @@ func InitRequest[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request if ResourceID(req.Old) == uuid.Nil && ResourceID(req.New) == uuid.Nil { // If the request action is a login or logout, we always want to audit it even if // there is no diff. This is so we can capture events where an API Key is never created - // because an unknown user fails to login. - // TODO: introduce the concept of an anonymous user so we always have a userID even - // when dealing with a mystery user. https://github.com/coder/coder/issues/6054 + // because a known user fails to login. if req.params.Action != database.AuditActionLogin && req.params.Action != database.AuditActionLogout { return } @@ -185,8 +183,13 @@ func InitRequest[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request key, ok := httpmw.APIKeyOptional(p.Request) if ok { userID = key.UserID - } else { + } else if req.UserID != uuid.Nil { userID = req.UserID + } else { + // if we do not have a user associated with the audit action + // we do not want to audit + // (this pertains to logins; we don't want to capture non-user login attempts) + return } ip := parseIP(p.Request.RemoteAddr) diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go index 2d39e9f4523ae..60d10af9ffc72 100644 --- a/coderd/userauth_test.go +++ b/coderd/userauth_test.go @@ -107,9 +107,7 @@ func TestUserOAuth2Github(t *testing.T) { t.Run("NotInAllowedOrganization", func(t *testing.T) { t.Parallel() - auditor := audit.NewMock() client := coderdtest.New(t, &coderdtest.Options{ - Auditor: auditor, GithubOAuth2Config: &coderd.GithubOAuth2Config{ OAuth2Config: &oauth2Config{}, ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) { @@ -122,19 +120,13 @@ func TestUserOAuth2Github(t *testing.T) { }, }, }) - numLogs := len(auditor.AuditLogs) resp := oauth2Callback(t, client) - numLogs++ // add an audit log for login require.Equal(t, http.StatusUnauthorized, resp.StatusCode) - require.Len(t, auditor.AuditLogs, numLogs) - require.Equal(t, database.AuditActionLogin, auditor.AuditLogs[numLogs-1].Action) }) t.Run("NotInAllowedTeam", func(t *testing.T) { t.Parallel() - auditor := audit.NewMock() client := coderdtest.New(t, &coderdtest.Options{ - Auditor: auditor, GithubOAuth2Config: &coderd.GithubOAuth2Config{ AllowOrganizations: []string{"coder"}, AllowTeams: []coderd.GithubOAuth2Team{{"another", "something"}, {"coder", "frontend"}}, @@ -157,20 +149,13 @@ func TestUserOAuth2Github(t *testing.T) { }, }, }) - numLogs := len(auditor.AuditLogs) resp := oauth2Callback(t, client) - numLogs++ // add an audit log for login - require.Equal(t, http.StatusUnauthorized, resp.StatusCode) - require.Len(t, auditor.AuditLogs, numLogs) - require.Equal(t, database.AuditActionLogin, auditor.AuditLogs[numLogs-1].Action) }) t.Run("UnverifiedEmail", func(t *testing.T) { t.Parallel() - auditor := audit.NewMock() client := coderdtest.New(t, &coderdtest.Options{ - Auditor: auditor, GithubOAuth2Config: &coderd.GithubOAuth2Config{ OAuth2Config: &oauth2Config{}, AllowOrganizations: []string{"coder"}, @@ -193,23 +178,16 @@ func TestUserOAuth2Github(t *testing.T) { }, }, }) - numLogs := len(auditor.AuditLogs) _ = coderdtest.CreateFirstUser(t, client) - numLogs++ // add an audit log for user create resp := oauth2Callback(t, client) - numLogs++ // add an audit log for login require.Equal(t, http.StatusBadRequest, resp.StatusCode) - require.Len(t, auditor.AuditLogs, numLogs) - require.Equal(t, database.AuditActionLogin, auditor.AuditLogs[numLogs-1].Action) }) t.Run("BlockSignups", func(t *testing.T) { t.Parallel() - auditor := audit.NewMock() client := coderdtest.New(t, &coderdtest.Options{ - Auditor: auditor, GithubOAuth2Config: &coderd.GithubOAuth2Config{ OAuth2Config: &oauth2Config{}, AllowOrganizations: []string{"coder"}, @@ -233,20 +211,14 @@ func TestUserOAuth2Github(t *testing.T) { }, }, }) - numLogs := len(auditor.AuditLogs) resp := oauth2Callback(t, client) - numLogs++ // add an audit log for login require.Equal(t, http.StatusForbidden, resp.StatusCode) - require.Len(t, auditor.AuditLogs, numLogs) - require.Equal(t, database.AuditActionLogin, auditor.AuditLogs[numLogs-1].Action) }) t.Run("MultiLoginNotAllowed", func(t *testing.T) { t.Parallel() - auditor := audit.NewMock() client := coderdtest.New(t, &coderdtest.Options{ - Auditor: auditor, GithubOAuth2Config: &coderd.GithubOAuth2Config{ OAuth2Config: &oauth2Config{}, AllowOrganizations: []string{"coder"}, @@ -270,20 +242,15 @@ func TestUserOAuth2Github(t *testing.T) { }, }, }) - numLogs := len(auditor.AuditLogs) // Creates the first user with login_type 'password'. _ = coderdtest.CreateFirstUser(t, client) - numLogs++ // add an audit log for user create // Attempting to login should give us a 403 since the user // already has a login_type of 'password'. resp := oauth2Callback(t, client) - numLogs++ // add an audit log for login require.Equal(t, http.StatusForbidden, resp.StatusCode) - require.Len(t, auditor.AuditLogs, numLogs) - require.Equal(t, database.AuditActionLogin, auditor.AuditLogs[numLogs-1].Action) }) t.Run("Signup", func(t *testing.T) { t.Parallel() @@ -524,9 +491,7 @@ func TestUserOAuth2Github(t *testing.T) { }) t.Run("SignupFailedInactiveInOrg", func(t *testing.T) { t.Parallel() - auditor := audit.NewMock() client := coderdtest.New(t, &coderdtest.Options{ - Auditor: auditor, GithubOAuth2Config: &coderd.GithubOAuth2Config{ AllowSignups: true, AllowOrganizations: []string{"coder"}, @@ -557,14 +522,10 @@ func TestUserOAuth2Github(t *testing.T) { }, }, }) - numLogs := len(auditor.AuditLogs) resp := oauth2Callback(t, client) - numLogs++ // add an audit log for login require.Equal(t, http.StatusUnauthorized, resp.StatusCode) - require.Len(t, auditor.AuditLogs, numLogs) - require.Equal(t, database.AuditActionLogin, auditor.AuditLogs[numLogs-1].Action) }) } @@ -829,33 +790,24 @@ func TestUserOIDC(t *testing.T) { t.Run("NoIDToken", func(t *testing.T) { t.Parallel() - auditor := audit.NewMock() client := coderdtest.New(t, &coderdtest.Options{ - Auditor: auditor, OIDCConfig: &coderd.OIDCConfig{ OAuth2Config: &oauth2Config{}, }, }) - numLogs := len(auditor.AuditLogs) resp := oidcCallback(t, client, "asdf") - numLogs++ // add an audit log for login - require.Equal(t, http.StatusBadRequest, resp.StatusCode) - require.Len(t, auditor.AuditLogs, numLogs) - require.Equal(t, database.AuditActionLogin, auditor.AuditLogs[numLogs-1].Action) }) t.Run("BadVerify", func(t *testing.T) { t.Parallel() - auditor := audit.NewMock() verifier := oidc.NewVerifier("", &oidc.StaticKeySet{ PublicKeys: []crypto.PublicKey{}, }, &oidc.Config{}) provider := &oidc.Provider{} client := coderdtest.New(t, &coderdtest.Options{ - Auditor: auditor, OIDCConfig: &coderd.OIDCConfig{ OAuth2Config: &oauth2Config{ token: (&oauth2.Token{ @@ -868,14 +820,10 @@ func TestUserOIDC(t *testing.T) { Verifier: verifier, }, }) - numLogs := len(auditor.AuditLogs) resp := oidcCallback(t, client, "asdf") - numLogs++ // add an audit log for login require.Equal(t, http.StatusBadRequest, resp.StatusCode) - require.Len(t, auditor.AuditLogs, numLogs) - require.Equal(t, database.AuditActionLogin, auditor.AuditLogs[numLogs-1].Action) }) } diff --git a/coderd/users_test.go b/coderd/users_test.go index 22360744e4d79..6980d005745c3 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -92,10 +92,7 @@ func TestPostLogin(t *testing.T) { t.Parallel() t.Run("InvalidUser", func(t *testing.T) { t.Parallel() - auditor := audit.NewMock() - client := coderdtest.New(t, &coderdtest.Options{Auditor: auditor}) - numLogs := len(auditor.AuditLogs) - + client := coderdtest.New(t, nil) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() @@ -103,13 +100,9 @@ func TestPostLogin(t *testing.T) { Email: "my@email.org", Password: "password", }) - numLogs++ // add an audit log for login var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) - - require.Len(t, auditor.AuditLogs, numLogs) - require.Equal(t, database.AuditActionLogin, auditor.AuditLogs[numLogs-1].Action) }) t.Run("BadPassword", func(t *testing.T) { diff --git a/site/src/components/AuditLogRow/AuditLogDescription/AuditLogDescription.test.tsx b/site/src/components/AuditLogRow/AuditLogDescription/AuditLogDescription.test.tsx index cb07125efaeba..4de181cbef02d 100644 --- a/site/src/components/AuditLogRow/AuditLogDescription/AuditLogDescription.test.tsx +++ b/site/src/components/AuditLogRow/AuditLogDescription/AuditLogDescription.test.tsx @@ -4,7 +4,6 @@ import { MockWorkspaceCreateAuditLogForDifferentOwner, MockAuditLogSuccessfulLogin, MockAuditLogUnsuccessfulLoginKnownUser, - MockAuditLogUnsuccessfulLoginUnknownUser, } from "testHelpers/entities" import { AuditLogDescription } from "./AuditLogDescription" import { AuditLogRow } from "../AuditLogRow" @@ -101,25 +100,6 @@ describe("AuditLogDescription", () => { ), ).toBeInTheDocument() - const statusPill = screen.getByRole("status") - expect(statusPill).toHaveTextContent("401") - }) - it("renders the correct string for unsuccessful login for an unknown user", async () => { - render() - - expect( - screen.getByText( - t("auditLog:table.logRow.description.unlinkedAuditDescription", { - truncatedDescription: "an unknown user logged in", - target: "", - onBehalfOf: undefined, - }) - .replace(/<[^>]*>/g, " ") - .replace(/\s{2,}/g, " ") - .trim(), - ), - ).toBeInTheDocument() - const statusPill = screen.getByRole("status") expect(statusPill).toHaveTextContent("401") }) diff --git a/site/src/components/AuditLogRow/AuditLogDescription/AuditLogDescription.tsx b/site/src/components/AuditLogRow/AuditLogDescription/AuditLogDescription.tsx index 129e48094165e..4e20ee9541be5 100644 --- a/site/src/components/AuditLogRow/AuditLogDescription/AuditLogDescription.tsx +++ b/site/src/components/AuditLogRow/AuditLogDescription/AuditLogDescription.tsx @@ -11,7 +11,7 @@ export const AuditLogDescription: FC<{ auditLog: AuditLog }> = ({ const { t } = useTranslation("auditLog") let target = auditLog.resource_target.trim() - const user = auditLog.user ? auditLog.user.username.trim() : "an unknown user" + const user = auditLog.user?.username.trim() if (auditLog.resource_type === "workspace_build") { return @@ -30,7 +30,7 @@ export const AuditLogDescription: FC<{ auditLog: AuditLog }> = ({ const onBehalfOf = auditLog.additional_fields.workspace_owner && auditLog.additional_fields.workspace_owner !== "unknown" && - auditLog.additional_fields.workspace_owner !== auditLog.user?.username + auditLog.additional_fields.workspace_owner.trim() !== user ? `on behalf of ${auditLog.additional_fields.workspace_owner}` : "" diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 478e841ddd29b..a1fb78c6e6636 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -1384,12 +1384,6 @@ export const MockAuditLogUnsuccessfulLoginKnownUser: TypesGen.AuditLog = { status_code: 401, } -export const MockAuditLogUnsuccessfulLoginUnknownUser: TypesGen.AuditLog = { - ...MockAuditLogSuccessfulLogin, - status_code: 401, - user: undefined, -} - export const MockWorkspaceQuota: TypesGen.WorkspaceQuota = { credits_consumed: 0, budget: 100,