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

Commit 2a72ca2

Browse files
committed
Add auditing and system admin realm joining
1 parent fffa6a4 commit 2a72ca2

File tree

17 files changed

+571
-20
lines changed

17 files changed

+571
-20
lines changed

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

+1
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@
239239
<h6 class="dropdown-header">Manage realm</h6>
240240
<a class="dropdown-item" href="/apikeys">API keys</a>
241241
<a class="dropdown-item" href="/mobile-apps">Mobile apps</a>
242+
<a class="dropdown-item" href="/realm/events">Event log</a>
242243
<a class="dropdown-item" href="/realm/keys">Signing keys</a>
243244
<a class="dropdown-item" href="/realm/stats">Statistics</a>
244245
<a class="dropdown-item" href="/users">Users</a>
+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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 audited 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.Entries}}
28+
<div class="list-group-item list-group-item-action">
29+
<div class="row">
30+
<div class="col-sm-3 col-md-2">
31+
<span class="small text-monospace">
32+
{{$event.CreatedAt.Format "2006-01-02"}}
33+
{{$event.CreatedAt.Format "3:04 PM"}}
34+
</span>
35+
</div>
36+
<div class="col-sm-9 col-md-10">
37+
<span class="text-primary">{{$event.User.Name}}</span>
38+
39+
{{$event.Action}}
40+
41+
{{if eq $event.TargetType "users"}}
42+
{{with $user := index $events.PreloadedUsers $event.TargetID}}
43+
<span class="text-primary">{{$user.Name}}</span>
44+
{{end}}
45+
{{else if eq $event.TargetType "realms"}}
46+
{{with $realm := index $events.PreloadedRealms $event.TargetID}}
47+
<span class="text-primary">{{$realm.Name}}</span>
48+
{{end}}
49+
{{end}}
50+
</div>
51+
</div>
52+
</div>
53+
{{end}}
54+
</div>
55+
</div>
56+
</main>
57+
58+
{{template "scripts" .}}
59+
60+
</body>
61+
</html>
62+
{{end}}

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

