Merge branch 'develop' of github.com:Budibase/budibase into plugins-dev-experience

This commit is contained in:
Andrew Kingston 2022-08-22 17:54:25 +01:00
commit f14af4bae9
14 changed files with 440 additions and 17 deletions

View File

@ -1,4 +1,4 @@
name: Budibase Smoke Test name: Budibase Nightly Tests
on: on:
workflow_dispatch: workflow_dispatch:
@ -6,7 +6,7 @@ on:
- cron: "0 5 * * *" # every day at 5AM - cron: "0 5 * * *" # every day at 5AM
jobs: jobs:
release: nightly:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -43,6 +43,18 @@ jobs:
name: Test Reports name: Test Reports
path: packages/builder/cypress/reports/testReport.html path: packages/builder/cypress/reports/testReport.html
# TODO: enable once running in QA test env
# - name: Configure AWS Credentials
# uses: aws-actions/configure-aws-credentials@v1
# with:
# aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
# aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# aws-region: eu-west-1
# - name: Upload test results HTML
# uses: aws-actions/configure-aws-credentials@v1
# run: aws s3 cp packages/builder/cypress/reports/testReport.html s3://{{ secrets.BUDI_QA_REPORTS_BUCKET_NAME }}/$GITHUB_RUN_ID/index.html
- name: Cypress Discord Notify - name: Cypress Discord Notify
run: yarn test:e2e:ci:notify run: yarn test:e2e:ci:notify
env: env:

View File

