├── .github
└── FUNDING.yml
├── .gitattributes
├── remove-docker-images.sh
├── documentation
├── openldap.jpeg
├── project-diagram.jpeg
└── project-diagram.excalidraw
├── import-openldap-users.sh
├── .mvn
└── wrapper
│ └── maven-wrapper.properties
├── simple-service
├── src
│ ├── main
│ │ ├── resources
│ │ │ ├── application.properties
│ │ │ └── banner.txt
│ │ └── java
│ │ │ └── com
│ │ │ └── ivanfranchin
│ │ │ └── simpleservice
│ │ │ ├── SimpleServiceApplication.java
│ │ │ └── controller
│ │ │ └── SimpleServiceController.java
│ └── test
│ │ └── java
│ │ └── com
│ │ └── ivanfranchin
│ │ └── simpleservice
│ │ └── SimpleServiceApplicationTests.java
└── pom.xml
├── shutdown-environment.sh
├── .gitignore
├── scripts
└── my-functions.sh
├── pom.xml
├── ldap
└── ldap-mycompany-com.ldif
├── init-environment.sh
├── mvnw.cmd
├── mvnw
└── README.md
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: ivangfr
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | /mvnw text eol=lf
2 | *.cmd text eol=crlf
3 |
--------------------------------------------------------------------------------
/remove-docker-images.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | docker rmi ivanfranchin/simple-service:1.0.0
4 |
--------------------------------------------------------------------------------
/documentation/openldap.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivangfr/springboot-kong-plugins/HEAD/documentation/openldap.jpeg
--------------------------------------------------------------------------------
/documentation/project-diagram.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivangfr/springboot-kong-plugins/HEAD/documentation/project-diagram.jpeg
--------------------------------------------------------------------------------
/import-openldap-users.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ldapadd -x -D "cn=admin,dc=mycompany,dc=com" -w admin -H ldap:// -f ldap/ldap-mycompany-com.ldif
4 |
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | wrapperVersion=3.3.4
2 | distributionType=only-script
3 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
4 |
--------------------------------------------------------------------------------
/simple-service/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.application.name=simple-service
2 |
3 | management.endpoints.web.exposure.include=health,beans
4 | management.endpoint.health.show-details=always
5 |
--------------------------------------------------------------------------------
/simple-service/src/test/java/com/ivanfranchin/simpleservice/SimpleServiceApplicationTests.java:
--------------------------------------------------------------------------------
1 | package com.ivanfranchin.simpleservice;
2 |
3 | import org.junit.jupiter.api.Disabled;
4 | import org.junit.jupiter.api.Test;
5 | import org.springframework.boot.test.context.SpringBootTest;
6 |
7 | @Disabled
8 | @SpringBootTest
9 | class SimpleServiceApplicationTests {
10 |
11 | @Test
12 | void contextLoads() {
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/simple-service/src/main/resources/banner.txt:
--------------------------------------------------------------------------------
1 | _ _ _
2 | ___(_)_ __ ___ _ __ | | ___ ___ ___ _ ____ _(_) ___ ___
3 | / __| | '_ ` _ \| '_ \| |/ _ \_____/ __|/ _ \ '__\ \ / / |/ __/ _ \
4 | \__ \ | | | | | | |_) | | __/_____\__ \ __/ | \ V /| | (_| __/
5 | |___/_|_| |_| |_| .__/|_|\___| |___/\___|_| \_/ |_|\___\___|
6 | |_|
7 | :: Spring Boot :: ${spring-boot.formatted-version}
8 |
--------------------------------------------------------------------------------
/simple-service/src/main/java/com/ivanfranchin/simpleservice/SimpleServiceApplication.java:
--------------------------------------------------------------------------------
1 | package com.ivanfranchin.simpleservice;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class SimpleServiceApplication {
8 |
9 | public static void main(String[] args) {
10 | SpringApplication.run(SimpleServiceApplication.class, args);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/shutdown-environment.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | echo
4 | echo "Starting the environment shutdown"
5 | echo "================================="
6 |
7 | echo
8 | echo "Removing containers"
9 | echo "-------------------"
10 | docker rm -fv simple-service kong-database kong phpldapadmin openldap
11 |
12 | echo
13 | echo "Removing network"
14 | echo "----------------"
15 | docker network rm springboot-kong-net
16 |
17 | echo
18 | echo "Environment shutdown successfully"
19 | echo "================================="
20 | echo
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | !.mvn/wrapper/maven-wrapper.jar
3 | !**/src/main/**/target/
4 | !**/src/test/**/target/
5 |
6 | ### STS ###
7 | .apt_generated
8 | .classpath
9 | .factorypath
10 | .project
11 | .settings
12 | .springBeans
13 | .sts4-cache
14 |
15 | ### IntelliJ IDEA ###
16 | .idea
17 | *.iws
18 | *.iml
19 | *.ipr
20 |
21 | ### NetBeans ###
22 | /nbproject/private/
23 | /nbbuild/
24 | /dist/
25 | /nbdist/
26 | /.nb-gradle/
27 | build/
28 | !**/src/main/**/build/
29 | !**/src/test/**/build/
30 |
31 | ### VS Code ###
32 | .vscode/
33 |
34 | ### MAC OS ###
35 | *.DS_Store
36 |
--------------------------------------------------------------------------------
/scripts/my-functions.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | TIMEOUT=120
4 |
5 | # -- wait_for_container_log --
6 | # $1: docker container name
7 | # S2: spring value to wait to appear in container logs
8 | function wait_for_container_log() {
9 | local log_waiting="Waiting for string '$2' in the $1 logs ..."
10 | echo "${log_waiting} It will timeout in ${TIMEOUT}s"
11 | SECONDS=0
12 |
13 | while true ; do
14 | local log=$(docker logs $1 2>&1 | grep "$2")
15 | if [ -n "$log" ] ; then
16 | echo $log
17 | break
18 | fi
19 |
20 | if [ $SECONDS -ge $TIMEOUT ] ; then
21 | echo "${log_waiting} TIMEOUT"
22 | break;
23 | fi
24 | sleep 1
25 | done
26 | }
--------------------------------------------------------------------------------
/simple-service/src/main/java/com/ivanfranchin/simpleservice/controller/SimpleServiceController.java:
--------------------------------------------------------------------------------
1 | package com.ivanfranchin.simpleservice.controller;
2 |
3 | import jakarta.servlet.http.HttpServletRequest;
4 | import org.springframework.web.bind.annotation.GetMapping;
5 | import org.springframework.web.bind.annotation.RequestMapping;
6 | import org.springframework.web.bind.annotation.RestController;
7 |
8 | @RestController
9 | @RequestMapping("/api")
10 | public class SimpleServiceController {
11 |
12 | @GetMapping("/public")
13 | public String getPublicString() {
14 | return "It is public.";
15 | }
16 |
17 | @GetMapping("/private")
18 | public String getPrivateString(HttpServletRequest request) {
19 | return request.getHeader("X-Credential-Identifier") + ", it is private.";
20 | }
21 | }
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | org.springframework.boot
7 | spring-boot-starter-parent
8 | 4.0.0
9 |
10 |
11 | com.ivanfranchin
12 | springboot-kong-plugins
13 | 1.0.0
14 | pom
15 | springboot-kong-plugins
16 | Demo project for Spring Boot
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | 25
32 |
33 |
34 | simple-service
35 |
36 |
37 |
--------------------------------------------------------------------------------
/simple-service/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | com.ivanfranchin
7 | springboot-kong-plugins
8 | 1.0.0
9 | ../pom.xml
10 |
11 | simple-service
12 | simple-service
13 | Demo project for Spring Boot
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | org.springframework.boot
30 | spring-boot-starter-actuator
31 |
32 |
33 | org.springframework.boot
34 | spring-boot-starter-webmvc
35 |
36 |
37 |
38 | org.springframework.boot
39 | spring-boot-starter-actuator-test
40 | test
41 |
42 |
43 | org.springframework.boot
44 | spring-boot-starter-webmvc-test
45 | test
46 |
47 |
48 |
49 |
50 |
51 |
52 | org.graalvm.buildtools
53 | native-maven-plugin
54 |
55 |
56 | org.springframework.boot
57 | spring-boot-maven-plugin
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/ldap/ldap-mycompany-com.ldif:
--------------------------------------------------------------------------------
1 | # LDIF Export for dc=mycompany,dc=com
2 | # Server: openldap (openldap)
3 | # Search Scope: sub
4 | # Search Filter: (objectClass=*)
5 | # Total Entries: 10
6 | #
7 | # Generated by phpLDAPadmin (http://phpldapadmin.sourceforge.net) on March 6, 2018 5:56 pm
8 | # Version: 1.2.3
9 |
10 | version: 1
11 |
12 | ## Entry 1: dc=mycompany,dc=com
13 | #dn: dc=mycompany,dc=com
14 | #dc: mycompany
15 | #o: "MyCompany Inc."
16 | #objectclass: top
17 | #objectclass: dcObject
18 | #objectclass: organization
19 |
20 | ## Entry 2: cn=admin,dc=mycompany,dc=com
21 | #dn: cn=admin,dc=mycompany,dc=com
22 | #cn: admin
23 | #description: LDAP administrator
24 | #objectclass: simpleSecurityObject
25 | #objectclass: organizationalRole
26 | #userpassword: {SSHA}iGmwNRRDcGxmafPklI1kcKivNq8cCb4j
27 |
28 | # Entry 3: ou=groups,dc=mycompany,dc=com
29 | dn: ou=groups,dc=mycompany,dc=com
30 | objectclass: organizationalUnit
31 | objectclass: top
32 | ou: groups
33 |
34 | # Entry 4: cn=admin,ou=groups,dc=mycompany,dc=com
35 | dn: cn=admin,ou=groups,dc=mycompany,dc=com
36 | cn: admin
37 | gidnumber: 501
38 | memberuid: 1003
39 | objectclass: posixGroup
40 | objectclass: top
41 |
42 | # Entry 5: cn=developers,ou=groups,dc=mycompany,dc=com
43 | dn: cn=developers,ou=groups,dc=mycompany,dc=com
44 | cn: developers
45 | gidnumber: 500
46 | memberuid: 1000
47 | memberuid: 1001
48 | memberuid: 1002
49 | objectclass: posixGroup
50 | objectclass: top
51 |
52 | # Entry 6: ou=users,dc=mycompany,dc=com
53 | dn: ou=users,dc=mycompany,dc=com
54 | objectclass: organizationalUnit
55 | objectclass: top
56 | ou: users
57 |
58 | # Entry 7: cn=Bill Gates,ou=users,dc=mycompany,dc=com
59 | dn: cn=Bill Gates,ou=users,dc=mycompany,dc=com
60 | cn: Bill Gates
61 | gidnumber: 500
62 | givenname: bill
63 | homedirectory: /home/users/bgates
64 | objectclass: inetOrgPerson
65 | objectclass: posixAccount
66 | objectclass: top
67 | sn: gates
68 | uid: bgates
69 | uidnumber: 1000
70 | userpassword: {MD5}ICy5YqxZB1uWSwcVLSNLcA==
71 |
72 | # Entry 8: cn=Ivan Franchin,ou=users,dc=mycompany,dc=com
73 | dn: cn=Ivan Franchin,ou=users,dc=mycompany,dc=com
74 | cn: Ivan Franchin
75 | gidnumber: 501
76 | givenname: Ivan
77 | homedirectory: /home/users/ifranchin
78 | objectclass: inetOrgPerson
79 | objectclass: posixAccount
80 | objectclass: top
81 | sn: Franchin
82 | uid: ifranchin
83 | uidnumber: 1003
84 | userpassword: {MD5}ICy5YqxZB1uWSwcVLSNLcA==
85 |
86 | # Entry 9: cn=Mark Cuban,ou=users,dc=mycompany,dc=com
87 | dn: cn=Mark Cuban,ou=users,dc=mycompany,dc=com
88 | cn: Mark Cuban
89 | gidnumber: 500
90 | givenname: Mark
91 | homedirectory: /home/users/mcuban
92 | objectclass: inetOrgPerson
93 | objectclass: posixAccount
94 | objectclass: top
95 | sn: Cuban
96 | uid: mcuban
97 | uidnumber: 1002
98 | userpassword: {MD5}ICy5YqxZB1uWSwcVLSNLcA==
99 |
100 | # Entry 10: cn=Steve Jobs,ou=users,dc=mycompany,dc=com
101 | dn: cn=Steve Jobs,ou=users,dc=mycompany,dc=com
102 | cn: Steve Jobs
103 | gidnumber: 500
104 | givenname: Steve
105 | homedirectory: /home/users/sjobs
106 | objectclass: inetOrgPerson
107 | objectclass: posixAccount
108 | objectclass: top
109 | sn: Jobs
110 | uid: sjobs
111 | uidnumber: 1001
112 | userpassword: {MD5}ICy5YqxZB1uWSwcVLSNLcA==
--------------------------------------------------------------------------------
/init-environment.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | SIMPLE_SERVICE_VERSION="1.0.0"
4 | OPENLDAP_VERSION="1.5.0"
5 | PHPLDAPADMIN_VERSION="0.9.0"
6 | POSTGRES_VERSION="18.0"
7 | KONG_VERSION="3.9.1"
8 |
9 | if [[ "$(docker images -q ivanfranchin/simple-service:${SIMPLE_SERVICE_VERSION} 2> /dev/null)" == "" ]] ; then
10 | echo "[WARNING] Before initialize the environment, build the simple-service Docker image: ./docker-build.sh [native]"
11 | exit 1
12 | fi
13 |
14 | source scripts/my-functions.sh
15 |
16 | echo
17 | echo "Starting environment"
18 | echo "===================="
19 |
20 | echo
21 | echo "Creating network"
22 | echo "----------------"
23 | docker network create springboot-kong-net
24 |
25 | echo
26 | echo "Starting simple-service"
27 | echo "-----------------------"
28 | docker run -d \
29 | --name simple-service \
30 | --restart=unless-stopped \
31 | --network=springboot-kong-net \
32 | ivanfranchin/simple-service:${SIMPLE_SERVICE_VERSION}
33 |
34 | echo
35 | echo "Starting openldap"
36 | echo "-----------------"
37 | docker run -d \
38 | --name openldap \
39 | -p 389:389 \
40 | -e "LDAP_ORGANISATION=MyCompany Inc." \
41 | -e "LDAP_DOMAIN=mycompany.com" \
42 | --restart=unless-stopped \
43 | --network=springboot-kong-net \
44 | osixia/openldap:${OPENLDAP_VERSION}
45 |
46 | echo
47 | echo "Starting phpldapadmin"
48 | echo "---------------------"
49 | docker run -d \
50 | --name phpldapadmin \
51 | -p 6443:443 \
52 | -e "PHPLDAPADMIN_LDAP_HOSTS=openldap" \
53 | --restart=unless-stopped \
54 | --network=springboot-kong-net \
55 | osixia/phpldapadmin:${PHPLDAPADMIN_VERSION}
56 |
57 | echo
58 | echo "Starting kong-database"
59 | echo "----------------------"
60 | docker run -d \
61 | --name kong-database \
62 | -p 5432:5432 \
63 | -e "POSTGRES_USER=kong" \
64 | -e "POSTGRES_PASSWORD=kong" \
65 | -e "POSTGRES_DB=kong" \
66 | --restart=unless-stopped \
67 | --network=springboot-kong-net \
68 | postgres:${POSTGRES_VERSION}
69 |
70 | echo
71 | wait_for_container_log "kong-database" "port 5432"
72 |
73 | echo
74 | echo "Running kong-database migration"
75 | echo "-------------------------------"
76 | docker run --rm \
77 | -e "KONG_DATABASE=postgres" \
78 | -e "KONG_PG_HOST=kong-database" \
79 | -e "KONG_PG_PASSWORD=kong" \
80 | --network=springboot-kong-net \
81 | kong:${KONG_VERSION} kong migrations bootstrap
82 |
83 | echo
84 | echo "Starting kong"
85 | echo "-------------"
86 | docker run -d \
87 | --name kong \
88 | -p 8000:8000 \
89 | -p 8443:8443 \
90 | -p 8001:8001 \
91 | -p 8444:8444 \
92 | -e "KONG_DATABASE=postgres" \
93 | -e "KONG_PG_HOST=kong-database" \
94 | -e "KONG_PG_PASSWORD=kong" \
95 | -e "KONG_PROXY_ACCESS_LOG=/dev/stdout" \
96 | -e "KONG_ADMIN_ACCESS_LOG=/dev/stdout" \
97 | -e "KONG_PROXY_ERROR_LOG=/dev/stderr" \
98 | -e "KONG_ADMIN_ERROR_LOG=/dev/stderr" \
99 | -e "KONG_ADMIN_LISTEN=0.0.0.0:8001" \
100 | -e "KONG_ADMIN_LISTEN_SSL=0.0.0.0:8444" \
101 | --restart=unless-stopped \
102 | --network=springboot-kong-net \
103 | kong:${KONG_VERSION}
104 |
105 | echo
106 | wait_for_container_log "kong" "finished preloading"
107 |
108 | echo
109 | wait_for_container_log "simple-service" "Started"
110 |
111 | echo
112 | echo "Environment Up and Running"
113 | echo "=========================="
114 | echo
115 |
--------------------------------------------------------------------------------
/mvnw.cmd:
--------------------------------------------------------------------------------
1 | <# : batch portion
2 | @REM ----------------------------------------------------------------------------
3 | @REM Licensed to the Apache Software Foundation (ASF) under one
4 | @REM or more contributor license agreements. See the NOTICE file
5 | @REM distributed with this work for additional information
6 | @REM regarding copyright ownership. The ASF licenses this file
7 | @REM to you under the Apache License, Version 2.0 (the
8 | @REM "License"); you may not use this file except in compliance
9 | @REM with the License. You may obtain a copy of the License at
10 | @REM
11 | @REM http://www.apache.org/licenses/LICENSE-2.0
12 | @REM
13 | @REM Unless required by applicable law or agreed to in writing,
14 | @REM software distributed under the License is distributed on an
15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | @REM KIND, either express or implied. See the License for the
17 | @REM specific language governing permissions and limitations
18 | @REM under the License.
19 | @REM ----------------------------------------------------------------------------
20 |
21 | @REM ----------------------------------------------------------------------------
22 | @REM Apache Maven Wrapper startup batch script, version 3.3.4
23 | @REM
24 | @REM Optional ENV vars
25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution
26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
28 | @REM ----------------------------------------------------------------------------
29 |
30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
31 | @SET __MVNW_CMD__=
32 | @SET __MVNW_ERROR__=
33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
34 | @SET PSModulePath=
35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
37 | )
38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
39 | @SET __MVNW_PSMODULEP_SAVE=
40 | @SET __MVNW_ARG0_NAME__=
41 | @SET MVNW_USERNAME=
42 | @SET MVNW_PASSWORD=
43 | @IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
44 | @echo Cannot start maven from wrapper >&2 && exit /b 1
45 | @GOTO :EOF
46 | : end batch / begin powershell #>
47 |
48 | $ErrorActionPreference = "Stop"
49 | if ($env:MVNW_VERBOSE -eq "true") {
50 | $VerbosePreference = "Continue"
51 | }
52 |
53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
55 | if (!$distributionUrl) {
56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
57 | }
58 |
59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
60 | "maven-mvnd-*" {
61 | $USE_MVND = $true
62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
63 | $MVN_CMD = "mvnd.cmd"
64 | break
65 | }
66 | default {
67 | $USE_MVND = $false
68 | $MVN_CMD = $script -replace '^mvnw','mvn'
69 | break
70 | }
71 | }
72 |
73 | # apply MVNW_REPOURL and calculate MAVEN_HOME
74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
75 | if ($env:MVNW_REPOURL) {
76 | $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
78 | }
79 | $distributionUrlName = $distributionUrl -replace '^.*/',''
80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
81 |
82 | $MAVEN_M2_PATH = "$HOME/.m2"
83 | if ($env:MAVEN_USER_HOME) {
84 | $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
85 | }
86 |
87 | if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
88 | New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
89 | }
90 |
91 | $MAVEN_WRAPPER_DISTS = $null
92 | if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
93 | $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
94 | } else {
95 | $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
96 | }
97 |
98 | $MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
99 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
100 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
101 |
102 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
103 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
104 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
105 | exit $?
106 | }
107 |
108 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
109 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
110 | }
111 |
112 | # prepare tmp dir
113 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
114 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
115 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
116 | trap {
117 | if ($TMP_DOWNLOAD_DIR.Exists) {
118 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
119 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
120 | }
121 | }
122 |
123 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
124 |
125 | # Download and Install Apache Maven
126 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
127 | Write-Verbose "Downloading from: $distributionUrl"
128 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
129 |
130 | $webclient = New-Object System.Net.WebClient
131 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
132 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
133 | }
134 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
135 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
136 |
137 | # If specified, validate the SHA-256 sum of the Maven distribution zip file
138 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
139 | if ($distributionSha256Sum) {
140 | if ($USE_MVND) {
141 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
142 | }
143 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
144 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
145 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
146 | }
147 | }
148 |
149 | # unzip and move
150 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
151 |
152 | # Find the actual extracted directory name (handles snapshots where filename != directory name)
153 | $actualDistributionDir = ""
154 |
155 | # First try the expected directory name (for regular distributions)
156 | $expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
157 | $expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
158 | if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
159 | $actualDistributionDir = $distributionUrlNameMain
160 | }
161 |
162 | # If not found, search for any directory with the Maven executable (for snapshots)
163 | if (!$actualDistributionDir) {
164 | Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
165 | $testPath = Join-Path $_.FullName "bin/$MVN_CMD"
166 | if (Test-Path -Path $testPath -PathType Leaf) {
167 | $actualDistributionDir = $_.Name
168 | }
169 | }
170 | }
171 |
172 | if (!$actualDistributionDir) {
173 | Write-Error "Could not find Maven distribution directory in extracted archive"
174 | }
175 |
176 | Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
177 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
178 | try {
179 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
180 | } catch {
181 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
182 | Write-Error "fail to move MAVEN_HOME"
183 | }
184 | } finally {
185 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
186 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
187 | }
188 |
189 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
190 |
--------------------------------------------------------------------------------
/mvnw:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # ----------------------------------------------------------------------------
3 | # Licensed to the Apache Software Foundation (ASF) under one
4 | # or more contributor license agreements. See the NOTICE file
5 | # distributed with this work for additional information
6 | # regarding copyright ownership. The ASF licenses this file
7 | # to you under the Apache License, Version 2.0 (the
8 | # "License"); you may not use this file except in compliance
9 | # with the License. You may obtain a copy of the License at
10 | #
11 | # http://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing,
14 | # software distributed under the License is distributed on an
15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | # KIND, either express or implied. See the License for the
17 | # specific language governing permissions and limitations
18 | # under the License.
19 | # ----------------------------------------------------------------------------
20 |
21 | # ----------------------------------------------------------------------------
22 | # Apache Maven Wrapper startup batch script, version 3.3.4
23 | #
24 | # Optional ENV vars
25 | # -----------------
26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source
27 | # MVNW_REPOURL - repo url base for downloading maven distribution
28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
30 | # ----------------------------------------------------------------------------
31 |
32 | set -euf
33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x
34 |
35 | # OS specific support.
36 | native_path() { printf %s\\n "$1"; }
37 | case "$(uname)" in
38 | CYGWIN* | MINGW*)
39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
40 | native_path() { cygpath --path --windows "$1"; }
41 | ;;
42 | esac
43 |
44 | # set JAVACMD and JAVACCMD
45 | set_java_home() {
46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
47 | if [ -n "${JAVA_HOME-}" ]; then
48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then
49 | # IBM's JDK on AIX uses strange locations for the executables
50 | JAVACMD="$JAVA_HOME/jre/sh/java"
51 | JAVACCMD="$JAVA_HOME/jre/sh/javac"
52 | else
53 | JAVACMD="$JAVA_HOME/bin/java"
54 | JAVACCMD="$JAVA_HOME/bin/javac"
55 |
56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
59 | return 1
60 | fi
61 | fi
62 | else
63 | JAVACMD="$(
64 | 'set' +e
65 | 'unset' -f command 2>/dev/null
66 | 'command' -v java
67 | )" || :
68 | JAVACCMD="$(
69 | 'set' +e
70 | 'unset' -f command 2>/dev/null
71 | 'command' -v javac
72 | )" || :
73 |
74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
76 | return 1
77 | fi
78 | fi
79 | }
80 |
81 | # hash string like Java String::hashCode
82 | hash_string() {
83 | str="${1:-}" h=0
84 | while [ -n "$str" ]; do
85 | char="${str%"${str#?}"}"
86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
87 | str="${str#?}"
88 | done
89 | printf %x\\n $h
90 | }
91 |
92 | verbose() { :; }
93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
94 |
95 | die() {
96 | printf %s\\n "$1" >&2
97 | exit 1
98 | }
99 |
100 | trim() {
101 | # MWRAPPER-139:
102 | # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
103 | # Needed for removing poorly interpreted newline sequences when running in more
104 | # exotic environments such as mingw bash on Windows.
105 | printf "%s" "${1}" | tr -d '[:space:]'
106 | }
107 |
108 | scriptDir="$(dirname "$0")"
109 | scriptName="$(basename "$0")"
110 |
111 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
112 | while IFS="=" read -r key value; do
113 | case "${key-}" in
114 | distributionUrl) distributionUrl=$(trim "${value-}") ;;
115 | distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
116 | esac
117 | done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
118 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
119 |
120 | case "${distributionUrl##*/}" in
121 | maven-mvnd-*bin.*)
122 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
123 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
124 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
125 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
126 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
127 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;;
128 | *)
129 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
130 | distributionPlatform=linux-amd64
131 | ;;
132 | esac
133 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
134 | ;;
135 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
136 | *) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
137 | esac
138 |
139 | # apply MVNW_REPOURL and calculate MAVEN_HOME
140 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
141 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
142 | distributionUrlName="${distributionUrl##*/}"
143 | distributionUrlNameMain="${distributionUrlName%.*}"
144 | distributionUrlNameMain="${distributionUrlNameMain%-bin}"
145 | MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
146 | MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
147 |
148 | exec_maven() {
149 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
150 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
151 | }
152 |
153 | if [ -d "$MAVEN_HOME" ]; then
154 | verbose "found existing MAVEN_HOME at $MAVEN_HOME"
155 | exec_maven "$@"
156 | fi
157 |
158 | case "${distributionUrl-}" in
159 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
160 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
161 | esac
162 |
163 | # prepare tmp dir
164 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
165 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
166 | trap clean HUP INT TERM EXIT
167 | else
168 | die "cannot create temp dir"
169 | fi
170 |
171 | mkdir -p -- "${MAVEN_HOME%/*}"
172 |
173 | # Download and Install Apache Maven
174 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
175 | verbose "Downloading from: $distributionUrl"
176 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
177 |
178 | # select .zip or .tar.gz
179 | if ! command -v unzip >/dev/null; then
180 | distributionUrl="${distributionUrl%.zip}.tar.gz"
181 | distributionUrlName="${distributionUrl##*/}"
182 | fi
183 |
184 | # verbose opt
185 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
186 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
187 |
188 | # normalize http auth
189 | case "${MVNW_PASSWORD:+has-password}" in
190 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
191 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
192 | esac
193 |
194 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
195 | verbose "Found wget ... using wget"
196 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
197 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
198 | verbose "Found curl ... using curl"
199 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
200 | elif set_java_home; then
201 | verbose "Falling back to use Java to download"
202 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
203 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
204 | cat >"$javaSource" <<-END
205 | public class Downloader extends java.net.Authenticator
206 | {
207 | protected java.net.PasswordAuthentication getPasswordAuthentication()
208 | {
209 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
210 | }
211 | public static void main( String[] args ) throws Exception
212 | {
213 | setDefault( new Downloader() );
214 | java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
215 | }
216 | }
217 | END
218 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java
219 | verbose " - Compiling Downloader.java ..."
220 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
221 | verbose " - Running Downloader.java ..."
222 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
223 | fi
224 |
225 | # If specified, validate the SHA-256 sum of the Maven distribution zip file
226 | if [ -n "${distributionSha256Sum-}" ]; then
227 | distributionSha256Result=false
228 | if [ "$MVN_CMD" = mvnd.sh ]; then
229 | echo "Checksum validation is not supported for maven-mvnd." >&2
230 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
231 | exit 1
232 | elif command -v sha256sum >/dev/null; then
233 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
234 | distributionSha256Result=true
235 | fi
236 | elif command -v shasum >/dev/null; then
237 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
238 | distributionSha256Result=true
239 | fi
240 | else
241 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
242 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
243 | exit 1
244 | fi
245 | if [ $distributionSha256Result = false ]; then
246 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
247 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
248 | exit 1
249 | fi
250 | fi
251 |
252 | # unzip and move
253 | if command -v unzip >/dev/null; then
254 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
255 | else
256 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
257 | fi
258 |
259 | # Find the actual extracted directory name (handles snapshots where filename != directory name)
260 | actualDistributionDir=""
261 |
262 | # First try the expected directory name (for regular distributions)
263 | if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
264 | if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
265 | actualDistributionDir="$distributionUrlNameMain"
266 | fi
267 | fi
268 |
269 | # If not found, search for any directory with the Maven executable (for snapshots)
270 | if [ -z "$actualDistributionDir" ]; then
271 | # enable globbing to iterate over items
272 | set +f
273 | for dir in "$TMP_DOWNLOAD_DIR"/*; do
274 | if [ -d "$dir" ]; then
275 | if [ -f "$dir/bin/$MVN_CMD" ]; then
276 | actualDistributionDir="$(basename "$dir")"
277 | break
278 | fi
279 | fi
280 | done
281 | set -f
282 | fi
283 |
284 | if [ -z "$actualDistributionDir" ]; then
285 | verbose "Contents of $TMP_DOWNLOAD_DIR:"
286 | verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
287 | die "Could not find Maven distribution directory in extracted archive"
288 | fi
289 |
290 | verbose "Found extracted Maven distribution directory: $actualDistributionDir"
291 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
292 | mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
293 |
294 | clean || :
295 | exec_maven "$@"
296 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # springboot-kong-plugins
2 |
3 | The goal of this project is to create a simple [`Spring Boot`](https://docs.spring.io/spring-boot/index.html) REST API and secure it with [`Kong`](https://konghq.com/kong/) using the `LDAP Authentication` and `Basic Authentication` plugins. Besides, we will explore more plugins that `Kong` offers, such as the `Rate Limiting` and `Prometheus` plugins.
4 |
5 | ## Proof-of-Concepts & Articles
6 |
7 | On [ivangfr.github.io](https://ivangfr.github.io), I have compiled my Proof-of-Concepts (PoCs) and articles. You can easily search for the technology you are interested in by using the filter. Who knows, perhaps I have already implemented a PoC or written an article about what you are looking for.
8 |
9 | ## Additional Readings
10 |
11 | - \[**Medium**\] [**Using Kong to secure a Simple Spring Boot REST API with Basic Authentication plugin**](https://medium.com/@ivangfr/using-kong-to-secure-a-simple-spring-boot-rest-api-with-basic-authentication-plugin-90f3529043f3)
12 | - \[**Medium**\] [**Using Kong to secure a Simple Spring Boot REST API with LDAP Authentication plugin**](https://medium.com/@ivangfr/using-kong-to-secure-a-simple-spring-boot-rest-api-with-ldap-authentication-plugin-3a499e01382a)
13 | - \[**Medium**\] [**Using Kong to configure Rate Limiting to a Simple Spring Boot REST API**](https://medium.com/@ivangfr/using-kong-to-configure-rate-limiting-to-a-simple-spring-boot-rest-api-33b1899077d)
14 | - \[**Medium**\] [**Using Kong to secure a Simple Spring Boot REST API with Kong OIDC plugin and Keycloak**](https://medium.com/@ivangfr/using-kong-to-secure-a-simple-spring-boot-rest-api-with-kong-oidc-plugin-and-keycloak-c8fa8de32e6e)
15 |
16 | ## Project Diagram
17 |
18 | 
19 |
20 | ## Application
21 |
22 | - ### simple-service
23 |
24 | `Spring Boot` Java Web application that exposes two endpoints:
25 | - `/api/public`: that can be accessed by anyone; it is not secured.
26 | - `/api/private`: that must be accessed only by authenticated users.
27 |
28 | ## Prerequisites
29 |
30 | - [`Java 25`](https://www.oracle.com/java/technologies/downloads/#java25) or higher.
31 | - A containerization tool (e.g., [`Docker`](https://www.docker.com), [`Podman`](https://podman.io), etc.)
32 |
33 | ## Run application during development using Maven
34 |
35 | - Open a terminal and navigate to the `springboot-kong-plugins` root folder.
36 |
37 | - Run the command below to start:
38 | ```bash
39 | ./mvnw clean spring-boot:run --projects simple-service
40 | ```
41 |
42 | - Open another terminal and call the application endpoints:
43 | ```bash
44 | curl -i localhost:8080/api/public
45 | curl -i localhost:8080/api/private
46 | curl -i localhost:8080/actuator/beans
47 | curl -i localhost:8080/actuator/health
48 | ```
49 |
50 | - To stop, go to the terminal where the application is running and press `Ctrl+C`.
51 |
52 | ## Build application Docker Image
53 |
54 | - In a terminal, make sure you are in the `springboot-kong-plugins` root folder.
55 |
56 | - Build Docker Image
57 | - JVM
58 | ```bash
59 | ./build-docker-images.sh
60 | ```
61 | - Native
62 | ```bash
63 | ./build-docker-images.sh native
64 | ```
65 |
66 | ## Test application Docker Image
67 |
68 | - In a terminal, run the following command:
69 | ```bash
70 | docker run --rm -p 8080:8080 --name simple-service ivanfranchin/simple-service:1.0.0
71 | ```
72 |
73 | - Open another terminal and call the application endpoints:
74 | ```bash
75 | curl -i localhost:8080/api/public
76 | curl -i localhost:8080/api/private
77 | curl -i localhost:8080/actuator/beans
78 | curl -i localhost:8080/actuator/health
79 | ```
80 |
81 | - To stop, go to the terminal where the application is running and press `Ctrl+C`.
82 |
83 | ## Initialize Environment
84 |
85 | - In a terminal, make sure you are in the `springboot-kong-plugins` root folder.
86 |
87 | - Run the following script:
88 | ```bash
89 | ./init-environment.sh
90 | ```
91 | > **Note**: `simple-service` application is running as a Docker container. The container does not expose any port to the HOST machine. So, it cannot be accessed directly, forcing the caller to use `Kong` as gateway server in order to access it.
92 |
93 | ## Import OpenLDAP Users
94 |
95 | The `LDIF` file that we will use, `ldap/ldap-mycompany-com.ldif`, has already a pre-defined structure for `mycompany.com`. Basically, it has 2 groups (`developers` and `admin`) and 4 users (`Bill Gates`, `Steve Jobs`, `Mark Cuban` and `Ivan Franchin`). Besides, it is defined that `Bill Gates`, `Steve Jobs` and `Mark Cuban` belong to the `developers` group, and `Ivan Franchin` belongs to the `admin` group.
96 |
97 | ```text
98 | Bill Gates > username: bgates, password: 123
99 | Steve Jobs > username: sjobs, password: 123
100 | Mark Cuban > username: mcuban, password: 123
101 | Ivan Franchin > username: ifranchin, password: 123
102 | ```
103 |
104 | There are two ways to import those users: by running a script or using `phpldapadmin`.
105 |
106 | ### Import users running a script
107 |
108 | - In another terminal, make sure you are in the `springboot-kong-plugins` root folder.
109 |
110 | - Run the following script:
111 | ```bash
112 | ./import-openldap-users.sh
113 | ```
114 |
115 | - Check users imported using [`ldapsearch`](https://linux.die.net/man/1/ldapsearch):
116 | ```bash
117 | ldapsearch -x -D "cn=admin,dc=mycompany,dc=com" \
118 | -w admin -H ldap://localhost:389 \
119 | -b "ou=users,dc=mycompany,dc=com" \
120 | -s sub "(uid=*)"
121 | ```
122 |
123 | ### Import users using phpldapadmin
124 |
125 | - Access https://localhost:6443
126 |
127 | - Login with the credentials:
128 | ```text
129 | Login DN: cn=admin,dc=mycompany,dc=com
130 | Password: admin
131 | ```
132 |
133 | - Import the file `ldap/ldap-mycompany-com.ldif`.
134 |
135 | - You should see something like:
136 |
137 | 
138 |
139 | ## Kong
140 |
141 | In a terminal, follow the steps below to configure `Kong`.
142 |
143 | ### Check Status
144 |
145 | - Before starting, check if `Kong` admin API is accessible:
146 | ```bash
147 | curl -I http://localhost:8001
148 | ```
149 |
150 | It should return:
151 | ```text
152 | HTTP/1.1 200 OK
153 | ```
154 |
155 | ### Add Service
156 |
157 | 1. The following call will add the `simple-service` service:
158 | ```bash
159 | curl -i -X POST http://localhost:8001/services \
160 | -d "name=simple-service" \
161 | -d "protocol=http" \
162 | -d "host=simple-service" \
163 | -d "port=8080"
164 | ```
165 |
166 | 2. \[Optional\] To list all services run:
167 | ```bash
168 | curl -i http://localhost:8001/services
169 | ```
170 |
171 | ### Add routes
172 |
173 | 1. One default route for the service, no specific `path` included:
174 | ```bash
175 | curl -i -X POST http://localhost:8001/services/simple-service/routes \
176 | -d "name=simple-service-default" \
177 | -d "protocols[]=http" \
178 | -d "hosts[]=simple-service"
179 | ```
180 |
181 | 2. Another route specifically for `/api/private` endpoint (it will be secured and only accessible by LDAP users):
182 | ```bash
183 | curl -i -X POST http://localhost:8001/services/simple-service/routes \
184 | -d "name=simple-service-api-private" \
185 | -d "protocols[]=http" \
186 | -d "hosts[]=simple-service" \
187 | -d "paths[]=/api/private" \
188 | -d "strip_path=false"
189 | ```
190 |
191 | 3. Finally, one route for `/actuator/beans` endpoint (it will be secured and only accessible by pre-defined users):
192 | ```bash
193 | curl -i -X POST http://localhost:8001/services/simple-service/routes \
194 | -d "name=simple-service-actuator-beans" \
195 | -d "protocols[]=http" \
196 | -d "hosts[]=simple-service" \
197 | -d "paths[]=/actuator/beans" \
198 | -d "strip_path=false"
199 | ```
200 |
201 | 4. \[Optional\] To list all `simple-service` routes run:
202 | ```bash
203 | curl -i http://localhost:8001/services/simple-service/routes
204 | ```
205 |
206 | ### Call the endpoints
207 |
208 | 1. Call the `/api/public` endpoint:
209 | ```bash
210 | curl -i http://localhost:8000/api/public -H 'Host: simple-service'
211 | ```
212 |
213 | It should return:
214 | ```text
215 | HTTP/1.1 200
216 | It is public.
217 | ```
218 |
219 | 2. Call the `/api/private` endpoint:
220 | ```bash
221 | curl -i http://localhost:8000/api/private -H 'Host: simple-service'
222 | ```
223 |
224 | It should return:
225 | ```text
226 | HTTP/1.1 200
227 | null, it is private.
228 | ```
229 |
230 | > **Note**: This endpoint is not secured by the application, that is why the response is returned. The idea is to use `Kong` to secure it. It will be done on the next steps.
231 |
232 | 3. Call the `/actuator/beans` endpoint:
233 | ```bash
234 | curl -i http://localhost:8000/actuator/beans -H 'Host: simple-service'
235 | ```
236 |
237 | It should return:
238 | ```text
239 | HTTP/1.1 200
240 | {"contexts":{"simple-service":{"beans":...
241 | ```
242 |
243 | > **Note**: As happened previously with `/api/private`, `/actuator/beans` endpoint is not secured by the application. We will use `Kong` to secure it on the next steps.
244 |
245 | ## Plugins
246 |
247 | In this project, we are going to add these plugins: `LDAP Authentication`, `Basic Authentication`, `Rate Limiting` and `Prometheus`. Please refer to https://konghq.com/plugins for more.
248 |
249 | ### Add LDAP Authentication plugin
250 |
251 | The `LDAP Authentication` plugin will be used to secure the `/api/private` endpoint.
252 |
253 | 1. Add plugin to route `simple-service-api-private`:
254 | ```bash
255 | curl -i -X POST http://localhost:8001/routes/simple-service-api-private/plugins \
256 | -d "name=ldap-auth" \
257 | -d "config.hide_credentials=true" \
258 | -d "config.ldap_host=openldap" \
259 | -d "config.ldap_port=389" \
260 | -d "config.start_tls=false" \
261 | -d "config.base_dn=ou=users,dc=mycompany,dc=com" \
262 | -d "config.verify_ldap_host=false" \
263 | -d "config.attribute=cn" \
264 | -d "config.cache_ttl=60" \
265 | -d "config.header_type=ldap"
266 | ```
267 |
268 | 2. Try to call the `/api/private` endpoint without credentials:
269 | ```bash
270 | curl -i http://localhost:8000/api/private -H 'Host: simple-service'
271 | ```
272 |
273 | It should return:
274 | ```text
275 | HTTP/1.1 401 Unauthorized
276 | {
277 | "message":"Unauthorized",
278 | "request_id":"..."
279 | }
280 | ```
281 |
282 | 3. Call the `/api/private` endpoint using Bill Gates base64-encoded credentials:
283 | ```bash
284 | curl -i http://localhost:8000/api/private \
285 | -H "Authorization:ldap $(echo -n 'Bill Gates':123 | base64)" \
286 | -H 'Host: simple-service'
287 | ```
288 |
289 | It should return:
290 | ```text
291 | HTTP/1.1 200
292 | Bill Gates, it is private.
293 | ```
294 |
295 | ### Add Basic Authentication plugin
296 |
297 | The `Basic Authentication` plugin will be used to secure the `/actuator/beans` endpoint.
298 |
299 | 1. Add plugin to route `simple-service-actuator-beans`:
300 | ```bash
301 | curl -i -X POST http://localhost:8001/routes/simple-service-actuator-beans/plugins \
302 | -d "name=basic-auth" \
303 | -d "config.hide_credentials=true"
304 | ```
305 |
306 | 2. Try to call the `/actuator/beans` endpoint without credentials:
307 | ```bash
308 | curl -i http://localhost:8000/actuator/beans -H 'Host: simple-service'
309 | ```
310 |
311 | It should return:
312 | ```text
313 | HTTP/1.1 401 Unauthorized
314 | {
315 | "message":"Unauthorized",
316 | "request_id":"..."
317 | }
318 | ```
319 |
320 | 3. Create a consumer:
321 | ```bash
322 | curl -i -X POST http://localhost:8001/consumers -d "username=ivanfranchin"
323 | ```
324 |
325 | 4. Create a credential for consumer:
326 | ```bash
327 | curl -i -X POST http://localhost:8001/consumers/ivanfranchin/basic-auth \
328 | -d "username=ivan.franchin" \
329 | -d "password=123"
330 | ```
331 |
332 | 5. Call the `/api/private` endpoint using `ivan.franchin` credentials:
333 | ```bash
334 | curl -i -u ivan.franchin:123 http://localhost:8000/actuator/beans -H 'Host: simple-service'
335 | ```
336 |
337 | It should return:
338 | ```text
339 | HTTP/1.1 200
340 | {"contentDescriptor":{"providerVersion":...
341 | ```
342 |
343 | 6. Let's create another consumer just for testing purposes:
344 | ```bash
345 | curl -i -X POST http://localhost:8001/consumers -d "username=administrator"
346 |
347 | curl -i -X POST http://localhost:8001/consumers/administrator/basic-auth \
348 | -d "username=administrator" \
349 | -d "password=123"
350 | ```
351 |
352 | ### Add Rate Limiting plugin
353 |
354 | We are going to add the following rate limits:
355 | - `/api/public` and `/actuator/health`: one request per second.
356 | - `/api/private`: five requests a minute.
357 | - `/actuator/beans`: two requests a minute or 100 requests an hour.
358 |
359 | 1. Add plugin to route `simple-service-default`:
360 | ```bash
361 | curl -i -X POST http://localhost:8001/routes/simple-service-default/plugins \
362 | -d "name=rate-limiting" \
363 | -d "config.second=1"
364 | ```
365 |
366 | 2. Add plugin to route `simple-service-api-private`:
367 | ```bash
368 | curl -i -X POST http://localhost:8001/routes/simple-service-api-private/plugins \
369 | -d "name=rate-limiting" \
370 | -d "config.minute=5"
371 | ```
372 |
373 | 3. Add plugin to route `simple-service-actuator-beans`:
374 | ```bash
375 | curl -i -X POST http://localhost:8001/routes/simple-service-actuator-beans/plugins \
376 | -d "name=rate-limiting" \
377 | -d "config.minute=2" \
378 | -d "config.hour=100"
379 | ```
380 |
381 | 4. Make some calls to these endpoints.
382 |
383 | - Test `/api/public`:
384 | ```bash
385 | curl -i http://localhost:8000/api/public -H 'Host: simple-service'
386 | curl -i http://localhost:8000/actuator/health -H 'Host: simple-service'
387 | ```
388 |
389 | - Test `/actuator/beans`:
390 | ```bash
391 | curl -i -u ivan.franchin:123 http://localhost:8000/actuator/beans -H 'Host: simple-service'
392 | curl -i -u administrator:123 http://localhost:8000/actuator/beans -H 'Host: simple-service'
393 | ```
394 |
395 | - Test `/api/private`:
396 | ```bash
397 | curl -i http://localhost:8000/api/private \
398 | -H "Authorization:ldap $(echo -n 'Bill Gates':123 | base64)" \
399 | -H 'Host: simple-service'
400 |
401 | curl -i http://localhost:8000/api/private \
402 | -H "Authorization:ldap $(echo -n 'Mark Cuban':123 | base64)" \
403 | -H 'Host: simple-service'
404 | ```
405 |
406 | 5. After exceeding some calls in a minute, you should see:
407 | ```text
408 | HTTP/1.1 429 Too Many Requests
409 | {
410 | "message":"API rate limit exceeded",
411 | "request_id":"..."
412 | }
413 | ```
414 |
415 | ### Add Prometheus plugin
416 |
417 | 1. Add plugin to `simple-service`:
418 | ```bash
419 | curl -i -X POST http://localhost:8001/services/simple-service/plugins -d "name=prometheus"
420 | ```
421 |
422 | 2. Make some requests to `simple-service` endpoints.
423 |
424 | 3. You can see some metrics:
425 | ```bash
426 | curl -i http://localhost:8001/metrics
427 | ```
428 |
429 | ## Shutdown
430 |
431 | In a terminal and, inside the `springboot-kong-plugins` root folder, run the following script:
432 | ```bash
433 | ./shutdown-environment.sh
434 | ```
435 |
436 | ## Cleanup
437 |
438 | To remove the Docker image created by this project, go to a terminal and, inside the `springboot-kong-plugins` root folder, run the script below:
439 | ```bash
440 | ./remove-docker-images.sh
441 | ```
442 |
--------------------------------------------------------------------------------
/documentation/project-diagram.excalidraw:
--------------------------------------------------------------------------------
1 | {
2 | "type": "excalidraw",
3 | "version": 2,
4 | "source": "https://excalidraw.com",
5 | "elements": [
6 | {
7 | "type": "rectangle",
8 | "version": 3261,
9 | "versionNonce": 1471067378,
10 | "isDeleted": false,
11 | "id": "NKmNZxYxWMCKh3prRiPwX",
12 | "fillStyle": "hachure",
13 | "strokeWidth": 1,
14 | "strokeStyle": "solid",
15 | "roughness": 1,
16 | "opacity": 100,
17 | "angle": 0,
18 | "x": 368.1188471829882,
19 | "y": -3.567554579105547,
20 | "strokeColor": "#000000",
21 | "backgroundColor": "#228be6",
22 | "width": 209.18356323242188,
23 | "height": 99.67071533203125,
24 | "seed": 1122908819,
25 | "groupIds": [],
26 | "frameId": null,
27 | "roundness": {
28 | "type": 3
29 | },
30 | "boundElements": [
31 | {
32 | "type": "text",
33 | "id": "GrVT2PZuYu1cRv3sXwFoI"
34 | },
35 | {
36 | "id": "e41iX43mW9vtzgsSg3s9a",
37 | "type": "arrow"
38 | },
39 | {
40 | "id": "cA-77Pr2VQvDPRulaOUIC",
41 | "type": "arrow"
42 | }
43 | ],
44 | "updated": 1688485436165,
45 | "link": null,
46 | "locked": false
47 | },
48 | {
49 | "type": "text",
50 | "version": 2215,
51 | "versionNonce": 1352954034,
52 | "isDeleted": false,
53 | "id": "GrVT2PZuYu1cRv3sXwFoI",
54 | "fillStyle": "hachure",
55 | "strokeWidth": 1,
56 | "strokeStyle": "solid",
57 | "roughness": 0,
58 | "opacity": 100,
59 | "angle": 0,
60 | "x": 409.8786662137499,
61 | "y": 29.46780308691008,
62 | "strokeColor": "#000000",
63 | "backgroundColor": "transparent",
64 | "width": 125.66392517089844,
65 | "height": 33.6,
66 | "seed": 294979727,
67 | "groupIds": [],
68 | "frameId": null,
69 | "roundness": null,
70 | "boundElements": [],
71 | "updated": 1688485412964,
72 | "link": null,
73 | "locked": false,
74 | "fontSize": 28,
75 | "fontFamily": 1,
76 | "text": "simple-api",
77 | "textAlign": "center",
78 | "verticalAlign": "middle",
79 | "containerId": "NKmNZxYxWMCKh3prRiPwX",
80 | "originalText": "simple-api",
81 | "lineHeight": 1.2,
82 | "baseline": 24
83 | },
84 | {
85 | "type": "ellipse",
86 | "version": 2406,
87 | "versionNonce": 762232558,
88 | "isDeleted": false,
89 | "id": "zYllgBlgP7S7-phqNnnEr",
90 | "fillStyle": "hachure",
91 | "strokeWidth": 2,
92 | "strokeStyle": "solid",
93 | "roughness": 1,
94 | "opacity": 100,
95 | "angle": 6.272333650882224,
96 | "x": -498.58298558848367,
97 | "y": -5.525828645617317,
98 | "strokeColor": "#000000",
99 | "backgroundColor": "transparent",
100 | "width": 26.930389404296875,
101 | "height": 27.545562744140625,
102 | "seed": 1024421939,
103 | "groupIds": [
104 | "4D1ojplACrlVIaNZ7P0FH"
105 | ],
106 | "frameId": null,
107 | "roundness": {
108 | "type": 2
109 | },
110 | "boundElements": [],
111 | "updated": 1688485467337,
112 | "link": null,
113 | "locked": false
114 | },
115 | {
116 | "type": "line",
117 | "version": 2424,
118 | "versionNonce": 890466034,
119 | "isDeleted": false,
120 | "id": "iW_3iMfYwsgECYYtnEK33",
121 | "fillStyle": "hachure",
122 | "strokeWidth": 2,
123 | "strokeStyle": "solid",
124 | "roughness": 1,
125 | "opacity": 100,
126 | "angle": 6.272333650882224,
127 | "x": -486.77159404522524,
128 | "y": 22.49735448920915,
129 | "strokeColor": "#000000",
130 | "backgroundColor": "transparent",
131 | "width": 0.473419189453125,
132 | "height": 40.3687744140625,
133 | "seed": 1958277587,
134 | "groupIds": [
135 | "4D1ojplACrlVIaNZ7P0FH"
136 | ],
137 | "frameId": null,
138 | "roundness": {
139 | "type": 2
140 | },
141 | "boundElements": [],
142 | "updated": 1688485467337,
143 | "link": null,
144 | "locked": false,
145 | "startBinding": null,
146 | "endBinding": null,
147 | "lastCommittedPoint": null,
148 | "startArrowhead": null,
149 | "endArrowhead": null,
150 | "points": [
151 | [
152 | 0,
153 | 0
154 | ],
155 | [
156 | -0.473419189453125,
157 | 40.3687744140625
158 | ]
159 | ]
160 | },
161 | {
162 | "type": "line",
163 | "version": 2375,
164 | "versionNonce": 1800712494,
165 | "isDeleted": false,
166 | "id": "8JTNvN86yjteIVYunsqxA",
167 | "fillStyle": "hachure",
168 | "strokeWidth": 2,
169 | "strokeStyle": "solid",
170 | "roughness": 1,
171 | "opacity": 100,
172 | "angle": 6.272333650882224,
173 | "x": -486.6479773937564,
174 | "y": 64.3927683736398,
175 | "strokeColor": "#000000",
176 | "backgroundColor": "transparent",
177 | "width": 17.21380615234375,
178 | "height": 33.91400146484375,
179 | "seed": 721502067,
180 | "groupIds": [
181 | "4D1ojplACrlVIaNZ7P0FH"
182 | ],
183 | "frameId": null,
184 | "roundness": {
185 | "type": 2
186 | },
187 | "boundElements": [],
188 | "updated": 1688485467337,
189 | "link": null,
190 | "locked": false,
191 | "startBinding": null,
192 | "endBinding": null,
193 | "lastCommittedPoint": null,
194 | "startArrowhead": null,
195 | "endArrowhead": null,
196 | "points": [
197 | [
198 | 0,
199 | 0
200 | ],
201 | [
202 | -17.21380615234375,
203 | 33.91400146484375
204 | ]
205 | ]
206 | },
207 | {
208 | "type": "line",
209 | "version": 2394,
210 | "versionNonce": 644661426,
211 | "isDeleted": false,
212 | "id": "hMliKg9HCYu-lEplUg1Y6",
213 | "fillStyle": "hachure",
214 | "strokeWidth": 2,
215 | "strokeStyle": "solid",
216 | "roughness": 1,
217 | "opacity": 100,
218 | "angle": 6.272333650882224,
219 | "x": -486.5249350420221,
220 | "y": 64.34802718106147,
221 | "strokeColor": "#000000",
222 | "backgroundColor": "transparent",
223 | "width": 12.9422607421875,
224 | "height": 35.16510009765625,
225 | "seed": 976148755,
226 | "groupIds": [
227 | "4D1ojplACrlVIaNZ7P0FH"
228 | ],
229 | "frameId": null,
230 | "roundness": {
231 | "type": 2
232 | },
233 | "boundElements": [],
234 | "updated": 1688485467337,
235 | "link": null,
236 | "locked": false,
237 | "startBinding": null,
238 | "endBinding": null,
239 | "lastCommittedPoint": null,
240 | "startArrowhead": null,
241 | "endArrowhead": null,
242 | "points": [
243 | [
244 | 0,
245 | 0
246 | ],
247 | [
248 | 12.9422607421875,
249 | 35.16510009765625
250 | ]
251 | ]
252 | },
253 | {
254 | "type": "line",
255 | "version": 2410,
256 | "versionNonce": 1413634926,
257 | "isDeleted": false,
258 | "id": "pnA0DA25tJxDKgaSQnlkY",
259 | "fillStyle": "hachure",
260 | "strokeWidth": 2,
261 | "strokeStyle": "solid",
262 | "roughness": 1,
263 | "opacity": 100,
264 | "angle": 6.272333650882224,
265 | "x": -485.89939884573437,
266 | "y": 39.822759267178014,
267 | "strokeColor": "#000000",
268 | "backgroundColor": "transparent",
269 | "width": 29.445220947265625,
270 | "height": 20.990234375,
271 | "seed": 1874345651,
272 | "groupIds": [
273 | "4D1ojplACrlVIaNZ7P0FH"
274 | ],
275 | "frameId": null,
276 | "roundness": {
277 | "type": 2
278 | },
279 | "boundElements": [],
280 | "updated": 1688485467337,
281 | "link": null,
282 | "locked": false,
283 | "startBinding": null,
284 | "endBinding": null,
285 | "lastCommittedPoint": null,
286 | "startArrowhead": null,
287 | "endArrowhead": null,
288 | "points": [
289 | [
290 | 0,
291 | 0
292 | ],
293 | [
294 | 29.445220947265625,
295 | -20.990234375
296 | ]
297 | ]
298 | },
299 | {
300 | "type": "line",
301 | "version": 2449,
302 | "versionNonce": 1278597746,
303 | "isDeleted": false,
304 | "id": "HhvpXqS4JXS1iu_1aiVep",
305 | "fillStyle": "hachure",
306 | "strokeWidth": 2,
307 | "strokeStyle": "solid",
308 | "roughness": 1,
309 | "opacity": 100,
310 | "angle": 6.272333650882224,
311 | "x": -486.7361550344051,
312 | "y": 39.39964561015921,
313 | "strokeColor": "#000000",
314 | "backgroundColor": "transparent",
315 | "width": 25.4169921875,
316 | "height": 9.85821533203125,
317 | "seed": 641986643,
318 | "groupIds": [
319 | "4D1ojplACrlVIaNZ7P0FH"
320 | ],
321 | "frameId": null,
322 | "roundness": {
323 | "type": 2
324 | },
325 | "boundElements": [],
326 | "updated": 1688485467337,
327 | "link": null,
328 | "locked": false,
329 | "startBinding": null,
330 | "endBinding": null,
331 | "lastCommittedPoint": null,
332 | "startArrowhead": null,
333 | "endArrowhead": null,
334 | "points": [
335 | [
336 | 0,
337 | 0
338 | ],
339 | [
340 | -25.4169921875,
341 | -9.85821533203125
342 | ]
343 | ]
344 | },
345 | {
346 | "id": "yq2DoeUuGGRn5Tpcgo65K",
347 | "type": "rectangle",
348 | "x": -356.82044382806384,
349 | "y": -95.87286213043433,
350 | "width": 622,
351 | "height": 270,
352 | "angle": 0,
353 | "strokeColor": "#1e1e1e",
354 | "backgroundColor": "#ced4da",
355 | "fillStyle": "hachure",
356 | "strokeWidth": 1,
357 | "strokeStyle": "solid",
358 | "roughness": 1,
359 | "opacity": 100,
360 | "groupIds": [],
361 | "frameId": null,
362 | "roundness": {
363 | "type": 3
364 | },
365 | "seed": 184251438,
366 | "version": 181,
367 | "versionNonce": 2075956590,
368 | "isDeleted": false,
369 | "boundElements": [
370 | {
371 | "type": "text",
372 | "id": "e_lE1uPiw_dih596oGhQ1"
373 | },
374 | {
375 | "id": "845xbLnqOP2wQQV1nvRvu",
376 | "type": "arrow"
377 | }
378 | ],
379 | "updated": 1688485517019,
380 | "link": null,
381 | "locked": false
382 | },
383 | {
384 | "id": "e_lE1uPiw_dih596oGhQ1",
385 | "type": "text",
386 | "x": -75.71042795892322,
387 | "y": -90.87286213043433,
388 | "width": 59.77996826171875,
389 | "height": 35,
390 | "angle": 0,
391 | "strokeColor": "#1e1e1e",
392 | "backgroundColor": "#d0bfff",
393 | "fillStyle": "hachure",
394 | "strokeWidth": 1,
395 | "strokeStyle": "solid",
396 | "roughness": 1,
397 | "opacity": 100,
398 | "groupIds": [],
399 | "frameId": null,
400 | "roundness": null,
401 | "seed": 1159528434,
402 | "version": 6,
403 | "versionNonce": 1569152050,
404 | "isDeleted": false,
405 | "boundElements": null,
406 | "updated": 1688485351151,
407 | "link": null,
408 | "locked": false,
409 | "text": "Kong",
410 | "fontSize": 28,
411 | "fontFamily": 1,
412 | "textAlign": "center",
413 | "verticalAlign": "top",
414 | "baseline": 25,
415 | "containerId": "yq2DoeUuGGRn5Tpcgo65K",
416 | "originalText": "Kong",
417 | "lineHeight": 1.25,
418 | "isFrameName": false
419 | },
420 | {
421 | "id": "vT7xqFC8XvdmCUm0BPueT",
422 | "type": "rectangle",
423 | "x": -330.49454661126697,
424 | "y": -38.2530806362937,
425 | "width": 570.5958251953125,
426 | "height": 199.2347412109375,
427 | "angle": 0,
428 | "strokeColor": "#1e1e1e",
429 | "backgroundColor": "#ffc9c9",
430 | "fillStyle": "hachure",
431 | "strokeWidth": 1,
432 | "strokeStyle": "solid",
433 | "roughness": 1,
434 | "opacity": 100,
435 | "groupIds": [],
436 | "frameId": null,
437 | "roundness": {
438 | "type": 3
439 | },
440 | "seed": 1959187950,
441 | "version": 202,
442 | "versionNonce": 1921968946,
443 | "isDeleted": false,
444 | "boundElements": [
445 | {
446 | "type": "text",
447 | "id": "1mWDD9jN6AydafRc4RspG"
448 | },
449 | {
450 | "id": "845xbLnqOP2wQQV1nvRvu",
451 | "type": "arrow"
452 | },
453 | {
454 | "id": "iIqc-emFEX2a-iJ_qh_vf",
455 | "type": "arrow"
456 | },
457 | {
458 | "id": "cA-77Pr2VQvDPRulaOUIC",
459 | "type": "arrow"
460 | }
461 | ],
462 | "updated": 1688485436165,
463 | "link": null,
464 | "locked": false
465 | },
466 | {
467 | "id": "1mWDD9jN6AydafRc4RspG",
468 | "type": "text",
469 | "x": -90.3046128344115,
470 | "y": -33.2530806362937,
471 | "width": 90.21595764160156,
472 | "height": 35,
473 | "angle": 0,
474 | "strokeColor": "#1e1e1e",
475 | "backgroundColor": "#d0bfff",
476 | "fillStyle": "hachure",
477 | "strokeWidth": 1,
478 | "strokeStyle": "solid",
479 | "roughness": 1,
480 | "opacity": 100,
481 | "groupIds": [],
482 | "frameId": null,
483 | "roundness": null,
484 | "seed": 364938162,
485 | "version": 31,
486 | "versionNonce": 580753394,
487 | "isDeleted": false,
488 | "boundElements": null,
489 | "updated": 1688485348472,
490 | "link": null,
491 | "locked": false,
492 | "text": "Plugins",
493 | "fontSize": 28,
494 | "fontFamily": 1,
495 | "textAlign": "center",
496 | "verticalAlign": "top",
497 | "baseline": 25,
498 | "containerId": "vT7xqFC8XvdmCUm0BPueT",
499 | "originalText": "Plugins",
500 | "lineHeight": 1.25,
501 | "isFrameName": false
502 | },
503 | {
504 | "id": "yjm7fXLQKIEZ3lseoexVB",
505 | "type": "rectangle",
506 | "x": -310.7942902636107,
507 | "y": 28.142595022885985,
508 | "width": 252.19769287109375,
509 | "height": 45,
510 | "angle": 0,
511 | "strokeColor": "#1e1e1e",
512 | "backgroundColor": "#ffec99",
513 | "fillStyle": "hachure",
514 | "strokeWidth": 1,
515 | "strokeStyle": "solid",
516 | "roughness": 1,
517 | "opacity": 100,
518 | "groupIds": [],
519 | "frameId": null,
520 | "roundness": {
521 | "type": 3
522 | },
523 | "seed": 1186205806,
524 | "version": 200,
525 | "versionNonce": 558040626,
526 | "isDeleted": false,
527 | "boundElements": [
528 | {
529 | "type": "text",
530 | "id": "5B1VqU9ug2fRrInXd2b5i"
531 | }
532 | ],
533 | "updated": 1688485483139,
534 | "link": null,
535 | "locked": false
536 | },
537 | {
538 | "id": "5B1VqU9ug2fRrInXd2b5i",
539 | "type": "text",
540 | "x": -240.1153809618529,
541 | "y": 38.142595022885985,
542 | "width": 110.83987426757812,
543 | "height": 25,
544 | "angle": 0,
545 | "strokeColor": "#1e1e1e",
546 | "backgroundColor": "#e9ecef",
547 | "fillStyle": "hachure",
548 | "strokeWidth": 1,
549 | "strokeStyle": "solid",
550 | "roughness": 1,
551 | "opacity": 100,
552 | "groupIds": [],
553 | "frameId": null,
554 | "roundness": null,
555 | "seed": 1206241006,
556 | "version": 129,
557 | "versionNonce": 1291660782,
558 | "isDeleted": false,
559 | "boundElements": null,
560 | "updated": 1688485483139,
561 | "link": null,
562 | "locked": false,
563 | "text": "Prometheus",
564 | "fontSize": 20,
565 | "fontFamily": 1,
566 | "textAlign": "center",
567 | "verticalAlign": "middle",
568 | "baseline": 18,
569 | "containerId": "yjm7fXLQKIEZ3lseoexVB",
570 | "originalText": "Prometheus",
571 | "lineHeight": 1.25,
572 | "isFrameName": false
573 | },
574 | {
575 | "type": "rectangle",
576 | "version": 286,
577 | "versionNonce": 385331758,
578 | "isDeleted": false,
579 | "id": "plVbfOHEM64DkbuzmFalq",
580 | "fillStyle": "hachure",
581 | "strokeWidth": 1,
582 | "strokeStyle": "solid",
583 | "roughness": 1,
584 | "opacity": 100,
585 | "angle": 0,
586 | "x": -309.9827363085326,
587 | "y": 89.28448650237817,
588 | "strokeColor": "#1e1e1e",
589 | "backgroundColor": "#96f2d7",
590 | "width": 252.19769287109375,
591 | "height": 45,
592 | "seed": 1811015346,
593 | "groupIds": [],
594 | "frameId": null,
595 | "roundness": {
596 | "type": 3
597 | },
598 | "boundElements": [
599 | {
600 | "type": "text",
601 | "id": "N2-kv1UOVVwUa50zfC4Ku"
602 | }
603 | ],
604 | "updated": 1688485505793,
605 | "link": null,
606 | "locked": false
607 | },
608 | {
609 | "type": "text",
610 | "version": 225,
611 | "versionNonce": 1477719090,
612 | "isDeleted": false,
613 | "id": "N2-kv1UOVVwUa50zfC4Ku",
614 | "fillStyle": "hachure",
615 | "strokeWidth": 1,
616 | "strokeStyle": "solid",
617 | "roughness": 1,
618 | "opacity": 100,
619 | "angle": 0,
620 | "x": -247.67383738275134,
621 | "y": 99.28448650237817,
622 | "strokeColor": "#1e1e1e",
623 | "backgroundColor": "#e9ecef",
624 | "width": 127.57989501953125,
625 | "height": 25,
626 | "seed": 841367666,
627 | "groupIds": [],
628 | "frameId": null,
629 | "roundness": null,
630 | "boundElements": [],
631 | "updated": 1688485499246,
632 | "link": null,
633 | "locked": false,
634 | "fontSize": 20,
635 | "fontFamily": 1,
636 | "text": "Rate Limiting",
637 | "textAlign": "center",
638 | "verticalAlign": "middle",
639 | "containerId": "plVbfOHEM64DkbuzmFalq",
640 | "originalText": "Rate Limiting",
641 | "lineHeight": 1.25,
642 | "baseline": 18
643 | },
644 | {
645 | "type": "rectangle",
646 | "version": 296,
647 | "versionNonce": 1616421298,
648 | "isDeleted": false,
649 | "id": "lPy7uiSeiRj77CYjOpaml",
650 | "fillStyle": "hachure",
651 | "strokeWidth": 1,
652 | "strokeStyle": "solid",
653 | "roughness": 1,
654 | "opacity": 100,
655 | "angle": 0,
656 | "x": -37.761361796813844,
657 | "y": 28.142595022885985,
658 | "strokeColor": "#1e1e1e",
659 | "backgroundColor": "#a5d8ff",
660 | "width": 252.19769287109375,
661 | "height": 45,
662 | "seed": 1286297326,
663 | "groupIds": [],
664 | "frameId": null,
665 | "roundness": {
666 | "type": 3
667 | },
668 | "boundElements": [
669 | {
670 | "type": "text",
671 | "id": "LnNCNAWCxPZgo09VeLKtv"
672 | }
673 | ],
674 | "updated": 1688485483139,
675 | "link": null,
676 | "locked": false
677 | },
678 | {
679 | "type": "text",
680 | "version": 245,
681 | "versionNonce": 1749092974,
682 | "isDeleted": false,
683 | "id": "LnNCNAWCxPZgo09VeLKtv",
684 | "fillStyle": "hachure",
685 | "strokeWidth": 1,
686 | "strokeStyle": "solid",
687 | "roughness": 1,
688 | "opacity": 100,
689 | "angle": 0,
690 | "x": -13.792428691345094,
691 | "y": 38.142595022885985,
692 | "strokeColor": "#1e1e1e",
693 | "backgroundColor": "#e9ecef",
694 | "width": 204.25982666015625,
695 | "height": 25,
696 | "seed": 658393390,
697 | "groupIds": [],
698 | "frameId": null,
699 | "roundness": null,
700 | "boundElements": [],
701 | "updated": 1688485483139,
702 | "link": null,
703 | "locked": false,
704 | "fontSize": 20,
705 | "fontFamily": 1,
706 | "text": "Basic Authentication",
707 | "textAlign": "center",
708 | "verticalAlign": "middle",
709 | "containerId": "lPy7uiSeiRj77CYjOpaml",
710 | "originalText": "Basic Authentication",
711 | "lineHeight": 1.25,
712 | "baseline": 18
713 | },
714 | {
715 | "type": "rectangle",
716 | "version": 205,
717 | "versionNonce": 2147326834,
718 | "isDeleted": false,
719 | "id": "BIeqT_OAZ0rURxrChhuro",
720 | "fillStyle": "hachure",
721 | "strokeWidth": 1,
722 | "strokeStyle": "solid",
723 | "roughness": 1,
724 | "opacity": 100,
725 | "angle": 0,
726 | "x": -36.231637675720094,
727 | "y": 89.03717204925317,
728 | "strokeColor": "#1e1e1e",
729 | "backgroundColor": "#d0bfff",
730 | "width": 252.19769287109375,
731 | "height": 45,
732 | "seed": 1092832046,
733 | "groupIds": [],
734 | "frameId": null,
735 | "roundness": {
736 | "type": 3
737 | },
738 | "boundElements": [
739 | {
740 | "type": "text",
741 | "id": "c19rNMWCoq5dNa0il79ei"
742 | },
743 | {
744 | "id": "jEzcfI7u3r5UZUY7aqiXu",
745 | "type": "arrow"
746 | }
747 | ],
748 | "updated": 1688485483139,
749 | "link": null,
750 | "locked": false
751 | },
752 | {
753 | "type": "text",
754 | "version": 152,
755 | "versionNonce": 1020365998,
756 | "isDeleted": false,
757 | "id": "c19rNMWCoq5dNa0il79ei",
758 | "fillStyle": "hachure",
759 | "strokeWidth": 1,
760 | "strokeStyle": "solid",
761 | "roughness": 1,
762 | "opacity": 100,
763 | "angle": 0,
764 | "x": -12.642709453063844,
765 | "y": 99.03717204925317,
766 | "strokeColor": "#1e1e1e",
767 | "backgroundColor": "#e9ecef",
768 | "width": 205.01983642578125,
769 | "height": 25,
770 | "seed": 1764603246,
771 | "groupIds": [],
772 | "frameId": null,
773 | "roundness": null,
774 | "boundElements": [],
775 | "updated": 1688485483139,
776 | "link": null,
777 | "locked": false,
778 | "fontSize": 20,
779 | "fontFamily": 1,
780 | "text": "LDAP Authentication",
781 | "textAlign": "center",
782 | "verticalAlign": "middle",
783 | "containerId": "BIeqT_OAZ0rURxrChhuro",
784 | "originalText": "LDAP Authentication",
785 | "lineHeight": 1.25,
786 | "baseline": 18
787 | },
788 | {
789 | "type": "rectangle",
790 | "version": 3385,
791 | "versionNonce": 1321821038,
792 | "isDeleted": false,
793 | "id": "QKow7SMAYTWIfyKcS4_zw",
794 | "fillStyle": "hachure",
795 | "strokeWidth": 1,
796 | "strokeStyle": "solid",
797 | "roughness": 1,
798 | "opacity": 100,
799 | "angle": 0,
800 | "x": 368.1188471829882,
801 | "y": -118.39416339996558,
802 | "strokeColor": "#000000",
803 | "backgroundColor": "#b2f2bb",
804 | "width": 209.18356323242188,
805 | "height": 99.67071533203125,
806 | "seed": 421184622,
807 | "groupIds": [],
808 | "frameId": null,
809 | "roundness": {
810 | "type": 3
811 | },
812 | "boundElements": [
813 | {
814 | "type": "text",
815 | "id": "fs6YWFtJPWXmK2TBDm3j1"
816 | },
817 | {
818 | "id": "iIqc-emFEX2a-iJ_qh_vf",
819 | "type": "arrow"
820 | }
821 | ],
822 | "updated": 1688485497517,
823 | "link": null,
824 | "locked": false
825 | },
826 | {
827 | "type": "text",
828 | "version": 2349,
829 | "versionNonce": 871860850,
830 | "isDeleted": false,
831 | "id": "fs6YWFtJPWXmK2TBDm3j1",
832 | "fillStyle": "hachure",
833 | "strokeWidth": 1,
834 | "strokeStyle": "solid",
835 | "roughness": 0,
836 | "opacity": 100,
837 | "angle": 0,
838 | "x": 411.8946497952929,
839 | "y": -85.35880573394996,
840 | "strokeColor": "#000000",
841 | "backgroundColor": "transparent",
842 | "width": 121.6319580078125,
843 | "height": 33.6,
844 | "seed": 1583613614,
845 | "groupIds": [],
846 | "frameId": null,
847 | "roundness": null,
848 | "boundElements": [],
849 | "updated": 1688485412964,
850 | "link": null,
851 | "locked": false,
852 | "fontSize": 28,
853 | "fontFamily": 1,
854 | "text": "Postgres",
855 | "textAlign": "center",
856 | "verticalAlign": "middle",
857 | "containerId": "QKow7SMAYTWIfyKcS4_zw",
858 | "originalText": "Postgres",
859 | "lineHeight": 1.2,
860 | "baseline": 24
861 | },
862 | {
863 | "type": "rectangle",
864 | "version": 3389,
865 | "versionNonce": 1261655282,
866 | "isDeleted": false,
867 | "id": "ifVkhBNuL_bAm3dvA3bM3",
868 | "fillStyle": "hachure",
869 | "strokeWidth": 1,
870 | "strokeStyle": "solid",
871 | "roughness": 1,
872 | "opacity": 100,
873 | "angle": 0,
874 | "x": 368.1188471829882,
875 | "y": 113.02179118987817,
876 | "strokeColor": "#000000",
877 | "backgroundColor": "#ffec99",
878 | "width": 209.18356323242188,
879 | "height": 99.67071533203125,
880 | "seed": 902278510,
881 | "groupIds": [],
882 | "frameId": null,
883 | "roundness": {
884 | "type": 3
885 | },
886 | "boundElements": [
887 | {
888 | "type": "text",
889 | "id": "xNVxGmWqxjLhlgc6m1smY"
890 | },
891 | {
892 | "id": "jEzcfI7u3r5UZUY7aqiXu",
893 | "type": "arrow"
894 | }
895 | ],
896 | "updated": 1688485509905,
897 | "link": null,
898 | "locked": false
899 | },
900 | {
901 | "type": "text",
902 | "version": 2350,
903 | "versionNonce": 1813479086,
904 | "isDeleted": false,
905 | "id": "xNVxGmWqxjLhlgc6m1smY",
906 | "fillStyle": "hachure",
907 | "strokeWidth": 1,
908 | "strokeStyle": "solid",
909 | "roughness": 0,
910 | "opacity": 100,
911 | "angle": 0,
912 | "x": 403.7046626126757,
913 | "y": 146.0571488558938,
914 | "strokeColor": "#000000",
915 | "backgroundColor": "transparent",
916 | "width": 138.01193237304688,
917 | "height": 33.6,
918 | "seed": 126603182,
919 | "groupIds": [],
920 | "frameId": null,
921 | "roundness": null,
922 | "boundElements": [],
923 | "updated": 1688485508297,
924 | "link": null,
925 | "locked": false,
926 | "fontSize": 28,
927 | "fontFamily": 1,
928 | "text": "OpenLDAP",
929 | "textAlign": "center",
930 | "verticalAlign": "middle",
931 | "containerId": "ifVkhBNuL_bAm3dvA3bM3",
932 | "originalText": "OpenLDAP",
933 | "lineHeight": 1.2,
934 | "baseline": 24
935 | },
936 | {
937 | "id": "845xbLnqOP2wQQV1nvRvu",
938 | "type": "arrow",
939 | "x": -455.95577402825916,
940 | "y": 47.31652995940942,
941 | "width": 111.79153442382812,
942 | "height": 0.006500244140625,
943 | "angle": 0,
944 | "strokeColor": "#1e1e1e",
945 | "backgroundColor": "#d0bfff",
946 | "fillStyle": "hachure",
947 | "strokeWidth": 1,
948 | "strokeStyle": "solid",
949 | "roughness": 1,
950 | "opacity": 100,
951 | "groupIds": [],
952 | "frameId": null,
953 | "roundness": {
954 | "type": 2
955 | },
956 | "seed": 1716859630,
957 | "version": 221,
958 | "versionNonce": 1317734190,
959 | "isDeleted": false,
960 | "boundElements": null,
961 | "updated": 1688485474620,
962 | "link": null,
963 | "locked": false,
964 | "points": [
965 | [
966 | 0,
967 | 0
968 | ],
969 | [
970 | 111.79153442382812,
971 | 0.006500244140625
972 | ]
973 | ],
974 | "lastCommittedPoint": null,
975 | "startBinding": null,
976 | "endBinding": {
977 | "elementId": "vT7xqFC8XvdmCUm0BPueT",
978 | "focus": 0.1407539760446932,
979 | "gap": 13.669692993164062
980 | },
981 | "startArrowhead": "arrow",
982 | "endArrowhead": "arrow"
983 | },
984 | {
985 | "id": "iIqc-emFEX2a-iJ_qh_vf",
986 | "type": "arrow",
987 | "x": 265.52657155279553,
988 | "y": -34.33733966949683,
989 | "width": 102.23004150390625,
990 | "height": 34.598876953125,
991 | "angle": 0,
992 | "strokeColor": "#1e1e1e",
993 | "backgroundColor": "#d0bfff",
994 | "fillStyle": "hachure",
995 | "strokeWidth": 1,
996 | "strokeStyle": "dashed",
997 | "roughness": 1,
998 | "opacity": 100,
999 | "groupIds": [],
1000 | "frameId": null,
1001 | "roundness": {
1002 | "type": 2
1003 | },
1004 | "seed": 13695534,
1005 | "version": 79,
1006 | "versionNonce": 1544518194,
1007 | "isDeleted": false,
1008 | "boundElements": null,
1009 | "updated": 1688485463218,
1010 | "link": null,
1011 | "locked": false,
1012 | "points": [
1013 | [
1014 | 0,
1015 | 0
1016 | ],
1017 | [
1018 | 102.23004150390625,
1019 | -34.598876953125
1020 | ]
1021 | ],
1022 | "lastCommittedPoint": null,
1023 | "startBinding": {
1024 | "elementId": "vT7xqFC8XvdmCUm0BPueT",
1025 | "focus": 0.04822230860267914,
1026 | "gap": 25.42529296875
1027 | },
1028 | "endBinding": {
1029 | "elementId": "QKow7SMAYTWIfyKcS4_zw",
1030 | "focus": 0.4211744726536373,
1031 | "gap": 1
1032 | },
1033 | "startArrowhead": null,
1034 | "endArrowhead": null
1035 | },
1036 | {
1037 | "id": "cA-77Pr2VQvDPRulaOUIC",
1038 | "type": "arrow",
1039 | "x": 265.55879811529553,
1040 | "y": 50.10220500823755,
1041 | "width": 99.5701904296875,
1042 | "height": 1.3055419921875,
1043 | "angle": 0,
1044 | "strokeColor": "#1e1e1e",
1045 | "backgroundColor": "#d0bfff",
1046 | "fillStyle": "hachure",
1047 | "strokeWidth": 1,
1048 | "strokeStyle": "solid",
1049 | "roughness": 1,
1050 | "opacity": 100,
1051 | "groupIds": [],
1052 | "frameId": null,
1053 | "roundness": {
1054 | "type": 2
1055 | },
1056 | "seed": 1462251634,
1057 | "version": 123,
1058 | "versionNonce": 1921915118,
1059 | "isDeleted": false,
1060 | "boundElements": null,
1061 | "updated": 1688485436165,
1062 | "link": null,
1063 | "locked": false,
1064 | "points": [
1065 | [
1066 | 0,
1067 | 0
1068 | ],
1069 | [
1070 | 99.5701904296875,
1071 | 1.3055419921875
1072 | ]
1073 | ],
1074 | "lastCommittedPoint": null,
1075 | "startBinding": {
1076 | "elementId": "vT7xqFC8XvdmCUm0BPueT",
1077 | "focus": -0.14838348983948454,
1078 | "gap": 25.45751953125
1079 | },
1080 | "endBinding": {
1081 | "elementId": "NKmNZxYxWMCKh3prRiPwX",
1082 | "focus": -0.12792319973486768,
1083 | "gap": 2.989858638005188
1084 | },
1085 | "startArrowhead": "arrow",
1086 | "endArrowhead": "arrow"
1087 | },
1088 | {
1089 | "id": "jEzcfI7u3r5UZUY7aqiXu",
1090 | "type": "arrow",
1091 | "x": 219.81624440435803,
1092 | "y": 119.12694814816359,
1093 | "width": 142.9097900390625,
1094 | "height": 41.19620985732408,
1095 | "angle": 0,
1096 | "strokeColor": "#1e1e1e",
1097 | "backgroundColor": "#d0bfff",
1098 | "fillStyle": "hachure",
1099 | "strokeWidth": 1,
1100 | "strokeStyle": "dashed",
1101 | "roughness": 1,
1102 | "opacity": 100,
1103 | "groupIds": [],
1104 | "frameId": null,
1105 | "roundness": {
1106 | "type": 2
1107 | },
1108 | "seed": 1199098670,
1109 | "version": 154,
1110 | "versionNonce": 508524782,
1111 | "isDeleted": false,
1112 | "boundElements": null,
1113 | "updated": 1688485508298,
1114 | "link": null,
1115 | "locked": false,
1116 | "points": [
1117 | [
1118 | 0,
1119 | 0
1120 | ],
1121 | [
1122 | 142.9097900390625,
1123 | 41.19620985732408
1124 | ]
1125 | ],
1126 | "lastCommittedPoint": null,
1127 | "startBinding": {
1128 | "elementId": "BIeqT_OAZ0rURxrChhuro",
1129 | "focus": -0.5068931527936216,
1130 | "gap": 3.850189208984375
1131 | },
1132 | "endBinding": {
1133 | "elementId": "ifVkhBNuL_bAm3dvA3bM3",
1134 | "focus": -0.36470205159331864,
1135 | "gap": 5.392812739567688
1136 | },
1137 | "startArrowhead": null,
1138 | "endArrowhead": null
1139 | }
1140 | ],
1141 | "appState": {
1142 | "gridSize": null,
1143 | "viewBackgroundColor": "#ffffff"
1144 | },
1145 | "files": {}
1146 | }
--------------------------------------------------------------------------------