diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aaf8489019..6316bf1837 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,6 @@ name: Budibase Release -on: +on: push: branches: - master @@ -15,20 +15,20 @@ env: POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }} POSTHOG_URL: ${{ secrets.POSTHOG_URL }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} - + jobs: release: - runs-on: ubuntu-latest + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: node-version: 12.x - - run: yarn - - run: yarn bootstrap - - run: yarn lint - - run: yarn build + - run: yarn + - run: yarn bootstrap + - run: yarn lint + - run: yarn build - run: yarn test - name: Configure AWS Credentials @@ -41,11 +41,11 @@ jobs: - name: Publish budibase packages to NPM env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: | + run: | # setup the username and email. I tend to use 'GitHub Actions Bot' with no email by default git config user.name "Budibase Release Bot" git config user.email "<>" - echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc + echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc yarn release - name: 'Get Previous tag' @@ -86,4 +86,3 @@ jobs: charts_dir: docs env: CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - \ No newline at end of file diff --git a/LICENSE b/LICENSE index a6bd926020..9c0a8c22a0 100644 --- a/LICENSE +++ b/LICENSE @@ -5,8 +5,7 @@ Each Budibase package has its own license: builder: GPLv3 server: GPLv3 client: MPLv2.0 -standard-components: MPLv2.0 -You can consider Budibase to be GPLv3 licensed. +You can consider Budibase to be GPLv3 licensed. The apps that you build with Budibase do not fall under GPLv3 - hence why our components and client library are licensed differently. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..e414f48cb8 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,15 @@ +# Security Policy + +## Versions + +As an open source product, we will only patch the latest major version for security vulnerabilities. Previous versions of budibase will not be retroactively patched. + +## Disclosing + +You can get in touch with us regarding a vulnerability via email at community@budibase.com. + +You can also disclose via huntr.dev. If you believe you have found a vulnerability, please disclose it on huntr and let us know. + +https://huntr.dev/bounties/disclose + +This will enable us to review the vulnerability and potentially reward you for your work. diff --git a/hosting/docker-compose.yaml b/hosting/docker-compose.yaml index 560b273668..18b93fdf61 100644 --- a/hosting/docker-compose.yaml +++ b/hosting/docker-compose.yaml @@ -51,6 +51,7 @@ services: INTERNAL_API_KEY: ${INTERNAL_API_KEY} REDIS_URL: redis-service:6379 REDIS_PASSWORD: ${REDIS_PASSWORD} + ACCOUNT_PORTAL_URL: https://portal.budi.live volumes: - ./logs:/logs depends_on: @@ -107,7 +108,7 @@ services: depends_on: - couchdb-service command: ["sh","-c","sleep 10 && $${PUT_CALL}/_users && $${PUT_CALL}/_replicator; fg;"] - + redis-service: restart: always image: redis @@ -116,7 +117,7 @@ services: - "${REDIS_PORT}:6379" volumes: - redis_data:/data - + watchtower-service: image: containrrr/watchtower ports: diff --git a/hosting/kubernetes/budibase/templates/worker-service-deployment.yaml b/hosting/kubernetes/budibase/templates/worker-service-deployment.yaml index 703d59c075..6c165872c8 100644 --- a/hosting/kubernetes/budibase/templates/worker-service-deployment.yaml +++ b/hosting/kubernetes/budibase/templates/worker-service-deployment.yaml @@ -87,6 +87,8 @@ spec: {{ end }} - name: SELF_HOSTED value: {{ .Values.globals.selfHosted | quote }} + - name: ACCOUNT_PORTAL_URL + value: {{ .Values.globals.accountPortalUrl | quote }} image: budibase/worker imagePullPolicy: Always name: bbworker diff --git a/hosting/kubernetes/budibase/values.yaml b/hosting/kubernetes/budibase/values.yaml index 30594f95e3..1113842c8b 100644 --- a/hosting/kubernetes/budibase/values.yaml +++ b/hosting/kubernetes/budibase/values.yaml @@ -44,7 +44,7 @@ ingress: nginx: true certificateArn: "" className: "" - annotations: + annotations: kubernetes.io/ingress.class: nginx hosts: - host: # change if using custom domain @@ -55,7 +55,7 @@ ingress: service: name: proxy-service port: - number: 10000 + number: 10000 resources: {} # We usually recommend not to specify default resources and to leave this as a conscious @@ -86,9 +86,10 @@ globals: budibaseEnv: PRODUCTION enableAnalytics: false posthogToken: "" - sentryDSN: "" + sentryDSN: "" logLevel: info selfHosted: 1 + accountPortalUrL: "" createSecrets: true # creates an internal API key, JWT secrets and redis password for you # if createSecrets is set to false, you can hard-code your secrets here @@ -120,7 +121,7 @@ services: password: "" # only change if pointing to existing couch server port: 5984 storage: 100Mi - + redis: enabled: true # disable if using external redis port: 6379 @@ -128,15 +129,15 @@ services: url: "" # only change if pointing to existing redis cluster and enabled: false password: "budibase" # recommended to override if using built-in redis storage: 100Mi - + objectStore: minio: true browser: true port: 9000 replicaCount: 1 accessKey: "" # AWS_ACCESS_KEY if using S3 or existing minio access key - secretKey: "" # AWS_SECRET_ACCESS_KEY if using S3 or existing minio secret - region: "" # AWS_REGION if using S3 or existing minio secret - url: "" # only change if pointing to existing minio cluster and minio: false + secretKey: "" # AWS_SECRET_ACCESS_KEY if using S3 or existing minio secret + region: "" # AWS_REGION if using S3 or existing minio secret + url: "" # only change if pointing to existing minio cluster and minio: false storage: 100Mi diff --git a/hosting/kubernetes/envoy/envoy.yaml b/hosting/kubernetes/envoy/envoy.yaml index 0b4c9204eb..4bf751b3a3 100644 --- a/hosting/kubernetes/envoy/envoy.yaml +++ b/hosting/kubernetes/envoy/envoy.yaml @@ -28,12 +28,12 @@ static_resources: - match: { prefix: "/builder" } route: cluster: app-service - + - match: { prefix: "/app_" } 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 @@ -50,13 +50,13 @@ static_resources: route: cluster: app-service - # special case for when API requests are made, can just forward, not to minio + # special case for when API requests are made, can just forward, not to minio - match: { prefix: "/api/" } route: cluster: app-service - match: { prefix: "/worker/" } - route: + route: cluster: worker-service prefix_rewrite: "/" @@ -85,7 +85,7 @@ static_resources: - lb_endpoints: - endpoint: address: - socket_address: + socket_address: address: app-service.budibase.svc.cluster.local port_value: 4002 @@ -113,7 +113,7 @@ static_resources: - lb_endpoints: - endpoint: address: - socket_address: + socket_address: address: worker-service.budibase.svc.cluster.local port_value: 4001 diff --git a/lerna.json b/lerna.json index 93e1890431..9b2b1cac6d 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "0.9.139", + "version": "0.9.140-alpha.0", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/package.json b/package.json index 7e329faafa..3df577ca58 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev", "bootstrap": "lerna link && lerna bootstrap", "build": "lerna run build", - "initialise": "lerna run initialise", "publishdev": "lerna run publishdev", "publishnpm": "yarn build && lerna publish --force-publish", "release": "yarn build && lerna publish patch --yes --force-publish", @@ -49,6 +48,8 @@ "release:helm": "./scripts/release_helm_chart.sh", "multi:enable": "lerna run multi:enable", "multi:disable": "lerna run multi:disable", + "selfhost:enable": "lerna run selfhost:enable", + "selfhost:disable": "lerna run selfhost:disable", "postinstall": "husky install" } } diff --git a/packages/auth/package.json b/packages/auth/package.json index 32dc672a71..448b408742 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/auth", - "version": "0.9.139", + "version": "0.9.140-alpha.0", "description": "Authentication middlewares for budibase builder and apps", "main": "src/index.js", "author": "Budibase", diff --git a/packages/auth/scripts/jestSetup.js b/packages/auth/scripts/jestSetup.js index 07648f693f..93dbf3fd5a 100644 --- a/packages/auth/scripts/jestSetup.js +++ b/packages/auth/scripts/jestSetup.js @@ -1,5 +1,6 @@ const env = require("../src/environment") +env._set("SELF_HOSTED", "1") env._set("NODE_ENV", "jest") env._set("JWT_SECRET", "test-jwtsecret") env._set("LOG_LEVEL", "silent") diff --git a/packages/auth/src/cache/user.js b/packages/auth/src/cache/user.js index e0936182a7..51bed0210e 100644 --- a/packages/auth/src/cache/user.js +++ b/packages/auth/src/cache/user.js @@ -1,9 +1,42 @@ const redis = require("../redis/authRedis") const { getTenantId, lookupTenantId, getGlobalDB } = require("../tenancy") +const env = require("../environment") +const accounts = require("../cloud/accounts") const EXPIRY_SECONDS = 3600 -exports.getUser = async (userId, tenantId = null) => { +/** + * The default populate user function + */ +const populateFromDB = async (userId, tenantId) => { + const user = await getGlobalDB(tenantId).get(userId) + user.budibaseAccess = true + + if (!env.SELF_HOSTED) { + const account = await accounts.getAccount(user.email) + if (account) { + user.account = account + user.accountPortalAccess = true + } + } + + return user +} + +/** + * Get the requested user by id. + * Use redis cache to first read the user. + * If not present fallback to loading the user directly and re-caching. + * @param {*} userId the id of the user to get + * @param {*} tenantId the tenant of the user to get + * @param {*} populateUser function to provide the user for re-caching. default to couch db + * @returns + */ +exports.getUser = async ( + userId, + tenantId = null, + populateUser = populateFromDB +) => { if (!tenantId) { try { tenantId = getTenantId() @@ -15,7 +48,7 @@ exports.getUser = async (userId, tenantId = null) => { // try cache let user = await client.get(userId) if (!user) { - user = await getGlobalDB(tenantId).get(userId) + user = await populateUser(userId, tenantId) client.store(userId, user, EXPIRY_SECONDS) } if (user && !user.tenantId && tenantId) { diff --git a/packages/auth/src/cloud/accounts.js b/packages/auth/src/cloud/accounts.js new file mode 100644 index 0000000000..a102df8920 --- /dev/null +++ b/packages/auth/src/cloud/accounts.js @@ -0,0 +1,22 @@ +const API = require("./api") +const env = require("../environment") + +const api = new API(env.ACCOUNT_PORTAL_URL) + +// TODO: Authorization + +exports.getAccount = async email => { + const payload = { + email, + } + const response = await api.post(`/api/accounts/search`, { + body: payload, + }) + const json = await response.json() + + if (response.status !== 200) { + throw Error(`Error getting account by email ${email}`, json) + } + + return json[0] +} diff --git a/packages/auth/src/cloud/api.js b/packages/auth/src/cloud/api.js new file mode 100644 index 0000000000..ffa785d02a --- /dev/null +++ b/packages/auth/src/cloud/api.js @@ -0,0 +1,44 @@ +const fetch = require("node-fetch") +class API { + constructor(host) { + this.host = host + } + + apiCall = + method => + async (url = "", options = {}) => { + if (!options.headers) { + options.headers = {} + } + + if (!options.headers["Content-Type"]) { + options.headers = { + "Content-Type": "application/json", + Accept: "application/json", + ...options.headers, + } + } + + let json = options.headers["Content-Type"] === "application/json" + + const requestOptions = { + method: method, + body: json ? JSON.stringify(options.body) : options.body, + headers: options.headers, + // TODO: See if this is necessary + credentials: "include", + } + + const resp = await fetch(`${this.host}${url}`, requestOptions) + + return resp + } + + post = this.apiCall("POST") + get = this.apiCall("GET") + patch = this.apiCall("PATCH") + del = this.apiCall("DELETE") + put = this.apiCall("PUT") +} + +module.exports = API diff --git a/packages/auth/src/db/utils.js b/packages/auth/src/db/utils.js index 65f530fc7b..a1a831523e 100644 --- a/packages/auth/src/db/utils.js +++ b/packages/auth/src/db/utils.js @@ -35,10 +35,6 @@ exports.APP_PREFIX = DocumentTypes.APP + SEPARATOR exports.APP_DEV = exports.APP_DEV_PREFIX = DocumentTypes.APP_DEV + SEPARATOR exports.SEPARATOR = SEPARATOR -function isDevApp(app) { - return app.appId.startsWith(exports.APP_DEV_PREFIX) -} - /** * If creating DB allDocs/query params with only a single top level ID this can be used, this * is usually the case as most of our docs are top level e.g. tables, automations, users and so on. @@ -62,6 +58,18 @@ function getDocParams(docType, docId = null, otherProps = {}) { } } +exports.isDevAppID = appId => { + return appId.startsWith(exports.APP_DEV_PREFIX) +} + +exports.isProdAppID = appId => { + return appId.startsWith(exports.APP_PREFIX) && !exports.isDevAppID(appId) +} + +function isDevApp(app) { + return exports.isDevAppID(app.appId) +} + /** * Given an app ID this will attempt to retrieve the tenant ID from it. * @return {null|string} The tenant ID found within the app ID. diff --git a/packages/auth/src/environment.js b/packages/auth/src/environment.js index cddd7ab98a..bae5c65a1b 100644 --- a/packages/auth/src/environment.js +++ b/packages/auth/src/environment.js @@ -20,6 +20,8 @@ module.exports = { MINIO_URL: process.env.MINIO_URL, INTERNAL_API_KEY: process.env.INTERNAL_API_KEY, MULTI_TENANCY: process.env.MULTI_TENANCY, + ACCOUNT_PORTAL_URL: process.env.ACCOUNT_PORTAL_URL, + SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED), isTest, _set(key, value) { process.env[key] = value diff --git a/packages/auth/src/middleware/authenticated.js b/packages/auth/src/middleware/authenticated.js index e3705a9a24..944f3ee9d9 100644 --- a/packages/auth/src/middleware/authenticated.js +++ b/packages/auth/src/middleware/authenticated.js @@ -21,7 +21,10 @@ function finalise( * 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 }) => { +module.exports = ( + noAuthPatterns = [], + opts = { publicAllowed: false, populateUser: null } +) => { const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : [] return async (ctx, next) => { let publicEndpoint = false @@ -46,7 +49,15 @@ module.exports = (noAuthPatterns = [], opts = { publicAllowed: false }) => { error = "No session found" } else { try { - user = await getUser(userId, session.tenantId) + if (opts && opts.populateUser) { + user = await getUser( + userId, + session.tenantId, + opts.populateUser(ctx) + ) + } else { + user = await getUser(userId, session.tenantId) + } delete user.password authenticated = true } catch (err) { diff --git a/packages/auth/src/redis/utils.js b/packages/auth/src/redis/utils.js index 09b4905298..6befecd9ba 100644 --- a/packages/auth/src/redis/utils.js +++ b/packages/auth/src/redis/utils.js @@ -8,11 +8,13 @@ const REDIS_PASSWORD = !env.REDIS_PASSWORD ? "budibase" : env.REDIS_PASSWORD exports.Databases = { PW_RESETS: "pwReset", + VERIFICATIONS: "verification", INVITATIONS: "invitation", DEV_LOCKS: "devLocks", DEBOUNCE: "debounce", SESSIONS: "session", USER_CACHE: "users", + FLAGS: "flags", } exports.SEPARATOR = SEPARATOR diff --git a/packages/auth/src/tenancy/tenancy.js b/packages/auth/src/tenancy/tenancy.js index 6e18ea7154..ebd573496c 100644 --- a/packages/auth/src/tenancy/tenancy.js +++ b/packages/auth/src/tenancy/tenancy.js @@ -63,6 +63,7 @@ exports.tryAddTenant = async (tenantId, userId, email) => { } if (emailDoc) { emailDoc.tenantId = tenantId + emailDoc.userId = userId promises.push(db.put(emailDoc)) } if (tenants.tenantIds.indexOf(tenantId) === -1) { diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 408494b199..123d168fee 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.139", + "version": "0.9.140-alpha.0", "license": "AGPL-3.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", diff --git a/packages/bbui/src/ActionButton/ActionButton.svelte b/packages/bbui/src/ActionButton/ActionButton.svelte index 83f71d385b..b518ac3d92 100644 --- a/packages/bbui/src/ActionButton/ActionButton.svelte +++ b/packages/bbui/src/ActionButton/ActionButton.svelte @@ -12,6 +12,7 @@ export let dataCy = null export let size = "M" export let active = false + export let fullWidth = false function longPress(element) { if (!longPressable) return @@ -40,6 +41,7 @@ class:spectrum-ActionButton--quiet={quiet} class:spectrum-ActionButton--emphasized={emphasized} class:is-selected={selected} + class:fullWidth class="spectrum-ActionButton spectrum-ActionButton--size{size}" class:active {disabled} @@ -71,6 +73,9 @@ diff --git a/packages/bbui/src/Modal/ModalContent.svelte b/packages/bbui/src/Modal/ModalContent.svelte index bba72e6093..678a813a61 100644 --- a/packages/bbui/src/Modal/ModalContent.svelte +++ b/packages/bbui/src/Modal/ModalContent.svelte @@ -46,8 +46,10 @@

