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 all 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 Down Expand Up @@ -315,7 +315,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 @@ -338,7 +338,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 @@ -354,5 +354,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
48 changes: 39 additions & 9 deletions coderd/audit/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ type Request[T Auditable] struct {

Old T
New T

// This optional field can be passed in when the userID cannot be determined from the API Key
// such as in the case of login, when the audit log is created prior the API Key's existence.
UserID uuid.UUID
}

type BuildAuditParams[T Auditable] struct {
Expand Down Expand Up @@ -64,6 +68,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 +92,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 +115,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 All @@ -130,7 +141,14 @@ func InitRequest[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request

// If no resources were provided, there's nothing we can audit.
if ResourceID(req.Old) == uuid.Nil && ResourceID(req.New) == uuid.Nil {
return
// 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
if req.params.Action != database.AuditActionLogin && req.params.Action != database.AuditActionLogout {
return
}
}

var diffRaw = []byte("{}")
Expand All @@ -150,16 +168,24 @@ 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]),
ResourceID: either(req.Old, req.New, ResourceID[T]),
ResourceTarget: either(req.Old, req.New, ResourceTarget[T]),
ResourceType: either(req.Old, req.New, ResourceType[T], req.params.Action),
ResourceID: either(req.Old, req.New, ResourceID[T], req.params.Action),
ResourceTarget: either(req.Old, req.New, ResourceTarget[T], req.params.Action),
Action: p.Action,
Diff: diffRaw,
StatusCode: int32(sw.Status),
Expand Down Expand Up @@ -202,9 +228,9 @@ func BuildAudit[T Auditable](ctx context.Context, p *BuildAuditParams[T]) {
UserID: p.UserID,
Ip: ip,
UserAgent: sql.NullString{},
ResourceType: either(p.Old, p.New, ResourceType[T]),
ResourceID: either(p.Old, p.New, ResourceID[T]),
ResourceTarget: either(p.Old, p.New, ResourceTarget[T]),
ResourceType: either(p.Old, p.New, ResourceType[T], p.Action),
ResourceID: either(p.Old, p.New, ResourceID[T], p.Action),
ResourceTarget: either(p.Old, p.New, ResourceTarget[T], p.Action),
Action: p.Action,
Diff: diffRaw,
StatusCode: int32(p.Status),
Expand All @@ -221,11 +247,15 @@ func BuildAudit[T Auditable](ctx context.Context, p *BuildAuditParams[T]) {
}
}

func either[T Auditable, R any](old, new T, fn func(T) R) R {
func either[T Auditable, R any](old, new T, fn func(T) R, auditAction database.AuditAction) R {
if ResourceID(new) != uuid.Nil {
return fn(new)
} else if ResourceID(old) != uuid.Nil {
return fn(old)
} else if auditAction == database.AuditActionLogin || auditAction == database.AuditActionLogout {
// If the request action is a login or logout, we always want to audit it even if
// there is no diff. See the comment in audit.InitRequest for more detail.
return fn(old)
} else {
panic("both old and new are nil")
}
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 @@ -61,10 +61,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 @@ -285,8 +286,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 @@ -448,8 +449,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 @@ -127,8 +127,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 @@ -646,8 +646,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
Loading