Skip to content
This repository was archived by the owner on Jul 12, 2023. It is now read-only.

Commit 66aaf0b

Browse files
authored
Add auditing and system admin realm joining (#705)
* Add auditing and expand system admin perms * Review feedback
1 parent a03c142 commit 66aaf0b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1215
-150
lines changed

cmd/e2e-runner/main.go

+10-5
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"net/http"
2424
"os"
2525
"strconv"
26+
"time"
2627

2728
"github.com/google/exposure-notifications-server/pkg/logging"
2829
"github.com/google/exposure-notifications-server/pkg/observability"
@@ -110,7 +111,7 @@ func realMain(ctx context.Context) error {
110111
}
111112
realm = database.NewRealmWithDefaults(realmName)
112113
realm.RegionCode = realmRegionCode
113-
if err := db.SaveRealm(realm); err != nil {
114+
if err := db.SaveRealm(realm, database.System); err != nil {
114115
return fmt.Errorf("failed to create realm %+v: %w: %v", realm, err, realm.ErrorMessages())
115116
}
116117
}
@@ -124,7 +125,7 @@ func realMain(ctx context.Context) error {
124125
adminKey, err := realm.CreateAuthorizedApp(db, &database.AuthorizedApp{
125126
Name: adminKeyName + suffix,
126127
APIKeyType: database.APIUserTypeAdmin,
127-
})
128+
}, database.System)
128129
if err != nil {
129130
return fmt.Errorf("error trying to create a new Admin API Key: %w", err)
130131
}
@@ -134,7 +135,9 @@ func realMain(ctx context.Context) error {
134135
if err != nil {
135136
logger.Errorf("admin API key cleanup failed: %w", err)
136137
}
137-
if err := app.Disable(db); err != nil {
138+
now := time.Now().UTC()
139+
app.DeletedAt = &now
140+
if err := db.SaveAuthorizedApp(app, database.System); err != nil {
138141
logger.Errorf("admin API key disable failed: %w", err)
139142
}
140143
logger.Info("successfully cleaned up e2e test admin key")
@@ -143,7 +146,7 @@ func realMain(ctx context.Context) error {
143146
deviceKey, err := realm.CreateAuthorizedApp(db, &database.AuthorizedApp{
144147
Name: deviceKeyName + suffix,
145148
APIKeyType: database.APIUserTypeDevice,
146-
})
149+
}, database.System)
147150
if err != nil {
148151
return fmt.Errorf("error trying to create a new Device API Key: %w", err)
149152
}
@@ -153,7 +156,9 @@ func realMain(ctx context.Context) error {
153156
if err != nil {
154157
logger.Errorf("device API key cleanup failed: %w", err)
155158
}
156-
if err := app.Disable(db); err != nil {
159+
now := time.Now().UTC()
160+
app.DeletedAt = &now
161+
if err := db.SaveAuthorizedApp(app, database.System); err != nil {
157162
logger.Errorf("device API key disable failed: %w", err)
158163
}
159164
logger.Info("successfully cleaned up e2e test device key")

cmd/server/assets/admin/realms/edit.html

+33
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{{define "admin/realms/edit"}}
22

3+
{{$currentUser := .currentUser}}
34
{{$realm := .realm}}
45
{{$systemSMSConfig := .systemSMSConfig}}
56

@@ -87,6 +88,38 @@ <h1>Edit realm</h1>
8788
</div>
8889
</div>
8990

91+
{{if $currentUser.CanAdminRealm $realm.ID}}
92+
<div class="card mb-3 shadow-sm">
93+
<div class="card-header">Leave realm</div>
94+
<div class="card-body">
95+
<p>
96+
You are currently a member of this realm. Click the button to leave.
97+
</p>
98+
<a href="/admin/realms/{{$realm.ID}}/leave" class="btn btn-block btn-danger"
99+
data-method="PATCH"
100+
data-confirm="Are you sure you want to leave this realm? This event will be logged and audited.">
101+
Leave realm
102+
</a>
103+
</div>
104+
</div>
105+
{{else}}
106+
<div class="card mb-3 shadow-sm">
107+
<div class="card-header">Join realm</div>
108+
<div class="card-body">
109+
<p>
110+
Click the button below to join the realm to debug or support the
111+
realm. This will also set {{$realm.Name}} as your current active
112+
realm. Only do this after gaining permission from the realm
113+
administrator.
114+
</p>
115+
<a href="/admin/realms/{{$realm.ID}}/join" class="btn btn-block btn-danger"
116+
data-method="PATCH"
117+
data-confirm="Are you sure you want to join this realm? This event will be logged and audited.">
118+
Join realm
119+
</a>
120+
</div>
121+
</div>
122+
{{end}}
90123
</main>
91124

92125
{{template "scripts" .}}

cmd/server/assets/admin/realms/index.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
<table class="table table-bordered table-striped bg-white">
3030
<thead>
3131
<tr>
32-
<th scope="col" width="50">ID</th>
32+
<th scope="col" width="50" class="text-center">ID</th>
3333
<th scope="col">Name</th>
3434
<th scope="col" width="125">Region code</th>
3535
<th scope="col" width="125">Signing key</th>
@@ -38,7 +38,7 @@
3838
<tbody>
3939
{{range $realms}}
4040
<tr>
41-
<td>{{.ID}}</td>
41+
<td class="text-center">{{.ID}}</td>
4242
<td>
4343
<a href="/admin/realms/{{.ID}}/edit">{{.Name}}</a>
4444
</td>

cmd/server/assets/header.html

+31
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@
243243
<h6 class="dropdown-header">Manage realm</h6>
244244
<a class="dropdown-item" href="/apikeys">API keys</a>
245245
<a class="dropdown-item" href="/mobile-apps">Mobile apps</a>
246+
<a class="dropdown-item" href="/realm/events">Event log</a>
246247
<a class="dropdown-item" href="/realm/keys">Signing keys</a>
247248
<a class="dropdown-item" href="/realm/stats">Statistics</a>
248249
<a class="dropdown-item" href="/users">Users</a>
@@ -375,6 +376,36 @@ <h6 class="dropdown-header">Actions</h6>
375376
document.getSelection().removeAllRanges();
376377
});
377378

379+
$('[data-timestamp]').each(function(i, e) {
380+
let $this = $(e);
381+
let date = new Date($this.data('timestamp'));
382+
383+
let year = date.getFullYear();
384+
let month = date.getMonth() + 1;
385+
if (month < 10) {
386+
month = `0${month}`;
387+
}
388+
let day = date.getDate();
389+
if (day < 10) {
390+
day = `0${day}`;
391+
}
392+
let ampm = 'AM';
393+
let hours = date.getHours();
394+
if (hours > 12) {
395+
ampm = 'PM';
396+
hours = hours - 12;
397+
}
398+
if (hours < 10) {
399+
hours = `0${hours}`;
400+
}
401+
let minutes = date.getMinutes();
402+
if (minutes < 10) {
403+
minutes = `0${minutes}`;
404+
}
405+
406+
$this.html(`${year}-${month}-${day} ${hours}:${minutes} ${ampm}`);
407+
});
408+
378409
// Toast shows alerts/flash messages.
379410
$('.toast').toast('show');
380411

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{{define "realmadmin/events"}}
2+
3+
{{$realm := .realm}}
4+
{{$events := .events}}
5+
6+
<!doctype html>
7+
<html lang="en">
8+
<head>
9+
{{template "head" .}}
10+
</head>
11+
12+
<body class="tab-content">
13+
{{template "navbar" .}}
14+
15+
<main role="main" class="container">
16+
{{template "flash" .}}
17+
18+
<h1>Realm event log</h1>
19+
<p>
20+
The list below shows the past 30 days of events that have occurred on this
21+
realm. Not all events are recorded for auditing to preserve privacy.
22+
</p>
23+
24+
<div class="card mb-3 shadow-sm">
25+
<div class="card-header">Events</div>
26+
<div class="list-group list-group-flush">
27+
{{range $event := $events}}
28+
<div class="list-group-item flex-column align-items-start">
29+
<div class="d-flex w-100 justify-content-between">
30+
<h5 class="mb-1">{{$event.Action}}</h5>
31+
<small
32+
data-timestamp="{{$event.CreatedAt.Format "1/02/2006 3:04:05 PM UTC"}}"
33+
data-toggle="tooltip" title="{{$event.CreatedAt.Format "2006-02-01 15:04 UTC"}}">
34+
{{$event.CreatedAt.Format "2006-02-01 15:04"}}
35+
</small>
36+
</div>
37+
<div>
38+
<span class="text-primary text-nowrap">{{$event.ActorDisplay}}</span>
39+
40+
<span>{{$event.Action}}</span>
41+
42+
<span class="text-primary text-nowrap">{{$event.TargetDisplay}}</span>
43+
44+
{{if $event.Diff}}
45+
<br>
46+
<a href="#" data-toggle="collapse" data-target="#collapseDiff{{$event.ID}}"
47+
aria-expanded="true" aria-controls="collapseDiff{{$event.ID}}"
48+
class="small text-muted">
49+
Toggle diff
50+
</a>
51+
<pre id="collapseDiff{{$event.ID}}" class="collapse mt-3 mb-1"><code>{{$event.Diff}}</code></pre>
52+
{{end}}
53+
</div>
54+
</div>
55+
{{end}}
56+
</div>
57+
</div>
58+
</main>
59+
60+
{{template "scripts" .}}
61+
62+
</body>
63+
</html>
64+
{{end}}

cmd/server/assets/users/_form.html

+3-3
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@
3535
</div>
3636

3737
<div class="form-group">
38-
<div class="form-check">
39-
<input type="checkbox" id="admin" name="admin" class="form-check-input"
38+
<div class="custom-control custom-checkbox">
39+
<input type="checkbox" id="admin" name="admin" class="custom-control-input"
4040
{{if $user.CanAdminRealm $currentRealm.ID}} checked{{end}}>
41-
<label class="form-check-label" for="admin">Admin</label>
41+
<label class="custom-control-label" for="admin">Admin</label>
4242
</div>
4343
</div>
4444

cmd/server/assets/users/index.html

-7
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,6 @@ <h1>Users</h1>
7272
title="Remove this user">
7373
<span class="oi oi-trash" aria-hidden="true"></span>
7474
</a>
75-
{{else if $currentUser.Admin}}
76-
{{- /* system admins can remove themselves */ -}}
77-
<a href="/users/{{.ID}}" class="d-block text-danger" data-method="DELETE"
78-
data-confirm="Are you sure you want to leave {{.Name}}?" data-toggle="tooltip"
79-
title="Leave this realm">
80-
<span class="oi oi-account-logout" aria-hidden="true"></span>
81-
</a>
8275
{{end}}
8376
</td>
8477
</tr>

cmd/server/main.go

+4
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ func realMain(ctx context.Context) error {
403403
realmSub.Handle("/settings/enable-express", realmadminController.HandleEnableExpress()).Methods("POST")
404404
realmSub.Handle("/settings/disable-express", realmadminController.HandleDisableExpress()).Methods("POST")
405405
realmSub.Handle("/stats", realmadminController.HandleShow()).Methods("GET")
406+
realmSub.Handle("/events", realmadminController.HandleEvents()).Methods("GET")
406407

407408
realmKeysController, err := realmkeys.New(ctx, cfg, db, certificateSigner, cacher, h)
408409
if err != nil {
@@ -438,10 +439,13 @@ func realMain(ctx context.Context) error {
438439
adminSub.Use(rateLimit)
439440

440441
adminController := admin.New(ctx, cfg, db, auth, h)
442+
adminSub.Handle("", http.RedirectHandler("/admin/realms", http.StatusSeeOther)).Methods("GET")
441443
adminSub.Handle("/realms", adminController.HandleRealmsIndex()).Methods("GET")
442444
adminSub.Handle("/realms", adminController.HandleRealmsCreate()).Methods("POST")
443445
adminSub.Handle("/realms/new", adminController.HandleRealmsCreate()).Methods("GET")
444446
adminSub.Handle("/realms/{id:[0-9]+}/edit", adminController.HandleRealmsUpdate()).Methods("GET")
447+
adminSub.Handle("/realms/{id:[0-9]+}/join", adminController.HandleRealmsJoin()).Methods("PATCH")
448+
adminSub.Handle("/realms/{id:[0-9]+}/leave", adminController.HandleRealmsLeave()).Methods("PATCH")
445449
adminSub.Handle("/realms/{id:[0-9]+}", adminController.HandleRealmsUpdate()).Methods("PATCH")
446450

447451
adminSub.Handle("/sms", adminController.HandleSMSUpdate()).Methods("GET", "POST")

pkg/config/cleanup_server_config.go

+8
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package config
1616

1717
import (
1818
"context"
19+
"fmt"
1920
"time"
2021

2122
"github.com/google/exposure-notifications-verification-server/pkg/database"
@@ -43,6 +44,7 @@ type CleanupConfig struct {
4344
VerificationCodeMaxAge time.Duration `env:"VERIFICATION_CODE_MAX_AGE,default=24h"`
4445
VerificationTokenMaxAge time.Duration `env:"VERIFICATION_TOKEN_MAX_AGE,default=24h"`
4546
MobileAppMaxAge time.Duration `env:"MOBILE_APP_MAX_AGE,default=168h"`
47+
AuditEntryMaxAge time.Duration `env:"AUDIT_ENTRY_MAX_AGE,default=720h"`
4648
}
4749

4850
// NewCleanupConfig returns the environment config for the cleanup server.
@@ -64,6 +66,7 @@ func (c *CleanupConfig) Validate() error {
6466
{c.CleanupPeriod, "CLEANUP_PERIOD"},
6567
{c.VerificationCodeMaxAge, "VERIFICATION_CODE_MAX_AGE"},
6668
{c.VerificationTokenMaxAge, "VERIFICATION_TOKEN_MAX_AGE"},
69+
{c.AuditEntryMaxAge, "AUDIT_ENTRY_MAX_AGE"},
6770
}
6871

6972
for _, f := range fields {
@@ -72,6 +75,11 @@ func (c *CleanupConfig) Validate() error {
7275
}
7376
}
7477

78+
// Audit entries need to persist for at least 7 days. The default is 30d ays.
79+
if c.AuditEntryMaxAge < 7*24*time.Hour {
80+
return fmt.Errorf("AUDIT_ENTRY_MAX_AGE must be at least 7 days")
81+
}
82+
7583
return nil
7684
}
7785

0 commit comments

Comments
 (0)