2022-01-25 14:34:00 +00:00
|
|
|
package service
|
|
|
|
|
|
|
|
import (
|
2022-03-08 16:27:38 +00:00
|
|
|
"bytes"
|
2022-01-25 14:34:00 +00:00
|
|
|
"context"
|
2022-03-08 16:27:38 +00:00
|
|
|
"crypto/rand"
|
|
|
|
"encoding/base64"
|
|
|
|
"errors"
|
|
|
|
"html/template"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"strings"
|
2022-01-25 14:34:00 +00:00
|
|
|
"time"
|
|
|
|
|
2022-03-08 16:27:38 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/logging"
|
2022-12-21 17:29:51 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/publicip"
|
2022-03-08 16:27:38 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
2022-01-25 14:34:00 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
2022-03-08 16:27:38 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/sso"
|
|
|
|
"github.com/go-kit/kit/log/level"
|
2022-01-25 14:34:00 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Get Info About Session
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type getInfoAboutSessionRequest struct {
|
|
|
|
ID uint `url:"id"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type getInfoAboutSessionResponse struct {
|
|
|
|
SessionID uint `json:"session_id"`
|
|
|
|
UserID uint `json:"user_id"`
|
|
|
|
CreatedAt time.Time `json:"created_at"`
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r getInfoAboutSessionResponse) error() error { return r.Err }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func getInfoAboutSessionEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2022-01-25 14:34:00 +00:00
|
|
|
req := request.(*getInfoAboutSessionRequest)
|
|
|
|
session, err := svc.GetInfoAboutSession(ctx, req.ID)
|
|
|
|
if err != nil {
|
|
|
|
return getInfoAboutSessionResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return getInfoAboutSessionResponse{
|
|
|
|
SessionID: session.ID,
|
|
|
|
UserID: session.UserID,
|
|
|
|
CreatedAt: session.CreatedAt,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) GetInfoAboutSession(ctx context.Context, id uint) (*fleet.Session, error) {
|
|
|
|
session, err := svc.ds.SessionByID(ctx, id)
|
|
|
|
if err != nil {
|
2023-11-02 17:32:34 +00:00
|
|
|
svc.authz.SkipAuthorization(ctx)
|
2022-01-25 14:34:00 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := svc.authz.Authorize(ctx, session, fleet.ActionRead); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = svc.validateSession(ctx, session)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return session, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Delete Session
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type deleteSessionRequest struct {
|
|
|
|
ID uint `url:"id"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type deleteSessionResponse struct {
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r deleteSessionResponse) error() error { return r.Err }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func deleteSessionEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2022-01-25 14:34:00 +00:00
|
|
|
req := request.(*deleteSessionRequest)
|
|
|
|
err := svc.DeleteSession(ctx, req.ID)
|
|
|
|
if err != nil {
|
|
|
|
return deleteSessionResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
return deleteSessionResponse{}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) DeleteSession(ctx context.Context, id uint) error {
|
|
|
|
session, err := svc.ds.SessionByID(ctx, id)
|
|
|
|
if err != nil {
|
2023-11-02 17:32:34 +00:00
|
|
|
svc.authz.SkipAuthorization(ctx)
|
2022-01-25 14:34:00 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := svc.authz.Authorize(ctx, session, fleet.ActionWrite); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return svc.ds.DestroySession(ctx, session)
|
|
|
|
}
|
2022-03-08 16:27:38 +00:00
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Login
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type loginRequest struct {
|
|
|
|
Email string `json:"email"`
|
|
|
|
Password string `json:"password"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type loginResponse struct {
|
|
|
|
User *fleet.User `json:"user,omitempty"`
|
|
|
|
AvailableTeams []*fleet.TeamSummary `json:"available_teams"`
|
|
|
|
Token string `json:"token,omitempty"`
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r loginResponse) error() error { return r.Err }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func loginEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2022-03-08 16:27:38 +00:00
|
|
|
req := request.(*loginRequest)
|
|
|
|
req.Email = strings.ToLower(req.Email)
|
|
|
|
|
Add read replica testing helpers and fix non-sso login bug (#4908)
not set on the INSERT.
- OUT: Only sets the ID on the passed session and returns it. (`CreatedAt`, `AccessedAt`, are not set.)
New version:
```go
func (ds *Datastore) NewSession(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
sqlStatement := `
INSERT INTO sessions (
user_id,
` + "`key`" + `
)
VALUES(?,?)
`
result, err := ds.writer.ExecContext(ctx, sqlStatement, userID, sessionKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting session")
}
id, _ := result.LastInsertId() // cannot fail with the mysql driver
return ds.sessionByID(ctx, ds.writer, uint(id))
}
```
- IN: Define arguments that are truly used when creating a session.
- OUT: Load and return the fleet.Session struct with all values set (using the `ds.writer` to support read replicas correctly).
PS: The new `NewSession` version mimics what we already do with other entities, like policies (`Datastore.NewGlobalPolicy`).
2022-04-04 23:52:05 +00:00
|
|
|
user, session, err := svc.Login(ctx, req.Email, req.Password)
|
2022-03-08 16:27:38 +00:00
|
|
|
if err != nil {
|
|
|
|
return loginResponse{Err: err}, nil
|
|
|
|
}
|
Add read replica testing helpers and fix non-sso login bug (#4908)
not set on the INSERT.
- OUT: Only sets the ID on the passed session and returns it. (`CreatedAt`, `AccessedAt`, are not set.)
New version:
```go
func (ds *Datastore) NewSession(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
sqlStatement := `
INSERT INTO sessions (
user_id,
` + "`key`" + `
)
VALUES(?,?)
`
result, err := ds.writer.ExecContext(ctx, sqlStatement, userID, sessionKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting session")
}
id, _ := result.LastInsertId() // cannot fail with the mysql driver
return ds.sessionByID(ctx, ds.writer, uint(id))
}
```
- IN: Define arguments that are truly used when creating a session.
- OUT: Load and return the fleet.Session struct with all values set (using the `ds.writer` to support read replicas correctly).
PS: The new `NewSession` version mimics what we already do with other entities, like policies (`Datastore.NewGlobalPolicy`).
2022-04-04 23:52:05 +00:00
|
|
|
// Add viewer to context to allow access to service teams for list of available teams.
|
|
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{
|
|
|
|
User: user,
|
|
|
|
Session: session,
|
|
|
|
})
|
2022-03-08 16:27:38 +00:00
|
|
|
availableTeams, err := svc.ListAvailableTeamsForUser(ctx, user)
|
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, fleet.ErrMissingLicense) {
|
|
|
|
availableTeams = []*fleet.TeamSummary{}
|
|
|
|
} else {
|
|
|
|
return loginResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
}
|
Add read replica testing helpers and fix non-sso login bug (#4908)
not set on the INSERT.
- OUT: Only sets the ID on the passed session and returns it. (`CreatedAt`, `AccessedAt`, are not set.)
New version:
```go
func (ds *Datastore) NewSession(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
sqlStatement := `
INSERT INTO sessions (
user_id,
` + "`key`" + `
)
VALUES(?,?)
`
result, err := ds.writer.ExecContext(ctx, sqlStatement, userID, sessionKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting session")
}
id, _ := result.LastInsertId() // cannot fail with the mysql driver
return ds.sessionByID(ctx, ds.writer, uint(id))
}
```
- IN: Define arguments that are truly used when creating a session.
- OUT: Load and return the fleet.Session struct with all values set (using the `ds.writer` to support read replicas correctly).
PS: The new `NewSession` version mimics what we already do with other entities, like policies (`Datastore.NewGlobalPolicy`).
2022-04-04 23:52:05 +00:00
|
|
|
return loginResponse{user, availableTeams, session.Key, nil}, nil
|
2022-03-08 16:27:38 +00:00
|
|
|
}
|
|
|
|
|
Add read replica testing helpers and fix non-sso login bug (#4908)
not set on the INSERT.
- OUT: Only sets the ID on the passed session and returns it. (`CreatedAt`, `AccessedAt`, are not set.)
New version:
```go
func (ds *Datastore) NewSession(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
sqlStatement := `
INSERT INTO sessions (
user_id,
` + "`key`" + `
)
VALUES(?,?)
`
result, err := ds.writer.ExecContext(ctx, sqlStatement, userID, sessionKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting session")
}
id, _ := result.LastInsertId() // cannot fail with the mysql driver
return ds.sessionByID(ctx, ds.writer, uint(id))
}
```
- IN: Define arguments that are truly used when creating a session.
- OUT: Load and return the fleet.Session struct with all values set (using the `ds.writer` to support read replicas correctly).
PS: The new `NewSession` version mimics what we already do with other entities, like policies (`Datastore.NewGlobalPolicy`).
2022-04-04 23:52:05 +00:00
|
|
|
func (svc *Service) Login(ctx context.Context, email, password string) (*fleet.User, *fleet.Session, error) {
|
2022-03-08 16:27:38 +00:00
|
|
|
// skipauth: No user context available yet to authorize against.
|
|
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
|
2022-12-21 17:29:51 +00:00
|
|
|
logging.WithLevel(logging.WithExtras(logging.WithNoUser(ctx),
|
|
|
|
"op", "login",
|
|
|
|
"email", email,
|
|
|
|
"public_ip", publicip.FromContext(ctx),
|
|
|
|
), level.Info)
|
2022-03-08 16:27:38 +00:00
|
|
|
|
|
|
|
// If there is an error, sleep until the request has taken at least 1
|
|
|
|
// second. This means that generally a login failure for any reason will
|
|
|
|
// take ~1s and frustrate a timing attack.
|
|
|
|
var err error
|
|
|
|
defer func(start time.Time) {
|
|
|
|
if err != nil {
|
2023-01-20 15:43:22 +00:00
|
|
|
if err := svc.ds.NewActivity(ctx, nil, fleet.ActivityTypeUserFailedLogin{
|
|
|
|
Email: email,
|
|
|
|
PublicIP: publicip.FromContext(ctx),
|
|
|
|
}); err != nil {
|
|
|
|
logging.WithExtras(logging.WithNoUser(ctx),
|
|
|
|
"msg", "failed to generate failed login activity",
|
|
|
|
)
|
|
|
|
}
|
2022-03-08 16:27:38 +00:00
|
|
|
time.Sleep(time.Until(start.Add(1 * time.Second)))
|
|
|
|
}
|
|
|
|
}(time.Now())
|
|
|
|
|
|
|
|
user, err := svc.ds.UserByEmail(ctx, email)
|
|
|
|
var nfe fleet.NotFoundError
|
|
|
|
if errors.As(err, &nfe) {
|
Add read replica testing helpers and fix non-sso login bug (#4908)
not set on the INSERT.
- OUT: Only sets the ID on the passed session and returns it. (`CreatedAt`, `AccessedAt`, are not set.)
New version:
```go
func (ds *Datastore) NewSession(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
sqlStatement := `
INSERT INTO sessions (
user_id,
` + "`key`" + `
)
VALUES(?,?)
`
result, err := ds.writer.ExecContext(ctx, sqlStatement, userID, sessionKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting session")
}
id, _ := result.LastInsertId() // cannot fail with the mysql driver
return ds.sessionByID(ctx, ds.writer, uint(id))
}
```
- IN: Define arguments that are truly used when creating a session.
- OUT: Load and return the fleet.Session struct with all values set (using the `ds.writer` to support read replicas correctly).
PS: The new `NewSession` version mimics what we already do with other entities, like policies (`Datastore.NewGlobalPolicy`).
2022-04-04 23:52:05 +00:00
|
|
|
return nil, nil, fleet.NewAuthFailedError("user not found")
|
2022-03-08 16:27:38 +00:00
|
|
|
}
|
|
|
|
if err != nil {
|
Add read replica testing helpers and fix non-sso login bug (#4908)
not set on the INSERT.
- OUT: Only sets the ID on the passed session and returns it. (`CreatedAt`, `AccessedAt`, are not set.)
New version:
```go
func (ds *Datastore) NewSession(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
sqlStatement := `
INSERT INTO sessions (
user_id,
` + "`key`" + `
)
VALUES(?,?)
`
result, err := ds.writer.ExecContext(ctx, sqlStatement, userID, sessionKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting session")
}
id, _ := result.LastInsertId() // cannot fail with the mysql driver
return ds.sessionByID(ctx, ds.writer, uint(id))
}
```
- IN: Define arguments that are truly used when creating a session.
- OUT: Load and return the fleet.Session struct with all values set (using the `ds.writer` to support read replicas correctly).
PS: The new `NewSession` version mimics what we already do with other entities, like policies (`Datastore.NewGlobalPolicy`).
2022-04-04 23:52:05 +00:00
|
|
|
return nil, nil, fleet.NewAuthFailedError(err.Error())
|
2022-03-08 16:27:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if err = user.ValidatePassword(password); err != nil {
|
Add read replica testing helpers and fix non-sso login bug (#4908)
not set on the INSERT.
- OUT: Only sets the ID on the passed session and returns it. (`CreatedAt`, `AccessedAt`, are not set.)
New version:
```go
func (ds *Datastore) NewSession(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
sqlStatement := `
INSERT INTO sessions (
user_id,
` + "`key`" + `
)
VALUES(?,?)
`
result, err := ds.writer.ExecContext(ctx, sqlStatement, userID, sessionKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting session")
}
id, _ := result.LastInsertId() // cannot fail with the mysql driver
return ds.sessionByID(ctx, ds.writer, uint(id))
}
```
- IN: Define arguments that are truly used when creating a session.
- OUT: Load and return the fleet.Session struct with all values set (using the `ds.writer` to support read replicas correctly).
PS: The new `NewSession` version mimics what we already do with other entities, like policies (`Datastore.NewGlobalPolicy`).
2022-04-04 23:52:05 +00:00
|
|
|
return nil, nil, fleet.NewAuthFailedError("invalid password")
|
2022-03-08 16:27:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if user.SSOEnabled {
|
Add read replica testing helpers and fix non-sso login bug (#4908)
not set on the INSERT.
- OUT: Only sets the ID on the passed session and returns it. (`CreatedAt`, `AccessedAt`, are not set.)
New version:
```go
func (ds *Datastore) NewSession(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
sqlStatement := `
INSERT INTO sessions (
user_id,
` + "`key`" + `
)
VALUES(?,?)
`
result, err := ds.writer.ExecContext(ctx, sqlStatement, userID, sessionKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting session")
}
id, _ := result.LastInsertId() // cannot fail with the mysql driver
return ds.sessionByID(ctx, ds.writer, uint(id))
}
```
- IN: Define arguments that are truly used when creating a session.
- OUT: Load and return the fleet.Session struct with all values set (using the `ds.writer` to support read replicas correctly).
PS: The new `NewSession` version mimics what we already do with other entities, like policies (`Datastore.NewGlobalPolicy`).
2022-04-04 23:52:05 +00:00
|
|
|
return nil, nil, fleet.NewAuthFailedError("password login disabled for sso users")
|
2022-03-08 16:27:38 +00:00
|
|
|
}
|
|
|
|
|
Add read replica testing helpers and fix non-sso login bug (#4908)
not set on the INSERT.
- OUT: Only sets the ID on the passed session and returns it. (`CreatedAt`, `AccessedAt`, are not set.)
New version:
```go
func (ds *Datastore) NewSession(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
sqlStatement := `
INSERT INTO sessions (
user_id,
` + "`key`" + `
)
VALUES(?,?)
`
result, err := ds.writer.ExecContext(ctx, sqlStatement, userID, sessionKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting session")
}
id, _ := result.LastInsertId() // cannot fail with the mysql driver
return ds.sessionByID(ctx, ds.writer, uint(id))
}
```
- IN: Define arguments that are truly used when creating a session.
- OUT: Load and return the fleet.Session struct with all values set (using the `ds.writer` to support read replicas correctly).
PS: The new `NewSession` version mimics what we already do with other entities, like policies (`Datastore.NewGlobalPolicy`).
2022-04-04 23:52:05 +00:00
|
|
|
session, err := svc.makeSession(ctx, user.ID)
|
2022-03-08 16:27:38 +00:00
|
|
|
if err != nil {
|
Add read replica testing helpers and fix non-sso login bug (#4908)
not set on the INSERT.
- OUT: Only sets the ID on the passed session and returns it. (`CreatedAt`, `AccessedAt`, are not set.)
New version:
```go
func (ds *Datastore) NewSession(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
sqlStatement := `
INSERT INTO sessions (
user_id,
` + "`key`" + `
)
VALUES(?,?)
`
result, err := ds.writer.ExecContext(ctx, sqlStatement, userID, sessionKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting session")
}
id, _ := result.LastInsertId() // cannot fail with the mysql driver
return ds.sessionByID(ctx, ds.writer, uint(id))
}
```
- IN: Define arguments that are truly used when creating a session.
- OUT: Load and return the fleet.Session struct with all values set (using the `ds.writer` to support read replicas correctly).
PS: The new `NewSession` version mimics what we already do with other entities, like policies (`Datastore.NewGlobalPolicy`).
2022-04-04 23:52:05 +00:00
|
|
|
return nil, nil, fleet.NewAuthFailedError(err.Error())
|
2022-03-08 16:27:38 +00:00
|
|
|
}
|
|
|
|
|
2022-12-23 16:05:16 +00:00
|
|
|
if err := svc.ds.NewActivity(ctx, user, fleet.ActivityTypeUserLoggedIn{
|
|
|
|
PublicIP: publicip.FromContext(ctx),
|
2022-12-21 17:29:51 +00:00
|
|
|
}); err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
Add read replica testing helpers and fix non-sso login bug (#4908)
not set on the INSERT.
- OUT: Only sets the ID on the passed session and returns it. (`CreatedAt`, `AccessedAt`, are not set.)
New version:
```go
func (ds *Datastore) NewSession(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
sqlStatement := `
INSERT INTO sessions (
user_id,
` + "`key`" + `
)
VALUES(?,?)
`
result, err := ds.writer.ExecContext(ctx, sqlStatement, userID, sessionKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting session")
}
id, _ := result.LastInsertId() // cannot fail with the mysql driver
return ds.sessionByID(ctx, ds.writer, uint(id))
}
```
- IN: Define arguments that are truly used when creating a session.
- OUT: Load and return the fleet.Session struct with all values set (using the `ds.writer` to support read replicas correctly).
PS: The new `NewSession` version mimics what we already do with other entities, like policies (`Datastore.NewGlobalPolicy`).
2022-04-04 23:52:05 +00:00
|
|
|
return user, session, nil
|
2022-03-08 16:27:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Logout
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type logoutResponse struct {
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r logoutResponse) error() error { return r.Err }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func logoutEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2022-03-08 16:27:38 +00:00
|
|
|
err := svc.Logout(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return logoutResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
return logoutResponse{}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) Logout(ctx context.Context) error {
|
|
|
|
// skipauth: Any user can always log out of their own session.
|
|
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
|
|
|
|
logging.WithLevel(ctx, level.Info)
|
|
|
|
|
|
|
|
return svc.DestroySession(ctx)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) DestroySession(ctx context.Context) error {
|
|
|
|
vc, ok := viewer.FromContext(ctx)
|
|
|
|
if !ok {
|
2023-04-13 21:37:42 +00:00
|
|
|
return fleet.NewAuthRequiredError(fleet.ErrNoContext.Error())
|
2022-03-08 16:27:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
session, err := svc.ds.SessionByID(ctx, vc.SessionID())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := svc.authz.Authorize(ctx, session, fleet.ActionWrite); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return svc.ds.DestroySession(ctx, session)
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Initiate SSO
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type initiateSSORequest struct {
|
2023-03-01 23:18:40 +00:00
|
|
|
// RelayURL is the URL path that the IdP will redirect to once authenticated
|
|
|
|
// (e.g. "/dashboard").
|
2022-03-08 16:27:38 +00:00
|
|
|
RelayURL string `json:"relay_url"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type initiateSSOResponse struct {
|
|
|
|
URL string `json:"url,omitempty"`
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r initiateSSOResponse) error() error { return r.Err }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func initiateSSOEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2022-03-08 16:27:38 +00:00
|
|
|
req := request.(*initiateSSORequest)
|
|
|
|
idProviderURL, err := svc.InitiateSSO(ctx, req.RelayURL)
|
|
|
|
if err != nil {
|
|
|
|
return initiateSSOResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
return initiateSSOResponse{URL: idProviderURL}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// InitiateSSO initiates a Single Sign-On flow for a request to visit the
|
|
|
|
// protected URL identified by redirectURL. It returns the URL of the identity
|
|
|
|
// provider to make a request to to proceed with the authentication via that
|
|
|
|
// external service, and stores ephemeral session state to validate the
|
|
|
|
// callback from the identity provider to finalize the SSO flow.
|
|
|
|
func (svc *Service) InitiateSSO(ctx context.Context, redirectURL string) (string, error) {
|
|
|
|
// skipauth: User context does not yet exist. Unauthenticated users may
|
|
|
|
// initiate SSO.
|
|
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
|
2022-09-29 14:25:45 +00:00
|
|
|
logging.WithLevel(logging.WithNoUser(ctx), level.Info)
|
2022-03-08 16:27:38 +00:00
|
|
|
|
|
|
|
appConfig, err := svc.ds.AppConfig(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return "", ctxerr.Wrap(ctx, err, "InitiateSSO getting app config")
|
|
|
|
}
|
|
|
|
|
2023-06-07 19:06:36 +00:00
|
|
|
if appConfig.SSOSettings == nil || !appConfig.SSOSettings.EnableSSO {
|
2022-09-19 17:53:44 +00:00
|
|
|
err := &fleet.BadRequestError{Message: "organization not configured to use sso"}
|
2023-04-27 12:43:20 +00:00
|
|
|
return "", ctxerr.Wrap(ctx, newSSOError(err, ssoOrgDisabled), "initiate sso")
|
2022-06-21 13:04:50 +00:00
|
|
|
}
|
|
|
|
|
2023-04-27 12:43:20 +00:00
|
|
|
metadata, err := sso.GetMetadata(&appConfig.SSOSettings.SSOProviderSettings)
|
2022-03-08 16:27:38 +00:00
|
|
|
if err != nil {
|
2023-11-03 14:33:25 +00:00
|
|
|
return "", ctxerr.Wrap(ctx, badRequestErr("Could not get SSO Metadata. Check your SSO settings.", err))
|
2022-03-08 16:27:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
serverURL := appConfig.ServerSettings.ServerURL
|
|
|
|
settings := sso.Settings{
|
|
|
|
Metadata: metadata,
|
|
|
|
// Construct call back url to send to idp
|
2022-04-20 16:46:45 +00:00
|
|
|
AssertionConsumerServiceURL: serverURL + svc.config.Server.URLPrefix + "/api/v1/fleet/sso/callback",
|
2022-03-08 16:27:38 +00:00
|
|
|
SessionStore: svc.ssoSessionStore,
|
|
|
|
OriginalURL: redirectURL,
|
|
|
|
}
|
|
|
|
|
|
|
|
// If issuer is not explicitly set, default to host name.
|
|
|
|
var issuer string
|
|
|
|
entityID := appConfig.SSOSettings.EntityID
|
|
|
|
if entityID == "" {
|
|
|
|
u, err := url.Parse(serverURL)
|
|
|
|
if err != nil {
|
|
|
|
return "", ctxerr.Wrap(ctx, err, "parse server url")
|
|
|
|
}
|
|
|
|
issuer = u.Hostname()
|
|
|
|
} else {
|
|
|
|
issuer = entityID
|
|
|
|
}
|
|
|
|
|
|
|
|
idpURL, err := sso.CreateAuthorizationRequest(&settings, issuer)
|
|
|
|
if err != nil {
|
|
|
|
return "", ctxerr.Wrap(ctx, err, "InitiateSSO creating authorization")
|
|
|
|
}
|
|
|
|
|
|
|
|
return idpURL, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Callback SSO
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type callbackSSORequest struct{}
|
|
|
|
|
|
|
|
func (callbackSSORequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
|
|
|
err := r.ParseForm()
|
|
|
|
if err != nil {
|
Add UUID to Fleet errors and clean up error msgs (#10411)
#8129
Apart from fixing the issue in #8129, this change also introduces UUIDs
to Fleet errors. To be able to match a returned error from the API to a
error in the Fleet logs. See
https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for
more context.
Samples with the changes in this PR:
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d ''
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "Expected JSON Body"
}
],
"uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0"
}
```
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd'
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "json decoder error"
}
],
"uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d"
}
```
```
curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams"
{
"message": "Authentication required",
"errors": [
{
"name": "base",
"reason": "Authentication required"
}
],
"uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Authorization header required",
"errors": [
{
"name": "base",
"reason": "Authorization header required"
}
],
"uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Permission Denied",
"uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06"
}
```
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-03-13 16:44:06 +00:00
|
|
|
return nil, ctxerr.Wrap(ctx, &fleet.BadRequestError{
|
|
|
|
Message: "failed to parse form",
|
|
|
|
InternalErr: err,
|
|
|
|
}, "decode sso callback")
|
2022-03-08 16:27:38 +00:00
|
|
|
}
|
|
|
|
authResponse, err := sso.DecodeAuthResponse(r.FormValue("SAMLResponse"))
|
|
|
|
if err != nil {
|
Add UUID to Fleet errors and clean up error msgs (#10411)
#8129
Apart from fixing the issue in #8129, this change also introduces UUIDs
to Fleet errors. To be able to match a returned error from the API to a
error in the Fleet logs. See
https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for
more context.
Samples with the changes in this PR:
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d ''
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "Expected JSON Body"
}
],
"uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0"
}
```
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd'
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "json decoder error"
}
],
"uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d"
}
```
```
curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams"
{
"message": "Authentication required",
"errors": [
{
"name": "base",
"reason": "Authentication required"
}
],
"uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Authorization header required",
"errors": [
{
"name": "base",
"reason": "Authorization header required"
}
],
"uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Permission Denied",
"uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06"
}
```
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-03-13 16:44:06 +00:00
|
|
|
return nil, ctxerr.Wrap(ctx, &fleet.BadRequestError{
|
|
|
|
Message: "failed to decode SAMLResponse",
|
|
|
|
InternalErr: err,
|
|
|
|
}, "decoding sso callback")
|
2022-03-08 16:27:38 +00:00
|
|
|
}
|
|
|
|
return authResponse, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type callbackSSOResponse struct {
|
|
|
|
content string
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r callbackSSOResponse) error() error { return r.Err }
|
|
|
|
|
|
|
|
// If html is present we return a web page
|
|
|
|
func (r callbackSSOResponse) html() string { return r.content }
|
|
|
|
|
|
|
|
func makeCallbackSSOEndpoint(urlPrefix string) handlerFunc {
|
2022-12-27 14:26:59 +00:00
|
|
|
return func(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2022-03-08 16:27:38 +00:00
|
|
|
authResponse := request.(fleet.Auth)
|
2022-08-15 17:42:33 +00:00
|
|
|
session, err := getSSOSession(ctx, svc, authResponse)
|
2022-03-08 16:27:38 +00:00
|
|
|
var resp callbackSSOResponse
|
|
|
|
if err != nil {
|
2023-01-20 15:43:22 +00:00
|
|
|
if err := svc.NewActivity(ctx, nil, fleet.ActivityTypeUserFailedLogin{
|
|
|
|
Email: authResponse.UserID(),
|
|
|
|
PublicIP: publicip.FromContext(ctx),
|
|
|
|
}); err != nil {
|
|
|
|
logging.WithLevel(logging.WithExtras(logging.WithNoUser(ctx),
|
|
|
|
"msg", "failed to generate failed login activity",
|
|
|
|
), level.Info)
|
|
|
|
}
|
|
|
|
|
Add UUID to Fleet errors and clean up error msgs (#10411)
#8129
Apart from fixing the issue in #8129, this change also introduces UUIDs
to Fleet errors. To be able to match a returned error from the API to a
error in the Fleet logs. See
https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for
more context.
Samples with the changes in this PR:
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d ''
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "Expected JSON Body"
}
],
"uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0"
}
```
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd'
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "json decoder error"
}
],
"uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d"
}
```
```
curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams"
{
"message": "Authentication required",
"errors": [
{
"name": "base",
"reason": "Authentication required"
}
],
"uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Authorization header required",
"errors": [
{
"name": "base",
"reason": "Authorization header required"
}
],
"uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Permission Denied",
"uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06"
}
```
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-03-13 16:44:06 +00:00
|
|
|
var ssoErr *ssoError
|
2022-06-21 13:04:50 +00:00
|
|
|
|
|
|
|
status := ssoOtherError
|
|
|
|
if errors.As(err, &ssoErr) {
|
|
|
|
status = ssoErr.code
|
|
|
|
}
|
2022-03-08 16:27:38 +00:00
|
|
|
// redirect to login page on front end if there was some problem,
|
|
|
|
// errors should still be logged
|
|
|
|
session = &fleet.SSOSession{
|
2022-06-21 13:04:50 +00:00
|
|
|
RedirectURL: urlPrefix + "/login?status=" + string(status),
|
2022-03-08 16:27:38 +00:00
|
|
|
Token: "",
|
|
|
|
}
|
|
|
|
resp.Err = err
|
|
|
|
}
|
|
|
|
relayStateLoadPage := ` <html>
|
|
|
|
<script type='text/javascript'>
|
|
|
|
var redirectURL = {{ .RedirectURL }};
|
|
|
|
window.localStorage.setItem('FLEET::auth_token', '{{ .Token }}');
|
|
|
|
window.location = redirectURL;
|
|
|
|
</script>
|
|
|
|
<body>
|
|
|
|
Redirecting to Fleet at {{ .RedirectURL }} ...
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
`
|
|
|
|
tmpl, err := template.New("relayStateLoader").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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-15 17:42:33 +00:00
|
|
|
func getSSOSession(ctx context.Context, svc fleet.Service, auth fleet.Auth) (*fleet.SSOSession, error) {
|
|
|
|
redirectURL, err := svc.InitSSOCallback(ctx, auth)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
user, err := svc.GetSSOUser(ctx, auth)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return svc.LoginSSOUser(ctx, user, redirectURL)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) InitSSOCallback(ctx context.Context, auth fleet.Auth) (string, error) {
|
2022-03-08 16:27:38 +00:00
|
|
|
// skipauth: User context does not yet exist. Unauthenticated users may
|
|
|
|
// hit the SSO callback.
|
|
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
|
2022-09-29 14:25:45 +00:00
|
|
|
logging.WithLevel(logging.WithNoUser(ctx), level.Info)
|
2022-03-08 16:27:38 +00:00
|
|
|
|
|
|
|
appConfig, err := svc.ds.AppConfig(ctx)
|
|
|
|
if err != nil {
|
2022-08-15 17:42:33 +00:00
|
|
|
return "", ctxerr.Wrap(ctx, err, "get config for sso")
|
2022-03-08 16:27:38 +00:00
|
|
|
}
|
|
|
|
|
2023-06-07 19:06:36 +00:00
|
|
|
if appConfig.SSOSettings == nil || !appConfig.SSOSettings.EnableSSO {
|
2022-06-21 13:04:50 +00:00
|
|
|
err := ctxerr.New(ctx, "organization not configured to use sso")
|
Add UUID to Fleet errors and clean up error msgs (#10411)
#8129
Apart from fixing the issue in #8129, this change also introduces UUIDs
to Fleet errors. To be able to match a returned error from the API to a
error in the Fleet logs. See
https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for
more context.
Samples with the changes in this PR:
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d ''
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "Expected JSON Body"
}
],
"uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0"
}
```
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd'
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "json decoder error"
}
],
"uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d"
}
```
```
curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams"
{
"message": "Authentication required",
"errors": [
{
"name": "base",
"reason": "Authentication required"
}
],
"uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Authorization header required",
"errors": [
{
"name": "base",
"reason": "Authorization header required"
}
],
"uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Permission Denied",
"uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06"
}
```
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-03-13 16:44:06 +00:00
|
|
|
return "", ctxerr.Wrap(ctx, newSSOError(err, ssoOrgDisabled), "callback sso")
|
2022-06-21 13:04:50 +00:00
|
|
|
}
|
|
|
|
|
2023-03-01 23:18:40 +00:00
|
|
|
// Load the request metadata if available.
|
2022-03-08 16:27:38 +00:00
|
|
|
var metadata *sso.Metadata
|
|
|
|
var redirectURL string
|
|
|
|
if appConfig.SSOSettings.EnableSSOIdPLogin && auth.RequestID() == "" {
|
|
|
|
// Missing request ID indicates this was IdP-initiated. Only allow if
|
|
|
|
// configured to do so.
|
2023-04-27 12:43:20 +00:00
|
|
|
metadata, err = sso.GetMetadata(&appConfig.SSOSettings.SSOProviderSettings)
|
2022-03-08 16:27:38 +00:00
|
|
|
if err != nil {
|
2022-08-15 17:42:33 +00:00
|
|
|
return "", ctxerr.Wrap(ctx, err, "get sso metadata")
|
2022-03-08 16:27:38 +00:00
|
|
|
}
|
|
|
|
redirectURL = "/"
|
|
|
|
} else {
|
2023-04-27 12:43:20 +00:00
|
|
|
var session *sso.Session
|
|
|
|
session, metadata, err = svc.ssoSessionStore.Fullfill(auth.RequestID())
|
2022-03-08 16:27:38 +00:00
|
|
|
if err != nil {
|
2023-04-27 12:43:20 +00:00
|
|
|
return "", ctxerr.Wrap(ctx, err, "validate request in session")
|
2022-03-08 16:27:38 +00:00
|
|
|
}
|
|
|
|
redirectURL = session.OriginalURL
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate response
|
2023-04-27 12:43:20 +00:00
|
|
|
err = sso.ValidateAudiences(
|
|
|
|
*metadata,
|
|
|
|
auth,
|
2022-03-08 16:27:38 +00:00
|
|
|
appConfig.SSOSettings.EntityID,
|
|
|
|
appConfig.ServerSettings.ServerURL,
|
2022-04-20 16:46:45 +00:00
|
|
|
appConfig.ServerSettings.ServerURL+svc.config.Server.URLPrefix+"/api/v1/fleet/sso/callback", // ACS
|
2023-04-27 12:43:20 +00:00
|
|
|
)
|
2022-03-08 16:27:38 +00:00
|
|
|
if err != nil {
|
2023-04-27 12:43:20 +00:00
|
|
|
return "", ctxerr.Wrap(ctx, err, "validating sso response")
|
2022-03-08 16:27:38 +00:00
|
|
|
}
|
|
|
|
|
2022-08-15 17:42:33 +00:00
|
|
|
return redirectURL, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) GetSSOUser(ctx context.Context, auth fleet.Auth) (*fleet.User, error) {
|
2022-03-08 16:27:38 +00:00
|
|
|
user, err := svc.ds.UserByEmail(ctx, auth.UserID())
|
|
|
|
if err != nil {
|
2022-06-21 13:04:50 +00:00
|
|
|
var nfe notFoundErrorInterface
|
|
|
|
if errors.As(err, &nfe) {
|
Add UUID to Fleet errors and clean up error msgs (#10411)
#8129
Apart from fixing the issue in #8129, this change also introduces UUIDs
to Fleet errors. To be able to match a returned error from the API to a
error in the Fleet logs. See
https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for
more context.
Samples with the changes in this PR:
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d ''
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "Expected JSON Body"
}
],
"uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0"
}
```
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd'
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "json decoder error"
}
],
"uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d"
}
```
```
curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams"
{
"message": "Authentication required",
"errors": [
{
"name": "base",
"reason": "Authentication required"
}
],
"uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Authorization header required",
"errors": [
{
"name": "base",
"reason": "Authorization header required"
}
],
"uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Permission Denied",
"uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06"
}
```
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-03-13 16:44:06 +00:00
|
|
|
return nil, ctxerr.Wrap(ctx, newSSOError(err, ssoAccountInvalid))
|
2022-06-21 13:04:50 +00:00
|
|
|
}
|
2022-03-08 16:27:38 +00:00
|
|
|
return nil, ctxerr.Wrap(ctx, err, "find user in sso callback")
|
|
|
|
}
|
2022-08-15 17:42:33 +00:00
|
|
|
return user, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) LoginSSOUser(ctx context.Context, user *fleet.User, redirectURL string) (*fleet.SSOSession, error) {
|
2022-09-29 14:25:45 +00:00
|
|
|
logging.WithExtras(ctx, "email", user.Email)
|
|
|
|
|
2022-03-08 16:27:38 +00:00
|
|
|
// if the user is not sso enabled they are not authorized
|
|
|
|
if !user.SSOEnabled {
|
2022-06-21 13:04:50 +00:00
|
|
|
err := ctxerr.New(ctx, "user not configured to use sso")
|
Add UUID to Fleet errors and clean up error msgs (#10411)
#8129
Apart from fixing the issue in #8129, this change also introduces UUIDs
to Fleet errors. To be able to match a returned error from the API to a
error in the Fleet logs. See
https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for
more context.
Samples with the changes in this PR:
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d ''
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "Expected JSON Body"
}
],
"uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0"
}
```
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd'
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "json decoder error"
}
],
"uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d"
}
```
```
curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams"
{
"message": "Authentication required",
"errors": [
{
"name": "base",
"reason": "Authentication required"
}
],
"uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Authorization header required",
"errors": [
{
"name": "base",
"reason": "Authorization header required"
}
],
"uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Permission Denied",
"uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06"
}
```
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-03-13 16:44:06 +00:00
|
|
|
return nil, ctxerr.Wrap(ctx, newSSOError(err, ssoAccountDisabled))
|
2022-03-08 16:27:38 +00:00
|
|
|
}
|
Add read replica testing helpers and fix non-sso login bug (#4908)
not set on the INSERT.
- OUT: Only sets the ID on the passed session and returns it. (`CreatedAt`, `AccessedAt`, are not set.)
New version:
```go
func (ds *Datastore) NewSession(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
sqlStatement := `
INSERT INTO sessions (
user_id,
` + "`key`" + `
)
VALUES(?,?)
`
result, err := ds.writer.ExecContext(ctx, sqlStatement, userID, sessionKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting session")
}
id, _ := result.LastInsertId() // cannot fail with the mysql driver
return ds.sessionByID(ctx, ds.writer, uint(id))
}
```
- IN: Define arguments that are truly used when creating a session.
- OUT: Load and return the fleet.Session struct with all values set (using the `ds.writer` to support read replicas correctly).
PS: The new `NewSession` version mimics what we already do with other entities, like policies (`Datastore.NewGlobalPolicy`).
2022-04-04 23:52:05 +00:00
|
|
|
session, err := svc.makeSession(ctx, user.ID)
|
2022-03-08 16:27:38 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "make session in sso callback")
|
|
|
|
}
|
|
|
|
result := &fleet.SSOSession{
|
Add read replica testing helpers and fix non-sso login bug (#4908)
not set on the INSERT.
- OUT: Only sets the ID on the passed session and returns it. (`CreatedAt`, `AccessedAt`, are not set.)
New version:
```go
func (ds *Datastore) NewSession(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
sqlStatement := `
INSERT INTO sessions (
user_id,
` + "`key`" + `
)
VALUES(?,?)
`
result, err := ds.writer.ExecContext(ctx, sqlStatement, userID, sessionKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting session")
}
id, _ := result.LastInsertId() // cannot fail with the mysql driver
return ds.sessionByID(ctx, ds.writer, uint(id))
}
```
- IN: Define arguments that are truly used when creating a session.
- OUT: Load and return the fleet.Session struct with all values set (using the `ds.writer` to support read replicas correctly).
PS: The new `NewSession` version mimics what we already do with other entities, like policies (`Datastore.NewGlobalPolicy`).
2022-04-04 23:52:05 +00:00
|
|
|
Token: session.Key,
|
2022-03-08 16:27:38 +00:00
|
|
|
RedirectURL: redirectURL,
|
|
|
|
}
|
2022-12-22 23:06:33 +00:00
|
|
|
err = svc.ds.NewActivity(
|
|
|
|
ctx,
|
|
|
|
user,
|
2022-12-23 16:05:16 +00:00
|
|
|
fleet.ActivityTypeUserLoggedIn{
|
|
|
|
PublicIP: publicip.FromContext(ctx),
|
2022-12-22 23:06:33 +00:00
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "create activity in sso callback")
|
|
|
|
}
|
2022-03-08 16:27:38 +00:00
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// SSO Settings
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type ssoSettingsResponse struct {
|
|
|
|
Settings *fleet.SessionSSOSettings `json:"settings,omitempty"`
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r ssoSettingsResponse) error() error { return r.Err }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func settingsSSOEndpoint(ctx context.Context, _ interface{}, svc fleet.Service) (errorer, error) {
|
2022-03-08 16:27:38 +00:00
|
|
|
settings, err := svc.SSOSettings(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return ssoSettingsResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
return ssoSettingsResponse{Settings: settings}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// SSOSettings returns a subset of the Single Sign-On settings as configured in
|
|
|
|
// the app config. Those can be exposed e.g. via the response to an HTTP request,
|
|
|
|
// and as such should not contain sensitive information.
|
|
|
|
func (svc *Service) SSOSettings(ctx context.Context) (*fleet.SessionSSOSettings, error) {
|
|
|
|
// skipauth: Basic SSO settings are available to unauthenticated users (so
|
|
|
|
// that they have the necessary information to initiate SSO).
|
|
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
|
2022-09-29 14:25:45 +00:00
|
|
|
logging.WithLevel(logging.WithNoUser(ctx), level.Info)
|
2022-03-08 16:27:38 +00:00
|
|
|
|
|
|
|
appConfig, err := svc.ds.AppConfig(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "SessionSSOSettings getting app config")
|
|
|
|
}
|
|
|
|
|
2023-06-07 19:06:36 +00:00
|
|
|
var ssoSettings fleet.SSOSettings
|
|
|
|
if appConfig.SSOSettings != nil {
|
|
|
|
ssoSettings = *appConfig.SSOSettings
|
|
|
|
}
|
|
|
|
|
2022-03-08 16:27:38 +00:00
|
|
|
settings := &fleet.SessionSSOSettings{
|
2023-06-07 19:06:36 +00:00
|
|
|
IDPName: ssoSettings.IDPName,
|
|
|
|
IDPImageURL: ssoSettings.IDPImageURL,
|
|
|
|
SSOEnabled: ssoSettings.EnableSSO,
|
2022-03-08 16:27:38 +00:00
|
|
|
}
|
|
|
|
return settings, nil
|
|
|
|
}
|
|
|
|
|
Add read replica testing helpers and fix non-sso login bug (#4908)
not set on the INSERT.
- OUT: Only sets the ID on the passed session and returns it. (`CreatedAt`, `AccessedAt`, are not set.)
New version:
```go
func (ds *Datastore) NewSession(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
sqlStatement := `
INSERT INTO sessions (
user_id,
` + "`key`" + `
)
VALUES(?,?)
`
result, err := ds.writer.ExecContext(ctx, sqlStatement, userID, sessionKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting session")
}
id, _ := result.LastInsertId() // cannot fail with the mysql driver
return ds.sessionByID(ctx, ds.writer, uint(id))
}
```
- IN: Define arguments that are truly used when creating a session.
- OUT: Load and return the fleet.Session struct with all values set (using the `ds.writer` to support read replicas correctly).
PS: The new `NewSession` version mimics what we already do with other entities, like policies (`Datastore.NewGlobalPolicy`).
2022-04-04 23:52:05 +00:00
|
|
|
// makeSession creates a new session for the given user.
|
|
|
|
func (svc *Service) makeSession(ctx context.Context, userID uint) (*fleet.Session, error) {
|
2022-03-08 16:27:38 +00:00
|
|
|
sessionKeySize := svc.config.Session.KeySize
|
|
|
|
key := make([]byte, sessionKeySize)
|
|
|
|
_, err := rand.Read(key)
|
|
|
|
if err != nil {
|
Add read replica testing helpers and fix non-sso login bug (#4908)
not set on the INSERT.
- OUT: Only sets the ID on the passed session and returns it. (`CreatedAt`, `AccessedAt`, are not set.)
New version:
```go
func (ds *Datastore) NewSession(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
sqlStatement := `
INSERT INTO sessions (
user_id,
` + "`key`" + `
)
VALUES(?,?)
`
result, err := ds.writer.ExecContext(ctx, sqlStatement, userID, sessionKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting session")
}
id, _ := result.LastInsertId() // cannot fail with the mysql driver
return ds.sessionByID(ctx, ds.writer, uint(id))
}
```
- IN: Define arguments that are truly used when creating a session.
- OUT: Load and return the fleet.Session struct with all values set (using the `ds.writer` to support read replicas correctly).
PS: The new `NewSession` version mimics what we already do with other entities, like policies (`Datastore.NewGlobalPolicy`).
2022-04-04 23:52:05 +00:00
|
|
|
return nil, err
|
2022-03-08 16:27:38 +00:00
|
|
|
}
|
Add read replica testing helpers and fix non-sso login bug (#4908)
not set on the INSERT.
- OUT: Only sets the ID on the passed session and returns it. (`CreatedAt`, `AccessedAt`, are not set.)
New version:
```go
func (ds *Datastore) NewSession(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
sqlStatement := `
INSERT INTO sessions (
user_id,
` + "`key`" + `
)
VALUES(?,?)
`
result, err := ds.writer.ExecContext(ctx, sqlStatement, userID, sessionKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting session")
}
id, _ := result.LastInsertId() // cannot fail with the mysql driver
return ds.sessionByID(ctx, ds.writer, uint(id))
}
```
- IN: Define arguments that are truly used when creating a session.
- OUT: Load and return the fleet.Session struct with all values set (using the `ds.writer` to support read replicas correctly).
PS: The new `NewSession` version mimics what we already do with other entities, like policies (`Datastore.NewGlobalPolicy`).
2022-04-04 23:52:05 +00:00
|
|
|
session, err := svc.ds.NewSession(ctx, userID, base64.StdEncoding.EncodeToString(key))
|
2022-03-08 16:27:38 +00:00
|
|
|
if err != nil {
|
Add read replica testing helpers and fix non-sso login bug (#4908)
not set on the INSERT.
- OUT: Only sets the ID on the passed session and returns it. (`CreatedAt`, `AccessedAt`, are not set.)
New version:
```go
func (ds *Datastore) NewSession(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
sqlStatement := `
INSERT INTO sessions (
user_id,
` + "`key`" + `
)
VALUES(?,?)
`
result, err := ds.writer.ExecContext(ctx, sqlStatement, userID, sessionKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting session")
}
id, _ := result.LastInsertId() // cannot fail with the mysql driver
return ds.sessionByID(ctx, ds.writer, uint(id))
}
```
- IN: Define arguments that are truly used when creating a session.
- OUT: Load and return the fleet.Session struct with all values set (using the `ds.writer` to support read replicas correctly).
PS: The new `NewSession` version mimics what we already do with other entities, like policies (`Datastore.NewGlobalPolicy`).
2022-04-04 23:52:05 +00:00
|
|
|
return nil, ctxerr.Wrap(ctx, err, "creating new session")
|
2022-03-08 16:27:38 +00:00
|
|
|
}
|
Add read replica testing helpers and fix non-sso login bug (#4908)
not set on the INSERT.
- OUT: Only sets the ID on the passed session and returns it. (`CreatedAt`, `AccessedAt`, are not set.)
New version:
```go
func (ds *Datastore) NewSession(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
sqlStatement := `
INSERT INTO sessions (
user_id,
` + "`key`" + `
)
VALUES(?,?)
`
result, err := ds.writer.ExecContext(ctx, sqlStatement, userID, sessionKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting session")
}
id, _ := result.LastInsertId() // cannot fail with the mysql driver
return ds.sessionByID(ctx, ds.writer, uint(id))
}
```
- IN: Define arguments that are truly used when creating a session.
- OUT: Load and return the fleet.Session struct with all values set (using the `ds.writer` to support read replicas correctly).
PS: The new `NewSession` version mimics what we already do with other entities, like policies (`Datastore.NewGlobalPolicy`).
2022-04-04 23:52:05 +00:00
|
|
|
return session, nil
|
2022-03-08 16:27:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) GetSessionByKey(ctx context.Context, key string) (*fleet.Session, error) {
|
|
|
|
session, err := svc.ds.SessionByKey(ctx, key)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = svc.validateSession(ctx, session)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return session, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) validateSession(ctx context.Context, session *fleet.Session) error {
|
|
|
|
if session == nil {
|
|
|
|
return fleet.NewAuthRequiredError("active session not present")
|
|
|
|
}
|
|
|
|
|
|
|
|
sessionDuration := svc.config.Session.Duration
|
|
|
|
if session.APIOnly != nil && *session.APIOnly {
|
|
|
|
sessionDuration = 0 // make API-only tokens unlimited
|
|
|
|
}
|
|
|
|
|
|
|
|
// duration 0 = unlimited
|
|
|
|
if sessionDuration != 0 && time.Since(session.AccessedAt) >= sessionDuration {
|
|
|
|
err := svc.ds.DestroySession(ctx, session)
|
|
|
|
if err != nil {
|
|
|
|
return ctxerr.Wrap(ctx, err, "destroying session")
|
|
|
|
}
|
|
|
|
return fleet.NewAuthRequiredError("expired session")
|
|
|
|
}
|
|
|
|
|
|
|
|
return svc.ds.MarkSessionAccessed(ctx, session)
|
|
|
|
}
|
2022-07-01 19:52:55 +00:00
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// 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 {
|
Add UUID to Fleet errors and clean up error msgs (#10411)
#8129
Apart from fixing the issue in #8129, this change also introduces UUIDs
to Fleet errors. To be able to match a returned error from the API to a
error in the Fleet logs. See
https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for
more context.
Samples with the changes in this PR:
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d ''
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "Expected JSON Body"
}
],
"uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0"
}
```
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd'
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "json decoder error"
}
],
"uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d"
}
```
```
curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams"
{
"message": "Authentication required",
"errors": [
{
"name": "base",
"reason": "Authentication required"
}
],
"uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Authorization header required",
"errors": [
{
"name": "base",
"reason": "Authorization header required"
}
],
"uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Permission Denied",
"uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06"
}
```
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-03-13 16:44:06 +00:00
|
|
|
return nil, ctxerr.Wrap(ctx, &fleet.BadRequestError{
|
|
|
|
Message: "failed to parse form",
|
|
|
|
InternalErr: err,
|
|
|
|
}, "decode demo login")
|
2022-07-01 19:52:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2022-12-27 14:26:59 +00:00
|
|
|
return func(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2022-07-01 19:52:55 +00:00
|
|
|
req := request.(demologinRequest)
|
|
|
|
|
2022-07-27 19:47:39 +00:00
|
|
|
if !svc.SandboxEnabled() {
|
2022-07-01 19:52:55 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|