mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 01:18:42 +00:00
Add server support for Fleet Sandbox demo login (#6387)
* Add server support for Fleet Sandbox demo login
This adds an endpoint `/api/latest/fleet/demologin` that provides a
redirect for the fleetdm.com portion of Fleet Sandbox to automatically
log in a user. The username and password must be provided as form
values. The endpoint is only enabled if `FLEET_DEMO=1` is set in the
server environment.
This was tested locally with the following HTML served by `python3 -m
http.server`, and the Fleet server running with `FLEET_DEMO=1
./build/fleet serve --dev`:
```
<!DOCTYPE html>
<body>
<form
method="post"
action="https://localhost:8080/api/latest/fleet/demologin"
id="demologin"
>
<input type="hidden" name="email" value="admin@example.com" />
<input type="hidden" name="password" value="admin123123#" />
<input type="submit"/>
</form>
<script type="text/javascript">
document.forms["demologin"].submit();
</script>
</body>
</html>
```
For Fleet sandbox purposes, the `action` should be set to the correct
hostname for the sandbox instance, while the `email` and `password`
should be set to the same credentials that were provided when creating
the instance.
* lucas comments
* Add integration tests
* Fix status codes and add comments
Co-authored-by: Martin Angers <martin.n.angers@gmail.com>
This commit is contained in:
parent
2fa678cb19
commit
03734a37aa
4 changed files with 133 additions and 0 deletions
|
|
@ -445,6 +445,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
|||
ne.POST("/api/v1/fleet/sso", initiateSSOEndpoint, initiateSSORequest{})
|
||||
ne.POST("/api/v1/fleet/sso/callback", makeCallbackSSOEndpoint(config.Server.URLPrefix), callbackSSORequest{})
|
||||
ne.GET("/api/v1/fleet/sso", settingsSSOEndpoint, nil)
|
||||
|
||||
// the websocket distributed query results endpoint is a bit different - the
|
||||
// provided path is a prefix, not an exact match, and it is not a go-kit
|
||||
// endpoint but a raw http.Handler. It uses the NoAuthEndpointer because
|
||||
|
|
@ -464,6 +465,10 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
|||
|
||||
ne.WithCustomMiddleware(limiter.Limit("login", throttled.RateQuota{MaxRate: loginRateLimit, MaxBurst: 9})).
|
||||
POST("/api/_version_/fleet/login", loginEndpoint, loginRequest{})
|
||||
|
||||
// Fleet Sandbox demo login (always errors unless FLEET_DEMO environment variable is set)
|
||||
ne.WithCustomMiddleware(limiter.Limit("login", throttled.RateQuota{MaxRate: loginRateLimit, MaxBurst: 9})).
|
||||
POST("/api/_version_/fleet/demologin", makeDemologinEndpoint(config.Server.URLPrefix), demologinRequest{})
|
||||
}
|
||||
|
||||
func newServer(e endpoint.Endpoint, decodeFn kithttp.DecodeRequestFunc, opts []kithttp.ServerOption) http.Handler {
|
||||
|
|
|
|||
|
|
@ -5130,6 +5130,43 @@ func (s *integrationTestSuite) TestSSODisabled() {
|
|||
require.Contains(t, string(body), "/login?status=org_disabled") // html contains a script that redirects to this path
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestFleetSandboxDemoLogin() {
|
||||
t := s.T()
|
||||
|
||||
validEmail := testUsers["user1"].Email
|
||||
validPwd := testUsers["user1"].PlaintextPassword
|
||||
wrongPwd := "nope"
|
||||
hdrs := map[string]string{"Content-Type": "application/x-www-form-urlencoded"}
|
||||
|
||||
os.Unsetenv("FLEET_DEMO") // ensure it is not accidentally set
|
||||
|
||||
// without the FLEET_DEMO env var set, the login always fails
|
||||
formBody := make(url.Values)
|
||||
formBody.Set("email", validEmail)
|
||||
formBody.Set("password", validPwd)
|
||||
res := s.DoRawWithHeaders("POST", "/api/v1/fleet/demologin", []byte(formBody.Encode()), http.StatusInternalServerError, hdrs)
|
||||
require.NotEqual(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
// with the FLEET_DEMO env var set, the login works as expected, validating
|
||||
// the credentials
|
||||
os.Setenv("FLEET_DEMO", "1")
|
||||
defer os.Unsetenv("FLEET_DEMO")
|
||||
|
||||
formBody.Set("email", validEmail)
|
||||
formBody.Set("password", wrongPwd)
|
||||
res = s.DoRawWithHeaders("POST", "/api/v1/fleet/demologin", []byte(formBody.Encode()), http.StatusUnauthorized, hdrs)
|
||||
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
|
||||
|
||||
formBody.Set("email", validEmail)
|
||||
formBody.Set("password", validPwd)
|
||||
res = s.DoRawWithHeaders("POST", "/api/v1/fleet/demologin", []byte(formBody.Encode()), http.StatusOK, hdrs)
|
||||
resBody, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
require.Contains(t, string(resBody), `window.location = "/"`)
|
||||
require.Regexp(t, `window.localStorage.setItem\('FLEET::auth_token', '[^']+'\)`, string(resBody))
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestGetHostBatteries() {
|
||||
t := s.T()
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -594,3 +595,89 @@ func (svc *Service) validateSession(ctx context.Context, session *fleet.Session)
|
|||
|
||||
return svc.ds.MarkSessionAccessed(ctx, session)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Demo Login
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// This is a special kind of login where the username and password come in form values rather than
|
||||
// JSON as is typical for API requests. This is intended to support logins from demo environments,
|
||||
// when users are being redirected from fleetdm.com.
|
||||
|
||||
type demologinRequest struct {
|
||||
Email string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (demologinRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, &badRequestError{message: err.Error()}, "decode demo login")
|
||||
}
|
||||
|
||||
return demologinRequest{
|
||||
Email: r.FormValue("email"),
|
||||
Password: r.FormValue("password"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type demologinResponse struct {
|
||||
content string
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r demologinResponse) error() error { return r.Err }
|
||||
|
||||
// If html is present we return a web page
|
||||
func (r demologinResponse) html() string { return r.content }
|
||||
|
||||
func makeDemologinEndpoint(urlPrefix string) handlerFunc {
|
||||
return func(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
|
||||
req := request.(demologinRequest)
|
||||
|
||||
// Undocumented FLEET_DEMO environment variable, as this endpoint is intended only to be
|
||||
// used in the Fleet Sandbox demo environment.
|
||||
if os.Getenv("FLEET_DEMO") != "1" {
|
||||
return nil, errors.New("this endpoint only enabled in demo mode")
|
||||
}
|
||||
|
||||
_, sess, err := svc.Login(ctx, req.Email, req.Password)
|
||||
|
||||
// This endpoint handles errors slightly differently in that we want to still return the
|
||||
// HTML page redirect to login if there was some error, so we can't just return the response
|
||||
// error without doing the rest of the logic.
|
||||
|
||||
session := struct {
|
||||
Token string
|
||||
}{}
|
||||
var resp demologinResponse
|
||||
if err != nil {
|
||||
resp.Err = err
|
||||
}
|
||||
if sess != nil {
|
||||
session.Token = sess.Key
|
||||
}
|
||||
|
||||
relayStateLoadPage := `<!DOCTYPE html>
|
||||
<script type='text/javascript'>
|
||||
window.localStorage.setItem('FLEET::auth_token', '{{ .Token }}');
|
||||
window.location = "/";
|
||||
</script>
|
||||
<body>
|
||||
Redirecting to Fleet...
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
tmpl, err := template.New("demologin").Parse(relayStateLoadPage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var writer bytes.Buffer
|
||||
err = tmpl.Execute(&writer, session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.content = writer.String()
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
|
|
@ -24,6 +25,9 @@ func encodeResponse(ctx context.Context, w http.ResponseWriter, response interfa
|
|||
// page and the error will be logged
|
||||
if page, ok := response.(htmlPage); ok {
|
||||
w.Header().Set("Content-Type", "text/html; charset=UTF-8")
|
||||
if coder, ok := page.error().(kithttp.StatusCoder); ok {
|
||||
w.WriteHeader(coder.StatusCode())
|
||||
}
|
||||
_, err := io.WriteString(w, page.html())
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue