2016-09-26 18:48:55 +00:00
|
|
|
package service
|
2016-09-01 04:51:38 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
2016-09-26 17:14:39 +00:00
|
|
|
"fmt"
|
2016-09-29 04:21:39 +00:00
|
|
|
"reflect"
|
2016-09-01 04:51:38 +00:00
|
|
|
|
2016-09-26 17:14:39 +00:00
|
|
|
jwt "github.com/dgrijalva/jwt-go"
|
2016-09-01 04:51:38 +00:00
|
|
|
"github.com/go-kit/kit/endpoint"
|
2016-09-29 04:21:39 +00:00
|
|
|
hostctx "github.com/kolide/kolide-ose/server/contexts/host"
|
2016-09-26 17:14:39 +00:00
|
|
|
"github.com/kolide/kolide-ose/server/contexts/token"
|
|
|
|
"github.com/kolide/kolide-ose/server/contexts/viewer"
|
2016-09-29 00:46:45 +00:00
|
|
|
"github.com/kolide/kolide-ose/server/kolide"
|
2016-09-01 04:51:38 +00:00
|
|
|
"golang.org/x/net/context"
|
|
|
|
)
|
|
|
|
|
2016-09-26 17:14:39 +00:00
|
|
|
var errNoContext = errors.New("context key not set")
|
2016-09-01 04:51:38 +00:00
|
|
|
|
2016-09-29 04:21:39 +00:00
|
|
|
// authenticatedHost wraps an endpoint, checks the validity of the node_key
|
|
|
|
// provided in the request, and attaches the corresponding osquery host to the
|
|
|
|
// context for the request
|
|
|
|
func authenticatedHost(svc kolide.Service, next endpoint.Endpoint) endpoint.Endpoint {
|
2016-09-08 01:24:11 +00:00
|
|
|
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
2016-09-29 04:21:39 +00:00
|
|
|
nodeKey, err := getNodeKey(request)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
host, err := svc.AuthenticateHost(ctx, nodeKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx = hostctx.NewContext(ctx, *host)
|
|
|
|
return next(ctx, request)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func getNodeKey(r interface{}) (string, error) {
|
|
|
|
// Retrieve node key by reflection (note that our options here
|
|
|
|
// are limited by the fact that request is an interface{})
|
|
|
|
v := reflect.ValueOf(r)
|
|
|
|
if v.Kind() != reflect.Struct {
|
|
|
|
return "", osqueryError{
|
|
|
|
message: "request type is not struct. This is likely a Kolide programmer error.",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
nodeKeyField := v.FieldByName("NodeKey")
|
|
|
|
if !nodeKeyField.IsValid() {
|
|
|
|
return "", osqueryError{
|
|
|
|
message: "request struct missing NodeKey. This is likely a Kolide programmer error.",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if nodeKeyField.Kind() != reflect.String {
|
|
|
|
return "", osqueryError{
|
|
|
|
message: "NodeKey is not a string. This is likely a Kolide programmer error.",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nodeKeyField.String(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// authenticatedUser wraps an endpoint, requires that the Kolide user is
|
|
|
|
// authenticated, and populates the context with a Viewer struct for that user.
|
|
|
|
func authenticatedUser(jwtKey string, svc kolide.Service, next endpoint.Endpoint) endpoint.Endpoint {
|
|
|
|
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
|
|
|
// first check if already successfully set
|
2016-09-26 17:14:39 +00:00
|
|
|
if _, ok := viewer.FromContext(ctx); ok {
|
|
|
|
return next(ctx, request)
|
|
|
|
}
|
2016-09-29 04:21:39 +00:00
|
|
|
|
2016-09-26 17:14:39 +00:00
|
|
|
// if not succesful, try again this time with errors
|
|
|
|
bearer, ok := token.FromContext(ctx)
|
|
|
|
if !ok {
|
|
|
|
return nil, authError{reason: "no auth token"}
|
|
|
|
}
|
2016-09-29 04:21:39 +00:00
|
|
|
|
2016-09-26 17:14:39 +00:00
|
|
|
v, err := authViewer(ctx, jwtKey, bearer, svc)
|
2016-09-08 01:24:11 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2016-09-29 04:21:39 +00:00
|
|
|
|
2016-09-26 17:14:39 +00:00
|
|
|
ctx = viewer.NewContext(ctx, *v)
|
2016-09-08 01:24:11 +00:00
|
|
|
return next(ctx, request)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-09-26 17:14:39 +00:00
|
|
|
// authViewer creates an authenticated viewer by validating a JWT token.
|
|
|
|
func authViewer(ctx context.Context, jwtKey string, bearerToken string, svc kolide.Service) (*viewer.Viewer, error) {
|
|
|
|
jwtToken, err := jwt.Parse(bearerToken, func(token *jwt.Token) (interface{}, error) {
|
|
|
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
|
|
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
|
|
|
}
|
|
|
|
return []byte(jwtKey), nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
2016-09-29 00:46:45 +00:00
|
|
|
return nil, authError{reason: err.Error()}
|
2016-09-26 17:14:39 +00:00
|
|
|
}
|
|
|
|
claims, ok := jwtToken.Claims.(jwt.MapClaims)
|
|
|
|
if !ok {
|
2016-09-29 00:46:45 +00:00
|
|
|
return nil, authError{reason: "no jwt claims"}
|
2016-09-26 17:14:39 +00:00
|
|
|
}
|
|
|
|
sessionKeyClaim, ok := claims["session_key"]
|
|
|
|
if !ok {
|
2016-09-29 00:46:45 +00:00
|
|
|
return nil, authError{reason: "no session_key in JWT claims"}
|
2016-09-26 17:14:39 +00:00
|
|
|
}
|
|
|
|
sessionKey, ok := sessionKeyClaim.(string)
|
|
|
|
if !ok {
|
2016-09-29 00:46:45 +00:00
|
|
|
return nil, authError{reason: "non-string key in sessionClaim"}
|
2016-09-26 17:14:39 +00:00
|
|
|
}
|
|
|
|
session, err := svc.GetSessionByKey(ctx, sessionKey)
|
|
|
|
if err != nil {
|
2016-09-29 00:46:45 +00:00
|
|
|
return nil, authError{reason: err.Error()}
|
2016-09-26 17:14:39 +00:00
|
|
|
}
|
|
|
|
user, err := svc.User(ctx, session.UserID)
|
|
|
|
if err != nil {
|
2016-09-29 00:46:45 +00:00
|
|
|
return nil, authError{reason: err.Error()}
|
2016-09-26 17:14:39 +00:00
|
|
|
}
|
|
|
|
return &viewer.Viewer{User: user, Session: session}, nil
|
|
|
|
}
|
|
|
|
|
2016-09-01 04:51:38 +00:00
|
|
|
func mustBeAdmin(next endpoint.Endpoint) endpoint.Endpoint {
|
|
|
|
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
2016-09-26 17:14:39 +00:00
|
|
|
vc, ok := viewer.FromContext(ctx)
|
|
|
|
if !ok {
|
|
|
|
return nil, errNoContext
|
2016-09-01 04:51:38 +00:00
|
|
|
}
|
|
|
|
if !vc.IsAdmin() {
|
2016-09-23 02:41:58 +00:00
|
|
|
return nil, permissionError{message: "must be an admin"}
|
2016-09-01 04:51:38 +00:00
|
|
|
}
|
|
|
|
return next(ctx, request)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-09-15 14:52:17 +00:00
|
|
|
func canPerformActions(next endpoint.Endpoint) endpoint.Endpoint {
|
|
|
|
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
2016-09-26 17:14:39 +00:00
|
|
|
vc, ok := viewer.FromContext(ctx)
|
|
|
|
if !ok {
|
|
|
|
return nil, errNoContext
|
2016-09-15 14:52:17 +00:00
|
|
|
}
|
|
|
|
if !vc.CanPerformActions() {
|
2016-09-23 02:41:58 +00:00
|
|
|
return nil, permissionError{message: "no read permissions"}
|
2016-09-15 14:52:17 +00:00
|
|
|
}
|
|
|
|
return next(ctx, request)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-09-01 04:51:38 +00:00
|
|
|
func canReadUser(next endpoint.Endpoint) endpoint.Endpoint {
|
|
|
|
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
2016-09-26 17:14:39 +00:00
|
|
|
vc, ok := viewer.FromContext(ctx)
|
|
|
|
if !ok {
|
|
|
|
return nil, errNoContext
|
2016-09-01 04:51:38 +00:00
|
|
|
}
|
|
|
|
uid := requestUserIDFromContext(ctx)
|
|
|
|
if !vc.CanPerformReadActionOnUser(uid) {
|
2016-09-23 02:41:58 +00:00
|
|
|
return nil, permissionError{message: "no read permissions on user"}
|
2016-09-01 04:51:38 +00:00
|
|
|
}
|
|
|
|
return next(ctx, request)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func canModifyUser(next endpoint.Endpoint) endpoint.Endpoint {
|
|
|
|
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
2016-09-26 17:14:39 +00:00
|
|
|
vc, ok := viewer.FromContext(ctx)
|
|
|
|
if !ok {
|
|
|
|
return nil, errNoContext
|
2016-09-01 04:51:38 +00:00
|
|
|
}
|
|
|
|
uid := requestUserIDFromContext(ctx)
|
|
|
|
if !vc.CanPerformWriteActionOnUser(uid) {
|
2016-09-23 02:41:58 +00:00
|
|
|
return nil, permissionError{message: "no write permissions on user"}
|
2016-09-01 04:51:38 +00:00
|
|
|
}
|
|
|
|
return next(ctx, request)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-09-15 14:52:17 +00:00
|
|
|
type permission int
|
|
|
|
|
|
|
|
const (
|
|
|
|
anyone permission = iota
|
|
|
|
self
|
|
|
|
admin
|
|
|
|
)
|
|
|
|
|
|
|
|
func validateModifyUserRequest(next endpoint.Endpoint) endpoint.Endpoint {
|
|
|
|
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
|
|
|
r := request.(modifyUserRequest)
|
2016-09-26 17:14:39 +00:00
|
|
|
vc, ok := viewer.FromContext(ctx)
|
|
|
|
if !ok {
|
|
|
|
return nil, errNoContext
|
2016-09-15 14:52:17 +00:00
|
|
|
}
|
|
|
|
uid := requestUserIDFromContext(ctx)
|
|
|
|
p := r.payload
|
|
|
|
must := requireRoleForUserModification(p)
|
|
|
|
|
2016-09-23 02:41:58 +00:00
|
|
|
var badArgs []invalidArgument
|
|
|
|
if !vc.IsAdmin() {
|
|
|
|
for _, field := range must[admin] {
|
|
|
|
badArgs = append(badArgs, invalidArgument{name: field, reason: "must be an admin"})
|
2016-09-15 14:52:17 +00:00
|
|
|
}
|
|
|
|
}
|
2016-09-23 02:41:58 +00:00
|
|
|
if !vc.CanPerformWriteActionOnUser(uid) {
|
|
|
|
for _, field := range must[self] {
|
|
|
|
badArgs = append(badArgs, invalidArgument{name: field, reason: "no write permissions on user"})
|
2016-09-15 14:52:17 +00:00
|
|
|
}
|
|
|
|
}
|
2016-09-23 02:41:58 +00:00
|
|
|
if len(badArgs) != 0 {
|
|
|
|
return nil, permissionError{badArgs: badArgs}
|
|
|
|
}
|
2016-09-15 14:52:17 +00:00
|
|
|
return next(ctx, request)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// checks if fields were set in a user payload
|
|
|
|
// returns a map of updated fields for each role required
|
|
|
|
func requireRoleForUserModification(p kolide.UserPayload) map[permission][]string {
|
|
|
|
must := make(map[permission][]string)
|
|
|
|
adminFields := []string{}
|
|
|
|
if p.Enabled != nil {
|
|
|
|
adminFields = append(adminFields, "enabled")
|
|
|
|
}
|
|
|
|
if p.Admin != nil {
|
|
|
|
adminFields = append(adminFields, "admin")
|
|
|
|
}
|
|
|
|
if p.AdminForcedPasswordReset != nil {
|
|
|
|
adminFields = append(adminFields, "force_password_reset")
|
|
|
|
}
|
|
|
|
if len(adminFields) != 0 {
|
|
|
|
must[admin] = adminFields
|
|
|
|
}
|
|
|
|
|
|
|
|
selfFields := []string{}
|
|
|
|
if p.Username != nil {
|
|
|
|
selfFields = append(selfFields, "username")
|
|
|
|
}
|
|
|
|
if p.GravatarURL != nil {
|
|
|
|
selfFields = append(selfFields, "gravatar_url")
|
|
|
|
}
|
|
|
|
if p.Position != nil {
|
|
|
|
selfFields = append(selfFields, "position")
|
|
|
|
}
|
|
|
|
if p.Email != nil {
|
|
|
|
selfFields = append(selfFields, "email")
|
|
|
|
}
|
2016-09-26 16:29:51 +00:00
|
|
|
if p.Password != nil {
|
|
|
|
selfFields = append(selfFields, "password")
|
|
|
|
}
|
2016-09-15 14:52:17 +00:00
|
|
|
// self is always a must, otherwise
|
|
|
|
// anyone can edit the field, and we don't have that requirement
|
|
|
|
must[self] = selfFields
|
|
|
|
return must
|
|
|
|
}
|
|
|
|
|
2016-09-01 04:51:38 +00:00
|
|
|
func requestUserIDFromContext(ctx context.Context) uint {
|
|
|
|
userID, ok := ctx.Value("request-id").(uint)
|
|
|
|
if !ok {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
return userID
|
|
|
|
}
|