go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/appengine/gaeauth/server/internal/authdbimpl/handlers.go (about) 1 // Copyright 2015 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package authdbimpl 16 17 import ( 18 "context" 19 "fmt" 20 "io" 21 "net/http" 22 "net/url" 23 24 "google.golang.org/appengine" 25 26 "go.chromium.org/luci/gae/service/info" 27 28 "go.chromium.org/luci/common/errors" 29 "go.chromium.org/luci/common/logging" 30 "go.chromium.org/luci/common/retry/transient" 31 "go.chromium.org/luci/server/auth/service" 32 "go.chromium.org/luci/server/router" 33 ) 34 35 const ( 36 pubSubPullURLPath = "/auth/pubsub/authdb:pull" // dev server only 37 pubSubPushURLPath = "/auth/pubsub/authdb:push" 38 ) 39 40 // InstallHandlers installs PubSub related HTTP handlers. 41 func InstallHandlers(r *router.Router, base router.MiddlewareChain) { 42 if appengine.IsDevAppServer() { 43 r.GET(pubSubPullURLPath, base, pubSubPull) 44 } 45 r.POST(pubSubPushURLPath, base, pubSubPush) 46 } 47 48 // setupPubSub creates a subscription to AuthDB service notification stream. 49 func setupPubSub(ctx context.Context, baseURL, authServiceURL string) error { 50 pushURL := "" 51 if !info.IsDevAppServer(ctx) { 52 pushURL = baseURL + pubSubPushURLPath // push in prod, pull on dev server 53 } 54 service := getAuthService(ctx, authServiceURL) 55 return service.EnsureSubscription(ctx, subscriptionName(ctx, authServiceURL), pushURL) 56 } 57 58 // killPubSub removes PubSub subscription created with setupPubSub. 59 func killPubSub(ctx context.Context, authServiceURL string) error { 60 service := getAuthService(ctx, authServiceURL) 61 return service.DeleteSubscription(ctx, subscriptionName(ctx, authServiceURL)) 62 } 63 64 // subscriptionName returns full PubSub subscription name for AuthDB 65 // change notifications stream from given auth service. 66 func subscriptionName(ctx context.Context, authServiceURL string) string { 67 subIDPrefix := "gae-v1" 68 if info.IsDevAppServer(ctx) { 69 subIDPrefix = "dev-app-server-v1" 70 } 71 serviceURL, err := url.Parse(authServiceURL) 72 if err != nil { 73 panic(err) 74 } 75 return fmt.Sprintf("projects/%s/subscriptions/%s+%s", info.AppID(ctx), subIDPrefix, serviceURL.Host) 76 } 77 78 // pubSubPull is HTTP handler that pulls PubSub messages from AuthDB change 79 // notification topic. 80 // 81 // Used only on dev server for manual testing. Prod services use push-based 82 // delivery. 83 func pubSubPull(c *router.Context) { 84 if !appengine.IsDevAppServer() { 85 replyError(c.Request.Context(), c.Writer, errors.New("not a dev server")) 86 return 87 } 88 processPubSubRequest(c.Request.Context(), c.Writer, c.Request, func(ctx context.Context, srv authService, serviceURL string) (*service.Notification, error) { 89 return srv.PullPubSub(ctx, subscriptionName(ctx, serviceURL)) 90 }) 91 } 92 93 // pubSubPush is HTTP handler that processes incoming PubSub push notifications. 94 // 95 // It uses the signature inside PubSub message body for authentication. Skips 96 // messages not signed by currently configured auth service. 97 func pubSubPush(c *router.Context) { 98 processPubSubRequest(c.Request.Context(), c.Writer, c.Request, func(ctx context.Context, srv authService, serviceURL string) (*service.Notification, error) { 99 body, err := io.ReadAll(c.Request.Body) 100 if err != nil { 101 return nil, err 102 } 103 return srv.ProcessPubSubPush(ctx, body) 104 }) 105 } 106 107 type notifcationGetter func(context.Context, authService, string) (*service.Notification, error) 108 109 // processPubSubRequest is common wrapper for pubSubPull and pubSubPush. 110 // 111 // It implements most logic of notification handling. Calls supplied callback 112 // to actually get service.Notification, since this part is different from Pull 113 // and Push subscriptions. 114 func processPubSubRequest(ctx context.Context, rw http.ResponseWriter, r *http.Request, callback notifcationGetter) { 115 ctx = defaultNS(ctx) 116 info, err := GetLatestSnapshotInfo(ctx) 117 if err != nil { 118 replyError(ctx, rw, err) 119 return 120 } 121 if info == nil { 122 // Return HTTP 200 to avoid a redelivery. 123 replyOK(ctx, rw, "Auth Service URL is not configured, skipping the message") 124 return 125 } 126 srv := getAuthService(ctx, info.AuthServiceURL) 127 128 notify, err := callback(ctx, srv, info.AuthServiceURL) 129 if err != nil { 130 replyError(ctx, rw, err) 131 return 132 } 133 134 // notify may be nil if PubSub messages didn't pass authentication. 135 if notify == nil { 136 replyOK(ctx, rw, "No new valid AuthDB change notifications") 137 return 138 } 139 140 // Don't bother processing late messages (ack them though). 141 latest := info 142 if notify.Revision > info.Rev { 143 var err error 144 if latest, err = syncAuthDB(ctx); err != nil { 145 replyError(ctx, rw, err) 146 return 147 } 148 } 149 150 if err := notify.Acknowledge(ctx); err != nil { 151 replyError(ctx, rw, err) 152 return 153 } 154 155 replyOK( 156 ctx, rw, "Processed PubSub notification for rev %d: %d -> %d", 157 notify.Revision, info.Rev, latest.Rev) 158 } 159 160 // replyError sends HTTP 500 on transient errors, HTTP 400 on fatal ones. 161 func replyError(ctx context.Context, rw http.ResponseWriter, err error) { 162 logging.Errorf(ctx, "Error while processing PubSub notification - %s", err) 163 if transient.Tag.In(err) { 164 http.Error(rw, err.Error(), http.StatusInternalServerError) 165 } else { 166 http.Error(rw, err.Error(), http.StatusBadRequest) 167 } 168 } 169 170 // replyOK sends HTTP 200. 171 func replyOK(ctx context.Context, rw http.ResponseWriter, msg string, args ...any) { 172 logging.Infof(ctx, msg, args...) 173 rw.Write([]byte(fmt.Sprintf(msg, args...))) 174 }