mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 01:27:41 +00:00
Merge pull request #713 from akhilmhdh/feat/secret-reference
secret reference
This commit is contained in:
Generated
+223
-32
@@ -71,6 +71,7 @@
|
|||||||
"@types/node": "^18.11.3",
|
"@types/node": "^18.11.3",
|
||||||
"@types/nodemailer": "^6.4.6",
|
"@types/nodemailer": "^6.4.6",
|
||||||
"@types/passport": "^1.0.12",
|
"@types/passport": "^1.0.12",
|
||||||
|
"@types/picomatch": "^2.3.0",
|
||||||
"@types/supertest": "^2.0.12",
|
"@types/supertest": "^2.0.12",
|
||||||
"@types/swagger-jsdoc": "^6.0.1",
|
"@types/swagger-jsdoc": "^6.0.1",
|
||||||
"@types/swagger-ui-express": "^4.1.3",
|
"@types/swagger-ui-express": "^4.1.3",
|
||||||
@@ -2200,6 +2201,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jest/reporters/node_modules/glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jest/schemas": {
|
"node_modules/@jest/schemas": {
|
||||||
"version": "29.4.3",
|
"version": "29.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz",
|
||||||
@@ -3241,6 +3262,12 @@
|
|||||||
"@types/express": "*"
|
"@types/express": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/picomatch": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/prettier": {
|
"node_modules/@types/prettier": {
|
||||||
"version": "2.7.2",
|
"version": "2.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz",
|
||||||
@@ -5680,25 +5707,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/glob": {
|
|
||||||
"version": "7.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
|
||||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
|
||||||
"dependencies": {
|
|
||||||
"fs.realpath": "^1.0.0",
|
|
||||||
"inflight": "^1.0.4",
|
|
||||||
"inherits": "2",
|
|
||||||
"minimatch": "^3.1.1",
|
|
||||||
"once": "^1.3.0",
|
|
||||||
"path-is-absolute": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/glob-parent": {
|
"node_modules/glob-parent": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
@@ -6481,6 +6489,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jest-config/node_modules/glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jest-diff": {
|
"node_modules/jest-diff": {
|
||||||
"version": "29.5.0",
|
"version": "29.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz",
|
||||||
@@ -6776,6 +6804,26 @@
|
|||||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jest-runtime/node_modules/glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jest-snapshot": {
|
"node_modules/jest-snapshot": {
|
||||||
"version": "29.5.0",
|
"version": "29.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.5.0.tgz",
|
||||||
@@ -11071,6 +11119,25 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rimraf/node_modules/glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ripemd160": {
|
"node_modules/ripemd160": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
|
||||||
@@ -11669,6 +11736,25 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/swagger-autogen/node_modules/glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/swagger-ui-dist": {
|
"node_modules/swagger-ui-dist": {
|
||||||
"version": "4.19.0",
|
"version": "4.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.19.0.tgz",
|
||||||
@@ -11723,6 +11809,26 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/test-exclude/node_modules/glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/text-hex": {
|
"node_modules/text-hex": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
|
||||||
@@ -14154,6 +14260,22 @@
|
|||||||
"string-length": "^4.0.1",
|
"string-length": "^4.0.1",
|
||||||
"strip-ansi": "^6.0.0",
|
"strip-ansi": "^6.0.0",
|
||||||
"v8-to-istanbul": "^9.0.1"
|
"v8-to-istanbul": "^9.0.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@jest/schemas": {
|
"@jest/schemas": {
|
||||||
@@ -14988,6 +15110,12 @@
|
|||||||
"@types/express": "*"
|
"@types/express": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/picomatch": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/prettier": {
|
"@types/prettier": {
|
||||||
"version": "2.7.2",
|
"version": "2.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz",
|
||||||
@@ -16808,19 +16936,6 @@
|
|||||||
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
|
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"glob": {
|
|
||||||
"version": "7.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
|
||||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
|
||||||
"requires": {
|
|
||||||
"fs.realpath": "^1.0.0",
|
|
||||||
"inflight": "^1.0.4",
|
|
||||||
"inherits": "2",
|
|
||||||
"minimatch": "^3.1.1",
|
|
||||||
"once": "^1.3.0",
|
|
||||||
"path-is-absolute": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"glob-parent": {
|
"glob-parent": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
@@ -17371,6 +17486,22 @@
|
|||||||
"pretty-format": "^29.5.0",
|
"pretty-format": "^29.5.0",
|
||||||
"slash": "^3.0.0",
|
"slash": "^3.0.0",
|
||||||
"strip-json-comments": "^3.1.1"
|
"strip-json-comments": "^3.1.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"jest-diff": {
|
"jest-diff": {
|
||||||
@@ -17606,6 +17737,22 @@
|
|||||||
"jest-util": "^29.5.0",
|
"jest-util": "^29.5.0",
|
||||||
"slash": "^3.0.0",
|
"slash": "^3.0.0",
|
||||||
"strip-bom": "^4.0.0"
|
"strip-bom": "^4.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"jest-snapshot": {
|
"jest-snapshot": {
|
||||||
@@ -20674,6 +20821,21 @@
|
|||||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"glob": "^7.1.3"
|
"glob": "^7.1.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"requires": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ripemd160": {
|
"ripemd160": {
|
||||||
@@ -21129,6 +21291,19 @@
|
|||||||
"version": "7.4.1",
|
"version": "7.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||||
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="
|
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="
|
||||||
|
},
|
||||||
|
"glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"requires": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -21174,6 +21349,22 @@
|
|||||||
"@istanbuljs/schema": "^0.1.2",
|
"@istanbuljs/schema": "^0.1.2",
|
||||||
"glob": "^7.1.4",
|
"glob": "^7.1.4",
|
||||||
"minimatch": "^3.0.4"
|
"minimatch": "^3.0.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"text-hex": {
|
"text-hex": {
|
||||||
|
|||||||
@@ -89,6 +89,7 @@
|
|||||||
"@types/node": "^18.11.3",
|
"@types/node": "^18.11.3",
|
||||||
"@types/nodemailer": "^6.4.6",
|
"@types/nodemailer": "^6.4.6",
|
||||||
"@types/passport": "^1.0.12",
|
"@types/passport": "^1.0.12",
|
||||||
|
"@types/picomatch": "^2.3.0",
|
||||||
"@types/supertest": "^2.0.12",
|
"@types/supertest": "^2.0.12",
|
||||||
"@types/swagger-jsdoc": "^6.0.1",
|
"@types/swagger-jsdoc": "^6.0.1",
|
||||||
"@types/swagger-ui-express": "^4.1.3",
|
"@types/swagger-ui-express": "^4.1.3",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
ACTION_UPDATE_SECRETS,
|
ACTION_UPDATE_SECRETS,
|
||||||
ALGORITHM_AES_256_GCM,
|
ALGORITHM_AES_256_GCM,
|
||||||
ENCODING_SCHEME_UTF8,
|
ENCODING_SCHEME_UTF8,
|
||||||
SECRET_PERSONAL,
|
SECRET_PERSONAL
|
||||||
} from "../../variables";
|
} from "../../variables";
|
||||||
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
|
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
|
||||||
import { EventService } from "../../services";
|
import { EventService } from "../../services";
|
||||||
@@ -21,7 +21,7 @@ import { PERMISSION_WRITE_SECRETS } from "../../variables";
|
|||||||
import {
|
import {
|
||||||
userHasNoAbility,
|
userHasNoAbility,
|
||||||
userHasWorkspaceAccess,
|
userHasWorkspaceAccess,
|
||||||
userHasWriteOnlyAbility,
|
userHasWriteOnlyAbility
|
||||||
} from "../../ee/helpers/checkMembershipPermissions";
|
} from "../../ee/helpers/checkMembershipPermissions";
|
||||||
import Tag from "../../models/tag";
|
import Tag from "../../models/tag";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
@@ -30,8 +30,9 @@ import Folder from "../../models/folder";
|
|||||||
import {
|
import {
|
||||||
getFolderByPath,
|
getFolderByPath,
|
||||||
getFolderIdFromServiceToken,
|
getFolderIdFromServiceToken,
|
||||||
searchByFolderId,
|
searchByFolderId
|
||||||
} from "../../services/FolderService";
|
} from "../../services/FolderService";
|
||||||
|
import { isValidScope } from "../../helpers/secrets";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Peform a batch of any specified CUD secret operations
|
* Peform a batch of any specified CUD secret operations
|
||||||
@@ -47,7 +48,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
requests,
|
requests,
|
||||||
secretPath,
|
secretPath
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
@@ -63,7 +64,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
// get secret blind index salt
|
// get secret blind index salt
|
||||||
const salt = await SecretService.getSecretBlindIndexSalt({
|
const salt = await SecretService.getSecretBlindIndexSalt({
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
});
|
});
|
||||||
|
|
||||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||||
@@ -73,22 +74,17 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||||
const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload;
|
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath);
|
||||||
|
|
||||||
// in service token when not giving secretpath folderid must be root
|
// in service token when not giving secretpath folderid must be root
|
||||||
// this is to avoid giving folderid when service tokens are used
|
// this is to avoid giving folderid when service tokens are used
|
||||||
if (
|
if ((!secretPath && folderId !== "root") || (secretPath && !isValidScopeAccess)) {
|
||||||
(!secretPath && folderId !== "root") ||
|
|
||||||
(secretPath && secretPath !== serviceTkScopedSecretPath)
|
|
||||||
) {
|
|
||||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (secretPath) {
|
if (secretPath) {
|
||||||
folderId = await getFolderIdFromServiceToken(
|
folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
secretPath
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for await (const request of requests) {
|
for await (const request of requests) {
|
||||||
@@ -97,12 +93,10 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
let secretBlindIndex = "";
|
let secretBlindIndex = "";
|
||||||
switch (request.method) {
|
switch (request.method) {
|
||||||
case "POST":
|
case "POST":
|
||||||
secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt(
|
secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
|
||||||
{
|
secretName: request.secret.secretName,
|
||||||
secretName: request.secret.secretName,
|
salt
|
||||||
salt,
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
createSecrets.push({
|
createSecrets.push({
|
||||||
...request.secret,
|
...request.secret,
|
||||||
@@ -113,16 +107,14 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
folder: folderId,
|
folder: folderId,
|
||||||
secretBlindIndex,
|
secretBlindIndex,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "PATCH":
|
case "PATCH":
|
||||||
secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt(
|
secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
|
||||||
{
|
secretName: request.secret.secretName,
|
||||||
secretName: request.secret.secretName,
|
salt
|
||||||
salt,
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
updateSecrets.push({
|
updateSecrets.push({
|
||||||
...request.secret,
|
...request.secret,
|
||||||
@@ -130,7 +122,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
secretBlindIndex,
|
secretBlindIndex,
|
||||||
folder: folderId,
|
folder: folderId,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "DELETE":
|
case "DELETE":
|
||||||
@@ -150,9 +142,9 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
...n._doc,
|
...n._doc,
|
||||||
_id: new Types.ObjectId(),
|
_id: new Types.ObjectId(),
|
||||||
secret: n._id,
|
secret: n._id,
|
||||||
isDeleted: false,
|
isDeleted: false
|
||||||
};
|
};
|
||||||
}),
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const addAction = (await EELogService.createAction({
|
const addAction = (await EELogService.createAction({
|
||||||
@@ -161,7 +153,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
serviceAccountId: req.serviceAccount?._id,
|
serviceAccountId: req.serviceAccount?._id,
|
||||||
serviceTokenDataId: req.serviceTokenData?._id,
|
serviceTokenDataId: req.serviceTokenData?._id,
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
secretIds: createdSecrets.map((n) => n._id),
|
secretIds: createdSecrets.map((n) => n._id)
|
||||||
})) as IAction;
|
})) as IAction;
|
||||||
actions.push(addAction);
|
actions.push(addAction);
|
||||||
|
|
||||||
@@ -175,8 +167,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
folderId,
|
folderId,
|
||||||
channel,
|
channel,
|
||||||
userAgent: req.headers?.["user-agent"],
|
userAgent: req.headers?.["user-agent"]
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,7 +187,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
listedSecretsObj = req.secrets.reduce(
|
listedSecretsObj = req.secrets.reduce(
|
||||||
(obj: any, secret: ISecret) => ({
|
(obj: any, secret: ISecret) => ({
|
||||||
...obj,
|
...obj,
|
||||||
[secret._id.toString()]: secret,
|
[secret._id.toString()]: secret
|
||||||
}),
|
}),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
@@ -204,16 +196,16 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
updateOne: {
|
updateOne: {
|
||||||
filter: {
|
filter: {
|
||||||
_id: new Types.ObjectId(u._id),
|
_id: new Types.ObjectId(u._id),
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId)
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
$inc: {
|
$inc: {
|
||||||
version: 1,
|
version: 1
|
||||||
},
|
},
|
||||||
...u,
|
...u,
|
||||||
_id: new Types.ObjectId(u._id),
|
_id: new Types.ObjectId(u._id)
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await Secret.bulkWrite(updateOperations);
|
await Secret.bulkWrite(updateOperations);
|
||||||
@@ -240,25 +232,25 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
tags: u.tags,
|
tags: u.tags,
|
||||||
folder: u.folder,
|
folder: u.folder
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
await EESecretService.addSecretVersions({
|
await EESecretService.addSecretVersions({
|
||||||
secretVersions,
|
secretVersions
|
||||||
});
|
});
|
||||||
|
|
||||||
updatedSecrets = await Secret.find({
|
updatedSecrets = await Secret.find({
|
||||||
_id: {
|
_id: {
|
||||||
$in: updateSecrets.map((u) => new Types.ObjectId(u._id)),
|
$in: updateSecrets.map((u) => new Types.ObjectId(u._id))
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateAction = (await EELogService.createAction({
|
const updateAction = (await EELogService.createAction({
|
||||||
name: ACTION_UPDATE_SECRETS,
|
name: ACTION_UPDATE_SECRETS,
|
||||||
userId: req.user._id,
|
userId: req.user._id,
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
secretIds: updatedSecrets.map((u) => u._id),
|
secretIds: updatedSecrets.map((u) => u._id)
|
||||||
})) as IAction;
|
})) as IAction;
|
||||||
actions.push(updateAction);
|
actions.push(updateAction);
|
||||||
|
|
||||||
@@ -272,8 +264,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
folderId,
|
folderId,
|
||||||
channel,
|
channel,
|
||||||
userAgent: req.headers?.["user-agent"],
|
userAgent: req.headers?.["user-agent"]
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -282,19 +274,19 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
if (deleteSecrets.length > 0) {
|
if (deleteSecrets.length > 0) {
|
||||||
await Secret.deleteMany({
|
await Secret.deleteMany({
|
||||||
_id: {
|
_id: {
|
||||||
$in: deleteSecrets,
|
$in: deleteSecrets
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await EESecretService.markDeletedSecretVersions({
|
await EESecretService.markDeletedSecretVersions({
|
||||||
secretIds: deleteSecrets,
|
secretIds: deleteSecrets
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteAction = (await EELogService.createAction({
|
const deleteAction = (await EELogService.createAction({
|
||||||
name: ACTION_DELETE_SECRETS,
|
name: ACTION_DELETE_SECRETS,
|
||||||
userId: req.user._id,
|
userId: req.user._id,
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
secretIds: deleteSecrets,
|
secretIds: deleteSecrets
|
||||||
})) as IAction;
|
})) as IAction;
|
||||||
actions.push(deleteAction);
|
actions.push(deleteAction);
|
||||||
|
|
||||||
@@ -307,8 +299,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
environment,
|
environment,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
userAgent: req.headers?.["user-agent"],
|
userAgent: req.headers?.["user-agent"]
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -320,22 +312,22 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
actions,
|
actions,
|
||||||
channel,
|
channel,
|
||||||
ipAddress: req.realIP,
|
ipAddress: req.realIP
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// // trigger event - push secrets
|
// // trigger event - push secrets
|
||||||
await EventService.handleEvent({
|
await EventService.handleEvent({
|
||||||
event: eventPushSecrets({
|
event: eventPushSecrets({
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
}),
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
environment,
|
environment,
|
||||||
folderId,
|
folderId
|
||||||
});
|
});
|
||||||
|
|
||||||
const resObj: { [key: string]: ISecret[] | string[] } = {};
|
const resObj: { [key: string]: ISecret[] | string[] } = {};
|
||||||
@@ -418,7 +410,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
const {
|
const {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
secretPath,
|
secretPath
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
@@ -435,8 +427,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
);
|
);
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
throw UnauthorizedRequestError({
|
throw UnauthorizedRequestError({
|
||||||
message:
|
message: "You do not have the necessary permission(s) perform this action"
|
||||||
"You do not have the necessary permission(s) perform this action",
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -449,28 +440,27 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
// case: create 1 secret
|
// case: create 1 secret
|
||||||
listOfSecretsToCreate = [req.body.secrets];
|
listOfSecretsToCreate = [req.body.secrets];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||||
const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload;
|
const isValidScopeAccess = isValidScope(
|
||||||
|
req.authData.authPayload,
|
||||||
|
environment,
|
||||||
|
secretPath || "/"
|
||||||
|
);
|
||||||
|
|
||||||
// in service token when not giving secretpath folderid must be root
|
// in service token when not giving secretpath folderid must be root
|
||||||
// this is to avoid giving folderid when service tokens are used
|
// this is to avoid giving folderid when service tokens are used
|
||||||
if (
|
if ((!secretPath && folderId !== "root") || (secretPath && !isValidScopeAccess)) {
|
||||||
(!secretPath && folderId !== "root") ||
|
|
||||||
(secretPath && secretPath !== serviceTkScopedSecretPath)
|
|
||||||
) {
|
|
||||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (secretPath) {
|
if (secretPath) {
|
||||||
folderId = await getFolderIdFromServiceToken(
|
folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
secretPath
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get secret blind index salt
|
// get secret blind index salt
|
||||||
const salt = await SecretService.getSecretBlindIndexSalt({
|
const salt = await SecretService.getSecretBlindIndexSalt({
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
});
|
});
|
||||||
|
|
||||||
type secretsToCreateType = {
|
type secretsToCreateType = {
|
||||||
@@ -502,15 +492,14 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
secretCommentCiphertext,
|
secretCommentCiphertext,
|
||||||
secretCommentIV,
|
secretCommentIV,
|
||||||
secretCommentTag,
|
secretCommentTag,
|
||||||
tags,
|
tags
|
||||||
}: secretsToCreateType) => {
|
}: secretsToCreateType) => {
|
||||||
let secretBlindIndex;
|
let secretBlindIndex;
|
||||||
if (secretName) {
|
if (secretName) {
|
||||||
secretBlindIndex =
|
secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
|
||||||
await SecretService.generateSecretBlindIndexWithSalt({
|
secretName,
|
||||||
secretName,
|
salt
|
||||||
salt,
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -532,22 +521,22 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
secretCommentTag,
|
secretCommentTag,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
tags,
|
tags
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const newlyCreatedSecrets: ISecret[] = (
|
const newlyCreatedSecrets: ISecret[] = (await Secret.insertMany(secretsToInsert)).map(
|
||||||
await Secret.insertMany(secretsToInsert)
|
(insertedSecret) => insertedSecret.toObject()
|
||||||
).map((insertedSecret) => insertedSecret.toObject());
|
);
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
// trigger event - push secrets
|
// trigger event - push secrets
|
||||||
await EventService.handleEvent({
|
await EventService.handleEvent({
|
||||||
event: eventPushSecrets({
|
event: eventPushSecrets({
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
}),
|
})
|
||||||
});
|
});
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
@@ -567,7 +556,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
secretKeyTag,
|
secretKeyTag,
|
||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag
|
||||||
}) =>
|
}) =>
|
||||||
new SecretVersion({
|
new SecretVersion({
|
||||||
secret: _id,
|
secret: _id,
|
||||||
@@ -586,9 +575,9 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
secretValueTag,
|
secretValueTag,
|
||||||
folder: folderId,
|
folder: folderId,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
})
|
})
|
||||||
),
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
const addAction = await EELogService.createAction({
|
const addAction = await EELogService.createAction({
|
||||||
@@ -597,7 +586,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
serviceAccountId: req.serviceAccount?._id,
|
serviceAccountId: req.serviceAccount?._id,
|
||||||
serviceTokenDataId: req.serviceTokenData?._id,
|
serviceTokenDataId: req.serviceTokenData?._id,
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
secretIds: newlyCreatedSecrets.map((n) => n._id),
|
secretIds: newlyCreatedSecrets.map((n) => n._id)
|
||||||
});
|
});
|
||||||
|
|
||||||
// (EE) create (audit) log
|
// (EE) create (audit) log
|
||||||
@@ -609,14 +598,14 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
actions: [addAction],
|
actions: [addAction],
|
||||||
channel,
|
channel,
|
||||||
ipAddress: req.realIP,
|
ipAddress: req.realIP
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
environment,
|
environment,
|
||||||
folderId,
|
folderId
|
||||||
});
|
});
|
||||||
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
@@ -624,7 +613,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
event: "secrets added",
|
event: "secrets added",
|
||||||
distinctId: await TelemetryService.getDistinctId({
|
distinctId: await TelemetryService.getDistinctId({
|
||||||
authData: req.authData,
|
authData: req.authData
|
||||||
}),
|
}),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: listOfSecretsToCreate.length,
|
numberOfSecrets: listOfSecretsToCreate.length,
|
||||||
@@ -632,13 +621,13 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
folderId,
|
folderId,
|
||||||
userAgent: req.headers?.["user-agent"],
|
userAgent: req.headers?.["user-agent"]
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
secrets: newlyCreatedSecrets,
|
secrets: newlyCreatedSecrets
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -696,10 +685,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
const environment = req.query.environment as string;
|
const environment = req.query.environment as string;
|
||||||
|
|
||||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||||
if (
|
if ((!folders && folderId && folderId !== "root") || (!folders && secretPath)) {
|
||||||
(!folders && folderId && folderId !== "root") ||
|
|
||||||
(!folders && secretPath)
|
|
||||||
) {
|
|
||||||
res.send({ secrets: [] });
|
res.send({ secrets: [] });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -712,13 +698,15 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||||
const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload;
|
const isValidScopeAccess = isValidScope(
|
||||||
|
req.authData.authPayload,
|
||||||
|
environment,
|
||||||
|
(secretPath as string) || "/"
|
||||||
|
);
|
||||||
|
|
||||||
// in service token when not giving secretpath folderid must be root
|
// in service token when not giving secretpath folderid must be root
|
||||||
// this is to avoid giving folderid when service tokens are used
|
// this is to avoid giving folderid when service tokens are used
|
||||||
if (
|
if ((!secretPath && folderId !== "root") || (secretPath && !isValidScopeAccess)) {
|
||||||
(!secretPath && folderId !== "root") ||
|
|
||||||
(secretPath && secretPath !== serviceTkScopedSecretPath)
|
|
||||||
) {
|
|
||||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -738,8 +726,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
// query tags table to get all tags ids for the tag names for the given workspace
|
// query tags table to get all tags ids for the tag names for the given workspace
|
||||||
let tagIds = [];
|
let tagIds = [];
|
||||||
const tagNamesList =
|
const tagNamesList = typeof tagSlugs === "string" && tagSlugs !== "" ? tagSlugs.split(",") : [];
|
||||||
typeof tagSlugs === "string" && tagSlugs !== "" ? tagSlugs.split(",") : [];
|
|
||||||
if (tagNamesList != undefined && tagNamesList.length != 0) {
|
if (tagNamesList != undefined && tagNamesList.length != 0) {
|
||||||
const workspaceFromDB = await Tag.find({ workspace: workspaceId });
|
const workspaceFromDB = await Tag.find({ workspace: workspaceId });
|
||||||
tagIds = _.map(tagNamesList, (tagName: string) => {
|
tagIds = _.map(tagNamesList, (tagName: string) => {
|
||||||
@@ -762,8 +749,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
);
|
);
|
||||||
if (hasNoAccess) {
|
if (hasNoAccess) {
|
||||||
throw UnauthorizedRequestError({
|
throw UnauthorizedRequestError({
|
||||||
message:
|
message: "You do not have the necessary permission(s) perform this action"
|
||||||
"You do not have the necessary permission(s) perform this action",
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -773,8 +759,8 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
folder: folderId,
|
folder: folderId,
|
||||||
$or: [
|
$or: [
|
||||||
{ user: req.user._id }, // personal secrets for this user
|
{ user: req.user._id }, // personal secrets for this user
|
||||||
{ user: { $exists: false } }, // shared secrets from workspace
|
{ user: { $exists: false } } // shared secrets from workspace
|
||||||
],
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
if (tagIds.length > 0) {
|
if (tagIds.length > 0) {
|
||||||
@@ -801,8 +787,8 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
environment,
|
environment,
|
||||||
$or: [
|
$or: [
|
||||||
{ user: userId }, // personal secrets for this user
|
{ user: userId }, // personal secrets for this user
|
||||||
{ user: { $exists: false } }, // shared secrets from workspace
|
{ user: { $exists: false } } // shared secrets from workspace
|
||||||
],
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
if (tagIds.length > 0) {
|
if (tagIds.length > 0) {
|
||||||
@@ -820,7 +806,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
environment,
|
environment,
|
||||||
folder: folderId,
|
folder: folderId,
|
||||||
user: { $exists: false }, // shared secrets only from workspace
|
user: { $exists: false } // shared secrets only from workspace
|
||||||
};
|
};
|
||||||
|
|
||||||
if (tagIds.length > 0) {
|
if (tagIds.length > 0) {
|
||||||
@@ -838,7 +824,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
serviceAccountId: req.serviceAccount?._id,
|
serviceAccountId: req.serviceAccount?._id,
|
||||||
serviceTokenDataId: req.serviceTokenData?._id,
|
serviceTokenDataId: req.serviceTokenData?._id,
|
||||||
workspaceId: new Types.ObjectId(workspaceId as string),
|
workspaceId: new Types.ObjectId(workspaceId as string),
|
||||||
secretIds: secrets.map((n: any) => n._id),
|
secretIds: secrets.map((n: any) => n._id)
|
||||||
});
|
});
|
||||||
|
|
||||||
readAction &&
|
readAction &&
|
||||||
@@ -849,7 +835,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId: new Types.ObjectId(workspaceId as string),
|
workspaceId: new Types.ObjectId(workspaceId as string),
|
||||||
actions: [readAction],
|
actions: [readAction],
|
||||||
channel,
|
channel,
|
||||||
ipAddress: req.realIP,
|
ipAddress: req.realIP
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
@@ -857,7 +843,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
event: "secrets pulled",
|
event: "secrets pulled",
|
||||||
distinctId: await TelemetryService.getDistinctId({
|
distinctId: await TelemetryService.getDistinctId({
|
||||||
authData: req.authData,
|
authData: req.authData
|
||||||
}),
|
}),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: secrets.length,
|
numberOfSecrets: secrets.length,
|
||||||
@@ -865,13 +851,13 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
channel,
|
channel,
|
||||||
folderId,
|
folderId,
|
||||||
userAgent: req.headers?.["user-agent"],
|
userAgent: req.headers?.["user-agent"]
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
secrets,
|
secrets
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -925,9 +911,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
const channel = req.headers?.["user-agent"]?.toLowerCase().includes("mozilla")
|
const channel = req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli";
|
||||||
? "web"
|
|
||||||
: "cli";
|
|
||||||
|
|
||||||
interface PatchSecret {
|
interface PatchSecret {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -943,51 +927,47 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateOperationsToPerform = req.body.secrets.map(
|
const updateOperationsToPerform = req.body.secrets.map((secret: PatchSecret) => {
|
||||||
(secret: PatchSecret) => {
|
const {
|
||||||
const {
|
secretKeyCiphertext,
|
||||||
secretKeyCiphertext,
|
secretKeyIV,
|
||||||
secretKeyIV,
|
secretKeyTag,
|
||||||
secretKeyTag,
|
secretValueCiphertext,
|
||||||
secretValueCiphertext,
|
secretValueIV,
|
||||||
secretValueIV,
|
secretValueTag,
|
||||||
secretValueTag,
|
secretCommentCiphertext,
|
||||||
secretCommentCiphertext,
|
secretCommentIV,
|
||||||
secretCommentIV,
|
secretCommentTag,
|
||||||
secretCommentTag,
|
tags
|
||||||
tags,
|
} = secret;
|
||||||
} = secret;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
updateOne: {
|
updateOne: {
|
||||||
filter: { _id: new Types.ObjectId(secret.id) },
|
filter: { _id: new Types.ObjectId(secret.id) },
|
||||||
update: {
|
update: {
|
||||||
$inc: {
|
$inc: {
|
||||||
version: 1,
|
version: 1
|
||||||
},
|
|
||||||
secretKeyCiphertext,
|
|
||||||
secretKeyIV,
|
|
||||||
secretKeyTag,
|
|
||||||
secretValueCiphertext,
|
|
||||||
secretValueIV,
|
|
||||||
secretValueTag,
|
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
|
||||||
tags,
|
|
||||||
...(secretCommentCiphertext !== undefined &&
|
|
||||||
secretCommentIV &&
|
|
||||||
secretCommentTag
|
|
||||||
? {
|
|
||||||
secretCommentCiphertext,
|
|
||||||
secretCommentIV,
|
|
||||||
secretCommentTag,
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
},
|
},
|
||||||
},
|
secretKeyCiphertext,
|
||||||
};
|
secretKeyIV,
|
||||||
}
|
secretKeyTag,
|
||||||
);
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
|
tags,
|
||||||
|
...(secretCommentCiphertext !== undefined && secretCommentIV && secretCommentTag
|
||||||
|
? {
|
||||||
|
secretCommentCiphertext,
|
||||||
|
secretCommentIV,
|
||||||
|
secretCommentTag
|
||||||
|
}
|
||||||
|
: {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
await Secret.bulkWrite(updateOperationsToPerform);
|
await Secret.bulkWrite(updateOperationsToPerform);
|
||||||
|
|
||||||
@@ -1009,7 +989,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
|||||||
secretCommentCiphertext,
|
secretCommentCiphertext,
|
||||||
secretCommentIV,
|
secretCommentIV,
|
||||||
secretCommentTag,
|
secretCommentTag,
|
||||||
tags,
|
tags
|
||||||
} = secretModificationsBySecretId[secret._id.toString()];
|
} = secretModificationsBySecretId[secret._id.toString()];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -1018,9 +998,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
|||||||
workspace: secret.workspace,
|
workspace: secret.workspace,
|
||||||
type: secret.type,
|
type: secret.type,
|
||||||
environment: secret.environment,
|
environment: secret.environment,
|
||||||
secretKeyCiphertext: secretKeyCiphertext
|
secretKeyCiphertext: secretKeyCiphertext ? secretKeyCiphertext : secret.secretKeyCiphertext,
|
||||||
? secretKeyCiphertext
|
|
||||||
: secret.secretKeyCiphertext,
|
|
||||||
secretKeyIV: secretKeyIV ? secretKeyIV : secret.secretKeyIV,
|
secretKeyIV: secretKeyIV ? secretKeyIV : secret.secretKeyIV,
|
||||||
secretKeyTag: secretKeyTag ? secretKeyTag : secret.secretKeyTag,
|
secretKeyTag: secretKeyTag ? secretKeyTag : secret.secretKeyTag,
|
||||||
secretValueCiphertext: secretValueCiphertext
|
secretValueCiphertext: secretValueCiphertext
|
||||||
@@ -1031,17 +1009,13 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
|||||||
secretCommentCiphertext: secretCommentCiphertext
|
secretCommentCiphertext: secretCommentCiphertext
|
||||||
? secretCommentCiphertext
|
? secretCommentCiphertext
|
||||||
: secret.secretCommentCiphertext,
|
: secret.secretCommentCiphertext,
|
||||||
secretCommentIV: secretCommentIV
|
secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV,
|
||||||
? secretCommentIV
|
secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag,
|
||||||
: secret.secretCommentIV,
|
|
||||||
secretCommentTag: secretCommentTag
|
|
||||||
? secretCommentTag
|
|
||||||
: secret.secretCommentTag,
|
|
||||||
tags: tags ? tags : secret.tags,
|
tags: tags ? tags : secret.tags,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
};
|
};
|
||||||
}),
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
await EESecretService.addSecretVersions(secretVersions);
|
await EESecretService.addSecretVersions(secretVersions);
|
||||||
@@ -1062,8 +1036,8 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
|||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await EventService.handleEvent({
|
await EventService.handleEvent({
|
||||||
event: eventPushSecrets({
|
event: eventPushSecrets({
|
||||||
workspaceId: new Types.ObjectId(key),
|
workspaceId: new Types.ObjectId(key)
|
||||||
}),
|
})
|
||||||
});
|
});
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
@@ -1073,7 +1047,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
|||||||
serviceAccountId: req.serviceAccount?._id,
|
serviceAccountId: req.serviceAccount?._id,
|
||||||
serviceTokenDataId: req.serviceTokenData?._id,
|
serviceTokenDataId: req.serviceTokenData?._id,
|
||||||
workspaceId: new Types.ObjectId(key),
|
workspaceId: new Types.ObjectId(key),
|
||||||
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id),
|
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
|
||||||
});
|
});
|
||||||
|
|
||||||
// (EE) create (audit) log
|
// (EE) create (audit) log
|
||||||
@@ -1085,7 +1059,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId: new Types.ObjectId(key),
|
workspaceId: new Types.ObjectId(key),
|
||||||
actions: [updateAction],
|
actions: [updateAction],
|
||||||
channel,
|
channel,
|
||||||
ipAddress: req.realIP,
|
ipAddress: req.realIP
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
@@ -1101,15 +1075,15 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
|||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
event: "secrets modified",
|
event: "secrets modified",
|
||||||
distinctId: await TelemetryService.getDistinctId({
|
distinctId: await TelemetryService.getDistinctId({
|
||||||
authData: req.authData,
|
authData: req.authData
|
||||||
}),
|
}),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: workspaceSecretObj[key].length,
|
numberOfSecrets: workspaceSecretObj[key].length,
|
||||||
environment: workspaceSecretObj[key][0].environment,
|
environment: workspaceSecretObj[key][0].environment,
|
||||||
workspaceId: key,
|
workspaceId: key,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
userAgent: req.headers?.["user-agent"],
|
userAgent: req.headers?.["user-agent"]
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1117,9 +1091,9 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
|||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
secrets: await Secret.find({
|
secrets: await Secret.find({
|
||||||
_id: {
|
_id: {
|
||||||
$in: req.secrets.map((secret: ISecret) => secret._id),
|
$in: req.secrets.map((secret: ISecret) => secret._id)
|
||||||
},
|
}
|
||||||
}),
|
})
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1179,12 +1153,12 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
await Secret.deleteMany({
|
await Secret.deleteMany({
|
||||||
_id: {
|
_id: {
|
||||||
$in: toDelete,
|
$in: toDelete
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await EESecretService.markDeletedSecretVersions({
|
await EESecretService.markDeletedSecretVersions({
|
||||||
secretIds: toDelete,
|
secretIds: toDelete
|
||||||
});
|
});
|
||||||
|
|
||||||
// group secrets into workspaces so deleted secrets can
|
// group secrets into workspaces so deleted secrets can
|
||||||
@@ -1202,8 +1176,8 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
|||||||
// trigger event - push secrets
|
// trigger event - push secrets
|
||||||
await EventService.handleEvent({
|
await EventService.handleEvent({
|
||||||
event: eventPushSecrets({
|
event: eventPushSecrets({
|
||||||
workspaceId: new Types.ObjectId(key),
|
workspaceId: new Types.ObjectId(key)
|
||||||
}),
|
})
|
||||||
});
|
});
|
||||||
const deleteAction = await EELogService.createAction({
|
const deleteAction = await EELogService.createAction({
|
||||||
name: ACTION_DELETE_SECRETS,
|
name: ACTION_DELETE_SECRETS,
|
||||||
@@ -1211,7 +1185,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
|||||||
serviceAccountId: req.serviceAccount?._id,
|
serviceAccountId: req.serviceAccount?._id,
|
||||||
serviceTokenDataId: req.serviceTokenData?._id,
|
serviceTokenDataId: req.serviceTokenData?._id,
|
||||||
workspaceId: new Types.ObjectId(key),
|
workspaceId: new Types.ObjectId(key),
|
||||||
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id),
|
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
|
||||||
});
|
});
|
||||||
|
|
||||||
// (EE) create (audit) log
|
// (EE) create (audit) log
|
||||||
@@ -1223,7 +1197,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId: new Types.ObjectId(key),
|
workspaceId: new Types.ObjectId(key),
|
||||||
actions: [deleteAction],
|
actions: [deleteAction],
|
||||||
channel,
|
channel,
|
||||||
ipAddress: req.realIP,
|
ipAddress: req.realIP
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
@@ -1237,20 +1211,20 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
|||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
event: "secrets deleted",
|
event: "secrets deleted",
|
||||||
distinctId: await TelemetryService.getDistinctId({
|
distinctId: await TelemetryService.getDistinctId({
|
||||||
authData: req.authData,
|
authData: req.authData
|
||||||
}),
|
}),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: workspaceSecretObj[key].length,
|
numberOfSecrets: workspaceSecretObj[key].length,
|
||||||
environment: workspaceSecretObj[key][0].environment,
|
environment: workspaceSecretObj[key][0].environment,
|
||||||
workspaceId: key,
|
workspaceId: key,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
userAgent: req.headers?.["user-agent"],
|
userAgent: req.headers?.["user-agent"]
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
secrets: req.secrets,
|
secrets: req.secrets
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ import { Request, Response } from "express";
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import { ServiceAccount, ServiceTokenData, User } from "../../models";
|
import { ServiceAccount, ServiceTokenData, User } from "../../models";
|
||||||
import {
|
import { AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT } from "../../variables";
|
||||||
AUTH_MODE_JWT,
|
|
||||||
AUTH_MODE_SERVICE_ACCOUNT,
|
|
||||||
} from "../../variables";
|
|
||||||
import { getSaltRounds } from "../../config";
|
import { getSaltRounds } from "../../config";
|
||||||
import { BadRequestError } from "../../utils/errors";
|
import { BadRequestError } from "../../utils/errors";
|
||||||
import Folder from "../../models/folder";
|
import Folder from "../../models/folder";
|
||||||
@@ -46,14 +43,13 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
if (!(req.authData.authPayload instanceof ServiceTokenData))
|
if (!(req.authData.authPayload instanceof ServiceTokenData))
|
||||||
throw BadRequestError({
|
throw BadRequestError({
|
||||||
message: "Failed accepted client validation for service token data",
|
message: "Failed accepted client validation for service token data"
|
||||||
});
|
});
|
||||||
|
|
||||||
const serviceTokenData = await ServiceTokenData.findById(
|
const serviceTokenData = await ServiceTokenData.findById(req.authData.authPayload._id)
|
||||||
req.authData.authPayload._id
|
|
||||||
)
|
|
||||||
.select("+encryptedKey +iv +tag")
|
.select("+encryptedKey +iv +tag")
|
||||||
.populate("user").lean();
|
.populate("user")
|
||||||
|
.lean();
|
||||||
|
|
||||||
return res.status(200).json(serviceTokenData);
|
return res.status(200).json(serviceTokenData);
|
||||||
};
|
};
|
||||||
@@ -68,29 +64,7 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
|
|||||||
export const createServiceTokenData = async (req: Request, res: Response) => {
|
export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||||
let serviceTokenData;
|
let serviceTokenData;
|
||||||
|
|
||||||
const {
|
const { name, workspaceId, encryptedKey, iv, tag, expiresIn, permissions, scopes } = req.body;
|
||||||
name,
|
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
encryptedKey,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
expiresIn,
|
|
||||||
secretPath,
|
|
||||||
permissions,
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
const folders = await Folder.findOne({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (folders) {
|
|
||||||
const folder = getFolderByPath(folders.nodes, secretPath);
|
|
||||||
if (folder == undefined) {
|
|
||||||
throw BadRequestError({ message: "Path for service token does not exist" })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const secret = crypto.randomBytes(16).toString("hex");
|
const secret = crypto.randomBytes(16).toString("hex");
|
||||||
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
||||||
@@ -103,10 +77,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
let user, serviceAccount;
|
let user, serviceAccount;
|
||||||
|
|
||||||
if (
|
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User) {
|
||||||
req.authData.authMode === AUTH_MODE_JWT &&
|
|
||||||
req.authData.authPayload instanceof User
|
|
||||||
) {
|
|
||||||
user = req.authData.authPayload._id;
|
user = req.authData.authPayload._id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,17 +91,16 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
|||||||
serviceTokenData = await new ServiceTokenData({
|
serviceTokenData = await new ServiceTokenData({
|
||||||
name,
|
name,
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
environment,
|
|
||||||
user,
|
user,
|
||||||
serviceAccount,
|
serviceAccount,
|
||||||
|
scopes,
|
||||||
lastUsed: new Date(),
|
lastUsed: new Date(),
|
||||||
expiresAt,
|
expiresAt,
|
||||||
secretHash,
|
secretHash,
|
||||||
encryptedKey,
|
encryptedKey,
|
||||||
iv,
|
iv,
|
||||||
tag,
|
tag,
|
||||||
secretPath,
|
permissions
|
||||||
permissions,
|
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
// return service token data without sensitive data
|
// return service token data without sensitive data
|
||||||
@@ -142,7 +112,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
serviceToken,
|
serviceToken,
|
||||||
serviceTokenData,
|
serviceTokenData
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -155,11 +125,9 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
|||||||
export const deleteServiceTokenData = async (req: Request, res: Response) => {
|
export const deleteServiceTokenData = async (req: Request, res: Response) => {
|
||||||
const { serviceTokenDataId } = req.params;
|
const { serviceTokenDataId } = req.params;
|
||||||
|
|
||||||
const serviceTokenData = await ServiceTokenData.findByIdAndDelete(
|
const serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
|
||||||
serviceTokenDataId
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
serviceTokenData,
|
serviceTokenData
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+141
-169
@@ -4,13 +4,14 @@ import {
|
|||||||
DeleteSecretParams,
|
DeleteSecretParams,
|
||||||
GetSecretParams,
|
GetSecretParams,
|
||||||
GetSecretsParams,
|
GetSecretsParams,
|
||||||
UpdateSecretParams,
|
UpdateSecretParams
|
||||||
} from "../interfaces/services/SecretService";
|
} from "../interfaces/services/SecretService";
|
||||||
import {
|
import {
|
||||||
ISecret,
|
ISecret,
|
||||||
|
IServiceTokenData,
|
||||||
Secret,
|
Secret,
|
||||||
SecretBlindIndexData,
|
SecretBlindIndexData,
|
||||||
ServiceTokenData,
|
ServiceTokenData
|
||||||
} from "../models";
|
} from "../models";
|
||||||
import { SecretVersion } from "../ee/models";
|
import { SecretVersion } from "../ee/models";
|
||||||
import {
|
import {
|
||||||
@@ -18,7 +19,7 @@ import {
|
|||||||
InternalServerError,
|
InternalServerError,
|
||||||
SecretBlindIndexDataNotFoundError,
|
SecretBlindIndexDataNotFoundError,
|
||||||
SecretNotFoundError,
|
SecretNotFoundError,
|
||||||
UnauthorizedRequestError,
|
UnauthorizedRequestError
|
||||||
} from "../utils/errors";
|
} from "../utils/errors";
|
||||||
import {
|
import {
|
||||||
ACTION_ADD_SECRETS,
|
ACTION_ADD_SECRETS,
|
||||||
@@ -29,22 +30,35 @@ import {
|
|||||||
ENCODING_SCHEME_BASE64,
|
ENCODING_SCHEME_BASE64,
|
||||||
ENCODING_SCHEME_UTF8,
|
ENCODING_SCHEME_UTF8,
|
||||||
SECRET_PERSONAL,
|
SECRET_PERSONAL,
|
||||||
SECRET_SHARED,
|
SECRET_SHARED
|
||||||
} from "../variables";
|
} from "../variables";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import * as argon2 from "argon2";
|
import * as argon2 from "argon2";
|
||||||
import {
|
import {
|
||||||
decryptSymmetric128BitHexKeyUTF8,
|
decryptSymmetric128BitHexKeyUTF8,
|
||||||
encryptSymmetric128BitHexKeyUTF8,
|
encryptSymmetric128BitHexKeyUTF8
|
||||||
} from "../utils/crypto";
|
} from "../utils/crypto";
|
||||||
import { TelemetryService } from "../services";
|
import { TelemetryService } from "../services";
|
||||||
import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
|
import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
|
||||||
import { EELogService, EESecretService } from "../ee/services";
|
import { EELogService, EESecretService } from "../ee/services";
|
||||||
import {
|
import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/auth";
|
||||||
getAuthDataPayloadIdObj,
|
|
||||||
getAuthDataPayloadUserObj,
|
|
||||||
} from "../utils/auth";
|
|
||||||
import { getFolderIdFromServiceToken } from "../services/FolderService";
|
import { getFolderIdFromServiceToken } from "../services/FolderService";
|
||||||
|
import picomatch from "picomatch";
|
||||||
|
|
||||||
|
export const isValidScope = (
|
||||||
|
authPayload: IServiceTokenData,
|
||||||
|
environment: string,
|
||||||
|
secretPath: string
|
||||||
|
) => {
|
||||||
|
const { scopes: tkScopes } = authPayload;
|
||||||
|
const validScope = tkScopes.find(
|
||||||
|
(scope) =>
|
||||||
|
picomatch.isMatch(secretPath, scope.secretPath, { strictSlashes: false }) &&
|
||||||
|
scope.environment === environment
|
||||||
|
);
|
||||||
|
|
||||||
|
return Boolean(validScope);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an object containing secret [secret] but with its value, key, comment decrypted.
|
* Returns an object containing secret [secret] but with its value, key, comment decrypted.
|
||||||
@@ -54,26 +68,19 @@ import { getFolderIdFromServiceToken } from "../services/FolderService";
|
|||||||
* @param {String} key - symmetric key to use to decrypt secret
|
* @param {String} key - symmetric key to use to decrypt secret
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const repackageSecretToRaw = ({
|
export const repackageSecretToRaw = ({ secret, key }: { secret: ISecret; key: string }) => {
|
||||||
secret,
|
|
||||||
key,
|
|
||||||
}: {
|
|
||||||
secret: ISecret;
|
|
||||||
key: string;
|
|
||||||
}) => {
|
|
||||||
|
|
||||||
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext: secret.secretKeyCiphertext,
|
ciphertext: secret.secretKeyCiphertext,
|
||||||
iv: secret.secretKeyIV,
|
iv: secret.secretKeyIV,
|
||||||
tag: secret.secretKeyTag,
|
tag: secret.secretKeyTag,
|
||||||
key,
|
key
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretValue = decryptSymmetric128BitHexKeyUTF8({
|
const secretValue = decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext: secret.secretValueCiphertext,
|
ciphertext: secret.secretValueCiphertext,
|
||||||
iv: secret.secretValueIV,
|
iv: secret.secretValueIV,
|
||||||
tag: secret.secretValueTag,
|
tag: secret.secretValueTag,
|
||||||
key,
|
key
|
||||||
});
|
});
|
||||||
|
|
||||||
let secretComment = "";
|
let secretComment = "";
|
||||||
@@ -83,11 +90,11 @@ export const repackageSecretToRaw = ({
|
|||||||
ciphertext: secret.secretCommentCiphertext,
|
ciphertext: secret.secretCommentCiphertext,
|
||||||
iv: secret.secretCommentIV,
|
iv: secret.secretCommentIV,
|
||||||
tag: secret.secretCommentTag,
|
tag: secret.secretCommentTag,
|
||||||
key,
|
key
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return ({
|
return {
|
||||||
_id: secret._id,
|
_id: secret._id,
|
||||||
version: secret.version,
|
version: secret.version,
|
||||||
workspace: secret.workspace,
|
workspace: secret.workspace,
|
||||||
@@ -96,9 +103,9 @@ export const repackageSecretToRaw = ({
|
|||||||
user: secret.user,
|
user: secret.user,
|
||||||
secretKey,
|
secretKey,
|
||||||
secretValue,
|
secretValue,
|
||||||
secretComment,
|
secretComment
|
||||||
});
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create secret blind index data containing encrypted blind index [salt]
|
* Create secret blind index data containing encrypted blind index [salt]
|
||||||
@@ -107,7 +114,7 @@ export const repackageSecretToRaw = ({
|
|||||||
* @param {Types.ObjectId} obj.workspaceId
|
* @param {Types.ObjectId} obj.workspaceId
|
||||||
*/
|
*/
|
||||||
export const createSecretBlindIndexDataHelper = async ({
|
export const createSecretBlindIndexDataHelper = async ({
|
||||||
workspaceId,
|
workspaceId
|
||||||
}: {
|
}: {
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -121,7 +128,7 @@ export const createSecretBlindIndexDataHelper = async ({
|
|||||||
const {
|
const {
|
||||||
ciphertext: encryptedSaltCiphertext,
|
ciphertext: encryptedSaltCiphertext,
|
||||||
iv: saltIV,
|
iv: saltIV,
|
||||||
tag: saltTag,
|
tag: saltTag
|
||||||
} = client.encryptSymmetric(salt, rootEncryptionKey);
|
} = client.encryptSymmetric(salt, rootEncryptionKey);
|
||||||
|
|
||||||
return await new SecretBlindIndexData({
|
return await new SecretBlindIndexData({
|
||||||
@@ -130,16 +137,16 @@ export const createSecretBlindIndexDataHelper = async ({
|
|||||||
saltIV,
|
saltIV,
|
||||||
saltTag,
|
saltTag,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_BASE64,
|
keyEncoding: ENCODING_SCHEME_BASE64
|
||||||
}).save();
|
}).save();
|
||||||
} else {
|
} else {
|
||||||
const {
|
const {
|
||||||
ciphertext: encryptedSaltCiphertext,
|
ciphertext: encryptedSaltCiphertext,
|
||||||
iv: saltIV,
|
iv: saltIV,
|
||||||
tag: saltTag,
|
tag: saltTag
|
||||||
} = encryptSymmetric128BitHexKeyUTF8({
|
} = encryptSymmetric128BitHexKeyUTF8({
|
||||||
plaintext: salt,
|
plaintext: salt,
|
||||||
key: encryptionKey,
|
key: encryptionKey
|
||||||
});
|
});
|
||||||
|
|
||||||
return await new SecretBlindIndexData({
|
return await new SecretBlindIndexData({
|
||||||
@@ -148,7 +155,7 @@ export const createSecretBlindIndexDataHelper = async ({
|
|||||||
saltIV,
|
saltIV,
|
||||||
saltTag,
|
saltTag,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
}).save();
|
}).save();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -160,7 +167,7 @@ export const createSecretBlindIndexDataHelper = async ({
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getSecretBlindIndexSaltHelper = async ({
|
export const getSecretBlindIndexSaltHelper = async ({
|
||||||
workspaceId,
|
workspaceId
|
||||||
}: {
|
}: {
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -168,36 +175,30 @@ export const getSecretBlindIndexSaltHelper = async ({
|
|||||||
const rootEncryptionKey = await getRootEncryptionKey();
|
const rootEncryptionKey = await getRootEncryptionKey();
|
||||||
|
|
||||||
const secretBlindIndexData = await SecretBlindIndexData.findOne({
|
const secretBlindIndexData = await SecretBlindIndexData.findOne({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId
|
||||||
}).select("+algorithm +keyEncoding");
|
}).select("+algorithm +keyEncoding");
|
||||||
|
|
||||||
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
|
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
|
||||||
|
|
||||||
if (
|
if (rootEncryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64) {
|
||||||
rootEncryptionKey &&
|
|
||||||
secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64
|
|
||||||
) {
|
|
||||||
return client.decryptSymmetric(
|
return client.decryptSymmetric(
|
||||||
secretBlindIndexData.encryptedSaltCiphertext,
|
secretBlindIndexData.encryptedSaltCiphertext,
|
||||||
rootEncryptionKey,
|
rootEncryptionKey,
|
||||||
secretBlindIndexData.saltIV,
|
secretBlindIndexData.saltIV,
|
||||||
secretBlindIndexData.saltTag
|
secretBlindIndexData.saltTag
|
||||||
);
|
);
|
||||||
} else if (
|
} else if (encryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8) {
|
||||||
encryptionKey &&
|
|
||||||
secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8
|
|
||||||
) {
|
|
||||||
// decrypt workspace salt
|
// decrypt workspace salt
|
||||||
return decryptSymmetric128BitHexKeyUTF8({
|
return decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
|
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
|
||||||
iv: secretBlindIndexData.saltIV,
|
iv: secretBlindIndexData.saltIV,
|
||||||
tag: secretBlindIndexData.saltTag,
|
tag: secretBlindIndexData.saltTag,
|
||||||
key: encryptionKey,
|
key: encryptionKey
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
throw InternalServerError({
|
throw InternalServerError({
|
||||||
message: "Failed to obtain workspace salt needed for secret blind indexing",
|
message: "Failed to obtain workspace salt needed for secret blind indexing"
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -210,7 +211,7 @@ export const getSecretBlindIndexSaltHelper = async ({
|
|||||||
*/
|
*/
|
||||||
export const generateSecretBlindIndexWithSaltHelper = async ({
|
export const generateSecretBlindIndexWithSaltHelper = async ({
|
||||||
secretName,
|
secretName,
|
||||||
salt,
|
salt
|
||||||
}: {
|
}: {
|
||||||
secretName: string;
|
secretName: string;
|
||||||
salt: string;
|
salt: string;
|
||||||
@@ -224,7 +225,7 @@ export const generateSecretBlindIndexWithSaltHelper = async ({
|
|||||||
memoryCost: 65536, // default pool of 64 MiB per thread.
|
memoryCost: 65536, // default pool of 64 MiB per thread.
|
||||||
hashLength: 32,
|
hashLength: 32,
|
||||||
parallelism: 1,
|
parallelism: 1,
|
||||||
raw: true,
|
raw: true
|
||||||
})
|
})
|
||||||
).toString("base64");
|
).toString("base64");
|
||||||
|
|
||||||
@@ -240,7 +241,7 @@ export const generateSecretBlindIndexWithSaltHelper = async ({
|
|||||||
*/
|
*/
|
||||||
export const generateSecretBlindIndexHelper = async ({
|
export const generateSecretBlindIndexHelper = async ({
|
||||||
secretName,
|
secretName,
|
||||||
workspaceId,
|
workspaceId
|
||||||
}: {
|
}: {
|
||||||
secretName: string;
|
secretName: string;
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
@@ -250,16 +251,13 @@ export const generateSecretBlindIndexHelper = async ({
|
|||||||
const rootEncryptionKey = await getRootEncryptionKey();
|
const rootEncryptionKey = await getRootEncryptionKey();
|
||||||
|
|
||||||
const secretBlindIndexData = await SecretBlindIndexData.findOne({
|
const secretBlindIndexData = await SecretBlindIndexData.findOne({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId
|
||||||
}).select("+algorithm +keyEncoding");
|
}).select("+algorithm +keyEncoding");
|
||||||
|
|
||||||
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
|
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
|
||||||
|
|
||||||
let salt;
|
let salt;
|
||||||
if (
|
if (rootEncryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64) {
|
||||||
rootEncryptionKey &&
|
|
||||||
secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64
|
|
||||||
) {
|
|
||||||
salt = client.decryptSymmetric(
|
salt = client.decryptSymmetric(
|
||||||
secretBlindIndexData.encryptedSaltCiphertext,
|
secretBlindIndexData.encryptedSaltCiphertext,
|
||||||
rootEncryptionKey,
|
rootEncryptionKey,
|
||||||
@@ -269,32 +267,29 @@ export const generateSecretBlindIndexHelper = async ({
|
|||||||
|
|
||||||
const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({
|
const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({
|
||||||
secretName,
|
secretName,
|
||||||
salt,
|
salt
|
||||||
});
|
});
|
||||||
|
|
||||||
return secretBlindIndex;
|
return secretBlindIndex;
|
||||||
} else if (
|
} else if (encryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8) {
|
||||||
encryptionKey &&
|
|
||||||
secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8
|
|
||||||
) {
|
|
||||||
// decrypt workspace salt
|
// decrypt workspace salt
|
||||||
salt = decryptSymmetric128BitHexKeyUTF8({
|
salt = decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
|
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
|
||||||
iv: secretBlindIndexData.saltIV,
|
iv: secretBlindIndexData.saltIV,
|
||||||
tag: secretBlindIndexData.saltTag,
|
tag: secretBlindIndexData.saltTag,
|
||||||
key: encryptionKey,
|
key: encryptionKey
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({
|
const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({
|
||||||
secretName,
|
secretName,
|
||||||
salt,
|
salt
|
||||||
});
|
});
|
||||||
|
|
||||||
return secretBlindIndex;
|
return secretBlindIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw InternalServerError({
|
throw InternalServerError({
|
||||||
message: "Failed to generate secret blind index",
|
message: "Failed to generate secret blind index"
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -323,38 +318,32 @@ export const createSecretHelper = async ({
|
|||||||
secretCommentCiphertext,
|
secretCommentCiphertext,
|
||||||
secretCommentIV,
|
secretCommentIV,
|
||||||
secretCommentTag,
|
secretCommentTag,
|
||||||
secretPath = "/",
|
secretPath = "/"
|
||||||
}: CreateSecretParams) => {
|
}: CreateSecretParams) => {
|
||||||
|
|
||||||
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||||
secretName,
|
secretName,
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
});
|
});
|
||||||
|
|
||||||
// if using service token filter towards the folderId by secretpath
|
// if using service token filter towards the folderId by secretpath
|
||||||
if (authData.authPayload instanceof ServiceTokenData) {
|
if (authData.authPayload instanceof ServiceTokenData) {
|
||||||
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
|
if (!isValidScope(authData.authPayload, environment, secretPath)) {
|
||||||
if (secretPath !== serviceTkScopedSecretPath) {
|
|
||||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const folderId = await getFolderIdFromServiceToken(
|
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
secretPath
|
|
||||||
);
|
|
||||||
|
|
||||||
const exists = await Secret.exists({
|
const exists = await Secret.exists({
|
||||||
secretBlindIndex,
|
secretBlindIndex,
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
folder: folderId,
|
folder: folderId,
|
||||||
type,
|
type,
|
||||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
|
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exists)
|
if (exists)
|
||||||
throw BadRequestError({
|
throw BadRequestError({
|
||||||
message: "Failed to create secret that already exists",
|
message: "Failed to create secret that already exists"
|
||||||
});
|
});
|
||||||
|
|
||||||
if (type === SECRET_PERSONAL) {
|
if (type === SECRET_PERSONAL) {
|
||||||
@@ -365,13 +354,12 @@ export const createSecretHelper = async ({
|
|||||||
secretBlindIndex,
|
secretBlindIndex,
|
||||||
folder: folderId,
|
folder: folderId,
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
type: SECRET_SHARED,
|
type: SECRET_SHARED
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!exists)
|
if (!exists)
|
||||||
throw BadRequestError({
|
throw BadRequestError({
|
||||||
message:
|
message: "Failed to create personal secret override for no corresponding shared secret"
|
||||||
"Failed to create personal secret override for no corresponding shared secret",
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,7 +382,7 @@ export const createSecretHelper = async ({
|
|||||||
secretCommentTag,
|
secretCommentTag,
|
||||||
folder: folderId,
|
folder: folderId,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
const secretVersion = new SecretVersion({
|
const secretVersion = new SecretVersion({
|
||||||
@@ -414,12 +402,12 @@ export const createSecretHelper = async ({
|
|||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
});
|
});
|
||||||
|
|
||||||
// (EE) add version for new secret
|
// (EE) add version for new secret
|
||||||
await EESecretService.addSecretVersions({
|
await EESecretService.addSecretVersions({
|
||||||
secretVersions: [secretVersion],
|
secretVersions: [secretVersion]
|
||||||
});
|
});
|
||||||
|
|
||||||
// (EE) create (audit) log
|
// (EE) create (audit) log
|
||||||
@@ -427,7 +415,7 @@ export const createSecretHelper = async ({
|
|||||||
name: ACTION_ADD_SECRETS,
|
name: ACTION_ADD_SECRETS,
|
||||||
...getAuthDataPayloadIdObj(authData),
|
...getAuthDataPayloadIdObj(authData),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
secretIds: [secret._id],
|
secretIds: [secret._id]
|
||||||
});
|
});
|
||||||
|
|
||||||
action &&
|
action &&
|
||||||
@@ -436,14 +424,14 @@ export const createSecretHelper = async ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
actions: [action],
|
actions: [action],
|
||||||
channel: authData.authChannel,
|
channel: authData.authChannel,
|
||||||
ipAddress: authData.authIP,
|
ipAddress: authData.authIP
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
folderId,
|
folderId
|
||||||
});
|
});
|
||||||
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
@@ -452,7 +440,7 @@ export const createSecretHelper = async ({
|
|||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
event: "secrets added",
|
event: "secrets added",
|
||||||
distinctId: await TelemetryService.getDistinctId({
|
distinctId: await TelemetryService.getDistinctId({
|
||||||
authData,
|
authData
|
||||||
}),
|
}),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: 1,
|
numberOfSecrets: 1,
|
||||||
@@ -460,8 +448,8 @@ export const createSecretHelper = async ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
folderId,
|
folderId,
|
||||||
channel: authData.authChannel,
|
channel: authData.authChannel,
|
||||||
userAgent: authData.authUserAgent,
|
userAgent: authData.authUserAgent
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,21 +468,16 @@ export const getSecretsHelper = async ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
authData,
|
authData,
|
||||||
secretPath = "/",
|
secretPath = "/"
|
||||||
}: GetSecretsParams) => {
|
}: GetSecretsParams) => {
|
||||||
let secrets: ISecret[] = [];
|
let secrets: ISecret[] = [];
|
||||||
// if using service token filter towards the folderId by secretpath
|
// if using service token filter towards the folderId by secretpath
|
||||||
if (authData.authPayload instanceof ServiceTokenData) {
|
if (authData.authPayload instanceof ServiceTokenData) {
|
||||||
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
|
if (!isValidScope(authData.authPayload, environment, secretPath)) {
|
||||||
if (secretPath !== serviceTkScopedSecretPath) {
|
|
||||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const folderId = await getFolderIdFromServiceToken(
|
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
secretPath
|
|
||||||
);
|
|
||||||
|
|
||||||
// get personal secrets first
|
// get personal secrets first
|
||||||
secrets = await Secret.find({
|
secrets = await Secret.find({
|
||||||
@@ -502,8 +485,10 @@ export const getSecretsHelper = async ({
|
|||||||
environment,
|
environment,
|
||||||
folder: folderId,
|
folder: folderId,
|
||||||
type: SECRET_PERSONAL,
|
type: SECRET_PERSONAL,
|
||||||
...getAuthDataPayloadUserObj(authData),
|
...getAuthDataPayloadUserObj(authData)
|
||||||
}).populate("tags").lean();
|
})
|
||||||
|
.populate("tags")
|
||||||
|
.lean();
|
||||||
|
|
||||||
// concat with shared secrets
|
// concat with shared secrets
|
||||||
secrets = secrets.concat(
|
secrets = secrets.concat(
|
||||||
@@ -513,9 +498,11 @@ export const getSecretsHelper = async ({
|
|||||||
folder: folderId,
|
folder: folderId,
|
||||||
type: SECRET_SHARED,
|
type: SECRET_SHARED,
|
||||||
secretBlindIndex: {
|
secretBlindIndex: {
|
||||||
$nin: secrets.map((secret) => secret.secretBlindIndex),
|
$nin: secrets.map((secret) => secret.secretBlindIndex)
|
||||||
},
|
}
|
||||||
}).populate("tags").lean()
|
})
|
||||||
|
.populate("tags")
|
||||||
|
.lean()
|
||||||
);
|
);
|
||||||
|
|
||||||
// (EE) create (audit) log
|
// (EE) create (audit) log
|
||||||
@@ -523,7 +510,7 @@ export const getSecretsHelper = async ({
|
|||||||
name: ACTION_READ_SECRETS,
|
name: ACTION_READ_SECRETS,
|
||||||
...getAuthDataPayloadIdObj(authData),
|
...getAuthDataPayloadIdObj(authData),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
secretIds: secrets.map((secret) => secret._id),
|
secretIds: secrets.map((secret) => secret._id)
|
||||||
});
|
});
|
||||||
|
|
||||||
action &&
|
action &&
|
||||||
@@ -532,7 +519,7 @@ export const getSecretsHelper = async ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
actions: [action],
|
actions: [action],
|
||||||
channel: authData.authChannel,
|
channel: authData.authChannel,
|
||||||
ipAddress: authData.authIP,
|
ipAddress: authData.authIP
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
@@ -541,7 +528,7 @@ export const getSecretsHelper = async ({
|
|||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
event: "secrets pulled",
|
event: "secrets pulled",
|
||||||
distinctId: await TelemetryService.getDistinctId({
|
distinctId: await TelemetryService.getDistinctId({
|
||||||
authData,
|
authData
|
||||||
}),
|
}),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: secrets.length,
|
numberOfSecrets: secrets.length,
|
||||||
@@ -549,8 +536,8 @@ export const getSecretsHelper = async ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
folderId,
|
folderId,
|
||||||
channel: authData.authChannel,
|
channel: authData.authChannel,
|
||||||
userAgent: authData.authUserAgent,
|
userAgent: authData.authUserAgent
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -573,25 +560,20 @@ export const getSecretHelper = async ({
|
|||||||
environment,
|
environment,
|
||||||
type,
|
type,
|
||||||
authData,
|
authData,
|
||||||
secretPath = "/",
|
secretPath = "/"
|
||||||
}: GetSecretParams) => {
|
}: GetSecretParams) => {
|
||||||
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||||
secretName,
|
secretName,
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
});
|
});
|
||||||
let secret: ISecret | null = null;
|
let secret: ISecret | null = null;
|
||||||
// if using service token filter towards the folderId by secretpath
|
// if using service token filter towards the folderId by secretpath
|
||||||
if (authData.authPayload instanceof ServiceTokenData) {
|
if (authData.authPayload instanceof ServiceTokenData) {
|
||||||
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
|
if (!isValidScope(authData.authPayload, environment, secretPath)) {
|
||||||
if (secretPath !== serviceTkScopedSecretPath) {
|
|
||||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const folderId = await getFolderIdFromServiceToken(
|
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
secretPath
|
|
||||||
);
|
|
||||||
|
|
||||||
// try getting personal secret first (if exists)
|
// try getting personal secret first (if exists)
|
||||||
secret = await Secret.findOne({
|
secret = await Secret.findOne({
|
||||||
@@ -600,7 +582,7 @@ export const getSecretHelper = async ({
|
|||||||
environment,
|
environment,
|
||||||
folder: folderId,
|
folder: folderId,
|
||||||
type: type ?? SECRET_PERSONAL,
|
type: type ?? SECRET_PERSONAL,
|
||||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
|
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
|
||||||
}).lean();
|
}).lean();
|
||||||
|
|
||||||
if (!secret) {
|
if (!secret) {
|
||||||
@@ -611,7 +593,7 @@ export const getSecretHelper = async ({
|
|||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
environment,
|
environment,
|
||||||
folder: folderId,
|
folder: folderId,
|
||||||
type: SECRET_SHARED,
|
type: SECRET_SHARED
|
||||||
}).lean();
|
}).lean();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -622,7 +604,7 @@ export const getSecretHelper = async ({
|
|||||||
name: ACTION_READ_SECRETS,
|
name: ACTION_READ_SECRETS,
|
||||||
...getAuthDataPayloadIdObj(authData),
|
...getAuthDataPayloadIdObj(authData),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
secretIds: [secret._id],
|
secretIds: [secret._id]
|
||||||
});
|
});
|
||||||
|
|
||||||
action &&
|
action &&
|
||||||
@@ -631,7 +613,7 @@ export const getSecretHelper = async ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
actions: [action],
|
actions: [action],
|
||||||
channel: authData.authChannel,
|
channel: authData.authChannel,
|
||||||
ipAddress: authData.authIP,
|
ipAddress: authData.authIP
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
@@ -640,7 +622,7 @@ export const getSecretHelper = async ({
|
|||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
event: "secrets pull",
|
event: "secrets pull",
|
||||||
distinctId: await TelemetryService.getDistinctId({
|
distinctId: await TelemetryService.getDistinctId({
|
||||||
authData,
|
authData
|
||||||
}),
|
}),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: 1,
|
numberOfSecrets: 1,
|
||||||
@@ -648,8 +630,8 @@ export const getSecretHelper = async ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
folderId,
|
folderId,
|
||||||
channel: authData.authChannel,
|
channel: authData.authChannel,
|
||||||
userAgent: authData.authUserAgent,
|
userAgent: authData.authUserAgent
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -679,26 +661,21 @@ export const updateSecretHelper = async ({
|
|||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
secretPath,
|
secretPath
|
||||||
}: UpdateSecretParams) => {
|
}: UpdateSecretParams) => {
|
||||||
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||||
secretName,
|
secretName,
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
});
|
});
|
||||||
|
|
||||||
let secret: ISecret | null = null;
|
let secret: ISecret | null = null;
|
||||||
// if using service token filter towards the folderId by secretpath
|
// if using service token filter towards the folderId by secretpath
|
||||||
if (authData.authPayload instanceof ServiceTokenData) {
|
if (authData.authPayload instanceof ServiceTokenData) {
|
||||||
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
|
if (!isValidScope(authData.authPayload, environment, secretPath)) {
|
||||||
if (secretPath !== serviceTkScopedSecretPath) {
|
|
||||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const folderId = await getFolderIdFromServiceToken(
|
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
secretPath
|
|
||||||
);
|
|
||||||
|
|
||||||
if (type === SECRET_SHARED) {
|
if (type === SECRET_SHARED) {
|
||||||
// case: update shared secret
|
// case: update shared secret
|
||||||
@@ -708,16 +685,16 @@ export const updateSecretHelper = async ({
|
|||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
environment,
|
environment,
|
||||||
folder: folderId,
|
folder: folderId,
|
||||||
type,
|
type
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
$inc: { version: 1 },
|
$inc: { version: 1 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true,
|
new: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -730,16 +707,16 @@ export const updateSecretHelper = async ({
|
|||||||
environment,
|
environment,
|
||||||
type,
|
type,
|
||||||
folder: folderId,
|
folder: folderId,
|
||||||
...getAuthDataPayloadUserObj(authData),
|
...getAuthDataPayloadUserObj(authData)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
$inc: { version: 1 },
|
$inc: { version: 1 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true,
|
new: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -763,12 +740,12 @@ export const updateSecretHelper = async ({
|
|||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
});
|
});
|
||||||
|
|
||||||
// (EE) add version for new secret
|
// (EE) add version for new secret
|
||||||
await EESecretService.addSecretVersions({
|
await EESecretService.addSecretVersions({
|
||||||
secretVersions: [secretVersion],
|
secretVersions: [secretVersion]
|
||||||
});
|
});
|
||||||
|
|
||||||
// (EE) create (audit) log
|
// (EE) create (audit) log
|
||||||
@@ -776,7 +753,7 @@ export const updateSecretHelper = async ({
|
|||||||
name: ACTION_UPDATE_SECRETS,
|
name: ACTION_UPDATE_SECRETS,
|
||||||
...getAuthDataPayloadIdObj(authData),
|
...getAuthDataPayloadIdObj(authData),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
secretIds: [secret._id],
|
secretIds: [secret._id]
|
||||||
});
|
});
|
||||||
|
|
||||||
action &&
|
action &&
|
||||||
@@ -785,14 +762,14 @@ export const updateSecretHelper = async ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
actions: [action],
|
actions: [action],
|
||||||
channel: authData.authChannel,
|
channel: authData.authChannel,
|
||||||
ipAddress: authData.authIP,
|
ipAddress: authData.authIP
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
folderId: secret?.folder,
|
folderId: secret?.folder
|
||||||
});
|
});
|
||||||
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
@@ -801,7 +778,7 @@ export const updateSecretHelper = async ({
|
|||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
event: "secrets modified",
|
event: "secrets modified",
|
||||||
distinctId: await TelemetryService.getDistinctId({
|
distinctId: await TelemetryService.getDistinctId({
|
||||||
authData,
|
authData
|
||||||
}),
|
}),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: 1,
|
numberOfSecrets: 1,
|
||||||
@@ -809,8 +786,8 @@ export const updateSecretHelper = async ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
folderId,
|
folderId,
|
||||||
channel: authData.authChannel,
|
channel: authData.authChannel,
|
||||||
userAgent: authData.authUserAgent,
|
userAgent: authData.authUserAgent
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -833,26 +810,20 @@ export const deleteSecretHelper = async ({
|
|||||||
environment,
|
environment,
|
||||||
type,
|
type,
|
||||||
authData,
|
authData,
|
||||||
secretPath = "/",
|
secretPath = "/"
|
||||||
}: DeleteSecretParams) => {
|
}: DeleteSecretParams) => {
|
||||||
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||||
secretName,
|
secretName,
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
});
|
});
|
||||||
|
|
||||||
// if using service token filter towards the folderId by secretpath
|
// if using service token filter towards the folderId by secretpath
|
||||||
if (authData.authPayload instanceof ServiceTokenData) {
|
if (authData.authPayload instanceof ServiceTokenData) {
|
||||||
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
|
if (!isValidScope(authData.authPayload, environment, secretPath)) {
|
||||||
|
|
||||||
if (secretPath !== serviceTkScopedSecretPath) {
|
|
||||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const folderId = await getFolderIdFromServiceToken(
|
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
secretPath
|
|
||||||
);
|
|
||||||
|
|
||||||
let secrets: ISecret[] = [];
|
let secrets: ISecret[] = [];
|
||||||
let secret: ISecret | null = null;
|
let secret: ISecret | null = null;
|
||||||
@@ -862,7 +833,7 @@ export const deleteSecretHelper = async ({
|
|||||||
secretBlindIndex,
|
secretBlindIndex,
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
environment,
|
environment,
|
||||||
folder: folderId,
|
folder: folderId
|
||||||
}).lean();
|
}).lean();
|
||||||
|
|
||||||
secret = await Secret.findOneAndDelete({
|
secret = await Secret.findOneAndDelete({
|
||||||
@@ -870,14 +841,14 @@ export const deleteSecretHelper = async ({
|
|||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
environment,
|
environment,
|
||||||
type,
|
type,
|
||||||
folder: folderId,
|
folder: folderId
|
||||||
}).lean();
|
}).lean();
|
||||||
|
|
||||||
await Secret.deleteMany({
|
await Secret.deleteMany({
|
||||||
secretBlindIndex,
|
secretBlindIndex,
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
environment,
|
environment,
|
||||||
folder: folderId,
|
folder: folderId
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
secret = await Secret.findOneAndDelete({
|
secret = await Secret.findOneAndDelete({
|
||||||
@@ -886,7 +857,7 @@ export const deleteSecretHelper = async ({
|
|||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
environment,
|
environment,
|
||||||
type,
|
type,
|
||||||
...getAuthDataPayloadUserObj(authData),
|
...getAuthDataPayloadUserObj(authData)
|
||||||
}).lean();
|
}).lean();
|
||||||
|
|
||||||
if (secret) {
|
if (secret) {
|
||||||
@@ -897,7 +868,7 @@ export const deleteSecretHelper = async ({
|
|||||||
if (!secret) throw SecretNotFoundError();
|
if (!secret) throw SecretNotFoundError();
|
||||||
|
|
||||||
await EESecretService.markDeletedSecretVersions({
|
await EESecretService.markDeletedSecretVersions({
|
||||||
secretIds: secrets.map((secret) => secret._id),
|
secretIds: secrets.map((secret) => secret._id)
|
||||||
});
|
});
|
||||||
|
|
||||||
// (EE) create (audit) log
|
// (EE) create (audit) log
|
||||||
@@ -905,22 +876,23 @@ export const deleteSecretHelper = async ({
|
|||||||
name: ACTION_DELETE_SECRETS,
|
name: ACTION_DELETE_SECRETS,
|
||||||
...getAuthDataPayloadIdObj(authData),
|
...getAuthDataPayloadIdObj(authData),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
secretIds: secrets.map((secret) => secret._id),
|
secretIds: secrets.map((secret) => secret._id)
|
||||||
});
|
});
|
||||||
|
|
||||||
action && (await EELogService.createLog({
|
action &&
|
||||||
...getAuthDataPayloadIdObj(authData),
|
(await EELogService.createLog({
|
||||||
workspaceId,
|
...getAuthDataPayloadIdObj(authData),
|
||||||
actions: [action],
|
workspaceId,
|
||||||
channel: authData.authChannel,
|
actions: [action],
|
||||||
ipAddress: authData.authIP,
|
channel: authData.authChannel,
|
||||||
}));
|
ipAddress: authData.authIP
|
||||||
|
}));
|
||||||
|
|
||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
folderId: secret?.folder,
|
folderId: secret?.folder
|
||||||
});
|
});
|
||||||
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
@@ -929,7 +901,7 @@ export const deleteSecretHelper = async ({
|
|||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
event: "secrets deleted",
|
event: "secrets deleted",
|
||||||
distinctId: await TelemetryService.getDistinctId({
|
distinctId: await TelemetryService.getDistinctId({
|
||||||
authData,
|
authData
|
||||||
}),
|
}),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: secrets.length,
|
numberOfSecrets: secrets.length,
|
||||||
@@ -937,13 +909,13 @@ export const deleteSecretHelper = async ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
folderId,
|
folderId,
|
||||||
channel: authData.authChannel,
|
channel: authData.authChannel,
|
||||||
userAgent: authData.authUserAgent,
|
userAgent: authData.authUserAgent
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return ({
|
return {
|
||||||
secrets,
|
secrets,
|
||||||
secret,
|
secret
|
||||||
});
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ export interface IServiceTokenData extends Document {
|
|||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
name: string;
|
name: string;
|
||||||
workspace: Types.ObjectId;
|
workspace: Types.ObjectId;
|
||||||
environment: string;
|
scopes: Array<{
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
}>;
|
||||||
user: Types.ObjectId;
|
user: Types.ObjectId;
|
||||||
serviceAccount: Types.ObjectId;
|
serviceAccount: Types.ObjectId;
|
||||||
lastUsed: Date;
|
lastUsed: Date;
|
||||||
@@ -13,7 +16,6 @@ export interface IServiceTokenData extends Document {
|
|||||||
encryptedKey: string;
|
encryptedKey: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
secretPath: string;
|
|
||||||
permissions: string[];
|
permissions: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,68 +23,72 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
|
|||||||
{
|
{
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true
|
||||||
},
|
},
|
||||||
workspace: {
|
workspace: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: "Workspace",
|
ref: "Workspace",
|
||||||
required: true,
|
required: true
|
||||||
},
|
},
|
||||||
environment: {
|
scopes: {
|
||||||
type: String,
|
type: [
|
||||||
required: true,
|
{
|
||||||
|
environment: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
secretPath: {
|
||||||
|
type: String,
|
||||||
|
default: "/",
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
required: true
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: "User",
|
ref: "User",
|
||||||
required: true,
|
required: true
|
||||||
},
|
},
|
||||||
serviceAccount: {
|
serviceAccount: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: "ServiceAccount",
|
ref: "ServiceAccount"
|
||||||
},
|
},
|
||||||
lastUsed: {
|
lastUsed: {
|
||||||
type: Date,
|
type: Date
|
||||||
},
|
},
|
||||||
expiresAt: {
|
expiresAt: {
|
||||||
type: Date,
|
type: Date
|
||||||
},
|
},
|
||||||
secretHash: {
|
secretHash: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
select: false,
|
select: false
|
||||||
},
|
},
|
||||||
encryptedKey: {
|
encryptedKey: {
|
||||||
type: String,
|
type: String,
|
||||||
select: false,
|
select: false
|
||||||
},
|
},
|
||||||
iv: {
|
iv: {
|
||||||
type: String,
|
type: String,
|
||||||
select: false,
|
select: false
|
||||||
},
|
},
|
||||||
tag: {
|
tag: {
|
||||||
type: String,
|
type: String,
|
||||||
select: false,
|
select: false
|
||||||
},
|
},
|
||||||
permissions: {
|
permissions: {
|
||||||
type: [String],
|
type: [String],
|
||||||
enum: ["read", "write"],
|
enum: ["read", "write"],
|
||||||
default: ["read"],
|
default: ["read"]
|
||||||
},
|
}
|
||||||
secretPath: {
|
|
||||||
type: String,
|
|
||||||
default: "/",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true,
|
timestamps: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const ServiceTokenData = model<IServiceTokenData>(
|
const ServiceTokenData = model<IServiceTokenData>("ServiceTokenData", serviceTokenDataSchema);
|
||||||
"ServiceTokenData",
|
|
||||||
serviceTokenDataSchema
|
|
||||||
);
|
|
||||||
|
|
||||||
export default ServiceTokenData;
|
export default ServiceTokenData;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
requireAuth,
|
requireAuth,
|
||||||
requireServiceTokenDataAuth,
|
requireServiceTokenDataAuth,
|
||||||
requireWorkspaceAuth,
|
requireWorkspaceAuth,
|
||||||
validateRequest,
|
validateRequest
|
||||||
} from "../../middleware";
|
} from "../../middleware";
|
||||||
import { body, param } from "express-validator";
|
import { body, param } from "express-validator";
|
||||||
import {
|
import {
|
||||||
@@ -13,14 +13,14 @@ import {
|
|||||||
AUTH_MODE_SERVICE_ACCOUNT,
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
AUTH_MODE_SERVICE_TOKEN,
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
MEMBER,
|
MEMBER,
|
||||||
PERMISSION_WRITE_SECRETS,
|
PERMISSION_WRITE_SECRETS
|
||||||
} from "../../variables";
|
} from "../../variables";
|
||||||
import { serviceTokenDataController } from "../../controllers/v2";
|
import { serviceTokenDataController } from "../../controllers/v2";
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/",
|
"/",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AUTH_MODE_SERVICE_TOKEN],
|
acceptedAuthModes: [AUTH_MODE_SERVICE_TOKEN]
|
||||||
}),
|
}),
|
||||||
serviceTokenDataController.getServiceTokenData
|
serviceTokenDataController.getServiceTokenData
|
||||||
);
|
);
|
||||||
@@ -28,33 +28,30 @@ router.get(
|
|||||||
router.post(
|
router.post(
|
||||||
"/",
|
"/",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT],
|
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT]
|
||||||
}),
|
}),
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
locationWorkspaceId: "body",
|
locationWorkspaceId: "body",
|
||||||
locationEnvironment: "body",
|
locationEnvironment: "body",
|
||||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
requiredPermissions: [PERMISSION_WRITE_SECRETS]
|
||||||
}),
|
}),
|
||||||
body("name").exists().isString().trim(),
|
body("name").exists().isString().trim(),
|
||||||
body("workspaceId").exists().isString().trim(),
|
body("workspaceId").exists().isString().trim(),
|
||||||
body("environment").exists().isString().trim(),
|
body("scopes").exists().isArray(),
|
||||||
|
body("scopes.*.environment").exists().isString().trim(),
|
||||||
|
body("scopes.*.secretPath").exists().isString().trim(),
|
||||||
body("encryptedKey").exists().isString().trim(),
|
body("encryptedKey").exists().isString().trim(),
|
||||||
body("iv").exists().isString().trim(),
|
body("iv").exists().isString().trim(),
|
||||||
body("secretPath").isString().default("/").trim(),
|
|
||||||
body("tag").exists().isString().trim(),
|
body("tag").exists().isString().trim(),
|
||||||
body("expiresIn").exists().isNumeric(), // measured in ms
|
body("expiresIn").exists().isNumeric(), // measured in ms
|
||||||
body("permissions")
|
body("permissions")
|
||||||
.isArray({ min: 1 })
|
.isArray({ min: 1 })
|
||||||
.custom((value: string[]) => {
|
.custom((value: string[]) => {
|
||||||
const allowedPermissions = ["read", "write"];
|
const allowedPermissions = ["read", "write"];
|
||||||
const invalidValues = value.filter(
|
const invalidValues = value.filter((v) => !allowedPermissions.includes(v));
|
||||||
(v) => !allowedPermissions.includes(v)
|
|
||||||
);
|
|
||||||
if (invalidValues.length > 0) {
|
if (invalidValues.length > 0) {
|
||||||
throw new Error(
|
throw new Error(`permissions contains invalid values: ${invalidValues.join(", ")}`);
|
||||||
`permissions contains invalid values: ${invalidValues.join(", ")}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -66,10 +63,10 @@ router.post(
|
|||||||
router.delete(
|
router.delete(
|
||||||
"/:serviceTokenDataId",
|
"/:serviceTokenDataId",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||||
}),
|
}),
|
||||||
requireServiceTokenDataAuth({
|
requireServiceTokenDataAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
}),
|
}),
|
||||||
param("serviceTokenDataId").exists().trim(),
|
param("serviceTokenDataId").exists().trim(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ import {
|
|||||||
Secret,
|
Secret,
|
||||||
SecretBlindIndexData,
|
SecretBlindIndexData,
|
||||||
ServiceTokenData,
|
ServiceTokenData,
|
||||||
Workspace,
|
Workspace
|
||||||
} from "../../models";
|
} from "../../models";
|
||||||
import { generateKeyPair } from "../../utils/crypto";
|
import { generateKeyPair } from "../../utils/crypto";
|
||||||
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config";
|
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config";
|
||||||
import {
|
import {
|
||||||
ALGORITHM_AES_256_GCM,
|
ALGORITHM_AES_256_GCM,
|
||||||
ENCODING_SCHEME_BASE64,
|
ENCODING_SCHEME_BASE64,
|
||||||
ENCODING_SCHEME_UTF8,
|
ENCODING_SCHEME_UTF8
|
||||||
} from "../../variables";
|
} from "../../variables";
|
||||||
import { InternalServerError } from "../errors";
|
import { InternalServerError } from "../errors";
|
||||||
|
|
||||||
@@ -29,10 +29,7 @@ import { InternalServerError } from "../errors";
|
|||||||
* corresponding secret versions
|
* corresponding secret versions
|
||||||
*/
|
*/
|
||||||
export const backfillSecretVersions = async () => {
|
export const backfillSecretVersions = async () => {
|
||||||
await Secret.updateMany(
|
await Secret.updateMany({ version: { $exists: false } }, { $set: { version: 1 } });
|
||||||
{ version: { $exists: false } },
|
|
||||||
{ $set: { version: 1 } }
|
|
||||||
);
|
|
||||||
|
|
||||||
const unversionedSecrets: ISecret[] = await Secret.aggregate([
|
const unversionedSecrets: ISecret[] = await Secret.aggregate([
|
||||||
{
|
{
|
||||||
@@ -40,14 +37,14 @@ export const backfillSecretVersions = async () => {
|
|||||||
from: "secretversions",
|
from: "secretversions",
|
||||||
localField: "_id",
|
localField: "_id",
|
||||||
foreignField: "secret",
|
foreignField: "secret",
|
||||||
as: "versions",
|
as: "versions"
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$match: {
|
$match: {
|
||||||
versions: { $size: 0 },
|
versions: { $size: 0 }
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (unversionedSecrets.length > 0) {
|
if (unversionedSecrets.length > 0) {
|
||||||
@@ -62,9 +59,9 @@ export const backfillSecretVersions = async () => {
|
|||||||
workspace: s.workspace,
|
workspace: s.workspace,
|
||||||
environment: s.environment,
|
environment: s.environment,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
})
|
})
|
||||||
),
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log("Migration: Secret version migration v1 complete");
|
console.log("Migration: Secret version migration v1 complete");
|
||||||
@@ -80,8 +77,8 @@ export const backfillBots = async () => {
|
|||||||
const workspaceIdsWithBot = await Bot.distinct("workspace");
|
const workspaceIdsWithBot = await Bot.distinct("workspace");
|
||||||
const workspaceIdsToAddBot = await Workspace.distinct("_id", {
|
const workspaceIdsToAddBot = await Workspace.distinct("_id", {
|
||||||
_id: {
|
_id: {
|
||||||
$nin: workspaceIdsWithBot,
|
$nin: workspaceIdsWithBot
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (workspaceIdsToAddBot.length === 0) return;
|
if (workspaceIdsToAddBot.length === 0) return;
|
||||||
@@ -94,7 +91,7 @@ export const backfillBots = async () => {
|
|||||||
const {
|
const {
|
||||||
ciphertext: encryptedPrivateKey,
|
ciphertext: encryptedPrivateKey,
|
||||||
iv,
|
iv,
|
||||||
tag,
|
tag
|
||||||
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
|
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
|
||||||
|
|
||||||
return new Bot({
|
return new Bot({
|
||||||
@@ -106,16 +103,16 @@ export const backfillBots = async () => {
|
|||||||
iv,
|
iv,
|
||||||
tag,
|
tag,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_BASE64,
|
keyEncoding: ENCODING_SCHEME_BASE64
|
||||||
});
|
});
|
||||||
} else if (encryptionKey) {
|
} else if (encryptionKey) {
|
||||||
const {
|
const {
|
||||||
ciphertext: encryptedPrivateKey,
|
ciphertext: encryptedPrivateKey,
|
||||||
iv,
|
iv,
|
||||||
tag,
|
tag
|
||||||
} = encryptSymmetric128BitHexKeyUTF8({
|
} = encryptSymmetric128BitHexKeyUTF8({
|
||||||
plaintext: privateKey,
|
plaintext: privateKey,
|
||||||
key: encryptionKey,
|
key: encryptionKey
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Bot({
|
return new Bot({
|
||||||
@@ -127,13 +124,12 @@ export const backfillBots = async () => {
|
|||||||
iv,
|
iv,
|
||||||
tag,
|
tag,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
throw InternalServerError({
|
throw InternalServerError({
|
||||||
message:
|
message: "Failed to backfill workspace bots due to missing encryption key"
|
||||||
"Failed to backfill workspace bots due to missing encryption key",
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -149,13 +145,11 @@ export const backfillSecretBlindIndexData = async () => {
|
|||||||
const encryptionKey = await getEncryptionKey();
|
const encryptionKey = await getEncryptionKey();
|
||||||
const rootEncryptionKey = await getRootEncryptionKey();
|
const rootEncryptionKey = await getRootEncryptionKey();
|
||||||
|
|
||||||
const workspaceIdsBlindIndexed = await SecretBlindIndexData.distinct(
|
const workspaceIdsBlindIndexed = await SecretBlindIndexData.distinct("workspace");
|
||||||
"workspace"
|
|
||||||
);
|
|
||||||
const workspaceIdsToBlindIndex = await Workspace.distinct("_id", {
|
const workspaceIdsToBlindIndex = await Workspace.distinct("_id", {
|
||||||
_id: {
|
_id: {
|
||||||
$nin: workspaceIdsBlindIndexed,
|
$nin: workspaceIdsBlindIndexed
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (workspaceIdsToBlindIndex.length === 0) return;
|
if (workspaceIdsToBlindIndex.length === 0) return;
|
||||||
@@ -168,7 +162,7 @@ export const backfillSecretBlindIndexData = async () => {
|
|||||||
const {
|
const {
|
||||||
ciphertext: encryptedSaltCiphertext,
|
ciphertext: encryptedSaltCiphertext,
|
||||||
iv: saltIV,
|
iv: saltIV,
|
||||||
tag: saltTag,
|
tag: saltTag
|
||||||
} = client.encryptSymmetric(salt, rootEncryptionKey);
|
} = client.encryptSymmetric(salt, rootEncryptionKey);
|
||||||
|
|
||||||
return new SecretBlindIndexData({
|
return new SecretBlindIndexData({
|
||||||
@@ -177,16 +171,16 @@ export const backfillSecretBlindIndexData = async () => {
|
|||||||
saltIV,
|
saltIV,
|
||||||
saltTag,
|
saltTag,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_BASE64,
|
keyEncoding: ENCODING_SCHEME_BASE64
|
||||||
});
|
});
|
||||||
} else if (encryptionKey) {
|
} else if (encryptionKey) {
|
||||||
const {
|
const {
|
||||||
ciphertext: encryptedSaltCiphertext,
|
ciphertext: encryptedSaltCiphertext,
|
||||||
iv: saltIV,
|
iv: saltIV,
|
||||||
tag: saltTag,
|
tag: saltTag
|
||||||
} = encryptSymmetric128BitHexKeyUTF8({
|
} = encryptSymmetric128BitHexKeyUTF8({
|
||||||
plaintext: salt,
|
plaintext: salt,
|
||||||
key: encryptionKey,
|
key: encryptionKey
|
||||||
});
|
});
|
||||||
|
|
||||||
return new SecretBlindIndexData({
|
return new SecretBlindIndexData({
|
||||||
@@ -195,13 +189,12 @@ export const backfillSecretBlindIndexData = async () => {
|
|||||||
saltIV,
|
saltIV,
|
||||||
saltTag,
|
saltTag,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
throw InternalServerError({
|
throw InternalServerError({
|
||||||
message:
|
message: "Failed to backfill secret blind index data due to missing encryption key"
|
||||||
"Failed to backfill secret blind index data due to missing encryption key",
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -219,17 +212,17 @@ export const backfillEncryptionMetadata = async () => {
|
|||||||
await Secret.updateMany(
|
await Secret.updateMany(
|
||||||
{
|
{
|
||||||
algorithm: {
|
algorithm: {
|
||||||
$exists: false,
|
$exists: false
|
||||||
},
|
},
|
||||||
keyEncoding: {
|
keyEncoding: {
|
||||||
$exists: false,
|
$exists: false
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -237,17 +230,17 @@ export const backfillEncryptionMetadata = async () => {
|
|||||||
await SecretVersion.updateMany(
|
await SecretVersion.updateMany(
|
||||||
{
|
{
|
||||||
algorithm: {
|
algorithm: {
|
||||||
$exists: false,
|
$exists: false
|
||||||
},
|
},
|
||||||
keyEncoding: {
|
keyEncoding: {
|
||||||
$exists: false,
|
$exists: false
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -255,17 +248,17 @@ export const backfillEncryptionMetadata = async () => {
|
|||||||
await SecretBlindIndexData.updateMany(
|
await SecretBlindIndexData.updateMany(
|
||||||
{
|
{
|
||||||
algorithm: {
|
algorithm: {
|
||||||
$exists: false,
|
$exists: false
|
||||||
},
|
},
|
||||||
keyEncoding: {
|
keyEncoding: {
|
||||||
$exists: false,
|
$exists: false
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -273,17 +266,17 @@ export const backfillEncryptionMetadata = async () => {
|
|||||||
await Bot.updateMany(
|
await Bot.updateMany(
|
||||||
{
|
{
|
||||||
algorithm: {
|
algorithm: {
|
||||||
$exists: false,
|
$exists: false
|
||||||
},
|
},
|
||||||
keyEncoding: {
|
keyEncoding: {
|
||||||
$exists: false,
|
$exists: false
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -291,17 +284,17 @@ export const backfillEncryptionMetadata = async () => {
|
|||||||
await BackupPrivateKey.updateMany(
|
await BackupPrivateKey.updateMany(
|
||||||
{
|
{
|
||||||
algorithm: {
|
algorithm: {
|
||||||
$exists: false,
|
$exists: false
|
||||||
},
|
},
|
||||||
keyEncoding: {
|
keyEncoding: {
|
||||||
$exists: false,
|
$exists: false
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -309,17 +302,17 @@ export const backfillEncryptionMetadata = async () => {
|
|||||||
await IntegrationAuth.updateMany(
|
await IntegrationAuth.updateMany(
|
||||||
{
|
{
|
||||||
algorithm: {
|
algorithm: {
|
||||||
$exists: false,
|
$exists: false
|
||||||
},
|
},
|
||||||
keyEncoding: {
|
keyEncoding: {
|
||||||
$exists: false,
|
$exists: false
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -328,26 +321,26 @@ export const backfillSecretFolders = async () => {
|
|||||||
await Secret.updateMany(
|
await Secret.updateMany(
|
||||||
{
|
{
|
||||||
folder: {
|
folder: {
|
||||||
$exists: false,
|
$exists: false
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
folder: "root",
|
folder: "root"
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
await SecretVersion.updateMany(
|
await SecretVersion.updateMany(
|
||||||
{
|
{
|
||||||
folder: {
|
folder: {
|
||||||
$exists: false,
|
$exists: false
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
folder: "root",
|
folder: "root"
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -355,20 +348,20 @@ export const backfillSecretFolders = async () => {
|
|||||||
await SecretVersion.updateMany(
|
await SecretVersion.updateMany(
|
||||||
{
|
{
|
||||||
tags: {
|
tags: {
|
||||||
$exists: false,
|
$exists: false
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
tags: [],
|
tags: []
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
let secretSnapshots = await SecretSnapshot.find({
|
let secretSnapshots = await SecretSnapshot.find({
|
||||||
environment: {
|
environment: {
|
||||||
$exists: false,
|
$exists: false
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
.populate<{ secretVersions: ISecretVersion[] }>("secretVersions")
|
.populate<{ secretVersions: ISecretVersion[] }>("secretVersions")
|
||||||
.limit(50);
|
.limit(50);
|
||||||
@@ -377,8 +370,7 @@ export const backfillSecretFolders = async () => {
|
|||||||
for (const secSnapshot of secretSnapshots) {
|
for (const secSnapshot of secretSnapshots) {
|
||||||
const groupSnapByEnv: Record<string, Array<ISecretVersion>> = {};
|
const groupSnapByEnv: Record<string, Array<ISecretVersion>> = {};
|
||||||
secSnapshot.secretVersions.forEach((secVer) => {
|
secSnapshot.secretVersions.forEach((secVer) => {
|
||||||
if (!groupSnapByEnv?.[secVer.environment])
|
if (!groupSnapByEnv?.[secVer.environment]) groupSnapByEnv[secVer.environment] = [];
|
||||||
groupSnapByEnv[secVer.environment] = [];
|
|
||||||
groupSnapByEnv[secVer.environment].push(secVer);
|
groupSnapByEnv[secVer.environment].push(secVer);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -390,7 +382,7 @@ export const backfillSecretFolders = async () => {
|
|||||||
...secSnapshot.toObject({ virtuals: false }),
|
...secSnapshot.toObject({ virtuals: false }),
|
||||||
_id: new Types.ObjectId(),
|
_id: new Types.ObjectId(),
|
||||||
environment: snapEnv,
|
environment: snapEnv,
|
||||||
secretVersions: secretIdsOfEnvGroup,
|
secretVersions: secretIdsOfEnvGroup
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -400,8 +392,8 @@ export const backfillSecretFolders = async () => {
|
|||||||
|
|
||||||
secretSnapshots = await SecretSnapshot.find({
|
secretSnapshots = await SecretSnapshot.find({
|
||||||
environment: {
|
environment: {
|
||||||
$exists: false,
|
$exists: false
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
.populate<{ secretVersions: ISecretVersion[] }>("secretVersions")
|
.populate<{ secretVersions: ISecretVersion[] }>("secretVersions")
|
||||||
.limit(50);
|
.limit(50);
|
||||||
@@ -414,13 +406,13 @@ export const backfillServiceToken = async () => {
|
|||||||
await ServiceTokenData.updateMany(
|
await ServiceTokenData.updateMany(
|
||||||
{
|
{
|
||||||
secretPath: {
|
secretPath: {
|
||||||
$exists: false,
|
$exists: false
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
secretPath: "/",
|
secretPath: "/"
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
console.log("Migration: Service token migration v1 complete");
|
console.log("Migration: Service token migration v1 complete");
|
||||||
@@ -430,14 +422,33 @@ export const backfillIntegration = async () => {
|
|||||||
await Integration.updateMany(
|
await Integration.updateMany(
|
||||||
{
|
{
|
||||||
secretPath: {
|
secretPath: {
|
||||||
$exists: false,
|
$exists: false
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
secretPath: "/",
|
secretPath: "/"
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
console.log("Migration: Integration migration v1 complete");
|
console.log("Migration: Integration migration v1 complete");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const backfillServiceTokenMultiScope = async () => {
|
||||||
|
await ServiceTokenData.updateMany(
|
||||||
|
{
|
||||||
|
scopes: {
|
||||||
|
$exists: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
scopes: [{ environment: "$environment", secretPath: "$secretPath" }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("Migration: Service token migration v2 complete");
|
||||||
|
};
|
||||||
|
|||||||
@@ -14,17 +14,15 @@ import {
|
|||||||
backfillSecretFolders,
|
backfillSecretFolders,
|
||||||
backfillSecretVersions,
|
backfillSecretVersions,
|
||||||
backfillServiceToken,
|
backfillServiceToken,
|
||||||
|
backfillServiceTokenMultiScope
|
||||||
} from "./backfillData";
|
} from "./backfillData";
|
||||||
import {
|
import { reencryptBotPrivateKeys, reencryptSecretBlindIndexDataSalts } from "./reencryptData";
|
||||||
reencryptBotPrivateKeys,
|
|
||||||
reencryptSecretBlindIndexDataSalts,
|
|
||||||
} from "./reencryptData";
|
|
||||||
import {
|
import {
|
||||||
getClientIdGoogle,
|
getClientIdGoogle,
|
||||||
getClientSecretGoogle,
|
getClientSecretGoogle,
|
||||||
getMongoURL,
|
getMongoURL,
|
||||||
getNodeEnv,
|
getNodeEnv,
|
||||||
getSentryDSN,
|
getSentryDSN
|
||||||
} from "../../config";
|
} from "../../config";
|
||||||
import { initializePassport } from "../auth";
|
import { initializePassport } from "../auth";
|
||||||
|
|
||||||
@@ -79,6 +77,7 @@ export const setup = async () => {
|
|||||||
await backfillSecretFolders();
|
await backfillSecretFolders();
|
||||||
await backfillServiceToken();
|
await backfillServiceToken();
|
||||||
await backfillIntegration();
|
await backfillIntegration();
|
||||||
|
await backfillServiceTokenMultiScope();
|
||||||
|
|
||||||
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
|
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
|
||||||
// to base64 256-bit ROOT_ENCRYPTION_KEY
|
// to base64 256-bit ROOT_ENCRYPTION_KEY
|
||||||
@@ -90,7 +89,7 @@ export const setup = async () => {
|
|||||||
dsn: await getSentryDSN(),
|
dsn: await getSentryDSN(),
|
||||||
tracesSampleRate: 1.0,
|
tracesSampleRate: 1.0,
|
||||||
debug: (await getNodeEnv()) === "production" ? false : true,
|
debug: (await getNodeEnv()) === "production" ? false : true,
|
||||||
environment: await getNodeEnv(),
|
environment: await getNodeEnv()
|
||||||
});
|
});
|
||||||
|
|
||||||
await createTestUserForDevelopment();
|
await createTestUserForDevelopment();
|
||||||
|
|||||||
@@ -1,22 +1,19 @@
|
|||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import {
|
import {
|
||||||
ISecret,
|
ISecret,
|
||||||
IServiceAccount,
|
IServiceAccount,
|
||||||
IServiceTokenData,
|
IServiceTokenData,
|
||||||
IUser,
|
IUser,
|
||||||
ServiceAccount,
|
ServiceAccount,
|
||||||
ServiceTokenData,
|
ServiceTokenData,
|
||||||
User,
|
User
|
||||||
} from "../models";
|
} from "../models";
|
||||||
|
import { ServiceTokenDataNotFoundError, UnauthorizedRequestError } from "../utils/errors";
|
||||||
import {
|
import {
|
||||||
ServiceTokenDataNotFoundError,
|
AUTH_MODE_API_KEY,
|
||||||
UnauthorizedRequestError,
|
AUTH_MODE_JWT,
|
||||||
} from "../utils/errors";
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
import {
|
AUTH_MODE_SERVICE_TOKEN
|
||||||
AUTH_MODE_API_KEY,
|
|
||||||
AUTH_MODE_JWT,
|
|
||||||
AUTH_MODE_SERVICE_ACCOUNT,
|
|
||||||
AUTH_MODE_SERVICE_TOKEN,
|
|
||||||
} from "../variables";
|
} from "../variables";
|
||||||
import { validateUserClientForWorkspace } from "./user";
|
import { validateUserClientForWorkspace } from "./user";
|
||||||
import { validateServiceAccountClientForWorkspace } from "./serviceAccount";
|
import { validateServiceAccountClientForWorkspace } from "./serviceAccount";
|
||||||
@@ -30,65 +27,71 @@ import { validateServiceAccountClientForWorkspace } from "./serviceAccount";
|
|||||||
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
|
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
|
||||||
*/
|
*/
|
||||||
export const validateClientForServiceTokenData = async ({
|
export const validateClientForServiceTokenData = async ({
|
||||||
authData,
|
authData,
|
||||||
serviceTokenDataId,
|
serviceTokenDataId,
|
||||||
acceptedRoles,
|
acceptedRoles
|
||||||
}: {
|
}: {
|
||||||
authData: {
|
authData: {
|
||||||
authMode: string;
|
authMode: string;
|
||||||
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||||
};
|
};
|
||||||
serviceTokenDataId: Types.ObjectId;
|
serviceTokenDataId: Types.ObjectId;
|
||||||
acceptedRoles: Array<"admin" | "member">;
|
acceptedRoles: Array<"admin" | "member">;
|
||||||
}) => {
|
}) => {
|
||||||
const serviceTokenData = await ServiceTokenData
|
const serviceTokenData = await ServiceTokenData.findById(serviceTokenDataId)
|
||||||
.findById(serviceTokenDataId)
|
.select("+encryptedKey +iv +tag")
|
||||||
.select("+encryptedKey +iv +tag")
|
.populate<{ user: IUser }>("user");
|
||||||
.populate<{ user: IUser }>("user");
|
|
||||||
|
|
||||||
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({
|
if (!serviceTokenData)
|
||||||
message: "Failed to find service token data",
|
throw ServiceTokenDataNotFoundError({
|
||||||
|
message: "Failed to find service token data"
|
||||||
});
|
});
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||||
await validateUserClientForWorkspace({
|
await validateUserClientForWorkspace({
|
||||||
user: authData.authPayload,
|
user: authData.authPayload,
|
||||||
workspaceId: serviceTokenData.workspace,
|
workspaceId: serviceTokenData.workspace,
|
||||||
acceptedRoles,
|
acceptedRoles
|
||||||
});
|
});
|
||||||
|
|
||||||
return serviceTokenData;
|
return serviceTokenData;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
if (
|
||||||
await validateServiceAccountClientForWorkspace({
|
authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
|
||||||
serviceAccount: authData.authPayload,
|
authData.authPayload instanceof ServiceAccount
|
||||||
workspaceId: serviceTokenData.workspace,
|
) {
|
||||||
});
|
await validateServiceAccountClientForWorkspace({
|
||||||
|
serviceAccount: authData.authPayload,
|
||||||
|
workspaceId: serviceTokenData.workspace
|
||||||
|
});
|
||||||
|
|
||||||
return serviceTokenData;
|
return serviceTokenData;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: "Failed service token authorization for service token data",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
|
||||||
await validateUserClientForWorkspace({
|
|
||||||
user: authData.authPayload,
|
|
||||||
workspaceId: serviceTokenData.workspace,
|
|
||||||
acceptedRoles,
|
|
||||||
});
|
|
||||||
|
|
||||||
return serviceTokenData;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
authData.authMode === AUTH_MODE_SERVICE_TOKEN &&
|
||||||
|
authData.authPayload instanceof ServiceTokenData
|
||||||
|
) {
|
||||||
throw UnauthorizedRequestError({
|
throw UnauthorizedRequestError({
|
||||||
message: "Failed client authorization for service token data",
|
message: "Failed service token authorization for service token data"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
||||||
|
await validateUserClientForWorkspace({
|
||||||
|
user: authData.authPayload,
|
||||||
|
workspaceId: serviceTokenData.workspace,
|
||||||
|
acceptedRoles
|
||||||
|
});
|
||||||
|
|
||||||
|
return serviceTokenData;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: "Failed client authorization for service token data"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate that service token (client) can access workspace
|
* Validate that service token (client) can access workspace
|
||||||
@@ -101,42 +104,42 @@ export const validateClientForServiceTokenData = async ({
|
|||||||
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
|
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
|
||||||
*/
|
*/
|
||||||
export const validateServiceTokenDataClientForWorkspace = async ({
|
export const validateServiceTokenDataClientForWorkspace = async ({
|
||||||
serviceTokenData,
|
serviceTokenData,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
requiredPermissions,
|
requiredPermissions
|
||||||
}: {
|
}: {
|
||||||
serviceTokenData: IServiceTokenData;
|
serviceTokenData: IServiceTokenData;
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
environment?: string;
|
environment?: string;
|
||||||
requiredPermissions?: string[];
|
requiredPermissions?: string[];
|
||||||
}) => {
|
}) => {
|
||||||
if (!serviceTokenData.workspace.equals(workspaceId)) {
|
if (!serviceTokenData.workspace.equals(workspaceId)) {
|
||||||
// case: invalid workspaceId passed
|
// case: invalid workspaceId passed
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: "Failed service token authorization for the given workspace"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (environment) {
|
||||||
|
// case: environment is specified
|
||||||
|
|
||||||
|
if (!serviceTokenData.scopes.find(({ environment: tkEnv }) => tkEnv === environment)) {
|
||||||
|
// case: invalid environment passed
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: "Failed service token authorization for the given workspace environment"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
requiredPermissions?.forEach((permission) => {
|
||||||
|
if (!serviceTokenData.permissions.includes(permission)) {
|
||||||
throw UnauthorizedRequestError({
|
throw UnauthorizedRequestError({
|
||||||
message: "Failed service token authorization for the given workspace",
|
message: `Failed service token authorization for the given workspace environment action: ${permission}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
if (environment) {
|
}
|
||||||
// case: environment is specified
|
};
|
||||||
|
|
||||||
if (serviceTokenData.environment !== environment) {
|
|
||||||
// case: invalid environment passed
|
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: "Failed service token authorization for the given workspace environment",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
requiredPermissions?.forEach((permission) => {
|
|
||||||
if (!serviceTokenData.permissions.includes(permission)) {
|
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: `Failed service token authorization for the given workspace environment action: ${permission}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate that service token (client) can access secrets
|
* Validate that service token (client) can access secrets
|
||||||
@@ -147,36 +150,35 @@ export const validateServiceTokenDataClientForWorkspace = async ({
|
|||||||
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
|
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
|
||||||
*/
|
*/
|
||||||
export const validateServiceTokenDataClientForSecrets = async ({
|
export const validateServiceTokenDataClientForSecrets = async ({
|
||||||
serviceTokenData,
|
serviceTokenData,
|
||||||
secrets,
|
secrets,
|
||||||
requiredPermissions,
|
requiredPermissions
|
||||||
}: {
|
}: {
|
||||||
serviceTokenData: IServiceTokenData;
|
serviceTokenData: IServiceTokenData;
|
||||||
secrets: ISecret[];
|
secrets: ISecret[];
|
||||||
requiredPermissions?: string[];
|
requiredPermissions?: string[];
|
||||||
}) => {
|
}) => {
|
||||||
|
secrets.forEach((secret: ISecret) => {
|
||||||
|
if (!serviceTokenData.workspace.equals(secret.workspace)) {
|
||||||
|
// case: invalid workspaceId passed
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: "Failed service token authorization for the given workspace"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
secrets.forEach((secret: ISecret) => {
|
if (!serviceTokenData.scopes.find(({ environment: tkEnv }) => tkEnv === secret.environment)) {
|
||||||
if (!serviceTokenData.workspace.equals(secret.workspace)) {
|
// case: invalid environment passed
|
||||||
// case: invalid workspaceId passed
|
throw UnauthorizedRequestError({
|
||||||
throw UnauthorizedRequestError({
|
message: "Failed service token authorization for the given workspace environment"
|
||||||
message: "Failed service token authorization for the given workspace",
|
});
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (serviceTokenData.environment !== secret.environment) {
|
requiredPermissions?.forEach((permission) => {
|
||||||
// case: invalid environment passed
|
if (!serviceTokenData.permissions.includes(permission)) {
|
||||||
throw UnauthorizedRequestError({
|
throw UnauthorizedRequestError({
|
||||||
message: "Failed service token authorization for the given workspace environment",
|
message: `Failed service token authorization for the given workspace environment action: ${permission}`
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
requiredPermissions?.forEach((permission) => {
|
|
||||||
if (!serviceTokenData.permissions.includes(permission)) {
|
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: `Failed service token authorization for the given workspace environment action: ${permission}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -181,14 +181,16 @@ type GetServiceTokenDetailsResponse struct {
|
|||||||
ID string `json:"_id"`
|
ID string `json:"_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Workspace string `json:"workspace"`
|
Workspace string `json:"workspace"`
|
||||||
Environment string `json:"environment"`
|
|
||||||
ExpiresAt time.Time `json:"expiresAt"`
|
ExpiresAt time.Time `json:"expiresAt"`
|
||||||
EncryptedKey string `json:"encryptedKey"`
|
EncryptedKey string `json:"encryptedKey"`
|
||||||
Iv string `json:"iv"`
|
Iv string `json:"iv"`
|
||||||
Tag string `json:"tag"`
|
Tag string `json:"tag"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
SecretPath string `json:"secretPath"`
|
Scopes []struct {
|
||||||
|
Environment string `json:"environment"`
|
||||||
|
SecretPath string `json:"secretPath"`
|
||||||
|
} `json:"scopes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetAccessibleEnvironmentsRequest struct {
|
type GetAccessibleEnvironmentsRequest struct {
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ var exportCmd = &cobra.Command{
|
|||||||
|
|
||||||
var output string
|
var output string
|
||||||
if shouldExpandSecrets {
|
if shouldExpandSecrets {
|
||||||
substitutions := util.SubstituteSecrets(secrets)
|
substitutions := util.ExpandSecrets(secrets, infisicalToken)
|
||||||
output, err = formatEnvs(substitutions, format)
|
output, err = formatEnvs(substitutions, format)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err)
|
util.HandleError(err)
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ var runCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if shouldExpandSecrets {
|
if shouldExpandSecrets {
|
||||||
secrets = util.SubstituteSecrets(secrets)
|
secrets = util.ExpandSecrets(secrets, infisicalToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
secretsByKey := getSecretsByKeys(secrets)
|
secretsByKey := getSecretsByKeys(secrets)
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ var secretsCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if shouldExpandSecrets {
|
if shouldExpandSecrets {
|
||||||
secrets = util.SubstituteSecrets(secrets)
|
secrets = util.ExpandSecrets(secrets, infisicalToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
visualize.PrintAllSecretDetails(secrets)
|
visualize.PrintAllSecretDetails(secrets)
|
||||||
|
|||||||
@@ -45,5 +45,5 @@ func PrintErrorMessageAndExit(messages ...string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func printError(e error) {
|
func printError(e error) {
|
||||||
color.New(color.FgRed).Fprintf(os.Stderr, "Hmm, we ran into an error: %v", e)
|
color.New(color.FgRed).Fprintf(os.Stderr, "Hmm, we ran into an error: %v\n", e)
|
||||||
}
|
}
|
||||||
|
|||||||
+108
-21
@@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ import (
|
|||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.SingleEnvironmentVariable, api.GetServiceTokenDetailsResponse, error) {
|
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string) ([]models.SingleEnvironmentVariable, api.GetServiceTokenDetailsResponse, error) {
|
||||||
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
|
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
|
||||||
if len(serviceTokenParts) < 4 {
|
if len(serviceTokenParts) < 4 {
|
||||||
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
|
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
|
||||||
@@ -34,10 +35,19 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl
|
|||||||
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to get service token details. [err=%v]", err)
|
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to get service token details. [err=%v]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if multiple scopes are there then user needs to specify which environment and secret path
|
||||||
|
if environment == "" {
|
||||||
|
if len(serviceTokenDetails.Scopes) != 1 {
|
||||||
|
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("you need to provide the --env for multiple environment scoped token")
|
||||||
|
} else {
|
||||||
|
environment = serviceTokenDetails.Scopes[0].Environment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
|
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
|
||||||
WorkspaceId: serviceTokenDetails.Workspace,
|
WorkspaceId: serviceTokenDetails.Workspace,
|
||||||
Environment: serviceTokenDetails.Environment,
|
Environment: environment,
|
||||||
SecretPath: serviceTokenDetails.SecretPath,
|
SecretPath: secretPath,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -189,11 +199,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
log.Debug().Msg("Trying to fetch secrets using service token")
|
log.Debug().Msg("Trying to fetch secrets using service token")
|
||||||
secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(infisicalToken)
|
secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(infisicalToken, params.Environment, params.SecretsPath)
|
||||||
|
|
||||||
// if serviceTokenDetails.Environment != params.Environment {
|
|
||||||
// PrintErrorMessageAndExit(fmt.Sprintf("Fetch secrets failed: token allows [%s] environment access, not [%s]. Service tokens are environment-specific; no need for --env flag.", params.Environment, serviceTokenDetails.Environment))
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return secretsToReturn, errorToReturn
|
return secretsToReturn, errorToReturn
|
||||||
@@ -279,22 +285,103 @@ func getExpandedEnvVariable(secrets []models.SingleEnvironmentVariable, variable
|
|||||||
return "${" + variableWeAreLookingFor + "}"
|
return "${" + variableWeAreLookingFor + "}"
|
||||||
}
|
}
|
||||||
|
|
||||||
func SubstituteSecrets(secrets []models.SingleEnvironmentVariable) []models.SingleEnvironmentVariable {
|
var secRefRegex = regexp.MustCompile(`\${([^\}]*)}`)
|
||||||
hashMapOfCompleteVariables := make(map[string]string)
|
|
||||||
hashMapOfSelfRefs := make(map[string]string)
|
|
||||||
expandedSecrets := []models.SingleEnvironmentVariable{}
|
|
||||||
|
|
||||||
for _, secret := range secrets {
|
|
||||||
expandedVariable := getExpandedEnvVariable(secrets, secret.Key, hashMapOfCompleteVariables, hashMapOfSelfRefs)
|
|
||||||
expandedSecrets = append(expandedSecrets, models.SingleEnvironmentVariable{
|
|
||||||
Key: secret.Key,
|
|
||||||
Value: expandedVariable,
|
|
||||||
Type: secret.Type,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
func recursivelyExpandSecret(expandedSecs map[string]string, interpolatedSecs map[string]string, crossSecRefFetch func(env string, path []string, key string) string, key string) string {
|
||||||
|
if v, ok := expandedSecs[key]; ok {
|
||||||
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
return expandedSecrets
|
interpolatedVal, ok := interpolatedSecs[key]
|
||||||
|
if !ok {
|
||||||
|
HandleError(fmt.Errorf("Could not find refered secret - %s", key), "Kindly check whether its provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
refs := secRefRegex.FindAllStringSubmatch(interpolatedVal, -1)
|
||||||
|
for _, val := range refs {
|
||||||
|
// key: "${something}" val: [${something},something]
|
||||||
|
interpolatedExp, interpolationKey := val[0], val[1]
|
||||||
|
ref := strings.Split(interpolationKey, ".")
|
||||||
|
|
||||||
|
// ${KEY1} => [key1]
|
||||||
|
if len(ref) == 1 {
|
||||||
|
val := recursivelyExpandSecret(expandedSecs, interpolatedSecs, crossSecRefFetch, interpolationKey)
|
||||||
|
interpolatedVal = strings.ReplaceAll(interpolatedVal, interpolatedExp, val)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// cross board reference ${env.folder.key1} => [env folder key1]
|
||||||
|
if len(ref) > 1 {
|
||||||
|
secEnv, tmpSecPath, secKey := ref[0], ref[1:len(ref)-1], ref[len(ref)-1]
|
||||||
|
interpolatedSecs[interpolationKey] = crossSecRefFetch(secEnv, tmpSecPath, secKey) // get the reference value
|
||||||
|
val := recursivelyExpandSecret(expandedSecs, interpolatedSecs, crossSecRefFetch, interpolationKey)
|
||||||
|
interpolatedVal = strings.ReplaceAll(interpolatedVal, interpolatedExp, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
expandedSecs[key] = interpolatedVal
|
||||||
|
return interpolatedVal
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]models.SingleEnvironmentVariable {
|
||||||
|
secretMapByName := make(map[string]models.SingleEnvironmentVariable, len(secrets))
|
||||||
|
|
||||||
|
for _, secret := range secrets {
|
||||||
|
secretMapByName[secret.Key] = secret
|
||||||
|
}
|
||||||
|
|
||||||
|
return secretMapByName
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExpandSecrets(secrets []models.SingleEnvironmentVariable, infisicalToken string) []models.SingleEnvironmentVariable {
|
||||||
|
expandedSecs := make(map[string]string)
|
||||||
|
interpolatedSecs := make(map[string]string)
|
||||||
|
// map[env.secret-path][keyname]Secret
|
||||||
|
crossEnvRefSecs := make(map[string]map[string]models.SingleEnvironmentVariable) // a cache to hold all cross board reference secrets
|
||||||
|
|
||||||
|
for _, sec := range secrets {
|
||||||
|
// get all references in a secret
|
||||||
|
refs := secRefRegex.FindAllStringSubmatch(sec.Value, -1)
|
||||||
|
// nil means its a secret without reference
|
||||||
|
if refs == nil {
|
||||||
|
expandedSecs[sec.Key] = sec.Value // atomic secrets without any interpolation
|
||||||
|
} else {
|
||||||
|
interpolatedSecs[sec.Key] = sec.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, sec := range secrets {
|
||||||
|
// already present pick that up
|
||||||
|
if expandedVal, ok := expandedSecs[sec.Key]; ok {
|
||||||
|
secrets[i].Value = expandedVal
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
expandedVal := recursivelyExpandSecret(expandedSecs, interpolatedSecs, func(env string, secPaths []string, secKey string) string {
|
||||||
|
secPaths = append([]string{"/"}, secPaths...)
|
||||||
|
secPath := path.Join(secPaths...)
|
||||||
|
|
||||||
|
secPathDot := strings.Join(secPaths, ".")
|
||||||
|
uniqKey := fmt.Sprintf("%s.%s", env, secPathDot)
|
||||||
|
|
||||||
|
if crossRefSec, ok := crossEnvRefSecs[uniqKey]; !ok {
|
||||||
|
// if not in cross reference cache, fetch it from server
|
||||||
|
refSecs, err := GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: env, InfisicalToken: infisicalToken, SecretsPath: secPath})
|
||||||
|
if err != nil {
|
||||||
|
HandleError(err, fmt.Sprintf("Could not fetch secrets in environment: %s secret-path: %s", env, secPath), "If you are using a service token to fetch secrets, please ensure it is valid")
|
||||||
|
}
|
||||||
|
refSecsByKey := getSecretsByKeys(refSecs)
|
||||||
|
// save it to avoid calling api again for same environment and folder path
|
||||||
|
crossEnvRefSecs[uniqKey] = refSecsByKey
|
||||||
|
return refSecsByKey[secKey].Value
|
||||||
|
} else {
|
||||||
|
return crossRefSec[secKey].Value
|
||||||
|
}
|
||||||
|
}, sec.Key)
|
||||||
|
|
||||||
|
secrets[i].Value = expandedVal
|
||||||
|
}
|
||||||
|
return secrets
|
||||||
}
|
}
|
||||||
|
|
||||||
func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType string) []models.SingleEnvironmentVariable {
|
func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType string) []models.SingleEnvironmentVariable {
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ For more information on integrations, [refer infisical integration](/integration
|
|||||||
|
|
||||||
You can scope the secrets that can be read and written using an Infisical token by providing the secret path option when creating the token.
|
You can scope the secrets that can be read and written using an Infisical token by providing the secret path option when creating the token.
|
||||||
|
|
||||||
|
You can provide the folder path as glob if you want to have access to multiple folders and the tokens do support multi-environment.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
For more information, [refer infisical token section.](./token)
|
For more information, [refer infisical token section.](./token)
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
title: "Reference Secrets"
|
||||||
|
description: "How to use reference secrets in Infisical"
|
||||||
|
---
|
||||||
|
|
||||||
|
You can use the interpolation syntax to reference a secret in the same environment, another folder, or another environment
|
||||||
|
The interpolation syntax is a way of referencing a secret by using a special placeholder. The placeholder is the name of the secret, followed by the environment or folder name, separated by a colon.
|
||||||
|
|
||||||
|
For example, to reference a secret named mysecret in the same environment, you would use the placeholder `${mysecret}`.
|
||||||
|
|
||||||
|
While for another environment like `test` would be `${test.mysecret}`
|
||||||
|
|
||||||
|
Some more examples of referencing are
|
||||||
|
|
||||||
|
| Syntax | Environment | Folder | Secret Key |
|
||||||
|
| --------------------- | ----------- | ------------ | ---------- |
|
||||||
|
| `${KEY1}` | same env | ssame folder | KEY1 |
|
||||||
|
| `${dev.KEY2}` | dev | / | KEY2 |
|
||||||
|
| `${test.frontend.KEY2}` | test | /frontend | KEY2 |
|
||||||
|
|
||||||
|
# Permission system for reference
|
||||||
|
|
||||||
|
When you use the infisical CLI to log in, the permission system will work the same way as your user permissions.
|
||||||
|
This means that if you have permission to access other environments, your references to those environments will be resolved.
|
||||||
|
|
||||||
|
When using the Infisical CLI with a service token, the service token must have permissions to the referenced environment and folder path.
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 309 KiB After Width: | Height: | Size: 70 KiB |
@@ -39,9 +39,8 @@ The operator can be install via [Helm](helm.sh) or [kubectl](https://github.com/
|
|||||||
|
|
||||||
## Sync Infisical Secrets to your cluster
|
## Sync Infisical Secrets to your cluster
|
||||||
To retrieve secrets from an Infisical project and save them as native Kubernetes secrets within a specific namespace, utilize the `InfisicalSecret` custom resource definition (CRD).
|
To retrieve secrets from an Infisical project and save them as native Kubernetes secrets within a specific namespace, utilize the `InfisicalSecret` custom resource definition (CRD).
|
||||||
This resource can be created after installing the Infisical operator. For each new managed secret, you will need to create a new InfisicalSecret CRD.
|
|
||||||
|
|
||||||
```yaml
|
```yaml example-infisical-secret-crd.yaml
|
||||||
apiVersion: secrets.infisical.com/v1alpha1
|
apiVersion: secrets.infisical.com/v1alpha1
|
||||||
kind: InfisicalSecret
|
kind: InfisicalSecret
|
||||||
metadata:
|
metadata:
|
||||||
@@ -50,15 +49,18 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
# The host that should be used to pull secrets from. If left empty, the value specified in Global configuration will be used
|
# The host that should be used to pull secrets from. If left empty, the value specified in Global configuration will be used
|
||||||
hostAPI: https://app.infisical.com/api
|
hostAPI: https://app.infisical.com/api
|
||||||
resyncInterval: 60 # <-- the time in seconds between secret re-sync. Faster re-syncs will require higher rate limits
|
resyncInterval:
|
||||||
authentication:
|
authentication:
|
||||||
serviceToken:
|
serviceToken:
|
||||||
serviceTokenSecretReference:
|
serviceTokenSecretReference:
|
||||||
secretName: service-token
|
secretName: service-token
|
||||||
secretNamespace: option
|
secretNamespace: option
|
||||||
|
secretsScope:
|
||||||
|
envSlug: dev
|
||||||
|
secretsPath: "/"
|
||||||
managedSecretReference:
|
managedSecretReference:
|
||||||
secretName: managed-secret # <-- the name of kubernetes secret that will be created
|
secretName: managed-secret # <-- the name of kubernetes secret that will be created
|
||||||
secretNamespace: default # <-- where the kubernetes secret that will be created
|
secretNamespace: default # <-- where the kubernetes secret should be created
|
||||||
```
|
```
|
||||||
### InfisicalSecret CRD properties
|
### InfisicalSecret CRD properties
|
||||||
|
|
||||||
@@ -86,45 +88,59 @@ Default re-sync interval is every 1 minute.
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="authentication">
|
<Accordion title="authentication">
|
||||||
The `authentication` property tells the operator where it should look to find credentials needed to fetch secrets from Infisical.
|
This block defines the method that will be used to authenticate with Infisical so that secrets can be fetched. Currently, only [Service Tokens](../../documentation/platform/token) can be used to authenticate with Infisical.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
<Tabs>
|
<Accordion title="authentication.serviceToken.serviceTokenSecretReference">
|
||||||
<Tab title="Service Token">
|
The service token required to authenticate with Infisical needs to be stored in a Kubernetes secret. This block defines the reference to the name and name space of secret that stores this service token.
|
||||||
Authenticating with service tokens is a great option when you have a small number of services you'd like to fetch secrets for and are looking for the least amount of setup.
|
Follow the instructions below to create and store the service token in a Kubernetes secrets and reference it in your CRD.
|
||||||
|
|
||||||
#### 1. Generate service token
|
#### 1. Generate service token
|
||||||
|
|
||||||
You can generate a [service token](../../documentation/platform/token) for an Infisical project by heading over to the Infisical dashboard then to Project Settings.
|
You can generate a [service token](../../documentation/platform/token) for an Infisical project by heading over to the Infisical dashboard then to Project Settings.
|
||||||
|
|
||||||
#### 2. Create Kubernetes secret containing service token
|
#### 2. Create Kubernetes secret containing service token
|
||||||
|
|
||||||
Once you have generated the service token, you will need to create a Kubernetes secret containing the service token you generated.
|
Once you have generated the service token, you will need to create a Kubernetes secret containing the service token you generated.
|
||||||
To quickly create a Kubernetes secret containing the generated service token, you can run the command below.
|
To quickly create a Kubernetes secret containing the generated service token, you can run the command below. Make sure you replace `<your-service-token-here>` with your service token.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
kubectl create secret generic service-token --from-literal=infisicalToken=<your-service-token-here>
|
kubectl create secret generic service-token --from-literal=infisicalToken=<your-service-token-here>
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 3. Add reference for the Kubernetes secret containing service token
|
#### 3. Add reference for the Kubernetes secret containing service token
|
||||||
|
|
||||||
Once the secret is created, add the name and namespace of the secret that was just created under `authentication.serviceToken.serviceTokenSecretReference` field in the InfisicalSecret resource.
|
Once the secret is created, add the name and namespace of the secret that was just created under `authentication.serviceToken.serviceTokenSecretReference` field in the InfisicalSecret resource.
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
```yaml
|
```yaml
|
||||||
apiVersion: secrets.infisical.com/v1alpha1
|
apiVersion: secrets.infisical.com/v1alpha1
|
||||||
kind: InfisicalSecret
|
kind: InfisicalSecret
|
||||||
metadata:
|
metadata:
|
||||||
name: infisicalsecret-sample-crd
|
name: infisicalsecret-sample-crd
|
||||||
spec:
|
spec:
|
||||||
authentication:
|
authentication:
|
||||||
serviceToken:
|
serviceToken:
|
||||||
serviceTokenSecretReference:
|
serviceTokenSecretReference:
|
||||||
secretName: service-token # <-- name of the Kubernetes secret that stores our service token
|
secretName: service-token # <-- name of the Kubernetes secret that stores our service token
|
||||||
secretNamespace: option # <-- namespace of the Kubernetes secret that stores our service token
|
secretNamespace: option # <-- namespace of the Kubernetes secret that stores our service token
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
</Tab>
|
</Accordion>
|
||||||
</Tabs>
|
|
||||||
|
<Accordion title="authentication.serviceToken.secretsScope">
|
||||||
|
This block defines the scope of what secrets should be fetched. This is needed as your service token can have access to multiple folders and environments.
|
||||||
|
A scope is defined by `envSlug` and `secretsPath`.
|
||||||
|
|
||||||
|
#### envSlug
|
||||||
|
|
||||||
|
This refers to the short hand name of an environment. For example for the `development` environment the environment slug is `dev`. You can locate the slug of your environment by heading to your project settings in the Infisical dashboard.
|
||||||
|
|
||||||
|
#### secretsPath
|
||||||
|
|
||||||
|
secretsPath is the path to the secret in the given environment. For example a path of `/` would refer to the root of the environment whereas `/folder1` would refer to the secrets in folder1 from the root.
|
||||||
|
|
||||||
|
Both fields are required.
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="managedSecretReference">
|
<Accordion title="managedSecretReference">
|
||||||
|
|||||||
@@ -110,6 +110,7 @@
|
|||||||
"documentation/platform/organization",
|
"documentation/platform/organization",
|
||||||
"documentation/platform/project",
|
"documentation/platform/project",
|
||||||
"documentation/platform/folder",
|
"documentation/platform/folder",
|
||||||
|
"documentation/platform/secret-reference",
|
||||||
"documentation/platform/pit-recovery",
|
"documentation/platform/pit-recovery",
|
||||||
"documentation/platform/secret-versioning",
|
"documentation/platform/secret-versioning",
|
||||||
"documentation/platform/audit-logs",
|
"documentation/platform/audit-logs",
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
|
export type ServiceTokenScope = {
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ServiceToken = {
|
export type ServiceToken = {
|
||||||
_id: string;
|
_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
workspace: string;
|
workspace: string;
|
||||||
environment: string;
|
scopes: ServiceTokenScope[];
|
||||||
secretPath: string;
|
|
||||||
user: string;
|
user: string;
|
||||||
expiresAt: string;
|
expiresAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -14,9 +18,8 @@ export type ServiceToken = {
|
|||||||
export type CreateServiceTokenDTO = {
|
export type CreateServiceTokenDTO = {
|
||||||
name: string;
|
name: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
environment: string;
|
scopes: ServiceTokenScope[];
|
||||||
expiresIn: number;
|
expiresIn: number;
|
||||||
secretPath: string;
|
|
||||||
encryptedKey: string;
|
encryptedKey: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
|
|||||||
+305
-276
@@ -1,9 +1,9 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
import { faCheck, faCopy, faPlus, faTrashCan } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
@@ -27,10 +27,7 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
import {
|
import { useCreateServiceToken, useGetUserWsKey } from "@app/hooks/api";
|
||||||
useCreateServiceToken,
|
|
||||||
useGetUserWsKey
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const apiTokenExpiry = [
|
const apiTokenExpiry = [
|
||||||
@@ -44,8 +41,23 @@ const apiTokenExpiry = [
|
|||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
name: yup.string().max(100).required().label("Service Token Name"),
|
name: yup.string().max(100).required().label("Service Token Name"),
|
||||||
environment: yup.string().max(50).required().label("Environment"),
|
scopes: yup
|
||||||
secretPath: yup.string().required().default("/").label("Secret Path"),
|
.array(
|
||||||
|
yup.object({
|
||||||
|
environment: yup.string().max(50).required().label("Environment"),
|
||||||
|
secretPath: yup
|
||||||
|
.string()
|
||||||
|
.required()
|
||||||
|
.default("/")
|
||||||
|
.label("Secret Path")
|
||||||
|
.transform((val) =>
|
||||||
|
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.min(1)
|
||||||
|
.required()
|
||||||
|
.label("Scope"),
|
||||||
expiresIn: yup.string().optional().label("Service Token Expiration"),
|
expiresIn: yup.string().optional().label("Service Token Expiration"),
|
||||||
permissions: yup
|
permissions: yup
|
||||||
.object()
|
.object()
|
||||||
@@ -60,284 +72,301 @@ const schema = yup.object({
|
|||||||
export type FormData = yup.InferType<typeof schema>;
|
export type FormData = yup.InferType<typeof schema>;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
popUp: UsePopUpState<["createAPIToken"]>;
|
popUp: UsePopUpState<["createAPIToken"]>;
|
||||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["createAPIToken"]>, state?: boolean) => void;
|
handlePopUpToggle: (popUpName: keyof UsePopUpState<["createAPIToken"]>, state?: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AddServiceTokenModal = ({
|
export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||||
popUp,
|
const { t } = useTranslation();
|
||||||
handlePopUpToggle
|
const { createNotification } = useNotificationContext();
|
||||||
}: Props) => {
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { t } = useTranslation();
|
const {
|
||||||
const { createNotification } = useNotificationContext();
|
control,
|
||||||
const { currentWorkspace } = useWorkspace();
|
reset,
|
||||||
const {
|
handleSubmit,
|
||||||
control,
|
formState: { isSubmitting }
|
||||||
reset,
|
} = useForm<FormData>({
|
||||||
handleSubmit,
|
resolver: yupResolver(schema),
|
||||||
formState: { isSubmitting }
|
defaultValues: {
|
||||||
} = useForm<FormData>({
|
scopes: [{ secretPath: "/", environment: currentWorkspace?.environments?.[0]?.slug }]
|
||||||
resolver: yupResolver(schema)
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const [newToken, setToken] = useState("");
|
const { fields: tokenScopes, append, remove } = useFieldArray({ control, name: "scopes" });
|
||||||
const [isTokenCopied, setIsTokenCopied] = useToggle(false);
|
|
||||||
|
|
||||||
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
|
const [newToken, setToken] = useState("");
|
||||||
const createServiceToken = useCreateServiceToken();
|
const [isTokenCopied, setIsTokenCopied] = useToggle(false);
|
||||||
const hasServiceToken = Boolean(newToken);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
|
||||||
let timer: NodeJS.Timeout;
|
const createServiceToken = useCreateServiceToken();
|
||||||
if (isTokenCopied) {
|
const hasServiceToken = Boolean(newToken);
|
||||||
timer = setTimeout(() => setIsTokenCopied.off(), 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
useEffect(() => {
|
||||||
}, [isTokenCopied]);
|
let timer: NodeJS.Timeout;
|
||||||
|
if (isTokenCopied) {
|
||||||
|
timer = setTimeout(() => setIsTokenCopied.off(), 2000);
|
||||||
|
}
|
||||||
|
|
||||||
const copyTokenToClipboard = () => {
|
return () => clearTimeout(timer);
|
||||||
navigator.clipboard.writeText(newToken);
|
}, [isTokenCopied]);
|
||||||
setIsTokenCopied.on();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onFormSubmit = async ({
|
const copyTokenToClipboard = () => {
|
||||||
|
navigator.clipboard.writeText(newToken);
|
||||||
|
setIsTokenCopied.on();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ name, scopes, expiresIn, permissions }: FormData) => {
|
||||||
|
try {
|
||||||
|
if (!currentWorkspace?._id) return;
|
||||||
|
if (!latestFileKey) return;
|
||||||
|
|
||||||
|
const key = decryptAssymmetric({
|
||||||
|
ciphertext: latestFileKey.encryptedKey,
|
||||||
|
nonce: latestFileKey.nonce,
|
||||||
|
publicKey: latestFileKey.sender.publicKey,
|
||||||
|
privateKey: localStorage.getItem("PRIVATE_KEY") as string
|
||||||
|
});
|
||||||
|
|
||||||
|
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||||
|
|
||||||
|
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||||
|
plaintext: key,
|
||||||
|
key: randomBytes
|
||||||
|
});
|
||||||
|
|
||||||
|
const { serviceToken } = await createServiceToken.mutateAsync({
|
||||||
|
encryptedKey: ciphertext,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
scopes,
|
||||||
|
expiresIn: Number(expiresIn),
|
||||||
name,
|
name,
|
||||||
environment,
|
workspaceId: currentWorkspace._id,
|
||||||
secretPath,
|
randomBytes,
|
||||||
expiresIn,
|
permissions: Object.entries(permissions)
|
||||||
permissions
|
.filter(([, permissionsValue]) => permissionsValue)
|
||||||
}: FormData) => {
|
.map(([permissionsKey]) => permissionsKey)
|
||||||
try {
|
});
|
||||||
if (!currentWorkspace?._id) return;
|
|
||||||
if (!latestFileKey) return;
|
|
||||||
|
|
||||||
const key = decryptAssymmetric({
|
setToken(serviceToken);
|
||||||
ciphertext: latestFileKey.encryptedKey,
|
createNotification({
|
||||||
nonce: latestFileKey.nonce,
|
text: "Successfully created a service token",
|
||||||
publicKey: latestFileKey.sender.publicKey,
|
type: "success"
|
||||||
privateKey: localStorage.getItem("PRIVATE_KEY") as string
|
});
|
||||||
});
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to create a service token",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
return (
|
||||||
|
<Modal
|
||||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
isOpen={popUp?.createAPIToken?.isOpen}
|
||||||
plaintext: key,
|
onOpenChange={(open) => {
|
||||||
key: randomBytes
|
handlePopUpToggle("createAPIToken", open);
|
||||||
});
|
reset();
|
||||||
|
setToken("");
|
||||||
const { serviceToken } = await createServiceToken.mutateAsync({
|
}}
|
||||||
encryptedKey: ciphertext,
|
>
|
||||||
iv,
|
<ModalContent
|
||||||
tag,
|
title={
|
||||||
environment,
|
t("section.token.add-dialog.title", {
|
||||||
secretPath,
|
target: currentWorkspace?.name
|
||||||
expiresIn: Number(expiresIn),
|
}) as string
|
||||||
name,
|
|
||||||
workspaceId: currentWorkspace._id,
|
|
||||||
randomBytes,
|
|
||||||
permissions: Object.entries(permissions)
|
|
||||||
.filter(([, permissionsValue]) => permissionsValue)
|
|
||||||
.map(([permissionsKey]) => permissionsKey)
|
|
||||||
});
|
|
||||||
|
|
||||||
setToken(serviceToken);
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text: "Successfully created a service token",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to create a service token",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
subTitle={t("section.token.add-dialog.description") as string}
|
||||||
|
>
|
||||||
return (
|
{!hasServiceToken ? (
|
||||||
<Modal
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
isOpen={popUp?.createAPIToken?.isOpen}
|
<Controller
|
||||||
onOpenChange={(open) => {
|
control={control}
|
||||||
handlePopUpToggle("createAPIToken", open);
|
name="name"
|
||||||
reset();
|
defaultValue=""
|
||||||
setToken("");
|
render={({ field, fieldState: { error } }) => (
|
||||||
}}
|
<FormControl
|
||||||
>
|
label={t("section.token.add-dialog.name")}
|
||||||
<ModalContent
|
isError={Boolean(error)}
|
||||||
title={
|
errorText={error?.message}
|
||||||
t("section.token.add-dialog.title", {
|
>
|
||||||
target: currentWorkspace?.name
|
<Input {...field} placeholder="Type your token name" />
|
||||||
}) as string
|
</FormControl>
|
||||||
}
|
|
||||||
subTitle={t("section.token.add-dialog.description") as string}
|
|
||||||
>
|
|
||||||
{!hasServiceToken ? (
|
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="name"
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label={t("section.token.add-dialog.name")}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="Type your token name" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="environment"
|
|
||||||
defaultValue={currentWorkspace?.environments?.[0]?.slug}
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Environment"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{currentWorkspace?.environments.map(({ name, slug }) => (
|
|
||||||
<SelectItem value={slug} key={slug}>
|
|
||||||
{name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="secretPath"
|
|
||||||
defaultValue="/"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Secrets Path"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
helperText="Tokens can be scoped to a folder path. Default path is /"
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="Provide a path, default is /" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="expiresIn"
|
|
||||||
defaultValue={String(apiTokenExpiry?.[0]?.value)}
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Expiration"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{apiTokenExpiry.map(({ label, value }) => (
|
|
||||||
<SelectItem value={String(value || "")} key={label}>
|
|
||||||
{label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="permissions"
|
|
||||||
defaultValue={{
|
|
||||||
read: true,
|
|
||||||
write: false
|
|
||||||
}}
|
|
||||||
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
|
||||||
const options = [
|
|
||||||
{
|
|
||||||
label: "Read (default)",
|
|
||||||
value: "read"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Write (optional)",
|
|
||||||
value: "write"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
label="Permissions"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
{options.map(({ label, value: optionValue }) => {
|
|
||||||
return (
|
|
||||||
<Checkbox
|
|
||||||
id={value[optionValue]}
|
|
||||||
key={optionValue}
|
|
||||||
className="data-[state=checked]:bg-primary"
|
|
||||||
isChecked={value[optionValue]}
|
|
||||||
isDisabled={optionValue === "read"}
|
|
||||||
onCheckedChange={(state) => {
|
|
||||||
onChange({
|
|
||||||
...value,
|
|
||||||
[optionValue]: state
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Checkbox>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="mt-8 flex items-center">
|
|
||||||
<Button
|
|
||||||
className="mr-4"
|
|
||||||
type="submit"
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
<ModalClose asChild>
|
|
||||||
<Button variant="plain" colorSchema="secondary">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</ModalClose>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
) : (
|
|
||||||
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
|
||||||
<p className="mr-4 break-all">{newToken}</p>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="copy icon"
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="group relative"
|
|
||||||
onClick={copyTokenToClipboard}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isTokenCopied ? faCheck : faCopy} />
|
|
||||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
|
||||||
{t("common.click-to-copy")}
|
|
||||||
</span>
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</ModalContent>
|
/>
|
||||||
</Modal>
|
{tokenScopes.map(({ id }, index) => (
|
||||||
);
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
}
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`scopes.${index}.environment`}
|
||||||
|
defaultValue={currentWorkspace?.environments?.[0]?.slug}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className="mb-0"
|
||||||
|
label={index === 0 ? "Environment" : undefined}
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{currentWorkspace?.environments.map(({ name, slug }) => (
|
||||||
|
<SelectItem value={slug} key={slug}>
|
||||||
|
{name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`scopes.${index}.secretPath`}
|
||||||
|
defaultValue="/"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
label={index === 0 ? "Secrets Path" : undefined}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="can be /, /nested/**, /**/deep" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
className="p-3"
|
||||||
|
ariaLabel="remove"
|
||||||
|
colorSchema="danger"
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrashCan} size="sm" />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="my-4 ml-1">
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() =>
|
||||||
|
append({
|
||||||
|
environment: currentWorkspace?.environments?.[0]?.slug || "",
|
||||||
|
secretPath: ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
Add Scope
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="expiresIn"
|
||||||
|
defaultValue={String(apiTokenExpiry?.[0]?.value)}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Expiration" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{apiTokenExpiry.map(({ label, value }) => (
|
||||||
|
<SelectItem value={String(value || "")} key={label}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="permissions"
|
||||||
|
defaultValue={{
|
||||||
|
read: true,
|
||||||
|
write: false
|
||||||
|
}}
|
||||||
|
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
label: "Read (default)",
|
||||||
|
value: "read"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Write (optional)",
|
||||||
|
value: "write"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
label="Permissions"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{options.map(({ label, value: optionValue }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
id={value[optionValue]}
|
||||||
|
key={optionValue}
|
||||||
|
className="data-[state=checked]:bg-primary"
|
||||||
|
isChecked={value[optionValue]}
|
||||||
|
isDisabled={optionValue === "read"}
|
||||||
|
onCheckedChange={(state) => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
[optionValue]: state
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
type="submit"
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button variant="plain" colorSchema="secondary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||||
|
<p className="mr-4 break-all">{newToken}</p>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={copyTokenToClipboard}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isTokenCopied ? faCheck : faCopy} />
|
||||||
|
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||||
|
{t("common.click-to-copy")}
|
||||||
|
</span>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+25
-31
@@ -3,14 +3,9 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
|||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import {
|
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||||
Button,
|
|
||||||
DeleteActionModal,
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import {
|
import { useDeleteServiceToken } from "@app/hooks/api";
|
||||||
useDeleteServiceToken
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
|
|
||||||
import { AddServiceTokenModal } from "./AddServiceTokenModal";
|
import { AddServiceTokenModal } from "./AddServiceTokenModal";
|
||||||
import { ServiceTokenTable } from "./ServiceTokenTable";
|
import { ServiceTokenTable } from "./ServiceTokenTable";
|
||||||
@@ -29,7 +24,9 @@ export const ServiceTokenSection = () => {
|
|||||||
|
|
||||||
const onDeleteApproved = async () => {
|
const onDeleteApproved = async () => {
|
||||||
try {
|
try {
|
||||||
deleteServiceToken.mutateAsync((popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.id);
|
deleteServiceToken.mutateAsync(
|
||||||
|
(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.id
|
||||||
|
);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Successfully deleted service token",
|
text: "Successfully deleted service token",
|
||||||
type: "success"
|
type: "success"
|
||||||
@@ -46,32 +43,29 @@ export const ServiceTokenSection = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 p-4 bg-mineshaft-900 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
<div className="mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="flex justify-between mb-8">
|
<div className="mb-2 flex justify-between">
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">{t("section.token.service-tokens")}</p>
|
<p className="text-xl font-semibold text-mineshaft-100">
|
||||||
<Button
|
{t("section.token.service-tokens")}
|
||||||
colorSchema="secondary"
|
</p>
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
<Button
|
||||||
onClick={() => {
|
colorSchema="secondary"
|
||||||
handlePopUpOpen("createAPIToken");
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
}}
|
onClick={() => {
|
||||||
>
|
handlePopUpOpen("createAPIToken");
|
||||||
Create token
|
}}
|
||||||
</Button>
|
>
|
||||||
|
Create token
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-400 mb-8">{t("section.token.service-tokens-description")}</p>
|
<p className="mb-8 text-gray-400">{t("section.token.service-tokens-description")}</p>
|
||||||
<ServiceTokenTable
|
<ServiceTokenTable handlePopUpOpen={handlePopUpOpen} />
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
<AddServiceTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
/>
|
|
||||||
<AddServiceTokenModal
|
|
||||||
popUp={popUp}
|
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
|
||||||
/>
|
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.deleteAPITokenConfirmation.isOpen}
|
isOpen={popUp.deleteAPITokenConfirmation.isOpen}
|
||||||
title={
|
title={`Delete ${
|
||||||
`Delete ${(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name || " "} service token?`
|
(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name || " "
|
||||||
}
|
} service token?`}
|
||||||
onChange={(isOpen) => handlePopUpToggle("deleteAPITokenConfirmation", isOpen)}
|
onChange={(isOpen) => handlePopUpToggle("deleteAPITokenConfirmation", isOpen)}
|
||||||
deleteKey={(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name}
|
deleteKey={(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name}
|
||||||
onClose={() => handlePopUpClose("deleteAPITokenConfirmation")}
|
onClose={() => handlePopUpClose("deleteAPITokenConfirmation")}
|
||||||
|
|||||||
+76
-65
@@ -1,4 +1,4 @@
|
|||||||
import { faKey, faTrashCan } from "@fortawesome/free-solid-svg-icons";
|
import { faFolder, faKey, faTrashCan } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -18,71 +18,82 @@ import { useGetUserWsServiceTokens } from "@app/hooks/api";
|
|||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (
|
handlePopUpOpen: (
|
||||||
popUpName: keyof UsePopUpState<["deleteAPITokenConfirmation"]>,
|
popUpName: keyof UsePopUpState<["deleteAPITokenConfirmation"]>,
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
id
|
id
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ServiceTokenTable = ({
|
export const ServiceTokenTable = ({ handlePopUpOpen }: Props) => {
|
||||||
handlePopUpOpen
|
const { currentWorkspace } = useWorkspace();
|
||||||
}: Props) => {
|
const { data, isLoading } = useGetUserWsServiceTokens({
|
||||||
const { currentWorkspace } = useWorkspace();
|
workspaceID: currentWorkspace?._id || ""
|
||||||
const { data, isLoading } = useGetUserWsServiceTokens({
|
});
|
||||||
workspaceID: currentWorkspace?._id || ""
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>Token Name</Th>
|
<Th>Token Name</Th>
|
||||||
<Th>Environment</Th>
|
<Th>Envrionment - Secret Path</Th>
|
||||||
<Th>Secret Path</Th>
|
<Th>Valid Until</Th>
|
||||||
<Th>Valid Until</Th>
|
<Th aria-label="button" />
|
||||||
<Th aria-label="button" />
|
</Tr>
|
||||||
</Tr>
|
</THead>
|
||||||
</THead>
|
<TBody>
|
||||||
<TBody>
|
{isLoading && <TableSkeleton columns={4} key="project-service-tokens" />}
|
||||||
{isLoading && <TableSkeleton columns={4} key="project-service-tokens" />}
|
{!isLoading &&
|
||||||
{!isLoading && data && data.map((row) => (
|
data &&
|
||||||
<Tr key={row._id}>
|
data.map((row) => (
|
||||||
<Td>{row.name}</Td>
|
<Tr key={row._id}>
|
||||||
<Td>{row.environment}</Td>
|
<Td>{row.name}</Td>
|
||||||
<Td>{row.secretPath}</Td>
|
<Td>
|
||||||
<Td>{row.expiresAt && new Date(row.expiresAt).toUTCString()}</Td>
|
<div className="mb-2 flex flex-col flex-wrap space-y-1">
|
||||||
<Td className="flex items-center justify-end">
|
{row?.scopes.map(({ secretPath, environment }) => (
|
||||||
<IconButton
|
<div
|
||||||
onClick={() =>
|
key={`${row._id}-${environment}-${secretPath}`}
|
||||||
handlePopUpOpen("deleteAPITokenConfirmation", {
|
className="inline-flex items-center space-x-1 rounded-md border border-mineshaft-600 p-1 px-2"
|
||||||
name: row.name,
|
>
|
||||||
id: row._id
|
<div className="mr-2 border-r border-mineshaft-600 pr-2">{environment}</div>
|
||||||
})
|
<FontAwesomeIcon icon={faFolder} size="sm" />
|
||||||
}
|
<span className="pl-2">{secretPath}</span>
|
||||||
colorSchema="danger"
|
</div>
|
||||||
ariaLabel="delete"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faTrashCan} />
|
|
||||||
</IconButton>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
))}
|
))}
|
||||||
{!isLoading && data && data?.length === 0 && (
|
</div>
|
||||||
<Tr>
|
</Td>
|
||||||
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400">
|
<Td>{row.expiresAt && new Date(row.expiresAt).toUTCString()}</Td>
|
||||||
<EmptyState title="No service tokens found" icon={faKey} />
|
<Td>
|
||||||
</Td>
|
<IconButton
|
||||||
</Tr>
|
onClick={() =>
|
||||||
)}
|
handlePopUpOpen("deleteAPITokenConfirmation", {
|
||||||
</TBody>
|
name: row.name,
|
||||||
</Table>
|
id: row._id
|
||||||
</TableContainer>
|
})
|
||||||
);
|
}
|
||||||
}
|
colorSchema="danger"
|
||||||
|
ariaLabel="delete"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrashCan} />
|
||||||
|
</IconButton>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
{!isLoading && data && data?.length === 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400">
|
||||||
|
<EmptyState title="No service tokens found" icon={faKey} />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ type: application
|
|||||||
# This is the chart version. This version number should be incremented each time you make changes
|
# This is the chart version. This version number should be incremented each time you make changes
|
||||||
# to the chart and its templates, including the app version.
|
# to the chart and its templates, including the app version.
|
||||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
version: 0.1.6
|
version: 0.2.0
|
||||||
# This is the version number of the application being deployed. This version number should be
|
# This is the version number of the application being deployed. This version number should be
|
||||||
# incremented each time you make changes to the application. Versions are not expected to
|
# incremented each time you make changes to the application. Versions are not expected to
|
||||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||||
# It is recommended to use it with quotes.
|
# It is recommended to use it with quotes.
|
||||||
appVersion: "0.1.8"
|
appVersion: "0.2.0"
|
||||||
|
|||||||
@@ -63,6 +63,16 @@ spec:
|
|||||||
type: object
|
type: object
|
||||||
serviceToken:
|
serviceToken:
|
||||||
properties:
|
properties:
|
||||||
|
secretsScope:
|
||||||
|
properties:
|
||||||
|
envSlug:
|
||||||
|
type: string
|
||||||
|
secretsPath:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- envSlug
|
||||||
|
- secretsPath
|
||||||
|
type: object
|
||||||
serviceTokenSecretReference:
|
serviceTokenSecretReference:
|
||||||
properties:
|
properties:
|
||||||
secretName:
|
secretName:
|
||||||
@@ -77,6 +87,7 @@ spec:
|
|||||||
- secretNamespace
|
- secretNamespace
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
- secretsScope
|
||||||
- serviceTokenSecretReference
|
- serviceTokenSecretReference
|
||||||
type: object
|
type: object
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@@ -4,8 +4,19 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Authentication struct {
|
||||||
|
// +kubebuilder:validation:Optional
|
||||||
|
ServiceAccount ServiceAccountDetails `json:"serviceAccount"`
|
||||||
|
// +kubebuilder:validation:Optional
|
||||||
|
ServiceToken ServiceTokenDetails `json:"serviceToken"`
|
||||||
|
}
|
||||||
|
|
||||||
type ServiceTokenDetails struct {
|
type ServiceTokenDetails struct {
|
||||||
|
// +kubebuilder:validation:Required
|
||||||
ServiceTokenSecretReference KubeSecretReference `json:"serviceTokenSecretReference"`
|
ServiceTokenSecretReference KubeSecretReference `json:"serviceTokenSecretReference"`
|
||||||
|
|
||||||
|
// +kubebuilder:validation:Required
|
||||||
|
SecretsScope SecretScopeInWorkspace `json:"secretsScope"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServiceAccountDetails struct {
|
type ServiceAccountDetails struct {
|
||||||
@@ -14,11 +25,12 @@ type ServiceAccountDetails struct {
|
|||||||
EnvironmentName string `json:"environmentName"`
|
EnvironmentName string `json:"environmentName"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Authentication struct {
|
type SecretScopeInWorkspace struct {
|
||||||
// +kubebuilder:validation:Optional
|
// +kubebuilder:validation:Required
|
||||||
ServiceAccount ServiceAccountDetails `json:"serviceAccount"`
|
SecretsPath string `json:"secretsPath"`
|
||||||
// +kubebuilder:validation:Optional
|
|
||||||
ServiceToken ServiceTokenDetails `json:"serviceToken"`
|
// +kubebuilder:validation:Required
|
||||||
|
EnvSlug string `json:"envSlug"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type KubeSecretReference struct {
|
type KubeSecretReference struct {
|
||||||
|
|||||||
@@ -157,6 +157,21 @@ func (in *KubeSecretReference) DeepCopy() *KubeSecretReference {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *SecretScopeInWorkspace) DeepCopyInto(out *SecretScopeInWorkspace) {
|
||||||
|
*out = *in
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretScopeInWorkspace.
|
||||||
|
func (in *SecretScopeInWorkspace) DeepCopy() *SecretScopeInWorkspace {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(SecretScopeInWorkspace)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *ServiceAccountDetails) DeepCopyInto(out *ServiceAccountDetails) {
|
func (in *ServiceAccountDetails) DeepCopyInto(out *ServiceAccountDetails) {
|
||||||
*out = *in
|
*out = *in
|
||||||
@@ -177,6 +192,7 @@ func (in *ServiceAccountDetails) DeepCopy() *ServiceAccountDetails {
|
|||||||
func (in *ServiceTokenDetails) DeepCopyInto(out *ServiceTokenDetails) {
|
func (in *ServiceTokenDetails) DeepCopyInto(out *ServiceTokenDetails) {
|
||||||
*out = *in
|
*out = *in
|
||||||
out.ServiceTokenSecretReference = in.ServiceTokenSecretReference
|
out.ServiceTokenSecretReference = in.ServiceTokenSecretReference
|
||||||
|
out.SecretsScope = in.SecretsScope
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceTokenDetails.
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceTokenDetails.
|
||||||
|
|||||||
@@ -63,6 +63,16 @@ spec:
|
|||||||
type: object
|
type: object
|
||||||
serviceToken:
|
serviceToken:
|
||||||
properties:
|
properties:
|
||||||
|
secretsScope:
|
||||||
|
properties:
|
||||||
|
envSlug:
|
||||||
|
type: string
|
||||||
|
secretsPath:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- envSlug
|
||||||
|
- secretsPath
|
||||||
|
type: object
|
||||||
serviceTokenSecretReference:
|
serviceTokenSecretReference:
|
||||||
properties:
|
properties:
|
||||||
secretName:
|
secretName:
|
||||||
@@ -77,6 +87,7 @@ spec:
|
|||||||
- secretNamespace
|
- secretNamespace
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
- secretsScope
|
||||||
- serviceTokenSecretReference
|
- serviceTokenSecretReference
|
||||||
type: object
|
type: object
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ kind: InfisicalSecret
|
|||||||
metadata:
|
metadata:
|
||||||
name: infisicalsecret-sample
|
name: infisicalsecret-sample
|
||||||
spec:
|
spec:
|
||||||
hostAPI: http://localhost:7070/api
|
hostAPI: http://localhost:8764/api
|
||||||
resyncInterval: 60
|
resyncInterval: 10
|
||||||
authentication:
|
authentication:
|
||||||
serviceAccount:
|
serviceAccount:
|
||||||
serviceAccountSecretReference:
|
serviceAccountSecretReference:
|
||||||
@@ -16,10 +16,13 @@ spec:
|
|||||||
serviceTokenSecretReference:
|
serviceTokenSecretReference:
|
||||||
secretName: service-token
|
secretName: service-token
|
||||||
secretNamespace: default
|
secretNamespace: default
|
||||||
|
secretsScope:
|
||||||
|
envSlug: dev
|
||||||
|
secretsPath: "/"
|
||||||
managedSecretReference:
|
managedSecretReference:
|
||||||
secretName: managed-secret
|
secretName: managed-secret
|
||||||
secretNamespace: default
|
secretNamespace: default
|
||||||
# To be depreciated soon
|
# # To be depreciated soon
|
||||||
tokenSecretReference:
|
# tokenSecretReference:
|
||||||
secretName: service-token
|
# secretName: service-token
|
||||||
secretNamespace: default
|
# secretNamespace: default
|
||||||
|
|||||||
@@ -219,7 +219,10 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
|
|||||||
fmt.Println("ReconcileInfisicalSecret: Fetched secrets via service account")
|
fmt.Println("ReconcileInfisicalSecret: Fetched secrets via service account")
|
||||||
|
|
||||||
} else if infisicalToken != "" {
|
} else if infisicalToken != "" {
|
||||||
plainTextSecretsFromApi, fullEncryptedSecretsResponse, err = util.GetPlainTextSecretsViaServiceToken(infisicalToken, secretVersionBasedOnETag)
|
envSlug := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.EnvSlug
|
||||||
|
secretsPath := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.SecretsPath
|
||||||
|
|
||||||
|
plainTextSecretsFromApi, fullEncryptedSecretsResponse, err = util.GetPlainTextSecretsViaServiceToken(infisicalToken, secretVersionBasedOnETag, envSlug, secretsPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
|
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,16 @@ spec:
|
|||||||
type: object
|
type: object
|
||||||
serviceToken:
|
serviceToken:
|
||||||
properties:
|
properties:
|
||||||
|
secretsScope:
|
||||||
|
properties:
|
||||||
|
envSlug:
|
||||||
|
type: string
|
||||||
|
secretsPath:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- envSlug
|
||||||
|
- secretsPath
|
||||||
|
type: object
|
||||||
serviceTokenSecretReference:
|
serviceTokenSecretReference:
|
||||||
properties:
|
properties:
|
||||||
secretName:
|
secretName:
|
||||||
@@ -83,6 +93,7 @@ spec:
|
|||||||
- secretNamespace
|
- secretNamespace
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
- secretsScope
|
||||||
- serviceTokenSecretReference
|
- serviceTokenSecretReference
|
||||||
type: object
|
type: object
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package util
|
|||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Infisical/infisical/k8-operator/packages/api"
|
"github.com/Infisical/infisical/k8-operator/packages/api"
|
||||||
@@ -48,7 +50,7 @@ func GetServiceTokenDetails(infisicalToken string) (api.GetServiceTokenDetailsRe
|
|||||||
return serviceTokenDetails, nil
|
return serviceTokenDetails, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV3Response, error) {
|
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string, envSlug string, secretPath string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV3Response, error) {
|
||||||
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
|
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
|
||||||
if len(serviceTokenParts) < 4 {
|
if len(serviceTokenParts) < 4 {
|
||||||
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
|
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
|
||||||
@@ -68,9 +70,9 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([
|
|||||||
|
|
||||||
encryptedSecretsResponse, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
|
encryptedSecretsResponse, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
|
||||||
WorkspaceId: serviceTokenDetails.Workspace,
|
WorkspaceId: serviceTokenDetails.Workspace,
|
||||||
Environment: serviceTokenDetails.Environment,
|
Environment: envSlug,
|
||||||
ETag: etag,
|
ETag: etag,
|
||||||
SecretPath: serviceTokenDetails.SecretPath,
|
SecretPath: secretPath,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -92,7 +94,10 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([
|
|||||||
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
|
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return plainTextSecrets, encryptedSecretsResponse, nil
|
// expand secrets that are referenced
|
||||||
|
expandedSecrets := ExpandSecrets(plainTextSecrets, fullServiceToken)
|
||||||
|
|
||||||
|
return expandedSecrets, encryptedSecretsResponse, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetches plaintext secrets from an API endpoint using a service account.
|
// Fetches plaintext secrets from an API endpoint using a service account.
|
||||||
@@ -252,3 +257,104 @@ func GetPlainTextSecrets(key []byte, encryptedSecretsResponse api.GetEncryptedSe
|
|||||||
|
|
||||||
return plainTextSecrets, nil
|
return plainTextSecrets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var secRefRegex = regexp.MustCompile(`\${([^\}]*)}`)
|
||||||
|
|
||||||
|
func recursivelyExpandSecret(expandedSecs map[string]string, interpolatedSecs map[string]string, crossSecRefFetch func(env string, path []string, key string) string, key string) string {
|
||||||
|
if v, ok := expandedSecs[key]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
interpolatedVal, ok := interpolatedSecs[key]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
// panic(fmt.Errorf("Could not find referred secret with key name %s", key), "Please check it refers a")
|
||||||
|
}
|
||||||
|
|
||||||
|
refs := secRefRegex.FindAllStringSubmatch(interpolatedVal, -1)
|
||||||
|
for _, val := range refs {
|
||||||
|
// key: "${something}" val: [${something},something]
|
||||||
|
interpolatedExp, interpolationKey := val[0], val[1]
|
||||||
|
ref := strings.Split(interpolationKey, ".")
|
||||||
|
|
||||||
|
// ${KEY1} => [key1]
|
||||||
|
if len(ref) == 1 {
|
||||||
|
val := recursivelyExpandSecret(expandedSecs, interpolatedSecs, crossSecRefFetch, interpolationKey)
|
||||||
|
interpolatedVal = strings.ReplaceAll(interpolatedVal, interpolatedExp, val)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// cross board reference ${env.folder.key1} => [env folder key1]
|
||||||
|
if len(ref) > 1 {
|
||||||
|
secEnv, tmpSecPath, secKey := ref[0], ref[1:len(ref)-1], ref[len(ref)-1]
|
||||||
|
interpolatedSecs[interpolationKey] = crossSecRefFetch(secEnv, tmpSecPath, secKey) // get the reference value
|
||||||
|
val := recursivelyExpandSecret(expandedSecs, interpolatedSecs, crossSecRefFetch, interpolationKey)
|
||||||
|
interpolatedVal = strings.ReplaceAll(interpolatedVal, interpolatedExp, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
expandedSecs[key] = interpolatedVal
|
||||||
|
return interpolatedVal
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExpandSecrets(secrets []model.SingleEnvironmentVariable, infisicalToken string) []model.SingleEnvironmentVariable {
|
||||||
|
expandedSecs := make(map[string]string)
|
||||||
|
interpolatedSecs := make(map[string]string)
|
||||||
|
// map[env.secret-path][keyname]Secret
|
||||||
|
crossEnvRefSecs := make(map[string]map[string]model.SingleEnvironmentVariable) // a cache to hold all cross board reference secrets
|
||||||
|
|
||||||
|
for _, sec := range secrets {
|
||||||
|
// get all references in a secret
|
||||||
|
refs := secRefRegex.FindAllStringSubmatch(sec.Value, -1)
|
||||||
|
// nil means its a secret without reference
|
||||||
|
if refs == nil {
|
||||||
|
expandedSecs[sec.Key] = sec.Value // atomic secrets without any interpolation
|
||||||
|
} else {
|
||||||
|
interpolatedSecs[sec.Key] = sec.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, sec := range secrets {
|
||||||
|
// already present pick that up
|
||||||
|
if expandedVal, ok := expandedSecs[sec.Key]; ok {
|
||||||
|
secrets[i].Value = expandedVal
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
expandedVal := recursivelyExpandSecret(expandedSecs, interpolatedSecs, func(env string, secPaths []string, secKey string) string {
|
||||||
|
secPaths = append([]string{"/"}, secPaths...)
|
||||||
|
secPath := path.Join(secPaths...)
|
||||||
|
|
||||||
|
secPathDot := strings.Join(secPaths, ".")
|
||||||
|
uniqKey := fmt.Sprintf("%s.%s", env, secPathDot)
|
||||||
|
|
||||||
|
if crossRefSec, ok := crossEnvRefSecs[uniqKey]; !ok {
|
||||||
|
// if not in cross reference cache, fetch it from server
|
||||||
|
refSecs, _, err := GetPlainTextSecretsViaServiceToken(infisicalToken, "", env, secPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("HELLO===>", "MOO", err)
|
||||||
|
// HandleError(err, fmt.Sprintf("Could not fetch secrets in environment: %s secret-path: %s", env, secPath), "If you are using a service token to fetch secrets, please ensure it is valid")
|
||||||
|
}
|
||||||
|
refSecsByKey := getSecretsByKeys(refSecs)
|
||||||
|
// save it to avoid calling api again for same environment and folder path
|
||||||
|
crossEnvRefSecs[uniqKey] = refSecsByKey
|
||||||
|
return refSecsByKey[secKey].Value
|
||||||
|
} else {
|
||||||
|
return crossRefSec[secKey].Value
|
||||||
|
}
|
||||||
|
}, sec.Key)
|
||||||
|
|
||||||
|
secrets[i].Value = expandedVal
|
||||||
|
}
|
||||||
|
return secrets
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSecretsByKeys(secrets []model.SingleEnvironmentVariable) map[string]model.SingleEnvironmentVariable {
|
||||||
|
secretMapByName := make(map[string]model.SingleEnvironmentVariable, len(secrets))
|
||||||
|
|
||||||
|
for _, secret := range secrets {
|
||||||
|
secretMapByName[secret.Key] = secret
|
||||||
|
}
|
||||||
|
|
||||||
|
return secretMapByName
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user