1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-01-08 04:21:26 +02:00
sap-jenkins-library/pkg/abap/build/connector.go
tiloKo 1259a32de1
Enable logon to AAKaaS via Certificate (mTLS) (#4860)
* originHash

* analysis output

* first shot

* add cert logon to piper http client

* allow initial user/pw for certificate logon

* credentials -> parameters

* encode user cert in pem

* key as well

* fix unit tests after merge

* other aakaas steps

* 2nd conn in register packages
2024-03-12 14:27:00 +01:00

316 lines
10 KiB
Go

package build
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"io"
"net/http"
"net/http/cookiejar"
"net/url"
"strconv"
"time"
"github.com/SAP/jenkins-library/pkg/abaputils"
piperhttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/pkg/errors"
"golang.org/x/crypto/pkcs12"
)
// Connector : Connector Utility Wrapping http client
type Connector struct {
Client piperhttp.Sender
DownloadClient piperhttp.Downloader
Header map[string][]string
Baseurl string
Parameters url.Values
MaxRuntime time.Duration // just as handover parameter for polling functions
PollingInterval time.Duration // just as handover parameter for polling functions
}
// ConnectorConfiguration : Handover Structure for Connector Creation
type ConnectorConfiguration struct {
CfAPIEndpoint string
CfOrg string
CfSpace string
CfServiceInstance string
CfServiceKeyName string
Host string
Username string
Password string
AddonDescriptor string
MaxRuntimeInMinutes int
CertificateNames []string
Parameters url.Values
}
// HTTPSendLoader : combine both interfaces [sender, downloader]
type HTTPSendLoader interface {
piperhttp.Sender
piperhttp.Downloader
}
// ******** technical communication calls ********
// GetToken : Get the X-CRSF Token from ABAP Backend for later post
func (conn *Connector) GetToken(appendum string) error {
url := conn.createUrl(appendum)
conn.Header["X-CSRF-Token"] = []string{"Fetch"}
response, err := conn.Client.SendRequest("HEAD", url, nil, conn.Header, nil)
if err != nil {
if response == nil {
return errors.Wrap(err, "Fetching X-CSRF-Token failed")
}
defer response.Body.Close()
errorbody, _ := io.ReadAll(response.Body)
return errors.Wrapf(err, "Fetching X-CSRF-Token failed: %v", string(errorbody))
}
defer response.Body.Close()
token := response.Header.Get("X-CSRF-Token")
conn.Header["X-CSRF-Token"] = []string{token}
return nil
}
// Get : http get request
func (conn Connector) Get(appendum string) ([]byte, error) {
url := conn.createUrl(appendum)
response, err := conn.Client.SendRequest("GET", url, nil, conn.Header, nil)
if err != nil {
if response == nil || response.Body == nil {
return nil, errors.Wrap(err, "Get failed")
}
defer response.Body.Close()
errorbody, _ := io.ReadAll(response.Body)
return errorbody, errors.Wrapf(err, "Get failed: %v", string(errorbody))
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
return body, err
}
// Post : http post request
func (conn Connector) Post(appendum string, importBody string) ([]byte, error) {
url := conn.createUrl(appendum)
var response *http.Response
var err error
if importBody == "" {
response, err = conn.Client.SendRequest("POST", url, nil, conn.Header, nil)
} else {
response, err = conn.Client.SendRequest("POST", url, bytes.NewBuffer([]byte(importBody)), conn.Header, nil)
}
if err != nil {
if response == nil {
return nil, errors.Wrap(err, "Post failed")
}
defer response.Body.Close()
errorbody, _ := io.ReadAll(response.Body)
return errorbody, errors.Wrapf(err, "Post failed: %v", string(errorbody))
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
return body, err
}
// Download : download a file via http
func (conn Connector) Download(appendum string, downloadPath string) error {
url := conn.createUrl(appendum)
err := conn.DownloadClient.DownloadFile(url, downloadPath, nil, nil)
return err
}
// create url
func (conn Connector) createUrl(appendum string) string {
myUrl := conn.Baseurl + appendum
if len(conn.Parameters) == 0 {
return myUrl
}
myUrl = myUrl + "?" + conn.Parameters.Encode()
return myUrl
}
// InitAAKaaS : initialize Connector for communication with AAKaaS backend
func (conn *Connector) InitAAKaaS(aAKaaSEndpoint string, username string, password string, inputclient piperhttp.Sender, originHash string, certFile string, certPass string) error {
conn.Client = inputclient
conn.Header = make(map[string][]string)
conn.Header["Accept"] = []string{"application/json"}
conn.Header["Content-Type"] = []string{"application/json"}
conn.Header["User-Agent"] = []string{"Piper-abapAddonAssemblyKit/1.0"}
if originHash != "" {
conn.Header["build-config-token"] = []string{originHash}
log.Entry().Info("Origin info for restricted scenario added")
}
cookieJar, _ := cookiejar.New(nil)
conn.Baseurl = aAKaaSEndpoint
tlsCertificates, err := conn.handleLogonCertificate(certFile, certPass)
if err != nil {
return errors.Wrap(err, "Handling certificates for client logon failed")
}
conn.Client.SetOptions(piperhttp.ClientOptions{
Username: username,
Password: password,
CookieJar: cookieJar,
Certificates: tlsCertificates,
})
if tlsCertificates != nil {
log.Entry().Info("Logon procedure: via Certificate")
return nil
} else if username == "" || password == "" {
return errors.New("username/password for AAKaaS must not be initial") //leads to redirect to login page which causes HTTP200 instead of HTTP401 and thus side effects
} else {
log.Entry().Info("Logon procedure: via Password")
return nil
}
}
func (conn *Connector) handleLogonCertificate(certFile, certPass string) ([]tls.Certificate, error) {
var tlsCertificate tls.Certificate
if certFile != "" && certPass != "" {
certFileInBytes, err := base64.StdEncoding.DecodeString(certFile)
if err != nil {
return nil, errors.Wrap(err, "Base64 decoding of certificate File string failed")
}
pemBlocks, err := pkcs12.ToPEM(certFileInBytes, certPass)
if err != nil {
return nil, errors.Wrap(err, "Decoding certificate File from PKCS12 to PEM failed")
}
var key []byte
var userCertificate []byte
for _, pemBlock := range pemBlocks {
if pemBlock.Type == "PRIVATE KEY" {
key = pem.EncodeToMemory(pemBlock)
}
if pemBlock.Type == "CERTIFICATE" {
var tempCert, err = x509.ParseCertificate(pemBlock.Bytes)
if err != nil {
return nil, errors.Wrap(err, "Parsing x509 Certificate from PEM Block failed")
}
if tempCert.IsCA == false { //We ignore the 2 additional CA Certificates
userCertificate = pem.EncodeToMemory(pemBlock)
}
}
}
tlsCertificate, err = tls.X509KeyPair(userCertificate, key)
if err != nil {
return nil, errors.Wrap(err, "Creating x509 Key Pair failed")
}
return []tls.Certificate{tlsCertificate}, nil
} else {
return nil, nil
}
}
// InitBuildFramework : initialize Connector for communication with ABAP SCP instance
func (conn *Connector) InitBuildFramework(config ConnectorConfiguration, com abaputils.Communication, inputclient HTTPSendLoader) error {
conn.Client = inputclient
conn.Header = make(map[string][]string)
conn.Header["Accept"] = []string{"application/json"}
conn.Header["Content-Type"] = []string{"application/json"}
conn.DownloadClient = inputclient
conn.DownloadClient.SetOptions(piperhttp.ClientOptions{TransportTimeout: 20 * time.Second})
// Mapping for options
subOptions := abaputils.AbapEnvironmentOptions{}
subOptions.CfAPIEndpoint = config.CfAPIEndpoint
subOptions.CfServiceInstance = config.CfServiceInstance
subOptions.CfServiceKeyName = config.CfServiceKeyName
subOptions.CfOrg = config.CfOrg
subOptions.CfSpace = config.CfSpace
subOptions.Host = config.Host
subOptions.Password = config.Password
subOptions.Username = config.Username
// Determine the host, user and password, either via the input parameters or via a cloud foundry service key
connectionDetails, err := com.GetAbapCommunicationArrangementInfo(subOptions, "/sap/opu/odata/BUILD/CORE_SRV")
if err != nil {
return errors.Wrap(err, "Parameters for the ABAP Connection not available")
}
conn.DownloadClient.SetOptions(piperhttp.ClientOptions{
Username: connectionDetails.User,
Password: connectionDetails.Password,
})
cookieJar, _ := cookiejar.New(nil)
conn.Client.SetOptions(piperhttp.ClientOptions{
Username: connectionDetails.User,
Password: connectionDetails.Password,
CookieJar: cookieJar,
TrustedCerts: config.CertificateNames,
})
conn.Baseurl = connectionDetails.URL
conn.Parameters = config.Parameters
return nil
}
// UploadSarFile : upload *.sar file
func (conn Connector) UploadSarFile(appendum string, sarFile []byte) error {
url := conn.createUrl(appendum)
response, err := conn.Client.SendRequest("PUT", url, bytes.NewBuffer(sarFile), conn.Header, nil)
if err != nil {
defer response.Body.Close()
errorbody, _ := io.ReadAll(response.Body)
return errors.Wrapf(err, "Upload of SAR file failed: %v", string(errorbody))
}
defer response.Body.Close()
return nil
}
// UploadSarFileInChunks : upload *.sar file in chunks
func (conn Connector) UploadSarFileInChunks(appendum string, fileName string, sarFile []byte) error {
//Maybe Next Refactoring step to read the file in chunks, too?
//In case it turns out to be not reliable add a retry mechanism
url := conn.createUrl(appendum)
header := make(map[string][]string)
header["Content-Disposition"] = []string{"form-data; name=\"file\"; filename=\"" + fileName + "\""}
//chunkSize := 10000 // 10KB for testing
//chunkSize := 1000000 //1MB for Testing,
chunkSize := 10000000 //10MB
log.Entry().Infof("Upload in chunks of %d bytes", chunkSize)
sarFileBuffer := bytes.NewBuffer(sarFile)
fileSize := sarFileBuffer.Len()
for sarFileBuffer.Len() > 0 {
startOffset := fileSize - sarFileBuffer.Len()
nextChunk := bytes.NewBuffer(sarFileBuffer.Next(chunkSize))
endOffset := fileSize - sarFileBuffer.Len()
header["Content-Range"] = []string{"bytes " + strconv.Itoa(startOffset) + " - " + strconv.Itoa(endOffset) + " / " + strconv.Itoa(fileSize)}
log.Entry().Info(header["Content-Range"])
response, err := conn.Client.SendRequest("POST", url, nextChunk, header, nil)
if err != nil {
if response != nil && response.Body != nil {
errorbody, _ := io.ReadAll(response.Body)
response.Body.Close()
return errors.Wrapf(err, "Upload of SAR file failed: %v", string(errorbody))
} else {
return err
}
}
response.Body.Close()
}
return nil
}