Skip to content

Commit b462fde

Browse files
Merge branch 'dapr:main' into main
2 parents b78f753 + 506118f commit b462fde

File tree

31 files changed

+996
-59
lines changed

31 files changed

+996
-59
lines changed

.github/scripts/components-scripts/conformance-bindings.azure.eventgrid-setup.sh

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,48 @@
22

33
set -e
44

5-
# Start ngrok
6-
wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip
7-
unzip -qq ngrok-stable-linux-amd64.zip
5+
echo "Downloading ngrok..."
6+
wget https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-amd64.tgz
7+
tar -xzf ngrok-v3-stable-linux-amd64.tgz
88
./ngrok authtoken ${AzureEventGridNgrokToken}
9-
./ngrok http -log=stdout --log-level debug -host-header=localhost 9000 > /tmp/ngrok.log &
9+
10+
echo "Starting ngrok tunnel..."
11+
./ngrok http --log=stdout --log-level=debug --host-header=localhost 9000 > /tmp/ngrok.log 2>&1 &
12+
NGROK_PID=$!
13+
14+
echo "Waiting for ngrok to start..."
1015
sleep 10
1116

12-
NGROK_ENDPOINT=`cat /tmp/ngrok.log | grep -Eom1 'https://.*' | sed 's/\s.*//'`
17+
# Ensure ngrok is still running
18+
if ! kill -0 $NGROK_PID 2>/dev/null; then
19+
echo "Ngrok process died. Log output:"
20+
cat /tmp/ngrok.log
21+
exit 1
22+
fi
23+
24+
echo "Getting tunnel URL from ngrok API..."
25+
MAX_RETRIES=30
26+
RETRY_COUNT=0
27+
28+
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
29+
if curl -s http://localhost:4040/api/tunnels > /tmp/ngrok_api.json 2>/dev/null; then
30+
# Extract the public URL from the API response
31+
NGROK_ENDPOINT=$(cat /tmp/ngrok_api.json | grep -o '"public_url":"[^"]*"' | head -1 | cut -d'"' -f4)
32+
33+
if [ -n "$NGROK_ENDPOINT" ] && [ "$NGROK_ENDPOINT" != "null" ]; then
34+
echo "Successfully got tunnel URL from API: $NGROK_ENDPOINT"
35+
break
36+
fi
37+
fi
38+
39+
echo "Waiting for ngrok API to be ready... (attempt $((RETRY_COUNT + 1))/$MAX_RETRIES)"
40+
sleep 2
41+
RETRY_COUNT=$((RETRY_COUNT + 1))
42+
done
43+
1344
echo "Ngrok endpoint: ${NGROK_ENDPOINT}"
1445
echo "AzureEventGridSubscriberEndpoint=${NGROK_ENDPOINT}/api/events" >> $GITHUB_ENV
46+
echo "Ngrok log:"
1547
cat /tmp/ngrok.log
1648

1749
# Schedule trigger to kill ngrok

.github/workflows/components-contrib-all.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,19 +68,20 @@ jobs:
6868
GOLANGCI_LINT_VER: "v1.64.6"
6969
strategy:
7070
matrix:
71-
os: [ubuntu-latest, windows-latest, macOS-latest]
71+
# TODO: @nelson-parente Upgrade macos latest once dns is fixed.
72+
os: [ubuntu-latest, windows-latest, macos-14]
7273
target_arch: [arm, amd64]
7374
include:
7475
- os: ubuntu-latest
7576
target_os: linux
7677
- os: windows-latest
7778
target_os: windows
78-
- os: macOS-latest
79+
- os: macos-14
7980
target_os: darwin
8081
exclude:
8182
- os: windows-latest
8283
target_arch: arm
83-
- os: macOS-latest
84+
- os: macos-14
8485
target_arch: arm
8586
steps:
8687
- name: Set default payload repo and ref

bindings/azure/eventgrid/eventgrid.go