{title} +

{#if showDivider} @@ -120,4 +122,9 @@ .close-icon :global(svg) { margin-right: 0; } + + .header-spacing { + display: flex; + justify-content: space-between; + } diff --git a/packages/bbui/src/ProgressCircle/ProgressCircle.svelte b/packages/bbui/src/ProgressCircle/ProgressCircle.svelte index 9c8181ec7c..0428263346 100644 --- a/packages/bbui/src/ProgressCircle/ProgressCircle.svelte +++ b/packages/bbui/src/ProgressCircle/ProgressCircle.svelte @@ -13,7 +13,7 @@ } } - export let value = false + export let value = null export let minValue = 0 export let maxValue = 100 @@ -42,7 +42,7 @@
diff --git a/packages/bbui/src/SideNavigation/Item.svelte b/packages/bbui/src/SideNavigation/Item.svelte index f50270dfbd..dfebdb46a6 100644 --- a/packages/bbui/src/SideNavigation/Item.svelte +++ b/packages/bbui/src/SideNavigation/Item.svelte @@ -13,6 +13,7 @@ class="spectrum-SideNav-item" class:is-selected={selected} class:is-disabled={disabled} + on:click > {#if heading}
diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ResultsModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ResultsModal.svelte new file mode 100644 index 0000000000..7dfdff20a7 --- /dev/null +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ResultsModal.svelte @@ -0,0 +1,114 @@ + + + +
+
+ {#if isTrigger || testResult[0].outputs.success} +
+ +
+ {:else} +
+ +
+ {/if} +
+
+ +
{ + inputToggled = !inputToggled + }} + class="toggle splitHeader" + > +
+
+ + Input + +
+
+
+ {#if inputToggled} + + {:else} + + {/if} +
+
+ {#if inputToggled} +
+