Skip to content

feat: audit login #5925

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Feb 6, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions coderd/apikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) {
return
}

cookie, err := api.createAPIKey(ctx, createAPIKeyParams{
cookie, _, err := api.createAPIKey(ctx, createAPIKeyParams{
UserID: user.ID,
LoginType: database.LoginTypeToken,
ExpiresAt: database.Now().Add(lifeTime),
Expand Down Expand Up @@ -108,7 +108,7 @@ func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) {
}

lifeTime := time.Hour * 24 * 7
cookie, err := api.createAPIKey(ctx, createAPIKeyParams{
cookie, _, err := api.createAPIKey(ctx, createAPIKeyParams{
UserID: user.ID,
LoginType: database.LoginTypePassword,
RemoteAddr: r.RemoteAddr,
Expand Down Expand Up @@ -281,10 +281,10 @@ func (api *API) validateAPIKeyLifetime(lifetime time.Duration) error {
return nil
}

func (api *API) createAPIKey(ctx context.Context, params createAPIKeyParams) (*http.Cookie, error) {
func (api *API) createAPIKey(ctx context.Context, params createAPIKeyParams) (*http.Cookie, *database.APIKey, error) {
keyID, keySecret, err := GenerateAPIKeyIDSecret()
if err != nil {
return nil, xerrors.Errorf("generate API key: %w", err)
return nil, nil, xerrors.Errorf("generate API key: %w", err)
}
hashed := sha256.Sum256([]byte(keySecret))

Expand All @@ -310,7 +310,7 @@ func (api *API) createAPIKey(ctx context.Context, params createAPIKeyParams) (*h
switch scope {
case database.APIKeyScopeAll, database.APIKeyScopeApplicationConnect:
default:
return nil, xerrors.Errorf("invalid API key scope: %q", scope)
return nil, nil, xerrors.Errorf("invalid API key scope: %q", scope)
}

key, err := api.Database.InsertAPIKey(ctx, database.InsertAPIKeyParams{
Expand All @@ -333,7 +333,7 @@ func (api *API) createAPIKey(ctx context.Context, params createAPIKeyParams) (*h
Scope: scope,
})
if err != nil {
return nil, xerrors.Errorf("insert API key: %w", err)
return nil, nil, xerrors.Errorf("insert API key: %w", err)
}

api.Telemetry.Report(&telemetry.Snapshot{
Expand All @@ -349,5 +349,5 @@ func (api *API) createAPIKey(ctx context.Context, params createAPIKeyParams) (*h
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: api.SecureAuthCookie,
}, nil
}, &key, nil
}
10 changes: 10 additions & 0 deletions coderd/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,12 @@ func auditLogDescription(alog database.GetAuditLogsOffsetRow, additionalFields a
codersdk.AuditAction(alog.Action).Friendly(),
)

// API Key resources do not have targets and follow the below format:
// "User {logged in | logged out}"
if alog.ResourceType == database.ResourceTypeApiKey {
return str
}

// Strings for starting/stopping workspace builds follow the below format:
// "{user | 'Coder automatically'} started build #{build_number} for workspace {target}"
// where target is a workspace (name) instead of a workspace build
Expand Down Expand Up @@ -488,6 +494,10 @@ func actionFromString(actionString string) string {
return actionString
case codersdk.AuditActionStop:
return actionString
case codersdk.AuditActionLogin:
return actionString
case codersdk.AuditActionLogout:
return actionString
default:
}
return ""
Expand Down
22 changes: 19 additions & 3 deletions coderd/audit/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ type RequestParams struct {
type Request[T Auditable] struct {
params *RequestParams

Old T
New T
Old T
New T
UserId uuid.UUID
}

type BuildAuditParams[T Auditable] struct {
Expand Down Expand Up @@ -64,6 +65,9 @@ func ResourceTarget[T Auditable](tgt T) string {
return typed.PublicKey
case database.AuditableGroup:
return typed.Group.Name
case database.APIKey:
// this isn't used
return ""
default:
panic(fmt.Sprintf("unknown resource %T", tgt))
}
Expand All @@ -85,6 +89,8 @@ func ResourceID[T Auditable](tgt T) uuid.UUID {
return typed.UserID
case database.AuditableGroup:
return typed.Group.ID
case database.APIKey:
return typed.UserID
default:
panic(fmt.Sprintf("unknown resource %T", tgt))
}
Expand All @@ -106,6 +112,8 @@ func ResourceType[T Auditable](tgt T) database.ResourceType {
return database.ResourceTypeGitSshKey
case database.AuditableGroup:
return database.ResourceTypeGroup
case database.APIKey:
return database.ResourceTypeApiKey
default:
panic(fmt.Sprintf("unknown resource %T", tgt))
}
Expand Down Expand Up @@ -150,11 +158,19 @@ func InitRequest[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request
p.AdditionalFields = json.RawMessage("{}")
}

var userID uuid.UUID
key, ok := httpmw.APIKeyOptional(p.Request)
if ok {
userID = key.UserID
} else {
userID = req.UserId
}

ip := parseIP(p.Request.RemoteAddr)
auditLog := database.AuditLog{
ID: uuid.New(),
Time: database.Now(),
UserID: httpmw.APIKey(p.Request).UserID,
UserID: userID,
Ip: ip,
UserAgent: sql.NullString{String: p.Request.UserAgent(), Valid: true},
ResourceType: either(req.Old, req.New, ResourceType[T]),
Expand Down
4 changes: 3 additions & 1 deletion coderd/database/dump.sql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- It's not possible to drop enum values from enum types, so the UP has "IF NOT
-- EXISTS".
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
ALTER TYPE audit_action
ADD VALUE IF NOT EXISTS 'login';

ALTER TYPE audit_action
ADD VALUE IF NOT EXISTS 'logout';

ALTER TYPE resource_type
ADD VALUE IF NOT EXISTS 'api_key';

8 changes: 7 additions & 1 deletion coderd/database/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions coderd/gitsshkey_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ func TestGitSSHKey(t *testing.T) {
require.NotEmpty(t, key2.PublicKey)
require.NotEqual(t, key2.PublicKey, key1.PublicKey)

require.Len(t, auditor.AuditLogs, 1)
assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs[0].Action)
require.Len(t, auditor.AuditLogs, 2)
assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs[1].Action)
})
}

Expand Down
17 changes: 9 additions & 8 deletions coderd/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,11 @@ func TestPostTemplateByOrganization(t *testing.T) {
assert.Equal(t, expected.Name, got.Name)
assert.Equal(t, expected.Description, got.Description)

require.Len(t, auditor.AuditLogs, 3)
assert.Equal(t, database.AuditActionCreate, auditor.AuditLogs[0].Action)
assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs[1].Action)
assert.Equal(t, database.AuditActionCreate, auditor.AuditLogs[2].Action)
require.Len(t, auditor.AuditLogs, 4)
assert.Equal(t, database.AuditActionLogin, auditor.AuditLogs[0].Action)
assert.Equal(t, database.AuditActionCreate, auditor.AuditLogs[1].Action)
assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs[2].Action)
assert.Equal(t, database.AuditActionCreate, auditor.AuditLogs[3].Action)
})

t.Run("AlreadyExists", func(t *testing.T) {
Expand Down Expand Up @@ -320,8 +321,8 @@ func TestPatchTemplateMeta(t *testing.T) {
assert.Equal(t, req.DefaultTTLMillis, updated.DefaultTTLMillis)
assert.False(t, req.AllowUserCancelWorkspaceJobs)

require.Len(t, auditor.AuditLogs, 4)
assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs[3].Action)
require.Len(t, auditor.AuditLogs, 5)
assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs[4].Action)
})

t.Run("NoMaxTTL", func(t *testing.T) {
Expand Down Expand Up @@ -483,8 +484,8 @@ func TestDeleteTemplate(t *testing.T) {
err := client.DeleteTemplate(ctx, template.ID)
require.NoError(t, err)

require.Len(t, auditor.AuditLogs, 4)
assert.Equal(t, database.AuditActionDelete, auditor.AuditLogs[3].Action)
require.Len(t, auditor.AuditLogs, 5)
assert.Equal(t, database.AuditActionDelete, auditor.AuditLogs[4].Action)
})

t.Run("Workspaces", func(t *testing.T) {
Expand Down
8 changes: 4 additions & 4 deletions coderd/templateversions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
require.Equal(t, "bananas", version.Name)
require.Equal(t, provisionerdserver.ScopeOrganization, version.Job.Tags[provisionerdserver.TagScope])

require.Len(t, auditor.AuditLogs, 1)
assert.Equal(t, database.AuditActionCreate, auditor.AuditLogs[0].Action)
require.Len(t, auditor.AuditLogs, 2)
assert.Equal(t, database.AuditActionCreate, auditor.AuditLogs[1].Action)
})
t.Run("Example", func(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -645,8 +645,8 @@ func TestPatchActiveTemplateVersion(t *testing.T) {
})
require.NoError(t, err)

require.Len(t, auditor.AuditLogs, 4)
assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs[3].Action)
require.Len(t, auditor.AuditLogs, 5)
assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs[4].Action)
})
}

Expand Down
2 changes: 1 addition & 1 deletion coderd/userauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,7 @@ func (api *API) oauthLogin(r *http.Request, params oauthLoginParams) (*http.Cook
return nil, xerrors.Errorf("in tx: %w", err)
}

cookie, err := api.createAPIKey(ctx, createAPIKeyParams{
cookie, _, err := api.createAPIKey(ctx, createAPIKeyParams{
UserID: user.ID,
LoginType: params.LoginType,
RemoteAddr: r.RemoteAddr,
Expand Down
Loading