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

Commit

Permalink
complete admin console for authorized apps
Browse files Browse the repository at this point in the history
  • Loading branch information
mikehelmick committed May 22, 2020
1 parent cd9fc52 commit 2350038
Show file tree
Hide file tree
Showing 13 changed files with 442 additions and 104 deletions.
55 changes: 54 additions & 1 deletion internal/authorizedapp/database/authorized_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"context"
"database/sql"
"fmt"
"strings"
"time"

"github.com/google/exposure-notifications-server/internal/authorizedapp/model"
Expand All @@ -37,10 +38,62 @@ func New(db *database.DB) *AuthorizedAppDB {
}
}

func (aa *AuthorizedAppDB) InsertAuthorizedApp(ctx context.Context, m *model.AuthorizedApp) error {
if errors := m.Validate(); len(errors) > 0 {
return fmt.Errorf("AuthorizedApp invalid: %v", strings.Join(errors, ", "))
}

return aa.db.InTx(ctx, pgx.Serializable, func(tx pgx.Tx) error {
result, err := tx.Exec(ctx, `
INSERT INTO
AuthorizedApp
(app_package_name, platform, allowed_regions,
safetynet_disabled, safetynet_apk_digest, safetynet_cts_profile_match, safetynet_basic_integrity, safetynet_past_seconds, safetynet_future_seconds,
devicecheck_disabled, devicecheck_team_id, devicecheck_key_id, devicecheck_private_key_secret)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
`, m.AppPackageName, m.Platform, m.AllAllowedRegions(),
m.SafetyNetDisabled, m.SafetyNetApkDigestSHA256, m.SafetyNetCTSProfileMatch, m.SafetyNetBasicIntegrity, int64(m.SafetyNetPastTime.Seconds()), int64(m.SafetyNetFutureTime.Seconds()),
m.DeviceCheckDisabled, m.DeviceCheckTeamID, m.DeviceCheckKeyID, m.DeviceCheckPrivateKeySecret)

if err != nil {
return fmt.Errorf("inserting authorizedapp: %w", err)
}
if result.RowsAffected() != 1 {
return fmt.Errorf("no rows inserted")
}
return nil
})
}

func (aa *AuthorizedAppDB) DeleteAuthorizedApp(ctx context.Context, appPackageName string) error {
var count int64
err := aa.db.InTx(ctx, pgx.Serializable, func(tx pgx.Tx) error {
result, err := tx.Exec(ctx, `
DELETE FROM
AuthorizedApp
WHERE
app_package_name = $1
`, appPackageName)
if err != nil {
return fmt.Errorf("deleting authorized app: %w", err)
}
count = result.RowsAffected()
return nil
})
if err != nil {
return err
}
if count == 0 {
return fmt.Errorf("no rows were deleted")
}
return nil
}

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)
return nil, fmt.Errorf("acquiring connection: %w", err)
}
defer conn.Release()

Expand Down
42 changes: 41 additions & 1 deletion internal/authorizedapp/database/authorized_app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,47 @@ func TestMain(m *testing.M) {
os.Exit(m.Run())
}

func TestAuthorizedAppLifecycle(t *testing.T) {
if testDB == nil {
t.Skip("no test DB")
}
defer coredb.ResetTestDB(t, testDB)
ctx := context.Background()
aadb := New(testDB)
sm := &testSecretManager{
values: map[string]string{},
}

source := &model.AuthorizedApp{
AppPackageName: "myapp",
Platform: "both",
AllowedRegions: map[string]struct{}{"US": {}},
SafetyNetDisabled: true,
DeviceCheckDisabled: true,
}

if err := aadb.InsertAuthorizedApp(ctx, source); err != nil {
t.Fatal(err)
}

readBack, err := aadb.GetAuthorizedApp(ctx, sm, source.AppPackageName)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(source, readBack); diff != "" {
t.Errorf("mismatch (-want, +got):\n%s", diff)
}

if err := aadb.DeleteAuthorizedApp(ctx, source.AppPackageName); err != nil {
t.Fatal(err)
}

readBack, err = aadb.GetAuthorizedApp(ctx, sm, source.AppPackageName)
if readBack != nil {
t.Fatal("expected record to be deleted, but it wasn't")
}
}

