Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,17 @@ GRID_KEY_PATH=/home/mytkom/.globus/userkey.pem

CERN_REDIRECT_URL=http://localhost:8088/callback
CERN_REALM_URL=https://auth.cern.ch/auth/realms/cern
CCDB_URL=http://ccdb-test.cern.ch:8080
CCDB_URL=https://alice-ccdb.cern.ch
CCDB_UPLOAD_SUBDIR=Users/m/mmytkows/test
# Dir (or single file) with CERN Root CA 2 + CERN Grid CA (1). Both needed:
# alice-ccdb leaf is signed by Grid CA; server often omits that intermediate.
# mkdir -p certs && curl -fsSL -o certs/cern-root-ca-2.crt \
# "https://ca.cern.ch/cafiles/certificates/CERN%20Root%20Certification%20Authority%202.crt" && \
# curl -fsSL -o certs/cern-grid-ca-1.crt \
# "https://ca.cern.ch/cafiles/certificates/CERN%20Grid%20Certification%20Authority(1).crt"
CCDB_CA_CERT_PATH=
ALICETRAINT_NN_ARCHITECTURE_PATH=web/nn_architectures/proposed.json

# If you do not want to install alien CA certs in ~/.globus/certificates
# clone it yourself (https://github.com/alisw/alien-cas) and fill it with dirpath
JALIEN_CERT_CA_DIR=
JALIEN_CERT_CA_DIR=
16 changes: 16 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ RUN chmod 0400 ./userkey.pem
RUN git clone --depth=1 --branch master https://github.com/alisw/alien-cas.git /app/alien-cas
RUN openssl rehash /app/alien-cas

# CERN CA bundle for CCDB TLS. alice-ccdb is issued by CERN Grid CA, which
# chains to CERN Root CA 2; server often omits the intermediate, so both needed.
# Soft-fail so offline/CI builds still succeed.
RUN mkdir -p /app/certs && \
(curl -fsSL --retry 3 --retry-delay 2 \
-o /app/certs/cern-root-ca-2.crt \
"https://ca.cern.ch/cafiles/certificates/CERN%20Root%20Certification%20Authority%202.crt" \
&& curl -fsSL --retry 3 --retry-delay 2 \
-o /app/certs/cern-grid-ca-1.crt \
"https://ca.cern.ch/cafiles/certificates/CERN%20Grid%20Certification%20Authority(1).crt" \
&& echo "CERN CCDB CA bundle downloaded") \
|| (echo "WARNING: failed to download CERN CA bundle; CCDB will fall back to system roots" >&2; \
rm -f /app/certs/cern-root-ca-2.crt /app/certs/cern-grid-ca-1.crt)

# --- runtime stage (same for both) ---
FROM registry.access.redhat.com/ubi9 AS runtime

Expand All @@ -53,11 +67,13 @@ ENV JALIEN_CERT_CA_DIR=/app/alien-cas
COPY --from=builder /app/AliceTraINT ./
COPY --from=builder /app/usercert.pem ./usercert.pem
COPY --from=builder /app/userkey.pem ./userkey.pem
COPY --from=builder /app/certs ./certs
COPY --from=builder /app/.env ./

RUN chmod 0400 ./userkey.pem
ENV GRID_CERT_PATH=./usercert.pem
ENV GRID_KEY_PATH=./userkey.pem
ENV CCDB_CA_CERT_PATH=/app/certs

# Docs and web templates are loaded at the execution
# of Go binary so they can be copied here and css generated
Expand Down
28 changes: 17 additions & 11 deletions internal/ccdb/requests.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,15 @@ import (
)

