From aecee669016ab41bb7a1936c10d23d28708e2cbe Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Thu, 16 Sep 2021 01:26:40 +0100 Subject: [PATCH] SSH key management via Web UI --- Cargo.lock | 1 + chartered-db/Cargo.toml | 1 + chartered-frontend/package-lock.json | 359 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ chartered-frontend/package.json | 1 + chartered-db/src/lib.rs | 2 ++ chartered-db/src/users.rs | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ chartered-frontend/src/index.tsx | 4 ++++ chartered-web/src/main.rs | 20 +++++++++++++------- chartered-frontend/src/sections/Nav.tsx | 18 +----------------- chartered-frontend/src/pages/ssh-keys/AddSshKeys.tsx | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ chartered-frontend/src/pages/ssh-keys/ListSshKeys.tsx | 118 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ chartered-web/src/endpoints/web_api/mod.rs | 4 ++++ chartered-web/src/endpoints/web_api/ssh_key.rs | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 13 files changed, 770 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fcb4322..82e042f 100644 --- a/Cargo.lock +++ a/Cargo.lock @@ -198,6 +198,7 @@ "serde", "serde_json", "thiserror", + "thrussh-keys", "tokio", ] diff --git a/chartered-db/Cargo.toml b/chartered-db/Cargo.toml index d864b79..b3305ba 100644 --- a/chartered-db/Cargo.toml +++ a/chartered-db/Cargo.toml @@ -20,3 +20,4 @@ thiserror = "1" tokio = "1" dotenv = "0.15" +thrussh-keys = "0.21" diff --git a/chartered-frontend/package-lock.json b/chartered-frontend/package-lock.json index a37e503..968988d 100644 --- a/chartered-frontend/package-lock.json +++ a/chartered-frontend/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "bootstrap": "^5.1.1", "react": "^17.0.2", + "react-bootstrap": "^2.0.0-beta.6", "react-bootstrap-icons": "^1.5.0", "react-dom": "^17.0.2", "react-markdown": "^7.0.1", @@ -1778,12 +1779,73 @@ "version": "2.10.1", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.1.tgz", "integrity": "sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw==", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" } }, + "node_modules/@react-aria/ssr": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.1.0.tgz", + "integrity": "sha512-RxqQKmE8sO7TGdrcSlHTcVzMP450hqowtBSd2bBS9oPlcokVkaGq28c3Rwa8ty5ctw4EBCjXqjP7xdcKMGDzug==", + "dependencies": { + "@babel/runtime": "^7.6.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1" + } + }, + "node_modules/@restart/context": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz", + "integrity": "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q==", + "peerDependencies": { + "react": ">=16.3.2" + } + }, + "node_modules/@restart/hooks": { + "version": "0.3.27", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.27.tgz", + "integrity": "sha512-s984xV/EapUIfkjlf8wz9weP2O9TNKR96C68FfMEy2bE69+H4cNv3RD4Mf97lW7Htt7PjZrYTjSC8f3SB9VCXw==", + "dependencies": { + "dequal": "^2.0.2" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@restart/ui": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-0.2.2.tgz", + "integrity": "sha512-PgNkiyOaWwx8ttQ45KNABXU3780fB/UxNFxcsCpC4RRAlaByZHHbNLOKfhuFs+ZUU0uLxEH9wYQEhDAZc6ajkA==", + "dependencies": { + "@babel/runtime": "^7.13.16", + "@popperjs/core": "^2.9.2", + "@react-aria/ssr": "^3.0.1", + "@restart/hooks": "^0.4.0", + "@types/warning": "^3.0.0", + "dequal": "^2.0.2", + "dom-helpers": "^5.2.0", + "prop-types": "^15.7.2", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, + "node_modules/@restart/ui/node_modules/@restart/hooks": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.0.tgz", + "integrity": "sha512-+RenTVobiCHPjUTbhQDV8m0PU1xEWqgloMIIOlf86oKnfghKR/l4tKto7TH543shEQZZa7ARSMTvT0cXN9u8+g==", + "dependencies": { + "dequal": "^2.0.2" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@types/debug": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", @@ -1800,6 +1862,11 @@ "@types/unist": "*" } }, + "node_modules/@types/invariant": { + "version": "2.2.35", + "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz", + "integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg==" + }, "node_modules/@types/mdast": { "version": "3.0.10", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz", @@ -1844,6 +1911,14 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.9.tgz", "integrity": "sha512-wIvGxLfgpVDSAMH5utdL9Ngm5Owu0VsGmldro3ORLXV8CShrL8awVj06NuEXFQ5xyaYfdca7Sgbk/50Ri1GdPg==", "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.2.tgz", + "integrity": "sha512-KibDWL6nshuOJ0fu8ll7QnV/LVTo3PzQ9aCPnRUYPfX7eZohHwLIdNHj7pftanREzHNP4/nJa8oeM73uSiavMQ==", "dependencies": { "@types/react": "*" } @@ -1857,6 +1932,11 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" + }, + "node_modules/@types/warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz", + "integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI=" }, "node_modules/@zeit/schemas": { "version": "2.6.0", @@ -2946,6 +3026,11 @@ "engines": { "node": ">=0.10.0" } + }, + "node_modules/classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" }, "node_modules/cli-boxes": { "version": "1.0.0", @@ -3839,6 +3924,14 @@ "dev": true, "engines": { "node": ">= 0.6" + } + }, + "node_modules/dequal": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz", + "integrity": "sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==", + "engines": { + "node": ">=6" } }, "node_modules/des.js": { @@ -3873,6 +3966,15 @@ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } }, "node_modules/dom-serializer": { "version": "0.2.2", @@ -5410,6 +5512,14 @@ }, "engines": { "node": ">= 0.4" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" } }, "node_modules/is-absolute-url": { @@ -8490,6 +8600,18 @@ "react-is": "^16.8.1" } }, + "node_modules/prop-types-extra": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", + "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", + "dependencies": { + "react-is": "^16.3.2", + "warning": "^4.0.0" + }, + "peerDependencies": { + "react": ">=0.14.0" + } + }, "node_modules/property-information": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.0.1.tgz", @@ -8706,6 +8828,34 @@ }, "engines": { "node": ">=0.10.0" + } + }, + "node_modules/react-bootstrap": { + "version": "2.0.0-beta.6", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.0.0-beta.6.tgz", + "integrity": "sha512-eHEkmESWYYgNLjqbz31G696Eytu+6GeF8CPHQ8t9Se12dUEej8OjBakyQP0OGms9yy1ZZeLG/Fvuo7VxiwMcuQ==", + "dependencies": { + "@babel/runtime": "^7.14.0", + "@restart/context": "^2.1.4", + "@restart/hooks": "^0.3.26", + "@restart/ui": "^0.2.1", + "@types/invariant": "^2.2.33", + "@types/prop-types": "^15.7.3", + "@types/react": ">=16.14.8", + "@types/react-transition-group": "^4.4.1", + "@types/warning": "^3.0.0", + "classnames": "^2.3.1", + "dom-helpers": "^5.2.1", + "invariant": "^2.2.4", + "prop-types": "^15.7.2", + "prop-types-extra": "^1.1.0", + "react-transition-group": "^4.4.1", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" } }, "node_modules/react-bootstrap-icons": { @@ -8736,6 +8886,11 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, "node_modules/react-markdown": { "version": "7.0.1", @@ -8820,6 +8975,21 @@ }, "peerDependencies": { "react": ">= 0.14.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", + "integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" } }, "node_modules/readable-stream": { @@ -10742,6 +10912,20 @@ }, "funding": { "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/uncontrollable": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "dependencies": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0" } }, "node_modules/uncss": { @@ -11304,6 +11488,14 @@ "domexception": "^1.0.1", "webidl-conversions": "^4.0.2", "xml-name-validator": "^3.0.0" + } + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" } }, "node_modules/wcwidth": { @@ -12675,9 +12867,57 @@ "@popperjs/core": { "version": "2.10.1", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.1.tgz", - "integrity": "sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw==", - "peer": true + "integrity": "sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw==" + }, + "@react-aria/ssr": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.1.0.tgz", + "integrity": "sha512-RxqQKmE8sO7TGdrcSlHTcVzMP450hqowtBSd2bBS9oPlcokVkaGq28c3Rwa8ty5ctw4EBCjXqjP7xdcKMGDzug==", + "requires": { + "@babel/runtime": "^7.6.2" + } }, + "@restart/context": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz", + "integrity": "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q==", + "requires": {} + }, + "@restart/hooks": { + "version": "0.3.27", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.27.tgz", + "integrity": "sha512-s984xV/EapUIfkjlf8wz9weP2O9TNKR96C68FfMEy2bE69+H4cNv3RD4Mf97lW7Htt7PjZrYTjSC8f3SB9VCXw==", + "requires": { + "dequal": "^2.0.2" + } + }, + "@restart/ui": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-0.2.2.tgz", + "integrity": "sha512-PgNkiyOaWwx8ttQ45KNABXU3780fB/UxNFxcsCpC4RRAlaByZHHbNLOKfhuFs+ZUU0uLxEH9wYQEhDAZc6ajkA==", + "requires": { + "@babel/runtime": "^7.13.16", + "@popperjs/core": "^2.9.2", + "@react-aria/ssr": "^3.0.1", + "@restart/hooks": "^0.4.0", + "@types/warning": "^3.0.0", + "dequal": "^2.0.2", + "dom-helpers": "^5.2.0", + "prop-types": "^15.7.2", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + }, + "dependencies": { + "@restart/hooks": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.0.tgz", + "integrity": "sha512-+RenTVobiCHPjUTbhQDV8m0PU1xEWqgloMIIOlf86oKnfghKR/l4tKto7TH543shEQZZa7ARSMTvT0cXN9u8+g==", + "requires": { + "dequal": "^2.0.2" + } + } + } + }, "@types/debug": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", @@ -12694,6 +12934,11 @@ "@types/unist": "*" } }, + "@types/invariant": { + "version": "2.2.35", + "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz", + "integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg==" + }, "@types/mdast": { "version": "3.0.10", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz", @@ -12738,6 +12983,14 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.9.tgz", "integrity": "sha512-wIvGxLfgpVDSAMH5utdL9Ngm5Owu0VsGmldro3ORLXV8CShrL8awVj06NuEXFQ5xyaYfdca7Sgbk/50Ri1GdPg==", "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/react-transition-group": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.2.tgz", + "integrity": "sha512-KibDWL6nshuOJ0fu8ll7QnV/LVTo3PzQ9aCPnRUYPfX7eZohHwLIdNHj7pftanREzHNP4/nJa8oeM73uSiavMQ==", "requires": { "@types/react": "*" } @@ -12751,6 +13004,11 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" + }, + "@types/warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz", + "integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI=" }, "@zeit/schemas": { "version": "2.6.0", @@ -13632,6 +13890,11 @@ } } } + }, + "classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" }, "cli-boxes": { "version": "1.0.0", @@ -14371,6 +14634,11 @@ "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", "dev": true }, + "dequal": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz", + "integrity": "sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==" + }, "des.js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", @@ -14404,6 +14672,15 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true } + } + }, + "dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" } }, "dom-serializer": { @@ -15618,6 +15895,14 @@ "get-intrinsic": "^1.1.0", "has": "^1.0.3", "side-channel": "^1.0.4" + } + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "requires": { + "loose-envify": "^1.0.0" } }, "is-absolute-url": { @@ -17940,6 +18225,15 @@ "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.8.1" + } + }, + "prop-types-extra": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", + "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", + "requires": { + "react-is": "^16.3.2", + "warning": "^4.0.0" } }, "property-information": { @@ -18116,6 +18410,30 @@ "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" + } + }, + "react-bootstrap": { + "version": "2.0.0-beta.6", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.0.0-beta.6.tgz", + "integrity": "sha512-eHEkmESWYYgNLjqbz31G696Eytu+6GeF8CPHQ8t9Se12dUEej8OjBakyQP0OGms9yy1ZZeLG/Fvuo7VxiwMcuQ==", + "requires": { + "@babel/runtime": "^7.14.0", + "@restart/context": "^2.1.4", + "@restart/hooks": "^0.3.26", + "@restart/ui": "^0.2.1", + "@types/invariant": "^2.2.33", + "@types/prop-types": "^15.7.3", + "@types/react": ">=16.14.8", + "@types/react-transition-group": "^4.4.1", + "@types/warning": "^3.0.0", + "classnames": "^2.3.1", + "dom-helpers": "^5.2.1", + "invariant": "^2.2.4", + "prop-types": "^15.7.2", + "prop-types-extra": "^1.1.0", + "react-transition-group": "^4.4.1", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" } }, "react-bootstrap-icons": { @@ -18141,6 +18459,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, "react-markdown": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-7.0.1.tgz", @@ -18209,6 +18532,17 @@ "lowlight": "^1.17.0", "prismjs": "^1.22.0", "refractor": "^3.2.0" + } + }, + "react-transition-group": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", + "integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" } }, "readable-stream": { @@ -19761,6 +20095,17 @@ "has-bigints": "^1.0.1", "has-symbols": "^1.0.2", "which-boxed-primitive": "^1.0.2" + } + }, + "uncontrollable": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "requires": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" } }, "uncss": { @@ -20200,6 +20545,14 @@ "domexception": "^1.0.1", "webidl-conversions": "^4.0.2", "xml-name-validator": "^3.0.0" + } + }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" } }, "wcwidth": { diff --git a/chartered-frontend/package.json b/chartered-frontend/package.json index c51949e..b938b02 100644 --- a/chartered-frontend/package.json +++ a/chartered-frontend/package.json @@ -22,6 +22,7 @@ "dependencies": { "bootstrap": "^5.1.1", "react": "^17.0.2", + "react-bootstrap": "^2.0.0-beta.6", "react-bootstrap-icons": "^1.5.0", "react-dom": "^17.0.2", "react-markdown": "^7.0.1", diff --git a/chartered-db/src/lib.rs b/chartered-db/src/lib.rs index d81889b..37d3bf7 100644 --- a/chartered-db/src/lib.rs +++ a/chartered-db/src/lib.rs @@ -59,6 +59,8 @@ Query(#[from] diesel::result::Error), /// Failed to complete query task: `{0}` TaskJoin(#[from] tokio::task::JoinError), + /// Key parse failure: `{0}` + KeyParse(#[from] thrussh_keys::Error), } diesel_infix_operator!(BitwiseAnd, " & ", diesel::sql_types::Integer); diff --git a/chartered-db/src/users.rs b/chartered-db/src/users.rs index 70e3b95..f0e6c40 100644 --- a/chartered-db/src/users.rs +++ a/chartered-db/src/users.rs @@ -1,10 +1,11 @@ use super::{ schema::{user_api_keys, user_crate_permissions, user_ssh_keys, users}, ConnectionPool, Result, }; use diesel::{insert_into, prelude::*, Associations, Identifiable, Queryable}; use rand::{thread_rng, Rng}; -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; +use thrussh_keys::PublicKeyBase64; #[derive(Identifiable, Queryable, Associations, PartialEq, Eq, Hash, Debug)] pub struct User { @@ -60,8 +61,6 @@ ) -> Result> { use crate::schema::user_ssh_keys::dsl::ssh_key; - eprintln!("ssh key: {:x?}", given_ssh_key); - tokio::task::spawn_blocking(move || { let conn = conn.get()?; @@ -71,6 +70,93 @@ .select((user_ssh_keys::all_columns, users::all_columns)) .get_result(&conn) .optional()?) + }) + .await? + } + + /// Parses an ssh key from its `ssh-add -L` format (`ssh-ed25519 AAAAC3N...`) and + /// inserts it to the database for the user. + pub async fn insert_ssh_key( + self: Arc, + conn: ConnectionPool, + ssh_key: &str, + ) -> Result<()> { + let mut split = ssh_key.split_whitespace(); + + let key = match (split.next(), split.next()) { + (Some(_), Some(key)) => key, + (Some(key), None) => key, + _ => return Err(thrussh_keys::Error::CouldNotReadKey.into()), + }; + + let parsed_key = thrussh_keys::parse_public_key_base64(key)?; + + tokio::task::spawn_blocking(move || { + use crate::schema::user_ssh_keys::dsl::{ssh_key, user_id}; + + let conn = conn.get()?; + + insert_into(crate::schema::user_ssh_keys::dsl::user_ssh_keys) + .values(( + ssh_key.eq(parsed_key.public_key_bytes()), + user_id.eq(self.id), + )) + .execute(&conn)?; + + Ok(()) + }) + .await? + } + + pub async fn delete_user_ssh_key_by_id( + self: Arc, + conn: ConnectionPool, + ssh_key_id: i32, + ) -> Result { + use crate::schema::user_ssh_keys::dsl::{id, user_id, user_ssh_keys}; + + tokio::task::spawn_blocking(move || { + let conn = conn.get()?; + + let rows = diesel::delete( + user_ssh_keys + .filter(user_id.eq(self.id)) + .filter(id.eq(ssh_key_id)), + ) + .execute(&conn)?; + + Ok(rows > 0) + }) + .await? + } + + /// Get all the SSH keys for the user, returned as `id:fingerprint`. + pub async fn list_ssh_keys( + self: Arc, + conn: ConnectionPool, + ) -> Result> { + tokio::task::spawn_blocking(move || { + use crate::schema::user_ssh_keys::dsl::user_id; + + let conn = conn.get()?; + + let selected: Vec<(i32, Vec)> = crate::schema::user_ssh_keys::table + .filter(user_id.eq(self.id)) + .inner_join(users::table) + .select((user_ssh_keys::id, user_ssh_keys::ssh_key)) + .load(&conn)?; + + Ok(selected + .into_iter() + .map(|(id, key)| { + ( + id, + thrussh_keys::key::parse_public_key(&key) + .map(|v| v.fingerprint()) + .unwrap_or_else(|e| format!("INVALID: {}", e)), + ) + }) + .collect()) }) .await? } diff --git a/chartered-frontend/src/index.tsx b/chartered-frontend/src/index.tsx index 0567dd3..b8aaad5 100644 --- a/chartered-frontend/src/index.tsx +++ a/chartered-frontend/src/index.tsx @@ -17,6 +17,8 @@ import Login from "./pages/Login"; import Dashboard from "./pages/Dashboard"; import SingleCrate from "./pages/SingleCrate"; +import ListSshKeys from "./pages/ssh-keys/ListSshKeys"; +import AddSshKeys from "./pages/ssh-keys/AddSshKeys"; function App() { return ( @@ -28,6 +30,8 @@ } /> } /> } /> + } /> + } /> diff --git a/chartered-web/src/main.rs b/chartered-web/src/main.rs index f9adac0..ae2c61b 100644 --- a/chartered-web/src/main.rs +++ a/chartered-web/src/main.rs @@ -5,16 +5,14 @@ mod middleware; use axum::{ - body::Body, handler::{delete, get, post, put}, - http::header::ACCESS_CONTROL_ALLOW_ORIGIN, http::Method, AddExtensionLayer, Router, }; use tower::ServiceBuilder; use tower_http::cors::{Any, CorsLayer}; -use tower_http::set_header::SetResponseHeaderLayer; + #[allow(clippy::unused_async)] async fn hello_world() -> &'static str { "hello, world!" @@ -71,9 +69,11 @@ let web_unauthenticated = axum_box_after_every_route!(Router::new().route("/login", post(endpoints::web_api::login))); - let web_authenticated = axum_box_after_every_route!( - Router::new().route("/crates/:crate", get(endpoints::web_api::crate_info)) - ) + let web_authenticated = axum_box_after_every_route!(Router::new() + .route("/crates/:crate", get(endpoints::web_api::crate_info)) + .route("/ssh-key", get(endpoints::web_api::get_ssh_keys)) + .route("/ssh-key", put(endpoints::web_api::add_ssh_key)) + .route("/ssh-key/:id", delete(endpoints::web_api::delete_ssh_key))) .layer( ServiceBuilder::new() .layer_fn(middleware::auth::AuthMiddleware) @@ -93,7 +93,13 @@ // TODO!!! .layer( CorsLayer::new() - .allow_methods(vec![Method::GET, Method::POST, Method::OPTIONS]) + .allow_methods(vec![ + Method::GET, + Method::POST, + Method::DELETE, + Method::PUT, + Method::OPTIONS, + ]) .allow_origin(Any) .allow_credentials(false), ) diff --git a/chartered-frontend/src/sections/Nav.tsx b/chartered-frontend/src/sections/Nav.tsx index 8002bc4..210efa2 100644 --- a/chartered-frontend/src/sections/Nav.tsx +++ a/chartered-frontend/src/sections/Nav.tsx @@ -26,23 +26,7 @@ Home
  • - Link -
  • -
  • - - - -
  • - -
  • - Disabled + SSH Keys
  • diff --git a/chartered-frontend/src/pages/ssh-keys/AddSshKeys.tsx b/chartered-frontend/src/pages/ssh-keys/AddSshKeys.tsx new file mode 100644 index 0000000..adb7a5e 100644 --- /dev/null +++ a/chartered-frontend/src/pages/ssh-keys/AddSshKeys.tsx @@ -1,0 +1,85 @@ +import React = require("react"); +import { useState, useEffect } from 'react'; +import { Link, useHistory } from 'react-router-dom'; + +import Nav from "../../sections/Nav"; +import { useAuth } from "../../useAuth"; +import { authenticatedEndpoint } from "../../util"; + +import { Plus } from "react-bootstrap-icons"; + +export default function ListSshKeys() { + const auth = useAuth(); + const router = useHistory(); + + const [sshKey, setSshKey] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const submitSshKey = async (evt) => { + evt.preventDefault(); + + setError(""); + setLoading(true); + + try { + let res = await fetch(authenticatedEndpoint(auth, 'ssh-key'), { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ key: sshKey }), + }); + let json = await res.json(); + + if (json.error) { + throw new Error(json.error); + } + + setSshKey(""); + router.push("/ssh-keys/list"); + } catch (e) { + setError(e.message); + } finally { + setLoading(false); + } + }; + + return ( +
    +