func TestGetAuthorizedApp(t *testing.T) {
if testDB == nil {
t.Skip("no test DB")
Expand Down Expand Up @@ -100,7 +141,6 @@ func TestGetAuthorizedApp(t *testing.T) {
INSERT INTO AuthorizedApp (app_package_name, platform, allowed_regions)
VALUES ($1, $2, $3)
`,

args: []interface{}{"myapp", "android", []string{"US"}},
exp: &model.AuthorizedApp{
AppPackageName: "myapp",
Expand Down
38 changes: 38 additions & 0 deletions internal/authorizedapp/model/authorized_app_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,44 @@ func NewAuthorizedApp() *AuthorizedApp {
}
}

func (c *AuthorizedApp) AllAllowedRegions() []string {
regions := []string{}
for k, _ := range c.AllowedRegions {
regions = append(regions, k)
}
return regions
}

func (c *AuthorizedApp) Validate() []string {
errors := make([]string, 0)
if c.AppPackageName == "" {
errors = append(errors, "AppPackageName cannot be empty")
}
if !(c.Platform == "android" || c.Platform == "ios" || c.Platform == "both") {
errors = append(errors, "platform must be one if {'android', 'ios', or 'both'}")
}
if c.SafetyNetDisabled == false {
if c.SafetyNetPastTime < 0 {
errors = append(errors, "SafetyNetPastTime cannot be negative")
}
if c.SafetyNetFutureTime < 0 {
errors = append(errors, "SafetyNetFutureTime cannot be negative")
}
}
if c.DeviceCheckDisabled == false {
if c.DeviceCheckKeyID == "" {
errors = append(errors, "When DeviceCheck is enabled, DeviceCheckKeyID cannot be empty")
}
if c.DeviceCheckTeamID == "" {
errors = append(errors, "When DeviceCheck is enabled, DeviceCheckTeamID cannot be empty")
}
if c.DeviceCheckPrivateKeySecret == "" {
errors = append(errors, "When DeviceCheck is enabled, DeviceCheckPrivateKeySecret cannot be empty")
}
}
return errors
}

// IsIOS returns true if the platform is equal to `iosDevice`
func (c *AuthorizedApp) IsIOS() bool {
return c.Platform == iosDevice || c.Platform == bothPlatforms
Expand Down
176 changes: 164 additions & 12 deletions tools/admin-console/authorizedapps.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import (
"encoding/base64"
"fmt"
"net/http"
"strconv"
"strings"
"time"

authorizedappdb "github.com/google/exposure-notifications-server/internal/authorizedapp/database"
"github.com/google/exposure-notifications-server/internal/authorizedapp/model"
Expand All @@ -37,24 +40,171 @@ func (h *appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
h.handleGet(w, r)
return
} else if r.Method == "POST" {
h.handlePost(w, r)
return
}
// else if r.Method == "POST" {
// h.handlePost(w, r)
// return
//}

m := map[string]interface{}{}
m["error"] = fmt.Errorf("Invalid request")
h.config.RenderTemplate(w, "error", &m)

m := TemplateMap{}
m.AddErrors("Invalid request.")
h.config.RenderTemplate(w, "error", m)
return
}

func (h *appHandler) renderError(m TemplateMap, msg string, w http.ResponseWriter) {
m.AddErrors(msg)
h.config.RenderTemplate(w, "error", m)
}

func populateAuthorizedApp(a *model.AuthorizedApp, r *http.Request) []string {
a.AppPackageName = r.FormValue("AppPackageName")
a.Platform = r.FormValue("Platform")

a.AllowedRegions = make(map[string]struct{})
regions := strings.Split(r.FormValue("Regions"), "\n")
for _, region := range regions {
a.AllowedRegions[region] = struct{}{}
}

var err error
errors := []string{}
// SafetyNet pieces.
a.SafetyNetDisabled, err = strconv.ParseBool(r.FormValue("SafetyNetDisabled"))
if err != nil {
errors = append(errors, fmt.Sprintf("SafetyNetDisabled, invalid value: %v", err))
}
a.SafetyNetApkDigestSHA256 = strings.Split(r.FormValue("SafetyNetApkDigestSHA256"), "\n")
a.SafetyNetBasicIntegrity, err = strconv.ParseBool(r.FormValue("SafetyNetBasicIntegrity"))
if err != nil {
errors = append(errors, fmt.Sprintf("SafetyNetBasicIntegrity, invalid value: %v", err))
}
a.SafetyNetCTSProfileMatch, err = strconv.ParseBool(r.FormValue("SafetyNetCTSProfileMatch"))
if err != nil {
errors = append(errors, fmt.Sprintf("SafetyNetCTSProfileMatch, invalid value: %v", err))
}
a.SafetyNetPastTime, err = time.ParseDuration(r.FormValue("SafetyNetPastTime"))
if err != nil {
errors = append(errors, fmt.Sprintf("SafetyNetPastTime, invalid value: %v", err))
}
a.SafetyNetFutureTime, err = time.ParseDuration(r.FormValue("SafetyNetFutureTime"))
if err != nil {
errors = append(errors, fmt.Sprintf("SafetyNetFutureTime, invalid value: %v", err))
}

// DeviceCheck pieces
a.DeviceCheckDisabled, err = strconv.ParseBool(r.FormValue("DeviceCheckDisabled"))
if err != nil {
errors = append(errors, fmt.Sprintf("DeviceCheckDisabled, invalid value: %v", err))
}
a.DeviceCheckKeyID = r.FormValue("DeviceCheckKeyID")
a.DeviceCheckTeamID = r.FormValue("DeviceCheckTeamID")
a.DeviceCheckPrivateKeySecret = r.FormValue("DeviceCheckPrivateKeySecret")

return errors
}

func decodePriorKey(priorKey string) (string, error) {
if priorKey != "" {
bytes, err := base64.StdEncoding.DecodeString(priorKey)
if err != nil {
return "", err
}
priorKey = string(bytes)
}
return priorKey, nil
}

func (h *appHandler) handlePost(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
m := TemplateMap{}
err := r.ParseForm()
if err != nil {
defer h.renderError(m, "Invalid request", w)
return
}

aadb := authorizedappdb.New(h.env.Database())

if action := r.FormValue("TODO"); action == "save" {
priorKey, err := decodePriorKey(r.FormValue("Key"))
if err != nil {
defer h.renderError(m, "Invalid request", w)
return
}

// Create new, or load previous.
authApp := model.NewAuthorizedApp()
if priorKey != "" {
authApp, err = aadb.GetAuthorizedApp(ctx, h.env.SecretManager(), priorKey)
if err != nil {
defer h.renderError(m, "Invalid request, app to edit not found.", w)
return
}
}
errors := populateAuthorizedApp(authApp, r)
if len(errors) > 0 {
m.AddErrors(errors...)
m["app"] = authApp
h.config.RenderTemplate(w, "app_view", m)
return
}

errors = authApp.Validate()
if len(errors) > 0 {
m.AddErrors(errors...)
m["app"] = authApp
h.config.RenderTemplate(w, "app_view", m)
return
}

if priorKey != "" {
if err := aadb.DeleteAuthorizedApp(ctx, priorKey); err != nil {
m.AddErrors(fmt.Sprintf("Error removing old version: %v", err))
m["app"] = authApp
h.config.RenderTemplate(w, "app_view", m)
return
}
}

if err := aadb.InsertAuthorizedApp(ctx, authApp); err != nil {
m.AddErrors(fmt.Sprintf("Error inserting authorized app: %v", err))
} else {
m.AddSuccess(fmt.Sprintf("Saved authorized app: %v", authApp.AppPackageName))
}
m["app"] = authApp
h.config.RenderTemplate(w, "app_view", m)
return

} else if action == "delete" {
priorKey, err := decodePriorKey(r.FormValue("Key"))
if err != nil {
defer h.renderError(m, "Invalid request", w)
return
}

_, err = aadb.GetAuthorizedApp(ctx, h.env.SecretManager(), priorKey)
if err != nil {
defer h.renderError(m, "Couldn't find record to delete.", w)
return
}

if err := aadb.DeleteAuthorizedApp(ctx, priorKey); err != nil {
defer h.renderError(m, fmt.Sprintf("Error deleting authorized app: %v", err), w)
return
}

m.AddSuccess(fmt.Sprintf("Successfully deleted app `%v`", priorKey))
m["app"] = model.NewAuthorizedApp()
h.config.RenderTemplate(w, "app_view", m)
return
}

h.renderError(m, "Invalid action requested", w)
}

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

appIds := r.URL.Query()["apn"]
appID := ""
Expand All @@ -65,18 +215,20 @@ func (h *appHandler) handleGet(w http.ResponseWriter, r *http.Request) {
authorizedApp := model.NewAuthorizedApp()

if appID == "" {
m.AddJumbotron("Authorized Applications", "Create New Authorized Application")
m["new"] = true
} else {
aadb := authorizedappdb.NewAuthorizedAppDB(h.env.Database())
aadb := authorizedappdb.New(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)
h.config.RenderTemplate(w, "error", m)
return
}
m.AddJumbotron("Authorized Applications", fmt.Sprintf("Edit: `%v`", authorizedApp.AppPackageName))
}
m["app"] = authorizedApp
m["previousKey"] = base64.StdEncoding.EncodeToString([]byte(authorizedApp.AppPackageName))
h.config.RenderTemplate(w, "app_view", &m)
h.config.RenderTemplate(w, "app_view", m)
}
Loading

0 comments on commit 2350038

Please sign in to comment.