EdgexAgent/device-gps-go/vendor/github.com/openziti/sdk-golang/ziti/client.go
2025-07-10 20:30:06 +08:00

507 lines
16 KiB
Go

/*
Copyright 2019 NetFoundry Inc.
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
https://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 ziti
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha1"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"github.com/go-openapi/strfmt"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/michaelquigley/pfxlog"
"github.com/openziti/edge-api/rest_client_api_client/authentication"
"github.com/openziti/edge-api/rest_client_api_client/current_api_session"
"github.com/openziti/edge-api/rest_client_api_client/current_identity"
"github.com/openziti/edge-api/rest_client_api_client/informational"
"github.com/openziti/edge-api/rest_client_api_client/posture_checks"
"github.com/openziti/edge-api/rest_client_api_client/service"
"github.com/openziti/edge-api/rest_client_api_client/session"
"github.com/openziti/edge-api/rest_model"
"github.com/openziti/edge-api/rest_util"
"github.com/openziti/foundation/v2/genext"
nfPem "github.com/openziti/foundation/v2/pem"
"github.com/openziti/foundation/v2/versions"
"github.com/openziti/identity"
apis "github.com/openziti/sdk-golang/edge-apis"
"github.com/openziti/sdk-golang/ziti/edge/posture"
"github.com/openziti/transport/v2"
"github.com/pkg/errors"
"strings"
"sync/atomic"
)
// CtrlClient is a stateful version of ZitiEdgeClient that simplifies operations
type CtrlClient struct {
*apis.ClientApiClient
Credentials apis.Credentials
lastServiceUpdate *strfmt.DateTime
ApiSessionCertificateDetail rest_model.CurrentAPISessionCertificateDetail
ApiSessionCsr x509.CertificateRequest
ApiSessionCertificate *x509.Certificate
ApiSessionPrivateKey *ecdsa.PrivateKey
ApiSessionCertInstance string
PostureCache *posture.Cache
ConfigTypes []string
supportsConfigTypesOnServiceList atomic.Bool
capabilitiesLoaded atomic.Bool
}
// GetCurrentApiSession returns the current cached ApiSession or nil
func (self *CtrlClient) GetCurrentApiSession() apis.ApiSession {
return self.ClientApiClient.GetCurrentApiSession()
}
// Refresh will contact the controller extending the current ApiSession for legacy API Sessions
func (self *CtrlClient) Refresh() (apis.ApiSession, error) {
if apiSession := self.GetCurrentApiSession(); apiSession != nil {
newApiSession, err := self.API.RefreshApiSession(apiSession, self.HttpClient)
if err != nil {
return nil, err
}
self.ApiSession.Store(&newApiSession)
return newApiSession, nil
}
return nil, errors.New("no api session")
}
// IsServiceListUpdateAvailable will contact the controller to determine if a new set of services are available. Service
// updates could entail gaining/losing services access via policy or runtime authorization revocation due to posture
// checks.
func (self *CtrlClient) IsServiceListUpdateAvailable() (bool, *strfmt.DateTime, error) {
resp, err := self.API.CurrentAPISession.ListServiceUpdates(current_api_session.NewListServiceUpdatesParams(), self.GetCurrentApiSession())
if err != nil {
return true, nil, err
}
return self.lastServiceUpdate == nil || !resp.Payload.Data.LastChangeAt.Equal(*self.lastServiceUpdate), resp.Payload.Data.LastChangeAt, nil
}
// Authenticate attempts to use authenticate, overwriting any existing ApiSession.
func (self *CtrlClient) Authenticate() (apis.ApiSession, error) {
var err error
self.ApiSessionCertificate = nil
apiSession, err := self.ClientApiClient.Authenticate(self.Credentials, self.ConfigTypes)
if err != nil {
return nil, rest_util.WrapErr(err)
}
_, err = self.GetIdentity()
if err != nil {
return nil, rest_util.WrapErr(err)
}
return apiSession, nil
}
// AuthenticateMFA handles MFA authentication queries may be provided. AuthenticateMFA allows
// the current identity for their current api session to attempt to pass MFA authentication.
func (self *CtrlClient) AuthenticateMFA(code string) error {
params := authentication.NewAuthenticateMfaParams()
params.MfaAuth = &rest_model.MfaCode{
Code: &code,
}
_, err := self.API.Authentication.AuthenticateMfa(params, self.GetCurrentApiSession())
if err != nil {
return rest_util.WrapErr(err)
}
return nil
}
// SendPostureResponse creates a posture response (some state data the controller has requested) for services. This
// information is used to determine runtime authorization access to services via posture checks.
func (self *CtrlClient) SendPostureResponse(response rest_model.PostureResponseCreate) error {
params := posture_checks.NewCreatePostureResponseParams()
params.PostureResponse = response
_, err := self.API.PostureChecks.CreatePostureResponse(params, self.GetCurrentApiSession())
if err != nil {
return rest_util.WrapErr(err)
}
return nil
}
// SendPostureResponseBulk provides the same functionality as SendPostureResponse but allows multiple responses
// to be sent in a single request.
func (self *CtrlClient) SendPostureResponseBulk(responses []rest_model.PostureResponseCreate) error {
params := posture_checks.NewCreatePostureResponseBulkParams()
params.PostureResponse = responses
_, err := self.API.PostureChecks.CreatePostureResponseBulk(params, self.GetCurrentApiSession())
if err != nil {
return rest_util.WrapErr(err)
}
return nil
}
// GetCurrentIdentity returns the rest_model.IdentityDetail for the currently authenticated ApiSession.
func (self *CtrlClient) GetCurrentIdentity() (*rest_model.IdentityDetail, error) {
params := current_identity.NewGetCurrentIdentityParams()
resp, err := self.API.CurrentIdentity.GetCurrentIdentity(params, self.GetCurrentApiSession())
if err != nil {
return nil, rest_util.WrapErr(err)
}
return resp.Payload.Data, nil
}
// GetSession returns the full rest_model.SessionDetail for a specific id. Does not function with JWT backed sessions.
func (self *CtrlClient) GetSession(id string) (*rest_model.SessionDetail, error) {
params := session.NewDetailSessionParams()
params.ID = id
resp, err := self.API.Session.DetailSession(params, self.GetCurrentApiSession())
if err != nil {
return nil, rest_util.WrapErr(err)
}
self.sanitizeSessionUrls(resp.Payload.Data)
return resp.Payload.Data, nil
}
func (self *CtrlClient) GetSessionFromJwt(sessionToken string) (*rest_model.SessionDetail, error) {
parser := jwt.NewParser()
serviceAccessClaims := &apis.ServiceAccessClaims{}
_, _, err := parser.ParseUnverified(sessionToken, serviceAccessClaims)
if err != nil {
return nil, err
}
params := service.NewListServiceEdgeRoutersParams()
params.SessionToken = &sessionToken
params.ID = serviceAccessClaims.Subject //service id
resp, err := self.API.Service.ListServiceEdgeRouters(params, self.GetCurrentApiSession())
if err != nil {
return nil, rest_util.WrapErr(err)
}
createdAt := strfmt.DateTime(serviceAccessClaims.IssuedAt.Time)
sessionType := rest_model.DialBind(serviceAccessClaims.Type)
sessionDetail := &rest_model.SessionDetail{
BaseEntity: rest_model.BaseEntity{
Links: nil,
CreatedAt: &createdAt,
ID: &serviceAccessClaims.ID,
},
APISessionID: &serviceAccessClaims.ApiSessionId,
IdentityID: &serviceAccessClaims.IdentityId,
ServiceID: &serviceAccessClaims.Subject,
Token: &sessionToken,
Type: &sessionType,
}
for _, er := range resp.Payload.Data.EdgeRouters {
sessionDetail.EdgeRouters = append(sessionDetail.EdgeRouters, &rest_model.SessionEdgeRouter{
CommonEdgeRouterProperties: *er,
})
}
self.sanitizeSessionUrls(sessionDetail)
return sessionDetail, nil
}
// GetIdentity returns the identity.Identity used to facilitate authentication. Each identity.Identity instance
// may provide authentication material in the form of x509 certificates and private keys and/or trusted CA pools.
func (self *CtrlClient) GetIdentity() (identity.Identity, error) {
if idProvider, ok := self.Credentials.(apis.IdentityProvider); ok {
return idProvider.GetIdentity(), nil
}
if self.ApiSessionCertificate == nil {
err := self.EnsureApiSessionCertificate()
if err != nil {
return nil, fmt.Errorf("could not ensure an API Session certificate is available: %v", err)
}
}
return identity.NewClientTokenIdentityWithPool([]*x509.Certificate{self.ApiSessionCertificate}, self.ApiSessionPrivateKey, self.HttpTransport.TLSClientConfig.RootCAs), nil
}
// EnsureApiSessionCertificate will create an ApiSessionCertificate if one does not already exist.
func (self *CtrlClient) EnsureApiSessionCertificate() error {
if self.ApiSessionCertificate == nil {
return self.NewApiSessionCertificate()
}
return nil
}
// NewApiSessionCertificate will create a new ephemeral private key used to generate an ephemeral certificate
// that may be used with the current ApiSession. The generated certificate and private key are scoped to the
// ApiSession used to create it.
func (self *CtrlClient) NewApiSessionCertificate() error {
if self.ApiSessionCertInstance == "" {
self.ApiSessionCertInstance = uuid.NewString()
}
if self.ApiSessionPrivateKey == nil {
var err error
self.ApiSessionPrivateKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return fmt.Errorf("could not generate private key for api session certificate: %v", err)
}
}
csrTemplate := &x509.CertificateRequest{
Subject: pkix.Name{
Organization: []string{"Ziti SDK"},
OrganizationalUnit: []string{"golang"},
CommonName: "golang-sdk-" + self.ApiSessionCertInstance + "-" + uuid.NewString(),
},
}
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, self.ApiSessionPrivateKey)
if err != nil {
panic(err)
}
block := &pem.Block{
Type: "CERTIFICATE REQUEST",
Headers: nil,
Bytes: csrBytes,
}
csrPemString := string(pem.EncodeToMemory(block))
params := current_api_session.NewCreateCurrentAPISessionCertificateParams()
params.SessionCertificate = &rest_model.CurrentAPISessionCertificateCreate{
Csr: &csrPemString,
}
resp, err := self.API.CurrentAPISession.CreateCurrentAPISessionCertificate(params, self.GetCurrentApiSession())
if err != nil {
return rest_util.WrapErr(err)
}
certs := nfPem.PemBytesToCertificates([]byte(*resp.Payload.Data.Certificate))
if len(certs) == 0 {
return fmt.Errorf("expected at least 1 certificate creating an API Session Certificate, got 0")
}
pfxlog.Logger().Infof("new API Session Certificate: %x", sha1.Sum(certs[0].Raw))
self.ApiSessionCertificate = certs[0]
return nil
}
// GetServices will fetch the list of services that the identity of the current ApiSession has access to for dialing
// or binding.
func (self *CtrlClient) GetServices() ([]*rest_model.ServiceDetail, error) {
params := service.NewListServicesParams()
pageOffset := int64(0)
pageLimit := int64(500)
params.Limit = &pageLimit
if self.supportsSetOfConfigTypesOnServiceList() {
params.ConfigTypes = self.ConfigTypes
}
var services []*rest_model.ServiceDetail
for {
params.Offset = &pageOffset
resp, err := self.API.Service.ListServices(params, self.GetCurrentApiSession())
if err != nil {
return nil, rest_util.WrapErr(err)
}
if services == nil {
services = make([]*rest_model.ServiceDetail, 0, *resp.Payload.Meta.Pagination.TotalCount)
}
services = append(services, resp.Payload.Data...)
pageOffset += pageLimit
if pageOffset >= *resp.Payload.Meta.Pagination.TotalCount {
break
}
}
return services, nil
}
// GetService will fetch the specific service requested. If the service doesn't exist,
// nil will be returned
func (self *CtrlClient) GetService(name string) (*rest_model.ServiceDetail, error) {
params := service.NewListServicesParams()
filter := fmt.Sprintf(`name="%s"`, name)
params.Filter = &filter
resp, err := self.API.Service.ListServices(params, nil)
if err != nil {
return nil, rest_util.WrapErr(err)
}
if len(resp.Payload.Data) > 0 {
return resp.Payload.Data[0], nil
}
return nil, nil
}
// GetServiceTerminators returns the client terminator details for a specific service.
func (self *CtrlClient) GetServiceTerminators(svc *rest_model.ServiceDetail, offset int, limit int) ([]*rest_model.TerminatorClientDetail, int, error) {
params := service.NewListServiceTerminatorsParams()
pageOffset := int64(offset)
params.Offset = &pageOffset
pageLimit := int64(limit)
params.Limit = &pageLimit
params.ID = *svc.ID
resp, err := self.API.Service.ListServiceTerminators(params, self.GetCurrentApiSession())
if err != nil {
return nil, 0, rest_util.WrapErr(err)
}
return resp.Payload.Data, int(*resp.Payload.Meta.Pagination.TotalCount), nil
}
// CreateSession will attempt to obtain a session token for a specific service id and type.
func (self *CtrlClient) CreateSession(id string, sessionType SessionType) (*rest_model.SessionDetail, error) {
params := session.NewCreateSessionParams()
params.Session = &rest_model.SessionCreate{
ServiceID: id,
Type: rest_model.DialBind(sessionType),
}
resp, err := self.API.Session.CreateSession(params, self.GetCurrentApiSession())
if err != nil {
return nil, rest_util.WrapErr(err)
}
self.sanitizeSessionUrls(resp.Payload.Data)
return resp.Payload.Data, nil
}
// EnrollMfa will attempt to start TOTP MFA enrollment for the currently authenticated identity.
func (self *CtrlClient) EnrollMfa() (*rest_model.DetailMfa, error) {
enrollMfaParams := current_identity.NewEnrollMfaParams()
apiSession := self.GetCurrentApiSession()
_, enrollMfaErr := self.API.CurrentIdentity.EnrollMfa(enrollMfaParams, apiSession)
if enrollMfaErr != nil {
return nil, enrollMfaErr
}
detailMfaParams := current_identity.NewDetailMfaParams()
detailMfaResp, detailMfaErr := self.API.CurrentIdentity.DetailMfa(detailMfaParams, apiSession)
if detailMfaErr != nil {
return nil, rest_util.WrapErr(detailMfaErr)
}
return detailMfaResp.Payload.Data, nil
}
// VerifyMfa will complete a TOTP MFA enrollment created via EnrollMfa.
func (self *CtrlClient) VerifyMfa(code string) error {
params := current_identity.NewVerifyMfaParams()
params.MfaValidation = &rest_model.MfaCode{
Code: &code,
}
_, err := self.API.CurrentIdentity.VerifyMfa(params, self.GetCurrentApiSession())
return rest_util.WrapErr(err)
}
// RemoveMfa will remove the currently enrolled TOTP MFA added by EnrollMfa() and verified by VerifyMfa()
func (self *CtrlClient) RemoveMfa(code string) error {
params := current_identity.NewDeleteMfaParams()
params.MfaValidationCode = &code
_, err := self.API.CurrentIdentity.DeleteMfa(params, self.GetCurrentApiSession())
return rest_util.WrapErr(err)
}
// sanitizeSessionUrls will transform ER urls to transport friendly URIs and remove
// any addresses that cannot be parsed
func (self *CtrlClient) sanitizeSessionUrls(session *rest_model.SessionDetail) {
for _, edgeRouter := range session.EdgeRouters {
newUrls := map[string]string{}
for protocol, url := range edgeRouter.SupportedProtocols {
url = strings.Replace(url, "://", ":", 1)
if _, err := transport.ParseAddress(url); err == nil {
newUrls[protocol] = url
} else {
pfxlog.Logger().WithError(err).Debugf("ignoring address [%s] for router [%s], as it can't be parsed", url, genext.OrDefault(edgeRouter.Name))
}
}
edgeRouter.SupportedProtocols = newUrls
}
}
func (self *CtrlClient) loadCtrlCapabilities() {
result, _ := self.API.Informational.ListVersion(informational.NewListVersionParams())
if result != nil && result.Payload != nil && result.Payload.Data != nil {
if sv, err := versions.ParseSemVer(result.Payload.Data.Version); err == nil {
if sv.Equals(versions.MustParseSemVer("0.0.0")) || sv.CompareTo(versions.MustParseSemVer("1.1.0")) >= 0 {
self.supportsConfigTypesOnServiceList.Store(true)
}
}
}
self.capabilitiesLoaded.Store(true)
}
func (self *CtrlClient) supportsSetOfConfigTypesOnServiceList() bool {
if !self.capabilitiesLoaded.Load() {
self.loadCtrlCapabilities()
}
return self.supportsConfigTypesOnServiceList.Load()
}