External vault - GCP secert manager intergation[INS-4801] (#4)

* Add GCP secret manager UI
* Add GCP auth validation
* Unify the error message from external vault function
This commit is contained in:
Kent Wang
2025-02-20 17:56:47 +08:00
committed by Jay Wu
parent 153c4e44bd
commit 9c2c5ff1fb
15 changed files with 714 additions and 31 deletions

298
package-lock.json generated
View File

@@ -2538,6 +2538,18 @@
"tweetnacl": "^1.0.3"
}
},
"node_modules/@google-cloud/secret-manager": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/@google-cloud/secret-manager/-/secret-manager-5.6.0.tgz",
"integrity": "sha512-0daW/OXQEVc6VQKPyJTQNyD+563I/TYQ7GCQJx4dq3lB666R9FUPvqHx9b/o/qQtZ5pfuoCbGZl3krpxgTSW8Q==",
"license": "Apache-2.0",
"dependencies": {
"google-gax": "^4.0.3"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@grpc/grpc-js": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.0.tgz",
@@ -7613,7 +7625,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
"integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10"
@@ -7719,7 +7730,6 @@
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz",
"integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/chai": {
@@ -8074,6 +8084,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/long": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
"license": "MIT"
},
"node_modules/@types/marked": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz",
@@ -8290,7 +8306,6 @@
"version": "2.48.12",
"resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz",
"integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/caseless": "*",
@@ -8367,7 +8382,6 @@
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
"integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/trusted-types": {
@@ -9953,6 +9967,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/bignumber.js": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz",
"integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -10247,7 +10270,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/buffer-fill": {
@@ -12291,6 +12313,18 @@
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
"license": "MIT"
},
"node_modules/duplexify": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz",
"integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.4.1",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1",
"stream-shift": "^1.0.2"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -12301,7 +12335,6 @@
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
@@ -13815,6 +13848,12 @@
"node": ">=4"
}
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
"node_modules/extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
@@ -14181,7 +14220,6 @@
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
"integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
@@ -14381,6 +14419,70 @@
"dev": true,
"license": "MIT"
},
"node_modules/gaxios": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
"license": "Apache-2.0",
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"is-stream": "^2.0.0",
"node-fetch": "^2.6.9",
"uuid": "^9.0.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/gaxios/node_modules/agent-base": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/gaxios/node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/gaxios/node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/gcp-metadata": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
"integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
"license": "Apache-2.0",
"dependencies": {
"gaxios": "^6.1.1",
"google-logging-utils": "^0.0.2",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -14593,6 +14695,76 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/google-auth-library": {
"version": "9.15.1",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
"integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
"license": "Apache-2.0",
"dependencies": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"gaxios": "^6.1.1",
"gcp-metadata": "^6.1.0",
"gtoken": "^7.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/google-auth-library/node_modules/jwa": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/google-auth-library/node_modules/jws": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.0",
"safe-buffer": "^5.0.1"
}
},
"node_modules/google-gax": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.4.1.tgz",
"integrity": "sha512-Phyp9fMfA00J3sZbJxbbB4jC55b7DBjE3F6poyL3wKMEBVKA79q6BGuHcTiM28yOzVql0NDbRL8MLLh8Iwk9Dg==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/grpc-js": "^1.10.9",
"@grpc/proto-loader": "^0.7.13",
"@types/long": "^4.0.0",
"abort-controller": "^3.0.0",
"duplexify": "^4.0.0",
"google-auth-library": "^9.3.0",
"node-fetch": "^2.7.0",
"object-hash": "^3.0.0",
"proto3-json-serializer": "^2.0.2",
"protobufjs": "^7.3.2",
"retry-request": "^7.0.0",
"uuid": "^9.0.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/google-logging-utils": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
"integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"node_modules/google-protobuf": {
"version": "3.21.4",
"resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz",
@@ -14728,6 +14900,40 @@
"dev": true,
"license": "Standard 'no charge' license: https://gsap.com/standard-license. Club GSAP members get more: https://gsap.com/licensing/. Why GreenSock doesn't employ an MIT license: https://gsap.com/why-license/"
},
"node_modules/gtoken": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
"license": "MIT",
"dependencies": {
"gaxios": "^6.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/gtoken/node_modules/jwa": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/gtoken/node_modules/jws": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.0",
"safe-buffer": "^5.0.1"
}
},
"node_modules/har-schema": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
@@ -15067,7 +15273,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
"integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@tootallnate/once": "2",
@@ -16429,6 +16634,15 @@
"node": ">=0.8.0"
}
},
"node_modules/json-bigint": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
"license": "MIT",
"dependencies": {
"bignumber.js": "^9.0.0"
}
},
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@@ -18831,7 +19045,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -20145,6 +20358,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/proto3-json-serializer": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz",
"integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==",
"license": "Apache-2.0",
"dependencies": {
"protobufjs": "^7.2.5"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/protobufjs": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz",
@@ -20739,7 +20964,6 @@
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true,
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
@@ -20960,6 +21184,20 @@
"node": ">= 4"
}
},
"node_modules/retry-request": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz",
"integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==",
"license": "MIT",
"dependencies": {
"@types/request": "^2.48.8",
"extend": "^3.0.2",
"teeny-request": "^9.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -21916,11 +22154,25 @@
"duplexer": "~0.1.1"
}
},
"node_modules/stream-events": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
"integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==",
"license": "MIT",
"dependencies": {
"stubs": "^3.0.0"
}
},
"node_modules/stream-shift": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
"integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==",
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
@@ -22138,6 +22390,12 @@
"integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==",
"license": "MIT"
},
"node_modules/stubs": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
"integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==",
"license": "MIT"
},
"node_modules/style-mod": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
@@ -22518,6 +22776,22 @@
"dev": true,
"license": "ISC"
},
"node_modules/teeny-request": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz",
"integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==",
"license": "Apache-2.0",
"dependencies": {
"http-proxy-agent": "^5.0.0",
"https-proxy-agent": "^5.0.0",
"node-fetch": "^2.6.9",
"stream-events": "^1.0.5",
"uuid": "^9.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/temp-file": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz",
@@ -24755,6 +25029,7 @@
"@connectrpc/connect": "^1.4.0",
"@connectrpc/connect-node": "^1.4.0",
"@getinsomnia/node-libcurl": "^2.33.7",
"@google-cloud/secret-manager": "^5.6.0",
"@grpc/grpc-js": "^1.12.0",
"@grpc/proto-loader": "^0.7.13",
"@seald-io/nedb": "^4.0.4",
@@ -24770,6 +25045,7 @@
"electron-context-menu": "^3.6.1",
"electron-log": "^4.4.8",
"fastq": "^1.17.1",
"google-auth-library": "^9.15.0",
"graphql": "^16.8.1",
"graphql-ws": "^5.16.0",
"grpc-reflection-js": "Kong/grpc-reflection-js#master",

View File

@@ -43,6 +43,7 @@
"@connectrpc/connect": "^1.4.0",
"@connectrpc/connect-node": "^1.4.0",
"@getinsomnia/node-libcurl": "^2.33.7",
"@google-cloud/secret-manager": "^5.6.0",
"@grpc/grpc-js": "^1.12.0",
"@grpc/proto-loader": "^0.7.13",
"@seald-io/nedb": "^4.0.4",
@@ -58,6 +59,7 @@
"electron-context-menu": "^3.6.1",
"electron-log": "^4.4.8",
"fastq": "^1.17.1",
"google-auth-library": "^9.15.0",
"graphql": "^16.8.1",
"graphql-ws": "^5.16.0",
"grpc-reflection-js": "Kong/grpc-reflection-js#master",

View File

@@ -3,6 +3,7 @@ import * as models from '../../../models';
import type { AWSTemporaryCredential, BaseCloudCredential, CloudProviderName } from '../../../models/cloud-credential';
import { ipcMainHandle, ipcMainOn } from '../electron';
import { type AWSGetSecretConfig, AWSService } from './aws-service';
import { type GCPGetSecretConfig, GCPService } from './gcp-servcie';
import { type MaxAgeUnit, VaultCache } from './vault-cache';
// in-memory cache for fetched vault secrets
@@ -22,7 +23,7 @@ export interface CloudServiceSecretOption<T extends {}> extends CloudServiceAuth
secretId: string;
config: T;
}
export type CloudServiceGetSecretConfig = AWSGetSecretConfig;
export type CloudServiceGetSecretConfig = AWSGetSecretConfig | GCPGetSecretConfig;
export function registerCloudServiceHandlers() {
ipcMainHandle('cloudService.authenticate', (_event, options) => cloudServiceProviderAuthentication(options));
@@ -37,6 +38,8 @@ class ServiceFactory {
switch (name) {
case 'aws':
return new AWSService(credential as AWSTemporaryCredential);
case 'gcp':
return new GCPService(credential as string);
default:
throw new Error('Invalid cloud service provider name');
}

View File

@@ -0,0 +1,138 @@
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
import crypto from 'crypto';
import { readFileSync } from 'fs';
import { GoogleAuth, type JWTInput } from 'google-auth-library';
import type { CloudProviderName } from '../../../models/cloud-credential';
import { isValidJSONString } from '../../../utils/json';
import type { CloudServiceResult, GCPSecretConfig, ICloudService } from './types';
export const providerName: CloudProviderName = 'gcp';
export type GCPGetSecretConfig = Omit<GCPSecretConfig, 'secretName'>;
export class GCPService implements ICloudService {
_keyPath: string;
constructor(keyPath: string) {
this._keyPath = keyPath;
}
_validateKeyPath(): { isValid: true; credentials: JWTInput } | { isValid: false; errorMessage: string } {
const requiredFields = ['project_id', 'private_key_id', 'private_key', 'client_email'];
const keyPath = this._keyPath;
let isValid = true;
let errorMessage = '';
try {
const fileContent = readFileSync(keyPath, 'utf-8');
if (isValidJSONString(fileContent)) {
const serviceAccountKey = JSON.parse(fileContent.toString()) as JWTInput;
isValid = requiredFields.every(field => {
isValid = field in serviceAccountKey;
if (!isValid) {
errorMessage = `Required field: ${field} is missing`;
}
return isValid;
});
if (isValid) {
return {
isValid,
credentials: serviceAccountKey,
};
}
} else {
isValid = false;
errorMessage = `Invalid JSON in file ${keyPath}`;
};
} catch (error) {
isValid = false;
errorMessage = error.message || error.toString();
};
return { isValid, errorMessage };
}
async authenticate(): Promise<CloudServiceResult<{}>> {
const validateResult = this._validateKeyPath();
if (validateResult.isValid) {
const auth = new GoogleAuth({
credentials: validateResult.credentials,
// General scope for GCP
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
});
try {
const client = await auth.getClient();
// use get access token to validate credential
await client.getAccessToken();
return {
success: true,
result: {},
};
} catch (error) {
return {
success: false,
result: null,
error: { errorMessage: error?.message, errorCode: error?.code },
};
}
} else {
return {
success: false,
result: null,
error: { errorMessage: validateResult.errorMessage, errorCode: '' },
};
}
}
getUniqueCacheKey(secretName: string, config?: GCPGetSecretConfig) {
const keyPath = this._keyPath;
const { version = 'latest' } = config || {};
const uniqueKey = `${providerName}:${keyPath}:${secretName}:${version}`;
const uniqueKeyHash = crypto.createHash('md5').update(uniqueKey).digest('hex');
return uniqueKeyHash;
}
async getSecret(secretName: string, config: GCPGetSecretConfig): Promise<CloudServiceResult<{ value: string }>> {
const { version } = config;
const secretVersion = version || 'latest';
const validateResult = this._validateKeyPath();
if (validateResult.isValid) {
const { credentials } = validateResult;
const { project_id } = credentials;
const secretClient = new SecretManagerServiceClient({
credentials,
});
const fullPathSecretNamePattern = /^projects\/[a-z0-9-]+\/secrets\/[a-zA-Z0-9_-]+$/;
const fullPathSecretNameWithVersionPattern = /^projects\/[a-z0-9-]+\/secrets\/[a-zA-Z0-9_-]+\/versions\/[a-zA-Z0-9_-]+$/;
let finalSecretName: string;
if (fullPathSecretNamePattern.test(secretName)) {
// if secret name in pattern /projects/<project_id>/secrets/<secret_name> which is copied from gcp
finalSecretName = `${secretName}/versions/${secretVersion}`;
} else if (fullPathSecretNameWithVersionPattern.test(secretName)) {
// if secret name with version in pattern /projects/<project_id>/secrets/<secret_name>/versions/<version> which is copied from gcp
finalSecretName = secretName;
} else {
finalSecretName = `projects/${project_id}/secrets/${secretName}/versions/${secretVersion}`;
}
try {
const [versionResponse] = await secretClient.accessSecretVersion({ name: finalSecretName });
const secretResult = versionResponse.payload?.data?.toString() || '';
return {
success: true,
result: { value: secretResult },
};
} catch (error) {
console.error(error);
return {
success: false,
result: null,
error: { errorMessage: error.toString(), errorCode: error?.code },
};
}
} else {
return {
success: false,
result: null,
error: { errorMessage: validateResult.errorMessage, errorCode: '' },
};
}
}
};

View File

@@ -22,4 +22,9 @@ export interface AWSSecretConfig {
SecretKey?: string;
};
export type ExternalVaultConfig = AWSSecretConfig;
export interface GCPSecretConfig {
secretName: string;
version?: string;
}
export type ExternalVaultConfig = AWSSecretConfig | GCPSecretConfig;

View File

@@ -12,7 +12,7 @@ export interface AWSTemporaryCredential {
sessionToken: string;
region: string;
}
interface IBaseCloudCredential {
export interface IBaseCloudCredential {
name: string;
provider: CloudProviderName;
}
@@ -21,7 +21,18 @@ export interface AWSCloudCredential extends IBaseCloudCredential {
provider: 'aws';
credentials: AWSTemporaryCredential;
}
export type BaseCloudCredential = AWSCloudCredential;
export type CloudeProviderCredentialType = AWSTemporaryCredential;
export interface AWSCloudCredential extends IBaseCloudCredential {
name: string;
provider: 'aws';
credentials: AWSTemporaryCredential;
}
export interface GCPCloudCredential extends IBaseCloudCredential {
name: string;
provider: 'gcp';
credentials: string;
}
export type BaseCloudCredential = AWSCloudCredential | GCPCloudCredential;
export type CloudProviderCredential = BaseModel & BaseCloudCredential;
export const name = 'Cloud Credential';

View File

@@ -5,6 +5,7 @@ import { useFetcher } from 'react-router-dom';
import { type BaseCloudCredential, type CloudProviderCredential, type CloudProviderName, getProviderDisplayName } from '../../../../models/cloud-credential';
import { Icon } from '../../icon';
import { AWSCredentialForm } from './aws-credential-form';
import { GCPCredentialForm } from './gcp-credential-form';
export interface CloudCredentialModalProps {
provider: CloudProviderName;
@@ -87,6 +88,14 @@ export const CloudCredentialModal = (props: CloudCredentialModalProps) => {
errorMessage={fetchErrorMessage}
/>
}
{provider === 'gcp' &&
<GCPCredentialForm
data={providerCredential}
isLoading={cloudCredentialFetcher.state !== 'idle'}
onSubmit={handleFormSubmit}
errorMessage={fetchErrorMessage}
/>
}
</div>
)}
</Dialog>

View File

@@ -0,0 +1,111 @@
import React, { useState } from 'react';
import { Button, Input, Label, TextField } from 'react-aria-components';
import { type BaseCloudCredential, type CloudProviderCredential, type CloudProviderName } from '../../../../models/cloud-credential';
import { HelpTooltip } from '../../help-tooltip';
import { Icon } from '../../icon';
export interface GCPCredentialFormProps {
data?: CloudProviderCredential;
onSubmit: (newData: BaseCloudCredential) => void;
isLoading: boolean;
errorMessage?: string;
}
const initialFormValue = {
name: '',
};
export const providerType: CloudProviderName = 'gcp';
export const GCPCredentialForm = (props: GCPCredentialFormProps) => {
const { data, onSubmit, isLoading, errorMessage } = props;
const [inputKeyPath, setInputKeyPath] = useState(data?.credentials as string);
const isEdit = !!data;
const { name } = data || initialFormValue;
const handleSelectFile = async () => {
const { canceled, filePaths } = await window.dialog.showOpenDialog({
title: 'Select Service Account Key File',
buttonLabel: 'Select',
properties: ['openFile'],
filters: [
{ name: 'JSON File', extensions: ['json'] },
],
});
if (canceled) {
return;
}
const selectedFile = filePaths[0];
setInputKeyPath(selectedFile);
};
return (
<form
className='flex flex-col gap-2 flex-shrink-0'
onSubmit={e => {
e.preventDefault();
e.stopPropagation();
const formData = new FormData(e.currentTarget);
const { name } = Object.fromEntries(formData.entries()) as Record<string, string>;
const newData = {
name,
provider: providerType,
credentials: inputKeyPath!,
};
onSubmit(newData);
}}
>
<div className='flex flex-col gap-2'>
<TextField
className="flex flex-col gap-2"
defaultValue={name}
>
<Label className='col-span-4'>
Credential Name:
</Label>
<Input
required
className='py-1 h-8 w-full pl-2 pr-7 rounded-sm border border-solid border-[--hl-sm] bg-[--color-bg] text-[--color-font] focus:outline-none focus:ring-1 focus:ring-[--hl-md] transition-colors flex-1 placeholder:italic placeholder:opacity-60 col-span-3'
type="text"
name="name"
placeholder="Credential name"
/>
</TextField>
<div>
<label>
Service Account Key File Path:
<HelpTooltip className='ml-2 sapce-left'>Enter the path of your service account key file which is generated in GCP console</HelpTooltip>
</label>
</div>
<div className='mt-2 flex gap-3'>
<Input
className='py-1 w-4/5 pl-2 pr-7 rounded-sm border border-solid border-[--hl-sm] bg-[--color-bg] text-[--color-font] focus:outline-none focus:ring-1 focus:ring-[--hl-md] transition-colors flex-1 placeholder:italic placeholder:opacity-60 col-span-3'
placeholder="Service account key path"
aria-label='Input Serice Account Key Path'
value={inputKeyPath}
onChange={e => setInputKeyPath(e.target.value)}
/>
<Button
className="flex-shrink-0 border-solid border border-[--hl-`sm] py-1 items-center justify-center px-4 aria-pressed:bg-[--hl-sm] aria-selected:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent transition-all text-base"
onPress={handleSelectFile}
>
<Icon icon="file" className='mr-2' />
<span>Select File</span>
</Button>
</div>
{(errorMessage) &&
<p className="notice error margin-top-sm no-margin-bottom">{errorMessage}</p>
}
<div className='w-full flex flex-row items-center justify-end gap-[--padding-md] pt-[--padding-md]'>
<Button
className="hover:no-underline text-right bg-[--color-surprise] hover:bg-opacity-90 border border-solid border-[--hl-md] py-2 px-3 text-[--color-font-surprise] transition-colors rounded-sm"
type='submit'
isDisabled={isLoading || !inputKeyPath}
>
{isLoading && <Icon icon="spinner" className="text-[--color-font] animate-spin m-auto inline-block mr-2" />}
{isEdit ? 'Update' : 'Create'}
</Button>
</div>
</div>
</form >
);
};

View File

@@ -9,7 +9,9 @@ import { Icon } from '../icon';
import { showModal } from '../modals';
import { AskModal } from '../modals/ask-modal';
import { CloudCredentialModal } from '../modals/cloud-credential-modal/cloud-credential-modal';
import { SvgIcon } from '../svg-icon';
import { UpgradeNotice } from '../upgrade-notice';
import { NumberSetting } from './number-setting';
interface createCredentialItemType {
name: string;
@@ -22,6 +24,11 @@ const createCredentialItemList: createCredentialItemType[] = [
name: getProviderDisplayName('aws'),
icon: <i className="ml-1 fa-brands fa-aws" />,
},
{
id: 'gcp',
name: getProviderDisplayName('gcp'),
icon: <SvgIcon icon='gcp-logo' className='ml-1' />,
},
];
const buttonClassName = 'disabled:opacity-50 h-7 aspect-square aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] transition-all text-sm py-1 px-2';
@@ -137,12 +144,14 @@ export const CloudServiceCredentialList = () => {
</td>
<td className='w-52 whitespace-nowrap'>
<div className='flex gap-2'>
<Button
className={`${buttonClassName} w-16`}
onPress={() => setModalState({ show: true, provider: provider!, credential: cloudCred })}
>
<Icon icon="edit" />&nbsp;&nbsp;Edit
</Button>
{(provider === 'aws' || provider === 'gcp') &&
<Button
className={`${buttonClassName} w-16`}
onPress={() => setModalState({ show: true, provider: provider!, credential: cloudCred })}
>
<Icon icon="edit" />&nbsp;&nbsp;Edit
</Button>
}
<Button
className={`${buttonClassName} w-20`}
onPress={() => handleDeleteItem(_id, name)}
@@ -157,6 +166,24 @@ export const CloudServiceCredentialList = () => {
</tbody>
</table>
}
<div>
<h2 className='font-bold pt-5 pb-2 text-lg bg-[--color-bg] z-10'>Cloud Secret Config</h2>
<div className="form-row items-end justify-between">
<NumberSetting
label="Secret Cache Duration(min)"
setting="vaultSecretCacheDuration"
help="Enter the amount of time in minutes external vault secrets are cached in Insomnia. Enter 0 to disable cache. Click the Reset Cache button to clear all cache."
min={0}
max={720}
/>
<button
className="w-32 flex items-center gap-2 border border-solid border-[--hl-lg] px-[--padding-md] h-[--line-height-xs] rounded-[--radius-md] hover:bg-[--hl-xs] pointer mb-[--padding-sm] ml-[--padding-sm]"
onClick={() => window.main.cloudService.clearCache()}
>
Reset Cache
</button>
</div>
</div>
{modalState && modalState.show &&
<CloudCredentialModal
provider={modalState.provider}

View File

@@ -1,5 +1,7 @@
import type { AWSGetSecretConfig } from '../../../main/ipc/cloud-service-integration/aws-service';
import type { CloudServiceSecretOption } from '../../../main/ipc/cloud-service-integration/cloud-service';
import type { GCPGetSecretConfig } from '../../../main/ipc/cloud-service-integration/gcp-servcie';
import type { GCPSecretConfig } from '../../../main/ipc/cloud-service-integration/types';
import type { AWSSecretConfig, ExternalVaultConfig } from '../../../main/ipc/cloud-service-integration/types';
import type { CloudProviderCredential, CloudProviderName } from '../../../models/cloud-credential';
@@ -7,6 +9,8 @@ export const getExternalVault = async (provider: CloudProviderName, providerCred
switch (provider) {
case 'aws':
return getAWSSecret(secretConfig as AWSSecretConfig, providerCredential);
case 'gcp':
return getGCPSecret(secretConfig as GCPSecretConfig, providerCredential);
default:
return '';
}
@@ -50,3 +54,23 @@ export const getAWSSecret = async (secretConfig: AWSSecretConfig, providerCreden
throw new Error(`Get secret from AWS failed: ${error?.errorMessage}`);
}
};
export const getGCPSecret = async (secretConfig: GCPSecretConfig, providerCredential: CloudProviderCredential) => {
const { secretName, version } = secretConfig;
if (!secretName) {
throw new Error('Get secret from GCP failed: Secret Name is required');
}
const getSecretOption: CloudServiceSecretOption<GCPGetSecretConfig> = {
provider: 'gcp',
secretId: secretName,
credentials: providerCredential.credentials,
config: { version },
};
const secretResult = await window.main.cloudService.getSecret(getSecretOption);
const { success, error, result } = secretResult;
if (success && result) {
return result.value;
} else {
throw new Error(`Get secret from GCP failed: ${error?.errorMessage}`);
}
};

View File

@@ -2,12 +2,13 @@ import React, { useState } from 'react';
import { Button } from 'react-aria-components';
import { debounce } from '../../../../common/misc';
import type { AWSSecretConfig, ExternalVaultConfig } from '../../../../main/ipc/cloud-service-integration/types';
import type { AWSSecretConfig, ExternalVaultConfig, GCPSecretConfig } from '../../../../main/ipc/cloud-service-integration/types';
import { type CloudProviderCredential, type CloudProviderName, type } from '../../../../models/cloud-credential';
import { Icon } from '../../icon';
import { CloudCredentialModal } from '../../modals/cloud-credential-modal/cloud-credential-modal';
import type { ArgConfigFormProps } from '../tag-editor-arg-sub-form';
import { AWSSecretManagerForm } from './aws-secret-manager-form';
import { GCPSecretManagerForm } from './gcp-secret-manager-form';
export const ExternalVaultForm = (props: ArgConfigFormProps) => {
const { onChange, configValue, activeTagData, docs } = props;
@@ -34,6 +35,15 @@ export const ExternalVaultForm = (props: ArgConfigFormProps) => {
/>
);
break;
case 'gcp':
SubForm = (
<GCPSecretManagerForm
formData={formData as GCPSecretConfig}
onChange={handleFormChange}
activeTagData={activeTagData}
/>
);
break;
default:
SubForm = null;
};

View File

@@ -0,0 +1,58 @@
import React from 'react';
import type { GCPSecretConfig } from '../../../../main/ipc/cloud-service-integration/types';
import type { NunjucksParsedTag } from '../../../../templating/utils';
import { HelpTooltip } from '../../help-tooltip';
export interface GCPSecretManagerFormProps {
formData: GCPSecretConfig;
onChange: (newConfig: GCPSecretConfig) => void;
activeTagData: NunjucksParsedTag;
}
export const GCPSecretManagerForm = (props: GCPSecretManagerFormProps) => {
const { formData, onChange } = props;
const {
secretName,
version = 'latest',
} = formData;
const handleOnChange = (name: keyof GCPSecretConfig, newValue: string) => {
const newConfig = {
...formData,
[name]: newValue,
};
onChange(newConfig as unknown as GCPSecretConfig);
};
return (
<form id='gcp-secret-manager-form'>
<div className="form-row">
<div className="form-control">
<label>
Secret Name
<input
name='secretName'
defaultValue={secretName}
onChange={e => handleOnChange('secretName', e.target.value)}
/>
</label>
</div>
</div>
<div className="form-row">
<div className="form-control">
<label>
Version
<HelpTooltip className="space-left">
Optional version of the secret to retrieve, by default as latest.
</HelpTooltip>
<input
name='version'
defaultValue={version}
onChange={e => handleOnChange('version', e.target.value)}
/>
</label>
</div>
</div>
</form>
);
};

View File

@@ -35,6 +35,7 @@ const localTemplatePlugins: { templateTag: PluginTemplateTag }[] = [
type: 'enum',
options: [
{ displayName: 'AWS Secrets Manager', value: 'aws' },
{ displayName: 'GCP Secret Manager', value: 'gcp' },
],
},
{

View File

@@ -2,6 +2,7 @@ import React from 'react';
import type { BaseModel } from '../../../models';
import type { NunjucksParsedTag } from '../../../templating/utils';
import { isValidJSONString } from '../../../utils/json';
import { ExternalVaultForm } from './external-vault/external-vault-form';
export interface ArgConfigFormProps {
@@ -14,14 +15,13 @@ export interface ArgConfigFormProps {
const formTagNameMapping = {
'vault': ExternalVaultForm,
};
const isValidJSONString = (input: string) => {
try {
const parsedJson = JSON.parse(input);
// Check if the parsed JSON is an object and not an array or null
return typeof parsedJson === 'object' && parsedJson !== null && !Array.isArray(parsedJson);
} catch (error) {
return false;
}
const isValidJSONObjectString = (input: string) => {
if (isValidJSONString(input)) {
const parsedContent = JSON.parse(input);
// Check if the parsed JSON is an real object.
return typeof parsedContent === 'object' && parsedContent !== null && !Array.isArray(parsedContent);
};
return false;
};
export const couldRenderForm = (name: string) => name in formTagNameMapping;
@@ -30,7 +30,7 @@ export const ArgConfigSubForm = (props: ArgConfigFormProps) => {
const tagName = activeTagDefinition.name as keyof typeof formTagNameMapping;
const ConfigForm = formTagNameMapping[tagName];
if (ConfigForm && isValidJSONString(configValue)) {
if (ConfigForm && isValidJSONObjectString(configValue)) {
return <ConfigForm {...props} />;
}
return configValue;

View File

@@ -0,0 +1,8 @@
export const isValidJSONString = (input: string): boolean => {
try {
JSON.parse(input);
return true;
} catch (error) {
return false;
}
};