Lines changed: 103 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,14 @@ const (
5151
// Format for the "jwks_uri" endpoint
5252
// The %s refers to the tenant ID
5353
jwksURIFormat = "https://login.microsoftonline.com/%s/discovery/v2.0/keys"
54-
// Format for the "iss" claim in the JWT
54+
// Format for the "iss" claim in the JWT (Azure AD v2.0)
5555
// The %s refers to the tenant ID
5656
jwtIssuerFormat = "https://login.microsoftonline.com/%s/v2.0"
57+
// Format for the "iss" claim in the JWT (Azure AD v1.0 - legacy)
58+
// The %s refers to the tenant ID
59+
jwtIssuerV1Format = "https://sts.windows.net/%s/"
60+
// Eventgrid managed identity issuer
61+
eventGridIssuer = "https://eventgrid.azure.net"
5762
)
5863

5964
// AzureEventGrid allows sending/receiving Azure Event Grid events.
@@ -111,34 +116,113 @@ func (a *AzureEventGrid) Init(_ context.Context, metadata bindings.Metadata) err
111116

112117
var matchAuthHeader = regexp.MustCompile(`(?i)^(Bearer )?(([A-Za-z0-9_-]+\.){2}[A-Za-z0-9_-]+)$`)
113118

114-
func (a *AzureEventGrid) validateAuthHeader(ctx context.Context, authorizationHeader string) bool {
119+
func (a *AzureEventGrid) validateAuthHeader(ctx *fasthttp.RequestCtx) bool {
115120
// Extract the bearer token from the header
121+
authorizationHeader := string(ctx.Request.Header.Peek("authorization"))
116122
if authorizationHeader == "" {
117123
a.logger.Error("Incoming webhook request does not contain an Authorization header")
118124
return false
119125
}
126+
120127
match := matchAuthHeader.FindStringSubmatch(authorizationHeader)
121128
if len(match) < 3 {
122129
a.logger.Error("Incoming webhook request does not contain a valid bearer token in the Authorization header")
123130
return false
124131
}
125132
token := match[2]
126133

127-
// Validate the JWT
128-
_, err := jwt.ParseString(
129-
token,
130-
jwt.WithKeySet(a.jwks, jws.WithInferAlgorithmFromKey(true)),
131-
jwt.WithAudience(a.metadata.azureClientID),
132-
jwt.WithIssuer(fmt.Sprintf(jwtIssuerFormat, a.metadata.azureTenantID)),
133-
jwt.WithAcceptableSkew(5*time.Minute),
134-
jwt.WithContext(ctx),
135-
)
134+
// First, parse the JWT to see what claims we received
135+
parsedToken, err := jwt.ParseString(token, jwt.WithVerify(false))
136136
if err != nil {
137-
a.logger.Errorf("Failed to validate JWT in the incoming webhook request: %v", err)
137+
a.logger.Errorf("Failed to parse JWT: %v", err)
138138
return false
139139
}
140140

141-
return true
141+
actualIssuer := parsedToken.Issuer()
142+
azureADV2Issuer := fmt.Sprintf(jwtIssuerFormat, a.metadata.azureTenantID)
143+
expectedAudience := a.metadata.azureClientID
144+
switch actualIssuer {
145+
case azureADV2Issuer:
146+
// AzureAD v2.0 issuer
147+
_, err = jwt.ParseString(
148+
token,
149+
jwt.WithKeySet(a.jwks, jws.WithInferAlgorithmFromKey(true)),
150+
jwt.WithAudience(expectedAudience),
151+
jwt.WithIssuer(azureADV2Issuer),
152+
jwt.WithAcceptableSkew(5*time.Minute),
153+
jwt.WithContext(context.Background()),
154+
)
155+
if err == nil {
156+
return true
157+
}
158+
159+
// Also check webhook URL as audience
160+
_, err = jwt.ParseString(
161+
token,
162+
jwt.WithKeySet(a.jwks, jws.WithInferAlgorithmFromKey(true)),
163+
jwt.WithAudience(a.metadata.SubscriberEndpoint),
164+
jwt.WithIssuer(azureADV2Issuer),
165+
jwt.WithAcceptableSkew(5*time.Minute),
166+
jwt.WithContext(context.Background()),
167+
)
168+
if err == nil {
169+
return true
170+
}
171+
172+
a.logger.Errorf("JWT validation failed for AzureAD v2.0 issuer")
173+
return false
174+
175+
case fmt.Sprintf(jwtIssuerV1Format, a.metadata.azureTenantID):
176+
// AzureAD v1.0 issuer
177+
a.logger.Infof("Detected AzureAD v1.0 issuer, validating...")
178+
_, err = jwt.ParseString(
179+
token,
180+
jwt.WithKeySet(a.jwks, jws.WithInferAlgorithmFromKey(true)),
181+
jwt.WithAudience(expectedAudience),
182+
jwt.WithIssuer(actualIssuer),
183+
jwt.WithAcceptableSkew(5*time.Minute),
184+
jwt.WithContext(context.Background()),
185+
)
186+
if err == nil {
187+
return true
188+
}
189+
_, err = jwt.ParseString(
190+
token,
191+
jwt.WithKeySet(a.jwks, jws.WithInferAlgorithmFromKey(true)),
192+
jwt.WithAudience(a.metadata.SubscriberEndpoint),
193+
jwt.WithIssuer(actualIssuer),
194+
jwt.WithAcceptableSkew(5*time.Minute),
195+
jwt.WithContext(context.Background()),
196+
)
197+
if err == nil {
198+
return true
199+
}
200+
201+
a.logger.Errorf("JWT validation failed for AzureAD v1.0 issuer")
202+
return false
203+
204+
case eventGridIssuer:
205+
// eventgrid managed identity issuer - use webhook URL as audience
206+
_, err = jwt.ParseString(
207+
token,
208+
jwt.WithKeySet(a.jwks, jws.WithInferAlgorithmFromKey(true)),
209+
jwt.WithAudience(a.metadata.SubscriberEndpoint),
210+
jwt.WithIssuer(eventGridIssuer),
211+
jwt.WithAcceptableSkew(5*time.Minute),
212+
jwt.WithContext(context.Background()),
213+
)
214+
if err == nil {
215+
return true
216+
}
217+
218+
a.logger.Errorf("JWT validation failed for eventgrid issuer: %v", err)
219+
return false
220+
221+
default:
222+
a.logger.Errorf("Unexpected JWT issuer: %s. Expected either '%s', '%s', or '%s'",
223+
actualIssuer, azureADV2Issuer, fmt.Sprintf(jwtIssuerV1Format, a.metadata.azureTenantID), eventGridIssuer)
224+
return false
225+
}
142226
}
143227

144228
// Initializes the JWKS cache
@@ -284,10 +368,12 @@ func (a *AzureEventGrid) requestHandler(handler bindings.Handler) fasthttp.Reque
284368
return
285369
}
286370

