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  }