Adds AAD auth to laboratory and CLI

This commit is contained in:
Noel Bundick 2020-07-31 19:02:46 +00:00
Родитель 244416de22
Коммит ea12c1c5f2
11 изменённых файлов: 710 добавлений и 110 удалений

Просмотреть файл

@ -30,3 +30,10 @@ AZURE_CLIENT_ID=00000000-0000-0000-0000-000000000000
# Functional test variables
TEST_QUEUE_SERVICE_URL=https://mystorage.queue.core.windows.net
TEST_BASE_VOLUME_PATH=/tmp/sds/volumes/
# Laboratory AAD auth
# If AUTH_MODE is not set, auth is disabled
AUTH_MODE=aad
AUTH_TENANT_ID=00000000-0000-0000-0000-000000000000
AUTH_LABORATORY_CLIENT_ID=00000000-0000-0000-0000-000000000000
AUTH_CLI_CLIENT_ID=00000000-0000-0000-0000-000000000000

300
package-lock.json сгенерированный
Просмотреть файл

@ -152,6 +152,54 @@
"adal-node": "^0.1.28"
}
},
"@azure/msal-common": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-1.0.0.tgz",
"integrity": "sha512-l/+1Z9kQWLAlMwJ/c3MGhy4ujtEAK/3CMUaUXHvjUIsQknLFRb9+b3id5YSuToPfAvdUkAQGDZiQXosv1I+eLA==",
"requires": {
"debug": "^4.1.1"
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"@azure/msal-node": {
"version": "1.0.0-alpha.3",
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.0.0-alpha.3.tgz",
"integrity": "sha512-DVV0VqCscFxjLjg5+DH0RXzToEdtUx7syl/C35ePuBDVeSHQ/mmD3v0yacTg7MN4biU1qYnv/xDunLLi5pWCVA==",
"requires": {
"@azure/msal-common": "^1.0.0",
"axios": "^0.19.2",
"debug": "^4.1.1"
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"@azure/storage-queue": {
"version": "12.0.4",
"resolved": "https://registry.npmjs.org/@azure/storage-queue/-/storage-queue-12.0.4.tgz",
@ -446,6 +494,25 @@
}
}
},
"@types/passport": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.4.tgz",
"integrity": "sha512-h5OfAbfBBYSzjeU0GTuuqYEk9McTgWeGQql9g3gUw2/NNCfD7VgExVRYJVVeU13Twn202Mvk9BT0bUrl30sEgA==",
"dev": true,
"requires": {
"@types/express": "*"
}
},
"@types/passport-azure-ad": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/@types/passport-azure-ad/-/passport-azure-ad-4.0.7.tgz",
"integrity": "sha512-5O0OspIhlOyQA20kZoEW+8/8zmFa1StkzshZ/+QDapcmfb3AJb3N9wUq3trpWRHwpXUSyZMsqTvc7bYtBIRcPg==",
"dev": true,
"requires": {
"@types/express": "*",
"@types/passport": "*"
}
},
"@types/qs": {
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.1.tgz",
@ -814,6 +881,16 @@
"safer-buffer": "~2.1.0"
}
},
"asn1.js": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
"integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
"requires": {
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0"
}
},
"assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
@ -863,6 +940,11 @@
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
},
"base64url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A=="
},
"bcp47": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/bcp47/-/bcp47-1.1.2.tgz",
@ -914,6 +996,11 @@
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
},
"bn.js": {
"version": "4.11.9",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw=="
},
"body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
@ -1009,6 +1096,11 @@
"fill-range": "^7.0.1"
}
},
"brorand": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
},
"browser-stdout": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
@ -1046,11 +1138,48 @@
"integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
"dev": true
},
"bunyan": {
"version": "1.8.14",
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.14.tgz",
"integrity": "sha512-LlahJUxXzZLuw/hetUQJmRgZ1LF6+cr5TPpRj6jf327AsiIq2jhYEH4oqUUkVKTor+9w2BT3oxVwhzE5lw9tcg==",
"requires": {
"dtrace-provider": "~0.8",
"moment": "^2.19.3",
"mv": "~2",
"safe-json-stringify": "~1"
}
},
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"cache-manager": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-2.11.1.tgz",
"integrity": "sha512-XhUuc9eYwkzpK89iNewFwtvcDYMUsvtwzHeyEOPJna/WsVsXcrzsA1ft2M0QqPNunEzLhNCYPo05tEfG+YuNow==",
"requires": {
"async": "1.5.2",
"lodash.clonedeep": "4.5.0",
"lru-cache": "4.0.0"
},
"dependencies": {
"async": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo="
},
"lru-cache": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.0.tgz",
"integrity": "sha1-tcvwFVbBaWb+vlTO7A+03JDfbCg=",
"requires": {
"pseudomap": "^1.0.1",
"yallist": "^2.0.0"
}
}
}
},
"cacheable-request": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
@ -1759,6 +1888,15 @@
"resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.2.tgz",
"integrity": "sha512-fmrwR04lsniq/uSr8yikThDTrM7epXHBAAjH9TbeH3rEA8tdCO7mRzB9hdmdGyJCxF8KERo9CITcm3kGuoyMhg=="
},
"dtrace-provider": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz",
"integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==",
"optional": true,
"requires": {
"nan": "^2.14.0"
}
},
"duplexer3": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
@ -1792,6 +1930,20 @@
"resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz",
"integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA=="
},
"elliptic": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
"requires": {
"bn.js": "^4.4.0",
"brorand": "^1.0.1",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.0"
}
},
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@ -2425,12 +2577,31 @@
"integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==",
"dev": true
},
"hash.js": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"requires": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
}
},
"he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true
},
"hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
"integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
"requires": {
"hash.js": "^1.0.3",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.1"
}
},
"hosted-git-info": {
"version": "2.8.6",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.6.tgz",
@ -2851,6 +3022,16 @@
"safe-buffer": "^5.0.1"
}
},
"jwk-to-pem": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-1.2.6.tgz",
"integrity": "sha1-1QfOzkAInFJI4J7GgmaiAwqcYyU=",
"requires": {
"asn1.js": "^4.5.2",
"elliptic": "^6.2.3",
"safe-buffer": "^5.0.1"
}
},
"jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
@ -2913,6 +3094,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
},
"lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
},
"lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
@ -3063,6 +3249,16 @@
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
"dev": true
},
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
},
"minimalistic-crypto-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@ -3313,6 +3509,41 @@
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
"dev": true
},
"mv": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
"integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=",
"optional": true,
"requires": {
"mkdirp": "~0.5.1",
"ncp": "~2.0.0",
"rimraf": "~2.4.0"
},
"dependencies": {
"glob": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
"integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
"optional": true,
"requires": {
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "2 || 3",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"rimraf": {
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
"integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=",
"optional": true,
"requires": {
"glob": "^6.0.1"
}
}
}
},
"nan": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz",
@ -3326,8 +3557,7 @@
"ncp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
"integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=",
"dev": true
"integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M="
},
"needle": {
"version": "2.4.1",
@ -3584,6 +3814,11 @@
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
},
"oauth": {
"version": "0.9.14",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.14.tgz",
"integrity": "sha1-xXSIg6QLU94wrenKvyEAQUuKCXE="
},
"oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
@ -3760,6 +3995,45 @@
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"passport": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.3.2.tgz",
"integrity": "sha1-ndAJ+RXo/glbASSgG4+C2gdRAQI=",
"requires": {
"passport-strategy": "1.x.x",
"pause": "0.0.1"
}
},
"passport-azure-ad": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/passport-azure-ad/-/passport-azure-ad-4.2.1.tgz",
"integrity": "sha512-pyaGhuvxHTUu/jrCCBOtR3GoSC12+u7B/iEQVK7z+JdDQZE/I+3oMgN1Ls4umnb5TfPuVyM76kvjqwB9kAjBgw==",
"requires": {
"async": "^1.5.2",
"base64url": "^3.0.0",
"bunyan": "^1.8.0",
"cache-manager": "^2.0.0",
"jwk-to-pem": "^1.2.6",
"jws": "^3.1.3",
"lodash": "^4.11.2",
"oauth": "0.9.14",
"passport": "^0.3.2",
"request": "^2.72.0",
"valid-url": "^1.0.6"
},
"dependencies": {
"async": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo="
}
}
},
"passport-strategy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
"integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ="
},
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
@ -3812,6 +4086,11 @@
"integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=",
"dev": true
},
"pause": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10="
},
"performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
@ -3869,8 +4148,7 @@
"pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
"integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
"dev": true
"integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM="
},
"psl": {
"version": "1.7.0",
@ -4196,6 +4474,12 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
"integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
},
"safe-json-stringify": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz",
"integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==",
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@ -5505,6 +5789,11 @@
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
},
"valid-url": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz",
"integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA="
},
"validate-npm-package-license": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
@ -5739,8 +6028,7 @@
"yallist": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
"dev": true
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
},
"yamljs": {
"version": "0.3.0",

Просмотреть файл

@ -28,6 +28,7 @@
},
"dependencies": {
"@azure/identity": "^1.0.3",
"@azure/msal-node": "^1.0.0-alpha.3",
"@azure/storage-queue": "^12.0.4",
"axios": "^0.19.2",
"body-parser": "^1.19.0",
@ -40,6 +41,7 @@
"io-ts": "^2.2.2",
"js-yaml": "^3.13.1",
"luxon": "^1.24.1",
"passport-azure-ad": "^4.2.1",
"reflect-metadata": "^0.1.13",
"sequelize": "^5.21.7",
"sequelize-typescript": "^1.1.0",
@ -64,6 +66,7 @@
"@types/mocha": "^7.0.2",
"@types/nock": "^11.1.0",
"@types/node": "^12.12.37",
"@types/passport-azure-ad": "^4.0.7",
"@types/request-promise": "^4.1.46",
"@types/sinon": "^7.5.2",
"@types/sinonjs__fake-timers": "^6.0.1",

155
src/cli/conn.ts Normal file
Просмотреть файл

@ -0,0 +1,155 @@
import * as msal from '@azure/msal-node';
import { promises as fs } from 'fs';
import * as path from 'path';
import * as os from 'os';
import * as yaml from 'js-yaml';
import * as t from 'io-ts';
import * as url from 'url';
import {
IllegalOperationError,
LaboratoryClient,
validate,
ClientConnectionInfoType,
} from '../laboratory';
import { lab } from '../../test/unit/laboratory/shared';
// global client
let client: LaboratoryClient | undefined;
const configDir = path.join(os.homedir(), '.sds');
const connFilePath = 'sds.yaml';
const tokenCachePath = 'accessTokens.json';
const cachePlugin = {
async readFromStorage() {
return readConfig(tokenCachePath);
},
async writeToStorage(getMergedState: (oldState: string) => string) {
let oldFile = '';
try {
oldFile = await readConfig(tokenCachePath);
} finally {
const mergedState = getMergedState(oldFile);
await writeConfig(tokenCachePath, mergedState);
}
},
};
export async function initConnection(host: string) {
const labUrl = url.parse(host);
const endpoint = labUrl.href;
const connectionInfo = await new LaboratoryClient(
endpoint
).negotiateConnection();
const config: IConnectConfiguration = {
endpoint,
...connectionInfo,
};
await writeConfig(connFilePath, yaml.safeDump(config));
const newClient = buildClient(config);
await newClient.validateConnection();
client = newClient;
}
export async function getLabClient(): Promise<LaboratoryClient> {
try {
if (client) {
return client;
}
const text = await readConfig(connFilePath);
const config = validate(ConnectConfigurationType, yaml.safeLoad(text));
client = buildClient(config);
return client;
} catch {
throw new IllegalOperationError(
'No laboratory connection. Use the "connect" command to specify a laboratory.'
);
}
}
// tslint:disable-next-line:variable-name
const ConnectConfigurationType = t.intersection([
t.type({
endpoint: t.string,
}),
ClientConnectionInfoType,
t.partial({
// from msal-common/AccountInfo
account: t.type({
homeAccountId: t.string,
environment: t.string,
tenantId: t.string,
username: t.string,
}),
}),
]);
type IConnectConfiguration = t.TypeOf<typeof ConnectConfigurationType>;
function acquireAADAccessToken(config: IConnectConfiguration) {
if (config.type !== 'aad') {
throw new Error(
'Cannot retrieve an AAD access token for a non-AAD connection'
);
}
return async () => {
const pca = new msal.PublicClientApplication({
auth: {
clientId: config.clientId,
authority: config.authority,
},
cache: {
cachePlugin,
},
});
const cache = pca.getTokenCache();
try {
await cache.readFromPersistence();
const silentResult = await pca.acquireTokenSilent({
account: config.account!,
scopes: config.scopes,
});
cache.writeToPersistence();
return silentResult.accessToken;
} catch (e) {
const deviceCodeResult = await pca.acquireTokenByDeviceCode({
deviceCodeCallback: response => console.log(response.message),
scopes: config.scopes,
});
config.account = deviceCodeResult.account;
await writeConfig(connFilePath, yaml.safeDump(config));
cache.writeToPersistence();
return deviceCodeResult.accessToken;
}
};
}
function buildClient(config: IConnectConfiguration): LaboratoryClient {
const tokenRetriever =
config.type === 'aad' ? acquireAADAccessToken(config) : undefined;
return new LaboratoryClient(config.endpoint, tokenRetriever);
}
async function readConfig(filePath: string): Promise<string> {
const fullPath = path.join(configDir, filePath);
try {
return await fs.readFile(fullPath, 'utf8');
} catch (e) {
const err = e as NodeJS.ErrnoException;
if (err.code === 'ENOENT') {
return '';
}
throw e;
}
}
async function writeConfig(filePath: string, data: string): Promise<void> {
const fullPath = path.join(configDir, filePath);
await fs.mkdir(configDir, { recursive: true });
await fs.writeFile(fullPath, data);
}

Просмотреть файл

@ -1,12 +1,9 @@
import { Command } from 'commander';
import * as fs from 'fs';
import { Decoder } from 'io-ts';
import * as t from 'io-ts';
import * as yaml from 'js-yaml';
import { DateTime } from 'luxon';
import * as os from 'os';
import * as path from 'path';
import * as url from 'url';
import {
BenchmarkType,
@ -17,11 +14,11 @@ import {
IllegalOperationError,
IRun,
ISuite,
LaboratoryClient,
SuiteType,
validate,
} from '../laboratory';
import { initConnection, getLabClient } from './conn';
import { decodeError } from './decode_error';
import { configureDemo } from './demo';
import { formatChoices, formatTable, Alignment } from './formatting';
@ -149,21 +146,11 @@ function examples(argv: string[]) {
///////////////////////////////////////////////////////////////////////////////
async function connect(host: string) {
if (host === undefined) {
if (connection) {
console.log(`Connected to ${connection!.endpoint}.`);
} else {
console.log(
'No laboratory connection. Use the "connect" command to specify a laboratory.'
);
}
} else {
const labUrl = url.parse(host);
const endpoint = labUrl.href;
const config = yaml.safeDump({ endpoint });
fs.writeFileSync(sdsFile, config);
tryInitializeConnection();
console.log(`Connected to ${endpoint}.`);
throw new Error('You must specify a host.');
}
await initConnection(host);
console.log(`Connected to ${host}.`);
}
///////////////////////////////////////////////////////////////////////////////
@ -189,7 +176,7 @@ async function createHelper<T>(ops: ISpecOps<T>, specFile: string) {
//
///////////////////////////////////////////////////////////////////////////////
async function demo() {
configureDemo(getLab());
configureDemo(await getLabClient());
}
///////////////////////////////////////////////////////////////////////////////
@ -265,7 +252,7 @@ async function listHelper<T extends IEntityBase>(
//
///////////////////////////////////////////////////////////////////////////////
async function results(benchmark: string, suite: string) {
const results = await getLab().allRunResults(benchmark, suite);
const results = await (await getLabClient()).allRunResults(benchmark, suite);
if (results.length === 0) {
console.log(`No matching results`);
@ -320,7 +307,10 @@ async function results(benchmark: string, suite: string) {
//
///////////////////////////////////////////////////////////////////////////////
async function run(candidate: string, suite: string) {
const run = await getLab().createRunRequest({ candidate, suite });
const run = await (await getLabClient()).createRunRequest({
candidate,
suite,
});
console.log(`Scheduling run ${run.name}`);
}
@ -371,29 +361,30 @@ const benchmarkOps: ISpecOps<IBenchmark> = {
name: () => 'benchmark',
load: (specFile: string) => load(BenchmarkType, specFile),
format: (spec: IBenchmark) => formatSpec(spec),
all: () => getLab().allBenchmarks(),
one: (name: string) => getLab().oneBenchmark(name),
upsert: (spec: IBenchmark, name?: string) =>
getLab().upsertBenchmark(spec, name),
all: async () => (await getLabClient()).allBenchmarks(),
one: async (name: string) => (await getLabClient()).oneBenchmark(name),
upsert: async (spec: IBenchmark, name?: string) =>
(await getLabClient()).upsertBenchmark(spec, name),
};
const candidateOps: ISpecOps<ICandidate> = {
name: () => 'candidate',
load: (specFile: string) => load(CandidateType, specFile),
format: (spec: ICandidate) => formatSpec(spec),
all: () => getLab().allCandidates(),
one: (name: string) => getLab().oneCandidate(name),
upsert: (spec: ICandidate, name?: string) =>
getLab().upsertCandidate(spec, name),
all: async () => (await getLabClient()).allCandidates(),
one: async (name: string) => (await getLabClient()).oneCandidate(name),
upsert: async (spec: ICandidate, name?: string) =>
(await getLabClient()).upsertCandidate(spec, name),
};
const suiteOps: ISpecOps<ISuite> = {
name: () => 'suite',
load: (specFile: string) => load(SuiteType, specFile),
format: (spec: ISuite) => formatSpec(spec),
all: () => getLab().allSuites(),
one: (name: string) => getLab().oneSuite(name),
upsert: (spec: ISuite, name?: string) => getLab().upsertSuite(spec, name),
all: async () => (await getLabClient()).allSuites(),
one: async (name: string) => (await getLabClient()).oneSuite(name),
upsert: async (spec: ISuite, name?: string) =>
(await getLabClient()).upsertSuite(spec, name),
};
const runOps: ISpecOps<IRun> = {
@ -402,8 +393,8 @@ const runOps: ISpecOps<IRun> = {
throw new IllegalOperationError(`Load operation not supported for IRun.`);
},
format: (spec: IRun) => formatSpec(spec),
all: () => getLab().allRuns(),
one: (name: string) => getLab().oneRun(name),
all: async () => (await getLabClient()).allRuns(),
one: async (name: string) => (await getLabClient()).oneRun(name),
upsert: (spec: IRun, name?: string) => {
throw new IllegalOperationError(`Upsert operation not supported for IRun.`);
},
@ -455,47 +446,4 @@ function formatSpec(spec: object) {
return yaml.safeDump(spec, {});
}
///////////////////////////////////////////////////////////////////////////////
//
// Connection management
//
///////////////////////////////////////////////////////////////////////////////
// tslint:disable-next-line:variable-name
const ConnectConfigurationType = t.type({
endpoint: t.string,
});
export type IConnectConfiguration = t.TypeOf<typeof ConnectConfigurationType>;
const sdsFile = path.join(os.homedir(), '.sds');
let connection: IConnectConfiguration | undefined;
let lab: LaboratoryClient | undefined;
function getLab(): LaboratoryClient {
if (connection === undefined || lab === undefined) {
tryInitializeConnection();
}
if (connection === undefined || lab === undefined) {
const message =
'No laboratory connection. Use the "connect" command to specify a laboratory.';
throw new IllegalOperationError(message);
}
return lab;
}
function tryInitializeConnection() {
try {
const yamlText = fs.readFileSync(sdsFile, 'utf8');
const root = yaml.safeLoad(yamlText);
connection = validate(ConnectConfigurationType, root);
lab = new LaboratoryClient(connection.endpoint);
} catch (e) {
const err = e as NodeJS.ErrnoException;
if (err.code !== 'ENOENT') {
const message = `Invalid ~/.sds file: "${err.message}"`;
console.log(message);
}
}
}
main(process.argv);

Просмотреть файл

@ -20,6 +20,14 @@ import {
AzureStorageQueueConfiguration,
} from './queue';
import {
AADConfiguration,
AuthConfiguration,
LaboratoryConfiguration,
AuthMode,
NoAuthConfiguration,
} from './laboratory/server/configuration';
class AzureCredential {
private static instance: TokenCredential;
@ -114,7 +122,45 @@ export function ParseDatabaseConfiguration(): DatabaseConfiguration {
}
}
export function ParseLaboratoryConfiguration() {
function ParseAuthConfiguration(): AuthConfiguration {
const authMode = env.get('AUTH_MODE').asString();
if (authMode === 'aad') {
const tenantId = env
.get('AUTH_TENANT_ID')
.required()
.asString();
const laboratoryClientId = env
.get('AUTH_LABORATORY_CLIENT_ID')
.required()
.asString();
const cliClientId = env
.get('AUTH_CLI_CLIENT_ID')
.required()
.asString();
const scopes = env
.get('AUTH_SCOPES')
.default('laboratory')
.asArray(' ')
.map(s => `api://${laboratoryClientId}/${s}`);
// offline_access is required to use refresh tokens
scopes.push('offline_access');
const config: AADConfiguration = {
mode: AuthMode.AAD,
tenantId,
laboratoryClientId,
cliClientId,
scopes,
};
return config;
} else {
return NoAuthConfiguration;
}
}
export function ParseLaboratoryConfiguration(): LaboratoryConfiguration {
const port = env
.get('PORT')
.default(3000)
@ -138,5 +184,6 @@ export function ParseLaboratoryConfiguration() {
port,
queue: ParseQueueConfiguration(),
database: ParseDatabaseConfiguration(),
auth: ParseAuthConfiguration(),
};
}

Просмотреть файл

@ -6,8 +6,10 @@ import {
BenchmarkType,
CandidateArrayType,
CandidateType,
ClientConnectionInfoType,
IBenchmark,
ICandidate,
IClientConnectionInfo,
ILaboratory,
IllegalOperationError,
IReportRunResults,
@ -28,15 +30,48 @@ import {
validate,
} from '../logic';
const config: AxiosRequestConfig = {
// TODO: put credentials here.
};
// A TokenRetriever should return a valid OAuth2 Bearer token
type TokenRetriever = () => Promise<string>;
export class LaboratoryClient implements ILaboratory {
endpoint: string;
tokenRetriever?: TokenRetriever;
constructor(endpoint: string) {
constructor(endpoint: string, tokenRetriever?: TokenRetriever) {
this.endpoint = endpoint;
this.tokenRetriever = tokenRetriever;
}
private async getConfig(): Promise<AxiosRequestConfig> {
if (this.tokenRetriever) {
return {
headers: {
Authorization: `Bearer ${await this.tokenRetriever()}`,
},
};
}
return {};
}
/////////////////////////////////////////////////////////////////////////////
//
// Connection info
//
/////////////////////////////////////////////////////////////////////////////
async negotiateConnection(): Promise<IClientConnectionInfo> {
const url = new URL('connect', this.endpoint);
const response = await axios.get(url.toString());
const connectionInfo = validate(ClientConnectionInfoType, response.data);
return connectionInfo;
}
async validateConnection(): Promise<void> {
const url = new URL('connect/validate', this.endpoint);
const response = await axios.get(url.toString(), await this.getConfig());
if (response.status !== 200) {
throw new Error('There was something wrong with the connection');
}
}
/////////////////////////////////////////////////////////////////////////////
@ -46,7 +81,7 @@ export class LaboratoryClient implements ILaboratory {
/////////////////////////////////////////////////////////////////////////////
async allBenchmarks(): Promise<IBenchmark[]> {
const url = new URL('benchmarks', this.endpoint);
const response = await axios.get(url.toString(), config);
const response = await axios.get(url.toString(), await this.getConfig());
const benchmarks = validate(BenchmarkArrayType, response.data);
return benchmarks;
}
@ -54,7 +89,7 @@ export class LaboratoryClient implements ILaboratory {
async oneBenchmark(rawName: string): Promise<IBenchmark> {
const name = normalizeName(rawName);
const url = new URL(`benchmarks/${name}`, this.endpoint);
const response = await axios.get(url.toString(), config);
const response = await axios.get(url.toString(), await this.getConfig());
const benchmark = validate(BenchmarkType, response.data);
return benchmark;
}
@ -69,7 +104,7 @@ export class LaboratoryClient implements ILaboratory {
throw new IllegalOperationError(message);
}
const url = new URL(`benchmarks/${name}`, this.endpoint);
await axios.put(url.toString(), benchmark, config);
await axios.put(url.toString(), benchmark, await this.getConfig());
}
/////////////////////////////////////////////////////////////////////////////
@ -79,7 +114,7 @@ export class LaboratoryClient implements ILaboratory {
/////////////////////////////////////////////////////////////////////////////
async allCandidates(): Promise<ICandidate[]> {
const url = new URL('candidates', this.endpoint);
const response = await axios.get(url.toString(), config);
const response = await axios.get(url.toString(), await this.getConfig());
const candidates = validate(CandidateArrayType, response.data);
return candidates;
}
@ -87,7 +122,7 @@ export class LaboratoryClient implements ILaboratory {
async oneCandidate(rawName: string): Promise<ICandidate> {
const name = normalizeName(rawName);
const url = new URL(`candidates/${name}`, this.endpoint);
const response = await axios.get(url.toString(), config);
const response = await axios.get(url.toString(), await this.getConfig());
const candidate = validate(CandidateType, response.data);
return candidate;
}
@ -102,7 +137,7 @@ export class LaboratoryClient implements ILaboratory {
throw new IllegalOperationError(message);
}
const url = new URL(`candidates/${name}`, this.endpoint);
await axios.put(url.toString(), candidate, config);
await axios.put(url.toString(), candidate, await this.getConfig());
}
/////////////////////////////////////////////////////////////////////////////
@ -112,7 +147,7 @@ export class LaboratoryClient implements ILaboratory {
/////////////////////////////////////////////////////////////////////////////
async allSuites(): Promise<ISuite[]> {
const url = new URL('suites', this.endpoint);
const response = await axios.get(url.toString(), config);
const response = await axios.get(url.toString(), await this.getConfig());
const suites = validate(SuiteArrayType, response.data);
return suites;
}
@ -120,7 +155,7 @@ export class LaboratoryClient implements ILaboratory {
async oneSuite(rawName: string): Promise<ISuite> {
const name = normalizeName(rawName);
const url = new URL(`suites/${name}`, this.endpoint);
const response = await axios.get(url.toString(), config);
const response = await axios.get(url.toString(), await this.getConfig());
const suite = validate(SuiteType, response.data);
return suite;
}
@ -132,7 +167,7 @@ export class LaboratoryClient implements ILaboratory {
throw new IllegalOperationError(message);
}
const url = new URL(`suites/${name}`, this.endpoint);
await axios.put(url.toString(), suite, config);
await axios.put(url.toString(), suite, await this.getConfig());
}
/////////////////////////////////////////////////////////////////////////////
@ -142,7 +177,7 @@ export class LaboratoryClient implements ILaboratory {
/////////////////////////////////////////////////////////////////////////////
async allRuns(): Promise<IRun[]> {
const url = new URL('runs', this.endpoint);
const response = await axios.get(url.toString(), config);
const response = await axios.get(url.toString(), await this.getConfig());
const runs = validate(RunArrayType, response.data);
return runs;
}
@ -150,14 +185,18 @@ export class LaboratoryClient implements ILaboratory {
async oneRun(rawName: string): Promise<IRun> {
const name = normalizeRunName(rawName);
const url = new URL(`runs/${name}`, this.endpoint);
const response = await axios.get(url.toString(), config);
const response = await axios.get(url.toString(), await this.getConfig());
const run = validate(RunType, response.data);
return run;
}
async createRunRequest(spec: IRunRequest): Promise<IRun> {
const url = new URL('runs', this.endpoint);
const response = await axios.post(url.toString(), spec, config);
const response = await axios.post(
url.toString(),
spec,
await this.getConfig()
);
const run = validate(RunType, response.data);
return run;
}
@ -166,21 +205,21 @@ export class LaboratoryClient implements ILaboratory {
const name = normalizeRunName(rawName);
const url = new URL(`runs/${name}`, this.endpoint);
const body: IUpdateRunStatus = { status };
await axios.patch(url.toString(), body, config);
await axios.patch(url.toString(), body, await this.getConfig());
}
async reportRunResults(rawName: string, measures: Measures): Promise<void> {
const name = normalizeRunName(rawName);
const url = new URL(`runs/${name}/results`, this.endpoint);
const body: IReportRunResults = { measures };
await axios.post(url.toString(), body, config);
await axios.post(url.toString(), body, await this.getConfig());
}
async allRunResults(benchmark: string, suite: string): Promise<IResult[]> {
const b = normalizeName(benchmark);
const s = normalizeName(suite);
const url = new URL(`runs?benchmark=${b}&suite=${s}`, this.endpoint);
const response = await axios.get(url.toString(), config);
const response = await axios.get(url.toString(), await this.getConfig());
const results = validate(ResultArrayType, response.data);
return results;
}

Просмотреть файл

@ -27,6 +27,26 @@ const createEnum = <E>(e: any, name: string): t.Type<E> => {
return t.keyof(keys, name) as any;
};
///////////////////////////////////////////////////////////////////////////////
//
// IClientConnectionInfo
//
///////////////////////////////////////////////////////////////////////////////
// tslint:disable-next-line:variable-name
export const ClientConnectionInfoType = t.union([
t.type({
type: t.literal('aad'),
clientId: t.string,
authority: t.string,
scopes: t.array(t.string),
}),
t.type({
type: t.literal('unauthenticated'),
}),
]);
export type IClientConnectionInfo = t.TypeOf<typeof ClientConnectionInfoType>;
///////////////////////////////////////////////////////////////////////////////
//
// EntityBase

Просмотреть файл

@ -1,10 +1,12 @@
import * as express from 'express';
import * as errorhandler from 'strong-error-handler';
import * as passport from 'passport';
import { BearerStrategy } from 'passport-azure-ad';
import { IClientConnectionInfo } from '../../laboratory';
import { ILaboratory } from '../logic';
import { setErrorStatus } from './errors';
import {
createBenchmarkRouter,
createCandidateRouter,
@ -12,11 +14,70 @@ import {
createSuiteRouter,
} from './routes';
export async function createApp(lab: ILaboratory): Promise<express.Express> {
const app = express()
.use(express.json())
import {
AuthConfiguration,
AADConfiguration,
AuthMode,
NoAuthConfiguration,
} from './configuration';
// Set up application routes
function configureAADAuth(app: express.Express, config: AADConfiguration) {
passport.use(
new BearerStrategy(
{
identityMetadata: `https://login.microsoftonline.com/${config.tenantId}/v2.0/.well-known/openid-configuration`,
clientID: config.laboratoryClientId,
},
(token, done) => {
return done(null, token);
}
)
);
// unauthenticated endpoint for clients to retrieve connection info
app
.get('/connect', (req, res) => {
const connectionInfo: IClientConnectionInfo = {
type: 'aad',
clientId: config.cliClientId,
authority: `https://login.microsoftonline.com/${config.tenantId}`,
scopes: config.scopes,
};
res.json(connectionInfo);
})
// require all endpoints to be authenticated
.use(passport.initialize())
.all('*', passport.authenticate('oauth-bearer', { session: false }));
}
export async function createApp(
lab: ILaboratory,
auth: AuthConfiguration = NoAuthConfiguration
): Promise<express.Express> {
const app = express().use(express.json());
// configure authorization
switch (auth.mode) {
case AuthMode.AAD:
configureAADAuth(app, auth as AADConfiguration);
break;
case AuthMode.None:
default:
app.get('/connect', (req, res) => {
const connectionInfo: IClientConnectionInfo = {
type: 'unauthenticated',
};
res.json(connectionInfo);
});
break;
}
// Set up application routes
app
.get('/connect/validate', (req, res) => {
res.status(200).end();
})
.use(createBenchmarkRouter(lab))
.use(createCandidateRouter(lab))
.use(createRunRouter(lab))

Просмотреть файл

@ -0,0 +1,32 @@
import { QueueConfiguration } from '../../queue';
import { DatabaseConfiguration } from '../../database';
export enum AuthMode {
AAD = 'AAD',
None = 'none',
}
export interface AuthConfiguration {
mode: AuthMode;
}
export interface AADConfiguration extends AuthConfiguration {
mode: AuthMode.AAD;
tenantId: string;
laboratoryClientId: string;
cliClientId: string;
scopes: string[];
}
// tslint:disable-next-line:variable-name
export const NoAuthConfiguration = {
mode: AuthMode.None,
};
export interface LaboratoryConfiguration {
endpointBaseUrl: string;
port: number;
queue: QueueConfiguration;
database: DatabaseConfiguration;
auth: AuthConfiguration;
}

Просмотреть файл

@ -16,7 +16,7 @@ async function main(argv: string[]) {
const lab = new SequelizeLaboratory(config.endpointBaseUrl, queue);
const app = await createApp(lab);
const app = await createApp(lab, config.auth);
app.listen(config.port, () => {
console.log('Starting SDS laboratory service.');
console.log(`Service url is ${config.endpointBaseUrl}.`);