mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: audit login (#5925)
* added migration for api key resource * sort of working * auditing login * passing the correct user id * added and fixed tests * gen documentation * formatting and lint * lint * audit Github oauth and write tests * audit oauth and write tests * added defer fn for login error auditing * fixed test * feat: audit logout (#5998) * Update coderd/userauth.go Co-authored-by: Steven Masley <Emyrk@users.noreply.github.com> * fix test * bypassing diff generation if login/logout * lint --------- Co-authored-by: Steven Masley <Emyrk@users.noreply.github.com>
This commit is contained in:
co-authored by
Steven Masley
parent
060eeed5c3
commit
46fe59f5e7
+129
-1
@@ -20,6 +20,7 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/coderd/audit"
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/codersdk"
|
||||
@@ -105,7 +106,9 @@ 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) {
|
||||
@@ -118,13 +121,19 @@ 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"}},
|
||||
@@ -147,12 +156,20 @@ 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"},
|
||||
@@ -175,13 +192,23 @@ 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"},
|
||||
@@ -205,12 +232,20 @@ 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"},
|
||||
@@ -234,16 +269,26 @@ 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()
|
||||
auditor := audit.NewMock()
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
Auditor: auditor,
|
||||
GithubOAuth2Config: &coderd.GithubOAuth2Config{
|
||||
OAuth2Config: &oauth2Config{},
|
||||
AllowOrganizations: []string{"coder"},
|
||||
@@ -272,7 +317,11 @@ func TestUserOAuth2Github(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
numLogs := len(auditor.AuditLogs)
|
||||
|
||||
resp := oauth2Callback(t, client)
|
||||
numLogs++ // add an audit log for login
|
||||
|
||||
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
|
||||
client.SetSessionToken(authCookieValue(resp.Cookies()))
|
||||
@@ -281,10 +330,15 @@ func TestUserOAuth2Github(t *testing.T) {
|
||||
require.Equal(t, "kyle@coder.com", user.Email)
|
||||
require.Equal(t, "kyle", user.Username)
|
||||
require.Equal(t, "/hello-world", user.AvatarURL)
|
||||
|
||||
require.Len(t, auditor.AuditLogs, numLogs)
|
||||
require.Equal(t, database.AuditActionLogin, auditor.AuditLogs[numLogs-1].Action)
|
||||
})
|
||||
t.Run("SignupAllowedTeam", 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"},
|
||||
@@ -315,12 +369,20 @@ func TestUserOAuth2Github(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
numLogs := len(auditor.AuditLogs)
|
||||
|
||||
resp := oauth2Callback(t, client)
|
||||
numLogs++ // add an audit log for login
|
||||
|
||||
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
require.Len(t, auditor.AuditLogs, numLogs)
|
||||
require.Equal(t, database.AuditActionLogin, auditor.AuditLogs[numLogs-1].Action)
|
||||
})
|
||||
t.Run("SignupAllowedTeamInFirstOrganization", 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", "nil"},
|
||||
@@ -359,12 +421,20 @@ func TestUserOAuth2Github(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
numLogs := len(auditor.AuditLogs)
|
||||
|
||||
resp := oauth2Callback(t, client)
|
||||
numLogs++ // add an audit log for login
|
||||
|
||||
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
require.Len(t, auditor.AuditLogs, numLogs)
|
||||
require.Equal(t, database.AuditActionLogin, auditor.AuditLogs[numLogs-1].Action)
|
||||
})
|
||||
t.Run("SignupAllowedTeamInSecondOrganization", 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", "nil"},
|
||||
@@ -403,12 +473,20 @@ func TestUserOAuth2Github(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
numLogs := len(auditor.AuditLogs)
|
||||
|
||||
resp := oauth2Callback(t, client)
|
||||
numLogs++ // add an audit log for login
|
||||
|
||||
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
require.Len(t, auditor.AuditLogs, numLogs)
|
||||
require.Equal(t, database.AuditActionLogin, auditor.AuditLogs[numLogs-1].Action)
|
||||
})
|
||||
t.Run("SignupAllowEveryone", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
auditor := audit.NewMock()
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
Auditor: auditor,
|
||||
GithubOAuth2Config: &coderd.GithubOAuth2Config{
|
||||
AllowSignups: true,
|
||||
AllowEveryone: true,
|
||||
@@ -433,12 +511,20 @@ func TestUserOAuth2Github(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
numLogs := len(auditor.AuditLogs)
|
||||
|
||||
resp := oauth2Callback(t, client)
|
||||
numLogs++ // add an audit log for login
|
||||
|
||||
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
require.Len(t, auditor.AuditLogs, numLogs)
|
||||
require.Equal(t, database.AuditActionLogin, auditor.AuditLogs[numLogs-1].Action)
|
||||
})
|
||||
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"},
|
||||
@@ -469,8 +555,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.StatusUnauthorized, resp.StatusCode)
|
||||
require.Len(t, auditor.AuditLogs, numLogs)
|
||||
require.Equal(t, database.AuditActionLogin, auditor.AuditLogs[numLogs-1].Action)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -634,6 +726,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
tc := tc
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
auditor := audit.NewMock()
|
||||
conf := coderdtest.NewOIDCConfig(t, "")
|
||||
|
||||
config := conf.OIDCConfig(t, tc.UserInfoClaims)
|
||||
@@ -642,9 +735,13 @@ func TestUserOIDC(t *testing.T) {
|
||||
config.IgnoreEmailVerified = tc.IgnoreEmailVerified
|
||||
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
Auditor: auditor,
|
||||
OIDCConfig: config,
|
||||
})
|
||||
numLogs := len(auditor.AuditLogs)
|
||||
|
||||
resp := oidcCallback(t, client, conf.EncodeClaims(t, tc.IDTokenClaims))
|
||||
numLogs++ // add an audit log for login
|
||||
assert.Equal(t, tc.StatusCode, resp.StatusCode)
|
||||
|
||||
ctx, _ := testutil.Context(t)
|
||||
@@ -654,6 +751,9 @@ func TestUserOIDC(t *testing.T) {
|
||||
user, err := client.User(ctx, "me")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.Username, user.Username)
|
||||
|
||||
require.Len(t, auditor.AuditLogs, numLogs)
|
||||
require.Equal(t, database.AuditActionLogin, auditor.AuditLogs[numLogs-1].Action)
|
||||
}
|
||||
|
||||
if tc.AvatarURL != "" {
|
||||
@@ -661,26 +761,33 @@ func TestUserOIDC(t *testing.T) {
|
||||
user, err := client.User(ctx, "me")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.AvatarURL, user.AvatarURL)
|
||||
|
||||
require.Len(t, auditor.AuditLogs, numLogs)
|
||||
require.Equal(t, database.AuditActionLogin, auditor.AuditLogs[numLogs-1].Action)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("AlternateUsername", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auditor := audit.NewMock()
|
||||
conf := coderdtest.NewOIDCConfig(t, "")
|
||||
|
||||
config := conf.OIDCConfig(t, nil)
|
||||
config.AllowSignups = true
|
||||
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
Auditor: auditor,
|
||||
OIDCConfig: config,
|
||||
})
|
||||
numLogs := len(auditor.AuditLogs)
|
||||
|
||||
code := conf.EncodeClaims(t, jwt.MapClaims{
|
||||
"email": "jon@coder.com",
|
||||
})
|
||||
resp := oidcCallback(t, client, code)
|
||||
numLogs++ // add an audit log for login
|
||||
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
|
||||
ctx, _ := testutil.Context(t)
|
||||
@@ -697,12 +804,17 @@ func TestUserOIDC(t *testing.T) {
|
||||
"sub": "diff",
|
||||
})
|
||||
resp = oidcCallback(t, client, code)
|
||||
numLogs++ // add an audit log for login
|
||||
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
|
||||
client.SetSessionToken(authCookieValue(resp.Cookies()))
|
||||
user, err = client.User(ctx, "me")
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.HasPrefix(user.Username, "jon-"), "username %q should have prefix %q", user.Username, "jon-")
|
||||
|
||||
require.Len(t, auditor.AuditLogs, numLogs)
|
||||
require.Equal(t, database.AuditActionLogin, auditor.AuditLogs[numLogs-1].Action)
|
||||
})
|
||||
|
||||
t.Run("Disabled", func(t *testing.T) {
|
||||
@@ -714,23 +826,33 @@ 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{
|
||||
@@ -743,8 +865,14 @@ 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)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user