diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..b89ca2f78c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,14 @@ +--- +name: Feature Request +about: Request a new budibase feature or enhancement +title: '' +labels: enhancement +assignees: '' + +--- + +**Describe the feature request** +A clear and concise description of what the feature request. + +**Screenshots** +If applicable, add screenshots to help explain your problem. diff --git a/README.md b/README.md index c57b319ec5..9f9092b399 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ - **Open source and extensible.** Budibase is open-source - licensed as GPL v3. This should fill you with confidence that Budibase will always be around. You can also code against Budibase or fork it and make changes as you please, providing a developer-friendly experience. -- **Load data or start from scratch.** Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, mySQL, Airtable, S3, DyanmoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new data sources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas). +- **Load data or start from scratch.** Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new data sources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas). - **Design and build apps with powerful pre-made components.** Budibase comes out of the box with beautifully designed, powerful components which you can use like building blocks to build your UI. We also expose a lot of your favourite CSS styling options so you can go that extra creative mile. [Request new component](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas). diff --git a/hosting/envoy.dev.yaml.hbs b/hosting/envoy.dev.yaml.hbs index 76417b3e0d..01d5a09efa 100644 --- a/hosting/envoy.dev.yaml.hbs +++ b/hosting/envoy.dev.yaml.hbs @@ -26,10 +26,18 @@ static_resources: cluster: couchdb-service prefix_rewrite: "/" + - match: { prefix: "/api/system/" } + route: + cluster: worker-dev + - match: { prefix: "/api/admin/" } route: cluster: worker-dev + - match: { prefix: "/api/global/" } + route: + cluster: worker-dev + - match: { prefix: "/api/" } route: cluster: server-dev diff --git a/hosting/envoy.yaml b/hosting/envoy.yaml index d7b34f4d5e..d5f9ebee28 100644 --- a/hosting/envoy.yaml +++ b/hosting/envoy.yaml @@ -37,11 +37,19 @@ static_resources: route: cluster: app-service - # special case for worker admin API + # special cases for worker admin (deprecated), global and system API + - match: { prefix: "/api/global/" } + route: + cluster: worker-service + - match: { prefix: "/api/admin/" } route: cluster: worker-service + - match: { prefix: "/api/system/" } + route: + cluster: worker-service + - match: { path: "/" } route: cluster: app-service diff --git a/i18n/README.id.md b/i18n/README.id.md index 7f0229f345..d4a25f569c 100644 --- a/i18n/README.id.md +++ b/i18n/README.id.md @@ -8,10 +8,10 @@

- Membangun, mengotomatisasi dan hosting sendiri aplikasi internal dalam hitungan menit + Membangun, mengotomatisasi dan hosting sendiri aplikasi internal dalam hitungan menit

- Budibase adalah sebuah platform low-code dengan sumber terbuka, bertujuan untuk membantu para developer dan professional IT, untuk membangun, mengotomatisasi, dan mengirimkan aplikasi internal di infrastruktur sendiri dalam waktu hitungan menit. + Budibase adalah sebuah platform low-code dengan sumber terbuka, bertujuan untuk membantu para developer dan professional IT, untuk membangun, mengotomatisasi, dan mengirimkan aplikasi internal di infrastruktur sendiri dalam waktu hitungan menit.

@@ -39,7 +39,7 @@

- Mari mulai + Memulai · Dokumentasi · @@ -53,17 +53,17 @@

## ✨ Fitur-fitur -- **Membangun dan mengirimkan software nyata.** Tidak seperti platform lain, dengan Budibase Anda membuat dan mengirimkan aplikasi-aplikasi satu halaman. Aplikasi-aplikasi Budibase memiliki kinerja yang matang dan dapat dirancang secara responsif, memberikan pengalaman yang menyenangkan bagi pengguna. +- **Membangun dan mengirimkan perangkat lunak sebenarnya .** Tidak seperti platform lain, dengan Budibase Anda membuat dan mengirimkan aplikasi-aplikasi satu halaman. Aplikasi-aplikasi Budibase memiliki kinerja yang matang dan dapat dirancang secara responsif, memberikan pengalaman yang menyenangkan bagi pengguna. -- **Sumber terbuka dan dapat dikembangkan.** Budibase adalah platform dengan sumber terbuka - dilisensikan sebagai GPL v3. Ini akan membuat Anda yakin bahwa Budibase akan selalu ada. Anda juga dapat berkontribusi terhadap kode Budibase atau melakukan fork dan membuat perubahan sesuka Anda, memberikan pengalaman yang ramah developer. +- **Sumber terbuka dan dapat dikembangkan.** Budibase adalah platform dengan sumber terbuka - dilisensikan sebagai GPL v3. Ini akan membuat Anda yakin bahwa Budibase akan selalu ada. Anda juga dapat berkontribusi terhadap kode Budibase atau melakukan fork dan membuat perubahan sesuka Anda, memberikan pengalaman yang ramah developer. -- **Muat data atau mulai dari awal.** Budibase menarik data dari berbagai sumber, termasuk MongoDB, CouchDB, PostgreSQL, mySQL, Airtable, S3, DyanmoDB, atau REST API. Dan tidak seperti platform lain, dengan Budibase Anda dapat memulai dari awal dan membuat aplikasi bisnis tanpa sumber data sekalipun. [Permintaan untuk sumber data baru](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas). +- **Memuat data atau mulai dari awal.** Budibase menarik data dari berbagai sumber, termasuk MongoDB, CouchDB, PostgreSQL, mySQL, Airtable, S3, DyanmoDB, atau REST API. Dan tidak seperti platform lain, dengan Budibase Anda dapat memulai dari awal dan membuat aplikasi bisnis tanpa sumber data sekalipun. [Permintaan untuk sumber data baru](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas). -- **Rancang dan bangun aplikasi dengan komponen siap pakai yang kuat.** Budibase tersedia dengan siap pakai dan dirancang dengan indah, komponen-komponen kuat yang dapat anda gunakan layaknya blok penyusun untuk merancang UI Anda. Kami juga menyediakan banyak sekali pilihan-pilihan CSS favorit yang dapat digunakan sesuai dengan kreatifitas yang Anda inginkan. [Permintaan komponen baru [Permintaan untuk komponen baru](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas). +- **Rancang dan bangun aplikasi dengan komponen siap pakai yang kuat.** Budibase tersedia dengan siap pakai dan dirancang dengan indah, komponen-komponen kuat yang dapat anda gunakan layaknya blok penyusun untuk merancang antar muka Anda. Kami juga menyediakan banyak sekali pilihan-pilihan CSS favorit yang dapat digunakan sesuai dengan kreatifitas yang Anda inginkan. [Permintaan komponen baru [Permintaan untuk komponen baru](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas). -- **Otomatiskan proses, integrasikan dengan aplikasi lain, dan sambungkan ke webhook.** Menghemat waktu dengan mengotomatisasikan proses dan alur kerja yang masih dikerjakan secara manual. Dari menghubungkan ke webhooks, hingga mengotomatiasi email-email, cukup beritahu Budibase apa yang harus dilakukan dan biarkan dia berkerja untuk Anda. Anda bisa dengan mudah [membuat otomatisasi baru untuk Budibase disini ](https://github.com/Budibase/automations) atau [minta otomatisasi baru](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas). +- **Otomatiskan proses, integrasikan dengan aplikasi lain, dan sambungkan ke webhook.** Menghemat waktu dengan mengotomatisasikan proses dan alur kerja yang masih dikerjakan secara manual. Dari menghubungkan ke webhooks, hingga mengotomatiasi surel, cukup beritahu Budibase apa yang harus dilakukan dan biarkan dia berkerja untuk Anda. Anda bisa dengan mudah [membuat otomatisasi baru untuk Budibase disini ](https://github.com/Budibase/automations) atau [minta otomatisasi baru](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas). -- **Surga bagi Admin.** Budibase dibuat untuk berkembang. Dengan Budibase, Anda dapat menghosting sendiri di infrastruktrur Anda punya dan secara global mengelola para pengguna, orientasi, SMTP, aplikasi-aplikasi, grup-grup, tema, dan masih masih banyak lagi. Anda juga dapat menyediakan para pengguna/grup dengan sebuah aplikasi portal dan menyebarkan pengelolaan pengguna kepada manaajer grup. +- **Surga bagi Admin.** Budibase dibuat untuk berkembang. Dengan Budibase, Anda dapat menghosting sendiri di infrastruktrur Anda punya dan secara global mengelola para pengguna, orientasi, SMTP, aplikasi-aplikasi, grup-grup, tema, dan masih masih banyak lagi. Anda juga dapat menyediakan para pengguna/grup dengan sebuah aplikasi portal dan menyebarkan pengelolaan pengguna kepada manaajer grup.
@@ -77,7 +77,7 @@ Saat ini ada 2 cara untuk memulai Budibase, yaitu: Digital Ocean dan Docker. ### Memulai Dengan Digital Ocean Cara paling mudah dan cepat untuk memulai adalah dengan menggunakan Digital Ocean: -1-klik Digital Ocean deploy +1-klik Digital Ocean deploy digital ocean badge @@ -85,17 +85,17 @@ Cara paling mudah dan cepat untuk memulai adalah dengan menggunakan Digital Ocea

