Skip to content

Commit baff5af

Browse files
authored
Add OIDC Policy IDP TLS validation (#8556)
1 parent 563bcfb commit baff5af

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1570
-141
lines changed

build/Dockerfile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -662,8 +662,12 @@ LABEL org.nginx.kic.image.build.version="local"
662662
COPY --link --chown=101:0 nginx-ingress /
663663
# root is required for `setcap` invocation
664664
USER 0
665-
RUN --mount=type=bind,target=/tmp [ -z "${BUILD_OS##*plus*}" ] && PLUS=-plus; cp -a /tmp/internal/configs/version1/nginx$PLUS.ingress.tmpl /tmp/internal/configs/version1/nginx$PLUS.tmpl \
666-
/tmp/internal/configs/version2/nginx$PLUS.virtualserver.tmpl /tmp/internal/configs/version2/nginx$PLUS.transportserver.tmpl / \
665+
RUN --mount=type=bind,target=/tmp if [ -z "${BUILD_OS##*plus*}" ]; then PLUS=-plus; fi \
666+
&& cp -a /tmp/internal/configs/version1/nginx$PLUS.ingress.tmpl \
667+
/tmp/internal/configs/version1/nginx$PLUS.tmpl \
668+
/tmp/internal/configs/version2/nginx$PLUS.virtualserver.tmpl \
669+
/tmp/internal/configs/version2/nginx$PLUS.transportserver.tmpl / \
670+
&& if [ -z "${BUILD_OS##*plus*}" ]; then cp -a /tmp/internal/configs/version2/oidc.tmpl /; fi \
667671
&& chown -R 101:0 /*.tmpl \
668672
&& chmod -R g=u /*.tmpl \
669673
&& setcap 'cap_net_bind_service=+ep' /nginx-ingress && setcap -v 'cap_net_bind_service=+ep' /nginx-ingress

build/scripts/common.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ set -e
44

55
PLUS=""
66
if [ -z "${BUILD_OS##*plus*}" ]; then
7-
mkdir -p /etc/nginx/oidc/
7+
mkdir -p /etc/nginx/oidc/ /etc/nginx/oidc-conf.d/
88
cp -a /code/internal/configs/oidc/* /etc/nginx/oidc/
99
mkdir -p /etc/nginx/state_files/
1010
mkdir -p /etc/nginx/reporting/
1111
mkdir -p /etc/nginx/secrets/mgmt/
1212
PLUS=-plus
13+
cp -a /code/internal/configs/version2/oidc.tmpl /
1314
fi
1415

1516
mkdir -p /etc/nginx/njs/ && cp -a /code/internal/configs/njs/* /etc/nginx/njs/

cmd/nginx-ingress/main.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ const (
7979
socketPath = "/var/lib/nginx"
8080
fatalEventFlushTime = 200 * time.Millisecond
8181
secretErrorReason = "SecretError"
82+
fileErrorReason = "FileError"
8283
configMapErrorReason = "ConfigMapError"
8384
)
8485

@@ -191,6 +192,12 @@ func main() {
191192
if err != nil {
192193
logEventAndExit(ctx, eventRecorder, pod, secretErrorReason, err)
193194
}
195+
196+
caBundlePath, err := nginxManager.GetOSCABundlePath()
197+
if err != nil {
198+
logEventAndExit(ctx, eventRecorder, pod, fileErrorReason, err)
199+
}
200+
194201
globalConfigurationValidator := createGlobalConfigurationValidator()
195202

196203
mustProcessGlobalConfiguration(ctx)
@@ -226,6 +233,7 @@ func main() {
226233
StaticSSLPath: staticSSLPath,
227234
NginxVersion: nginxVersion,
228235
AppProtectBundlePath: appProtectBundlePath,
236+
DefaultCABundle: caBundlePath,
229237
}
230238

231239
if *nginxPlus {
@@ -541,11 +549,13 @@ func createTemplateExecutors(ctx context.Context) (*version1.TemplateExecutor, *
541549
nginxIngressTemplatePath := "nginx.ingress.tmpl"
542550
nginxVirtualServerTemplatePath := "nginx.virtualserver.tmpl"
543551
nginxTransportServerTemplatePath := "nginx.transportserver.tmpl"
552+
nginxOIDCConfTemplatePath := ""
544553
if *nginxPlus {
545554
nginxConfTemplatePath = "nginx-plus.tmpl"
546555
nginxIngressTemplatePath = "nginx-plus.ingress.tmpl"
547556
nginxVirtualServerTemplatePath = "nginx-plus.virtualserver.tmpl"
548557
nginxTransportServerTemplatePath = "nginx-plus.transportserver.tmpl"
558+
nginxOIDCConfTemplatePath = "oidc.tmpl"
549559
}
550560

551561
if *mainTemplatePath != "" {
@@ -566,7 +576,7 @@ func createTemplateExecutors(ctx context.Context) (*version1.TemplateExecutor, *
566576
nl.Fatalf(l, "Error creating TemplateExecutor: %v", err)
567577
}
568578

569-
templateExecutorV2, err := version2.NewTemplateExecutor(nginxVirtualServerTemplatePath, nginxTransportServerTemplatePath)
579+
templateExecutorV2, err := version2.NewTemplateExecutor(nginxVirtualServerTemplatePath, nginxTransportServerTemplatePath, nginxOIDCConfTemplatePath)
570580
if err != nil {
571581
nl.Fatalf(l, "Error creating TemplateExecutorV2: %v", err)
572582
}

config/crd/bases/k8s.nginx.org_policies.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,16 +390,38 @@ spec:
390390
with a + sign, for example openid+profile+email, openid+email+userDefinedScope.
391391
The default is openid.
392392
type: string
393+
sslVerify:
394+
default: false
395+
description: Enables verification of the IDP server SSL certificate.
396+
Default is false.
397+
type: boolean
398+
sslVerifyDepth:
399+
default: 1
400+
description: Sets the verification depth in the IDP server certificates
401+
chain. The default is 1.
402+
minimum: 0
403+
type: integer
393404
tokenEndpoint:
394405
description: URL for the token endpoint provided by your OpenID
395406
Connect provider.
396407
type: string
408+
trustedCertSecret:
409+
description: The name of the Kubernetes secret that stores the
410+
CA certificate for IDP server verification. It must be in the
411+
same namespace as the Policy resource. The secret must be of
412+
the type nginx.org/ca, and the certificate must be stored in
413+
the secret under the key ca.crt.
414+
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
415+
type: string
397416
zoneSyncLeeway:
398417
description: Specifies the maximum timeout in milliseconds for
399418
synchronizing ID/access tokens and shared values between Ingress
400419
Controller pods. The default is 200.
401420
type: integer
402421
type: object
422+
x-kubernetes-validations:
423+
- message: trustedCertSecret can be set only if sslVerify is true
424+
rule: (self.sslVerify == true) || (self.sslVerify == false && !has(self.trustedCertSecret))
403425
rateLimit:
404426
description: The rate limit policy controls the rate of processing
405427
requests per a defined key.

deploy/crds.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,16 +561,38 @@ spec:
561561
with a + sign, for example openid+profile+email, openid+email+userDefinedScope.
562562
The default is openid.
563563
type: string
564+
sslVerify:
565+
default: false
566+
description: Enables verification of the IDP server SSL certificate.
567+
Default is false.
568+
type: boolean
569+
sslVerifyDepth:
570+
default: 1
571+
description: Sets the verification depth in the IDP server certificates
572+
chain. The default is 1.
573+
minimum: 0
574+
type: integer
564575
tokenEndpoint:
565576
description: URL for the token endpoint provided by your OpenID
566577
Connect provider.
567578
type: string
579+
trustedCertSecret:
580+
description: The name of the Kubernetes secret that stores the
581+
CA certificate for IDP server verification. It must be in the
582+
same namespace as the Policy resource. The secret must be of
583+
the type nginx.org/ca, and the certificate must be stored in
584+
the secret under the key ca.crt.
585+
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
586+
type: string
568587
zoneSyncLeeway:
569588
description: Specifies the maximum timeout in milliseconds for
570589
synchronizing ID/access tokens and shared values between Ingress
571590
Controller pods. The default is 200.
572591
type: integer
573592
type: object
593+
x-kubernetes-validations:
594+
- message: trustedCertSecret can be set only if sslVerify is true
595+
rule: (self.sslVerify == true) || (self.sslVerify == false && !has(self.trustedCertSecret))
574596
rateLimit:
575597
description: The rate limit policy controls the rate of processing
576598
requests per a defined key.

docs/crd/k8s.nginx.org_policies.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ The `.spec` object supports the following fields:
7474
| `oidc.postLogoutRedirectURI` | `string` | URI to redirect to after the logout has been performed. Requires endSessionEndpoint. The default is /_logout. |
7575
| `oidc.redirectURI` | `string` | Allows overriding the default redirect URI. The default is /_codexch. |
7676
| `oidc.scope` | `string` | List of OpenID Connect scopes. The scope openid always needs to be present and others can be added concatenating them with a + sign, for example openid+profile+email, openid+email+userDefinedScope. The default is openid. |
77+
| `oidc.sslVerify` | `boolean` | Enables verification of the IDP server SSL certificate. Default is false. |
78+
| `oidc.sslVerifyDepth` | `integer` | Sets the verification depth in the IDP server certificates chain. The default is 1. |
7779
| `oidc.tokenEndpoint` | `string` | URL for the token endpoint provided by your OpenID Connect provider. |
80+
| `oidc.trustedCertSecret` | `string` | The name of the Kubernetes secret that stores the CA certificate for IDP server verification. It must be in the same namespace as the Policy resource. The secret must be of the type nginx.org/ca, and the certificate must be stored in the secret under the key ca.crt. |
7881
| `oidc.zoneSyncLeeway` | `integer` | Specifies the maximum timeout in milliseconds for synchronizing ID/access tokens and shared values between Ingress Controller pods. The default is 200. |
7982
| `rateLimit` | `object` | The rate limit policy controls the rate of processing requests per a defined key. |
8083
| `rateLimit.burst` | `integer` | Excessive requests are delayed until their number exceeds the burst size, in which case the request is terminated with an error. |
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
apiVersion: v1
2+
kind: Secret
3+
metadata:
4+
name: keycloak-ca
5+
type: nginx.org/ca
6+
data:
7+
ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZ4ekNDQTYrZ0F3SUJBZ0lVSnIxb2VDQTcxTmhjQ3VIVmh1NHVQcXNEVDhjd0RRWUpLb1pJaHZjTkFRRUwKQlFBd2N6RUxNQWtHQTFVRUJoTUNTVVV4RFRBTEJnTlZCQWdNQkVOdmNtc3hEVEFMQmdOVkJBY01CRU52Y21zeApDekFKQmdOVkJBb01Ba1kxTVF3d0NnWURWUVFMREFOT1NVTXhLekFwQmdOVkJBTU1JbXRsZVdOc2IyRnJMbVJsClptRjFiSFF1YzNaakxtTnNkWE4wWlhJdWJHOWpZV3d3SGhjTk1qVXhNVEUzTVRJeU9EVTVXaGNOTXpVeE1URTEKTVRJeU9EVTVXakJ6TVFzd0NRWURWUVFHRXdKSlJURU5NQXNHQTFVRUNBd0VRMjl5YXpFTk1Bc0dBMVVFQnd3RQpRMjl5YXpFTE1Ba0dBMVVFQ2d3Q1JqVXhEREFLQmdOVkJBc01BMDVKUXpFck1Da0dBMVVFQXd3aWEyVjVZMnh2CllXc3VaR1ZtWVhWc2RDNXpkbU11WTJ4MWMzUmxjaTVzYjJOaGJEQ0NBaUl3RFFZSktvWklodmNOQVFFQkJRQUQKZ2dJUEFEQ0NBZ29DZ2dJQkFLK1M5cVRXdGZ3YU1GdjI3aU5Ob3FJU2FaMXJ4R0VlZlpTd29ZTCtBZEVpTXhEMgpxN0dvbkc3QlZaOVoxQWpMTlhtcGwyeFF1ZVZBN25RYXpXa2JJazhhQytJOWwwQ1NudXNiK1UwMUV6V0g5MVJ2CnNRM0pFdlV1WXlma3loNnRGeGoyNTJFMTQ3TE1HcTlXOHVlRDNJYnNWVjRZWlBDY0VHN1c3aEYzSTVObllYa1kKYzVKWFFtKzNTODl5V2hWUDg1MHpGTEpTVUFkcHhuMW9qdmFhV3FZWHVrODd4ajU3U1VueGpzR3pTN1dxMDJxVAo0WjVBNENjUGR5Y2Vha2MzcDRLQXVQaHZSYnltK2l1ME13WWFDZ3ZDZnV2eTZLa0l5c1lCL0dUUmhKcFltU2FKCmNsUXFxT3NRTjd6U3crZDlrOFNlSnFONGVCMHVFbmNkSkRwcDJTa05qa3ZRaEhkRllncm5KN1dZVmZBSmxibkIKSUc3VVBmRCtIZDduRkhFdWIvdE4zN20xRnkyZjhjUnNkNWZ1Q29NNHhoelRucm1wbDFiTEZYN2dOLzgrNGZmZwpzc2VCSmZqbExKbCtzTTA4RlI0aVFicGMzK0xZbkpzOXdsUE82VTBzQ2VJUXB4SnVTUEh5aENtN2hFR2N5NGdKCi9SdENwZHVabitrMDdnTXVnUlVVdUxNV2JNWjhaTEh2cWNwbTliY05aQXNxRERzdkIxMTdldFh4Q1luVC9DRGMKYVJBMVh3a2ptNS9DY0k0U0JqNzQrZC8rMnJDbHBEZGl0dWxyTnJ6WjcrVTZFSU1pdDV3SnJ0V09nVlpYdGNxOAp3UlFWdHZINVNkZjZnWVlvOE9HejhRenAzZEJ5TEVaWDU1V0FzT084dWhhcUJGenk1TEVaSlk1WEo2SlpBZ01CCkFBR2pVekJSTUIwR0ExVWREZ1FXQkJUcmR5VTZzRVhRWHR5S1doRFhVbHJaMmpjQXhEQWZCZ05WSFNNRUdEQVcKZ0JUcmR5VTZzRVhRWHR5S1doRFhVbHJaMmpjQXhEQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BMEdDU3FHU0liMwpEUUVCQ3dVQUE0SUNBUUNTTDNFcDBrWUFBUnIrSStVcjhRdmhkdDg1NUtIbFIwenMyZkN3elFORjArUlFXTm1WCjF5ZitUaEkwRW13NnVaWk5rYXNWenFQcENZNU81VWkwK2ZhL09LaUs1eGdXVjVlais1SEJndFk1VitVVE9yYm0KTlpiODE2UnBya1NadTBFWlpqNVNnUEhmSzUraVNuWnVKL0J2Nk13WjNYR2F1N3pHdlBBRGpqSVBwMEJqczluVwpWUXQyQWRmZlQrUXd1UHgreVJEZTFEdHhpSkEvMlQxN0c3eUN3NnBDWHJsVHdNckk5dTVtSHhmM0FSaVJOZ01mCndPSHJBNXNJYkhFVVQ5QXc3WXp6OFZMNHk0d29la3IzRmwxUjh2OVkzKzJaNmw2TWNIS3FGV2owVkxCU0JFdnAKZzFBSGtZcUdRL2NzbXBOSWMvQ2ZUdWFSdzVsTFZqNlpVMjNmaENsYW9ldWUyejk2U0w5NGltZnFlbUJNOGtwNQp3djNoS3dqdVBsMGxEbkxKd21IOHFMK094L0Y3eTZseWhnSnRVeGtsc0hWQXlPeDlrekM4ZkNZL3BMbDhqc1lvCnh0c29ZMlRhcW1uSEUwNk5KaUk4VVBLd3NzM1M1cUFEV2xzSk4rc2ZURzViVTFZWlVUcjhybi9yc2VlQ1dpOFkKTFdXV3JHeVBjeVd3ZDNJY0pZSVIyWEdXNHNDYkpUdHllMGszRm9WUHV3VHdSVGlkVnVaWjN4OXIzbkpuSk84WAphdkpXa1Z3b0paK1ZRd1AyN1BPc1RFZVR2cFNWMjZkNENJYlZmSnRDZldhd1cybUU5QlozZ1RBbmFuK2pEQTl4CndTektWSW0yYnBIZCtHU0QwUXJvZkNXL2h1OUN2Q2p4aGR0aHlSYzJKOWd2SzFXMjAwZW9HdFl6d0E9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
apiVersion: v1
2+
kind: Secret
3+
metadata:
4+
name: keycloak-tls
5+
type: kubernetes.io/tls
6+
data:
7+
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZ4ekNDQTYrZ0F3SUJBZ0lVSnIxb2VDQTcxTmhjQ3VIVmh1NHVQcXNEVDhjd0RRWUpLb1pJaHZjTkFRRUwKQlFBd2N6RUxNQWtHQTFVRUJoTUNTVVV4RFRBTEJnTlZCQWdNQkVOdmNtc3hEVEFMQmdOVkJBY01CRU52Y21zeApDekFKQmdOVkJBb01Ba1kxTVF3d0NnWURWUVFMREFOT1NVTXhLekFwQmdOVkJBTU1JbXRsZVdOc2IyRnJMbVJsClptRjFiSFF1YzNaakxtTnNkWE4wWlhJdWJHOWpZV3d3SGhjTk1qVXhNVEUzTVRJeU9EVTVXaGNOTXpVeE1URTEKTVRJeU9EVTVXakJ6TVFzd0NRWURWUVFHRXdKSlJURU5NQXNHQTFVRUNBd0VRMjl5YXpFTk1Bc0dBMVVFQnd3RQpRMjl5YXpFTE1Ba0dBMVVFQ2d3Q1JqVXhEREFLQmdOVkJBc01BMDVKUXpFck1Da0dBMVVFQXd3aWEyVjVZMnh2CllXc3VaR1ZtWVhWc2RDNXpkbU11WTJ4MWMzUmxjaTVzYjJOaGJEQ0NBaUl3RFFZSktvWklodmNOQVFFQkJRQUQKZ2dJUEFEQ0NBZ29DZ2dJQkFLK1M5cVRXdGZ3YU1GdjI3aU5Ob3FJU2FaMXJ4R0VlZlpTd29ZTCtBZEVpTXhEMgpxN0dvbkc3QlZaOVoxQWpMTlhtcGwyeFF1ZVZBN25RYXpXa2JJazhhQytJOWwwQ1NudXNiK1UwMUV6V0g5MVJ2CnNRM0pFdlV1WXlma3loNnRGeGoyNTJFMTQ3TE1HcTlXOHVlRDNJYnNWVjRZWlBDY0VHN1c3aEYzSTVObllYa1kKYzVKWFFtKzNTODl5V2hWUDg1MHpGTEpTVUFkcHhuMW9qdmFhV3FZWHVrODd4ajU3U1VueGpzR3pTN1dxMDJxVAo0WjVBNENjUGR5Y2Vha2MzcDRLQXVQaHZSYnltK2l1ME13WWFDZ3ZDZnV2eTZLa0l5c1lCL0dUUmhKcFltU2FKCmNsUXFxT3NRTjd6U3crZDlrOFNlSnFONGVCMHVFbmNkSkRwcDJTa05qa3ZRaEhkRllncm5KN1dZVmZBSmxibkIKSUc3VVBmRCtIZDduRkhFdWIvdE4zN20xRnkyZjhjUnNkNWZ1Q29NNHhoelRucm1wbDFiTEZYN2dOLzgrNGZmZwpzc2VCSmZqbExKbCtzTTA4RlI0aVFicGMzK0xZbkpzOXdsUE82VTBzQ2VJUXB4SnVTUEh5aENtN2hFR2N5NGdKCi9SdENwZHVabitrMDdnTXVnUlVVdUxNV2JNWjhaTEh2cWNwbTliY05aQXNxRERzdkIxMTdldFh4Q1luVC9DRGMKYVJBMVh3a2ptNS9DY0k0U0JqNzQrZC8rMnJDbHBEZGl0dWxyTnJ6WjcrVTZFSU1pdDV3SnJ0V09nVlpYdGNxOAp3UlFWdHZINVNkZjZnWVlvOE9HejhRenAzZEJ5TEVaWDU1V0FzT084dWhhcUJGenk1TEVaSlk1WEo2SlpBZ01CCkFBR2pVekJSTUIwR0ExVWREZ1FXQkJUcmR5VTZzRVhRWHR5S1doRFhVbHJaMmpjQXhEQWZCZ05WSFNNRUdEQVcKZ0JUcmR5VTZzRVhRWHR5S1doRFhVbHJaMmpjQXhEQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BMEdDU3FHU0liMwpEUUVCQ3dVQUE0SUNBUUNTTDNFcDBrWUFBUnIrSStVcjhRdmhkdDg1NUtIbFIwenMyZkN3elFORjArUlFXTm1WCjF5ZitUaEkwRW13NnVaWk5rYXNWenFQcENZNU81VWkwK2ZhL09LaUs1eGdXVjVlais1SEJndFk1VitVVE9yYm0KTlpiODE2UnBya1NadTBFWlpqNVNnUEhmSzUraVNuWnVKL0J2Nk13WjNYR2F1N3pHdlBBRGpqSVBwMEJqczluVwpWUXQyQWRmZlQrUXd1UHgreVJEZTFEdHhpSkEvMlQxN0c3eUN3NnBDWHJsVHdNckk5dTVtSHhmM0FSaVJOZ01mCndPSHJBNXNJYkhFVVQ5QXc3WXp6OFZMNHk0d29la3IzRmwxUjh2OVkzKzJaNmw2TWNIS3FGV2owVkxCU0JFdnAKZzFBSGtZcUdRL2NzbXBOSWMvQ2ZUdWFSdzVsTFZqNlpVMjNmaENsYW9ldWUyejk2U0w5NGltZnFlbUJNOGtwNQp3djNoS3dqdVBsMGxEbkxKd21IOHFMK094L0Y3eTZseWhnSnRVeGtsc0hWQXlPeDlrekM4ZkNZL3BMbDhqc1lvCnh0c29ZMlRhcW1uSEUwNk5KaUk4VVBLd3NzM1M1cUFEV2xzSk4rc2ZURzViVTFZWlVUcjhybi9yc2VlQ1dpOFkKTFdXV3JHeVBjeVd3ZDNJY0pZSVIyWEdXNHNDYkpUdHllMGszRm9WUHV3VHdSVGlkVnVaWjN4OXIzbkpuSk84WAphdkpXa1Z3b0paK1ZRd1AyN1BPc1RFZVR2cFNWMjZkNENJYlZmSnRDZldhd1cybUU5QlozZ1RBbmFuK2pEQTl4CndTektWSW0yYnBIZCtHU0QwUXJvZkNXL2h1OUN2Q2p4aGR0aHlSYzJKOWd2SzFXMjAwZW9HdFl6d0E9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
8+
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRZ0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQ1N3d2dna29BZ0VBQW9JQ0FRQ3ZrdmFrMXJYOEdqQmIKOXU0alRhS2lFbW1kYThSaEhuMlVzS0dDL2dIUklqTVE5cXV4cUp4dXdWV2ZXZFFJeXpWNXFaZHNVTG5sUU81MApHczFwR3lKUEdndmlQWmRBa3A3ckcvbE5OUk0xaC9kVWI3RU55UkwxTG1NbjVNb2VyUmNZOXVkaE5lT3l6QnF2ClZ2TG5nOXlHN0ZWZUdHVHduQkJ1MXU0UmR5T1RaMkY1R0hPU1YwSnZ0MHZQY2xvVlQvT2RNeFN5VWxBSGFjWjkKYUk3Mm1scW1GN3BQTzhZK2UwbEo4WTdCczB1MXF0TnFrK0dlUU9BbkQzY25IbXBITjZlQ2dMajRiMFc4cHZvcgp0RE1HR2dvTHduN3I4dWlwQ01yR0FmeGswWVNhV0prbWlYSlVLcWpyRURlODBzUG5mWlBFbmlhamVIZ2RMaEozCkhTUTZhZGtwRFk1TDBJUjNSV0lLNXllMW1GWHdDWlc1d1NCdTFEM3cvaDNlNXhSeExtLzdUZCs1dFJjdG4vSEUKYkhlWDdncURPTVljMDU2NXFaZFd5eFYrNERmL1B1SDM0TExIZ1NYNDVTeVpmckROUEJVZUlrRzZYTi9pMkp5YgpQY0pUenVsTkxBbmlFS2NTYmtqeDhvUXB1NFJCbk11SUNmMGJRcVhibVovcE5PNERMb0VWRkxpekZtekdmR1N4Cjc2bktadlczRFdRTEtndzdMd2RkZTNyVjhRbUowL3dnM0drUU5WOEpJNXVmd25DT0VnWSsrUG5mL3Rxd3BhUTMKWXJicGF6YTgyZS9sT2hDRElyZWNDYTdWam9GV1Y3WEt2TUVVRmJieCtVblgrb0dHS1BEaHMvRU02ZDNRY2l4RwpWK2VWZ0xEanZMb1dxZ1JjOHVTeEdTV09WeWVpV1FJREFRQUJBb0lDQUI4YzFtczRoekJBL2MvV0xyWC8xSEdPCi9MdENOUjhXdFo5TE81dklZazhLbGUwTUlUbk96TVhOcWR3ZW9YWGJlTUx4L0J6Y0kwME9XQk1vQ3IxMDZ2d0UKZkJXZjMzVTRaa1A0aFpHYWRhaDNTeXRoellqSldId3RON0lDbDVTZkRLaEdYSk03NXZrd3RRdmNSeGdpcEVvZQppRFF2ODNjMTJLMmpsYlZ2bk5US3JabTFiUW1DUUFvbSs1NnJ2MjNtYUorelJSZ2lnUDhIVGY2OE1CVmdIZTh2CjVqcVRONXFyNHoxZ3VuRDEwbFZEaThwbm9VUVhjQUZMK3N2cVZsLy9hMFl6aEZPMStEQXBrTXg4MXN2ZWdtZzYKRTU3QlFWeHU2K3Z4dnlXb2dTeU94Ymp3QTF3SjRUd2llQllVYlZYUXlZWStsazlDa2xwdFp5VkhlenVFdFZBRwoxSkdDb3NyZkl0eUE3YXdSVXFSMWFwajJPS0Jyd1dROW5nK0dvR0RRNnBLTHdzN1F6TUlkUVNMZGY2SVZMZWE4CjJTZ1AyK0hycUIrR1g5ZlJGdUFPRTRQNGprNGtMT3FTSkR4OENvdkdiS3NSUm1RR1lGL282dFhJYm5zZFVNaUsKZUVuYUVINXRmVWZ1WXNlWTlmR3pTUGFJT1FLeXI4TWJtanp3NFprNE9VUmxRWE41K3kzOGxXREVyb3NybG9VUwpZSkxucU5sVUEvNk96d3RTaCtRenFLeGEzcS9jYndFbU54NDYzOTlzRDNxSndXQlBXcng1REtiSlp4SWtQNFVOCnE1YUVGZW5kY09mZTNxNG9uQm1FQ28weW1zRko0eGNTUFdYUGJtRk91dHJiTnN5R0xDRURlUjNpRmM0Qk12aTYKZURwSHl0MTlXTDloLysyVC9OeG5Bb0lCQVFEd2JLaTd3TUgyWUlUYzBkTi82SEtiaVdJQXVFZkZMbzZSOUxuTwpaWnR2QW5tQTNSTmpJanB3R1BGQ3A0QjdjdEpiVWtIbzlSemdZQ3RKeGF6ZE5wWXRRaFhMRFZ0c0ExZ21zQUo0CmJwcVUrVnhoeTd5R1lzZDNZVDRVcUVRZFNtbHdYRHhuSFlsb0hWd3ExQlRxeWNZSjNYZGV6bE9BS0JzZFR6eGwKZmJGdjJjSXM3aTBUcU1VbzZSd1hVcnhSSU85TGNJL2ZaSWROVmd4MVk2Nm9SaXoxdnlVYVlISUNGM3V6OHl6VgpYc1JoMVZzNFdXVXRhTmlzbXBWTkFnTEtHTUthWGlKNU9pRjA1RWljcUs0bWtNR2lUMFFaZElpNkdyd3dEMkU5Cm5qZjY2bkwycmtGQXRudHpaVkdXMTVwVkUxc2ZJV0UrbHcycGlhSjFESTVNVm4zYkFvSUJBUUM2OHNtWTZFK2QKQmVzTUpuSXhVaXdPUWZ6a256OG5udGVvMzFSb2c5R1dDejBnNlJFVnBRdDZkVU43Wi8yQnZKVUtkNk0yZmlYYwppUmNFRTF6RHpaZ3RMVmhWbG5ZaWZnT1lsbU9NRlRZbHRGUnQ0emgzbUM1TUxOVThVVmhNUjE2cHYvVkF2RmhpCnlxbVNQVG1acjNVZkNKdjZjeFZRbFZJYlVTTVI3bmlSSW13T2JPYUJ3OFpCV0JaOUVCYi90N0ROUVpCV2ZKdTIKejdESXhsUXBxcFM1aWZLRlZpT2pCL0hXcnJ1RHpLUjROdFFDSkRoZERoUlZCWWxsSkpKRE9GV3VCbm5hS3EveQpiZWlrV2dSZmI3YUNSOU5LUVk3aHM4d2dDL0MzNFVBR2lyN2pJNURpQmFKOGVlM0xoMk5GRTI4VWFUTW5mSDNJClFQbVN1MUI2NjJqYkFvSUJBSEZ6QktnY0RDckRYczZJWUtIeHdPcnVDQVhJNzJ6M1RDVkpjc2dYSUNKZzY0N0kKUTFhN0Z4SkFZdEFPRkUyc1grRGh6dUlyajZXOUc1QWpMQy95aXlqdUR6U1NwL292RmRDanEzYkMwa1RMNmpEbgpuNTFXVFVOaTZwVjYxVEZ4SkpIMXBEY1FNLytpSXhTK29PUXR0RHFCZTh1TDF0RVptN25YNHVzTlJjWSszaWF2CmVTdldycnBnVFhZZi8yYlZBTFg3ZHBoMmFuWXV6WkF6S242VEpySUxzV2xoNjBwYlpHOEVwN3BEanEyUHJRekkKK2pwVVNESWllNk1yK0w3K3NnMS9zQXErU0gxTkg0cDAra0NPZkNDb0FMMTJSUEowblNxY2gwazVPTGM1SEdpVQp6NHZHMERnaXJqNWNuS0hha1Z2K04xSCttMTdONkpBTkRiU3Q5NU1DZ2dFQkFLS3R4UG4zSmRoSkh4bEtsMUlOCjVHSmZ6N1lPVVVHaitweHNBcUtVR3B4TG1WejdFeS9YbUI1dXpsTWowYmpFcHBrZU5IdWwyRUtKVk9ycUFtNHMKaVFDL0ZjQWNseDQ2czl4aSthc2JoaXZYT1NVS2RjZTBPSTEyOGZOMEFiY1czK3d0S3ppeTdPTEM0ajVzWXFRMgp4MTlDK2FBOTVzMWhzcm9zcDZ6aDdDNjNXbnBQRDJMYVBybjc4azNQNDRPUWtCeDhzaUpnZW92aFBUL3BQYkdvClM1VU0wbXB1NDhIcGx1dXV6MlBJZjFKUXU3cEZWSHE5VnJvSmdGN3dMUXFyaWZ0T2pWaG9qd1VSMlVDelNGelgKOUdSNEpnZlc5b08zRnFqSVd5ZFhyb1JDMWdzSGx2cm4xbFlsTCtWTklmZ3BDaDhqMEN6TEt4VklYU1R2TlFCUgp1OE1DZ2dFQWJuQ0JSdmtNUnh6S1NkM1V5USszV0tBdnYzVHk2L1phNy8rVVBhZzFRTE5RSksram50L3AvNWpzCkRCRWhPSDhMT2MxTnEzSEpWUVBjV2kwVXo1aDhOWVJwUWx2QWNlRjRTb3d4ZGIrWWRTS1RIN3daVDFwa1dkY0kKWTNib1N0Y0hsOVlCM1Q5STVIZWR4dHgzMytNSkppMXcwVGswK3lNcHBJUmV2VFg3WjVpbnZpbHE3eWJITzd6NwoxbHRHaGJJTjZSblVzcy9mZE1ianJ4N3dreEtqMmlKLzYyM3B1Z0tsakdndUlVR1l3ODNaeUREaGp3ZVJleTlMCmphQVViN01CQ2xsR0NNS3hEZGtsUUN3eHh0YmpySk1QZ0JSbVRjK093QzNwUDNqemJLNlRuQWhnL3JxeEpaYnAKN3hFL0lyd3lxOXJtY3lqZzlsVkh0RHRFN1RDSVRnPT0KLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo=

0 commit comments

Comments
 (0)