cmd/admingolangorg: run under IAP and support playground removals
In locking down our permissions, I blocked access to this site and
x/build/rmplaysnippet. Put the site behind IAP, and add playground
removal support.
Change-Id: I7722f5911f31a140c93780fe0417dd2368276926
Reviewed-on: https://go-review.googlesource.com/c/website/+/435455
Run-TryBot: Heschi Kreinick <heschi@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Auto-Submit: Heschi Kreinick <heschi@google.com>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
diff --git a/cmd/admingolangorg/README.md b/cmd/admingolangorg/README.md
index af4f7b3..dc80664 100644
--- a/cmd/admingolangorg/README.md
+++ b/cmd/admingolangorg/README.md
@@ -1,7 +1,6 @@
# admingolangorg
-This app serves as the [admin interface](https://admin-dot-golang-org.appspot.com) for the go.dev/s link
-shortener. Its functionality may be expanded in the future.
+This app serves as the [admin interface](https://admin-dot-golang-org.appspot.com) for the go.dev/s link shortener. It can also remove unwanted playground snippets.
## Deployment:
diff --git a/cmd/admingolangorg/app.yaml b/cmd/admingolangorg/app.yaml
index 9c77bf9..732c020 100644
--- a/cmd/admingolangorg/app.yaml
+++ b/cmd/admingolangorg/app.yaml
@@ -5,12 +5,12 @@
env_variables:
GOLANGORG_REDIS_ADDR: 10.0.0.4:6379 # instance "gophercache"
DATASTORE_PROJECT_ID: golang-org
+ IAP_AUDIENCE: /projects/397748307997/apps/golang-org
handlers:
- url: .*
script: auto
secure: always
- login: admin # THIS MUST BE SET
vpc_access_connector:
name: 'projects/golang-org/locations/us-central1/connectors/golang-vpc-connector'
diff --git a/cmd/admingolangorg/index.html b/cmd/admingolangorg/index.html
new file mode 100644
index 0000000..31a6f28
--- /dev/null
+++ b/cmd/admingolangorg/index.html
@@ -0,0 +1,11 @@
+<!--
+ Copyright 2022 The Go Authors. All rights reserved.
+ Use of this source code is governed by a BSD-style
+ license that can be found in the LICENSE file.
+-->
+
+<!doctype HTML>
+<html lang="en">
+ <p><a href="/shortlink">administer short links</a></p>
+ <p><a href="/snippet">remove a snippet</a></p>
+</html>
diff --git a/cmd/admingolangorg/main.go b/cmd/admingolangorg/main.go
index a8dd26a..fb358d6 100644
--- a/cmd/admingolangorg/main.go
+++ b/cmd/admingolangorg/main.go
@@ -8,18 +8,34 @@
import (
"context"
+ _ "embed"
+ "fmt"
"log"
"net/http"
"os"
"strings"
+ "time"
"cloud.google.com/go/datastore"
"golang.org/x/website/internal/memcache"
"golang.org/x/website/internal/short"
+ "google.golang.org/api/idtoken"
)
+//go:embed index.html
+var index string
+
func main() {
- http.HandleFunc("/", short.AdminHandler(getClients()))
+ audience := os.Getenv("IAP_AUDIENCE")
+ dsClient, mcClient := getClients()
+ mux := http.NewServeMux()
+ mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ w.Write([]byte(index))
+ return
+ }))
+ mux.Handle("/shortlink", short.AdminHandler(dsClient, mcClient))
+ mux.Handle("/snippet", &snippetHandler{dsClient})
port := os.Getenv("PORT")
if port == "" {
port = "8080"
@@ -27,7 +43,58 @@
}
log.Printf("Listening on port %s", port)
- log.Fatal(http.ListenAndServe(":"+port, nil))
+ log.Fatal(http.ListenAndServe(":"+port, iapAuth(audience, mux)))
+}
+
+type snippetHandler struct {
+ ds *datastore.Client
+}
+
+//go:embed snippet.html
+var snippetForm string
+
+func (h *snippetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ snippetLink := r.FormValue("snippet")
+ if snippetLink == "" {
+ w.Header().Set("Content-Type", "text/html")
+ w.Write([]byte(snippetForm))
+ return
+ }
+
+ prefixes := []string{
+ "https://play.golang.org/p/",
+ "http://play.golang.org/p/",
+ "https://go.dev/play/p/",
+ "http://go.dev/play/p/",
+ }
+ var snippetID string
+ for _, p := range prefixes {
+ if strings.HasPrefix(snippetLink, p) {
+ snippetID = strings.TrimPrefix(snippetLink, p)
+ break
+ }
+ }
+ if !strings.Contains(snippetLink, "/") {
+ snippetID = snippetLink
+ }
+ if snippetID == "" {
+ w.WriteHeader(http.StatusBadRequest)
+ fmt.Fprintf(w, "must specify snippet URL or ID\n")
+ return
+ }
+
+ k := datastore.NameKey("Snippet", snippetID, nil)
+ if h.ds.Get(r.Context(), k, new(struct{})) == datastore.ErrNoSuchEntity {
+ w.WriteHeader(http.StatusNotFound)
+ fmt.Fprintf(w, "Snippet with ID %q does not exist\n", snippetID)
+ return
+ }
+ if err := h.ds.Delete(r.Context(), k); err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ fmt.Fprintf(w, "Unable to delete Snippet with ID %q: %v\n", snippetID, err)
+ return
+ }
+ w.Write([]byte("snippet deleted\n"))
}
func getClients() (*datastore.Client, *memcache.Client) {
@@ -49,3 +116,33 @@
return datastoreClient, memcacheClient
}
+
+func iapAuth(audience string, h http.Handler) http.Handler {
+ // https://cloud.google.com/iap/docs/signed-headers-howto#verifying_the_jwt_payload
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ jwt := r.Header.Get("x-goog-iap-jwt-assertion")
+ if jwt == "" {
+ w.WriteHeader(http.StatusUnauthorized)
+ fmt.Fprintf(w, "must run under IAP\n")
+ return
+ }
+
+ payload, err := idtoken.Validate(r.Context(), jwt, audience)
+ if err != nil {
+ w.WriteHeader(http.StatusUnauthorized)
+ log.Printf("JWT validation error: %v", err)
+ return
+ }
+ if payload.Issuer != "https://cloud.google.com/iap" {
+ w.WriteHeader(http.StatusUnauthorized)
+ log.Printf("Incorrect issuer: %q", payload.Issuer)
+ return
+ }
+ if payload.Expires+30 < time.Now().Unix() || payload.IssuedAt-30 > time.Now().Unix() {
+ w.WriteHeader(http.StatusUnauthorized)
+ log.Printf("Bad JWT times: expires %v, issued %v", time.Unix(payload.Expires, 0), time.Unix(payload.IssuedAt, 0))
+ return
+ }
+ h.ServeHTTP(w, r)
+ })
+}
diff --git a/cmd/admingolangorg/snippet.html b/cmd/admingolangorg/snippet.html
new file mode 100644
index 0000000..fc51718
--- /dev/null
+++ b/cmd/admingolangorg/snippet.html
@@ -0,0 +1,15 @@
+<!--
+ Copyright 2022 The Go Authors. All rights reserved.
+ Use of this source code is governed by a BSD-style
+ license that can be found in the LICENSE file.
+-->
+
+<!doctype HTML>
+<html lang="en">
+<p>Delete a snippet</p>
+<form method="POST">
+<label for="snippet">Snippet URL</label>
+<input type="text" name="snippet" id="snippet" placeholder="https://go.dev/play/p/XXXXX" required>
+<input type="submit" name="submit" value="Remove">
+</form>
+</html>