github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/controlsocket/handlers.go (about) 1 // Copyright 2023 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package controlsocket 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "io" 10 "net/http" 11 "strings" 12 13 "github.com/gorilla/mux" 14 "github.com/juju/errors" 15 "github.com/juju/names/v5" 16 17 "github.com/juju/juju/core/permission" 18 "github.com/juju/juju/environs/bootstrap" 19 "github.com/juju/juju/state" 20 stateerrors "github.com/juju/juju/state/errors" 21 ) 22 23 const ( 24 // jujuMetricsUserPrefix defines the "namespace" in which this worker is 25 // allowed to create/remove users. 26 jujuMetricsUserPrefix = "juju-metrics-" 27 28 // userCreator is the listed "creator" of metrics users in state. 29 // This user CANNOT be a local user (it must have a domain), otherwise the 30 // model addUser code will complain about the user not existing. 31 userCreator = "controller@juju" 32 ) 33 34 func (w *Worker) registerHandlers(r *mux.Router) { 35 r.HandleFunc("/metrics-users", w.handleAddMetricsUser). 36 Methods(http.MethodPost) 37 r.HandleFunc("/metrics-users/{username}", w.handleRemoveMetricsUser). 38 Methods(http.MethodDelete) 39 } 40 41 type addMetricsUserBody struct { 42 Username string `json:"username"` 43 Password string `json:"password"` 44 } 45 46 func (w *Worker) handleAddMetricsUser(resp http.ResponseWriter, req *http.Request) { 47 var parsedBody addMetricsUserBody 48 defer req.Body.Close() 49 err := json.NewDecoder(req.Body).Decode(&parsedBody) 50 if errors.Is(err, io.EOF) { 51 w.writeResponse(resp, http.StatusBadRequest, errorf("missing request body")) 52 return 53 } else if err != nil { 54 w.writeResponse(resp, http.StatusBadRequest, errorf("request body is not valid JSON: %v", err)) 55 return 56 } 57 58 code, err := w.addMetricsUser(parsedBody.Username, parsedBody.Password) 59 if err != nil { 60 w.writeResponse(resp, code, errorf("%v", err)) 61 return 62 } 63 64 w.writeResponse(resp, code, infof("created user %q", parsedBody.Username)) 65 } 66 67 func (w *Worker) addMetricsUser(username, password string) (int, error) { 68 err := validateMetricsUsername(username) 69 if err != nil { 70 return http.StatusBadRequest, err 71 } 72 73 if password == "" { 74 return http.StatusBadRequest, errors.NotValidf("empty password") 75 } 76 77 user, err := w.config.State.AddUser(username, username, password, userCreator) 78 cleanup := true 79 // Error handling here is a bit subtle. 80 switch { 81 case errors.Is(err, errors.AlreadyExists): 82 // Retrieve existing user 83 user, err = w.config.State.User(names.NewUserTag(username)) 84 if err != nil { 85 return http.StatusInternalServerError, 86 fmt.Errorf("retrieving existing user %q: %v", username, err) 87 } 88 89 // We want this operation to be idempotent, but at the same time, this 90 // worker shouldn't mess with users that have not been created by it. 91 // So ensure the user is identical to what we would have created, and 92 // otherwise error. 93 if user.CreatedBy() != userCreator { 94 return http.StatusConflict, errors.AlreadyExistsf("user %q (created by %q)", user.Name(), user.CreatedBy()) 95 } 96 if !user.PasswordValid(password) { 97 return http.StatusConflict, errors.AlreadyExistsf("user %q", user.Name()) 98 } 99 100 case err == nil: 101 // At this point, the operation is in a partially completed state - we've 102 // added the user, but haven't granted them the correct model permissions. 103 // If there is an error granting permissions, we should attempt to "rollback" 104 // and remove the user again. 105 defer func() { 106 if cleanup == false { 107 // Operation successful - nothing to clean up 108 return 109 } 110 111 err := w.config.State.RemoveUser(user.UserTag()) 112 if err != nil { 113 // Best we can do here is log an error. 114 w.config.Logger.Warningf("add metrics user failed, but could not clean up user %q: %v", 115 username, err) 116 } 117 }() 118 119 default: 120 return http.StatusInternalServerError, errors.Annotatef(err, "failed to create user %q: %v", username, err) 121 } 122 123 // Give the new user permission to access the metrics endpoint. 124 var model model 125 model, err = w.config.State.Model() 126 if err != nil { 127 return http.StatusInternalServerError, errors.Annotatef(err, "retrieving current model: %v", err) 128 } 129 130 _, err = model.AddUser(state.UserAccessSpec{ 131 User: user.UserTag(), 132 CreatedBy: names.NewUserTag(userCreator), 133 Access: permission.ReadAccess, 134 }) 135 if err != nil && !errors.Is(err, errors.AlreadyExists) { 136 return http.StatusInternalServerError, errors.Annotatef(err, "adding user %q to model %q: %v", username, bootstrap.ControllerModelName, err) 137 } 138 139 cleanup = false 140 return http.StatusOK, nil 141 } 142 143 func (w *Worker) handleRemoveMetricsUser(resp http.ResponseWriter, req *http.Request) { 144 username := mux.Vars(req)["username"] 145 code, err := w.removeMetricsUser(username) 146 if err != nil { 147 w.writeResponse(resp, code, errorf("%v", err)) 148 return 149 } 150 151 w.writeResponse(resp, code, infof("deleted user %q", username)) 152 } 153 154 func (w *Worker) removeMetricsUser(username string) (int, error) { 155 err := validateMetricsUsername(username) 156 if err != nil { 157 return http.StatusBadRequest, err 158 } 159 160 userTag := names.NewUserTag(username) 161 // We shouldn't mess with users that weren't created by us. 162 user, err := w.config.State.User(userTag) 163 if errors.Is(err, errors.NotFound) || errors.Is(err, errors.UserNotFound) || stateerrors.IsDeletedUserError(err) { 164 // succeed as no-op 165 return http.StatusOK, nil 166 } else if err != nil { 167 return http.StatusInternalServerError, err 168 } 169 if user.CreatedBy() != userCreator { 170 return http.StatusForbidden, errors.Forbiddenf("cannot remove user %q created by %q", user.Name(), user.CreatedBy()) 171 } 172 173 err = w.config.State.RemoveUser(userTag) 174 // Any "not found" errors should have been caught above, so fail here. 175 if err != nil { 176 return http.StatusInternalServerError, err 177 } 178 179 return http.StatusOK, nil 180 } 181 182 func validateMetricsUsername(username string) error { 183 if username == "" { 184 return errors.BadRequestf("missing username") 185 } 186 187 if !names.IsValidUserName(username) { 188 return errors.NotValidf("username %q", username) 189 } 190 191 if !strings.HasPrefix(username, jujuMetricsUserPrefix) { 192 return errors.BadRequestf("metrics username %q should have prefix %q", username, jujuMetricsUserPrefix) 193 } 194 195 return nil 196 } 197 198 func (w *Worker) writeResponse(resp http.ResponseWriter, statusCode int, body any) { 199 w.config.Logger.Debugf("operation finished with HTTP status %v", statusCode) 200 resp.Header().Set("Content-Type", "application/json") 201 202 message, err := json.Marshal(body) 203 if err != nil { 204 w.config.Logger.Errorf("error marshalling response body to JSON: %v", err) 205 w.config.Logger.Errorf("response body was %#v", body) 206 207 // Mark this as an "internal server error" 208 statusCode = http.StatusInternalServerError 209 // Just write an empty response 210 message = []byte("{}") 211 } 212 213 resp.WriteHeader(statusCode) 214 w.config.Logger.Tracef("returning response %q", message) 215 _, err = resp.Write(message) 216 if err != nil { 217 w.config.Logger.Warningf("error writing HTTP response: %v", err) 218 } 219 } 220 221 // infof returns an informational response body that can be marshalled into 222 // JSON (in the case of a successful operation). It has the form 223 // 224 // {"message": <provided info message>} 225 func infof(format string, args ...any) any { 226 return struct { 227 Message string `json:"message"` 228 }{ 229 Message: fmt.Sprintf(format, args...), 230 } 231 } 232 233 // errorf returns an error response body that can be marshalled into JSON (in 234 // the case of a failed operation). It has the form 235 // 236 // {"error": <provided error message>} 237 func errorf(format string, args ...any) any { 238 return struct { 239 Error string `json:"error"` 240 }{ 241 Error: fmt.Sprintf(format, args...), 242 } 243 }