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. +

+ +
+
Details
+
+
+ {{ .csrfField }} + +
+ +
+ + {{if $realm.ErrorsFor "name"}} +
+ {{joinStrings ($realm.ErrorsFor "name") ", "}} +
+ {{end}} +
+
+ +
+ +
+ + {{if $realm.ErrorsFor "regionCode"}} +
+ {{joinStrings ($realm.ErrorsFor "regionCode") ", "}} +
+ {{end}} + + Used in creating deep link SMS for multi-helath authority apps. Region should + be ISO 3166-1 country codes and ISO 3166-2 subdivision codes where applicable. + For example, Washington State would be US-WA. + +
+
+ + {{if .supportsPerRealmSigning}} +
+ +
+ + {{if $realm.ErrorsFor "UseRealmCertificateKey"}} +
+ {{joinStrings ($realm.ErrorsFor "UseRealmCertificateKey") ", "}} +
+ {{end}} + + It is recommended that you create a realm specific signing key when creating a new realm. However, it + is important to note that this once a realm is created, you cannot switch back to using the system + signing key. + +
+
+ +
+ +
+ + {{if $realm.ErrorsFor "certificateIssuer"}} +
+ {{joinStrings ($realm.ErrorsFor "certificateIssuer") ", "}} +
+ {{end}} + + This value is specific to the health authority.
After created using realm specific keys, this field cannot be changed. +
+
+
+
+ +
+ + {{if $realm.ErrorsFor "certificateAudience"}} +
+ {{joinStrings ($realm.ErrorsFor "certificateAudience") ", "}} +
+ {{end}} + + The audience (aud) value is provided the key server operator.
+ After upgrading to use realm specific keys, this field cannot be changed. +
+
+
+ {{end}} + +
+
+ +
+
+
+
+
+
+ + {{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}} +
+ + + + + + + + + + {{range .realms}} + + + + + + {{end}} + +
NameRegion codeSigning key
{{.Name}}{{.RegionCode}} + {{if .UseRealmCertificateKey}}Realm{{else}}System{{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 }