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

Add auditing and system admin realm joining #705

Merged
merged 2 commits into from
Sep 30, 2020
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
15 changes: 10 additions & 5 deletions cmd/e2e-runner/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"net/http"
"os"
"strconv"
"time"

"github.com/google/exposure-notifications-server/pkg/logging"
"github.com/google/exposure-notifications-server/pkg/observability"
Expand Down Expand Up @@ -110,7 +111,7 @@ func realMain(ctx context.Context) error {
}
realm = database.NewRealmWithDefaults(realmName)
realm.RegionCode = realmRegionCode
if err := db.SaveRealm(realm); err != nil {
if err := db.SaveRealm(realm, database.System); err != nil {
return fmt.Errorf("failed to create realm %+v: %w: %v", realm, err, realm.ErrorMessages())
}
}
Expand All @@ -124,7 +125,7 @@ func realMain(ctx context.Context) error {
adminKey, err := realm.CreateAuthorizedApp(db, &database.AuthorizedApp{
Name: adminKeyName + suffix,
APIKeyType: database.APIUserTypeAdmin,
})
}, database.System)
if err != nil {
return fmt.Errorf("error trying to create a new Admin API Key: %w", err)
}
Expand All @@ -134,7 +135,9 @@ func realMain(ctx context.Context) error {
if err != nil {
logger.Errorf("admin API key cleanup failed: %w", err)
}
if err := app.Disable(db); err != nil {
now := time.Now().UTC()
app.DeletedAt = &now
if err := db.SaveAuthorizedApp(app, database.System); err != nil {
logger.Errorf("admin API key disable failed: %w", err)
}
logger.Info("successfully cleaned up e2e test admin key")
Expand All @@ -143,7 +146,7 @@ func realMain(ctx context.Context) error {
deviceKey, err := realm.CreateAuthorizedApp(db, &database.AuthorizedApp{
Name: deviceKeyName + suffix,
APIKeyType: database.APIUserTypeDevice,
})
}, database.System)
if err != nil {
return fmt.Errorf("error trying to create a new Device API Key: %w", err)
}
Expand All @@ -153,7 +156,9 @@ func realMain(ctx context.Context) error {
if err != nil {
logger.Errorf("device API key cleanup failed: %w", err)
}
if err := app.Disable(db); err != nil {
now := time.Now().UTC()
app.DeletedAt = &now
if err := db.SaveAuthorizedApp(app, database.System); err != nil {
logger.Errorf("device API key disable failed: %w", err)
}
logger.Info("successfully cleaned up e2e test device key")
Expand Down
33 changes: 33 additions & 0 deletions cmd/server/assets/admin/realms/edit.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{{define "admin/realms/edit"}}

{{$currentUser := .currentUser}}
{{$realm := .realm}}
{{$systemSMSConfig := .systemSMSConfig}}

Expand Down Expand Up @@ -87,6 +88,38 @@ <h1>Edit realm</h1>
</div>
</div>

{{if $currentUser.CanAdminRealm $realm.ID}}
<div class="card mb-3 shadow-sm">
<div class="card-header">Leave realm</div>
<div class="card-body">
<p>
You are currently a member of this realm. Click the button to leave.
</p>
<a href="/admin/realms/{{$realm.ID}}/leave" class="btn btn-block btn-danger"
data-method="PATCH"
data-confirm="Are you sure you want to leave this realm? This event will be logged and audited.">
Leave realm
</a>
</div>
</div>
{{else}}
<div class="card mb-3 shadow-sm">
<div class="card-header">Join realm</div>
<div class="card-body">
<p>
Click the button below to join the realm to debug or support the
realm. This will also set {{$realm.Name}} as your current active
realm. Only do this after gaining permission from the realm
administrator.
</p>
<a href="/admin/realms/{{$realm.ID}}/join" class="btn btn-block btn-danger"
data-method="PATCH"
data-confirm="Are you sure you want to join this realm? This event will be logged and audited.">
Join realm
</a>
</div>
</div>
{{end}}
</main>

{{template "scripts" .}}
Expand Down
4 changes: 2 additions & 2 deletions cmd/server/assets/admin/realms/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
<table class="table table-bordered table-striped bg-white">
<thead>
<tr>
<th scope="col" width="50">ID</th>
<th scope="col" width="50" class="text-center">ID</th>
<th scope="col">Name</th>
<th scope="col" width="125">Region code</th>
<th scope="col" width="125">Signing key</th>
Expand All @@ -38,7 +38,7 @@
<tbody>
{{range $realms}}
<tr>
<td>{{.ID}}</td>
<td class="text-center">{{.ID}}</td>
<td>
<a href="/admin/realms/{{.ID}}/edit">{{.Name}}</a>
</td>
Expand Down
31 changes: 31 additions & 0 deletions cmd/server/assets/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@
<h6 class="dropdown-header">Manage realm</h6>
<a class="dropdown-item" href="/apikeys">API keys</a>
<a class="dropdown-item" href="/mobile-apps">Mobile apps</a>
<a class="dropdown-item" href="/realm/events">Event log</a>
<a class="dropdown-item" href="/realm/keys">Signing keys</a>
<a class="dropdown-item" href="/realm/stats">Statistics</a>
<a class="dropdown-item" href="/users">Users</a>
Expand Down Expand Up @@ -375,6 +376,36 @@ <h6 class="dropdown-header">Actions</h6>
document.getSelection().removeAllRanges();
});

$('[data-timestamp]').each(function(i, e) {
let $this = $(e);
let date = new Date($this.data('timestamp'));

let year = date.getFullYear();
let month = date.getMonth() + 1;
if (month < 10) {
month = `0${month}`;
}
let day = date.getDate();
if (day < 10) {
day = `0${day}`;
}
let ampm = 'AM';
let hours = date.getHours();
if (hours > 12) {
ampm = 'PM';
hours = hours - 12;
}
if (hours < 10) {
hours = `0${hours}`;
}
let minutes = date.getMinutes();
if (minutes < 10) {
minutes = `0${minutes}`;
}

$this.html(`${year}-${month}-${day} ${hours}:${minutes} ${ampm}`);
});

// Toast shows alerts/flash messages.
$('.toast').toast('show');

Expand Down
64 changes: 64 additions & 0 deletions cmd/server/assets/realmadmin/events.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{{define "realmadmin/events"}}

{{$realm := .realm}}
{{$events := .events}}

<!doctype html>
<html lang="en">
<head>
{{template "head" .}}
</head>

<body class="tab-content">
{{template "navbar" .}}

<main role="main" class="container">
{{template "flash" .}}

<h1>Realm event log</h1>
<p>
The list below shows the past 30 days of events that have occurred on this
realm. Not all events are recorded for auditing to preserve privacy.
</p>

<div class="card mb-3 shadow-sm">
<div class="card-header">Events</div>
<div class="list-group list-group-flush">
{{range $event := $events}}
<div class="list-group-item flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{$event.Action}}</h5>
<small
data-timestamp="{{$event.CreatedAt.Format "1/02/2006 3:04:05 PM UTC"}}"
data-toggle="tooltip" title="{{$event.CreatedAt.Format "2006-02-01 15:04 UTC"}}">
{{$event.CreatedAt.Format "2006-02-01 15:04"}}
</small>
</div>
<div>
<span class="text-primary text-nowrap">{{$event.ActorDisplay}}</span>

<span>{{$event.Action}}</span>

<span class="text-primary text-nowrap">{{$event.TargetDisplay}}</span>

{{if $event.Diff}}
<br>
<a href="#" data-toggle="collapse" data-target="#collapseDiff{{$event.ID}}"
aria-expanded="true" aria-controls="collapseDiff{{$event.ID}}"
class="small text-muted">
Toggle diff
</a>
<pre id="collapseDiff{{$event.ID}}" class="collapse mt-3 mb-1"><code>{{$event.Diff}}</code></pre>
{{end}}
</div>
</div>
{{end}}
</div>
</div>
</main>

{{template "scripts" .}}

</body>
</html>
{{end}}
6 changes: 3 additions & 3 deletions cmd/server/assets/users/_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@
</div>

<div class="form-group">
<div class="form-check">
<input type="checkbox" id="admin" name="admin" class="form-check-input"
<div class="custom-control custom-checkbox">
<input type="checkbox" id="admin" name="admin" class="custom-control-input"
{{if $user.CanAdminRealm $currentRealm.ID}} checked{{end}}>
<label class="form-check-label" for="admin">Admin</label>
<label class="custom-control-label" for="admin">Admin</label>
</div>
</div>

Expand Down
7 changes: 0 additions & 7 deletions cmd/server/assets/users/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,6 @@ <h1>Users</h1>
title="Remove this user">
<span class="oi oi-trash" aria-hidden="true"></span>
</a>
{{else if $currentUser.Admin}}
{{- /* system admins can remove themselves */ -}}
<a href="/users/{{.ID}}" class="d-block text-danger" data-method="DELETE"
data-confirm="Are you sure you want to leave {{.Name}}?" data-toggle="tooltip"
title="Leave this realm">
<span class="oi oi-account-logout" aria-hidden="true"></span>
</a>
{{end}}
</td>
</tr>
Expand Down
4 changes: 4 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ func realMain(ctx context.Context) error {
realmSub.Handle("/settings/enable-express", realmadminController.HandleEnableExpress()).Methods("POST")
realmSub.Handle("/settings/disable-express", realmadminController.HandleDisableExpress()).Methods("POST")
realmSub.Handle("/stats", realmadminController.HandleShow()).Methods("GET")
realmSub.Handle("/events", realmadminController.HandleEvents()).Methods("GET")

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

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

adminSub.Handle("/sms", adminController.HandleSMSUpdate()).Methods("GET", "POST")
Expand Down
8 changes: 8 additions & 0 deletions pkg/config/cleanup_server_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package config

import (
"context"
"fmt"
"time"

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

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

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

// Audit entries need to persist for at least 7 days. The default is 30d ays.
if c.AuditEntryMaxAge < 7*24*time.Hour {
return fmt.Errorf("AUDIT_ENTRY_MAX_AGE must be at least 7 days")
}

return nil
}

Expand Down
Loading