@ -0,0 +1,178 @@
import filterTests from "../../support/filterTests"
// const interact = require("../support/interact")
filterTests(["smoke", "all"], () => {
context("Auth Configuration", () => {
before(() => {
cy.login()
})
after(() => {
cy.get(".spectrum-SideNav li").contains("Auth").click()
cy.location().should(loc => {
expect(loc.pathname).to.eq("/builder/portal/manage/auth")
})
cy.get("[data-cy=new-scope-input]").clear()
cy.get("div.content").scrollTo("bottom")
cy.get("[data-cy=oidc-active]").click()
cy.get("[data-cy=oidc-active]").should('not.be.checked')
cy.intercept("POST", "/api/global/configs").as("updateAuth")
cy.get("button[data-cy=oidc-save]").contains("Save").click({force: true})
cy.wait("@updateAuth")
cy.get("@updateAuth").its("response.statusCode").should("eq", 200)
cy.get(".spectrum-Toast-content")
.contains("Settings saved")
.should("be.visible")
})
it("Should allow updating of the OIDC config", () => {
cy.get(".spectrum-SideNav li").contains("Auth").click()
cy.location().should(loc => {
expect(loc.pathname).to.eq("/builder/portal/manage/auth")
})
cy.get("div.content").scrollTo("bottom")
cy.get(".spectrum-Toast .spectrum-ClearButton").click()
cy.get("input[data-cy=configUrl]").type("http://budi-auth.com/v2")
cy.get("input[data-cy=clientID]").type("34ac6a13-f24a-4b52-c70d-fa544ffd11b2")
cy.get("input[data-cy=clientSecret]").type("12A8Q~4nS_DWhOOJ2vWIRsNyDVsdtXPD.Zxa9df_")
cy.get("button[data-cy=oidc-save]").should("not.be.disabled");
cy.intercept("POST", "/api/global/configs").as("updateAuth")
cy.get("button[data-cy=oidc-save]").contains("Save").click({force: true})
cy.wait("@updateAuth")
cy.get("@updateAuth").its("response.statusCode").should("eq", 200)
cy.get(".spectrum-Toast-content")
.contains("Settings saved")
.should("be.visible")
})
it("Should display default scopes in advanced config.", () => {
cy.get(".spectrum-SideNav li").contains("Auth").click()
cy.location().should(loc => {
expect(loc.pathname).to.eq("/builder/portal/manage/auth")
})
cy.get("div.content").scrollTo("bottom")
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
cy.get(".spectrum-Tags-item").contains("openid")
cy.get(".spectrum-Tags-item").contains("openid").find(".spectrum-ClearButton").should("not.exist")
cy.get(".spectrum-Tags-item").contains("offline_access")
cy.get(".spectrum-Tags-item").contains("email")
cy.get(".spectrum-Tags-item").contains("profile")
})
it("Add a new scopes", () => {
cy.get(".spectrum-SideNav li").contains("Auth").click()
cy.location().should(loc => {
expect(loc.pathname).to.eq("/builder/portal/manage/auth")
})
cy.get("div.content").scrollTo("bottom")
cy.get("[data-cy=new-scope-input]").type("Sample{enter}")
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 5)
cy.get(".spectrum-Tags-item").contains("Sample")
cy.get(".auth-form input.spectrum-Textfield-input").type("Another ")
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 6)
cy.get(".spectrum-Tags-item").contains("Another")
cy.get("button[data-cy=oidc-save]").should("not.be.disabled");
cy.intercept("POST", "/api/global/configs").as("updateAuth")
cy.get("button[data-cy=oidc-save]").contains("Save").click({force: true})
cy.wait("@updateAuth")
cy.get("@updateAuth").its("response.statusCode").should("eq", 200)
cy.reload()
cy.get("div.content").scrollTo("bottom")
cy.get(".spectrum-Tags-item").contains("openid")
cy.get(".spectrum-Tags-item").contains("offline_access")
cy.get(".spectrum-Tags-item").contains("email")
cy.get(".spectrum-Tags-item").contains("profile")
cy.get(".spectrum-Tags-item").contains("Sample")
cy.get(".spectrum-Tags-item").contains("Another")
})
it("Should allow the removal of auth scopes", () => {
cy.get(".spectrum-SideNav li").contains("Auth").click()
cy.location().should(loc => {
expect(loc.pathname).to.eq("/builder/portal/manage/auth")
})
cy.get("div.content").scrollTo("bottom")
cy.get(".spectrum-Tags-item").contains("offline_access").parent().find(".spectrum-ClearButton").click()
cy.get(".spectrum-Tags-item").contains("profile").parent().find(".spectrum-ClearButton").click()
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
cy.get(".spectrum-Tags-item").contains("offline_access").should("not.exist")
cy.get(".spectrum-Tags-item").contains("profile").should("not.exist")
cy.get("button[data-cy=oidc-save]").should("not.be.disabled");
cy.intercept("POST", "/api/global/configs").as("updateAuth")
cy.get("button[data-cy=oidc-save]").contains("Save").click({force: true})
cy.wait("@updateAuth")
cy.get("@updateAuth").its("response.statusCode").should("eq", 200)
cy.get(".spectrum-Toast-content")
.contains("Settings saved")
.should("be.visible")
cy.reload()
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
cy.get(".spectrum-Tags-item").contains("offline_access").should("not.exist")
cy.get(".spectrum-Tags-item").contains("profile").should("not.exist")
})
it("Should allow auth scopes to be reset to the core defaults.", () => {
cy.get(".spectrum-SideNav li").contains("Auth").click()
cy.get("div.content").scrollTo("bottom")
cy.get("[data-cy=restore-oidc-default-scopes]").click({force: true})
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
cy.get(".spectrum-Tags-item").contains("openid")
cy.get(".spectrum-Tags-item").contains("offline_access")
cy.get(".spectrum-Tags-item").contains("email")
cy.get(".spectrum-Tags-item").contains("profile")
})
it("Should not allow invalid characters in the auth scopes", () => {
cy.get("[data-cy=new-scope-input]").type("thisIsInvalid\\{enter}")
cy.get(".spectrum-Form-itemField .error").contains("Auth scopes cannot contain spaces, double quotes or backslashes")
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
cy.get("[data-cy=new-scope-input]").clear()
cy.get("[data-cy=new-scope-input]").type("alsoInvalid\"{enter}")
cy.get(".spectrum-Form-itemField .error").contains("Auth scopes cannot contain spaces, double quotes or backslashes")
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
cy.get("[data-cy=new-scope-input]").clear()
})
it("Should not allow duplicate auth scopes", () => {
cy.get("[data-cy=new-scope-input]").type("offline_access{enter}")
cy.get(".spectrum-Form-itemField .error").contains("Auth scope already exists")
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
})
})
})

