diff --git a/cmd/server/assets/admin/new.html b/cmd/server/assets/admin/new.html
new file mode 100644
index 000000000..6939cfa2f
--- /dev/null
+++ b/cmd/server/assets/admin/new.html
@@ -0,0 +1,122 @@
+{{define "admin/newrealm"}}
+
+{{$realm := .realm}}
+
+
+
+
+ {{template "head" .}}
+
+
+
+ {{template "navbar" .}}
+
+
+ {{template "flash" .}}
+
+ New realm
+
+ Use the form below to create a new realm.
+
+
+
+
+
+ {{template "scripts" .}}
+
+
+{{end}}
diff --git a/cmd/server/assets/admin/realms.html b/cmd/server/assets/admin/realms.html
new file mode 100644
index 000000000..4f7583077
--- /dev/null
+++ b/cmd/server/assets/admin/realms.html
@@ -0,0 +1,53 @@
+{{define "admin/realms"}}
+
+
+
+
+ {{template "head" .}}
+
+
+
+ {{template "navbar" .}}
+
+
+ {{template "flash" .}}
+
+ Realms
+
+ These are the currently available realms. You can also create a new realm.
+
+
+ {{if .realms}}
+
+
+
+
+ Name |
+ Region code |
+ Signing key |
+
+
+
+ {{range .realms}}
+
+ {{.Name}} |
+ {{.RegionCode}} |
+
+ {{if .UseRealmCertificateKey}}Realm{{else}}System{{end}}
+ |
+
+ {{end}}
+
+
+
+ {{else}}
+
+ There are no realms.
+
+ {{end}}
+
+
+ {{template "scripts" .}}
+
+
+{{end}}
diff --git a/cmd/server/assets/header.html b/cmd/server/assets/header.html
index a5dfc3a39..2d995f255 100644
--- a/cmd/server/assets/header.html
+++ b/cmd/server/assets/header.html
@@ -189,6 +189,12 @@
{{end}}
{{end}}
+ {{if .currentUser.Admin}}
+
+ Realms
+
+ {{end}}
+
{{if gt (len .currentUser.Realms) 1}}
Change realm
diff --git a/cmd/server/main.go b/cmd/server/main.go
index 2314fb3c4..83a7b9257 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -26,6 +26,7 @@ import (
"github.com/google/exposure-notifications-verification-server/pkg/cache"
"github.com/google/exposure-notifications-verification-server/pkg/config"
"github.com/google/exposure-notifications-verification-server/pkg/controller"
+ "github.com/google/exposure-notifications-verification-server/pkg/controller/admin"
"github.com/google/exposure-notifications-verification-server/pkg/controller/apikey"
"github.com/google/exposure-notifications-verification-server/pkg/controller/codestatus"
"github.com/google/exposure-notifications-verification-server/pkg/controller/home"
@@ -180,6 +181,7 @@ func realMain(ctx context.Context) error {
requireVerified := middleware.RequireVerified(ctx, auth, db, h, config.SessionDuration)
requireAdmin := middleware.RequireRealmAdmin(ctx, h)
requireRealm := middleware.RequireRealm(ctx, cacher, db, h)
+ requireSystemAdmin := middleware.RequireAdmin(ctx, h)
rateLimit := httplimiter.Handle
{
@@ -336,6 +338,20 @@ func realMain(ctx context.Context) error {
jwksSub.Handle("/{realm}", jwksController.HandleIndex()).Methods("GET")
}
+ // System admin.
+ {
+ adminSub := r.PathPrefix("/admin").Subrouter()
+ adminSub.Use(requireAuth)
+ adminSub.Use(requireVerified)
+ adminSub.Use(requireSystemAdmin)
+ adminSub.Use(rateLimit)
+
+ adminController := admin.New(ctx, config, db, h)
+ adminSub.Handle("/realms", adminController.HandleIndex()).Methods("GET")
+ adminSub.Handle("/realms/create", adminController.HandleCreateRealm()).Methods("GET")
+ adminSub.Handle("/realms/create", adminController.HandleCreateRealm()).Methods("POST")
+ }
+
// Wrap the main router in the mutating middleware method. This cannot be
// inserted as middleware because gorilla processes the method before
// middleware.
diff --git a/pkg/controller/admin/admin.go b/pkg/controller/admin/admin.go
new file mode 100644
index 000000000..663b96849
--- /dev/null
+++ b/pkg/controller/admin/admin.go
@@ -0,0 +1,46 @@
+// 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.
+
+// Package admin contains controllers for system wide administrative actions.
+package admin
+
+import (
+ "context"
+
+ "github.com/google/exposure-notifications-verification-server/pkg/config"
+ "github.com/google/exposure-notifications-verification-server/pkg/database"
+ "github.com/google/exposure-notifications-verification-server/pkg/render"
+
+ "github.com/google/exposure-notifications-server/pkg/logging"
+
+ "go.uber.org/zap"
+)
+
+type Controller struct {
+ config *config.ServerConfig
+ db *database.Database
+ h *render.Renderer
+ logger *zap.SugaredLogger
+}
+
+func New(ctx context.Context, config *config.ServerConfig, db *database.Database, h *render.Renderer) *Controller {
+ logger := logging.FromContext(ctx).Named("admin")
+
+ return &Controller{
+ config: config,
+ db: db,
+ h: h,
+ logger: logger,
+ }
+}
diff --git a/pkg/controller/admin/create_realm.go b/pkg/controller/admin/create_realm.go
new file mode 100644
index 000000000..f9c4e48c6
--- /dev/null
+++ b/pkg/controller/admin/create_realm.go
@@ -0,0 +1,111 @@
+// 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.
+
+package admin
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+
+ "github.com/google/exposure-notifications-verification-server/pkg/controller"
+ "github.com/google/exposure-notifications-verification-server/pkg/database"
+)
+
+func (c *Controller) HandleCreateRealm() http.Handler {
+ type FormData struct {
+ Name string `form:"name"`
+ RegionCode string `form:"regionCode"`
+ UseRealmCertificateKey bool `form:"useRealmCertificateKey"`
+ CertificateIssuer string `form:"certificateIssuer"`
+ CertificateAudience string `form:"certificateAudiance"`
+ }
+
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ session := controller.SessionFromContext(ctx)
+ if session == nil {
+ controller.MissingSession(w, r, c.h)
+ return
+ }
+
+ user := controller.UserFromContext(ctx)
+ if user == nil {
+ controller.MissingUser(w, r, c.h)
+ return
+ }
+
+ flash := controller.Flash(session)
+
+ // Requested form, stop processing.
+ if r.Method == http.MethodGet {
+ var realm database.Realm
+ realm.UseRealmCertificateKey = true
+ c.renderNew(ctx, w, &realm)
+ return
+ }
+
+ var form FormData
+ if err := controller.BindForm(w, r, &form); err != nil {
+ var realm database.Realm
+ realm.UseRealmCertificateKey = true
+ flash.Error("Failed to process form: %v", err)
+ c.renderNew(ctx, w, &realm)
+ return
+ }
+
+ realm := database.NewRealmWithDefaults(form.Name)
+ realm.RegionCode = form.RegionCode
+ realm.UseRealmCertificateKey = form.UseRealmCertificateKey
+ realm.CertificateIssuer = form.CertificateIssuer
+ realm.CertificateAudience = form.CertificateAudience
+ if err := c.db.SaveRealm(realm); err != nil {
+ flash.Error("Failed to create realm: %v", err)
+ c.renderNew(ctx, w, realm)
+ return
+ }
+ flash.Alert("Created realm: %q.", realm.Name)
+
+ user.Realms = append(user.Realms, realm)
+ user.AdminRealms = append(user.AdminRealms, realm)
+ if err := c.db.SaveUser(user); err != nil {
+ flash.Error("Failed to add you as an admin to the realm: %v", err)
+ c.renderNew(ctx, w, realm)
+ return
+ }
+ flash.Alert("Added you as a user and admin to the realm.")
+
+ if realm.UseRealmCertificateKey {
+ // If we are using realm specific keys - we need to create the first one.
+ keyID, err := realm.CreateSigningKeyVersion(ctx, c.db)
+ if err != nil {
+ flash.Error("Failed to create signing keys for realm. This can be done from the realm's admin screens.")
+ http.Redirect(w, r, "/admin/realms", http.StatusSeeOther)
+ return
+ }
+ flash.Alert("Created initial signing key for realm, id: %q", keyID)
+ }
+
+ http.Redirect(w, r, "/admin/realms", http.StatusSeeOther)
+ })
+}
+
+func (c *Controller) renderNew(ctx context.Context, w http.ResponseWriter, realm *database.Realm) {
+ m := controller.TemplateMapFromContext(ctx)
+ fmt.Printf("errors %+v", realm.Errors())
+ m["realm"] = realm
+ m["supportsPerRealmSigning"] = c.db.SupportsPerRealmSigning()
+ c.h.RenderHTML(w, "admin/newrealm", m)
+}
diff --git a/pkg/controller/admin/index.go b/pkg/controller/admin/index.go
new file mode 100644
index 000000000..a4833fccd
--- /dev/null
+++ b/pkg/controller/admin/index.go
@@ -0,0 +1,37 @@
+// 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.
+
+package admin
+
+import (
+ "net/http"
+
+ "github.com/google/exposure-notifications-verification-server/pkg/controller"
+)
+
+func (c *Controller) HandleIndex() http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ realms, err := c.db.GetRealms()
+ if err != nil {
+ controller.InternalError(w, r, c.h, err)
+ return
+ }
+
+ m := controller.TemplateMapFromContext(ctx)
+ m["realms"] = realms
+ c.h.RenderHTML(w, "admin/realms", m)
+ })
+}
diff --git a/pkg/controller/user/create.go b/pkg/controller/user/create.go
index cc5ab7af7..09c64350e 100644
--- a/pkg/controller/user/create.go
+++ b/pkg/controller/user/create.go
@@ -68,6 +68,7 @@ func (c *Controller) HandleCreate() http.Handler {
// See if the user already exists by email - they may be a member of another
// realm.
user, err := c.db.FindUserByEmail(form.Email)
+ alreadyExists := true
if err != nil {
if !database.IsNotFound(err) {
controller.InternalError(w, r, c.h, err)
@@ -75,11 +76,14 @@ func (c *Controller) HandleCreate() http.Handler {
}
user = new(database.User)
+ alreadyExists = false
}
- // Build the user struct
- user.Email = form.Email
- user.Name = form.Name
+ // Build the user struct - keeping email and name if user already exists in another realm.
+ if !alreadyExists {
+ user.Email = form.Email
+ user.Name = form.Name
+ }
user.Realms = append(user.Realms, realm)
if form.Admin {
diff --git a/pkg/database/realm.go b/pkg/database/realm.go
index 498f9d098..99c8180dd 100644
--- a/pkg/database/realm.go
+++ b/pkg/database/realm.go
@@ -440,9 +440,6 @@ func (db *Database) GetRealms() ([]*Realm, error) {
}
func (db *Database) SaveRealm(r *Realm) error {
- if r.Model.ID == 0 {
- return db.db.Create(r).Error
- }
return db.db.Save(r).Error
}