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

Commit

Permalink
start of the admin conosole for configuring the EN Server.
Browse files Browse the repository at this point in the history
  • Loading branch information
mikehelmick committed May 21, 2020
1 parent f113c0d commit 2865b87
Show file tree
Hide file tree
Showing 11 changed files with 420 additions and 11 deletions.
41 changes: 41 additions & 0 deletions internal/authorizedapp/database/authorized_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,43 @@ func NewAuthorizedAppDB(db *database.DB) *AuthorizedAppDB {
}
}

func (aa *AuthorizedAppDB) GetAllAuthorizedApps(ctx context.Context, sm secrets.SecretManager) ([]*model.AuthorizedApp, error) {
conn, err := aa.db.Pool.Acquire(ctx)
if err != nil {
return nil, fmt.Errorf("acquiring connection: %v", err)
}
defer conn.Release()

query := `
SELECT
app_package_name, platform, allowed_regions,
safetynet_apk_digest, safetynet_cts_profile_match, safetynet_basic_integrity, safetynet_past_seconds, safetynet_future_seconds,
devicecheck_team_id, devicecheck_key_id, devicecheck_private_key_secret
FROM
AuthorizedApp
ORDER BY app_package_name ASC`

rows, err := conn.Query(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()

var result []*model.AuthorizedApp
for rows.Next() {
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterating rows: %w", err)
}

app, err := scanOneAuthorizedApp(ctx, rows, sm)
if err != nil {
return nil, fmt.Errorf("error reading authorized apps: %w", err)
}
result = append(result, app)
}
return result, nil
}

