diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 588f0c54ae..aaee3923ef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,6 @@ name: Budibase Release -on: +on: push: branches: - master @@ -9,20 +9,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 @@ -35,19 +35,19 @@ 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 - id: previoustag - uses: "WyriHaximus/github-action-get-previous-tag@v1" + - name: 'Get Previous tag' + id: previoustag + uses: "WyriHaximus/github-action-get-previous-tag@v1" - name: Build/release Docker images - run: | + run: | docker login -u $DOCKER_USER -p $DOCKER_PASSWORD yarn build yarn build:docker @@ -68,4 +68,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 9cdf2b2114..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,9 +117,11 @@ services: - "${REDIS_PORT}:6379" volumes: - redis_data:/data - + watchtower-service: image: containrrr/watchtower + ports: + - "${WATCHTOWER_PORT}:8080" volumes: - /var/run/docker.sock:/var/run/docker.sock command: --debug --http-api-update bbapps bbworker @@ -128,8 +131,6 @@ services: - WATCHTOWER_CLEANUP=true labels: - "com.centurylinklabs.watchtower.enable=false" - ports: - - 6161:8080 volumes: diff --git a/hosting/hosting.properties b/hosting/hosting.properties index d11972bc4b..c8e2f5c606 100644 --- a/hosting/hosting.properties +++ b/hosting/hosting.properties @@ -17,4 +17,5 @@ WORKER_PORT=4003 MINIO_PORT=4004 COUCH_DB_PORT=4005 REDIS_PORT=6379 +WATCHTOWER_PORT=6161 BUDIBASE_ENVIRONMENT=PRODUCTION 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 0e7859d887..c36f04936f 100644 --- a/hosting/kubernetes/envoy/envoy.yaml +++ b/hosting/kubernetes/envoy/envoy.yaml @@ -28,27 +28,35 @@ 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 + - match: { prefix: "/api/admin/" } route: cluster: worker-service + - match: { prefix: "/api/system/" } + route: + cluster: worker-service + - match: { path: "/" } 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: "/" @@ -77,7 +85,7 @@ static_resources: - lb_endpoints: - endpoint: address: - socket_address: + socket_address: address: app-service.budibase.svc.cluster.local port_value: 4002 @@ -105,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 8188c1c229..6171a81f87 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "0.9.105-alpha.31", + "version": "0.9.125-alpha.13", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/package.json b/package.json index 05c69e54dc..f87c3715aa 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", @@ -48,6 +47,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 ad5afd5691..4fc09157b0 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/auth", - "version": "0.9.105-alpha.31", + "version": "0.9.125-alpha.13", "description": "Authentication middlewares for budibase builder and apps", "main": "src/index.js", "author": "Budibase", diff --git a/packages/auth/src/cache/user.js b/packages/auth/src/cache/user.js index 4a19da489f..2b2693ca01 100644 --- a/packages/auth/src/cache/user.js +++ b/packages/auth/src/cache/user.js @@ -3,7 +3,29 @@ const { getTenantId, lookupTenantId, getGlobalDB } = require("../tenancy") 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 + 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,9 +37,13 @@ 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) { + // make sure the tenant ID is always correct/set + user.tenantId = tenantId + } return user } diff --git a/packages/auth/src/db/utils.js b/packages/auth/src/db/utils.js index 4cd29c9bc8..a5d7c1f100 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,35 @@ 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. + */ +exports.getTenantIDFromAppID = appId => { + const split = appId.split(SEPARATOR) + const hasDev = split[1] === DocumentTypes.DEV + if ((hasDev && split.length === 3) || (!hasDev && split.length === 2)) { + return null + } + if (hasDev) { + return split[2] + } else { + return split[1] + } +} + /** * Generates a new workspace ID. * @returns {string} The new workspace ID which the workspace doc can be stored under. diff --git a/packages/auth/src/index.js b/packages/auth/src/index.js index 5421dea214..569456ea10 100644 --- a/packages/auth/src/index.js +++ b/packages/auth/src/index.js @@ -11,6 +11,7 @@ const { oidc, auditLog, tenancy, + appTenancy, } = require("./middleware") const { setDB } = require("./db") const userCache = require("./cache/user") @@ -57,6 +58,7 @@ module.exports = { oidc, jwt: require("jsonwebtoken"), buildTenancyMiddleware: tenancy, + buildAppTenancyMiddleware: appTenancy, auditLog, }, cache: { diff --git a/packages/auth/src/middleware/appTenancy.js b/packages/auth/src/middleware/appTenancy.js new file mode 100644 index 0000000000..30fc4f7453 --- /dev/null +++ b/packages/auth/src/middleware/appTenancy.js @@ -0,0 +1,25 @@ +const { + isMultiTenant, + updateTenantId, + isTenantIdSet, + DEFAULT_TENANT_ID, +} = require("../tenancy") +const ContextFactory = require("../tenancy/FunctionContext") +const { getTenantIDFromAppID } = require("../db/utils") + +module.exports = () => { + return ContextFactory.getMiddleware(ctx => { + // if not in multi-tenancy mode make sure its default and exit + if (!isMultiTenant()) { + updateTenantId(DEFAULT_TENANT_ID) + return + } + // if tenant ID already set no need to continue + if (isTenantIdSet()) { + return + } + const appId = ctx.appId ? ctx.appId : ctx.user ? ctx.user.appId : null + const tenantId = getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID + updateTenantId(tenantId) + }) +} 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/middleware/index.js b/packages/auth/src/middleware/index.js index 689859a139..059f20af8b 100644 --- a/packages/auth/src/middleware/index.js +++ b/packages/auth/src/middleware/index.js @@ -5,6 +5,7 @@ const oidc = require("./passport/oidc") const authenticated = require("./authenticated") const auditLog = require("./auditLog") const tenancy = require("./tenancy") +const appTenancy = require("./appTenancy") module.exports = { google, @@ -14,4 +15,5 @@ module.exports = { authenticated, auditLog, tenancy, + appTenancy, } diff --git a/packages/auth/src/middleware/tenancy.js b/packages/auth/src/middleware/tenancy.js index b80b9a6763..adfd36a503 100644 --- a/packages/auth/src/middleware/tenancy.js +++ b/packages/auth/src/middleware/tenancy.js @@ -2,12 +2,17 @@ const { setTenantId } = require("../tenancy") const ContextFactory = require("../tenancy/FunctionContext") const { buildMatcherRegex, matches } = require("./matchers") -module.exports = (allowQueryStringPatterns, noTenancyPatterns) => { +module.exports = ( + allowQueryStringPatterns, + noTenancyPatterns, + opts = { noTenancyRequired: false } +) => { const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns) const noTenancyOptions = buildMatcherRegex(noTenancyPatterns) return ContextFactory.getMiddleware(ctx => { - const allowNoTenant = !!matches(ctx, noTenancyOptions) + const allowNoTenant = + opts.noTenancyRequired || !!matches(ctx, noTenancyOptions) const allowQs = !!matches(ctx, allowQsOptions) setTenantId(ctx, { allowQs, allowNoTenant }) }) diff --git a/packages/auth/src/redis/index.js b/packages/auth/src/redis/index.js index 4f2b5288ea..94b453c8f6 100644 --- a/packages/auth/src/redis/index.js +++ b/packages/auth/src/redis/index.js @@ -56,9 +56,13 @@ function init() { if (CLIENT) { CLIENT.disconnect() } - const { opts, host, port } = getRedisOptions(CLUSTERED) + + const { redisProtocolUrl, opts, host, port } = getRedisOptions(CLUSTERED) + if (CLUSTERED) { CLIENT = new Redis.Cluster([{ host, port }], opts) + } else if (redisProtocolUrl) { + CLIENT = new Redis(redisProtocolUrl) } else { CLIENT = new Redis(opts) } diff --git a/packages/auth/src/redis/utils.js b/packages/auth/src/redis/utils.js index 415dcbf463..6befecd9ba 100644 --- a/packages/auth/src/redis/utils.js +++ b/packages/auth/src/redis/utils.js @@ -8,17 +8,27 @@ 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 exports.getRedisOptions = (clustered = false) => { - const [host, port] = REDIS_URL.split(":") + const [host, port, ...rest] = REDIS_URL.split(":") + + let redisProtocolUrl + + // fully qualified redis URL + if (rest.length && /rediss?/.test(host)) { + redisProtocolUrl = REDIS_URL + } + const opts = { connectTimeout: CONNECT_TIMEOUT_MS, } @@ -33,7 +43,7 @@ exports.getRedisOptions = (clustered = false) => { opts.port = port opts.password = REDIS_PASSWORD } - return { opts, host, port } + return { opts, host, port, redisProtocolUrl } } exports.addDbPrefix = (db, key) => { diff --git a/packages/auth/src/tenancy/context.js b/packages/auth/src/tenancy/context.js index f3f1f541e9..b1ef5a5807 100644 --- a/packages/auth/src/tenancy/context.js +++ b/packages/auth/src/tenancy/context.js @@ -21,9 +21,7 @@ exports.doInTenant = (tenantId, task) => { cls.setOnContext(TENANT_ID, tenantId) // invoke the task - const result = task() - - return result + return task() }) } diff --git a/packages/bbui/package.json b/packages/bbui/package.json index f6275b7fe3..6b4b21e61b 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.105-alpha.31", + "version": "0.9.125-alpha.13", "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} +
+