### Memulai dengan Docker -Untuk memulai, Anda harus memiliki docker dan docker compose terinstal di mesin anda. -Setelah Anda docker terinstal, prosesnya akan memakan waktu 5 menit, dengan empat langkah ini: +Untuk memulai, Anda harus memiliki docker dan docker compose terinstall di mesin anda. +Setelah Anda docker terinstall, prosesnya akan memakan waktu 5 menit, dengan empat langkah ini: -1. Instal CLI Budibase. +1. Install CLI Budibase. ``` $ npm i -g @budibase/cli ``` -2. Mempersiapkan Budibase (pilih tempat menyimpan Budibase dan port untuk menjalankannya) +2. Mempersiapkan Budibase (pilih tempat menyimpan Budibase dan port untuk menjalankannya) ``` budi hosting --init @@ -144,7 +144,7 @@ Jika Anda memiliki pertanyaan atau ingin berbicara dengan pengguna Budibase lain ## ❗ Kode etik -Budibase berdedikasi untuk memberikan pengalaman yang ramah, beragam, dan bebas pelecehan bagi semua orang. Kami mengharapkan semua orang di komunitas Budibase untuk mematuhi [**Kode etik**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md). Mohon dibac. +Budibase berdedikasi untuk memberikan pengalaman yang ramah, beragam, dan bebas pelecehan bagi semua orang. Kami mengharapkan semua orang di komunitas Budibase untuk mematuhi [**Kode etik**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md). Mohon dibaca.
--- @@ -153,19 +153,19 @@ Budibase berdedikasi untuk memberikan pengalaman yang ramah, beragam, dan bebas ## 🙌 Berkontribusi untuk Budibase -Dari membuka laporan bug hingga membuat pull request: setiap kontribusi dihargai dan disambut. Jika Anda berencana untuk menerapkan fitur baru atau mengubah API, harap buat issue terlebih dahulu. Dengan cara ini kami dapat memastikan pekerjaan Anda tidak sia-sia. +Dari membuka laporan bug hingga membuat pull request: setiap kontribusi dihargai dan disambut. Jika Anda berencana untuk menerapkan fitur baru atau mengubah API, harap buat issue terlebih dahulu. Dengan cara ini kami dapat memastikan pekerjaan Anda tidak sia-sia. ### Tidak yakin memulai dari mana? -Sebuat tempat yang baik untuk mulai berkontribusi, adalah [Masalah pertama di proyek](https://github.com/Budibase/budibase/projects/22). +Sebuat tempat yang baik untuk mulai berkontribusi, adalah [masalah pertama di proyek](https://github.com/Budibase/budibase/projects/22). ### Bagaimana repositori diatur -Budibase adalah monorepo yang dikelola oleh lerna. Lerna mengelola pembangunan dan penerbitan paket budibase. Pada level tinggi, inilah paket-paket yang membentuk Budibase. +Budibase adalah monorepo yang dikelola oleh lerna. Lerna mengelola pembangunan dan penerbitan paket Budibase. Pada level tinggi, inilah paket-paket yang membentuk Budibase. -- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - berisi kode untuk aplikasi svelte sisi klien pembangun budibase. +- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - berisi kode untuk aplikasi svelte sisi klien pembangun Budibase. -- [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client) - Modul yang berjalan di browser yang bertanggung jawab untuk membaca definisi JSON dan membuat aplikasi web yang hidup dan bernafas darinya. +- [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client) - Modul yang berjalan di browser yang bertanggung jawab untuk membaca definisi JSON dan membuat aplikasi web yang hidup dan bernafas darinya. -- [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - Server Budibase. Aplikasi Koa ini bertanggung jawab untuk melayani JS untuk aplikasi builder dan budibase, serta menyediakan API untuk interaksi dengan database dan sistem file. +- [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - Server Budibase. Aplikasi Koa ini bertanggung jawab untuk melayani JS untuk aplikasi builder dan budibase, serta menyediakan API untuk interaksi dengan database dan sistem file. Untuk informasi lebih lanjut, lihat [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md)