// Inspired by retrieveHeaders method of O2 CcdbApi
func doRemoteHeaderCall(url, uniqueAgentID string, timestamp int64) (map[string]string, error) {
func doRemoteHeaderCall(url, uniqueAgentID string, timestamp int64, tlsConfig *tls.Config) (map[string]string, error) {
transport := http.DefaultTransport.(*http.Transport).Clone()
if tlsConfig != nil {
transport.TLSClientConfig = tlsConfig
}

client := &http.Client{
Timeout: 10 * time.Second,
Timeout: 10 * time.Second,
Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
Expand All @@ -30,12 +36,20 @@ func doRemoteHeaderCall(url, uniqueAgentID string, timestamp int64) (map[string]
req.Header.Set("User-Agent", uniqueAgentID)

resp, err := client.Do(req)
if err != nil && !isUnsupportedProtocol(err) {
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
//nolint:errcheck
defer resp.Body.Close()

if resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("CCDB: run information not found")
}

if resp.StatusCode < 200 || resp.StatusCode >= 400 {
return nil, fmt.Errorf("CCDB: unexpected status %d", resp.StatusCode)
}

headers := make(map[string]string)
for key, values := range resp.Header {
if len(values) == 1 {
Expand All @@ -45,17 +59,9 @@ func doRemoteHeaderCall(url, uniqueAgentID string, timestamp int64) (map[string]
}
}

if resp.StatusCode == http.StatusNotFound {
headers = nil
}

return headers, nil
}

func isUnsupportedProtocol(err error) bool {
return strings.Contains(err.Error(), "unsupported protocol")
}

func removeExtension(filename string) string {
ext := filepath.Ext(filename)
return strings.TrimSuffix(filename, ext)
Expand Down
13 changes: 7 additions & 6 deletions internal/ccdb/run_information.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ccdb

import (
"crypto/tls"
"fmt"
"strconv"
)
Expand All @@ -16,32 +17,32 @@ const (
AGENT string = "AliceTraINT_Agent/1.0"
)

func GetRunInformation(baseURL string, runNumber uint64) (*RunInformation, error) {
func GetRunInformation(baseURL string, runNumber uint64, tlsConfig *tls.Config) (*RunInformation, error) {
url := fmt.Sprintf("%s/%s/%d", baseURL, RCT_ENDPOINT, runNumber)

headers, err := doRemoteHeaderCall(url, AGENT, -1)
headers, err := doRemoteHeaderCall(url, AGENT, -1, tlsConfig)
if err != nil {
return nil, err
}

sorStr, sorOk := headerValue(headers, "Sor", "SOR")
if !sorOk {
return nil, fmt.Errorf("CCDB: SOR not present for run %d (headers: %v)", runNumber, headers)
return nil, fmt.Errorf("CCDB: SOR not present for run %d", runNumber)
}

eorStr, eorOk := headerValue(headers, "Eor", "EOR")
if !eorOk {
return nil, fmt.Errorf("CCDB: EOR not present for run %d (headers: %v)", runNumber, headers)
return nil, fmt.Errorf("CCDB: EOR not present for run %d", runNumber)
}

sor, err := parseUint64(sorStr)
if err != nil {
return nil, err
return nil, fmt.Errorf("CCDB: invalid SOR for run %d: %w", runNumber, err)
}

eor, err := parseUint64(eorStr)
if err != nil {
return nil, err
return nil, fmt.Errorf("CCDB: invalid EOR for run %d: %w", runNumber, err)
}

return &RunInformation{
Expand Down
14 changes: 2 additions & 12 deletions internal/ccdb/upload_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,6 @@ import (
"io"
)

func UploadFile(uploadSubdirUrl string, cert *tls.Certificate, sor, eor uint64, filename string, file io.Reader) error {
ssl := &tls.Config{
Certificates: []tls.Certificate{*cert},
InsecureSkipVerify: true,
}

err := uploadFile(filename, uploadSubdirUrl, file, sor, eor, ssl)
if err != nil {
return err
}

return nil
func UploadFile(uploadSubdirUrl string, tlsConfig *tls.Config, sor, eor uint64, filename string, file io.Reader) error {
return uploadFile(filename, uploadSubdirUrl, file, sor, eor, tlsConfig)
}
2 changes: 2 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Config struct {
MonalisaBaseURL string
CCDBBaseURL string
CCDBUploadSubdir string
CCDBCACertPath string
CertPath string
KeyPath string
DataDirPath string
Expand Down Expand Up @@ -65,6 +66,7 @@ func LoadConfig() *Config {
MonalisaBaseURL: getEnv("MONALISA_URL", "https://alimonitor.cern.ch"),
CCDBBaseURL: getEnv("CCDB_URL", "http://ccdb-test.cern.ch:8080"),
CCDBUploadSubdir: getEnv("CCDB_UPLOAD_SUBDIR", "/Users/m/mmytkows"),
CCDBCACertPath: getEnv("CCDB_CA_CERT_PATH", ""),
CertPath: getEnv("GRID_CERT_PATH", ""),
KeyPath: getEnv("GRID_KEY_PATH", ""),
DataDirPath: getEnv("ALICETRAINT_DATA_DIR_PATH", "data"),
Expand Down
109 changes: 109 additions & 0 deletions internal/gridtls/cert_file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package gridtls

import (
"crypto/x509"
"encoding/pem"
"fmt"
"os"
"path/filepath"
"strings"
)

// LoadCertPoolWithOptionalFile returns the system cert pool with optional
// extra CA material appended. certPath may be a single PEM/DER file or a
// directory of .crt/.pem files. Empty path leaves the system pool unchanged;
// a non-nil error is a soft warning for the caller.
func LoadCertPoolWithOptionalFile(certPath string) (*x509.CertPool, error) {
pool, err := x509.SystemCertPool()
if err != nil || pool == nil {
pool = x509.NewCertPool()
if err != nil {
err = fmt.Errorf("system cert pool unavailable: %w", err)
}
}

if certPath == "" {
return pool, err
}

if appendErr := appendCertPath(pool, certPath); appendErr != nil {
if err != nil {
return pool, fmt.Errorf("%v; also failed to load %s: %w", err, certPath, appendErr)
}
return pool, fmt.Errorf("failed to load extra CA %s: %w", certPath, appendErr)
}

return pool, nil
}

func appendCertPath(pool *x509.CertPool, certPath string) error {
info, err := os.Stat(certPath)
if err != nil {
return err
}
if !info.IsDir() {
return appendCertFile(pool, certPath)
}

entries, err := os.ReadDir(certPath)
if err != nil {
return err
}

loaded := 0
var firstErr error
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
lower := strings.ToLower(name)
if !strings.HasSuffix(lower, ".crt") && !strings.HasSuffix(lower, ".pem") && !strings.HasSuffix(lower, ".cer") {
continue
}
if fileErr := appendCertFile(pool, filepath.Join(certPath, name)); fileErr != nil {
if firstErr == nil {
firstErr = fmt.Errorf("%s: %w", name, fileErr)
}
continue
}
loaded++
}

if loaded == 0 {
if firstErr != nil {
return firstErr
}
return fmt.Errorf("no CA certificates found in directory")
}
return nil
}

func appendCertFile(pool *x509.CertPool, certPath string) error {
data, err := os.ReadFile(certPath)
if err != nil {
return err
}
if len(data) == 0 {
return fmt.Errorf("file is empty")
}

if pool.AppendCertsFromPEM(data) {
return nil
}

// CERN Root CA 2 is published as DER (.crt).
cert, err := x509.ParseCertificate(data)
if err != nil {
block, _ := pem.Decode(data)
if block != nil {
cert, err = x509.ParseCertificate(block.Bytes)
}
if err != nil {
return fmt.Errorf("not valid PEM or DER certificate: %w", err)
}
}

pool.AddCert(cert)
return nil
}
64 changes: 64 additions & 0 deletions internal/gridtls/cert_file_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package gridtls

import (
"os"
"path/filepath"
"testing"
)

func TestAppendCertFile_DER(t *testing.T) {
path := "/tmp/cern-root-ca-2.crt"
if _, err := os.Stat(path); err != nil {
t.Skip("CERN Root CA fixture missing at", path)
}

pool, err := LoadCertPoolWithOptionalFile(path)
if err != nil {
t.Fatalf("LoadCertPoolWithOptionalFile: %v", err)
}
if pool == nil {
t.Fatal("expected non-nil pool")
}
}

func TestLoadCertPoolWithOptionalFile_Missing(t *testing.T) {
pool, err := LoadCertPoolWithOptionalFile("/nonexistent/cern-root-ca-2.crt")
if pool == nil {
t.Fatal("expected fallback pool")
}
if err == nil {
t.Fatal("expected soft error for missing file")
}
}

func TestLoadCertPoolWithOptionalFile_Directory(t *testing.T) {
rootPath := "/tmp/cern-root-ca-2.crt"
gridPath := "/tmp/cern-grid-ca-1.crt"
if _, err := os.Stat(rootPath); err != nil {
t.Skip("CERN Root CA fixture missing")
}
if _, err := os.Stat(gridPath); err != nil {
t.Skip("CERN Grid CA fixture missing")
}

dir := t.TempDir()
mustCopy := func(src, name string) {
data, err := os.ReadFile(src)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, name), data, 0o644); err != nil {
t.Fatal(err)
}
}
mustCopy(rootPath, "cern-root-ca-2.crt")
mustCopy(gridPath, "cern-grid-ca-1.crt")

pool, err := LoadCertPoolWithOptionalFile(dir)
if err != nil {
t.Fatalf("load dir: %v", err)
}
if pool == nil {
t.Fatal("expected non-nil pool")
}
}
Loading
Loading