Skip to content

Commit ff15b83

Browse files
Merge branch 'master' into feat/zombie-on-policy-fail
2 parents 8c9bea4 + d94119c commit ff15b83

28 files changed

Lines changed: 4606 additions & 1582 deletions

.github/workflows/jira-pr-validator.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ jobs:
1717
uses: TykTechnologies/jira-linter@38a9cabef56171c4e52ea698fa7be3db5fca3a49 # main
1818
with:
1919
jira-base-url: 'https://tyktech.atlassian.net'
20+
jira-user-email: ${{ secrets.JIRA_USER_EMAIL }}
2021
jira-api-token: ${{ secrets.JIRA_TOKEN }}

.github/workflows/plugin-compiler-build.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,11 @@ jobs:
2626

2727
docker-build:
2828
needs: [dep-guard]
29+
if: |
30+
!cancelled() &&
31+
(needs.dep-guard.result == 'success' || needs.dep-guard.result == 'skipped') &&
32+
!github.event.pull_request.draft
2933
runs-on: ubuntu-latest
30-
if: ${{ !github.event.pull_request.draft }}
3134
permissions:
3235
id-token: write
3336
steps:

.github/workflows/release.yml

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ on:
1616
schedule:
1717
- cron: "0 0 * * 1"
1818
pull_request:
19+
types: [opened, synchronize, reopened, labeled]
1920
push:
2021
branches:
2122
- master
@@ -391,8 +392,11 @@ jobs:
391392
!dist/*PAYG*.rpm
392393
!dist/*fips*.rpm
393394
resolve-dashboard-image:
394-
if: github.event.pull_request.draft == false
395395
needs: goreleaser
396+
if: |
397+
!cancelled() &&
398+
needs.goreleaser.result == 'success' &&
399+
github.event.pull_request.draft == false
396400
runs-on: ${{ vars.DEFAULT_RUNNER }}
397401
permissions:
398402
id-token: write
@@ -407,6 +411,7 @@ jobs:
407411
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
408412
with:
409413
fetch-depth: 0
414+
persist-credentials: false
410415
ref: ${{ github.event.pull_request.head.sha }}
411416
- name: Check for relevant package changes in PR
412417
id: check_changes
@@ -516,6 +521,7 @@ jobs:
516521
COMMIT_SHA: ${{ github.sha }}
517522
PR_NUMBER: ${{ github.event.pull_request.number }}
518523
HAS_RELEVANT_CHANGES: ${{ steps.check_changes.outputs.has_relevant_changes }}
524+
ORG_GH_TOKEN: ${{ secrets.ORG_GH_TOKEN }}
519525
run: |
520526
echo "=================================="
521527
echo "📊 Dashboard Image Resolution"
@@ -530,14 +536,23 @@ jobs:
530536
echo "Has relevant changes in PR: $HAS_RELEVANT_CHANGES"
531537
echo "=================================="
532538
533-
# Only use custom build strategies for PRs targeting master
539+
# For non-master base branches, check if the same branch exists in tyk-analytics
534540
if [ "$BASE_REF" != "master" ]; then
535-
echo "ℹ️ Strategy: Use gromit default (base branch is not master)"
536-
echo " → Custom builds only for master branch PRs"
537-
echo "dashboard_image=" >> $GITHUB_OUTPUT
538-
echo "needs_build=false" >> $GITHUB_OUTPUT
539-
echo "dashboard_branch=" >> $GITHUB_OUTPUT
540-
echo "strategy=gromit-default" >> $GITHUB_OUTPUT
541+
if git ls-remote --exit-code --heads "https://x-access-token:${ORG_GH_TOKEN}@github.com/TykTechnologies/tyk-analytics.git" "refs/heads/$BASE_REF" > /dev/null 2>&1; then
542+
echo "📋 Strategy: Use release branch '$BASE_REF' from tyk-analytics"
543+
echo " → Base branch exists in tyk-analytics, using it directly"
544+
echo "dashboard_image=${REGISTRY}/tyk-analytics:${BASE_REF}" >> $GITHUB_OUTPUT
545+
echo "needs_build=false" >> $GITHUB_OUTPUT
546+
echo "dashboard_branch=$BASE_REF" >> $GITHUB_OUTPUT
547+
echo "strategy=release-branch-match" >> $GITHUB_OUTPUT
548+
else
549+
echo "ℹ️ Strategy: Use gromit default (base branch '$BASE_REF' not found in tyk-analytics)"
550+
echo " → Falling back to gromit default"
551+
echo "dashboard_image=" >> $GITHUB_OUTPUT
552+
echo "needs_build=false" >> $GITHUB_OUTPUT
553+
echo "dashboard_branch=" >> $GITHUB_OUTPUT
554+
echo "strategy=gromit-default" >> $GITHUB_OUTPUT
555+
fi
541556
542557
# Strategy 1: Matching branch exists in tyk-analytics → use gromit
543558
elif [ "$BRANCH_EXISTS" = "true" ]; then
@@ -582,8 +597,11 @@ jobs:
582597
echo "✅ Resolution complete"
583598
echo "=================================="
584599
build-dashboard-image:
585-
if: needs.resolve-dashboard-image.outputs.needs_build == 'true'
586600
needs: resolve-dashboard-image
601+
if: |
602+
!cancelled() &&
603+
needs.resolve-dashboard-image.result == 'success' &&
604+
needs.resolve-dashboard-image.outputs.needs_build == 'true'
587605
runs-on: ${{ vars.DEFAULT_RUNNER }}
588606
permissions:
589607
id-token: write
@@ -762,9 +780,12 @@ jobs:
762780
echo "image=$IMAGE" >> $GITHUB_OUTPUT
763781
echo "✅ Dashboard image built and pushed: $IMAGE"
764782
test-controller-api:
765-
if: github.event.pull_request.draft == false
766783
needs:
767784
- goreleaser
785+
if: |
786+
!cancelled() &&
787+
needs.goreleaser.result == 'success' &&
788+
github.event.pull_request.draft == false
768789
runs-on: ${{ vars.DEFAULT_RUNNER }}
769790
outputs:
770791
envfiles: ${{ steps.params.outputs.envfiles }}
@@ -784,9 +805,9 @@ jobs:
784805
- goreleaser
785806
- resolve-dashboard-image
786807
- build-dashboard-image
787-
# build-dashboard-image may be skipped, so use if: always() to run regardless
808+
# build-dashboard-image may be skipped, so use !cancelled() to run regardless
788809
if: |
789-
always() &&
810+
!cancelled() &&
790811
needs.test-controller-api.result == 'success' &&
791812
needs.goreleaser.result == 'success' &&
792813
needs.resolve-dashboard-image.result == 'success' &&
@@ -862,7 +883,7 @@ jobs:
862883
name: Aggregated CI Status
863884
runs-on: ${{ vars.DEFAULT_RUNNER }}
864885
# Dynamically determine which jobs to depend on based on repository configuration
865-
needs: [goreleaser, api-tests]
886+
needs: [goreleaser, api-tests, dep-guard]
866887
if: ${{ always() && github.event_name == 'pull_request' }}
867888
steps:
868889
- name: Aggregate results
@@ -889,9 +910,12 @@ jobs:
889910
890911
echo "✅ All required jobs succeeded"
891912
test-controller-distros:
892-
if: github.event.pull_request.draft == false
893913
needs:
894914
- goreleaser
915+
if: |
916+
!cancelled() &&
917+
needs.goreleaser.result == 'success' &&
918+
github.event.pull_request.draft == false
895919
runs-on: ${{ vars.DEFAULT_RUNNER }}
896920
outputs:
897921
deb: ${{ steps.params.outputs.deb }}
@@ -916,6 +940,9 @@ jobs:
916940
runs-on: ${{ vars.DEFAULT_RUNNER }}
917941
needs:
918942
- test-controller-distros
943+
if: |
944+
!cancelled() &&
945+
needs.test-controller-distros.result == 'success'
919946
strategy:
920947
fail-fast: true
921948
matrix:
@@ -975,6 +1002,9 @@ jobs:
9751002
runs-on: ${{ vars.DEFAULT_RUNNER }}
9761003
needs:
9771004
- test-controller-distros
1005+
if: |
1006+
!cancelled() &&
1007+
needs.test-controller-distros.result == 'success'
9781008
strategy:
9791009
fail-fast: true
9801010
matrix:
@@ -1028,6 +1058,9 @@ jobs:
10281058
release-tests:
10291059
needs:
10301060
- goreleaser
1061+
if: |
1062+
!cancelled() &&
1063+
needs.goreleaser.result == 'success'
10311064
permissions:
10321065
id-token: write # This is required for requesting the JWT
10331066
contents: read # This is required for actions/checkout
@@ -1036,6 +1069,9 @@ jobs:
10361069
secrets: inherit
10371070
sbom:
10381071
needs: goreleaser
1072+
if: |
1073+
!cancelled() &&
1074+
needs.goreleaser.result == 'success'
10391075
uses: TykTechnologies/github-actions/.github/workflows/sbom.yaml@42304edda365365e0a887cf018d8edc34b960b82 # main
10401076
secrets:
10411077
DEPDASH_URL: ${{ secrets.DEPDASH_URL }}

apidef/mcp/validator_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ func TestGetMCPSchema(t *testing.T) {
422422
t.Run("return error when requested version is not of semver", func(t *testing.T) {
423423
reqOASVersion := "a.0.3"
424424
_, err = GetMCPSchema(reqOASVersion)
425-
expectedErr := fmt.Errorf("Malformed version: %s", reqOASVersion)
425+
expectedErr := fmt.Errorf("malformed version: %s", reqOASVersion)
426426
assert.Equal(t, expectedErr, err)
427427
})
428428

apidef/oas/validator_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ func TestGetOASSchema(t *testing.T) {
411411
t.Run("return error when requested version is not of semver", func(t *testing.T) {
412412
reqOASVersion := "a.0.3"
413413
_, err = GetOASSchema(reqOASVersion)
414-
expectedErr := fmt.Errorf("Malformed version: %s", reqOASVersion)
414+
expectedErr := fmt.Errorf("malformed version: %s", reqOASVersion)
415415
assert.Equal(t, expectedErr, err)
416416
})
417417

apidef/streams/validator_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ func TestGetOASSchema(t *testing.T) {
298298
t.Run("return error when requested version is not of semver", func(t *testing.T) {
299299
reqOASVersion := "a.0.3"
300300
_, err = GetOASSchema(reqOASVersion)
301-
expectedErr := fmt.Errorf("Malformed version: %s", reqOASVersion)
301+
expectedErr := fmt.Errorf("malformed version: %s", reqOASVersion)
302302
assert.Equal(t, expectedErr, err)
303303
})
304304

gateway/external_services_e2e_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,3 +538,63 @@ func TestE2E_CompleteAPIFlowWithExternalServices(t *testing.T) {
538538
assert.Equal(t, 30*time.Second, webhookClient.Timeout)
539539
assert.Equal(t, 10*time.Second, healthClient.Timeout)
540540
}
541+
542+
func TestE2E_VaultHotReload(t *testing.T) {
543+
vaultData := map[string]string{
544+
"cert_file": "/initial/cert.pem",
545+
"key_file": "/initial/key.pem",
546+
"ca_file": "/initial/ca.pem",
547+
}
548+
549+
mockVault := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
550+
response := map[string]interface{}{
551+
"data": map[string]interface{}{
552+
"data": map[string]interface{}{
553+
"cert_file": vaultData["cert_file"],
554+
"key_file": vaultData["key_file"],
555+
"ca_file": vaultData["ca_file"],
556+
},
557+
},
558+
}
559+
w.Header().Set("Content-Type", "application/json")
560+
require.NoError(t, json.NewEncoder(w).Encode(response))
561+
}))
562+
defer mockVault.Close()
563+
564+
ts := StartTest(func(globalConf *config.Config) {
565+
globalConf.KV.Vault = config.VaultConfig{
566+
Address: mockVault.URL,
567+
Token: "test-token",
568+
KVVersion: 2,
569+
}
570+
globalConf.ExternalServices = config.ExternalServiceConfig{
571+
OAuth: config.ServiceConfig{
572+
MTLS: config.MTLSConfig{
573+
Enabled: true,
574+
CertFile: "vault://secret/oauth.cert_file",
575+
KeyFile: "vault://secret/oauth.key_file",
576+
CAFile: "vault://secret/oauth.ca_file",
577+
},
578+
},
579+
}
580+
})
581+
defer ts.Close()
582+
583+
require.NoError(t, ts.Gw.afterConfSetup())
584+
585+
conf := ts.Gw.GetConfig()
586+
assert.Equal(t, "/initial/cert.pem", conf.ExternalServices.OAuth.MTLS.CertFile)
587+
assert.Equal(t, "/initial/key.pem", conf.ExternalServices.OAuth.MTLS.KeyFile)
588+
assert.Equal(t, "/initial/ca.pem", conf.ExternalServices.OAuth.MTLS.CAFile)
589+
590+
vaultData["cert_file"] = "/updated/cert.pem"
591+
vaultData["key_file"] = "/updated/key.pem"
592+
vaultData["ca_file"] = "/updated/ca.pem"
593+
594+
ts.Gw.DoReload()
595+
596+
conf = ts.Gw.GetConfig()
597+
assert.Equal(t, "/updated/cert.pem", conf.ExternalServices.OAuth.MTLS.CertFile)
598+
assert.Equal(t, "/updated/key.pem", conf.ExternalServices.OAuth.MTLS.KeyFile)
599+
assert.Equal(t, "/updated/ca.pem", conf.ExternalServices.OAuth.MTLS.CAFile)
600+
}

gateway/mw_certificate_check.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,9 @@ import (
66
"net/http"
77
"time"
88

9-
"github.com/sirupsen/logrus"
10-
119
"github.com/TykTechnologies/tyk/certs"
1210
"github.com/TykTechnologies/tyk/internal/certcheck"
1311
"github.com/TykTechnologies/tyk/internal/crypto"
14-
"github.com/TykTechnologies/tyk/pkg/errpack"
1512
"github.com/TykTechnologies/tyk/storage"
1613
)
1714

@@ -114,11 +111,6 @@ func (m *CertificateCheckMW) ProcessRequest(w http.ResponseWriter, r *http.Reque
114111
certIDs := append(m.Spec.ClientCertificates, m.Spec.GlobalConfig.Security.Certificates.API...)
115112
apiCerts := m.Gw.CertificateManager.List(certIDs, certs.CertificatePublic)
116113
if err := crypto.ValidateRequestCerts(r, apiCerts); err != nil {
117-
log.
118-
WithField("api_id", m.Spec.APIID).
119-
WithField("api_name", m.Spec.Name).
120-
WithField("mw", m.Name()).
121-
Log(errpack.LogLevel(err, logrus.WarnLevel), "Certificate validation failed: ", err)
122114
m.batchCertificatesExpirationCheck(apiCerts)
123115
return err, http.StatusForbidden
124116
}

gateway/mw_jsonrpc_access_control.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"net/http"
66

77
"github.com/TykTechnologies/tyk/internal/httpctx"
8+
"github.com/TykTechnologies/tyk/internal/mcp"
89
"github.com/TykTechnologies/tyk/internal/middleware"
910
)
1011

@@ -53,7 +54,7 @@ func (m *JSONRPCAccessControlMiddleware) ProcessRequest(w http.ResponseWriter, r
5354
return nil, http.StatusOK
5455
}
5556

56-
if checkAccessControlRules(accessDef.JSONRPCMethodsAccessRights, state.Method) {
57+
if mcp.CheckAccessControlRules(accessDef.JSONRPCMethodsAccessRights, state.Method) {
5758
writeJSONRPCAccessDenied(w, r, fmt.Sprintf("method '%s' is not available", state.Method))
5859
return nil, middleware.StatusRespond
5960
}

gateway/mw_jsonrpc_helpers.go

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,48 +5,8 @@ import (
55

66
"github.com/TykTechnologies/tyk/internal/httpctx"
77
jsonrpcerrors "github.com/TykTechnologies/tyk/internal/jsonrpc/errors"
8-
"github.com/TykTechnologies/tyk/regexp"
9-
"github.com/TykTechnologies/tyk/user"
108
)
119

12-
// checkAccessControlRules evaluates allow/block lists against a name.
13-
// Returns true if the name is denied, false if permitted.
14-
//
15-
// Evaluation order:
16-
// 1. Blocked is checked first — if matched, the request is denied.
17-
// 2. If Allowed is non-empty and the name does not match any entry, the request is denied.
18-
// 3. If both lists are empty, access is permitted.
19-
func checkAccessControlRules(rules user.AccessControlRules, name string) bool {
20-
for _, pattern := range rules.Blocked {
21-
if matchPattern(pattern, name) {
22-
return true
23-
}
24-
}
25-
26-
if len(rules.Allowed) == 0 {
27-
return false
28-
}
29-
30-
for _, pattern := range rules.Allowed {
31-
if matchPattern(pattern, name) {
32-
return false
33-
}
34-
}
35-
36-
return true
37-
}
38-
39-
// matchPattern tests name against a regex pattern anchored with ^...$, enforcing full-match semantics.
40-
// Uses the tyk/regexp package which caches compiled patterns.
41-
// Falls back to exact-string comparison if the pattern is not valid regex.
42-
func matchPattern(pattern, name string) bool {
43-
re, err := regexp.Compile("^(?:" + pattern + ")$")
44-
if err != nil {
45-
return pattern == name
46-
}
47-
return re.MatchString(name)
48-
}
49-
5010
// writeJSONRPCAccessDenied writes a JSON-RPC 2.0 error response for access-denied cases.
5111
// Delegates to jsonrpcerrors.WriteJSONRPCError for consistent response shape and HTTP→JSON-RPC
5212
// error code mapping across all error paths in the gateway.

0 commit comments

Comments
 (0)