commit 79b0cc0818f227a3716a3e308e671ac050c942c8 Author: yo Date: Fri Apr 15 16:58:35 2022 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d931c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +work/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ba4def2 --- /dev/null +++ b/Makefile @@ -0,0 +1,73 @@ +# Created by: yo000 + +PORTNAME= haproxy-spoe-auth +PORTVERSION= 1.0.0 +DISTVERSIONPREFIX=v +PORTREVISION= 1 +CATEGORIES= net + +MAINTAINER= johan@nosd.in +COMMENT= HAProxy plugin for authorizing users against LDAP + +LICENSE= BSD3CLAUSE + +USES= go +USE_GITHUB= yes + +GH_ACCOUNT= criteo + +GH_TUPLE= blang:semver:v3.5.1:blang_semver/vendor/github.com/blang/semver \ + coreos:go-oidc:v3.0.0:coreos_go_oidc_v3/vendor/github.com/coreos/go-oidc/v3 \ + criteo:haproxy-spoe-go:v1.0.6:criteo_haproxy_spoe_go/vendor/github.com/criteo/haproxy-spoe-go \ + davecgh:go-spew:v1.1.1:davecgh_go_spew/vendor/github.com/davecgh/go-spew \ + fsnotify:fsnotify:v1.4.9:fsnotify_fsnotify/vendor/github.com/fsnotify/fsnotify \ + go-asn1-ber:asn1-ber:f715ec2f112d:go_asn1_ber_asn1_ber/vendor/gopkg.in/asn1-ber.v1 \ + go-delve:delve:v1.8.0:go_delve_delve/vendor/github.com/go-delve/delve \ + go-ini:ini:v1.62.0:go_ini_ini/vendor/gopkg.in/ini.v1 \ + go-ldap:ldap:v3.0.3:go_ldap_ldap/vendor/gopkg.in/ldap.v3 \ + go-yaml:yaml:496545a6307b:go_yaml_yaml_1/vendor/gopkg.in/yaml.v3 \ + go-yaml:yaml:v2.4.0:go_yaml_yaml/vendor/gopkg.in/yaml.v2 \ + golang:appengine:v1.6.7:golang_appengine/vendor/google.golang.org/appengine \ + golang:crypto:75b288015ac9:golang_crypto/vendor/golang.org/x/crypto \ + golang:net:4f30a5c0130f:golang_net/vendor/golang.org/x/net \ + golang:oauth2:2e8d93401602:golang_oauth2/vendor/golang.org/x/oauth2 \ + golang:protobuf:v1.5.2:golang_protobuf/vendor/github.com/golang/protobuf \ + golang:sys:9d821ace8654:golang_sys/vendor/golang.org/x/sys \ + golang:text:v0.3.7:golang_text/vendor/golang.org/x/text \ + hashicorp:hcl:v1.0.0:hashicorp_hcl/vendor/github.com/hashicorp/hcl \ + magiconair:properties:v1.8.5:magiconair_properties/vendor/github.com/magiconair/properties \ + mitchellh:mapstructure:v1.4.1:mitchellh_mapstructure/vendor/github.com/mitchellh/mapstructure \ + pelletier:go-toml:v1.9.3:pelletier_go_toml/vendor/github.com/pelletier/go-toml \ + pkg:errors:v0.9.1:pkg_errors/vendor/github.com/pkg/errors \ + pmezard:go-difflib:v1.0.0:pmezard_go_difflib/vendor/github.com/pmezard/go-difflib \ + protocolbuffers:protobuf-go:v1.26.0:protocolbuffers_protobuf_go/vendor/google.golang.org/protobuf \ + sirupsen:logrus:v1.7.0:sirupsen_logrus/vendor/github.com/sirupsen/logrus \ + spf13:afero:v1.6.0:spf13_afero/vendor/github.com/spf13/afero \ + spf13:cast:v1.3.1:spf13_cast/vendor/github.com/spf13/cast \ + spf13:jwalterweatherman:v1.1.0:spf13_jwalterweatherman/vendor/github.com/spf13/jwalterweatherman \ + spf13:pflag:v1.0.5:spf13_pflag/vendor/github.com/spf13/pflag \ + spf13:viper:v1.8.1:spf13_viper/vendor/github.com/spf13/viper \ + square:go-jose:v2.5.1:square_go_jose/vendor/gopkg.in/square/go-jose.v2 \ + stretchr:testify:v1.7.0:stretchr_testify/vendor/github.com/stretchr/testify \ + subosito:gotenv:v1.2.0:subosito_gotenv/vendor/github.com/subosito/gotenv \ + tebeka:selenium:v0.9.9:tebeka_selenium/vendor/github.com/tebeka/selenium \ + vmihailenco:msgpack:v5.3.4:vmihailenco_msgpack_v5/vendor/github.com/vmihailenco/msgpack/v5 \ + vmihailenco:tagparser:v2.0.0:vmihailenco_tagparser_v2/vendor/github.com/vmihailenco/tagparser/v2 + +GO_PKGNAME= github.com/${GH_ACCOUNT}/${PORTNAME} + +GO_TARGET= cmd/haproxy-spoe-auth/main.go:haproxy-spoe-auth +ETCDIR= ${PREFIX}/etc/haproxy + +post-install: + ${MKDIR} ${STAGEDIR}${ETCDIR} + ${INSTALL_DATA} ${WRKSRC}/resources/haproxy/haproxy.cfg \ + ${STAGEDIR}${ETCDIR}/haproxy.cfg.spoe-auth.sample + ${INSTALL_DATA} ${FILESDIR}/haproxy-spoe-auth.yml.sample \ + ${STAGEDIR}${ETCDIR}/haproxy-spoe-auth.yml.sample + ${INSTALL_DATA} ${FILESDIR}/spoe-auth.conf.sample \ + ${STAGEDIR}${ETCDIR}/spoe-auth.conf.sample + +USE_RC_SUBR= haproxy-spoe-auth + +.include diff --git a/distinfo b/distinfo new file mode 100644 index 0000000..1d4e244 --- /dev/null +++ b/distinfo @@ -0,0 +1,77 @@ +TIMESTAMP = 1650024511 +SHA256 (criteo-haproxy-spoe-auth-v1.0.0_GH0.tar.gz) = ee60ac07a57a29d3aadd00ed100745830f8456c5b085c8862a52699e1530c417 +SIZE (criteo-haproxy-spoe-auth-v1.0.0_GH0.tar.gz) = 147928 +SHA256 (blang-semver-v3.5.1_GH0.tar.gz) = 3d9da53f4c2d3169bfa9b25f2f36f301a37556a47259c870881524c643c69c57 +SIZE (blang-semver-v3.5.1_GH0.tar.gz) = 14831 +SHA256 (coreos-go-oidc-v3.0.0_GH0.tar.gz) = 4326069f4ce38fb0a48959a04e7569dde971e06ca32070e828a33df4764f36e0 +SIZE (coreos-go-oidc-v3.0.0_GH0.tar.gz) = 28621 +SHA256 (criteo-haproxy-spoe-go-v1.0.6_GH0.tar.gz) = c94f4a598c5593e1d2a8ee6dc21d2f30ef1b2de7327c6cd9b583983d0df6c300 +SIZE (criteo-haproxy-spoe-go-v1.0.6_GH0.tar.gz) = 16619 +SHA256 (davecgh-go-spew-v1.1.1_GH0.tar.gz) = 7d82b9bb7291adbe7498fe946920ab3e7fc9e6cbfc3b2294693fad00bf0dd17e +SIZE (davecgh-go-spew-v1.1.1_GH0.tar.gz) = 42152 +SHA256 (fsnotify-fsnotify-v1.4.9_GH0.tar.gz) = 4f888b1cb132026227826751d156c0a2958e7d492e5e38386cde8848ef494dcb +SIZE (fsnotify-fsnotify-v1.4.9_GH0.tar.gz) = 31900 +SHA256 (go-asn1-ber-asn1-ber-f715ec2f112d_GH0.tar.gz) = 084d4021bd3a9ae361fff6c59910d83227116dee977ec09bfe9322db43020b0d +SIZE (go-asn1-ber-asn1-ber-f715ec2f112d_GH0.tar.gz) = 13199 +SHA256 (go-delve-delve-v1.8.0_GH0.tar.gz) = 086106a4776fa155bf20c37d27b9caed55255be6359c7f0bda1893d3adbb614e +SIZE (go-delve-delve-v1.8.0_GH0.tar.gz) = 9007735 +SHA256 (go-ini-ini-v1.62.0_GH0.tar.gz) = 926ca2ff49018dc5c0991f3a790bc2083a3c52c470167d42f0f0bcd6642ff64e +SIZE (go-ini-ini-v1.62.0_GH0.tar.gz) = 50314 +SHA256 (go-ldap-ldap-v3.0.3_GH0.tar.gz) = ca5febd665ec382d044d8e1fa28d5fa95e2fec3df515ed2f2d7a974e70bec362 +SIZE (go-ldap-ldap-v3.0.3_GH0.tar.gz) = 41078 +SHA256 (go-yaml-yaml-496545a6307b_GH0.tar.gz) = ed0e11dc14bbbd4127031d7e8b9e58dad885e2c44a16359d2f64b71d1d1f692a +SIZE (go-yaml-yaml-496545a6307b_GH0.tar.gz) = 90156 +SHA256 (go-yaml-yaml-v2.4.0_GH0.tar.gz) = d8e94679e5fff6bd1a35e10241543929a5f3da44f701755babf99b3daf0faac0 +SIZE (go-yaml-yaml-v2.4.0_GH0.tar.gz) = 73209 +SHA256 (golang-appengine-v1.6.7_GH0.tar.gz) = c623d81235f7c9699e299b328191d813337dd57dcc800d7afdb5130e8c321a8f +SIZE (golang-appengine-v1.6.7_GH0.tar.gz) = 333007 +SHA256 (golang-crypto-75b288015ac9_GH0.tar.gz) = 6e74e21bf9dfdbf0a8dac8cb205fbc3bfd8dff308a24080b9d6093a3858f0db2 +SIZE (golang-crypto-75b288015ac9_GH0.tar.gz) = 1729931 +SHA256 (golang-net-4f30a5c0130f_GH0.tar.gz) = ae79c93eb97940fdc6a0e99203c1af7760d9e99caca486be30570a4bd239427a +SIZE (golang-net-4f30a5c0130f_GH0.tar.gz) = 1261780 +SHA256 (golang-oauth2-2e8d93401602_GH0.tar.gz) = 666f0dd6ef39ba66a52c7fc02c8730be742a5f14419fc7c3a70f0442f6a5bc92 +SIZE (golang-oauth2-2e8d93401602_GH0.tar.gz) = 79381 +SHA256 (golang-protobuf-v1.5.2_GH0.tar.gz) = 088cc0f3ba18fb8f9d00319568ff0af5a06d8925a6e6cb983bb837b4efb703b3 +SIZE (golang-protobuf-v1.5.2_GH0.tar.gz) = 171702 +SHA256 (golang-sys-9d821ace8654_GH0.tar.gz) = f5c6bb1aca047e2b6a1d8776c405c057a8ca1c25da4e2818b02a0ee758efa68c +SIZE (golang-sys-9d821ace8654_GH0.tar.gz) = 1218827 +SHA256 (golang-text-v0.3.7_GH0.tar.gz) = 7cab2f6c3133ac1d422edd952b0dd2082fa55a73c2663fb2defd9bf83d649b26 +SIZE (golang-text-v0.3.7_GH0.tar.gz) = 8354718 +SHA256 (hashicorp-hcl-v1.0.0_GH0.tar.gz) = 50632428210503070fd2fde748c88b7414bf84a6a0eadebf9d8e596a033bead2 +SIZE (hashicorp-hcl-v1.0.0_GH0.tar.gz) = 70658 +SHA256 (magiconair-properties-v1.8.5_GH0.tar.gz) = f85ea629d145006f4df18fd8251fa005d95c311b068848043232f52d247ba45c +SIZE (magiconair-properties-v1.8.5_GH0.tar.gz) = 30514 +SHA256 (mitchellh-mapstructure-v1.4.1_GH0.tar.gz) = d936baa5006f7dda1346aff863745a110981f7583a1184e93b9077fa52cd4048 +SIZE (mitchellh-mapstructure-v1.4.1_GH0.tar.gz) = 27826 +SHA256 (pelletier-go-toml-v1.9.3_GH0.tar.gz) = 9cde535cade919d774a185846ba84cffda814d30cc9d2791f5fd1a3b5856c2a4 +SIZE (pelletier-go-toml-v1.9.3_GH0.tar.gz) = 106357 +SHA256 (pkg-errors-v0.9.1_GH0.tar.gz) = 56bfd893023daa498508bfe161de1be83299fcf15376035e7df79cbd7d6fa608 +SIZE (pkg-errors-v0.9.1_GH0.tar.gz) = 13415 +SHA256 (pmezard-go-difflib-v1.0.0_GH0.tar.gz) = 28f3dc1b5c0efd61203ab07233f774740d3bf08da4d8153fb5310db6cea0ebda +SIZE (pmezard-go-difflib-v1.0.0_GH0.tar.gz) = 11398 +SHA256 (protocolbuffers-protobuf-go-v1.26.0_GH0.tar.gz) = 26218474bcf776ecf32d7d194c6bfaca8e7b4f0c087e5b595fd50fbb31409676 +SIZE (protocolbuffers-protobuf-go-v1.26.0_GH0.tar.gz) = 1270215 +SHA256 (sirupsen-logrus-v1.7.0_GH0.tar.gz) = a7baaa1c646441d002f3867b5998b6b45b629ecfad317d468a981e23e0c9c6ca +SIZE (sirupsen-logrus-v1.7.0_GH0.tar.gz) = 46392 +SHA256 (spf13-afero-v1.6.0_GH0.tar.gz) = d1942de010ac7932bd21618aaf478b4f1413980449c061032f18beac7805d068 +SIZE (spf13-afero-v1.6.0_GH0.tar.gz) = 62130 +SHA256 (spf13-cast-v1.3.1_GH0.tar.gz) = 4fa8d06903b490ae6f1316e55c5446d5648eea2b450671ebc54d4bbe79bc46b1 +SIZE (spf13-cast-v1.3.1_GH0.tar.gz) = 11102 +SHA256 (spf13-jwalterweatherman-v1.1.0_GH0.tar.gz) = 4fd850a792c5738954c4801cf549d8d0bf53edd17139cd39d179aa5abf7ec68d +SIZE (spf13-jwalterweatherman-v1.1.0_GH0.tar.gz) = 6871 +SHA256 (spf13-pflag-v1.0.5_GH0.tar.gz) = 9a2cae1f8e8ab0d2cc8ebe468e871af28d9ac0962cf0520999e3ba85f0c7b808 +SIZE (spf13-pflag-v1.0.5_GH0.tar.gz) = 50796 +SHA256 (spf13-viper-v1.8.1_GH0.tar.gz) = af59d3ee994ab946c6bdce204a481e82d6c8f3eca97d80ccd72d89a5555bba59 +SIZE (spf13-viper-v1.8.1_GH0.tar.gz) = 94755 +SHA256 (square-go-jose-v2.5.1_GH0.tar.gz) = 74c65592183c542b254eb2933f7a99ee869abdf9e7ac02aad4d9f0dce980ace8 +SIZE (square-go-jose-v2.5.1_GH0.tar.gz) = 309860 +SHA256 (stretchr-testify-v1.7.0_GH0.tar.gz) = 560c0984072cb436b17bbce5699b205d5aa2beb58ef7a94530d7724b5739a8d6 +SIZE (stretchr-testify-v1.7.0_GH0.tar.gz) = 91073 +SHA256 (subosito-gotenv-v1.2.0_GH0.tar.gz) = 5f6826992c11981018c77377f33dbc56d0be932e0d38a2f51e795c99725e7ba5 +SIZE (subosito-gotenv-v1.2.0_GH0.tar.gz) = 7359 +SHA256 (tebeka-selenium-v0.9.9_GH0.tar.gz) = 82846f237b742983a293619e712dcf167e3d7998df3239f3443303d9036ad412 +SIZE (tebeka-selenium-v0.9.9_GH0.tar.gz) = 55084 +SHA256 (vmihailenco-msgpack-v5.3.4_GH0.tar.gz) = 9972c3d0f882f22153a8d5697f7f1233fe8070fe3457bd98d1bfdce1682d4368 +SIZE (vmihailenco-msgpack-v5.3.4_GH0.tar.gz) = 35432 +SHA256 (vmihailenco-tagparser-v2.0.0_GH0.tar.gz) = 676b99c051fef68d1b0fb0385103de0e42a3ee556919b2b54ff5d3445bac56dd +SIZE (vmihailenco-tagparser-v2.0.0_GH0.tar.gz) = 3683 diff --git a/files/haproxy-spoe-auth.in b/files/haproxy-spoe-auth.in new file mode 100644 index 0000000..e53a3ee --- /dev/null +++ b/files/haproxy-spoe-auth.in @@ -0,0 +1,57 @@ +#!/bin/sh + +# PROVIDE: haproxy-spoe-auth +# REQUIRE: LOGIN +# KEYWORD: shutdown +# +# Add the following lines to /etc/rc.conf.local or /etc/rc.conf +# to enable this service: +# +# haproxy_spoe_auth_enable (bool): Set to NO by default. +# Set it to YES to enable haproxy-spoe-auth. +# haproxy_spoe_auth_user (string): Set user that haproxy-spoe-auth will run under +# Default is "haproxy". +# haproxy_spoe_auth_group (string): Set group that haproxy-spoe-auth will run under +# Default is "haproxy". +# haproxy_spoe_auth_config (string): Set config file +# Default is "/usr/local/etc/haproxy/haproxy-spoe-auth.yml". +# haproxy_spoe_auth_log (string): Set log file +# Default is "/var/log/haproxy-spoe-auth.log". + +. /etc/rc.subr + +name=haproxy_spoe_auth +rcvar=haproxy_spoe_auth_enable + +load_rc_config $name + +: ${haproxy_spoe_auth_enable:="NO"} +: ${haproxy_spoe_auth_user:="haproxy"} +: ${haproxy_spoe_auth_group:="haproxy"} +: ${haproxy_spoe_auth_config:="/usr/local/etc/haproxy/haproxy-spoe-auth.yml"} +: ${haproxy_spoe_auth_log:="/var/log/haproxy-spoe-auth.log"} +: ${haproxy_spoe_auth_restart_delay:="5"} + + +pidfile=/var/run/haproxy-spoe-auth.pid +command="/usr/sbin/daemon" +haproxy_spoe_auth_command="%%PREFIX%%/bin/haproxy-spoe-auth" +command_args="-rP ${pidfile} -S -R ${haproxy_spoe_auth_restart_delay} -T haproxy-spoe-auth \ + -o ${haproxy_spoe_auth_log} \ + ${haproxy_spoe_auth_command} -config ${haproxy_spoe_auth_config}" + +required_files="${haproxy_spoe_auth_config}" + +start_precmd=haproxy_spoe_auth_startprecmd + +haproxy_spoe_auth_startprecmd() +{ + if [ ! -e ${pidfile} ]; then + install -o ${haproxy_spoe_auth_user} -g ${haproxy_spoe_auth_group} /dev/null ${pidfile}; + fi + touch ${haproxy_spoe_auth_log} + chown ${haproxy_spoe_auth_user} ${haproxy_spoe_auth_log} +} + +run_rc_command "$1" + diff --git a/files/haproxy-spoe-auth.yml.sample b/files/haproxy-spoe-auth.yml.sample new file mode 100644 index 0000000..8893ddd --- /dev/null +++ b/files/haproxy-spoe-auth.yml.sample @@ -0,0 +1,52 @@ +server: + # The address the server will listen on + addr: 127.0.0.1:8081 + # The verbosity of the logs: info or debug + log_level: info + +# If set, the LDAP authenticator is enabled +ldap: + # The hostname an port to the ldap server + hostname: ldap + port: 389 + # The DN and password of the user to bind with in order to perform the search query to find the user + user_dn: cn=admin,dc=example,dc=com + password: password + # The base DN used for the search queries + base_dn: dc=example,dc=com + # The filter for the query searching for the user provided + user_filter: "(cn={login})" + +# If set, the OpenID Connect authenticator is enabled +oidc: + # The URL to the OpenID Connect provider. This is the URL hosting the discovery endpoint + provider_url: http://dex.example.com:9080/dex + # The client_id and client_secret of the app representing the SPOE agent + # The callback the OIDC server will redirect the user to once authentication is done + oauth2_callback_path: /oauth2/callback + # The path to the logout endpoint to redirect the user to. + oauth2_logout_path: /oauth2/logout + # The path the oidc client uses for a healthcheck + oauth2_healthcheck_path: /health + # The SPOE agent will open a dedicated port for the HTTP server handling the callback. This is the address the server listens on + callback_addr: ":5000" + + # Various properties of the cookie holding the ID Token of the user + cookie_name: authsession + cookie_secure: false + cookie_ttl_seconds: 3600 + # The secret used to sign the state parameter + signature_secret: myunsecuresecret + # The secret used to encrypt the cookie in order to guarantee the privacy of the data in case of leak + encryption_secret: anotherunsecuresecret + + # A mapping of client credentials per protected domain + clients: + app2.example.com: + client_id: app2-client + client_secret: app2-secret + redirect_url: http://app2.example.com:9080/oauth2/callback + app3.example.com: + client_id: app3-client + client_secret: app3-secret + redirect_url: http://app3.example.com:9080/oauth2/callback diff --git a/files/patch-internal__auth__authenticator_ldap.go b/files/patch-internal__auth__authenticator_ldap.go new file mode 100644 index 0000000..a01217e --- /dev/null +++ b/files/patch-internal__auth__authenticator_ldap.go @@ -0,0 +1,14 @@ +diff --git internal/auth/authenticator_ldap.go internal/auth/authenticator_ldap.go +index b42c0f6..147dca0 100644 +--- internal/auth/authenticator_ldap.go ++++ internal/auth/authenticator_ldap.go +@@ -137,6 +137,6 @@ func (la *LDAPAuthenticator) Authenticate(msg *spoe.Message) (bool, []spoe.Actio + return false, nil, err + } + +- logrus.Debug("User is authenticated") +- return true, nil, nil ++ logrus.Debugf("User %s is authenticated", username) ++ return true, []spoe.Action{SetAuthenticatedUsernameMessage(username)}, nil + } + diff --git a/files/patch-internal__auth__messages.go b/files/patch-internal__auth__messages.go new file mode 100644 index 0000000..69d1584 --- /dev/null +++ b/files/patch-internal__auth__messages.go @@ -0,0 +1,19 @@ +diff --git internal/auth/messages.go internal/auth/messages.go +index 1ca6706..bdf8cb3 100644 +--- internal/auth/messages.go ++++ internal/auth/messages.go +@@ -19,3 +19,13 @@ func BuildHasErrorMessage() spoe.ActionSetVar { + Value: true, + } + } ++ ++// SetAuthenticatedUsername build a message containing the authenticated username ++func SetAuthenticatedUsernameMessage(username string) spoe.ActionSetVar { ++ return spoe.ActionSetVar{ ++ Name: "auth_username", ++ Scope: spoe.VarScopeSession, ++ Value: username, ++ } ++} ++ + diff --git a/files/spoe-auth.conf.sample b/files/spoe-auth.conf.sample new file mode 100644 index 0000000..28390a6 --- /dev/null +++ b/files/spoe-auth.conf.sample @@ -0,0 +1,20 @@ +[spoe-auth] +spoe-agent auth-agents + messages try-auth-ldap + messages try-auth-oidc + + option var-prefix auth + + timeout hello 2s + timeout idle 2m + timeout processing 1s + + use-backend backend_spoe-agent + +spoe-message try-auth-ldap + args authorization=req.hdr(Authorization) + event on-frontend-http-request if { hdr_beg(host) -i app1.example.com } || { hdr_beg(host) -i app2.example.com } || { hdr_beg(host) -i app3.example.com } + +spoe-message try-auth-oidc + args arg_ssl=ssl_fc arg_host=req.hdr(Host) arg_pathq=pathq arg_cookie=req.cook(authsession) + event on-frontend-http-request if { hdr_beg(host) -i app1.example.com } || { hdr_beg(host) -i app2.example.com } || { hdr_beg(host) -i app3.example.com } diff --git a/pkg-descr b/pkg-descr new file mode 100644 index 0000000..9148579 --- /dev/null +++ b/pkg-descr @@ -0,0 +1,3 @@ +Plugin for authorizing users against LDAP + +WWW: https://github.com/criteo/haproxy-spoe-auth diff --git a/pkg-message b/pkg-message new file mode 100644 index 0000000..71895a1 --- /dev/null +++ b/pkg-message @@ -0,0 +1,9 @@ +[ +{ type: install + message: <