+3
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 {
@@ -442,6 +443,8 @@ func realMain(ctx context.Context) error {
442443
adminSub.Handle("/realms", adminController.HandleRealmsCreate()).Methods("POST")
443444
adminSub.Handle("/realms/new", adminController.HandleRealmsCreate()).Methods("GET")
444445
adminSub.Handle("/realms/{id:[0-9]+}/edit", adminController.HandleRealmsUpdate()).Methods("GET")
446+
adminSub.Handle("/realms/{id:[0-9]+}/join", adminController.HandleRealmsJoin()).Methods("PATCH")
447+
adminSub.Handle("/realms/{id:[0-9]+}/leave", adminController.HandleRealmsLeave()).Methods("PATCH")
445448
adminSub.Handle("/realms/{id:[0-9]+}", adminController.HandleRealmsUpdate()).Methods("PATCH")
446449

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

pkg/config/cleanup_server_config.go

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ type CleanupConfig struct {
4343
VerificationCodeMaxAge time.Duration `env:"VERIFICATION_CODE_MAX_AGE,default=24h"`
4444
VerificationTokenMaxAge time.Duration `env:"VERIFICATION_TOKEN_MAX_AGE,default=24h"`
4545
MobileAppMaxAge time.Duration `env:"MOBILE_APP_MAX_AGE,default=168h"`
46+
AuditEntryMaxAge time.Duration `env:"AUDIT_ENTRY_MAX_AGE,default=720h"`
4647
}
4748

4849
// NewCleanupConfig returns the environment config for the cleanup server.

pkg/controller/admin/realms.go

+129
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ package admin
1616

1717
import (
1818
"context"
19+
"fmt"
1920
"net/http"
2021

2122
"github.com/google/exposure-notifications-verification-server/pkg/controller"
2223
"github.com/google/exposure-notifications-verification-server/pkg/database"
2324
"github.com/gorilla/mux"
25+
"github.com/jinzhu/gorm"
2426
)
2527

2628
func (c *Controller) HandleRealmsIndex() http.Handler {
@@ -199,3 +201,130 @@ func (c *Controller) renderEditRealm(ctx context.Context, w http.ResponseWriter,
199201
m["supportsPerRealmSigning"] = c.db.SupportsPerRealmSigning()
200202
c.h.RenderHTML(w, "admin/realms/edit", m)
201203
}
204+
205+
func (c *Controller) HandleRealmsJoin() http.Handler {
206+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
207+
ctx := r.Context()
208+
vars := mux.Vars(r)
209+
210+
session := controller.SessionFromContext(ctx)
211+
if session == nil {
212+
controller.MissingSession(w, r, c.h)
213+
return
214+
}
215+
flash := controller.Flash(session)
216+
217+
user := controller.UserFromContext(ctx)
218+
if user == nil {
219+
controller.MissingUser(w, r, c.h)
220+
return
221+
}
222+
223+
realm, err := c.db.FindRealm(vars["id"])
224+
if err != nil {
225+
controller.InternalError(w, r, c.h, err)
226+
return
227+
}
228+
229+
user.Realms = append(user.Realms, realm)
230+
user.AdminRealms = append(user.AdminRealms, realm)
231+
232+
// Do the membership update and audit entry in a transaction because we need
233+
// both to succeed to continue.
234+
if err := c.db.RawDB().Transaction(func(tx *gorm.DB) error {
235+
// Save the user
236+
if err := database.SaveUser(tx, user); err != nil {
237+
return fmt.Errorf("failed to save user: %w", err)
238+
}
239+
240+
// Create the audit entry
241+
audit := &database.AuditEntry{
242+
UserID: user.ID,
243+
Action: "added user",
244+
TargetType: "users",
245+
TargetID: user.ID,
246+
SourceType: "realms",
247+
SourceID: realm.ID,
248+
}
249+
if err := database.SaveAuditEntry(tx, audit); err != nil {
250+
return fmt.Errorf("failed to save audit: %w", err)
251+
}
252+
253+
return nil
254+
}); err != nil {
255+
flash.Error("Failed to join %q: %v", realm.Name, err)
256+
controller.Back(w, r, c.h)
257+
return
258+
}
259+
260+
// Store the current realm on the session.
261+
controller.StoreSessionRealm(session, realm)
262+
263+
flash.Alert("Successfully joined %q", realm.Name)
264+
controller.Back(w, r, c.h)
265+
})
266+
}
267+
268+
func (c *Controller) HandleRealmsLeave() http.Handler {
269+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
270+
ctx := r.Context()
271+
vars := mux.Vars(r)
272+
273+
session := controller.SessionFromContext(ctx)
274+
if session == nil {
275+
controller.MissingSession(w, r, c.h)
276+
return
277+
}
278+
flash := controller.Flash(session)
279+
280+
user := controller.UserFromContext(ctx)
281+
if user == nil {
282+
controller.MissingUser(w, r, c.h)
283+
return
284+
}
285+
286+
realm, err := c.db.FindRealm(vars["id"])
287+
if err != nil {
288+
controller.InternalError(w, r, c.h, err)
289+
return
290+
}
291+
292+
user.RemoveRealm(realm)
293+
294+
// Do the membership update and audit entry in a transaction because we need
295+
// both to succeed to continue.
296+
if err := c.db.RawDB().Transaction(func(tx *gorm.DB) error {
297+
// Save the user
298+
if err := database.SaveUser(tx, user); err != nil {
299+
return fmt.Errorf("failed to save user: %w", err)
300+
}
301+
302+
// Create the audit entry
303+
audit := &database.AuditEntry{
304+
UserID: user.ID,
305+
Action: "removed user",
306+
TargetType: "users",
307+
TargetID: user.ID,
308+
SourceType: "realms",
309+
SourceID: realm.ID,
310+
}
311+
if err := database.SaveAuditEntry(tx, audit); err != nil {
312+
return fmt.Errorf("failed to save audit: %w", err)
313+
}
314+
315+
return nil
316+
}); err != nil {
317+
flash.Error("Failed to leave %q: %v", realm.Name, err)
318+
controller.Back(w, r, c.h)
319+
return
320+
}
321+
322+
// If the currently-selected realm is the realm the admin just left, clear
323+
// it.
324+
if controller.RealmIDFromSession(session) == realm.ID {
325+
controller.ClearSessionRealm(session)
326+
}
327+
flash.Alert("Successfully left %q", realm.Name)
328+
controller.Back(w, r, c.h)
329+
})
330+
}

pkg/controller/cleanup/cleanup.go

+6
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ func (c *Controller) HandleCleanup() http.Handler {
9797
c.logger.Infof("purged %v mobile apps tokens", count)
9898
}
9999

100+
if count, err := c.db.PurgeAuditEntries(c.config.AuditEntryMaxAge); err != nil {
101+
c.logger.Errorf("db.PurgeAuditEntries: %v", err)
102+
} else {
103+
c.logger.Infof("purged %v audit entries", count)
104+
}
105+
100106
c.h.RenderJSON(w, http.StatusOK, &CleanupResult{true})
101107
})
102108
}

pkg/controller/realmadmin/events.go

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2020 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package realmadmin
16+
17+
import (
18+
"context"
19+
"net/http"
20+
21+
"github.com/google/exposure-notifications-verification-server/pkg/controller"
22+
"github.com/google/exposure-notifications-verification-server/pkg/database"
23+
)
24+
25+
func (c *Controller) HandleEvents() http.Handler {
26+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
27+
ctx := r.Context()
28+
29+
realm := controller.RealmFromContext(ctx)
30+
if realm == nil {
31+
controller.MissingRealm(w, r, c.h)
32+
return
33+
}
34+
35+
events, err := realm.Audits(c.db)
36+
if err != nil {
37+
controller.InternalError(w, r, c.h, err)
38+
return
39+
}
40+
41+
c.renderEvents(ctx, w, realm, events)
42+
})
43+
}
44+
45+
func (c *Controller) renderEvents(ctx context.Context, w http.ResponseWriter, realm *database.Realm, events *database.AuditList) {
46+
m := controller.TemplateMapFromContext(ctx)
47+
m["user"] = realm
48+
m["events"] = events
49+
c.h.RenderHTML(w, "realmadmin/events", m)
50+
}

0 commit comments

Comments
 (0)