├── .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 | ![project-diagram](documentation/project-diagram.jpeg) 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 | ![openldap](documentation/openldap.jpeg) 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 | } --------------------------------------------------------------------------------