go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/config/appengine/gaeconfig/validation.go (about)

     1  // Copyright 2018 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 gaeconfig
    16  
    17  import (
    18  	"context"
    19  	"net/http"
    20  	"strings"
    21  
    22  	"go.chromium.org/luci/gae/service/info"
    23  
    24  	"go.chromium.org/luci/appengine/gaeauth/server"
    25  	"go.chromium.org/luci/auth/identity"
    26  	"go.chromium.org/luci/common/logging"
    27  	"go.chromium.org/luci/config/server/cfgmodule"
    28  	"go.chromium.org/luci/config/validation"
    29  	"go.chromium.org/luci/config/vars"
    30  	"go.chromium.org/luci/server/auth"
    31  	"go.chromium.org/luci/server/auth/signing"
    32  	"go.chromium.org/luci/server/router"
    33  )
    34  
    35  func init() {
    36  	RegisterVars(&vars.Vars)
    37  }
    38  
    39  // RegisterVars registers placeholders that can be used in config set names.
    40  //
    41  // Registers:
    42  //
    43  //	${appid} - expands into a GAE app ID of the running service.
    44  //	${config_service_appid} - expands into a GAE app ID of a LUCI Config
    45  //	    service that the running service is using (or empty string if
    46  //	    unconfigured).
    47  //
    48  // This function is called during init() with the default var set.
    49  func RegisterVars(vars *vars.VarSet) {
    50  	vars.Register("appid", func(c context.Context) (string, error) {
    51  		return info.TrimmedAppID(c), nil
    52  	})
    53  	vars.Register("config_service_appid", GetConfigServiceAppID)
    54  }
    55  
    56  // InstallValidationHandlers installs handlers for config validation.
    57  //
    58  // It ensures that caller is either the config service itself or a member of a
    59  // trusted group, both of which are configurable in the appengine app settings.
    60  // It requires that the hostname, the email of config service and the name of
    61  // the trusted group have been defined in the appengine app settings page before
    62  // the installed endpoints are called.
    63  func InstallValidationHandlers(r *router.Router, base router.MiddlewareChain, rules *validation.RuleSet) {
    64  	a := auth.Authenticator{
    65  		Methods: []auth.Method{
    66  			&server.OAuth2Method{Scopes: []string{server.EmailScope}},
    67  		},
    68  	}
    69  	base = base.Extend(a.GetMiddleware(), func(c *router.Context, next router.Handler) {
    70  		cc, w := c.Request.Context(), c.Writer
    71  		switch yep, err := isAuthorizedCall(cc, mustFetchCachedSettings(cc)); {
    72  		case err != nil:
    73  			errStatus(cc, w, err, http.StatusInternalServerError, "Unable to perform authorization")
    74  		case !yep:
    75  			errStatus(cc, w, nil, http.StatusForbidden, "Insufficient authority for validation")
    76  		default:
    77  			next(c)
    78  		}
    79  	})
    80  	cfgmodule.InstallHandlers(r, base, rules)
    81  }
    82  
    83  func errStatus(c context.Context, w http.ResponseWriter, err error, status int, msg string) {
    84  	if status >= http.StatusInternalServerError {
    85  		if err != nil {
    86  			c = logging.SetError(c, err)
    87  		}
    88  		logging.Errorf(c, "%s", msg)
    89  	}
    90  	w.WriteHeader(status)
    91  	w.Write([]byte(msg))
    92  }
    93  
    94  // isAuthorizedCall returns true if the current caller is allowed to call the
    95  // config validation endpoints.
    96  //
    97  // This is either the service account of the config service, or someone from
    98  // an admin group.
    99  func isAuthorizedCall(c context.Context, s *Settings) (bool, error) {
   100  	// Someone from an admin group (if it is configured)? This is useful locally
   101  	// during development.
   102  	if s.AdministratorsGroup != "" {
   103  		switch yep, err := auth.IsMember(c, s.AdministratorsGroup); {
   104  		case err != nil:
   105  			return false, err
   106  		case yep:
   107  			return true, nil
   108  		}
   109  	}
   110  
   111  	// The config server itself (if it is configured)? May be empty when
   112  	// running stuff locally.
   113  	if s.ConfigServiceHost != "" {
   114  		info, err := signing.FetchServiceInfoFromLUCIService(c, "https://"+s.ConfigServiceHost)
   115  		if err != nil {
   116  			return false, err
   117  		}
   118  		caller := auth.CurrentIdentity(c)
   119  		if caller.Kind() == identity.User && caller.Value() == info.ServiceAccountName {
   120  			return true, nil
   121  		} else {
   122  			// TODO(yiwzhang): Temporarily allow both old and new LUCI Config service
   123  			// account to make the validation request. Revert this change after the
   124  			// old LUCI Config service is fully deprecated and all traffic have been
   125  			// migrated to the new LUCI Config service.
   126  			allowedGroup := "service-accounts-luci-config"
   127  			if strings.Contains(s.ConfigServiceHost, "dev") {
   128  				allowedGroup += "-dev"
   129  			}
   130  			switch yes, err := auth.IsMember(c, allowedGroup); {
   131  			case err != nil:
   132  				return false, err
   133  			case yes:
   134  				return true, nil
   135  			}
   136  		}
   137  	}
   138  
   139  	// A total stranger.
   140  	return false, nil
   141  }
   142  
   143  // GetConfigServiceAppID looks up the app ID of the LUCI Config service, as set
   144  // in the app's settings.
   145  //
   146  // Returns an empty string if the LUCI Config integration is not configured for
   147  // the app.
   148  func GetConfigServiceAppID(c context.Context) (string, error) {
   149  	s, err := FetchCachedSettings(c)
   150  	switch {
   151  	case err != nil:
   152  		return "", err
   153  	case s.ConfigServiceHost == "":
   154  		return "", nil
   155  	}
   156  	info, err := signing.FetchServiceInfoFromLUCIService(c, "https://"+s.ConfigServiceHost)
   157  	if err != nil {
   158  		return "", err
   159  	}
   160  	return info.AppID, nil
   161  }