func (st *state) loginForVersion(tag names.Tag, password, nonce string, macaroons []macaroon.Slice, vers int) error { var result params.LoginResultV1 request := ¶ms.LoginRequest{ AuthTag: tagToString(tag), Credentials: password, Nonce: nonce, Macaroons: macaroons, } if tag == nil { // Add any macaroons from the cookie jar that might work for // authenticating the login request. request.Macaroons = append(request.Macaroons, httpbakery.MacaroonsForURL(st.bakeryClient.Client.Jar, st.cookieURL)..., ) } err := st.APICall("Admin", vers, "", "Login", request, &result) if err != nil { return errors.Trace(err) } if result.DischargeRequired != nil { // The result contains a discharge-required // macaroon. We discharge it and retry // the login request with the original macaroon // and its discharges. if result.DischargeRequiredReason == "" { result.DischargeRequiredReason = "no reason given for discharge requirement" } if err := st.bakeryClient.HandleError(st.cookieURL, &httpbakery.Error{ Message: result.DischargeRequiredReason, Code: httpbakery.ErrDischargeRequired, Info: &httpbakery.ErrorInfo{ Macaroon: result.DischargeRequired, MacaroonPath: "/", }, }); err != nil { return errors.Trace(err) } // Add the macaroons that have been saved by HandleError to our login request. request.Macaroons = httpbakery.MacaroonsForURL(st.bakeryClient.Client.Jar, st.cookieURL) result = params.LoginResultV1{} // zero result err = st.APICall("Admin", vers, "", "Login", request, &result) if err != nil { return errors.Trace(err) } if result.DischargeRequired != nil { return errors.Errorf("login with discharged macaroons failed: %s", result.DischargeRequiredReason) } } servers := params.NetworkHostsPorts(result.Servers) err = st.setLoginResult(tag, result.ModelTag, result.ControllerTag, servers, result.Facades) if err != nil { return errors.Trace(err) } st.serverVersion, err = version.Parse(result.ServerVersion) if err != nil { return errors.Trace(err) } return nil }
func (a *admin) doLogin(req params.LoginRequest, loginVersion int) (params.LoginResultV1, error) { var fail params.LoginResultV1 a.mu.Lock() defer a.mu.Unlock() if a.loggedIn { // This can only happen if Login is called concurrently. return fail, errAlreadyLoggedIn } // authedApi is the API method finder we'll use after getting logged in. var authedApi rpc.MethodFinder = newApiRoot(a.root.state, a.root.resources, a.root) // Use the login validation function, if one was specified. if a.srv.validator != nil { err := a.srv.validator(req) switch err { case params.UpgradeInProgressError: authedApi = newUpgradingRoot(authedApi) case AboutToRestoreError: authedApi = newAboutToRestoreRoot(authedApi) case RestoreInProgressError: authedApi = newRestoreInProgressRoot(authedApi) case nil: // in this case no need to wrap authed api so we do nothing default: return fail, errors.Trace(err) } } var agentPingerNeeded = true isUser := true kind := names.UserTagKind if req.AuthTag != "" { var err error kind, err = names.TagKind(req.AuthTag) if err != nil || kind != names.UserTagKind { isUser = false // Users are not rate limited, all other entities are. if !a.srv.limiter.Acquire() { logger.Debugf("rate limiting for agent %s", req.AuthTag) return fail, common.ErrTryAgain } defer a.srv.limiter.Release() } } serverOnlyLogin := a.root.modelUUID == "" entity, lastConnection, err := doCheckCreds(a.root.state, req, !serverOnlyLogin, a.srv.authCtxt) if err != nil { if err, ok := errors.Cause(err).(*common.DischargeRequiredError); ok { loginResult := params.LoginResultV1{ DischargeRequired: err.Macaroon, DischargeRequiredReason: err.Error(), } logger.Infof("login failed with discharge-required error: %v", err) return loginResult, nil } if a.maintenanceInProgress() { // An upgrade, restore or similar operation is in // progress. It is possible for logins to fail until this // is complete due to incomplete or updating data. Mask // transitory and potentially confusing errors from failed // logins with a more helpful one. return fail, MaintenanceNoLoginError } // Here we have a special case. The machine agents that manage // models in the controller model need to be able to // open API connections to other models. In those cases, we // need to look in the controller database to check the creds // against the machine if and only if the entity tag is a machine tag, // and the machine exists in the controller model, and the // machine has the manage state job. If all those parts are valid, we // can then check the credentials against the controller model // machine. if kind != names.MachineTagKind { return fail, errors.Trace(err) } entity, err = a.checkCredsOfControllerMachine(req) if err != nil { return fail, errors.Trace(err) } // If we are here, then the entity will refer to a controller // machine in the controller model, and we don't need a pinger // for it as we already have one running in the machine agent api // worker for the controller model. agentPingerNeeded = false } a.root.entity = entity if a.reqNotifier != nil { a.reqNotifier.login(entity.Tag().String()) } // We have authenticated the user; enable the appropriate API // to serve to them. a.loggedIn = true if agentPingerNeeded { if err := startPingerIfAgent(a.root, entity); err != nil { return fail, errors.Trace(err) } } var maybeUserInfo *params.AuthUserInfo var envUser *state.ModelUser // Send back user info if user if isUser && !serverOnlyLogin { maybeUserInfo = ¶ms.AuthUserInfo{ Identity: entity.Tag().String(), LastConnection: lastConnection, } envUser, err = a.root.state.ModelUser(entity.Tag().(names.UserTag)) if err != nil { return fail, errors.Annotatef(err, "missing ModelUser for logged in user %s", entity.Tag()) } if envUser.ReadOnly() { logger.Debugf("model user %s is READ ONLY", entity.Tag()) } } // Fetch the API server addresses from state. hostPorts, err := a.root.state.APIHostPorts() if err != nil { return fail, errors.Trace(err) } logger.Debugf("hostPorts: %v", hostPorts) environ, err := a.root.state.Model() if err != nil { return fail, errors.Trace(err) } loginResult := params.LoginResultV1{ Servers: params.FromNetworkHostsPorts(hostPorts), ModelTag: environ.Tag().String(), ControllerTag: environ.ControllerTag().String(), Facades: DescribeFacades(), UserInfo: maybeUserInfo, ServerVersion: jujuversion.Current.String(), } // For sufficiently modern login versions, stop serving the // controller model at the root of the API. if serverOnlyLogin { authedApi = newRestrictedRoot(authedApi) // Remove the ModelTag from the response as there is no // model here. loginResult.ModelTag = "" // Strip out the facades that are not supported from the result. var facades []params.FacadeVersions for _, facade := range loginResult.Facades { if restrictedRootNames.Contains(facade.Name) { facades = append(facades, facade) } } loginResult.Facades = facades } if envUser != nil { authedApi = newClientAuthRoot(authedApi, envUser) } a.root.rpcConn.ServeFinder(authedApi, serverError) return loginResult, nil }
func (st *state) loginV2(tag names.Tag, password, nonce string) error { var result params.LoginResultV1 request := ¶ms.LoginRequest{ AuthTag: tagToString(tag), Credentials: password, Nonce: nonce, } if tag == nil { // Add any macaroons that might work for authenticating the login request. request.Macaroons = httpbakery.MacaroonsForURL(st.bakeryClient.Client.Jar, st.cookieURL) } err := st.APICall("Admin", 2, "", "Login", request, &result) if err != nil { // If the server complains about an empty tag it may be that we are // talking to an older server version that does not understand facades and // expects a params.Creds request instead of a params.LoginRequest. We // return a CodNotImplemented error to force login down to V1, which // supports older server logins. This may mask an actual empty tag in // params.LoginRequest, but that would be picked up in loginV1. V1 will // also produce a warning that we are ignoring an invalid API, so we do not // need to add one here. if err.Error() == `"" is not a valid tag` { return ¶ms.Error{ Message: err.Error(), Code: params.CodeNotImplemented, } } return errors.Trace(err) } if result.DischargeRequired != nil { // The result contains a discharge-required // macaroon. We discharge it and retry // the login request with the original macaroon // and its discharges. if result.DischargeRequiredReason == "" { result.DischargeRequiredReason = "no reason given for discharge requirement" } if err := st.bakeryClient.HandleError(st.cookieURL, &httpbakery.Error{ Message: result.DischargeRequiredReason, Code: httpbakery.ErrDischargeRequired, Info: &httpbakery.ErrorInfo{ Macaroon: result.DischargeRequired, MacaroonPath: "/", }, }); err != nil { return errors.Trace(err) } // Add the macaroons that have been saved by HandleError to our login request. request.Macaroons = httpbakery.MacaroonsForURL(st.bakeryClient.Client.Jar, st.cookieURL) result = params.LoginResultV1{} // zero result err = st.APICall("Admin", 2, "", "Login", request, &result) if err != nil { return errors.Trace(err) } if result.DischargeRequired != nil { return errors.Errorf("login with discharged macaroons failed: %s", result.DischargeRequiredReason) } } servers := params.NetworkHostsPorts(result.Servers) err = st.setLoginResult(tag, result.EnvironTag, result.ServerTag, servers, result.Facades) if err != nil { return errors.Trace(err) } st.serverVersion, err = version.Parse(result.ServerVersion) if err != nil { return errors.Trace(err) } return nil }