diff --git a/.infisicalignore b/.infisicalignore index b00bf0995..02cdd4f0e 100644 --- a/.infisicalignore +++ b/.infisicalignore @@ -28,3 +28,15 @@ frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow docs/cli/commands/user.mdx:generic-api-key:51 frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx:generic-api-key:76 docs/integrations/app-connections/hashicorp-vault.mdx:generic-api-key:188 +cli/detect/config/gitleaks.toml:gcp-api-key:567 +cli/detect/config/gitleaks.toml:gcp-api-key:569 +cli/detect/config/gitleaks.toml:gcp-api-key:570 +cli/detect/config/gitleaks.toml:gcp-api-key:572 +cli/detect/config/gitleaks.toml:gcp-api-key:574 +cli/detect/config/gitleaks.toml:gcp-api-key:575 +cli/detect/config/gitleaks.toml:gcp-api-key:576 +cli/detect/config/gitleaks.toml:gcp-api-key:577 +cli/detect/config/gitleaks.toml:gcp-api-key:578 +cli/detect/config/gitleaks.toml:gcp-api-key:579 +cli/detect/config/gitleaks.toml:gcp-api-key:581 +cli/detect/config/gitleaks.toml:gcp-api-key:582 diff --git a/backend/package-lock.json b/backend/package-lock.json index 0bc3aadd1..59698d5b3 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -90,6 +90,7 @@ "mysql2": "^3.9.8", "nanoid": "^3.3.8", "nodemailer": "^6.9.9", + "oci-sdk": "^2.108.0", "odbc": "^2.4.9", "openid-client": "^5.6.5", "ora": "^7.0.1", @@ -10656,6 +10657,12 @@ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" }, + "node_modules/@types/isomorphic-fetch": { + "version": "0.0.35", + "resolved": "https://registry.npmjs.org/@types/isomorphic-fetch/-/isomorphic-fetch-0.0.35.tgz", + "integrity": "sha512-DaZNUvLDCAnCTjgwxgiL1eQdxIKEpNLOlTNtAgnZc50bG2copGhRrFN9/PxPBuJe+tZVLCbQ7ls0xveXVRPkvw==", + "license": "MIT" + }, "node_modules/@types/jmespath": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/@types/jmespath/-/jmespath-0.15.2.tgz", @@ -10689,6 +10696,12 @@ "integrity": "sha512-2h3tFvkbHksiNcDiUdcJ08gXWG10fnahp30GJ2Tbt4vd4pfsbfkoKTaTbYykFoppaJ6DL3914nQ3PU1vVIlBRQ==", "dev": true }, + "node_modules/@types/jssha": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/jssha/-/jssha-2.0.0.tgz", + "integrity": "sha512-oBnY3csYnXfqZXDRBJwP1nDDJCW/+VMJ88UHT4DCy0deSXpJIQvMCwYlnmdW4M+u7PiSfQc44LmiFcUbJ8hLEw==", + "license": "MIT" + }, "node_modules/@types/ldapjs": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-2.2.5.tgz", @@ -10780,6 +10793,15 @@ "@types/node": "*" } }, + "node_modules/@types/opossum": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@types/opossum/-/opossum-4.1.1.tgz", + "integrity": "sha512-9TMnd8AWRVtnZMqBbbzceQoJdafErgUViogFaQ3eetsbeLtiFFZ695mepNaLtlfJi4uRP3GmHfe3CJ2DZKaxYA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/passport": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.16.tgz", @@ -11027,6 +11049,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/sshpk": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@types/sshpk/-/sshpk-1.10.3.tgz", + "integrity": "sha512-cru1waDhHZnZuB18E6Dgf2UXf8U93mdOEDcKYe5jTri+fpucidSs7DLmGICpLxN+95aYkwtgeyny9fBFzQVdmA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", @@ -12359,6 +12390,12 @@ "fastq": "^1.17.1" } }, + "node_modules/await-semaphore": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/await-semaphore/-/await-semaphore-0.1.3.tgz", + "integrity": "sha512-d1W2aNSYcz/sxYO4pMGX9vq65qOTu0P800epMud+6cYYX0QcT7zyqcxec3VWzpgvdXo57UWmVbZpLMjX2m1I7Q==", + "license": "MIT" + }, "node_modules/aws-sdk": { "version": "2.1553.0", "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1553.0.tgz", @@ -12589,6 +12626,21 @@ "node": ">= 10.0.0" } }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bcrypt-pbkdf/node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, "node_modules/bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", @@ -13833,6 +13885,18 @@ "dev": true, "license": "MIT" }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/data-urls": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", @@ -14369,6 +14433,22 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ecc-jsbn/node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "license": "MIT" + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -14666,6 +14746,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es6-promise": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.6.tgz", + "integrity": "sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q==", + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", @@ -16407,6 +16493,15 @@ "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==" }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -17036,6 +17131,20 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/http-signature": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.1.tgz", + "integrity": "sha512-Y29YKEc8MQsjch/VzkUVJ+2MXd9WcR42fK5u36CZf4G8bXw2DXMTWuESiB0R6m59JAWxlPPw5/Fri/t/AyyueA==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.14.1" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -17741,6 +17850,16 @@ "node": ">=18" } }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", @@ -17967,6 +18086,12 @@ "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, "node_modules/json-schema-ref-resolver": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", @@ -18073,6 +18198,44 @@ "npm": ">=6" } }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/jsprim/node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/jsprim/node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "node_modules/jsrp": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/jsrp/-/jsrp-0.2.4.tgz", @@ -18083,6 +18246,16 @@ "randombytes": "^2.0.0" } }, + "node_modules/jssha": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-2.4.1.tgz", + "integrity": "sha512-77DN1YurYgh+7FPCTJ2CQ6hVDHgIWiHxm4Y5/mAdnpETKYagX22pVWMz4xfKF5fcpNfMaztgVj+/B1bt2k23Eg==", + "deprecated": "jsSHA versions < 3.0.0 will no longer receive feature updates", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, "node_modules/jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -19955,6 +20128,1722 @@ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true }, + "node_modules/oci-accessgovernancecp": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-accessgovernancecp/-/oci-accessgovernancecp-2.108.0.tgz", + "integrity": "sha512-lohjenh/9XOWSt34clBbCMIa460TC1Lxrj+myry0JrFR8P5zzehqjmLDEUpDjpXx0oACP5t+3bhwuDn/GDzj7w==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-adm": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-adm/-/oci-adm-2.108.0.tgz", + "integrity": "sha512-V8faYUwFeQFYFcl6bqnxlF9CzILH6VAb/kzXH9sHX8R2OYF8vXW7rTH72VlW5vxqEDzX0zYhHu46sJo/C5vkBw==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-aianomalydetection": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-aianomalydetection/-/oci-aianomalydetection-2.108.0.tgz", + "integrity": "sha512-tJvJ/Mh0owQAIKVsTZyiPXymmUKP1b99yZDYg4rWy3mojrUZ6wHAT5OGgMOa9cSdfTvlkTnyZ194yt54ymG38g==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-aidocument": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-aidocument/-/oci-aidocument-2.108.0.tgz", + "integrity": "sha512-fLGR1rnbhPOgKZ2NReWiYR83XyNY3wW5jV91Q0twSnFuFbkWGE0b/P/89ire06DMkRvkb/nESuYActhDUFhcGg==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-ailanguage": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-ailanguage/-/oci-ailanguage-2.108.0.tgz", + "integrity": "sha512-DhwnTXbSs3Z43B4+sK3l7NU+hbOcfk/ZBWfcy32jOA0DsOaH2WUiaUss801IvoE8iaWuAFbNX1odpphgFFHfwg==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-aispeech": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-aispeech/-/oci-aispeech-2.108.0.tgz", + "integrity": "sha512-WZUUugibvl5qaX8IgiUj/1hIC3PAZIm9uPQnLMGXeFYmO/Zu5YunVJ85T/qTI3U7UY8O3kdGiXsUKFKCTnZc1A==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-aivision": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-aivision/-/oci-aivision-2.108.0.tgz", + "integrity": "sha512-cgoQ73OfY2+6AELGzXqv4nf9EIUXFx8ENYgRgg6P4DnyFY04NgeUufiZGM2nB4XByxJ862DzBw2YydKlTXPi7A==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-analytics": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-analytics/-/oci-analytics-2.108.0.tgz", + "integrity": "sha512-p09Hk1fFz85nhvkWaFDEEUNwUJFBQFXQpj4OZzGA8orERJhi1dzd6X1Px1dCHpXTaPG8S8NWk2tRV/uloVd7AQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-announcementsservice": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-announcementsservice/-/oci-announcementsservice-2.108.0.tgz", + "integrity": "sha512-rYBcCHP+jZ4CGkJ0mUd6jdFU149AQjxqagoXH/LMUYvSS3ATUf91LlbiWQW22w5k0Otl5SSdMmEiIV7h7NUYyg==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-apigateway": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-apigateway/-/oci-apigateway-2.108.0.tgz", + "integrity": "sha512-h6fIWU0kDPTxeqOsNJL35nPrDL8yr4YEKyuhJ4SQsA3wY2BqGs//eX+we4tTXHkEFBtbVJdRWYrt7BU8adsG9w==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-apmconfig": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-apmconfig/-/oci-apmconfig-2.108.0.tgz", + "integrity": "sha512-a7YYSKFjdrH9nrngT1OwQ/40yTnPo1SzVas9ImeFLkUFm/5lC/D8t6Rmv7dlR/WO7U5o7e5dN5zLEDVf9LZqBg==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-apmcontrolplane": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-apmcontrolplane/-/oci-apmcontrolplane-2.108.0.tgz", + "integrity": "sha512-jIeCJVr+Ci+3Ogifcwe5OYyeQg6otmoT+UiGoMHcUn+gNTgPcG5tSsqQ7C/2KL/qg1P+XEBoDtMen5wBXionPg==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-apmsynthetics": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-apmsynthetics/-/oci-apmsynthetics-2.108.0.tgz", + "integrity": "sha512-h13UuPx0UUHV+IyoJtsov4KvNwB0l4QJABW8K49xsOkllUd4IO4VomJw1yhrau9OVXe8QE0P2GyCAHdH1rJPfw==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-apmtraces": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-apmtraces/-/oci-apmtraces-2.108.0.tgz", + "integrity": "sha512-ufH4/WYXd2N6AveLCKDynaCx/T9UgzQ/LOatIbtZ1Q86ywd5aS4vdnzKxWUMdDNI//10z92nWeCZCqGov0HgkA==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-appmgmtcontrol": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-appmgmtcontrol/-/oci-appmgmtcontrol-2.108.0.tgz", + "integrity": "sha512-JgFGFoJZW0gTtc010K19uIXGOevqpOT575ndRGxpQO04U+41GRDb+IOUe2NYe4ehw1rfYRA4/uUM0xdcbzPPRg==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-artifacts": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-artifacts/-/oci-artifacts-2.108.0.tgz", + "integrity": "sha512-ZljcFpyjVuQZiu4V/gKTEjxu1pMiQVH9o5k+Ys++cMtQWBvGyMlnxrDwpFRi16vHoXuPxKw2ygvjqM2wNN8M/g==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-audit": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-audit/-/oci-audit-2.108.0.tgz", + "integrity": "sha512-i7iH6sMzqGi0zl3SwNatnnzqb5CkKXr1sIW8uiamBghhyjIE8sehrW/eQl+hrzKNk/piSvi/5f5ftZ0MIQjxww==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-autoscaling": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-autoscaling/-/oci-autoscaling-2.108.0.tgz", + "integrity": "sha512-HCIU06FXuDa3suv/t0q+dchZAPFr6tRswccRIytvVm4eBJHiB6vblEqrJ6jgPzBd6F+y+hPZ4ZlJ1L2MLE6WYA==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-bastion": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-bastion/-/oci-bastion-2.108.0.tgz", + "integrity": "sha512-6Ys6CAO6K+ylKkjQcyBr7oglRQZWd4RZPJdhFqybIQmeysKzzaY44zoWMah7tcYryu3sKaypsmrypIOTXHIwdA==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-bds": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-bds/-/oci-bds-2.108.0.tgz", + "integrity": "sha512-eaWmH312PSJd1WiS6eV9KoKUGDzq94UwlaaqslS6Yo8cOLBWhNhrd4yoIcL/rGWpURLAuNfFkqqaIPEUWSzw1A==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-blockchain": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-blockchain/-/oci-blockchain-2.108.0.tgz", + "integrity": "sha512-KTVP/Nlki8Z5ZekU39N/IMEr7LhXbRtz+8u7e8VnGmiHbrJGEA377KQv0cUDczQ8vyC+7IbWLIGvXUgXYVd93A==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-budget": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-budget/-/oci-budget-2.108.0.tgz", + "integrity": "sha512-fy6DKzWD+HgDXjx0HzjgKz5nIRrDeZhn8EIiAMaCqUsPFA982iNw8BRwgw5k0tU6BZf/kwDeLUDc3O1NvlhUDQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-capacitymanagement": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-capacitymanagement/-/oci-capacitymanagement-2.108.0.tgz", + "integrity": "sha512-CEIoKbD49h7naGRFgyqfDnyEtQZfAl4b9IJsx+jJXvJ2sTmhYJagbWoBA/MkoHoYvRfUE3o2VVM1SkBMKLjE/Q==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-certificates": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-certificates/-/oci-certificates-2.108.0.tgz", + "integrity": "sha512-OmeY3hj3VX5r0IkyZg/IMv14CmVnhIUA04aAhtA9F94TlxGKgg6muQ5OppPhnmKrrvuBZhV+8w/3p0BUB0gt1g==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-certificatesmanagement": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-certificatesmanagement/-/oci-certificatesmanagement-2.108.0.tgz", + "integrity": "sha512-yDkpv49vDkGun6Byju19Uxm5+aR38zA1vEexW33hDWyOghLvZP1Jasu41G6xljytoIIy24wykBTZ3IAnB6I00g==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-cims": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-cims/-/oci-cims-2.108.0.tgz", + "integrity": "sha512-3lny4DzRAwtBGGs35K7LsVf388V3AQAyuiIhWDGBB71HtgBw9VGrL3sBB3jyO5WCjwE/akKEXJLKPvpjBgZBGw==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-cloudbridge": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-cloudbridge/-/oci-cloudbridge-2.108.0.tgz", + "integrity": "sha512-R6diQhWNusQ7jJU/z45IyrquJz5iZd1NHovNP9TwtkQw2yPrdIbMjLZYhoWSsVDNOnffn1q1VreYNYkid/4qoA==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-cloudguard": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-cloudguard/-/oci-cloudguard-2.108.0.tgz", + "integrity": "sha512-0qrH8OM1f1pIHc8tqOpeZfh+DRPlP88FikVf8woCeM8ekD4ysV04+zATAf+w8VVeASkHbYQnrTQB+UgtXMOB7A==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-cloudmigrations": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-cloudmigrations/-/oci-cloudmigrations-2.108.0.tgz", + "integrity": "sha512-ZITVnShAItKIoB2ONp4+XONUVUjKyh5dMg0Mh2Ik1OL2JpKr6tu5KWWUie15azaDj4TqQ022mSbHfdsV51DEIw==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-clusterplacementgroups": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-clusterplacementgroups/-/oci-clusterplacementgroups-2.108.0.tgz", + "integrity": "sha512-3TpH2710n4yJFI/oeMyEND719KbgiuP/OD9jjZMiGIDDi9XjGwZnsKNYP6K0kgRriwEDtUNZdFBDtJa69XyB7Q==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-common": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-common/-/oci-common-2.108.0.tgz", + "integrity": "sha512-H7kaU/A57ksvmXlLLFnTo91CeG6m3M5nbqYbWgniHl84vEmaM0vHhe5C9jQOpPUuhMdRRB2GareJYBjP79cqBg==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "@types/isomorphic-fetch": "0.0.35", + "@types/jsonwebtoken": "9.0.0", + "@types/jssha": "2.0.0", + "@types/opossum": "4.1.1", + "@types/sshpk": "1.10.3", + "es6-promise": "4.2.6", + "http-signature": "1.3.1", + "isomorphic-fetch": "3.0.0", + "jsonwebtoken": "9.0.0", + "jssha": "2.4.1", + "opossum": "5.0.1", + "sshpk": "1.16.1", + "uuid": "3.3.3" + } + }, + "node_modules/oci-common/node_modules/@types/jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-mM4TkDpA9oixqg1Fv2vVpOFyIVLJjm5x4k0V+K/rEsizfjD7Tk7LKk3GTtbB7KCfP0FEHQtsZqFxYA0+sijNVg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/oci-common/node_modules/jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/oci-common/node_modules/uuid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/oci-computecloudatcustomer": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-computecloudatcustomer/-/oci-computecloudatcustomer-2.108.0.tgz", + "integrity": "sha512-UU7GHrvMm6cJ1LeRbJfazq0/FrKEphePulLhvGn3IMiDxYRuAfON8RNydaJGZ+Q8s1Qv4BdID0//zPO7QkiD2Q==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-computeinstanceagent": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-computeinstanceagent/-/oci-computeinstanceagent-2.108.0.tgz", + "integrity": "sha512-1vn2zjyyCOOAtTKiyOG9pm9OxD0VPXZH7HPQP3CRcTalahfFY3WTy0Ti51/Ozk0rOLKUBpyJzcfsknaO87p4LQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-containerengine": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-containerengine/-/oci-containerengine-2.108.0.tgz", + "integrity": "sha512-OcQUtL/3rthwVx3rOTC1vJDTc6FL/kr0gQpqbjRtU5HBGnZE7Y7+R+CLWi+WejX6qP4IgU+Wv2T2Jig4tROZEQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-containerinstances": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-containerinstances/-/oci-containerinstances-2.108.0.tgz", + "integrity": "sha512-0JFULah06CupSJxrHZeOvdSYn6OkYw+/KY3eCb49K6Ht9/dtumHKTLVvGrr/b9JlAtl8ZPdFOd8hNb9WA/dAGQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-core": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-core/-/oci-core-2.108.0.tgz", + "integrity": "sha512-Nuowt0mFE+f1LDT+VFwQt9JRNzTsHkdRd8CPMBgS+czyyI89UsIkAYj9eVzgbyV98Btzv8aX/BF7EGh1BhYJKQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-dashboardservice": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-dashboardservice/-/oci-dashboardservice-2.108.0.tgz", + "integrity": "sha512-zmg7hgVjqXJ0zgf/53bxBnpiZ2nbb8InccjElbiApvUhICMw65BsDCh0JDxyZUbfFBORZSVJcWbr3J5PzudE1w==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-database": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-database/-/oci-database-2.108.0.tgz", + "integrity": "sha512-q4Jb9ZosdVbCFtqqDBy1RY0zqk4hSljtvGu+z5A3DyZ6DfL7ALUah8GweZeVWsX6vvfwrc7H1ca15ksxGwl9Lw==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-databasemanagement": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-databasemanagement/-/oci-databasemanagement-2.108.0.tgz", + "integrity": "sha512-JQ0ysKWcG21jDGSCiOw1T/uqY+ChGG1SOKa/kMa4stLON86t/DAcjPE343uUtld4fts6GMBDXkGnx8hWW9BaQg==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-databasemigration": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-databasemigration/-/oci-databasemigration-2.108.0.tgz", + "integrity": "sha512-OIEx0CNTi9m+ydeFCKCOslcWOWoX+xJkWMbiGGESQdjmSSczkMXoAK4Kn+XmUcRxap2DczBCICS+8dafl7Z76w==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-databasetools": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-databasetools/-/oci-databasetools-2.108.0.tgz", + "integrity": "sha512-CYTfqYOdL/INiTTAfRB+DJS06PUclNL6q1AWMYFgDi++R4jcIueyJDXqqnSzwkt/iBzA8IW72er5N8pvVxgYRw==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-datacatalog": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-datacatalog/-/oci-datacatalog-2.108.0.tgz", + "integrity": "sha512-T4J175I1229EUpc68HaRMlIhIpQgRf2ajCCNriniwaaz7EWtej4LKRvzaw/eWe3DwHFI4kEb+9WXlpzHj0C7Hw==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-dataflow": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-dataflow/-/oci-dataflow-2.108.0.tgz", + "integrity": "sha512-GHPiHHdEC0onqBA4GCHFQ8RdijYmro3hLYXztgrI+uMoIEb/ms9ayBptKliEHwlHmEF85or7/sn5JB2xqXGxPQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-dataintegration": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-dataintegration/-/oci-dataintegration-2.108.0.tgz", + "integrity": "sha512-/BoQhwoBsrK2wGaO9uV6idnIooPb9GTOuE30p+c02Bm7yuFmXTGgA/mQxIEk6TcBvcJGss+3pWiyDrq03gEyqw==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-datalabelingservice": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-datalabelingservice/-/oci-datalabelingservice-2.108.0.tgz", + "integrity": "sha512-JE43+obBvanuiJepAGtCrz80giIMB2o8v5sP1Qe0xs7zuB1HtZ1k+tRuof2ipznbnLScCyUZGdROMReE5IiEmQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-datalabelingservicedataplane": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-datalabelingservicedataplane/-/oci-datalabelingservicedataplane-2.108.0.tgz", + "integrity": "sha512-m22njdO3pogqpbeOgJM+ArCwJJvWhHB2Nz13/Kj6YXPuQqFv0TUzGHEL2LuhqpNFdM4CvF5Dtb472q4Kav5+/A==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-datasafe": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-datasafe/-/oci-datasafe-2.108.0.tgz", + "integrity": "sha512-uZ18rhS9FmP//IrRunBQvdwbjdqQiLyKlIdaL0M8BiI+MDrE3ZXL5veZUS8GmD+xeBZyCHV8cqJAvQfcgsi2oA==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-datascience": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-datascience/-/oci-datascience-2.108.0.tgz", + "integrity": "sha512-nh/LpXBVYvBrS3Rp8K7J/l8JurDm1geFEgu9oNXxJ+fheofpAZ6HHXeRIVadXHARXbgLuu7ta+pmB1IBft6OEg==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-dblm": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-dblm/-/oci-dblm-2.108.0.tgz", + "integrity": "sha512-RJGrKUtzhWeXono6lUvNrRD/xrR4jVrmVNEgpSFTIkDM0rRlKP2lSqWdfZdB+qTt3QaNjKsRWm9TRxYyrroW+w==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-delegateaccesscontrol": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-delegateaccesscontrol/-/oci-delegateaccesscontrol-2.108.0.tgz", + "integrity": "sha512-Imri3k0tESbq4xNxlHwqfcRZgvVPxx2Lt29TMuC34mrj2zhNlcKIsg0xgvpmfB3NrEjKz9M/X4MSpIfjzVUJ+w==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-demandsignal": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-demandsignal/-/oci-demandsignal-2.108.0.tgz", + "integrity": "sha512-ZvpAJf5QnpeQ5rkMGuQIa04Dv0Q4gS+nmyWLXyIipMV5mrbkyWAdWo4crauZxiwBt6yyJe5r3zBkrNTtXWQubg==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-desktops": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-desktops/-/oci-desktops-2.108.0.tgz", + "integrity": "sha512-sEBs1QzvOj1z6NQjZHln65bXyIqOla34lJLsN260GL2AjcV5V/j/8668FP7InxfRFvrb+2+jcefeI54tZbx+NQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-devops": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-devops/-/oci-devops-2.108.0.tgz", + "integrity": "sha512-HgLwTv4+TA/sXIRPGFflyiHQWL9OjQXvej2V+nwiujeiMYsJ66bvTTE3FIm3ku+6apHlD6zQQ8s8VBAJ/IR/bA==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-disasterrecovery": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-disasterrecovery/-/oci-disasterrecovery-2.108.0.tgz", + "integrity": "sha512-GULI5fQg+8qWzw9Nk2U4p+COOiXBq5+6+XHRjfriEg1EkIwF+TUt71cwyOUaE+jKZQdI/gHz1ViQdy972S318w==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-dns": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-dns/-/oci-dns-2.108.0.tgz", + "integrity": "sha512-93hiGQU6tNwL++Qq6MNbw9RD6CFLl+6pUPaybnosR+/sjYnh0IinwFSaLChizzevSCWWILTr2h/BETafnDXXXA==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-dts": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-dts/-/oci-dts-2.108.0.tgz", + "integrity": "sha512-62/xBcPGA6IlAgez0Vrakz7OLav4DTA8SEGpUJPDWdMAAMoJFeSSCVpPh/tVRY3jzjM+3rTTQzmXiI9yix0EaQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-email": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-email/-/oci-email-2.108.0.tgz", + "integrity": "sha512-xIZDTjxuOuK3gcMYUPXUf39NOVQB8frrSJvGTa8Lp4aQNDGgkbwpONCbmyJj7pb0tvOag1likbRuQi9M8Cmjwg==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-emaildataplane": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-emaildataplane/-/oci-emaildataplane-2.108.0.tgz", + "integrity": "sha512-eSKh1yTNTwEF/YfuR2AaTmhyipG3jOBPA7kICx0Syrba4pPqvkNqgJBq1pQcJHH0wBxTZZTag7MlMPs4ibIz5g==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-emwarehouse": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-emwarehouse/-/oci-emwarehouse-2.108.0.tgz", + "integrity": "sha512-3vqgsNxz5jTXrrlZQT4Vl9zgGMABqD/OViv4oI/8zNS6eHC5zuUDVbGqe9sZNHlO1VtNOzDlRxbJHCzxj1mnwQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-events": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-events/-/oci-events-2.108.0.tgz", + "integrity": "sha512-4JoHrafbesO+cIXH9BADXZrZM2gCeUeNMTwZc38FP6rhOTK45CrYYP8Izbe+hzoKXCDsbp4rvLsxOdu9gJg1jQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-filestorage": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-filestorage/-/oci-filestorage-2.108.0.tgz", + "integrity": "sha512-XOce/0fDnnsgyRG3vKuHNXpbxAmQ/Erd2KwX9GGPlf6ig4uQHO4X6i2ylVD90OfaPxYbzwdV96j7cTwwlVHfXw==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-fleetappsmanagement": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-fleetappsmanagement/-/oci-fleetappsmanagement-2.108.0.tgz", + "integrity": "sha512-OHq1Ctm3EJM6jasYvtHO5HaypR14tw8F6BJPOemCd3sRzoUiqIiZ1qxi1USYq5Salpng+FfvL18Qt4XEZhW+9Q==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-fleetsoftwareupdate": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-fleetsoftwareupdate/-/oci-fleetsoftwareupdate-2.108.0.tgz", + "integrity": "sha512-M5UEi4Kl1vNGUtnf4eoc74nFzZ8k0engo5L8ufJ/lszPtcMJ0pTMFuHB6A9lOSgaXnaIXRIOEj7aW4Wo9lj+6w==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-functions": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-functions/-/oci-functions-2.108.0.tgz", + "integrity": "sha512-nD5rVZ3Pve7oTq+Dvpj1uPymRArq45U8Lm7J/EZCwwI9sFTu5ZQyL0nbXNHJRda24W+QdnWeHGQSIB4hcA806A==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-fusionapps": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-fusionapps/-/oci-fusionapps-2.108.0.tgz", + "integrity": "sha512-B8sNAB6er9LBd0C/l3qYRPLqGzf0s0lgyYWDEZsPT5q+1wS9fSmBAvyAfU9bUrL8GgsCaNC4W96ROfEjQ/ZKwA==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-generativeai": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-generativeai/-/oci-generativeai-2.108.0.tgz", + "integrity": "sha512-g2EGfBMDzVvo44IEPqLBBHf3pei0DvIOQUT23BgYO08nwfsNmHacuEx8yl6jZvb00/58qN+LjKBQ0807eoHGow==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-generativeaiagent": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-generativeaiagent/-/oci-generativeaiagent-2.108.0.tgz", + "integrity": "sha512-LbzrMlJsORYF67rcJl4gMmNXzzKk09HbnyoqxRT96tHYbSC72w7xMCMnc9xN2746K52mxT4ZsyOI+kPcD/XTww==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-generativeaiagentruntime": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-generativeaiagentruntime/-/oci-generativeaiagentruntime-2.108.0.tgz", + "integrity": "sha512-+TvJklyLWOlq70arKPnkfiCx8WTEDvb2sZgZiq7arxBJYUL3burg/bLO3Di+XlGaHMyoaBbKLW5tJM77rdFNKg==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-generativeaiinference": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-generativeaiinference/-/oci-generativeaiinference-2.108.0.tgz", + "integrity": "sha512-OBKKowDh7duUiMRS2LWmLWDGpDz6th7Ef0NznAYGniXAzxf3x1VcGkGYHi/8NEB5GvTzCxbPXYBunA/C0oUL8w==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-genericartifactscontent": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-genericartifactscontent/-/oci-genericartifactscontent-2.108.0.tgz", + "integrity": "sha512-6hpnmK4TQG5rUtlib89BQjMte4ho6xhkCH+nyWqbUwUTDMn3L5/On8Nk/O3Rchm6/lgObHZ4v+1LNt1vxE96JQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-globallydistributeddatabase": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-globallydistributeddatabase/-/oci-globallydistributeddatabase-2.108.0.tgz", + "integrity": "sha512-db79VO9Z/dCyhVCIcvg5suqFoUwo7UhH9zT14T8rPTvSuLm3ISZsEYU6XEshXefsIIV5huzElus/SvCEI/JC8g==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-goldengate": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-goldengate/-/oci-goldengate-2.108.0.tgz", + "integrity": "sha512-6pn1HAIXsvfcFaiSdXQtTViRXqQtubyktQyY4hXQ0HyDU+Iv9ZaIXrJ8Z/aAWSRVvH3Rw0W7L5Le4Q1ar+FJ9g==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-governancerulescontrolplane": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-governancerulescontrolplane/-/oci-governancerulescontrolplane-2.108.0.tgz", + "integrity": "sha512-EdtoyGwHAoug/1hHkx3fa/7cOQ55TzAC9lvHJg23uUgM4zHbEjvRuMJjEoyMo6NBWoLk5jM67ESAeczkOCsGaQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-healthchecks": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-healthchecks/-/oci-healthchecks-2.108.0.tgz", + "integrity": "sha512-/98NvgW1uKMxyC+6pvLGaVapMkxsh1qhKjBTXJShIkmGKUycXIksCkknMRCeik98FWmvoSWfu9I0dnTewjTq/w==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-identity": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-identity/-/oci-identity-2.108.0.tgz", + "integrity": "sha512-yetR36jJYFEIthzBe7qBSiZQczKIcYT6SQAejxlAXTwAW5uSsRaR6tmv1H24hB/csjVfhZeHFjeDYP99oxsonQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-identitydataplane": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-identitydataplane/-/oci-identitydataplane-2.108.0.tgz", + "integrity": "sha512-WZdy59Lwy85swqYJb6U3pUBUfDNPVPn4mUd/LseezvoDTHwiNTw66DtEQK87XBNMa8O57t4GMdtRV798WqM1fg==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-identitydomains": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-identitydomains/-/oci-identitydomains-2.108.0.tgz", + "integrity": "sha512-XQtHk2IA51gKvFAkXcKh/w7NhpoAUXMgy7/4ni8OKhR3Ru7bpGI0cea87iV/eNo0n/p3PxtAqy6AQF+/5VHdyA==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-integration": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-integration/-/oci-integration-2.108.0.tgz", + "integrity": "sha512-wmNN4T536iyf5UJaibyHpaGPrA5jNG0r7CwdN0cxQXOw1BhWV8vLXQfEaE/TRDJEmFahbOvdHdEIBH+IExKMrA==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-jms": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-jms/-/oci-jms-2.108.0.tgz", + "integrity": "sha512-R7NCobTxuMx8NWx6Vbov5cjBpLct9EW7cFmqurKTWdlWOC98spIujtMOkHnIcB4G5a6PaOWgViXhrjsVSarJaw==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-jmsjavadownloads": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-jmsjavadownloads/-/oci-jmsjavadownloads-2.108.0.tgz", + "integrity": "sha512-JeVPC3nvB6MxvL9P3fYm8MkCKSWJDWUrn0i6S0iEAVMJwjfoXkamt1UL1P2r9jCxnkJK9nxJh0YnUUXsSUWrYw==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-keymanagement": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-keymanagement/-/oci-keymanagement-2.108.0.tgz", + "integrity": "sha512-4hbzgIZI6C5TpUhPzt2jJASfke342aoOqH0oYKN1kb2cK+3BfyNFqh0loDgUtWkoKlk94joPAbHaf4ebmYmeHw==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-licensemanager": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-licensemanager/-/oci-licensemanager-2.108.0.tgz", + "integrity": "sha512-OKjENCbpN6LOHSJLEbomhh9+cMxOMRmkKaJultzGvyMJ0GnULgFC5+6n/de+2/+rchQyai62JhZlU7fiXAUcFw==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-limits": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-limits/-/oci-limits-2.108.0.tgz", + "integrity": "sha512-q8r56EfgjmFmUP6Jj7Bl668Jv3M+4AQM2kwWDMCWWJVIiyWLEmHURmiLkstnxlT9qfrjsHpyOp6Uy8fdm/idVg==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-loadbalancer": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-loadbalancer/-/oci-loadbalancer-2.108.0.tgz", + "integrity": "sha512-N+PyjBLP2ng2HFNlL+iuSHvJHGJQmHYIYW8wuZ4sYvA9rz4um/PQMPD6OgRz+kO3dsxF/8dPkwnQ33RwHwxMfQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-lockbox": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-lockbox/-/oci-lockbox-2.108.0.tgz", + "integrity": "sha512-CrVmzyvjBpy7yVfOso2x0M16h+p0zNzn75/7QVx9ifwFxBhSJpiztjH0YTADHd1l0KMNXa1ZDsxKq0mOQlYp0Q==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-loganalytics": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-loganalytics/-/oci-loganalytics-2.108.0.tgz", + "integrity": "sha512-5uFYU/1uHJYg8evPECvXC6oTZhYwXUk3CKkEcklXQAHptDEMYzVu24S/nBkqgdjHAXWtIHElh8V0lrgboseodA==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-logging": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-logging/-/oci-logging-2.108.0.tgz", + "integrity": "sha512-A7Gu+hoJGOI2tBrCkLdMHEabQmmUEAPZEUVEq9MrAsTqJYjZi89V7KNiaLx15ARCUaj9ivUYN9eamLfPSQQaJw==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-loggingingestion": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-loggingingestion/-/oci-loggingingestion-2.108.0.tgz", + "integrity": "sha512-otlfcKBUpAvg81fbyIHMUMNuTuErmrNq/yK2SynwqJUTcZ9EjbaysqCwed67tRDyETqru1/qX2tmI2XCQkATCQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-loggingsearch": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-loggingsearch/-/oci-loggingsearch-2.108.0.tgz", + "integrity": "sha512-Pj629/S9LYPH+wkDdi1gazYzv8V7PvvW6OgKsg5zH6XKBhZrD4k3yHmW1nNoEVmR3JHFui4DcK8YBj3Jv2oLCA==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-lustrefilestorage": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-lustrefilestorage/-/oci-lustrefilestorage-2.108.0.tgz", + "integrity": "sha512-WtQxJp2gQP/K411FHlh2bg8UW8Wx76b+ZA1ApySQR7IiXicDs0kSsfNluKat5DytFnAMt97Qh5pq2KnjwKg88w==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-managementagent": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-managementagent/-/oci-managementagent-2.108.0.tgz", + "integrity": "sha512-VCmMUoet6AsZXmKneykIxddRr6Wo01Aj1ByCnlg9PaqmWbNNv5PkmdzCDrpV8GNhpn2IyfAp61lr/lwI3imD+Q==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-managementdashboard": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-managementdashboard/-/oci-managementdashboard-2.108.0.tgz", + "integrity": "sha512-tVuAB2xRUiSYLjuUBiqtkqvszpmTQrVJLCZB92H+SzaNFeqa2OjoH/qK0jU6LOQuLfMr7SG3atATGYNqFLVMBw==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-marketplace": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-marketplace/-/oci-marketplace-2.108.0.tgz", + "integrity": "sha512-zMsftpZM6VThpicbhciK/b1irkrJPbqX45aHvj2iS1q+I7h7dG1WD+LMK/K6oS53idzFclHOvBCN0DMg1OYEjw==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-marketplaceprivateoffer": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-marketplaceprivateoffer/-/oci-marketplaceprivateoffer-2.108.0.tgz", + "integrity": "sha512-JMdMPLpRwiARCgjiEVbyVweYZjmIt2KntqG7o6SEDoIoE2j74poqHjP75M4794pNynFHT4AZJzb/Ryv1hwuWhQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-marketplacepublisher": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-marketplacepublisher/-/oci-marketplacepublisher-2.108.0.tgz", + "integrity": "sha512-EjjACVK6JDn3/m1kXadeJa+s7zUsPR9uVq+e6QFwyeB9ro1HnzG+qyYJikoDc/huF3ZYwQFlWcTFR3Q/x4acJg==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-mediaservices": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-mediaservices/-/oci-mediaservices-2.108.0.tgz", + "integrity": "sha512-AB1dRo+g12Qq6ep/BrtEHjEG49NpePqgDt6/WZSre1BpEcfyONTCMQDaI7HpeUH2OuEsn/duXLEHVLE1w0zQzg==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-mngdmac": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-mngdmac/-/oci-mngdmac-2.108.0.tgz", + "integrity": "sha512-kZg+mDSIeMQqBhQz0JmcSQKKdMizR975xxkLbLdUXdhtAoCcFMagYm4NqsdHGMbchMJ2JfKpB3ylPexoRyY89g==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-monitoring": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-monitoring/-/oci-monitoring-2.108.0.tgz", + "integrity": "sha512-nWRALVeyuIzFi7wRSb+hsYl8S7le7jlZKa47eCLlSHziHTJrDUT7PLAeSTdAVkPRzircHAFeD+H1SB8tVsyrzQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-mysql": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-mysql/-/oci-mysql-2.108.0.tgz", + "integrity": "sha512-Zr9B8hgwQy1Z+BTStUdrVnjj+2ZkeR3+NFlhxPGt2LvU+vm92XOUD33h6MbogswpJ/cb7RlazKxKQKJtPHfJPw==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-networkfirewall": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-networkfirewall/-/oci-networkfirewall-2.108.0.tgz", + "integrity": "sha512-TvGwagr0Qyt/BFnxyrVmPnsFryRn9snLXwJPwQU2MMcGlnJHUDcsidhKxV/tJzSHYNINmYy9IXOy4D+E8a+wUA==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-networkloadbalancer": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-networkloadbalancer/-/oci-networkloadbalancer-2.108.0.tgz", + "integrity": "sha512-XnuIvO4GRyKjRxigAMBl1zjKhxnGMyuo9fkx53nbPaWdmMM0mEbL6csKxTsozQxc29gUtUcZJI/tqvC/yBNQ+g==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-nosql": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-nosql/-/oci-nosql-2.108.0.tgz", + "integrity": "sha512-OJtPwgNmMPslXj/QIaT7NQQxPUrY1zCAPSZdCBfhlQH/D7x62w5A8BVP+HtkTA4LyN/GgGqnk/L01uni7lng8w==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-objectstorage": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-objectstorage/-/oci-objectstorage-2.108.0.tgz", + "integrity": "sha512-TjG6tf8RpnCz00loEy8Nhe/FD0P+TOkzyBaVoGODDr6rR1F7PmWL4k3u8rQA7tvTtJsIO//XaSaObcnW7ytM3g==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "await-semaphore": "^0.1.3", + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-oce": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-oce/-/oci-oce-2.108.0.tgz", + "integrity": "sha512-h+WUIkNpLTCjLLPM3xsMwR7r3K+NzId2Sxh4yIHbzOewRJIk1o/qpZzSyMevnUIL86S9IYPkVvjIVJnU2Dueyw==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-ocicontrolcenter": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-ocicontrolcenter/-/oci-ocicontrolcenter-2.108.0.tgz", + "integrity": "sha512-I6MfZbsYHkNojoqvzKdkz8vlQu/ZcYm80mZf9XpK9HY00SyY/SfPtwjutyGi46L8dIBVoXHhUNP3hQcVD5T0rQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-ocvp": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-ocvp/-/oci-ocvp-2.108.0.tgz", + "integrity": "sha512-JCriglSsxC1YXfUd/3xVxMojZT+b/+Go3hRIxom4gkOJ3Ceo0x7VXfJKeYGnjqERP5YIA2KWztgHQwAgAG0xFA==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-oda": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-oda/-/oci-oda-2.108.0.tgz", + "integrity": "sha512-QNkLczqrgaVVmnyYTRaiEF3cfohzgIYLyKpBT7Bg4Bu2kP0ljAQV0/BxD1XXpkUpC5atK5wjEq5urUWdybKqZg==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-onesubscription": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-onesubscription/-/oci-onesubscription-2.108.0.tgz", + "integrity": "sha512-Er6TBhzziC5uHURHLl1AjHjPp2r8wvYH7p59aJ0uyuXucZ0rGVJ9IkmU8T+TNznSx59iEhcmsYvaZyYUPI3RkQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-ons": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-ons/-/oci-ons-2.108.0.tgz", + "integrity": "sha512-C1Z/OBjPFeL3wa1M8AHArCbeNqul3GoO9QbEwZQ5qsNyKlF9Ol06UcdXA9vhSl25q+sH905JetuGaa9knIKA1A==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-opa": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-opa/-/oci-opa-2.108.0.tgz", + "integrity": "sha512-8jB/IqlevkwXlq9DSzTJ+2re+Y6vLCKFyVhQX323Sc/BamrEP2YnCU5HInSy+9GzQWbkBJ872CPvuQxLpUgHCA==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-opensearch": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-opensearch/-/oci-opensearch-2.108.0.tgz", + "integrity": "sha512-/Mofttk4YRa+/unJEFwis8gFgjQGCh55My5dzeKASBBlRGuSJTHWwJDBkZAsY/Boizen7nb2nvvgzQpB7Q42Zg==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-operatoraccesscontrol": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-operatoraccesscontrol/-/oci-operatoraccesscontrol-2.108.0.tgz", + "integrity": "sha512-Ln9tyjySUvWA7kHx7EQ6z0+wWuHoYQ7e9AvsxIVCuL6WzVy+CEONknNhqXqfMmSQHexgpXPaVJtTd8HRoISGUA==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-opsi": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-opsi/-/oci-opsi-2.108.0.tgz", + "integrity": "sha512-AhO4cME5h4dMsZSXlWKU4fAjf+G6KOSDC/q1/MKzsuFOVIw8YaNWEldOai1k+z5VRC1EWmuneFvgnK4EVnaeMA==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-optimizer": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-optimizer/-/oci-optimizer-2.108.0.tgz", + "integrity": "sha512-FsmroTYeawQAiEraHHt3EG0WiMwjfjYCpSbhl0Rh4kBMEG27BbW6SBzE4J/Aam2ArJLM8NSdakDwAt3y8oJZdA==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-osmanagement": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-osmanagement/-/oci-osmanagement-2.108.0.tgz", + "integrity": "sha512-FJsODD7muZCnpgGAL3t6rBvbg+cJRvcrjtVkPx147tED2wI1Hxrc/bXAhanC375rn4k8vFvrFjNSKCfF1VCG7A==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-osmanagementhub": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-osmanagementhub/-/oci-osmanagementhub-2.108.0.tgz", + "integrity": "sha512-u4yPdLxYGoSGMrI3jE5N0ruhH719Sm/Ga6uhONhufGew3g/fszaTQfjvDML9kSo+BtReiaua6gfAidi24dTfVQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-ospgateway": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-ospgateway/-/oci-ospgateway-2.108.0.tgz", + "integrity": "sha512-2DGQ903/wtQRvNEwScYTpr+pjcVmumAQw+ihWBV3WQ5bFCZNaFEfSyEu7wvFYxRMI4WvfwjivUmVUmSbTIVMzg==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-osubbillingschedule": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-osubbillingschedule/-/oci-osubbillingschedule-2.108.0.tgz", + "integrity": "sha512-2iN2hoYUyR9yg/RUR7VnKVmW+N0HHBuO8cVXmgjdoybzS9rDLvAmy/JsoHS0tMt/a4ExAIi9iC228Fwk4x3Sgg==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-osuborganizationsubscription": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-osuborganizationsubscription/-/oci-osuborganizationsubscription-2.108.0.tgz", + "integrity": "sha512-58WtIRE8+jK6V1Lzt9cToiv2wr4XIX41P3M6ab/vljusW5bJ+trNRrJXugcq6BE3+s9ysebdVY9mLe3O14qNLQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-osubsubscription": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-osubsubscription/-/oci-osubsubscription-2.108.0.tgz", + "integrity": "sha512-cCSjWrJVsOTkt2IhokdUkrx95VxLh4HuK51EAfo7ITLuOnMkScoDSFgTB48+Ktx3qjEMG9fNrM9lRxtktTpe3A==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-osubusage": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-osubusage/-/oci-osubusage-2.108.0.tgz", + "integrity": "sha512-bICbOu3MbKRnhV8iFLOHVhs31bQlmZS8cO8qWCFQgci7dt401mrxj+MFO0Yzq/Hk5unpbuFonLjlHsiD456ZDg==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-psql": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-psql/-/oci-psql-2.108.0.tgz", + "integrity": "sha512-w3ruZcKn++JnnYiPu6gmnSCjD/NQ1SGCJUIGDcc+O2qI01tfVXa3BI/c2AY7Y5Z+DbRD/efO0hSZt/9mtkS5IQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-queue": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-queue/-/oci-queue-2.108.0.tgz", + "integrity": "sha512-G3VUM2a9X1Gu0KnhYsCQlbH/3kHvbMsOH1IbA/XAJPAmpWp4JcXhFzWzW03/aymkoqoA1Vt3p77u9ltmrhgG7g==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-recovery": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-recovery/-/oci-recovery-2.108.0.tgz", + "integrity": "sha512-6OSclD5wagdrJGZtRvJbE1FJq8wl9igGevDMrd1o5rPJoGd/pCmZBn9bvfLPL1Mw9h0YGSAKZhAeMd3pDCNaIQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-redis": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-redis/-/oci-redis-2.108.0.tgz", + "integrity": "sha512-MzCORmjESnRs9BkOc3gnrbNaNOwWgZI+tgK2xTVY65PIy4PyYq5qvwgO3wmnLc4sULI4nIoWpoMSDxYyly7Zgg==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-resourcemanager": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-resourcemanager/-/oci-resourcemanager-2.108.0.tgz", + "integrity": "sha512-IBfQL1K7YaDyvO1UUM2277aH7gejRv6bcAD6G2kv7D/0PamavlDf1PJ1oGv8kQrPCjoALiBNNohelTEltywNOA==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-resourcescheduler": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-resourcescheduler/-/oci-resourcescheduler-2.108.0.tgz", + "integrity": "sha512-DI2w49VFfzo1y3y4uaqa5sFqqlpZlyQm6qpuhIaCjjDyrvZpbLLAC8Q9dFKq15PpzA7vlKQPPnBFeoW6YAX4pQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-resourcesearch": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-resourcesearch/-/oci-resourcesearch-2.108.0.tgz", + "integrity": "sha512-9HCm5fVmZf9ANW02YL3UYn1xGy5b2WMkABmVjosAs2rsMbRPieoOuu0zKK+d2YG2cXXHc1cGfFALQV9rfRSI2w==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-rover": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-rover/-/oci-rover-2.108.0.tgz", + "integrity": "sha512-uy3oNTMQDaLLy7EUoOljcXsnSKshQof9ESajQOKq4+EXNiFQ9fa5PNTTVl6g10+GS5Mak4KVLTIU7UnMYqwX9A==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-sch": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-sch/-/oci-sch-2.108.0.tgz", + "integrity": "sha512-4W+LA2lXN/rKoj8ZRu5HMzFq0De/VLTtUTVY37srgYBs0mp7z+crX4VenA9sIiytYzrY3cijxsDZOe6EBxnMqQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-sdk": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-sdk/-/oci-sdk-2.108.0.tgz", + "integrity": "sha512-wc5FXeAGUxBzTbRohdn7zD9328akY6CZ9qZoMzdXNe7dn65flxn1iO/clsw0zc3StRgy+0NGia1ZvsHIgkWzcg==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-accessgovernancecp": "2.108.0", + "oci-adm": "2.108.0", + "oci-aianomalydetection": "2.108.0", + "oci-aidocument": "2.108.0", + "oci-ailanguage": "2.108.0", + "oci-aispeech": "2.108.0", + "oci-aivision": "2.108.0", + "oci-analytics": "2.108.0", + "oci-announcementsservice": "2.108.0", + "oci-apigateway": "2.108.0", + "oci-apmconfig": "2.108.0", + "oci-apmcontrolplane": "2.108.0", + "oci-apmsynthetics": "2.108.0", + "oci-apmtraces": "2.108.0", + "oci-appmgmtcontrol": "2.108.0", + "oci-artifacts": "2.108.0", + "oci-audit": "2.108.0", + "oci-autoscaling": "2.108.0", + "oci-bastion": "2.108.0", + "oci-bds": "2.108.0", + "oci-blockchain": "2.108.0", + "oci-budget": "2.108.0", + "oci-capacitymanagement": "2.108.0", + "oci-certificates": "2.108.0", + "oci-certificatesmanagement": "2.108.0", + "oci-cims": "2.108.0", + "oci-cloudbridge": "2.108.0", + "oci-cloudguard": "2.108.0", + "oci-cloudmigrations": "2.108.0", + "oci-clusterplacementgroups": "2.108.0", + "oci-common": "2.108.0", + "oci-computecloudatcustomer": "2.108.0", + "oci-computeinstanceagent": "2.108.0", + "oci-containerengine": "2.108.0", + "oci-containerinstances": "2.108.0", + "oci-core": "2.108.0", + "oci-dashboardservice": "2.108.0", + "oci-database": "2.108.0", + "oci-databasemanagement": "2.108.0", + "oci-databasemigration": "2.108.0", + "oci-databasetools": "2.108.0", + "oci-datacatalog": "2.108.0", + "oci-dataflow": "2.108.0", + "oci-dataintegration": "2.108.0", + "oci-datalabelingservice": "2.108.0", + "oci-datalabelingservicedataplane": "2.108.0", + "oci-datasafe": "2.108.0", + "oci-datascience": "2.108.0", + "oci-dblm": "2.108.0", + "oci-delegateaccesscontrol": "2.108.0", + "oci-demandsignal": "2.108.0", + "oci-desktops": "2.108.0", + "oci-devops": "2.108.0", + "oci-disasterrecovery": "2.108.0", + "oci-dns": "2.108.0", + "oci-dts": "2.108.0", + "oci-email": "2.108.0", + "oci-emaildataplane": "2.108.0", + "oci-emwarehouse": "2.108.0", + "oci-events": "2.108.0", + "oci-filestorage": "2.108.0", + "oci-fleetappsmanagement": "2.108.0", + "oci-fleetsoftwareupdate": "2.108.0", + "oci-functions": "2.108.0", + "oci-fusionapps": "2.108.0", + "oci-generativeai": "2.108.0", + "oci-generativeaiagent": "2.108.0", + "oci-generativeaiagentruntime": "2.108.0", + "oci-generativeaiinference": "2.108.0", + "oci-genericartifactscontent": "2.108.0", + "oci-globallydistributeddatabase": "2.108.0", + "oci-goldengate": "2.108.0", + "oci-governancerulescontrolplane": "2.108.0", + "oci-healthchecks": "2.108.0", + "oci-identity": "2.108.0", + "oci-identitydataplane": "2.108.0", + "oci-identitydomains": "2.108.0", + "oci-integration": "2.108.0", + "oci-jms": "2.108.0", + "oci-jmsjavadownloads": "2.108.0", + "oci-keymanagement": "2.108.0", + "oci-licensemanager": "2.108.0", + "oci-limits": "2.108.0", + "oci-loadbalancer": "2.108.0", + "oci-lockbox": "2.108.0", + "oci-loganalytics": "2.108.0", + "oci-logging": "2.108.0", + "oci-loggingingestion": "2.108.0", + "oci-loggingsearch": "2.108.0", + "oci-lustrefilestorage": "2.108.0", + "oci-managementagent": "2.108.0", + "oci-managementdashboard": "2.108.0", + "oci-marketplace": "2.108.0", + "oci-marketplaceprivateoffer": "2.108.0", + "oci-marketplacepublisher": "2.108.0", + "oci-mediaservices": "2.108.0", + "oci-mngdmac": "2.108.0", + "oci-monitoring": "2.108.0", + "oci-mysql": "2.108.0", + "oci-networkfirewall": "2.108.0", + "oci-networkloadbalancer": "2.108.0", + "oci-nosql": "2.108.0", + "oci-objectstorage": "2.108.0", + "oci-oce": "2.108.0", + "oci-ocicontrolcenter": "2.108.0", + "oci-ocvp": "2.108.0", + "oci-oda": "2.108.0", + "oci-onesubscription": "2.108.0", + "oci-ons": "2.108.0", + "oci-opa": "2.108.0", + "oci-opensearch": "2.108.0", + "oci-operatoraccesscontrol": "2.108.0", + "oci-opsi": "2.108.0", + "oci-optimizer": "2.108.0", + "oci-osmanagement": "2.108.0", + "oci-osmanagementhub": "2.108.0", + "oci-ospgateway": "2.108.0", + "oci-osubbillingschedule": "2.108.0", + "oci-osuborganizationsubscription": "2.108.0", + "oci-osubsubscription": "2.108.0", + "oci-osubusage": "2.108.0", + "oci-psql": "2.108.0", + "oci-queue": "2.108.0", + "oci-recovery": "2.108.0", + "oci-redis": "2.108.0", + "oci-resourcemanager": "2.108.0", + "oci-resourcescheduler": "2.108.0", + "oci-resourcesearch": "2.108.0", + "oci-rover": "2.108.0", + "oci-sch": "2.108.0", + "oci-secrets": "2.108.0", + "oci-securityattribute": "2.108.0", + "oci-servicecatalog": "2.108.0", + "oci-servicemanagerproxy": "2.108.0", + "oci-servicemesh": "2.108.0", + "oci-stackmonitoring": "2.108.0", + "oci-streaming": "2.108.0", + "oci-tenantmanagercontrolplane": "2.108.0", + "oci-threatintelligence": "2.108.0", + "oci-usage": "2.108.0", + "oci-usageapi": "2.108.0", + "oci-vault": "2.108.0", + "oci-vbsinst": "2.108.0", + "oci-visualbuilder": "2.108.0", + "oci-vnmonitoring": "2.108.0", + "oci-vulnerabilityscanning": "2.108.0", + "oci-waa": "2.108.0", + "oci-waas": "2.108.0", + "oci-waf": "2.108.0", + "oci-workrequests": "2.108.0", + "oci-zpr": "2.108.0" + } + }, + "node_modules/oci-secrets": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-secrets/-/oci-secrets-2.108.0.tgz", + "integrity": "sha512-GFFCuaKnS8pX7mE4mvZn/3m+rlksbheRNBg0e3dADAE9/G8hcRDabfUcp8ee0I2IOGlfmPx1MqqVxGdOB5qePA==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-securityattribute": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-securityattribute/-/oci-securityattribute-2.108.0.tgz", + "integrity": "sha512-5q7X2iTIFONcQZLMMyuSPEGwv+/H1zp6+A8pizNEKnt/Ky1Y6J7mVt8rIfjkmW2adjCMbJvjOd/FEx1qpPMSdA==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-servicecatalog": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-servicecatalog/-/oci-servicecatalog-2.108.0.tgz", + "integrity": "sha512-wawMy6pyaaLGb//qDSRZY3RDlBAdcgiH5rT8HWIvjpty5/LUfAFEoc6GT+hXESJJnTgKPv3jVRsauKGaYY0ThQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-servicemanagerproxy": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-servicemanagerproxy/-/oci-servicemanagerproxy-2.108.0.tgz", + "integrity": "sha512-ze38V56A7Lj2bmu0zrJJP/p0zJXawdUZO4vzVNKTRCMuCHA/bpNgxhsqrZftrlh/hJHIvzTUorNqKG6db4rvpw==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-servicemesh": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-servicemesh/-/oci-servicemesh-2.108.0.tgz", + "integrity": "sha512-PKCePlf3UBtmXXqkCLQb3ckhYcMPwUjigKQHJXLomqRsN/WWS4cjaXFfwPms1LHYiljFaUBBpYPEXXuNBIAxrw==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-stackmonitoring": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-stackmonitoring/-/oci-stackmonitoring-2.108.0.tgz", + "integrity": "sha512-MnWwot6txJhUFjmToZLg/MqxOy9oUcuosOv1ndRt1KJzgHlVqNDKhlYSzsY3M1I/luLKl56MmGUXEmPByTVNsA==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-streaming": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-streaming/-/oci-streaming-2.108.0.tgz", + "integrity": "sha512-EJflloCRvhKpmbMWLtDKgYQDiinZgIyyAArl0YChUuLXYs7ntXNdK7vkiz/xYtez1j06JSGVDlhoJGnWHnLLOA==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-tenantmanagercontrolplane": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-tenantmanagercontrolplane/-/oci-tenantmanagercontrolplane-2.108.0.tgz", + "integrity": "sha512-qOrkZhRI54+dncswCWrzgYNUhC1v/RVrBm/3M34RrZP3XIUYk42FbWEGmAUIf8CsKqAnMihHGZhYmKLF+CnQEQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-threatintelligence": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-threatintelligence/-/oci-threatintelligence-2.108.0.tgz", + "integrity": "sha512-xU9XRZRfTrXN4+UzsPwQbZXmwyU9IY5CpXvsAO/PjdIZJGwl69CGlxSbbLk0ye9qC+0zdADN8fspHp7u++jJ3w==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-usage": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-usage/-/oci-usage-2.108.0.tgz", + "integrity": "sha512-qjP75B0BchoIHc2VCQF8Yehx69/2F/M0UT74GcGjiNewkjxlJwiDCegj+IeVHZ91OCmb1Dqva3u2zUfwxbaqIw==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-usageapi": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-usageapi/-/oci-usageapi-2.108.0.tgz", + "integrity": "sha512-hKcssMA1aHia+EM1rD+tGt/njsUn3nYmHJKMzywLHKYnmsbtVVVli8bKlELBFkLVjkIyKPY7XJQfCRr5TPQBHw==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-vault": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-vault/-/oci-vault-2.108.0.tgz", + "integrity": "sha512-wDq/hibUkif9rYJOhkY6/D9RXhSCsMuaKQeR0WaO6MdYhe7zbo0uXlADn8nPHWJ257CUBoDcpPMWP+mlWyVq9g==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-vbsinst": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-vbsinst/-/oci-vbsinst-2.108.0.tgz", + "integrity": "sha512-ZH6igsrlPrkC6DS9g6c7F6nSAb6/s7NuT11ENc/i2zG2DtsZBOTkr5l8+/mG5AvzMocKRd7NrcHjR4IMxrnhMQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-visualbuilder": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-visualbuilder/-/oci-visualbuilder-2.108.0.tgz", + "integrity": "sha512-G2oISBuIwvzl6sJV4KwbZX5G5GwkDmKG4JX2jxp/WNTtawsUz/OfpbpPyx+y5raWCUh3Uji5vWMlDVXfhzzzhg==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-vnmonitoring": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-vnmonitoring/-/oci-vnmonitoring-2.108.0.tgz", + "integrity": "sha512-8oVv+nQddteOdUiqDZGxBJgwkib1NUt6WifsaP3Y+GJEzV42vNHxvjpmJbMuR7TTW4cjBV8mQS2TbTQmPhX/JQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-vulnerabilityscanning": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-vulnerabilityscanning/-/oci-vulnerabilityscanning-2.108.0.tgz", + "integrity": "sha512-duvDY4zrDXWdRWWyBGLpQSvfpjYHfNJHyxxMacHu5l0sZM3iKMjx56288PYSTanicknSJ7dKeh/2R5XlOGO35w==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-waa": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-waa/-/oci-waa-2.108.0.tgz", + "integrity": "sha512-k0yhzlWvM6ry7/eScX/nIB98q6s+yuMP/GUagja/U1AbVGUjLHBjbhi8hbjqtpfuRaUN3luxMS8nrs0t6ZQesQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-waas": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-waas/-/oci-waas-2.108.0.tgz", + "integrity": "sha512-KtjN2JZ7tN5rAZr5viJQs24xRGBWrjP7ZmeHPkdGW+96rHqExlDNfJAcyk0hhJNECSu/g400aPlQ9djfELXPMA==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-waf": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-waf/-/oci-waf-2.108.0.tgz", + "integrity": "sha512-delccqk+FkW2l9e0Bf9SzD6VYojctk8mJx6IasxGl/w3Dc3C9HoBi2l4tfz2sBgBExDDWAMwxUa3ygCX2N3r8Q==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-workrequests": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-workrequests/-/oci-workrequests-2.108.0.tgz", + "integrity": "sha512-vwIM+cEDZ2BhKX+bOH2POTMTnfFnfjs0QxprHb1F05wu5/a3ea35oCkcchyqa1uHh75tqnO/dGDlp6st6+IeIQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, + "node_modules/oci-zpr": { + "version": "2.108.0", + "resolved": "https://registry.npmjs.org/oci-zpr/-/oci-zpr-2.108.0.tgz", + "integrity": "sha512-Ktxh08Mozp4LJ5ADuDpfnaMZBSJQ04xSvjCod/kf2G3WTQ7OUctvppY1S7X2kCPnEfNORevkE/LPDDpCnZpGgQ==", + "license": "(UPL-1.0 OR Apache-2.0)", + "dependencies": { + "oci-common": "2.108.0", + "oci-workrequests": "2.108.0" + } + }, "node_modules/octokit-auth-probot": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/octokit-auth-probot/-/octokit-auth-probot-2.0.0.tgz", @@ -20275,6 +22164,15 @@ "node": ">=0.10" } }, + "node_modules/opossum": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/opossum/-/opossum-5.0.1.tgz", + "integrity": "sha512-iUDUQmFl3RanaBVLMDTZ6WtXj/Hk84pwJ5JWoJaQd1lXGifdApHhszI3biZvdBDdpTERCmB6x+7+uNvzhzVZIg==", + "license": "Apache-2.0", + "engines": { + "node": ">= 10" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -24108,6 +26006,43 @@ "node": ">= 0.6" } }, + "node_modules/sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sshpk/node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "license": "MIT" + }, + "node_modules/sshpk/node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, "node_modules/ssri": { "version": "10.0.6", "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", @@ -26901,6 +28836,12 @@ "node": ">=18" } }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, "node_modules/whatwg-mimetype": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index a8ab76cf8..30aa9f68c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -38,8 +38,8 @@ "build:frontend": "npm run build --prefix ../frontend", "start": "node --enable-source-maps dist/main.mjs", "type:check": "tsc --noEmit", - "lint:fix": "eslint --fix --ext js,ts ./src", - "lint": "eslint 'src/**/*.ts'", + "lint:fix": "node --max-old-space-size=8192 ./node_modules/.bin/eslint --fix --ext js,ts ./src", + "lint": "node --max-old-space-size=8192 ./node_modules/.bin/eslint 'src/**/*.ts'", "test:unit": "vitest run -c vitest.unit.config.ts", "test:e2e": "vitest run -c vitest.e2e.config.ts --bail=1", "test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1", @@ -209,6 +209,7 @@ "mysql2": "^3.9.8", "nanoid": "^3.3.8", "nodemailer": "^6.9.9", + "oci-sdk": "^2.108.0", "odbc": "^2.4.9", "openid-client": "^5.6.5", "ora": "^7.0.1", diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 7070e07a6..b098368c4 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -81,6 +81,7 @@ import { TOrgServiceFactory } from "@app/services/org/org-service"; import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service"; import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service"; import { TPkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service"; +import { TPkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service"; import { TProjectServiceFactory } from "@app/services/project/project-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service"; @@ -234,6 +235,7 @@ declare module "fastify" { certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory; certificateEst: TCertificateEstServiceFactory; pkiCollection: TPkiCollectionServiceFactory; + pkiSubscriber: TPkiSubscriberServiceFactory; secretScanning: TSecretScanningServiceFactory; license: TLicenseServiceFactory; trustedIp: TTrustedIpServiceFactory; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 8161fb426..276669b2a 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -212,6 +212,9 @@ import { TPkiCollections, TPkiCollectionsInsert, TPkiCollectionsUpdate, + TPkiSubscribers, + TPkiSubscribersInsert, + TPkiSubscribersUpdate, TProjectBots, TProjectBotsInsert, TProjectBotsUpdate, @@ -567,6 +570,11 @@ declare module "knex/types/tables" { TPkiCollectionItemsInsert, TPkiCollectionItemsUpdate >; + [TableName.PkiSubscriber]: KnexOriginal.CompositeTableType< + TPkiSubscribers, + TPkiSubscribersInsert, + TPkiSubscribersUpdate + >; [TableName.UserGroupMembership]: KnexOriginal.CompositeTableType< TUserGroupMembership, TUserGroupMembershipInsert, diff --git a/backend/src/db/migrations/20250508160957_pki-subscriber.ts b/backend/src/db/migrations/20250508160957_pki-subscriber.ts new file mode 100644 index 000000000..0e1b50f03 --- /dev/null +++ b/backend/src/db/migrations/20250508160957_pki-subscriber.ts @@ -0,0 +1,46 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.PkiSubscriber))) { + await knex.schema.createTable(TableName.PkiSubscriber, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + t.string("projectId").notNullable(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.uuid("caId").nullable(); + t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("SET NULL"); + t.string("name").notNullable(); + t.string("commonName").notNullable(); + t.specificType("subjectAlternativeNames", "text[]").notNullable(); + t.string("ttl").notNullable(); + t.specificType("keyUsages", "text[]").notNullable(); + t.specificType("extendedKeyUsages", "text[]").notNullable(); + t.string("status").notNullable(); // active / disabled + t.unique(["projectId", "name"]); + }); + await createOnUpdateTrigger(knex, TableName.PkiSubscriber); + } + + const hasSubscriberCol = await knex.schema.hasColumn(TableName.Certificate, "pkiSubscriberId"); + if (!hasSubscriberCol) { + await knex.schema.alterTable(TableName.Certificate, (t) => { + t.uuid("pkiSubscriberId").nullable(); + t.foreign("pkiSubscriberId").references("id").inTable(TableName.PkiSubscriber).onDelete("SET NULL"); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasSubscriberCol = await knex.schema.hasColumn(TableName.Certificate, "pkiSubscriberId"); + if (hasSubscriberCol) { + await knex.schema.alterTable(TableName.Certificate, (t) => { + t.dropColumn("pkiSubscriberId"); + }); + } + + await knex.schema.dropTableIfExists(TableName.PkiSubscriber); + await dropOnUpdateTrigger(knex, TableName.PkiSubscriber); +} diff --git a/backend/src/db/schemas/certificates.ts b/backend/src/db/schemas/certificates.ts index 533f9b898..cbd4f64f9 100644 --- a/backend/src/db/schemas/certificates.ts +++ b/backend/src/db/schemas/certificates.ts @@ -24,7 +24,8 @@ export const CertificatesSchema = z.object({ caCertId: z.string().uuid(), certificateTemplateId: z.string().uuid().nullable().optional(), keyUsages: z.string().array().nullable().optional(), - extendedKeyUsages: z.string().array().nullable().optional() + extendedKeyUsages: z.string().array().nullable().optional(), + pkiSubscriberId: z.string().uuid().nullable().optional() }); export type TCertificates = z.infer; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index b4b6abdd5..0bf44c413 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -70,6 +70,7 @@ export * from "./organizations"; export * from "./pki-alerts"; export * from "./pki-collection-items"; export * from "./pki-collections"; +export * from "./pki-subscribers"; export * from "./project-bots"; export * from "./project-environments"; export * from "./project-gateways"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index d598b254f..730474ad1 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -21,6 +21,7 @@ export enum TableName { CertificateBody = "certificate_bodies", CertificateSecret = "certificate_secrets", CertificateTemplate = "certificate_templates", + PkiSubscriber = "pki_subscribers", PkiAlert = "pki_alerts", PkiCollection = "pki_collections", PkiCollectionItem = "pki_collection_items", diff --git a/backend/src/db/schemas/pki-subscribers.ts b/backend/src/db/schemas/pki-subscribers.ts new file mode 100644 index 000000000..08db19806 --- /dev/null +++ b/backend/src/db/schemas/pki-subscribers.ts @@ -0,0 +1,27 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const PkiSubscribersSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + projectId: z.string(), + caId: z.string().uuid().nullable().optional(), + name: z.string(), + commonName: z.string(), + subjectAlternativeNames: z.string().array(), + ttl: z.string(), + keyUsages: z.string().array(), + extendedKeyUsages: z.string().array(), + status: z.string() +}); + +export type TPkiSubscribers = z.infer; +export type TPkiSubscribersInsert = Omit, TImmutableDBKeys>; +export type TPkiSubscribersUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/ee/routes/v1/ssh-host-router.ts b/backend/src/ee/routes/v1/ssh-host-router.ts index 93748c27f..4c749f6f5 100644 --- a/backend/src/ee/routes/v1/ssh-host-router.ts +++ b/backend/src/ee/routes/v1/ssh-host-router.ts @@ -73,7 +73,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const host = await server.services.sshHost.getSshHost({ + const host = await server.services.sshHost.getSshHostById({ sshHostId: req.params.sshHostId, actor: req.permission.type, actorId: req.permission.id, diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 65021a253..365ada987 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -19,7 +19,7 @@ import { TProjectPermission } from "@app/lib/types"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types"; import { ActorType } from "@app/services/auth/auth-type"; -import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; +import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types"; @@ -260,6 +260,13 @@ export enum EventType { GET_PKI_COLLECTION_ITEMS = "get-pki-collection-items", ADD_PKI_COLLECTION_ITEM = "add-pki-collection-item", DELETE_PKI_COLLECTION_ITEM = "delete-pki-collection-item", + CREATE_PKI_SUBSCRIBER = "create-pki-subscriber", + UPDATE_PKI_SUBSCRIBER = "update-pki-subscriber", + DELETE_PKI_SUBSCRIBER = "delete-pki-subscriber", + GET_PKI_SUBSCRIBER = "get-pki-subscriber", + ISSUE_PKI_SUBSCRIBER_CERT = "issue-pki-subscriber-cert", + SIGN_PKI_SUBSCRIBER_CERT = "sign-pki-subscriber-cert", + LIST_PKI_SUBSCRIBER_CERTS = "list-pki-subscriber-certs", CREATE_KMS = "create-kms", UPDATE_KMS = "update-kms", DELETE_KMS = "delete-kms", @@ -2020,6 +2027,77 @@ interface DeletePkiCollectionItem { }; } +interface CreatePkiSubscriber { + type: EventType.CREATE_PKI_SUBSCRIBER; + metadata: { + pkiSubscriberId: string; + caId?: string; + name: string; + commonName: string; + ttl: string; + subjectAlternativeNames: string[]; + keyUsages: CertKeyUsage[]; + extendedKeyUsages: CertExtendedKeyUsage[]; + }; +} + +interface UpdatePkiSubscriber { + type: EventType.UPDATE_PKI_SUBSCRIBER; + metadata: { + pkiSubscriberId: string; + caId?: string; + name?: string; + commonName?: string; + ttl?: string; + subjectAlternativeNames?: string[]; + keyUsages?: CertKeyUsage[]; + extendedKeyUsages?: CertExtendedKeyUsage[]; + }; +} + +interface DeletePkiSubscriber { + type: EventType.DELETE_PKI_SUBSCRIBER; + metadata: { + pkiSubscriberId: string; + name: string; + }; +} + +interface GetPkiSubscriber { + type: EventType.GET_PKI_SUBSCRIBER; + metadata: { + pkiSubscriberId: string; + name: string; + }; +} + +interface IssuePkiSubscriberCert { + type: EventType.ISSUE_PKI_SUBSCRIBER_CERT; + metadata: { + subscriberId: string; + name: string; + serialNumber: string; + }; +} + +interface SignPkiSubscriberCert { + type: EventType.SIGN_PKI_SUBSCRIBER_CERT; + metadata: { + subscriberId: string; + name: string; + serialNumber: string; + }; +} + +interface ListPkiSubscriberCerts { + type: EventType.LIST_PKI_SUBSCRIBER_CERTS; + metadata: { + subscriberId: string; + name: string; + projectId: string; + }; +} + interface CreateKmsEvent { type: EventType.CREATE_KMS; metadata: { @@ -2988,6 +3066,13 @@ export type Event = | GetPkiCollectionItems | AddPkiCollectionItem | DeletePkiCollectionItem + | CreatePkiSubscriber + | UpdatePkiSubscriber + | DeletePkiSubscriber + | GetPkiSubscriber + | IssuePkiSubscriberCert + | SignPkiSubscriberCert + | ListPkiSubscriberCerts | CreateKmsEvent | UpdateKmsEvent | DeleteKmsEvent diff --git a/backend/src/ee/services/permission/default-roles.ts b/backend/src/ee/services/permission/default-roles.ts index 44fcb7825..a018ffe81 100644 --- a/backend/src/ee/services/permission/default-roles.ts +++ b/backend/src/ee/services/permission/default-roles.ts @@ -9,6 +9,7 @@ import { ProjectPermissionIdentityActions, ProjectPermissionKmipActions, ProjectPermissionMemberActions, + ProjectPermissionPkiSubscriberActions, ProjectPermissionSecretActions, ProjectPermissionSecretRotationActions, ProjectPermissionSecretSyncActions, @@ -76,6 +77,18 @@ const buildAdminPermissionRules = () => { ProjectPermissionSub.SshHosts ); + can( + [ + ProjectPermissionPkiSubscriberActions.Edit, + ProjectPermissionPkiSubscriberActions.Read, + ProjectPermissionPkiSubscriberActions.Create, + ProjectPermissionPkiSubscriberActions.Delete, + ProjectPermissionPkiSubscriberActions.IssueCert, + ProjectPermissionPkiSubscriberActions.ListCerts + ], + ProjectPermissionSub.PkiSubscribers + ); + can( [ ProjectPermissionMemberActions.Create, @@ -113,7 +126,6 @@ const buildAdminPermissionRules = () => { can( [ - ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue, ProjectPermissionSecretActions.Create, @@ -194,7 +206,6 @@ const buildMemberPermissionRules = () => { can( [ - ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue, ProjectPermissionSecretActions.Edit, @@ -338,6 +349,7 @@ const buildMemberPermissionRules = () => { can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates); can([ProjectPermissionSshHostActions.Read], ProjectPermissionSub.SshHosts); + can([ProjectPermissionPkiSubscriberActions.Read], ProjectPermissionSub.PkiSubscribers); can( [ @@ -372,9 +384,10 @@ const buildMemberPermissionRules = () => { const buildViewerPermissionRules = () => { const { can, rules } = new AbilityBuilder>(createMongoAbility); - can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets); - can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets); - can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets); + can( + [ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue], + ProjectPermissionSub.Secrets + ); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 7463c35f6..5474facf6 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -87,6 +87,15 @@ export enum ProjectPermissionSshHostActions { IssueHostCert = "issue-host-cert" } +export enum ProjectPermissionPkiSubscriberActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + IssueCert = "issue-cert", + ListCerts = "list-certs" +} + export enum ProjectPermissionSecretSyncActions { Read = "read", Create = "create", @@ -143,6 +152,7 @@ export enum ProjectPermissionSub { SshCertificateTemplates = "ssh-certificate-templates", SshHosts = "ssh-hosts", SshHostGroups = "ssh-host-groups", + PkiSubscribers = "pki-subscribers", PkiAlerts = "pki-alerts", PkiCollections = "pki-collections", Kms = "kms", @@ -190,6 +200,11 @@ export type SshHostSubjectFields = { hostname: string; }; +export type PkiSubscriberSubjectFields = { + name: string; + // (dangtony98): consider adding [commonName] as a subject field in the future +}; + export type ProjectPermissionSet = | [ ProjectPermissionSecretActions, @@ -249,6 +264,13 @@ export type ProjectPermissionSet = ProjectPermissionSshHostActions, ProjectPermissionSub.SshHosts | (ForcedSubject & SshHostSubjectFields) ] + | [ + ProjectPermissionPkiSubscriberActions, + ( + | ProjectPermissionSub.PkiSubscribers + | (ForcedSubject & PkiSubscriberSubjectFields) + ) + ] | [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] @@ -399,6 +421,21 @@ const SshHostConditionSchema = z }) .partial(); +const PkiSubscriberConditionSchema = z + .object({ + name: z.union([ + z.string(), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN] + }) + .partial() + ]) + }) + .partial(); + const GeneralPermissionSchema = [ z.object({ subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."), @@ -663,6 +700,16 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [ "When specified, only matching conditions will be allowed to access given resource." ).optional() }), + z.object({ + subject: z.literal(ProjectPermissionSub.PkiSubscribers).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionPkiSubscriberActions).describe( + "Describe what action an entity can take." + ), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), + conditions: PkiSubscriberConditionSchema.describe( + "When specified, only matching conditions will be allowed to access given resource." + ).optional() + }), z.object({ subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."), inverted: z.boolean().optional().describe("Whether rule allows or forbids."), diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts index be4a7ab3b..3877cbaf8 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts @@ -334,7 +334,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { db.ref("secretId").withSchema(TableName.SecretApprovalRequestSecret).as("commitSecretId"), db.ref("id").withSchema(TableName.SecretApprovalRequestSecret).as("commitId"), db.raw( - `DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank` + `DENSE_RANK() OVER (PARTITION BY ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."createdAt" DESC) as rank` ), db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"), db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"), @@ -483,7 +483,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { db.ref("secretId").withSchema(TableName.SecretApprovalRequestSecretV2).as("commitSecretId"), db.ref("id").withSchema(TableName.SecretApprovalRequestSecretV2).as("commitId"), db.raw( - `DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank` + `DENSE_RANK() OVER (PARTITION BY ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."createdAt" DESC) as rank` ), db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"), db.ref("allowedSelfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policyAllowedSelfApprovals"), diff --git a/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts b/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts index 1eacc7602..1137660d6 100644 --- a/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts +++ b/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts @@ -186,13 +186,42 @@ export const sshHostGroupServiceFactory = ({ }); const updatedSshHostGroup = await sshHostGroupDAL.transaction(async (tx) => { - await sshHostGroupDAL.updateById( - sshHostGroupId, - { - name - }, - tx - ); + if (name && name !== sshHostGroup.name) { + // (dangtony98): room to optimize check to ensure that + // the SSH host group name is unique across the whole org + const project = await projectDAL.findById(sshHostGroup.projectId, tx); + if (!project) throw new NotFoundError({ message: `Project with ID '${sshHostGroup.projectId}' not found` }); + const projects = await projectDAL.find( + { + orgId: project.orgId + }, + { tx } + ); + + const existingSshHostGroup = await sshHostGroupDAL.find( + { + name, + $in: { + projectId: projects.map((p) => p.id) + } + }, + { tx } + ); + + if (existingSshHostGroup.length) { + throw new BadRequestError({ + message: `SSH host group with name '${name}' already exists in the organization` + }); + } + await sshHostGroupDAL.updateById( + sshHostGroupId, + { + name + }, + tx + ); + } + if (loginMappings) { await sshHostLoginUserDAL.delete({ sshHostGroupId: sshHostGroup.id }, tx); if (loginMappings.length) { diff --git a/backend/src/ee/services/ssh-host/ssh-host-service.ts b/backend/src/ee/services/ssh-host/ssh-host-service.ts index 79c74cd57..e41a8b403 100644 --- a/backend/src/ee/services/ssh-host/ssh-host-service.ts +++ b/backend/src/ee/services/ssh-host/ssh-host-service.ts @@ -335,7 +335,7 @@ export const sshHostServiceFactory = ({ return host; }; - const getSshHost = async ({ sshHostId, actorId, actorAuthMethod, actor, actorOrgId }: TGetSshHostDTO) => { + const getSshHostById = async ({ sshHostId, actorId, actorAuthMethod, actor, actorOrgId }: TGetSshHostDTO) => { const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId); if (!host) { throw new NotFoundError({ @@ -631,7 +631,7 @@ export const sshHostServiceFactory = ({ createSshHost, updateSshHost, deleteSshHost, - getSshHost, + getSshHostById, issueSshHostUserCert, issueSshHostHostCert, getSshHostUserCaPk, diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index b7ff888a5..cabc8e1db 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -47,6 +47,7 @@ export enum ApiDocsTags { PkiCertificateTemplates = "PKI Certificate Templates", PkiCertificateCollections = "PKI Certificate Collections", PkiAlerting = "PKI Alerting", + PkiSubscribers = "PKI Subscribers", SshCertificates = "SSH Certificates", SshCertificateAuthorities = "SSH Certificate Authorities", SshCertificateTemplates = "SSH Certificate Templates", @@ -674,6 +675,9 @@ export const PROJECTS = { commonName: "The common name of the certificate to filter by.", offset: "The offset to start from. If you enter 10, it will start from the 10th certificate.", limit: "The number of certificates to return." + }, + LIST_PKI_SUBSCRIBERS: { + projectId: "The ID of the project to list PKI subscribers for." } } as const; @@ -1766,6 +1770,67 @@ export const ALERTS = { } }; +export const PKI_SUBSCRIBERS = { + GET: { + subscriberName: "The name of the PKI subscriber to get.", + projectId: "The ID of the project to get the PKI subscriber for." + }, + CREATE: { + projectId: "The ID of the project to create the PKI subscriber in.", + caId: "The ID of the CA that will issue certificates for the PKI subscriber.", + name: "The name of the PKI subscriber.", + commonName: "The common name (CN) to be used on certificates issued for this subscriber.", + status: "The status of the PKI subscriber. This can be one of active or disabled.", + ttl: "The time to live for the certificates issued for this subscriber such as 1m, 1h, 1d, 1y, ...", + subjectAlternativeNames: + "A list of Subject Alternative Names (SANs) to be used on certificates issued for this subscriber; these can be host names or email addresses.", + keyUsages: "The key usage extension to be used on certificates issued for this subscriber.", + extendedKeyUsages: "The extended key usage extension to be used on certificates issued for this subscriber." + }, + UPDATE: { + projectId: "The ID of the project to update the PKI subscriber in.", + subscriberName: "The name of the PKI subscriber to update.", + caId: "The ID of the CA that will issue certificates for the PKI subscriber to update to.", + name: "The name of the PKI subscriber to update to.", + commonName: "The common name (CN) to be used on certificates issued for this subscriber to update to.", + status: "The status of the PKI subscriber to update to. This can be one of active or disabled.", + ttl: "The time to live for the certificates issued for this subscriber such as 1m, 1h, 1d, 1y, ...", + subjectAlternativeNames: + "A comma-delimited list of Subject Alternative Names (SANs) to be used on certificates issued for this subscriber; these can be host names or email addresses.", + keyUsages: "The key usage extension to be used on certificates issued for this subscriber to update to.", + extendedKeyUsages: + "The extended key usage extension to be used on certificates issued for this subscriber to update to." + }, + DELETE: { + subscriberName: "The name of the PKI subscriber to delete.", + projectId: "The ID of the project of the PKI subscriber to delete." + }, + ISSUE_CERT: { + subscriberName: "The name of the PKI subscriber to issue the certificate for.", + projectId: "The ID of the project of the PKI subscriber to issue the certificate for.", + certificate: "The issued certificate.", + issuingCaCertificate: "The certificate of the issuing CA.", + certificateChain: "The certificate chain of the issued certificate.", + privateKey: "The private key of the issued certificate.", + serialNumber: "The serial number of the issued certificate." + }, + SIGN_CERT: { + subscriberName: "The name of the PKI subscriber to sign the certificate for.", + projectId: "The ID of the project of the PKI subscriber to sign the certificate for.", + csr: "The CSR to be used to sign the certificate.", + certificate: "The signed certificate.", + issuingCaCertificate: "The certificate of the issuing CA.", + certificateChain: "The certificate chain of the signed certificate.", + serialNumber: "The serial number of the signed certificate." + }, + LIST_CERTS: { + subscriberName: "The name of the PKI subscriber to list the certificates for.", + projectId: "The ID of the project of the PKI subscriber to list the certificates for.", + offset: "The offset to start from.", + limit: "The number of certificates to return." + } +}; + export const PKI_COLLECTIONS = { CREATE: { projectId: "The ID of the project to create the PKI collection in.", @@ -2009,6 +2074,13 @@ export const AppConnections = { AZURE_CLIENT_SECRETS: { code: "The OAuth code to use to connect with Azure Client Secrets.", tenantId: "The Tenant ID to use to connect with Azure Client Secrets." + }, + OCI: { + userOcid: "The OCID (Oracle Cloud Identifier) of the user making the request.", + tenancyOcid: "The OCID (Oracle Cloud Identifier) of the tenancy in Oracle Cloud Infrastructure.", + region: "The region identifier in Oracle Cloud Infrastructure where the vault is located.", + fingerprint: "The fingerprint of the public key uploaded to the user's API keys.", + privateKey: "The private key content in PEM format used to sign API requests." } } }; @@ -2156,6 +2228,11 @@ export const SecretSyncs = { TEAMCITY: { project: "The TeamCity project to sync secrets to.", buildConfig: "The TeamCity build configuration to sync secrets to." + }, + OCI_VAULT: { + compartmentOcid: "The OCID (Oracle Cloud Identifier) of the compartment where the vault is located.", + vaultOcid: "The OCID (Oracle Cloud Identifier) of the vault to sync secrets to.", + keyOcid: "The OCID (Oracle Cloud Identifier) of the encryption key to use when creating secrets in the vault." } } }; diff --git a/backend/src/server/plugins/serve-ui.ts b/backend/src/server/plugins/serve-ui.ts index 9f91d9774..22c097726 100644 --- a/backend/src/server/plugins/serve-ui.ts +++ b/backend/src/server/plugins/serve-ui.ts @@ -57,7 +57,9 @@ export const registerServeUI = async ( reply.callNotFound(); return; } - return reply.sendFile("index.html"); + // reference: https://github.com/fastify/fastify-static?tab=readme-ov-file#managing-cache-control-headers + // to avoid ui bundle skew on new deployment + return reply.sendFile("index.html", { maxAge: 0, immutable: false }); } }); } diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 89f92fd92..b05b253d6 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -199,6 +199,8 @@ import { pkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-servic import { pkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal"; import { pkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal"; import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service"; +import { pkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; +import { pkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service"; import { projectDALFactory } from "@app/services/project/project-dal"; import { projectQueueFactory } from "@app/services/project/project-queue"; import { projectServiceFactory } from "@app/services/project/project-service"; @@ -831,6 +833,7 @@ export const registerRoutes = async ( const pkiAlertDAL = pkiAlertDALFactory(db); const pkiCollectionDAL = pkiCollectionDALFactory(db); const pkiCollectionItemDAL = pkiCollectionItemDALFactory(db); + const pkiSubscriberDAL = pkiSubscriberDALFactory(db); const certificateService = certificateServiceFactory({ certificateDAL, @@ -965,6 +968,20 @@ export const registerRoutes = async ( projectDAL }); + const pkiSubscriberService = pkiSubscriberServiceFactory({ + pkiSubscriberDAL, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + certificateAuthoritySecretDAL, + certificateAuthorityCrlDAL, + certificateDAL, + certificateBodyDAL, + certificateSecretDAL, + projectDAL, + kmsService, + permissionService + }); + const projectTemplateService = projectTemplateServiceFactory({ licenseService, permissionService, @@ -1062,6 +1079,7 @@ export const registerRoutes = async ( projectRoleDAL, folderDAL, licenseService, + pkiSubscriberDAL, certificateAuthorityDAL, certificateDAL, pkiAlertDAL, @@ -1757,6 +1775,7 @@ export const registerRoutes = async ( certificateEst: certificateEstService, pkiAlert: pkiAlertService, pkiCollection: pkiCollectionService, + pkiSubscriber: pkiSubscriberService, secretScanning: secretScanningService, license: licenseService, trustedIp: trustedIpService, diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index f6c260ea5..b9ce3deb8 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -38,6 +38,7 @@ import { } from "@app/services/app-connection/humanitec"; import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap"; import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql"; +import { OCIConnectionListItemSchema, SanitizedOCIConnectionSchema } from "@app/services/app-connection/oci"; import { PostgresConnectionListItemSchema, SanitizedPostgresConnectionSchema @@ -76,7 +77,8 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedAzureClientSecretsConnectionSchema.options, ...SanitizedWindmillConnectionSchema.options, ...SanitizedLdapConnectionSchema.options, - ...SanitizedTeamCityConnectionSchema.options + ...SanitizedTeamCityConnectionSchema.options, + ...SanitizedOCIConnectionSchema.options ]); const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ @@ -97,7 +99,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ AzureClientSecretsConnectionListItemSchema, WindmillConnectionListItemSchema, LdapConnectionListItemSchema, - TeamCityConnectionListItemSchema + TeamCityConnectionListItemSchema, + OCIConnectionListItemSchema ]); export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index eeae5e5e3..6f6fa1991 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -13,6 +13,7 @@ import { registerHCVaultConnectionRouter } from "./hc-vault-connection-router"; import { registerHumanitecConnectionRouter } from "./humanitec-connection-router"; import { registerLdapConnectionRouter } from "./ldap-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; +import { registerOCIConnectionRouter } from "./oci-connection-router"; import { registerPostgresConnectionRouter } from "./postgres-connection-router"; import { registerTeamCityConnectionRouter } from "./teamcity-connection-router"; import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router"; @@ -40,5 +41,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { + registerAppConnectionEndpoints({ + app: AppConnection.OCI, + server, + sanitizedResponseSchema: SanitizedOCIConnectionSchema, + createSchema: CreateOCIConnectionSchema, + updateSchema: UpdateOCIConnectionSchema + }); + + // The following endpoints are for internal Infisical App use only and not part of the public API + server.route({ + method: "GET", + url: `/:connectionId/compartments`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z + .object({ + id: z.string(), + name: z.string() + }) + .array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const compartments = await server.services.appConnection.oci.listCompartments(connectionId, req.permission); + return compartments; + } + }); + + server.route({ + method: "GET", + url: `/:connectionId/vaults`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + querystring: z.object({ + compartmentOcid: z.string().min(1, "Compartment OCID required") + }), + response: { + 200: z + .object({ + id: z.string(), + displayName: z.string() + }) + .array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + const { compartmentOcid } = req.query; + + const vaults = await server.services.appConnection.oci.listVaults( + { connectionId, compartmentOcid }, + req.permission + ); + return vaults; + } + }); + + server.route({ + method: "GET", + url: `/:connectionId/vault-keys`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + querystring: z.object({ + compartmentOcid: z.string().min(1, "Compartment OCID required"), + vaultOcid: z.string().min(1, "Vault OCID required") + }), + response: { + 200: z + .object({ + id: z.string(), + displayName: z.string() + }) + .array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + const { compartmentOcid, vaultOcid } = req.query; + + const keys = await server.services.appConnection.oci.listVaultKeys( + { connectionId, compartmentOcid, vaultOcid }, + req.permission + ); + return keys; + } + }); +}; diff --git a/backend/src/server/routes/v1/identity-router.ts b/backend/src/server/routes/v1/identity-router.ts index 7731aad98..0e127796a 100644 --- a/backend/src/server/routes/v1/identity-router.ts +++ b/backend/src/server/routes/v1/identity-router.ts @@ -52,7 +52,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ identity: IdentitiesSchema.extend({ - authMethods: z.array(z.string()) + authMethods: z.array(z.string()), + metadata: z.object({ id: z.string(), key: z.string(), value: z.string() }).array() }) }) } @@ -123,7 +124,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - identity: IdentitiesSchema + identity: IdentitiesSchema.extend({ + metadata: z.object({ id: z.string(), key: z.string(), value: z.string() }).array() + }) }) } }, @@ -227,8 +230,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { identity: IdentityOrgMembershipsSchema.extend({ metadata: z .object({ - key: z.string().trim().min(1), id: z.string().trim().min(1), + key: z.string().trim().min(1), value: z.string().trim().min(1) }) .array() diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index b07683c11..018e457fa 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -34,6 +34,7 @@ import { registerOrgRouter } from "./organization-router"; import { registerPasswordRouter } from "./password-router"; import { registerPkiAlertRouter } from "./pki-alert-router"; import { registerPkiCollectionRouter } from "./pki-collection-router"; +import { registerPkiSubscriberRouter } from "./pki-subscriber-router"; import { registerProjectEnvRouter } from "./project-env-router"; import { registerProjectKeyRouter } from "./project-key-router"; import { registerProjectMembershipRouter } from "./project-membership-router"; @@ -107,6 +108,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" }); await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" }); await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" }); + await pkiRouter.register(registerPkiSubscriberRouter, { prefix: "/subscribers" }); }, { prefix: "/pki" } ); diff --git a/backend/src/server/routes/v1/pki-subscriber-router.ts b/backend/src/server/routes/v1/pki-subscriber-router.ts new file mode 100644 index 000000000..d04b8b4bb --- /dev/null +++ b/backend/src/server/routes/v1/pki-subscriber-router.ts @@ -0,0 +1,478 @@ +import { z } from "zod"; + +import { CertificatesSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags, PKI_SUBSCRIBERS } from "@app/lib/api-docs"; +import { ms } from "@app/lib/ms"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; +import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types"; +import { validateAltNameField } from "@app/services/certificate-authority/certificate-authority-validators"; +import { sanitizedPkiSubscriber } from "@app/services/pki-subscriber/pki-subscriber-schema"; +import { PkiSubscriberStatus } from "@app/services/pki-subscriber/pki-subscriber-types"; +import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; + +export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/:subscriberName", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiSubscribers], + description: "Get PKI Subscriber", + params: z.object({ + subscriberName: z.string().describe(PKI_SUBSCRIBERS.GET.subscriberName) + }), + querystring: z.object({ + projectId: z.string().describe(PKI_SUBSCRIBERS.GET.projectId) + }), + response: { + 200: sanitizedPkiSubscriber + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const subscriber = await server.services.pkiSubscriber.getSubscriber({ + subscriberName: req.params.subscriberName, + projectId: req.query.projectId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: subscriber.projectId, + event: { + type: EventType.GET_PKI_SUBSCRIBER, + metadata: { + pkiSubscriberId: subscriber.id, + name: subscriber.name + } + } + }); + + return subscriber; + } + }); + + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiSubscribers], + description: "Create PKI Subscriber", + body: z.object({ + projectId: z.string().trim().describe(PKI_SUBSCRIBERS.CREATE.projectId), + caId: z + .string() + .trim() + .uuid("CA ID must be a valid UUID") + .min(1, "CA ID is required") + .describe(PKI_SUBSCRIBERS.CREATE.caId), + name: slugSchema({ min: 1, max: 64, field: "name" }).describe(PKI_SUBSCRIBERS.CREATE.name), + commonName: z.string().trim().min(1).describe(PKI_SUBSCRIBERS.CREATE.commonName), + status: z + .nativeEnum(PkiSubscriberStatus) + .default(PkiSubscriberStatus.ACTIVE) + .describe(PKI_SUBSCRIBERS.CREATE.status), + ttl: z + .string() + .trim() + .refine((val) => ms(val) > 0, "TTL must be a positive number") + .describe(PKI_SUBSCRIBERS.CREATE.ttl), + subjectAlternativeNames: validateAltNameField + .array() + .default([]) + .transform((arr) => Array.from(new Set(arr))) + .describe(PKI_SUBSCRIBERS.CREATE.subjectAlternativeNames), + keyUsages: z + .nativeEnum(CertKeyUsage) + .array() + .default([CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]) + .transform((arr) => Array.from(new Set(arr))) + .describe(PKI_SUBSCRIBERS.CREATE.keyUsages), + extendedKeyUsages: z + .nativeEnum(CertExtendedKeyUsage) + .array() + .default([]) + .transform((arr) => Array.from(new Set(arr))) + .describe(PKI_SUBSCRIBERS.CREATE.extendedKeyUsages) + }), + response: { + 200: sanitizedPkiSubscriber + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const subscriber = await server.services.pkiSubscriber.createSubscriber({ + ...req.body, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: subscriber.projectId, + event: { + type: EventType.CREATE_PKI_SUBSCRIBER, + metadata: { + pkiSubscriberId: subscriber.id, + caId: subscriber.caId ?? undefined, + name: subscriber.name, + commonName: subscriber.commonName, + ttl: subscriber.ttl, + subjectAlternativeNames: subscriber.subjectAlternativeNames, + keyUsages: subscriber.keyUsages as CertKeyUsage[], + extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[] + } + } + }); + + return subscriber; + } + }); + + server.route({ + method: "PATCH", + url: "/:subscriberName", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiSubscribers], + description: "Update PKI Subscriber", + params: z.object({ + subscriberName: z.string().trim().describe(PKI_SUBSCRIBERS.UPDATE.subscriberName) + }), + body: z.object({ + projectId: z.string().trim().describe(PKI_SUBSCRIBERS.UPDATE.projectId), + caId: z + .string() + .trim() + .uuid("CA ID must be a valid UUID") + .min(1, "CA ID is required") + .optional() + .describe(PKI_SUBSCRIBERS.UPDATE.caId), + name: slugSchema({ min: 1, max: 64, field: "name" }).describe(PKI_SUBSCRIBERS.UPDATE.name).optional(), + commonName: z.string().trim().min(1).describe(PKI_SUBSCRIBERS.UPDATE.commonName).optional(), + status: z.nativeEnum(PkiSubscriberStatus).optional().describe(PKI_SUBSCRIBERS.UPDATE.status), + subjectAlternativeNames: validateAltNameField + .array() + .optional() + .describe(PKI_SUBSCRIBERS.UPDATE.subjectAlternativeNames), + ttl: z + .string() + .trim() + .refine((val) => ms(val) > 0, "TTL must be a positive number") + .optional() + .describe(PKI_SUBSCRIBERS.UPDATE.ttl), + keyUsages: z + .nativeEnum(CertKeyUsage) + .array() + .transform((arr) => Array.from(new Set(arr))) + .optional() + .describe(PKI_SUBSCRIBERS.UPDATE.keyUsages), + extendedKeyUsages: z + .nativeEnum(CertExtendedKeyUsage) + .array() + .transform((arr) => Array.from(new Set(arr))) + .optional() + .describe(PKI_SUBSCRIBERS.UPDATE.extendedKeyUsages) + }), + response: { + 200: sanitizedPkiSubscriber + } + }, + handler: async (req) => { + const subscriber = await server.services.pkiSubscriber.updateSubscriber({ + subscriberName: req.params.subscriberName, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: subscriber.projectId, + event: { + type: EventType.UPDATE_PKI_SUBSCRIBER, + metadata: { + pkiSubscriberId: subscriber.id, + caId: subscriber.caId ?? undefined, + name: subscriber.name, + commonName: subscriber.commonName, + ttl: subscriber.ttl, + subjectAlternativeNames: subscriber.subjectAlternativeNames, + keyUsages: subscriber.keyUsages as CertKeyUsage[], + extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[] + } + } + }); + + return subscriber; + } + }); + + server.route({ + method: "DELETE", + url: "/:subscriberName", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiSubscribers], + description: "Delete PKI Subscriber", + params: z.object({ + subscriberName: z.string().describe(PKI_SUBSCRIBERS.DELETE.subscriberName) + }), + body: z.object({ + projectId: z.string().trim().describe(PKI_SUBSCRIBERS.DELETE.projectId) + }), + response: { + 200: sanitizedPkiSubscriber + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const subscriber = await server.services.pkiSubscriber.deleteSubscriber({ + subscriberName: req.params.subscriberName, + projectId: req.body.projectId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: subscriber.projectId, + event: { + type: EventType.DELETE_PKI_SUBSCRIBER, + metadata: { + pkiSubscriberId: subscriber.id, + name: subscriber.name + } + } + }); + + return subscriber; + } + }); + + server.route({ + method: "POST", + url: "/:subscriberName/issue-certificate", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiSubscribers], + description: "Issue certificate", + params: z.object({ + subscriberName: z.string().describe(PKI_SUBSCRIBERS.ISSUE_CERT.subscriberName) + }), + body: z.object({ + projectId: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.projectId) + }), + response: { + 200: z.object({ + certificate: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.certificate), + issuingCaCertificate: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.issuingCaCertificate), + certificateChain: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.certificateChain), + privateKey: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.privateKey), + serialNumber: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.serialNumber) + }) + } + }, + handler: async (req) => { + const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, subscriber } = + await server.services.pkiSubscriber.issueSubscriberCert({ + subscriberName: req.params.subscriberName, + projectId: req.body.projectId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: subscriber.projectId, + event: { + type: EventType.ISSUE_PKI_SUBSCRIBER_CERT, + metadata: { + subscriberId: subscriber.id, + name: subscriber.name, + serialNumber + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.IssueCert, + distinctId: getTelemetryDistinctId(req), + properties: { + subscriberId: subscriber.id, + commonName: subscriber.commonName, + ...req.auditLogInfo + } + }); + + return { + certificate, + certificateChain, + issuingCaCertificate, + privateKey, + serialNumber + }; + } + }); + + server.route({ + method: "POST", + url: "/:subscriberName/sign-certificate", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiSubscribers], + description: "Sign certificate", + params: z.object({ + subscriberName: z.string().describe(PKI_SUBSCRIBERS.SIGN_CERT.subscriberName) + }), + body: z.object({ + projectId: z.string().trim().describe(PKI_SUBSCRIBERS.SIGN_CERT.projectId), + csr: z.string().trim().min(1).max(3000).describe(PKI_SUBSCRIBERS.SIGN_CERT.csr) + }), + response: { + 200: z.object({ + certificate: z.string().trim().describe(PKI_SUBSCRIBERS.SIGN_CERT.certificate), + issuingCaCertificate: z.string().trim().describe(PKI_SUBSCRIBERS.SIGN_CERT.issuingCaCertificate), + certificateChain: z.string().trim().describe(PKI_SUBSCRIBERS.SIGN_CERT.certificateChain), + serialNumber: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.serialNumber) + }) + } + }, + handler: async (req) => { + const { certificate, certificateChain, issuingCaCertificate, serialNumber, subscriber } = + await server.services.pkiSubscriber.signSubscriberCert({ + subscriberName: req.params.subscriberName, + projectId: req.body.projectId, + csr: req.body.csr, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: subscriber.projectId, + event: { + type: EventType.SIGN_PKI_SUBSCRIBER_CERT, + metadata: { + subscriberId: subscriber.id, + name: subscriber.name, + serialNumber + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SignCert, + distinctId: getTelemetryDistinctId(req), + properties: { + subscriberId: subscriber.id, + commonName: subscriber.commonName, + ...req.auditLogInfo + } + }); + + return { + certificate, + certificateChain, + issuingCaCertificate, + serialNumber + }; + } + }); + + server.route({ + method: "GET", + url: "/:subscriberName/certificates", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiSubscribers], + description: "List PKI Subscriber certificates", + params: z.object({ + subscriberName: z.string().describe(PKI_SUBSCRIBERS.GET.subscriberName) + }), + querystring: z.object({ + projectId: z.string().trim().describe(PKI_SUBSCRIBERS.LIST_CERTS.projectId), + offset: z.coerce.number().min(0).max(100).default(0).describe(PKI_SUBSCRIBERS.LIST_CERTS.offset), + limit: z.coerce.number().min(1).max(100).default(25).describe(PKI_SUBSCRIBERS.LIST_CERTS.limit) + }), + response: { + 200: z.object({ + certificates: z.array(CertificatesSchema), + totalCount: z.number() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { totalCount, certificates } = await server.services.pkiSubscriber.listSubscriberCerts({ + subscriberName: req.params.subscriberName, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.query + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.query.projectId, + event: { + type: EventType.LIST_PKI_SUBSCRIBER_CERTS, + metadata: { + subscriberId: req.params.subscriberName, + name: req.params.subscriberName, + projectId: req.query.projectId + } + } + }); + + return { + certificates, + totalCount + }; + } + }); +}; diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index 75b3ac68e..b5bd62ad6 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -10,6 +10,7 @@ import { registerGcpSyncRouter } from "./gcp-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router"; import { registerHCVaultSyncRouter } from "./hc-vault-sync-router"; import { registerHumanitecSyncRouter } from "./humanitec-sync-router"; +import { registerOCIVaultSyncRouter } from "./oci-vault-sync-router"; import { registerTeamCitySyncRouter } from "./teamcity-sync-router"; import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router"; import { registerVercelSyncRouter } from "./vercel-sync-router"; @@ -31,5 +32,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record + registerSyncSecretsEndpoints({ + destination: SecretSync.OCIVault, + server, + responseSchema: OCIVaultSyncSchema, + createSchema: CreateOCIVaultSyncSchema, + updateSchema: UpdateOCIVaultSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts index 359040d7f..a7a561738 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts @@ -24,6 +24,7 @@ import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/ import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github"; import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault"; import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec"; +import { OCIVaultSyncListItemSchema, OCIVaultSyncSchema } from "@app/services/secret-sync/oci-vault"; import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity"; import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud"; import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel"; @@ -43,7 +44,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [ VercelSyncSchema, WindmillSyncSchema, HCVaultSyncSchema, - TeamCitySyncSchema + TeamCitySyncSchema, + OCIVaultSyncSchema ]); const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [ @@ -60,7 +62,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [ VercelSyncListItemSchema, WindmillSyncListItemSchema, HCVaultSyncListItemSchema, - TeamCitySyncListItemSchema + TeamCitySyncListItemSchema, + OCIVaultSyncListItemSchema ]); export const registerSecretSyncRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index 498fb8e8b..3d92bfb1a 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -24,6 +24,7 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types"; import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema"; +import { sanitizedPkiSubscriber } from "@app/services/pki-subscriber/pki-subscriber-schema"; import { ProjectFilterType } from "@app/services/project/project-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; @@ -490,6 +491,38 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/:projectId/pki-subscribers", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiSubscribers], + params: z.object({ + projectId: z.string().trim().describe(PROJECTS.LIST_PKI_SUBSCRIBERS.projectId) + }), + response: { + 200: z.object({ + subscribers: z.array(sanitizedPkiSubscriber) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const subscribers = await server.services.project.listProjectPkiSubscribers({ + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + projectId: req.params.projectId + }); + + return { subscribers }; + } + }); + server.route({ method: "GET", url: "/:projectId/certificate-templates", @@ -628,6 +661,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SshHosts], params: z.object({ projectId: z.string().trim().describe(PROJECTS.LIST_SSH_HOSTS.projectId) }), @@ -666,6 +701,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SshHostGroups], params: z.object({ projectId: z.string().trim().describe(PROJECTS.LIST_SSH_HOST_GROUPS.projectId) }), diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index c2912c2b6..6e09f1293 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -16,7 +16,8 @@ export enum AppConnection { Auth0 = "auth0", HCVault = "hashicorp-vault", LDAP = "ldap", - TeamCity = "teamcity" + TeamCity = "teamcity", + OCI = "oci" } export enum AWSRegion { diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 95afdcbd2..f6fd894a6 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -53,6 +53,7 @@ import { } from "./humanitec"; import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap"; import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql"; +import { getOCIConnectionListItem, OCIConnectionMethod, validateOCIConnectionCredentials } from "./oci"; import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres"; import { getTeamCityConnectionListItem, @@ -91,7 +92,8 @@ export const listAppConnectionOptions = () => { getAuth0ConnectionListItem(), getHCVaultConnectionListItem(), getLdapConnectionListItem(), - getTeamCityConnectionListItem() + getTeamCityConnectionListItem(), + getOCIConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -160,7 +162,8 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.HCVault]: validateHCVaultConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator + [AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.OCI]: validateOCIConnectionCredentials as TAppConnectionCredentialsValidator }; return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); @@ -176,6 +179,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case GitHubConnectionMethod.OAuth: return "OAuth"; case AwsConnectionMethod.AccessKey: + case OCIConnectionMethod.AccessKey: return "Access Key"; case AwsConnectionMethod.AssumeRole: return "Assume Role"; @@ -250,5 +254,6 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Auth0]: platformManagedCredentialsNotSupported, [AppConnection.HCVault]: platformManagedCredentialsNotSupported, [AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future - [AppConnection.TeamCity]: platformManagedCredentialsNotSupported + [AppConnection.TeamCity]: platformManagedCredentialsNotSupported, + [AppConnection.OCI]: platformManagedCredentialsNotSupported }; diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 05e00446c..c32336453 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -18,5 +18,6 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Auth0]: "Auth0", [AppConnection.HCVault]: "Hashicorp Vault", [AppConnection.LDAP]: "LDAP", - [AppConnection.TeamCity]: "TeamCity" + [AppConnection.TeamCity]: "TeamCity", + [AppConnection.OCI]: "OCI" }; diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index 7a8b1a09c..85b63138a 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -49,6 +49,8 @@ import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec"; import { humanitecConnectionService } from "./humanitec/humanitec-connection-service"; import { ValidateLdapConnectionCredentialsSchema } from "./ldap"; import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql"; +import { ValidateOCIConnectionCredentialsSchema } from "./oci"; +import { ociConnectionService } from "./oci/oci-connection-service"; import { ValidatePostgresConnectionCredentialsSchema } from "./postgres"; import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity"; import { teamcityConnectionService } from "./teamcity/teamcity-connection-service"; @@ -85,7 +87,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record>>; @@ -150,6 +157,7 @@ export type TAppConnectionInput = { id: string } & ( | THCVaultConnectionInput | TLdapConnectionInput | TTeamCityConnectionInput + | TOCIConnectionInput ); export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput; @@ -180,7 +188,8 @@ export type TAppConnectionConfig = | TAuth0ConnectionConfig | THCVaultConnectionConfig | TLdapConnectionConfig - | TTeamCityConnectionConfig; + | TTeamCityConnectionConfig + | TOCIConnectionConfig; export type TValidateAppConnectionCredentialsSchema = | TValidateAwsConnectionCredentialsSchema @@ -200,7 +209,8 @@ export type TValidateAppConnectionCredentialsSchema = | TValidateAuth0ConnectionCredentialsSchema | TValidateHCVaultConnectionCredentialsSchema | TValidateLdapConnectionCredentialsSchema - | TValidateTeamCityConnectionCredentialsSchema; + | TValidateTeamCityConnectionCredentialsSchema + | TValidateOCIConnectionCredentialsSchema; export type TListAwsConnectionKmsKeys = { connectionId: string; diff --git a/backend/src/services/app-connection/oci/index.ts b/backend/src/services/app-connection/oci/index.ts new file mode 100644 index 000000000..eb2850d34 --- /dev/null +++ b/backend/src/services/app-connection/oci/index.ts @@ -0,0 +1,4 @@ +export * from "./oci-connection-enums"; +export * from "./oci-connection-fns"; +export * from "./oci-connection-schemas"; +export * from "./oci-connection-types"; diff --git a/backend/src/services/app-connection/oci/oci-connection-enums.ts b/backend/src/services/app-connection/oci/oci-connection-enums.ts new file mode 100644 index 000000000..1b4319651 --- /dev/null +++ b/backend/src/services/app-connection/oci/oci-connection-enums.ts @@ -0,0 +1,3 @@ +export enum OCIConnectionMethod { + AccessKey = "access-key" +} diff --git a/backend/src/services/app-connection/oci/oci-connection-fns.ts b/backend/src/services/app-connection/oci/oci-connection-fns.ts new file mode 100644 index 000000000..5dcf6ee7a --- /dev/null +++ b/backend/src/services/app-connection/oci/oci-connection-fns.ts @@ -0,0 +1,139 @@ +import { common, identity, keymanagement } from "oci-sdk"; + +import { BadRequestError } from "@app/lib/errors"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +import { OCIConnectionMethod } from "./oci-connection-enums"; +import { TOCIConnection, TOCIConnectionConfig } from "./oci-connection-types"; + +export const getOCIProvider = async (config: TOCIConnectionConfig) => { + const { + credentials: { fingerprint, privateKey, region, tenancyOcid, userOcid } + } = config; + + const provider = new common.SimpleAuthenticationDetailsProvider( + tenancyOcid, + userOcid, + fingerprint, + privateKey, + null, + common.Region.fromRegionId(region) + ); + + return provider; +}; + +export const getOCIConnectionListItem = () => { + return { + name: "OCI" as const, + app: AppConnection.OCI as const, + methods: Object.values(OCIConnectionMethod) as [OCIConnectionMethod.AccessKey] + }; +}; + +export const validateOCIConnectionCredentials = async (config: TOCIConnectionConfig) => { + const provider = await getOCIProvider(config); + + try { + const identityClient = new identity.IdentityClient({ + authenticationDetailsProvider: provider + }); + + // Get user details - a lightweight call that validates all credentials + await identityClient.getUser({ userId: config.credentials.userOcid }); + } catch (error: unknown) { + if (error instanceof Error) { + throw new BadRequestError({ + message: `Failed to validate credentials: ${error.message || "Unknown error"}` + }); + } + throw new BadRequestError({ + message: "Unable to validate connection: verify credentials" + }); + } + + return config.credentials; +}; + +export const listOCICompartments = async (appConnection: TOCIConnection) => { + const provider = await getOCIProvider(appConnection); + + const identityClient = new identity.IdentityClient({ authenticationDetailsProvider: provider }); + const keyManagementClient = new keymanagement.KmsVaultClient({ + authenticationDetailsProvider: provider + }); + + const rootCompartment = await identityClient + .getTenancy({ + tenancyId: appConnection.credentials.tenancyOcid + }) + .then((response) => ({ + ...response.tenancy, + id: appConnection.credentials.tenancyOcid, + name: response.tenancy.name ? `${response.tenancy.name} (root)` : "root" + })); + + const compartments = await identityClient.listCompartments({ + compartmentId: appConnection.credentials.tenancyOcid, + compartmentIdInSubtree: true, + accessLevel: identity.requests.ListCompartmentsRequest.AccessLevel.Any, + lifecycleState: identity.models.Compartment.LifecycleState.Active + }); + + const allCompartments = [rootCompartment, ...compartments.items]; + const filteredCompartments = []; + + for await (const compartment of allCompartments) { + try { + // Check if user can list vaults in this compartment + await keyManagementClient.listVaults({ + compartmentId: compartment.id, + limit: 1 + }); + + filteredCompartments.push(compartment); + } catch (error) { + // Do nothing + } + } + + return filteredCompartments; +}; + +export const listOCIVaults = async (appConnection: TOCIConnection, compartmentOcid: string) => { + const provider = await getOCIProvider(appConnection); + + const keyManagementClient = new keymanagement.KmsVaultClient({ + authenticationDetailsProvider: provider + }); + + const vaults = await keyManagementClient.listVaults({ + compartmentId: compartmentOcid + }); + + return vaults.items.filter((v) => v.lifecycleState === keymanagement.models.Vault.LifecycleState.Active); +}; + +export const listOCIVaultKeys = async (appConnection: TOCIConnection, compartmentOcid: string, vaultOcid: string) => { + const provider = await getOCIProvider(appConnection); + + const kmsVaultClient = new keymanagement.KmsVaultClient({ + authenticationDetailsProvider: provider + }); + + const vault = await kmsVaultClient.getVault({ + vaultId: vaultOcid + }); + + const keyManagementClient = new keymanagement.KmsManagementClient({ + authenticationDetailsProvider: provider + }); + + keyManagementClient.endpoint = vault.vault.managementEndpoint; + + const keys = await keyManagementClient.listKeys({ + compartmentId: compartmentOcid + }); + + return keys.items.filter((v) => v.lifecycleState === keymanagement.models.KeySummary.LifecycleState.Enabled); +}; diff --git a/backend/src/services/app-connection/oci/oci-connection-schemas.ts b/backend/src/services/app-connection/oci/oci-connection-schemas.ts new file mode 100644 index 000000000..f09564455 --- /dev/null +++ b/backend/src/services/app-connection/oci/oci-connection-schemas.ts @@ -0,0 +1,65 @@ +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { OCIConnectionMethod } from "./oci-connection-enums"; + +export const OCIConnectionAccessTokenCredentialsSchema = z.object({ + userOcid: z.string().trim().min(1, "User OCID required").describe(AppConnections.CREDENTIALS.OCI.userOcid), + tenancyOcid: z.string().trim().min(1, "Tenancy OCID required").describe(AppConnections.CREDENTIALS.OCI.tenancyOcid), + region: z.string().trim().min(1, "Region required").describe(AppConnections.CREDENTIALS.OCI.region), + fingerprint: z.string().trim().min(1, "Fingerprint required").describe(AppConnections.CREDENTIALS.OCI.fingerprint), + privateKey: z.string().trim().min(1, "Private Key required").describe(AppConnections.CREDENTIALS.OCI.privateKey) +}); + +const BaseOCIConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.OCI) }); + +export const OCIConnectionSchema = BaseOCIConnectionSchema.extend({ + method: z.literal(OCIConnectionMethod.AccessKey), + credentials: OCIConnectionAccessTokenCredentialsSchema +}); + +export const SanitizedOCIConnectionSchema = z.discriminatedUnion("method", [ + BaseOCIConnectionSchema.extend({ + method: z.literal(OCIConnectionMethod.AccessKey), + credentials: OCIConnectionAccessTokenCredentialsSchema.pick({ + userOcid: true, + tenancyOcid: true, + region: true, + fingerprint: true + }) + }) +]); + +export const ValidateOCIConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z.literal(OCIConnectionMethod.AccessKey).describe(AppConnections.CREATE(AppConnection.OCI).method), + credentials: OCIConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.OCI).credentials + ) + }) +]); + +export const CreateOCIConnectionSchema = ValidateOCIConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.OCI) +); + +export const UpdateOCIConnectionSchema = z + .object({ + credentials: OCIConnectionAccessTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.OCI).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.OCI)); + +export const OCIConnectionListItemSchema = z.object({ + name: z.literal("OCI"), + app: z.literal(AppConnection.OCI), + methods: z.nativeEnum(OCIConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/oci/oci-connection-service.ts b/backend/src/services/app-connection/oci/oci-connection-service.ts new file mode 100644 index 000000000..2d72135e5 --- /dev/null +++ b/backend/src/services/app-connection/oci/oci-connection-service.ts @@ -0,0 +1,70 @@ +import { logger } from "@app/lib/logger"; +import { OrgServiceActor } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { listOCICompartments, listOCIVaultKeys, listOCIVaults } from "./oci-connection-fns"; +import { TOCIConnection } from "./oci-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +type TListOCIVaultsDTO = { + connectionId: string; + compartmentOcid: string; +}; + +type TListOCIVaultKeysDTO = { + connectionId: string; + compartmentOcid: string; + vaultOcid: string; +}; + +export const ociConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listCompartments = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor); + + try { + const compartments = await listOCICompartments(appConnection); + return compartments; + } catch (error) { + logger.error(error, "Failed to establish connection with OCI"); + return []; + } + }; + + const listVaults = async ({ connectionId, compartmentOcid }: TListOCIVaultsDTO, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor); + + try { + const vaults = await listOCIVaults(appConnection, compartmentOcid); + return vaults; + } catch (error) { + logger.error(error, "Failed to establish connection with OCI"); + return []; + } + }; + + const listVaultKeys = async ( + { connectionId, compartmentOcid, vaultOcid }: TListOCIVaultKeysDTO, + actor: OrgServiceActor + ) => { + const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor); + + try { + const keys = await listOCIVaultKeys(appConnection, compartmentOcid, vaultOcid); + return keys; + } catch (error) { + logger.error(error, "Failed to establish connection with OCI"); + return []; + } + }; + + return { + listCompartments, + listVaults, + listVaultKeys + }; +}; diff --git a/backend/src/services/app-connection/oci/oci-connection-types.ts b/backend/src/services/app-connection/oci/oci-connection-types.ts new file mode 100644 index 000000000..74ddfe0c8 --- /dev/null +++ b/backend/src/services/app-connection/oci/oci-connection-types.ts @@ -0,0 +1,22 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateOCIConnectionSchema, + OCIConnectionSchema, + ValidateOCIConnectionCredentialsSchema +} from "./oci-connection-schemas"; + +export type TOCIConnection = z.infer; + +export type TOCIConnectionInput = z.infer & { + app: AppConnection.OCI; +}; + +export type TValidateOCIConnectionCredentialsSchema = typeof ValidateOCIConnectionCredentialsSchema; + +export type TOCIConnectionConfig = DiscriminativePick & { + orgId: string; +}; diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index e1d7ce5cb..d504e38ed 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -1169,7 +1169,7 @@ export const certificateAuthorityServiceFactory = ({ ProjectPermissionSub.Certificates ); - if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); + if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" }); if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); if (ca.requireTemplateForIssuance && !certificateTemplate) { throw new BadRequestError({ message: "Certificate template is required for issuance" }); @@ -1520,7 +1520,7 @@ export const certificateAuthorityServiceFactory = ({ ); } - if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); + if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" }); if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); if (ca.requireTemplateForIssuance && !certificateTemplate) { throw new BadRequestError({ message: "Certificate template is required for issuance" }); diff --git a/backend/src/services/certificate-authority/certificate-authority-validators.ts b/backend/src/services/certificate-authority/certificate-authority-validators.ts index 979a3b9c5..4820cfe00 100644 --- a/backend/src/services/certificate-authority/certificate-authority-validators.ts +++ b/backend/src/services/certificate-authority/certificate-authority-validators.ts @@ -10,6 +10,18 @@ const isValidDate = (dateString: string) => { export const validateCaDateField = z.string().trim().refine(isValidDate, { message: "Invalid date format" }); +export const validateAltNameField = z + .string() + .trim() + .refine( + (name) => { + return isFQDN(name) || z.string().email().safeParse(name).success || isValidIp(name); + }, + { + message: "SAN must be a valid hostname, email address, or IP address" + } + ); + export const validateAltNamesField = z .string() .trim() diff --git a/backend/src/services/certificate/certificate-dal.ts b/backend/src/services/certificate/certificate-dal.ts index 71c70838c..aafbe56f4 100644 --- a/backend/src/services/certificate/certificate-dal.ts +++ b/backend/src/services/certificate/certificate-dal.ts @@ -44,8 +44,27 @@ export const certificateDALFactory = (db: TDbClient) => { } }; + const countCertificatesForPkiSubscriber = async (subscriberId: string) => { + try { + interface CountResult { + count: string; + } + + const query = db + .replicaNode()(TableName.Certificate) + .where(`${TableName.Certificate}.pkiSubscriberId`, subscriberId); + + const count = await query.count("*").first(); + + return parseInt((count as unknown as CountResult).count || "0", 10); + } catch (error) { + throw new DatabaseError({ error, name: "Count all subscriber certificates" }); + } + }; + return { ...certificateOrm, - countCertificatesInProject + countCertificatesInProject, + countCertificatesForPkiSubscriber }; }; diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index 6f72b3c6e..fd893713e 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -106,18 +106,29 @@ export const identityServiceFactory = ({ }, tx ); + + let insertedMetadata: Array<{ + id: string; + key: string; + value: string; + }> = []; + if (metadata && metadata.length) { - await identityMetadataDAL.insertMany( - metadata.map(({ key, value }) => ({ - identityId: newIdentity.id, - orgId, - key, - value - })), - tx - ); + const rowsToInsert = metadata.map(({ key, value }) => ({ + identityId: newIdentity.id, + orgId, + key, + value + })); + + insertedMetadata = await identityMetadataDAL.insertMany(rowsToInsert, tx); } - return { ...newIdentity, authMethods: [] }; + + return { + ...newIdentity, + authMethods: [], + metadata: insertedMetadata + }; }); await licenseService.updateSubscriptionOrgMemberCount(orgId); @@ -189,21 +200,31 @@ export const identityServiceFactory = ({ tx ); } + let insertedMetadata: Array<{ + id: string; + key: string; + value: string; + }> = []; + if (metadata) { await identityMetadataDAL.delete({ orgId: identityOrgMembership.orgId, identityId: id }, tx); + if (metadata.length) { - await identityMetadataDAL.insertMany( - metadata.map(({ key, value }) => ({ - identityId: newIdentity.id, - orgId: identityOrgMembership.orgId, - key, - value - })), - tx - ); + const rowsToInsert = metadata.map(({ key, value }) => ({ + identityId: newIdentity.id, + orgId: identityOrgMembership.orgId, + key, + value + })); + + insertedMetadata = await identityMetadataDAL.insertMany(rowsToInsert, tx); } } - return newIdentity; + + return { + ...newIdentity, + metadata: insertedMetadata + }; }); return { ...identity, orgId: identityOrgMembership.orgId }; @@ -224,6 +245,7 @@ export const identityServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + return identity; }; diff --git a/backend/src/services/pki-subscriber/pki-subscriber-dal.ts b/backend/src/services/pki-subscriber/pki-subscriber-dal.ts new file mode 100644 index 000000000..1899c63a6 --- /dev/null +++ b/backend/src/services/pki-subscriber/pki-subscriber-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TPkiSubscriberDALFactory = ReturnType; + +export const pkiSubscriberDALFactory = (db: TDbClient) => { + const pkiSubscriberOrm = ormify(db, TableName.PkiSubscriber); + return pkiSubscriberOrm; +}; diff --git a/backend/src/services/pki-subscriber/pki-subscriber-schema.ts b/backend/src/services/pki-subscriber/pki-subscriber-schema.ts new file mode 100644 index 000000000..7ffeea3fa --- /dev/null +++ b/backend/src/services/pki-subscriber/pki-subscriber-schema.ts @@ -0,0 +1,14 @@ +import { PkiSubscribersSchema } from "@app/db/schemas"; + +export const sanitizedPkiSubscriber = PkiSubscribersSchema.pick({ + id: true, + projectId: true, + caId: true, + name: true, + commonName: true, + status: true, + subjectAlternativeNames: true, + ttl: true, + keyUsages: true, + extendedKeyUsages: true +}); diff --git a/backend/src/services/pki-subscriber/pki-subscriber-service.ts b/backend/src/services/pki-subscriber/pki-subscriber-service.ts new file mode 100644 index 000000000..5b15786b1 --- /dev/null +++ b/backend/src/services/pki-subscriber/pki-subscriber-service.ts @@ -0,0 +1,805 @@ +/* eslint-disable no-bitwise */ +import { ForbiddenError, subject } from "@casl/ability"; +import * as x509 from "@peculiar/x509"; +import crypto, { KeyObject } from "crypto"; +import { z } from "zod"; + +import { ActionProjectType } from "@app/db/schemas"; +import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { + ProjectPermissionPkiSubscriberActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; +import { isFQDN } from "@app/lib/validator/validate-url"; +import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; +import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; +import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; +import { + CertExtendedKeyUsage, + CertExtendedKeyUsageOIDToName, + CertKeyAlgorithm, + CertKeyUsage, + CertStatus +} from "@app/services/certificate/certificate-types"; +import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; +import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; +import { + createSerialNumber, + getCaCertChain, + getCaCredentials, + keyAlgorithmToAlgCfg, + parseDistinguishedName +} from "@app/services/certificate-authority/certificate-authority-fns"; +import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal"; +import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; + +import { + PkiSubscriberStatus, + TCreatePkiSubscriberDTO, + TDeletePkiSubscriberDTO, + TGetPkiSubscriberDTO, + TIssuePkiSubscriberCertDTO, + TListPkiSubscriberCertsDTO, + TSignPkiSubscriberCertDTO, + TUpdatePkiSubscriberDTO +} from "./pki-subscriber-types"; + +type TPkiSubscriberServiceFactoryDep = { + pkiSubscriberDAL: Pick< + TPkiSubscriberDALFactory, + "create" | "findById" | "updateById" | "deleteById" | "transaction" | "find" | "findOne" + >; + certificateAuthorityDAL: Pick; + certificateAuthorityCertDAL: Pick; + certificateAuthoritySecretDAL: Pick; + certificateAuthorityCrlDAL: Pick; + certificateDAL: Pick; + certificateBodyDAL: Pick; + certificateSecretDAL: Pick; + projectDAL: Pick; + kmsService: Pick; + permissionService: Pick; +}; + +export type TPkiSubscriberServiceFactory = ReturnType; + +export const pkiSubscriberServiceFactory = ({ + pkiSubscriberDAL, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + certificateAuthoritySecretDAL, + certificateAuthorityCrlDAL, + certificateDAL, + certificateBodyDAL, + certificateSecretDAL, + projectDAL, + kmsService, + permissionService +}: TPkiSubscriberServiceFactoryDep) => { + const createSubscriber = async ({ + name, + commonName, + status, + caId, + ttl, + subjectAlternativeNames, + keyUsages, + extendedKeyUsages, + projectId, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TCreatePkiSubscriberDTO) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPkiSubscriberActions.Create, + subject(ProjectPermissionSub.PkiSubscribers, { + name + }) + ); + + const newSubscriber = await pkiSubscriberDAL.create({ + caId, + projectId, + name, + commonName, + status, + ttl, + subjectAlternativeNames, + keyUsages, + extendedKeyUsages + }); + + return newSubscriber; + }; + + const getSubscriber = async ({ + subscriberName, + projectId, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TGetPkiSubscriberDTO) => { + const subscriber = await pkiSubscriberDAL.findOne({ + name: subscriberName, + projectId + }); + + if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: subscriber.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPkiSubscriberActions.Read, + subject(ProjectPermissionSub.PkiSubscribers, { + name: subscriber.name + }) + ); + + return subscriber; + }; + + const updateSubscriber = async ({ + subscriberName, + projectId, + name, + commonName, + status, + caId, + ttl, + subjectAlternativeNames, + keyUsages, + extendedKeyUsages, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TUpdatePkiSubscriberDTO) => { + const subscriber = await pkiSubscriberDAL.findOne({ + name: subscriberName, + projectId + }); + if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: subscriber.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPkiSubscriberActions.Edit, + subject(ProjectPermissionSub.PkiSubscribers, { + name: subscriber.name + }) + ); + + const updatedSubscriber = await pkiSubscriberDAL.updateById(subscriber.id, { + caId, + name, + commonName, + status, + ttl, + subjectAlternativeNames, + keyUsages, + extendedKeyUsages + }); + + return updatedSubscriber; + }; + + const deleteSubscriber = async ({ + subscriberName, + projectId, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TDeletePkiSubscriberDTO) => { + const subscriber = await pkiSubscriberDAL.findOne({ + name: subscriberName, + projectId + }); + if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: subscriber.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPkiSubscriberActions.Delete, + subject(ProjectPermissionSub.PkiSubscribers, { + name: subscriber.name + }) + ); + + await pkiSubscriberDAL.deleteById(subscriber.id); + + return subscriber; + }; + + const issueSubscriberCert = async ({ + subscriberName, + projectId, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TIssuePkiSubscriberCertDTO) => { + const subscriber = await pkiSubscriberDAL.findOne({ + name: subscriberName, + projectId + }); + if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` }); + if (!subscriber.caId) throw new BadRequestError({ message: "Subscriber does not have an assigned issuing CA" }); + + const ca = await certificateAuthorityDAL.findById(subscriber.caId); + if (!ca) throw new NotFoundError({ message: `CA with ID '${subscriber.caId}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: ca.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPkiSubscriberActions.IssueCert, + subject(ProjectPermissionSub.PkiSubscribers, { + name: subscriber.name + }) + ); + + if (subscriber.status !== PkiSubscriberStatus.ACTIVE) + throw new BadRequestError({ message: "Subscriber is not active" }); + if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" }); + if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + if (ca.requireTemplateForIssuance) { + throw new BadRequestError({ message: "Certificate template is required for issuance" }); + } + const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); + + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); + + const decryptedCaCert = await kmsDecryptor({ + cipherTextBlob: caCert.encryptedCertificate + }); + + const caCertObj = new x509.X509Certificate(decryptedCaCert); + const notBeforeDate = new Date(); + const notAfterDate = new Date(new Date().getTime() + ms(subscriber.ttl)); + const caCertNotBeforeDate = new Date(caCertObj.notBefore); + const caCertNotAfterDate = new Date(caCertObj.notAfter); + + // check not before constraint + if (notBeforeDate < caCertNotBeforeDate) { + throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" }); + } + + // check not after constraint + if (notAfterDate > caCertNotAfterDate) { + throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); + } + + const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); + const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + + const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({ + name: `CN=${subscriber.commonName}`, + keys: leafKeys, + signingAlgorithm: alg, + extensions: [ + // eslint-disable-next-line no-bitwise + new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment) + ], + attributes: [new x509.ChallengePasswordAttribute("password")] + }); + + const { caPrivateKey, caSecret } = await getCaCredentials({ + caId: ca.id, + certificateAuthorityDAL, + certificateAuthoritySecretDAL, + projectDAL, + kmsService + }); + + const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); + const appCfg = getConfig(); + + const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; + const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`; + + const extensions: x509.Extension[] = [ + new x509.BasicConstraintsExtension(false), + new x509.CRLDistributionPointsExtension([distributionPointUrl]), + await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), + await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey), + new x509.AuthorityInfoAccessExtension({ + caIssuers: new x509.GeneralName("url", caIssuerUrl) + }), + new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy + ]; + + const selectedKeyUsages = subscriber.keyUsages as CertKeyUsage[]; + const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0); + if (keyUsagesBitValue) { + extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true)); + } + + if (subscriber.extendedKeyUsages.length) { + const extendedKeyUsagesExtension = new x509.ExtendedKeyUsageExtension( + subscriber.extendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku as CertExtendedKeyUsage]), + true + ); + extensions.push(extendedKeyUsagesExtension); + } + + let altNamesArray: { type: "email" | "dns"; value: string }[] = []; + + if (subscriber.subjectAlternativeNames?.length) { + altNamesArray = subscriber.subjectAlternativeNames.map((altName) => { + if (z.string().email().safeParse(altName).success) { + return { type: "email", value: altName }; + } + + if (isFQDN(altName, { allow_wildcard: true })) { + return { type: "dns", value: altName }; + } + + throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` }); + }); + + const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); + extensions.push(altNamesExtension); + } + + const serialNumber = createSerialNumber(); + const leafCert = await x509.X509CertificateGenerator.create({ + serialNumber, + subject: csrObj.subject, + issuer: caCertObj.subject, + notBefore: notBeforeDate, + notAfter: notAfterDate, + signingKey: caPrivateKey, + publicKey: csrObj.publicKey, + signingAlgorithm: alg, + extensions + }); + + const skLeafObj = KeyObject.from(leafKeys.privateKey); + const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string; + + const kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); + const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({ + plainText: Buffer.from(new Uint8Array(leafCert.rawData)) + }); + const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({ + plainText: Buffer.from(skLeaf) + }); + + const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ + caCertId: caCert.id, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + projectDAL, + kmsService + }); + + const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim(); + + const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({ + plainText: Buffer.from(certificateChainPem) + }); + + await certificateDAL.transaction(async (tx) => { + const cert = await certificateDAL.create( + { + caId: ca.id, + caCertId: caCert.id, + pkiSubscriberId: subscriber.id, + status: CertStatus.ACTIVE, + friendlyName: subscriber.commonName, + commonName: subscriber.commonName, + altNames: subscriber.subjectAlternativeNames.join(","), + serialNumber, + notBefore: notBeforeDate, + notAfter: notAfterDate, + keyUsages: selectedKeyUsages, + extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[] + }, + tx + ); + + await certificateBodyDAL.create( + { + certId: cert.id, + encryptedCertificate, + encryptedCertificateChain + }, + tx + ); + + await certificateSecretDAL.create( + { + certId: cert.id, + encryptedPrivateKey + }, + tx + ); + }); + + return { + certificate: leafCert.toString("pem"), + certificateChain: certificateChainPem, + issuingCaCertificate, + privateKey: skLeaf, + serialNumber, + ca, + subscriber + }; + }; + + const signSubscriberCert = async ({ + subscriberName, + projectId, + csr, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TSignPkiSubscriberCertDTO) => { + const appCfg = getConfig(); + const subscriber = await pkiSubscriberDAL.findOne({ + name: subscriberName, + projectId + }); + if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` }); + if (!subscriber.caId) throw new BadRequestError({ message: "Subscriber does not have an assigned issuing CA" }); + + const ca = await certificateAuthorityDAL.findById(subscriber.caId); + if (!ca) throw new NotFoundError({ message: `CA with ID '${subscriber.caId}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: ca.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPkiSubscriberActions.IssueCert, + subject(ProjectPermissionSub.PkiSubscribers, { + name: subscriber.name + }) + ); + + if (subscriber.status !== PkiSubscriberStatus.ACTIVE) + throw new BadRequestError({ message: "Subscriber is not active" }); + if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" }); + if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + if (ca.requireTemplateForIssuance) { + throw new BadRequestError({ message: "Certificate template is required for issuance" }); + } + const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); + + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); + + const decryptedCaCert = await kmsDecryptor({ + cipherTextBlob: caCert.encryptedCertificate + }); + + const caCertObj = new x509.X509Certificate(decryptedCaCert); + const notBeforeDate = new Date(); + const notAfterDate = new Date(new Date().getTime() + ms(subscriber.ttl)); + const caCertNotBeforeDate = new Date(caCertObj.notBefore); + const caCertNotAfterDate = new Date(caCertObj.notAfter); + + // check not before constraint + if (notBeforeDate < caCertNotBeforeDate) { + throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" }); + } + + // check not after constraint + if (notAfterDate > caCertNotAfterDate) { + throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); + } + + const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); + + const csrObj = new x509.Pkcs10CertificateRequest(csr); + + const dn = parseDistinguishedName(csrObj.subject); + const cn = dn.commonName; + if (cn !== subscriber.commonName) { + throw new BadRequestError({ message: "Common name (CN) in the CSR does not match the subscriber's common name" }); + } + + const { caPrivateKey, caSecret } = await getCaCredentials({ + caId: ca.id, + certificateAuthorityDAL, + certificateAuthoritySecretDAL, + projectDAL, + kmsService + }); + + const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); + const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; + const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`; + + const extensions: x509.Extension[] = [ + new x509.BasicConstraintsExtension(false), + await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), + await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey), + new x509.CRLDistributionPointsExtension([distributionPointUrl]), + new x509.AuthorityInfoAccessExtension({ + caIssuers: new x509.GeneralName("url", caIssuerUrl) + }), + new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy + ]; + + // handle key usages + const csrKeyUsageExtension = csrObj.getExtension("2.5.29.15") as x509.KeyUsagesExtension; + let csrKeyUsages: CertKeyUsage[] = []; + if (csrKeyUsageExtension) { + csrKeyUsages = Object.values(CertKeyUsage).filter( + (keyUsage) => (x509.KeyUsageFlags[keyUsage] & csrKeyUsageExtension.usages) !== 0 + ); + } + + const selectedKeyUsages = subscriber.keyUsages as CertKeyUsage[]; + + if (csrKeyUsages.some((keyUsage) => !selectedKeyUsages.includes(keyUsage))) { + throw new BadRequestError({ + message: "Invalid key usage value based on subscriber's specified key usages" + }); + } + + const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0); + if (keyUsagesBitValue) { + extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true)); + } + + // handle extended key usages + const csrExtendedKeyUsageExtension = csrObj.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension; + let csrExtendedKeyUsages: CertExtendedKeyUsage[] = []; + if (csrExtendedKeyUsageExtension) { + csrExtendedKeyUsages = csrExtendedKeyUsageExtension.usages.map( + (ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string] + ); + } + + const selectedExtendedKeyUsages = subscriber.extendedKeyUsages as CertExtendedKeyUsage[]; + if (csrExtendedKeyUsages.some((eku) => !selectedExtendedKeyUsages.includes(eku))) { + throw new BadRequestError({ + message: "Invalid extended key usage value based on subscriber's specified extended key usages" + }); + } + + if (selectedExtendedKeyUsages.length) { + extensions.push( + new x509.ExtendedKeyUsageExtension( + selectedExtendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku]), + true + ) + ); + } + + // attempt to read from CSR if altNames is not explicitly provided + let altNamesArray: { + type: "email" | "dns"; + value: string; + }[] = []; + + const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17"); + if (sanExtension) { + const sanNames = new x509.GeneralNames(sanExtension.value); + + altNamesArray = sanNames.items + .filter((value) => value.type === "email" || value.type === "dns") + .map((name) => ({ + type: name.type as "email" | "dns", + value: name.value + })); + } + + if ( + altNamesArray + .map((altName) => altName.value) + .some((altName) => !subscriber.subjectAlternativeNames.includes(altName)) + ) { + throw new BadRequestError({ + message: "Invalid subject alternative name based on subscriber's specified subject alternative names" + }); + } + + if (altNamesArray.length) { + const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); + extensions.push(altNamesExtension); + } + + const serialNumber = createSerialNumber(); + const leafCert = await x509.X509CertificateGenerator.create({ + serialNumber, + subject: csrObj.subject, + issuer: caCertObj.subject, + notBefore: notBeforeDate, + notAfter: notAfterDate, + signingKey: caPrivateKey, + publicKey: csrObj.publicKey, + signingAlgorithm: alg, + extensions + }); + + const kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); + const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({ + plainText: Buffer.from(new Uint8Array(leafCert.rawData)) + }); + + const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ + caCertId: ca.activeCaCertId, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + projectDAL, + kmsService + }); + + const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim(); + + const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({ + plainText: Buffer.from(certificateChainPem) + }); + + await certificateDAL.transaction(async (tx) => { + const cert = await certificateDAL.create( + { + caId: ca.id, + caCertId: caCert.id, + pkiSubscriberId: subscriber.id, + status: CertStatus.ACTIVE, + friendlyName: subscriber.commonName, + commonName: subscriber.commonName, + altNames: subscriber.subjectAlternativeNames.join(","), + serialNumber, + notBefore: notBeforeDate, + notAfter: notAfterDate, + keyUsages: selectedKeyUsages, + extendedKeyUsages: selectedExtendedKeyUsages + }, + tx + ); + + await certificateBodyDAL.create( + { + certId: cert.id, + encryptedCertificate, + encryptedCertificateChain + }, + tx + ); + + return cert; + }); + + return { + certificate: leafCert.toString("pem"), + certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(), + issuingCaCertificate, + serialNumber, + ca, + commonName: subscriber.commonName, + subscriber + }; + }; + + const listSubscriberCerts = async ({ + subscriberName, + projectId, + offset, + limit, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TListPkiSubscriberCertsDTO) => { + const subscriber = await pkiSubscriberDAL.findOne({ + name: subscriberName, + projectId + }); + if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: subscriber.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPkiSubscriberActions.ListCerts, + subject(ProjectPermissionSub.PkiSubscribers, { + name: subscriber.name + }) + ); + + const certificates = await certificateDAL.find( + { + pkiSubscriberId: subscriber.id + }, + { offset, limit, sort: [["updatedAt", "desc"]] } + ); + + const count = await certificateDAL.countCertificatesForPkiSubscriber(subscriber.id); + + return { + certificates, + totalCount: count + }; + }; + + return { + createSubscriber, + getSubscriber, + updateSubscriber, + deleteSubscriber, + issueSubscriberCert, + signSubscriberCert, + listSubscriberCerts + }; +}; diff --git a/backend/src/services/pki-subscriber/pki-subscriber-types.ts b/backend/src/services/pki-subscriber/pki-subscriber-types.ts new file mode 100644 index 000000000..690148f16 --- /dev/null +++ b/backend/src/services/pki-subscriber/pki-subscriber-types.ts @@ -0,0 +1,54 @@ +import { TProjectPermission } from "@app/lib/types"; + +import { CertExtendedKeyUsage, CertKeyUsage } from "../certificate/certificate-types"; + +export enum PkiSubscriberStatus { + ACTIVE = "active", + DISABLED = "disabled" +} + +export type TCreatePkiSubscriberDTO = { + caId: string; + name: string; + commonName: string; + status: PkiSubscriberStatus; + ttl: string; + subjectAlternativeNames: string[]; + keyUsages: CertKeyUsage[]; + extendedKeyUsages: CertExtendedKeyUsage[]; +} & TProjectPermission; + +export type TGetPkiSubscriberDTO = { + subscriberName: string; +} & TProjectPermission; + +export type TUpdatePkiSubscriberDTO = { + subscriberName: string; + caId?: string; + name?: string; + commonName?: string; + status?: PkiSubscriberStatus; + ttl?: string; + subjectAlternativeNames?: string[]; + keyUsages?: CertKeyUsage[]; + extendedKeyUsages?: CertExtendedKeyUsage[]; +} & TProjectPermission; + +export type TDeletePkiSubscriberDTO = { + subscriberName: string; +} & TProjectPermission; + +export type TIssuePkiSubscriberCertDTO = { + subscriberName: string; +} & TProjectPermission; + +export type TSignPkiSubscriberCertDTO = { + subscriberName: string; + csr: string; +} & TProjectPermission; + +export type TListPkiSubscriberCertsDTO = { + subscriberName: string; + offset: number; + limit: number; +} & TProjectPermission; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 7ab45baa7..8cfa20697 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -15,6 +15,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { ProjectPermissionActions, ProjectPermissionCertificateActions, + ProjectPermissionPkiSubscriberActions, ProjectPermissionSecretActions, ProjectPermissionSshHostActions, ProjectPermissionSub @@ -35,6 +36,7 @@ import { groupBy } from "@app/lib/fn"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TProjectPermission } from "@app/lib/types"; import { TQueueServiceFactory } from "@app/queue"; +import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; import { ActorType } from "../auth/auth-type"; import { TCertificateDALFactory } from "../certificate/certificate-dal"; @@ -86,6 +88,7 @@ import { TListProjectCasDTO, TListProjectCertificateTemplatesDTO, TListProjectCertsDTO, + TListProjectPkiSubscribersDTO, TListProjectsDTO, TListProjectSshCasDTO, TListProjectSshCertificatesDTO, @@ -145,6 +148,7 @@ type TProjectServiceFactoryDep = { "findById" | "findByIdWithWorkflowIntegrationDetails" >; projectUserMembershipRoleDAL: Pick; + pkiSubscriberDAL: Pick; certificateAuthorityDAL: Pick; certificateDAL: Pick; certificateTemplateDAL: Pick; @@ -207,6 +211,7 @@ export const projectServiceFactory = ({ certificateTemplateDAL, pkiCollectionDAL, pkiAlertDAL, + pkiSubscriberDAL, sshCertificateAuthorityDAL, sshCertificateAuthoritySecretDAL, sshCertificateDAL, @@ -1057,6 +1062,45 @@ export const projectServiceFactory = ({ }; }; + /** + * Return list of PKI subscribers for project + */ + const listProjectPkiSubscribers = async ({ + actorId, + actorOrgId, + actorAuthMethod, + actor, + projectId + }: TListProjectPkiSubscribersDTO) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + const allowedSubscribers = []; + + // (dangtony98): room to optimize + const subscribers = await pkiSubscriberDAL.find({ projectId }); + + for (const subscriber of subscribers) { + const canRead = permission.can( + ProjectPermissionPkiSubscriberActions.Read, + subject(ProjectPermissionSub.PkiSubscribers, { + name: subscriber.name + }) + ); + if (canRead) { + allowedSubscribers.push(subscriber); + } + } + + return allowedSubscribers; + }; + /** * Return list of certificate templates for project */ @@ -1156,17 +1200,15 @@ export const projectServiceFactory = ({ const hosts = await sshHostDAL.findSshHostsWithLoginMappings(projectId); for (const host of hosts) { - try { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSshHostActions.Read, - subject(ProjectPermissionSub.SshHosts, { - hostname: host.hostname - }) - ); + const canRead = permission.can( + ProjectPermissionSshHostActions.Read, + subject(ProjectPermissionSub.SshHosts, { + hostname: host.hostname + }) + ); + if (canRead) { allowedHosts.push(host); - } catch { - // intentionally ignore projects where user lacks access } } @@ -1930,6 +1972,7 @@ export const projectServiceFactory = ({ listProjectSshCas, listProjectSshHosts, listProjectSshHostGroups, + listProjectPkiSubscribers, listProjectSshCertificates, listProjectSshCertificateTemplates, updateVersionLimit, diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index dc26d2357..9f74e123c 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -155,6 +155,7 @@ export type TListProjectCertificateTemplatesDTO = TProjectPermission; export type TListProjectSshCasDTO = TProjectPermission; export type TListProjectSshHostsDTO = TProjectPermission; export type TListProjectSshCertificateTemplatesDTO = TProjectPermission; +export type TListProjectPkiSubscribersDTO = TProjectPermission; export type TListProjectSshCertificatesDTO = { offset: number; limit: number; diff --git a/backend/src/services/secret-sync/oci-vault/index.ts b/backend/src/services/secret-sync/oci-vault/index.ts new file mode 100644 index 000000000..cee990de4 --- /dev/null +++ b/backend/src/services/secret-sync/oci-vault/index.ts @@ -0,0 +1,4 @@ +export * from "./oci-vault-sync-constants"; +export * from "./oci-vault-sync-fns"; +export * from "./oci-vault-sync-schemas"; +export * from "./oci-vault-sync-types"; diff --git a/backend/src/services/secret-sync/oci-vault/oci-vault-sync-constants.ts b/backend/src/services/secret-sync/oci-vault/oci-vault-sync-constants.ts new file mode 100644 index 000000000..9e2aad056 --- /dev/null +++ b/backend/src/services/secret-sync/oci-vault/oci-vault-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const OCI_VAULT_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "OCI Vault", + destination: SecretSync.OCIVault, + connection: AppConnection.OCI, + canImportSecrets: true +}; diff --git a/backend/src/services/secret-sync/oci-vault/oci-vault-sync-fns.ts b/backend/src/services/secret-sync/oci-vault/oci-vault-sync-fns.ts new file mode 100644 index 000000000..95045e1e9 --- /dev/null +++ b/backend/src/services/secret-sync/oci-vault/oci-vault-sync-fns.ts @@ -0,0 +1,292 @@ +import { secrets, vault } from "oci-sdk"; + +import { delay } from "@app/lib/delay"; +import { getOCIProvider } from "@app/services/app-connection/oci"; +import { + TCreateOCIVaultVariable, + TDeleteOCIVaultVariable, + TOCIVaultListVariables, + TOCIVaultSyncWithCredentials, + TUnmarkOCIVaultVariableFromDeletion, + TUpdateOCIVaultVariable +} from "@app/services/secret-sync/oci-vault/oci-vault-sync-types"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; + +const listOCIVaultVariables = async ({ provider, compartmentId, vaultId, onlyActive }: TOCIVaultListVariables) => { + const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider }); + const secretsClient = new secrets.SecretsClient({ authenticationDetailsProvider: provider }); + + const secretsRes = await vaultsClient.listSecrets({ + compartmentId, + vaultId, + lifecycleState: onlyActive ? vault.models.SecretSummary.LifecycleState.Active : undefined + }); + + const result: Record = {}; + + for await (const s of secretsRes.items) { + let secretValue = ""; + + if (s.lifecycleState === vault.models.SecretSummary.LifecycleState.Active) { + const secretBundle = await secretsClient.getSecretBundle({ + secretId: s.id + }); + + secretValue = Buffer.from(secretBundle.secretBundle.secretBundleContent?.content || "", "base64").toString( + "utf-8" + ); + } + + result[s.secretName] = { + ...s, + name: s.secretName, + value: secretValue + }; + } + + return result; +}; + +const createOCIVaultVariable = async ({ + provider, + compartmentId, + vaultId, + keyId, + name, + value +}: TCreateOCIVaultVariable) => { + if (!value) return; + + const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider }); + + return vaultsClient.createSecret({ + createSecretDetails: { + compartmentId, + vaultId, + keyId, + secretName: name, + enableAutoGeneration: false, + secretContent: { + content: Buffer.from(value).toString("base64"), + contentType: "BASE64" + } + } + }); +}; + +const updateOCIVaultVariable = async ({ provider, secretId, value }: TUpdateOCIVaultVariable) => { + if (!value) return; + + const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider }); + + return vaultsClient.updateSecret({ + secretId, + updateSecretDetails: { + enableAutoGeneration: false, + secretContent: { + content: Buffer.from(value).toString("base64"), + contentType: "BASE64" + } + } + }); +}; + +const deleteOCIVaultVariable = async ({ provider, secretId }: TDeleteOCIVaultVariable) => { + const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider }); + + // Schedule a secret deletion 7 days from now. OCI Vault requires a MINIMUM buffer period of 7 days + return vaultsClient.scheduleSecretDeletion({ + secretId, + scheduleSecretDeletionDetails: { + timeOfDeletion: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) + } + }); +}; + +const unmarkOCIVaultVariableFromDeletion = async ({ provider, secretId }: TUnmarkOCIVaultVariableFromDeletion) => { + const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider }); + + return vaultsClient.cancelSecretDeletion({ + secretId + }); +}; + +export const OCIVaultSyncFns = { + syncSecrets: async (secretSync: TOCIVaultSyncWithCredentials, secretMap: TSecretMap) => { + const { + connection, + destinationConfig: { compartmentOcid, vaultOcid, keyOcid } + } = secretSync; + + const provider = await getOCIProvider(connection); + const variables = await listOCIVaultVariables({ provider, compartmentId: compartmentOcid, vaultId: vaultOcid }); + + // Throw an error if any keys are updating in OCI vault to prevent skipped updates + if ( + Object.entries(variables).some( + ([, secret]) => + secret.lifecycleState === vault.models.SecretSummary.LifecycleState.Updating || + secret.lifecycleState === vault.models.SecretSummary.LifecycleState.CancellingDeletion || + secret.lifecycleState === vault.models.SecretSummary.LifecycleState.Creating || + secret.lifecycleState === vault.models.SecretSummary.LifecycleState.Deleting || + secret.lifecycleState === vault.models.SecretSummary.LifecycleState.SchedulingDeletion + ) + ) { + throw new SecretSyncError({ + error: "Cannot sync while keys are updating in OCI Vault." + }); + } + + // Create secrets + for await (const entry of Object.entries(secretMap)) { + const [key, { value }] = entry; + + // skip secrets that don't have a value set + if (!value) { + // eslint-disable-next-line no-continue + continue; + } + + const existingVariable = Object.values(variables).find((v) => v.secretName === key); + + if (!existingVariable) { + try { + await createOCIVaultVariable({ + compartmentId: compartmentOcid, + vaultId: vaultOcid, + provider, + keyId: keyOcid, + name: key, + value + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } else if (existingVariable.lifecycleState === vault.models.SecretSummary.LifecycleState.PendingDeletion) { + // If a secret exists but is pending deletion, cancel the deletion and update the secret + await unmarkOCIVaultVariableFromDeletion({ + provider, + compartmentId: compartmentOcid, + vaultId: vaultOcid, + secretId: existingVariable.id + }); + + const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider }); + const MAX_RETRIES = 10; + + for (let i = 0; i < MAX_RETRIES; i += 1) { + // eslint-disable-next-line no-await-in-loop + await delay(5000); + + // eslint-disable-next-line no-await-in-loop + const secret = await vaultsClient.getSecret({ + secretId: existingVariable.id + }); + + if (secret.secret.lifecycleState === vault.models.SecretSummary.LifecycleState.Active) { + // eslint-disable-next-line no-await-in-loop + await updateOCIVaultVariable({ + provider, + compartmentId: compartmentOcid, + vaultId: vaultOcid, + secretId: existingVariable.id, + value + }); + break; + } + + if (i === MAX_RETRIES - 1) { + throw new SecretSyncError({ + error: "Failed to update secret after cancelling deletion.", + secretKey: key + }); + } + } + } + } + + // Update and delete secrets + for await (const [key, variable] of Object.entries(variables)) { + // Only update / delete active secrets + if (variable.lifecycleState === vault.models.SecretSummary.LifecycleState.Active) { + if (key in secretMap && secretMap[key].value.length > 0) { + if (variable.value !== secretMap[key].value) { + try { + await updateOCIVaultVariable({ + compartmentId: compartmentOcid, + vaultId: vaultOcid, + provider, + secretId: variable.id, + value: secretMap[key].value + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + } else if (!secretSync.syncOptions.disableSecretDeletion) { + try { + await deleteOCIVaultVariable({ + compartmentId: compartmentOcid, + vaultId: vaultOcid, + provider, + secretId: variable.id + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + } + } + }, + removeSecrets: async (secretSync: TOCIVaultSyncWithCredentials, secretMap: TSecretMap) => { + const { + connection, + destinationConfig: { compartmentOcid, vaultOcid } + } = secretSync; + + const provider = await getOCIProvider(connection); + const variables = await listOCIVaultVariables({ + provider, + compartmentId: compartmentOcid, + vaultId: vaultOcid, + onlyActive: true + }); + + for await (const [key, variable] of Object.entries(variables)) { + if (key in secretMap) { + try { + await deleteOCIVaultVariable({ + compartmentId: compartmentOcid, + vaultId: vaultOcid, + provider, + secretId: variable.id + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + } + }, + getSecrets: async (secretSync: TOCIVaultSyncWithCredentials) => { + const { + connection, + destinationConfig: { compartmentOcid, vaultOcid } + } = secretSync; + + const provider = await getOCIProvider(connection); + return listOCIVaultVariables({ provider, compartmentId: compartmentOcid, vaultId: vaultOcid, onlyActive: true }); + } +}; diff --git a/backend/src/services/secret-sync/oci-vault/oci-vault-sync-schemas.ts b/backend/src/services/secret-sync/oci-vault/oci-vault-sync-schemas.ts new file mode 100644 index 000000000..84a58bc8a --- /dev/null +++ b/backend/src/services/secret-sync/oci-vault/oci-vault-sync-schemas.ts @@ -0,0 +1,70 @@ +import RE2 from "re2"; +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const OCIVaultSyncDestinationConfigSchema = z.object({ + compartmentOcid: z + .string() + .trim() + .min(1, "Compartment OCID required") + .refine( + (val) => new RE2("^ocid1\\.(tenancy|compartment)\\.oc1\\..+$").test(val), + "Invalid Compartment OCID format. Must start with ocid1.tenancy.oc1. or ocid1.compartment.oc1." + ) + .describe(SecretSyncs.DESTINATION_CONFIG.OCI_VAULT.compartmentOcid), + vaultOcid: z + .string() + .trim() + .min(1, "Vault OCID required") + .refine( + (val) => new RE2("^ocid1\\.vault\\.oc1\\..+$").test(val), + "Invalid Vault OCID format. Must start with ocid1.vault.oc1." + ) + .describe(SecretSyncs.DESTINATION_CONFIG.OCI_VAULT.vaultOcid), + keyOcid: z + .string() + .trim() + .min(1, "Key OCID required") + .refine( + (val) => new RE2("^ocid1\\.key\\.oc1\\..+$").test(val), + "Invalid Key OCID format. Must start with ocid1.key.oc1." + ) + .describe(SecretSyncs.DESTINATION_CONFIG.OCI_VAULT.keyOcid) +}); + +const OCIVaultSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +export const OCIVaultSyncSchema = BaseSecretSyncSchema(SecretSync.OCIVault, OCIVaultSyncOptionsConfig).extend({ + destination: z.literal(SecretSync.OCIVault), + destinationConfig: OCIVaultSyncDestinationConfigSchema +}); + +export const CreateOCIVaultSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.OCIVault, + OCIVaultSyncOptionsConfig +).extend({ + destinationConfig: OCIVaultSyncDestinationConfigSchema +}); + +export const UpdateOCIVaultSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.OCIVault, + OCIVaultSyncOptionsConfig +).extend({ + destinationConfig: OCIVaultSyncDestinationConfigSchema.optional() +}); + +export const OCIVaultSyncListItemSchema = z.object({ + name: z.literal("OCI Vault"), + connection: z.literal(AppConnection.OCI), + destination: z.literal(SecretSync.OCIVault), + canImportSecrets: z.literal(true) +}); diff --git a/backend/src/services/secret-sync/oci-vault/oci-vault-sync-types.ts b/backend/src/services/secret-sync/oci-vault/oci-vault-sync-types.ts new file mode 100644 index 000000000..c040cd0c0 --- /dev/null +++ b/backend/src/services/secret-sync/oci-vault/oci-vault-sync-types.ts @@ -0,0 +1,48 @@ +import { SimpleAuthenticationDetailsProvider } from "oci-sdk"; +import { z } from "zod"; + +import { TOCIConnection } from "@app/services/app-connection/oci"; + +import { CreateOCIVaultSyncSchema, OCIVaultSyncListItemSchema, OCIVaultSyncSchema } from "./oci-vault-sync-schemas"; + +export type TOCIVaultSync = z.infer; + +export type TOCIVaultSyncInput = z.infer; + +export type TOCIVaultSyncListItem = z.infer; + +export type TOCIVaultSyncWithCredentials = TOCIVaultSync & { + connection: TOCIConnection; +}; + +export type TOCIVaultVariable = { + id: string; + name: string; + value: string; +}; + +export type TOCIVaultListVariables = { + provider: SimpleAuthenticationDetailsProvider; + compartmentId: string; + vaultId: string; + onlyActive?: boolean; // Whether to filter for only active secrets. Removes deleted / scheduled for deletion secrets +}; + +export type TCreateOCIVaultVariable = TOCIVaultListVariables & { + keyId: string; + name: string; + value: string; +}; + +export type TUpdateOCIVaultVariable = TOCIVaultListVariables & { + secretId: string; + value: string; +}; + +export type TDeleteOCIVaultVariable = TOCIVaultListVariables & { + secretId: string; +}; + +export type TUnmarkOCIVaultVariableFromDeletion = TOCIVaultListVariables & { + secretId: string; +}; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index 9d59ebb76..a0982c5b6 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -12,7 +12,8 @@ export enum SecretSync { Vercel = "vercel", Windmill = "windmill", HCVault = "hashicorp-vault", - TeamCity = "teamcity" + TeamCity = "teamcity", + OCIVault = "oci-vault" } export enum SecretSyncInitialSyncBehavior { diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 5749852d7..d029a000a 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -28,6 +28,7 @@ import { GcpSyncFns } from "./gcp/gcp-sync-fns"; import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault"; import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec"; import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns"; +import { OCI_VAULT_SYNC_LIST_OPTION, OCIVaultSyncFns } from "./oci-vault"; import { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity"; import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud"; import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel"; @@ -47,7 +48,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION, [SecretSync.Windmill]: WINDMILL_SYNC_LIST_OPTION, [SecretSync.HCVault]: HC_VAULT_SYNC_LIST_OPTION, - [SecretSync.TeamCity]: TEAMCITY_SYNC_LIST_OPTION + [SecretSync.TeamCity]: TEAMCITY_SYNC_LIST_OPTION, + [SecretSync.OCIVault]: OCI_VAULT_SYNC_LIST_OPTION }; export const listSecretSyncOptions = () => { @@ -148,6 +150,8 @@ export const SecretSyncFns = { return HCVaultSyncFns.syncSecrets(secretSync, secretMap); case SecretSync.TeamCity: return TeamCitySyncFns.syncSecrets(secretSync, secretMap); + case SecretSync.OCIVault: + return OCIVaultSyncFns.syncSecrets(secretSync, secretMap); default: throw new Error( `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -213,6 +217,9 @@ export const SecretSyncFns = { case SecretSync.TeamCity: secretMap = await TeamCitySyncFns.getSecrets(secretSync); break; + case SecretSync.OCIVault: + secretMap = await OCIVaultSyncFns.getSecrets(secretSync); + break; default: throw new Error( `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -270,6 +277,8 @@ export const SecretSyncFns = { return HCVaultSyncFns.removeSecrets(secretSync, secretMap); case SecretSync.TeamCity: return TeamCitySyncFns.removeSecrets(secretSync, secretMap); + case SecretSync.OCIVault: + return OCIVaultSyncFns.removeSecrets(secretSync, secretMap); default: throw new Error( `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index c6d7adc8c..21cb912b4 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -15,7 +15,8 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.Vercel]: "Vercel", [SecretSync.Windmill]: "Windmill", [SecretSync.HCVault]: "Hashicorp Vault", - [SecretSync.TeamCity]: "TeamCity" + [SecretSync.TeamCity]: "TeamCity", + [SecretSync.OCIVault]: "OCI Vault" }; export const SECRET_SYNC_CONNECTION_MAP: Record = { @@ -32,5 +33,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.Vercel]: AppConnection.Vercel, [SecretSync.Windmill]: AppConnection.Windmill, [SecretSync.HCVault]: AppConnection.HCVault, - [SecretSync.TeamCity]: AppConnection.TeamCity + [SecretSync.TeamCity]: AppConnection.TeamCity, + [SecretSync.OCIVault]: AppConnection.OCI }; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index e88174cc6..64d027e18 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -67,6 +67,7 @@ import { THumanitecSyncListItem, THumanitecSyncWithCredentials } from "./humanitec"; +import { TOCIVaultSync, TOCIVaultSyncInput, TOCIVaultSyncListItem, TOCIVaultSyncWithCredentials } from "./oci-vault"; import { TTeamCitySync, TTeamCitySyncInput, @@ -95,7 +96,8 @@ export type TSecretSync = | TVercelSync | TWindmillSync | THCVaultSync - | TTeamCitySync; + | TTeamCitySync + | TOCIVaultSync; export type TSecretSyncWithCredentials = | TAwsParameterStoreSyncWithCredentials @@ -111,7 +113,8 @@ export type TSecretSyncWithCredentials = | TVercelSyncWithCredentials | TWindmillSyncWithCredentials | THCVaultSyncWithCredentials - | TTeamCitySyncWithCredentials; + | TTeamCitySyncWithCredentials + | TOCIVaultSyncWithCredentials; export type TSecretSyncInput = | TAwsParameterStoreSyncInput @@ -127,7 +130,8 @@ export type TSecretSyncInput = | TVercelSyncInput | TWindmillSyncInput | THCVaultSyncInput - | TTeamCitySyncInput; + | TTeamCitySyncInput + | TOCIVaultSyncInput; export type TSecretSyncListItem = | TAwsParameterStoreSyncListItem @@ -143,7 +147,8 @@ export type TSecretSyncListItem = | TVercelSyncListItem | TWindmillSyncListItem | THCVaultSyncListItem - | TTeamCitySyncListItem; + | TTeamCitySyncListItem + | TOCIVaultSyncListItem; export type TSyncOptionsConfig = { canImportSecrets: boolean; diff --git a/backend/src/services/telemetry/telemetry-types.ts b/backend/src/services/telemetry/telemetry-types.ts index 9e046cdbd..a370d0332 100644 --- a/backend/src/services/telemetry/telemetry-types.ts +++ b/backend/src/services/telemetry/telemetry-types.ts @@ -189,6 +189,7 @@ export type TSignCertificateEvent = { properties: { caId?: string; certificateTemplateId?: string; + subscriberId?: string; commonName: string; userAgent?: string; }; @@ -199,6 +200,7 @@ export type TIssueCertificateEvent = { properties: { caId?: string; certificateTemplateId?: string; + subscriberId?: string; commonName: string; userAgent?: string; }; diff --git a/docs/api-reference/endpoints/app-connections/oci/available.mdx b/docs/api-reference/endpoints/app-connections/oci/available.mdx new file mode 100644 index 000000000..19d83e5b7 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/oci/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/oci/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/oci/create.mdx b/docs/api-reference/endpoints/app-connections/oci/create.mdx new file mode 100644 index 000000000..e15877121 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/oci/create.mdx @@ -0,0 +1,8 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/oci" +--- + + + Check out the configuration docs for [OCI Connections](/integrations/app-connections/oci) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/app-connections/oci/delete.mdx b/docs/api-reference/endpoints/app-connections/oci/delete.mdx new file mode 100644 index 000000000..990700885 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/oci/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/oci/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/oci/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/oci/get-by-id.mdx new file mode 100644 index 000000000..a7541b227 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/oci/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/oci/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/oci/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/oci/get-by-name.mdx new file mode 100644 index 000000000..1c920e14f --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/oci/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/oci/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/oci/list.mdx b/docs/api-reference/endpoints/app-connections/oci/list.mdx new file mode 100644 index 000000000..ba4430073 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/oci/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/oci" +--- diff --git a/docs/api-reference/endpoints/app-connections/oci/update.mdx b/docs/api-reference/endpoints/app-connections/oci/update.mdx new file mode 100644 index 000000000..c012009a2 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/oci/update.mdx @@ -0,0 +1,8 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/oci/{connectionId}" +--- + + + Check out the configuration docs for [OCI Connections](/integrations/app-connections/oci) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/pki/subscribers/create.mdx b/docs/api-reference/endpoints/pki/subscribers/create.mdx new file mode 100644 index 000000000..14a53b7fa --- /dev/null +++ b/docs/api-reference/endpoints/pki/subscribers/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/pki/subscribers" +--- diff --git a/docs/api-reference/endpoints/pki/subscribers/delete.mdx b/docs/api-reference/endpoints/pki/subscribers/delete.mdx new file mode 100644 index 000000000..5975b89e9 --- /dev/null +++ b/docs/api-reference/endpoints/pki/subscribers/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/pki/subscribers/{subscriberName}" +--- diff --git a/docs/api-reference/endpoints/pki/subscribers/issue-cert.mdx b/docs/api-reference/endpoints/pki/subscribers/issue-cert.mdx new file mode 100644 index 000000000..be57ab01b --- /dev/null +++ b/docs/api-reference/endpoints/pki/subscribers/issue-cert.mdx @@ -0,0 +1,4 @@ +--- +title: "Issue Certificate" +openapi: "POST /api/v1/pki/subscribers/{subscriberName}/issue-cert" +--- diff --git a/docs/api-reference/endpoints/pki/subscribers/list-certs.mdx b/docs/api-reference/endpoints/pki/subscribers/list-certs.mdx new file mode 100644 index 000000000..3a4607303 --- /dev/null +++ b/docs/api-reference/endpoints/pki/subscribers/list-certs.mdx @@ -0,0 +1,4 @@ +--- +title: "List Certificates" +openapi: "GET /api/v1/pki/subscribers/{subscriberName}/certificates" +--- diff --git a/docs/api-reference/endpoints/pki/subscribers/read.mdx b/docs/api-reference/endpoints/pki/subscribers/read.mdx new file mode 100644 index 000000000..0d223217d --- /dev/null +++ b/docs/api-reference/endpoints/pki/subscribers/read.mdx @@ -0,0 +1,4 @@ +--- +title: "Retrieve" +openapi: "GET /api/v1/pki/subscribers/{subscriberName}" +--- diff --git a/docs/api-reference/endpoints/pki/subscribers/sign-cert.mdx b/docs/api-reference/endpoints/pki/subscribers/sign-cert.mdx new file mode 100644 index 000000000..d31d30239 --- /dev/null +++ b/docs/api-reference/endpoints/pki/subscribers/sign-cert.mdx @@ -0,0 +1,4 @@ +--- +title: "Sign Certificate" +openapi: "POST /api/v1/pki/subscribers/{subscriberName}/sign-certificate" +--- diff --git a/docs/api-reference/endpoints/pki/subscribers/update.mdx b/docs/api-reference/endpoints/pki/subscribers/update.mdx new file mode 100644 index 000000000..5b62cbe7d --- /dev/null +++ b/docs/api-reference/endpoints/pki/subscribers/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/pki/subscribers/{subscriberName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/oci-vault/create.mdx b/docs/api-reference/endpoints/secret-syncs/oci-vault/create.mdx new file mode 100644 index 000000000..fa3ac2738 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/oci-vault/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/oci-vault" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/oci-vault/delete.mdx b/docs/api-reference/endpoints/secret-syncs/oci-vault/delete.mdx new file mode 100644 index 000000000..81f208308 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/oci-vault/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/oci-vault/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/oci-vault/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/oci-vault/get-by-id.mdx new file mode 100644 index 000000000..52b3201dc --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/oci-vault/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/oci-vault/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/oci-vault/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/oci-vault/get-by-name.mdx new file mode 100644 index 000000000..eabc8794c --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/oci-vault/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/oci-vault/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/oci-vault/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/oci-vault/import-secrets.mdx new file mode 100644 index 000000000..27ca686d6 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/oci-vault/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/oci-vault/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/oci-vault/list.mdx b/docs/api-reference/endpoints/secret-syncs/oci-vault/list.mdx new file mode 100644 index 000000000..88cd2a44a --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/oci-vault/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/oci-vault" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/oci-vault/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/oci-vault/remove-secrets.mdx new file mode 100644 index 000000000..e98e7140e --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/oci-vault/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/oci-vault/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/oci-vault/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/oci-vault/sync-secrets.mdx new file mode 100644 index 000000000..38ea4331c --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/oci-vault/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/oci-vault/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/oci-vault/update.mdx b/docs/api-reference/endpoints/secret-syncs/oci-vault/update.mdx new file mode 100644 index 000000000..06f1d9d1c --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/oci-vault/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/oci-vault/{syncId}" +--- diff --git a/docs/api-reference/endpoints/ssh/groups/add-host.mdx b/docs/api-reference/endpoints/ssh/groups/add-host.mdx index 9f903eccd..77257cd40 100644 --- a/docs/api-reference/endpoints/ssh/groups/add-host.mdx +++ b/docs/api-reference/endpoints/ssh/groups/add-host.mdx @@ -1,4 +1,4 @@ --- title: "Add Host" -openapi: "POST /api/v1/ssh/host-groups/{sshHostGroupId}/hosts" +openapi: "POST /api/v1/ssh/host-groups/{sshHostGroupId}/hosts/{hostId}" --- diff --git a/docs/api-reference/endpoints/ssh/groups/remove-host.mdx b/docs/api-reference/endpoints/ssh/groups/remove-host.mdx index 6933e5c9f..b1de7f4ae 100644 --- a/docs/api-reference/endpoints/ssh/groups/remove-host.mdx +++ b/docs/api-reference/endpoints/ssh/groups/remove-host.mdx @@ -1,4 +1,4 @@ --- title: "Remove Host" -openapi: "DELETE /api/v1/ssh/host-groups/{sshHostGroupId}/hosts/{sshHostId}" +openapi: "DELETE /api/v1/ssh/host-groups/{sshHostGroupId}/hosts/{hostId}" --- diff --git a/docs/api-reference/endpoints/ssh/hosts/list-my.mdx b/docs/api-reference/endpoints/ssh/hosts/list-my.mdx index 2b7ab51c0..6ccc4e325 100644 --- a/docs/api-reference/endpoints/ssh/hosts/list-my.mdx +++ b/docs/api-reference/endpoints/ssh/hosts/list-my.mdx @@ -1,4 +1,4 @@ --- title: "List My Hosts" -openapi: "GET /api/v1/ssh/hosts/" +openapi: "GET /api/v1/ssh/hosts" --- diff --git a/docs/documentation/platform/github-org-sync.mdx b/docs/documentation/platform/github-org-sync.mdx index 00c9bf4c4..519e12db8 100644 --- a/docs/documentation/platform/github-org-sync.mdx +++ b/docs/documentation/platform/github-org-sync.mdx @@ -13,7 +13,7 @@ To enable and configure GitHub Organization Synchronization, follow these steps: - 1. Navigate to **Organization Settings** and select the **Security Tab**. + 1. Navigate to the **Single Sign-On (SSO)** page and select the **Provisioning** tab. ![config](../../images/platform/external-syncs/github-org-sync-section.png) 2. Click the **Configure** button and provide the name of your GitHub Organization. ![config-modal](../../images/platform/external-syncs/github-org-sync-config-modal.png) diff --git a/docs/documentation/platform/ldap/general.mdx b/docs/documentation/platform/ldap/general.mdx index 939eaa727..c1d062ef5 100644 --- a/docs/documentation/platform/ldap/general.mdx +++ b/docs/documentation/platform/ldap/general.mdx @@ -18,7 +18,9 @@ Prerequisites: - In Infisical, head to your Organization Settings > Security > LDAP and select **Manage**. + In Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Select **Connect** for **LDAP**. + + ![LDAP SSO Connect](../../../images/sso/connect-ldap.png) Next, input your LDAP server settings. diff --git a/docs/documentation/platform/ldap/jumpcloud.mdx b/docs/documentation/platform/ldap/jumpcloud.mdx index 39579b785..d520598d1 100644 --- a/docs/documentation/platform/ldap/jumpcloud.mdx +++ b/docs/documentation/platform/ldap/jumpcloud.mdx @@ -27,7 +27,9 @@ Prerequisites: ![LDAP JumpCloud](/images/platform/ldap/jumpcloud/ldap-jumpcloud-enable-bind-dn.png) - In Infisical, head to your Organization Settings > Security > LDAP and select **Manage**. + In Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Select **Connect** for **LDAP**. + + ![LDAP SSO Connect](../../../images/sso/connect-ldap.png) Next, input your JumpCloud LDAP server settings. diff --git a/docs/documentation/platform/pki/certificates.mdx b/docs/documentation/platform/pki/certificates.mdx index 4976b5e18..f1c434e9c 100644 --- a/docs/documentation/platform/pki/certificates.mdx +++ b/docs/documentation/platform/pki/certificates.mdx @@ -75,8 +75,8 @@ In the following steps, we explore how to issue a X.509 certificate under a CA. Here's some guidance on each field: - Friendly Name: A friendly name for the certificate; this is only for display and defaults to the common name of the certificate if left empty. - - Common Name (CN): The (common) name for the certificate like `service.acme.com`. - - Alternative Names (SANs): A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses like `app1.acme.com, app2.acme.com`. + - Common Name (CN): The common name for the certificate like `service.acme.com`. + - Alternative Names (SANs): A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be hostnames or email addresses like `app1.acme.com, app2.acme.com`. - TTL: The lifetime of the certificate in seconds. - Key Usage: The key usage extension of the certificate. - Extended Key Usage: The extended key usage extension of the certificate. @@ -240,7 +240,7 @@ openssl verify -crl_check -CAfile chain.pem -CRLfile crl.pem cert.pem ``` Note that you can also obtain the CRL from the certificate itself by -referencing the CRL distribution point extension on the certificate itself. +referencing the CRL distribution point extension on the certificate. To check a certificate against the CRL distribution point specified within it with OpenSSL, you can use the following command: diff --git a/docs/documentation/platform/pki/overview.mdx b/docs/documentation/platform/pki/overview.mdx index 259f15a5d..8ee9b113d 100644 --- a/docs/documentation/platform/pki/overview.mdx +++ b/docs/documentation/platform/pki/overview.mdx @@ -4,9 +4,10 @@ sidebarTitle: "Overview" description: "Learn how to create a Private CA hierarchy and issue X.509 certificates." --- -Infisical can be used to create a Private Certificate Authority (CA) hierarchy and issue X.509 certificates for internal use. This allows you to manage your own PKI infrastructure and issue digital certificates for services, applications, and devices. +Infisical can be used to create a Private Certificate Authority (CA) hierarchy and issue X.509 certificates for internal use. This allows you to manage your own PKI infrastructure and issue digital certificates for subscribers such as services, applications, and devices. -Infisical's internal PKI offering is split into two modules: +Infisical's PKI offering is split into three components: -- [Private CA](/documentation/platform/pki/private-ca): Infisical lets you create private CAs, including root and intermediary CAs. -- [Certificates](/documentation/platform/pki/certificates): Infisical allows you to issue X.509 certificates using the private CAs you create. +- [Certificate Authorities](/documentation/platform/pki/private-ca): Create and manage private CAs, including root and intermediate CAs. +- [Subscribers](/documentation/platform/pki/subscribers): Define and manage entities that will request X.509 certificates from CAs. This module provides a centralized view of all subscribers, enabling you to issue certificates and monitor their status. +- [Certificates](/documentation/platform/pki/certificates): Track and monitor issued X.509 certificates, maintaining a comprehensive inventory of all active and expired certificates. diff --git a/docs/documentation/platform/pki/private-ca.mdx b/docs/documentation/platform/pki/private-ca.mdx index d7f3f896c..7d7ee1220 100644 --- a/docs/documentation/platform/pki/private-ca.mdx +++ b/docs/documentation/platform/pki/private-ca.mdx @@ -7,7 +7,7 @@ description: "Learn how to create a Private CA hierarchy with Infisical." ## Concept The first step to creating your Internal PKI is to create a Private Certificate Authority (CA) hierarchy that is a structure of entities -used to issue digital certificates for services, applications, and devices. +used to issue digital certificates for your [subscribers](/documentation/platform/pki/subscribers).
@@ -24,7 +24,7 @@ graph TD A typical workflow for setting up a Private CA hierarchy consists of the following steps: -1. Configuring an Infisical root CA with details like name, validity period, and path length — This step is optional if you wish to use an external root CA. +1. Configuring an Infisical root CA with details like name, validity period, and path length — This step is optional if you wish to use an external root CA with Infisical only serving the intermediate CAs. 2. Configuring and chaining intermediate CA(s) with details like name, validity period, path length, and imported certificate to your Root CA. 3. Managing the CA lifecycle events such as CA succession. @@ -99,7 +99,7 @@ consisting of an (optional) root CA and an intermediate CA. ![pki cas](/images/platform/pki/ca/cas.png) Great! You've successfully created a Private CA hierarchy with a root CA and an intermediate CA. - Now check out the [Certificates](/documentation/platform/pki/certificates) page to learn more about how to issue X.509 certificates using the intermediate CA. + Now check out the [Subscribers](/documentation/platform/pki/subscribers) page to learn more about how to issue X.509 certificates using the intermediate CA. 2.3b. If you have an external root CA, select **External CA** for the **Parent CA Type** field. @@ -110,7 +110,7 @@ consisting of an (optional) root CA and an intermediate CA. Finally, press **Install** to import the certificate and certificate chain as part of the installation step for the intermediate CA Great! You've successfully created a Private CA hierarchy with an intermediate CA chained to an external root CA. - Now check out the [Certificates](/documentation/platform/pki/certificates) page to learn more about how to issue X.509 certificates using the intermediate CA. + Now check out the [Subscribers](/documentation/platform/pki/subscribers) page to learn more about how to issue X.509 certificates using the intermediate CA. @@ -255,7 +255,7 @@ consisting of an (optional) root CA and an intermediate CA. } ``` - Great! You’ve successfully created a Private CA hierarchy with a root CA and an intermediate CA. Now check out the Certificates page to learn more about how to issue X.509 certificates using the intermediate CA. + Great! You’ve successfully created a Private CA hierarchy with a root CA and an intermediate CA. Now check out the [Subscribers](/documentation/platform/pki/subscribers) page to learn more about how to issue X.509 certificates using the intermediate CA. diff --git a/docs/documentation/platform/pki/subscribers.mdx b/docs/documentation/platform/pki/subscribers.mdx new file mode 100644 index 000000000..3aebe50e2 --- /dev/null +++ b/docs/documentation/platform/pki/subscribers.mdx @@ -0,0 +1,130 @@ +--- +title: "Subscribers" +sidebarTitle: "Subscribers" +description: "Learn how to manage PKI subscribers and issue X.509 certificates for them." +--- + +## Concept + +In Infisical PKI, subscribers are logical representations of entities such as devices, servers, applications that request and receive certificates from Certificate Authorities (CAs). + +
+ +```mermaid +graph TD +A[Issuing CA] --> C1[Certificate] + C1 --> S1[Subscriber] + A --> C2[Certificate] + C2 --> S2[Subscriber] +``` + +
+ +## Workflow + +The typical workflow for managing subscribers consists of the following steps: + +1. Creating a subscriber and defining which (issuing) CA will issue X.509 certificates for it as well as attributes to be included on the certificates including common name, subject alternative names, TLL, etc. +2. Requesting for a certificate against the subscriber with or without a certificate signing request (CSR). +3. Managing certificate lifecycle events such as certificate renewal and revocation. As part of the certificate revocation flow, + you can also query for a Certificate Revocation List [CRL](https://en.wikipedia.org/wiki/Certificate_revocation_list), a time-stamped, signed + data structure issued by a CA containing a list of revoked certificates to check if a certificate has been revoked. + + + Note that this workflow can be executed via the Infisical UI or manually such + as via API. + + +## Guide to Issuing Certificates with Subscribers + +In the following steps, we explore how to issue a X.509 certificate for a subscriber. + + + + A subscriber is the logical representation of an entity that requests and + receives certificates from a CA. With a subscriber, you can specify the + attributes that must be present on the X.509 certificates issued for it. + + Head to your Infisical PKI Project > Subscribers to create a subscriber. + + ![pki create subscriber](/images/platform/pki/subscriber/subscriber-create.png) + + ![pki create subscriber 2](/images/platform/pki/subscriber/subscriber-create-2.png) + + Here's some guidance on each field. + + - Subscriber Name: A slug-friendly name for the subscriber such as `web-service`. + - Issuing CA: The Certificate Authority (CA) that will issue X.509 certificates for the subscriber. + - Common Name (CN): The common name to be included on certificates to be issued to the subscriber. + - Subject Alternative Names (SANs): A comma-delimited list of Subject Alternative Names (SANs) to be included on certificates; these can be hostnames or email addresses like `app1.acme.com, app2.acme.com`. + - TTL: The lifetime of the certificate. + - Key Usage: The key usage extension of the certificate. + - Extended Key Usage: The extended key usage extension of the certificate. + + + It's possible to issue certificates for a subscriber with or without a certificate signing request (CSR). + - If requesting without a CSR, the attributes specified on the subscriber will be used to issue a certificate for the subscriber. + - If requesting with a CSR, the attributes on it will be validated against the attributes specified on the subscriber + and a certificate is only issued if they comply. + + + + + Once you have created a subscriber from step 1, you can issue a certificate for it. + + Press on the subscriber you want to issue a certificate for and click on the **Issue Certificate** button on that subscriber's page. + + ![pki issue subscriber certificate](/images/platform/pki/subscriber/subscriber-issue-cert.png) + + ![pki issue subscriber certificate 2](/images/platform/pki/subscriber/subscriber-issue-cert-2.png) + + + + +## Guide to Revoking Certificates + +In the following steps, we explore how to revoke a X.509 certificate and obtain a Certificate Revocation List (CRL) for a CA. + + + + Assuming that you've issued a certificate for a subscriber, you can revoke it by + selecting the **Revoke Certificate** option on the certificate you wish to revoke + on the subscriber's page. + + ![pki revoke subscriber certificate](/images/platform/pki/subscriber/subscriber-revoke-cert.png) + + + + In order to check the revocation status of a certificate, you can check it + against the CRL of a CA by heading to its Issuing CA and downloading the CRL. + + ![pki view crl](/images/platform/pki/subscriber/subscriber-ca-crl.png) + + To verify a certificate against the + downloaded CRL with OpenSSL, you can use the following command: + +```bash +openssl verify -crl_check -CAfile chain.pem -CRLfile crl.pem cert.pem +``` + +Note that you can also obtain the CRL from the certificate itself by +referencing the CRL distribution point extension on the certificate. + +To check a certificate against the CRL distribution point specified within it with OpenSSL, you can use the following command: + +```bash +openssl verify -verbose -crl_check -crl_download -CAfile chain.pem cert.pem +``` + + + + +## FAQ + + + + To renew a certificate, you have to issue a new certificate for the same + subscriber. The original certificate will continue to be valid through its + original TTL unless explicitly revoked. + + diff --git a/docs/documentation/platform/pr-workflows.mdx b/docs/documentation/platform/pr-workflows.mdx index 187bae5d4..ffa85f6c5 100644 --- a/docs/documentation/platform/pr-workflows.mdx +++ b/docs/documentation/platform/pr-workflows.mdx @@ -5,23 +5,23 @@ description: "Learn how to enable a set of policies to manage changes to sensiti Approval Workflows is a paid feature. - - If you're using Infisical Cloud, then it is available under the **Pro Tier** and **Enterprise Tire**. + + If you're using Infisical Cloud, then it is available under the **Pro Tier** and **Enterprise Tier**. If you're self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it. ## Problem at hand -Updating secrets in high-stakes environments (e.g., production) can have a number of problematic issues: -- Most developers should not have access to secrets in production environments. Yet, they are the ones who often need to add new secrets or change the existing ones. Many organizations have in-house policies with regards to what person should be contacted in the case of needing to make changes to secrets. This slows down software development lifecycle and distracts engineers from working on things that matter the most. -- As a general rule, before making changes in production environments, those changes have to be looked over by at least another person. An extra pair of eyes can help reduce the risk of human error and make sure that the change will not affect the application in an unintended way. -- After making updates to secrets, the corresponding applications need to be redeployed with the right set of secrets and configurations. This process is often not automated and hence prone to human error. +Updating secrets in high-stakes environments (e.g., production) can have a number of problematic issues: +- Most developers should not have access to secrets in production environments. Yet, they are the ones who often need to add new secrets or change the existing ones. Many organizations have in-house policies with regards to what person should be contacted in the case of needing to make changes to secrets. This slows down software development lifecycle and distracts engineers from working on things that matter the most. +- As a general rule, before making changes in production environments, those changes have to be looked over by at least another person. An extra pair of eyes can help reduce the risk of human error and make sure that the change will not affect the application in an unintended way. +- After making updates to secrets, the corresponding applications need to be redeployed with the right set of secrets and configurations. This process is often not automated and hence prone to human error. ## Solution -As a wide-spread software engineering practice, developers have to submit their code as a PR that needs to be approved before the code is merged into the main branch. +As a wide-spread software engineering practice, developers have to submit their code as a PR that needs to be approved before the code is merged into the main branch. -In a similar way, to solve the above-mentioned issues, Infisical provides a feature called `Approval Workflows` for secret management. This is a set of policies and workflows that help advance access controls, compliance procedures, and stability of a particular environment. In other words, **Approval Workflows** help you secure, stabilize, and streamline the change of secrets in high-stakes environments. +In a similar way, to solve the above-mentioned issues, Infisical provides a feature called `Approval Workflows` for secret management. This is a set of policies and workflows that help advance access controls, compliance procedures, and stability of a particular environment. In other words, **Approval Workflows** help you secure, stabilize, and streamline the change of secrets in high-stakes environments. ### Setting a policy @@ -33,6 +33,10 @@ First, you would need to create a set of policies for a certain environment. In The enforcement level determines how strict the policy is. A **Hard** enforcement level means that any change that matches the policy will need full approval prior merging. A **Soft** enforcement level allows for break glass functionality on the request. If a change request is bypassed, the approvers will be notified via email. +### Self approvals + +If the **Self Approvals** option is enabled, users who are designated as approvers on the policy can approve requests that they themselves have submitted. + ### Example of creating a change policy When creating a policy, you can choose the type of policy you want to create. In this case, we will be creating a `Change Policy`. Other types of policies include `Access Policy` that creates policies for **[Access Requests](/documentation/platform/access-controls/access-requests)**. @@ -41,10 +45,18 @@ When creating a policy, you can choose the type of policy you want to create. In ### Example of updating secrets with Approval workflows -When a user submits a change to an enviropnment that is under a particular policy, a corresponsing change request will go to a predefined approver (or multiple approvers). +When a user submits a change to an environment that is under a particular policy, a corresponding change request will go to a predefined approver (or multiple approvers). ![secret update change requests](../../images/platform/pr-workflows/secret-update-request.png) Approvers are notified by email and/or Slack as soon as the request is initiated. In the Infisical Dashboard, they will be able to `approve` and `merge` (or `deny`) a request for a change in a particular environment. After that, depending on the workflows setup, the change will be automatically propagated to the right applications (e.g., using [Infisical Kubernetes Operator](https://infisical.com/docs/integrations/platforms/kubernetes)). ![secrets update pull request](../../images/platform/pr-workflows/secret-update-pr.png) + +## FAQ + + + + Yes, if you'd like to require an approval from an approver other than the one who created the request, then you can disable the **Self Approvals** feature inside of your target policy. + + diff --git a/docs/documentation/platform/scim/azure.mdx b/docs/documentation/platform/scim/azure.mdx index 0e86f6149..e755f8750 100644 --- a/docs/documentation/platform/scim/azure.mdx +++ b/docs/documentation/platform/scim/azure.mdx @@ -15,7 +15,7 @@ Prerequisites: - In Infisical, head to your Organization Settings > Security > SCIM Configuration and + In Infisical, head to the **Single Sign-On (SSO)** page and select the **Provisioning** tab. Under SCIM Configuration, press the **Enable SCIM provisioning** toggle to allow Azure to provision/deprovision users for your organization. ![SCIM enable provisioning](/images/platform/scim/scim-enable-provisioning.png) diff --git a/docs/documentation/platform/scim/jumpcloud.mdx b/docs/documentation/platform/scim/jumpcloud.mdx index 42d33247a..be4caf738 100644 --- a/docs/documentation/platform/scim/jumpcloud.mdx +++ b/docs/documentation/platform/scim/jumpcloud.mdx @@ -15,7 +15,7 @@ Prerequisites: - In Infisical, head to your Organization Settings > Security > SCIM Configuration and + In Infisical, head to the **Single Sign-On (SSO)** page and select the **Provisioning** tab. Under SCIM Configuration, press the **Enable SCIM provisioning** toggle to allow JumpCloud to provision/deprovision users and user groups for your organization. ![SCIM enable provisioning](/images/platform/scim/scim-enable-provisioning.png) diff --git a/docs/documentation/platform/scim/okta.mdx b/docs/documentation/platform/scim/okta.mdx index d33bd242d..cf2c17724 100644 --- a/docs/documentation/platform/scim/okta.mdx +++ b/docs/documentation/platform/scim/okta.mdx @@ -15,7 +15,7 @@ Prerequisites: - In Infisical, head to your Organization Settings > Security > SCIM Configuration and + In Infisical, head to the **Single Sign-On (SSO)** page and select the **Provisioning** tab. Under SCIM Configuration, press the **Enable SCIM provisioning** toggle to allow Okta to provision/deprovision users and user groups for your organization. ![SCIM enable provisioning](/images/platform/scim/scim-enable-provisioning.png) diff --git a/docs/documentation/platform/sso/auth0-oidc.mdx b/docs/documentation/platform/sso/auth0-oidc.mdx index e8b532c1c..0665a7b30 100644 --- a/docs/documentation/platform/sso/auth0-oidc.mdx +++ b/docs/documentation/platform/sso/auth0-oidc.mdx @@ -39,8 +39,8 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO." - 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click **Connect**. - ![OIDC auth0 manage org Infisical](../../../images/sso/auth0-oidc/org-oidc-overview.png) + 3.1. Back in Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Click **Connect** for **OIDC**. + ![OIDC SSO Connect](../../../images/sso/connect-oidc.png) 3.2. For configuration type, select **Discovery URL**. Then, set **Discovery Document URL**, **JWT Signature Algorithm**, **Client ID**, and **Client Secret** from step 2.1 and 2.2. ![OIDC auth0 paste values into Infisical](../../../images/sso/auth0-oidc/org-update-oidc.png) diff --git a/docs/documentation/platform/sso/auth0-saml.mdx b/docs/documentation/platform/sso/auth0-saml.mdx index b426d1aae..562360ecb 100644 --- a/docs/documentation/platform/sso/auth0-saml.mdx +++ b/docs/documentation/platform/sso/auth0-saml.mdx @@ -12,7 +12,9 @@ description: "Learn how to configure Auth0 SAML for Infisical SSO." - In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Auth0, then click **Connect** again. + In Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Click **Connect** for **SAML** under the Connect to an Identity Provider section. Select **Auth0**, then click **Connect** again. + + ![SSO connect section](../../../images/sso/connect-saml.png) Next, note the **Application Callback URL** and **Audience** to use when configuring the Auth0 SAML application. diff --git a/docs/documentation/platform/sso/azure.mdx b/docs/documentation/platform/sso/azure.mdx index 282cddae5..137dc6564 100644 --- a/docs/documentation/platform/sso/azure.mdx +++ b/docs/documentation/platform/sso/azure.mdx @@ -12,7 +12,9 @@ description: "Learn how to configure Microsoft Entra ID for Infisical SSO." - In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Azure / Entra, then click **Connect** again. + In Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Click **Connect** for **SAML** under the Connect to an Identity Provider section. Select **Azure / Entra**, then click **Connect** again. + + ![SSO connect section](../../../images/sso/connect-saml.png) Next, copy the **Reply URL (Assertion Consumer Service URL)** and **Identifier (Entity ID)** to use when configuring the Azure SAML application. diff --git a/docs/documentation/platform/sso/general-oidc.mdx b/docs/documentation/platform/sso/general-oidc.mdx index 76e364b2f..a10b05cfc 100644 --- a/docs/documentation/platform/sso/general-oidc.mdx +++ b/docs/documentation/platform/sso/general-oidc.mdx @@ -28,8 +28,8 @@ Prerequisites: 1.4. Access the IdP’s OIDC discovery document (usually located at `https:///.well-known/openid-configuration`). This document contains important endpoints such as authorization, token, userinfo, and keys. - 2.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect. - ![OIDC general manage org Infisical](../../../images/sso/general-oidc/org-oidc-manage.png) + 2.1. Back in Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Select **Connect** for **OIDC**. + ![OIDC SSO Connect](../../../images/sso/connect-oidc.png) 2.2. You can configure OIDC either through the Discovery URL (Recommended) or by inputting custom endpoints. diff --git a/docs/documentation/platform/sso/google-saml.mdx b/docs/documentation/platform/sso/google-saml.mdx index 87ffa8412..99223c815 100644 --- a/docs/documentation/platform/sso/google-saml.mdx +++ b/docs/documentation/platform/sso/google-saml.mdx @@ -12,7 +12,9 @@ description: "Learn how to configure Google SAML for Infisical SSO." - In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Google, then click **Connect** again. + In Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Click **Connect** for **SAML** under the Connect to an Identity Provider section. Select **Google**, then click **Connect** again. + + ![SSO connect section](../../../images/sso/connect-saml.png) Next, note the **ACS URL** and **SP Entity ID** to use when configuring the Google SAML application. diff --git a/docs/documentation/platform/sso/jumpcloud.mdx b/docs/documentation/platform/sso/jumpcloud.mdx index 6ca20c752..0898c0715 100644 --- a/docs/documentation/platform/sso/jumpcloud.mdx +++ b/docs/documentation/platform/sso/jumpcloud.mdx @@ -12,7 +12,9 @@ description: "Learn how to configure JumpCloud SAML for Infisical SSO." - In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select JumpCloud, then click **Connect** again. + In Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Click **Connect** for **SAML** under the Connect to an Identity Provider section. Select **JumpCloud**, then click **Connect** again. + + ![SSO connect section](../../../images/sso/connect-saml.png) Next, copy the **ACS URL** and **SP Entity ID** to use when configuring the JumpCloud SAML application. diff --git a/docs/documentation/platform/sso/keycloak-oidc/group-membership-mapping.mdx b/docs/documentation/platform/sso/keycloak-oidc/group-membership-mapping.mdx index c423bac5a..29bca5a8d 100644 --- a/docs/documentation/platform/sso/keycloak-oidc/group-membership-mapping.mdx +++ b/docs/documentation/platform/sso/keycloak-oidc/group-membership-mapping.mdx @@ -53,7 +53,7 @@ Infisical groups not present in their groups claim. 2.1. In Infisical, create any groups you would like to sync users to. Make sure the name of the Infisical group is an exact match of the Keycloak group name. ![OIDC keycloak infisical group](/images/sso/keycloak-oidc/group-membership-mapping/create-infisical-group.png) - 2.2. Next, enable **OIDC Group Membership Mapping** in Organization Settings > Security. + 2.2. Next, enable **OIDC Group Membership Mapping** on the **Single Sign-On (SSO)** page under the **General** tab. ![OIDC keycloak enable group membership mapping](/images/sso/keycloak-oidc/group-membership-mapping/enable-group-membership-mapping.png) 2.3. The next time a user logs in they will be synced to their matching Keycloak groups. diff --git a/docs/documentation/platform/sso/keycloak-oidc/overview.mdx b/docs/documentation/platform/sso/keycloak-oidc/overview.mdx index 803818a0e..06d8dfa43 100644 --- a/docs/documentation/platform/sso/keycloak-oidc/overview.mdx +++ b/docs/documentation/platform/sso/keycloak-oidc/overview.mdx @@ -66,8 +66,8 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO." - 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect. - ![OIDC keycloak manage org Infisical](/images/sso/keycloak-oidc/manage-org-oidc.png) + 3.1. Back in Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Click **Connect** for **OIDC**. + ![OIDC SSO Connect](../../../../images/sso/connect-oidc.png) 3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **JWT Signature Algorithm**, **Client ID**, and **Client Secret**. ![OIDC keycloak paste values into Infisical](/images/sso/keycloak-oidc/create-oidc.png) diff --git a/docs/documentation/platform/sso/keycloak-saml.mdx b/docs/documentation/platform/sso/keycloak-saml.mdx index 7e4004122..ba6aa0c3a 100644 --- a/docs/documentation/platform/sso/keycloak-saml.mdx +++ b/docs/documentation/platform/sso/keycloak-saml.mdx @@ -12,9 +12,9 @@ description: "Learn how to configure Keycloak SAML for Infisical SSO." - In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Keycloak, then click **Connect** again. + In Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Click **Connect** for **SAML** under the Connect to an Identity Provider section. Select **Keycloak**, then click **Connect** again. - ![Keycloak SAML organization security section](../../../images/sso/keycloak/org-security-section.png) + ![SSO connect section](../../../images/sso/connect-saml.png) Next, copy the **Valid redirect URI** and **SP Entity ID** to use when configuring the Keycloak SAML application. diff --git a/docs/documentation/platform/sso/okta.mdx b/docs/documentation/platform/sso/okta.mdx index 1abd03d6f..2af689e4c 100644 --- a/docs/documentation/platform/sso/okta.mdx +++ b/docs/documentation/platform/sso/okta.mdx @@ -12,8 +12,10 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO." - In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Okta, then click **Connect** again. - + In Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Click **Connect** for **SAML** under the Connect to an Identity Provider section. Select **Okta**, then click **Connect** again. + + ![SSO connect section](../../../images/sso/connect-saml.png) + Next, copy the **Single sign-on URL** and **Audience URI (SP Entity ID)** to use when configuring the Okta SAML 2.0 application. ![Okta SAML initial configuration](../../../images/sso/okta/init-config.png) diff --git a/docs/images/app-connections/oci/add-api-key.png b/docs/images/app-connections/oci/add-api-key.png new file mode 100644 index 000000000..049ea4c87 Binary files /dev/null and b/docs/images/app-connections/oci/add-api-key.png differ diff --git a/docs/images/app-connections/oci/app-connection-created.png b/docs/images/app-connections/oci/app-connection-created.png new file mode 100644 index 000000000..73edfa441 Binary files /dev/null and b/docs/images/app-connections/oci/app-connection-created.png differ diff --git a/docs/images/app-connections/oci/app-connection-modal.png b/docs/images/app-connections/oci/app-connection-modal.png new file mode 100644 index 000000000..c4ca6c0fb Binary files /dev/null and b/docs/images/app-connections/oci/app-connection-modal.png differ diff --git a/docs/images/app-connections/oci/app-connection-option.png b/docs/images/app-connections/oci/app-connection-option.png new file mode 100644 index 000000000..1651316c6 Binary files /dev/null and b/docs/images/app-connections/oci/app-connection-option.png differ diff --git a/docs/images/app-connections/oci/click-create-policy.png b/docs/images/app-connections/oci/click-create-policy.png new file mode 100644 index 000000000..edc5a74e9 Binary files /dev/null and b/docs/images/app-connections/oci/click-create-policy.png differ diff --git a/docs/images/app-connections/oci/click-create-user.png b/docs/images/app-connections/oci/click-create-user.png new file mode 100644 index 000000000..d4422b1a4 Binary files /dev/null and b/docs/images/app-connections/oci/click-create-user.png differ diff --git a/docs/images/app-connections/oci/create-group.png b/docs/images/app-connections/oci/create-group.png new file mode 100644 index 000000000..9063ed737 Binary files /dev/null and b/docs/images/app-connections/oci/create-group.png differ diff --git a/docs/images/app-connections/oci/create-policy.png b/docs/images/app-connections/oci/create-policy.png new file mode 100644 index 000000000..ea666e09e Binary files /dev/null and b/docs/images/app-connections/oci/create-policy.png differ diff --git a/docs/images/app-connections/oci/create-user.png b/docs/images/app-connections/oci/create-user.png new file mode 100644 index 000000000..f10488544 Binary files /dev/null and b/docs/images/app-connections/oci/create-user.png differ diff --git a/docs/images/app-connections/oci/search-domains.png b/docs/images/app-connections/oci/search-domains.png new file mode 100644 index 000000000..b56f85350 Binary files /dev/null and b/docs/images/app-connections/oci/search-domains.png differ diff --git a/docs/images/app-connections/oci/search-policies.png b/docs/images/app-connections/oci/search-policies.png new file mode 100644 index 000000000..d541fdbbe Binary files /dev/null and b/docs/images/app-connections/oci/search-policies.png differ diff --git a/docs/images/app-connections/oci/select-api-keys.png b/docs/images/app-connections/oci/select-api-keys.png new file mode 100644 index 000000000..7c63e0919 Binary files /dev/null and b/docs/images/app-connections/oci/select-api-keys.png differ diff --git a/docs/images/app-connections/oci/select-domain.png b/docs/images/app-connections/oci/select-domain.png new file mode 100644 index 000000000..9190de801 Binary files /dev/null and b/docs/images/app-connections/oci/select-domain.png differ diff --git a/docs/images/app-connections/oci/select-groups.png b/docs/images/app-connections/oci/select-groups.png new file mode 100644 index 000000000..d958900a3 Binary files /dev/null and b/docs/images/app-connections/oci/select-groups.png differ diff --git a/docs/images/app-connections/oci/select-users.png b/docs/images/app-connections/oci/select-users.png new file mode 100644 index 000000000..392fd7000 Binary files /dev/null and b/docs/images/app-connections/oci/select-users.png differ diff --git a/docs/images/app-connections/oci/user-info.png b/docs/images/app-connections/oci/user-info.png new file mode 100644 index 000000000..24688d084 Binary files /dev/null and b/docs/images/app-connections/oci/user-info.png differ diff --git a/docs/images/platform/external-syncs/github-org-sync-active.png b/docs/images/platform/external-syncs/github-org-sync-active.png index bb5ce1ca3..1137d7601 100644 Binary files a/docs/images/platform/external-syncs/github-org-sync-active.png and b/docs/images/platform/external-syncs/github-org-sync-active.png differ diff --git a/docs/images/platform/external-syncs/github-org-sync-config-modal.png b/docs/images/platform/external-syncs/github-org-sync-config-modal.png index b856048e3..d02cd4589 100644 Binary files a/docs/images/platform/external-syncs/github-org-sync-config-modal.png and b/docs/images/platform/external-syncs/github-org-sync-config-modal.png differ diff --git a/docs/images/platform/external-syncs/github-org-sync-section.png b/docs/images/platform/external-syncs/github-org-sync-section.png index dad1fa425..870b9d055 100644 Binary files a/docs/images/platform/external-syncs/github-org-sync-section.png and b/docs/images/platform/external-syncs/github-org-sync-section.png differ diff --git a/docs/images/platform/pki/subscriber/subscriber-ca-crl.png b/docs/images/platform/pki/subscriber/subscriber-ca-crl.png new file mode 100644 index 000000000..35f7dad65 Binary files /dev/null and b/docs/images/platform/pki/subscriber/subscriber-ca-crl.png differ diff --git a/docs/images/platform/pki/subscriber/subscriber-create-2.png b/docs/images/platform/pki/subscriber/subscriber-create-2.png new file mode 100644 index 000000000..fdfa44d27 Binary files /dev/null and b/docs/images/platform/pki/subscriber/subscriber-create-2.png differ diff --git a/docs/images/platform/pki/subscriber/subscriber-create.png b/docs/images/platform/pki/subscriber/subscriber-create.png new file mode 100644 index 000000000..8a4709ea3 Binary files /dev/null and b/docs/images/platform/pki/subscriber/subscriber-create.png differ diff --git a/docs/images/platform/pki/subscriber/subscriber-issue-cert-2.png b/docs/images/platform/pki/subscriber/subscriber-issue-cert-2.png new file mode 100644 index 000000000..916c5aab9 Binary files /dev/null and b/docs/images/platform/pki/subscriber/subscriber-issue-cert-2.png differ diff --git a/docs/images/platform/pki/subscriber/subscriber-issue-cert.png b/docs/images/platform/pki/subscriber/subscriber-issue-cert.png new file mode 100644 index 000000000..f96c7db28 Binary files /dev/null and b/docs/images/platform/pki/subscriber/subscriber-issue-cert.png differ diff --git a/docs/images/platform/pki/subscriber/subscriber-revoke-cert.png b/docs/images/platform/pki/subscriber/subscriber-revoke-cert.png new file mode 100644 index 000000000..4601991c8 Binary files /dev/null and b/docs/images/platform/pki/subscriber/subscriber-revoke-cert.png differ diff --git a/docs/images/platform/pr-workflows/create-change-policy.png b/docs/images/platform/pr-workflows/create-change-policy.png index 4ff1ad884..afe945b0a 100644 Binary files a/docs/images/platform/pr-workflows/create-change-policy.png and b/docs/images/platform/pr-workflows/create-change-policy.png differ diff --git a/docs/images/platform/scim/scim-enable-provisioning.png b/docs/images/platform/scim/scim-enable-provisioning.png index a4385244f..37fc658b5 100644 Binary files a/docs/images/platform/scim/scim-enable-provisioning.png and b/docs/images/platform/scim/scim-enable-provisioning.png differ diff --git a/docs/images/platform/scim/scim-group-mapping.png b/docs/images/platform/scim/scim-group-mapping.png index 76baa8d8d..37bfcf45a 100644 Binary files a/docs/images/platform/scim/scim-group-mapping.png and b/docs/images/platform/scim/scim-group-mapping.png differ diff --git a/docs/images/secret-syncs/oci-vault/configure-destination.png b/docs/images/secret-syncs/oci-vault/configure-destination.png new file mode 100644 index 000000000..553380635 Binary files /dev/null and b/docs/images/secret-syncs/oci-vault/configure-destination.png differ diff --git a/docs/images/secret-syncs/oci-vault/configure-details.png b/docs/images/secret-syncs/oci-vault/configure-details.png new file mode 100644 index 000000000..27cf890e8 Binary files /dev/null and b/docs/images/secret-syncs/oci-vault/configure-details.png differ diff --git a/docs/images/secret-syncs/oci-vault/configure-source.png b/docs/images/secret-syncs/oci-vault/configure-source.png new file mode 100644 index 000000000..0953466fc Binary files /dev/null and b/docs/images/secret-syncs/oci-vault/configure-source.png differ diff --git a/docs/images/secret-syncs/oci-vault/configure-sync-options.png b/docs/images/secret-syncs/oci-vault/configure-sync-options.png new file mode 100644 index 000000000..6f40e0dbb Binary files /dev/null and b/docs/images/secret-syncs/oci-vault/configure-sync-options.png differ diff --git a/docs/images/secret-syncs/oci-vault/copy-compartment-ocid.png b/docs/images/secret-syncs/oci-vault/copy-compartment-ocid.png new file mode 100644 index 000000000..fb4355807 Binary files /dev/null and b/docs/images/secret-syncs/oci-vault/copy-compartment-ocid.png differ diff --git a/docs/images/secret-syncs/oci-vault/review-configuration.png b/docs/images/secret-syncs/oci-vault/review-configuration.png new file mode 100644 index 000000000..2abe7820f Binary files /dev/null and b/docs/images/secret-syncs/oci-vault/review-configuration.png differ diff --git a/docs/images/secret-syncs/oci-vault/search-compartment.png b/docs/images/secret-syncs/oci-vault/search-compartment.png new file mode 100644 index 000000000..005f06ecd Binary files /dev/null and b/docs/images/secret-syncs/oci-vault/search-compartment.png differ diff --git a/docs/images/secret-syncs/oci-vault/select-compartment.png b/docs/images/secret-syncs/oci-vault/select-compartment.png new file mode 100644 index 000000000..3eae44c32 Binary files /dev/null and b/docs/images/secret-syncs/oci-vault/select-compartment.png differ diff --git a/docs/images/secret-syncs/oci-vault/select-option.png b/docs/images/secret-syncs/oci-vault/select-option.png new file mode 100644 index 000000000..49a61ccae Binary files /dev/null and b/docs/images/secret-syncs/oci-vault/select-option.png differ diff --git a/docs/images/secret-syncs/oci-vault/sync-created.png b/docs/images/secret-syncs/oci-vault/sync-created.png new file mode 100644 index 000000000..c68fedc68 Binary files /dev/null and b/docs/images/secret-syncs/oci-vault/sync-created.png differ diff --git a/docs/images/sso/auth0-oidc/org-oidc-overview.png b/docs/images/sso/auth0-oidc/org-oidc-overview.png deleted file mode 100644 index f5778b97a..000000000 Binary files a/docs/images/sso/auth0-oidc/org-oidc-overview.png and /dev/null differ diff --git a/docs/images/sso/connect-ldap.png b/docs/images/sso/connect-ldap.png new file mode 100644 index 000000000..419d6f8b7 Binary files /dev/null and b/docs/images/sso/connect-ldap.png differ diff --git a/docs/images/sso/connect-oidc.png b/docs/images/sso/connect-oidc.png new file mode 100644 index 000000000..43da1bb0a Binary files /dev/null and b/docs/images/sso/connect-oidc.png differ diff --git a/docs/images/sso/connect-saml.png b/docs/images/sso/connect-saml.png new file mode 100644 index 000000000..40de3a0d2 Binary files /dev/null and b/docs/images/sso/connect-saml.png differ diff --git a/docs/images/sso/general-oidc/org-oidc-manage.png b/docs/images/sso/general-oidc/org-oidc-manage.png deleted file mode 100644 index f5778b97a..000000000 Binary files a/docs/images/sso/general-oidc/org-oidc-manage.png and /dev/null differ diff --git a/docs/images/sso/keycloak-oidc/group-membership-mapping/enable-group-membership-mapping.png b/docs/images/sso/keycloak-oidc/group-membership-mapping/enable-group-membership-mapping.png index 199a7432a..d3b38c762 100644 Binary files a/docs/images/sso/keycloak-oidc/group-membership-mapping/enable-group-membership-mapping.png and b/docs/images/sso/keycloak-oidc/group-membership-mapping/enable-group-membership-mapping.png differ diff --git a/docs/images/sso/keycloak-oidc/manage-org-oidc.png b/docs/images/sso/keycloak-oidc/manage-org-oidc.png deleted file mode 100644 index f5778b97a..000000000 Binary files a/docs/images/sso/keycloak-oidc/manage-org-oidc.png and /dev/null differ diff --git a/docs/integrations/app-connections/oci.mdx b/docs/integrations/app-connections/oci.mdx new file mode 100644 index 000000000..ff51ce1d9 --- /dev/null +++ b/docs/integrations/app-connections/oci.mdx @@ -0,0 +1,189 @@ +--- +title: "OCI Connection" +description: "Learn how to configure an Oracle Cloud Infrastructure Connection for Infisical." +--- + +Infisical supports the use of [API Signing Key Authentication](https://docs.oracle.com/en-us/iaas/Content/API/Concepts/apisigningkey.htm) to connect with OCI. + +## Create OCI User + + + + ![Search Domains](/images/app-connections/oci/search-domains.png) + + + Select the domain in which you want to create the Infisical user account. + + ![Select Domain](/images/app-connections/oci/select-domain.png) + + + ![Select Users](/images/app-connections/oci/select-users.png) + + + ![Click Create User](/images/app-connections/oci/click-create-user.png) + + + The name, email, and username can be anything. + + ![Create User](/images/app-connections/oci/create-user.png) + + + After you've created a user, you'll be redirected to the user's page. Navigate to 'API keys'. + + ![Select API Keys](/images/app-connections/oci/select-api-keys.png) + + + Click on 'Add API key' and then download or import the private key. After you've obtained the private key, click 'Add'. + + ![Add API Key](/images/app-connections/oci/add-api-key.png) + + + After creating the API key, you'll be shown a modal with relevant information. Save the highlighted values (and the private key) for later steps. + + ![User Info](/images/app-connections/oci/user-info.png) + + + +## Create OCI Group + + + + ![Search Domains](/images/app-connections/oci/search-domains.png) + + + Select the domain in which you want to create the Infisical user account. + + ![Select Domain](/images/app-connections/oci/select-domain.png) + + + ![Select Groups](/images/app-connections/oci/select-groups.png) + + + The name and description can be anything. **Ensure that you assign the user created in earlier steps to this group**. + + ![Create Group](/images/app-connections/oci/create-group.png) + + + After creating the group, take note of its name. It will be used in later steps. + + + +## Create OCI Policy + + + + ![Search Policies](/images/app-connections/oci/search-policies.png) + + + ![Click Create Policy](/images/app-connections/oci/click-create-policy.png) + + + The name and description can be anything. Click 'Show manual editor' and paste in the policy rules relevant to your task: + + + + ``` + Allow group to manage secret-family in compartment + Allow group to use keys in compartment + Allow group to use vaults in compartment + Allow group to inspect compartments in tenancy + ``` + + - **Group Name:** The name of the group you created in earlier steps. + - **Compartment Name:** The name of the compartment which has your secrets vault. + + If you'd like to grant Infisical access to all compartments, replace instances of `compartment ` with `tenancy`. + + + + ![Create Policy](/images/app-connections/oci/create-policy.png) + + + **You must create this policy on the root compartment**, otherwise some functionality may not work. + + + + +## Create OCI Connection in Infisical + + + + + + In your Infisical dashboard, go to **Organization Settings** and select the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Click the **+ Add Connection** button and select the **OCI Connection** option from the available integrations. + + ![Select OCI Connection](/images/app-connections/oci/app-connection-option.png) + + + Complete the OCI Connection form by entering: + - A descriptive name for the connection + - An optional description for future reference + - The User OCID from [earlier steps](https://infisical.com/docs/integrations/app-connections/oci#create-oci-user) + - The Tenancy OCID from [earlier steps](https://infisical.com/docs/integrations/app-connections/oci#create-oci-user) + - The Region from [earlier steps](https://infisical.com/docs/integrations/app-connections/oci#create-oci-user) + - The Fingerprint from [earlier steps](https://infisical.com/docs/integrations/app-connections/oci#create-oci-user) + - The Private Key PEM from [earlier steps](https://infisical.com/docs/integrations/app-connections/oci#create-oci-user) + + ![OCI Connection Modal](/images/app-connections/oci/app-connection-modal.png) + + + After clicking Create, your **OCI Connection** is established and ready to use with your Infisical projects. + + ![OCI Connection Created](/images/app-connections/oci/app-connection-created.png) + + + + + To create an OCI Connection, make an API request to the [Create OCI Connection](/api-reference/endpoints/app-connections/oci/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/oci \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-oci-connection", + "method": "access-key", + "credentials": { + "userOcid": "ocid1.user.oc1..aaaaaaaagrp35tbkvvad4y2j7sug7xonua7dl2gfp4at2u5i5xj4ghnitg3a", + "tenancyOcid": "ocid1.tenancy.oc1..aaaaaaaaotfma465m4zumfe2ua64mj2m5dwmlw2llh4g4dnfttnakiifonta", + "region": "us-ashburn-1", + "fingerprint": "9c:f6:18:23:92:73:f8:e1:85:2c:6a:e3:2c:7d:ec:8f", + "privateKey": "[PRIVATE KEY PEM]" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", + "name": "my-oci-connection", + "description": null, + "version": 1, + "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", + "createdAt": "2025-04-23T19:46:34.831Z", + "updatedAt": "2025-04-23T19:46:34.831Z", + "isPlatformManagedCredentials": false, + "credentialsHash": "7c2d371dec195f82a6a0d5b41c970a229cfcaf88e894a5b6395e2dbd0280661f", + "app": "oci", + "method": "access-key", + "credentials": { + "userOcid": "ocid1.user.oc1..aaaaaaaagrp35tbkvvad4y2j7sug7xonua7dl2gfp4at2u5i5xj4ghnitg3a", + "tenancyOcid": "ocid1.tenancy.oc1..aaaaaaaaotfma465m4zumfe2ua64mj2m5dwmlw2llh4g4dnfttnakiifonta", + "region": "us-ashburn-1", + "fingerprint": "9c:f6:18:23:92:73:f8:e1:85:2c:6a:e3:2c:7d:ec:8f" + } + } + } + ``` + + diff --git a/docs/integrations/frameworks/pulumi.mdx b/docs/integrations/frameworks/pulumi.mdx new file mode 100644 index 000000000..11a8e0cb7 --- /dev/null +++ b/docs/integrations/frameworks/pulumi.mdx @@ -0,0 +1,14 @@ +--- +title: "Pulumi" +description: "Using Infisical with Pulumi via the Terraform Bridge" +--- + +Infisical can be integrated with Pulumi by leveraging Pulumi’s [Terraform Bridge](https://www.pulumi.com/blog/any-terraform-provider/), +which allows Terraform providers to be used seamlessly within Pulumi projects. This enables infrastructure and platform teams to manage Infisical secrets and resources +using Pulumi’s familiar programming languages (including TypeScript, Python, Go, and C#), without any change to existing workflows. + +The Terraform Bridge wraps the [Infisical Terraform provider](/integrations/frameworks/terraform) and exposes its resources (such as `infisical_secret`, `infisical_project`, and `infisical_service_token`) +in a Pulumi-compatible interface. This makes it easy to integrate secret management directly into Pulumi-based IaC pipelines, ensuring secrets stay in sync with +the rest of your cloud infrastructure. Authentication is handled through the same methods as Terraform: using environment variables such as `INFISICAL_TOKEN` and `INFISICAL_SITE_URL`. + +By bridging the Infisical provider, teams using Pulumi can adopt secure, centralized secrets management without compromising on their toolchain or language preferences. \ No newline at end of file diff --git a/docs/integrations/secret-syncs/oci-vault.mdx b/docs/integrations/secret-syncs/oci-vault.mdx new file mode 100644 index 000000000..7f3c5fd19 --- /dev/null +++ b/docs/integrations/secret-syncs/oci-vault.mdx @@ -0,0 +1,177 @@ +--- +title: "OCI Vault Sync" +description: "Learn how to configure an Oracle Cloud Infrastructure Vault Sync for Infisical." +--- + +**Prerequisites:** +- Create an [OCI Connection](/integrations/app-connections/oci) with the required **Secret Sync** permissions +- [Create](https://docs.oracle.com/en-us/iaas/Content/Identity/compartments/To_create_a_compartment.htm) or use an existing OCI Compartment (which the OCI Connection is authorized to access) +- [Create](https://docs.oracle.com/en-us/iaas/Content/KeyManagement/Tasks/managingvaults_topic-To_create_a_new_vault.htm#createnewvault) or use an existing OCI Vault + + + + + + Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + + ![Select OCI Vault](/images/secret-syncs/oci-vault/select-option.png) + + + Configure the **Source** from where secrets should be retrieved, then click **Next**. + + ![Configure Source](/images/secret-syncs/oci-vault/configure-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + + Configure the **Destination** to where secrets should be deployed, then click **Next**. + + ![Configure Destination](/images/secret-syncs/oci-vault/configure-destination.png) + + - **OCI Connection**: The OCI Connection to authenticate with. + - **Compartment**: The compartment where the vault is located. + - **Vault**: The vault to sync secrets to. + - **Encryption Key**: The encryption key to use when creating secrets in the vault. + + + Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + + ![Configure Sync Options](/images/secret-syncs/oci-vault/configure-sync-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over OCI Vault when keys conflict. + - **Import Secrets (Prioritize OCI Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from OCI Vault over Infisical when keys conflict. + + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + + Configure the **Details** of your OCI Vault Sync, then click **Next**. + + ![Configure Details](/images/secret-syncs/oci-vault/configure-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + + Review your OCI Vault Sync configuration, then click **Create Sync**. + + ![Review Configuration](/images/secret-syncs/oci-vault/review-configuration.png) + + + If enabled, your OCI Vault Sync will begin syncing your secrets to the destination endpoint. + + ![Sync Created](/images/secret-syncs/oci-vault/sync-created.png) + + + + + To create an **OCI Vault Sync**, make an API request to the [Create OCI Vault Sync](/api-reference/endpoints/secret-syncs/oci-vault/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/oci-vault \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-oci-vault-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "compartmentOcid": "...", + "vaultOcid": "...", + "keyOcid": "..." + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-oci-vault-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "oci", + "name": "my-oci-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "oci-vault", + "destinationConfig": { + "compartmentOcid": "...", + "vaultOcid": "...", + "keyOcid": "..." + } + } + } + ``` + + + +## FAQ + + + + When Infisical attempts to sync secrets, the sync will fail and attempt to re-sync if **any secret** has one of the following lifecycle states: + - SchedulingDeletion + - CancellingDeletion + - Deleting + - Creating + - Updating + + We do this to prevent any desync issues. + + + In the case that a variable is created or updated while it's scheduled for deletion in OCI Vault, we cancel the deletion and update the variable. This action may take up to a minute since Infisical must wait for OCI to completely cancel the deletion and then update the variable. + + diff --git a/docs/mint.json b/docs/mint.json index 40a9d97c2..61b89c609 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -112,6 +112,7 @@ "pages": [ "documentation/platform/pki/overview", "documentation/platform/pki/private-ca", + "documentation/platform/pki/subscribers", "documentation/platform/pki/certificates", "documentation/platform/pki/pki-issuer", "documentation/platform/pki/est", @@ -349,7 +350,8 @@ "group": "Linux Package", "pages": [ "self-hosting/deployment-options/native/linux-package/installation", - "self-hosting/deployment-options/native/linux-package/commands-configuration" + "self-hosting/deployment-options/native/linux-package/commands-configuration", + "self-hosting/deployment-options/linux-upgrade" ] }, "self-hosting/guides/upgrading-infisical", @@ -444,6 +446,7 @@ ] }, "integrations/frameworks/terraform", + "integrations/frameworks/pulumi", "integrations/platforms/ansible", "integrations/platforms/apache-airflow" ] @@ -468,6 +471,7 @@ "integrations/app-connections/humanitec", "integrations/app-connections/ldap", "integrations/app-connections/mssql", + "integrations/app-connections/oci", "integrations/app-connections/postgres", "integrations/app-connections/teamcity", "integrations/app-connections/terraform-cloud", @@ -494,6 +498,7 @@ "integrations/secret-syncs/github", "integrations/secret-syncs/hashicorp-vault", "integrations/secret-syncs/humanitec", + "integrations/secret-syncs/oci-vault", "integrations/secret-syncs/teamcity", "integrations/secret-syncs/terraform-cloud", "integrations/secret-syncs/vercel", @@ -1180,6 +1185,18 @@ "api-reference/endpoints/app-connections/mssql/delete" ] }, + { + "group": "OCI", + "pages": [ + "api-reference/endpoints/app-connections/oci/list", + "api-reference/endpoints/app-connections/oci/available", + "api-reference/endpoints/app-connections/oci/get-by-id", + "api-reference/endpoints/app-connections/oci/get-by-name", + "api-reference/endpoints/app-connections/oci/create", + "api-reference/endpoints/app-connections/oci/update", + "api-reference/endpoints/app-connections/oci/delete" + ] + }, { "group": "PostgreSQL", "pages": [ @@ -1383,6 +1400,20 @@ "api-reference/endpoints/secret-syncs/humanitec/remove-secrets" ] }, + { + "group": "OCI", + "pages": [ + "api-reference/endpoints/secret-syncs/oci-vault/list", + "api-reference/endpoints/secret-syncs/oci-vault/get-by-id", + "api-reference/endpoints/secret-syncs/oci-vault/get-by-name", + "api-reference/endpoints/secret-syncs/oci-vault/create", + "api-reference/endpoints/secret-syncs/oci-vault/update", + "api-reference/endpoints/secret-syncs/oci-vault/delete", + "api-reference/endpoints/secret-syncs/oci-vault/sync-secrets", + "api-reference/endpoints/secret-syncs/oci-vault/import-secrets", + "api-reference/endpoints/secret-syncs/oci-vault/remove-secrets" + ] + }, { "group": "TeamCity", "pages": [ @@ -1467,6 +1498,18 @@ { "group": "Infisical PKI", "pages": [ + { + "group": "Subscribers", + "pages": [ + "api-reference/endpoints/pki/subscribers/list-certs", + "api-reference/endpoints/pki/subscribers/create", + "api-reference/endpoints/pki/subscribers/read", + "api-reference/endpoints/pki/subscribers/update", + "api-reference/endpoints/pki/subscribers/delete", + "api-reference/endpoints/pki/subscribers/issue-cert", + "api-reference/endpoints/pki/subscribers/sign-cert" + ] + }, { "group": "Certificate Authorities", "pages": [ diff --git a/docs/self-hosting/deployment-options/linux-upgrade.mdx b/docs/self-hosting/deployment-options/linux-upgrade.mdx new file mode 100644 index 000000000..6712626bd --- /dev/null +++ b/docs/self-hosting/deployment-options/linux-upgrade.mdx @@ -0,0 +1,390 @@ +--- +title: "Upgrading" +description: "How to upgrade Infisical deployment using linux package" +--- + +This guide explains how to upgrade Infisical Linux package installations to newer versions. +The Infisical Linux package includes only the Infisical service component itself, as PostgreSQL and Redis databases are managed separately. +Upgrades for PostgreSQL and Redis are not covered in this guide as they depend on your specific database deployment method. + +## Upgrade Options + +There are two primary methods to upgrade Infisical: + +1. **Standard Upgrade (with brief downtime)**: The simplest approach that briefly takes Infisical offline during the upgrade. +2. **Minimal-Downtime Upgrade**: For multi-node deployments where high availability is required. + +## Before You Begin + +### Checking Your Current Version + +Before upgrading, note your current Infisical version: + +```bash +cat /opt/infisical-core/version-manifest.txt +``` + +Look for `infisical` component. This will be the version of Infisical currently installed. + +### Prerequisites + +- Verify that your PostgreSQL and Redis instances are up and running +- Back up your PostgreSQL database before proceeding with any upgrade +- Review release notes for the version you're upgrading to + +### Creating a Database Backup + +We strongly recommend backing up your database before upgrading. +Your backup approach may look different depending on how you configured PostgreSQL and whether it's self-managed or using a managed service. +Here is a sample of how you would perform a manual backup: + +```bash +# Example PostgreSQL backup command (adjust parameters as needed) +pg_dump -U -h -d > infisical_backup.sql +``` + +### Database Migrations During Upgrade + +By default, Infisical runs database migrations automatically on startup. + +- It uses database locks to ensure only one instance runs migrations at a time +- Other instances will wait for the lock to be released before continuing startup +- This prevents race conditions and database conflicts + +## Standard Upgrade (with Downtime) + +This method is suitable for single-node deployments or situations where a brief downtime is acceptable. + + + + ```bash + infisical-ctl stop + ``` + + +To upgrade to the latest version: + + + + ```bash + sudo apt-get update && sudo apt-get install -y infisical-core + ``` + + + ```bash + sudo yum update infisical-core + ``` + + + +To upgrade to a specific version: + + + + ```bash + sudo apt-get install -y infisical-core= + ``` + + + ```bash + sudo yum install infisical-core- + ``` + + + + + + ```bash + infisical-ctl reconfigure + ``` + + + + ```bash + infisical-ctl start + ``` + + + + ```bash + infisical-ctl status + ``` + + Check the logs for any issues: + ```bash + infisical-ctl tail + ``` + + + +## Minimal-Downtime Upgrade + +For multi-node setups where you need to maintain availability during upgrades, follow this procedure. This approach requires at least two Infisical nodes behind a load balancer. + +### Understanding Traffic Draining + +"Draining" a server means gracefully removing it from the pool of active servers without disrupting existing connections. When you drain a server: + +1. The load balancer stops sending new requests to the server +2. Existing connections are allowed to complete naturally +3. Once all connections finish, the server can be safely taken offline for maintenance + +This approach ensures users/machines do not experience sudden connection errors during the upgrade process. + +### Preparing for the Upgrade + +1. **Designate a deploy node**: Choose any single node that will run migrations. This node will be upgraded first. + +2. **Configure your load balancer**: Ensure your load balancer can perform health checks against Infisical's `api/status` endpoint. + +### Upgrade Process + +#### On the deploy node: + + + + +Drain the traffic on this node gracefully. You can do this in a number of ways depending on the load balancer you have configured. +Approaches for some common load balancers are provided below: + + + + If using NGINX as a load balancer, you can remove the server from the upstream pool temporarily: + ```bash + # Edit your NGINX configuration to comment out or remove the server + sudo nano /path/to/your/nginx-config.conf + + # Reload NGINX to apply changes + sudo nginx -s reload + ``` + + + If using HAProxy, you can put the server in maintenance mode: + ```bash + # Using the HAProxy socket command + echo "disable server infisical_backend/infisical-node1" | socat stdio /var/lib/haproxy/stats + ``` + + + Deregister the instance from the load balancer using the AWS console or CLI + + + Follow your load balancer's documentation for instructions on draining procedure + + + + + +Verify no new traffic is arriving before proceeding with the upgrade. + + + +```bash +infisical-ctl stop +``` + + + + +To upgrade to the latest version: + + + + ```bash + sudo apt-get update && sudo apt-get install -y infisical-core + ``` + + + ```bash + sudo yum update infisical-core + ``` + + + +To upgrade to a specific version: + + + + ```bash + sudo apt-get install -y infisical-core= + ``` + + + ```bash + sudo yum install infisical-core- + ``` + + + + + +```bash +infisical-ctl reconfigure +``` + + + +```bash +infisical-ctl tail +``` +Look for successful migration messages in the logs. + + + +Re-enable the server in your load balancer using the same method you used to remove it. + + + +#### On all remaining nodes (one at a time): + + + +Follow the same draining procedure as described for the deploy node: + +- Remove the server from your load balancer's active pool +- Wait for existing connections to complete +- Verify the node is no longer receiving traffic + + + +```bash +infisical-ctl stop +``` + + + +To upgrade to the latest version: + + + + ```bash + sudo apt-get update && sudo apt-get install -y infisical-core + ``` + + + ```bash + sudo yum update infisical-core + ``` + + + +To upgrade to a specific version: + + + + ```bash + sudo apt-get install -y infisical-core= + ``` + + + ```bash + sudo yum install infisical-core- + ``` + + + + + +```bash +infisical-ctl reconfigure +``` + + + +```bash +infisical-ctl status +infisical-ctl tail +``` + + + +- Check logs to ensure the service has started successfully +- Verify it can connect to the database and Redis + + + +Re-enable the server in your load balancer using the same method you used to remove it. + + + +Check logs and monitoring to ensure traffic is flowing correctly. + + + +Repeat steps 1-7 for each remaining node, one at a time. + + + +After all nodes are upgraded, verify that the application is functioning correctly: +- Test core functionality +- Check logs for any errors + + + +## Rolling Back + +If you need to roll back to a previous version of Infisical, follow steps below. + + + +```bash +infisical-ctl stop +``` + + + +For Debian/Ubuntu: +```bash +sudo apt-get install -y infisical-core= +``` + +For RHEL/CentOS/Amazon Linux: +```bash +sudo yum downgrade infisical-core- +``` + + + +Restore your Postgres/Redis database from backup. + + + +```bash +infisical-ctl reconfigure +``` + + + +```bash +infisical-ctl status +``` + + + +## Troubleshooting + + + +If you encounter database migration issues: + +1. Check the logs: + ```bash + infisical-ctl tail + ``` + +2. Ensure the database user has sufficient privileges to create/modify tables. + +3. If migrations fail repeatedly, consider restoring from the backup you took prior to upgrading. + + + + +1. Check for configuration errors: + ```bash + infisical-ctl tail + infisical-ctl status + ``` + +2. Verify all required environment variables are set in your `/etc/infisical/infisical.rb` file. + + \ No newline at end of file diff --git a/docs/self-hosting/guides/custom-certificates.mdx b/docs/self-hosting/guides/custom-certificates.mdx index 67b258d08..41947a0d9 100644 --- a/docs/self-hosting/guides/custom-certificates.mdx +++ b/docs/self-hosting/guides/custom-certificates.mdx @@ -4,19 +4,19 @@ description: "Learn how to configure Infisical with custom certificates" --- By default, the Infisical Docker image includes certificates from well-known public certificate authorities. -However, some integrations with Infisical may need to communicate with your internal services that use private certificate authorities. +However, some integrations with Infisical may need to communicate with your internal services that use private certificate authorities. To configure trust for custom certificates, follow these steps. This is particularly useful for connecting Infisical with self-hosted services like GitLab. ## Prerequisites - Docker - Standalone [Infisical image](https://hub.docker.com/r/infisical/infisical) -- Certificate public key `.pem` files +- Certificate public key `.crt` files ## Setup -1. Place all your public key `.pem` files into a single directory. -2. Mount the directory containing the `.pem` files to the `usr/local/share/ca-certificates/` path in the Infisical container. +1. Place all your public key `.crt` files into a single directory. +2. Mount the directory containing the `.crt` files to the `/usr/local/share/ca-certificates/` path in the Infisical container. 3. Set the following environment variable on your Infisical container: ``` NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e7f57e85c..121dcd094 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -78,7 +78,7 @@ "react-day-picker": "^9.4.3", "react-dom": "^18.3.1", "react-helmet": "^6.1.0", - "react-hook-form": "^7.54.0", + "react-hook-form": "^7.56.3", "react-i18next": "^15.2.0", "react-icons": "^5.4.0", "react-markdown": "^10.0.1", @@ -11484,9 +11484,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.54.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.0.tgz", - "integrity": "sha512-PS05+UQy/IdSbJNojBypxAo9wllhHgGmyr8/dyGQcPoiMf3e7Dfb9PWYVRco55bLbxH9S+1yDDJeTdlYCSxO3A==", + "version": "7.56.3", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.3.tgz", + "integrity": "sha512-IK18V6GVbab4TAo1/cz3kqajxbDPGofdF0w7VHdCo0Nt8PrPlOZcuuDq9YYIV1BtjcX78x0XsldbQRQnQXWXmw==", "license": "MIT", "engines": { "node": ">=18.0.0" diff --git a/frontend/package.json b/frontend/package.json index 6225b78f0..7cd636343 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -82,7 +82,7 @@ "react-day-picker": "^9.4.3", "react-dom": "^18.3.1", "react-helmet": "^6.1.0", - "react-hook-form": "^7.54.0", + "react-hook-form": "^7.56.3", "react-i18next": "^15.2.0", "react-icons": "^5.4.0", "react-markdown": "^10.0.1", diff --git a/frontend/public/images/integrations/Oracle.png b/frontend/public/images/integrations/Oracle.png new file mode 100644 index 000000000..14845d2f2 Binary files /dev/null and b/frontend/public/images/integrations/Oracle.png differ diff --git a/frontend/public/lotties/check.json b/frontend/public/lotties/check.json new file mode 100644 index 000000000..8d66090dc --- /dev/null +++ b/frontend/public/lotties/check.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":60,"w":500,"h":500,"nm":"system-regular-31-check","ddd":0,"assets":[{"id":"comp_1","nm":"hover-check","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[253.419,260.347,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[149.956,-122.947],[-31.321,57.362],[-83.54,5.208]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":1,"s":[100]},{"t":20,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":1,"s":[28.5]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.91,0.91,0.914,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-31-check').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[253.419,260.347,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[149.956,-122.947],[-31.321,57.362],[-83.54,5.208]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.05],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":21,"s":[0]},{"t":60,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.05],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":21,"s":[0]},{"t":60,"s":[28.5]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.91,0.91,0.914,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-31-check').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-180,"ix":10},"p":{"a":0,"k":[250.004,250.003,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.572,-106.399],[106.399,-2.572],[2.572,106.399],[-106.399,2.572]],"o":[[2.572,106.399],[-106.399,2.572],[-2.572,-106.399],[106.399,-2.572]],"v":[[192.652,-4.656],[4.656,192.652],[-192.652,4.656],[-4.656,-192.652]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.91,0.91,0.914,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-31-check').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":1,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,249.974,0],"ix":2,"l":2},"a":{"a":0,"k":[250,249.999,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.57,-1.64],[2.27,-0.05],[1.65,1.56],[0.05,2.27],[-4.68,0.11],[-0.07,0],[-1.6,-1.52],[-0.05,-2.27]],"o":[[-1.57,1.64],[-2.28,0.06],[-1.65,-1.56],[-0.11,-4.69],[0.07,0],[2.19,0],[1.64,1.57],[0.06,2.26]],"v":[[6.15,5.861],[0.2,8.491],[-5.87,6.151],[-8.5,0.201],[-0.21,-8.499],[0,-8.499],[5.86,-6.149],[8.49,-0.199]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[2.67,-0.06],[-0.13,-5.51],[-1.93,-1.84],[-2.58,0],[-0.08,0],[-1.84,1.93],[0.06,2.67],[1.93,1.84]],"o":[[-5.51,0.14],[0.06,2.67],[1.88,1.79],[0.08,0],[2.67,-0.06],[1.84,-1.93],[-0.06,-2.67],[-1.94,-1.84]],"v":[[-0.24,-9.999],[-10,0.241],[-6.9,7.241],[-0.01,10.001],[0.24,10.001],[7.24,6.901],[10,-0.239],[6.9,-7.239]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.91,0.91,0.914,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-31-check').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[250,249.999],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.3,-0.29],[0,0],[0,0],[0.29,-0.29],[-0.29,-0.29],[0,0],[-0.19,0],[-0.15,0.15],[0,0],[0.3,0.3]],"o":[[0,0],[0,0],[-0.29,-0.29],[-0.29,0.29],[0,0],[0.15,0.15],[0.19,0],[0,0],[0.3,-0.29],[-0.29,-0.29]],"v":[[3.476,-3.286],[-1.504,1.694],[-3.484,-0.276],[-4.544,-0.276],[-4.544,0.784],[-2.034,3.284],[-1.504,3.504],[-0.974,3.284],[4.536,-2.226],[4.536,-3.286]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.91,0.91,0.914,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-31-check').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[250.164,250.496],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":300,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,249.974,0],"ix":2,"l":2},"a":{"a":0,"k":[250,249.999,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.57,-1.64],[2.27,-0.05],[1.65,1.56],[0.05,2.27],[-4.68,0.11],[-0.07,0],[-1.6,-1.52],[-0.05,-2.27]],"o":[[-1.57,1.64],[-2.28,0.06],[-1.65,-1.56],[-0.11,-4.69],[0.07,0],[2.19,0],[1.64,1.57],[0.06,2.26]],"v":[[6.15,5.861],[0.2,8.491],[-5.87,6.151],[-8.5,0.201],[-0.21,-8.499],[0,-8.499],[5.86,-6.149],[8.49,-0.199]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[2.67,-0.06],[-0.13,-5.51],[-1.93,-1.84],[-2.58,0],[-0.08,0],[-1.84,1.93],[0.06,2.67],[1.93,1.84]],"o":[[-5.51,0.14],[0.06,2.67],[1.88,1.79],[0.08,0],[2.67,-0.06],[1.84,-1.93],[-0.06,-2.67],[-1.94,-1.84]],"v":[[-0.24,-9.999],[-10,0.241],[-6.9,7.241],[-0.01,10.001],[0.24,10.001],[7.24,6.901],[10,-0.239],[6.9,-7.239]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.91,0.91,0.914,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-31-check').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[250,249.999],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.3,-0.29],[0,0],[0,0],[0.29,-0.29],[-0.29,-0.29],[0,0],[-0.19,0],[-0.15,0.15],[0,0],[0.3,0.3]],"o":[[0,0],[0,0],[-0.29,-0.29],[-0.29,0.29],[0,0],[0.15,0.15],[0.19,0],[0,0],[0.3,-0.29],[-0.29,-0.29]],"v":[[3.476,-3.286],[-1.504,1.694],[-3.484,-0.276],[-4.544,-0.276],[-4.544,0.784],[-2.034,3.284],[-1.504,3.504],[-0.974,3.284],[4.536,-2.226],[4.536,-3.286]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.91,0.91,0.914,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-31-check').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[250.164,250.496],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"control","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"primary","np":3,"mn":"ADBE Color Control","ix":1,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[0.91,0.91,0.914],"ix":1}}]}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"hover-check","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":70,"st":0,"bm":0}],"markers":[{"tm":0,"cm":"default:hover-check","dr":60}],"props":{}} \ No newline at end of file diff --git a/frontend/public/lotties/pki-subscriber.json b/frontend/public/lotties/pki-subscriber.json new file mode 100644 index 000000000..f6e0ce16e --- /dev/null +++ b/frontend/public/lotties/pki-subscriber.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":89,"w":430,"h":430,"nm":"wired-outline-88-document-user","ddd":0,"assets":[{"id":"comp_1","nm":"Content-12","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"outline 4","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.41,"y":0},"t":6,"s":[0.044,-73.171,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":35,"s":[0.044,-117.966,0],"to":[0,0,0],"ti":[0,0,0]},{"t":50,"s":[0.044,-100.171,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,22.108],[22.108,0],[0,-22.108],[-22.108,0]],"o":[[0,-22.108],[-22.108,0],[0,22.108],[22.108,0]],"v":[[40.03,0],[0,-40.03],[-40.03,0],[0,40.03]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-88-document-user').layer('control').effect('secondary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-88-document-user').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".secondary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"secondary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"outline 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.14,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[214.956,575.075,0],"to":[0,0,0],"ti":[0,0,0]},{"t":29,"s":[214.956,315.075,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-30.376,0],[0,0],[0,-30.376]],"o":[[0,0],[0,0],[0,-30.376],[0,0],[30.376,0],[0,0]],"v":[[80.015,33.358],[-80.015,33.358],[-80.015,21.642],[-25.015,-33.358],[25.015,-33.358],[80.015,21.642]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-88-document-user').layer('control').effect('secondary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-88-document-user').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".secondary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"secondary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0}]},{"id":"comp_3","nm":"Content-36","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"outline 4","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.41,"y":0},"t":6,"s":[0.044,-73.171,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":35,"s":[0.044,-117.966,0],"to":[0,0,0],"ti":[0,0,0]},{"t":50,"s":[0.044,-100.171,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,22.108],[22.108,0],[0,-22.108],[-22.108,0]],"o":[[0,-22.108],[-22.108,0],[0,22.108],[22.108,0]],"v":[[40.03,0],[0,-40.03],[-40.03,0],[0,40.03]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-88-document-user').layer('control').effect('secondary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-88-document-user').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".secondary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"secondary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"outline 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.14,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[214.956,575.075,0],"to":[0,0,0],"ti":[0,0,0]},{"t":29,"s":[214.956,315.075,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-30.376,0],[0,0],[0,-30.376]],"o":[[0,0],[0,0],[0,-30.376],[0,0],[30.376,0],[0,0]],"v":[[80.015,33.358],[-80.015,33.358],[-80.015,21.642],[-25.015,-33.358],[25.015,-33.358],[80.015,21.642]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-88-document-user').layer('control').effect('secondary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-88-document-user').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".secondary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"secondary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0}]},{"id":"comp_4","nm":"hover-swipe","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Page-corner","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.001,249.76,0],"ix":2,"l":2},"a":{"a":0,"k":[250.001,249.76,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.22,"y":1},"o":{"x":0.333,"y":0},"t":42,"s":[{"i":[[0,0],[-49.694,-50.431],[0,0]],"o":[[0,0],[50.313,51.06],[0,0]],"v":[[-53.373,-53.373],[-0.373,-0.627],[53.373,53.373]],"c":false}]},{"t":89,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-53.373,-53.373],[-53.373,53.373],[53.373,53.373]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-88-document-user').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-88-document-user').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[330.06,116.567],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Page","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.243],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.326],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[9]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":47,"s":[-7]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":70,"s":[5]},{"t":89,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.243,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[317.001,368.76,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.326,"y":1},"o":{"x":0.333,"y":0},"t":20,"s":[351.001,381.76,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":42,"s":[291.751,356.51,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":65,"s":[321.001,369.26,0],"to":[0,0,0],"ti":[0,0,0]},{"t":80,"s":[317.001,368.76,0]}],"ix":2,"l":2},"a":{"a":0,"k":[352.001,403.76,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-53.373,-53.373],[-53.373,53.373],[53.373,53.373]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":20,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-53.373,-53.373],[-53.373,53.373],[53.373,53.373]],"c":false}]},{"t":38,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-213.237,-53.373],[-213.237,319.57],[53.373,319.57]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-88-document-user').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-88-document-user').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[330.06,116.567],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"t":20,"s":[100],"h":1},{"t":38,"s":[0],"h":1}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[26.69,-186.57],[-133.43,-186.57],[-133.43,186.57],[133.43,186.57],[133.43,-79.82]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-88-document-user').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-88-document-user').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[250,249.76],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"mask","parent":2,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.001,249.76,0],"ix":2,"l":2},"a":{"a":0,"k":[250.001,249.76,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":20,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[26.69,-186.57],[26.75,-186.57],[26.75,-79.76],[133.43,-79.76],[133.43,-79.82]],"c":true}]},{"t":38,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[26.69,-186.57],[-133.43,-186.57],[-133.43,186.57],[133.43,186.57],[133.43,-79.82]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250,249.76],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":51,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"Content-12","parent":2,"tt":2,"tp":3,"refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[215,215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":430,"h":430,"ip":0,"op":51,"st":-50,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"Content-12","parent":2,"refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[215,215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[348.631,28.403],[82.211,28.403],[82.211,401.557],[348.631,401.557]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"w":430,"h":430,"ip":37.5,"op":881.5,"st":37.5,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"control","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"stroke","np":3,"mn":"Pseudo/@@jxAy4KF1Sn6X4aYQ0vVH/w","ix":1,"en":1,"ef":[{"ty":7,"nm":"Menu","mn":"Pseudo/@@jxAy4KF1Sn6X4aYQ0vVH/w-0001","ix":1,"v":{"a":0,"k":3,"ix":1}}]},{"ty":5,"nm":"primary","np":3,"mn":"ADBE Color Control","ix":2,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]},{"ty":5,"nm":"secondary","np":3,"mn":"ADBE Color Control","ix":3,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]}],"ip":0,"op":360,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"hover-swipe","refId":"comp_4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[215,215,0],"ix":2,"l":2},"a":{"a":0,"k":[215,215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":430,"h":430,"ip":0,"op":99,"st":0,"bm":0}],"markers":[{"tm":0,"cm":"default:hover-swipe","dr":89}],"props":{}} \ No newline at end of file diff --git a/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEditRoleForm.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEditRoleForm.tsx index 7431b41fa..a4bc3a73a 100644 --- a/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEditRoleForm.tsx +++ b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEditRoleForm.tsx @@ -1,5 +1,5 @@ import { Controller, FormProvider, useForm } from "react-hook-form"; -import { faChevronLeft, faPlus, faSave } from "@fortawesome/free-solid-svg-icons"; +import { faChevronLeft, faSave } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { twMerge } from "tailwind-merge"; @@ -9,12 +9,11 @@ import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input } from "@app/components/v2"; import { ProjectPermissionSub } from "@app/context"; import { isCustomProjectRole } from "@app/helpers/roles"; -import { usePopUp } from "@app/hooks"; import { TProjectTemplate, useUpdateProjectTemplate } from "@app/hooks/api/projectTemplates"; import { slugSchema } from "@app/lib/schemas"; +import { AddPoliciesButton } from "@app/pages/project/RoleDetailsBySlugPage/components/AddPoliciesButton"; import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies"; import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState"; -import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal"; import { formRolePermission2API, PROJECT_PERMISSION_OBJECT, @@ -44,8 +43,6 @@ export const ProjectTemplateEditRoleForm = ({ role, isDisabled }: Props) => { - const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const); - const formMethods = useForm({ values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined, resolver: zodResolver(formSchema) @@ -120,7 +117,7 @@ export const ProjectTemplateEditRoleForm = ({ variant="outline_bg" type="submit" className={twMerge( - "h-10 rounded-r-none border border-primary", + "mr-4 h-10 border border-primary", isDirty && "bg-primary text-black" )} isDisabled={isSubmitting || !isDirty || isDisabled} @@ -129,19 +126,7 @@ export const ProjectTemplateEditRoleForm = ({ > Save - - handlePopUpToggle("addPolicy", isOpen)} - /> +
)} diff --git a/frontend/src/components/secret-syncs/SecretSyncStatusBadge.tsx b/frontend/src/components/secret-syncs/SecretSyncStatusBadge.tsx index dbf543f61..53b53d5c0 100644 --- a/frontend/src/components/secret-syncs/SecretSyncStatusBadge.tsx +++ b/frontend/src/components/secret-syncs/SecretSyncStatusBadge.tsx @@ -40,7 +40,14 @@ export const SecretSyncStatusBadge = ({ status }: Props) => { return ( - + {text} ); diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/OCIVaultSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/OCIVaultSyncFields.tsx new file mode 100644 index 000000000..26fa601f6 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/OCIVaultSyncFields.tsx @@ -0,0 +1,175 @@ +import { Controller, useFormContext, useWatch } from "react-hook-form"; +import { SingleValue } from "react-select"; +import { faCircleInfo } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2"; +import { + useOCIConnectionListCompartments, + useOCIConnectionListVaultKeys, + useOCIConnectionListVaults +} from "@app/hooks/api/appConnections/oci"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const OCIVaultSyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.OCIVault } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + + // Compartments + const { data: compartments, isLoading: isCompartmentsLoading } = useOCIConnectionListCompartments( + connectionId, + { + enabled: Boolean(connectionId) + } + ); + + // Vaults + const selectedCompartment = useWatch({ name: "destinationConfig.compartmentOcid", control }); + const { data: vaults, isLoading: isVaultsLoading } = useOCIConnectionListVaults( + { connectionId, compartmentOcid: selectedCompartment }, + { + enabled: Boolean(connectionId && selectedCompartment) + } + ); + + // Keys + const selectedVault = useWatch({ name: "destinationConfig.vaultOcid", control }); + const { data: keys, isLoading: isKeysLoading } = useOCIConnectionListVaultKeys( + { connectionId, compartmentOcid: selectedCompartment, vaultOcid: selectedVault }, + { + enabled: Boolean(connectionId && selectedCompartment && selectedVault) + } + ); + + return ( + <> + { + setValue("destinationConfig.compartmentOcid", ""); + setValue("destinationConfig.vaultOcid", ""); + setValue("destinationConfig.keyOcid", ""); + }} + /> + + ( + +
+ Don't see the compartment you're looking for?{" "} + +
+ + } + > + c.id === value) ?? null} + onChange={(option) => { + onChange((option as SingleValue<{ id: string }>)?.id ?? null); + setValue("destinationConfig.vaultOcid", ""); + setValue("destinationConfig.keyOcid", ""); + }} + options={compartments} + placeholder="Select a compartment..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + /> +
+ )} + /> + + ( + +
+ Don't see the vault you're looking for?{" "} + +
+ + } + > + v.id === value) ?? null} + onChange={(option) => { + onChange((option as SingleValue<{ id: string }>)?.id ?? null); + setValue("destinationConfig.keyOcid", ""); + }} + options={vaults} + placeholder="Select a vault..." + getOptionLabel={(option) => option.displayName} + getOptionValue={(option) => option.id} + /> +
+ )} + /> + + ( + +
+ Don't see the key you're looking for?{" "} + +
+ + } + > + v.id === value) ?? null} + onChange={(option) => { + onChange((option as SingleValue<{ id: string }>)?.id ?? null); + }} + options={keys} + placeholder="Select a key..." + getOptionLabel={(option) => option.displayName} + getOptionValue={(option) => option.id} + /> +
+ )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index 1d7a1dd55..2cac1ae20 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -13,6 +13,7 @@ import { GcpSyncFields } from "./GcpSyncFields"; import { GitHubSyncFields } from "./GitHubSyncFields"; import { HCVaultSyncFields } from "./HCVaultSyncFields"; import { HumanitecSyncFields } from "./HumanitecSyncFields"; +import { OCIVaultSyncFields } from "./OCIVaultSyncFields"; import { TeamCitySyncFields } from "./TeamCitySyncFields"; import { TerraformCloudSyncFields } from "./TerraformCloudSyncFields"; import { VercelSyncFields } from "./VercelSyncFields"; @@ -52,6 +53,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.TeamCity: return ; + case SecretSync.OCIVault: + return ; default: throw new Error(`Unhandled Destination Config Field: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index e4aa4ad65..9eec461cd 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -45,6 +45,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.Windmill: case SecretSync.HCVault: case SecretSync.TeamCity: + case SecretSync.OCIVault: AdditionalSyncOptionsFieldsComponent = null; break; default: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/OCIVaultSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/OCIVaultSyncReviewFields.tsx new file mode 100644 index 000000000..16166c88c --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/OCIVaultSyncReviewFields.tsx @@ -0,0 +1,26 @@ +import { useFormContext } from "react-hook-form"; + +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { GenericFieldLabel } from "@app/components/v2"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const OCIVaultSyncReviewFields = () => { + const { watch } = useFormContext(); + const compartmentOcid = watch("destinationConfig.compartmentOcid"); + const vaultOcid = watch("destinationConfig.vaultOcid"); + const keyOcid = watch("destinationConfig.keyOcid"); + + return ( + <> + + {compartmentOcid} + + + {vaultOcid} + + + {keyOcid} + + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index 62402e540..b7f62c139 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -23,6 +23,7 @@ import { GcpSyncReviewFields } from "./GcpSyncReviewFields"; import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields"; import { HCVaultSyncReviewFields } from "./HCVaultSyncReviewFields"; import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields"; +import { OCIVaultSyncReviewFields } from "./OCIVaultSyncReviewFields"; import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields"; import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields"; import { VercelSyncReviewFields } from "./VercelSyncReviewFields"; @@ -96,6 +97,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.TeamCity: DestinationFieldsComponent = ; break; + case SecretSync.OCIVault: + DestinationFieldsComponent = ; + break; default: throw new Error(`Unhandled Destination Review Fields: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/schemas/oci-vault-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/oci-vault-sync-destination-schema.ts new file mode 100644 index 000000000..84eb6a362 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/oci-vault-sync-destination-schema.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; + +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const OCIVaultSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.OCIVault), + destinationConfig: z.object({ + compartmentOcid: z + .string() + .trim() + .min(1, "Compartment OCID required") + .regex( + /^ocid1\.(tenancy|compartment)\.oc1\..+$/, + "Invalid Compartment OCID format. Must start with ocid1.tenancy.oc1. or ocid1.compartment.oc1." + ), + vaultOcid: z + .string() + .trim() + .min(1, "Vault OCID required") + .regex( + /^ocid1\.vault\.oc1\..+$/, + "Invalid Vault OCID format. Must start with ocid1.vault.oc1." + ), + keyOcid: z + .string() + .trim() + .min(1, "Key OCID required") + .regex(/^ocid1\.key\.oc1\..+$/, "Invalid Key OCID format. Must start with ocid1.key.oc1.") + }) + }) +); diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts index bc6184bc7..232b8cedf 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts @@ -10,6 +10,7 @@ import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema"; import { GitHubSyncDestinationSchema } from "./github-sync-destination-schema"; import { HCVaultSyncDestinationSchema } from "./hc-vault-sync-destination-schema"; import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema"; +import { OCIVaultSyncDestinationSchema } from "./oci-vault-sync-destination-schema"; import { TeamCitySyncDestinationSchema } from "./teamcity-sync-destination-schema"; import { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema"; import { VercelSyncDestinationSchema } from "./vercel-sync-destination-schema"; @@ -29,7 +30,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ VercelSyncDestinationSchema, WindmillSyncDestinationSchema, HCVaultSyncDestinationSchema, - TeamCitySyncDestinationSchema + TeamCitySyncDestinationSchema, + OCIVaultSyncDestinationSchema ]); export const SecretSyncFormSchema = SecretSyncUnionSchema; diff --git a/frontend/src/components/v2/GenericFieldLabel/GenericFieldLabel.tsx b/frontend/src/components/v2/GenericFieldLabel/GenericFieldLabel.tsx index 5200e1898..95eaf5745 100644 --- a/frontend/src/components/v2/GenericFieldLabel/GenericFieldLabel.tsx +++ b/frontend/src/components/v2/GenericFieldLabel/GenericFieldLabel.tsx @@ -6,14 +6,21 @@ type Props = { children?: ReactNode; className?: string; labelClassName?: string; + truncate?: boolean; }; -export const GenericFieldLabel = ({ label, children, className, labelClassName }: Props) => { +export const GenericFieldLabel = ({ + label, + children, + className, + labelClassName, + truncate +}: Props) => { return ( -
+

{label}

{children ? ( -

{children}

+

{children}

) : (

None

)} diff --git a/frontend/src/const/routes.ts b/frontend/src/const/routes.ts index 5efe7ca69..e390fbcc6 100644 --- a/frontend/src/const/routes.ts +++ b/frontend/src/const/routes.ts @@ -23,6 +23,10 @@ export const ROUTE_PATHS = Object.freeze({ "/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback" ) }, + SsoPage: setRoute( + "/organization/sso", + "/_authenticate/_inject-org-details/_org-layout/organization/sso" + ), SecretScanning: setRoute( "/organization/secret-scanning", "/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning" @@ -283,9 +287,13 @@ export const ROUTE_PATHS = Object.freeze({ "/cert-manager/$projectId/ca/$caId", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId" ), - OverviewPage: setRoute( - "/cert-manager/$projectId/overview", - "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview" + SubscribersPage: setRoute( + "/cert-manager/$projectId/subscribers", + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers" + ), + CertificatesPage: setRoute( + "/cert-manager/$projectId/certificates", + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates" ), CertificateAuthoritiesPage: setRoute( "/cert-manager/$projectId/certificate-authorities", @@ -298,6 +306,10 @@ export const ROUTE_PATHS = Object.freeze({ PkiCollectionDetailsByIDPage: setRoute( "/cert-manager/$projectId/pki-collections/$collectionId", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId" + ), + PkiSubscriberDetailsByIDPage: setRoute( + "/cert-manager/$projectId/subscribers/$subscriberName", + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName" ) }, Ssh: { diff --git a/frontend/src/context/ProjectPermissionContext/index.tsx b/frontend/src/context/ProjectPermissionContext/index.tsx index b195571f8..d7b7334ea 100644 --- a/frontend/src/context/ProjectPermissionContext/index.tsx +++ b/frontend/src/context/ProjectPermissionContext/index.tsx @@ -9,5 +9,7 @@ export { ProjectPermissionIdentityActions, ProjectPermissionKmipActions, ProjectPermissionMemberActions, + ProjectPermissionPkiSubscriberActions, + ProjectPermissionSshHostActions, ProjectPermissionSub } from "./types"; diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index d1a257653..a640f6eaa 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -95,6 +95,15 @@ export enum ProjectPermissionSshHostActions { IssueHostCert = "issue-host-cert" } +export enum ProjectPermissionPkiSubscriberActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + IssueCert = "issue-cert", + ListCerts = "list-certs" +} + export enum ProjectPermissionSecretRotationActions { Read = "read", ReadGeneratedCredentials = "read-generated-credentials", @@ -186,6 +195,7 @@ export enum ProjectPermissionSub { SshHostGroups = "ssh-host-groups", PkiAlerts = "pki-alerts", PkiCollections = "pki-collections", + PkiSubscribers = "pki-subscribers", Kms = "kms", Cmek = "cmek", SecretSyncs = "secret-syncs", @@ -220,6 +230,14 @@ export type SecretRotationSubjectFields = { secretPath: string; }; +export type SshHostSubjectFields = { + hostname: string; +}; + +export type PkiSubscriberSubjectFields = { + name: string; +}; + export type ProjectPermissionSet = | [ ProjectPermissionSecretActions, @@ -282,7 +300,20 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificates] | [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups] - | [ProjectPermissionSshHostActions, ProjectPermissionSub.SshHosts] + | [ + ProjectPermissionSshHostActions, + ( + | ProjectPermissionSub.SshHosts + | (ForcedSubject & SshHostSubjectFields) + ) + ] + | [ + ProjectPermissionPkiSubscriberActions, + ( + | ProjectPermissionSub.PkiSubscribers + | (ForcedSubject & PkiSubscriberSubjectFields) + ) + ] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] | [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs] diff --git a/frontend/src/context/index.tsx b/frontend/src/context/index.tsx index 04af3c8a4..91fcd9055 100644 --- a/frontend/src/context/index.tsx +++ b/frontend/src/context/index.tsx @@ -17,6 +17,8 @@ export { ProjectPermissionIdentityActions, ProjectPermissionKmipActions, ProjectPermissionMemberActions, + ProjectPermissionPkiSubscriberActions, + ProjectPermissionSshHostActions, ProjectPermissionSub, useProjectPermission } from "./ProjectPermissionContext"; diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 68715f9a0..8caa13a5b 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -30,6 +30,7 @@ import { VercelConnectionMethod, WindmillConnectionMethod } from "@app/hooks/api/appConnections/types"; +import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-connection"; export const APP_CONNECTION_MAP: Record< AppConnection, @@ -61,7 +62,8 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.Auth0]: { name: "Auth0", image: "Auth0.png", size: 40 }, [AppConnection.HCVault]: { name: "Hashicorp Vault", image: "Vault.png", size: 65 }, [AppConnection.LDAP]: { name: "LDAP", image: "LDAP.png", size: 65 }, - [AppConnection.TeamCity]: { name: "TeamCity", image: "TeamCity.png" } + [AppConnection.TeamCity]: { name: "TeamCity", image: "TeamCity.png" }, + [AppConnection.OCI]: { name: "OCI", image: "Oracle.png" } }; export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => { @@ -74,6 +76,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case GitHubConnectionMethod.OAuth: return { name: "OAuth", icon: faPassport }; case AwsConnectionMethod.AccessKey: + case OCIConnectionMethod.AccessKey: return { name: "Access Key", icon: faKey }; case AwsConnectionMethod.AssumeRole: return { name: "Assume Role", icon: faUser }; diff --git a/frontend/src/helpers/project.ts b/frontend/src/helpers/project.ts index cc5c6fa91..3e0d0f52e 100644 --- a/frontend/src/helpers/project.ts +++ b/frontend/src/helpers/project.ts @@ -61,6 +61,9 @@ export const initProjectHelper = async ({ projectName }: { projectName: string } return project; }; export const getProjectHomePage = (workspace: Workspace) => { + if (workspace.type === ProjectType.CertificateManager) { + return `/${workspace.type}/$projectId/subscribers` as const; + } return `/${workspace.type}/$projectId/overview` as const; }; diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 58d9f3e48..80df92ac3 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -47,6 +47,10 @@ export const SECRET_SYNC_MAP: Record = { [SecretSync.Vercel]: AppConnection.Vercel, [SecretSync.Windmill]: AppConnection.Windmill, [SecretSync.HCVault]: AppConnection.HCVault, - [SecretSync.TeamCity]: AppConnection.TeamCity + [SecretSync.TeamCity]: AppConnection.TeamCity, + [SecretSync.OCIVault]: AppConnection.OCI }; export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record< diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 5e1f84cb4..06a5056af 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -16,5 +16,6 @@ export enum AppConnection { Auth0 = "auth0", HCVault = "hashicorp-vault", LDAP = "ldap", - TeamCity = "teamcity" + TeamCity = "teamcity", + OCI = "oci" } diff --git a/frontend/src/hooks/api/appConnections/oci/index.ts b/frontend/src/hooks/api/appConnections/oci/index.ts new file mode 100644 index 000000000..2c1906d36 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/oci/index.ts @@ -0,0 +1,2 @@ +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/appConnections/oci/queries.tsx b/frontend/src/hooks/api/appConnections/oci/queries.tsx new file mode 100644 index 000000000..f0e2659b7 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/oci/queries.tsx @@ -0,0 +1,108 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { appConnectionKeys } from "../queries"; +import { + TListOCIVaultKeys, + TListOCIVaults, + TOCICompartment, + TOCIVault, + TOCIVaultKey +} from "./types"; + +const ociConnectionKeys = { + all: [...appConnectionKeys.all, "oci"] as const, + listCompartments: (connectionId: string) => + [...ociConnectionKeys.all, "compartments", connectionId] as const, + listVaults: (connectionId: string, compartmentOcid: string) => + [...ociConnectionKeys.all, "vaults", connectionId, compartmentOcid] as const, + listVaultKeys: (connectionId: string, compartmentOcid: string, vaultOcid: string) => + [...ociConnectionKeys.all, "keys", connectionId, compartmentOcid, vaultOcid] as const +}; + +export const useOCIConnectionListCompartments = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TOCICompartment[], + unknown, + TOCICompartment[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: ociConnectionKeys.listCompartments(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/oci/${connectionId}/compartments` + ); + + return data; + }, + ...options + }); +}; + +export const useOCIConnectionListVaults = ( + { connectionId, compartmentOcid }: TListOCIVaults, + options?: Omit< + UseQueryOptions< + TOCIVault[], + unknown, + TOCIVault[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: ociConnectionKeys.listVaults(connectionId, compartmentOcid), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/oci/${connectionId}/vaults`, + { + params: { + compartmentOcid + } + } + ); + + return data; + }, + ...options + }); +}; + +export const useOCIConnectionListVaultKeys = ( + { connectionId, compartmentOcid, vaultOcid }: TListOCIVaultKeys, + options?: Omit< + UseQueryOptions< + TOCIVaultKey[], + unknown, + TOCIVaultKey[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: ociConnectionKeys.listVaultKeys(connectionId, compartmentOcid, vaultOcid), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/oci/${connectionId}/vault-keys`, + { + params: { + compartmentOcid, + vaultOcid + } + } + ); + + return data; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/oci/types.ts b/frontend/src/hooks/api/appConnections/oci/types.ts new file mode 100644 index 000000000..da12116bd --- /dev/null +++ b/frontend/src/hooks/api/appConnections/oci/types.ts @@ -0,0 +1,27 @@ +// Response types +export type TOCICompartment = { + id: string; + name: string; +}; + +export type TOCIVault = { + id: string; + displayName: string; +}; + +export type TOCIVaultKey = { + id: string; + displayName: string; +}; + +// Param types +export type TListOCIVaults = { + connectionId: string; + compartmentOcid: string; +}; + +export type TListOCIVaultKeys = { + connectionId: string; + compartmentOcid: string; + vaultOcid: string; +}; diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index 910716c02..79cbb81b9 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -84,6 +84,10 @@ export type TTeamCityConnectionOption = TAppConnectionOptionBase & { app: AppConnection.TeamCity; }; +export type TOCIConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.OCI; +}; + export type TAppConnectionOption = | TAwsConnectionOption | TGitHubConnectionOption @@ -101,7 +105,8 @@ export type TAppConnectionOption = | TWindmillConnectionOption | TAuth0ConnectionOption | THCVaultConnectionOption - | TTeamCityConnectionOption; + | TTeamCityConnectionOption + | TOCIConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; @@ -122,4 +127,5 @@ export type TAppConnectionOptionMap = { [AppConnection.HCVault]: THCVaultConnectionOption; [AppConnection.LDAP]: TLdapConnectionOption; [AppConnection.TeamCity]: TTeamCityConnectionOption; + [AppConnection.OCI]: TOCIConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 00c0c3f3a..2b29c2cd4 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -13,6 +13,7 @@ import { THCVaultConnection } from "./hc-vault-connection"; import { THumanitecConnection } from "./humanitec-connection"; import { TLdapConnection } from "./ldap-connection"; import { TMsSqlConnection } from "./mssql-connection"; +import { TOCIConnection } from "./oci-connection"; import { TPostgresConnection } from "./postgres-connection"; import { TTeamCityConnection } from "./teamcity-connection"; import { TTerraformCloudConnection } from "./terraform-cloud-connection"; @@ -32,6 +33,7 @@ export * from "./hc-vault-connection"; export * from "./humanitec-connection"; export * from "./ldap-connection"; export * from "./mssql-connection"; +export * from "./oci-connection"; export * from "./postgres-connection"; export * from "./teamcity-connection"; export * from "./terraform-cloud-connection"; @@ -56,7 +58,8 @@ export type TAppConnection = | TAuth0Connection | THCVaultConnection | TLdapConnection - | TTeamCityConnection; + | TTeamCityConnection + | TOCIConnection; export type TAvailableAppConnection = Pick; @@ -102,4 +105,5 @@ export type TAppConnectionMap = { [AppConnection.HCVault]: THCVaultConnection; [AppConnection.LDAP]: TLdapConnection; [AppConnection.TeamCity]: TTeamCityConnection; + [AppConnection.OCI]: TOCIConnection; }; diff --git a/frontend/src/hooks/api/appConnections/types/oci-connection.ts b/frontend/src/hooks/api/appConnections/types/oci-connection.ts new file mode 100644 index 000000000..f6b5c2cad --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/oci-connection.ts @@ -0,0 +1,17 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum OCIConnectionMethod { + AccessKey = "access-key" +} + +export type TOCIConnection = TRootAppConnection & { app: AppConnection.OCI } & { + method: OCIConnectionMethod.AccessKey; + credentials: { + userOcid: string; + tenancyOcid: string; + region: string; + fingerprint: string; + privateKey: string; + }; +}; diff --git a/frontend/src/hooks/api/certificates/mutations.tsx b/frontend/src/hooks/api/certificates/mutations.tsx index 7e9cf4f91..74d6b5cbb 100644 --- a/frontend/src/hooks/api/certificates/mutations.tsx +++ b/frontend/src/hooks/api/certificates/mutations.tsx @@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { pkiSubscriberKeys } from "../pkiSubscriber/queries"; import { workspaceKeys } from "../workspace"; import { TCertificate, TDeleteCertDTO, TRevokeCertDTO } from "./types"; @@ -42,6 +43,9 @@ export const useRevokeCert = () => { queryClient.invalidateQueries({ queryKey: workspaceKeys.forWorkspaceCertificates(projectSlug) }); + queryClient.invalidateQueries({ + queryKey: pkiSubscriberKeys.allPkiSubscriberCertificates() + }); } }); }; diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 4bc06f7e3..4b4967f16 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -27,6 +27,7 @@ export * from "./orgAdmin"; export * from "./organization"; export * from "./pkiAlerts"; export * from "./pkiCollections"; +export * from "./pkiSubscriber"; export * from "./projectUserAdditionalPrivilege"; export * from "./rateLimit"; export * from "./roles"; diff --git a/frontend/src/hooks/api/pkiSubscriber/constants.tsx b/frontend/src/hooks/api/pkiSubscriber/constants.tsx new file mode 100644 index 000000000..1de5e9ddb --- /dev/null +++ b/frontend/src/hooks/api/pkiSubscriber/constants.tsx @@ -0,0 +1,20 @@ +export enum PkiSubscriberStatus { + ACTIVE = "active", + DISABLED = "disabled" +} + +export const pkiSubscriberStatusToNameMap: { [K in PkiSubscriberStatus]: string } = { + [PkiSubscriberStatus.ACTIVE]: "Active", + [PkiSubscriberStatus.DISABLED]: "Disabled" +}; + +export const getPkiSubscriberStatusBadgeVariant = (status: PkiSubscriberStatus) => { + switch (status) { + case PkiSubscriberStatus.ACTIVE: + return "success"; + case PkiSubscriberStatus.DISABLED: + return "danger"; + default: + return "primary"; + } +}; diff --git a/frontend/src/hooks/api/pkiSubscriber/index.tsx b/frontend/src/hooks/api/pkiSubscriber/index.tsx new file mode 100644 index 000000000..b086839df --- /dev/null +++ b/frontend/src/hooks/api/pkiSubscriber/index.tsx @@ -0,0 +1,7 @@ +export { + useCreatePkiSubscriber, + useDeletePkiSubscriber, + useIssuePkiSubscriberCert, + useUpdatePkiSubscriber +} from "./mutations"; +export { useGetPkiSubscriber, useGetPkiSubscriberCertificates } from "./queries"; diff --git a/frontend/src/hooks/api/pkiSubscriber/mutations.tsx b/frontend/src/hooks/api/pkiSubscriber/mutations.tsx new file mode 100644 index 000000000..a7d0eef92 --- /dev/null +++ b/frontend/src/hooks/api/pkiSubscriber/mutations.tsx @@ -0,0 +1,110 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TCreateCertificateResponse } from "../ca/types"; +import { workspaceKeys } from "../workspace/query-keys"; +import { pkiSubscriberKeys } from "./queries"; +import { + TCreatePkiSubscriberDTO, + TDeletePkiSubscriberDTO, + TIssuePkiSubscriberCertDTO, + TPkiSubscriber, + TUpdatePkiSubscriberDTO +} from "./types"; + +export const useCreatePkiSubscriber = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (body) => { + const { data: subscriber } = await apiRequest.post("/api/v1/pki/subscribers", body); + return subscriber; + }, + onSuccess: ({ projectId, name }) => { + queryClient.invalidateQueries({ + queryKey: workspaceKeys.getWorkspacePkiSubscribers(projectId) + }); + queryClient.invalidateQueries({ + queryKey: pkiSubscriberKeys.getPkiSubscriber({ + subscriberName: name, + projectId + }) + }); + } + }); +}; + +export const useUpdatePkiSubscriber = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ subscriberName, ...body }) => { + const { data: subscriber } = await apiRequest.patch( + `/api/v1/pki/subscribers/${subscriberName}`, + body + ); + return subscriber; + }, + onSuccess: ({ projectId, name }) => { + queryClient.invalidateQueries({ + queryKey: workspaceKeys.getWorkspacePkiSubscribers(projectId) + }); + queryClient.invalidateQueries({ + queryKey: pkiSubscriberKeys.getPkiSubscriber({ + subscriberName: name, + projectId + }) + }); + } + }); +}; + +export const useDeletePkiSubscriber = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ subscriberName, projectId }) => { + const { data: subscriber } = await apiRequest.delete( + `/api/v1/pki/subscribers/${subscriberName}`, + { + data: { + projectId + } + } + ); + return subscriber; + }, + onSuccess: ({ name, projectId }) => { + queryClient.invalidateQueries({ + queryKey: workspaceKeys.getWorkspacePkiSubscribers(projectId) + }); + queryClient.invalidateQueries({ + queryKey: pkiSubscriberKeys.getPkiSubscriber({ + subscriberName: name, + projectId + }) + }); + } + }); +}; + +export const useIssuePkiSubscriberCert = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ subscriberName, projectId }) => { + const { data } = await apiRequest.post( + `/api/v1/pki/subscribers/${subscriberName}/issue-certificate`, + { + projectId + } + ); + return data; + }, + onSuccess: (_, { subscriberName, projectId }) => { + queryClient.invalidateQueries({ + queryKey: pkiSubscriberKeys.forPkiSubscriberCertificates({ + subscriberName, + projectId + }) + }); + } + }); +}; diff --git a/frontend/src/hooks/api/pkiSubscriber/queries.tsx b/frontend/src/hooks/api/pkiSubscriber/queries.tsx new file mode 100644 index 000000000..d9948ed5c --- /dev/null +++ b/frontend/src/hooks/api/pkiSubscriber/queries.tsx @@ -0,0 +1,102 @@ +import { useQuery } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TCertificate } from "../certificates/types"; +import { TPkiSubscriber } from "./types"; + +export const pkiSubscriberKeys = { + getPkiSubscriber: ({ + subscriberName, + projectId + }: { + subscriberName: string; + projectId: string; + }) => [{ subscriberName, projectId }, "pki-subscriber"] as const, + allPkiSubscriberCertificates: () => ["pki-subscriber-certificates"] as const, + forPkiSubscriberCertificates: ({ + subscriberName, + projectId + }: { + subscriberName: string; + projectId: string; + }) => [...pkiSubscriberKeys.allPkiSubscriberCertificates(), subscriberName, projectId] as const, + specificPkiSubscriberCertificates: ({ + subscriberName, + projectId, + offset, + limit + }: { + subscriberName: string; + projectId: string; + offset: number; + limit: number; + }) => + [ + ...pkiSubscriberKeys.forPkiSubscriberCertificates({ subscriberName, projectId }), + { offset, limit, projectId } + ] as const +}; + +export const useGetPkiSubscriber = ({ + subscriberName, + projectId +}: { + subscriberName: string; + projectId: string; +}) => { + return useQuery({ + queryKey: pkiSubscriberKeys.getPkiSubscriber({ subscriberName, projectId }), + queryFn: async () => { + const { data: pkiSubscriber } = await apiRequest.get( + `/api/v1/pki/subscribers/${subscriberName}`, + { + params: { + projectId + } + } + ); + return pkiSubscriber; + }, + enabled: Boolean(subscriberName) && Boolean(projectId) + }); +}; + +export const useGetPkiSubscriberCertificates = ({ + subscriberName, + projectId, + offset, + limit +}: { + subscriberName: string; + projectId: string; + offset: number; + limit: number; +}) => { + return useQuery({ + queryKey: pkiSubscriberKeys.specificPkiSubscriberCertificates({ + subscriberName, + projectId, + offset, + limit + }), + queryFn: async () => { + const params = new URLSearchParams({ + offset: String(offset), + limit: String(limit), + projectId + }); + + const { + data: { certificates, totalCount } + } = await apiRequest.get<{ certificates: TCertificate[]; totalCount: number }>( + `/api/v1/pki/subscribers/${subscriberName}/certificates`, + { + params + } + ); + return { certificates, totalCount }; + }, + enabled: Boolean(subscriberName) && Boolean(projectId) + }); +}; diff --git a/frontend/src/hooks/api/pkiSubscriber/types.ts b/frontend/src/hooks/api/pkiSubscriber/types.ts new file mode 100644 index 000000000..e6050dd13 --- /dev/null +++ b/frontend/src/hooks/api/pkiSubscriber/types.ts @@ -0,0 +1,53 @@ +import { CertExtendedKeyUsage, CertKeyUsage } from "../certificates/enums"; + +export enum PkiSubscriberStatus { + ACTIVE = "active", + DISABLED = "disabled" +} + +export type TPkiSubscriber = { + id: string; + projectId: string; + caId: string; + name: string; + commonName: string; + status: PkiSubscriberStatus; + ttl: string; + subjectAlternativeNames: string[]; + keyUsages: CertKeyUsage[]; + extendedKeyUsages: CertExtendedKeyUsage[]; +}; + +export type TCreatePkiSubscriberDTO = { + projectId: string; + caId: string; + name: string; + commonName: string; + ttl: string; + subjectAlternativeNames: string[]; + keyUsages: CertKeyUsage[]; + extendedKeyUsages: CertExtendedKeyUsage[]; +}; + +export type TUpdatePkiSubscriberDTO = { + subscriberName: string; + projectId: string; + caId?: string; + name?: string; + commonName?: string; + status?: PkiSubscriberStatus; + ttl?: string; + subjectAlternativeNames?: string[]; + keyUsages?: CertKeyUsage[]; + extendedKeyUsages?: CertExtendedKeyUsage[]; +}; + +export type TDeletePkiSubscriberDTO = { + subscriberName: string; + projectId: string; +}; + +export type TIssuePkiSubscriberCertDTO = { + subscriberName: string; + projectId: string; +}; diff --git a/frontend/src/hooks/api/roles/types.ts b/frontend/src/hooks/api/roles/types.ts index ee95e8c23..12286bf9b 100644 --- a/frontend/src/hooks/api/roles/types.ts +++ b/frontend/src/hooks/api/roles/types.ts @@ -19,7 +19,7 @@ export type TProjectRole = { id: string; createdAt: string; updatedAt: string; - description?: string; + description?: string | null; permissions: TProjectPermission[]; }; @@ -76,7 +76,7 @@ export type TDeleteOrgRoleDTO = { export type TCreateProjectRoleDTO = { projectId: string; name: string; - description?: string; + description?: string | null; slug: string; permissions: TProjectPermission[]; }; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index d078765bc..65a31e427 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -12,7 +12,8 @@ export enum SecretSync { Vercel = "vercel", Windmill = "windmill", HCVault = "hashicorp-vault", - TeamCity = "teamcity" + TeamCity = "teamcity", + OCIVault = "oci-vault" } export enum SecretSyncStatus { diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index 2dba65649..e3de6029a 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -11,6 +11,7 @@ import { TGcpSync } from "./gcp-sync"; import { TGitHubSync } from "./github-sync"; import { THCVaultSync } from "./hc-vault-sync"; import { THumanitecSync } from "./humanitec-sync"; +import { TOCIVaultSync } from "./oci-vault-sync"; import { TTeamCitySync } from "./teamcity-sync"; import { TTerraformCloudSync } from "./terraform-cloud-sync"; import { TVercelSync } from "./vercel-sync"; @@ -36,7 +37,8 @@ export type TSecretSync = | TVercelSync | TWindmillSync | THCVaultSync - | TTeamCitySync; + | TTeamCitySync + | TOCIVaultSync; export type TListSecretSyncs = { secretSyncs: TSecretSync[] }; diff --git a/frontend/src/hooks/api/secretSyncs/types/oci-vault-sync.ts b/frontend/src/hooks/api/secretSyncs/types/oci-vault-sync.ts new file mode 100644 index 000000000..9dd0062f4 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/oci-vault-sync.ts @@ -0,0 +1,17 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +export type TOCIVaultSync = TRootSecretSync & { + destination: SecretSync.OCIVault; + destinationConfig: { + compartmentOcid: string; + vaultOcid: string; + keyOcid: string; + }; + connection: { + app: AppConnection.OCI; + name: string; + id: string; + }; +}; diff --git a/frontend/src/hooks/api/sshHost/types.ts b/frontend/src/hooks/api/sshHost/types.ts index ff33664b1..ba44bdde3 100644 --- a/frontend/src/hooks/api/sshHost/types.ts +++ b/frontend/src/hooks/api/sshHost/types.ts @@ -21,6 +21,7 @@ export type TSshHost = { hostCertTtl: string; loginMappings: TLoginMapping[]; }; + export type TCreateSshHostDTO = { projectId: string; hostname: string; diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx index b841f4bff..df2d55dc3 100644 --- a/frontend/src/hooks/api/workspace/index.tsx +++ b/frontend/src/hooks/api/workspace/index.tsx @@ -35,6 +35,7 @@ export { useListWorkspaceGroups, useListWorkspacePkiAlerts, useListWorkspacePkiCollections, + useListWorkspacePkiSubscribers, useListWorkspaceSshCas, useListWorkspaceSshCertificates, useListWorkspaceSshCertificateTemplates, diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 441b9aefa..278b62bc8 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -14,6 +14,7 @@ import { IntegrationAuth } from "../integrationAuth/types"; import { TIntegration } from "../integrations/types"; import { TPkiAlert } from "../pkiAlerts/types"; import { TPkiCollection } from "../pkiCollections/types"; +import { TPkiSubscriber } from "../pkiSubscriber/types"; import { EncryptedSecret } from "../secrets/types"; import { TSshCertificate, TSshCertificateAuthority } from "../sshCa/types"; import { TSshCertificateTemplate } from "../sshCertificateTemplates/types"; @@ -874,6 +875,21 @@ export const useListWorkspaceSshHosts = (projectId: string) => { }); }; +export const useListWorkspacePkiSubscribers = (projectId: string) => { + return useQuery({ + queryKey: workspaceKeys.getWorkspacePkiSubscribers(projectId), + queryFn: async () => { + const { + data: { subscribers } + } = await apiRequest.get<{ subscribers: TPkiSubscriber[] }>( + `/api/v2/workspace/${projectId}/pki-subscribers` + ); + return subscribers; + }, + enabled: Boolean(projectId) + }); +}; + export const useListWorkspaceSshHostGroups = (projectId: string) => { return useQuery({ queryKey: workspaceKeys.getWorkspaceSshHostGroups(projectId), diff --git a/frontend/src/hooks/api/workspace/query-keys.tsx b/frontend/src/hooks/api/workspace/query-keys.tsx index 335248a80..c10616b63 100644 --- a/frontend/src/hooks/api/workspace/query-keys.tsx +++ b/frontend/src/hooks/api/workspace/query-keys.tsx @@ -54,6 +54,8 @@ export const workspaceKeys = { }) => [...workspaceKeys.forWorkspaceCertificates(slug), { offset, limit }] as const, getWorkspacePkiAlerts: (workspaceId: string) => [{ workspaceId }, "workspace-pki-alerts"] as const, + getWorkspacePkiSubscribers: (projectId: string) => + [{ projectId }, "workspace-pki-subscribers"] as const, getWorkspacePkiCollections: (workspaceId: string) => [{ workspaceId }, "workspace-pki-collections"] as const, getWorkspaceCertificateTemplates: (workspaceId: string) => diff --git a/frontend/src/layouts/OrganizationLayout/ProductsSideBar/DefaultSideBar.tsx b/frontend/src/layouts/OrganizationLayout/ProductsSideBar/DefaultSideBar.tsx index 054a873a3..4d55edb58 100644 --- a/frontend/src/layouts/OrganizationLayout/ProductsSideBar/DefaultSideBar.tsx +++ b/frontend/src/layouts/OrganizationLayout/ProductsSideBar/DefaultSideBar.tsx @@ -5,22 +5,6 @@ import { Menu, MenuGroup, MenuItem } from "@app/components/v2"; export const DefaultSideBar = () => ( - - {({ isActive }) => ( - - Audit Logs - - )} - - - {({ isActive }) => ( - - Usage & Billing - - )} - - - {({ isActive }) => ( @@ -42,6 +26,29 @@ export const DefaultSideBar = () => ( )} + + {({ isActive }) => ( + + Single Sign-On (SSO) + + )} + + + + + {({ isActive }) => ( + + Audit Logs + + )} + + + {({ isActive }) => ( + + Usage & Billing + + )} + {({ isActive }) => ( diff --git a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx index 8cf807d56..b85499073 100644 --- a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx @@ -4,6 +4,7 @@ import { faArrowUpRightFromSquare, faBook, faCheck, + faCheckCircle, faCog, faDoorClosed, faEnvelope, @@ -118,6 +119,9 @@ export const MinimizedOrgSidebar = () => { [ linkOptions({ to: "/organization/access-management" }).to, linkOptions({ to: "/organization/app-connections" }).to, + linkOptions({ to: "/organization/billing" }).to, + linkOptions({ to: "/organization/sso" }).to, + linkOptions({ to: "/organization/gateways" }).to, linkOptions({ to: "/organization/settings" }).to, linkOptions({ to: "/organization/audit-logs" }).to ] as string[] @@ -387,6 +391,13 @@ export const MinimizedOrgSidebar = () => { Audit Logs + + } + > + SSO Settings + + }> Organization Settings diff --git a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx index 8533d7007..35534d4a2 100644 --- a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx +++ b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx @@ -104,7 +104,23 @@ export const ProjectLayout = () => { {isCertManager && ( <> + {({ isActive }) => ( + + Subscribers + + )} + + { {t("common.head-title", { title: "Alerting" })}
- + { handlePopUpClose("deleteCa"); navigate({ - to: `/${ProjectType.CertificateManager}/$projectId/overview` as const, + to: `/${ProjectType.CertificateManager}/$projectId/certificates` as const, params: { projectId } diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/CertificateAuthoritiesPage.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/CertificateAuthoritiesPage.tsx index f74ececaf..0efe2dbdc 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/CertificateAuthoritiesPage.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/CertificateAuthoritiesPage.tsx @@ -15,7 +15,10 @@ export const CertificateAuthoritiesPage = () => { {t("common.head-title", { title: "Certificate Authorities" })}
- + { {t("common.head-title", { title: "Certificates" })}
- + {/* If both are false, the section does not render. This is to prevent duplicate banners. */} {(canAccessCerts || canAccessPkiColl) && ( { - + @@ -85,7 +85,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { const { variant, label } = getCertValidUntilBadgeDetails(certificate.notAfter); return ( - +
Friendly NameCommon Name Status Not Before Not After
{certificate.friendlyName}{certificate.commonName} {certificate.status === CertStatus.REVOKED ? ( Revoked diff --git a/frontend/src/pages/cert-manager/CertificatesPage/route.tsx b/frontend/src/pages/cert-manager/CertificatesPage/route.tsx index 431812886..0bb7e7a71 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/route.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/route.tsx @@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { CertificatesPage } from "./CertificatesPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview" + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates" )({ component: CertificatesPage }); diff --git a/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx b/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx index 073cac6e8..e851e33d3 100644 --- a/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx +++ b/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx @@ -57,7 +57,7 @@ export const PkiCollectionPage = () => { }); handlePopUpClose("deletePkiCollection"); navigate({ - to: `/${ProjectType.CertificateManager}/$projectId/overview` as const, + to: `/${ProjectType.CertificateManager}/$projectId/certificates` as const, params: { projectId } diff --git a/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/routes.tsx b/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/routes.tsx index d59ebd265..e1ff5c1e7 100644 --- a/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/routes.tsx +++ b/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/routes.tsx @@ -13,7 +13,7 @@ export const Route = createFileRoute( { label: "Certificate Collections", link: linkOptions({ - to: "/cert-manager/$projectId/overview", + to: "/cert-manager/$projectId/certificates", params: { projectId: params.projectId } diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx new file mode 100644 index 000000000..ad6e88adf --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx @@ -0,0 +1,165 @@ +import { Helmet } from "react-helmet"; +import { useTranslation } from "react-i18next"; +import { useNavigate, useParams } from "@tanstack/react-router"; +import { twMerge } from "tailwind-merge"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Button, + DeleteActionModal, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + PageHeader, + Tooltip +} from "@app/components/v2"; +import { ROUTE_PATHS } from "@app/const/routes"; +import { + ProjectPermissionPkiSubscriberActions, + ProjectPermissionSub, + useWorkspace +} from "@app/context"; +import { useDeletePkiSubscriber, useGetPkiSubscriber } from "@app/hooks/api"; +import { ProjectType } from "@app/hooks/api/workspace/types"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { PkiSubscriberModal } from "../PkiSubscribersPage/components/PkiSubscriberModal"; +import { PkiSubscriberCertificatesSection, PkiSubscriberDetailsSection } from "./components"; + +const Page = () => { + const navigate = useNavigate(); + const { currentWorkspace } = useWorkspace(); + const projectId = currentWorkspace.id; + const subscriberName = useParams({ + from: ROUTE_PATHS.CertManager.PkiSubscriberDetailsByIDPage.id, + select: (el) => el.subscriberName + }); + const { data } = useGetPkiSubscriber({ + subscriberName, + projectId + }); + + const { mutateAsync: deletePkiSubscriber } = useDeletePkiSubscriber(); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "pkiSubscriber", + "deletePkiSubscriber" + ] as const); + + const onRemoveSubscriberSubmit = async (subscriberNameToDelete: string) => { + try { + if (!projectId) return; + + await deletePkiSubscriber({ subscriberName: subscriberNameToDelete, projectId }); + + createNotification({ + text: "Successfully deleted subscriber", + type: "success" + }); + + handlePopUpClose("deletePkiSubscriber"); + navigate({ + to: `/${ProjectType.CertificateManager}/$projectId/subscribers` as const, + params: { + projectId + } + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to delete subscriber", + type: "error" + }); + } + }; + + return ( +
+ {data && ( +
+ + + +
+ + + +
+
+ + + {(isAllowed) => ( + + handlePopUpOpen("deletePkiSubscriber", { + subscriberName: data.name + }) + } + disabled={!isAllowed} + > + Delete PKI Subscriber + + )} + + +
+
+
+
+ +
+
+ +
+
+
+ )} + + handlePopUpToggle("deletePkiSubscriber", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onRemoveSubscriberSubmit( + (popUp?.deletePkiSubscriber?.data as { subscriberName: string })?.subscriberName + ) + } + /> +
+ ); +}; + +export const PkiSubscriberDetailsByIDPage = () => { + const { t } = useTranslation(); + return ( + <> + + {t("common.head-title", { title: "PKI Subscriber" })} + + + + + + ); +}; diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberCertificatesSection.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberCertificatesSection.tsx new file mode 100644 index 000000000..2e486cf85 --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberCertificatesSection.tsx @@ -0,0 +1,27 @@ +import { usePopUp } from "@app/hooks"; +import { CertificateRevocationModal } from "@app/pages/cert-manager/CertificatesPage/components/CertificateRevocationModal"; + +import { PkiSubscriberCertificatesTable } from "./PkiSubscriberCertificatesTable"; + +type Props = { + subscriberName: string; +}; + +export const PkiSubscriberCertificatesSection = ({ subscriberName }: Props) => { + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["revokeCertificate"] as const); + + return ( +
+
+

Certificates

+
+
+ +
+ +
+ ); +}; diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberCertificatesTable.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberCertificatesTable.tsx new file mode 100644 index 000000000..e6e49b758 --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberCertificatesTable.tsx @@ -0,0 +1,176 @@ +import { useState } from "react"; +import { subject } from "@casl/ability"; +import { faCertificate, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; +import { twMerge } from "tailwind-merge"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Badge, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + EmptyState, + Pagination, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tooltip, + Tr +} from "@app/components/v2"; +import { + ProjectPermissionPkiSubscriberActions, + ProjectPermissionSub, + useProjectPermission, + useWorkspace +} from "@app/context"; +import { useGetPkiSubscriberCertificates } from "@app/hooks/api"; +import { CertStatus } from "@app/hooks/api/certificates/enums"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + subscriberName: string; + handlePopUpOpen?: (popUpName: keyof UsePopUpState<["revokeCertificate"]>, data?: object) => void; +}; + +const PER_PAGE_INIT = 25; + +export const PkiSubscriberCertificatesTable = ({ subscriberName, handlePopUpOpen }: Props) => { + const { currentWorkspace } = useWorkspace(); + const projectId = currentWorkspace.id; + const { permission } = useProjectPermission(); + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(PER_PAGE_INIT); + + const { data, isPending } = useGetPkiSubscriberCertificates({ + subscriberName, + projectId, + offset: (page - 1) * perPage, + limit: perPage + }); + + const getCertStatusBadge = (status: string, notAfter: string) => { + if (status === CertStatus.REVOKED) { + return Revoked; + } + + const expiryDate = new Date(notAfter); + const now = new Date(); + const daysUntilExpiry = Math.floor( + (expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24) + ); + + if (daysUntilExpiry < 0) { + return Expired; + } + + if (daysUntilExpiry < 30) { + return Expiring Soon; + } + + return Valid; + }; + + const canListPkiSubscriberCerts = permission.can( + ProjectPermissionPkiSubscriberActions.ListCerts, + subject(ProjectPermissionSub.PkiSubscribers, { + name: subscriberName + }) + ); + + return ( +
+ + + + + + + + + + + + {isPending && } + {!isPending && + data?.certificates?.map((certificate) => { + return ( + + + + + + + + ); + })} + +
Common NameStatusNot BeforeNot After +
{certificate.commonName}{getCertStatusBadge(certificate.status, certificate.notAfter)} + {certificate.notBefore + ? format(new Date(certificate.notBefore), "yyyy-MM-dd") + : "-"} + + {certificate.notAfter + ? format(new Date(certificate.notAfter), "yyyy-MM-dd") + : "-"} + + + +
+ + + +
+
+ + + {(isAllowed) => ( + + handlePopUpOpen && + handlePopUpOpen("revokeCertificate", { + serialNumber: certificate.serialNumber + }) + } + disabled={!isAllowed} + icon={} + > + Revoke Certificate + + )} + + +
+
+ {!isPending && data?.totalCount !== undefined && data.totalCount >= PER_PAGE_INIT && ( + setPage(newPage)} + onChangePerPage={(newPerPage) => setPerPage(newPerPage)} + /> + )} + {!isPending && !data?.certificates?.length && ( + + )} +
+
+ ); +}; diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberDetailsSection.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberDetailsSection.tsx new file mode 100644 index 000000000..5899f5004 --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberDetailsSection.tsx @@ -0,0 +1,190 @@ +import { useState } from "react"; +import { subject } from "@casl/ability"; +import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { Button, IconButton, Modal, ModalContent, Tooltip } from "@app/components/v2"; +import { + ProjectPermissionPkiSubscriberActions, + ProjectPermissionSub, + useProjectPermission, + useWorkspace +} from "@app/context"; +import { useTimedReset } from "@app/hooks"; +import { useGetPkiSubscriber, useIssuePkiSubscriberCert } from "@app/hooks/api"; +import { pkiSubscriberStatusToNameMap } from "@app/hooks/api/pkiSubscriber/constants"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +import { CertificateContent } from "../../CertificatesPage/components/CertificateContent"; + +type Props = { + subscriberName: string; + handlePopUpOpen: (popUpName: keyof UsePopUpState<["pkiSubscriber"]>, data?: object) => void; +}; + +type TCertificateDetails = { + serialNumber: string; + certificate: string; + certificateChain: string; + privateKey: string; +}; + +export const PkiSubscriberDetailsSection = ({ subscriberName, handlePopUpOpen }: Props) => { + const { currentWorkspace } = useWorkspace(); + const projectId = currentWorkspace.id; + const { permission } = useProjectPermission(); + const [certificateDetails, setCertificateDetails] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset({ + initialState: "Copy ID to clipboard" + }); + + const { data: pkiSubscriber } = useGetPkiSubscriber({ + subscriberName, + projectId + }); + + const { mutateAsync: issuePkiSubscriberCert, isPending: isIssuingCert } = + useIssuePkiSubscriberCert(); + + const onIssuePkiSubscriberCert = async () => { + try { + const response = await issuePkiSubscriberCert({ subscriberName, projectId }); + + setCertificateDetails({ + serialNumber: response.serialNumber, + certificate: response.certificate, + certificateChain: response.certificateChain, + privateKey: response.privateKey + }); + + setIsModalOpen(true); + + createNotification({ + text: "Successfully issued certificate", + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to issue certificate", + type: "error" + }); + } + }; + + const canIssuePkiSubscriberCert = permission.can( + ProjectPermissionPkiSubscriberActions.IssueCert, + subject(ProjectPermissionSub.PkiSubscribers, { + name: pkiSubscriber?.name ?? "" + }) + ); + + return pkiSubscriber ? ( +
+
+

PKI Subscriber Details

+ + {(isAllowed) => { + return ( + + { + e.stopPropagation(); + handlePopUpOpen("pkiSubscriber", { + subscriberName: pkiSubscriber.name + }); + }} + > + + + + ); + }} + +
+
+
+

PKI Subscriber ID

+
+

{pkiSubscriber.id}

+
+ + { + navigator.clipboard.writeText(pkiSubscriber.id); + setCopyTextId("Copied"); + }} + > + + + +
+
+
+
+

Name

+

{pkiSubscriber.name}

+
+
+

Status

+

+ {pkiSubscriberStatusToNameMap[pkiSubscriber.status]} +

+
+
+

Common Name

+

{pkiSubscriber.commonName}

+
+ {canIssuePkiSubscriberCert && ( + + )} +
+ + { + setIsModalOpen(isOpen); + if (!isOpen) { + setCertificateDetails(null); + } + }} + > + + {certificateDetails && ( + + )} + + +
+ ) : ( +
+ ); +}; diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/index.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/index.tsx new file mode 100644 index 000000000..4a671fdcd --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/index.tsx @@ -0,0 +1,2 @@ +export { PkiSubscriberCertificatesSection } from "./PkiSubscriberCertificatesSection"; +export { PkiSubscriberDetailsSection } from "./PkiSubscriberDetailsSection"; diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/route.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/route.tsx new file mode 100644 index 000000000..bb902bc76 --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/route.tsx @@ -0,0 +1,25 @@ +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { PkiSubscriberDetailsByIDPage } from "./PkiSubscriberDetailsByIDPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName" +)({ + component: PkiSubscriberDetailsByIDPage, + beforeLoad: ({ context, params }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Subscribers", + link: linkOptions({ + to: "/cert-manager/$projectId/subscribers", + params: { + projectId: params.projectId + } + }) + } + ] + }; + } +}); diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/PkiSubscribersPage.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/PkiSubscribersPage.tsx new file mode 100644 index 000000000..c95e9490e --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/PkiSubscribersPage.tsx @@ -0,0 +1,28 @@ +import { Helmet } from "react-helmet"; +import { useTranslation } from "react-i18next"; + +import { PageHeader } from "@app/components/v2"; + +import { PkiSubscriberSection } from "./components"; + +export const PkiSubscribersPage = () => { + const { t } = useTranslation(); + return ( + <> + + {t("common.head-title", { title: "PKI Subscribers" })} + +
+
+
+ + +
+
+
+ + ); +}; diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx new file mode 100644 index 000000000..951d78225 --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx @@ -0,0 +1,428 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + Button, + Checkbox, + FormControl, + Input, + Modal, + ModalContent, + Select, + SelectItem +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { + CaStatus, + useCreatePkiSubscriber, + useGetPkiSubscriber, + useListWorkspaceCas, + useListWorkspacePkiSubscribers, + useUpdatePkiSubscriber +} from "@app/hooks/api"; +import { + EXTENDED_KEY_USAGES_OPTIONS, + KEY_USAGES_OPTIONS +} from "@app/hooks/api/certificates/constants"; +import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + popUp: UsePopUpState<["pkiSubscriber"]>; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["pkiSubscriber"]>, state?: boolean) => void; +}; + +const schema = z + .object({ + name: z.string().trim().min(1, "Name is required"), + caId: z.string().min(1, "Issuing CA is required"), + commonName: z.string().trim().min(1, "Common Name is required"), + subjectAlternativeNames: z.string(), + ttl: z.string().trim(), + keyUsages: z.object({ + [CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(), + [CertKeyUsage.KEY_ENCIPHERMENT]: z.boolean().optional(), + [CertKeyUsage.NON_REPUDIATION]: z.boolean().optional(), + [CertKeyUsage.DATA_ENCIPHERMENT]: z.boolean().optional(), + [CertKeyUsage.KEY_AGREEMENT]: z.boolean().optional(), + [CertKeyUsage.KEY_CERT_SIGN]: z.boolean().optional(), + [CertKeyUsage.CRL_SIGN]: z.boolean().optional(), + [CertKeyUsage.ENCIPHER_ONLY]: z.boolean().optional(), + [CertKeyUsage.DECIPHER_ONLY]: z.boolean().optional() + }), + extendedKeyUsages: z.object({ + [CertExtendedKeyUsage.CLIENT_AUTH]: z.boolean().optional(), + [CertExtendedKeyUsage.CODE_SIGNING]: z.boolean().optional(), + [CertExtendedKeyUsage.EMAIL_PROTECTION]: z.boolean().optional(), + [CertExtendedKeyUsage.OCSP_SIGNING]: z.boolean().optional(), + [CertExtendedKeyUsage.SERVER_AUTH]: z.boolean().optional(), + [CertExtendedKeyUsage.TIMESTAMPING]: z.boolean().optional() + }) + }) + .required(); + +export type FormData = z.infer; + +export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => { + const { currentWorkspace } = useWorkspace(); + const projectId = currentWorkspace.id; + const { data: subscribers } = useListWorkspacePkiSubscribers(projectId); + const { data: cas } = useListWorkspaceCas({ + projectSlug: currentWorkspace?.slug ?? "", + status: CaStatus.ACTIVE + }); + + const { data: pkiSubscriber } = useGetPkiSubscriber({ + subscriberName: + (popUp?.pkiSubscriber?.data as { subscriberName: string })?.subscriberName || "", + projectId + }); + + const { mutateAsync: createMutateAsync } = useCreatePkiSubscriber(); + const { mutateAsync: updateMutateAsync } = useUpdatePkiSubscriber(); + + const { + control, + handleSubmit, + reset, + setValue, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + name: "", + caId: "", + commonName: "", + subjectAlternativeNames: "", + ttl: "", + keyUsages: { + [CertKeyUsage.DIGITAL_SIGNATURE]: true, + [CertKeyUsage.KEY_ENCIPHERMENT]: true + }, + extendedKeyUsages: {} + } + }); + + useEffect(() => { + if (pkiSubscriber) { + reset({ + name: pkiSubscriber.name, + caId: pkiSubscriber.caId || "", + commonName: pkiSubscriber.commonName, + subjectAlternativeNames: pkiSubscriber.subjectAlternativeNames.join(", ") || "", + ttl: pkiSubscriber.ttl || "", + keyUsages: Object.fromEntries((pkiSubscriber.keyUsages || []).map((name) => [name, true])), + extendedKeyUsages: Object.fromEntries( + (pkiSubscriber.extendedKeyUsages || []).map((name) => [name, true]) + ) + }); + } else { + reset({ + name: "", + caId: "", + commonName: "", + subjectAlternativeNames: "", + ttl: "", + keyUsages: { + [CertKeyUsage.DIGITAL_SIGNATURE]: true, + [CertKeyUsage.KEY_ENCIPHERMENT]: true + }, + extendedKeyUsages: {} + }); + } + }, [pkiSubscriber, reset]); + + useEffect(() => { + if (cas?.length) { + setValue("caId", cas[0].id); + } + }, [cas, setValue]); + + const onFormSubmit = async ({ + name, + caId, + commonName, + subjectAlternativeNames, + ttl, + keyUsages, + extendedKeyUsages + }: FormData) => { + try { + if (!projectId) return; + + if (!caId) { + createNotification({ + text: "Please select an Issuing CA", + type: "error" + }); + return; + } + + // Check if there is already a different subscriber with the same name + const existingNames = + subscribers?.filter((s) => s.id !== pkiSubscriber?.id).map((s) => s.name) || []; + + if (existingNames.includes(name.trim())) { + createNotification({ + text: "A subscriber with this name already exists.", + type: "error" + }); + return; + } + + const keyUsagesList = Object.entries(keyUsages) + .filter(([, value]) => value) + .map(([key]) => key as CertKeyUsage); + + const extendedKeyUsagesList = Object.entries(extendedKeyUsages) + .filter(([, value]) => value) + .map(([key]) => key as CertExtendedKeyUsage); + + const subjectAlternativeNamesList = subjectAlternativeNames + .split(",") + .map((san) => san.trim()) + .filter(Boolean); + + if (pkiSubscriber) { + await updateMutateAsync({ + subscriberName: pkiSubscriber.name, + projectId, + name, + caId, + commonName, + subjectAlternativeNames: subjectAlternativeNamesList, + ttl, + keyUsages: keyUsagesList, + extendedKeyUsages: extendedKeyUsagesList + }); + } else { + await createMutateAsync({ + projectId, + name, + caId, + commonName, + subjectAlternativeNames: subjectAlternativeNamesList, + ttl, + keyUsages: keyUsagesList, + extendedKeyUsages: extendedKeyUsagesList + }); + } + + reset(); + handlePopUpToggle("pkiSubscriber", false); + + createNotification({ + text: `Successfully ${pkiSubscriber ? "updated" : "added"} PKI subscriber`, + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: `Failed to ${pkiSubscriber ? "update" : "add"} PKI subscriber`, + type: "error" + }); + } + }; + + return ( + { + reset(); + handlePopUpToggle("pkiSubscriber", isOpen); + }} + > + +
+ {pkiSubscriber && ( + + + + )} + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + + + +
Key Usage
+
+ + { + return ( + +
+ {KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { + return ( + { + onChange({ + ...value, + [optionValue]: state + }); + }} + > + {label} + + ); + })} +
+
+ ); + }} + /> + { + return ( + +
+ {EXTENDED_KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { + return ( + { + onChange({ + ...value, + [optionValue]: state + }); + }} + > + {label} + + ); + })} +
+
+ ); + }} + /> +
+
+
+
+ + +
+ +
+
+ ); +}; diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberSection.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberSection.tsx new file mode 100644 index 000000000..f81636e49 --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberSection.tsx @@ -0,0 +1,151 @@ +import { faArrowUpRightFromSquare, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { Button, DeleteActionModal } from "@app/components/v2"; +import { + ProjectPermissionPkiSubscriberActions, + ProjectPermissionSub, + useWorkspace +} from "@app/context"; +import { useDeletePkiSubscriber, useUpdatePkiSubscriber } from "@app/hooks/api"; +import { PkiSubscriberStatus } from "@app/hooks/api/pkiSubscriber/types"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { PkiSubscriberModal } from "./PkiSubscriberModal"; +import { PkiSubscribersTable } from "./PkiSubscribersTable"; + +export const PkiSubscriberSection = () => { + const { currentWorkspace } = useWorkspace(); + const projectId = currentWorkspace.id; + const { mutateAsync: deletePkiSubscriber } = useDeletePkiSubscriber(); + const { mutateAsync: updatePkiSubscriber } = useUpdatePkiSubscriber(); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "pkiSubscriber", + "pkiSubscriberStatus", // enable / disable + "deletePkiSubscriber" + ] as const); + + const onRemovePkiSubscriberSubmit = async (subscriberName: string) => { + try { + const subscriber = await deletePkiSubscriber({ subscriberName, projectId }); + + createNotification({ + text: `Successfully deleted PKI subscriber: ${subscriber.name}`, + type: "success" + }); + + handlePopUpClose("deletePkiSubscriber"); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to delete PKI subscriber", + type: "error" + }); + } + }; + + const onUpdatePkiSubscriberStatus = async ({ + subscriberName, + status + }: { + subscriberName: string; + status: PkiSubscriberStatus; + }) => { + try { + if (!currentWorkspace?.slug) return; + + await updatePkiSubscriber({ subscriberName, projectId, status }); + + createNotification({ + text: `Successfully ${status === PkiSubscriberStatus.ACTIVE ? "enabled" : "disabled"} subscriber`, + type: "success" + }); + + handlePopUpClose("pkiSubscriberStatus"); + } catch (err) { + console.error(err); + createNotification({ + text: `Failed to ${status === PkiSubscriberStatus.ACTIVE ? "enable" : "disable"} subscriber`, + type: "error" + }); + } + }; + + const subscriberStatusData = popUp?.pkiSubscriberStatus?.data as { + status: PkiSubscriberStatus; + subscriberName: string; + }; + + const isEnabling = subscriberStatusData?.status === PkiSubscriberStatus.ACTIVE; + const subscriberName = subscriberStatusData?.subscriberName || ""; + + return ( +
+
+

Subscribers

+
+ + + Documentation{" "} + + + + + {(isAllowed) => ( + + )} + +
+
+ + + handlePopUpToggle("pkiSubscriberStatus", isOpen)} + deleteKey="confirm" + buttonColorSchema={isEnabling ? "primary" : "danger"} + buttonText={isEnabling ? "Enable" : "Disable"} + onDeleteApproved={() => onUpdatePkiSubscriberStatus(subscriberStatusData)} + /> + handlePopUpToggle("deletePkiSubscriber", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onRemovePkiSubscriberSubmit( + (popUp?.deletePkiSubscriber?.data as { subscriberName: string })?.subscriberName + ) + } + /> +
+ ); +}; diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscribersTable.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscribersTable.tsx new file mode 100644 index 000000000..3ba473985 --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscribersTable.tsx @@ -0,0 +1,188 @@ +import { + faBan, + faEllipsis, + faPencil, + faTrash, + faUserShield +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useNavigate } from "@tanstack/react-router"; +import { twMerge } from "tailwind-merge"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Badge, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + EmptyState, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tooltip, + Tr +} from "@app/components/v2"; +import { + ProjectPermissionPkiSubscriberActions, + ProjectPermissionSub, + useWorkspace +} from "@app/context"; +import { useListWorkspacePkiSubscribers } from "@app/hooks/api"; +import { + getPkiSubscriberStatusBadgeVariant, + PkiSubscriberStatus, + pkiSubscriberStatusToNameMap +} from "@app/hooks/api/pkiSubscriber/constants"; +import { ProjectType } from "@app/hooks/api/workspace/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["deletePkiSubscriber", "pkiSubscriber", "pkiSubscriberStatus"]>, + data?: object + ) => void; +}; + +export const PkiSubscribersTable = ({ handlePopUpOpen }: Props) => { + const navigate = useNavigate(); + const { currentWorkspace } = useWorkspace(); + const { data, isPending } = useListWorkspacePkiSubscribers(currentWorkspace?.id || ""); + return ( +
+ + + + + + + + + + + {isPending && } + {!isPending && + data && + data.length > 0 && + data.map((subscriber) => { + return ( + + navigate({ + to: `/${ProjectType.CertificateManager}/$projectId/subscribers/$subscriberName` as const, + params: { + projectId: currentWorkspace.id, + subscriberName: subscriber.name + } + }) + } + > + + + + + + ); + })} + +
NameStatusCommon Name +
{subscriber.name} + + {pkiSubscriberStatusToNameMap[subscriber.status]} + + {subscriber.commonName} + + +
+ + + +
+
+ + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("pkiSubscriber", { + subscriberName: subscriber.name + }); + }} + disabled={!isAllowed} + icon={} + > + Edit Subscriber + + )} + + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("pkiSubscriberStatus", { + subscriberName: subscriber.name, + status: + subscriber.status === PkiSubscriberStatus.ACTIVE + ? PkiSubscriberStatus.DISABLED + : PkiSubscriberStatus.ACTIVE + }); + }} + disabled={!isAllowed} + icon={} + > + {`${subscriber.status === PkiSubscriberStatus.ACTIVE ? "Disable" : "Enable"} Subscriber`} + + )} + + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("deletePkiSubscriber", { + subscriberName: subscriber.name + }); + }} + disabled={!isAllowed} + icon={} + > + Delete Subscriber + + )} + + +
+
+ {!isPending && data?.length === 0 && ( + + )} +
+
+ ); +}; diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/index.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/index.tsx new file mode 100644 index 000000000..4c9b89234 --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/index.tsx @@ -0,0 +1 @@ +export { PkiSubscriberSection } from "./PkiSubscriberSection"; diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/route.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/route.tsx new file mode 100644 index 000000000..d8d9fbadd --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/route.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { PkiSubscribersPage } from "./PkiSubscribersPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/" +)({ + component: PkiSubscribersPage +}); diff --git a/frontend/src/pages/cert-manager/layout.tsx b/frontend/src/pages/cert-manager/layout.tsx index 54a9b06d3..1b52793f8 100644 --- a/frontend/src/pages/cert-manager/layout.tsx +++ b/frontend/src/pages/cert-manager/layout.tsx @@ -31,7 +31,7 @@ export const Route = createFileRoute( { label: project.name, link: linkOptions({ - to: "/cert-manager/$projectId/overview", + to: "/cert-manager/$projectId/subscribers", params: { projectId: project.id } }) } diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx index e4c7aca98..508e385df 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx @@ -34,6 +34,7 @@ import { isCustomOrgRole } from "@app/helpers/roles"; import { usePopUp } from "@app/hooks"; import { useDeleteOrgRole, useGetOrgRoles, useUpdateOrg } from "@app/hooks/api"; import { TOrgRole } from "@app/hooks/api/roles/types"; +import { DuplicateOrgRoleModal } from "@app/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal"; import { RoleModal } from "@app/pages/organization/RoleByIDPage/components/RoleModal"; export const OrgRoleTable = () => { @@ -44,6 +45,7 @@ export const OrgRoleTable = () => { const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "role", "deleteRole", + "duplicateRole", "upgradePlan" ] as const); @@ -192,6 +194,25 @@ export const OrgRoleTable = () => { )} + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("duplicateRole", role); + }} + disabled={!isAllowed} + > + Duplicate Role + + )} + {!isDefaultOrgRole && ( { onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} text={(popUp.upgradePlan?.data as { description: string })?.description} /> + handlePopUpToggle("duplicateRole", isOpen)} + roleId={(popUp?.duplicateRole?.data as TOrgRole)?.id} + />
); }; diff --git a/frontend/src/pages/organization/AdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx b/frontend/src/pages/organization/AdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx index 4e33e5688..75b21b742 100644 --- a/frontend/src/pages/organization/AdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx +++ b/frontend/src/pages/organization/AdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx @@ -58,6 +58,15 @@ export const OrgAdminProjects = withPermission( await orgAdminAccessProject.mutateAsync({ projectId }); + if (type === ProjectType.CertificateManager) { + await navigate({ + to: "/cert-manager/$projectId/subscribers" as const, + params: { + projectId + } + }); + return; + } await navigate({ to: `/${type}/$projectId/overview` as const, params: { diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index 5c004ce96..238dc4e04 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -22,6 +22,7 @@ import { HCVaultConnectionForm } from "./HCVaultConnectionForm"; import { HumanitecConnectionForm } from "./HumanitecConnectionForm"; import { LdapConnectionForm } from "./LdapConnectionForm"; import { MsSqlConnectionForm } from "./MsSqlConnectionForm"; +import { OCIConnectionForm } from "./OCIConnectionForm"; import { PostgresConnectionForm } from "./PostgresConnectionForm"; import { TeamCityConnectionForm } from "./TeamCityConnectionForm"; import { TerraformCloudConnectionForm } from "./TerraformCloudConnectionForm"; @@ -101,6 +102,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.TeamCity: return ; + case AppConnection.OCI: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -173,6 +176,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.TeamCity: return ; + case AppConnection.OCI: + return ; default: throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/OCIConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/OCIConnectionForm.tsx new file mode 100644 index 000000000..6ea355dd2 --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/OCIConnectionForm.tsx @@ -0,0 +1,215 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + Input, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { OCIConnectionMethod, TOCIConnection } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TOCIConnection; + onSubmit: (formData: FormData) => void; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.OCI) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(OCIConnectionMethod.AccessKey), + credentials: z.object({ + userOcid: z + .string() + .trim() + .min(1, "User OCID required") + .regex(/^ocid1\.user\.oc1\.\..+$/, "Invalid User OCID format"), + tenancyOcid: z + .string() + .trim() + .min(1, "Tenancy OCID required") + .regex(/^ocid1\.tenancy\.oc1\.\..+$/, "Invalid Tenancy OCID format"), + region: z.string().trim().min(1, "Region required"), + fingerprint: z.string().trim().min(1, "Fingerprint required"), + privateKey: z.string().trim().min(1, "Private Key required") + }) + }) +]); + +type FormData = z.infer; + +export const OCIConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.OCI, + method: OCIConnectionMethod.AccessKey + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/organization/RoleByIDPage/RoleByIDPage.tsx b/frontend/src/pages/organization/RoleByIDPage/RoleByIDPage.tsx index a3165709a..7acc468f8 100644 --- a/frontend/src/pages/organization/RoleByIDPage/RoleByIDPage.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/RoleByIDPage.tsx @@ -19,6 +19,7 @@ import { ROUTE_PATHS } from "@app/const/routes"; import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { useDeleteOrgRole, useGetOrgRole } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; +import { DuplicateOrgRoleModal } from "@app/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal"; import { OrgAccessControlTabSections } from "@app/types/org"; import { RoleDetailsSection, RoleModal, RolePermissionsSection } from "./components"; @@ -36,7 +37,8 @@ export const Page = () => { const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "role", - "deleteOrgRole" + "deleteOrgRole", + "duplicateRole" ] as const); const onDeleteOrgRoleSubmit = async () => { @@ -106,6 +108,21 @@ export const Page = () => { )} + + {(isAllowed) => ( + { + handlePopUpOpen("duplicateRole"); + }} + disabled={!isAllowed} + > + Duplicate Role + + )} + {(isAllowed) => ( { deleteKey="confirm" onDeleteApproved={() => onDeleteOrgRoleSubmit()} /> + handlePopUpToggle("duplicateRole", isOpen)} + roleId={data?.id} + /> ); }; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal.tsx b/frontend/src/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal.tsx new file mode 100644 index 000000000..8995c03b3 --- /dev/null +++ b/frontend/src/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal.tsx @@ -0,0 +1,150 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useNavigate } from "@tanstack/react-router"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input, Modal, ModalContent, Spinner } from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { useCreateOrgRole, useGetOrgRole } from "@app/hooks/api"; +import { TOrgRole } from "@app/hooks/api/roles/types"; +import { slugSchema } from "@app/lib/schemas"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + roleId?: string; +}; + +const schema = z + .object({ + name: z.string().min(1, "Name required"), + description: z.string(), + slug: slugSchema({ min: 1 }) + }) + .required(); + +export type FormData = z.infer; + +type ContentProps = { + role: TOrgRole; + onClose: () => void; +}; + +const Content = ({ role, onClose }: ContentProps) => { + const { + control, + handleSubmit, + formState: { isSubmitting } + } = useForm({ + defaultValues: { + name: `${role.name} Duplicate` + }, + resolver: zodResolver(schema) + }); + + const createRole = useCreateOrgRole(); + const navigate = useNavigate(); + + const handleDuplicateRole = async (form: FormData) => { + const newRole = await createRole.mutateAsync({ + orgId: role.orgId, + permissions: role.permissions, + ...form + }); + + createNotification({ + type: "success", + text: "Role duplicated successfully" + }); + + navigate({ + to: "/organization/roles/$roleId", + params: { + roleId: newRole.id + } + }); + + onClose(); + }; + + return ( +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+ + +
+ + ); +}; + +export const DuplicateOrgRoleModal = ({ isOpen, onOpenChange, roleId }: Props) => { + const { currentOrg } = useOrganization(); + + const { data: role, isPending } = useGetOrgRole(currentOrg.id, roleId ?? ""); + + if (!roleId) return null; + + return ( + + + {/* eslint-disable-next-line no-nested-ternary */} + {isPending ? ( +
+ +

Loading Role...

+
+ ) : role ? ( + onOpenChange(false)} /> + ) : ( +

+ Error: could not find role with slug "{roleId}" +

+ )} +
+
+ ); +}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RoleModal.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RoleModal.tsx index da93d3cc0..88f568c4a 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/RoleModal.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/components/RoleModal.tsx @@ -13,7 +13,7 @@ import { slugSchema } from "@app/lib/schemas"; const schema = z .object({ - name: z.string(), + name: z.string().min(1, "Name required"), description: z.string(), slug: slugSchema({ min: 1 }) }) @@ -71,12 +71,6 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { const onFormSubmit = async ({ name, description, slug }: FormData) => { try { - console.log("onFormSubmit args: ", { - name, - description, - slug - }); - if (!orgId) return; if (role) { diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/index.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/index.tsx deleted file mode 100644 index 537be9831..000000000 --- a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { OrgAuthTab } from "./OrgAuthTab"; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx index 9cd59c0ad..bb2e9ea4d 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx @@ -4,8 +4,8 @@ import { createNotification } from "@app/components/notifications"; import { Button, DeleteActionModal } from "@app/components/v2"; import { useOrganization, useOrgPermission } from "@app/context"; import { useDeleteOrgById } from "@app/hooks/api"; +import { clearSession } from "@app/hooks/api/users/queries"; import { usePopUp } from "@app/hooks/usePopUp"; -import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils"; export const OrgDeleteSection = () => { const navigate = useNavigate(); @@ -13,9 +13,7 @@ export const OrgDeleteSection = () => { const { membership } = useOrgPermission(); - const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ - "deleteOrg" - ] as const); + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["deleteOrg"] as const); const { mutateAsync, isPending } = useDeleteOrgById(); @@ -32,9 +30,8 @@ export const OrgDeleteSection = () => { type: "success" }); - await navigateUserToOrg(navigate); - - handlePopUpClose("deleteOrg"); + clearSession(); + navigate({ to: "/login" }); } catch (err) { console.error(err); createNotification({ diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgGenericAuthSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgGenericAuthSection.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgGenericAuthSection.tsx rename to frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgGenericAuthSection.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgSecurityTab.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgSecurityTab.tsx new file mode 100644 index 000000000..981681b7d --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgSecurityTab.tsx @@ -0,0 +1,35 @@ +import { Link } from "@tanstack/react-router"; + +import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { withPermission } from "@app/hoc"; + +import { OrgGenericAuthSection } from "./OrgGenericAuthSection"; +import { OrgUserAccessTokenLimitSection } from "./OrgUserAccessTokenLimitSection"; + +export const OrgSecurityTab = withPermission( + () => { + return ( + <> + +

+ SSO Settings have been relocated:{" "} + + Click here to view SSO Settings + +

+
+ + + + ); + }, + { action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Sso } +); diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgUserAccessTokenLimitSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgUserAccessTokenLimitSection.tsx similarity index 95% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgUserAccessTokenLimitSection.tsx rename to frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgUserAccessTokenLimitSection.tsx index 43e72bd41..bc020d3a1 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgUserAccessTokenLimitSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgUserAccessTokenLimitSection.tsx @@ -86,13 +86,12 @@ export const OrgUserAccessTokenLimitSection = () => { ]; return ( -
+
-

User Token Expiration

+

Session Length

- This defines the maximum time a user token will be valid. After this time, the user will - need to re-authenticate. + Specify the duration of each login session for users in this organization.

{(isAllowed) => ( diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/index.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/index.tsx new file mode 100644 index 000000000..565772a72 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/index.tsx @@ -0,0 +1 @@ +export * from "./OrgSecurityTab"; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx index 92979d5f7..e5532838c 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx @@ -10,9 +10,9 @@ import { ProjectType } from "@app/hooks/api/workspace/types"; import { AuditLogStreamsTab } from "../AuditLogStreamTab"; import { ImportTab } from "../ImportTab"; import { KmipTab } from "../KmipTab/OrgKmipTab"; -import { OrgAuthTab } from "../OrgAuthTab"; import { OrgEncryptionTab } from "../OrgEncryptionTab"; import { OrgGeneralTab } from "../OrgGeneralTab"; +import { OrgSecurityTab } from "../OrgSecurityTab"; import { OrgWorkflowIntegrationTab } from "../OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab"; export const OrgTabGroup = () => { @@ -21,7 +21,7 @@ export const OrgTabGroup = () => { }); const tabs = [ { name: "General", key: "tab-org-general", component: OrgGeneralTab }, - { name: "Security", key: "tab-org-security", component: OrgAuthTab }, + { name: "Security", key: "tab-org-security", component: OrgSecurityTab }, { name: "Encryption", key: "tab-org-encryption", component: OrgEncryptionTab }, { name: "Workflow Integrations", diff --git a/frontend/src/pages/organization/SsoPage/SsoPage.tsx b/frontend/src/pages/organization/SsoPage/SsoPage.tsx new file mode 100644 index 000000000..2ee19a56c --- /dev/null +++ b/frontend/src/pages/organization/SsoPage/SsoPage.tsx @@ -0,0 +1,21 @@ +import { Helmet } from "react-helmet"; + +import { PageHeader } from "@app/components/v2"; + +import { SsoTabGroup } from "./components/SsoTabGroup"; + +export const SsoPage = () => { + return ( + <> + + Single Sign-On (SSO) + +
+
+ + +
+
+ + ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/ExternalGroupOrgRoleMappings.tsx b/frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/ExternalGroupOrgRoleMappings.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/ExternalGroupOrgRoleMappings.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/ExternalGroupOrgRoleMappings.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/GithubOrgSyncConfigModal.tsx b/frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/GithubOrgSyncConfigModal.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/GithubOrgSyncConfigModal.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/GithubOrgSyncConfigModal.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgGithubSyncSection.tsx b/frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/OrgGithubSyncSection.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgGithubSyncSection.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/OrgGithubSyncSection.tsx diff --git a/frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/OrgProvisioningTab.tsx b/frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/OrgProvisioningTab.tsx new file mode 100644 index 000000000..43822aaff --- /dev/null +++ b/frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/OrgProvisioningTab.tsx @@ -0,0 +1,17 @@ +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { withPermission } from "@app/hoc"; + +import { OrgGithubSyncSection } from "./OrgGithubSyncSection"; +import { OrgScimSection } from "./OrgSCIMSection"; + +export const OrgProvisioningTab = withPermission( + () => { + return ( + <> + + + + ); + }, + { action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Sso } +); diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgSCIMSection.tsx b/frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/OrgSCIMSection.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgSCIMSection.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/OrgSCIMSection.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/ScimTokenModal.tsx b/frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/ScimTokenModal.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/ScimTokenModal.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/ScimTokenModal.tsx diff --git a/frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/index.tsx b/frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/index.tsx new file mode 100644 index 000000000..c39b77b6a --- /dev/null +++ b/frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/index.tsx @@ -0,0 +1 @@ +export * from "./OrgProvisioningTab"; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/LDAPGroupMapModal.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/LDAPGroupMapModal.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/LDAPGroupMapModal.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgSsoTab/LDAPGroupMapModal.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/LDAPModal.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/LDAPModal.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/LDAPModal.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgSsoTab/LDAPModal.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OIDCModal.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OIDCModal.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OIDCModal.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OIDCModal.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgGeneralAuthSection.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgGeneralAuthSection.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgLDAPSection.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgLDAPSection.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgLDAPSection.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgLDAPSection.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgOIDCSection.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgOIDCSection.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgOIDCSection.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgOIDCSection.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgSSOSection.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgSSOSection.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgSSOSection.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgSSOSection.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgAuthTab.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgSsoTab.tsx similarity index 93% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgAuthTab.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgSsoTab.tsx index 05d105192..65f97cc2d 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgAuthTab.tsx +++ b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgSsoTab.tsx @@ -17,16 +17,12 @@ import { LoginMethod } from "@app/hooks/api/admin/types"; import { LDAPModal } from "./LDAPModal"; import { OIDCModal } from "./OIDCModal"; import { OrgGeneralAuthSection } from "./OrgGeneralAuthSection"; -import { OrgGenericAuthSection } from "./OrgGenericAuthSection"; -import { OrgGithubSyncSection } from "./OrgGithubSyncSection"; import { OrgLDAPSection } from "./OrgLDAPSection"; import { OrgOIDCSection } from "./OrgOIDCSection"; -import { OrgScimSection } from "./OrgSCIMSection"; import { OrgSSOSection } from "./OrgSSOSection"; -import { OrgUserAccessTokenLimitSection } from "./OrgUserAccessTokenLimitSection"; import { SSOModal } from "./SSOModal"; -export const OrgAuthTab = withPermission( +export const OrgSsoTab = withPermission( () => { const { config: { enabledLoginMethods } @@ -167,8 +163,6 @@ export const OrgAuthTab = withPermission( return ( <> - - {shouldShowCreateIdentityProviderView ? ( createIdentityProviderView ) : ( @@ -183,8 +177,6 @@ export const OrgAuthTab = withPermission( {isLdapConfigured && shouldDisplaySection(LoginMethod.LDAP) && } )} - - handlePopUpToggle("upgradePlan", isOpen)} diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/SSOModal.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/SSOModal.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/SSOModal.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgSsoTab/SSOModal.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/SSOModalHeader.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/SSOModalHeader.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/SSOModalHeader.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgSsoTab/SSOModalHeader.tsx diff --git a/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/index.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/index.tsx new file mode 100644 index 000000000..fd02ae3e3 --- /dev/null +++ b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/index.tsx @@ -0,0 +1 @@ +export { OrgSsoTab } from "./OrgSsoTab"; diff --git a/frontend/src/pages/organization/SsoPage/components/SsoTabGroup/SsoTabGroup.tsx b/frontend/src/pages/organization/SsoPage/components/SsoTabGroup/SsoTabGroup.tsx new file mode 100644 index 000000000..273265285 --- /dev/null +++ b/frontend/src/pages/organization/SsoPage/components/SsoTabGroup/SsoTabGroup.tsx @@ -0,0 +1,37 @@ +import { useState } from "react"; +import { useSearch } from "@tanstack/react-router"; + +import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; +import { ROUTE_PATHS } from "@app/const/routes"; + +import { OrgProvisioningTab } from "../OrgProvisioningTab"; +import { OrgSsoTab } from "../OrgSsoTab"; + +export const SsoTabGroup = () => { + const search = useSearch({ + from: ROUTE_PATHS.Organization.SsoPage.id + }); + const tabs = [ + { name: "General", key: "tab-sso-auth", component: OrgSsoTab }, + { name: "Provisioning", key: "tab-sso-identity", component: OrgProvisioningTab } + ]; + + const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key); + + return ( + + + {tabs.map((tab) => ( + + {tab.name} + + ))} + + {tabs.map(({ key, component: Component }) => ( + + + + ))} + + ); +}; diff --git a/frontend/src/pages/organization/SsoPage/components/SsoTabGroup/index.tsx b/frontend/src/pages/organization/SsoPage/components/SsoTabGroup/index.tsx new file mode 100644 index 000000000..0aca705c8 --- /dev/null +++ b/frontend/src/pages/organization/SsoPage/components/SsoTabGroup/index.tsx @@ -0,0 +1 @@ +export { SsoTabGroup } from "./SsoTabGroup"; diff --git a/frontend/src/pages/organization/SsoPage/route.tsx b/frontend/src/pages/organization/SsoPage/route.tsx new file mode 100644 index 000000000..c3b144573 --- /dev/null +++ b/frontend/src/pages/organization/SsoPage/route.tsx @@ -0,0 +1,33 @@ +import { faHome } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router"; +import { zodValidator } from "@tanstack/zod-adapter"; +import { z } from "zod"; + +import { SsoPage } from "./SsoPage"; + +const SettingsPageQueryParams = z.object({ + selectedTab: z.string().catch("") +}); + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/organization/sso" +)({ + component: SsoPage, + validateSearch: zodValidator(SettingsPageQueryParams), + search: { + middlewares: [stripSearchParams({ selectedTab: "" })] + }, + context: () => ({ + breadcrumbs: [ + { + label: "Home", + icon: () => , + link: linkOptions({ to: "/" }) + }, + { + label: "Single Sign-On (SSO)" + } + ] + }) +}); diff --git a/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx b/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx index 550e0e1a1..9f04ff722 100644 --- a/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx @@ -25,13 +25,15 @@ import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@a import { usePopUp } from "@app/hooks"; import { useDeleteProjectRole, useGetProjectRoles } from "@app/hooks/api"; import { ProjectMembershipRole, TProjectRole } from "@app/hooks/api/roles/types"; +import { DuplicateProjectRoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal"; import { RoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/RoleModal"; export const ProjectRoleList = () => { const navigate = useNavigate(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "role", - "deleteRole" + "deleteRole", + "duplicateRole" ] as const); const { currentWorkspace } = useWorkspace(); const projectId = currentWorkspace?.id || ""; @@ -139,6 +141,25 @@ export const ProjectRoleList = () => { )} + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("duplicateRole", role); + }} + disabled={!isAllowed} + > + Duplicate Role + + )} + {!isNonMutatable && ( { onClose={() => handlePopUpClose("deleteRole")} onDeleteApproved={handleRoleDelete} /> + handlePopUpToggle("duplicateRole", isOpen)} + roleSlug={(popUp?.duplicateRole?.data as TProjectRole)?.slug} + />
); }; diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx index 50d392748..14eac860d 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx @@ -1,12 +1,6 @@ import { Controller, FormProvider, useForm } from "react-hook-form"; import { subject } from "@casl/ability"; -import { - faCaretDown, - faChevronLeft, - faClock, - faPlus, - faSave -} from "@fortawesome/free-solid-svg-icons"; +import { faCaretDown, faChevronLeft, faClock, faSave } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { format, formatDistance } from "date-fns"; @@ -33,16 +27,15 @@ import { useProjectPermission, useWorkspace } from "@app/context"; -import { usePopUp } from "@app/hooks"; import { useCreateIdentityProjectAdditionalPrivilege, useGetIdentityProjectPrivilegeDetails, useUpdateIdentityProjectAdditionalPrivilege } from "@app/hooks/api"; import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/identityProjectAdditionalPrivilege/types"; +import { AddPoliciesButton } from "@app/pages/project/RoleDetailsBySlugPage/components/AddPoliciesButton"; import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies"; import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState"; -import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal"; import { formRolePermission2API, PROJECT_PERMISSION_OBJECT, @@ -97,7 +90,6 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({ ProjectPermissionIdentityActions.Edit, subject(ProjectPermissionSub.Identity, { identityId }) ); - const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const); const form = useForm({ values: privilegeDetails @@ -224,7 +216,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({ variant="outline_bg" type="submit" className={twMerge( - "h-10 rounded-r-none border border-primary", + "mr-4 h-10 border border-primary", isDirty && "bg-primary text-black" )} isDisabled={isSubmitting || !isDirty || isDisabled} @@ -233,15 +225,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({ > Save - +
@@ -382,10 +366,6 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({ ) )} - handlePopUpToggle("addPolicy", isOpen)} - /> ); diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx index 5fee21f9c..fb12f8381 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx @@ -1,11 +1,5 @@ import { Controller, FormProvider, useForm } from "react-hook-form"; -import { - faCaretDown, - faChevronLeft, - faClock, - faPlus, - faSave -} from "@fortawesome/free-solid-svg-icons"; +import { faCaretDown, faChevronLeft, faClock, faSave } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { format, formatDistance } from "date-fns"; @@ -32,16 +26,15 @@ import { useProjectPermission, useWorkspace } from "@app/context"; -import { usePopUp } from "@app/hooks"; import { useCreateProjectUserAdditionalPrivilege, useGetProjectUserPrivilegeDetails, useUpdateProjectUserAdditionalPrivilege } from "@app/hooks/api"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/projectUserAdditionalPrivilege/types"; +import { AddPoliciesButton } from "@app/pages/project/RoleDetailsBySlugPage/components/AddPoliciesButton"; import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies"; import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState"; -import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal"; import { formRolePermission2API, PROJECT_PERMISSION_OBJECT, @@ -83,8 +76,6 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({ projectMembershipId, isDisabled }: Props) => { - const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const); - const isCreate = !privilegeId; const { currentWorkspace } = useWorkspace(); const projectId = currentWorkspace?.id || ""; @@ -221,7 +212,7 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({ variant="outline_bg" type="submit" className={twMerge( - "h-10 rounded-r-none border border-primary", + "mr-4 h-10 border border-primary", isDirty && "bg-primary text-black" )} isDisabled={isSubmitting || !isDirty || isDisabled} @@ -230,15 +221,7 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({ > Save - + @@ -377,10 +360,6 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({ ))} - handlePopUpToggle("addPolicy", isOpen)} - /> ); diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx index 0431726da..3f5de1ca7 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx @@ -19,6 +19,7 @@ import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@a import { useDeleteProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { usePopUp } from "@app/hooks/usePopUp"; +import { DuplicateProjectRoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal"; import { ProjectAccessControlTabs } from "@app/types/project"; import { RoleDetailsSection } from "./components/RoleDetailsSection"; @@ -40,7 +41,8 @@ const Page = () => { const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "role", - "deleteRole" + "deleteRole", + "duplicateRole" ] as const); const onDeleteRoleSubmit = async () => { @@ -117,6 +119,24 @@ const Page = () => {
)} + + {(isAllowed) => ( + { + handlePopUpOpen("duplicateRole"); + }} + disabled={!isAllowed} + > + Duplicate Role + + )} + { deleteKey="confirm" onDeleteApproved={() => onDeleteRoleSubmit()} /> + handlePopUpToggle("duplicateRole", isOpen)} + roleSlug={roleSlug} + /> ); }; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/AddPoliciesButton.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/AddPoliciesButton.tsx new file mode 100644 index 000000000..f438f50e8 --- /dev/null +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/AddPoliciesButton.tsx @@ -0,0 +1,78 @@ +import { faAngleDown, faLayerGroup, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, + IconButton +} from "@app/components/v2"; +import { usePopUp } from "@app/hooks"; +import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal"; +import { PolicyTemplateModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicyTemplateModal"; + +type Props = { + isDisabled?: boolean; +}; + +export const AddPoliciesButton = ({ isDisabled }: Props) => { + const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([ + "addPolicy", + "addPolicyOptions", + "applyTemplate" + ] as const); + + return ( + <> + + handlePopUpToggle("addPolicyOptions", isOpen)} + > + + + + + + +
+ +
+
+
+ handlePopUpToggle("addPolicy", isOpen)} + /> + handlePopUpToggle("applyTemplate", isOpen)} + /> + + ); +}; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal.tsx new file mode 100644 index 000000000..4ac54e973 --- /dev/null +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal.tsx @@ -0,0 +1,153 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useNavigate } from "@tanstack/react-router"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input, Modal, ModalContent, Spinner } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { useCreateProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api"; +import { TProjectRole } from "@app/hooks/api/roles/types"; +import { slugSchema } from "@app/lib/schemas"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + roleSlug?: string; +}; + +const schema = z + .object({ + name: z.string().min(1, "Name required"), + description: z.string(), + slug: slugSchema({ min: 1 }) + }) + .required(); + +export type FormData = z.infer; + +type ContentProps = { + role: TProjectRole; + onClose: () => void; +}; + +const Content = ({ role, onClose }: ContentProps) => { + const { + control, + handleSubmit, + formState: { isSubmitting } + } = useForm({ + defaultValues: { + name: `${role.name} Duplicate` + }, + resolver: zodResolver(schema) + }); + + const { currentWorkspace } = useWorkspace(); + + const createRole = useCreateProjectRole(); + const navigate = useNavigate(); + + const handleDuplicateRole = async (form: FormData) => { + const newRole = await createRole.mutateAsync({ + projectId: currentWorkspace.id, + permissions: role.permissions, + ...form + }); + + createNotification({ + type: "success", + text: "Role duplicated successfully" + }); + + navigate({ + to: `/${currentWorkspace.type}/$projectId/roles/$roleSlug` as const, + params: { + roleSlug: newRole.slug, + projectId: currentWorkspace.id + } + }); + + onClose(); + }; + + return ( +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+ + +
+ + ); +}; + +export const DuplicateProjectRoleModal = ({ isOpen, onOpenChange, roleSlug }: Props) => { + const { currentWorkspace } = useWorkspace(); + + const { data: role, isPending } = useGetProjectRoleBySlug(currentWorkspace.id, roleSlug ?? ""); + + if (!roleSlug) return null; + + return ( + + + {/* eslint-disable-next-line no-nested-ternary */} + {isPending ? ( +
+ +

Loading Role...

+
+ ) : role ? ( + onOpenChange(false)} /> + ) : ( +

+ Error: could not find role with slug "{roleSlug}" +

+ )} +
+
+ ); +}; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx index 6773f0658..50b43e101 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx @@ -1,5 +1,5 @@ -import { cloneElement, useState } from "react"; -import { Controller, useFieldArray, useFormContext } from "react-hook-form"; +import { cloneElement, ReactNode, useState } from "react"; +import { Control, Controller, useFieldArray, useFormContext, useWatch } from "react-hook-form"; import { faChevronDown, faChevronRight, @@ -29,6 +29,43 @@ type Props = { isDisabled?: boolean; }; +type ActionProps = { + value: string; + subject: ProjectPermissionSub; + rootIndex: number; + label: ReactNode; + isDisabled?: boolean; + control: Control; +}; + +const ActionCheckbox = ({ value, subject, isDisabled, rootIndex, label, control }: ActionProps) => { + // scott: using Controller caused discrepancy between field value and actual value, this is a hacky fix + const fieldValue = useWatch({ + control, + name: `permissions.${subject}.${rootIndex}.${value}` as any + }); + const { setValue } = useFormContext(); + + return ( +
+ + setValue(`permissions.${subject}.${rootIndex}.${value}`, isChecked, { + shouldDirty: true, + shouldTouch: true, + shouldValidate: true + }) + } + id={`permissions.${subject}.${rootIndex}.${String(value)}`} + > + {label} + +
+ ); +}; + export const GeneralPermissionPolicies = >({ subject, actions, @@ -41,11 +78,18 @@ export const GeneralPermissionPolicies = ({ + control, + name: `permissions.${subject}` + }); + const [isOpen, setIsOpen] = useToggle(); const [draggedItem, setDraggedItem] = useState(null); const [dragOverItem, setDragOverItem] = useState(null); - if (!fields.length) return
; + if (!watchFields || !Array.isArray(watchFields) || watchFields.length === 0) return
; const handleDragStart = (_: React.DragEvent, index: number) => { setDraggedItem(index); @@ -194,25 +238,14 @@ export const GeneralPermissionPolicies = { - return ( -
- - {label} - -
- ); - }} + subject={subject} + isDisabled={isDisabled} /> ); })} diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PkiSubscriberPermissionConditions.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PkiSubscriberPermissionConditions.tsx new file mode 100644 index 000000000..58840fe2f --- /dev/null +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PkiSubscriberPermissionConditions.tsx @@ -0,0 +1,173 @@ +import { Controller, useFieldArray, useFormContext } from "react-hook-form"; +import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { + Button, + FormControl, + IconButton, + Input, + Select, + SelectItem, + Tooltip +} from "@app/components/v2"; +import { + PermissionConditionOperators, + ProjectPermissionSub +} from "@app/context/ProjectPermissionContext/types"; + +import { getConditionOperatorHelperInfo } from "./PermissionConditionHelpers"; +import { TFormSchema } from "./ProjectRoleModifySection.utils"; + +type Props = { + position?: number; + isDisabled?: boolean; +}; + +export const PkiSubscriberPermissionConditions = ({ position = 0, isDisabled }: Props) => { + const { + control, + watch, + formState: { errors } + } = useFormContext(); + + const permissionSubject = ProjectPermissionSub.PkiSubscribers; + const items = useFieldArray({ + control, + name: `permissions.${permissionSubject}.${position}.conditions` + }); + + return ( +
+

Conditions

+

+ Conditions determine when a policy will be applied (always if no conditions are present). +

+

+ All conditions must evaluate to true for the policy to take effect. +

+
+ {items.fields.map((el, index) => { + const condition = + (watch(`permissions.${permissionSubject}.${position}.conditions.${index}`) as { + lhs: string; + rhs: string; + operator: string; + }) || {}; + + return ( +
+
+ ( + + + + )} + /> +
+
+ ( + + + + )} + /> + + + +
+
+ ( + + + + )} + /> +
+
+ items.remove(index)} + > + + +
+
+ ); + })} +
+ {errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message && ( +
+ + {errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message} +
+ )} +
+ +
+
+ ); +}; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicyTemplateModal.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicyTemplateModal.tsx new file mode 100644 index 000000000..5f729983c --- /dev/null +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicyTemplateModal.tsx @@ -0,0 +1,202 @@ +import { useState } from "react"; +import { useFormContext } from "react-hook-form"; + +import { createNotification } from "@app/components/notifications"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + Button, + Modal, + ModalClose, + ModalContent +} from "@app/components/v2"; +import { ProjectPermissionSub } from "@app/context"; +import { useGetProjectTypeFromRoute } from "@app/hooks"; +import { ProjectType } from "@app/hooks/api/workspace/types"; + +import { + PROJECT_PERMISSION_OBJECT, + RoleTemplate, + RoleTemplates, + TFormSchema +} from "./ProjectRoleModifySection.utils"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; +}; + +type ContentProps = { + onClose: () => void; +}; + +const Content = ({ onClose }: ContentProps) => { + const rootForm = useFormContext(); + const projectType = useGetProjectTypeFromRoute(); + + const [selectedTemplate, setSelectedTemplate] = useState(); + const [conflictingSubjects, setConflictingSubjects] = useState([]); + const [showConflictingSubjects, setShowConflictingSubjects] = useState(false); + + const templates = RoleTemplates[projectType ?? ProjectType.SecretManager]; + + const onSubmit = (skipConflicting = false) => { + if (!selectedTemplate) { + createNotification({ type: "error", text: "Please select a template" }); + return; + } + + selectedTemplate.permissions.forEach(({ subject, actions }) => { + if (skipConflicting && conflictingSubjects.includes(subject)) return; + + rootForm.setValue( + `permissions.${subject}`, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error akhilmhdh: this is because of ts collision with both + [Object.fromEntries(actions.map((action) => [action, true]))], + { + shouldDirty: true, + shouldTouch: true, + shouldValidate: true + } + ); + }); + + onClose(); + }; + + const onApply = () => { + if (!selectedTemplate) { + createNotification({ type: "error", text: "Please select a template" }); + return; + } + + const conflictingPolicies: ProjectPermissionSub[] = []; + + selectedTemplate.permissions.forEach(({ subject }) => { + const rootPolicyValue = rootForm.getValues("permissions")?.[subject]; + + if (rootPolicyValue?.length) { + conflictingPolicies.push(subject); + } + }); + + if (conflictingPolicies.length) { + setConflictingSubjects(conflictingPolicies); + setShowConflictingSubjects(true); + return; + } + + onSubmit(); + }; + + return ( + <> + + +
+ {conflictingSubjects.map((subject) => ( +
+ + {PROJECT_PERMISSION_OBJECT[subject].title} + +
+ ))} +
+
+ + + + + + +
+
+
+ + setSelectedTemplate(templates.find((template) => template.id === value)) + } + collapsible + className="w-full border-collapse" + > + {templates.map(({ name, description, permissions, id }) => ( + + +
+ {name} + {description} +
+
+ +
+ Grants the following permissions: +
+ {permissions + .map((permission) => ({ + ...permission, + object: PROJECT_PERMISSION_OBJECT[permission.subject] + })) + .sort((a, b) => a.object.title.localeCompare(b.object.title)) + .map(({ subject, actions, object }) => { + return ( +
+ {object.title} +
    + {actions.map((action) => ( +
  • + {object.actions.find((a) => a.value === action)?.label} +
  • + ))} +
+
+ ); + })} +
+
+
+
+ ))} +
+
+ + + + +
+ + ); +}; + +export const PolicyTemplateModal = ({ isOpen, onOpenChange }: Props) => { + return ( + + + onOpenChange(false)} /> + + + ); +}; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index bd5cdaa4e..e4b9ed302 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -17,6 +17,7 @@ import { ProjectPermissionIdentityActions, ProjectPermissionKmipActions, ProjectPermissionMemberActions, + ProjectPermissionPkiSubscriberActions, ProjectPermissionSecretActions, ProjectPermissionSecretRotationActions, ProjectPermissionSecretSyncActions, @@ -131,6 +132,15 @@ const SshHostPolicyActionSchema = z.object({ [ProjectPermissionSshHostActions.IssueHostCert]: z.boolean().optional() }); +const PkiSubscriberPolicyActionSchema = z.object({ + [ProjectPermissionPkiSubscriberActions.Read]: z.boolean().optional(), + [ProjectPermissionPkiSubscriberActions.Create]: z.boolean().optional(), + [ProjectPermissionPkiSubscriberActions.Edit]: z.boolean().optional(), + [ProjectPermissionPkiSubscriberActions.Delete]: z.boolean().optional(), + [ProjectPermissionPkiSubscriberActions.IssueCert]: z.boolean().optional(), + [ProjectPermissionPkiSubscriberActions.ListCerts]: z.boolean().optional() +}); + const SecretRollbackPolicyActionSchema = z.object({ read: z.boolean().optional(), create: z.boolean().optional() @@ -180,7 +190,7 @@ const ConditionSchema = z export const projectRoleFormSchema = z.object({ name: z.string().trim(), - description: z.string().trim().optional(), + description: z.string().trim().nullish(), slug: z .string() .trim() @@ -230,6 +240,12 @@ export const projectRoleFormSchema = z.object({ [ProjectPermissionSub.IpAllowList]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.CertificateAuthorities]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Certificates]: CertificatePolicyActionSchema.array().default([]), + [ProjectPermissionSub.PkiSubscribers]: PkiSubscriberPolicyActionSchema.extend({ + inverted: z.boolean().optional(), + conditions: ConditionSchema + }) + .array() + .default([]), [ProjectPermissionSub.PkiAlerts]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.PkiCollections]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]), @@ -271,6 +287,7 @@ type TConditionalFields = | ProjectPermissionSub.SecretFolders | ProjectPermissionSub.SecretImports | ProjectPermissionSub.DynamicSecrets + | ProjectPermissionSub.PkiSubscribers | ProjectPermissionSub.SshHosts | ProjectPermissionSub.SecretRotation | ProjectPermissionSub.Identity; @@ -284,7 +301,8 @@ export const isConditionalSubjects = ( subject === ProjectPermissionSub.SecretFolders || subject === ProjectPermissionSub.Identity || subject === ProjectPermissionSub.SshHosts || - subject === ProjectPermissionSub.SecretRotation; + subject === ProjectPermissionSub.SecretRotation || + subject === ProjectPermissionSub.PkiSubscribers; const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => { const formConditions: z.infer = []; @@ -715,6 +733,33 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { inverted }); } + + if (subject === ProjectPermissionSub.PkiSubscribers) { + if (!formVal[subject]) formVal[subject] = []; + + formVal[subject]!.push({ + [ProjectPermissionPkiSubscriberActions.Edit]: action.includes( + ProjectPermissionPkiSubscriberActions.Edit + ), + [ProjectPermissionPkiSubscriberActions.Delete]: action.includes( + ProjectPermissionPkiSubscriberActions.Delete + ), + [ProjectPermissionPkiSubscriberActions.Create]: action.includes( + ProjectPermissionPkiSubscriberActions.Create + ), + [ProjectPermissionPkiSubscriberActions.Read]: action.includes( + ProjectPermissionPkiSubscriberActions.Read + ), + [ProjectPermissionPkiSubscriberActions.IssueCert]: action.includes( + ProjectPermissionPkiSubscriberActions.IssueCert + ), + [ProjectPermissionPkiSubscriberActions.ListCerts]: action.includes( + ProjectPermissionPkiSubscriberActions.ListCerts + ), + conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], + inverted + }); + } }); return formVal; @@ -877,19 +922,19 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { title: "Dynamic Secrets", actions: [ { - label: "Read root credentials", + label: "Read Root Credentials", value: ProjectPermissionDynamicSecretActions.ReadRootCredential }, { - label: "Create root credentials", + label: "Create Root Credentials", value: ProjectPermissionDynamicSecretActions.CreateRootCredential }, { - label: "Modify root credentials", + label: "Modify Root Credentials", value: ProjectPermissionDynamicSecretActions.EditRootCredential }, { - label: "Remove root credentials", + label: "Remove Root Credentials", value: ProjectPermissionDynamicSecretActions.DeleteRootCredential }, { label: "Manage Leases", value: ProjectPermissionDynamicSecretActions.Lease } @@ -1104,6 +1149,17 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { { label: "Remove", value: "delete" } ] }, + [ProjectPermissionSub.PkiSubscribers]: { + title: "PKI Subscribers", + actions: [ + { label: "Read", value: ProjectPermissionPkiSubscriberActions.Read }, + { label: "Create", value: ProjectPermissionPkiSubscriberActions.Create }, + { label: "Modify", value: ProjectPermissionPkiSubscriberActions.Edit }, + { label: "Remove", value: ProjectPermissionPkiSubscriberActions.Delete }, + { label: "Issue Certificate", value: ProjectPermissionPkiSubscriberActions.IssueCert }, + { label: "List Certificates", value: ProjectPermissionPkiSubscriberActions.ListCerts } + ] + }, [ProjectPermissionSub.PkiCollections]: { title: "PKI Collections", actions: [ @@ -1174,23 +1230,23 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { title: "KMIP", actions: [ { - label: "Read clients", + label: "Read Clients", value: ProjectPermissionKmipActions.ReadClients }, { - label: "Create clients", + label: "Create Clients", value: ProjectPermissionKmipActions.CreateClients }, { - label: "Modify clients", + label: "Modify Clients", value: ProjectPermissionKmipActions.UpdateClients }, { - label: "Delete clients", + label: "Delete Clients", value: ProjectPermissionKmipActions.DeleteClients }, { - label: "Generate client certificates", + label: "Generate Client Certificates", value: ProjectPermissionKmipActions.GenerateClientCertificates } ] @@ -1233,6 +1289,7 @@ const KmsPermissionSubjects = (enabled = false) => ({ const CertificateManagerPermissionSubjects = (enabled = false) => ({ [ProjectPermissionSub.PkiCollections]: enabled, [ProjectPermissionSub.PkiAlerts]: enabled, + [ProjectPermissionSub.PkiSubscribers]: enabled, [ProjectPermissionSub.CertificateAuthorities]: enabled, [ProjectPermissionSub.CertificateTemplates]: enabled, [ProjectPermissionSub.Certificates]: enabled @@ -1280,3 +1337,340 @@ export const ProjectTypePermissionSubjects: Record< ...SecretsManagerPermissionSubjects() } }; + +export type RoleTemplate = { + id: string; + name: string; + description: string; + permissions: { subject: ProjectPermissionSub; actions: string[] }[]; +}; + +const projectManagerTemplate = ( + additionalPermissions: RoleTemplate["permissions"] = [] +): RoleTemplate => ({ + id: "project-manager", + name: "Project Management Policies", + description: "Grants access to manage project members and settings", + permissions: [ + { + subject: ProjectPermissionSub.AuditLogs, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.Groups, + actions: Object.values(ProjectPermissionGroupActions) + }, + { + subject: ProjectPermissionSub.Member, + actions: Object.values(ProjectPermissionMemberActions) + }, + { + subject: ProjectPermissionSub.Identity, + actions: Object.values(ProjectPermissionIdentityActions) + }, + { + subject: ProjectPermissionSub.Project, + actions: [ProjectPermissionActions.Edit, ProjectPermissionActions.Delete] + }, + { subject: ProjectPermissionSub.Role, actions: Object.values(ProjectPermissionActions) }, + { + subject: ProjectPermissionSub.Settings, + actions: [ProjectPermissionActions.Read, ProjectPermissionActions.Edit] + }, + ...additionalPermissions + ] +}); + +export const RoleTemplates: Record = { + [ProjectType.SSH]: [ + { + id: "ssh-viewer", + name: "SSH Viewing Policies", + description: "Grants read access to SSH certificates and hosts", + permissions: [ + { + subject: ProjectPermissionSub.SshCertificateAuthorities, + actions: [ProjectPermissionActions.Read] + }, + { + subject: ProjectPermissionSub.SshCertificates, + actions: [ProjectPermissionActions.Read] + }, + { + subject: ProjectPermissionSub.SshCertificateTemplates, + actions: [ProjectPermissionActions.Read] + }, + { + subject: ProjectPermissionSub.SshHosts, + actions: [ProjectPermissionSshHostActions.Read] + }, + { + subject: ProjectPermissionSub.SshHostGroups, + actions: [ProjectPermissionActions.Read] + } + ] + }, + { + id: "ssh-cert-editor", + name: "SSH Certificate Editing Policies", + description: "Grants read and edit access to SSH certificates", + permissions: [ + { + subject: ProjectPermissionSub.SshCertificateAuthorities, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.SshCertificates, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.SshCertificateTemplates, + actions: Object.values(ProjectPermissionActions) + } + ] + }, + { + id: "ssh-host-editor", + name: "SSH Host Editing Policies", + description: "Grants read and edit access to SSH hosts", + permissions: [ + { + subject: ProjectPermissionSub.SshHosts, + actions: Object.values(ProjectPermissionSshHostActions) + }, + { + subject: ProjectPermissionSub.SshHostGroups, + actions: Object.values(ProjectPermissionActions) + } + ] + }, + projectManagerTemplate() + ], + [ProjectType.KMS]: [ + { + id: "kms-viewer", + name: "KMS Viewing Policies", + description: "Grants read access to KMS keys and KMIP clients", + permissions: [ + { + subject: ProjectPermissionSub.Cmek, + actions: [ProjectPermissionCmekActions.Read] + }, + { + subject: ProjectPermissionSub.Kmip, + actions: [ProjectPermissionKmipActions.ReadClients] + } + ] + }, + { + id: "key-editor", + name: "KMS Key Editing Policies", + description: "Grants read and edit access to KMS keys", + permissions: [ + { + subject: ProjectPermissionSub.Cmek, + actions: Object.values(ProjectPermissionCmekActions) + } + ] + }, + { + id: "kmip-editor", + name: "KMIP Client Editing Policies", + description: "Grants read and edit access to KMIP clients", + permissions: [ + { + subject: ProjectPermissionSub.Kmip, + actions: Object.values(ProjectPermissionKmipActions) + } + ] + }, + projectManagerTemplate() + ], + [ProjectType.CertificateManager]: [ + { + id: "cert-viewer", + name: "Certificate Viewing Policies", + description: "Grants read access to certificates and related resources", + permissions: [ + { + subject: ProjectPermissionSub.PkiCollections, + actions: [ProjectPermissionActions.Read] + }, + { + subject: ProjectPermissionSub.PkiAlerts, + actions: [ProjectPermissionActions.Read] + }, + { + subject: ProjectPermissionSub.CertificateAuthorities, + actions: [ProjectPermissionActions.Read] + }, + { + subject: ProjectPermissionSub.CertificateTemplates, + actions: [ProjectPermissionActions.Read] + }, + { + subject: ProjectPermissionSub.Certificates, + actions: [ + ProjectPermissionCertificateActions.Read, + ProjectPermissionCertificateActions.ReadPrivateKey + ] + } + ] + }, + { + id: "cert-editor", + name: "Certificate Editing Policies", + description: "Grants read and edit access to certificates and related resources", + permissions: [ + { + subject: ProjectPermissionSub.PkiCollections, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.PkiAlerts, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.CertificateAuthorities, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.CertificateTemplates, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.Certificates, + actions: Object.values(ProjectPermissionCertificateActions) + } + ] + }, + projectManagerTemplate() + ], + [ProjectType.SecretManager]: [ + { + id: "secret-viewer", + name: "Secret Viewing Policies", + description: "Grants read access to secrets and related resources", + permissions: [ + { + subject: ProjectPermissionSub.SecretRollback, + actions: [ProjectPermissionActions.Read] + }, + { + subject: ProjectPermissionSub.SecretImports, + actions: [ProjectPermissionActions.Read] + }, + { + subject: ProjectPermissionSub.Secrets, + actions: [ + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue + ] + }, + { + subject: ProjectPermissionSub.DynamicSecrets, + actions: [ProjectPermissionDynamicSecretActions.ReadRootCredential] + }, + { + subject: ProjectPermissionSub.Environments, + actions: [ProjectPermissionActions.Read] + }, + { + subject: ProjectPermissionSub.Tags, + actions: [ProjectPermissionActions.Read] + }, + { + subject: ProjectPermissionSub.SecretRotation, + actions: [ProjectPermissionSecretRotationActions.Read] + }, + { + subject: ProjectPermissionSub.Integrations, + actions: [ProjectPermissionActions.Read] + }, + { + subject: ProjectPermissionSub.SecretSyncs, + actions: [ProjectPermissionSecretSyncActions.Read] + } + ] + }, + { + id: "secret-editor", + name: "Secret Editing Policies", + description: "Grants read and edit access to secrets and related resources", + permissions: [ + { + subject: ProjectPermissionSub.Environments, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.DynamicSecrets, + actions: Object.values(ProjectPermissionDynamicSecretActions) + }, + { + subject: ProjectPermissionSub.Secrets, + actions: [ + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.Edit, + ProjectPermissionSecretActions.Create, + ProjectPermissionSecretActions.Delete + ] + }, + { + subject: ProjectPermissionSub.SecretRollback, + actions: [ProjectPermissionActions.Read, ProjectPermissionActions.Create] + }, + { + subject: ProjectPermissionSub.Tags, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.SecretImports, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.SecretRotation, + actions: Object.values(ProjectPermissionSecretRotationActions) + }, + { + subject: ProjectPermissionSub.SecretFolders, + actions: [ + ProjectPermissionActions.Create, + ProjectPermissionActions.Edit, + ProjectPermissionActions.Delete + ] + }, + { + subject: ProjectPermissionSub.Integrations, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.SecretSyncs, + actions: Object.values(ProjectPermissionSecretSyncActions) + } + ] + }, + projectManagerTemplate([ + { + subject: ProjectPermissionSub.IpAllowList, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.Kms, + actions: [ProjectPermissionActions.Edit] + }, + { + subject: ProjectPermissionSub.SecretApproval, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.ServiceTokens, + actions: Object.values(ProjectPermissionActions) + }, + { + subject: ProjectPermissionSub.Webhooks, + actions: Object.values(ProjectPermissionActions) + } + ]) + ] +}; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx index e97e60edf..2e51de9b7 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx @@ -17,7 +17,7 @@ import { slugSchema } from "@app/lib/schemas"; const schema = z .object({ - name: z.string(), + name: z.string().min(1, "Name required"), description: z.string(), slug: slugSchema({ min: 1 }) }) @@ -62,7 +62,7 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { if (role) { reset({ name: role.name, - description: role.description, + description: role.description || "", slug: role.slug }); } else { diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx index 8dc9a2fa1..593b40e93 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx @@ -1,7 +1,7 @@ import { useMemo } from "react"; import { FormProvider, useForm } from "react-hook-form"; import { MongoAbility, MongoQuery, RawRuleOf } from "@casl/ability"; -import { faPlus, faSave } from "@fortawesome/free-solid-svg-icons"; +import { faSave } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { twMerge } from "tailwind-merge"; @@ -12,17 +12,17 @@ import { Button } from "@app/components/v2"; import { ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; import { evaluatePermissionsAbility } from "@app/helpers/permissions"; -import { usePopUp } from "@app/hooks"; import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { ProjectType } from "@app/hooks/api/workspace/types"; -import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal"; +import { AddPoliciesButton } from "./AddPoliciesButton"; import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionConditions"; import { GeneralPermissionConditions } from "./GeneralPermissionConditions"; import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies"; import { IdentityManagementPermissionConditions } from "./IdentityManagementPermissionConditions"; import { PermissionEmptyState } from "./PermissionEmptyState"; +import { PkiSubscriberPermissionConditions } from "./PkiSubscriberPermissionConditions"; import { formRolePermission2API, isConditionalSubjects, @@ -59,6 +59,10 @@ export const renderConditionalComponents = ( return ; } + if (subject === ProjectPermissionSub.PkiSubscribers) { + return ; + } + return ; } @@ -86,8 +90,6 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { const { mutateAsync: updateRole } = useUpdateProjectRole(); - const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const); - const onSubmit = async (el: TFormSchema) => { try { if (!projectId || !role?.id) return; @@ -151,7 +153,7 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { variant="outline_bg" type="submit" className={twMerge( - "h-10 rounded-r-none border border-primary", + "mr-4 h-10 border border-primary", isDirty && "bg-primary text-black" )} isDisabled={isSubmitting || !isDirty} @@ -160,15 +162,7 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { > Save - +
)} @@ -190,10 +184,6 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { ))}
- handlePopUpToggle("addPolicy", isOpen)} - /> diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.tsx index 6a1482d98..172dda302 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.tsx @@ -1,11 +1,9 @@ import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; -import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNavigate, useSearch } from "@tanstack/react-router"; import { ProjectPermissionCan } from "@app/components/permissions"; -import { Badge, PageHeader, Tab, TabList, TabPanel, Tabs, Tooltip } from "@app/components/v2"; +import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types"; @@ -54,16 +52,7 @@ export const IntegrationsListPage = () => { Secret Syncs - - Native Integrations - -
- - Legacy - -
-
-
+ Native Integrations Framework Integrations @@ -81,26 +70,6 @@ export const IntegrationsListPage = () => {
-
-
- - Native Integrations Transitioning to Legacy Status -
-

- Native integrations are now a legacy feature and we will begin a phased - deprecation in 2026. We recommend migrating to our new{" "} - - Secret Syncs - {" "} - feature which offers the same functionality as Native Integrations with improved - stability, insights, re-configurability, and customization. -

-
{ + const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync); + + return ; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx index abfbf100c..898980881 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx @@ -10,6 +10,7 @@ import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol"; import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol"; import { HCVaultSyncDestinationCol } from "./HCVaultSyncDestinationCol"; import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol"; +import { OCIVaultSyncDestinationCol } from "./OCIVaultSyncDestinationCol"; import { TeamCitySyncDestinationCol } from "./TeamCitySyncDestinationCol"; import { TerraformCloudSyncDestinationCol } from "./TerraformCloudSyncDestinationCol"; import { VercelSyncDestinationCol } from "./VercelSyncDestinationCol"; @@ -49,6 +50,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.TeamCity: return ; + case SecretSync.OCIVault: + return ; default: throw new Error( `Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}` diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts index b1fe387e5..4bf0eeed2 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts @@ -102,6 +102,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { primaryText = destinationConfig.project; secondaryText = destinationConfig.buildConfig; break; + case SecretSync.OCIVault: + primaryText = destinationConfig.compartmentOcid; + secondaryText = destinationConfig.vaultOcid; + break; default: throw new Error(`Unhandled Destination Col Values ${destination}`); } diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/OCIVaultSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/OCIVaultSyncDestinationSection.tsx new file mode 100644 index 000000000..8a987ae26 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/OCIVaultSyncDestinationSection.tsx @@ -0,0 +1,42 @@ +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { Tooltip } from "@app/components/v2"; +import { TOCIVaultSync } from "@app/hooks/api/secretSyncs/types/oci-vault-sync"; + +type Props = { + secretSync: TOCIVaultSync; +}; + +export const OCIVaultSyncDestinationSection = ({ secretSync }: Props) => { + const { + destinationConfig: { compartmentOcid, keyOcid, vaultOcid } + } = secretSync; + + return ( + <> + +
+ + {compartmentOcid.substring(0, 21)}... + {compartmentOcid.substring(compartmentOcid.length - 6)} + +
+
+ +
+ + {vaultOcid.substring(0, 15)}... + {vaultOcid.substring(vaultOcid.length - 6)} + +
+
+ +
+ + {keyOcid.substring(0, 13)}... + {keyOcid.substring(keyOcid.length - 6)} + +
+
+ + ); +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx index 4ea5798d9..b0c989ee2 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -20,6 +20,7 @@ import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection"; import { GitHubSyncDestinationSection } from "./GitHubSyncDestinationSection"; import { HCVaultSyncDestinationSection } from "./HCVaultSyncDestinationSection"; import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection"; +import { OCIVaultSyncDestinationSection } from "./OCIVaultSyncDestinationSection"; import { TeamCitySyncDestinationSection } from "./TeamCitySyncDestinationSection"; import { TerraformCloudSyncDestinationSection } from "./TerraformCloudSyncDestinationSection"; import { VercelSyncDestinationSection } from "./VercelSyncDestinationSection"; @@ -81,6 +82,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.TeamCity: DestinationComponents = ; break; + case SecretSync.OCIVault: + DestinationComponents = ; + break; default: throw new Error(`Unhandled Destination Section components: ${destination}`); } diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx index 5995e7cd1..ca229b55c 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -54,6 +54,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = case SecretSync.Windmill: case SecretSync.HCVault: case SecretSync.TeamCity: + case SecretSync.OCIVault: AdditionalSyncOptionsComponent = null; break; default: diff --git a/frontend/src/pages/ssh/SshHostsPage/components/SshHostsTable.tsx b/frontend/src/pages/ssh/SshHostsPage/components/SshHostsTable.tsx index e3c53860e..62f563cce 100644 --- a/frontend/src/pages/ssh/SshHostsPage/components/SshHostsTable.tsx +++ b/frontend/src/pages/ssh/SshHostsPage/components/SshHostsTable.tsx @@ -30,7 +30,7 @@ import { Tooltip, Tr } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionSshHostActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { fetchSshHostUserCaPublicKey, useListWorkspaceSshHosts } from "@app/hooks/api"; import { LoginMappingSource } from "@app/hooks/api/sshHost/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -221,7 +221,7 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => { Download User CA Public Key {(isAllowed) => ( @@ -243,7 +243,7 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => { )} {(isAllowed) => ( diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 5ca262a1d..f457cf209 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -42,6 +42,7 @@ import { Route as adminLayoutImport } from './pages/admin/layout' import { Route as authProviderSuccessPageRouteImport } from './pages/auth/ProviderSuccessPage/route' import { Route as authProviderErrorPageRouteImport } from './pages/auth/ProviderErrorPage/route' import { Route as userPersonalSettingsPageRouteImport } from './pages/user/PersonalSettingsPage/route' +import { Route as organizationSsoPageRouteImport } from './pages/organization/SsoPage/route' import { Route as organizationSecretScanningPageRouteImport } from './pages/organization/SecretScanningPage/route' import { Route as organizationBillingPageRouteImport } from './pages/organization/BillingPage/route' import { Route as organizationAuditLogsPageRouteImport } from './pages/organization/AuditLogsPage/route' @@ -118,8 +119,10 @@ import { Route as secretManagerSecretDashboardPageRouteImport } from './pages/se import { Route as secretManagerIntegrationsSelectIntegrationAuthPageRouteImport } from './pages/secret-manager/integrations/SelectIntegrationAuthPage/route' import { Route as secretManagerIntegrationsDetailsByIDPageRouteImport } from './pages/secret-manager/IntegrationsDetailsByIDPage/route' import { Route as organizationAppConnectionsOauthCallbackPageRouteImport } from './pages/organization/AppConnections/OauthCallbackPage/route' +import { Route as certManagerPkiSubscriberDetailsByIDPageRouteImport } from './pages/cert-manager/PkiSubscriberDetailsByIDPage/route' import { Route as certManagerCertAuthDetailsByIDPageRouteImport } from './pages/cert-manager/CertAuthDetailsByIDPage/route' import { Route as secretManagerIntegrationsListPageRouteImport } from './pages/secret-manager/IntegrationsListPage/route' +import { Route as certManagerPkiSubscribersPageRouteImport } from './pages/cert-manager/PkiSubscribersPage/route' import { Route as secretManagerIntegrationsWindmillConfigurePageRouteImport } from './pages/secret-manager/integrations/WindmillConfigurePage/route' import { Route as secretManagerIntegrationsWindmillAuthorizePageRouteImport } from './pages/secret-manager/integrations/WindmillAuthorizePage/route' import { Route as secretManagerIntegrationsVercelConfigurePageRouteImport } from './pages/secret-manager/integrations/VercelConfigurePage/route' @@ -250,6 +253,10 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLa createFileRoute( '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations', )() +const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersImport = + createFileRoute( + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers', + )() // Create/Update Routes @@ -539,6 +546,12 @@ const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdRoute = getParentRoute: () => organizationLayoutRoute, } as any) +const organizationSsoPageRouteRoute = organizationSsoPageRouteImport.update({ + id: '/sso', + path: '/sso', + getParentRoute: () => AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute, +} as any) + const organizationSecretScanningPageRouteRoute = organizationSecretScanningPageRouteImport.update({ id: '/secret-scanning', @@ -848,6 +861,15 @@ const secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute = } as any, ) +const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute = + AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersImport.update( + { + id: '/subscribers', + path: '/subscribers', + getParentRoute: () => certManagerLayoutRoute, + } as any, + ) + const projectAccessControlPageRouteCertManagerRoute = projectAccessControlPageRouteCertManagerImport.update({ id: '/access-management', @@ -949,8 +971,8 @@ const certManagerSettingsPageRouteRoute = const certManagerCertificatesPageRouteRoute = certManagerCertificatesPageRouteImport.update({ - id: '/overview', - path: '/overview', + id: '/certificates', + path: '/certificates', getParentRoute: () => certManagerLayoutRoute, } as any) @@ -1103,6 +1125,14 @@ const organizationAppConnectionsOauthCallbackPageRouteRoute = AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute, } as any) +const certManagerPkiSubscriberDetailsByIDPageRouteRoute = + certManagerPkiSubscriberDetailsByIDPageRouteImport.update({ + id: '/$subscriberName', + path: '/$subscriberName', + getParentRoute: () => + AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute, + } as any) + const certManagerCertAuthDetailsByIDPageRouteRoute = certManagerCertAuthDetailsByIDPageRouteImport.update({ id: '/ca/$caId', @@ -1118,6 +1148,14 @@ const secretManagerIntegrationsListPageRouteRoute = AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRoute, } as any) +const certManagerPkiSubscribersPageRouteRoute = + certManagerPkiSubscribersPageRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => + AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute, + } as any) + const secretManagerIntegrationsWindmillConfigurePageRouteRoute = secretManagerIntegrationsWindmillConfigurePageRouteImport.update({ id: '/windmill/create', @@ -2017,6 +2055,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof organizationSecretScanningPageRouteImport parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport } + '/_authenticate/_inject-org-details/_org-layout/organization/sso': { + id: '/_authenticate/_inject-org-details/_org-layout/organization/sso' + path: '/sso' + fullPath: '/organization/sso' + preLoaderRoute: typeof organizationSsoPageRouteImport + parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport + } '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId': { id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId' path: '/cert-manager/$projectId' @@ -2234,10 +2279,10 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof certManagerCertificateAuthoritiesPageRouteImport parentRoute: typeof certManagerLayoutImport } - '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview': { - id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview' - path: '/overview' - fullPath: '/cert-manager/$projectId/overview' + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates': { + id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates' + path: '/certificates' + fullPath: '/cert-manager/$projectId/certificates' preLoaderRoute: typeof certManagerCertificatesPageRouteImport parentRoute: typeof certManagerLayoutImport } @@ -2346,6 +2391,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectAccessControlPageRouteCertManagerImport parentRoute: typeof certManagerLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers': { + id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers' + path: '/subscribers' + fullPath: '/cert-manager/$projectId/subscribers' + preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersImport + parentRoute: typeof certManagerLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/integrations/azure-app-configuration/oauth2/callback': { id: '/_authenticate/_inject-org-details/_org-layout/integrations/azure-app-configuration/oauth2/callback' path: '/azure-app-configuration/oauth2/callback' @@ -2437,6 +2489,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectAccessControlPageRouteSshImport parentRoute: typeof sshLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/': { + id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/' + path: '/' + fullPath: '/cert-manager/$projectId/subscribers/' + preLoaderRoute: typeof certManagerPkiSubscribersPageRouteImport + parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersImport + } '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/': { id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/' path: '/' @@ -2451,6 +2510,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof certManagerCertAuthDetailsByIDPageRouteImport parentRoute: typeof certManagerLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName': { + id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName' + path: '/$subscriberName' + fullPath: '/cert-manager/$projectId/subscribers/$subscriberName' + preLoaderRoute: typeof certManagerPkiSubscriberDetailsByIDPageRouteImport + parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersImport + } '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback': { id: '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback' path: '/$appConnection/oauth/callback' @@ -3227,6 +3293,7 @@ interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren { organizationAuditLogsPageRouteRoute: typeof organizationAuditLogsPageRouteRoute organizationBillingPageRouteRoute: typeof organizationBillingPageRouteRoute organizationSecretScanningPageRouteRoute: typeof organizationSecretScanningPageRouteRoute + organizationSsoPageRouteRoute: typeof organizationSsoPageRouteRoute AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren @@ -3254,6 +3321,7 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica organizationBillingPageRouteRoute: organizationBillingPageRouteRoute, organizationSecretScanningPageRouteRoute: organizationSecretScanningPageRouteRoute, + organizationSsoPageRouteRoute: organizationSsoPageRouteRoute, AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren, AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute: @@ -3292,12 +3360,31 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteWithChildren = AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren, ) +interface AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteChildren { + certManagerPkiSubscribersPageRouteRoute: typeof certManagerPkiSubscribersPageRouteRoute + certManagerPkiSubscriberDetailsByIDPageRouteRoute: typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute +} + +const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteChildren = + { + certManagerPkiSubscribersPageRouteRoute: + certManagerPkiSubscribersPageRouteRoute, + certManagerPkiSubscriberDetailsByIDPageRouteRoute: + certManagerPkiSubscriberDetailsByIDPageRouteRoute, + } + +const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren = + AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute._addFileChildren( + AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteChildren, + ) + interface certManagerLayoutRouteChildren { certManagerAlertingPageRouteRoute: typeof certManagerAlertingPageRouteRoute certManagerCertificateAuthoritiesPageRouteRoute: typeof certManagerCertificateAuthoritiesPageRouteRoute certManagerCertificatesPageRouteRoute: typeof certManagerCertificatesPageRouteRoute certManagerSettingsPageRouteRoute: typeof certManagerSettingsPageRouteRoute projectAccessControlPageRouteCertManagerRoute: typeof projectAccessControlPageRouteCertManagerRoute + AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren certManagerCertAuthDetailsByIDPageRouteRoute: typeof certManagerCertAuthDetailsByIDPageRouteRoute projectIdentityDetailsByIDPageRouteCertManagerRoute: typeof projectIdentityDetailsByIDPageRouteCertManagerRoute projectMemberDetailsByIDPageRouteCertManagerRoute: typeof projectMemberDetailsByIDPageRouteCertManagerRoute @@ -3313,6 +3400,8 @@ const certManagerLayoutRouteChildren: certManagerLayoutRouteChildren = { certManagerSettingsPageRouteRoute: certManagerSettingsPageRouteRoute, projectAccessControlPageRouteCertManagerRoute: projectAccessControlPageRouteCertManagerRoute, + AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute: + AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren, certManagerCertAuthDetailsByIDPageRouteRoute: certManagerCertAuthDetailsByIDPageRouteRoute, projectIdentityDetailsByIDPageRouteCertManagerRoute: @@ -3956,6 +4045,7 @@ export interface FileRoutesByFullPath { '/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute '/organization/billing': typeof organizationBillingPageRouteRoute '/organization/secret-scanning': typeof organizationSecretScanningPageRouteRoute + '/organization/sso': typeof organizationSsoPageRouteRoute '/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren '/kms/$projectId': typeof kmsLayoutRouteWithChildren '/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren @@ -3983,7 +4073,7 @@ export interface FileRoutesByFullPath { '/organization/ssh/settings': typeof organizationSshSettingsPageRouteRoute '/cert-manager/$projectId/alerting': typeof certManagerAlertingPageRouteRoute '/cert-manager/$projectId/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute - '/cert-manager/$projectId/overview': typeof certManagerCertificatesPageRouteRoute + '/cert-manager/$projectId/certificates': typeof certManagerCertificatesPageRouteRoute '/cert-manager/$projectId/settings': typeof certManagerSettingsPageRouteRoute '/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute '/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute @@ -3999,6 +4089,7 @@ export interface FileRoutesByFullPath { '/ssh/$projectId/overview': typeof sshSshHostsPageRouteRoute '/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute '/cert-manager/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute + '/cert-manager/$projectId/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren '/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute '/integrations/azure-key-vault/oauth2/callback': typeof secretManagerIntegrationsRouteAzureKeyVaultOauthRedirectRoute '/integrations/bitbucket/oauth2/callback': typeof secretManagerIntegrationsRouteBitbucketOauthRedirectRoute @@ -4012,8 +4103,10 @@ export interface FileRoutesByFullPath { '/secret-manager/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute '/secret-manager/$projectId/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteWithChildren '/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute + '/cert-manager/$projectId/subscribers/': typeof certManagerPkiSubscribersPageRouteRoute '/secret-manager/$projectId/integrations/': typeof secretManagerIntegrationsListPageRouteRoute '/cert-manager/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute + '/cert-manager/$projectId/subscribers/$subscriberName': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute '/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute '/secret-manager/$projectId/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute '/secret-manager/$projectId/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute @@ -4143,6 +4236,7 @@ export interface FileRoutesByTo { '/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute '/organization/billing': typeof organizationBillingPageRouteRoute '/organization/secret-scanning': typeof organizationSecretScanningPageRouteRoute + '/organization/sso': typeof organizationSsoPageRouteRoute '/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren '/kms/$projectId': typeof kmsLayoutRouteWithChildren '/secret-manager/$projectId': typeof secretManagerLayoutRouteWithChildren @@ -4166,7 +4260,7 @@ export interface FileRoutesByTo { '/organization/ssh/settings': typeof organizationSshSettingsPageRouteRoute '/cert-manager/$projectId/alerting': typeof certManagerAlertingPageRouteRoute '/cert-manager/$projectId/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute - '/cert-manager/$projectId/overview': typeof certManagerCertificatesPageRouteRoute + '/cert-manager/$projectId/certificates': typeof certManagerCertificatesPageRouteRoute '/cert-manager/$projectId/settings': typeof certManagerSettingsPageRouteRoute '/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute '/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute @@ -4194,8 +4288,10 @@ export interface FileRoutesByTo { '/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute '/secret-manager/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute '/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute + '/cert-manager/$projectId/subscribers': typeof certManagerPkiSubscribersPageRouteRoute '/secret-manager/$projectId/integrations': typeof secretManagerIntegrationsListPageRouteRoute '/cert-manager/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute + '/cert-manager/$projectId/subscribers/$subscriberName': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute '/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute '/secret-manager/$projectId/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute '/secret-manager/$projectId/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute @@ -4335,6 +4431,7 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/billing': typeof organizationBillingPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning': typeof organizationSecretScanningPageRouteRoute + '/_authenticate/_inject-org-details/_org-layout/organization/sso': typeof organizationSsoPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/kms/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutKmsProjectIdRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren @@ -4366,7 +4463,7 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout': typeof sshLayoutRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/alerting': typeof certManagerAlertingPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute - '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview': typeof certManagerCertificatesPageRouteRoute + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates': typeof certManagerCertificatesPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/settings': typeof certManagerSettingsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip': typeof kmsKmipPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview': typeof kmsOverviewPageRouteRoute @@ -4382,6 +4479,7 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview': typeof sshSshHostsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings': typeof sshSettingsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/access-management': typeof projectAccessControlPageRouteCertManagerRoute + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute '/_authenticate/_inject-org-details/_org-layout/integrations/azure-key-vault/oauth2/callback': typeof secretManagerIntegrationsRouteAzureKeyVaultOauthRedirectRoute '/_authenticate/_inject-org-details/_org-layout/integrations/bitbucket/oauth2/callback': typeof secretManagerIntegrationsRouteBitbucketOauthRedirectRoute @@ -4395,8 +4493,10 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/access-management': typeof projectAccessControlPageRouteSecretManagerRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/access-management': typeof projectAccessControlPageRouteSshRoute + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/': typeof certManagerPkiSubscribersPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/': typeof secretManagerIntegrationsListPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute @@ -4532,6 +4632,7 @@ export interface FileRouteTypes { | '/organization/audit-logs' | '/organization/billing' | '/organization/secret-scanning' + | '/organization/sso' | '/cert-manager/$projectId' | '/kms/$projectId' | '/organization/app-connections' @@ -4559,7 +4660,7 @@ export interface FileRouteTypes { | '/organization/ssh/settings' | '/cert-manager/$projectId/alerting' | '/cert-manager/$projectId/certificate-authorities' - | '/cert-manager/$projectId/overview' + | '/cert-manager/$projectId/certificates' | '/cert-manager/$projectId/settings' | '/kms/$projectId/kmip' | '/kms/$projectId/overview' @@ -4575,6 +4676,7 @@ export interface FileRouteTypes { | '/ssh/$projectId/overview' | '/ssh/$projectId/settings' | '/cert-manager/$projectId/access-management' + | '/cert-manager/$projectId/subscribers' | '/integrations/azure-app-configuration/oauth2/callback' | '/integrations/azure-key-vault/oauth2/callback' | '/integrations/bitbucket/oauth2/callback' @@ -4588,8 +4690,10 @@ export interface FileRouteTypes { | '/secret-manager/$projectId/access-management' | '/secret-manager/$projectId/integrations' | '/ssh/$projectId/access-management' + | '/cert-manager/$projectId/subscribers/' | '/secret-manager/$projectId/integrations/' | '/cert-manager/$projectId/ca/$caId' + | '/cert-manager/$projectId/subscribers/$subscriberName' | '/organization/app-connections/$appConnection/oauth/callback' | '/secret-manager/$projectId/integrations/$integrationId' | '/secret-manager/$projectId/integrations/select-integration-auth' @@ -4718,6 +4822,7 @@ export interface FileRouteTypes { | '/organization/audit-logs' | '/organization/billing' | '/organization/secret-scanning' + | '/organization/sso' | '/cert-manager/$projectId' | '/kms/$projectId' | '/secret-manager/$projectId' @@ -4741,7 +4846,7 @@ export interface FileRouteTypes { | '/organization/ssh/settings' | '/cert-manager/$projectId/alerting' | '/cert-manager/$projectId/certificate-authorities' - | '/cert-manager/$projectId/overview' + | '/cert-manager/$projectId/certificates' | '/cert-manager/$projectId/settings' | '/kms/$projectId/kmip' | '/kms/$projectId/overview' @@ -4769,8 +4874,10 @@ export interface FileRouteTypes { | '/kms/$projectId/access-management' | '/secret-manager/$projectId/access-management' | '/ssh/$projectId/access-management' + | '/cert-manager/$projectId/subscribers' | '/secret-manager/$projectId/integrations' | '/cert-manager/$projectId/ca/$caId' + | '/cert-manager/$projectId/subscribers/$subscriberName' | '/organization/app-connections/$appConnection/oauth/callback' | '/secret-manager/$projectId/integrations/$integrationId' | '/secret-manager/$projectId/integrations/select-integration-auth' @@ -4908,6 +5015,7 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs' | '/_authenticate/_inject-org-details/_org-layout/organization/billing' | '/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning' + | '/_authenticate/_inject-org-details/_org-layout/organization/sso' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId' | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId' | '/_authenticate/_inject-org-details/_org-layout/organization/app-connections' @@ -4939,7 +5047,7 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/alerting' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-authorities' - | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview' + | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/settings' | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip' | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview' @@ -4955,6 +5063,7 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview' | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/access-management' + | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers' | '/_authenticate/_inject-org-details/_org-layout/integrations/azure-app-configuration/oauth2/callback' | '/_authenticate/_inject-org-details/_org-layout/integrations/azure-key-vault/oauth2/callback' | '/_authenticate/_inject-org-details/_org-layout/integrations/bitbucket/oauth2/callback' @@ -4968,8 +5077,10 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/access-management' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations' | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/access-management' + | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId' + | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName' | '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth' @@ -5304,6 +5415,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/organization/audit-logs", "/_authenticate/_inject-org-details/_org-layout/organization/billing", "/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning", + "/_authenticate/_inject-org-details/_org-layout/organization/sso", "/_authenticate/_inject-org-details/_org-layout/organization/app-connections", "/_authenticate/_inject-org-details/_org-layout/organization/gateways", "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing", @@ -5353,6 +5465,10 @@ export const routeTree = rootRoute "filePath": "organization/SecretScanningPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization" }, + "/_authenticate/_inject-org-details/_org-layout/organization/sso": { + "filePath": "organization/SsoPage/route.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/organization" + }, "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId": { "filePath": "", "parent": "/_authenticate/_inject-org-details/_org-layout", @@ -5486,9 +5602,10 @@ export const routeTree = rootRoute "children": [ "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/alerting", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-authorities", - "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview", + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/settings", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/access-management", + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/identities/$identityId", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/members/$membershipId", @@ -5550,7 +5667,7 @@ export const routeTree = rootRoute "filePath": "cert-manager/CertificateAuthoritiesPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout" }, - "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview": { + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates": { "filePath": "cert-manager/CertificatesPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout" }, @@ -5614,6 +5731,14 @@ export const routeTree = rootRoute "filePath": "project/AccessControlPage/route-cert-manager.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout" }, + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers": { + "filePath": "", + "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout", + "children": [ + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/", + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName" + ] + }, "/_authenticate/_inject-org-details/_org-layout/integrations/azure-app-configuration/oauth2/callback": { "filePath": "secret-manager/integrations/route-azure-app-configurations-oauth-redirect.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/integrations" @@ -5746,6 +5871,10 @@ export const routeTree = rootRoute "filePath": "project/AccessControlPage/route-ssh.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout" }, + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/": { + "filePath": "cert-manager/PkiSubscribersPage/route.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers" + }, "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/": { "filePath": "secret-manager/IntegrationsListPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations" @@ -5754,6 +5883,10 @@ export const routeTree = rootRoute "filePath": "cert-manager/CertAuthDetailsByIDPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout" }, + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName": { + "filePath": "cert-manager/PkiSubscriberDetailsByIDPage/route.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers" + }, "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback": { "filePath": "organization/AppConnections/OauthCallbackPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization/app-connections" diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index d1beb5507..d6553e663 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -28,6 +28,7 @@ const organizationRoutes = route("/organization", [ index("organization/SettingsPage/route.tsx"), route("/oauth/callback", "organization/SettingsPage/OauthCallbackPage/route.tsx") ]), + route("/sso", "organization/SsoPage/route.tsx"), route("/secret-scanning", "organization/SecretScanningPage/route.tsx"), route("/groups/$groupId", "organization/GroupDetailsByIDPage/route.tsx"), route("/members/$membershipId", "organization/UserDetailsByIDPage/route.tsx"), @@ -288,7 +289,11 @@ const secretManagerIntegrationsRedirect = route("/integrations", [ const certManagerRoutes = route("/cert-manager/$projectId", [ layout("cert-manager-layout", "cert-manager/layout.tsx", [ - route("/overview", "cert-manager/CertificatesPage/route.tsx"), + route("/subscribers", [ + index("cert-manager/PkiSubscribersPage/route.tsx"), + route("/$subscriberName", "cert-manager/PkiSubscriberDetailsByIDPage/route.tsx") + ]), + route("/certificates", "cert-manager/CertificatesPage/route.tsx"), route("/certificate-authorities", "cert-manager/CertificateAuthoritiesPage/route.tsx"), route("/alerting", "cert-manager/AlertingPage/route.tsx"), route("/ca/$caId", "cert-manager/CertAuthDetailsByIDPage/route.tsx"),