mirror of
https://github.com/empayre/fleet.git
synced 2024-11-07 01:15:22 +00:00
18faa5a06b
- Add policy.rego file defining authorization policies. - Add Go integrations to evaluate Rego policies (via OPA). - Add middleware to ensure requests without authorization check are rejected (guard against programmer error). - Add authorization checks to most service endpoints.
370 lines
10 KiB
Go
370 lines
10 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/xml"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
jwt "github.com/dgrijalva/jwt-go"
|
|
"github.com/fleetdm/fleet/server/contexts/viewer"
|
|
"github.com/fleetdm/fleet/server/kolide"
|
|
"github.com/fleetdm/fleet/server/sso"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
func (svc *Service) SSOSettings(ctx context.Context) (*kolide.SSOSettings, error) {
|
|
// skipauth: Basic SSO settings are available to unauthenticated users (so
|
|
// that they have the necessary information to initiate SSO).
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
appConfig, err := svc.ds.AppConfig()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "SSOSettings getting app config")
|
|
}
|
|
settings := &kolide.SSOSettings{
|
|
IDPName: appConfig.IDPName,
|
|
IDPImageURL: appConfig.IDPImageURL,
|
|
SSOEnabled: appConfig.EnableSSO,
|
|
}
|
|
return settings, nil
|
|
}
|
|
|
|
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)
|
|
|
|
appConfig, err := svc.ds.AppConfig()
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "InitiateSSO getting app config")
|
|
}
|
|
|
|
metadata, err := svc.getMetadata(appConfig)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "InitiateSSO getting metadata")
|
|
}
|
|
|
|
settings := sso.Settings{
|
|
Metadata: metadata,
|
|
// Construct call back url to send to idp
|
|
AssertionConsumerServiceURL: appConfig.KolideServerURL + svc.config.Server.URLPrefix + "/api/v1/fleet/sso/callback",
|
|
SessionStore: svc.ssoSessionStore,
|
|
OriginalURL: redirectURL,
|
|
}
|
|
|
|
// If issuer is not explicitly set, default to host name.
|
|
var issuer string
|
|
if appConfig.EntityID == "" {
|
|
u, err := url.Parse(appConfig.KolideServerURL)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "parsing kolide server url")
|
|
}
|
|
issuer = u.Hostname()
|
|
} else {
|
|
issuer = appConfig.EntityID
|
|
}
|
|
idpURL, err := sso.CreateAuthorizationRequest(&settings, issuer)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "InitiateSSO creating authorization")
|
|
}
|
|
|
|
return idpURL, nil
|
|
}
|
|
|
|
func (svc *Service) getMetadata(config *kolide.AppConfig) (*sso.Metadata, error) {
|
|
if config.MetadataURL != "" {
|
|
metadata, err := sso.GetMetadata(config.MetadataURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return metadata, nil
|
|
}
|
|
if config.Metadata != "" {
|
|
metadata, err := sso.ParseMetadata(config.Metadata)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return metadata, nil
|
|
}
|
|
return nil, errors.Errorf("missing metadata for idp %s", config.IDPName)
|
|
}
|
|
|
|
func (svc *Service) CallbackSSO(ctx context.Context, auth kolide.Auth) (*kolide.SSOSession, error) {
|
|
// skipauth: User context does not yet exist. Unauthenticated users may
|
|
// hit the SSO callback.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
appConfig, err := svc.ds.AppConfig()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "get config for sso")
|
|
}
|
|
|
|
// Load the request metadata if available
|
|
|
|
// localhost:9080/simplesaml/saml2/idp/SSOService.php?spentityid=https://localhost:8080
|
|
var metadata *sso.Metadata
|
|
var redirectURL string
|
|
if appConfig.EnableSSOIdPLogin && auth.RequestID() == "" {
|
|
// Missing request ID indicates this was IdP-initiated. Only allow if
|
|
// configured to do so.
|
|
metadata, err = svc.getMetadata(appConfig)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "get sso metadata")
|
|
}
|
|
redirectURL = "/"
|
|
} else {
|
|
session, err := svc.ssoSessionStore.Get(auth.RequestID())
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "sso request invalid")
|
|
}
|
|
// Remove session to so that is can't be reused before it expires.
|
|
err = svc.ssoSessionStore.Expire(auth.RequestID())
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "remove sso request")
|
|
}
|
|
if err := xml.Unmarshal([]byte(session.Metadata), &metadata); err != nil {
|
|
return nil, errors.Wrap(err, "unmarshal metadata")
|
|
}
|
|
redirectURL = session.OriginalURL
|
|
}
|
|
|
|
// Validate response
|
|
validator, err := sso.NewValidator(*metadata)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "create validator from metadata")
|
|
}
|
|
// make sure the response hasn't been tampered with
|
|
auth, err = validator.ValidateSignature(auth)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "signature validation failed")
|
|
}
|
|
// make sure the response isn't stale
|
|
err = validator.ValidateResponse(auth)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "response validation failed")
|
|
}
|
|
|
|
// Get and log in user
|
|
user, err := svc.userByEmailOrUsername(auth.UserID())
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "find user in sso callback")
|
|
}
|
|
// if the user is not sso enabled they are not authorized
|
|
if !user.SSOEnabled {
|
|
return nil, errors.New("user not configured to use sso")
|
|
}
|
|
token, err := svc.makeSession(user.ID)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "make session in sso callback")
|
|
}
|
|
result := &kolide.SSOSession{
|
|
Token: token,
|
|
RedirectURL: redirectURL,
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (svc *Service) Login(ctx context.Context, username, password string) (*kolide.User, string, error) {
|
|
// skipauth: No user context available yet to authorize against.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
// 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 {
|
|
time.Sleep(time.Until(start.Add(1 * time.Second)))
|
|
}
|
|
}(time.Now())
|
|
|
|
user, err := svc.userByEmailOrUsername(username)
|
|
if _, ok := err.(kolide.NotFoundError); ok {
|
|
return nil, "", kolide.NewAuthFailedError("user not found")
|
|
}
|
|
if err != nil {
|
|
return nil, "", kolide.NewAuthFailedError(err.Error())
|
|
}
|
|
|
|
if err = user.ValidatePassword(password); err != nil {
|
|
return nil, "", kolide.NewAuthFailedError("invalid password")
|
|
}
|
|
|
|
if user.SSOEnabled {
|
|
return nil, "", kolide.NewAuthFailedError("password login disabled for sso users")
|
|
}
|
|
|
|
token, err := svc.makeSession(user.ID)
|
|
if err != nil {
|
|
return nil, "", kolide.NewAuthFailedError(err.Error())
|
|
}
|
|
|
|
return user, token, nil
|
|
}
|
|
|
|
func (svc *Service) userByEmailOrUsername(username string) (*kolide.User, error) {
|
|
if strings.Contains(username, "@") {
|
|
return svc.ds.UserByEmail(username)
|
|
}
|
|
return svc.ds.User(username)
|
|
}
|
|
|
|
// makeSession is a helper that creates a new session after authentication
|
|
func (svc *Service) makeSession(id uint) (string, error) {
|
|
sessionKeySize := svc.config.Session.KeySize
|
|
key := make([]byte, sessionKeySize)
|
|
_, err := rand.Read(key)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
session := &kolide.Session{
|
|
UserID: id,
|
|
Key: base64.StdEncoding.EncodeToString(key),
|
|
AccessedAt: time.Now().UTC(),
|
|
}
|
|
|
|
session, err = svc.ds.NewSession(session)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "creating new session")
|
|
}
|
|
|
|
tokenString, err := generateJWT(session.Key, svc.config.Auth.JwtKey)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "generating JWT token")
|
|
}
|
|
|
|
return tokenString, nil
|
|
}
|
|
|
|
func (svc *Service) Logout(ctx context.Context) error {
|
|
// skipauth: Any user can always log out of their own session.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
// TODO: this should not return an error if the user wasn't logged in
|
|
return svc.DestroySession(ctx)
|
|
}
|
|
|
|
func (svc *Service) DestroySession(ctx context.Context) error {
|
|
vc, ok := viewer.FromContext(ctx)
|
|
if !ok {
|
|
return kolide.ErrNoContext
|
|
}
|
|
|
|
session, err := svc.ds.SessionByID(vc.SessionID())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := svc.authz.Authorize(ctx, session, kolide.ActionWrite); err != nil {
|
|
return err
|
|
}
|
|
|
|
return svc.ds.DestroySession(session)
|
|
}
|
|
|
|
func (svc *Service) GetInfoAboutSessionsForUser(ctx context.Context, id uint) ([]*kolide.Session, error) {
|
|
if err := svc.authz.Authorize(ctx, &kolide.Session{UserID: id}, kolide.ActionWrite); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var validatedSessions []*kolide.Session
|
|
|
|
sessions, err := svc.ds.ListSessionsForUser(id)
|
|
if err != nil {
|
|
return validatedSessions, err
|
|
}
|
|
|
|
for _, session := range sessions {
|
|
if svc.validateSession(session) == nil {
|
|
validatedSessions = append(validatedSessions, session)
|
|
}
|
|
}
|
|
|
|
return validatedSessions, nil
|
|
}
|
|
|
|
func (svc *Service) DeleteSessionsForUser(ctx context.Context, id uint) error {
|
|
if err := svc.authz.Authorize(ctx, &kolide.Session{UserID: id}, kolide.ActionWrite); err != nil {
|
|
return err
|
|
}
|
|
|
|
return svc.ds.DestroyAllSessionsForUser(id)
|
|
}
|
|
|
|
func (svc *Service) GetInfoAboutSession(ctx context.Context, id uint) (*kolide.Session, error) {
|
|
session, err := svc.ds.SessionByID(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := svc.authz.Authorize(ctx, &kolide.Session{UserID: id}, kolide.ActionRead); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = svc.validateSession(session)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return session, nil
|
|
}
|
|
|
|
func (svc *Service) GetSessionByKey(ctx context.Context, key string) (*kolide.Session, error) {
|
|
session, err := svc.ds.SessionByKey(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = svc.validateSession(session)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return session, nil
|
|
}
|
|
|
|
func (svc *Service) DeleteSession(ctx context.Context, id uint) error {
|
|
session, err := svc.ds.SessionByID(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := svc.authz.Authorize(ctx, session, kolide.ActionWrite); err != nil {
|
|
return err
|
|
}
|
|
|
|
return svc.ds.DestroySession(session)
|
|
}
|
|
|
|
func (svc *Service) validateSession(session *kolide.Session) error {
|
|
if session == nil {
|
|
return kolide.NewAuthRequiredError("active session not present")
|
|
}
|
|
|
|
sessionDuration := svc.config.Session.Duration
|
|
// duration 0 = unlimited
|
|
if sessionDuration != 0 && time.Since(session.AccessedAt) >= sessionDuration {
|
|
err := svc.ds.DestroySession(session)
|
|
if err != nil {
|
|
return errors.Wrap(err, "destroying session")
|
|
}
|
|
return kolide.NewAuthRequiredError("expired session")
|
|
}
|
|
|
|
return svc.ds.MarkSessionAccessed(session)
|
|
}
|
|
|
|
// Given a session key create a JWT to be delivered to the client
|
|
func generateJWT(sessionKey, jwtKey string) (string, error) {
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
|
"session_key": sessionKey,
|
|
})
|
|
|
|
return token.SignedString([]byte(jwtKey))
|
|
}
|