287-
// Validate the Authorization header
288-
authorizationHeader := string(ctx.Request.Header.Peek("authorization"))
289-
// Note that ctx is a fasthttp context so it's actually tied to the server's lifecycle and not the request's
290-
if !a.validateAuthHeader(ctx, authorizationHeader) {
371+
// Options requests (webhook validation handshake) don't require authentication
372+
// Azure Event Grid sends options requests without authorization header during initial validation
373+
if method == http.MethodOptions {
374+
// Skip authentication for options requests
375+
} else if !a.validateAuthHeader(ctx) {
376+
// Note that ctx is a fasthttp context so it's actually tied to the server's lifecycle and not the request's
291377
ctx.Response.Header.SetStatusCode(http.StatusUnauthorized)
292378
_, err = ctx.Response.BodyWriter().Write([]byte("401 Unauthorized"))
293379
if err != nil {

conversation/echo/metadata.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# yaml-language-server: $schema=../../../component-metadata-schema.json
2+
schemaVersion: v1
3+
type: conversation
4+
name: echo
5+
version: v1
6+
status: alpha
7+
title: "Echo"
8+
urls:
9+
- title: Reference
10+
url: https://docs.dapr.io/reference/components-reference/supported-conversation/echo/
11+
metadata: []

crypto/kubernetes/secrets/component.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ func (k *kubeSecretsCrypto) retrieveKeyFromSecret(parentCtx context.Context, key
123123
// parseKeyString returns the secret name, key, and optional namespace from the key parameter.
124124
// If the key parameter doesn't contain a namespace, returns the default one.
125125
func (k *kubeSecretsCrypto) parseKeyString(param string) (namespace string, secret string, key string, err error) {
126-
parts := strings.Split(key, "/")
126+
parts := strings.Split(param, "/")
127127
switch len(parts) {
128128
case 3:
129129
namespace = parts[0]

state/aerospike/aerospike.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ import (
3434
)
3535

3636
type aerospikeMetadata struct {
37-
Hosts string
38-
Namespace string
39-
Set string // optional
37+
Hosts string `json:"hosts"`
38+
Namespace string `json:"namespace"`
39+
Set string `json:"set"` // optional
4040
}
4141

4242
var (

state/aerospike/metadata.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# yaml-language-server: $schema=../../../component-metadata-schema.json
2+
schemaVersion: v1
3+
type: state
4+
name: aerospike
5+
version: v1
6+
status: alpha
7+
title: "Aerospike"
8+
urls:
9+
- title: Reference
10+
url: https://docs.dapr.io/reference/components-reference/supported-state-stores/setup-aerospike/
11+
metadata:
12+
- name: hosts
13+
type: string
14+
required: true
15+
description: Comma-separated list of Aerospike hosts.
16+
example: "localhost:3000,aerospike:3000"
17+
- name: namespace
18+
type: string
19+
required: true
20+
description: The Aerospike namespace.
21+
example: "test"
22+
- name: set
23+
type: string
24+
required: false
25+
description: The Aerospike set name.
26+
example: "dapr-state"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# yaml-language-server: $schema=../../../component-metadata-schema.json
2+
schemaVersion: v1
3+
type: state
4+
name: alicloud.tablestore
5+
version: v1
6+
status: alpha
7+
title: "AliCloud TableStore"
8+
urls:
9+
- title: Reference
10+
url: https://docs.dapr.io/reference/components-reference/supported-state-stores/
11+
authenticationProfiles:
12+
- title: "Access Key Authentication"
13+
description: "Authenticate using an access key"
14+
metadata:
15+
- name: accessKeyID
16+
type: string
17+
required: true
18+
description: The access key ID of the AliCloud TableStore instance.
19+
example: "my_access_key_id"
20+
- name: accessKey
21+
type: string
22+
required: true
23+
description: The access key of the AliCloud TableStore instance.
24+
example: "my_access_key"
25+
metadata:
26+
- name: endpoint
27+
type: string
28+
required: true
29+
description: The endpoint of the AliCloud TableStore instance.
30+
example: "https://tablestore.aliyuncs.com"
31+
- name: instanceName
32+
type: string
33+
required: true
34+
description: The name of the AliCloud TableStore instance.
35+
example: "my_instance"
36+
- name: tableName
37+
type: string
38+
required: true
39+
description: The name of the table to use.
40+
example: "my_table"

state/couchbase/couchbase.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,12 @@ type Couchbase struct {
5757
}
5858

5959
type couchbaseMetadata struct {
60-
CouchbaseURL string
61-
Username string
62-
Password string
63-
BucketName string
64-
NumReplicasDurableReplication uint
65-
NumReplicasDurablePersistence uint
60+
CouchbaseURL string `json:"couchbaseURL"`
61+
Username string `json:"username"`
62+
Password string `json:"password"`
63+
BucketName string `json:"bucketName"`
64+
NumReplicasDurableReplication uint `json:"numReplicasDurableReplication"`
65+
NumReplicasDurablePersistence uint `json:"numReplicasDurablePersistence"`
6666
}
6767

6868
// NewCouchbaseStateStore returns a new couchbase state store.

0 commit comments

Comments
 (0)