diff --git a/CHANGELOG.md b/CHANGELOG.md index d9f0382..1a7c4a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.5.0] - 2019-07-09 + +### auth0-bitbucket-deploy v2.10.0 +### auth0-github-deploy v2.10.0 +### auth0-gitlab-deploy v2.11.0 +### auth0-visualstudio-deploy v2.9.0 + +- #### Added + - Tenant settings support (`tenant.json` file). + - Guardian settings support (`guardian/factors/*.json`, `guardian/providers/*.json`, `guardian/templates/*.json` files). + ## [1.4.2] - 2019-06-14 ### auth0-gitlab-deploy v2.10.2 diff --git a/package-lock.json b/package-lock.json index 069ff92..04d640e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "auth0-deploy-extensions", - "version": "1.4.2", + "version": "1.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -19,6 +19,58 @@ } } }, + "@sinonjs/commons": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz", + "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + }, + "dependencies": { + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + } + } + }, + "@sinonjs/formatio": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz", + "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + } + }, + "@sinonjs/samsam": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.2.tgz", + "integrity": "sha512-ILO/rR8LfAb60Y1Yfp9vxfYAASK43NFC2mLzpvLUbCQY/Qu8YwReboseu8aheCEkyElZF2L2T9mHcR2bgdvZyA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.0.2", + "array-from": "^2.1.1", + "lodash": "^4.17.11" + }, + "dependencies": { + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + } + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, "@types/babel-types": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.4.tgz", @@ -339,6 +391,12 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", + "dev": true + }, "array-slice": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", @@ -717,7 +775,7 @@ }, "form-data": { "version": "1.0.0-rc4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz", + "resolved": "http://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz", "integrity": "sha1-BaxrwiIntD5EYfSIFhVUaZ1Pi14=", "requires": { "async": "^1.5.2", @@ -727,7 +785,7 @@ "dependencies": { "async": { "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz", "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" } } @@ -812,7 +870,7 @@ }, "superagent": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-2.3.0.tgz", + "resolved": "http://registry.npmjs.org/superagent/-/superagent-2.3.0.tgz", "integrity": "sha1-cDUpoHFOV+EjlZ3e+84ZOy5Q0RU=", "requires": { "component-emitter": "^1.2.0", @@ -9932,6 +9990,12 @@ "integrity": "sha1-OGchPo3Xm/Ho8jAMDPwe+xgsDfE=", "dev": true }, + "just-extend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", + "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", + "dev": true + }, "jwa": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.2.0.tgz", @@ -10446,6 +10510,12 @@ "chalk": "^1.0.0" } }, + "lolex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.1.0.tgz", + "integrity": "sha512-BYxIEXiVq5lGIXeVHnsFzqa1TxN5acnKnPCdlZSpzm8viNEOhiigupA4vTQ9HEFQ6nLTQ9wQOgBknJgzUYQ9Aw==", + "dev": true + }, "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", @@ -11106,6 +11176,36 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "nise": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.0.tgz", + "integrity": "sha512-Z3sfYEkLFzFmL8KY6xnSJLRxwQwYBjOXi/24lb62ZnZiGA0JUzGGTI6TBIgfCSMIDl9Jlu8SRmHNACLTemDHww==", + "dev": true, + "requires": { + "@sinonjs/formatio": "^3.1.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^4.1.0", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + } + } + }, "nock": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/nock/-/nock-9.6.1.tgz", @@ -15943,6 +16043,44 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true }, + "sinon": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.3.2.tgz", + "integrity": "sha512-thErC1z64BeyGiPvF8aoSg0LEnptSaWE7YhdWWbWXgelOyThent7uKOnnEh9zBxDbKixtr5dEko+ws1sZMuFMA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.4.0", + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/samsam": "^3.3.1", + "diff": "^3.5.0", + "lolex": "^4.0.1", + "nise": "^1.4.10", + "supports-color": "^5.5.0" + }, + "dependencies": { + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "slash": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", diff --git a/package.json b/package.json index 3ba7d33..6984b77 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "auth0-deploy-extensions", - "version": "1.4.2", + "version": "1.5.0", "description": "Auth0 Deployment Extensions", "engines": { "node": "5.9.0" @@ -181,6 +181,7 @@ "redux-simple-router": "^2.0.4", "redux-static": "^1.0.0", "rimraf": "^2.5.2", + "sinon": "^7.3.2", "style-loader": "0.16.0", "url-loader": "^0.5.7", "webpack": "1.13.3", diff --git a/server/lib/providers/bitbucket.js b/server/lib/providers/bitbucket.js index 95e7b7a..ef151bc 100644 --- a/server/lib/providers/bitbucket.js +++ b/server/lib/providers/bitbucket.js @@ -1,4 +1,5 @@ import _ from 'lodash'; +import path from 'path'; import Promise from 'bluebird'; import { constants } from 'auth0-source-control-extension-tools'; @@ -104,10 +105,14 @@ const getTree = (parsedRepo, branch, sha) => { }; const promises = { databases: getDBConnectionsTree(params), + tenant: getTreeByDir(params, ''), rules: getTreeByDir(params, constants.RULES_DIRECTORY), pages: getTreeByDir(params, constants.PAGES_DIRECTORY), roles: getTreeByDir(params, constants.ROLES_DIRECTORY), emails: getTreeByDir(params, constants.EMAIL_TEMPLATES_DIRECTORY), + guardianFactors: getTreeByDir(params, path.join(constants.GUARDIAN_DIRECTORY, constants.GUARDIAN_FACTORS_DIRECTORY)), + guardianFactorTemplates: getTreeByDir(params, path.join(constants.GUARDIAN_DIRECTORY, constants.GUARDIAN_TEMPLATES_DIRECTORY)), + guardianFactorProviders: getTreeByDir(params, path.join(constants.GUARDIAN_DIRECTORY, constants.GUARDIAN_PROVIDERS_DIRECTORY)), clientGrants: getTreeByDir(params, constants.CLIENTS_GRANTS_DIRECTORY), connections: getTreeByDir(params, constants.CONNECTIONS_DIRECTORY), clients: getTreeByDir(params, constants.CLIENTS_DIRECTORY), @@ -118,7 +123,11 @@ const getTree = (parsedRepo, branch, sha) => { .then((result) => (_.union( result.rules, result.databases, + result.tenant, result.emails, + result.guardianFactors, + result.guardianFactorTemplates, + result.guardianFactorProviders, result.pages, result.roles, result.clients, @@ -222,6 +231,14 @@ const getRules = (parsedRepo, branch, files, shaToken) => { downloadRule(parsedRepo, branch, ruleName, rules[ruleName], shaToken), { concurrency: 2 }); }; +/* + * Try to download tenant settings. + */ +const getTenant = (parsedRepo, branch, files, shaToken) => { + const tenantFile = { configFile: _.find(files, f => utils.isTenantFile(f.path)) }; + return downloadConfigurable(parsedRepo, branch, 'tenant', tenantFile, shaToken); +}; + /* * Get email provider. */ @@ -332,10 +349,14 @@ export function getChanges({ repository, branch, sha }) { })), null, 2)}`); const promises = { + tenant: getTenant(parsedRepo, branch, files, sha), rules: getRules(parsedRepo, branch, files, sha), databases: getDatabaseData(parsedRepo, branch, files, sha), emailProvider: getEmailProvider(parsedRepo, branch, files, sha), emailTemplates: getHtmlTemplates(parsedRepo, branch, files, sha, constants.EMAIL_TEMPLATES_DIRECTORY, constants.EMAIL_TEMPLATES_NAMES), + guardianFactors: getConfigurables(parsedRepo, branch, files, sha, path.join(constants.GUARDIAN_DIRECTORY, constants.GUARDIAN_FACTORS_DIRECTORY)), + guardianFactorTemplates: getConfigurables(parsedRepo, branch, files, sha, path.join(constants.GUARDIAN_DIRECTORY, constants.GUARDIAN_TEMPLATES_DIRECTORY)), + guardianFactorProviders: getConfigurables(parsedRepo, branch, files, sha, path.join(constants.GUARDIAN_DIRECTORY, constants.GUARDIAN_PROVIDERS_DIRECTORY)), pages: getHtmlTemplates(parsedRepo, branch, files, sha, constants.PAGES_DIRECTORY, constants.PAGE_NAMES), roles: getConfigurables(parsedRepo, branch, files, sha, constants.ROLES_DIRECTORY), clients: getConfigurables(parsedRepo, branch, files, sha, constants.CLIENTS_DIRECTORY), diff --git a/server/lib/providers/github.js b/server/lib/providers/github.js index b129024..a25ff8d 100644 --- a/server/lib/providers/github.js +++ b/server/lib/providers/github.js @@ -1,4 +1,5 @@ import _ from 'lodash'; +import path from 'path'; import axios from 'axios'; import Promise from 'bluebird'; import GitHubApi from 'github'; @@ -254,6 +255,17 @@ const getEmailProvider = (repository, branch, files) => { configFile: _.find(files, f => utils.isEmailProvider(f.path)) } ); +/* + * Get tenant settings. + */ +const getTenant = (repository, branch, files) => + downloadConfigurable( + repository, + branch, + 'tenant', + { configFile: _.find(files, f => utils.isTenantFile(f.path)) } + ); + /* * Get all configurables (resource servers / clients). */ @@ -277,9 +289,13 @@ export const getChanges = ({ repository, branch, sha }) => const promises = { rules: getRules(repository, branch, files), + tenant: getTenant(repository, branch, files), databases: getDatabaseData(repository, branch, files), emailProvider: getEmailProvider(repository, branch, files), emailTemplates: getHtmlTemplates(repository, branch, files, constants.EMAIL_TEMPLATES_DIRECTORY, constants.EMAIL_TEMPLATES_NAMES), + guardianFactors: getConfigurables(repository, branch, files, path.join(constants.GUARDIAN_DIRECTORY, constants.GUARDIAN_FACTORS_DIRECTORY)), + guardianFactorTemplates: getConfigurables(repository, branch, files, path.join(constants.GUARDIAN_DIRECTORY, constants.GUARDIAN_TEMPLATES_DIRECTORY)), + guardianFactorProviders: getConfigurables(repository, branch, files, path.join(constants.GUARDIAN_DIRECTORY, constants.GUARDIAN_PROVIDERS_DIRECTORY)), pages: getHtmlTemplates(repository, branch, files, constants.PAGES_DIRECTORY, constants.PAGE_NAMES), roles: getConfigurables(repository, branch, files, constants.ROLES_DIRECTORY), clients: getConfigurables(repository, branch, files, constants.CLIENTS_DIRECTORY), diff --git a/server/lib/providers/gitlab.js b/server/lib/providers/gitlab.js index 63eb7a8..2bc2b0d 100644 --- a/server/lib/providers/gitlab.js +++ b/server/lib/providers/gitlab.js @@ -1,4 +1,5 @@ import _ from 'lodash'; +import path from 'path'; import Promise from 'bluebird'; import GitLabApi from 'gitlab'; import { constants } from 'auth0-source-control-extension-tools'; @@ -56,7 +57,8 @@ const getTreeByPath = (projectId, branch, directory) => .filter(f => utils.validFilesOnly(f.path)); files.forEach((elem, idx) => { - files[idx].path = `${utils.getBaseDir()}${directory}/${elem.name}`; + const dir = directory ? `${directory}/` : ''; + files[idx].path = `${utils.getBaseDir()}${dir}${elem.name}`; }); return files; }); @@ -115,9 +117,13 @@ const getDBConnectionsTree = (projectId, branch) => const getTree = (projectId, branch) => { // Getting separate trees for rules and connections, as GitLab does not provide full (recursive) tree const promises = { + tenant: getTreeByPath(projectId, branch, ''), rules: getTreeByPath(projectId, branch, constants.RULES_DIRECTORY), databases: getDBConnectionsTree(projectId, branch), emails: getTreeByPath(projectId, branch, constants.EMAIL_TEMPLATES_DIRECTORY), + guardianFactors: getTreeByPath(projectId, branch, path.join(constants.GUARDIAN_DIRECTORY, constants.GUARDIAN_FACTORS_DIRECTORY)), + guardianFactorTemplates: getTreeByPath(projectId, branch, path.join(constants.GUARDIAN_DIRECTORY, constants.GUARDIAN_TEMPLATES_DIRECTORY)), + guardianFactorProviders: getTreeByPath(projectId, branch, path.join(constants.GUARDIAN_DIRECTORY, constants.GUARDIAN_PROVIDERS_DIRECTORY)), pages: getTreeByPath(projectId, branch, constants.PAGES_DIRECTORY), roles: getTreeByPath(projectId, branch, constants.ROLES_DIRECTORY), clients: getTreeByPath(projectId, branch, constants.CLIENTS_DIRECTORY), @@ -129,9 +135,13 @@ const getTree = (projectId, branch) => { return Promise.props(promises) .then((result) => (_.union( + result.tenant, result.rules, result.databases, result.emails, + result.guardianFactors, + result.guardianFactorTemplates, + result.guardianFactorProviders, result.pages, result.roles, result.clients, @@ -313,6 +323,12 @@ const getHtmlTemplates = (projectId, branch, files, dir, allowedNames) => { downloadTemplate(projectId, branch, name, templates[name]), { concurrency: 2 }); }; +/* + * Get tenant settings. + */ +const getTenant = (projectId, branch, files) => + downloadConfigurable(projectId, branch, 'tenant', { configFile: _.find(files, f => utils.isTenantFile(f.path)) }); + /* * Get email provider. */ @@ -328,10 +344,14 @@ export const getChanges = ({ projectId, branch }) => logger.debug(`Files in tree: ${JSON.stringify(files.map(file => ({ name: file.path, id: file.id })), null, 2)}`); const promises = { + tenant: getTenant(projectId, branch, files), rules: getRules(projectId, branch, files), databases: getDatabaseData(projectId, branch, files), emailProvider: getEmailProvider(projectId, branch, files), emailTemplates: getHtmlTemplates(projectId, branch, files, constants.EMAIL_TEMPLATES_DIRECTORY, constants.EMAIL_TEMPLATES_NAMES), + guardianFactors: getConfigurables(projectId, branch, files, path.join(constants.GUARDIAN_DIRECTORY, constants.GUARDIAN_FACTORS_DIRECTORY)), + guardianFactorTemplates: getConfigurables(projectId, branch, files, path.join(constants.GUARDIAN_DIRECTORY, constants.GUARDIAN_TEMPLATES_DIRECTORY)), + guardianFactorProviders: getConfigurables(projectId, branch, files, path.join(constants.GUARDIAN_DIRECTORY, constants.GUARDIAN_PROVIDERS_DIRECTORY)), pages: getHtmlTemplates(projectId, branch, files, constants.PAGES_DIRECTORY, constants.PAGE_NAMES), roles: getConfigurables(projectId, branch, files, constants.ROLES_DIRECTORY), clients: getConfigurables(projectId, branch, files, constants.CLIENTS_DIRECTORY), diff --git a/server/lib/providers/tfs-git.js b/server/lib/providers/tfs-git.js index e4d43b8..f6463a5 100644 --- a/server/lib/providers/tfs-git.js +++ b/server/lib/providers/tfs-git.js @@ -285,6 +285,12 @@ const getHtmlTemplates = (repositoryId, branch, files, dir, allowedNames) => { downloadTemplate(repositoryId, branch, name, templates[name]), { concurrency: 2 }); }; +/* + * Get tenant settings. + */ +const getTenant = (projectId, branch, files) => + downloadConfigurable(projectId, branch, 'tenant', { configFile: _.find(files, f => utils.isTenantFile(f.path)) }); + /* * Get email provider. */ @@ -304,10 +310,14 @@ export const getChanges = ({ repositoryId, branch }) => })), null, 2)}`); const promises = { + tenant: getTenant(repositoryId, branch, files), rules: getRules(repositoryId, branch, files), databases: getDatabaseData(repositoryId, branch, files), emailProvider: getEmailProvider(repositoryId, branch, files), emailTemplates: getHtmlTemplates(repositoryId, branch, files, constants.EMAIL_TEMPLATES_DIRECTORY, constants.EMAIL_TEMPLATES_NAMES), + guardianFactors: getConfigurables(repositoryId, branch, files, path.join(constants.GUARDIAN_DIRECTORY, constants.GUARDIAN_FACTORS_DIRECTORY)), + guardianFactorTemplates: getConfigurables(repositoryId, branch, files, path.join(constants.GUARDIAN_DIRECTORY, constants.GUARDIAN_TEMPLATES_DIRECTORY)), + guardianFactorProviders: getConfigurables(repositoryId, branch, files, path.join(constants.GUARDIAN_DIRECTORY, constants.GUARDIAN_PROVIDERS_DIRECTORY)), pages: getHtmlTemplates(repositoryId, branch, files, constants.PAGES_DIRECTORY, constants.PAGE_NAMES), roles: getConfigurables(repositoryId, branch, files, constants.ROLES_DIRECTORY), clients: getConfigurables(repositoryId, branch, files, constants.CLIENTS_DIRECTORY), diff --git a/server/lib/providers/tfs-tfvc.js b/server/lib/providers/tfs-tfvc.js index c5c7ab3..8859752 100644 --- a/server/lib/providers/tfs-tfvc.js +++ b/server/lib/providers/tfs-tfvc.js @@ -1,4 +1,5 @@ import _ from 'lodash'; +import path from 'path'; import axios from 'axios'; import Promise from 'bluebird'; import { constants } from 'auth0-source-control-extension-tools'; @@ -50,8 +51,9 @@ export const hasChanges = (changesetId) => const getConfigurableTree = (project, directory) => new Promise((resolve, reject) => { try { + const dir = directory ? `/${directory}` : ''; getApi() - .then(api => api.getItems(project, `${utils.getPrefix()}/${directory}`)) + .then(api => api.getItems(project, `${utils.getPrefix()}${dir}`)) .then(data => { if (!data) { return resolve([]); @@ -131,9 +133,13 @@ const getTree = (project, changesetId) => new Promise((resolve, reject) => { // Getting separate trees for rules and connections, as tfsvc does not provide full (recursive) tree const promises = { + tenant: getConfigurableTree(project, ''), rules: getConfigurableTree(project, constants.RULES_DIRECTORY), databases: getConnectionsTree(project, changesetId), emails: getConfigurableTree(project, constants.EMAIL_TEMPLATES_DIRECTORY), + guardianFactors: getConfigurableTree(project, path.join(constants.GUARDIAN_DIRECTORY, constants.GUARDIAN_FACTORS_DIRECTORY)), + guardianFactorTemplates: getConfigurableTree(project, path.join(constants.GUARDIAN_DIRECTORY, constants.GUARDIAN_TEMPLATES_DIRECTORY)), + guardianFactorProviders: getConfigurableTree(project, path.join(constants.GUARDIAN_DIRECTORY, constants.GUARDIAN_PROVIDERS_DIRECTORY)), pages: getConfigurableTree(project, constants.PAGES_DIRECTORY), roles: getConfigurableTree(project, constants.ROLES_DIRECTORY), clients: getConfigurableTree(project, constants.CLIENTS_DIRECTORY), @@ -145,9 +151,13 @@ const getTree = (project, changesetId) => return Promise.props(promises) .then(result => resolve(_.union( + result.tenant, result.rules, result.databases, result.emails, + result.guardianFactors, + result.guardianFactorTemplates, + result.guardianFactorProviders, result.pages, result.roles, result.clients, @@ -341,6 +351,12 @@ const getHtmlTemplates = (changesetId, files, dir, allowedNames) => { downloadTemplate(changesetId, name, templates[name]), { concurrency: 2 }); }; +/* + * Get tenant settings. + */ +const getTenant = (changesetId, files) => + downloadConfigurable(changesetId, 'tenant', { configFile: _.find(files, f => utils.isTenantFile(f.path)) }); + /* * Get email provider. */ @@ -359,10 +375,14 @@ export const getChanges = ({ project, changesetId }) => })), null, 2)}`); const promises = { + tenant: getTenant(changesetId, files), rules: getRules(changesetId, files), databases: getDatabaseData(changesetId, files), emailProvider: getEmailProvider(changesetId, files), emailTemplates: getHtmlTemplates(changesetId, files, constants.EMAIL_TEMPLATES_DIRECTORY, constants.EMAIL_TEMPLATES_NAMES), + guardianFactors: getConfigurables(changesetId, files, path.join(constants.GUARDIAN_DIRECTORY, constants.GUARDIAN_FACTORS_DIRECTORY)), + guardianFactorTemplates: getConfigurables(changesetId, files, path.join(constants.GUARDIAN_DIRECTORY, constants.GUARDIAN_TEMPLATES_DIRECTORY)), + guardianFactorProviders: getConfigurables(changesetId, files, path.join(constants.GUARDIAN_DIRECTORY, constants.GUARDIAN_PROVIDERS_DIRECTORY)), pages: getHtmlTemplates(changesetId, files, constants.PAGES_DIRECTORY, constants.PAGE_NAMES), roles: getConfigurables(changesetId, files, constants.ROLES_DIRECTORY), clients: getConfigurables(changesetId, files, constants.CLIENTS_DIRECTORY), diff --git a/server/lib/utils.js b/server/lib/utils.js index 8800e7e..5f32ef0 100644 --- a/server/lib/utils.js +++ b/server/lib/utils.js @@ -37,11 +37,30 @@ const isTemplate = (file, dir, allowedNames) => file.indexOf(`${path.join(getPrefix(), dir)}/`) === 0 && allowedNames.indexOf(file.split('/').pop()) >= 0; /* - * Check if a file is part of the pages folder. + * Check if a file is the tenant settings file. + */ +const isTenantFile = (file) => + file === path.join(getPrefix(), 'tenant.json'); + +/* + * Check if a file is the email provider file. */ const isEmailProvider = (file) => file === path.join(getPrefix(), constants.EMAIL_TEMPLATES_DIRECTORY, 'provider.json'); +/* + * Check if a file is the guardian file. + */ +const isGuardianFile = (file) => { + const guardianDir = path.join(getPrefix(), constants.GUARDIAN_DIRECTORY); + const isJSON = file.endsWith('.json'); + const isGuardian = file.startsWith(path.join(guardianDir, constants.GUARDIAN_FACTORS_DIRECTORY)) + || file.startsWith(path.join(guardianDir, constants.GUARDIAN_PROVIDERS_DIRECTORY)) + || file.startsWith(path.join(guardianDir, constants.GUARDIAN_TEMPLATES_DIRECTORY)); + + return isJSON && isGuardian; +} + /* * Check if a file is part of configurable folder. */ @@ -95,8 +114,12 @@ const validFilesOnly = (fileName) => { return true; } else if (isTemplate(fileName, constants.EMAIL_TEMPLATES_DIRECTORY, constants.EMAIL_TEMPLATES_NAMES)) { return true; + } else if (isTenantFile(fileName)) { + return true; } else if (isEmailProvider(fileName)) { return true; + } else if (isGuardianFile(fileName)) { + return true; } else if (isRule(fileName)) { return /\.(js|json)$/i.test(fileName); } else if (isConfigurable(fileName, constants.ROLES_DIRECTORY)) { @@ -234,6 +257,14 @@ const extractFileContent = (item) => { return item || {}; }; +const checkSessionLifetime = (data, property) => { + const hours = data[property]; + if (hours !== undefined && !Number.isInteger(hours)) { + data[`${property}_in_minutes`] = Math.round(hours * 60); + delete data[property]; + } +}; + const unifyItem = (item, type) => { switch (type) { default: @@ -258,12 +289,23 @@ const unifyItem = (item, type) => { } case 'roles': case 'clientGrants': + case 'guardianFactors': + case 'guardianFactorTemplates': + case 'guardianFactorProviders': case 'emailProvider': { const data = extractFileContent(item.configFile); return ({ ...data }); } + case 'tenant': { + const data = extractFileContent(item.configFile); + checkSessionLifetime(data, 'session_lifetime'); + checkSessionLifetime(data, 'idle_session_lifetime'); + + return ({ ...data }); + } + case 'databases': { const settings = extractFileContent(item.settings); const customScripts = {}; @@ -367,6 +409,8 @@ module.exports = { isRule, isDatabaseConnection, isTemplate, + isTenantFile, + isGuardianFile, isEmailProvider, isConfigurable, getDatabaseFiles, diff --git a/tests/server/mocks/expected-data.js b/tests/server/mocks/expected-data.js index 27ce695..9c3b7e9 100644 --- a/tests/server/mocks/expected-data.js +++ b/tests/server/mocks/expected-data.js @@ -111,5 +111,40 @@ module.exports = { } ] } - ] + ], + guardianFactors: [ + { + name: 'sms', + enabled: true + }, + { + name: 'email', + enabled: false + } + ], + guardianFactorProviders: [ + { + name: 'sms', + provider: 'twilio', + auth_token: 'test_twilio_authtoken', + sid: 'test_twilio_sid', + from: '0800-TEST-NUMBER', + messaging_service_sid: 'test_copilot_sid' + } + ], + guardianFactorTemplates: [ + { + name: 'sms', + enrollment_message: 'test enroll {{ code }}', + verification_message: 'test verification {{ code }}' + } + ], + tenant: { + friendly_name: 'My Company', + support_email: 'support@company.com', + session_lifetime_in_minutes: 74, + default_directory: 'users', + sandbox_version: '4', + idle_session_lifetime: 72 + } }; diff --git a/tests/server/mocks/repo-tree-mock.js b/tests/server/mocks/repo-tree-mock.js index 4ea8f6d..f4ad934 100644 --- a/tests/server/mocks/repo-tree-mock.js +++ b/tests/server/mocks/repo-tree-mock.js @@ -77,5 +77,40 @@ module.exports = { }, pages: { 'login.html': 'login page html' + }, + 'guardian/factors': { + 'sms.json': { + name: 'sms', + enabled: true + }, + 'email.json': { + name: 'email', + enabled: false + } + }, + 'guardian/providers': { + 'sms-twilio.json': { + name: 'sms', + provider: 'twilio', + auth_token: 'test_twilio_authtoken', + sid: 'test_twilio_sid', + from: '0800-TEST-NUMBER', + messaging_service_sid: 'test_copilot_sid' + } + }, + 'guardian/templates': { + 'sms.json': { + name: 'sms', + enrollment_message: 'test enroll {{ code }}', + verification_message: 'test verification {{ code }}' + } + }, + 'tenant.json': { + friendly_name: 'My Company', + support_email: 'support@company.com', + session_lifetime: 1.23, + default_directory: 'users', + sandbox_version: '4', + idle_session_lifetime: 72 } }; diff --git a/tests/server/providers/bitbucket.tests.js b/tests/server/providers/bitbucket.tests.js index f61a556..278dfc6 100644 --- a/tests/server/providers/bitbucket.tests.js +++ b/tests/server/providers/bitbucket.tests.js @@ -22,20 +22,37 @@ const generateTree = () => { const items = Object.keys(files[type]); const tree = []; - for (let j = 0; j < items.length; j++) { - const name = items[j]; + if (type === 'tenant.json') { + const content = JSON.stringify(files[type]); + const path = `tenant/${type}`; - const content = (name.endsWith('.json')) ? JSON.stringify(files[type][name]) : files[type][name]; - const path = (type === 'database-connections') - ? `tenant/${type}/test-db/${name}` - : `tenant/${type}/${name}`; + tree.push({ type: 'blob', path, name: 'tenant.json' }); - tree.push({ type: 'blob', path, name }); + nock('https://api.bitbucket.org') + .get('/2.0/repositories/test/auth0/src/sha/tenant/') + .query(() => true) + .reply(200, { values: tree }); nock('https://api.bitbucket.org') .get(`/2.0/repositories/test/auth0/src/sha/${path}`) .query(() => true) .reply(200, content); + } else { + for (let j = 0; j < items.length; j++) { + const name = items[j]; + + const content = (name.endsWith('.json')) ? JSON.stringify(files[type][name]) : files[type][name]; + const path = (type === 'database-connections') + ? `tenant/${type}/test-db/${name}` + : `tenant/${type}/${name}`; + + tree.push({ type: 'blob', path, name }); + + nock('https://api.bitbucket.org') + .get(`/2.0/repositories/test/auth0/src/sha/${path}`) + .query(() => true) + .reply(200, content); + } } if (type === 'database-connections') { diff --git a/tests/server/providers/github.tests.js b/tests/server/providers/github.tests.js index 7bc1e9b..8b1b1d0 100644 --- a/tests/server/providers/github.tests.js +++ b/tests/server/providers/github.tests.js @@ -24,20 +24,32 @@ const generateTree = () => { const type = types[i]; const items = Object.keys(files[type]); - for (let j = 0; j < items.length; j++) { - const name = items[j]; - - const content = (name.endsWith('.json')) ? JSON.stringify(files[type][name]) : files[type][name]; - const sha = `${name}.sha`; - const path = (type === 'database-connections') - ? `tenant/${type}/test-db/${name}` - : `tenant/${type}/${name}`; + if (type === 'tenant.json') { + const content = JSON.stringify(files[type]); + const path = `tenant/${type}`; + const sha = `${type}.sha`; tree.push({ type: 'blob', path, sha }); nock('https://test.gh') .get(`/api/repos/test/repo/git/blobs/${sha}`) .reply(200, { content: new Buffer(content) }); + } else { + for (let j = 0; j < items.length; j++) { + const name = items[j]; + + const content = (name.endsWith('.json')) ? JSON.stringify(files[type][name]) : files[type][name]; + const sha = `${name}.sha`; + const path = (type === 'database-connections') + ? `tenant/${type}/test-db/${name}` + : `tenant/${type}/${name}`; + + tree.push({ type: 'blob', path, sha }); + + nock('https://test.gh') + .get(`/api/repos/test/repo/git/blobs/${sha}`) + .reply(200, { content: new Buffer(content) }); + } } } diff --git a/tests/server/providers/gitlab.tests.js b/tests/server/providers/gitlab.tests.js index 5371aa5..e9e1afd 100644 --- a/tests/server/providers/gitlab.tests.js +++ b/tests/server/providers/gitlab.tests.js @@ -22,34 +22,50 @@ const generateTree = () => { const items = Object.keys(files[type]); const tree = []; - for (let j = 0; j < items.length; j++) { - const name = items[j]; + if (type === 'tenant.json') { + const content = JSON.stringify(files[type]); + const path = `tenant/${type}`; - const content = (name.endsWith('.json')) ? JSON.stringify(files[type][name]) : files[type][name]; - const path = (type === 'database-connections') - ? `tenant/${type}/test-db/${name}` - : `tenant/${type}/${name}`; - - tree.push({ type: 'blob', path, name }); + tree.push({ type: 'blob', path, name: 'tenant.json' }); nock('https://test.gl') .get(`/api/v4/projects/projectId/repository/files/${path.replace(RegExp('/', 'g'), '%2F')}`) .query(() => true) .reply(200, { content: new Buffer(content) }); - } - - if (type === 'database-connections') { - nock('https://test.gl') - .get(`/api/v4/projects/projectId/repository/tree?ref=branch&path=tenant%2F${type}`) - .reply(200, [ { type: 'tree', path: 'tenant/database-connections/test-db', name: 'test-db' } ]); nock('https://test.gl') - .get(`/api/v4/projects/projectId/repository/tree?ref=branch&path=tenant%2F${type}%2Ftest-db`) + .get('/api/v4/projects/projectId/repository/tree?ref=branch&path=tenant%2F') .reply(200, tree); } else { - nock('https://test.gl') - .get(`/api/v4/projects/projectId/repository/tree?ref=branch&path=tenant%2F${type}`) - .reply(200, tree); + for (let j = 0; j < items.length; j++) { + const name = items[j]; + + const content = (name.endsWith('.json')) ? JSON.stringify(files[type][name]) : files[type][name]; + const path = (type === 'database-connections') + ? `tenant/${type}/test-db/${name}` + : `tenant/${type}/${name}`; + + tree.push({ type: 'blob', path, name }); + + nock('https://test.gl') + .get(`/api/v4/projects/projectId/repository/files/${path.replace(RegExp('/', 'g'), '%2F')}`) + .query(() => true) + .reply(200, { content: new Buffer(content) }); + } + + if (type === 'database-connections') { + nock('https://test.gl') + .get(`/api/v4/projects/projectId/repository/tree?ref=branch&path=tenant%2F${type}`) + .reply(200, [ { type: 'tree', path: 'tenant/database-connections/test-db', name: 'test-db' } ]); + + nock('https://test.gl') + .get(`/api/v4/projects/projectId/repository/tree?ref=branch&path=tenant%2F${type}%2Ftest-db`) + .reply(200, tree); + } else { + nock('https://test.gl') + .get(`/api/v4/projects/projectId/repository/tree?ref=branch&path=tenant%2F${type.replace(RegExp('/', 'g'), '%2F')}`) + .reply(200, tree); + } } } }; diff --git a/tests/server/providers/tfs-git.tests.js b/tests/server/providers/tfs-git.tests.js new file mode 100644 index 0000000..426b00a --- /dev/null +++ b/tests/server/providers/tfs-git.tests.js @@ -0,0 +1,154 @@ +import sinon from 'sinon'; +import expect from 'expect'; +import * as vso from 'vso-node-api'; + +import config from '../../../server/lib/config'; +import { hasChanges, getChanges } from '../../../server/lib/providers/tfs-git'; +import files from '../mocks/repo-tree-mock'; +import expectedResults from '../mocks/expected-data'; + + +const defaultConfig = { + INSTANCE: 'test-instance', + COLLECTION: 'defaultCollection', + REPOSITORY: 'test/auth0', + BRANCH: 'master', + AUTH_METHOD: 'pat', + TOKEN: 'secret_token', + BASE_DIR: 'tenant' +}; + +const contentsById = {}; + +const generateTree = () => { + const tree = []; + const types = Object.keys(files); + + for (let i = 0; i < types.length; i++) { + const type = types[i]; + const items = Object.keys(files[type]); + + if (type === 'tenant.json') { + const content = JSON.stringify(files[type]); + const sha = `${type}.sha`; + const path = `tenant/${type}`; + + contentsById[sha] = { on: (event, cb) => cb(content) }; + + tree.push({ gitObjectType: 3, relativePath: path, objectId: sha }); + } else { + for (let j = 0; j < items.length; j++) { + const name = items[j]; + + const content = (name.endsWith('.json')) ? JSON.stringify(files[type][name]) : files[type][name]; + const sha = `${type}-${name}.sha`; + const path = (type === 'database-connections') + ? `tenant/${type}/test-db/${name}` + : `tenant/${type}/${name}`; + + contentsById[sha] = { on: (event, cb) => cb(content) }; + tree.push({ gitObjectType: 3, relativePath: path, objectId: sha }); + } + } + } + + return tree; +}; + +const gitApi = {}; + +const stubs = []; + +describe('tfs-git', () => { + before((done) => { + config.setProvider((key) => defaultConfig[key], null); + + stubs.push(sinon.stub(vso, 'getPersonalAccessTokenHandler').callsFake((token) => { + expect(token).toEqual(defaultConfig.TOKEN); + return 'credentials'; + })); + + stubs.push(sinon.stub(vso, 'WebApi').callsFake(function(url, creds) { + expect(url).toEqual(`https://${defaultConfig.INSTANCE}.visualstudio.com/${defaultConfig.COLLECTION}`); + expect(creds).toEqual('credentials'); + + this.getGitApi = () => gitApi; + return this; + })); + + return done(); + }); + + describe('hasChanges', () => { + it('should return true if something has been changed', (done) => { + const data = { + changes: [ { item: { path: '/tenant/rules/rule1.js' } } ] + }; + + gitApi.getChanges = () => Promise.resolve(data); + + hasChanges('commit', 'repo') + .then((result) => { + expect(result).toEqual(true); + done(); + }) + .catch(done); + }); + + it('should return false if changes are irrelevant', (done) => { + const data = { + changes: [ { item: { path: '/tenant/readme.md' } } ] + }; + + gitApi.getChanges = () => Promise.resolve(data); + + hasChanges('commit', 'repo') + .then((result) => { + expect(result).toEqual(false); + done(); + }) + .catch(done); + }); + + it('should return true if some of changes are relevant', (done) => { + const data = { + changes: [ + { item: { path: '/tenant/readme.md' } }, + { item: { path: '/package.json' } }, + { item: { path: '/tenant/rules/rule1.js' } } + ] + }; + + gitApi.getChanges = () => Promise.resolve(data); + + hasChanges('commit', 'repo') + .then((result) => { + expect(result).toEqual(true); + done(); + }) + .catch(done); + }); + }); + + describe('getChanges', () => { + it('should get and format files', (done) => { + gitApi.getBranch = () => Promise.resolve({ commit: { commitId: 'commit-id' } }); + gitApi.getCommit = () => Promise.resolve({ treeId: 'tree-id' }); + gitApi.getTree = () => Promise.resolve({ treeEntries: generateTree() }); + gitApi.getBlobContent = (repo, fileId) => Promise.resolve(contentsById[fileId]); + + getChanges({ repositoryId: 'repo', branch: 'branch' }) + .then(results => { + expect(results).toEqual(expectedResults); + + done(); + }) + .catch(done); + }); + }); + + after((done) => { + stubs.forEach(stub => stub.restore()); + done(); + }); +}); diff --git a/tests/server/providers/tfs-tfvc.tests.js b/tests/server/providers/tfs-tfvc.tests.js new file mode 100644 index 0000000..a02053f --- /dev/null +++ b/tests/server/providers/tfs-tfvc.tests.js @@ -0,0 +1,162 @@ +import sinon from 'sinon'; +import expect from 'expect'; +import * as vso from 'vso-node-api'; +import nock from 'nock'; + +import config from '../../../server/lib/config'; +import { hasChanges, getChanges } from '../../../server/lib/providers/tfs-tfvc'; +import files from '../mocks/repo-tree-mock'; +import expectedResults from '../mocks/expected-data'; + + +const defaultConfig = { + TYPE: 'tfvc', + INSTANCE: 'test-instance', + COLLECTION: 'defaultCollection', + REPOSITORY: 'test/auth0', + BRANCH: 'master', + AUTH_METHOD: 'pat', + TOKEN: 'secret_token', + PROJECT_PATH: '$/TFVC-test/tenant' +}; + +const generateTreeByDir = (dir) => { + const splitDir = dir.split('/'); + let type = splitDir.pop(); + const subType = splitDir.pop(); + const tree = []; + + if (type === 'tenant') { + const content = JSON.stringify(files['tenant.json']); + const path = '$/TFVC-test/tenant/tenant.json'; + + nock('https://test-instance.visualstudio.com') + .get(`/defaultCollection/_apis/tfvc/items?path=${path}&api-version=5.0&includeContent=true`) + .reply(200, { content }); + + tree.push({ path, size: 1 }); + + return tree; + } + + if (type === 'database-connections') { + tree.push({ isFolder: true, path: 'tenant/database-connections/test-db' }); + return tree; + } + + if (type === 'test-db') { + type = 'database-connections'; + } + + if (subType === 'guardian') { + type = `${subType}/${type}`; + } + + const items = Object.keys(files[type]); + + for (let j = 0; j < items.length; j++) { + const name = items[j]; + + const content = (name.endsWith('.json')) ? JSON.stringify(files[type][name]) : files[type][name]; + const path = (type === 'database-connections') + ? `$/TFVC-test/tenant/${type}/test-db/${name}` + : `$/TFVC-test/tenant/${type}/${name}`; + + nock('https://test-instance.visualstudio.com') + .get(`/defaultCollection/_apis/tfvc/items?path=${path}&api-version=5.0&includeContent=true`) + .reply(200, { content }); + + tree.push({ path, size: 1 }); + } + + return tree; +}; + +const tfvcApi = {}; + +const stubs = []; + +describe('tfs-tfvc', () => { + before((done) => { + config.setProvider((key) => defaultConfig[key], null); + + stubs.push(sinon.stub(vso, 'getPersonalAccessTokenHandler').callsFake((token) => { + expect(token).toEqual(defaultConfig.TOKEN); + return 'credentials'; + })); + + stubs.push(sinon.stub(vso, 'WebApi').callsFake(function(url, creds) { + expect(url).toEqual(`https://${defaultConfig.INSTANCE}.visualstudio.com/${defaultConfig.COLLECTION}`); + expect(creds).toEqual('credentials'); + + this.getTfvcApi = () => tfvcApi; + return this; + })); + + return done(); + }); + + describe('hasChanges', () => { + it('should return true if something has been changed', (done) => { + const data = [ { item: { path: '$/TFVC-test/tenant/rules/rule1.js' } } ]; + + tfvcApi.getChangesetChanges = () => Promise.resolve(data); + + hasChanges('commit', 'repo') + .then((result) => { + expect(result).toEqual(true); + done(); + }) + .catch(done); + }); + + it('should return false if changes are irrelevant', (done) => { + const data = [ { item: { path: '$/TFVC-test/tenant/readme.md' } } ]; + + tfvcApi.getChangesetChanges = () => Promise.resolve(data); + + hasChanges('commit', 'repo') + .then((result) => { + expect(result).toEqual(false); + done(); + }) + .catch(done); + }); + + it('should return true if some of changes are relevant', (done) => { + const data = [ + { item: { path: '$/TFVC-test/tenant/readme.md' } }, + { item: { path: '$/TFVC-test/package.json' } }, + { item: { path: '$/TFVC-test/tenant/rules/rule1.js' } } + ]; + + tfvcApi.getChangesetChanges = () => Promise.resolve(data); + + hasChanges('commit', 'repo') + .then((result) => { + expect(result).toEqual(true); + done(); + }) + .catch(done); + }); + }); + + describe('getChanges', () => { + it('should get and format files', (done) => { + tfvcApi.getItems = (project, dir) => Promise.resolve(generateTreeByDir(dir)); + + getChanges({ project: 'project', changesetId: 'branch' }) + .then(results => { + expect(results).toEqual(expectedResults); + + done(); + }) + .catch(done); + }); + }); + + after((done) => { + stubs.forEach(stub => stub.restore()); + done(); + }); +}); diff --git a/webtask-templates/bitbucket.json b/webtask-templates/bitbucket.json index ce0aaeb..6f32759 100644 --- a/webtask-templates/bitbucket.json +++ b/webtask-templates/bitbucket.json @@ -1,8 +1,8 @@ { "title": "Bitbucket Deployments", "name": "auth0-bitbucket-deploy", - "version": "2.9.1", - "preVersion": "2.8.1", + "version": "2.10.0", + "preVersion": "2.9.1", "author": "auth0", "description": "This extension gives Auth0 customers the possibility to deploy Hosted Pages, Rules and Custom Database Connections from Bitbucket.", "type": "application", diff --git a/webtask-templates/github.json b/webtask-templates/github.json index b3ddf05..3ce0abc 100644 --- a/webtask-templates/github.json +++ b/webtask-templates/github.json @@ -1,8 +1,8 @@ { "title": "GitHub Deployments", "name": "auth0-github-deploy", - "version": "2.9.0", - "preVersion": "2.8.1", + "version": "2.10.0", + "preVersion": "2.9.1", "author": "auth0", "description": "This extension gives Auth0 customers the possibility to deploy Pages, Rules and Custom Database Connections from GitHub.", "type": "application", diff --git a/webtask-templates/gitlab.json b/webtask-templates/gitlab.json index d7940f4..c03a90f 100644 --- a/webtask-templates/gitlab.json +++ b/webtask-templates/gitlab.json @@ -1,8 +1,8 @@ { "title": "GitLab Deployments", "name": "auth0-gitlab-deploy", - "version": "2.10.2", - "preVersion": "2.9.1", + "version": "2.11.0", + "preVersion": "2.10.2", "author": "auth0", "description": "This extension gives Auth0 customers the possibility to deploy Hosted Pages, Rules and Custom Database Connections from GitLab.", "type": "application", diff --git a/webtask-templates/visualstudio.json b/webtask-templates/visualstudio.json index 29840d9..7f0566b 100644 --- a/webtask-templates/visualstudio.json +++ b/webtask-templates/visualstudio.json @@ -1,8 +1,8 @@ { "title": "Visual Studio Team Services Deployments", "name": "auth0-visualstudio-deploy", - "version": "2.8.2", - "preVersion": "2.7.1", + "version": "2.9.0", + "preVersion": "2.8.2", "author": "auth0", "description": "This extension gives Auth0 customers the possibility to deploy Hosted Pages, Rules and Custom Database Connections from Visual Studio Team Services.", "type": "application",