// Copyright 2022-2023 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package server

import (
	"bytes"
	"crypto/tls"
	"encoding/pem"
	"errors"
	"fmt"
	"time"
	"unicode"

	"github.com/nats-io/jwt/v2"
	"github.com/nats-io/nkeys"
)

const (
	AuthCalloutSubject    = "$SYS.REQ.USER.AUTH"
	AuthRequestSubject    = "nats-authorization-request"
	AuthRequestXKeyHeader = "Nats-Server-Xkey"
)

// Process a callout on this client's behalf.
func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorized bool, errStr string) {
	isOperatorMode := len(opts.TrustedKeys) > 0

	// this is the account the user connected in, or the one running the callout
	var acc *Account
	if !isOperatorMode && opts.AuthCallout != nil && opts.AuthCallout.Account != _EMPTY_ {
		aname := opts.AuthCallout.Account
		var err error
		acc, err = s.LookupAccount(aname)
		if err != nil {
			errStr = fmt.Sprintf("No valid account %q for auth callout request: %v", aname, err)
			s.Warnf(errStr)
			return false, errStr
		}
	} else {
		acc = c.acc
	}

	// Check if we have been requested to encrypt.
	var xkp nkeys.KeyPair
	var xkey string
	var pubAccXKey string
	if !isOperatorMode && opts.AuthCallout != nil && opts.AuthCallout.XKey != _EMPTY_ {
		pubAccXKey = opts.AuthCallout.XKey
	} else if isOperatorMode {
		pubAccXKey = acc.externalAuthXKey()
	}
	// If set grab server's xkey keypair and public key.
	if pubAccXKey != _EMPTY_ {
		// These are only set on creation, so lock not needed.
		xkp, xkey = s.xkp, s.info.XKey
	}

	// FIXME: so things like the server ID that get assigned, are used as a sort of nonce - but
	//  reality is that the keypair here, is generated, so the response generated a JWT has to be
	//  this user - no replay possible
	// Create a keypair for the user. We will expect this public user to be in the signed response.
	// This prevents replay attacks.
	ukp, _ := nkeys.CreateUser()
	pub, _ := ukp.PublicKey()

	reply := s.newRespInbox()
	respCh := make(chan string, 1)

	decodeResponse := func(rc *client, rmsg []byte, acc *Account) (*jwt.UserClaims, error) {
		account := acc.Name
		_, msg := rc.msgParts(rmsg)

		// This signals not authorized.
		// Since this is an account subscription will always have "\r\n".
		if len(msg) <= LEN_CR_LF {
			return nil, fmt.Errorf("auth callout violation: %q on account %q", "no reason supplied", account)
		}
		// Strip trailing CRLF.
		msg = msg[:len(msg)-LEN_CR_LF]
		encrypted := false
		// If we sent an encrypted request the response could be encrypted as well.
		// we are expecting the input to be `eyJ` if it is a JWT
		if xkp != nil && len(msg) > 0 && !bytes.HasPrefix(msg, []byte(jwtPrefix)) {
			var err error
			msg, err = xkp.Open(msg, pubAccXKey)
			if err != nil {
				return nil, fmt.Errorf("error decrypting auth callout response on account %q: %v", account, err)
			}
			encrypted = true
		}

		cr, err := jwt.DecodeAuthorizationResponseClaims(string(msg))
		if err != nil {
			return nil, err
		}
		vr := jwt.CreateValidationResults()
		cr.Validate(vr)
		if len(vr.Issues) > 0 {
			return nil, fmt.Errorf("authorization response had validation errors: %v", vr.Issues[0])
		}

		// the subject is the user id
		if cr.Subject != pub {
			return nil, errors.New("auth callout violation: auth callout response is not for expected user")
		}

		// check the audience to be the server ID
		if cr.Audience != s.info.ID {
			return nil, errors.New("auth callout violation: auth callout response is not for server")
		}

		// check if had an error message from the auth account
		if cr.Error != _EMPTY_ {
			return nil, fmt.Errorf("auth callout service returned an error: %v", cr.Error)
		}

		// if response is encrypted none of this is needed
		if isOperatorMode && !encrypted {
			pkStr := cr.Issuer
			if cr.IssuerAccount != _EMPTY_ {
				pkStr = cr.IssuerAccount
			}
			if pkStr != account {
				if _, ok := acc.signingKeys[pkStr]; !ok {
					return nil, errors.New("auth callout signing key is unknown")
				}
			}
		}

		return jwt.DecodeUserClaims(cr.Jwt)
	}

	// getIssuerAccount returns the issuer (as per JWT) - it also asserts that
	// only in operator mode we expect to receive `issuer_account`.
	getIssuerAccount := func(arc *jwt.UserClaims, account string) (string, error) {
		// Make sure correct issuer.
		var issuer string
		if opts.AuthCallout != nil {
			issuer = opts.AuthCallout.Issuer
		} else {
			// Operator mode is who we send the request on unless switching accounts.
			issuer = acc.Name
		}

		// the jwt issuer can be a signing key
		jwtIssuer := arc.Issuer
		if arc.IssuerAccount != _EMPTY_ {
			if !isOperatorMode {
				// this should be invalid - effectively it would allow the auth callout
				// to issue on another account which may be allowed given the configuration
				// where the auth callout account can handle multiple different ones..
				return _EMPTY_, fmt.Errorf("error non operator mode account %q: attempted to use issuer_account", account)
			}
			jwtIssuer = arc.IssuerAccount
		}

		if jwtIssuer != issuer {
			if !isOperatorMode {
				return _EMPTY_, fmt.Errorf("wrong issuer for auth callout response on account %q, expected %q got %q", account, issuer, jwtIssuer)
			} else if !acc.isAllowedAcount(jwtIssuer) {
				return _EMPTY_, fmt.Errorf("account %q not permitted as valid account option for auth callout for account %q",
					arc.Issuer, account)
			}
		}
		return jwtIssuer, nil
	}

	getExpirationAndAllowedConnections := func(arc *jwt.UserClaims, account string) (time.Duration, map[string]struct{}, error) {
		allowNow, expiration := validateTimes(arc)
		if !allowNow {
			c.Errorf("Outside connect times")
			return 0, nil, fmt.Errorf("authorized user on account %q outside of valid connect times", account)
		}

		allowedConnTypes, err := convertAllowedConnectionTypes(arc.User.AllowedConnectionTypes)
		if err != nil {
			c.Debugf("%v", err)
			if len(allowedConnTypes) == 0 {
				return 0, nil, fmt.Errorf("authorized user on account %q using invalid connection type", account)
			}
		}
		return expiration, allowedConnTypes, nil
	}

	assignAccountAndPermissions := func(arc *jwt.UserClaims, account string) (*Account, error) {
		// Apply to this client.
		var err error
		issuerAccount, err := getIssuerAccount(arc, account)
		if err != nil {
			return nil, err
		}

		// if we are not in operator mode, they can specify placement as a tag
		var placement string
		if !isOperatorMode {
			// only allow placement if we are not in operator mode
			placement = arc.Audience
		} else {
			placement = issuerAccount
		}

		targetAcc, err := s.LookupAccount(placement)
		if err != nil {
			return nil, fmt.Errorf("no valid account %q for auth callout response on account %q: %v", placement, account, err)
		}
		if isOperatorMode {
			// this will validate the signing key that emitted the user, and if it is a signing
			// key it assigns the permissions from the target account
			if scope, ok := targetAcc.hasIssuer(arc.Issuer); !ok {
				return nil, fmt.Errorf("user JWT issuer %q is not known", arc.Issuer)
			} else if scope != nil {
				// this possibly has to be different because it could just be a plain issued by a non-scoped signing key
				if err := scope.ValidateScopedSigner(arc); err != nil {
					return nil, fmt.Errorf("user JWT is not valid: %v", err)
				} else if uSc, ok := scope.(*jwt.UserScope); !ok {
					return nil, fmt.Errorf("user JWT is not a valid scoped user")
				} else if arc.User.UserPermissionLimits, err = processUserPermissionsTemplate(uSc.Template, arc, targetAcc); err != nil {
					return nil, fmt.Errorf("user JWT generated invalid permissions: %v", err)
				}
			}
		}

		return targetAcc, nil
	}

	processReply := func(_ *subscription, rc *client, racc *Account, subject, reply string, rmsg []byte) {
		titleCase := func(m string) string {
			r := []rune(m)
			return string(append([]rune{unicode.ToUpper(r[0])}, r[1:]...))
		}

		arc, err := decodeResponse(rc, rmsg, racc)
		if err != nil {
			respCh <- titleCase(err.Error())
			return
		}
		vr := jwt.CreateValidationResults()
		arc.Validate(vr)
		if len(vr.Issues) > 0 {
			respCh <- fmt.Sprintf("Error validating user JWT: %v", vr.Issues[0])
			return
		}

		// Make sure that the user is what we requested.
		if arc.Subject != pub {
			respCh <- fmt.Sprintf("Expected authorized user of %q but got %q on account %q", pub, arc.Subject, racc.Name)
			return
		}

		expiration, allowedConnTypes, err := getExpirationAndAllowedConnections(arc, racc.Name)
		if err != nil {
			respCh <- titleCase(err.Error())
			return
		}

		targetAcc, err := assignAccountAndPermissions(arc, racc.Name)
		if err != nil {
			respCh <- titleCase(err.Error())
			return
		}

		// Build internal user and bind to the targeted account.
		nkuser := buildInternalNkeyUser(arc, allowedConnTypes, targetAcc)
		if err := c.RegisterNkeyUser(nkuser); err != nil {
			respCh <- fmt.Sprintf("Could not register auth callout user: %v", err)
			return
		}

		// See if the response wants to override the username.
		if arc.Name != _EMPTY_ {
			c.mu.Lock()
			c.opts.Username = arc.Name
			// Clear any others.
			c.opts.Nkey = _EMPTY_
			c.pubKey = _EMPTY_
			c.opts.Token = _EMPTY_
			c.mu.Unlock()
		}

		// Check if we need to set an auth timer if the user jwt expires.
		c.setExpiration(arc.Claims(), expiration)

		respCh <- _EMPTY_
	}

	// create a subscription to receive a response from the authcallout
	sub, err := acc.subscribeInternal(reply, processReply)
	if err != nil {
		errStr = fmt.Sprintf("Error setting up reply subscription for auth request: %v", err)
		s.Warnf(errStr)
		return false, errStr
	}
	defer acc.unsubscribeInternal(sub)

	// Build our request claims - jwt subject should be nkey
	jwtSub := acc.Name
	if opts.AuthCallout != nil {
		jwtSub = opts.AuthCallout.Issuer
	}

	// The public key of the server, if set is available on Varz.Key
	// This means that when a service connects, it can now peer
	// authenticate if it wants to - but that also means that it needs to be
	// listening to cluster changes
	claim := jwt.NewAuthorizationRequestClaims(jwtSub)
	claim.Audience = AuthRequestSubject
	// Set expected public user nkey.
	claim.UserNkey = pub

	s.mu.RLock()
	claim.Server = jwt.ServerID{
		Name:    s.info.Name,
		Host:    s.info.Host,
		ID:      s.info.ID,
		Version: s.info.Version,
		Cluster: s.info.Cluster,
	}
	s.mu.RUnlock()

	// Tags
	claim.Server.Tags = s.getOpts().Tags

	// Check if we have been requested to encrypt.
	// FIXME: possibly this public key also needs to be on the
	//  Varz, because then it can be peer verified?
	if xkp != nil {
		claim.Server.XKey = xkey
	}

	authTimeout := secondsToDuration(s.getOpts().AuthTimeout)
	claim.Expires = time.Now().Add(time.Duration(authTimeout)).UTC().Unix()

	// Grab client info for the request.
	c.mu.Lock()
	c.fillClientInfo(&claim.ClientInformation)
	c.fillConnectOpts(&claim.ConnectOptions)
	// If we have a sig in the client opts, fill in nonce.
	if claim.ConnectOptions.SignedNonce != _EMPTY_ {
		claim.ClientInformation.Nonce = string(c.nonce)
	}

	// TLS
	if c.flags.isSet(handshakeComplete) && c.nc != nil {
		var ct jwt.ClientTLS
		conn := c.nc.(*tls.Conn)
		cs := conn.ConnectionState()
		ct.Version = tlsVersion(cs.Version)
		ct.Cipher = tlsCipher(cs.CipherSuite)
		// Check verified chains.
		for _, vs := range cs.VerifiedChains {
			var certs []string
			for _, c := range vs {
				blk := &pem.Block{
					Type:  "CERTIFICATE",
					Bytes: c.Raw,
				}
				certs = append(certs, string(pem.EncodeToMemory(blk)))
			}
			ct.VerifiedChains = append(ct.VerifiedChains, certs)
		}
		// If we do not have verified chains put in peer certs.
		if len(ct.VerifiedChains) == 0 {
			for _, c := range cs.PeerCertificates {
				blk := &pem.Block{
					Type:  "CERTIFICATE",
					Bytes: c.Raw,
				}
				ct.Certs = append(ct.Certs, string(pem.EncodeToMemory(blk)))
			}
		}
		claim.TLS = &ct
	}
	c.mu.Unlock()

	b, err := claim.Encode(s.kp)
	if err != nil {
		errStr = fmt.Sprintf("Error encoding auth request claim on account %q: %v", acc.Name, err)
		s.Warnf(errStr)
		return false, errStr
	}
	req := []byte(b)
	var hdr map[string]string

	// Check if we have been asked to encrypt.
	if xkp != nil {
		req, err = xkp.Seal([]byte(req), pubAccXKey)
		if err != nil {
			errStr = fmt.Sprintf("Error encrypting auth request claim on account %q: %v", acc.Name, err)
			s.Warnf(errStr)
			return false, errStr
		}
		hdr = map[string]string{AuthRequestXKeyHeader: xkey}
	}

	// Send out our request.
	if err := s.sendInternalAccountMsgWithReply(acc, AuthCalloutSubject, reply, hdr, req, false); err != nil {
		errStr = fmt.Sprintf("Error sending authorization request: %v", err)
		s.Debugf(errStr)
		return false, errStr
	}
	select {
	case errStr = <-respCh:
		if authorized = errStr == _EMPTY_; !authorized {
			s.Warnf(errStr)
		}
	case <-time.After(authTimeout):
		s.Debugf(fmt.Sprintf("Authorization callout response not received in time on account %q", acc.Name))
	}

	return authorized, errStr
}