// GetAuthorizedApp loads a single AuthorizedApp for the given name. If no row
// exists, this returns nil.
func (db *AuthorizedAppDB) GetAuthorizedApp(ctx context.Context, sm secrets.SecretManager, name string) (*model.AuthorizedApp, error) {
Expand All @@ -57,6 +94,10 @@ func (db *AuthorizedAppDB) GetAuthorizedApp(ctx context.Context, sm secrets.Secr

row := conn.QueryRow(ctx, query, name)

return scanOneAuthorizedApp(ctx, row, sm)
}

func scanOneAuthorizedApp(ctx context.Context, row pgx.Row, sm secrets.SecretManager) (*model.AuthorizedApp, error) {
config := model.NewAuthorizedApp()
var allowedRegions []string
var safetyNetPastSeconds, safetyNetFutureSeconds *int
Expand Down
9 changes: 9 additions & 0 deletions internal/authorizedapp/model/authorized_app_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package model

import (
"crypto/ecdsa"
"strings"
"time"
)

Expand Down Expand Up @@ -67,6 +68,14 @@ func (c *AuthorizedApp) IsAndroid() bool {
return c.Platform == androidDevice
}

func (c *AuthorizedApp) RegionsOnePerLine() string {
regions := []string{}
for r, _ := range c.AllowedRegions {
regions = append(regions, r)
}
return strings.Join(regions, "\n")
}

// IsAllowedRegion returns true if the regions list is empty or if the given
// region is in the list of allowed regions.
func (c *AuthorizedApp) IsAllowedRegion(s string) bool {
Expand Down
59 changes: 48 additions & 11 deletions internal/database/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,35 @@ func (db *DB) AddExportConfig(ctx context.Context, ec *ExportConfig) error {
})
}

func (db *DB) GetAllExportConfigs(ctx context.Context) ([]*ExportConfig, error) {
conn, err := db.Pool.Acquire(ctx)
if err != nil {
return nil, fmt.Errorf("acquiring connection: %w", err)
}
defer conn.Release()

rows, err := conn.Query(ctx, `
SELECT
config_id, bucket_name, filename_root, period_seconds, region, from_timestamp, thru_timestamp, signature_info_ids
FROM
ExportConfig`)
if err != nil {
return nil, err
}
defer rows.Close()

results := []*ExportConfig{}
for rows.Next() {
ec, err := scanOneExportConfig(rows)
if err != nil {
return nil, err
}
results = append(results, ec)
}

return results, nil
}

// IterateExportConfigs applies f to each ExportConfig whose FromTimestamp is
// before the given time. If f returns a non-nil error, the iteration stops, and
// the returned error will match f's error with errors.Is.
Expand Down Expand Up @@ -97,25 +126,33 @@ func (db *DB) IterateExportConfigs(ctx context.Context, t time.Time, f func(*Exp
}
defer rows.Close()
for rows.Next() {
var (
m ExportConfig
periodSeconds int
thru *time.Time
)
if err := rows.Scan(&m.ConfigID, &m.BucketName, &m.FilenameRoot, &periodSeconds, &m.Region, &m.From, &thru, &m.SignatureInfoIDs); err != nil {
m, err := scanOneExportConfig(rows)
if err != nil {
return err
}
m.Period = time.Duration(periodSeconds) * time.Second
if thru != nil {
m.Thru = *thru
}
if err := f(&m); err != nil {
if err = f(m); err != nil {
return err
}
}
return rows.Err()
}

func scanOneExportConfig(row pgx.Row) (*ExportConfig, error) {
var (
m ExportConfig
periodSeconds int
thru *time.Time
)
if err := row.Scan(&m.ConfigID, &m.BucketName, &m.FilenameRoot, &periodSeconds, &m.Region, &m.From, &thru, &m.SignatureInfoIDs); err != nil {
return nil, err
}
m.Period = time.Duration(periodSeconds) * time.Second
if thru != nil {
m.Thru = *thru
}
return &m, nil
}

func (db *DB) AddSignatureInfo(ctx context.Context, si *SignatureInfo) error {
if si.SigningKey == "" {
return fmt.Errorf("signing key cannot be empty for a signature info")
Expand Down
11 changes: 11 additions & 0 deletions internal/database/export_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ type ExportConfig struct {
SignatureInfoIDs []int64 `db:"signature_info_ids"`
}

func (e *ExportConfig) FormattedFromTime() string {
return e.From.Format(time.UnixDate)
}

func (e *ExportConfig) FormattedThruTime() string {
if e.Thru.IsZero() {
return ""
}
return e.Thru.Format(time.UnixDate)
}

type ExportBatch struct {
BatchID int64 `db:"batch_id" json:"batchID"`
ConfigID int64 `db:"config_id" json:"configID"`
Expand Down
60 changes: 60 additions & 0 deletions tools/admin-console/authorizedapps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, softwar
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"net/http"

authorizedappdb "github.com/google/exposure-notifications-server/internal/authorizedapp/database"
"github.com/google/exposure-notifications-server/internal/authorizedapp/model"
"github.com/google/exposure-notifications-server/internal/serverenv"
)

type appHandler struct {
config *Config
env *serverenv.ServerEnv
}

func NewAppHandler(c *Config, env *serverenv.ServerEnv) *appHandler {
return &appHandler{config: c, env: env}
}

func (h *appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
m := map[string]interface{}{}

appIds := r.URL.Query()["apn"]
appID := ""
if len(appIds) > 0 {
appID = appIds[0]
}

authorizedApp := &model.AuthorizedApp{}

if appID == "" {
m["new"] = true
} else {
aadb := authorizedappdb.NewAuthorizedAppDB(h.env.Database())
var err error
authorizedApp, err = aadb.GetAuthorizedApp(ctx, h.env.SecretManager(), appID)
if err != nil {
m["error"] = err
h.config.RenderTemplate(w, "error", &m)
return
}
}
m["app"] = authorizedApp
h.config.RenderTemplate(w, "app_view", &m)
}
54 changes: 54 additions & 0 deletions tools/admin-console/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// This tool provides a small admin UI. Requires connection to the database
// and permissions to access whatever else you might need to access.
package main

import (
"fmt"
"html/template"
"log"
"net/http"

"github.com/google/exposure-notifications-server/internal/database"
"github.com/google/exposure-notifications-server/internal/setup"
)

var _ setup.DBConfigProvider = (*Config)(nil)

type Config struct {
Port string `envconfig:"PORT" default:"8080"`
TemplatePath string `envconfig:"TEMPLATE_DIR" default:"tools/admin-console/templates"`
Database *database.Config
}

// DB returns the configuration for the databse.
func (c *Config) DB() *database.Config {
return c.Database
}

func (c *Config) RenderTemplate(w http.ResponseWriter, tmpl string, p *map[string]interface{}) {
file := fmt.Sprintf("%s/%s.html", c.TemplatePath, tmpl)
t, err := template.ParseFiles(file)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
if err := t.Execute(w, p); err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Fatalf("Error rendering html %v", err)
}
}
57 changes: 57 additions & 0 deletions tools/admin-console/index.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, softwar
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"net/http"

aadb "github.com/google/exposure-notifications-server/internal/authorizedapp/database"
"github.com/google/exposure-notifications-server/internal/serverenv"
)

type indexHandler struct {
config *Config
env *serverenv.ServerEnv
}

func NewIndexHandler(c *Config, env *serverenv.ServerEnv) *indexHandler {
return &indexHandler{config: c, env: env}
}

func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
m := map[string]interface{}{}

// Load authorized apps for index.
db := aadb.NewAuthorizedAppDB(h.env.Database())
apps, err := db.GetAllAuthorizedApps(ctx, h.env.SecretManager())
if err != nil {
m["error"] = err
h.config.RenderTemplate(w, "error", &m)
return
}
m["apps"] = apps

// Load export configurations.
exports, err := h.env.Database().GetAllExportConfigs(ctx)
if err != nil {
m["error"] = err
h.config.RenderTemplate(w, "error", &m)
return
}
m["exports"] = exports

h.config.RenderTemplate(w, "index", &m)
}
42 changes: 42 additions & 0 deletions tools/admin-console/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// This tool provides a small admin UI. Requires connection to the database
// and permissions to access whatever else you might need to access.
package main

import (
"context"
"log"
"net/http"

"github.com/google/exposure-notifications-server/internal/setup"
)

func main() {
ctx := context.Background()

var config Config
env, closer, err := setup.Setup(ctx, &config)
if err != nil {
log.Fatalf("setup.Setup: %v", err)
}
defer closer()

http.Handle("/", NewIndexHandler(&config, env))
http.Handle("/app", NewAppHandler(&config, env))

log.Printf("listening on http://localhost:" + config.Port)
log.Fatal(http.ListenAndServe(":"+config.Port, nil))
}
Loading

0 comments on commit 2865b87

Please sign in to comment.