/*
Copyright (c) 2018 VMware, Inc. All Rights Reserved.

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 sts

import (
	"context"
	"crypto/tls"
	"errors"
	"net/url"
	"time"

	"github.com/vmware/govmomi/lookup"
	"github.com/vmware/govmomi/lookup/types"
	"github.com/vmware/govmomi/sts/internal"
	"github.com/vmware/govmomi/vim25"
	"github.com/vmware/govmomi/vim25/soap"
)

const (
	Namespace = "oasis:names:tc:SAML:2.0:assertion"
	Path      = "/sts/STSService"
)

// Client is a soap.Client targeting the STS (Secure Token Service) API endpoint.
type Client struct {
	*soap.Client
}

// NewClient returns a client targeting the STS API endpoint.
// The Client.URL will be set to that of the Lookup Service's endpoint registration,
// as the SSO endpoint can be external to vCenter.  If the Lookup Service is not available,
// URL defaults to Path on the vim25.Client.URL.Host.
func NewClient(ctx context.Context, c *vim25.Client) (*Client, error) {
	filter := &types.LookupServiceRegistrationFilter{
		ServiceType: &types.LookupServiceRegistrationServiceType{
			Product: "com.vmware.cis",
			Type:    "sso:sts",
		},
		EndpointType: &types.LookupServiceRegistrationEndpointType{
			Protocol: "wsTrust",
			Type:     "com.vmware.cis.cs.identity.sso",
		},
	}

	url := lookup.EndpointURL(ctx, c, Path, filter)
	sc := c.Client.NewServiceClient(url, Namespace)

	return &Client{sc}, nil
}

// TokenRequest parameters for issuing a SAML token.
// At least one of Userinfo or Certificate must be specified.
type TokenRequest struct {
	Userinfo    *url.Userinfo    // Userinfo when set issues a Bearer token
	Certificate *tls.Certificate // Certificate when set issues a HoK token
	Lifetime    time.Duration    // Lifetime is the token's lifetime, defaults to 10m
	Renewable   bool             // Renewable allows the issued token to be renewed
	Delegatable bool             // Delegatable allows the issued token to be delegated (e.g. for use with ActAs)
	ActAs       bool             // ActAs allows to request an ActAs token based on the passed Token.
	Token       string           // Token for Renew request or Issue request ActAs identity or to be exchanged.
	KeyType     string           // KeyType for requested token (if not set will be decucted from Userinfo and Certificate options)
	KeyID       string           // KeyID used for signing the requests
}

func (c *Client) newRequest(req TokenRequest, kind string, s *Signer) (internal.RequestSecurityToken, error) {
	if req.Lifetime == 0 {
		req.Lifetime = 5 * time.Minute
	}

	created := time.Now().UTC()
	rst := internal.RequestSecurityToken{
		TokenType:          c.Namespace,
		RequestType:        "http://docs.oasis-open.org/ws-sx/ws-trust/200512/" + kind,
		SignatureAlgorithm: internal.SHA256,
		Lifetime: &internal.Lifetime{
			Created: created.Format(internal.Time),
			Expires: created.Add(req.Lifetime).Format(internal.Time),
		},
		Renewing: &internal.Renewing{
			Allow: req.Renewable,
			// /wst:RequestSecurityToken/wst:Renewing/@OK
			// "It NOT RECOMMENDED to use this as it can leave you open to certain types of security attacks.
			// Issuers MAY restrict the period after expiration during which time the token can be renewed.
			// This window is governed by the issuer's policy."
			OK: false,
		},
		Delegatable: req.Delegatable,
		KeyType:     req.KeyType,
	}

	if req.KeyType == "" {
		// Deduce KeyType based on Certificate nad Userinfo.
		if req.Certificate == nil {
			if req.Userinfo == nil {
				return rst, errors.New("one of TokenRequest Certificate or Userinfo is required")
			}
			rst.KeyType = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer"
		} else {
			rst.KeyType = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/PublicKey"
			// For HOK KeyID is required.
			if req.KeyID == "" {
				req.KeyID = newID()
			}
		}
	}

	if req.KeyID != "" {
		rst.UseKey = &internal.UseKey{Sig: req.KeyID}
		s.keyID = rst.UseKey.Sig
	}

	return rst, nil
}

func (s *Signer) setLifetime(lifetime *internal.Lifetime) error {
	var err error
	if lifetime != nil {
		s.Lifetime.Created, err = time.Parse(internal.Time, lifetime.Created)
		if err == nil {
			s.Lifetime.Expires, err = time.Parse(internal.Time, lifetime.Expires)
		}
	}
	return err
}

// Issue is used to request a security token.
// The returned Signer can be used to sign SOAP requests, such as the SessionManager LoginByToken method and the RequestSecurityToken method itself.
// One of TokenRequest Certificate or Userinfo is required, with Certificate taking precedence.
// When Certificate is set, a Holder-of-Key token will be requested.  Otherwise, a Bearer token is requested with the Userinfo credentials.
// See: http://docs.oasis-open.org/ws-sx/ws-trust/v1.4/errata01/os/ws-trust-1.4-errata01-os-complete.html#_Toc325658937
func (c *Client) Issue(ctx context.Context, req TokenRequest) (*Signer, error) {
	s := &Signer{
		Certificate: req.Certificate,
		keyID:       req.KeyID,
		Token:       req.Token,
		user:        req.Userinfo,
	}

	rst, err := c.newRequest(req, "Issue", s)
	if err != nil {
		return nil, err
	}

	if req.ActAs {
		rst.ActAs = &internal.Target{
			Token: req.Token,
		}
	}

	header := soap.Header{
		Security: s,
		Action:   rst.Action(),
	}

	res, err := internal.Issue(c.WithHeader(ctx, header), c, &rst)
	if err != nil {
		return nil, err
	}

	s.Token = res.RequestSecurityTokenResponse.RequestedSecurityToken.Assertion

	return s, s.setLifetime(res.RequestSecurityTokenResponse.Lifetime)
}

// Renew is used to request a security token renewal.
func (c *Client) Renew(ctx context.Context, req TokenRequest) (*Signer, error) {
	s := &Signer{
		Certificate: req.Certificate,
	}

	rst, err := c.newRequest(req, "Renew", s)
	if err != nil {
		return nil, err
	}

	if req.Token == "" {
		return nil, errors.New("TokenRequest Token is required")
	}

	rst.RenewTarget = &internal.Target{Token: req.Token}

	header := soap.Header{
		Security: s,
		Action:   rst.Action(),
	}

	res, err := internal.Renew(c.WithHeader(ctx, header), c, &rst)
	if err != nil {
		return nil, err
	}

	s.Token = res.RequestedSecurityToken.Assertion

	return s, s.setLifetime(res.Lifetime)
}
