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  }