// Fill in client information for the request.
// Lock should be held.
func (c *client) fillClientInfo(ci *jwt.ClientInformation) {
	if c == nil || (c.kind != CLIENT && c.kind != LEAF && c.kind != JETSTREAM && c.kind != ACCOUNT) {
		return
	}

	// Do it this way to fail to compile if fields are added to jwt.ClientInformation.
	*ci = jwt.ClientInformation{
		Host:    c.host,
		ID:      c.cid,
		User:    c.getRawAuthUser(),
		Name:    c.opts.Name,
		Tags:    c.tags,
		NameTag: c.nameTag,
		Kind:    c.kindString(),
		Type:    c.clientTypeString(),
		MQTT:    c.getMQTTClientID(),
	}
}

// Fill in client options.
// Lock should be held.
func (c *client) fillConnectOpts(opts *jwt.ConnectOptions) {
	if c == nil || (c.kind != CLIENT && c.kind != LEAF && c.kind != JETSTREAM && c.kind != ACCOUNT) {
		return
	}

	o := c.opts

	// Do it this way to fail to compile if fields are added to jwt.ClientInformation.
	*opts = jwt.ConnectOptions{
		JWT:         o.JWT,
		Nkey:        o.Nkey,
		SignedNonce: o.Sig,
		Token:       o.Token,
		Username:    o.Username,
		Password:    o.Password,
		Name:        o.Name,
		Lang:        o.Lang,
		Version:     o.Version,
		Protocol:    o.Protocol,
	}
}