View File

@ -17,7 +17,7 @@
"cy:run:ci:record": "xvfb-run cypress run --headed --browser chrome --record", "cy:run:ci:record": "xvfb-run cypress run --headed --browser chrome --record",
"cy:test": "start-server-and-test cy:setup http://localhost:4100/builder cy:run", "cy:test": "start-server-and-test cy:setup http://localhost:4100/builder cy:run",
"cy:ci": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci", "cy:ci": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci",
"cy:ci:record": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci:record && npm run cy:ci:report", "cy:ci:record": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci:record; npm run cy:ci:report",
"cy:ci:report": "mochawesome-merge cypress/reports/*.json > cypress/reports/testReport.json && marge cypress/reports/testReport.json --reportDir cypress/reports --inline", "cy:ci:report": "mochawesome-merge cypress/reports/*.json > cypress/reports/testReport.json && marge cypress/reports/testReport.json --reportDir cypress/reports --inline",
"cy:ci:notify": "node scripts/cypressResultsWebhook", "cy:ci:notify": "node scripts/cypressResultsWebhook",
"cy:debug": "start-server-and-test cy:setup http://localhost:4100/builder cy:open", "cy:debug": "start-server-and-test cy:setup http://localhost:4100/builder cy:open",

View File

@ -5,7 +5,6 @@ const path = require("path")
const fs = require("fs") const fs = require("fs")
const WEBHOOK_URL = process.env.CYPRESS_WEBHOOK_URL const WEBHOOK_URL = process.env.CYPRESS_WEBHOOK_URL
const OUTCOME = process.env.CYPRESS_OUTCOME
const DASHBOARD_URL = process.env.CYPRESS_DASHBOARD_URL const DASHBOARD_URL = process.env.CYPRESS_DASHBOARD_URL
const GIT_SHA = process.env.GITHUB_SHA const GIT_SHA = process.env.GITHUB_SHA
const GITHUB_ACTIONS_RUN_URL = process.env.GITHUB_ACTIONS_RUN_URL const GITHUB_ACTIONS_RUN_URL = process.env.GITHUB_ACTIONS_RUN_URL
@ -35,6 +34,8 @@ async function discordCypressResultsNotification(report) {
skipped, skipped,
} = report.stats } = report.stats
const OUTCOME = failures > 0 ? "failure" : "success"
const options = { const options = {
method: "POST", method: "POST",
headers: { headers: {
@ -114,7 +115,7 @@ async function discordCypressResultsNotification(report) {
} }
const response = await fetch(WEBHOOK_URL, options) const response = await fetch(WEBHOOK_URL, options)
if (response.status >= 400) { if (response.status >= 201) {
const text = await response.text() const text = await response.text()
console.error( console.error(
`Error sending discord webhook. \nStatus: ${response.status}. \nResponse Body: ${text}. \nRequest Body: ${options.body}` `Error sending discord webhook. \nStatus: ${response.status}. \nResponse Body: ${text}. \nRequest Body: ${options.body}`

View File

@ -40,6 +40,7 @@ const INITIAL_FRONTEND_STATE = {
devicePreview: false, devicePreview: false,
messagePassing: false, messagePassing: false,
continueIfAction: false, continueIfAction: false,
showNotificationAction: false,
}, },
errors: [], errors: [],
hasAppPackage: false, hasAppPackage: false,

View File

@ -0,0 +1,61 @@
<script>
import { Select, Label, Checkbox } from "@budibase/bbui"
import { onMount } from "svelte"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
export let parameters
export let bindings = []
const types = [
{
label: "Success",
value: "success",
},
{
label: "Warning",
value: "warning",
},
{
label: "Error",
value: "error",
},
{
label: "Info",
value: "info",
},
]
onMount(() => {
if (!parameters.type) {
parameters.type = "success"
}
if (parameters.autoDismiss == null) {
parameters.autoDismiss = true
}
})
</script>
<div class="root">
<Label>Type</Label>
<Select bind:value={parameters.type} options={types} placeholder={null} />
<Label>Message</Label>
<DrawerBindableInput
{bindings}
value={parameters.message}
on:change={e => (parameters.message = e.detail)}
/>
<Label />
<Checkbox text="Auto dismiss" bind:value={parameters.autoDismiss} />
</div>
<style>
.root {
display: grid;
column-gap: var(--spacing-l);
row-gap: var(--spacing-s);
grid-template-columns: 60px 1fr;
align-items: center;
max-width: 400px;
margin: 0 auto;
}
</style>

View File

@ -15,3 +15,4 @@ export { default as S3Upload } from "./S3Upload.svelte"
export { default as ExportData } from "./ExportData.svelte" export { default as ExportData } from "./ExportData.svelte"
export { default as ContinueIf } from "./ContinueIf.svelte" export { default as ContinueIf } from "./ContinueIf.svelte"
export { default as UpdateFieldValue } from "./UpdateFieldValue.svelte" export { default as UpdateFieldValue } from "./UpdateFieldValue.svelte"
export { default as ShowNotification } from "./ShowNotification.svelte"

View File

@ -110,6 +110,12 @@
"type": "logic", "type": "logic",
"component": "ContinueIf", "component": "ContinueIf",
"dependsOnFeature": "continueIfAction" "dependsOnFeature": "continueIfAction"
},
{
"name": "Show Notification",
"type": "application",
"component": "ShowNotification",
"dependsOnFeature": "showNotificationAction"
} }
] ]
} }

