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:
Kira Pilot
2023-02-06 15:12:50 -05:00
committed by GitHub
co-authored by Steven Masley
parent 060eeed5c3
commit 46fe59f5e7
29 changed files with 679 additions and 259 deletions
+129 -1
View File
@@ -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)
})
}