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 }