View File

@ -18,6 +18,8 @@
Body, Body,
Select, Select,
Toggle, Toggle,
Tag,
Tags,
} from "@budibase/bbui" } from "@budibase/bbui"
import { onMount } from "svelte" import { onMount } from "svelte"
import { API } from "api" import { API } from "api"
@ -29,6 +31,8 @@
OIDC: "oidc", OIDC: "oidc",
} }
const HasSpacesRegex = /[\\"\s]/
// Some older google configs contain a manually specified value - retain the functionality to edit the field // Some older google configs contain a manually specified value - retain the functionality to edit the field
// When there is no value or we are in the cloud - prohibit editing the field, must use platform url to change // When there is no value or we are in the cloud - prohibit editing the field, must use platform url to change
$: googleCallbackUrl = undefined $: googleCallbackUrl = undefined
@ -145,7 +149,6 @@
async function save(docs) { async function save(docs) {
let calls = [] let calls = []
// Only if the user has provided an image, upload it // Only if the user has provided an image, upload it
if (image) { if (image) {
let data = new FormData() let data = new FormData()
@ -157,7 +160,6 @@
}) })
) )
} }
docs.forEach(element => { docs.forEach(element => {
// Delete unsupported fields // Delete unsupported fields
delete element.createdAt delete element.createdAt
@ -199,7 +201,6 @@
} }
} }
}) })
if (calls.length) { if (calls.length) {
Promise.all(calls) Promise.all(calls)
.then(data => { .then(data => {
@ -215,6 +216,21 @@
} }
} }
let defaultScopes = ["profile", "email", "offline_access"]
const refreshScopes = idx => {
providers.oidc.config.configs[idx]["scopes"] =
providers.oidc.config.configs[idx]["scopes"]
}
let scopesFields = [
{
editing: true,
inputText: null,
error: null,
},
]
onMount(async () => { onMount(async () => {
try { try {
await organisation.init() await organisation.init()
@ -276,7 +292,7 @@
if (!oidcDoc?._id) { if (!oidcDoc?._id) {
providers.oidc = { providers.oidc = {
type: ConfigTypes.OIDC, type: ConfigTypes.OIDC,
config: { configs: [{ activated: true }] }, config: { configs: [{ activated: true, scopes: defaultScopes }] },
} }
} else { } else {
originalOidcDoc = cloneDeep(oidcDoc) originalOidcDoc = cloneDeep(oidcDoc)
@ -345,6 +361,7 @@
size="s" size="s"
cta cta
on:click={() => save([providers.oidc])} on:click={() => save([providers.oidc])}
dataCy={"oidc-save"}
> >
Save Save
</Button> </Button>
@ -362,6 +379,7 @@
bind:value={providers.oidc.config.configs[0][field.name]} bind:value={providers.oidc.config.configs[0][field.name]}
readonly={field.readonly} readonly={field.readonly}
placeholder={field.placeholder} placeholder={field.placeholder}
dataCy={field.name}
/> />
</div> </div>
{/each} {/each}
@ -392,15 +410,132 @@
<div class="form-row"> <div class="form-row">
<Label size="L">Activated</Label> <Label size="L">Activated</Label>
<Toggle <Toggle
dataCy={"oidc-active"}
text="" text=""
bind:value={providers.oidc.config.configs[0].activated} bind:value={providers.oidc.config.configs[0].activated}
/> />
</div> </div>
</Layout> </Layout>
<span class="advanced-config">
<Layout gap="XS" noPadding>
<Heading size="XS">
<div class="auth-scopes">
<div>Advanced</div>
<Button
secondary
newStyles
size="S"
on:click={() => {
providers.oidc.config.configs[0]["scopes"] = [...defaultScopes]
}}
dataCy={"restore-oidc-default-scopes"}
>
Restore Defaults
</Button>
</div>
</Heading>
<Body size="S">
Changes to your authentication scopes will only take effect when you
next log in. Please refer to your vendor documentation before
modification.
</Body>
<div class="auth-form">
<span class="add-new">
<Label size="L">{"Auth Scopes"}</Label>
<Input
dataCy={"new-scope-input"}
error={scopesFields[0].error}
placeholder={"New Scope"}
bind:value={scopesFields[0].inputText}
on:keyup={e => {
if (!scopesFields[0].inputText) {
scopesFields[0].error = null
}
if (
e.key === "Enter" ||
e.keyCode === 13 ||
e.code == "Space" ||
e.keyCode == 32
) {
let scopes = providers.oidc.config.configs[0]["scopes"]
? providers.oidc.config.configs[0]["scopes"]
: [...defaultScopes]
let update = scopesFields[0].inputText.trim()
if (HasSpacesRegex.test(update)) {
scopesFields[0].error =
"Auth scopes cannot contain spaces, double quotes or backslashes"
return
} else if (scopes.indexOf(update) > -1) {
scopesFields[0].error = "Auth scope already exists"
return
} else if (!update.length) {
scopesFields[0].inputText = null
scopesFields[0].error = null
return
} else {
scopesFields[0].error = null
scopes.push(update)
providers.oidc.config.configs[0]["scopes"] = scopes
scopesFields[0].inputText = null
}
}
}}
/>
</span>
<div class="tag-wrap">
<span />
<Tags>
<Tag closable={false}>openid</Tag>
{#each providers.oidc.config.configs[0]["scopes"] || [...defaultScopes] as tag, idx}
<Tag
closable={scopesFields[0].editing}
on:click={() => {
let idxScopes = providers.oidc.config.configs[0]["scopes"]
if (idxScopes.length == 1) {
idxScopes.pop()
} else {
idxScopes.splice(idx, 1)
refreshScopes(0)
}
}}
>
{tag}
</Tag>
{/each}
</Tags>
</div>
</div>
</Layout>
</span>
{/if} {/if}
</Layout> </Layout>
<style> <style>
.auth-scopes {
display: flex;
justify-content: space-between;
align-items: center;
}
.advanced-config :global(.spectrum-Tags-item) {
margin-left: 0px;
margin-top: var(--spacing-m);
margin-right: var(--spacing-m);
}
.auth-form > * {
display: grid;
grid-gap: var(--spacing-l);
grid-template-columns: 100px 1fr;
}
.advanced-config .auth-form .tag-wrap {
padding: 0px 5px 5px 0px;
}
.form-row { .form-row {
display: grid; display: grid;
grid-template-columns: 100px 1fr; grid-template-columns: 100px 1fr;

View File

@ -8,7 +8,8 @@
"devicePreview": true, "devicePreview": true,
"messagePassing": true, "messagePassing": true,
"rowSelection": true, "rowSelection": true,
"continueIfAction": true "continueIfAction": true,
"showNotificationAction": true
}, },
"layout": { "layout": {
"name": "Layout", "name": "Layout",

View File

@ -62,10 +62,14 @@ const createNotificationStore = () => {
subscribe: store.subscribe, subscribe: store.subscribe,
actions: { actions: {
send, send,
info: msg => send(msg, "info", "Info"), info: (msg, autoDismiss) =>
success: msg => send(msg, "success", "CheckmarkCircle"), send(msg, "info", "Info", autoDismiss ?? true),
warning: msg => send(msg, "warning", "Alert"), success: (msg, autoDismiss) =>
error: msg => send(msg, "error", "Alert", false), send(msg, "success", "CheckmarkCircle", autoDismiss ?? true),
warning: (msg, autoDismiss) =>
send(msg, "warning", "Alert", autoDismiss ?? true),
error: (msg, autoDismiss) =>
send(msg, "error", "Alert", autoDismiss ?? false),
blockNotifications, blockNotifications,
dismiss, dismiss,
}, },

View File

@ -300,6 +300,14 @@ const continueIfHandler = action => {
} }
} }
const showNotificationHandler = action => {
const { message, type, autoDismiss } = action.parameters
if (!message || !type) {
return
}
notificationStore.actions[type]?.(message, autoDismiss)
}
const handlerMap = { const handlerMap = {
["Save Row"]: saveRowHandler, ["Save Row"]: saveRowHandler,
["Duplicate Row"]: duplicateRowHandler, ["Duplicate Row"]: duplicateRowHandler,
@ -318,6 +326,7 @@ const handlerMap = {
["Upload File to S3"]: s3UploadHandler, ["Upload File to S3"]: s3UploadHandler,
["Export Data"]: exportDataHandler, ["Export Data"]: exportDataHandler,
["Continue if / Stop if"]: continueIfHandler, ["Continue if / Stop if"]: continueIfHandler,
["Show Notification"]: showNotificationHandler,
} }
const confirmTextMap = { const confirmTextMap = {

View File

@ -227,9 +227,22 @@ export const oidcPreAuth = async (ctx: any, next: any) => {
setCookie(ctx, configId, Cookies.OIDC_CONFIG) setCookie(ctx, configId, Cookies.OIDC_CONFIG)
const db = getGlobalDB()
const config = await core.db.getScopedConfig(db, {
type: Configs.OIDC,
group: ctx.query.group,
})
const chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0]
let authScopes =
chosenConfig.scopes?.length > 0
? chosenConfig.scopes
: ["profile", "email", "offline_access"]
return passport.authenticate(strategy, { return passport.authenticate(strategy, {
// required 'openid' scope is added by oidc strategy factory // required 'openid' scope is added by oidc strategy factory
scope: ["profile", "email", "offline_access"], //auth0 offline_access scope required for the refresh token behaviour. scope: authScopes,
})(ctx, next) })(ctx, next)
} }

View File

@ -53,6 +53,7 @@ function oidcValidation() {
name: Joi.string().allow("", null), name: Joi.string().allow("", null),
uuid: Joi.string().required(), uuid: Joi.string().required(),
activated: Joi.boolean().required(), activated: Joi.boolean().required(),
scopes: Joi.array().optional()
}) })
).required(true) ).required(true)
}).unknown(true) }).unknown(true)