@@ -188,7 +188,7 @@ Budibase adalah sumber terbuka, dilisensikan sebagai [GPL v3](https://www.gnu.or [![Stargazers over time](https://starchart.cc/Budibase/budibase.svg)](https://starchart.cc/Budibase/budibase) -Jika Anda mengalami masalah di antara pembaruan, silakan gunakan panduan [disini](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md#troubleshooting) untuk membersihkan environment Anda. +Jika Anda mengalami masalah di antara pembaruan, silakan gunakan panduan [disini](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md#troubleshooting) untuk membersihkan environment Anda.
@@ -227,4 +227,4 @@ Terima kasih untuk orang-orang hebat ini ([emoji key](https://allcontributors.or -This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! +Proyek ini mengikuti spesifikasi [all-contributors](https://github.com/all-contributors/all-contributors). Kontribusi dalam bentuk apa pun dipersilakan! diff --git a/lerna.json b/lerna.json index b18463a8b9..7e72aa146a 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "0.9.82", + "version": "0.9.99-alpha.6", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/package.json b/package.json index 4f545d935f..bb226ee568 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,8 @@ "test:e2e": "lerna run cy:test", "test:e2e:ci": "lerna run cy:ci", "build:docker": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh && cd -", - "build:docker:develop": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -" + "build:docker:develop": "node scripts/pinVersions && lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -", + "multi:enable": "lerna run multi:enable", + "multi:disable": "lerna run multi:disable" } } diff --git a/packages/auth/db.js b/packages/auth/db.js index 4b03ec36cc..a7b38821a7 100644 --- a/packages/auth/db.js +++ b/packages/auth/db.js @@ -1 +1,4 @@ -module.exports = require("./src/db/utils") +module.exports = { + ...require("./src/db/utils"), + ...require("./src/db/constants"), +} diff --git a/packages/auth/package.json b/packages/auth/package.json index 51ff579482..b88c12b389 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/auth", - "version": "0.9.82", + "version": "0.9.99-alpha.6", "description": "Authentication middlewares for budibase builder and apps", "main": "src/index.js", "author": "Budibase", @@ -13,6 +13,7 @@ "@techpass/passport-openidconnect": "^0.3.0", "aws-sdk": "^2.901.0", "bcryptjs": "^2.4.3", + "cls-hooked": "^4.2.2", "ioredis": "^4.27.1", "jsonwebtoken": "^8.5.1", "koa-passport": "^4.1.4", diff --git a/packages/auth/src/cache/user.js b/packages/auth/src/cache/user.js index 46202cbfe9..4a19da489f 100644 --- a/packages/auth/src/cache/user.js +++ b/packages/auth/src/cache/user.js @@ -1,15 +1,21 @@ -const { getDB } = require("../db") -const { StaticDatabases } = require("../db/utils") const redis = require("../redis/authRedis") +const { getTenantId, lookupTenantId, getGlobalDB } = require("../tenancy") const EXPIRY_SECONDS = 3600 -exports.getUser = async userId => { +exports.getUser = async (userId, tenantId = null) => { + if (!tenantId) { + try { + tenantId = getTenantId() + } catch (err) { + tenantId = await lookupTenantId(userId) + } + } const client = await redis.getUserClient() // try cache let user = await client.get(userId) if (!user) { - user = await getDB(StaticDatabases.GLOBAL.name).get(userId) + user = await getGlobalDB(tenantId).get(userId) client.store(userId, user, EXPIRY_SECONDS) } return user diff --git a/packages/auth/src/constants.js b/packages/auth/src/constants.js index c8cc34e937..4b4aef5a42 100644 --- a/packages/auth/src/constants.js +++ b/packages/auth/src/constants.js @@ -14,13 +14,14 @@ exports.Headers = { API_VER: "x-budibase-api-version", APP_ID: "x-budibase-app-id", TYPE: "x-budibase-type", + TENANT_ID: "x-budibase-tenant-id", } exports.GlobalRoles = { OWNER: "owner", ADMIN: "admin", BUILDER: "builder", - GROUP_MANAGER: "group_manager", + WORKSPACE_MANAGER: "workspace_manager", } exports.Configs = { @@ -31,3 +32,5 @@ exports.Configs = { OIDC: "oidc", OIDC_LOGOS: "logos_oidc", } + +exports.DEFAULT_TENANT_ID = "default" diff --git a/packages/auth/src/db/constants.js b/packages/auth/src/db/constants.js new file mode 100644 index 0000000000..77643ce4c5 --- /dev/null +++ b/packages/auth/src/db/constants.js @@ -0,0 +1,17 @@ +exports.SEPARATOR = "_" + +exports.StaticDatabases = { + GLOBAL: { + name: "global-db", + docs: { + apiKeys: "apikeys", + }, + }, + // contains information about tenancy and so on + PLATFORM_INFO: { + name: "global-info", + docs: { + tenants: "tenants", + }, + }, +} diff --git a/packages/auth/src/db/utils.js b/packages/auth/src/db/utils.js index 100dc005c8..7d3a69ccd7 100644 --- a/packages/auth/src/db/utils.js +++ b/packages/auth/src/db/utils.js @@ -1,36 +1,36 @@ const { newid } = require("../hashing") const Replication = require("./Replication") +const { DEFAULT_TENANT_ID } = require("../constants") +const env = require("../environment") +const { StaticDatabases, SEPARATOR } = require("./constants") +const { getTenantId } = require("../tenancy") const UNICODE_MAX = "\ufff0" -const SEPARATOR = "_" exports.ViewNames = { USER_BY_EMAIL: "by_email", } -exports.StaticDatabases = { - GLOBAL: { - name: "global-db", - }, - DEPLOYMENTS: { - name: "deployments", - }, -} +exports.StaticDatabases = StaticDatabases + +const PRE_APP = "app" +const PRE_DEV = "dev" const DocumentTypes = { USER: "us", - GROUP: "group", + WORKSPACE: "workspace", CONFIG: "config", TEMPLATE: "template", - APP: "app", - APP_DEV: "app_dev", - APP_METADATA: "app_metadata", + APP: PRE_APP, + DEV: PRE_DEV, + APP_DEV: `${PRE_APP}${SEPARATOR}${PRE_DEV}`, + APP_METADATA: `${PRE_APP}${SEPARATOR}metadata`, ROLE: "role", } exports.DocumentTypes = DocumentTypes exports.APP_PREFIX = DocumentTypes.APP + SEPARATOR -exports.APP_DEV_PREFIX = DocumentTypes.APP_DEV + SEPARATOR +exports.APP_DEV = exports.APP_DEV_PREFIX = DocumentTypes.APP_DEV + SEPARATOR exports.SEPARATOR = SEPARATOR function isDevApp(app) { @@ -61,21 +61,21 @@ function getDocParams(docType, docId = null, otherProps = {}) { } /** - * Generates a new group ID. - * @returns {string} The new group ID which the group doc can be stored under. + * Generates a new workspace ID. + * @returns {string} The new workspace ID which the workspace doc can be stored under. */ -exports.generateGroupID = () => { - return `${DocumentTypes.GROUP}${SEPARATOR}${newid()}` +exports.generateWorkspaceID = () => { + return `${DocumentTypes.WORKSPACE}${SEPARATOR}${newid()}` } /** - * Gets parameters for retrieving groups. + * Gets parameters for retrieving workspaces. */ -exports.getGroupParams = (id = "", otherProps = {}) => { +exports.getWorkspaceParams = (id = "", otherProps = {}) => { return { ...otherProps, - startkey: `${DocumentTypes.GROUP}${SEPARATOR}${id}`, - endkey: `${DocumentTypes.GROUP}${SEPARATOR}${id}${UNICODE_MAX}`, + startkey: `${DocumentTypes.WORKSPACE}${SEPARATOR}${id}`, + endkey: `${DocumentTypes.WORKSPACE}${SEPARATOR}${id}${UNICODE_MAX}`, } } @@ -103,14 +103,14 @@ exports.getGlobalUserParams = (globalId, otherProps = {}) => { /** * Generates a template ID. - * @param ownerId The owner/user of the template, this could be global or a group level. + * @param ownerId The owner/user of the template, this could be global or a workspace level. */ exports.generateTemplateID = ownerId => { return `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}` } /** - * Gets parameters for retrieving templates. Owner ID must be specified, either global or a group level. + * Gets parameters for retrieving templates. Owner ID must be specified, either global or a workspace level. */ exports.getTemplateParams = (ownerId, templateId, otherProps = {}) => { if (!templateId) { @@ -163,11 +163,26 @@ exports.getDeployedAppID = appId => { * different users/companies apps as there is no security around it - all apps are returned. * @return {Promise} returns the app information document stored in each app database. */ -exports.getAllApps = async ({ CouchDB, dev, all } = {}) => { +exports.getAllApps = async (CouchDB, { dev, all } = {}) => { + let tenantId = getTenantId() + if (!env.MULTI_TENANCY && !tenantId) { + tenantId = DEFAULT_TENANT_ID + } let allDbs = await CouchDB.allDbs() - const appDbNames = allDbs.filter(dbName => - dbName.startsWith(exports.APP_PREFIX) - ) + const appDbNames = allDbs.filter(dbName => { + const split = dbName.split(SEPARATOR) + // it is an app, check the tenantId + if (split[0] === DocumentTypes.APP) { + const noTenantId = split.length === 2 || split[1] === DocumentTypes.DEV + // tenantId is always right before the UUID + const possibleTenantId = split[split.length - 2] + return ( + (tenantId === DEFAULT_TENANT_ID && noTenantId) || + possibleTenantId === tenantId + ) + } + return false + }) const appPromises = appDbNames.map(db => // skip setup otherwise databases could be re-created new CouchDB(db, { skip_setup: true }).get(DocumentTypes.APP_METADATA) @@ -214,8 +229,8 @@ exports.dbExists = async (CouchDB, dbName) => { * Generates a new configuration ID. * @returns {string} The new configuration ID which the config doc can be stored under. */ -const generateConfigID = ({ type, group, user }) => { - const scope = [type, group, user].filter(Boolean).join(SEPARATOR) +const generateConfigID = ({ type, workspace, user }) => { + const scope = [type, workspace, user].filter(Boolean).join(SEPARATOR) return `${DocumentTypes.CONFIG}${SEPARATOR}${scope}` } @@ -223,8 +238,8 @@ const generateConfigID = ({ type, group, user }) => { /** * Gets parameters for retrieving configurations. */ -const getConfigParams = ({ type, group, user }, otherProps = {}) => { - const scope = [type, group, user].filter(Boolean).join(SEPARATOR) +const getConfigParams = ({ type, workspace, user }, otherProps = {}) => { + const scope = [type, workspace, user].filter(Boolean).join(SEPARATOR) return { ...otherProps, @@ -234,15 +249,15 @@ const getConfigParams = ({ type, group, user }, otherProps = {}) => { } /** - * Returns the most granular configuration document from the DB based on the type, group and userID passed. + * Returns the most granular configuration document from the DB based on the type, workspace and userID passed. * @param {Object} db - db instance to query - * @param {Object} scopes - the type, group and userID scopes of the configuration. + * @param {Object} scopes - the type, workspace and userID scopes of the configuration. * @returns The most granular configuration document based on the scope. */ -const getScopedFullConfig = async function (db, { type, user, group }) { +const getScopedFullConfig = async function (db, { type, user, workspace }) { const response = await db.allDocs( getConfigParams( - { type, user, group }, + { type, user, workspace }, { include_docs: true, } @@ -252,14 +267,14 @@ const getScopedFullConfig = async function (db, { type, user, group }) { function determineScore(row) { const config = row.doc - // Config is specific to a user and a group - if (config._id.includes(generateConfigID({ type, user, group }))) { + // Config is specific to a user and a workspace + if (config._id.includes(generateConfigID({ type, user, workspace }))) { return 4 } else if (config._id.includes(generateConfigID({ type, user }))) { // Config is specific to a user only return 3 - } else if (config._id.includes(generateConfigID({ type, group }))) { - // Config is specific to a group only + } else if (config._id.includes(generateConfigID({ type, workspace }))) { + // Config is specific to a workspace only return 2 } else if (config._id.includes(generateConfigID({ type }))) { // Config is specific to a type only diff --git a/packages/auth/src/db/views.js b/packages/auth/src/db/views.js index 1f1f28b917..1b48786e24 100644 --- a/packages/auth/src/db/views.js +++ b/packages/auth/src/db/views.js @@ -1,5 +1,4 @@ -const { DocumentTypes, ViewNames, StaticDatabases } = require("./utils") -const { getDB } = require("./index") +const { DocumentTypes, ViewNames } = require("./utils") function DesignDoc() { return { @@ -10,8 +9,7 @@ function DesignDoc() { } } -exports.createUserEmailView = async () => { - const db = getDB(StaticDatabases.GLOBAL.name) +exports.createUserEmailView = async db => { let designDoc try { designDoc = await db.get("_design/database") diff --git a/packages/auth/src/environment.js b/packages/auth/src/environment.js index 355843d02d..e12918f3ac 100644 --- a/packages/auth/src/environment.js +++ b/packages/auth/src/environment.js @@ -16,6 +16,7 @@ module.exports = { MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY, MINIO_URL: process.env.MINIO_URL, INTERNAL_API_KEY: process.env.INTERNAL_API_KEY, + MULTI_TENANCY: process.env.MULTI_TENANCY, isTest, _set(key, value) { process.env[key] = value diff --git a/packages/auth/src/index.js b/packages/auth/src/index.js index 98c558706a..5421dea214 100644 --- a/packages/auth/src/index.js +++ b/packages/auth/src/index.js @@ -2,6 +2,7 @@ const passport = require("koa-passport") const LocalStrategy = require("passport-local").Strategy const JwtStrategy = require("passport-jwt").Strategy const { StaticDatabases } = require("./db/utils") +const { getGlobalDB } = require("./tenancy") const { jwt, local, @@ -9,8 +10,9 @@ const { google, oidc, auditLog, + tenancy, } = require("./middleware") -const { setDB, getDB } = require("./db") +const { setDB } = require("./db") const userCache = require("./cache/user") // Strategies @@ -20,7 +22,7 @@ passport.use(new JwtStrategy(jwt.options, jwt.authenticate)) passport.serializeUser((user, done) => done(null, user)) passport.deserializeUser(async (user, done) => { - const db = getDB(StaticDatabases.GLOBAL.name) + const db = getGlobalDB() try { const user = await db.get(user._id) @@ -54,6 +56,7 @@ module.exports = { google, oidc, jwt: require("jsonwebtoken"), + buildTenancyMiddleware: tenancy, auditLog, }, cache: { diff --git a/packages/auth/src/middleware/authenticated.js b/packages/auth/src/middleware/authenticated.js index 647ec659f5..e3705a9a24 100644 --- a/packages/auth/src/middleware/authenticated.js +++ b/packages/auth/src/middleware/authenticated.js @@ -2,46 +2,34 @@ const { Cookies, Headers } = require("../constants") const { getCookie, clearCookie } = require("../utils") const { getUser } = require("../cache/user") const { getSession, updateSessionTTL } = require("../security/sessions") +const { buildMatcherRegex, matches } = require("./matchers") const env = require("../environment") -const PARAM_REGEX = /\/:(.*?)\//g - -function buildNoAuthRegex(patterns) { - return patterns.map(pattern => { - const isObj = typeof pattern === "object" && pattern.route - const method = isObj ? pattern.method : "GET" - let route = isObj ? pattern.route : pattern - - const matches = route.match(PARAM_REGEX) - if (matches) { - for (let match of matches) { - route = route.replace(match, "/.*/") - } - } - return { regex: new RegExp(route), method } - }) -} - -function finalise(ctx, { authenticated, user, internal, version } = {}) { +function finalise( + ctx, + { authenticated, user, internal, version, publicEndpoint } = {} +) { + ctx.publicEndpoint = publicEndpoint || false ctx.isAuthenticated = authenticated || false ctx.user = user ctx.internal = internal || false ctx.version = version } -module.exports = (noAuthPatterns = [], opts) => { - const noAuthOptions = noAuthPatterns ? buildNoAuthRegex(noAuthPatterns) : [] +/** + * This middleware is tenancy aware, so that it does not depend on other middlewares being used. + * The tenancy modules should not be used here and it should be assumed that the tenancy context + * has not yet been populated. + */ +module.exports = (noAuthPatterns = [], opts = { publicAllowed: false }) => { + const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : [] return async (ctx, next) => { + let publicEndpoint = false const version = ctx.request.headers[Headers.API_VER] // the path is not authenticated - const found = noAuthOptions.find(({ regex, method }) => { - return ( - regex.test(ctx.request.url) && - ctx.request.method.toLowerCase() === method.toLowerCase() - ) - }) - if (found != null) { - return next() + const found = matches(ctx, noAuthOptions) + if (found) { + publicEndpoint = true } try { // check the actual user is authenticated first @@ -58,7 +46,7 @@ module.exports = (noAuthPatterns = [], opts) => { error = "No session found" } else { try { - user = await getUser(userId) + user = await getUser(userId, session.tenantId) delete user.password authenticated = true } catch (err) { @@ -66,6 +54,7 @@ module.exports = (noAuthPatterns = [], opts) => { } } if (error) { + console.error("Auth Error", error) // remove the cookie as the user does not exist anymore clearCookie(ctx, Cookies.Auth) } else { @@ -74,22 +63,26 @@ module.exports = (noAuthPatterns = [], opts) => { } } const apiKey = ctx.request.headers[Headers.API_KEY] + const tenantId = ctx.request.headers[Headers.TENANT_ID] // this is an internal request, no user made it if (!authenticated && apiKey && apiKey === env.INTERNAL_API_KEY) { authenticated = true internal = true } + if (!user && tenantId) { + user = { tenantId } + } // be explicit if (authenticated !== true) { authenticated = false } // isAuthenticated is a function, so use a variable to be able to check authed state - finalise(ctx, { authenticated, user, internal, version }) + finalise(ctx, { authenticated, user, internal, version, publicEndpoint }) return next() } catch (err) { // allow configuring for public access - if (opts && opts.publicAllowed) { - finalise(ctx, { authenticated: false, version }) + if ((opts && opts.publicAllowed) || publicEndpoint) { + finalise(ctx, { authenticated: false, version, publicEndpoint }) } else { ctx.throw(err.status || 403, err) } diff --git a/packages/auth/src/middleware/index.js b/packages/auth/src/middleware/index.js index 35c7d9c388..689859a139 100644 --- a/packages/auth/src/middleware/index.js +++ b/packages/auth/src/middleware/index.js @@ -4,6 +4,7 @@ const google = require("./passport/google") const oidc = require("./passport/oidc") const authenticated = require("./authenticated") const auditLog = require("./auditLog") +const tenancy = require("./tenancy") module.exports = { google, @@ -12,4 +13,5 @@ module.exports = { local, authenticated, auditLog, + tenancy, } diff --git a/packages/auth/src/middleware/matchers.js b/packages/auth/src/middleware/matchers.js new file mode 100644 index 0000000000..a555823136 --- /dev/null +++ b/packages/auth/src/middleware/matchers.js @@ -0,0 +1,33 @@ +const PARAM_REGEX = /\/:(.*?)(\/.*)?$/g + +exports.buildMatcherRegex = patterns => { + if (!patterns) { + return [] + } + return patterns.map(pattern => { + const isObj = typeof pattern === "object" && pattern.route + const method = isObj ? pattern.method : "GET" + let route = isObj ? pattern.route : pattern + + const matches = route.match(PARAM_REGEX) + if (matches) { + for (let match of matches) { + const pattern = "/.*" + (match.endsWith("/") ? "/" : "") + route = route.replace(match, pattern) + } + } + return { regex: new RegExp(route), method } + }) +} + +exports.matches = (ctx, options) => { + return options.find(({ regex, method }) => { + const urlMatch = regex.test(ctx.request.url) + const methodMatch = + method === "ALL" + ? true + : ctx.request.method.toLowerCase() === method.toLowerCase() + + return urlMatch && methodMatch + }) +} diff --git a/packages/auth/src/middleware/passport/google.js b/packages/auth/src/middleware/passport/google.js index 68fe885512..07d6816c0b 100644 --- a/packages/auth/src/middleware/passport/google.js +++ b/packages/auth/src/middleware/passport/google.js @@ -27,13 +27,13 @@ async function authenticate(accessToken, refreshToken, profile, done) { * from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport. * @returns Dynamically configured Passport Google Strategy */ -exports.strategyFactory = async function (config) { +exports.strategyFactory = async function (config, callbackUrl) { try { - const { clientID, clientSecret, callbackURL } = config + const { clientID, clientSecret } = config - if (!clientID || !clientSecret || !callbackURL) { + if (!clientID || !clientSecret) { throw new Error( - "Configuration invalid. Must contain google clientID, clientSecret and callbackURL" + "Configuration invalid. Must contain google clientID and clientSecret" ) } @@ -41,7 +41,7 @@ exports.strategyFactory = async function (config) { { clientID: config.clientID, clientSecret: config.clientSecret, - callbackURL: config.callbackURL, + callbackURL: callbackUrl, }, authenticate ) diff --git a/packages/auth/src/middleware/passport/local.js b/packages/auth/src/middleware/passport/local.js index 16b53bf894..0db40d64eb 100644 --- a/packages/auth/src/middleware/passport/local.js +++ b/packages/auth/src/middleware/passport/local.js @@ -6,19 +6,23 @@ const { getGlobalUserByEmail } = require("../../utils") const { authError } = require("./utils") const { newid } = require("../../hashing") const { createASession } = require("../../security/sessions") +const { getTenantId } = require("../../tenancy") const INVALID_ERR = "Invalid Credentials" -exports.options = {} +exports.options = { + passReqToCallback: true, +} /** * Passport Local Authentication Middleware. - * @param {*} email - username to login with - * @param {*} password - plain text password to log in with - * @param {*} done - callback from passport to return user information and errors + * @param {*} ctx the request structure + * @param {*} email username to login with + * @param {*} password plain text password to log in with + * @param {*} done callback from passport to return user information and errors * @returns The authenticated user, or errors if they occur */ -exports.authenticate = async function (email, password, done) { +exports.authenticate = async function (ctx, email, password, done) { if (!email) return authError(done, "Email Required") if (!password) return authError(done, "Password Required") @@ -35,12 +39,14 @@ exports.authenticate = async function (email, password, done) { // authenticate if (await compare(password, dbUser.password)) { const sessionId = newid() - await createASession(dbUser._id, sessionId) + const tenantId = getTenantId() + await createASession(dbUser._id, { sessionId, tenantId }) dbUser.token = jwt.sign( { userId: dbUser._id, sessionId, + tenantId, }, env.JWT_SECRET ) diff --git a/packages/auth/src/middleware/passport/tests/google.spec.js b/packages/auth/src/middleware/passport/tests/google.spec.js index 30e582a68f..9cc878bba9 100644 --- a/packages/auth/src/middleware/passport/tests/google.spec.js +++ b/packages/auth/src/middleware/passport/tests/google.spec.js @@ -2,8 +2,9 @@ const { data } = require("./utilities/mock-data") +const TENANT_ID = "default" + const googleConfig = { - callbackURL: "http://somecallbackurl", clientID: data.clientID, clientSecret: data.clientSecret, } @@ -26,13 +27,14 @@ describe("google", () => { it("should create successfully create a google strategy", async () => { const google = require("../google") - - await google.strategyFactory(googleConfig) + + const callbackUrl = `/api/global/auth/${TENANT_ID}/google/callback` + await google.strategyFactory(googleConfig, callbackUrl) const expectedOptions = { clientID: googleConfig.clientID, clientSecret: googleConfig.clientSecret, - callbackURL: googleConfig.callbackURL, + callbackURL: callbackUrl, } expect(mockStrategy).toHaveBeenCalledWith( diff --git a/packages/auth/src/middleware/passport/third-party-common.js b/packages/auth/src/middleware/passport/third-party-common.js index 2ab2816391..7c03944232 100644 --- a/packages/auth/src/middleware/passport/third-party-common.js +++ b/packages/auth/src/middleware/passport/third-party-common.js @@ -1,11 +1,12 @@ const env = require("../../environment") const jwt = require("jsonwebtoken") -const database = require("../../db") -const { StaticDatabases, generateGlobalUserID } = require("../../db/utils") +const { generateGlobalUserID } = require("../../db/utils") const { authError } = require("./utils") const { newid } = require("../../hashing") const { createASession } = require("../../security/sessions") const { getGlobalUserByEmail } = require("../../utils") +const { getGlobalDB, getTenantId } = require("../../tenancy") +const fetch = require("node-fetch") /** * Common authentication logic for third parties. e.g. OAuth, OIDC. @@ -15,19 +16,21 @@ exports.authenticateThirdParty = async function ( requireLocalAccount = true, done ) { - if (!thirdPartyUser.provider) + if (!thirdPartyUser.provider) { return authError(done, "third party user provider required") - if (!thirdPartyUser.userId) + } + if (!thirdPartyUser.userId) { return authError(done, "third party user id required") - if (!thirdPartyUser.email) + } + if (!thirdPartyUser.email) { return authError(done, "third party user email required") - - const db = database.getDB(StaticDatabases.GLOBAL.name) - - let dbUser + } // use the third party id const userId = generateGlobalUserID(thirdPartyUser.userId) + const db = getGlobalDB() + + let dbUser // try to load by id try { @@ -65,7 +68,7 @@ exports.authenticateThirdParty = async function ( } } - dbUser = syncUser(dbUser, thirdPartyUser) + dbUser = await syncUser(dbUser, thirdPartyUser) // create or sync the user const response = await db.post(dbUser) @@ -73,7 +76,8 @@ exports.authenticateThirdParty = async function ( // authenticate const sessionId = newid() - await createASession(dbUser._id, sessionId) + const tenantId = getTenantId() + await createASession(dbUser._id, { sessionId, tenantId }) dbUser.token = jwt.sign( { @@ -86,10 +90,26 @@ exports.authenticateThirdParty = async function ( return done(null, dbUser) } +async function syncProfilePicture(user, thirdPartyUser) { + const pictureUrl = thirdPartyUser.profile._json.picture + if (pictureUrl) { + const response = await fetch(pictureUrl) + + if (response.status === 200) { + const type = response.headers.get("content-type") + if (type.startsWith("image/")) { + user.pictureUrl = pictureUrl + } + } + } + + return user +} + /** * @returns a user that has been sync'd with third party information */ -function syncUser(user, thirdPartyUser) { +async function syncUser(user, thirdPartyUser) { // provider user.provider = thirdPartyUser.provider user.providerType = thirdPartyUser.providerType @@ -112,6 +132,8 @@ function syncUser(user, thirdPartyUser) { } } + user = await syncProfilePicture(user, thirdPartyUser) + // profile user.thirdPartyProfile = { ...profile._json, diff --git a/packages/auth/src/middleware/tenancy.js b/packages/auth/src/middleware/tenancy.js new file mode 100644 index 0000000000..b80b9a6763 --- /dev/null +++ b/packages/auth/src/middleware/tenancy.js @@ -0,0 +1,14 @@ +const { setTenantId } = require("../tenancy") +const ContextFactory = require("../tenancy/FunctionContext") +const { buildMatcherRegex, matches } = require("./matchers") + +module.exports = (allowQueryStringPatterns, noTenancyPatterns) => { + const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns) + const noTenancyOptions = buildMatcherRegex(noTenancyPatterns) + + return ContextFactory.getMiddleware(ctx => { + const allowNoTenant = !!matches(ctx, noTenancyOptions) + const allowQs = !!matches(ctx, allowQsOptions) + setTenantId(ctx, { allowQs, allowNoTenant }) + }) +} diff --git a/packages/auth/src/security/sessions.js b/packages/auth/src/security/sessions.js index 4051df7123..328f74c794 100644 --- a/packages/auth/src/security/sessions.js +++ b/packages/auth/src/security/sessions.js @@ -12,12 +12,13 @@ function makeSessionID(userId, sessionId) { return `${userId}/${sessionId}` } -exports.createASession = async (userId, sessionId) => { +exports.createASession = async (userId, session) => { const client = await redis.getSessionClient() - const session = { + const sessionId = session.sessionId + session = { createdAt: new Date().toISOString(), lastAccessedAt: new Date().toISOString(), - sessionId, + ...session, userId, } await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS) diff --git a/packages/auth/src/tenancy/FunctionContext.js b/packages/auth/src/tenancy/FunctionContext.js new file mode 100644 index 0000000000..d97a3a30b4 --- /dev/null +++ b/packages/auth/src/tenancy/FunctionContext.js @@ -0,0 +1,73 @@ +const cls = require("cls-hooked") +const { newid } = require("../hashing") + +const REQUEST_ID_KEY = "requestId" + +class FunctionContext { + static getMiddleware(updateCtxFn = null) { + const namespace = this.createNamespace() + + return async function (ctx, next) { + await new Promise( + namespace.bind(function (resolve, reject) { + // store a contextual request ID that can be used anywhere (audit logs) + namespace.set(REQUEST_ID_KEY, newid()) + namespace.bindEmitter(ctx.req) + namespace.bindEmitter(ctx.res) + + if (updateCtxFn) { + updateCtxFn(ctx) + } + next().then(resolve).catch(reject) + }) + ) + } + } + + static run(callback) { + const namespace = this.createNamespace() + + return namespace.runAndReturn(callback) + } + + static setOnContext(key, value) { + const namespace = this.createNamespace() + namespace.set(key, value) + } + + static getContextStorage() { + if (this._namespace && this._namespace.active) { + let contextData = this._namespace.active + delete contextData.id + delete contextData._ns_name + return contextData + } + + return {} + } + + static getFromContext(key) { + const context = this.getContextStorage() + if (context) { + return context[key] + } else { + return null + } + } + + static destroyNamespace() { + if (this._namespace) { + cls.destroyNamespace("session") + this._namespace = null + } + } + + static createNamespace() { + if (!this._namespace) { + this._namespace = cls.createNamespace("session") + } + return this._namespace + } +} + +module.exports = FunctionContext diff --git a/packages/auth/src/tenancy/context.js b/packages/auth/src/tenancy/context.js new file mode 100644 index 0000000000..f3f1f541e9 --- /dev/null +++ b/packages/auth/src/tenancy/context.js @@ -0,0 +1,81 @@ +const env = require("../environment") +const { Headers } = require("../../constants") +const cls = require("./FunctionContext") + +exports.DEFAULT_TENANT_ID = "default" + +exports.isDefaultTenant = () => { + return exports.getTenantId() === exports.DEFAULT_TENANT_ID +} + +exports.isMultiTenant = () => { + return env.MULTI_TENANCY +} + +const TENANT_ID = "tenantId" + +// used for automations, API endpoints should always be in context already +exports.doInTenant = (tenantId, task) => { + return cls.run(() => { + // set the tenant id + cls.setOnContext(TENANT_ID, tenantId) + + // invoke the task + const result = task() + + return result + }) +} + +exports.updateTenantId = tenantId => { + cls.setOnContext(TENANT_ID, tenantId) +} + +exports.setTenantId = ( + ctx, + opts = { allowQs: false, allowNoTenant: false } +) => { + let tenantId + // exit early if not multi-tenant + if (!exports.isMultiTenant()) { + cls.setOnContext(TENANT_ID, this.DEFAULT_TENANT_ID) + return + } + + const allowQs = opts && opts.allowQs + const allowNoTenant = opts && opts.allowNoTenant + const header = ctx.request.headers[Headers.TENANT_ID] + const user = ctx.user || {} + if (allowQs) { + const query = ctx.request.query || {} + tenantId = query.tenantId + } + // override query string (if allowed) by user, or header + // URL params cannot be used in a middleware, as they are + // processed later in the chain + tenantId = user.tenantId || header || tenantId + + if (!tenantId && !allowNoTenant) { + ctx.throw(403, "Tenant id not set") + } + // check tenant ID just incase no tenant was allowed + if (tenantId) { + cls.setOnContext(TENANT_ID, tenantId) + } +} + +exports.isTenantIdSet = () => { + const tenantId = cls.getFromContext(TENANT_ID) + return !!tenantId +} + +exports.getTenantId = () => { + if (!exports.isMultiTenant()) { + return exports.DEFAULT_TENANT_ID + } + const tenantId = cls.getFromContext(TENANT_ID) + if (!tenantId) { + throw Error("Tenant id not found") + } + return tenantId +} diff --git a/packages/auth/src/tenancy/index.js b/packages/auth/src/tenancy/index.js new file mode 100644 index 0000000000..2fe257d885 --- /dev/null +++ b/packages/auth/src/tenancy/index.js @@ -0,0 +1,4 @@ +module.exports = { + ...require("./context"), + ...require("./tenancy"), +} diff --git a/packages/auth/src/tenancy/tenancy.js b/packages/auth/src/tenancy/tenancy.js new file mode 100644 index 0000000000..6e18ea7154 --- /dev/null +++ b/packages/auth/src/tenancy/tenancy.js @@ -0,0 +1,105 @@ +const { getDB } = require("../db") +const { SEPARATOR, StaticDatabases } = require("../db/constants") +const { getTenantId, DEFAULT_TENANT_ID, isMultiTenant } = require("./context") +const env = require("../environment") + +const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants +const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name + +exports.addTenantToUrl = url => { + const tenantId = getTenantId() + + if (isMultiTenant()) { + const char = url.indexOf("?") === -1 ? "?" : "&" + url += `${char}tenantId=${tenantId}` + } + + return url +} + +exports.doesTenantExist = async tenantId => { + const db = getDB(PLATFORM_INFO_DB) + let tenants + try { + tenants = await db.get(TENANT_DOC) + } catch (err) { + // if theres an error the doc doesn't exist, no tenants exist + return false + } + return ( + tenants && + Array.isArray(tenants.tenantIds) && + tenants.tenantIds.indexOf(tenantId) !== -1 + ) +} + +exports.tryAddTenant = async (tenantId, userId, email) => { + const db = getDB(PLATFORM_INFO_DB) + const getDoc = async id => { + if (!id) { + return null + } + try { + return await db.get(id) + } catch (err) { + return { _id: id } + } + } + let [tenants, userIdDoc, emailDoc] = await Promise.all([ + getDoc(TENANT_DOC), + getDoc(userId), + getDoc(email), + ]) + if (!Array.isArray(tenants.tenantIds)) { + tenants = { + _id: TENANT_DOC, + tenantIds: [], + } + } + let promises = [] + if (userIdDoc) { + userIdDoc.tenantId = tenantId + promises.push(db.put(userIdDoc)) + } + if (emailDoc) { + emailDoc.tenantId = tenantId + promises.push(db.put(emailDoc)) + } + if (tenants.tenantIds.indexOf(tenantId) === -1) { + tenants.tenantIds.push(tenantId) + promises.push(db.put(tenants)) + } + await Promise.all(promises) +} + +exports.getGlobalDB = (tenantId = null) => { + // tenant ID can be set externally, for example user API where + // new tenants are being created, this may be the case + if (!tenantId) { + tenantId = getTenantId() + } + + let dbName + + if (tenantId === DEFAULT_TENANT_ID) { + dbName = StaticDatabases.GLOBAL.name + } else { + dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}` + } + + return getDB(dbName) +} + +exports.lookupTenantId = async userId => { + const db = getDB(StaticDatabases.PLATFORM_INFO.name) + let tenantId = env.MULTI_TENANCY ? DEFAULT_TENANT_ID : null + try { + const doc = await db.get(userId) + if (doc && doc.tenantId) { + tenantId = doc.tenantId + } + } catch (err) { + // just return the default + } + return tenantId +} diff --git a/packages/auth/src/utils.js b/packages/auth/src/utils.js index 6bc1e0e3a6..5936948fd7 100644 --- a/packages/auth/src/utils.js +++ b/packages/auth/src/utils.js @@ -1,14 +1,9 @@ -const { - DocumentTypes, - SEPARATOR, - ViewNames, - StaticDatabases, -} = require("./db/utils") +const { DocumentTypes, SEPARATOR, ViewNames } = require("./db/utils") const jwt = require("jsonwebtoken") const { options } = require("./middleware/passport/jwt") const { createUserEmailView } = require("./db/views") -const { getDB } = require("./db") const { Headers } = require("./constants") +const { getGlobalDB } = require("./tenancy") const APP_PREFIX = DocumentTypes.APP + SEPARATOR @@ -111,7 +106,7 @@ exports.getGlobalUserByEmail = async email => { if (email == null) { throw "Must supply an email address to view" } - const db = getDB(StaticDatabases.GLOBAL.name) + const db = getGlobalDB() try { let users = ( await db.query(`database/${ViewNames.USER_BY_EMAIL}`, { @@ -123,7 +118,7 @@ exports.getGlobalUserByEmail = async email => { return users.length <= 1 ? users[0] : users } catch (err) { if (err != null && err.name === "not_found") { - await createUserEmailView() + await createUserEmailView(db) return exports.getGlobalUserByEmail(email) } else { throw err diff --git a/packages/auth/tenancy.js b/packages/auth/tenancy.js new file mode 100644 index 0000000000..9ca808b74e --- /dev/null +++ b/packages/auth/tenancy.js @@ -0,0 +1 @@ +module.exports = require("./src/tenancy") diff --git a/packages/auth/yarn.lock b/packages/auth/yarn.lock index 8957ecb0fc..b6be8ad1e8 100644 --- a/packages/auth/yarn.lock +++ b/packages/auth/yarn.lock @@ -798,6 +798,13 @@ ast-types@0.9.6: resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9" integrity sha1-ECyenpAF0+fjgpvwxPok7oYu6bk= +async-hook-jl@^1.7.6: + version "1.7.6" + resolved "https://registry.yarnpkg.com/async-hook-jl/-/async-hook-jl-1.7.6.tgz#4fd25c2f864dbaf279c610d73bf97b1b28595e68" + integrity sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg== + dependencies: + stack-chain "^1.3.7" + async@~2.1.4: version "2.1.5" resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc" @@ -1144,6 +1151,15 @@ clone-buffer@1.0.0: resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg= +cls-hooked@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/cls-hooked/-/cls-hooked-4.2.2.tgz#ad2e9a4092680cdaffeb2d3551da0e225eae1908" + integrity sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw== + dependencies: + async-hook-jl "^1.7.6" + emitter-listener "^1.0.1" + semver "^5.4.1" + cluster-key-slot@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d" @@ -1444,6 +1460,13 @@ electron-to-chromium@^1.3.723: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.775.tgz#046517d1f2cea753e06fff549995b9dc45e20082" integrity sha512-EGuiJW4yBPOTj2NtWGZcX93ZE8IGj33HJAx4d3ouE2zOfW2trbWU+t1e0yzLr1qQIw81++txbM3BH52QwSRE6Q== +emitter-listener@^1.0.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/emitter-listener/-/emitter-listener-1.1.2.tgz#56b140e8f6992375b3d7cb2cab1cc7432d9632e8" + integrity sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ== + dependencies: + shimmer "^1.2.0" + emittery@^0.7.1: version "0.7.2" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82" @@ -4035,7 +4058,7 @@ saxes@^5.0.1: dependencies: xmlchars "^2.2.0" -"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0: +"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -4096,6 +4119,11 @@ shellwords@^0.1.1: resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== +shimmer@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" + integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" @@ -4250,6 +4278,11 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" +stack-chain@^1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-1.3.7.tgz#d192c9ff4ea6a22c94c4dd459171e3f00cea1285" + integrity sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU= + stack-utils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz#cd5f030126ff116b78ccb3c027fe302713b61277" diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 9a6bf8e64b..40448b0c43 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "0.9.82", + "version": "0.9.99-alpha.6", "license": "AGPL-3.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", diff --git a/packages/bbui/src/Form/Checkbox.svelte b/packages/bbui/src/Form/Checkbox.svelte index 90a2cddda5..6aa88f6dee 100644 --- a/packages/bbui/src/Form/Checkbox.svelte +++ b/packages/bbui/src/Form/Checkbox.svelte @@ -9,6 +9,7 @@ export let text = null export let disabled = false export let error = null + export let size = "M" const dispatch = createEventDispatcher() const onChange = e => { @@ -18,5 +19,5 @@ - + diff --git a/packages/bbui/src/Form/Core/Checkbox.svelte b/packages/bbui/src/Form/Core/Checkbox.svelte index e9a6b56fd9..74745aa9a6 100644 --- a/packages/bbui/src/Form/Core/Checkbox.svelte +++ b/packages/bbui/src/Form/Core/Checkbox.svelte @@ -8,6 +8,7 @@ export let id = null export let text = null export let disabled = false + export let size = null const dispatch = createEventDispatcher() const onChange = event => { @@ -16,7 +17,7 @@