diff --git a/config/install_agent.tpl b/config/install_agent.tpl index e130b6c0..75997b77 100644 --- a/config/install_agent.tpl +++ b/config/install_agent.tpl @@ -220,28 +220,42 @@ function install_config() { echo "[agent] waiting generate config" port={{port}} console_endpoint="{{console_endpoint}}" - - location_ep=$(echo "$console_endpoint" | sed -nE 's/(.*):\/\/([^/:]*):?([0-9]*).*/\1:\/\/\2:\3/p') - server=${register_server:-$location_ep} + + # Keep the console base path in managed server URLs so register/sync/exchange + # requests still work when Console is deployed under a non-root path prefix. + server=${register_server:-$console_endpoint} echo "[agent] agent listening port $port, will register to console endpoint [ $server ]" cat < ${install_dir}/agent.yml configs.auto_reload: true env: - API_BINDING: "0.0.0.0:${port}" + WEB_BINDING: "0.0.0.0:${port}" + MANAGED: true + REMOTE_CONFIG_SERVERS: ["${server}"] + REMOTE_CONFIG_INTERVAL: "10s" + SECURITY_ENABLED: true + SECURITY_MANAGED_ENABLED: false path.data: data path.logs: log -path.configs: config +path.configs: "config" resource_limit.cpu.max_num_of_cpus: 1 -resource_limit.memory.max_in_bytes: 533708800 + +resource_limit: + memory: + max_in_bytes: 533708800 #50MB + +task: + max_concurrent_tasks: 3 stats: include_storage_stats_in_api: false elastic: skip_init_metadata_on_start: true + metadata_refresh: + enabled: false health_check: enabled: true interval: 60s @@ -252,24 +266,33 @@ elastic: disk_queue: max_msg_size: 20485760 max_bytes_per_file: 20485760 - max_used_bytes: 524288000 + max_used_bytes: 524288000 #500MB retention.max_num_of_local_files: 1 compress: - idle_threshold: 0 + idle_threshold: 1 num_of_files_decompress_ahead: 0 segment: enabled: true api: + disable_api_directory: true + enabled: false + +web: + embedding_api: false enabled: true - tls: - enabled: false - cert_file: "config/client.crt" - key_file: "config/client.key" - ca_file: "config/ca.crt" - skip_insecure_verify: false network: - binding: \$[[env.API_BINDING]] + binding: \$[[env.WEB_BINDING]] + ui: + vfs: true + security: + enabled: \$[[env.SECURITY_ENABLED]] + managed: \$[[env.SECURITY_MANAGED_ENABLED]] + +agent: + +metrics: + enabled: true badger: value_threshold: 1024 @@ -279,11 +302,13 @@ badger: configs: #for managed client's setting - managed: true # managed by remote servers + managed: \$[[env.MANAGED]] # managed by remote servers panic_on_config_error: false #ignore config error - interval: "10s" - servers: # config servers - - "${server}" + allow_generated_metrics_tasks: false # allow auto-generated metrics tasks (e.g. k8s) + interval: \$[[env.REMOTE_CONFIG_INTERVAL]] + servers: \$[[env.REMOTE_CONFIG_SERVERS]] # config servers + manager: + access_token: '$[[keystore.CONFIGS_MANAGER_ACCESS_TOKEN]]' soft_delete: false max_backup_files: 5 tls: #for mTLS connection with config servers @@ -298,6 +323,22 @@ node: EOF } +function install_keystore() { + access_token="{{access_token}}" + agent_svc=${install_dir}/${program_name}-${file_ext%%.*} + + if [[ -z "${access_token}" ]]; then + echo "Error: access token is empty." >&2 + exit 1 + fi + + echo "[agent] waiting write access token to keystore" + ( + cd "${install_dir}" + printf '%s' "${access_token}" | "./$(basename "${agent_svc}")" keystore add CONFIGS_MANAGER_ACCESS_TOKEN --stdin --force >/dev/null + ) +} + function uninstall_service() { agent_svc=${install_dir}/${program_name}-${file_ext%%.*} chmod 755 $agent_svc @@ -351,6 +392,7 @@ function main() { install_binary install_certs install_config + install_keystore uninstall_service install_service diff --git a/docs/content.en/docs/release-notes/_index.md b/docs/content.en/docs/release-notes/_index.md index 8b3c962c..35292956 100644 --- a/docs/content.en/docs/release-notes/_index.md +++ b/docs/content.en/docs/release-notes/_index.md @@ -11,7 +11,9 @@ Information about release notes of INFINI Console is provided here. ### ❌ Breaking changes ### 🚀 Features ### 🐛 Bug fix +- fix: secure managed Agent install, registration, token exchange, and config sync with scoped API tokens instead of the previous unauthenticated path ### ✈️ Improvements +- chore: align managed Agent bootstrap with the web endpoint and simplify the post-register token exchange flow and credential naming - chore: add native Go modules support for Console build and PR checks ## 1.30.2 (2026-03-16) diff --git a/go.mod b/go.mod index d8bd6fd6..e2f52e1e 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,6 @@ replace github.com/cihub/seelog => ../framework/lib/seelog require ( github.com/Knetic/govaluate v3.0.0+incompatible github.com/buger/jsonparser v1.2.0 - github.com/cihub/seelog v0.0.0-00010101000000-000000000000 github.com/crewjam/saml v0.5.1 github.com/emirpasic/gods v1.18.1 github.com/golang-jwt/jwt/v4 v4.5.2 @@ -23,103 +22,24 @@ require ( golang.org/x/oauth2 v0.36.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/yaml.v2 v2.4.0 - infini.sh/framework v0.0.0-00010101000000-000000000000 ) require ( - github.com/Azure/go-ntlmssp v0.1.0 // indirect - github.com/OneOfOne/xxhash v1.2.8 // indirect - github.com/RoaringBitmap/roaring v1.9.4 // indirect - github.com/andybalholm/brotli v1.1.1 // indirect - github.com/arl/statsviz v0.6.0 // indirect github.com/beevik/etree v1.5.0 // indirect - github.com/bits-and-blooms/bitset v1.12.0 // indirect - github.com/bkaradzic/go-lz4 v1.0.0 // indirect - github.com/caddyserver/certmagic v0.25.3 // indirect - github.com/caddyserver/zerossl v0.1.5 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dgraph-io/badger/v4 v4.7.0 // indirect - github.com/dgraph-io/ristretto v0.2.0 // indirect - github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/ebitengine/purego v0.10.0 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect - github.com/go-ldap/ldap/v3 v3.4.13 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect - github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/gookit/filter v1.2.3 // indirect - github.com/gookit/goutil v0.7.1 // indirect - github.com/gookit/validate v1.5.6 // indirect - github.com/gorilla/context v1.1.2 // indirect - github.com/gorilla/securecookie v1.1.2 // indirect - github.com/gorilla/sessions v1.4.0 // indirect - github.com/gorilla/websocket v1.5.3 // indirect - github.com/hashicorp/go-version v1.7.0 // indirect - github.com/jmoiron/jsonq v0.0.0-20150511023944-e874b168d07e // indirect github.com/jonboulle/clockwork v0.2.2 // indirect - github.com/josharian/intern v1.0.0 // indirect - github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect - github.com/kardianos/service v1.2.2 // indirect - github.com/klauspost/compress v1.18.0 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect - github.com/libdns/libdns v1.1.1 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect - github.com/mailru/easyjson v0.9.0 // indirect github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect - github.com/mattn/go-isatty v0.0.22 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/mholt/acmez/v3 v3.1.6 // indirect - github.com/miekg/dns v1.1.72 // indirect - github.com/mschoch/smat v0.2.0 // indirect - github.com/pierrec/lz4/v4 v4.1.22 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/rivo/uniseg v0.2.0 // indirect - github.com/rs/cors v1.11.1 // indirect - github.com/rs/xid v1.6.0 // indirect github.com/russellhaering/goxmldsig v1.4.0 // indirect - github.com/ryanuber/go-glob v1.0.0 // indirect - github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 // indirect github.com/segmentio/asm v1.1.3 // indirect - github.com/shirou/gopsutil/v4 v4.26.3 // indirect - github.com/tklauser/go-sysconf v0.3.16 // indirect - github.com/tklauser/numcpus v0.11.0 // indirect - github.com/twmb/franz-go v1.18.1 // indirect - github.com/twmb/franz-go/pkg/kadm v1.16.0 // indirect - github.com/twmb/franz-go/pkg/kmsg v1.11.2 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect - github.com/zeebo/blake3 v0.2.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel v1.35.0 // indirect - go.opentelemetry.io/otel/metric v1.35.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.1 // indirect - go.uber.org/zap/exp v0.3.0 // indirect - golang.org/x/mod v0.35.0 // indirect golang.org/x/net v0.54.0 // indirect - golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.45.0 // indirect - golang.org/x/term v0.43.0 // indirect - golang.org/x/text v0.37.0 // indirect - golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.44.0 // indirect google.golang.org/appengine v1.6.6 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect - gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index deb03063..236a18aa 100644 --- a/go.sum +++ b/go.sum @@ -1,77 +1,18 @@ -code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE= -code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM= -github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A= -github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= github.com/Knetic/govaluate v3.0.0+incompatible h1:7o6+MAPhYTCF0+fdvoz1xDedhRb4f6s9Tn1Tt7/WTEg= github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= -github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= -github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= -github.com/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv2QzDdQ= -github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90= -github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI= -github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= -github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= -github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= -github.com/arl/statsviz v0.6.0 h1:jbW1QJkEYQkufd//4NDYRSNBpwJNrdzPahF7ZmoGdyE= -github.com/arl/statsviz v0.6.0/go.mod h1:0toboo+YGSUXDaS4g1D5TVS4dXs7S7YYT5J/qnW2h8s= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/beevik/etree v1.5.0 h1:iaQZFSDS+3kYZiGoc9uKeOkUY3nYMXOKLl6KIJxiJWs= github.com/beevik/etree v1.5.0/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs= -github.com/bits-and-blooms/bitset v1.12.0 h1:U/q1fAF7xXRhFCrhROzIfffYnu+dlS38vCZtmFVPHmA= -github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/bkaradzic/go-lz4 v1.0.0 h1:RXc4wYsyz985CkXXeX04y4VnZFGG8Rd43pRaHsOXAKk= -github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4= github.com/buger/jsonparser v1.2.0 h1:4EFcvK1kD4jyj6YqNK6skK6w+y7FHHBR+XBCtxwu/6g= github.com/buger/jsonparser v1.2.0/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/caddyserver/certmagic v0.25.3 h1:mGf5ba8F7xA4c5jfDZZbK2buY1VEkbnwpMDixaju94A= -github.com/caddyserver/certmagic v0.25.3/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA= -github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE= -github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/crewjam/saml v0.5.1 h1:g+mfp0CrLuLRZCK793PgJcZeg5dS/0CDwoeAX2zcwNI= github.com/crewjam/saml v0.5.1/go.mod h1:r0fDkmFe5URDgPrmtH0IYokva6fac3AUdstiPhyEolQ= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgraph-io/badger/v4 v4.7.0 h1:Q+J8HApYAY7UMpL8d9owqiB+odzEc0zn/aqOD9jhc6Y= -github.com/dgraph-io/badger/v4 v4.7.0/go.mod h1:He7TzG3YBy3j4f5baj5B7Zl2XyfNe5bl4Udl0aPemVA= -github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE= -github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= -github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= -github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= -github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= -github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= -github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= -github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= -github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= -github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ= -github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= -github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -79,236 +20,60 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/gomarkdown/markdown v0.0.0-20260417124207-7d523f7318df h1:Mwihr/o+v4L5h56rwHLOE20+hh7Okhwno5BHz3zDuao= github.com/gomarkdown/markdown v0.0.0-20260417124207-7d523f7318df/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= -github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= -github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gookit/filter v1.2.3 h1:Zo7cBOtsVzAoa/jtf+Ury6zlsbJXqInFdUpbbnB2vMM= -github.com/gookit/filter v1.2.3/go.mod h1:nFLJcOV8dRgS1iiX23gUQgmHUhpuS40qCvAGgIvA1pM= -github.com/gookit/goutil v0.7.1 h1:AaFJPN9mrdeYBv8HOybri26EHGCC34WJVT7jUStGJsI= -github.com/gookit/goutil v0.7.1/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU= -github.com/gookit/validate v1.5.6 h1:D6vbSZzreuKYpeeXm5FDDEJy3K5E4lcWsQE4saSMZbU= -github.com/gookit/validate v1.5.6/go.mod h1:WYEHndRNepIIkM+6CtgEX9MQ9ToIQRhXxmz5oLHF/fc= -github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= -github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= -github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= -github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= -github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= -github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= -github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= -github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= -github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= -github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= -github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= -github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= -github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= -github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= -github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= -github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= -github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= -github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= -github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/jmoiron/jsonq v0.0.0-20150511023944-e874b168d07e h1:ZZCvgaRDZg1gC9/1xrsgaJzQUCQgniKtw0xjWywWAOE= -github.com/jmoiron/jsonq v0.0.0-20150511023944-e874b168d07e/go.mod h1:+rHyWac2R9oAZwFe1wGY2HBzFJJy++RHBg1cU23NkD8= github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= -github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= -github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX60= -github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/letsencrypt/challtestsrv v1.4.2 h1:0ON3ldMhZyWlfVNYYpFuWRTmZNnyfiL9Hh5YzC3JVwU= -github.com/letsencrypt/challtestsrv v1.4.2/go.mod h1:GhqMqcSoeGpYd5zX5TgwA6er/1MbWzx/o7yuuVya+Wk= -github.com/letsencrypt/pebble/v2 v2.10.0 h1:Wq6gYXlsY6ubqI3hhxsTzdyotvfdjFBxuwYqCLCnj/U= -github.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ= -github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U= -github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= -github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= -github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= -github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk= -github.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY= -github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= -github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= -github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= -github.com/nsqio/nsq v1.3.0 h1:v7NtyO844ieTIOCQEqQ7IUSSi1ImhgrTTto1rgIYGEU= -github.com/nsqio/nsq v1.3.0/go.mod h1:RxNr6UC0kSkNF44LnJrlN3U3CQnQGTXk+QKfSZLzqvc= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= -github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= -github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= -github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= -github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/r3labs/diff/v2 v2.15.1 h1:EOrVqPUzi+njlumoqJwiS/TgGgmZo83619FNDB9xQUg= github.com/r3labs/diff/v2 v2.15.1/go.mod h1:I8noH9Fc2fjSaMxqF3G2lhDdC0b+JXCfyx85tWFM9kc= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= -github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= -github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/russellhaering/goxmldsig v1.4.0 h1:8UcDh/xGyQiyrW+Fq5t8f+l2DLB1+zlhYzkPUJ7Qhys= github.com/russellhaering/goxmldsig v1.4.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= -github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= -github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= -github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= -github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= -github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 h1:qIQ0tWF9vxGtkJa24bR+2i53WBCz1nW/Pc47oVYauC4= -github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= -github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= -github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= -github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= -github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= -github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= -github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= -github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= -github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= -github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= -github.com/twmb/franz-go v1.18.1 h1:D75xxCDyvTqBSiImFx2lkPduE39jz1vaD7+FNc+vMkc= -github.com/twmb/franz-go v1.18.1/go.mod h1:Uzo77TarcLTUZeLuGq+9lNpSkfZI+JErv7YJhlDjs9M= -github.com/twmb/franz-go/pkg/kadm v1.16.0 h1:STMs1t5lYR5mR974PSiwNzE5TvsosByTp+rKXLOhAjE= -github.com/twmb/franz-go/pkg/kadm v1.16.0/go.mod h1:MUdcUtnf9ph4SFBLLA/XxE29rvLhWYLM9Ygb8dfSCvw= -github.com/twmb/franz-go/pkg/kmsg v1.11.2 h1:hIw75FpwcAjgeyfIGFqivAvwC5uNIOWRGvQgZhH4mhg= -github.com/twmb/franz-go/pkg/kmsg v1.11.2/go.mod h1:CFfkkLysDNmukPYhGzuUcDtf46gQSqCZHMW1T4Z+wDE= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= -github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= -github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= -github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= -github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= -github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= -github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= -go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= -go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= @@ -320,13 +85,9 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk= -gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= @@ -335,4 +96,3 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= diff --git a/main.go b/main.go index 5e5d2b01..c9de2e34 100644 --- a/main.go +++ b/main.go @@ -38,7 +38,6 @@ import ( elastic2 "infini.sh/framework/modules/elastic" _ "time/tzdata" - log "github.com/cihub/seelog" "infini.sh/console/config" "infini.sh/console/model" "infini.sh/console/model/alerting" @@ -52,6 +51,7 @@ import ( "infini.sh/framework/core/elastic" "infini.sh/framework/core/env" "infini.sh/framework/core/global" + "infini.sh/framework/core/log" "infini.sh/framework/core/module" "infini.sh/framework/core/orm" task1 "infini.sh/framework/core/task" diff --git a/model/instance.go b/model/instance.go index 80b26b58..9c422797 100644 --- a/model/instance.go +++ b/model/instance.go @@ -31,7 +31,11 @@ import ( "context" "errors" "fmt" + "infini.sh/framework/core/credential" + common2 "infini.sh/framework/modules/elastic/common" + "infini.sh/framework/modules/security/access_token" "net/http" + "strings" "time" "infini.sh/framework/core/model" @@ -157,9 +161,9 @@ func (inst *TaskWorker) TryConnectWithTimeout(duration time.Duration) error { } func (inst *TaskWorker) doRequest(req *util.Request, resBody interface{}) error { - if inst.BasicAuth != nil && inst.BasicAuth.Username != "" { - req.SetBasicAuth(inst.BasicAuth.Username, inst.BasicAuth.Password.Get()) - } + + _ = ApplyAuthFromInstance(&inst.Instance, req) + result, err := util.ExecuteRequest(req) if err != nil { return err @@ -172,3 +176,45 @@ func (inst *TaskWorker) doRequest(req *util.Request, resBody interface{}) error } return nil } + +func ApplyAuthFromInstance(inst *model.Instance, req *util.Request) error { + if inst.BasicAuth != nil && inst.BasicAuth.Username != "" { + req.SetBasicAuth(inst.BasicAuth.Username, inst.BasicAuth.Password.Get()) + } else if inst.AccessToken != "" { + req.AddHeader(model.API_TOKEN, inst.AccessToken) + } else if cred := inst.GetSystemString(model.CredentialIDSystemKey); cred != "" { + //try credential last + return ApplyCredentialToRequest(req, cred) + } + return nil +} + +func ApplyCredentialToRequest(req *util.Request, credentialID string) error { + if req == nil || strings.TrimSpace(credentialID) == "" { + return nil + } + + cred, err := common2.GetCredential(credentialID) + if err != nil { + return err + } + + switch cred.Type { + case credential.BasicAuth: + auth, err := cred.DecodeBasicAuth() + if err != nil { + return err + } + req.SetBasicAuth(auth.Username, auth.Password.Get()) + case credential.AccessToken: + token, err := cred.DecodeAccessToken() + if err != nil { + return err + } + req.AddHeader(access_token.HeaderAPIToken, token.AccessToken.Get()) + default: + return fmt.Errorf("unsupported credential type [%s]", cred.Type) + } + + return nil +} diff --git a/modules/elastic/api/manage.go b/modules/elastic/api/manage.go index 0928e00e..eb0a1dc6 100644 --- a/modules/elastic/api/manage.go +++ b/modules/elastic/api/manage.go @@ -139,8 +139,8 @@ func saveBasicAuthToCredential(name string, auth *model.BasicAuth) (string, erro Name: name, Type: credential.BasicAuth, Tags: []string{"ES"}, - Payload: map[string]interface{}{ - "basic_auth": map[string]interface{}{ + Payload: map[credential.CredentialType]interface{}{ + credential.BasicAuth: map[string]interface{}{ "username": auth.Username, "password": auth.Password.Get(), }, diff --git a/modules/elastic/api/v1/manage.go b/modules/elastic/api/v1/manage.go index ce2eb654..40edb9a0 100644 --- a/modules/elastic/api/v1/manage.go +++ b/modules/elastic/api/v1/manage.go @@ -113,8 +113,8 @@ func saveBasicAuthToCredential(conf *elastic.ElasticsearchConfig) (string, error Name: conf.Name, Type: credential.BasicAuth, Tags: []string{"ES"}, - Payload: map[string]interface{}{ - "basic_auth": map[string]interface{}{ + Payload: map[credential.CredentialType]interface{}{ + credential.BasicAuth: map[string]interface{}{ "username": conf.BasicAuth.Username, "password": conf.BasicAuth.Password.Get(), }, diff --git a/modules/security/credential/api/credential.go b/modules/security/credential/api/credential.go index 6f5c0f10..b2440b92 100644 --- a/modules/security/credential/api/credential.go +++ b/modules/security/credential/api/credential.go @@ -132,6 +132,22 @@ func (h *APIHandler) updateCredential(w http.ResponseWriter, req *http.Request, } } } + case credential.AccessToken: + var oldToken string + if oldParams, ok := obj.Payload[newObj.Type].(map[string]interface{}); ok { + if token, ok := oldParams["access_token"].(string); ok { + oldToken = token + } else { + http.Error(w, fmt.Sprintf("invalid access token of credential [%s]", obj.ID), http.StatusInternalServerError) + return + } + } + if params, ok := newObj.Payload[newObj.Type].(map[string]interface{}); ok { + if token, ok := params["access_token"].(string); ok && token != oldToken { + obj.Payload = newObj.Payload + encodeChanged = true + } + } default: h.WriteError(w, fmt.Sprintf("unsupport credential type [%s]", newObj.Type), http.StatusInternalServerError) return @@ -276,6 +292,7 @@ func (h *APIHandler) searchCredential(w http.ResponseWriter, req *http.Request, for _, hit := range searchRes.Hits.Hits { delete(hit.Source, "encrypt") util.MapStr(hit.Source).Delete("payload.basic_auth.password") + util.MapStr(hit.Source).Delete("payload.access_token.access_token") } } @@ -295,6 +312,15 @@ func (h *APIHandler) getCredential(w http.ResponseWriter, req *http.Request, ps }, http.StatusNotFound) return } - util.MapStr(obj.Payload).Delete("basic_auth.password") + payloadStr := make(util.MapStr, len(obj.Payload)) + for k, v := range obj.Payload { + payloadStr[string(k)] = v + } + payloadStr.Delete("basic_auth.password") + payloadStr.Delete("access_token.access_token") + obj.Payload = make(map[credential.CredentialType]interface{}, len(payloadStr)) + for k, v := range payloadStr { + obj.Payload[credential.CredentialType(k)] = v + } h.WriteGetOKJSON(w, id, obj) } diff --git a/plugin/api/email/server.go b/plugin/api/email/server.go index 02b942cd..8d515997 100644 --- a/plugin/api/email/server.go +++ b/plugin/api/email/server.go @@ -129,7 +129,7 @@ func saveBasicAuthToCredential(srv *model.EmailServer) (string, error) { Type: credential.BasicAuth, Tags: []string{"Email"}, Payload: map[string]interface{}{ - "basic_auth": map[string]interface{}{ + string(credential.BasicAuth): map[string]interface{}{ "username": srv.Auth.Username, "password": srv.Auth.Password.Get(), }, diff --git a/plugin/managed/server/config.go b/plugin/managed/server/config.go index e872c124..02d8b912 100644 --- a/plugin/managed/server/config.go +++ b/plugin/managed/server/config.go @@ -164,6 +164,10 @@ func (h APIHandler) syncConfigs(w http.ResponseWriter, req *http.Request, ps htt h.WriteError(w, err.Error(), http.StatusInternalServerError) } + if writeTokenAuthError(h, w, validateSyncRequestAuth(req, &obj.Client)) { + return + } + if global.Env().IsDebug { log.Trace("request:", util.MustToJSON(obj)) } diff --git a/plugin/managed/server/credential.go b/plugin/managed/server/credential.go new file mode 100644 index 00000000..93e377d7 --- /dev/null +++ b/plugin/managed/server/credential.go @@ -0,0 +1,294 @@ +package server + +import ( + "fmt" + model2 "infini.sh/console/model" + "net/http" + "strings" + + "infini.sh/framework/core/credential" + "infini.sh/framework/core/model" + "infini.sh/framework/core/orm" + "infini.sh/framework/core/util" +) + +const instanceAccessTokenKey = "access_token" +const agentAccessTokenKey = "agent_api_credential_id" +const managerAPICredentialIDKey = "manager_api_credential_id" + +// Keep reading the old key so existing managed instances continue to work +// before their next save migrates the field name. +const legacyManagerSyncCredentialIDKey = "manager_sync_credential_id" + +func clearInstanceAccessToken(instance *model.Instance) { + deleteInstanceSystemString(instance, instanceAccessTokenKey) +} + +func getInstanceCredentialName(instance *model.Instance) string { + if instance == nil { + return "agent" + } + if name := strings.TrimSpace(instance.Name); name != "" { + return name + } + if id := strings.TrimSpace(instance.ID); id != "" { + return id + } + return "agent" +} + +func getInstanceSystemString(instance *model.Instance, key string) string { + if instance == nil || instance.System == nil { + return "" + } + + value, ok := instance.GetSystemValue(key) + if !ok { + return "" + } + + text, ok := value.(string) + if !ok { + return "" + } + + return strings.TrimSpace(text) +} + +func setInstanceSystemString(instance *model.Instance, key, value string) { + if instance == nil || strings.TrimSpace(value) == "" { + return + } + instance.SetSystemValue(key, value) +} + +func deleteInstanceSystemString(instance *model.Instance, key string) { + if instance == nil || instance.System == nil { + return + } + delete(instance.System, key) + if len(instance.System) == 0 { + instance.System = nil + } +} + +func loadExistingInstance(instanceID string) (*model.Instance, error) { + if strings.TrimSpace(instanceID) == "" { + return nil, nil + } + + instance := &model.Instance{} + instance.ID = instanceID + exists, err := orm.Get(instance) + if err != nil { + return nil, err + } + if !exists { + return nil, nil + } + return instance, nil +} + +func preserveManagedCredentialIDs(instance, oldInst *model.Instance) { + if instance == nil || oldInst == nil { + return + } + + //get previous saved credential info + if id := oldInst.GetSystemString(model.CredentialIDSystemKey); id != "" { + instance.SetSystemValue(model.CredentialIDSystemKey, id) + } + + if getAgentAccessToken(instance) == "" { + setAgentAPICredentialID(instance, getAgentAccessToken(oldInst)) + } + + if getManagerAPICredentialID(instance) == "" { + setManagerAPICredentialID(instance, getManagerAPICredentialID(oldInst)) + } +} + +func upsertAccessToken(credentialID, name string, tags []string, accessToken string) (string, error) { + cred := credential.Credential{} + exists := false + if credentialID != "" { + cred.ID = credentialID + var err error + exists, err = orm.Get(&cred) + if err != nil { + return "", err + } + } + + cred.Name = name + cred.Type = credential.AccessToken + cred.Tags = tags + cred.Payload = map[credential.CredentialType]interface{}{ + credential.AccessToken: map[string]interface{}{ + "access_token": accessToken, + }, + } + + if err := cred.Encode(); err != nil { + return "", err + } + + ctx := &orm.Context{Refresh: orm.WaitForRefresh} + if exists { + cred.Invalid = false + if err := orm.Update(ctx, &cred); err != nil { + return "", err + } + return cred.ID, nil + } + + if err := orm.Create(ctx, &cred); err != nil { + return "", err + } + return cred.ID, nil +} + +func upsertAccessTokenToCredential(instance *model.Instance, accessToken string) error { + cred := credential.Credential{} + exists := false + if credentialID := instance.GetSystemString(model.CredentialIDSystemKey); credentialID != "" { + cred.ID = credentialID + var err error + exists, err = orm.Get(&cred) + if err != nil { + return err + } + } + + cred.Name = fmt.Sprintf("access_token for instance: %v/%v", instance.Name, instance.ID) + cred.Type = credential.AccessToken + + cred.Payload = map[credential.CredentialType]interface{}{ + credential.AccessToken: map[string]interface{}{ + "access_token": accessToken, + }, + } + + if err := cred.Encode(); err != nil { + return err + } + + ctx := &orm.Context{Refresh: orm.WaitForRefresh} + if exists { + cred.Invalid = false + if err := orm.Update(ctx, &cred); err != nil { + return err + } + instance.SetSystemValue(model.CredentialIDSystemKey, cred.ID) + return nil + } + + if err := orm.Create(ctx, &cred); err != nil { + return err + } + instance.SetSystemValue(model.CredentialIDSystemKey, cred.ID) + return nil +} + +func getAgentAccessToken(instance *model.Instance) string { + if instance == nil { + return "" + } + if accessToken := getInstanceSystemString(instance, agentAccessTokenKey); accessToken != "" { + return accessToken + } + return instance.AccessToken +} + +func setAgentAPICredentialID(instance *model.Instance, credentialID string) { + setInstanceSystemString(instance, agentAccessTokenKey, credentialID) +} + +func getManagerAPICredentialID(instance *model.Instance) string { + if credentialID := getInstanceSystemString(instance, managerAPICredentialIDKey); credentialID != "" { + return credentialID + } + return getInstanceSystemString(instance, legacyManagerSyncCredentialIDKey) +} + +func setManagerAPICredentialID(instance *model.Instance, credentialID string) { + setInstanceSystemString(instance, managerAPICredentialIDKey, credentialID) + deleteInstanceSystemString(instance, legacyManagerSyncCredentialIDKey) +} + +func upsertInstanceAgentAPICredential(instance *model.Instance, accessToken string) (string, error) { + if instance == nil { + return "", fmt.Errorf("instance is nil") + } + if strings.TrimSpace(accessToken) == "" { + return "", fmt.Errorf("access token is empty") + } + + e := upsertAccessTokenToCredential(instance, accessToken) + return "", e +} + +func upsertInstanceManagerAPICredential(instance *model.Instance, accessToken string) (string, error) { + if instance == nil { + return "", fmt.Errorf("instance is nil") + } + if strings.TrimSpace(accessToken) == "" { + return "", fmt.Errorf("access token is empty") + } + + //return upsertAccessTokenToCredential( + // getManagerAPICredentialID(instance), + // fmt.Sprintf("%s manager api", getInstanceCredentialName(instance)), + // []string{"agent", "managed", "manager_api", "access_token"}, + // accessToken, + //) + e := upsertAccessTokenToCredential(instance, accessToken) + return "", e +} + +func getInstanceByEndpoint(endpoint string) (*model.Instance, error) { + if strings.TrimSpace(endpoint) == "" { + return nil, nil + } + + queryDSL := util.MapStr{ + "size": 1, + "query": util.MapStr{ + "bool": util.MapStr{ + "must": []util.MapStr{ + { + "term": util.MapStr{ + "endpoint": endpoint, + }, + }, + }, + }, + }, + } + + q := orm.Query{RawQuery: util.MustToJSONBytes(queryDSL)} + err, res := orm.Search(&model.Instance{}, &q) + if err != nil { + return nil, err + } + if len(res.Result) == 0 { + return nil, nil + } + + obj := &model.Instance{} + util.MustFromJSONBytes(util.MustToJSONBytes(res.Result[0]), obj) + return obj, nil +} + +func prepareProxyAgentRequest(endpoint string, req *util.Request) error { + instance, err := getInstanceByEndpoint(endpoint) + if err != nil || instance == nil { + return err + } + + return model2.ApplyAuthFromInstance(instance, req) +} + +func isAuthorizedStatus(code int) bool { + return code == http.StatusOK +} diff --git a/plugin/managed/server/instance.go b/plugin/managed/server/instance.go index 0fe130b3..b9a995aa 100644 --- a/plugin/managed/server/instance.go +++ b/plugin/managed/server/instance.go @@ -30,19 +30,23 @@ package server import ( "context" "fmt" - "infini.sh/framework/core/event" - "infini.sh/framework/core/global" - "infini.sh/framework/core/task" + model2 "infini.sh/console/model" + "infini.sh/framework/core/security" + "infini.sh/framework/modules/security/access_token" "net/http" "strconv" "strings" "time" - log "github.com/cihub/seelog" + "infini.sh/framework/core/event" + "infini.sh/framework/core/global" + "infini.sh/framework/core/task" + "infini.sh/console/core/security/enum" "infini.sh/framework/core/api" httprouter "infini.sh/framework/core/api/router" elastic2 "infini.sh/framework/core/elastic" + "infini.sh/framework/core/log" "infini.sh/framework/core/model" "infini.sh/framework/core/orm" "infini.sh/framework/core/util" @@ -57,6 +61,7 @@ var instanceSecrets = map[string][]common.Secrets{} //map instance->secrets TODO func init() { //for public usage, agent can report self to server, usually need to enroll by manager api.HandleAPIMethod(api.POST, common.REGISTER_API, handler.registerInstance) //client register self to config servers + api.HandleAPIMethod(api.POST, instanceTokenExchangeAPI, handler.exchangeInstanceToken) //for public usage, get install script api.HandleAPIMethod(api.GET, GET_INSTALL_SCRIPT_API, handler.getInstallScript) @@ -85,7 +90,6 @@ func init() { } func (h APIHandler) registerInstance(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { - var obj = &model.Instance{} err := h.DecodeJSON(req, obj) if err != nil { @@ -97,13 +101,44 @@ func (h APIHandler) registerInstance(w http.ResponseWriter, req *http.Request, p return } - oldInst := &model.Instance{} - oldInst.ID = obj.ID - exists, err := orm.Get(oldInst) - if exists { + if writeTokenAuthError(h, w, validateRegisterRequestAuth(req, obj)) { + return + } + + //1. client report instance info include agent's access_token + //2. server received the instance info, save access token to credential db + //3. server generate a access_token return to client + + oldInst, err := loadExistingInstance(obj.ID) + if err != nil { + h.WriteError(w, err.Error(), http.StatusInternalServerError) + return + } + if oldInst != nil { obj.Created = oldInst.Created + + //get previous saved credential info + if id := oldInst.GetSystemString(model.CredentialIDSystemKey); id != "" { + obj.SetSystemValue(model.CredentialIDSystemKey, id) + } + + if obj.AccessToken != "" { + //save instance's reported agent to console's crendential + err := upsertAccessTokenToCredential(obj, obj.AccessToken) + if err != nil { + panic(err) + } + } + } - err = orm.Save(nil, obj) + + //cleanup instance's sensitive data + obj.AccessToken = "" + obj.BasicAuth = nil + + clearInstanceAccessToken(obj) + + err = orm.Save(orm.NewContextWithParent(req.Context()), obj) if err != nil { h.WriteError(w, err.Error(), http.StatusInternalServerError) return @@ -111,7 +146,15 @@ func (h APIHandler) registerInstance(w http.ResponseWriter, req *http.Request, p log.Infof("register instance: %v[%v], %v", obj.Name, obj.ID, obj.Endpoint) - h.WriteAckOKJSON(w) + permissions := []security.PermissionKey{} + //TODO add permission keys for config client's api permissions + + res, err := access_token.CreateAPIToken(nil, fmt.Sprintf("access_token for instance: %v(%v)", obj.Name, obj.ID), "for_instance", -1, permissions) + if err != nil { + panic(err) + } + + api.WriteAckJSON(w, true, 200, res) } func (h APIHandler) enrollInstance(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { @@ -438,7 +481,7 @@ func (h *APIHandler) clearInstanceByAppName(appName string) error { // check whether the instance is still online for _, instanceID := range toRemoveIDs { if inst, ok := instsCache[instanceID]; ok { - _, err = h.getInstanceInfo(inst.Endpoint, inst.BasicAuth) + err = h.checkInstanceInfo(inst) if err == nil { // Skip online instance, do not append to filtered list continue @@ -559,9 +602,8 @@ func (h *APIHandler) proxy(w http.ResponseWriter, req *http.Request, ps httprout Context: ctx, Body: reqBody, } - if obj.BasicAuth != nil { - req1.SetBasicAuth(obj.BasicAuth.Username, obj.BasicAuth.Password.Get()) - } + + _ = model2.ApplyAuthFromInstance(obj, req1) res, err := ProxyAgentRequest("runtime", obj.GetEndpoint(), req1, nil) if err != nil { @@ -592,6 +634,26 @@ func (h *APIHandler) getInstanceInfo(endpoint string, basicAuth *model.BasicAuth } +func (h *APIHandler) checkInstanceInfo(instance *model.Instance) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + req1 := &util.Request{ + Method: http.MethodGet, + Path: "/_info", + Context: ctx, + } + + _ = model2.ApplyAuthFromInstance(instance, req1) + + obj := &model.Instance{} + _, err := ProxyAgentRequest("runtime", instance.Endpoint, req1, obj) + if err != nil { + return err + } + return err + +} + func (h *APIHandler) tryConnect(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { var reqBody = struct { Endpoint string `json:"endpoint"` diff --git a/plugin/managed/server/managed_token.go b/plugin/managed/server/managed_token.go new file mode 100644 index 00000000..27b22efc --- /dev/null +++ b/plugin/managed/server/managed_token.go @@ -0,0 +1,131 @@ +package server + +import ( + "errors" + "fmt" + "strings" + "time" + + "infini.sh/framework/core/global" + "infini.sh/framework/core/model" + "infini.sh/framework/core/security" + "infini.sh/framework/core/util" + "infini.sh/framework/modules/security/access_token" +) + +var managedRegisterPermission = security.GetOrInitPermission("managed", "instance", "register") +var managedExchangePermission = security.GetOrInitPermission("managed", "instance", "exchange") +var managedSyncPermission = security.GetOrInitPermission("managed", "config", "sync") + +var errManagedTokenInvalid = errors.New("managed token is invalid") +var errManagedTokenExpired = errors.New("managed token is expired") + +func getBootstrapTokenPermissions() []security.PermissionKey { + return []security.PermissionKey{ + managedRegisterPermission, + managedExchangePermission, + } +} + +func getManagerTokenPermissions() []security.PermissionKey { + return []security.PermissionKey{ + managedRegisterPermission, + managedExchangePermission, + managedSyncPermission, + } +} + +func newManagedTokenUser(userID string, instance *model.Instance) *security.UserSessionInfo { + user := &security.UserSessionInfo{ + Provider: "managed_agent", + Login: userID, + } + if instance != nil && strings.TrimSpace(instance.ID) != "" { + user.Login = instance.ID + user.Set("instance_id", instance.ID) + user.Set("instance_name", instance.Name) + user.Set("endpoint", instance.Endpoint) + } + user.SetUserID(userID) + return user +} + +func issueManagedAPIToken(user *security.UserSessionInfo, name, typeName string, expiredAt int64, permissions []security.PermissionKey) (string, error) { + res, err := access_token.CreateAPIToken(user, name, typeName, expiredAt, permissions) + if err != nil { + return "", err + } + + token, ok := res["access_token"].(string) + if !ok || strings.TrimSpace(token) == "" { + return "", fmt.Errorf("failed to create managed api token") + } + return token, nil +} + +func getManagedAPIToken(token string) (*security.AccessToken, error) { + token = strings.TrimSpace(token) + if token == "" { + return nil, errManagedTokenInvalid + } + + record, err := access_token.GetToken(token) + if err != nil { + return nil, errManagedTokenInvalid + } + if record.ExpireIn > 0 && time.Now().After(time.Unix(record.ExpireIn, 0)) { + return nil, errManagedTokenExpired + } + return record, nil +} + +func getEffectiveManagedPermissions(token *security.AccessToken) []security.PermissionKey { + if token == nil { + return nil + } + + permissions := token.Permissions + if global.Env().SystemConfig.WebAppConfig.Security.Authentication.AccessToken.Native { + // In native mode, effective permissions are limited by both the token and + // the current owner's permissions, so revoked user permissions take effect + // immediately for managed tokens as well. + tokenLevel := security.ConvertPermissionKeysToHashSet(token.Permissions) + + user := security.UserSessionInfo{Provider: "native"} + user.SetUserID(token.GetOwnerID()) + + userLevel := security.ConvertPermissionKeysToHashSet(security.GetAllPermissionsForUser(&user)) + permissions = security.ConvertPermissionHashSetToKeys(security.IntersectSetsFast(tokenLevel, userLevel)) + } + + return permissions +} + +func requireManagedPermissions(token *security.AccessToken, permissions ...security.PermissionKey) error { + if len(permissions) == 0 { + return nil + } + if token == nil { + return errManagedTokenInvalid + } + + if !util.IsSuperset( + security.ConvertPermissionKeysToHashSet(getEffectiveManagedPermissions(token)), + security.ConvertPermissionKeysToHashSet(permissions), + ) { + return errManagedTokenInvalid + } + return nil +} + +func requireManagedInstance(token *security.AccessToken, instance *model.Instance) error { + if token == nil || instance == nil || strings.TrimSpace(instance.ID) == "" { + return errManagedTokenInvalid + } + + tokenInstanceID, ok := token.GetString("instance_id") + if !ok || strings.TrimSpace(tokenInstanceID) == "" || tokenInstanceID != instance.ID { + return errManagedTokenInvalid + } + return nil +} diff --git a/plugin/managed/server/manager.go b/plugin/managed/server/manager.go index 29ce6d8f..7ae7e634 100644 --- a/plugin/managed/server/manager.go +++ b/plugin/managed/server/manager.go @@ -106,6 +106,11 @@ func ProxyAgentRequest(tag, endpoint string, req *util.Request, responseObjectTo } }) + err = prepareProxyAgentRequest(endpoint, req) + if err != nil { + return nil, err + } + req.Url = endpoint + req.Path res, err = util.ExecuteRequestWithCatchFlag(mTLSClient, req, true) diff --git a/plugin/managed/server/register_token_test.go b/plugin/managed/server/register_token_test.go new file mode 100644 index 00000000..6ce37d40 --- /dev/null +++ b/plugin/managed/server/register_token_test.go @@ -0,0 +1,170 @@ +package server + +import ( + "os" + "strings" + "sync" + "testing" + "time" + + "infini.sh/framework/core/kv" + "infini.sh/framework/core/security" +) + +type memoryKVStore struct { + lock sync.RWMutex + data map[string]map[string][]byte +} + +func newMemoryKVStore() *memoryKVStore { + return &memoryKVStore{ + data: map[string]map[string][]byte{}, + } +} + +func (m *memoryKVStore) Open() error { return nil } +func (m *memoryKVStore) Close() error { return nil } + +func (m *memoryKVStore) GetValue(bucket string, key []byte) ([]byte, error) { + m.lock.RLock() + defer m.lock.RUnlock() + if m.data[bucket] == nil { + return nil, nil + } + value := m.data[bucket][string(key)] + if value == nil { + return nil, nil + } + buf := make([]byte, len(value)) + copy(buf, value) + return buf, nil +} + +func (m *memoryKVStore) GetCompressedValue(bucket string, key []byte) ([]byte, error) { + return m.GetValue(bucket, key) +} + +func (m *memoryKVStore) AddValueCompress(bucket string, key []byte, value []byte) error { + return m.AddValue(bucket, key, value) +} + +func (m *memoryKVStore) AddValue(bucket string, key []byte, value []byte) error { + m.lock.Lock() + defer m.lock.Unlock() + if m.data[bucket] == nil { + m.data[bucket] = map[string][]byte{} + } + buf := make([]byte, len(value)) + copy(buf, value) + m.data[bucket][string(key)] = buf + return nil +} + +func (m *memoryKVStore) ExistsKey(bucket string, key []byte) (bool, error) { + m.lock.RLock() + defer m.lock.RUnlock() + if m.data[bucket] == nil { + return false, nil + } + _, ok := m.data[bucket][string(key)] + return ok, nil +} + +func (m *memoryKVStore) DeleteKey(bucket string, key []byte) error { + m.lock.Lock() + defer m.lock.Unlock() + if m.data[bucket] != nil { + delete(m.data[bucket], string(key)) + } + return nil +} + +func TestMain(m *testing.M) { + kv.Register("managed-server-register-token-test", newMemoryKVStore()) + os.Exit(m.Run()) +} + +func TestIssueBootstrapTokenCanBeValidated(t *testing.T) { + token, err := issueBootstrapToken("user-1") + if err != nil { + t.Fatalf("issueBootstrapToken returned error: %v", err) + } + if token == "" { + t.Fatal("expected non-empty bootstrap token") + } + + if err := validateBootstrapToken(token); err != nil { + t.Fatalf("validateBootstrapToken returned error: %v", err) + } +} + +func TestValidateBootstrapTokenRejectsMissingOrUnknownToken(t *testing.T) { + if err := validateBootstrapToken(""); err == nil || !strings.Contains(err.Error(), "required") { + t.Fatalf("expected missing token error, got %v", err) + } + + if err := validateBootstrapToken("missing-token"); err == nil || !strings.Contains(err.Error(), "invalid") { + t.Fatalf("expected invalid token error, got %v", err) + } +} + +func TestGetBootstrapTokenUserIDReturnsOwner(t *testing.T) { + token, err := issueBootstrapToken("user-2") + if err != nil { + t.Fatalf("issueBootstrapToken returned error: %v", err) + } + + userID, err := getBootstrapTokenUserID(token) + if err != nil { + t.Fatalf("getBootstrapTokenUserID returned error: %v", err) + } + if userID != "user-2" { + t.Fatalf("expected token owner user-2, got %q", userID) + } +} + +func TestValidateBootstrapTokenRejectsExpiredToken(t *testing.T) { + user := &security.UserSessionInfo{ + Provider: "managed_agent", + Login: "user-3", + } + user.SetUserID("user-3") + + token, err := issueManagedAPIToken( + user, + "expired managed bootstrap", + "managed_agent_bootstrap", + time.Now().Add(-time.Minute).Unix(), + getBootstrapTokenPermissions(), + ) + if err != nil { + t.Fatalf("failed to create expired bootstrap token: %v", err) + } + + if err := validateBootstrapToken(token); err == nil || !strings.Contains(err.Error(), "expired") { + t.Fatalf("expected expired token error, got %v", err) + } +} + +func TestValidateBootstrapTokenRejectsTokenWithoutRegisterPermission(t *testing.T) { + user := &security.UserSessionInfo{ + Provider: "managed_agent", + Login: "user-4", + } + user.SetUserID("user-4") + + token, err := issueManagedAPIToken( + user, + "managed sync only", + "managed_agent_sync", + time.Now().Add(time.Hour).Unix(), + []security.PermissionKey{managedSyncPermission}, + ) + if err != nil { + t.Fatalf("failed to create managed sync token: %v", err) + } + + if err := validateBootstrapToken(token); err == nil || !strings.Contains(err.Error(), "invalid") { + t.Fatalf("expected invalid token error, got %v", err) + } +} diff --git a/plugin/managed/server/script.go b/plugin/managed/server/script.go index 4dbf660f..3daf3c68 100644 --- a/plugin/managed/server/script.go +++ b/plugin/managed/server/script.go @@ -28,12 +28,14 @@ package server import ( + "errors" "fmt" log "github.com/cihub/seelog" - "infini.sh/console/core/security" + rbac "infini.sh/console/core/security" "infini.sh/console/modules/agent/common" httprouter "infini.sh/framework/core/api/router" "infini.sh/framework/core/global" + "infini.sh/framework/core/security" "infini.sh/framework/core/util" "infini.sh/framework/lib/fasttemplate" "net/url" @@ -45,18 +47,63 @@ import ( "time" ) -type Token struct { +type installScriptToken struct { CreatedAt time.Time UserID string } -const ExpiredIn = time.Millisecond * 1000 * 60 * 60 +const managedInstallTokenTTL = time.Millisecond * 1000 * 60 * 60 const GET_INSTALL_SCRIPT_API = "/instance/_get_install_script" -var expiredTokenCache = util.NewCacheWithExpireOnAdd(ExpiredIn, 100) +var expiredTokenCache = util.NewCacheWithExpireOnAdd(managedInstallTokenTTL, 100) +var errBootstrapTokenRequired = errors.New("bootstrap token is required") +var errBootstrapTokenInvalid = errors.New("bootstrap token is invalid") +var errBootstrapTokenExpired = errors.New("bootstrap token is expired") + +func issueBootstrapToken(userID string) (string, error) { + return issueManagedAPIToken( + newManagedTokenUser(userID, nil), + "managed bootstrap", + "managed_agent_bootstrap", + time.Now().Add(managedInstallTokenTTL).Unix(), + getBootstrapTokenPermissions(), + ) +} + +func getBootstrapToken(tokenStr string, permissions ...security.PermissionKey) (*security.AccessToken, error) { + if strings.TrimSpace(tokenStr) == "" { + return nil, errBootstrapTokenRequired + } + + token, err := getManagedAPIToken(tokenStr) + if err != nil { + if errors.Is(err, errManagedTokenExpired) { + return nil, errBootstrapTokenExpired + } + return nil, errBootstrapTokenInvalid + } + if err := requireManagedPermissions(token, permissions...); err != nil { + return nil, errBootstrapTokenInvalid + } + + return token, nil +} + +func validateBootstrapToken(tokenStr string) error { + _, err := getBootstrapToken(tokenStr, managedRegisterPermission) + return err +} + +func getBootstrapTokenUserID(tokenStr string) (string, error) { + token, err := getBootstrapToken(tokenStr, managedExchangePermission) + if err != nil { + return "", err + } + return strings.TrimSpace(token.GetOwnerID()), nil +} func (h *APIHandler) generateInstallCommand(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { - claims, ok := req.Context().Value("user").(*security.UserClaims) + claims, ok := req.Context().Value("user").(*rbac.UserClaims) if !ok { h.WriteError(w, "user not found", http.StatusInternalServerError) return @@ -67,7 +114,7 @@ func (h *APIHandler) generateInstallCommand(w http.ResponseWriter, req *http.Req return } var ( - t *Token + t *installScriptToken tokenStr string ) @@ -75,21 +122,13 @@ func (h *APIHandler) generateInstallCommand(w http.ResponseWriter, req *http.Req location := "/opt/agent" tokenStr = util.GetUUID() - t = &Token{ + t = &installScriptToken{ CreatedAt: time.Now(), UserID: claims.UserId, } expiredTokenCache.Put(tokenStr, t) - consoleEndpoint := agCfg.Setup.ConsoleEndpoint - if consoleEndpoint == "" { - consoleEndpoint = getDefaultEndpoint(req) - } - - basePath := global.Env().SystemConfig.WebAppConfig.BasePath - if len(basePath) > 0 { - consoleEndpoint = fmt.Sprintf("%s%s", strings.TrimRight(consoleEndpoint, "/"), basePath) - } + consoleEndpoint := getConsoleEndpoint(req, agCfg.Setup.ConsoleEndpoint) endpoint, err := url.JoinPath(consoleEndpoint, GET_INSTALL_SCRIPT_API) if err != nil { @@ -100,7 +139,7 @@ func (h *APIHandler) generateInstallCommand(w http.ResponseWriter, req *http.Req "script": fmt.Sprintf(`curl -ksSL %s?token=%s |sudo bash -s -- -u %s -t %v`, endpoint, tokenStr, agCfg.Setup.DownloadURL, location), "token": tokenStr, - "expired_at": t.CreatedAt.Add(ExpiredIn), + "expired_at": t.CreatedAt.Add(managedInstallTokenTTL), }, http.StatusOK) } @@ -112,6 +151,18 @@ func getDefaultEndpoint(req *http.Request) string { return fmt.Sprintf("%s://%s", scheme, req.Host) } +func getConsoleEndpoint(req *http.Request, configured string) string { + endpoint := configured + if endpoint == "" { + endpoint = getDefaultEndpoint(req) + } + basePath := strings.TrimSpace(global.Env().SystemConfig.WebAppConfig.BasePath) + if basePath == "" || strings.HasSuffix(strings.TrimRight(endpoint, "/"), basePath) { + return endpoint + } + return fmt.Sprintf("%s%s", strings.TrimRight(endpoint, "/"), basePath) +} + func (h *APIHandler) getInstallScript(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { tokenStr := h.GetParameter(req, "token") @@ -126,8 +177,8 @@ func (h *APIHandler) getInstallScript(w http.ResponseWriter, req *http.Request, return } - t, ok := v.(*Token) - if !ok || t.CreatedAt.Add(ExpiredIn).Before(time.Now()) { + t, ok := v.(*installScriptToken) + if !ok || t.CreatedAt.Add(managedInstallTokenTTL).Before(time.Now()) { expiredTokenCache.Delete(tokenStr) h.WriteError(w, "token was expired", http.StatusUnauthorized) return @@ -160,9 +211,13 @@ func (h *APIHandler) getInstallScript(w http.ResponseWriter, req *http.Request, port = "8080" } - consoleEndpoint := agCfg.Setup.ConsoleEndpoint - if consoleEndpoint == "" { - consoleEndpoint = getDefaultEndpoint(req) + consoleEndpoint := getConsoleEndpoint(req, agCfg.Setup.ConsoleEndpoint) + + accessToken, err := issueBootstrapToken(t.UserID) + if err != nil { + log.Error(err) + h.WriteError(w, err.Error(), http.StatusInternalServerError) + return } _, err = tpl.Execute(w, map[string]interface{}{ @@ -172,6 +227,7 @@ func (h *APIHandler) getInstallScript(w http.ResponseWriter, req *http.Request, "client_key": clientKeyPEM, "ca_crt": caCert, "port": port, + "access_token": accessToken, "token": tokenStr, }) diff --git a/plugin/managed/server/token_exchange.go b/plugin/managed/server/token_exchange.go new file mode 100644 index 00000000..f1d04adf --- /dev/null +++ b/plugin/managed/server/token_exchange.go @@ -0,0 +1,243 @@ +package server + +import ( + "errors" + "fmt" + "net/http" + "strings" + + httprouter "infini.sh/framework/core/api/router" + "infini.sh/framework/core/model" + "infini.sh/framework/core/orm" + "infini.sh/framework/core/security" + "infini.sh/framework/modules/security/access_token" +) + +const instanceTokenExchangeAPI = "/instance/_exchange_token" + +// -1 means the exchanged manager api token never expires. +const managedAgentSyncTokenExpireAt int64 = -1 + +type tokenExchangeRequest struct { + InstanceID string `json:"instance_id,omitempty"` + AgentAPIToken string `json:"agent_api_token,omitempty"` +} + +type tokenExchangeResponse struct { + ManagerAPIToken string `json:"manager_api_token,omitempty"` +} + +var errManagerAPITokenRequired = errors.New("manager api token is required") +var errManagerAPITokenInvalid = errors.New("manager api token is invalid") + +func writeTokenAuthError(h APIHandler, w http.ResponseWriter, err error) bool { + if err == nil { + return false + } + + switch { + case errors.Is(err, errBootstrapTokenRequired), + errors.Is(err, errBootstrapTokenInvalid), + errors.Is(err, errBootstrapTokenExpired), + errors.Is(err, errManagerAPITokenRequired), + errors.Is(err, errManagerAPITokenInvalid): + h.WriteError(w, err.Error(), http.StatusUnauthorized) + default: + h.WriteError(w, err.Error(), http.StatusInternalServerError) + } + + return true +} + +func getManagerAPIToken(token string, instance *model.Instance, permissions ...security.PermissionKey) (*security.AccessToken, error) { + token = strings.TrimSpace(token) + if token == "" { + return nil, errManagerAPITokenRequired + } + + record, err := getManagedAPIToken(token) + if err != nil { + return nil, errManagerAPITokenInvalid + } + if err := requireManagedPermissions(record, permissions...); err != nil { + return nil, errManagerAPITokenInvalid + } + + instance, err = loadStoredInstance(instance) + if err != nil { + return nil, err + } + if instance == nil { + return nil, errManagerAPITokenInvalid + } + if err := requireManagedInstance(record, instance); err != nil { + return nil, errManagerAPITokenInvalid + } + return record, nil +} + +func validateManagerAPIToken(token string, instance *model.Instance, permissions ...security.PermissionKey) error { + _, err := getManagerAPIToken(token, instance, permissions...) + return err +} + +func authorizeExchangeToken(token string, instance *model.Instance) (string, string, error) { + _, err := getManagerAPIToken(token, instance, managedExchangePermission) + if err == nil { + return "", token, nil + } + if !errors.Is(err, errManagerAPITokenInvalid) { + return "", "", err + } + + userID, regErr := getBootstrapTokenUserID(token) + if regErr != nil { + return "", "", regErr + } + return userID, "", nil +} + +func loadStoredInstance(instance *model.Instance) (*model.Instance, error) { + if instance == nil || strings.TrimSpace(instance.ID) == "" { + return nil, nil + } + if getManagerAPICredentialID(instance) != "" { + return instance, nil + } + return loadExistingInstance(instance.ID) +} + +func validateTokenExchangeRequest(reqBody *tokenExchangeRequest) error { + if reqBody == nil { + return fmt.Errorf("empty request") + } + if strings.TrimSpace(reqBody.InstanceID) == "" { + return fmt.Errorf("empty instance id") + } + if strings.TrimSpace(reqBody.AgentAPIToken) == "" { + return fmt.Errorf("empty agent api token") + } + return nil +} + +func ensureManagerAPICredential(instance *model.Instance, managerAPIToken, userID string) (string, string, error) { + managerCredentialID := getManagerAPICredentialID(instance) + if strings.TrimSpace(managerAPIToken) == "" { + return issueManagerAPIToken(instance, userID) + } + if managerCredentialID != "" { + return managerAPIToken, managerCredentialID, nil + } + + managerCredentialID, err := upsertInstanceManagerAPICredential(instance, managerAPIToken) + if err != nil { + return "", "", err + } + return managerAPIToken, managerCredentialID, nil +} + +func persistExchangedTokens(instance *model.Instance, agentAPIToken, managerAPIToken, userID string) (string, error) { + agentCredentialID, err := upsertInstanceAgentAPICredential(instance, agentAPIToken) + if err != nil { + return "", err + } + setAgentAPICredentialID(instance, agentCredentialID) + + managerAPIToken, managerCredentialID, err := ensureManagerAPICredential(instance, managerAPIToken, userID) + if err != nil { + return "", err + } + setManagerAPICredentialID(instance, managerCredentialID) + clearInstanceAccessToken(instance) + + if err := orm.Save(nil, instance); err != nil { + return "", err + } + return managerAPIToken, nil +} + +func validateRegisterRequestAuth(req *http.Request, instance *model.Instance) error { + token := strings.TrimSpace(req.Header.Get(access_token.HeaderAPIToken)) + if token == "" { + return nil + } + + err := validateManagerAPIToken(token, instance, managedRegisterPermission) + if err == nil || !errors.Is(err, errManagerAPITokenInvalid) { + return err + } + + return validateBootstrapToken(token) +} + +func validateSyncRequestAuth(req *http.Request, instance *model.Instance) error { + managerAPIToken := strings.TrimSpace(req.Header.Get(access_token.HeaderAPIToken)) + if managerAPIToken == "" { + return nil + } + return validateManagerAPIToken(managerAPIToken, instance, managedSyncPermission) +} + +func issueManagerAPIToken(instance *model.Instance, userID string) (string, string, error) { + if instance == nil { + return "", "", fmt.Errorf("instance is nil") + } + if strings.TrimSpace(userID) == "" { + return "", "", fmt.Errorf("user id is empty") + } + + managerAPIToken, err := issueManagedAPIToken( + newManagedTokenUser(userID, instance), + fmt.Sprintf("%s manager api", getInstanceCredentialName(instance)), + "managed_agent_sync", + managedAgentSyncTokenExpireAt, + getManagerTokenPermissions(), + ) + if err != nil { + return "", "", err + } + + credentialID, err := upsertInstanceManagerAPICredential(instance, managerAPIToken) + if err != nil { + return "", "", err + } + + return managerAPIToken, credentialID, nil +} + +func (h APIHandler) exchangeInstanceToken(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { + reqBody := &tokenExchangeRequest{} + if err := h.DecodeJSON(req, reqBody); err != nil { + h.WriteError(w, err.Error(), http.StatusInternalServerError) + return + } + if err := validateTokenExchangeRequest(reqBody); err != nil { + h.WriteError(w, err.Error(), http.StatusBadRequest) + return + } + + instance, err := loadExistingInstance(reqBody.InstanceID) + if err != nil { + h.WriteError(w, err.Error(), http.StatusInternalServerError) + return + } + if instance == nil { + h.WriteError(w, "instance not found", http.StatusBadRequest) + return + } + obj := *instance + + userID, managerAPIToken, err := authorizeExchangeToken(strings.TrimSpace(req.Header.Get(access_token.HeaderAPIToken)), &obj) + if writeTokenAuthError(h, w, err) { + return + } + managerAPIToken, err = persistExchangedTokens(&obj, reqBody.AgentAPIToken, managerAPIToken, userID) + if err != nil { + h.WriteError(w, err.Error(), http.StatusInternalServerError) + return + } + + h.WriteJSON(w, tokenExchangeResponse{ + ManagerAPIToken: managerAPIToken, + }, http.StatusOK) +} diff --git a/plugin/managed/server/token_exchange_test.go b/plugin/managed/server/token_exchange_test.go new file mode 100644 index 00000000..531c9992 --- /dev/null +++ b/plugin/managed/server/token_exchange_test.go @@ -0,0 +1,29 @@ +package server + +import ( + "testing" + + "infini.sh/framework/core/model" + "infini.sh/framework/core/security" +) + +func TestRequireManagedPermissionsRejectsMissingPermission(t *testing.T) { + token := &security.AccessToken{ + Permissions: []security.PermissionKey{managedRegisterPermission}, + } + + if err := requireManagedPermissions(token, managedSyncPermission); err == nil { + t.Fatal("expected missing managed sync permission to be rejected") + } +} + +func TestRequireManagedInstanceRejectsMismatchedInstance(t *testing.T) { + token := &security.AccessToken{} + token.Set("instance_id", "instance-1") + + instance := &model.Instance{} + instance.ID = "instance-2" + if err := requireManagedInstance(token, instance); err == nil { + t.Fatal("expected mismatched instance id to be rejected") + } +} diff --git a/plugin/setup/setup.go b/plugin/setup/setup.go index cfc60b6d..4ef2177d 100644 --- a/plugin/setup/setup.go +++ b/plugin/setup/setup.go @@ -749,8 +749,8 @@ func createCred(name, username, password string) string { Name: name, Type: credential.BasicAuth, Tags: []string{"infini", "system"}, - Payload: map[string]interface{}{ - "basic_auth": map[string]interface{}{ + Payload: map[credential.CredentialType]interface{}{ + credential.BasicAuth: map[string]interface{}{ "username": username, "password": password, },